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
|
## 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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user