diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 814be356cc..2a200ba3ef 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1885,6 +1885,16 @@ class InterfaceFilterSet( PathEndpointFilterSet, CommonInterfaceFilterSet ): + virtual_chassis_member_or_master = MultiValueCharFilter( + method='filter_virtual_chassis_member_or_master', + field_name='name', + label=_('Virtual Chassis Interfaces for Device when device is master') + ) + virtual_chassis_member_or_master_id = MultiValueNumberFilter( + method='filter_virtual_chassis_member_or_master', + field_name='pk', + label=_('Virtual Chassis Interfaces for Device when device is master (ID)') + ) virtual_chassis_member = MultiValueCharFilter( method='filter_virtual_chassis_member', field_name='name', @@ -1995,11 +2005,14 @@ class Meta: 'cable_id', 'cable_end', ) - def filter_virtual_chassis_member(self, queryset, name, value): + def filter_virtual_chassis_member_or_master(self, queryset, name, value): + return self.filter_virtual_chassis_member(queryset, name, value, if_master=True) + + def filter_virtual_chassis_member(self, queryset, name, value, if_master=False): try: vc_interface_ids = [] for device in Device.objects.filter(**{f'{name}__in': value}): - vc_interface_ids.extend(device.vc_interfaces(if_master=False).values_list('id', flat=True)) + vc_interface_ids.extend(device.vc_interfaces(if_master=if_master).values_list('id', flat=True)) return queryset.filter(pk__in=vc_interface_ids) except Device.DoesNotExist: return queryset.none() diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 5e5d83b0b4..d3588da394 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -19,6 +19,11 @@ def __new__(mcs, name, bases, attrs): # Device component if hasattr(term_cls, 'device'): + # Dynamically change the param field for interfaces to use virtual_chassis filter + query_param_device_field = 'device_id' + if term_cls == Interface: + query_param_device_field = 'virtual_chassis_member_or_master_id' + attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), label=_('Device'), @@ -36,7 +41,7 @@ def __new__(mcs, name, bases, attrs): 'parent': 'device', }, query_params={ - 'device_id': f'$termination_{cable_end}_device', + query_param_device_field: f'$termination_{cable_end}_device', 'kind': 'physical', # Exclude virtual interfaces } ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 855c3abd39..0331de56c4 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -4373,6 +4373,9 @@ def setUpTestData(cls): ) Device.objects.bulk_create(devices) + virtual_chassis.master = devices[0] + virtual_chassis.save() + module_bays = ( ModuleBay(device=devices[0], name='Module Bay 1'), ModuleBay(device=devices[1], name='Module Bay 2'), @@ -4759,6 +4762,19 @@ def test_device(self): params = {'device': [devices[0].name, devices[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_virtual_chassis_member_or_master(self): + vc = VirtualChassis.objects.first() + master = vc.master + member = vc.members.exclude(pk=master.pk).first() + params = {'virtual_chassis_member_or_master_id': [master.pk,]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'virtual_chassis_member_or_master_id': [member.pk,]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'virtual_chassis_member_or_master': [master.name,]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'virtual_chassis_member_or_master': [member.name,]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_virtual_chassis_member(self): # Device 1A & 3 have 1 management interface, Device 1B has 1 interfaces devices = Device.objects.filter(name__in=['Device 1A', 'Device 3'])