diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py index dc91f5dc7..864b20d29 100644 --- a/netbox/dcim/api/serializers_/sites.py +++ b/netbox/dcim/api/serializers_/sites.py @@ -61,7 +61,7 @@ class SiteSerializer(NetBoxModelSerializer): # Related object counts circuit_count = RelatedObjectCountField('circuit_terminations') device_count = RelatedObjectCountField('devices') - prefix_count = RelatedObjectCountField('prefixes') + # prefix_count = RelatedObjectCountField('prefixes') rack_count = RelatedObjectCountField('racks') vlan_count = RelatedObjectCountField('vlans') virtualmachine_count = RelatedObjectCountField('virtual_machines') @@ -72,7 +72,7 @@ class SiteSerializer(NetBoxModelSerializer): 'id', 'url', 'display_url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', - 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count', + 'rack_count', 'virtualmachine_count', 'vlan_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index b52c36104..a6ef1a9fb 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -167,13 +167,11 @@ class PrefixImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Assigned tenant') ) - # site = CSVModelChoiceField( - # label=_('Site'), - # queryset=Site.objects.all(), - # required=False, - # to_field_name='name', - # help_text=_('Assigned site') - # ) + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) vlan_group = CSVModelChoiceField( label=_('VLAN group'), queryset=VLANGroup.objects.all(), @@ -204,9 +202,12 @@ class PrefixImportForm(NetBoxModelImportForm): class Meta: model = Prefix fields = ( - 'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', - 'description', 'comments', 'tags', + 'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan', 'status', 'role', 'scope_type', 'scope_id', 'is_pool', + 'mark_utilized', 'description', 'comments', 'tags', ) + labels = { + 'scope_id': 'Scope ID', + } def __init__(self, data=None, *args, **kwargs): super().__init__(data, *args, **kwargs) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 69812c94c..87c1bcda6 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -207,7 +207,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm): required=False, label=_('Scope type') ) - scope = DynamicModelChoiceField( + scope_id = DynamicModelChoiceField( label=_('Scope'), queryset=Site.objects.none(), # Initial queryset required=False, @@ -242,8 +242,8 @@ class PrefixForm(TenancyForm, NetBoxModelForm): class Meta: model = Prefix fields = [ - 'prefix', 'vrf', 'vlan', 'scope_type', 'scope', 'status', 'role', 'is_pool', 'mark_utilized', - 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'prefix', 'vrf', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group', 'tenant', + 'description', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -260,21 +260,21 @@ class PrefixForm(TenancyForm, NetBoxModelForm): try: scope_type = ContentType.objects.get(pk=scope_type_id) model = scope_type.model_class() - self.fields['scope'].queryset = model.objects.all() - self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower - self.fields['scope'].disabled = False - self.fields['scope'].label = _(bettertitle(model._meta.verbose_name)) + self.fields['scope_id'].queryset = model.objects.all() + self.fields['scope_id'].widget.attrs['selector'] = model._meta.label_lower + self.fields['scope_id'].disabled = False + self.fields['scope_id'].label = _(bettertitle(model._meta.verbose_name)) except ObjectDoesNotExist: pass if self.instance and scope_type_id != self.instance.scope_type_id: - self.initial['scope'] = None + self.initial['scope_id'] = None def clean(self): super().clean() # Assign the selected scope (if any) - self.instance.scope = self.cleaned_data.get('scope') + self.instance.scope = self.cleaned_data.get('scope_id') class IPRangeForm(TenancyForm, NetBoxModelForm): diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 95b311878..5ab6317aa 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -1,5 +1,6 @@ import datetime +from django.contrib.contenttypes.models import ContentType from django.test import override_settings from django.urls import reverse from netaddr import IPNetwork @@ -409,9 +410,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): Role.objects.bulk_create(roles) prefixes = ( - Prefix(prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), - Prefix(prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), - Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), + Prefix(prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]), + Prefix(prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]), + Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]), ) Prefix.objects.bulk_create(prefixes) @@ -419,7 +420,8 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): cls.form_data = { 'prefix': IPNetwork('192.0.2.0/24'), - 'site': sites[1].pk, + 'scope_type': ContentType.objects.get_for_model(Site).pk, + 'scope_id': sites[1].pk, 'vrf': vrfs[1].pk, 'tenant': None, 'vlan': None, @@ -445,7 +447,6 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) cls.bulk_edit_data = { - 'site': sites[1].pk, 'vrf': vrfs[1].pk, 'tenant': None, 'status': PrefixStatusChoices.STATUS_RESERVED, @@ -501,11 +502,13 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): """ Custom import test for YAML-based imports (versus CSV) """ - IMPORT_DATA = """ + site = Site.objects.get(name='Site 1') + IMPORT_DATA = f""" prefix: 10.1.1.0/24 status: active vlan: 101 -site: Site 1 +scope_type: dcim.site +scope_id: {site.pk} """ # Note, a site is not tied to the VLAN to verify the fix for #12622 VLAN.objects.create(vid=101, name='VLAN101') @@ -523,19 +526,21 @@ site: Site 1 prefix = Prefix.objects.get(prefix='10.1.1.0/24') self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE) self.assertEqual(prefix.vlan.vid, 101) - self.assertEqual(prefix.site.name, "Site 1") + self.assertEqual(prefix.scope, site) @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_prefix_import_with_vlan_group(self): """ This test covers a unique import edge case where VLAN group is specified during the import. """ - IMPORT_DATA = """ + site = Site.objects.get(name='Site 1') + IMPORT_DATA = f""" prefix: 10.1.2.0/24 status: active -vlan: 102 -site: Site 1 +scope_type: dcim.site +scope_id: {site.pk} vlan_group: Group 1 +vlan: 102 """ vlan_group = VLANGroup.objects.create(name='Group 1', slug='group-1', scope=Site.objects.get(name="Site 1")) VLAN.objects.create(vid=102, name='VLAN102', group=vlan_group) @@ -553,7 +558,7 @@ vlan_group: Group 1 prefix = Prefix.objects.get(prefix='10.1.2.0/24') self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE) self.assertEqual(prefix.vlan.vid, 102) - self.assertEqual(prefix.site.name, "Site 1") + self.assertEqual(prefix.scope, site) class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 1531667ef..5381ec187 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -352,7 +352,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView): def get_children(self, request, parent): return Prefix.objects.restrict(request.user, 'view').filter( prefix__net_contained_or_equal=str(parent.prefix) - ).prefetch_related('site', 'role', 'tenant', 'tenant__group', 'vlan') + ).prefetch_related('scope', 'role', 'tenant', 'tenant__group', 'vlan') def prep_table_data(self, request, queryset, parent): # Determine whether to show assigned prefixes, available prefixes, or both diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index ae6d3f4c2..e8aaad2e1 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -8,8 +8,7 @@ from netaddr import IPNetwork from rest_framework.test import APIClient from core.models import ObjectType -from dcim.models import Site -from ipam.models import Prefix +from dcim.models import Rack, Site from users.models import Group, ObjectPermission, Token, User from utilities.testing import TestCase from utilities.testing.api import APITestCase @@ -410,18 +409,18 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) Site.objects.bulk_create(cls.sites) - cls.prefixes = ( - Prefix(prefix=IPNetwork('10.0.0.0/24'), site=cls.sites[0]), - Prefix(prefix=IPNetwork('10.0.1.0/24'), site=cls.sites[0]), - Prefix(prefix=IPNetwork('10.0.2.0/24'), site=cls.sites[0]), - Prefix(prefix=IPNetwork('10.0.3.0/24'), site=cls.sites[1]), - Prefix(prefix=IPNetwork('10.0.4.0/24'), site=cls.sites[1]), - Prefix(prefix=IPNetwork('10.0.5.0/24'), site=cls.sites[1]), - Prefix(prefix=IPNetwork('10.0.6.0/24'), site=cls.sites[2]), - Prefix(prefix=IPNetwork('10.0.7.0/24'), site=cls.sites[2]), - Prefix(prefix=IPNetwork('10.0.8.0/24'), site=cls.sites[2]), + cls.racks = ( + Rack(name='Rack 1', site=cls.sites[0]), + Rack(name='Rack 2', site=cls.sites[0]), + Rack(name='Rack 3', site=cls.sites[0]), + Rack(name='Rack 4', site=cls.sites[1]), + Rack(name='Rack 5', site=cls.sites[1]), + Rack(name='Rack 6', site=cls.sites[1]), + Rack(name='Rack 7', site=cls.sites[2]), + Rack(name='Rack 8', site=cls.sites[2]), + Rack(name='Rack 9', site=cls.sites[2]), ) - Prefix.objects.bulk_create(cls.prefixes) + Rack.objects.bulk_create(cls.racks) def setUp(self): """ @@ -435,8 +434,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): def test_get_object(self): # Attempt to retrieve object without permission - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[0].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk}) response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 403) @@ -448,23 +446,21 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack)) # Retrieve permitted object - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[0].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk}) response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 200) # Attempt to retrieve non-permitted object - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[3].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[3].pk}) response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 404) @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) def test_list_objects(self): - url = reverse('ipam-api:prefix-list') + url = reverse('dcim-api:rack-list') # Attempt to list objects without permission response = self.client.get(url, **self.header) @@ -478,7 +474,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack)) # Retrieve all objects. Only permitted objects should be returned. response = self.client.get(url, **self.header) @@ -487,12 +483,12 @@ class ObjectPermissionAPIViewTestCase(TestCase): @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) def test_create_object(self): - url = reverse('ipam-api:prefix-list') + url = reverse('dcim-api:rack-list') data = { - 'prefix': '10.0.9.0/24', + 'name': 'Rack 10', 'site': self.sites[1].pk, } - initial_count = Prefix.objects.count() + initial_count = Rack.objects.count() # Attempt to create an object without permission response = self.client.post(url, data, format='json', **self.header) @@ -506,26 +502,25 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack)) # Attempt to create a non-permitted object response = self.client.post(url, data, format='json', **self.header) self.assertEqual(response.status_code, 403) - self.assertEqual(Prefix.objects.count(), initial_count) + self.assertEqual(Rack.objects.count(), initial_count) # Create a permitted object data['site'] = self.sites[0].pk response = self.client.post(url, data, format='json', **self.header) self.assertEqual(response.status_code, 201) - self.assertEqual(Prefix.objects.count(), initial_count + 1) + self.assertEqual(Rack.objects.count(), initial_count + 1) @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) def test_edit_object(self): # Attempt to edit an object without permission data = {'site': self.sites[0].pk} - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[0].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk}) response = self.client.patch(url, data, format='json', **self.header) self.assertEqual(response.status_code, 403) @@ -537,26 +532,23 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack)) # Attempt to edit a non-permitted object data = {'site': self.sites[0].pk} - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[3].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[3].pk}) response = self.client.patch(url, data, format='json', **self.header) self.assertEqual(response.status_code, 404) # Edit a permitted object data['status'] = 'reserved' - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[0].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk}) response = self.client.patch(url, data, format='json', **self.header) self.assertEqual(response.status_code, 200) # Attempt to modify a permitted object to a non-permitted object data['site'] = self.sites[1].pk - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[0].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk}) response = self.client.patch(url, data, format='json', **self.header) self.assertEqual(response.status_code, 403) @@ -564,8 +556,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): def test_delete_object(self): # Attempt to delete an object without permission - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[0].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk}) response = self.client.delete(url, format='json', **self.header) self.assertEqual(response.status_code, 403) @@ -577,16 +568,14 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack)) # Attempt to delete a non-permitted object - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[3].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[3].pk}) response = self.client.delete(url, format='json', **self.header) self.assertEqual(response.status_code, 404) # Delete a permitted object - url = reverse('ipam-api:prefix-detail', - kwargs={'pk': self.prefixes[0].pk}) + url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk}) response = self.client.delete(url, format='json', **self.header) self.assertEqual(response.status_code, 204)