diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 1fdc4aa6b..b04eeda7e 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1462,15 +1462,14 @@ class InterfaceFilterSet( PathEndpointFilterSet, CommonInterfaceFilterSet ): - virtual_chassis_for_device = django_filters.ModelMultipleChoiceFilter( - field_name='device__virtual_chassis__members', - queryset=Device.objects.all(), - to_field_name='name', - label=_('Virtual Chassis Interfaces for Device (ID)') + virtual_chassis_member = MultiValueCharFilter( + method='filter_virtual_chassis_member', + field_name='name', + label=_('Virtual Chassis Interfaces for Device') ) - virtual_chassis_for_device_id = django_filters.ModelMultipleChoiceFilter( - field_name='device__virtual_chassis__members', - queryset=Device.objects.all(), + virtual_chassis_member_id = MultiValueNumberFilter( + method='filter_virtual_chassis_member', + field_name='pk', label=_('Virtual Chassis Interfaces for Device (ID)') ) kind = django_filters.CharFilter( @@ -1539,6 +1538,16 @@ class InterfaceFilterSet( 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end', ] + def filter_virtual_chassis_member(self, queryset, name, value): + try: + vc_interface_ids = [] + devices = Device.objects.filter(**{'{}__in'.format(name): value}) + for device in devices: + vc_interface_ids.extend(device.vc_interfaces(if_master=False).values_list('id', flat=True)) + return queryset.filter(pk__in=vc_interface_ids) + except Device.DoesNotExist: + return queryset.none() + def filter_kind(self, queryset, name, value): value = value.strip().lower() return { diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 4c8d89519..0c86e0495 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1107,7 +1107,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): required=False, label=_('Parent interface'), query_params={ - 'virtual_chassis_for_device_id': '$device', + 'virtual_chassis_member_id': '$device', } ) bridge = DynamicModelChoiceField( @@ -1115,7 +1115,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): required=False, label=_('Bridged interface'), query_params={ - 'virtual_chassis_for_device_id': '$device', + 'virtual_chassis_member_id': '$device', } ) lag = DynamicModelChoiceField( @@ -1123,7 +1123,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): required=False, label=_('LAG interface'), query_params={ - 'virtual_chassis_for_device_id': '$device', + 'virtual_chassis_member_id': '$device', 'type': 'lag', } ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 2e8f6667f..39ce603fb 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -2822,11 +2822,44 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ) Rack.objects.bulk_create(racks) + # VirtualChassis assignment for filtering + virtual_chassis = VirtualChassis(name='Virtual Chassis') + virtual_chassis.save() + devices = ( - Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]), - Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]), - Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]), - Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections + Device( + name='Device 1', + device_type=device_types[0], + role=roles[0], site=sites[0], + location=locations[0], + rack=racks[0], + virtual_chassis=virtual_chassis, + vc_position=1, + vc_priority=1 + ), + Device( + name='Device 2', + device_type=device_types[1], + role=roles[1], site=sites[1], + location=locations[1], + rack=racks[1] + ), + Device( + name='Device 3', + device_type=device_types[2], + role=roles[2], site=sites[2], + location=locations[2], + rack=racks[2] + ), + Device( + name=None, + device_type=device_types[2], + role=roles[2], + site=sites[3], + virtual_chassis=virtual_chassis, + vc_position=2, + vc_priority=1 + ), # For cable connections ) Device.objects.bulk_create(devices) @@ -2858,11 +2891,6 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ) VirtualDeviceContext.objects.bulk_create(vdcs) - # VirtualChassis assignment for filtering - virtual_chassis = VirtualChassis.objects.create(master=devices[0]) - Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1) - Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2) - interfaces = ( Interface( device=devices[0], @@ -3116,12 +3144,14 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil params = {'device': [devices[0].name, devices[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_virtual_chassis_for_device(self): + def test_virtual_chassis_member(self): + # Device 1 & 3 have 1 management interface, Device "None" has 5 interfaces, 2 of which are management and + # will not be visible devices = Device.objects.filter(name__in=['Device 1', 'Device 3']) - params = {'device_id': [devices[0].pk, devices[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'device': [devices[0].name, devices[1].name]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'virtual_chassis_member_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + params = {'virtual_chassis_member': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) def test_module(self): modules = Module.objects.all()[:2]