mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-09 00:58:16 -06:00
Fixes #3782: Prevent power loops
This commit is contained in:
parent
b7e78028ce
commit
cce99f77ea
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user