From f6591a9b9a2b119ba7df50374ae90e7f32a2e066 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Tue, 8 Oct 2024 14:02:31 -0400 Subject: [PATCH] VLANTranslationPolicy and VLANTranslationRule models and all associated UI classes --- netbox/dcim/forms/model_forms.py | 3 +- .../0192_interface_vlan_translation_policy.py | 20 ++++ netbox/dcim/models/device_components.py | 7 ++ netbox/dcim/views.py | 8 +- netbox/ipam/api/serializers_/vlans.py | 18 +++- netbox/ipam/filtersets.py | 16 +++ netbox/ipam/forms/bulk_edit.py | 25 +++++ netbox/ipam/forms/bulk_import.py | 16 +++ netbox/ipam/forms/filtersets.py | 28 +++++ netbox/ipam/forms/model_forms.py | 28 +++++ ...antranslationpolicy_vlantranslationrule.py | 51 +++++++++ netbox/ipam/models/vlans.py | 58 +++++++++- netbox/ipam/tables/vlans.py | 75 +++++++++++++ netbox/ipam/urls.py | 16 +++ netbox/ipam/views.py | 100 ++++++++++++++++++ netbox/netbox/navigation/menu.py | 2 + netbox/templates/dcim/interface.html | 9 ++ .../templates/ipam/vlantranslationpolicy.html | 37 +++++++ .../templates/ipam/vlantranslationrule.html | 41 +++++++ ...041_vminterface_vlan_translation_policy.py | 20 ++++ .../virtualization/models/virtualmachines.py | 7 ++ 21 files changed, 581 insertions(+), 4 deletions(-) create mode 100644 netbox/dcim/migrations/0192_interface_vlan_translation_policy.py create mode 100644 netbox/ipam/migrations/0071_vlantranslationpolicy_vlantranslationrule.py create mode 100644 netbox/templates/ipam/vlantranslationpolicy.html create mode 100644 netbox/templates/ipam/vlantranslationrule.html create mode 100644 netbox/virtualization/migrations/0041_vminterface_vlan_translation_policy.py diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 095882d13..8bc776fdf 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -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( diff --git a/netbox/dcim/migrations/0192_interface_vlan_translation_policy.py b/netbox/dcim/migrations/0192_interface_vlan_translation_policy.py new file mode 100644 index 000000000..2df9f6a4b --- /dev/null +++ b/netbox/dcim/migrations/0192_interface_vlan_translation_policy.py @@ -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'), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 1a86a250c..a8ca6200f 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -725,6 +725,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', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 98665a7a0..5dd7fc0a5 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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, } diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py index 608fcf0b4..95eb3d63d 100644 --- a/netbox/ipam/api/serializers_/vlans.py +++ b/netbox/ipam/api/serializers_/vlans.py @@ -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'] diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 894219c64..bf9a41212 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -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', diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index f4a7eabb7..c268e8a56 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -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'), diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index dea250c79..85cbcd343 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -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'), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index a32694321..f714b14b9 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -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 = ( diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 156e7c435..794642fa9 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -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'), diff --git a/netbox/ipam/migrations/0071_vlantranslationpolicy_vlantranslationrule.py b/netbox/ipam/migrations/0071_vlantranslationpolicy_vlantranslationrule.py new file mode 100644 index 000000000..460e74fd1 --- /dev/null +++ b/netbox/ipam/migrations/0071_vlantranslationpolicy_vlantranslationrule.py @@ -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'), + }, + ), + ] diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 23f7c41c7..4b50168db 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -10,13 +10,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', ) @@ -273,3 +275,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]) diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index 5387ce24c..fb0cf9c9a 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -16,6 +16,9 @@ __all__ = ( 'VLANMembersTable', 'VLANTable', 'VLANVirtualMachinesTable', + 'VLANTranslationPolicyTable', + 'VLANTranslationRuleTable', + 'InterfaceVLANTranslationTable', ) AVAILABLE_LABEL = mark_safe('Available') @@ -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) diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 61deeff4b..d40f9c5dc 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -116,6 +116,22 @@ urlpatterns = [ path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), path('vlans//', 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//', 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//', 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'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 67d56f15e..f8c0fadd9 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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 # diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 9d8ffaaf8..737e399a5 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -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( diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 016a6c890..410909914 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -133,6 +133,10 @@ {% trans "VRF" %} {{ object.vrf|linkify|placeholder }} + + {% trans "VLAN Translation" %} + {{ object.vlan_translation_policy|linkify|placeholder }} + {% if not object.is_virtual %} @@ -355,6 +359,11 @@ {% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %} +
+
+ {% include 'inc/panel_table.html' with table=vlan_translation_table heading="VLAN Translation" %} +
+
{% if object.is_bridge %}
diff --git a/netbox/templates/ipam/vlantranslationpolicy.html b/netbox/templates/ipam/vlantranslationpolicy.html new file mode 100644 index 000000000..616205428 --- /dev/null +++ b/netbox/templates/ipam/vlantranslationpolicy.html @@ -0,0 +1,37 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block content %} +
+
+
+

{% trans "VLAN Translation Policy" %}

+ + + + + + + + + +
{% trans "DNS Name" %}{{ object.name|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
+
+ {% plugin_left_page object %} +
+
+ {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/netbox/templates/ipam/vlantranslationrule.html b/netbox/templates/ipam/vlantranslationrule.html new file mode 100644 index 000000000..646e88b2b --- /dev/null +++ b/netbox/templates/ipam/vlantranslationrule.html @@ -0,0 +1,41 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block content %} +
+
+
+

{% trans "VLAN Translation Rule" %}

+ + + + + + + + + + + + + +
{% trans "Policy" %}{{ object.policy|placeholder }}
{% trans "Local VID" %}{{ object.local_vid|placeholder }}
{% trans "Remote VID" %}{{ object.remote_vid|placeholder }}
+
+ {% plugin_left_page object %} +
+
+ {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/netbox/virtualization/migrations/0041_vminterface_vlan_translation_policy.py b/netbox/virtualization/migrations/0041_vminterface_vlan_translation_policy.py new file mode 100644 index 000000000..cc28e43cc --- /dev/null +++ b/netbox/virtualization/migrations/0041_vminterface_vlan_translation_policy.py @@ -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'), + ), + ] diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index 0767b2c13..bb03784da 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -368,6 +368,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')