{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py
index 12bf4baab..96b2cb071 100644
--- a/netbox/tenancy/views.py
+++ b/netbox/tenancy/views.py
@@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from netbox.views import generic
from utilities.query import count_related
diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po
index 35a1fae7a..7692853f4 100644
--- a/netbox/translations/en/LC_MESSAGES/django.po
+++ b/netbox/translations/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-28 19:20+0000\n"
+"POT-Creation-Date: 2024-10-29 21:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
diff --git a/netbox/virtualization/api/serializers_/virtualmachines.py b/netbox/virtualization/api/serializers_/virtualmachines.py
index ecdc40b30..9b7000def 100644
--- a/netbox/virtualization/api/serializers_/virtualmachines.py
+++ b/netbox/virtualization/api/serializers_/virtualmachines.py
@@ -8,7 +8,7 @@ from dcim.api.serializers_.sites import SiteSerializer
from dcim.choices import InterfaceModeChoices
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from ipam.api.serializers_.ip import IPAddressSerializer
-from ipam.api.serializers_.vlans import VLANSerializer
+from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
from ipam.api.serializers_.vrfs import VRFSerializer
from ipam.models import VLAN
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
@@ -90,6 +90,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
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)
count_ipaddresses = serializers.IntegerField(read_only=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', 'qinq_svlan', 'vrf',
- 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
- 'count_fhrp_groups',
+ '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')
diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py
index 297f8baf8..4527e7f4c 100644
--- a/netbox/virtualization/forms/model_forms.py
+++ b/netbox/virtualization/forms/model_forms.py
@@ -7,7 +7,7 @@ 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, VRF
+from ipam.models import IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms import ConfirmationForm
@@ -354,23 +354,31 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
required=False,
label=_('VRF')
)
+ vlan_translation_policy = DynamicModelChoiceField(
+ queryset=VLANTranslationPolicy.objects.all(),
+ required=False,
+ label=_('VLAN Translation Policy')
+ )
fieldsets = (
FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
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', 'qinq_svlan', 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', 'qinq_svlan', 'vrf', 'tags',
+ '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(),
diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py
index 09b797483..79b5cb216 100644
--- a/netbox/virtualization/graphql/types.py
+++ b/netbox/virtualization/graphql/types.py
@@ -101,6 +101,7 @@ class VMInterfaceType(IPAddressesMixin, ComponentType):
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')]]
bridge_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
diff --git a/netbox/virtualization/migrations/0042_vminterface_vlan_translation_policy.py b/netbox/virtualization/migrations/0042_vminterface_vlan_translation_policy.py
new file mode 100644
index 000000000..3a6d5e481
--- /dev/null
+++ b/netbox/virtualization/migrations/0042_vminterface_vlan_translation_policy.py
@@ -0,0 +1,18 @@
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ipam', '0074_vlantranslationpolicy_vlantranslationrule'),
+ ('virtualization', '0041_charfield_null_choices'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='vminterface',
+ name='vlan_translation_policy',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlantranslationpolicy'),
+ ),
+ ]
diff --git a/netbox/virtualization/migrations/0042_qinq_svlan.py b/netbox/virtualization/migrations/0043_qinq_svlan.py
similarity index 86%
rename from netbox/virtualization/migrations/0042_qinq_svlan.py
rename to netbox/virtualization/migrations/0043_qinq_svlan.py
index 0076a607f..422289fb7 100644
--- a/netbox/virtualization/migrations/0042_qinq_svlan.py
+++ b/netbox/virtualization/migrations/0043_qinq_svlan.py
@@ -1,5 +1,3 @@
-# Generated by Django 5.0.9 on 2024-10-21 20:26
-
import django.db.models.deletion
from django.db import migrations, models
@@ -7,8 +5,8 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('ipam', '0074_vlan_qinq'),
- ('virtualization', '0041_charfield_null_choices'),
+ ('ipam', '0075_vlan_qinq'),
+ ('virtualization', '0042_vminterface_vlan_translation_policy'),
]
operations = [
diff --git a/netbox/virtualization/tests/test_filtersets.py b/netbox/virtualization/tests/test_filtersets.py
index e278aee6c..0c7079bba 100644
--- a/netbox/virtualization/tests/test_filtersets.py
+++ b/netbox/virtualization/tests/test_filtersets.py
@@ -3,7 +3,7 @@ from django.test import TestCase
from dcim.choices import InterfaceModeChoices
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
from ipam.choices import VLANQinQRoleChoices
-from ipam.models import IPAddress, VLAN, VRF
+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 *
@@ -570,6 +570,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
)
VirtualMachine.objects.bulk_create(vms)
+ vlan_translation_policies = (
+ VLANTranslationPolicy(name='Policy 1'),
+ VLANTranslationPolicy(name='Policy 2'),
+ VLANTranslationPolicy(name='Policy 3'),
+ )
+ VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
+
interfaces = (
VMInterface(
virtual_machine=vms[0],
@@ -578,7 +585,8 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
mtu=100,
mac_address='00-00-00-00-00-01',
vrf=vrfs[0],
- description='foobar1'
+ description='foobar1',
+ vlan_translation_policy=vlan_translation_policies[0],
),
VMInterface(
virtual_machine=vms[1],
@@ -587,7 +595,8 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
mtu=200,
mac_address='00-00-00-00-00-02',
vrf=vrfs[1],
- description='foobar2'
+ description='foobar2',
+ vlan_translation_policy=vlan_translation_policies[0],
),
VMInterface(
virtual_machine=vms[2],
@@ -676,6 +685,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
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]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'vlan_translation_policy': [vlan_translation_policies[0].name, vlan_translation_policies[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class VirtualDiskTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualDisk.objects.all()
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index 0828d3a2a..35f2f8f75 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -6,7 +6,7 @@ from django.db.models import Prefetch, Sum
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from django.views.generic.base import RedirectView
from jinja2.exceptions import TemplateError
@@ -16,7 +16,7 @@ from dcim.models import Device
from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView
from ipam.models import IPAddress
-from ipam.tables import InterfaceVLANTable
+from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from tenancy.views import ObjectContactsView
@@ -516,6 +516,14 @@ class VMInterfaceView(generic.ObjectView):
orderable=False
)
+ # Get VLAN translation rules
+ vlan_translation_table = None
+ if instance.vlan_translation_policy:
+ vlan_translation_table = VLANTranslationRuleTable(
+ data=instance.vlan_translation_policy.rules.all(),
+ orderable=False
+ )
+
# Get assigned VLANs and annotate whether each is tagged or untagged
vlans = []
if instance.untagged_vlan is not None:
@@ -533,6 +541,7 @@ class VMInterfaceView(generic.ObjectView):
return {
'child_interfaces_table': child_interfaces_tables,
'vlan_table': vlan_table,
+ 'vlan_translation_table': vlan_translation_table,
}