diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index be93f33b9ba..845cc68d20a 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1154,7 +1154,6 @@ def clean(self): }) def delete(self, *args, **kwargs): - # Check for LAG interfaces split across member chassis interfaces = Interface.objects.filter( device__in=self.members.all(), @@ -1168,6 +1167,13 @@ def delete(self, *args, **kwargs): "interfaces." ).format(self=self, interfaces=InterfaceSpeedChoices)) + # Clear vc_position and vc_priority on member devices BEFORE calling super().delete() + # This must be done here because on_delete=SET_NULL executes before pre_delete signal + for device in self.members.all(): + device.vc_position = None + device.vc_priority = None + device.save() + return super().delete(*args, **kwargs) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 8f7d0d2ebdc..9295ddbdbab 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -1,6 +1,6 @@ import logging -from django.db.models.signals import post_save, post_delete, pre_delete +from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from dcim.choices import CableEndChoices, LinkStatusChoices @@ -85,18 +85,6 @@ def assign_virtualchassis_master(instance, created, **kwargs): master.save() -@receiver(pre_delete, sender=VirtualChassis) -def clear_virtualchassis_members(instance, **kwargs): - """ - When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members. - """ - devices = Device.objects.filter(virtual_chassis=instance.pk) - for device in devices: - device.vc_position = None - device.vc_priority = None - device.save() - - # # Cables # diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 676030a047b..877af600b90 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1031,3 +1031,92 @@ def test_vdc_duplicate_identifier(self): vdc2 = VirtualDeviceContext(device=device, name="VDC 2", identifier=1, status='active') with self.assertRaises(ValidationError): vdc2.full_clean() + + +class VirtualChassisTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + site = Site.objects.create(name='Test Site 1', slug='test-site-1') + manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') + devicetype = DeviceType.objects.create( + manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' + ) + role = DeviceRole.objects.create( + name='Test Device Role 1', slug='test-device-role-1', color='ff0000' + ) + Device.objects.create( + device_type=devicetype, role=role, name='TestDevice1', site=site + ) + Device.objects.create( + device_type=devicetype, role=role, name='TestDevice2', site=site + ) + + def test_virtualchassis_deletion_clears_vc_position(self): + """ + Test that when a VirtualChassis is deleted, member devices have their + vc_position and vc_priority fields set to None. + """ + devices = Device.objects.all() + device1 = devices[0] + device2 = devices[1] + + # Create a VirtualChassis with two member devices + vc = VirtualChassis.objects.create(name='Test VC', master=device1) + + device1.virtual_chassis = vc + device1.vc_position = 1 + device1.vc_priority = 10 + device1.save() + + device2.virtual_chassis = vc + device2.vc_position = 2 + device2.vc_priority = 20 + device2.save() + + # Verify devices are members of the VC with positions set + device1.refresh_from_db() + device2.refresh_from_db() + self.assertEqual(device1.virtual_chassis, vc) + self.assertEqual(device1.vc_position, 1) + self.assertEqual(device1.vc_priority, 10) + self.assertEqual(device2.virtual_chassis, vc) + self.assertEqual(device2.vc_position, 2) + self.assertEqual(device2.vc_priority, 20) + + # Delete the VirtualChassis + vc.delete() + + # Verify devices have vc_position and vc_priority set to None + device1.refresh_from_db() + device2.refresh_from_db() + self.assertIsNone(device1.virtual_chassis) + self.assertIsNone(device1.vc_position) + self.assertIsNone(device1.vc_priority) + self.assertIsNone(device2.virtual_chassis) + self.assertIsNone(device2.vc_position) + self.assertIsNone(device2.vc_priority) + + def test_virtualchassis_duplicate_vc_position(self): + """ + Test that two devices cannot be assigned to the same vc_position + within the same VirtualChassis. + """ + devices = Device.objects.all() + device1 = devices[0] + device2 = devices[1] + + # Create a VirtualChassis + vc = VirtualChassis.objects.create(name='Test VC') + + # Assign first device to vc_position 1 + device1.virtual_chassis = vc + device1.vc_position = 1 + device1.full_clean() + device1.save() + + # Try to assign second device to the same vc_position + device2.virtual_chassis = vc + device2.vc_position = 1 + with self.assertRaises(ValidationError): + device2.full_clean()