From 1f93471659a3f9f757592f54f454f33e2481598f Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Tue, 15 Apr 2025 19:47:51 +0200 Subject: [PATCH] Fixes #18978 - Allow filtering of Interfaces in the GUI by 802.1Q Mode (#19183) * feat(dcim): Add VLAN mode filter to CommonInterface Introduces a new FilterSet for VLAN mode in CommonInterfaceFilterSet. This allows filtering interfaces based on their VLAN mode using defined choices. * feat(dcim): Add VLAN mode filter to Interface FilterForm Add a field to InterfaceFilterSet to filter interfaces by 802.1Q VLAN mode. * feat(virtualization): Add VLAN mode filter to VMInterface Add a field to VMInterfaceFilterSet to filter interfaces by 802.1Q VLAN mode. * fix(dcim): Correct mode filter parameter type in tests Updates the `mode` filter parameter to accept a list instead of a single value in `test_filtersets.py`. Ensures proper count assertion for accurate test behavior. * feat(virtualization): Add tests for VLAN mode filtering Introduces tests to validate filtering by `mode` for VMInterface. Ensures correct filtering for 802.1Q VLAN mode. * refactor(virtualization): Reorganize FieldSets in FilterSets Splits the 'Attributes' FieldSet into two distinct FieldSets for better clarity: 'Attributes' and 'Addressing'. This improves form organization and makes it more intuitive for users. --- netbox/dcim/filtersets.py | 4 ++++ netbox/dcim/forms/filtersets.py | 6 ++++++ netbox/dcim/tests/test_filtersets.py | 2 +- netbox/virtualization/forms/filtersets.py | 10 +++++++++- netbox/virtualization/tests/test_filtersets.py | 6 ++++++ 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 636a4b6be..eaaee97e7 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1689,6 +1689,10 @@ class MACAddressFilterSet(NetBoxModelFilterSet): class CommonInterfaceFilterSet(django_filters.FilterSet): + mode = django_filters.MultipleChoiceFilter( + choices=InterfaceModeChoices, + label=_('802.1Q Mode') + ) vlan_id = django_filters.CharFilter( method='filter_vlan_id', label=_('Assigned VLAN') diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 4dbceb4f5..8328f502f 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1332,6 +1332,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')), FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')), FieldSet('poe_mode', 'poe_type', name=_('PoE')), + FieldSet('mode', name=_('802.1Q Switching')), FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1403,6 +1404,11 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): required=False, label=_('PoE type') ) + mode = forms.MultipleChoiceField( + choices=InterfaceModeChoices, + required=False, + label=_('802.1Q mode') + ) rf_role = forms.MultipleChoiceField( choices=WirelessRoleChoices, required=False, diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index a938e14c0..727389ef1 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -4153,7 +4153,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_mode(self): - params = {'mode': InterfaceModeChoices.MODE_ACCESS} + params = {'mode': [InterfaceModeChoices.MODE_ACCESS]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_description(self): diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 2b2d821fe..87803621a 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -1,6 +1,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ +from dcim.choices import * from dcim.models import Device, DeviceRole, Location, Platform, Region, Site, SiteGroup from extras.forms import LocalConfigContextFilterForm from extras.models import ConfigTemplate @@ -200,7 +201,9 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): fieldsets = ( FieldSet('q', 'filter_id', 'tag'), FieldSet('cluster_id', 'virtual_machine_id', name=_('Virtual Machine')), - FieldSet('enabled', 'mac_address', 'vrf_id', 'l2vpn_id', name=_('Attributes')), + FieldSet('enabled', name=_('Attributes')), + FieldSet('vrf_id', 'l2vpn_id', 'mac_address', name=_('Addressing')), + FieldSet('mode', name=_('802.1Q Switching')), ) selector_fields = ('filter_id', 'q', 'virtual_machine_id') cluster_id = DynamicModelMultipleChoiceField( @@ -237,6 +240,11 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): required=False, label=_('L2VPN') ) + mode = forms.MultipleChoiceField( + choices=InterfaceModeChoices, + required=False, + label=_('802.1Q mode') + ) tag = TagFilterField(model) diff --git a/netbox/virtualization/tests/test_filtersets.py b/netbox/virtualization/tests/test_filtersets.py index eef5d6b52..d53ca78de 100644 --- a/netbox/virtualization/tests/test_filtersets.py +++ b/netbox/virtualization/tests/test_filtersets.py @@ -605,6 +605,7 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): mtu=100, vrf=vrfs[0], description='foobar1', + mode=InterfaceModeChoices.MODE_ACCESS, vlan_translation_policy=vlan_translation_policies[0], ), VMInterface( @@ -614,6 +615,7 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): mtu=200, vrf=vrfs[1], description='foobar2', + mode=InterfaceModeChoices.MODE_TAGGED, vlan_translation_policy=vlan_translation_policies[0], ), VMInterface( @@ -699,6 +701,10 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_mode(self): + params = {'mode': [InterfaceModeChoices.MODE_ACCESS]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_vlan(self): vlan = VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE).first() params = {'vlan_id': vlan.pk}