From cf72b7cb635f0a175d69fe996fb1b14a523267fc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 28 Oct 2024 12:26:19 -0400 Subject: [PATCH] Misc cleanup; add tests for Q-in-Q fields --- docs/models/ipam/vlan.md | 2 +- netbox/dcim/graphql/types.py | 1 + netbox/dcim/models/device_components.py | 2 +- netbox/dcim/tests/test_api.py | 6 ++++ netbox/dcim/tests/test_filtersets.py | 31 ++++++++++++++++--- netbox/ipam/api/serializers_/vlans.py | 2 +- netbox/ipam/filtersets.py | 5 ++- netbox/ipam/forms/bulk_edit.py | 10 +++++- netbox/ipam/forms/bulk_import.py | 11 ++++--- netbox/ipam/forms/filtersets.py | 2 +- netbox/ipam/graphql/types.py | 6 +++- netbox/ipam/tests/test_api.py | 7 +++++ netbox/ipam/tests/test_filtersets.py | 24 ++++++++++++++ netbox/templates/ipam/vlan.html | 8 ++++- netbox/virtualization/graphql/types.py | 1 + netbox/virtualization/tests/test_api.py | 8 +++++ .../virtualization/tests/test_filtersets.py | 24 ++++++++++++-- 17 files changed, 127 insertions(+), 23 deletions(-) diff --git a/docs/models/ipam/vlan.md b/docs/models/ipam/vlan.md index 722c115ca..dc547ddbc 100644 --- a/docs/models/ipam/vlan.md +++ b/docs/models/ipam/vlan.md @@ -33,4 +33,4 @@ For VLANs which comprise a Q-in-Q/IEEE 802.1ad topology, this field indicates wh ### Q-in-Q Service VLAN -The designated parent service VLAN for a Q-in-Q customer VLAN. +The designated parent service VLAN for a Q-in-Q customer VLAN. This may be set only for Q-in-Q custom VLANs. diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index cd863837a..24ce83076 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -385,6 +385,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P wireless_link: Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')] | None untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None + qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]] tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index e43ea349c..da69f4138 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -576,7 +576,7 @@ class BaseInterface(models.Model): def clean(self): super().clean() - # Virtual Interfaces cannot have a Cable attached + # SVLAN can be defined only for Q-in-Q interfaces if self.qinq_svlan and self.mode != InterfaceModeChoices.MODE_Q_IN_Q: raise ValidationError({ 'qinq_svlan': _("Only Q-in-Q interfaces may specify a service VLAN.") diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 1b460cd59..f78722b67 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -7,6 +7,7 @@ from dcim.choices import * from dcim.constants import * from dcim.models import * from extras.models import ConfigTemplate +from ipam.choices import VLANQinQRoleChoices from ipam.models import ASN, RIR, VLAN, VRF from netbox.api.serializers import GenericObjectSerializer from tenancy.models import Tenant @@ -1618,6 +1619,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase VLAN(name='VLAN 1', vid=1), VLAN(name='VLAN 2', vid=2), VLAN(name='VLAN 3', vid=3), + VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), ) VLAN.objects.bulk_create(vlans) @@ -1676,18 +1678,22 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase 'vdcs': [vdcs[1].pk], 'name': 'Interface 7', 'type': InterfaceTypeChoices.TYPE_80211A, + 'mode': InterfaceModeChoices.MODE_Q_IN_Q, 'tx_power': 10, 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk], 'rf_channel': WirelessChannelChoices.CHANNEL_5G_32, + 'qinq_svlan': vlans[3].pk, }, { 'device': device.pk, 'vdcs': [vdcs[1].pk], 'name': 'Interface 8', 'type': InterfaceTypeChoices.TYPE_80211A, + 'mode': InterfaceModeChoices.MODE_Q_IN_Q, 'tx_power': 10, 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk], 'rf_channel': "", + 'qinq_svlan': vlans[3].pk, }, ] diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index d19c51564..04ab6793d 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -4,7 +4,8 @@ from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from dcim.choices import * from dcim.filtersets import * from dcim.models import * -from ipam.models import ASN, IPAddress, RIR, VRF +from ipam.choices import VLANQinQRoleChoices +from ipam.models import ASN, IPAddress, RIR, VLAN, VRF from netbox.choices import ColorChoices, WeightUnitChoices from tenancy.models import Tenant, TenantGroup from users.models import User @@ -3520,7 +3521,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = Interface.objects.all() filterset = InterfaceFilterSet - ignore_fields = ('tagged_vlans', 'untagged_vlan', 'vdcs') + ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs') @classmethod def setUpTestData(cls): @@ -3669,6 +3670,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ) VirtualDeviceContext.objects.bulk_create(vdcs) + vlans = ( + VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + VLAN(name='SVLAN 2', vid=1002, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + VLAN(name='SVLAN 3', vid=1003, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + ) + VLAN.objects.bulk_create(vlans) + interfaces = ( Interface( device=devices[0], @@ -3742,7 +3750,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil speed=100000, duplex='full', poe_mode=InterfacePoEModeChoices.MODE_PD, - poe_type=InterfacePoETypeChoices.TYPE_2_8023AT + poe_type=InterfacePoETypeChoices.TYPE_2_8023AT, + mode=InterfaceModeChoices.MODE_Q_IN_Q, + qinq_svlan=vlans[0] ), Interface( device=devices[4], @@ -3751,7 +3761,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, - tx_power=40 + tx_power=40, + mode=InterfaceModeChoices.MODE_Q_IN_Q, + qinq_svlan=vlans[1] ), Interface( device=devices[4], @@ -3760,7 +3772,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False, - tx_power=40 + tx_power=40, + mode=InterfaceModeChoices.MODE_Q_IN_Q, + qinq_svlan=vlans[2] ), Interface( device=devices[4], @@ -4016,6 +4030,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil params = {'vdc_identifier': vdc.values_list('identifier', flat=True)} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + def test_vlan(self): + vlan = VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE).first() + params = {'vlan_id': vlan.pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'vlan': vlan.vid} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = FrontPort.objects.all() diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py index da2fe989e..c01e6e356 100644 --- a/netbox/ipam/api/serializers_/vlans.py +++ b/netbox/ipam/api/serializers_/vlans.py @@ -64,7 +64,7 @@ class VLANSerializer(NetBoxModelSerializer): status = ChoiceField(choices=VLANStatusChoices, required=False) role = RoleSerializer(nested=True, required=False, allow_null=True) qinq_role = ChoiceField(choices=VLANQinQRoleChoices, required=False) - qinq_svlan = NestedVLANSerializer(required=False, allow_null=True) + qinq_svlan = NestedVLANSerializer(required=False, allow_null=True, default=None) l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True) # Related object counts diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 13dcd743f..26ce3eac0 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -1040,14 +1040,13 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): method='get_for_virtualmachine' ) qinq_role = django_filters.MultipleChoiceFilter( - choices=VLANQinQRoleChoices, - null_value=None + choices=VLANQinQRoleChoices ) qinq_svlan_id = django_filters.ModelMultipleChoiceFilter( queryset=VLAN.objects.all(), label=_('Q-in-Q SVLAN (ID)'), ) - qinq_svlan_vid = django_filters.NumberFilter( + qinq_svlan_vid = MultiValueNumberFilter( field_name='qinq_svlan__vid', label=_('Q-in-Q SVLAN number (1-4094)'), ) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 0887e7f98..a1ae33872 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -526,10 +526,18 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): required=False ) qinq_role = forms.ChoiceField( - label=_('Status'), + label=_('Q-in-Q role'), choices=add_blank_choice(VLANQinQRoleChoices), required=False ) + qinq_svlan = DynamicModelChoiceField( + label=_('Q-in-Q SVLAN'), + queryset=VLAN.objects.all(), + required=False, + query_params={ + 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE, + } + ) comments = CommentField() model = VLAN diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 0688021b3..5b4c06229 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -440,11 +440,6 @@ class VLANImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Assigned VLAN group') ) - qinq_role = CSVChoiceField( - label=_('Q-in-Q role'), - choices=VLANStatusChoices, - help_text=_('Operational status') - ) tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -464,6 +459,12 @@ class VLANImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Functional role') ) + qinq_role = CSVChoiceField( + label=_('Q-in-Q role'), + choices=VLANStatusChoices, + required=False, + help_text=_('Operational status') + ) qinq_svlan = CSVModelChoiceField( label=_('Q-in-Q SVLAN'), queryset=VLAN.objects.all(), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index c96147a86..08e5a2e58 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -467,7 +467,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')), - FieldSet('qinq_role', 'qinq_svlan', name=_('Q-in-Q/802.1ad')), + FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) selector_fields = ('filter_id', 'q', 'site_id') diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 9fe1fe466..d8f05b9d8 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -234,7 +234,7 @@ class ServiceTemplateType(NetBoxObjectType): @strawberry_django.type( models.VLAN, - fields='__all__', + exclude=('qinq_svlan',), filters=VLANFilter ) class VLANType(NetBoxObjectType): @@ -250,6 +250,10 @@ class VLANType(NetBoxObjectType): interfaces_as_tagged: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]] vminterfaces_as_tagged: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]] + @strawberry_django.field + def qinq_svlan(self) -> Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None: + return self.qinq_svlan + @strawberry_django.type( models.VLANGroup, diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 1d2cdf1b7..dcfea579c 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -980,6 +980,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase): VLAN(name='VLAN 1', vid=1, group=vlan_groups[0]), VLAN(name='VLAN 2', vid=2, group=vlan_groups[0]), VLAN(name='VLAN 3', vid=3, group=vlan_groups[0]), + VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), ) VLAN.objects.bulk_create(vlans) @@ -999,6 +1000,12 @@ class VLANTest(APIViewTestCases.APIViewTestCase): 'name': 'VLAN 6', 'group': vlan_groups[1].pk, }, + { + 'vid': 2001, + 'name': 'CVLAN 1', + 'qinq_role': VLANQinQRoleChoices.ROLE_CUSTOMER, + 'qinq_svlan': vlans[3].pk, + }, ] def test_delete_vlan_with_prefix(self): diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 7bc372fbf..02d4a244c 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1630,6 +1630,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): Site(name='Site 4', slug='site-4', region=regions[0], group=site_groups[0]), Site(name='Site 5', slug='site-5', region=regions[1], group=site_groups[1]), Site(name='Site 6', slug='site-6', region=regions[2], group=site_groups[2]), + Site(name='Site 7', slug='site-7'), ) Site.objects.bulk_create(sites) @@ -1784,9 +1785,21 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): # Create one globally available VLAN VLAN(vid=1000, name='Global VLAN'), + + # Create some Q-in-Q service VLANs + VLAN(vid=2001, name='SVLAN 1', site=sites[6], qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + VLAN(vid=2002, name='SVLAN 2', site=sites[6], qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + VLAN(vid=2003, name='SVLAN 3', site=sites[6], qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), ) VLAN.objects.bulk_create(vlans) + # Create Q-in-Q customer VLANs + VLAN.objects.bulk_create([ + VLAN(vid=3001, name='CVLAN 1', site=sites[6], qinq_svlan=vlans[29], qinq_role=VLANQinQRoleChoices.ROLE_CUSTOMER), + VLAN(vid=3002, name='CVLAN 2', site=sites[6], qinq_svlan=vlans[30], qinq_role=VLANQinQRoleChoices.ROLE_CUSTOMER), + VLAN(vid=3003, name='CVLAN 3', site=sites[6], qinq_svlan=vlans[31], qinq_role=VLANQinQRoleChoices.ROLE_CUSTOMER), + ]) + # Assign VLANs to device interfaces interfaces[0].untagged_vlan = vlans[0] interfaces[0].tagged_vlans.add(vlans[1]) @@ -1897,6 +1910,17 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'vminterface_id': vminterface_id} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_qinq_role(self): + params = {'qinq_role': [VLANQinQRoleChoices.ROLE_SERVICE, VLANQinQRoleChoices.ROLE_CUSTOMER]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) + + def test_qinq_svlan(self): + vlans = VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE)[:2] + params = {'qinq_svlan_id': [vlans[0].pk, vlans[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'qinq_svlan_vid': [vlans[0].vid, vlans[1].vid]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ServiceTemplate.objects.all() diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 5165be4bf..a10a1439a 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -64,7 +64,13 @@ {% trans "Q-in-Q Role" %} - {% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %} + + {% if object.qinq_role %} + {% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %} + {% else %} + {{ ''|placeholder }} + {% endif %} + {% if object.qinq_role == 'c-vlan' %} diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 2d872322b..09b797483 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -100,6 +100,7 @@ class VMInterfaceType(IPAddressesMixin, ComponentType): bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None + qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]] bridge_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]] diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 69728f67c..521064fc6 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -4,6 +4,7 @@ from rest_framework import status from dcim.choices import InterfaceModeChoices from dcim.models import Site from extras.models import ConfigTemplate +from ipam.choices import VLANQinQRoleChoices from ipam.models import VLAN, VRF from utilities.testing import APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine from virtualization.choices import * @@ -270,6 +271,7 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): VLAN(name='VLAN 1', vid=1), VLAN(name='VLAN 2', vid=2), VLAN(name='VLAN 3', vid=3), + VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), ) VLAN.objects.bulk_create(vlans) @@ -307,6 +309,12 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): 'untagged_vlan': vlans[2].pk, 'vrf': vrfs[2].pk, }, + { + 'virtual_machine': virtualmachine.pk, + 'name': 'Interface 7', + 'mode': InterfaceModeChoices.MODE_Q_IN_Q, + 'qinq_svlan': vlans[3].pk, + }, ] def test_bulk_delete_child_interfaces(self): diff --git a/netbox/virtualization/tests/test_filtersets.py b/netbox/virtualization/tests/test_filtersets.py index d2e6cc05f..e278aee6c 100644 --- a/netbox/virtualization/tests/test_filtersets.py +++ b/netbox/virtualization/tests/test_filtersets.py @@ -1,7 +1,9 @@ from django.test import TestCase +from dcim.choices import InterfaceModeChoices from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup -from ipam.models import IPAddress, VRF +from ipam.choices import VLANQinQRoleChoices +from ipam.models import IPAddress, VLAN, VRF from tenancy.models import Tenant, TenantGroup from utilities.testing import ChangeLoggedFilterSetTests, create_test_device from virtualization.choices import * @@ -528,7 +530,7 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests): class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VMInterface.objects.all() filterset = VMInterfaceFilterSet - ignore_fields = ('tagged_vlans', 'untagged_vlan',) + ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan') @classmethod def setUpTestData(cls): @@ -554,6 +556,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): ) VRF.objects.bulk_create(vrfs) + vlans = ( + VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + VLAN(name='SVLAN 2', vid=1002, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + VLAN(name='SVLAN 3', vid=1003, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + ) + VLAN.objects.bulk_create(vlans) + vms = ( VirtualMachine(name='Virtual Machine 1', cluster=clusters[0]), VirtualMachine(name='Virtual Machine 2', cluster=clusters[1]), @@ -587,7 +596,9 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): mtu=300, mac_address='00-00-00-00-00-03', vrf=vrfs[2], - description='foobar3' + description='foobar3', + mode=InterfaceModeChoices.MODE_Q_IN_Q, + qinq_svlan=vlans[0] ), ) VMInterface.objects.bulk_create(interfaces) @@ -658,6 +669,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_vlan(self): + vlan = VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE).first() + params = {'vlan_id': vlan.pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'vlan': vlan.vid} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + class VirtualDiskTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VirtualDisk.objects.all()