mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-18 07:24:28 +01:00
Fixes #21578: Enable assignment of scope object by name when bulk importing prefixes/VLAN groups (#21671)
This commit is contained in:
@@ -121,13 +121,52 @@ class ScopedImportForm(forms.Form):
|
||||
required=False,
|
||||
label=_('Scope type (app & model)')
|
||||
)
|
||||
scope_name = forms.CharField(
|
||||
required=False,
|
||||
label=_('Scope name'),
|
||||
help_text=_('Name of the assigned scope object (if not using ID)')
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
scope_id = self.cleaned_data.get('scope_id')
|
||||
scope_name = self.cleaned_data.get('scope_name')
|
||||
scope_type = self.cleaned_data.get('scope_type')
|
||||
if scope_type and not scope_id:
|
||||
|
||||
# Cannot specify both scope_name and scope_id
|
||||
if scope_name and scope_id:
|
||||
raise ValidationError(_("scope_name and scope_id are mutually exclusive."))
|
||||
|
||||
# Must specify scope_type with scope_name or scope_id
|
||||
if scope_name and not scope_type:
|
||||
raise ValidationError(_("scope_type must be specified when using scope_name"))
|
||||
if scope_id and not scope_type:
|
||||
raise ValidationError(_("scope_type must be specified when using scope_id"))
|
||||
|
||||
# Look up the scope object by name
|
||||
if scope_type and scope_name:
|
||||
model = scope_type.model_class()
|
||||
try:
|
||||
scope_obj = model.objects.get(name=scope_name)
|
||||
except model.DoesNotExist:
|
||||
raise ValidationError({
|
||||
'scope_name': _('{scope_type} "{name}" not found.').format(
|
||||
scope_type=bettertitle(model._meta.verbose_name),
|
||||
name=scope_name
|
||||
)
|
||||
})
|
||||
except model.MultipleObjectsReturned:
|
||||
raise ValidationError({
|
||||
'scope_name': _(
|
||||
'Multiple {scope_type} objects match "{name}". Use scope_id to specify the intended object.'
|
||||
).format(
|
||||
scope_type=bettertitle(model._meta.verbose_name),
|
||||
name=scope_name,
|
||||
)
|
||||
})
|
||||
self.cleaned_data['scope_id'] = scope_obj.pk
|
||||
elif scope_type and not scope_id:
|
||||
raise ValidationError({
|
||||
'scope_id': _(
|
||||
"Please select a {scope_type}."
|
||||
|
||||
@@ -210,8 +210,8 @@ class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = (
|
||||
'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan_site', 'vlan', 'status', 'role', 'scope_type', 'scope_id',
|
||||
'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan_site', 'vlan', 'status', 'role', 'scope_type', 'scope_name',
|
||||
'scope_id', 'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
)
|
||||
labels = {
|
||||
'scope_id': _('Scope ID'),
|
||||
@@ -474,7 +474,8 @@ class FHRPGroupImportForm(PrimaryModelImportForm):
|
||||
fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class VLANGroupImportForm(OrganizationalModelImportForm):
|
||||
class VLANGroupImportForm(ScopedImportForm, OrganizationalModelImportForm):
|
||||
# Override ScopedImportForm.scope_type to set custom queryset
|
||||
scope_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||
required=False,
|
||||
@@ -494,10 +495,11 @@ class VLANGroupImportForm(OrganizationalModelImportForm):
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = (
|
||||
'name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'tenant', 'description', 'owner', 'comments', 'tags'
|
||||
'name', 'slug', 'scope_type', 'scope_name', 'scope_id', 'vid_ranges', 'tenant', 'description', 'owner',
|
||||
'comments', 'tags',
|
||||
)
|
||||
labels = {
|
||||
'scope_id': 'Scope ID',
|
||||
'scope_id': _('Scope ID'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -435,13 +435,21 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
site = sites[0].pk
|
||||
cls.csv_data = (
|
||||
"vrf,prefix,status,scope_type,scope_id",
|
||||
f"VRF 1,10.4.0.0/16,active,dcim.site,{site}",
|
||||
f"VRF 1,10.5.0.0/16,active,dcim.site,{site}",
|
||||
f"VRF 1,10.6.0.0/16,active,dcim.site,{site}",
|
||||
)
|
||||
site = sites[0]
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"vrf,prefix,status,scope_type,scope_id",
|
||||
f"VRF 1,10.4.0.0/16,active,dcim.site,{site.pk}",
|
||||
f"VRF 1,10.5.0.0/16,active,dcim.site,{site.pk}",
|
||||
f"VRF 1,10.6.0.0/16,active,dcim.site,{site.pk}",
|
||||
),
|
||||
'scope_name': (
|
||||
"vrf,prefix,status,scope_type,scope_name",
|
||||
f"VRF 1,10.4.0.0/16,active,dcim.site,{site.name}",
|
||||
f"VRF 1,10.5.0.0/16,active,dcim.site,{site.name}",
|
||||
f"VRF 1,10.6.0.0/16,active,dcim.site,{site.name}",
|
||||
),
|
||||
}
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,description,status",
|
||||
@@ -532,6 +540,32 @@ scope_id: {site.pk}
|
||||
self.assertEqual(prefix.vlan.vid, 101)
|
||||
self.assertEqual(prefix.scope, site)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_prefix_import_with_scope_name(self):
|
||||
"""
|
||||
Test YAML-based import using scope_name instead of scope_id.
|
||||
"""
|
||||
site = Site.objects.get(name='Site 1')
|
||||
IMPORT_DATA = """
|
||||
prefix: 10.1.3.0/24
|
||||
status: active
|
||||
scope_type: dcim.site
|
||||
scope_name: Site 1
|
||||
"""
|
||||
# Add all required permissions to the test user
|
||||
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
|
||||
|
||||
form_data = {
|
||||
'data': IMPORT_DATA,
|
||||
'format': 'yaml'
|
||||
}
|
||||
response = self.client.post(reverse('ipam:prefix_bulk_import'), data=form_data, follow=True)
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
prefix = Prefix.objects.get(prefix='10.1.3.0/24')
|
||||
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE)
|
||||
self.assertEqual(prefix.scope, site)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_prefix_import_with_vlan_group(self):
|
||||
"""
|
||||
@@ -884,12 +918,20 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug,scope_type,scope_id,description",
|
||||
"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
|
||||
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group",
|
||||
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group",
|
||||
)
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"name,slug,scope_type,scope_id,description",
|
||||
"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
|
||||
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group",
|
||||
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group",
|
||||
),
|
||||
'scope_name': (
|
||||
"name,slug,scope_type,scope_name,description",
|
||||
"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
|
||||
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].name},Fifth VLAN group",
|
||||
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].name},Sixth VLAN group",
|
||||
),
|
||||
}
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
|
||||
@@ -74,8 +74,8 @@ class ClusterImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = (
|
||||
'name', 'type', 'group', 'status', 'scope_type', 'scope_id', 'tenant', 'description', 'owner', 'comments',
|
||||
'tags',
|
||||
'name', 'type', 'group', 'status', 'scope_type', 'scope_name', 'scope_id', 'tenant', 'description', 'owner',
|
||||
'comments', 'tags',
|
||||
)
|
||||
labels = {
|
||||
'scope_id': _('Scope ID'),
|
||||
|
||||
@@ -157,12 +157,20 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,type,status",
|
||||
"Cluster 4,Cluster Type 1,active",
|
||||
"Cluster 5,Cluster Type 1,active",
|
||||
"Cluster 6,Cluster Type 1,active",
|
||||
)
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"name,type,status,scope_type,scope_id",
|
||||
f"Cluster 4,Cluster Type 1,active,dcim.site,{sites[0].pk}",
|
||||
f"Cluster 5,Cluster Type 1,active,dcim.site,{sites[0].pk}",
|
||||
f"Cluster 6,Cluster Type 1,active,dcim.site,{sites[0].pk}",
|
||||
),
|
||||
'scope_name': (
|
||||
"name,type,status,scope_type,scope_name",
|
||||
f"Cluster 4,Cluster Type 1,active,dcim.site,{sites[0].name}",
|
||||
f"Cluster 5,Cluster Type 1,active,dcim.site,{sites[0].name}",
|
||||
f"Cluster 6,Cluster Type 1,active,dcim.site,{sites[0].name}",
|
||||
),
|
||||
}
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,comments",
|
||||
|
||||
@@ -76,7 +76,7 @@ class WirelessLANImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
model = WirelessLAN
|
||||
fields = (
|
||||
'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'scope_type',
|
||||
'scope_id', 'description', 'owner', 'comments', 'tags',
|
||||
'scope_name', 'scope_id', 'description', 'owner', 'comments', 'tags',
|
||||
)
|
||||
labels = {
|
||||
'scope_id': _('Scope ID'),
|
||||
|
||||
@@ -116,23 +116,42 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"group,ssid,status,tenant,scope_type,scope_id",
|
||||
"Wireless LAN Group 2,WLAN4,{status},{tenant},,".format(
|
||||
status=WirelessLANStatusChoices.STATUS_ACTIVE,
|
||||
tenant=tenants[0].name
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"group,ssid,status,tenant,scope_type,scope_id",
|
||||
"Wireless LAN Group 2,WLAN4,{status},{tenant},,".format(
|
||||
status=WirelessLANStatusChoices.STATUS_ACTIVE,
|
||||
tenant=tenants[0].name
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN5,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_DISABLED,
|
||||
tenant=tenants[1].name,
|
||||
site=sites[0].pk
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN6,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_RESERVED,
|
||||
tenant=tenants[2].name,
|
||||
site=sites[1].pk
|
||||
),
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN5,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_DISABLED,
|
||||
tenant=tenants[1].name,
|
||||
site=sites[0].pk
|
||||
'scope_name': (
|
||||
"group,ssid,status,tenant,scope_type,scope_name",
|
||||
"Wireless LAN Group 2,WLAN4,{status},{tenant},,".format(
|
||||
status=WirelessLANStatusChoices.STATUS_ACTIVE,
|
||||
tenant=tenants[0].name
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN5,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_DISABLED,
|
||||
tenant=tenants[1].name,
|
||||
site=sites[0].name
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN6,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_RESERVED,
|
||||
tenant=tenants[2].name,
|
||||
site=sites[1].name
|
||||
),
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN6,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_RESERVED,
|
||||
tenant=tenants[2].name,
|
||||
site=sites[1].pk
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,ssid",
|
||||
|
||||
Reference in New Issue
Block a user