diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index 10ae701bb..dc01f26da 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -9,7 +9,8 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.models import ConfigTemplate
-from ipam.models import VRF, IPAddress
+from ipam.choices import VLANQinQRoleChoices
+from ipam.models import VLAN, VRF, IPAddress, VLANGroup
from netbox.choices import *
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
@@ -17,7 +18,7 @@ from utilities.forms.fields import (
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField,
SlugField,
)
-from virtualization.models import Cluster, VMInterface, VirtualMachine
+from virtualization.models import Cluster, VirtualMachine, VMInterface
from wireless.choices import WirelessRoleChoices
from .common import ModuleCommonForm
@@ -938,7 +939,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
required=False,
to_field_name='name',
help_text=mark_safe(
- _('VDC names separated by commas, encased with double quotes. Example:') + ' vdc1,vdc2,vdc3'
+ _('VDC names separated by commas, encased with double quotes. Example:') + ' "vdc1,vdc2,vdc3"'
)
)
type = CSVChoiceField(
@@ -967,7 +968,41 @@ class InterfaceImportForm(NetBoxModelImportForm):
label=_('Mode'),
choices=InterfaceModeChoices,
required=False,
- help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
+ help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)'),
+ )
+ vlan_group = CSVModelChoiceField(
+ label=_('VLAN group'),
+ queryset=VLANGroup.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Filter VLANs available for assignment by group'),
+ )
+ untagged_vlan = CSVModelChoiceField(
+ label=_('Untagged VLAN'),
+ queryset=VLAN.objects.all(),
+ required=False,
+ to_field_name='vid',
+ help_text=_('Assigned untagged VLAN ID (filtered by VLAN group)'),
+ )
+ tagged_vlans = CSVModelMultipleChoiceField(
+ label=_('Tagged VLANs'),
+ queryset=VLAN.objects.all(),
+ required=False,
+ to_field_name='vid',
+ help_text=mark_safe(
+ _(
+ 'Assigned tagged VLAN IDs separated by commas, encased with double quotes '
+ '(filtered by VLAN group). Example:'
+ )
+ + ' "100,200,300"'
+ ),
+ )
+ qinq_svlan = CSVModelChoiceField(
+ label=_('Q-in-Q Service VLAN'),
+ queryset=VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
+ required=False,
+ to_field_name='vid',
+ help_text=_('Assigned Q-in-Q Service VLAN ID (filtered by VLAN group)'),
)
vrf = CSVModelChoiceField(
label=_('VRF'),
@@ -988,7 +1023,8 @@ class InterfaceImportForm(NetBoxModelImportForm):
fields = (
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
'mark_connected', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
- 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
+ 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'rf_role', 'rf_channel',
+ 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
)
def __init__(self, data=None, *args, **kwargs):
@@ -1005,6 +1041,13 @@ class InterfaceImportForm(NetBoxModelImportForm):
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params)
+ # Limit choices for VLANs to the assigned VLAN group
+ if vlan_group := data.get('vlan_group'):
+ params = {f"group__{self.fields['vlan_group'].to_field_name}": vlan_group}
+ self.fields['untagged_vlan'].queryset = self.fields['untagged_vlan'].queryset.filter(**params)
+ self.fields['tagged_vlans'].queryset = self.fields['tagged_vlans'].queryset.filter(**params)
+ self.fields['qinq_svlan'].queryset = self.fields['qinq_svlan'].queryset.filter(**params)
+
def clean_enabled(self):
# Make sure enabled is True when it's not included in the uploaded data
if 'enabled' not in self.data:
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index e1ba63ded..8a3fb539c 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -2834,10 +2834,19 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
}
cls.csv_data = (
- "device,name,type,vrf.pk,poe_mode,poe_type",
- f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
- f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
- f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
+ "device,name,type,vrf.pk,poe_mode,poe_type,mode,untagged_vlan,tagged_vlans",
+ (
+ f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
+ f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
+ ),
+ (
+ f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
+ f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
+ ),
+ (
+ f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
+ f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
+ ),
)
cls.csv_update_data = (
diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py
index 6b5b62d11..56a6d4402 100644
--- a/netbox/virtualization/forms/bulk_import.py
+++ b/netbox/virtualization/forms/bulk_import.py
@@ -1,13 +1,18 @@
from django.utils.translation import gettext_lazy as _
+from django.utils.safestring import mark_safe
from dcim.choices import InterfaceModeChoices
from dcim.forms.mixins import ScopedImportForm
from dcim.models import Device, DeviceRole, Platform, Site
from extras.models import ConfigTemplate
-from ipam.models import VRF
+from ipam.choices import VLANQinQRoleChoices
+from ipam.models import VLAN, VRF, VLANGroup
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
-from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
+from utilities.forms.fields import (
+ CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField,
+ SlugField,
+)
from virtualization.choices import *
from virtualization.models import *
@@ -158,20 +163,54 @@ class VMInterfaceImportForm(NetBoxModelImportForm):
queryset=VMInterface.objects.all(),
required=False,
to_field_name='name',
- help_text=_('Parent interface')
+ help_text=_('Parent interface'),
)
bridge = CSVModelChoiceField(
label=_('Bridge'),
queryset=VMInterface.objects.all(),
required=False,
to_field_name='name',
- help_text=_('Bridged interface')
+ help_text=_('Bridged interface'),
)
mode = CSVChoiceField(
label=_('Mode'),
choices=InterfaceModeChoices,
required=False,
- help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
+ help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)'),
+ )
+ vlan_group = CSVModelChoiceField(
+ label=_('VLAN group'),
+ queryset=VLANGroup.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Filter VLANs available for assignment by group'),
+ )
+ untagged_vlan = CSVModelChoiceField(
+ label=_('Untagged VLAN'),
+ queryset=VLAN.objects.all(),
+ required=False,
+ to_field_name='vid',
+ help_text=_('Assigned untagged VLAN ID (filtered by VLAN group)'),
+ )
+ tagged_vlans = CSVModelMultipleChoiceField(
+ label=_('Tagged VLANs'),
+ queryset=VLAN.objects.all(),
+ required=False,
+ to_field_name='vid',
+ help_text=mark_safe(
+ _(
+ 'Assigned tagged VLAN IDs separated by commas, encased with double quotes '
+ '(filtered by VLAN group). Example:'
+ )
+ + ' "100,200,300"'
+ ),
+ )
+ qinq_svlan = CSVModelChoiceField(
+ label=_('Q-in-Q Service VLAN'),
+ queryset=VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
+ required=False,
+ to_field_name='vid',
+ help_text=_('Assigned Q-in-Q Service VLAN ID (filtered by VLAN group)'),
)
vrf = CSVModelChoiceField(
label=_('VRF'),
@@ -185,7 +224,7 @@ class VMInterfaceImportForm(NetBoxModelImportForm):
model = VMInterface
fields = (
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode',
- 'vrf', 'tags'
+ 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'tags'
)
def __init__(self, data=None, *args, **kwargs):
@@ -200,6 +239,13 @@ class VMInterfaceImportForm(NetBoxModelImportForm):
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
+ # Limit choices for VLANs to the assigned VLAN group
+ if vlan_group := data.get('vlan_group'):
+ params = {f"group__{self.fields['vlan_group'].to_field_name}": vlan_group}
+ self.fields['untagged_vlan'].queryset = self.fields['untagged_vlan'].queryset.filter(**params)
+ self.fields['tagged_vlans'].queryset = self.fields['tagged_vlans'].queryset.filter(**params)
+ self.fields['qinq_svlan'].queryset = self.fields['qinq_svlan'].queryset.filter(**params)
+
def clean_enabled(self):
# Make sure enabled is True when it's not included in the uploaded data
if 'enabled' not in self.data:
diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py
index 35226c16d..0c1d2b53a 100644
--- a/netbox/virtualization/tests/test_views.py
+++ b/netbox/virtualization/tests/test_views.py
@@ -395,10 +395,19 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
}
cls.csv_data = (
- "virtual_machine,name,vrf.pk",
- f"Virtual Machine 2,Interface 4,{vrfs[0].pk}",
- f"Virtual Machine 2,Interface 5,{vrfs[0].pk}",
- f"Virtual Machine 2,Interface 6,{vrfs[0].pk}",
+ "virtual_machine,name,vrf.pk,mode,untagged_vlan,tagged_vlans",
+ (
+ f"Virtual Machine 2,Interface 4,{vrfs[0].pk},"
+ f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
+ ),
+ (
+ f"Virtual Machine 2,Interface 5,{vrfs[0].pk},"
+ f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
+ ),
+ (
+ f"Virtual Machine 2,Interface 6,{vrfs[0].pk},"
+ f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
+ ),
)
cls.csv_update_data = (