{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
diff --git a/netbox/virtualization/api/serializers_/virtualmachines.py b/netbox/virtualization/api/serializers_/virtualmachines.py
index 1b224c16a..2c00cac96 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
@@ -89,6 +89,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
required=False,
many=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)
@@ -105,6 +106,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
'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',
]
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 9ffc914ab..5971fc894 100644
--- a/netbox/virtualization/forms/model_forms.py
+++ b/netbox/virtualization/forms/model_forms.py
@@ -6,7 +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.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
@@ -343,20 +343,25 @@ 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', name=_('802.1Q Switching')),
+ FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', '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_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'vlan_translation_policy',
]
labels = {
'mode': '802.1Q Mode',
diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py
index 2d872322b..bed65a3b3 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
+ 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..e0992c9c8
--- /dev/null
+++ b/netbox/virtualization/migrations/0042_vminterface_vlan_translation_policy.py
@@ -0,0 +1,20 @@
+# Generated by Django 5.0.9 on 2024-10-11 19:45
+
+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/tests/test_filtersets.py b/netbox/virtualization/tests/test_filtersets.py
index d2e6cc05f..cd598274f 100644
--- a/netbox/virtualization/tests/test_filtersets.py
+++ b/netbox/virtualization/tests/test_filtersets.py
@@ -1,7 +1,7 @@
from django.test import TestCase
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
-from ipam.models import IPAddress, VRF
+from ipam.models import IPAddress, VLANTranslationPolicy, VRF
from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
from virtualization.choices import *
@@ -561,6 +561,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],
@@ -569,7 +576,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],
@@ -578,7 +586,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],
@@ -658,6 +667,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ 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 d1d65b1ff..35f2f8f75 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -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,
}