Skip to content

Commit c429cc3

Browse files
authored
Closes #14171: Add VLAN-related fields to import forms (#20730)
1 parent 032ed4f commit c429cc3

File tree

4 files changed

+126
-19
lines changed

4 files changed

+126
-19
lines changed

netbox/dcim/forms/bulk_import.py

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
from dcim.constants import *
1010
from dcim.models import *
1111
from extras.models import ConfigTemplate
12-
from ipam.models import VRF, IPAddress
12+
from ipam.choices import VLANQinQRoleChoices
13+
from ipam.models import VLAN, VRF, IPAddress, VLANGroup
1314
from netbox.choices import *
1415
from netbox.forms import NetBoxModelImportForm
1516
from tenancy.models import Tenant
1617
from utilities.forms.fields import (
1718
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField,
1819
SlugField,
1920
)
20-
from virtualization.models import Cluster, VMInterface, VirtualMachine
21+
from virtualization.models import Cluster, VirtualMachine, VMInterface
2122
from wireless.choices import WirelessRoleChoices
2223
from .common import ModuleCommonForm
2324

@@ -938,7 +939,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
938939
required=False,
939940
to_field_name='name',
940941
help_text=mark_safe(
941-
_('VDC names separated by commas, encased with double quotes. Example:') + ' <code>vdc1,vdc2,vdc3</code>'
942+
_('VDC names separated by commas, encased with double quotes. Example:') + ' <code>"vdc1,vdc2,vdc3"</code>'
942943
)
943944
)
944945
type = CSVChoiceField(
@@ -967,7 +968,41 @@ class InterfaceImportForm(NetBoxModelImportForm):
967968
label=_('Mode'),
968969
choices=InterfaceModeChoices,
969970
required=False,
970-
help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
971+
help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)'),
972+
)
973+
vlan_group = CSVModelChoiceField(
974+
label=_('VLAN group'),
975+
queryset=VLANGroup.objects.all(),
976+
required=False,
977+
to_field_name='name',
978+
help_text=_('Filter VLANs available for assignment by group'),
979+
)
980+
untagged_vlan = CSVModelChoiceField(
981+
label=_('Untagged VLAN'),
982+
queryset=VLAN.objects.all(),
983+
required=False,
984+
to_field_name='vid',
985+
help_text=_('Assigned untagged VLAN ID (filtered by VLAN group)'),
986+
)
987+
tagged_vlans = CSVModelMultipleChoiceField(
988+
label=_('Tagged VLANs'),
989+
queryset=VLAN.objects.all(),
990+
required=False,
991+
to_field_name='vid',
992+
help_text=mark_safe(
993+
_(
994+
'Assigned tagged VLAN IDs separated by commas, encased with double quotes '
995+
'(filtered by VLAN group). Example:'
996+
)
997+
+ ' <code>"100,200,300"</code>'
998+
),
999+
)
1000+
qinq_svlan = CSVModelChoiceField(
1001+
label=_('Q-in-Q Service VLAN'),
1002+
queryset=VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
1003+
required=False,
1004+
to_field_name='vid',
1005+
help_text=_('Assigned Q-in-Q Service VLAN ID (filtered by VLAN group)'),
9711006
)
9721007
vrf = CSVModelChoiceField(
9731008
label=_('VRF'),
@@ -988,7 +1023,8 @@ class Meta:
9881023
fields = (
9891024
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
9901025
'mark_connected', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
991-
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
1026+
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'rf_role', 'rf_channel',
1027+
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
9921028
)
9931029

9941030
def __init__(self, data=None, *args, **kwargs):
@@ -1005,6 +1041,13 @@ def __init__(self, data=None, *args, **kwargs):
10051041
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
10061042
self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params)
10071043

1044+
# Limit choices for VLANs to the assigned VLAN group
1045+
if vlan_group := data.get('vlan_group'):
1046+
params = {f"group__{self.fields['vlan_group'].to_field_name}": vlan_group}
1047+
self.fields['untagged_vlan'].queryset = self.fields['untagged_vlan'].queryset.filter(**params)
1048+
self.fields['tagged_vlans'].queryset = self.fields['tagged_vlans'].queryset.filter(**params)
1049+
self.fields['qinq_svlan'].queryset = self.fields['qinq_svlan'].queryset.filter(**params)
1050+
10081051
def clean_enabled(self):
10091052
# Make sure enabled is True when it's not included in the uploaded data
10101053
if 'enabled' not in self.data:

netbox/dcim/tests/test_views.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,10 +2834,19 @@ def setUpTestData(cls):
28342834
}
28352835

28362836
cls.csv_data = (
2837-
"device,name,type,vrf.pk,poe_mode,poe_type",
2838-
f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
2839-
f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
2840-
f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
2837+
"device,name,type,vrf.pk,poe_mode,poe_type,mode,untagged_vlan,tagged_vlans",
2838+
(
2839+
f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
2840+
f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
2841+
),
2842+
(
2843+
f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
2844+
f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
2845+
),
2846+
(
2847+
f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
2848+
f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
2849+
),
28412850
)
28422851

28432852
cls.csv_update_data = (

netbox/virtualization/forms/bulk_import.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
from django.utils.translation import gettext_lazy as _
2+
from django.utils.safestring import mark_safe
23

34
from dcim.choices import InterfaceModeChoices
45
from dcim.forms.mixins import ScopedImportForm
56
from dcim.models import Device, DeviceRole, Platform, Site
67
from extras.models import ConfigTemplate
7-
from ipam.models import VRF
8+
from ipam.choices import VLANQinQRoleChoices
9+
from ipam.models import VLAN, VRF, VLANGroup
810
from netbox.forms import NetBoxModelImportForm
911
from tenancy.models import Tenant
10-
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
12+
from utilities.forms.fields import (
13+
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField,
14+
SlugField,
15+
)
1116
from virtualization.choices import *
1217
from virtualization.models import *
1318

@@ -158,20 +163,54 @@ class VMInterfaceImportForm(NetBoxModelImportForm):
158163
queryset=VMInterface.objects.all(),
159164
required=False,
160165
to_field_name='name',
161-
help_text=_('Parent interface')
166+
help_text=_('Parent interface'),
162167
)
163168
bridge = CSVModelChoiceField(
164169
label=_('Bridge'),
165170
queryset=VMInterface.objects.all(),
166171
required=False,
167172
to_field_name='name',
168-
help_text=_('Bridged interface')
173+
help_text=_('Bridged interface'),
169174
)
170175
mode = CSVChoiceField(
171176
label=_('Mode'),
172177
choices=InterfaceModeChoices,
173178
required=False,
174-
help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
179+
help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)'),
180+
)
181+
vlan_group = CSVModelChoiceField(
182+
label=_('VLAN group'),
183+
queryset=VLANGroup.objects.all(),
184+
required=False,
185+
to_field_name='name',
186+
help_text=_('Filter VLANs available for assignment by group'),
187+
)
188+
untagged_vlan = CSVModelChoiceField(
189+
label=_('Untagged VLAN'),
190+
queryset=VLAN.objects.all(),
191+
required=False,
192+
to_field_name='vid',
193+
help_text=_('Assigned untagged VLAN ID (filtered by VLAN group)'),
194+
)
195+
tagged_vlans = CSVModelMultipleChoiceField(
196+
label=_('Tagged VLANs'),
197+
queryset=VLAN.objects.all(),
198+
required=False,
199+
to_field_name='vid',
200+
help_text=mark_safe(
201+
_(
202+
'Assigned tagged VLAN IDs separated by commas, encased with double quotes '
203+
'(filtered by VLAN group). Example:'
204+
)
205+
+ ' <code>"100,200,300"</code>'
206+
),
207+
)
208+
qinq_svlan = CSVModelChoiceField(
209+
label=_('Q-in-Q Service VLAN'),
210+
queryset=VLAN.objects.filter(qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
211+
required=False,
212+
to_field_name='vid',
213+
help_text=_('Assigned Q-in-Q Service VLAN ID (filtered by VLAN group)'),
175214
)
176215
vrf = CSVModelChoiceField(
177216
label=_('VRF'),
@@ -185,7 +224,7 @@ class Meta:
185224
model = VMInterface
186225
fields = (
187226
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode',
188-
'vrf', 'tags'
227+
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'tags'
189228
)
190229

191230
def __init__(self, data=None, *args, **kwargs):
@@ -200,6 +239,13 @@ def __init__(self, data=None, *args, **kwargs):
200239
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
201240
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
202241

242+
# Limit choices for VLANs to the assigned VLAN group
243+
if vlan_group := data.get('vlan_group'):
244+
params = {f"group__{self.fields['vlan_group'].to_field_name}": vlan_group}
245+
self.fields['untagged_vlan'].queryset = self.fields['untagged_vlan'].queryset.filter(**params)
246+
self.fields['tagged_vlans'].queryset = self.fields['tagged_vlans'].queryset.filter(**params)
247+
self.fields['qinq_svlan'].queryset = self.fields['qinq_svlan'].queryset.filter(**params)
248+
203249
def clean_enabled(self):
204250
# Make sure enabled is True when it's not included in the uploaded data
205251
if 'enabled' not in self.data:

netbox/virtualization/tests/test_views.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -395,10 +395,19 @@ def setUpTestData(cls):
395395
}
396396

397397
cls.csv_data = (
398-
"virtual_machine,name,vrf.pk",
399-
f"Virtual Machine 2,Interface 4,{vrfs[0].pk}",
400-
f"Virtual Machine 2,Interface 5,{vrfs[0].pk}",
401-
f"Virtual Machine 2,Interface 6,{vrfs[0].pk}",
398+
"virtual_machine,name,vrf.pk,mode,untagged_vlan,tagged_vlans",
399+
(
400+
f"Virtual Machine 2,Interface 4,{vrfs[0].pk},"
401+
f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
402+
),
403+
(
404+
f"Virtual Machine 2,Interface 5,{vrfs[0].pk},"
405+
f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
406+
),
407+
(
408+
f"Virtual Machine 2,Interface 6,{vrfs[0].pk},"
409+
f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
410+
),
402411
)
403412

404413
cls.csv_update_data = (

0 commit comments

Comments
 (0)