mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-25 12:59:59 -06:00
* Initial work on #13428 (QinQ) * Misc cleanup; add tests for Q-in-Q fields * Address PR feedback
This commit is contained in:
@@ -89,6 +89,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
qinq_svlan = VLANSerializer(nested=True, required=False, allow_null=True)
|
||||
vlan_translation_policy = VLANTranslationPolicySerializer(nested=True, required=False, allow_null=True)
|
||||
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
||||
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
||||
@@ -104,9 +105,9 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
||||
model = VMInterface
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu',
|
||||
'mac_address', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
|
||||
'vlan_translation_policy',
|
||||
'mac_address', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||
'vlan_translation_policy', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'count_ipaddresses', 'count_fhrp_groups',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from dcim.forms.common import InterfaceCommonForm
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.choices import VLANQinQRoleChoices
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
@@ -338,6 +339,16 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
||||
'available_on_virtualmachine': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
qinq_svlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
label=_('Q-in-Q Service VLAN'),
|
||||
query_params={
|
||||
'group_id': '$vlan_group',
|
||||
'available_on_virtualmachine': '$virtual_machine',
|
||||
'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
|
||||
}
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -354,17 +365,20 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
||||
FieldSet('vrf', 'mac_address', name=_('Addressing')),
|
||||
FieldSet('mtu', 'enabled', name=_('Operation')),
|
||||
FieldSet('parent', 'bridge', name=_('Related Interfaces')),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vlan_translation_policy', name=_('802.1Q Switching')),
|
||||
FieldSet(
|
||||
'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy',
|
||||
name=_('802.1Q Switching')
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
fields = [
|
||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
||||
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'vlan_translation_policy',
|
||||
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'tags',
|
||||
]
|
||||
labels = {
|
||||
'mode': '802.1Q Mode',
|
||||
'mode': _('802.1Q Mode'),
|
||||
}
|
||||
widgets = {
|
||||
'mode': HTMXSelect(),
|
||||
|
||||
@@ -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
|
||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-11 19:45
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
28
netbox/virtualization/migrations/0043_qinq_svlan.py
Normal file
28
netbox/virtualization/migrations/0043_qinq_svlan.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0075_vlan_qinq'),
|
||||
('virtualization', '0042_vminterface_vlan_translation_policy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vminterface',
|
||||
name='qinq_svlan',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss_svlan', to='ipam.vlan'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vminterface',
|
||||
name='tagged_vlans',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)ss_as_tagged', to='ipam.vlan'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vminterface',
|
||||
name='untagged_vlan',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss_as_untagged', to='ipam.vlan'),
|
||||
),
|
||||
]
|
||||
@@ -322,20 +322,6 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
untagged_vlan = models.ForeignKey(
|
||||
to='ipam.VLAN',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='vminterfaces_as_untagged',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_('untagged VLAN')
|
||||
)
|
||||
tagged_vlans = models.ManyToManyField(
|
||||
to='ipam.VLAN',
|
||||
related_name='vminterfaces_as_tagged',
|
||||
blank=True,
|
||||
verbose_name=_('tagged VLANs')
|
||||
)
|
||||
ip_addresses = GenericRelation(
|
||||
to='ipam.IPAddress',
|
||||
content_type_field='assigned_object_type',
|
||||
|
||||
@@ -151,8 +151,8 @@ class VMInterfaceTable(BaseInterfaceTable):
|
||||
model = VMInterface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created',
|
||||
'last_updated',
|
||||
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
||||
|
||||
@@ -175,7 +175,8 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
|
||||
model = VMInterface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
|
||||
row_attrs = {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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, VLANTranslationPolicy, VRF
|
||||
from ipam.choices import VLANQinQRoleChoices
|
||||
from ipam.models import IPAddress, VLAN, VLANTranslationPolicy, 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]),
|
||||
@@ -596,7 +605,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)
|
||||
@@ -667,6 +678,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)
|
||||
|
||||
def test_vlan_translation_policy(self):
|
||||
vlan_translation_policies = VLANTranslationPolicy.objects.all()[:2]
|
||||
params = {'vlan_translation_policy_id': [vlan_translation_policies[0].pk, vlan_translation_policies[1].pk]}
|
||||
|
||||
Reference in New Issue
Block a user