From 582ede8ed33838554c05c61d7acb5789d6db6012 Mon Sep 17 00:00:00 2001 From: Ryan Gillespie <24619595+nopg@users.noreply.github.com> Date: Thu, 20 Jun 2024 08:59:17 -0600 Subject: [PATCH] Fixes #15717 - Unable to assign a VM in Site to Cluster without Site (#15763) * Fixes #15717: Allow VM with Site to Cluster without Site * Fixes #15717: Allow VM with Site to Cluster without Site * Fixes #15717: Allow VM with Site to Cluster without Site * Fixes #15717: Allow VM with Site to Cluster without Site * Fixes #15717: Allow VM with Site to Cluster without Site --- netbox/dcim/forms/model_forms.py | 5 ++- netbox/dcim/tests/test_models.py | 31 +++++++++++++++++++ netbox/virtualization/forms/model_forms.py | 4 +-- .../virtualization/models/virtualmachines.py | 2 +- netbox/virtualization/tests/test_models.py | 3 ++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index d5cc0e856..c12ddccde 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -465,7 +465,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm): label=_('Cluster'), queryset=Cluster.objects.all(), required=False, - selector=True + selector=True, + query_params={ + 'site_id': ['$site', 'null'] + }, ) comments = CommentField() local_context_data = JSONField( diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index cab1760ed..9056a66c0 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -8,6 +8,7 @@ from dcim.models import * from extras.models import CustomField from tenancy.models import Tenant from utilities.data import drange +from virtualization.models import Cluster, ClusterType class LocationTestCase(TestCase): @@ -533,6 +534,36 @@ class DeviceTestCase(TestCase): device2.full_clean() device2.save() + def test_device_mismatched_site_cluster(self): + cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') + Cluster.objects.create(name='Cluster 1', type=cluster_type) + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + ) + Site.objects.bulk_create(sites) + + clusters = ( + Cluster(name='Cluster 1', type=cluster_type, site=sites[0]), + Cluster(name='Cluster 2', type=cluster_type, site=sites[1]), + Cluster(name='Cluster 3', type=cluster_type, site=None), + ) + Cluster.objects.bulk_create(clusters) + + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + + # Device with site only should pass + Device(name='device1', site=sites[0], device_type=device_type, role=device_role).full_clean() + + # Device with site, cluster non-site should pass + Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[2]).full_clean() + + # Device with mismatched site & cluster should fail + with self.assertRaises(ValidationError): + Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[1]).full_clean() + class CableTestCase(TestCase): diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index bfdfc9ada..5ea8a4614 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -178,8 +178,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): required=False, selector=True, query_params={ - 'site_id': '$site', - } + 'site_id': ['$site', 'null'] + }, ) device = DynamicModelChoiceField( label=_('Device'), diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index 92f1a9472..2ca1599bf 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -180,7 +180,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co }) # Validate site for cluster & device - if self.cluster and self.site and self.cluster.site != self.site: + if self.cluster and self.cluster.site is not None and self.cluster.site != self.site: raise ValidationError({ 'cluster': _( 'The selected cluster ({cluster}) is not assigned to this site ({site}).' diff --git a/netbox/virtualization/tests/test_models.py b/netbox/virtualization/tests/test_models.py index c94ff930e..a4e8d7947 100644 --- a/netbox/virtualization/tests/test_models.py +++ b/netbox/virtualization/tests/test_models.py @@ -63,6 +63,9 @@ class VirtualMachineTestCase(TestCase): # VM with site only should pass VirtualMachine(name='vm1', site=sites[0]).full_clean() + # VM with site, cluster non-site should pass + VirtualMachine(name='vm1', site=sites[0], cluster=clusters[2]).full_clean() + # VM with non-site cluster only should pass VirtualMachine(name='vm1', cluster=clusters[2]).full_clean()