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.
This commit is contained in:
Martin Hauser 2025-04-15 19:47:51 +02:00 committed by GitHub
parent 70cc7c7563
commit 1f93471659
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 26 additions and 2 deletions

View File

@ -1689,6 +1689,10 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
class CommonInterfaceFilterSet(django_filters.FilterSet): class CommonInterfaceFilterSet(django_filters.FilterSet):
mode = django_filters.MultipleChoiceFilter(
choices=InterfaceModeChoices,
label=_('802.1Q Mode')
)
vlan_id = django_filters.CharFilter( vlan_id = django_filters.CharFilter(
method='filter_vlan_id', method='filter_vlan_id',
label=_('Assigned VLAN') label=_('Assigned VLAN')

View File

@ -1332,6 +1332,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')), FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')), FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
FieldSet('poe_mode', 'poe_type', name=_('PoE')), 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('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('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet( FieldSet(
@ -1403,6 +1404,11 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
required=False, required=False,
label=_('PoE type') label=_('PoE type')
) )
mode = forms.MultipleChoiceField(
choices=InterfaceModeChoices,
required=False,
label=_('802.1Q mode')
)
rf_role = forms.MultipleChoiceField( rf_role = forms.MultipleChoiceField(
choices=WirelessRoleChoices, choices=WirelessRoleChoices,
required=False, required=False,

View File

@ -4153,7 +4153,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_mode(self): def test_mode(self):
params = {'mode': InterfaceModeChoices.MODE_ACCESS} params = {'mode': [InterfaceModeChoices.MODE_ACCESS]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self): def test_description(self):

View File

@ -1,6 +1,7 @@
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.models import Device, DeviceRole, Location, Platform, Region, Site, SiteGroup from dcim.models import Device, DeviceRole, Location, Platform, Region, Site, SiteGroup
from extras.forms import LocalConfigContextFilterForm from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate from extras.models import ConfigTemplate
@ -200,7 +201,9 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('cluster_id', 'virtual_machine_id', name=_('Virtual Machine')), 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') selector_fields = ('filter_id', 'q', 'virtual_machine_id')
cluster_id = DynamicModelMultipleChoiceField( cluster_id = DynamicModelMultipleChoiceField(
@ -237,6 +240,11 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
required=False, required=False,
label=_('L2VPN') label=_('L2VPN')
) )
mode = forms.MultipleChoiceField(
choices=InterfaceModeChoices,
required=False,
label=_('802.1Q mode')
)
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -605,6 +605,7 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
mtu=100, mtu=100,
vrf=vrfs[0], vrf=vrfs[0],
description='foobar1', description='foobar1',
mode=InterfaceModeChoices.MODE_ACCESS,
vlan_translation_policy=vlan_translation_policies[0], vlan_translation_policy=vlan_translation_policies[0],
), ),
VMInterface( VMInterface(
@ -614,6 +615,7 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
mtu=200, mtu=200,
vrf=vrfs[1], vrf=vrfs[1],
description='foobar2', description='foobar2',
mode=InterfaceModeChoices.MODE_TAGGED,
vlan_translation_policy=vlan_translation_policies[0], vlan_translation_policy=vlan_translation_policies[0],
), ),
VMInterface( VMInterface(
@ -699,6 +701,10 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 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): def test_vlan(self):
vlan = VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE).first() vlan = VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE).first()
params = {'vlan_id': vlan.pk} params = {'vlan_id': vlan.pk}