diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index bd466ca48e1..24bd3e62da2 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1175,6 +1175,14 @@ class InterfaceBulkEditForm( }, label=_('LAG') ) + vdcs = DynamicModelMultipleChoiceField( + queryset=VirtualDeviceContext.objects.all(), + required=False, + label='Virtual Device Contexts', + query_params={ + 'device_id': '$device', + } + ) speed = forms.IntegerField( required=False, widget=SelectSpeedWidget(), @@ -1240,14 +1248,14 @@ class InterfaceBulkEditForm( fieldsets = ( (None, ('module', 'type', 'label', 'speed', 'duplex', 'description')), ('Addressing', ('vrf', 'mac_address', 'wwn')), - ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), + ('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), ('PoE', ('poe_mode', 'poe_type')), ('Related Interfaces', ('parent', 'bridge', 'lag')), ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')), ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')), ) nullable_fields = ( - 'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description', + 'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description', 'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', ) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index da658d732f7..d29e8e2500e 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -11,7 +11,9 @@ from ipam.models import VRF from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField +from utilities.forms import ( + CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField, CSVModelMultipleChoiceField +) from virtualization.models import Cluster from wireless.choices import WirelessRoleChoices from .common import ModuleCommonForm @@ -667,6 +669,12 @@ class InterfaceImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Parent LAG interface') ) + vdcs = CSVModelMultipleChoiceField( + queryset=VirtualDeviceContext.objects.all(), + required=False, + to_field_name='name', + help_text='VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")' + ) type = CSVChoiceField( choices=InterfaceTypeChoices, help_text=_('Physical medium') @@ -706,7 +714,7 @@ class Meta: model = Interface fields = ( 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled', - 'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode', + 'mark_connected', 'mac_address', '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' ) @@ -722,6 +730,7 @@ def __init__(self, data=None, *args, **kwargs): self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params) self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params) self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params) + self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params) def clean_enabled(self): # Make sure enabled is True when it's not included in the uploaded data @@ -730,6 +739,12 @@ def clean_enabled(self): else: return self.cleaned_data['enabled'] + def clean_vdcs(self): + for vdc in self.cleaned_data['vdcs']: + if vdc.device != self.cleaned_data['device']: + raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}") + return self.cleaned_data['vdcs'] + class FrontPortImportForm(NetBoxModelImportForm): device = CSVModelChoiceField(