mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-31 04:46:26 -06:00
Initial work on #13428 (QinQ)
This commit is contained in:
parent
572aad0e20
commit
f15c26eac6
@ -120,6 +120,10 @@ The "native" (untagged) VLAN for the interface. Valid only when one of the above
|
|||||||
|
|
||||||
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
|
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
|
||||||
|
|
||||||
|
### Q-in-Q SVLAN
|
||||||
|
|
||||||
|
The assigned service VLAN (for Q-in-Q/802.1ad interfaces).
|
||||||
|
|
||||||
### Wireless Role
|
### Wireless Role
|
||||||
|
|
||||||
Indicates the configured role for wireless interfaces (access point or station).
|
Indicates the configured role for wireless interfaces (access point or station).
|
||||||
|
@ -26,3 +26,11 @@ The user-defined functional [role](./role.md) assigned to the VLAN.
|
|||||||
### VLAN Group or Site
|
### VLAN Group or Site
|
||||||
|
|
||||||
The [VLAN group](./vlangroup.md) or [site](../dcim/site.md) to which the VLAN is assigned.
|
The [VLAN group](./vlangroup.md) or [site](../dcim/site.md) to which the VLAN is assigned.
|
||||||
|
|
||||||
|
### Q-in-Q Role
|
||||||
|
|
||||||
|
For VLANs which comprise a Q-in-Q/IEEE 802.1ad topology, this field indicates whether the VLAN is treated as a service or customer VLAN.
|
||||||
|
|
||||||
|
### Q-in-Q Service VLAN
|
||||||
|
|
||||||
|
The designated parent service VLAN for a Q-in-Q customer VLAN.
|
||||||
|
@ -53,6 +53,10 @@ The "native" (untagged) VLAN for the interface. Valid only when one of the above
|
|||||||
|
|
||||||
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
|
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
|
||||||
|
|
||||||
|
### Q-in-Q SVLAN
|
||||||
|
|
||||||
|
The assigned service VLAN (for Q-in-Q/802.1ad interfaces).
|
||||||
|
|
||||||
### 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.
|
||||||
|
@ -196,6 +196,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
required=False,
|
required=False,
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
qinq_svlan = VLANSerializer(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)
|
||||||
@ -222,10 +223,10 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
||||||
'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description',
|
'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description',
|
||||||
'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width',
|
'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width',
|
||||||
'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link',
|
'tx_power', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'mark_connected', 'cable', 'cable_end',
|
||||||
'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination',
|
||||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
@ -1258,11 +1258,13 @@ class InterfaceModeChoices(ChoiceSet):
|
|||||||
MODE_ACCESS = 'access'
|
MODE_ACCESS = 'access'
|
||||||
MODE_TAGGED = 'tagged'
|
MODE_TAGGED = 'tagged'
|
||||||
MODE_TAGGED_ALL = 'tagged-all'
|
MODE_TAGGED_ALL = 'tagged-all'
|
||||||
|
MODE_Q_IN_Q = 'q-in-q'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(MODE_ACCESS, _('Access')),
|
(MODE_ACCESS, _('Access')),
|
||||||
(MODE_TAGGED, _('Tagged')),
|
(MODE_TAGGED, _('Tagged')),
|
||||||
(MODE_TAGGED_ALL, _('Tagged (All)')),
|
(MODE_TAGGED_ALL, _('Tagged (All)')),
|
||||||
|
(MODE_Q_IN_Q, _('Q-in-Q (802.1ad)')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1636,7 +1636,8 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(untagged_vlan_id=value) |
|
Q(untagged_vlan_id=value) |
|
||||||
Q(tagged_vlans=value)
|
Q(tagged_vlans=value) |
|
||||||
|
Q(qinq_svlan=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_vlan(self, queryset, name, value):
|
def filter_vlan(self, queryset, name, value):
|
||||||
@ -1645,7 +1646,8 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(untagged_vlan_id__vid=value) |
|
Q(untagged_vlan_id__vid=value) |
|
||||||
Q(tagged_vlans__vid=value)
|
Q(tagged_vlans__vid=value) |
|
||||||
|
Q(qinq_svlan__vid=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +37,8 @@ class InterfaceCommonForm(forms.Form):
|
|||||||
del self.fields['vlan_group']
|
del self.fields['vlan_group']
|
||||||
del self.fields['untagged_vlan']
|
del self.fields['untagged_vlan']
|
||||||
del self.fields['tagged_vlans']
|
del self.fields['tagged_vlans']
|
||||||
|
if interface_mode != InterfaceModeChoices.MODE_Q_IN_Q:
|
||||||
|
del self.fields['qinq_svlan']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
@ -7,6 +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.choices import VLANQinQRoleChoices
|
||||||
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
@ -1372,6 +1373,16 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
'available_on_device': '$device',
|
'available_on_device': '$device',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
qinq_svlan = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Q-in-Q Service VLAN'),
|
||||||
|
query_params={
|
||||||
|
'group_id': '$vlan_group',
|
||||||
|
'available_on_device': '$device',
|
||||||
|
'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
|
||||||
|
}
|
||||||
|
)
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -1391,7 +1402,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', 'qinq_svlan', 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 +1415,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', 'qinq_svlan', 'vrf', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'speed': NumberWithOptions(
|
'speed': NumberWithOptions(
|
||||||
|
30
netbox/dcim/migrations/0195_qinq_svlan.py
Normal file
30
netbox/dcim/migrations/0195_qinq_svlan.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-10-21 20:26
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0194_charfield_null_choices'),
|
||||||
|
('ipam', '0074_vlan_qinq'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='qinq_svlan',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss_svlan', to='ipam.vlan'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='tagged_vlans',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)ss_as_tagged', to='ipam.vlan'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='untagged_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss_as_untagged', to='ipam.vlan'),
|
||||||
|
),
|
||||||
|
]
|
@ -547,10 +547,41 @@ class BaseInterface(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('bridge interface')
|
verbose_name=_('bridge interface')
|
||||||
)
|
)
|
||||||
|
untagged_vlan = models.ForeignKey(
|
||||||
|
to='ipam.VLAN',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='%(class)ss_as_untagged',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('untagged VLAN')
|
||||||
|
)
|
||||||
|
tagged_vlans = models.ManyToManyField(
|
||||||
|
to='ipam.VLAN',
|
||||||
|
related_name='%(class)ss_as_tagged',
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('tagged VLANs')
|
||||||
|
)
|
||||||
|
qinq_svlan = models.ForeignKey(
|
||||||
|
to='ipam.VLAN',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='%(class)ss_svlan',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('Q-inQ SVLAN')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Virtual Interfaces cannot have a Cable attached
|
||||||
|
if self.qinq_svlan and self.mode != InterfaceModeChoices.MODE_Q_IN_Q:
|
||||||
|
raise ValidationError({
|
||||||
|
'qinq_svlan': _("Only Q-in-Q interfaces may specify a service VLAN.")
|
||||||
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
# Remove untagged VLAN assignment for non-802.1Q interfaces
|
# Remove untagged VLAN assignment for non-802.1Q interfaces
|
||||||
@ -690,20 +721,6 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('wireless LANs')
|
verbose_name=_('wireless LANs')
|
||||||
)
|
)
|
||||||
untagged_vlan = models.ForeignKey(
|
|
||||||
to='ipam.VLAN',
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
related_name='interfaces_as_untagged',
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_('untagged VLAN')
|
|
||||||
)
|
|
||||||
tagged_vlans = models.ManyToManyField(
|
|
||||||
to='ipam.VLAN',
|
|
||||||
related_name='interfaces_as_tagged',
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_('tagged VLANs')
|
|
||||||
)
|
|
||||||
vrf = models.ForeignKey(
|
vrf = models.ForeignKey(
|
||||||
to='ipam.VRF',
|
to='ipam.VRF',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
@ -585,6 +585,10 @@ class BaseInterfaceTable(NetBoxTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name=_('Tagged VLANs')
|
verbose_name=_('Tagged VLANs')
|
||||||
)
|
)
|
||||||
|
qinq_svlan = tables.Column(
|
||||||
|
verbose_name=_('Q-in-Q SVLAN'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
|
||||||
def value_ip_addresses(self, value):
|
def value_ip_addresses(self, value):
|
||||||
return ",".join([str(obj.address) for obj in value.all()])
|
return ",".join([str(obj.address) for obj in value.all()])
|
||||||
@ -635,11 +639,11 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
|||||||
model = models.Interface
|
model = models.Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||||
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role',
|
||||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
|
||||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
|
'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf',
|
||||||
'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created',
|
'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||||
'last_updated',
|
'inventory_items', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
@ -676,7 +680,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
||||||
'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses',
|
'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses',
|
||||||
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
|
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||||
|
@ -6,6 +6,7 @@ from ..field_serializers import IPAddressField
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'NestedIPAddressSerializer',
|
'NestedIPAddressSerializer',
|
||||||
|
'NestedVLANSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -16,3 +17,10 @@ class NestedIPAddressSerializer(WritableNestedSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = models.IPAddress
|
model = models.IPAddress
|
||||||
fields = ['id', 'url', 'display_url', 'display', 'family', 'address']
|
fields = ['id', 'url', 'display_url', 'display', 'family', 'address']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedVLANSerializer(WritableNestedSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.VLAN
|
||||||
|
fields = ['id', 'url', 'display', 'vid', 'name', 'description']
|
||||||
|
@ -11,6 +11,7 @@ from netbox.api.serializers import NetBoxModelSerializer
|
|||||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
||||||
|
from .nested import NestedVLANSerializer
|
||||||
from .roles import RoleSerializer
|
from .roles import RoleSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -62,6 +63,8 @@ class VLANSerializer(NetBoxModelSerializer):
|
|||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
||||||
role = RoleSerializer(nested=True, required=False, allow_null=True)
|
role = RoleSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
qinq_role = ChoiceField(choices=VLANQinQRoleChoices, required=False)
|
||||||
|
qinq_svlan = NestedVLANSerializer(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)
|
||||||
|
|
||||||
# Related object counts
|
# Related object counts
|
||||||
@ -71,8 +74,8 @@ class VLANSerializer(NetBoxModelSerializer):
|
|||||||
model = VLAN
|
model = VLAN
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role',
|
'id', 'url', 'display_url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role',
|
||||||
'description', 'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated',
|
'description', 'qinq_role', 'qinq_svlan', 'comments', 'l2vpn_termination', 'tags', 'custom_fields',
|
||||||
'prefix_count',
|
'created', 'last_updated', 'prefix_count',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description')
|
||||||
|
|
||||||
|
@ -157,6 +157,17 @@ class VLANStatusChoices(ChoiceSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VLANQinQRoleChoices(ChoiceSet):
|
||||||
|
|
||||||
|
ROLE_SERVICE = 's-vlan'
|
||||||
|
ROLE_CUSTOMER = 'c-vlan'
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(ROLE_SERVICE, _('Service'), 'blue'),
|
||||||
|
(ROLE_CUSTOMER, _('Customer'), 'orange'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
@ -1039,6 +1039,18 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
queryset=VirtualMachine.objects.all(),
|
queryset=VirtualMachine.objects.all(),
|
||||||
method='get_for_virtualmachine'
|
method='get_for_virtualmachine'
|
||||||
)
|
)
|
||||||
|
qinq_role = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=VLANQinQRoleChoices,
|
||||||
|
null_value=None
|
||||||
|
)
|
||||||
|
qinq_svlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
label=_('Q-in-Q SVLAN (ID)'),
|
||||||
|
)
|
||||||
|
qinq_svlan_vid = django_filters.NumberFilter(
|
||||||
|
field_name='qinq_svlan__vid',
|
||||||
|
label=_('Q-in-Q SVLAN number (1-4094)'),
|
||||||
|
)
|
||||||
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='l2vpn_terminations__l2vpn',
|
field_name='l2vpn_terminations__l2vpn',
|
||||||
queryset=L2VPN.objects.all(),
|
queryset=L2VPN.objects.all(),
|
||||||
|
@ -525,15 +525,21 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
qinq_role = forms.ChoiceField(
|
||||||
|
label=_('Status'),
|
||||||
|
choices=add_blank_choice(VLANQinQRoleChoices),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('status', 'role', 'tenant', 'description'),
|
FieldSet('status', 'role', 'tenant', 'description'),
|
||||||
|
FieldSet('qinq_role', 'qinq_svlan', name=_('Q-in-Q')),
|
||||||
FieldSet('region', 'site_group', 'site', 'group', name=_('Site & Group')),
|
FieldSet('region', 'site_group', 'site', 'group', name=_('Site & Group')),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'site', 'group', 'tenant', 'role', 'description', 'comments',
|
'site', 'group', 'tenant', 'role', 'description', 'qinq_role', 'qinq_svlan', 'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -440,6 +440,11 @@ class VLANImportForm(NetBoxModelImportForm):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Assigned VLAN group')
|
help_text=_('Assigned VLAN group')
|
||||||
)
|
)
|
||||||
|
qinq_role = CSVChoiceField(
|
||||||
|
label=_('Q-in-Q role'),
|
||||||
|
choices=VLANStatusChoices,
|
||||||
|
help_text=_('Operational status')
|
||||||
|
)
|
||||||
tenant = CSVModelChoiceField(
|
tenant = CSVModelChoiceField(
|
||||||
label=_('Tenant'),
|
label=_('Tenant'),
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
@ -459,10 +464,20 @@ class VLANImportForm(NetBoxModelImportForm):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Functional role')
|
help_text=_('Functional role')
|
||||||
)
|
)
|
||||||
|
qinq_svlan = CSVModelChoiceField(
|
||||||
|
label=_('Q-in-Q SVLAN'),
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='vid',
|
||||||
|
help_text=_("Service VLAN (for Q-in-Q/802.1ad customer VLANs)")
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments', 'tags')
|
fields = (
|
||||||
|
'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'qinq_role', 'qinq_svlan',
|
||||||
|
'comments', 'tags',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateImportForm(NetBoxModelImportForm):
|
class ServiceTemplateImportForm(NetBoxModelImportForm):
|
||||||
|
@ -467,6 +467,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||||
FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
|
FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
|
||||||
|
FieldSet('qinq_role', 'qinq_svlan', name=_('Q-in-Q/802.1ad')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
)
|
)
|
||||||
selector_fields = ('filter_id', 'q', 'site_id')
|
selector_fields = ('filter_id', 'q', 'site_id')
|
||||||
@ -513,6 +514,17 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('VLAN ID')
|
label=_('VLAN ID')
|
||||||
)
|
)
|
||||||
|
qinq_role = forms.MultipleChoiceField(
|
||||||
|
label=_('Q-in-Q role'),
|
||||||
|
choices=VLANQinQRoleChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
qinq_svlan_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
null_option='None',
|
||||||
|
label=_('Q-in-Q SVLAN')
|
||||||
|
)
|
||||||
l2vpn_id = DynamicModelMultipleChoiceField(
|
l2vpn_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=L2VPN.objects.all(),
|
queryset=L2VPN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -681,13 +681,21 @@ class VLANForm(TenancyForm, NetBoxModelForm):
|
|||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
qinq_svlan = DynamicModelChoiceField(
|
||||||
|
label=_('Q-in-Q SVLAN'),
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
|
||||||
|
}
|
||||||
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = [
|
fields = [
|
||||||
'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'description', 'comments',
|
'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'qinq_role', 'qinq_svlan',
|
||||||
'tags',
|
'description', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
30
netbox/ipam/migrations/0074_vlan_qinq.py
Normal file
30
netbox/ipam/migrations/0074_vlan_qinq.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0073_charfield_null_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='qinq_role',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='qinq_svlan',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='qinq_cvlans', to='ipam.vlan'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='vlan',
|
||||||
|
constraint=models.UniqueConstraint(fields=('qinq_svlan', 'vid'), name='ipam_vlan_unique_qinq_svlan_vid'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='vlan',
|
||||||
|
constraint=models.UniqueConstraint(fields=('qinq_svlan', 'name'), name='ipam_vlan_unique_qinq_svlan_name'),
|
||||||
|
),
|
||||||
|
]
|
@ -202,6 +202,21 @@ class VLAN(PrimaryModel):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text=_("The primary function of this VLAN")
|
help_text=_("The primary function of this VLAN")
|
||||||
)
|
)
|
||||||
|
qinq_svlan = models.ForeignKey(
|
||||||
|
to='self',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='qinq_cvlans',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
qinq_role = models.CharField(
|
||||||
|
verbose_name=_('Q-in-Q role'),
|
||||||
|
max_length=50,
|
||||||
|
choices=VLANQinQRoleChoices,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text=_("Customer/service VLAN designation (for Q-in-Q/IEEE 802.1ad)")
|
||||||
|
)
|
||||||
l2vpn_terminations = GenericRelation(
|
l2vpn_terminations = GenericRelation(
|
||||||
to='vpn.L2VPNTermination',
|
to='vpn.L2VPNTermination',
|
||||||
content_type_field='assigned_object_type',
|
content_type_field='assigned_object_type',
|
||||||
@ -212,7 +227,7 @@ class VLAN(PrimaryModel):
|
|||||||
objects = VLANQuerySet.as_manager()
|
objects = VLANQuerySet.as_manager()
|
||||||
|
|
||||||
clone_fields = [
|
clone_fields = [
|
||||||
'site', 'group', 'tenant', 'status', 'role', 'description',
|
'site', 'group', 'tenant', 'status', 'role', 'description', 'qinq_role', 'qinq_svlan',
|
||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -226,6 +241,14 @@ class VLAN(PrimaryModel):
|
|||||||
fields=('group', 'name'),
|
fields=('group', 'name'),
|
||||||
name='%(app_label)s_%(class)s_unique_group_name'
|
name='%(app_label)s_%(class)s_unique_group_name'
|
||||||
),
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('qinq_svlan', 'vid'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_qinq_svlan_vid'
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('qinq_svlan', 'name'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_qinq_svlan_name'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
verbose_name = _('VLAN')
|
verbose_name = _('VLAN')
|
||||||
verbose_name_plural = _('VLANs')
|
verbose_name_plural = _('VLANs')
|
||||||
@ -253,9 +276,18 @@ class VLAN(PrimaryModel):
|
|||||||
).format(ranges=ranges_to_string(self.group.vid_ranges), group=self.group)
|
).format(ranges=ranges_to_string(self.group.vid_ranges), group=self.group)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Only Q-in-Q customer VLANs may be assigned to a service VLAN
|
||||||
|
if self.qinq_svlan and self.qinq_role != VLANQinQRoleChoices.ROLE_CUSTOMER:
|
||||||
|
raise ValidationError({
|
||||||
|
'qinq_svlan': _("Only Q-in-Q customer VLANs maybe assigned to a service VLAN.")
|
||||||
|
})
|
||||||
|
|
||||||
def get_status_color(self):
|
def get_status_color(self):
|
||||||
return VLANStatusChoices.colors.get(self.status)
|
return VLANStatusChoices.colors.get(self.status)
|
||||||
|
|
||||||
|
def get_qinq_role_color(self):
|
||||||
|
return VLANQinQRoleChoices.colors.get(self.qinq_role)
|
||||||
|
|
||||||
def get_interfaces(self):
|
def get_interfaces(self):
|
||||||
# Return all device interfaces assigned to this VLAN
|
# Return all device interfaces assigned to this VLAN
|
||||||
return Interface.objects.filter(
|
return Interface.objects.filter(
|
||||||
|
@ -130,6 +130,13 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
verbose_name=_('Role'),
|
verbose_name=_('Role'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
qinq_role = columns.ChoiceFieldColumn(
|
||||||
|
verbose_name=_('Q-in-Q role')
|
||||||
|
)
|
||||||
|
qinq_svlan = tables.Column(
|
||||||
|
verbose_name=_('Q-in-Q SVLAN'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
l2vpn = tables.Column(
|
l2vpn = tables.Column(
|
||||||
accessor=tables.A('l2vpn_termination__l2vpn'),
|
accessor=tables.A('l2vpn_termination__l2vpn'),
|
||||||
linkify=True,
|
linkify=True,
|
||||||
@ -152,7 +159,7 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
model = VLAN
|
model = VLAN
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role',
|
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role',
|
||||||
'description', 'comments', 'tags', 'l2vpn', 'created', 'last_updated',
|
'qinq_role', 'qinq_svlan', 'description', 'comments', 'tags', 'l2vpn', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
|
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
|
@ -62,6 +62,16 @@
|
|||||||
<th scope="row">{% trans "Description" %}</th>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
<td>{{ object.description|placeholder }}</td>
|
<td>{{ object.description|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Q-in-Q Role" %}</th>
|
||||||
|
<td>{% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %}</td>
|
||||||
|
</tr>
|
||||||
|
{% if object.qinq_role == 'c-vlan' %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Q-in-Q SVLAN" %}</th>
|
||||||
|
<td>{{ object.qinq_svlan|linkify|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "L2VPN" %}</th>
|
<th scope="row">{% trans "L2VPN" %}</th>
|
||||||
<td>{{ object.l2vpn_termination.l2vpn|linkify|placeholder }}</td>
|
<td>{{ object.l2vpn_termination.l2vpn|linkify|placeholder }}</td>
|
||||||
@ -92,6 +102,21 @@
|
|||||||
</h2>
|
</h2>
|
||||||
{% htmx_table 'ipam:prefix_list' vlan_id=object.pk %}
|
{% htmx_table 'ipam:prefix_list' vlan_id=object.pk %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if object.qinq_role == 's-vlan' %}
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">
|
||||||
|
{% trans "Customer VLANs" %}
|
||||||
|
{% if perms.ipam.add_vlan %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'ipam:vlan_add' %}?qinq_role=c-vlan&qinq_svlan={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a VLAN" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% htmx_table 'ipam:vlan_list' qinq_svlan_id=object.pk %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,14 @@
|
|||||||
{% render_field form.tags %}
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="col-9 offset-3">{% trans "Q-in-Q (802.1ad)" %}</h2>
|
||||||
|
</div>
|
||||||
|
{% render_field form.qinq_role %}
|
||||||
|
{% render_field form.qinq_svlan %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2 class="col-9 offset-3">{% trans "Tenancy" %}</h2>
|
<h2 class="col-9 offset-3">{% trans "Tenancy" %}</h2>
|
||||||
|
@ -89,6 +89,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
qinq_svlan = VLANSerializer(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)
|
||||||
@ -103,8 +104,9 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
|||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = [
|
fields = [
|
||||||
'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', 'qinq_svlan', 'vrf',
|
||||||
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
|
'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
||||||
|
'count_fhrp_groups',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
|
||||||
|
|
||||||
|
@ -6,6 +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.choices import VLANQinQRoleChoices
|
||||||
from ipam.models import IPAddress, VLAN, VLANGroup, VRF
|
from ipam.models import IPAddress, VLAN, VLANGroup, VRF
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
@ -338,6 +339,16 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
|||||||
'available_on_virtualmachine': '$virtual_machine',
|
'available_on_virtualmachine': '$virtual_machine',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
qinq_svlan = DynamicModelChoiceField(
|
||||||
|
queryset=VLAN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Q-in-Q Service VLAN'),
|
||||||
|
query_params={
|
||||||
|
'group_id': '$vlan_group',
|
||||||
|
'available_on_virtualmachine': '$virtual_machine',
|
||||||
|
'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
|
||||||
|
}
|
||||||
|
)
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -349,14 +360,14 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
|||||||
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', 'qinq_svlan', 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', 'qinq_svlan', 'vrf', 'tags',
|
||||||
]
|
]
|
||||||
labels = {
|
labels = {
|
||||||
'mode': '802.1Q Mode',
|
'mode': '802.1Q Mode',
|
||||||
|
30
netbox/virtualization/migrations/0042_qinq_svlan.py
Normal file
30
netbox/virtualization/migrations/0042_qinq_svlan.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-10-21 20:26
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0074_vlan_qinq'),
|
||||||
|
('virtualization', '0041_charfield_null_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vminterface',
|
||||||
|
name='qinq_svlan',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss_svlan', to='ipam.vlan'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vminterface',
|
||||||
|
name='tagged_vlans',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)ss_as_tagged', to='ipam.vlan'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vminterface',
|
||||||
|
name='untagged_vlan',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss_as_untagged', to='ipam.vlan'),
|
||||||
|
),
|
||||||
|
]
|
@ -322,20 +322,6 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
untagged_vlan = models.ForeignKey(
|
|
||||||
to='ipam.VLAN',
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
related_name='vminterfaces_as_untagged',
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_('untagged VLAN')
|
|
||||||
)
|
|
||||||
tagged_vlans = models.ManyToManyField(
|
|
||||||
to='ipam.VLAN',
|
|
||||||
related_name='vminterfaces_as_tagged',
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_('tagged VLANs')
|
|
||||||
)
|
|
||||||
ip_addresses = GenericRelation(
|
ip_addresses = GenericRelation(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
content_type_field='assigned_object_type',
|
content_type_field='assigned_object_type',
|
||||||
|
@ -151,8 +151,8 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||||
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created',
|
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||||
'last_updated',
|
'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
||||||
|
|
||||||
@ -175,7 +175,8 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
|
|||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||||
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
|
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||||
|
'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
|
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
|
Loading…
Reference in New Issue
Block a user