diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index da69f4138..71d1e3f53 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -567,7 +567,7 @@ class BaseInterface(models.Model): related_name='%(class)ss_svlan', null=True, blank=True, - verbose_name=_('Q-inQ SVLAN') + verbose_name=_('Q-in-Q SVLAN') ) class Meta: diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 061dfe2bd..ab055c195 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -282,6 +282,12 @@ class VLAN(PrimaryModel): 'qinq_svlan': _("Only Q-in-Q customer VLANs maybe assigned to a service VLAN.") }) + # A Q-in-Q customer VLAN must be assigned to a service VLAN + if self.qinq_role == VLANQinQRoleChoices.ROLE_CUSTOMER and not self.qinq_svlan: + raise ValidationError({ + 'qinq_role': _("A Q-in-Q customer VLAN must be assigned to a service VLAN.") + }) + def get_status_color(self): return VLANStatusChoices.colors.get(self.status) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index d14fa0657..917b50f33 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -586,3 +586,24 @@ class TestVLANGroup(TestCase): vlangroup.vid_ranges = string_to_ranges('2-2') vlangroup.full_clean() vlangroup.save() + + +class TestVLAN(TestCase): + + @classmethod + def setUpTestData(cls): + VLAN.objects.bulk_create(( + VLAN(name='VLAN 1', vid=1, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE), + )) + + def test_qinq_role(self): + svlan = VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE).first() + + vlan = VLAN( + name='VLAN X', + vid=999, + qinq_role=VLANQinQRoleChoices.ROLE_SERVICE, + qinq_svlan=svlan + ) + with self.assertRaises(ValidationError): + vlan.full_clean()