Skip to content

Commit

Permalink
Misc cleanup; add tests for Q-in-Q fields
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Oct 28, 2024
1 parent ddcff55 commit 322ad85
Show file tree
Hide file tree
Showing 17 changed files with 127 additions and 23 deletions.
2 changes: 1 addition & 1 deletion docs/models/ipam/vlan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions netbox/dcim/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')]]
Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ class Meta:
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.")
Expand Down
6 changes: 6 additions & 0 deletions netbox/dcim/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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
Expand Down Expand Up @@ -1618,6 +1619,7 @@ def setUpTestData(cls):
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)

Expand Down Expand Up @@ -1676,18 +1678,22 @@ def setUpTestData(cls):
'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,
},
]

Expand Down
31 changes: 26 additions & 5 deletions netbox/dcim/tests/test_filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
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
Expand Down Expand Up @@ -3520,7 +3521,7 @@ def test_connected(self):
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):
Expand Down Expand Up @@ -3669,6 +3670,13 @@ def setUpTestData(cls):
)
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],
Expand Down Expand Up @@ -3742,7 +3750,9 @@ def setUpTestData(cls):
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],
Expand All @@ -3751,7 +3761,9 @@ def setUpTestData(cls):
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],
Expand All @@ -3760,7 +3772,9 @@ def setUpTestData(cls):
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],
Expand Down Expand Up @@ -4016,6 +4030,13 @@ def test_vdc_identifier(self):
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()
Expand Down
2 changes: 1 addition & 1 deletion netbox/ipam/api/serializers_/vlans.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions netbox/ipam/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)'),
)
Expand Down
10 changes: 9 additions & 1 deletion netbox/ipam/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions netbox/ipam/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand 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(),
Expand Down
2 changes: 1 addition & 1 deletion netbox/ipam/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
6 changes: 5 additions & 1 deletion netbox/ipam/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ class ServiceTemplateType(NetBoxObjectType):

@strawberry_django.type(
models.VLAN,
fields='__all__',
exclude=('qinq_svlan',),
filters=VLANFilter
)
class VLANType(NetBoxObjectType):
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions netbox/ipam/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,7 @@ def setUpTestData(cls):
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)

Expand All @@ -999,6 +1000,12 @@ def setUpTestData(cls):
'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):
Expand Down
24 changes: 24 additions & 0 deletions netbox/ipam/tests/test_filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,7 @@ def setUpTestData(cls):
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)

Expand Down Expand Up @@ -1784,9 +1785,21 @@ def setUpTestData(cls):

# 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])
Expand Down Expand Up @@ -1897,6 +1910,17 @@ def test_vminterface(self):
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()
Expand Down
8 changes: 7 additions & 1 deletion netbox/templates/ipam/vlan.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ <h2 class="card-header">{% trans "VLAN" %}</h2>
</tr>
<tr>
<th scope="row">{% trans "Q-in-Q Role" %}</th>
<td>{% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %}</td>
<td>
{% if object.qinq_role %}
{% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
{% if object.qinq_role == 'c-vlan' %}
<tr>
Expand Down
1 change: 1 addition & 0 deletions netbox/virtualization/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')]]
Expand Down
8 changes: 8 additions & 0 deletions netbox/virtualization/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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 *
Expand Down Expand Up @@ -270,6 +271,7 @@ def setUpTestData(cls):
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)

Expand Down Expand Up @@ -307,6 +309,12 @@ def setUpTestData(cls):
'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):
Expand Down
Loading

0 comments on commit 322ad85

Please sign in to comment.