From cce99f77ea31e14a680f9b774de8751a04417eb9 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Sat, 11 Jan 2020 23:23:26 +0000 Subject: [PATCH] Fixes #3782: Prevent power loops --- docs/release-notes/version-2.6.md | 1 + netbox/dcim/models.py | 37 +++++++++++++++++++++++++++++++ netbox/dcim/tests/test_models.py | 30 +++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 88cd9c120..e7eca0fdd 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -19,6 +19,7 @@ ## Bug Fixes * [#3589](https://github.com/netbox-community/netbox/issues/3589) - Fix validation on tagged VLANs of an interface +* [#3782](https://github.com/netbox-community/netbox/issues/3782) - Prevent power loops * [#3849](https://github.com/netbox-community/netbox/issues/3849) - Fix ordering of models when dumping data to JSON * [#3853](https://github.com/netbox-community/netbox/issues/3853) - Fix device role link on config context view * [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 69c3c3475..ace68cdaf 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -3099,6 +3099,43 @@ class Cable(ChangeLoggedModel): if self.termination_a == self.termination_b: raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type)) + # A power loop is prohibited. Power feeds cannot create loops as they're downstream-only. Cannot use power + # ports as one end may not have a device, like a power feed. This leaves power outlets which are always a part + # of a loop since they exist on a device and connect to another. + if 'poweroutlet' in [type_a, type_b]: + poweroutlet_type = ContentType.objects.get_for_model(PowerOutlet) + + # Using PK as the use of `.values` on the queryset will return IDs + destination = self.termination_b.device.pk + nodes = None + next_nodes = set([self.termination_a.device.pk]) + + # Pseudo-implementation of the Bellman-Ford algorithm + while nodes != next_nodes: + nodes = next_nodes + + # Destination found in the nodes; this cable would create a loop + if destination in nodes: + raise ValidationError('Creating this cable will introduce a power loop') + + # All power-outlet cables. Only the devices at each end of the cable are needed + poweroutlet_cables = Cable.objects.filter( + Q(termination_a_type=poweroutlet_type) | Q(termination_b_type=poweroutlet_type) + ).values('_termination_a_device', '_termination_b_device') + + # The next ring in the search will include the nodes of the previous ring. Otherwise, an existing + # loop (i.e. before this was implemented) will ping-pong the search forever as current and next rings + # will almost never match in order to break the loop (the exception is when the devices causing the + # loop are connected to the same exact set of devices). + next_nodes = nodes.copy() + + # Node is temination a + for cable in poweroutlet_cables.filter(_termination_a_device__in=nodes): + next_nodes.add(cable['_termination_b_device']) + # Node is termination b + for cable in poweroutlet_cables.filter(_termination_b_device__in=nodes): + next_nodes.add(cable['_termination_a_device']) + # A front port cannot be connected to its corresponding rear port if ( type_a in ['frontport', 'rearport'] and diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 2b5bed283..5f1327c02 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -300,12 +300,23 @@ class CableTestCase(TestCase): self.device2 = Device.objects.create( device_type=devicetype, device_role=devicerole, name='TestDevice2', site=site ) + self.device3 = Device.objects.create( + device_type=devicetype, device_role=devicerole, name='TestDevice3', 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() self.power_port1 = PowerPort.objects.create(device=self.device2, name='psu1') + self.power_port11 = PowerPort.objects.create(device=self.device1, name='psu11') + self.power_port21 = PowerPort.objects.create(device=self.device2, name='psu21') + self.power_port31 = PowerPort.objects.create(device=self.device3, name='psu31') + self.power_port32 = PowerPort.objects.create(device=self.device3, name='psu32') + self.power_outlet11 = PowerOutlet.objects.create(device=self.device1, name='outlet11') + self.power_outlet21 = PowerOutlet.objects.create(device=self.device2, name='outlet21') + self.power_outlet22 = PowerOutlet.objects.create(device=self.device2, name='outlet22') + self.power_outlet31 = PowerOutlet.objects.create(device=self.device3, name='outlet31') self.patch_pannel = Device.objects.create( device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site ) @@ -358,6 +369,25 @@ class CableTestCase(TestCase): with self.assertRaises(ValidationError): cable.clean() + def test_cable_cannot_create_power_loop(self): + """ + A power loop is prohibited, be it direct (device to itself) or indirect (spanning multiple devices) + """ + # Direct loop + with self.assertRaises(ValidationError): + Cable(termination_a=self.power_outlet11, termination_b=self.power_port11).clean() + + # Intentionally reversing the type on the terminations to ensure queryset functionality + Cable.objects.create(termination_a=self.power_outlet11, termination_b=self.power_port21) + Cable.objects.create(termination_b=self.power_outlet21, termination_a=self.power_port31) + + # Redundant connections are acceptable + Cable.objects.create(termination_a=self.power_outlet22, termination_b=self.power_port32) + + # Indirect loop + with self.assertRaises(ValidationError): + Cable(termination_a=self.power_outlet31, termination_b=self.power_port11).clean() + def test_cable_front_port_cannot_connect_to_corresponding_rear_port(self): """ A cable cannot connect a front port to its corresponding rear port