From 3e92aa9fe778509f8c653c4f1a8ab293c569e597 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Nov 2018 12:15:56 -0500 Subject: [PATCH] Fixes #2571: Enforce deletion of attached cable when deleting a termination point --- CHANGELOG.md | 1 + netbox/dcim/models.py | 12 ++++++++ netbox/dcim/signals.py | 10 ++++--- netbox/dcim/tests/test_models.py | 51 ++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d40cc01fd..49fdc6b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ NetBox now supports modeling physical cables for console, power, and interface c * [#2566](https://github.com/digitalocean/netbox/issues/2566) - Prevent both ends of a cable from connecting to the same termination point * [#2567](https://github.com/digitalocean/netbox/issues/2567) - Introduced proxy models to represent console/power/interface connections * [#2569](https://github.com/digitalocean/netbox/issues/2569) - Added LSH fiber type; removed SC duplex/simplex designations +* [#2571](https://github.com/digitalocean/netbox/issues/2571) - Enforce deletion of attached cable when deleting a termination point ## API Changes diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 22a830123..d3c18d7e2 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -73,6 +73,18 @@ class CableTermination(models.Model): null=True ) + # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted. + _cabled_as_a = GenericRelation( + to='dcim.Cable', + content_type_field='termination_a_type', + object_id_field='termination_a_id' + ) + _cabled_as_b = GenericRelation( + to='dcim.Cable', + content_type_field='termination_b_type', + object_id_field='termination_b_id' + ) + class Meta: abstract = True diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index b37c34226..0bb6690b4 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -45,10 +45,12 @@ def update_connected_endpoints(instance, **kwargs): def nullify_connected_endpoints(instance, **kwargs): # Disassociate the Cable from its termination points - instance.termination_a.cable = None - instance.termination_a.save() - instance.termination_b.cable = None - instance.termination_b.save() + if instance.termination_a is not None: + instance.termination_a.cable = None + instance.termination_a.save() + if instance.termination_b is not None: + instance.termination_b.cable = None + instance.termination_b.save() # If this Cable was part of a complete path, tear it down endpoint_a, endpoint_b = instance.get_path_endpoints() diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 012c7d121..d2072812c 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -149,3 +149,54 @@ class RackTestCase(TestCase): face=None, ) self.assertTrue(pdu) + + +class CableTestCase(TestCase): + + def setUp(self): + + 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' + ) + devicerole = DeviceRole.objects.create( + name='Test Device Role 1', slug='test-device-role-1', color='ff0000' + ) + self.device1 = Device.objects.create( + device_type=devicetype, device_role=devicerole, name='TestDevice1', site=site + ) + self.device2 = Device.objects.create( + device_type=devicetype, device_role=devicerole, name='TestDevice2', site=site + ) + self.interface1 = Interface.objects.create(device=self.device1, name='eth0') + self.interface2 = Interface.objects.create(device=self.device2, name='eth0') + self.cable = Cable(termination_a=self.interface1, termination_b=self.interface2) + self.cable.save() + + def test_cable_creation(self): + """ + When a new Cable is created, it must be cached on either termination point. + """ + interface1 = Interface.objects.get(pk=self.interface1.pk) + self.assertEqual(self.cable.termination_a, interface1) + interface2 = Interface.objects.get(pk=self.interface2.pk) + self.assertEqual(self.cable.termination_b, interface2) + + def test_cable_deletion(self): + """ + When a Cable is deleted, the `cable` field on its termination points must be nullified. + """ + self.cable.delete() + interface1 = Interface.objects.get(pk=self.interface1.pk) + self.assertIsNone(interface1.cable) + interface2 = Interface.objects.get(pk=self.interface2.pk) + self.assertIsNone(interface2.cable) + + def test_cabletermination_deletion(self): + """ + When a CableTermination object is deleted, its attached Cable (if any) must also be deleted. + """ + self.interface1.delete() + cable = Cable.objects.filter(pk=self.cable.pk).first() + self.assertIsNone(cable)