Fixes #3782: Prevent power loops

This commit is contained in:
Saria Hajjar 2020-01-11 23:23:26 +00:00
parent b7e78028ce
commit cce99f77ea
3 changed files with 68 additions and 0 deletions

View File

@ -19,6 +19,7 @@
## Bug Fixes ## Bug Fixes
* [#3589](https://github.com/netbox-community/netbox/issues/3589) - Fix validation on tagged VLANs of an interface * [#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 * [#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 * [#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 * [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses

View File

@ -3099,6 +3099,43 @@ class Cable(ChangeLoggedModel):
if self.termination_a == self.termination_b: if self.termination_a == self.termination_b:
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type)) 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 # A front port cannot be connected to its corresponding rear port
if ( if (
type_a in ['frontport', 'rearport'] and type_a in ['frontport', 'rearport'] and

View File

@ -300,12 +300,23 @@ class CableTestCase(TestCase):
self.device2 = Device.objects.create( self.device2 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='TestDevice2', site=site 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.interface1 = Interface.objects.create(device=self.device1, name='eth0')
self.interface2 = Interface.objects.create(device=self.device2, 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 = Cable(termination_a=self.interface1, termination_b=self.interface2)
self.cable.save() self.cable.save()
self.power_port1 = PowerPort.objects.create(device=self.device2, name='psu1') 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( self.patch_pannel = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site
) )
@ -358,6 +369,25 @@ class CableTestCase(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() 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): def test_cable_front_port_cannot_connect_to_corresponding_rear_port(self):
""" """
A cable cannot connect a front port to its corresponding rear port A cable cannot connect a front port to its corresponding rear port