From e4f22bc4941ecea0e3828c71b29ac91bfe213f65 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 Dec 2020 15:22:53 -0500 Subject: [PATCH 1/2] Employ signals to update child objects when RackGroup/Rack site assignment changes --- netbox/dcim/models/racks.py | 16 --------- netbox/dcim/signals.py | 42 +++++++++++++++++++++- netbox/dcim/tests/test_models.py | 61 ++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index cad20241b..de9021f35 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -326,22 +326,6 @@ class Rack(ChangeLoggedModel, CustomFieldModel): 'group': "Rack group must be from the same site, {}.".format(self.site) }) - def save(self, *args, **kwargs): - - # Record the original site assignment for this rack. - _site_id = None - if self.pk: - _site_id = Rack.objects.get(pk=self.pk).site_id - - super().save(*args, **kwargs) - - # Update racked devices if the assigned Site has been changed. - if _site_id is not None and self.site_id != _site_id: - devices = Device.objects.filter(rack=self) - for device in devices: - device.site = self.site - device.save() - def to_csv(self): return ( self.site.name, diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 33c4b461c..380c0aec0 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -2,12 +2,13 @@ import logging from cacheops import invalidate_obj from django.contrib.contenttypes.models import ContentType +from django.db.models import Q from django.db.models.signals import post_save, post_delete, pre_delete from django.db import transaction from django.dispatch import receiver from .choices import CableStatusChoices -from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis +from .models import Cable, CablePath, Device, PathEndpoint, Rack, RackGroup, VirtualChassis def create_cablepath(node): @@ -36,6 +37,40 @@ def rebuild_paths(obj): create_cablepath(cp.origin) +# +# Site/rack/device assignment +# + +@receiver(post_save, sender=RackGroup) +def handle_rackgroup_site_change(instance, created, **kwargs): + """ + Update child RackGroups and Racks if Site assignment has changed. We intentionally recurse through each child + object instead of calling update() on the QuerySet to ensure the proper change records get created for each. + """ + if not created: + for rackgroup in instance.get_children(): + rackgroup.site = instance.site + rackgroup.save() + for rack in Rack.objects.filter(group=instance).exclude(site=instance.site): + rack.site = instance.site + rack.save() + + +@receiver(post_save, sender=Rack) +def handle_rack_site_change(instance, created, **kwargs): + """ + Update child Devices if Site assignment has changed. + """ + if not created: + for device in Device.objects.filter(rack=instance).exclude(site=instance.site): + device.site = instance.site + device.save() + + +# +# Virtual chassis +# + @receiver(post_save, sender=VirtualChassis) def assign_virtualchassis_master(instance, created, **kwargs): """ @@ -60,6 +95,11 @@ def clear_virtualchassis_members(instance, **kwargs): device.save() +# +# Cables +# + + @receiver(post_save, sender=Cable) def update_connected_endpoints(instance, created, raw=False, **kwargs): """ diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index b20e21102..a372bb099 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -7,6 +7,39 @@ from dcim.models import * from tenancy.models import Tenant +class RackGroupTestCase(TestCase): + + def test_change_rackgroup_site(self): + """ + Check that all child RackGroups and Racks get updated when a RackGroup is moved to a new Site. Topology: + Site A + - RackGroup A1 + - RackGroup A2 + - Rack 2 + - Rack 1 + """ + site_a = Site.objects.create(name='Site A', slug='site-a') + site_b = Site.objects.create(name='Site B', slug='site-b') + + rackgroup_a1 = RackGroup(site=site_a, name='RackGroup A1', slug='rackgroup-a1') + rackgroup_a1.save() + rackgroup_a2 = RackGroup(site=site_a, parent=rackgroup_a1, name='RackGroup A2', slug='rackgroup-a2') + rackgroup_a2.save() + + rack1 = Rack.objects.create(site=site_a, group=rackgroup_a1, name='Rack 1') + rack2 = Rack.objects.create(site=site_a, group=rackgroup_a2, name='Rack 2') + + # Move RackGroup A1 to Site B + rackgroup_a1.site = site_b + rackgroup_a1.save() + + # Check that all objects within RackGroup A1 now belong to Site B + self.assertEqual(RackGroup.objects.get(pk=rackgroup_a1.pk).site, site_b) + self.assertEqual(RackGroup.objects.get(pk=rackgroup_a2.pk).site, site_b) + self.assertEqual(Rack.objects.get(pk=rack1.pk).site, site_b) + self.assertEqual(Rack.objects.get(pk=rack2.pk).site, site_b) + + class RackTestCase(TestCase): def setUp(self): @@ -154,6 +187,34 @@ class RackTestCase(TestCase): ) self.assertTrue(pdu) + def test_change_rack_site(self): + """ + Check that child Devices get updated when a Rack is moved to a new Site. + """ + site_a = Site.objects.create(name='Site A', slug='site-a') + site_b = Site.objects.create(name='Site B', slug='site-b') + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, model='Device Type 1', slug='device-type-1' + ) + device_role = DeviceRole.objects.create( + name='Device Role 1', slug='device-role-1', color='ff0000' + ) + + # Create Rack1 in Site A + rack1 = Rack.objects.create(site=site_a, name='Rack 1') + + # Create Device1 in Rack1 + device1 = Device.objects.create(site=site_a, rack=rack1, device_type=device_type, device_role=device_role) + + # Move Rack1 to Site B + rack1.site = site_b + rack1.save() + + # Check that Device1 is now assigned to Site B + self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b) + class DeviceTestCase(TestCase): From 8d9d4cec051c8b0e5433db538b2ae0a5debb92ea Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 23 Dec 2020 14:02:05 -0500 Subject: [PATCH 2/2] Extend handle_rackgroup_site_change() receiver to update power panels --- netbox/dcim/signals.py | 6 ++++-- netbox/dcim/tests/test_models.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 380c0aec0..277e3f060 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -2,13 +2,12 @@ import logging from cacheops import invalidate_obj from django.contrib.contenttypes.models import ContentType -from django.db.models import Q from django.db.models.signals import post_save, post_delete, pre_delete from django.db import transaction from django.dispatch import receiver from .choices import CableStatusChoices -from .models import Cable, CablePath, Device, PathEndpoint, Rack, RackGroup, VirtualChassis +from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, RackGroup, VirtualChassis def create_cablepath(node): @@ -54,6 +53,9 @@ def handle_rackgroup_site_change(instance, created, **kwargs): for rack in Rack.objects.filter(group=instance).exclude(site=instance.site): rack.site = instance.site rack.save() + for powerpanel in PowerPanel.objects.filter(rack_group=instance).exclude(site=instance.site): + powerpanel.site = instance.site + powerpanel.save() @receiver(post_save, sender=Rack) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index a372bb099..184681e90 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -29,6 +29,8 @@ class RackGroupTestCase(TestCase): rack1 = Rack.objects.create(site=site_a, group=rackgroup_a1, name='Rack 1') rack2 = Rack.objects.create(site=site_a, group=rackgroup_a2, name='Rack 2') + powerpanel1 = PowerPanel.objects.create(site=site_a, rack_group=rackgroup_a1, name='Power Panel 1') + # Move RackGroup A1 to Site B rackgroup_a1.site = site_b rackgroup_a1.save() @@ -38,6 +40,7 @@ class RackGroupTestCase(TestCase): self.assertEqual(RackGroup.objects.get(pk=rackgroup_a2.pk).site, site_b) self.assertEqual(Rack.objects.get(pk=rack1.pk).site, site_b) self.assertEqual(Rack.objects.get(pk=rack2.pk).site, site_b) + self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b) class RackTestCase(TestCase):