diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index c95e76d14..356f70373 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -1070,7 +1070,7 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet): method='get_for_site' ) available_on_device = django_filters.ModelChoiceFilter( - queryset=Device.objects.all(), + queryset=Device.objects.select_related('cluster'), method='get_for_device' ) available_on_virtualmachine = django_filters.ModelChoiceFilter( diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 4a48ee81d..d58632672 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -298,6 +298,18 @@ class VLANQuerySet(RestrictedQuerySet): # Find all relevant VLANGroups q = Q() + if device.cluster_id: + # The Device's physical scope is evaluated below. For valid assignments, + # the Cluster's physical scope is already represented by that hierarchy. + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'), + scope_id=device.cluster_id + ) + if device.cluster.group_id: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'), + scope_id=device.cluster.group_id + ) if device.site.region: q |= Q( scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 4e396a9f8..3b9288453 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -2188,6 +2188,26 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'available_on_device': device_id} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global + def test_available_on_device_cluster_scopes(self): + device = Device.objects.get(name='Device 1') + device.cluster = Cluster.objects.get(name='Cluster 1') + device.save(update_fields=('cluster',)) + + params = {'available_on_device': device.pk} + vlans = self.filterset(params, self.queryset).qs + + # VLANs from groups scoped to the assigned cluster or its cluster group + self.assertIn(VLAN.objects.get(name='Cluster 1'), vlans) + self.assertIn(VLAN.objects.get(name='Cluster Group 1'), vlans) + # VLANs from groups scoped to unrelated clusters or cluster groups + self.assertNotIn(VLAN.objects.get(name='Cluster 2'), vlans) + self.assertNotIn(VLAN.objects.get(name='Cluster Group 2'), vlans) + # Site, location, rack and global availability is unchanged + self.assertEqual( + set(vlans.values_list('vid', flat=True)), + {1, 4, 7, 10, 13, 16, 19, 500, 1000} + ) + def test_available_on_virtualmachine(self): vm_id = VirtualMachine.objects.first().pk params = {'available_on_virtualmachine': vm_id}