VLANTranslationPolicy and VLANTranslationRule models and all associated UI classes

This commit is contained in:
Brian Tiemann 2024-10-08 14:02:31 -04:00
parent ccb2480e98
commit aaa166a3b9
21 changed files with 581 additions and 4 deletions

View File

@ -1396,6 +1396,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
name=_('Wireless')
),
FieldSet('vlan_translation_policy', name=_('VLAN Translation'))
)
class Meta:
@ -1404,7 +1405,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'vlan_translation_policy',
]
widgets = {
'speed': NumberWithOptions(

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.9 on 2024-10-08 17:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0191_module_bay_rebuild'),
('ipam', '0071_vlantranslationpolicy_vlantranslationrule'),
]
operations = [
migrations.AddField(
model_name='interface',
name='vlan_translation_policy',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ipam.vlantranslationpolicy'),
),
]

View File

@ -735,6 +735,13 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
object_id_field='assigned_object_id',
related_query_name='interface',
)
vlan_translation_policy = models.ForeignKey(
to='ipam.VLANTranslationPolicy',
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('VLAN Translation Policy'),
)
clone_fields = (
'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role',

View File

@ -18,7 +18,7 @@ from jinja2.exceptions import TemplateError
from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, VLANGroup
from ipam.tables import InterfaceVLANTable
from ipam.tables import InterfaceVLANTable, InterfaceVLANTranslationTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from tenancy.views import ObjectContactsView
@ -2579,12 +2579,18 @@ class InterfaceView(generic.ObjectView):
data=vlans,
orderable=False
)
vlan_translation_table = InterfaceVLANTranslationTable(
interface=instance,
data=instance.vlan_translation_policy.rules.all() if instance.vlan_translation_policy else [],
orderable=False
)
return {
'vdc_table': vdc_table,
'bridge_interfaces_table': bridge_interfaces_tables,
'child_interfaces_table': child_interfaces_tables,
'vlan_table': vlan_table,
'vlan_translation_table': vlan_translation_table,
}

View File

@ -5,7 +5,7 @@ from rest_framework import serializers
from dcim.api.serializers_.sites import SiteSerializer
from ipam.choices import *
from ipam.constants import VLANGROUP_SCOPE_TYPES
from ipam.models import VLAN, VLANGroup
from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VLANTranslationRule
from netbox.api.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
@ -18,6 +18,8 @@ __all__ = (
'CreateAvailableVLANSerializer',
'VLANGroupSerializer',
'VLANSerializer',
'VLANTranslationPolicySerializer',
'VLANTranslationRuleSerializer',
)
@ -110,3 +112,17 @@ class CreateAvailableVLANSerializer(NetBoxModelSerializer):
def validate(self, data):
# Bypass model validation since we don't have a VID yet
return data
class VLANTranslationPolicySerializer(NetBoxModelSerializer):
class Meta:
model = VLANTranslationPolicy
fields = ['name', 'description']
class VLANTranslationRuleSerializer(NetBoxModelSerializer):
class Meta:
model = VLANTranslationRule
fields = ['policy', 'local_vid', 'remote_vid']

View File

@ -37,6 +37,8 @@ __all__ = (
'ServiceTemplateFilterSet',
'VLANFilterSet',
'VLANGroupFilterSet',
'VLANTranslationPolicyFilterSet',
'VLANTranslationRuleFilterSet',
'VRFFilterSet',
)
@ -1089,6 +1091,20 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
)
class VLANTranslationPolicyFilterSet(NetBoxModelFilterSet):
class Meta:
model = VLANTranslationPolicy
fields = ('id', 'name', 'description')
class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
class Meta:
model = VLANTranslationRule
fields = ('id', 'policy', 'local_vid', 'remote_vid')
class ServiceTemplateFilterSet(NetBoxModelFilterSet):
port = NumericArrayFilter(
field_name='ports',

View File

@ -33,6 +33,8 @@ __all__ = (
'ServiceTemplateBulkEditForm',
'VLANBulkEditForm',
'VLANGroupBulkEditForm',
'VLANTranslationPolicyBulkEditForm',
'VLANTranslationRuleBulkEditForm',
'VRFBulkEditForm',
)
@ -574,6 +576,29 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
)
class VLANTranslationPolicyBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = VLANTranslationPolicy
fieldsets = (
FieldSet('description'),
)
nullable_fields = ('description',)
class VLANTranslationRuleBulkEditForm(NetBoxModelBulkEditForm):
model = VLANTranslationRule
fieldsets = (
FieldSet('policy', 'local_vid', 'remote_vid'),
)
nullable_fields = ('description',)
class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
protocol = forms.ChoiceField(
label=_('Protocol'),

View File

@ -29,6 +29,8 @@ __all__ = (
'ServiceTemplateImportForm',
'VLANImportForm',
'VLANGroupImportForm',
'VLANTranslationPolicyImportForm',
'VLANTranslationRuleImportForm',
'VRFImportForm',
)
@ -464,6 +466,20 @@ class VLANImportForm(NetBoxModelImportForm):
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments', 'tags')
class VLANTranslationPolicyImportForm(NetBoxModelImportForm):
class Meta:
model = VLANTranslationPolicy
fields = ('name', 'description', 'tags')
class VLANTranslationRuleImportForm(NetBoxModelImportForm):
class Meta:
model = VLANTranslationRule
fields = ('policy', 'local_vid', 'remote_vid')
class ServiceTemplateImportForm(NetBoxModelImportForm):
protocol = CSVChoiceField(
label=_('Protocol'),

View File

@ -28,6 +28,8 @@ __all__ = (
'ServiceTemplateFilterForm',
'VLANFilterForm',
'VLANGroupFilterForm',
'VLANTranslationPolicyFilterForm',
'VLANTranslationRuleFilterForm',
'VRFFilterForm',
)
@ -460,6 +462,32 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class VLANTranslationPolicyFilterForm(NetBoxModelFilterSetForm):
model = VLANTranslationPolicy
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('name', 'description', name=_('Attributes')),
)
name = forms.CharField(
required=False,
label=_('Name')
)
description = forms.CharField(
required=False,
label=_('Name')
)
tag = TagFilterField(model)
class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm):
model = VLANTranslationRule
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('policy', 'local_vid', 'remote_vid', name=_('Attributes')),
)
tag = TagFilterField(model)
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VLAN
fieldsets = (

View File

@ -41,6 +41,8 @@ __all__ = (
'ServiceTemplateForm',
'VLANForm',
'VLANGroupForm',
'VLANTranslationPolicyForm',
'VLANTranslationRuleForm',
'VRFForm',
)
@ -654,6 +656,32 @@ class VLANForm(TenancyForm, NetBoxModelForm):
]
class VLANTranslationPolicyForm(NetBoxModelForm):
fieldsets = (
FieldSet('name', 'description', 'tags', name=_('VLAN Translation Policy')),
)
class Meta:
model = VLANTranslationPolicy
fields = [
'name', 'description',
]
class VLANTranslationRuleForm(NetBoxModelForm):
fieldsets = (
FieldSet('policy', 'local_vid', 'remote_vid', name=_('VLAN Translation Rule')),
)
class Meta:
model = VLANTranslationRule
fields = [
'policy', 'local_vid', 'remote_vid',
]
class ServiceTemplateForm(NetBoxModelForm):
ports = NumericArrayField(
label=_('Ports'),

View File

@ -0,0 +1,51 @@
# Generated by Django 5.0.9 on 2024-10-08 17:12
import django.db.models.deletion
import taggit.managers
import utilities.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0121_customfield_related_object_filter'),
('ipam', '0070_vlangroup_vlan_id_ranges'),
]
operations = [
migrations.CreateModel(
name='VLANTranslationPolicy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('slug', models.SlugField(max_length=100, unique=True)),
('name', models.CharField(max_length=100)),
('description', models.CharField(blank=True, max_length=200)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'VLAN translation policy',
'verbose_name_plural': 'VLAN translation policies',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='VLANTranslationRule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('local_vid', models.IntegerField()),
('remote_vid', models.IntegerField()),
('policy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='ipam.vlantranslationpolicy')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('policy', 'local_vid', 'remote_vid'),
},
),
]

View File

@ -11,13 +11,15 @@ from dcim.models import Interface
from ipam.choices import *
from ipam.constants import *
from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
from netbox.models import OrganizationalModel, PrimaryModel
from netbox.models import OrganizationalModel, PrimaryModel, NetBoxModel
from utilities.data import check_ranges_overlap, ranges_to_string
from virtualization.models import VMInterface
__all__ = (
'VLAN',
'VLANGroup',
'VLANTranslationPolicy',
'VLANTranslationRule',
)
@ -280,3 +282,57 @@ class VLAN(PrimaryModel):
@property
def l2vpn_termination(self):
return self.l2vpn_terminations.first()
class VLANTranslationPolicy(OrganizationalModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True,
)
class Meta:
verbose_name = _('VLAN translation policy')
verbose_name_plural = _('VLAN translation policies')
ordering = ('name',)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('ipam:vlantranslationpolicy', args=[self.pk])
class VLANTranslationRule(NetBoxModel):
policy = models.ForeignKey(
to=VLANTranslationPolicy,
related_name='rules',
on_delete=models.CASCADE,
)
local_vid = models.IntegerField()
remote_vid = models.IntegerField()
class Meta:
verbose_name = _('VLAN translation rule')
ordering = ('policy', 'local_vid', 'remote_vid',)
# Unique constraints are TBD
# constraints = (
# models.UniqueConstraint(
# fields=('policy', 'local_vid'),
# name='%(app_label)s_%(class)s_unique_policy_local_vid'
# ),
# models.UniqueConstraint(
# fields=('policy', 'remote_vid'),
# name='%(app_label)s_%(class)s_unique_policy_remote_vid'
# ),
# )
def __str__(self):
return f'{self.local_vid} -> {self.remote_vid} ({self.policy})'
def get_absolute_url(self):
return reverse('ipam:vlantranslationrule', args=[self.pk])

View File

@ -16,6 +16,9 @@ __all__ = (
'VLANMembersTable',
'VLANTable',
'VLANVirtualMachinesTable',
'VLANTranslationPolicyTable',
'VLANTranslationRuleTable',
'InterfaceVLANTranslationTable',
)
AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span>')
@ -244,3 +247,75 @@ class InterfaceVLANTable(NetBoxTable):
def __init__(self, interface, *args, **kwargs):
self.interface = interface
super().__init__(*args, **kwargs)
#
# VLAN Translation
#
class VLANTranslationPolicyTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
description = tables.Column(
verbose_name=_('Description'),
# linkify=True
)
tags = columns.TagColumn(
url_name='ipam:vlantranslationpolicy_list'
)
class Meta(NetBoxTable.Meta):
model = VLANTranslationPolicy
fields = (
'pk', 'id', 'name', 'description', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'description')
class VLANTranslationRuleTable(NetBoxTable):
policy = tables.Column(
verbose_name=_('Policy'),
linkify=True
)
local_vid = tables.Column(
verbose_name=_('Local VID'),
linkify=True
)
remote_vid = tables.Column(
verbose_name=_('Remote VID'),
)
tags = columns.TagColumn(
url_name='ipam:vlantranslationrule_list'
)
class Meta(NetBoxTable.Meta):
model = VLANTranslationRule
fields = (
'pk', 'id', 'name', 'policy', 'local_vid', 'remote_vid', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'local_vid', 'remote_vid', 'policy')
class InterfaceVLANTranslationTable(NetBoxTable):
policy = tables.Column(
verbose_name=_('Policy'),
linkify=True
)
local_vid = tables.Column(
verbose_name=_('Local VID'),
linkify=True,
)
remote_vid = tables.Column(
verbose_name=_('Remote VID'),
)
class Meta(NetBoxTable.Meta):
model = VLANTranslationRule
fields = ('local_vid', 'remote_vid')
default_columns = ('pk', 'local_vid', 'remote_vid', 'policy')
def __init__(self, interface, *args, **kwargs):
self.interface = interface
super().__init__(*args, **kwargs)

View File

@ -116,6 +116,22 @@ urlpatterns = [
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
path('vlans/<int:pk>/', include(get_model_urls('ipam', 'vlan'))),
# VLAN Translation Policies
path('vlan-translation-policies/', views.VLANTranslationPolicyListView.as_view(), name='vlantranslationpolicy_list'),
path('vlan-translation-policies/add/', views.VLANTranslationPolicyEditView.as_view(), name='vlantranslationpolicy_add'),
path('vlan-translation-policies/import/', views.VLANTranslationPolicyBulkImportView.as_view(), name='vlantranslationpolicy_import'),
path('vlan-translation-policies/edit/', views.VLANTranslationPolicyBulkEditView.as_view(), name='vlantranslationpolicy_bulk_edit'),
path('vlan-translation-policies/delete/', views.VLANTranslationPolicyBulkDeleteView.as_view(), name='vlantranslationpolicy_bulk_delete'),
path('vlan-translation-policies/<int:pk>/', include(get_model_urls('ipam', 'vlantranslationpolicy'))),
# VLAN Translation Rules
path('vlan-translation-rules/', views.VLANTranslationRuleListView.as_view(), name='vlantranslationrule_list'),
path('vlan-translation-rules/add/', views.VLANTranslationRuleEditView.as_view(), name='vlantranslationrule_add'),
path('vlan-translation-rules/import/', views.VLANTranslationRuleBulkImportView.as_view(), name='vlantranslationrule_import'),
path('vlan-translation-rules/edit/', views.VLANTranslationRuleBulkEditView.as_view(), name='vlantranslationrule_bulk_edit'),
path('vlan-translation-rules/delete/', views.VLANTranslationRuleBulkDeleteView.as_view(), name='vlantranslationrule_bulk_delete'),
path('vlan-translation-rules/<int:pk>/', include(get_model_urls('ipam', 'vlantranslationrule'))),
# Service templates
path('service-templates/', views.ServiceTemplateListView.as_view(), name='servicetemplate_list'),
path('service-templates/add/', views.ServiceTemplateEditView.as_view(), name='servicetemplate_add'),

View File

@ -986,6 +986,106 @@ class VLANGroupVLANsView(generic.ObjectChildrenView):
return queryset
#
# VLAN Translation Policies
#
class VLANTranslationPolicyListView(generic.ObjectListView):
queryset = VLANTranslationPolicy.objects.all()
filterset = filtersets.VLANTranslationPolicyFilterSet
filterset_form = forms.VLANTranslationPolicyFilterForm
table = tables.VLANTranslationPolicyTable
@register_model_view(VLANTranslationPolicy)
class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANTranslationPolicy.objects.all()
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(VLANTranslationPolicy, 'edit')
class VLANTranslationPolicyEditView(generic.ObjectEditView):
queryset = VLANTranslationPolicy.objects.all()
form = forms.VLANTranslationPolicyForm
@register_model_view(VLANTranslationPolicy, 'delete')
class VLANTranslationPolicyDeleteView(generic.ObjectDeleteView):
queryset = VLANTranslationPolicy.objects.all()
class VLANTranslationPolicyBulkImportView(generic.BulkImportView):
queryset = VLANTranslationPolicy.objects.all()
model_form = forms.VLANTranslationPolicyImportForm
class VLANTranslationPolicyBulkEditView(generic.BulkEditView):
queryset = VLANTranslationPolicy.objects.all()
filterset = filtersets.VLANTranslationPolicyFilterSet
table = tables.VLANTranslationPolicyTable
form = forms.VLANTranslationPolicyBulkEditForm
class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
queryset = VLANTranslationPolicy.objects.all()
filterset = filtersets.VLANTranslationPolicyFilterSet
table = tables.VLANTranslationPolicyTable
#
# VLAN Translation Policies
#
class VLANTranslationRuleListView(generic.ObjectListView):
queryset = VLANTranslationRule.objects.all()
filterset = filtersets.VLANTranslationRuleFilterSet
filterset_form = forms.VLANTranslationRuleFilterForm
table = tables.VLANTranslationRuleTable
@register_model_view(VLANTranslationRule)
class VLANTranslationRuleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANTranslationRule.objects.all()
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(VLANTranslationRule, 'edit')
class VLANTranslationRuleEditView(generic.ObjectEditView):
queryset = VLANTranslationRule.objects.all()
form = forms.VLANTranslationRuleForm
@register_model_view(VLANTranslationRule, 'delete')
class VLANTranslationRuleDeleteView(generic.ObjectDeleteView):
queryset = VLANTranslationRule.objects.all()
class VLANTranslationRuleBulkImportView(generic.BulkImportView):
queryset = VLANTranslationRule.objects.all()
model_form = forms.VLANTranslationRuleImportForm
class VLANTranslationRuleBulkEditView(generic.BulkEditView):
queryset = VLANTranslationRule.objects.all()
filterset = filtersets.VLANTranslationRuleFilterSet
table = tables.VLANTranslationRuleTable
form = forms.VLANTranslationRuleBulkEditForm
class VLANTranslationRuleBulkDeleteView(generic.BulkDeleteView):
queryset = VLANTranslationRule.objects.all()
filterset = filtersets.VLANTranslationRuleFilterSet
table = tables.VLANTranslationRuleTable
#
# FHRP groups
#

View File

@ -194,6 +194,8 @@ IPAM_MENU = Menu(
items=(
get_model_item('ipam', 'vlan', _('VLANs')),
get_model_item('ipam', 'vlangroup', _('VLAN Groups')),
get_model_item('ipam', 'vlantranslationpolicy', _('VLAN Translation Policies')),
get_model_item('ipam', 'vlantranslationrule', _('VLAN Translation Rules')),
),
),
MenuGroup(

View File

@ -133,6 +133,10 @@
<th scope="row">{% trans "VRF" %}</th>
<td>{{ object.vrf|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "VLAN Translation" %}</th>
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% if not object.is_virtual %}
@ -355,6 +359,11 @@
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=vlan_translation_table heading="VLAN Translation" %}
</div>
</div>
{% if object.is_bridge %}
<div class="row mb-3">
<div class="col col-md-12">

View File

@ -0,0 +1,37 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-4">
<div class="card">
<h2 class="card-header">{% trans "VLAN Translation Policy" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "DNS Name" %}</th>
<td>{{ object.name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-8">
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-4">
<div class="card">
<h2 class="card-header">{% trans "VLAN Translation Rule" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Policy" %}</th>
<td>{{ object.policy|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Local VID" %}</th>
<td>{{ object.local_vid|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Remote VID" %}</th>
<td>{{ object.remote_vid|placeholder }}</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-8">
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.9 on 2024-10-08 17:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0071_vlantranslationpolicy_vlantranslationrule'),
('virtualization', '0040_convert_disk_size'),
]
operations = [
migrations.AddField(
model_name='vminterface',
name='vlan_translation_policy',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ipam.vlantranslationpolicy'),
),
]

View File

@ -372,6 +372,13 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
object_id_field='assigned_object_id',
related_query_name='vminterface',
)
vlan_translation_policy = models.ForeignKey(
to='ipam.VLANTranslationPolicy',
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('VLAN Translation Policy'),
)
class Meta(ComponentModel.Meta):
verbose_name = _('interface')