mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
* VLANTranslationPolicy and VLANTranslationRule models and all associated UI classes
* Change VLANTranslationPolicy to a PrimaryModel and make name unique
* Add serializer classes to InterfaceSerializer
* Remake migrations
* Add GraphQL typing
* Skip tagged models in test
* Missing migration
* Remove get_absolute_url methods
* Remove package-lock.json
* Rebuild migration and add constraints and field options
* Rebuild migrations
* Use DynamicModelChoiceField for policy field
* Make vlan_translation_policy fields on filtersets more consistent with existing __name convention
* Add vlan_translation_table to VMInterface detail page
* Add vlan_translation_policy to VMInterfaceSerializer
* Move vlan_translation_policy fields to model and filterset mixins
* Protect in-use policies against deletion
* Add vlan_translation_policy to fields in VMInterfaceSerializer
* Cleanup indentation
* Remove unnecessary ordering column
* Rebuild migrations
* Search methods and registration
* Ensure 'id' column is present by default
* Add graphql types/filters/schema for VLANTranslationRule
* Filterset tests
* View tests
* API and viewset tests (incomplete)
* Add tags to VLANTranslationRuleForm
* Complete viewset tests for VLANTranslationRule
* Make VLANTranslationRule.policy nullable (but still required)
* Revert "Make VLANTranslationRule.policy nullable (but still required)"
This reverts commit 4c1bb437ef
.
* Revert nullability
* Explicitly prefetch policy in graphql
* Documentation of new and affected models
* Add note about select_related in graphql
* Rework policy/rule documentation
* Move vlan_translation_policy into 802.1Q Switching fieldset
* Remove redundant InterfaceVLANTranslationTable
* Conditionally include vlan_translation_table in interface.html and vminterface.html
* Add description field to VLANTranslationRule
* Define vlan_translation_table conditionally
* Add policy (name) filter to VLANTranslationRuleFilterSet
* Revert changes to adding-models.md (moved to another PR)
* Dynamic table for linked rules in vlantranslationpolicy.html
* Misc cleanup
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
19c5c32965
commit
f74a9a1c76
@ -142,3 +142,7 @@ The configured channel width of a wireless interface, in MHz. This is typically
|
|||||||
### Wireless LANs
|
### Wireless LANs
|
||||||
|
|
||||||
The [wireless LANs](../wireless/wirelesslan.md) for which this interface carries traffic. (Valid for wireless interfaces only.)
|
The [wireless LANs](../wireless/wirelesslan.md) for which this interface carries traffic. (Valid for wireless interfaces only.)
|
||||||
|
|
||||||
|
### VLAN Translation Policy
|
||||||
|
|
||||||
|
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).
|
||||||
|
26
docs/models/ipam/vlantranslationpolicy.md
Normal file
26
docs/models/ipam/vlantranslationpolicy.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# VLAN Translation Policies
|
||||||
|
|
||||||
|
VLAN translation is a feature that consists of VLAN translation policies and [VLAN translation rules](./vlantranslationrule.md). Many rules can belong to a policy, and each rule defines a mapping of a local to remote VLAN ID (VID). A policy can then be assigned to an [Interface](../dcim/interface.md) or [VMInterface](../virtualization/vminterface.md), and all VLAN translation rules associated with that policy will be visible in the interface details.
|
||||||
|
|
||||||
|
There are uniqueness constraints on `(policy, local_vid)` and on `(policy, remote_vid)` in the `VLANTranslationRule` model. Thus, you cannot have multiple rules linked to the same policy that have the same local VID or the same remote VID. A set of policies and rules might look like this:
|
||||||
|
|
||||||
|
Policy 1:
|
||||||
|
- Rule: 100 -> 200
|
||||||
|
- Rule: 101 -> 201
|
||||||
|
|
||||||
|
Policy 2:
|
||||||
|
- Rule: 100 -> 300
|
||||||
|
- Rule: 101 -> 301
|
||||||
|
|
||||||
|
However this is not allowed:
|
||||||
|
|
||||||
|
Policy 3:
|
||||||
|
- Rule: 100 -> 200
|
||||||
|
- Rule: 100 -> 300
|
||||||
|
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
A unique human-friendly name.
|
19
docs/models/ipam/vlantranslationrule.md
Normal file
19
docs/models/ipam/vlantranslationrule.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# VLAN Translation Rules
|
||||||
|
|
||||||
|
A VLAN translation rule represents a one-to-one mapping of a local VLAN ID (VID) to a remote VID. Many rules can belong to a single policy.
|
||||||
|
|
||||||
|
See [VLAN translation policies](./vlantranslationpolicy.md) for an overview of the VLAN Translation feature.
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### Policy
|
||||||
|
|
||||||
|
The [VLAN Translation Policy](./vlantranslationpolicy.md) to which this rule belongs.
|
||||||
|
|
||||||
|
### Local VID
|
||||||
|
|
||||||
|
VLAN ID (1-4094) in the local network which is to be translated to a remote VID.
|
||||||
|
|
||||||
|
### Remote VID
|
||||||
|
|
||||||
|
VLAN ID (1-4094) in the remote network to which the local VID will be translated.
|
@ -56,3 +56,7 @@ The tagged VLANs which are configured to be carried by this interface. Valid onl
|
|||||||
### VRF
|
### VRF
|
||||||
|
|
||||||
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
|
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
|
||||||
|
|
||||||
|
### VLAN Translation Policy
|
||||||
|
|
||||||
|
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).
|
||||||
|
@ -8,7 +8,7 @@ from dcim.models import (
|
|||||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
||||||
RearPort, VirtualDeviceContext,
|
RearPort, VirtualDeviceContext,
|
||||||
)
|
)
|
||||||
from ipam.api.serializers_.vlans import VLANSerializer
|
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
||||||
from ipam.api.serializers_.vrfs import VRFSerializer
|
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
@ -196,6 +196,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
required=False,
|
required=False,
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
vlan_translation_policy = VLANTranslationPolicySerializer(nested=True, required=False, allow_null=True)
|
||||||
vrf = VRFSerializer(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)
|
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
||||||
wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
|
wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
|
||||||
@ -225,7 +226,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link',
|
'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link',
|
||||||
'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', 'vlan_translation_policy'
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from circuits.models import CircuitTermination
|
|||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.filtersets import PrimaryIPFilterSet
|
from ipam.filtersets import PrimaryIPFilterSet
|
||||||
from ipam.models import ASN, IPAddress, VRF
|
from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
|
||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
from netbox.filtersets import (
|
from netbox.filtersets import (
|
||||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||||
@ -1629,6 +1629,17 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
|
|||||||
to_field_name='identifier',
|
to_field_name='identifier',
|
||||||
label=_('L2VPN'),
|
label=_('L2VPN'),
|
||||||
)
|
)
|
||||||
|
vlan_translation_policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vlan_translation_policy',
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
label=_('VLAN Translation Policy (ID)'),
|
||||||
|
)
|
||||||
|
vlan_translation_policy = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vlan_translation_policy__name',
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label=_('VLAN Translation Policy'),
|
||||||
|
)
|
||||||
|
|
||||||
def filter_vlan_id(self, queryset, name, value):
|
def filter_vlan_id(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
|
@ -7,7 +7,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from users.models import User
|
from users.models import User
|
||||||
@ -1382,6 +1382,11 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('WWN')
|
label=_('WWN')
|
||||||
)
|
)
|
||||||
|
vlan_translation_policy = DynamicModelChoiceField(
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('VLAN Translation Policy')
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@ -1391,7 +1396,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
||||||
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
||||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||||
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')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
|
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
|
||||||
name=_('Wireless')
|
name=_('Wireless')
|
||||||
@ -1404,7 +1409,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
|
'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',
|
'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',
|
'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 = {
|
widgets = {
|
||||||
'speed': NumberWithOptions(
|
'speed': NumberWithOptions(
|
||||||
|
@ -385,6 +385,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
|
|||||||
wireless_link: Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')] | None
|
wireless_link: Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')] | None
|
||||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
vrf: Annotated["VRFType", 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
|
||||||
|
|
||||||
vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]
|
vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||||
|
@ -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 = [
|
||||||
|
('dcim', '0194_charfield_null_choices'),
|
||||||
|
('ipam', '0074_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.PROTECT, to='ipam.vlantranslationpolicy'),
|
||||||
|
),
|
||||||
|
]
|
@ -547,6 +547,13 @@ class BaseInterface(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('bridge interface')
|
verbose_name=_('bridge interface')
|
||||||
)
|
)
|
||||||
|
vlan_translation_policy = models.ForeignKey(
|
||||||
|
to='ipam.VLANTranslationPolicy',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('VLAN Translation Policy'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -4,7 +4,7 @@ from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.filtersets import *
|
from dcim.filtersets import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import ASN, IPAddress, RIR, VRF
|
from ipam.models import ASN, IPAddress, RIR, VLANTranslationPolicy, VRF
|
||||||
from netbox.choices import ColorChoices, WeightUnitChoices
|
from netbox.choices import ColorChoices, WeightUnitChoices
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from users.models import User
|
from users.models import User
|
||||||
@ -3669,6 +3669,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
)
|
)
|
||||||
VirtualDeviceContext.objects.bulk_create(vdcs)
|
VirtualDeviceContext.objects.bulk_create(vdcs)
|
||||||
|
|
||||||
|
vlan_translation_policies = (
|
||||||
|
VLANTranslationPolicy(name='Policy 1'),
|
||||||
|
VLANTranslationPolicy(name='Policy 2'),
|
||||||
|
VLANTranslationPolicy(name='Policy 3'),
|
||||||
|
)
|
||||||
|
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
Interface(
|
Interface(
|
||||||
device=devices[0],
|
device=devices[0],
|
||||||
@ -3686,7 +3693,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
speed=1000000,
|
speed=1000000,
|
||||||
duplex='half',
|
duplex='half',
|
||||||
poe_mode=InterfacePoEModeChoices.MODE_PSE,
|
poe_mode=InterfacePoEModeChoices.MODE_PSE,
|
||||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
|
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
|
||||||
|
vlan_translation_policy=vlan_translation_policies[0],
|
||||||
),
|
),
|
||||||
Interface(
|
Interface(
|
||||||
device=devices[1],
|
device=devices[1],
|
||||||
@ -3711,7 +3719,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
speed=1000000,
|
speed=1000000,
|
||||||
duplex='full',
|
duplex='full',
|
||||||
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
||||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
|
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
|
||||||
|
vlan_translation_policy=vlan_translation_policies[0],
|
||||||
),
|
),
|
||||||
Interface(
|
Interface(
|
||||||
device=devices[3],
|
device=devices[3],
|
||||||
@ -3729,7 +3738,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
speed=100000,
|
speed=100000,
|
||||||
duplex='half',
|
duplex='half',
|
||||||
poe_mode=InterfacePoEModeChoices.MODE_PSE,
|
poe_mode=InterfacePoEModeChoices.MODE_PSE,
|
||||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
|
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
|
||||||
|
vlan_translation_policy=vlan_translation_policies[1],
|
||||||
),
|
),
|
||||||
Interface(
|
Interface(
|
||||||
device=devices[4],
|
device=devices[4],
|
||||||
@ -3742,7 +3752,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
speed=100000,
|
speed=100000,
|
||||||
duplex='full',
|
duplex='full',
|
||||||
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
||||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
|
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
|
||||||
|
vlan_translation_policy=vlan_translation_policies[1],
|
||||||
),
|
),
|
||||||
Interface(
|
Interface(
|
||||||
device=devices[4],
|
device=devices[4],
|
||||||
@ -4016,6 +4027,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
params = {'vdc_identifier': vdc.values_list('identifier', flat=True)}
|
params = {'vdc_identifier': vdc.values_list('identifier', flat=True)}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
|
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(), 4)
|
||||||
|
params = {'vlan_translation_policy': [vlan_translation_policies[0].name, vlan_translation_policies[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = FrontPort.objects.all()
|
queryset = FrontPort.objects.all()
|
||||||
|
@ -18,7 +18,7 @@ from jinja2.exceptions import TemplateError
|
|||||||
from circuits.models import Circuit, CircuitTermination
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView
|
||||||
from ipam.models import ASN, IPAddress, VLANGroup
|
from ipam.models import ASN, IPAddress, VLANGroup
|
||||||
from ipam.tables import InterfaceVLANTable
|
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from tenancy.views import ObjectContactsView
|
from tenancy.views import ObjectContactsView
|
||||||
@ -2580,11 +2580,20 @@ class InterfaceView(generic.ObjectView):
|
|||||||
orderable=False
|
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
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'vdc_table': vdc_table,
|
'vdc_table': vdc_table,
|
||||||
'bridge_interfaces_table': bridge_interfaces_tables,
|
'bridge_interfaces_table': bridge_interfaces_tables,
|
||||||
'child_interfaces_table': child_interfaces_tables,
|
'child_interfaces_table': child_interfaces_tables,
|
||||||
'vlan_table': vlan_table,
|
'vlan_table': vlan_table,
|
||||||
|
'vlan_translation_table': vlan_translation_table,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1172,6 +1172,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
'virtualmachine',
|
'virtualmachine',
|
||||||
'vlan',
|
'vlan',
|
||||||
'vlangroup',
|
'vlangroup',
|
||||||
|
'vlantranslationpolicy',
|
||||||
|
'vlantranslationrule',
|
||||||
'vminterface',
|
'vminterface',
|
||||||
'vrf',
|
'vrf',
|
||||||
'webhook',
|
'webhook',
|
||||||
|
@ -5,7 +5,7 @@ from rest_framework import serializers
|
|||||||
from dcim.api.serializers_.sites import SiteSerializer
|
from dcim.api.serializers_.sites import SiteSerializer
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import VLANGROUP_SCOPE_TYPES
|
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.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
@ -18,6 +18,8 @@ __all__ = (
|
|||||||
'CreateAvailableVLANSerializer',
|
'CreateAvailableVLANSerializer',
|
||||||
'VLANGroupSerializer',
|
'VLANGroupSerializer',
|
||||||
'VLANSerializer',
|
'VLANSerializer',
|
||||||
|
'VLANTranslationPolicySerializer',
|
||||||
|
'VLANTranslationRuleSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -110,3 +112,19 @@ class CreateAvailableVLANSerializer(NetBoxModelSerializer):
|
|||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
# Bypass model validation since we don't have a VID yet
|
# Bypass model validation since we don't have a VID yet
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRuleSerializer(NetBoxModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANTranslationRule
|
||||||
|
fields = ['id', 'policy', 'local_vid', 'remote_vid']
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicySerializer(NetBoxModelSerializer):
|
||||||
|
rules = VLANTranslationRuleSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANTranslationPolicy
|
||||||
|
fields = ['id', 'url', 'name', 'description', 'display', 'rules']
|
||||||
|
brief_fields = ('id', 'url', 'name', 'description', 'display')
|
||||||
|
@ -21,6 +21,8 @@ router.register('fhrp-groups', views.FHRPGroupViewSet)
|
|||||||
router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet)
|
router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet)
|
||||||
router.register('vlan-groups', views.VLANGroupViewSet)
|
router.register('vlan-groups', views.VLANGroupViewSet)
|
||||||
router.register('vlans', views.VLANViewSet)
|
router.register('vlans', views.VLANViewSet)
|
||||||
|
router.register('vlan-translation-policies', views.VLANTranslationPolicyViewSet)
|
||||||
|
router.register('vlan-translation-rules', views.VLANTranslationRuleViewSet)
|
||||||
router.register('service-templates', views.ServiceTemplateViewSet)
|
router.register('service-templates', views.ServiceTemplateViewSet)
|
||||||
router.register('services', views.ServiceViewSet)
|
router.register('services', views.ServiceViewSet)
|
||||||
|
|
||||||
|
@ -143,6 +143,18 @@ class VLANViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.VLANFilterSet
|
filterset_class = filtersets.VLANFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicyViewSet(NetBoxModelViewSet):
|
||||||
|
queryset = VLANTranslationPolicy.objects.all()
|
||||||
|
serializer_class = serializers.VLANTranslationPolicySerializer
|
||||||
|
filterset_class = filtersets.VLANTranslationPolicyFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRuleViewSet(NetBoxModelViewSet):
|
||||||
|
queryset = VLANTranslationRule.objects.all()
|
||||||
|
serializer_class = serializers.VLANTranslationRuleSerializer
|
||||||
|
filterset_class = filtersets.VLANTranslationRuleFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateViewSet(NetBoxModelViewSet):
|
class ServiceTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = ServiceTemplate.objects.all()
|
queryset = ServiceTemplate.objects.all()
|
||||||
serializer_class = serializers.ServiceTemplateSerializer
|
serializer_class = serializers.ServiceTemplateSerializer
|
||||||
|
@ -37,6 +37,8 @@ __all__ = (
|
|||||||
'ServiceTemplateFilterSet',
|
'ServiceTemplateFilterSet',
|
||||||
'VLANFilterSet',
|
'VLANFilterSet',
|
||||||
'VLANGroupFilterSet',
|
'VLANGroupFilterSet',
|
||||||
|
'VLANTranslationPolicyFilterSet',
|
||||||
|
'VLANTranslationRuleFilterSet',
|
||||||
'VRFFilterSet',
|
'VRFFilterSet',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1104,6 +1106,53 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicyFilterSet(NetBoxModelFilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANTranslationPolicy
|
||||||
|
fields = ('id', 'name', 'description')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
|
||||||
|
policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
label=_('VLAN Translation Policy (ID)'),
|
||||||
|
)
|
||||||
|
policy = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='policy__name',
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label=_('VLAN Translation Policy (name)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANTranslationRule
|
||||||
|
fields = ('id', 'policy_id', 'policy', 'local_vid', 'remote_vid', 'description')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(policy__name__icontains=value)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
int_value = int(value.strip())
|
||||||
|
qs_filter |= Q(local_vid=int_value)
|
||||||
|
qs_filter |= Q(remote_vid=int_value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateFilterSet(NetBoxModelFilterSet):
|
class ServiceTemplateFilterSet(NetBoxModelFilterSet):
|
||||||
port = NumericArrayFilter(
|
port = NumericArrayFilter(
|
||||||
field_name='ports',
|
field_name='ports',
|
||||||
|
@ -34,6 +34,8 @@ __all__ = (
|
|||||||
'ServiceTemplateBulkEditForm',
|
'ServiceTemplateBulkEditForm',
|
||||||
'VLANBulkEditForm',
|
'VLANBulkEditForm',
|
||||||
'VLANGroupBulkEditForm',
|
'VLANGroupBulkEditForm',
|
||||||
|
'VLANTranslationPolicyBulkEditForm',
|
||||||
|
'VLANTranslationRuleBulkEditForm',
|
||||||
'VRFBulkEditForm',
|
'VRFBulkEditForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -537,6 +539,36 @@ 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):
|
||||||
|
policy = DynamicModelChoiceField(
|
||||||
|
label=_('Policy'),
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
selector=True
|
||||||
|
)
|
||||||
|
local_vid = forms.IntegerField(required=False)
|
||||||
|
remote_vid = forms.IntegerField(required=False)
|
||||||
|
|
||||||
|
model = VLANTranslationRule
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('policy', 'local_vid', 'remote_vid'),
|
||||||
|
)
|
||||||
|
fields = ('policy', 'local_vid', 'remote_vid')
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
|
class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
protocol = forms.ChoiceField(
|
protocol = forms.ChoiceField(
|
||||||
label=_('Protocol'),
|
label=_('Protocol'),
|
||||||
|
@ -29,6 +29,8 @@ __all__ = (
|
|||||||
'ServiceTemplateImportForm',
|
'ServiceTemplateImportForm',
|
||||||
'VLANImportForm',
|
'VLANImportForm',
|
||||||
'VLANGroupImportForm',
|
'VLANGroupImportForm',
|
||||||
|
'VLANTranslationPolicyImportForm',
|
||||||
|
'VLANTranslationRuleImportForm',
|
||||||
'VRFImportForm',
|
'VRFImportForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -465,6 +467,20 @@ class VLANImportForm(NetBoxModelImportForm):
|
|||||||
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments', 'tags')
|
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):
|
class ServiceTemplateImportForm(NetBoxModelImportForm):
|
||||||
protocol = CSVChoiceField(
|
protocol = CSVChoiceField(
|
||||||
label=_('Protocol'),
|
label=_('Protocol'),
|
||||||
|
@ -28,6 +28,8 @@ __all__ = (
|
|||||||
'ServiceTemplateFilterForm',
|
'ServiceTemplateFilterForm',
|
||||||
'VLANFilterForm',
|
'VLANFilterForm',
|
||||||
'VLANGroupFilterForm',
|
'VLANGroupFilterForm',
|
||||||
|
'VLANTranslationPolicyFilterForm',
|
||||||
|
'VLANTranslationRuleFilterForm',
|
||||||
'VRFFilterForm',
|
'VRFFilterForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -461,6 +463,43 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicyFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
model = VLANTranslationPolicy
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('name', name=_('Attributes')),
|
||||||
|
)
|
||||||
|
name = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('Name')
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
model = VLANTranslationRule
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('policy_id', 'local_vid', 'remote_vid', name=_('Attributes')),
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
policy_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('VLAN Translation Policy')
|
||||||
|
)
|
||||||
|
local_vid = forms.IntegerField(
|
||||||
|
min_value=1,
|
||||||
|
required=False,
|
||||||
|
label=_('Local VLAN ID')
|
||||||
|
)
|
||||||
|
remote_vid = forms.IntegerField(
|
||||||
|
min_value=1,
|
||||||
|
required=False,
|
||||||
|
label=_('Remote VLAN ID')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
@ -41,6 +41,8 @@ __all__ = (
|
|||||||
'ServiceTemplateForm',
|
'ServiceTemplateForm',
|
||||||
'VLANForm',
|
'VLANForm',
|
||||||
'VLANGroupForm',
|
'VLANGroupForm',
|
||||||
|
'VLANTranslationPolicyForm',
|
||||||
|
'VLANTranslationRuleForm',
|
||||||
'VRFForm',
|
'VRFForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -691,6 +693,37 @@ class VLANForm(TenancyForm, NetBoxModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicyForm(NetBoxModelForm):
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('name', 'description', 'tags', name=_('VLAN Translation Policy')),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANTranslationPolicy
|
||||||
|
fields = [
|
||||||
|
'name', 'description', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRuleForm(NetBoxModelForm):
|
||||||
|
policy = DynamicModelChoiceField(
|
||||||
|
label=_('Policy'),
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
selector=True
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('policy', 'local_vid', 'remote_vid', 'description', 'tags', name=_('VLAN Translation Rule')),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VLANTranslationRule
|
||||||
|
fields = [
|
||||||
|
'policy', 'local_vid', 'remote_vid', 'description', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateForm(NetBoxModelForm):
|
class ServiceTemplateForm(NetBoxModelForm):
|
||||||
ports = NumericArrayField(
|
ports = NumericArrayField(
|
||||||
label=_('Ports'),
|
label=_('Ports'),
|
||||||
|
@ -19,6 +19,8 @@ __all__ = (
|
|||||||
'ServiceTemplateFilter',
|
'ServiceTemplateFilter',
|
||||||
'VLANFilter',
|
'VLANFilter',
|
||||||
'VLANGroupFilter',
|
'VLANGroupFilter',
|
||||||
|
'VLANTranslationPolicyFilter',
|
||||||
|
'VLANTranslationRuleFilter',
|
||||||
'VRFFilter',
|
'VRFFilter',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -113,6 +115,18 @@ class VLANGroupFilter(BaseFilterMixin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.filter(models.VLANTranslationPolicy, lookups=True)
|
||||||
|
@autotype_decorator(filtersets.VLANTranslationPolicyFilterSet)
|
||||||
|
class VLANTranslationPolicyFilter(BaseFilterMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.filter(models.VLANTranslationRule, lookups=True)
|
||||||
|
@autotype_decorator(filtersets.VLANTranslationRuleFilterSet)
|
||||||
|
class VLANTranslationRuleFilter(BaseFilterMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter(models.VRF, lookups=True)
|
@strawberry_django.filter(models.VRF, lookups=True)
|
||||||
@autotype_decorator(filtersets.VRFFilterSet)
|
@autotype_decorator(filtersets.VRFFilterSet)
|
||||||
class VRFFilter(BaseFilterMixin):
|
class VRFFilter(BaseFilterMixin):
|
||||||
|
@ -53,5 +53,11 @@ class IPAMQuery:
|
|||||||
vlan_group: VLANGroupType = strawberry_django.field()
|
vlan_group: VLANGroupType = strawberry_django.field()
|
||||||
vlan_group_list: List[VLANGroupType] = strawberry_django.field()
|
vlan_group_list: List[VLANGroupType] = strawberry_django.field()
|
||||||
|
|
||||||
|
vlan_translation_policy: VLANTranslationPolicyType = strawberry_django.field()
|
||||||
|
vlan_translation_policy_list: List[VLANTranslationPolicyType] = strawberry_django.field()
|
||||||
|
|
||||||
|
vlan_translation_rule: VLANTranslationRuleType = strawberry_django.field()
|
||||||
|
vlan_translation_rule_list: List[VLANTranslationRuleType] = strawberry_django.field()
|
||||||
|
|
||||||
vrf: VRFType = strawberry_django.field()
|
vrf: VRFType = strawberry_django.field()
|
||||||
vrf_list: List[VRFType] = strawberry_django.field()
|
vrf_list: List[VRFType] = strawberry_django.field()
|
||||||
|
@ -27,6 +27,8 @@ __all__ = (
|
|||||||
'ServiceTemplateType',
|
'ServiceTemplateType',
|
||||||
'VLANType',
|
'VLANType',
|
||||||
'VLANGroupType',
|
'VLANGroupType',
|
||||||
|
'VLANTranslationPolicyType',
|
||||||
|
'VLANTranslationRuleType',
|
||||||
'VRFType',
|
'VRFType',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -274,6 +276,24 @@ class VLANGroupType(OrganizationalObjectType):
|
|||||||
return self.scope
|
return self.scope
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(
|
||||||
|
models.VLANTranslationPolicy,
|
||||||
|
fields='__all__',
|
||||||
|
filters=VLANTranslationPolicyFilter
|
||||||
|
)
|
||||||
|
class VLANTranslationPolicyType(NetBoxObjectType):
|
||||||
|
rules: List[Annotated["VLANTranslationRuleType", strawberry.lazy('ipam.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(
|
||||||
|
models.VLANTranslationRule,
|
||||||
|
fields='__all__',
|
||||||
|
filters=VLANTranslationRuleFilter
|
||||||
|
)
|
||||||
|
class VLANTranslationRuleType(NetBoxObjectType):
|
||||||
|
policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] = strawberry_django.field(select_related=["policy"])
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.VRF,
|
models.VRF,
|
||||||
fields='__all__',
|
fields='__all__',
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-10-11 19:45
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
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', '0073_charfield_null_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
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)),
|
||||||
|
('comments', models.TextField(blank=True)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('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.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)])),
|
||||||
|
('remote_vid', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)])),
|
||||||
|
('policy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='ipam.vlantranslationpolicy')),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'VLAN translation rule',
|
||||||
|
'ordering': ('policy', 'local_vid',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='vlantranslationrule',
|
||||||
|
constraint=models.UniqueConstraint(fields=('policy', 'local_vid'), name='ipam_vlantranslationrule_unique_policy_local_vid'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='vlantranslationrule',
|
||||||
|
constraint=models.UniqueConstraint(fields=('policy', 'remote_vid'), name='ipam_vlantranslationrule_unique_policy_remote_vid'),
|
||||||
|
),
|
||||||
|
]
|
@ -10,13 +10,15 @@ from dcim.models import Interface
|
|||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
|
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 utilities.data import check_ranges_overlap, ranges_to_string
|
||||||
from virtualization.models import VMInterface
|
from virtualization.models import VMInterface
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'VLAN',
|
'VLAN',
|
||||||
'VLANGroup',
|
'VLANGroup',
|
||||||
|
'VLANTranslationPolicy',
|
||||||
|
'VLANTranslationRule',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -273,3 +275,73 @@ class VLAN(PrimaryModel):
|
|||||||
@property
|
@property
|
||||||
def l2vpn_termination(self):
|
def l2vpn_termination(self):
|
||||||
return self.l2vpn_terminations.first()
|
return self.l2vpn_terminations.first()
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicy(PrimaryModel):
|
||||||
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
|
max_length=100,
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('VLAN translation policy')
|
||||||
|
verbose_name_plural = _('VLAN translation policies')
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRule(NetBoxModel):
|
||||||
|
policy = models.ForeignKey(
|
||||||
|
to=VLANTranslationPolicy,
|
||||||
|
related_name='rules',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
local_vid = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('Local VLAN ID'),
|
||||||
|
validators=(
|
||||||
|
MinValueValidator(VLAN_VID_MIN),
|
||||||
|
MaxValueValidator(VLAN_VID_MAX)
|
||||||
|
),
|
||||||
|
help_text=_("Numeric VLAN ID (1-4094)")
|
||||||
|
)
|
||||||
|
remote_vid = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('Remote VLAN ID'),
|
||||||
|
validators=(
|
||||||
|
MinValueValidator(VLAN_VID_MIN),
|
||||||
|
MaxValueValidator(VLAN_VID_MAX)
|
||||||
|
),
|
||||||
|
help_text=_("Numeric VLAN ID (1-4094)")
|
||||||
|
)
|
||||||
|
prerequisite_models = (
|
||||||
|
'ipam.VLANTranslationPolicy',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('VLAN translation rule')
|
||||||
|
ordering = ('policy', 'local_vid',)
|
||||||
|
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 to_objectchange(self, action):
|
||||||
|
objectchange = super().to_objectchange(action)
|
||||||
|
objectchange.related_object = self.policy
|
||||||
|
return objectchange
|
||||||
|
@ -160,6 +160,27 @@ class VLANGroupIndex(SearchIndex):
|
|||||||
display_attrs = ('scope_type', 'description')
|
display_attrs = ('scope_type', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_search
|
||||||
|
class VLANTranslationPolicyIndex(SearchIndex):
|
||||||
|
model = models.VLANTranslationPolicy
|
||||||
|
fields = (
|
||||||
|
('name', 100),
|
||||||
|
('description', 500),
|
||||||
|
)
|
||||||
|
display_attrs = ('description',)
|
||||||
|
|
||||||
|
|
||||||
|
@register_search
|
||||||
|
class VLANTranslationRuleIndex(SearchIndex):
|
||||||
|
model = models.VLANTranslationRule
|
||||||
|
fields = (
|
||||||
|
('policy', 100),
|
||||||
|
('local_vid', 200),
|
||||||
|
('remote_vid', 200),
|
||||||
|
)
|
||||||
|
display_attrs = ('policy', 'local_vid', 'remote_vid')
|
||||||
|
|
||||||
|
|
||||||
@register_search
|
@register_search
|
||||||
class VRFIndex(SearchIndex):
|
class VRFIndex(SearchIndex):
|
||||||
model = models.VRF
|
model = models.VRF
|
||||||
|
@ -16,6 +16,8 @@ __all__ = (
|
|||||||
'VLANMembersTable',
|
'VLANMembersTable',
|
||||||
'VLANTable',
|
'VLANTable',
|
||||||
'VLANVirtualMachinesTable',
|
'VLANVirtualMachinesTable',
|
||||||
|
'VLANTranslationPolicyTable',
|
||||||
|
'VLANTranslationRuleTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span>')
|
AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span>')
|
||||||
@ -244,3 +246,54 @@ class InterfaceVLANTable(NetBoxTable):
|
|||||||
def __init__(self, interface, *args, **kwargs):
|
def __init__(self, interface, *args, **kwargs):
|
||||||
self.interface = interface
|
self.interface = interface
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VLAN Translation
|
||||||
|
#
|
||||||
|
|
||||||
|
class VLANTranslationPolicyTable(NetBoxTable):
|
||||||
|
name = tables.Column(
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
description = tables.Column(
|
||||||
|
verbose_name=_('Description'),
|
||||||
|
)
|
||||||
|
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'),
|
||||||
|
)
|
||||||
|
description = tables.Column(
|
||||||
|
verbose_name=_('Description'),
|
||||||
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
|
url_name='ipam:vlantranslationrule_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = VLANTranslationRule
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'policy', 'local_vid', 'remote_vid', 'description', 'tags', 'created', 'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'policy', 'local_vid', 'remote_vid', 'description')
|
||||||
|
@ -1020,6 +1020,112 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
|||||||
self.assertTrue(content['detail'].startswith('Unable to delete object.'))
|
self.assertTrue(content['detail'].startswith('Unable to delete object.'))
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicyTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = VLANTranslationPolicy
|
||||||
|
brief_fields = ['description', 'display', 'id', 'name', 'url',]
|
||||||
|
bulk_update_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
vlan_translation_policies = (
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 1',
|
||||||
|
description='foobar1',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 2',
|
||||||
|
description='foobar2',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 3',
|
||||||
|
description='foobar3',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||||
|
|
||||||
|
cls.create_data = [
|
||||||
|
{
|
||||||
|
'name': 'Policy 4',
|
||||||
|
'description': 'foobar4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Policy 5',
|
||||||
|
'description': 'foobar5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Policy 6',
|
||||||
|
'description': 'foobar6',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRuleTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = VLANTranslationRule
|
||||||
|
brief_fields = ['id', 'local_vid', 'policy', 'remote_vid',]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
vlan_translation_policies = (
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 1',
|
||||||
|
description='foobar1',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 2',
|
||||||
|
description='foobar2',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||||
|
|
||||||
|
vlan_translation_rules = (
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[0],
|
||||||
|
local_vid=100,
|
||||||
|
remote_vid=200,
|
||||||
|
description='foo',
|
||||||
|
),
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[0],
|
||||||
|
local_vid=101,
|
||||||
|
remote_vid=201,
|
||||||
|
description='bar',
|
||||||
|
),
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[1],
|
||||||
|
local_vid=102,
|
||||||
|
remote_vid=202,
|
||||||
|
description='baz',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationRule.objects.bulk_create(vlan_translation_rules)
|
||||||
|
|
||||||
|
cls.create_data = [
|
||||||
|
{
|
||||||
|
'policy': vlan_translation_policies[0].pk,
|
||||||
|
'local_vid': 300,
|
||||||
|
'remote_vid': 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'policy': vlan_translation_policies[0].pk,
|
||||||
|
'local_vid': 301,
|
||||||
|
'remote_vid': 401,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'policy': vlan_translation_policies[1].pk,
|
||||||
|
'local_vid': 302,
|
||||||
|
'remote_vid': 402,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
cls.bulk_update_data = {
|
||||||
|
'policy': vlan_translation_policies[1].pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ServiceTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ServiceTemplate
|
model = ServiceTemplate
|
||||||
brief_fields = ['description', 'display', 'id', 'name', 'ports', 'protocol', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'ports', 'protocol', 'url']
|
||||||
|
@ -1898,6 +1898,99 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicyTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = VLANTranslationPolicy.objects.all()
|
||||||
|
filterset = VLANTranslationPolicyFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
vlan_translation_policies = (
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 1',
|
||||||
|
description='foobar1',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 2',
|
||||||
|
description='foobar2',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 3',
|
||||||
|
description='foobar3',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
params = {'name': ['Policy 1', 'Policy 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = VLANTranslationRule.objects.all()
|
||||||
|
filterset = VLANTranslationRuleFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
vlan_translation_policies = (
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 1',
|
||||||
|
description='foobar1',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 2',
|
||||||
|
description='foobar2',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||||
|
|
||||||
|
vlan_translation_rules = (
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[0],
|
||||||
|
local_vid=100,
|
||||||
|
remote_vid=200,
|
||||||
|
description='foo',
|
||||||
|
),
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[0],
|
||||||
|
local_vid=101,
|
||||||
|
remote_vid=201,
|
||||||
|
description='bar',
|
||||||
|
),
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[1],
|
||||||
|
local_vid=100,
|
||||||
|
remote_vid=200,
|
||||||
|
description='baz',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationRule.objects.bulk_create(vlan_translation_rules)
|
||||||
|
|
||||||
|
def test_policy_id(self):
|
||||||
|
policies = VLANTranslationPolicy.objects.all()[:2]
|
||||||
|
params = {'policy_id': [policies[0].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'policy': [policies[0].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_local_vid(self):
|
||||||
|
params = {'local_vid': [100]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_remote_vid(self):
|
||||||
|
params = {'remote_vid': [200]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['foo', 'bar']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = ServiceTemplate.objects.all()
|
queryset = ServiceTemplate.objects.all()
|
||||||
filterset = ServiceTemplateFilterSet
|
filterset = ServiceTemplateFilterSet
|
||||||
|
@ -863,6 +863,121 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationPolicyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = VLANTranslationPolicy
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
vlan_translation_policies = (
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 1',
|
||||||
|
description='foobar1',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 2',
|
||||||
|
description='foobar2',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 3',
|
||||||
|
description='foobar3',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'Policy999',
|
||||||
|
'description': 'A new VLAN Translation Policy',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"name,description",
|
||||||
|
"Policy101,foobar1",
|
||||||
|
"Policy102,foobar2",
|
||||||
|
"Policy103,foobar3",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.csv_update_data = (
|
||||||
|
"id,name,description",
|
||||||
|
f"{vlan_translation_policies[0].pk},Policy101,New description 1",
|
||||||
|
f"{vlan_translation_policies[1].pk},Policy102,New description 2",
|
||||||
|
f"{vlan_translation_policies[2].pk},Policy103,New description 3",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VLANTranslationRuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = VLANTranslationRule
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
vlan_translation_policies = (
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 1',
|
||||||
|
description='foobar1',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 2',
|
||||||
|
description='foobar2',
|
||||||
|
),
|
||||||
|
VLANTranslationPolicy(
|
||||||
|
name='Policy 3',
|
||||||
|
description='foobar3',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||||
|
|
||||||
|
vlan_translation_rules = (
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[0],
|
||||||
|
local_vid=100,
|
||||||
|
remote_vid=200,
|
||||||
|
),
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[0],
|
||||||
|
local_vid=101,
|
||||||
|
remote_vid=201,
|
||||||
|
),
|
||||||
|
VLANTranslationRule(
|
||||||
|
policy=vlan_translation_policies[1],
|
||||||
|
local_vid=102,
|
||||||
|
remote_vid=202,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VLANTranslationRule.objects.bulk_create(vlan_translation_rules)
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'policy': vlan_translation_policies[0].pk,
|
||||||
|
'local_vid': 300,
|
||||||
|
'remote_vid': 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"policy,local_vid,remote_vid",
|
||||||
|
f"{vlan_translation_policies[0].pk},103,203",
|
||||||
|
f"{vlan_translation_policies[0].pk},104,204",
|
||||||
|
f"{vlan_translation_policies[1].pk},105,205",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.csv_update_data = (
|
||||||
|
"id,policy,local_vid,remote_vid",
|
||||||
|
f"{vlan_translation_rules[0].pk},{vlan_translation_policies[1].pk},105,205",
|
||||||
|
f"{vlan_translation_rules[1].pk},{vlan_translation_policies[1].pk},106,206",
|
||||||
|
f"{vlan_translation_rules[2].pk},{vlan_translation_policies[0].pk},107,207",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'policy': vlan_translation_policies[2].pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class ServiceTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = ServiceTemplate
|
model = ServiceTemplate
|
||||||
|
|
||||||
|
@ -116,6 +116,22 @@ urlpatterns = [
|
|||||||
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||||
path('vlans/<int:pk>/', include(get_model_urls('ipam', 'vlan'))),
|
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
|
# Service templates
|
||||||
path('service-templates/', views.ServiceTemplateListView.as_view(), name='servicetemplate_list'),
|
path('service-templates/', views.ServiceTemplateListView.as_view(), name='servicetemplate_list'),
|
||||||
path('service-templates/add/', views.ServiceTemplateEditView.as_view(), name='servicetemplate_add'),
|
path('service-templates/add/', views.ServiceTemplateEditView.as_view(), name='servicetemplate_add'),
|
||||||
|
@ -9,6 +9,7 @@ from circuits.models import Provider
|
|||||||
from dcim.filtersets import InterfaceFilterSet
|
from dcim.filtersets import InterfaceFilterSet
|
||||||
from dcim.forms import InterfaceFilterForm
|
from dcim.forms import InterfaceFilterForm
|
||||||
from dcim.models import Interface, Site
|
from dcim.models import Interface, Site
|
||||||
|
from ipam.tables import VLANTranslationRuleTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from tenancy.views import ObjectContactsView
|
from tenancy.views import ObjectContactsView
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
@ -986,6 +987,110 @@ class VLANGroupVLANsView(generic.ObjectChildrenView):
|
|||||||
return queryset
|
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):
|
||||||
|
vlan_translation_table = VLANTranslationRuleTable(
|
||||||
|
data=instance.rules.all(),
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'vlan_translation_table': vlan_translation_table,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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 Rules
|
||||||
|
#
|
||||||
|
|
||||||
|
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
|
# FHRP groups
|
||||||
#
|
#
|
||||||
|
@ -194,6 +194,8 @@ IPAM_MENU = Menu(
|
|||||||
items=(
|
items=(
|
||||||
get_model_item('ipam', 'vlan', _('VLANs')),
|
get_model_item('ipam', 'vlan', _('VLANs')),
|
||||||
get_model_item('ipam', 'vlangroup', _('VLAN Groups')),
|
get_model_item('ipam', 'vlangroup', _('VLAN Groups')),
|
||||||
|
get_model_item('ipam', 'vlantranslationpolicy', _('VLAN Translation Policies')),
|
||||||
|
get_model_item('ipam', 'vlantranslationrule', _('VLAN Translation Rules')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
|
@ -133,6 +133,10 @@
|
|||||||
<th scope="row">{% trans "VRF" %}</th>
|
<th scope="row">{% trans "VRF" %}</th>
|
||||||
<td>{{ object.vrf|linkify|placeholder }}</td>
|
<td>{{ object.vrf|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "VLAN Translation" %}</th>
|
||||||
|
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if not object.is_virtual %}
|
{% if not object.is_virtual %}
|
||||||
@ -355,6 +359,13 @@
|
|||||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if object.vlan_translation_policy %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
{% if object.is_bridge %}
|
{% if object.is_bridge %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
|
55
netbox/templates/ipam/vlantranslationpolicy.html
Normal file
55
netbox/templates/ipam/vlantranslationpolicy.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% 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 "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 mb-3">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">
|
||||||
|
{% trans "VLAN Translation Rules" %}
|
||||||
|
{% if perms.ipam.add_vlantranslationrule %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'ipam:vlantranslationrule_add' %}?device={{ object.device.pk }}&policy={{ object.pk }}&return_url={{ object.get_absolute_url }}"
|
||||||
|
class="btn btn-ghost-primary btn-sm">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Rule" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% htmx_table 'ipam:vlantranslationrule_list' policy_id=object.pk %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
45
netbox/templates/ipam/vlantranslationrule.html
Normal file
45
netbox/templates/ipam/vlantranslationrule.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% 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|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Local VID" %}</th>
|
||||||
|
<td>{{ object.local_vid }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Remote VID" %}</th>
|
||||||
|
<td>{{ object.remote_vid }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
|
<td>{{ object.description }}</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 %}
|
@ -67,6 +67,10 @@
|
|||||||
<th scope="row">{% trans "Tunnel" %}</th>
|
<th scope="row">{% trans "Tunnel" %}</th>
|
||||||
<td>{{ object.tunnel_termination.tunnel|linkify|placeholder }}</td>
|
<td>{{ object.tunnel_termination.tunnel|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "VLAN Translation" %}</th>
|
||||||
|
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
@ -100,6 +104,13 @@
|
|||||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if object.vlan_translation_policy %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
|
{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
|
||||||
|
@ -8,7 +8,7 @@ from dcim.api.serializers_.sites import SiteSerializer
|
|||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||||
from ipam.api.serializers_.ip import IPAddressSerializer
|
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.api.serializers_.vrfs import VRFSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||||
@ -89,6 +89,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
vlan_translation_policy = VLANTranslationPolicySerializer(nested=True, required=False, allow_null=True)
|
||||||
vrf = VRFSerializer(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)
|
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
||||||
count_ipaddresses = serializers.IntegerField(read_only=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',
|
'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu',
|
||||||
'mac_address', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination',
|
'mac_address', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination',
|
||||||
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
|
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
|
||||||
|
'vlan_translation_policy',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from dcim.forms.common import InterfaceCommonForm
|
from dcim.forms.common import InterfaceCommonForm
|
||||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||||
from extras.models import ConfigTemplate
|
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 netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
@ -343,20 +343,25 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('VRF')
|
label=_('VRF')
|
||||||
)
|
)
|
||||||
|
vlan_translation_policy = DynamicModelChoiceField(
|
||||||
|
queryset=VLANTranslationPolicy.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('VLAN Translation Policy')
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
|
FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
|
||||||
FieldSet('vrf', 'mac_address', name=_('Addressing')),
|
FieldSet('vrf', 'mac_address', name=_('Addressing')),
|
||||||
FieldSet('mtu', 'enabled', name=_('Operation')),
|
FieldSet('mtu', 'enabled', name=_('Operation')),
|
||||||
FieldSet('parent', 'bridge', name=_('Related Interfaces')),
|
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:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = [
|
fields = [
|
||||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
'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 = {
|
labels = {
|
||||||
'mode': '802.1Q Mode',
|
'mode': '802.1Q Mode',
|
||||||
|
@ -100,6 +100,7 @@ class VMInterfaceType(IPAddressesMixin, ComponentType):
|
|||||||
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
vrf: Annotated["VRFType", 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')]]
|
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||||
bridge_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
bridge_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
||||||
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,7 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
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 tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
@ -561,6 +561,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
VirtualMachine.objects.bulk_create(vms)
|
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 = (
|
interfaces = (
|
||||||
VMInterface(
|
VMInterface(
|
||||||
virtual_machine=vms[0],
|
virtual_machine=vms[0],
|
||||||
@ -569,7 +576,8 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
mtu=100,
|
mtu=100,
|
||||||
mac_address='00-00-00-00-00-01',
|
mac_address='00-00-00-00-00-01',
|
||||||
vrf=vrfs[0],
|
vrf=vrfs[0],
|
||||||
description='foobar1'
|
description='foobar1',
|
||||||
|
vlan_translation_policy=vlan_translation_policies[0],
|
||||||
),
|
),
|
||||||
VMInterface(
|
VMInterface(
|
||||||
virtual_machine=vms[1],
|
virtual_machine=vms[1],
|
||||||
@ -578,7 +586,8 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
mtu=200,
|
mtu=200,
|
||||||
mac_address='00-00-00-00-00-02',
|
mac_address='00-00-00-00-00-02',
|
||||||
vrf=vrfs[1],
|
vrf=vrfs[1],
|
||||||
description='foobar2'
|
description='foobar2',
|
||||||
|
vlan_translation_policy=vlan_translation_policies[0],
|
||||||
),
|
),
|
||||||
VMInterface(
|
VMInterface(
|
||||||
virtual_machine=vms[2],
|
virtual_machine=vms[2],
|
||||||
@ -658,6 +667,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'description': ['foobar1', 'foobar2']}
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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):
|
class VirtualDiskTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = VirtualDisk.objects.all()
|
queryset = VirtualDisk.objects.all()
|
||||||
|
@ -16,7 +16,7 @@ from dcim.models import Device
|
|||||||
from dcim.tables import DeviceTable
|
from dcim.tables import DeviceTable
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView
|
||||||
from ipam.models import IPAddress
|
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.constants import DEFAULT_ACTION_PERMISSIONS
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from tenancy.views import ObjectContactsView
|
from tenancy.views import ObjectContactsView
|
||||||
@ -516,6 +516,14 @@ class VMInterfaceView(generic.ObjectView):
|
|||||||
orderable=False
|
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
|
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||||
vlans = []
|
vlans = []
|
||||||
if instance.untagged_vlan is not None:
|
if instance.untagged_vlan is not None:
|
||||||
@ -533,6 +541,7 @@ class VMInterfaceView(generic.ObjectView):
|
|||||||
return {
|
return {
|
||||||
'child_interfaces_table': child_interfaces_tables,
|
'child_interfaces_table': child_interfaces_tables,
|
||||||
'vlan_table': vlan_table,
|
'vlan_table': vlan_table,
|
||||||
|
'vlan_translation_table': vlan_translation_table,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user