diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index aa0255e30..f9d8a1652 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -118,7 +118,8 @@ class VLANGroupSerializer(OrganizationalModelSerializer): queryset=ContentType.objects.filter( app_label='dcim', model__in=['region', 'sitegroup', 'site', 'location', 'rack'] - ) + ), + required=False ) scope = serializers.SerializerMethodField(read_only=True) vlan_count = serializers.IntegerField(read_only=True) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 37a3a9d2c..db7077848 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1265,6 +1265,8 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): ('dcim.site', 'Site'), ('dcim.location', 'Location'), ('dcim.rack', 'Rack'), + ('virtualization.clustergroup', 'Cluster group'), + ('virtualization.cluster', 'Cluster'), ), required=False, widget=StaticSelect2, @@ -1372,15 +1374,6 @@ class VLANCSVForm(CustomFieldModelCSVForm): 'name': 'VLAN name', } - def __init__(self, data=None, *args, **kwargs): - super().__init__(data, *args, **kwargs) - - if data: - - # Limit vlan queryset by assigned group - params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} - self.fields['group'].queryset = self.fields['group'].queryset.filter(**params) - class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index aa25fde46..26cb5299f 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -72,6 +72,15 @@ class VLANGroup(OrganizationalModel): def get_absolute_url(self): return reverse('ipam:vlangroup_vlans', args=[self.pk]) + def clean(self): + super().clean() + + # Validate scope assignment + if self.scope_type and not self.scope_id: + raise ValidationError("Cannot set scope_type without scope_id.") + if self.scope_id and not self.scope_type: + raise ValidationError("Cannot set scope_id without scope_type.") + def to_csv(self): return ( self.name, diff --git a/netbox/ipam/tests/test_filters.py b/netbox/ipam/tests/test_filters.py index b83ea6efe..90af26e9b 100644 --- a/netbox/ipam/tests/test_filters.py +++ b/netbox/ipam/tests/test_filters.py @@ -1,10 +1,10 @@ from django.test import TestCase -from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup +from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup from ipam.choices import * from ipam.filters import * from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF -from virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface +from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from tenancy.models import Tenant, TenantGroup @@ -715,34 +715,39 @@ class VLANGroupTestCase(TestCase): @classmethod def setUpTestData(cls): - regions = ( - Region(name='Test Region 1', slug='test-region-1'), - Region(name='Test Region 2', slug='test-region-2'), - Region(name='Test Region 3', slug='test-region-3'), - ) - for r in regions: - r.save() + region = Region(name='Region 1', slug='region-1') + region.save() - site_groups = ( - SiteGroup(name='Site Group 1', slug='site-group-1'), - SiteGroup(name='Site Group 2', slug='site-group-2'), - SiteGroup(name='Site Group 3', slug='site-group-3'), - ) - for site_group in site_groups: - site_group.save() + sitegroup = SiteGroup(name='Site Group 1', slug='site-group-1') + sitegroup.save() - sites = ( - Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]), - Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]), - Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]), - ) - Site.objects.bulk_create(sites) + site = Site(name='Site 1', slug='site-1') + site.save() + + location = Location(name='Location 1', slug='location-1', site=site) + location.save() + + rack = Rack(name='Rack 1', site=site) + rack.save() + + clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1') + clustertype.save() + + clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1') + clustergroup.save() + + cluster = Cluster(name='Cluster 1', type=clustertype) + cluster.save() vlan_groups = ( - VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0], description='A'), - VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1], description='B'), - VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=sites[2], description='C'), - VLANGroup(name='VLAN Group 4', slug='vlan-group-4', site=None), + VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=region, description='A'), + VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sitegroup, description='B'), + VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=site, description='C'), + VLANGroup(name='VLAN Group 4', slug='vlan-group-4', scope=location, description='D'), + VLANGroup(name='VLAN Group 5', slug='vlan-group-5', scope=rack, description='E'), + VLANGroup(name='VLAN Group 6', slug='vlan-group-6', scope=clustergroup, description='F'), + VLANGroup(name='VLAN Group 7', slug='vlan-group-7', scope=cluster, description='G'), + VLANGroup(name='VLAN Group 8', slug='vlan-group-8'), ) VLANGroup.objects.bulk_create(vlan_groups) @@ -763,25 +768,32 @@ class VLANGroupTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_region(self): - regions = Region.objects.all()[:2] - params = {'region_id': [regions[0].pk, regions[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'region': [regions[0].slug, regions[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'region': Region.objects.first().pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - def test_site_group(self): - site_groups = SiteGroup.objects.all()[:2] - params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'site_group': [site_groups[0].slug, site_groups[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_sitegroup(self): + params = {'sitegroup': SiteGroup.objects.first().pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_site(self): - sites = Site.objects.all()[:2] - params = {'site_id': [sites[0].pk, sites[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'site': [sites[0].slug, sites[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'site': Site.objects.first().pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_location(self): + params = {'location': Location.objects.first().pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_rack(self): + params = {'rack': Rack.objects.first().pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_clustergroup(self): + params = {'clustergroup': ClusterGroup.objects.first().pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_cluster(self): + params = {'cluster': Cluster.objects.first().pk} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) class VLANTestCase(TestCase): @@ -822,9 +834,9 @@ class VLANTestCase(TestCase): Role.objects.bulk_create(roles) groups = ( - VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]), - VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]), - VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=None), + VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]), + VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[1]), + VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=None), ) VLANGroup.objects.bulk_create(groups) diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index b105ea7d9..7d099e762 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -306,18 +306,22 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): @classmethod def setUpTestData(cls): - site = Site.objects.create(name='Site 1', slug='site-1') + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) VLANGroup.objects.bulk_create([ - VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site), - VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=site), - VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site), + VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]), + VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[0]), + VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]), ]) cls.form_data = { 'name': 'VLAN Group X', 'slug': 'vlan-group-x', - 'site': site.pk, + 'site': sites[1].pk, 'description': 'A new VLAN group', } @@ -342,8 +346,8 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): Site.objects.bulk_create(sites) vlangroups = ( - VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]), - VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]), + VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]), + VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[1]), ) VLANGroup.objects.bulk_create(vlangroups) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 822aca247..3f283a932 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -631,7 +631,7 @@ class IPAddressBulkDeleteView(generic.BulkDeleteView): # class VLANGroupListView(generic.ObjectListView): - queryset = VLANGroup.objects.prefetch_related('site').annotate( + queryset = VLANGroup.objects.annotate( vlan_count=count_related(VLAN, 'group') ) filterset = filters.VLANGroupFilterSet