mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Improved logic for recording cable path connection status
This commit is contained in:
parent
7dde370ee1
commit
a324638f1f
@ -237,7 +237,7 @@ class CircuitTermination(CableTermination):
|
|||||||
)
|
)
|
||||||
connection_status = models.NullBooleanField(
|
connection_status = models.NullBooleanField(
|
||||||
choices=CONNECTION_STATUS_CHOICES,
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
default=CONNECTION_STATUS_CONNECTED
|
blank=True
|
||||||
)
|
)
|
||||||
port_speed = models.PositiveIntegerField(
|
port_speed = models.PositiveIntegerField(
|
||||||
verbose_name='Port speed (Kbps)'
|
verbose_name='Port speed (Kbps)'
|
||||||
|
@ -88,7 +88,7 @@ class CableTermination(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def trace(self, position=1):
|
def trace(self, position=1, follow_circuits=False):
|
||||||
"""
|
"""
|
||||||
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
|
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
|
||||||
[
|
[
|
||||||
@ -97,7 +97,7 @@ class CableTermination(models.Model):
|
|||||||
(termination E, cable, termination F)
|
(termination E, cable, termination F)
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
def get_peer_port(termination, position=1):
|
def get_peer_port(termination, position=1, follow_circuits=False):
|
||||||
from circuits.models import CircuitTermination
|
from circuits.models import CircuitTermination
|
||||||
|
|
||||||
# Map a front port to its corresponding rear port
|
# Map a front port to its corresponding rear port
|
||||||
@ -117,7 +117,7 @@ class CableTermination(models.Model):
|
|||||||
return peer_port, 1
|
return peer_port, 1
|
||||||
|
|
||||||
# Follow a circuit to its other termination
|
# Follow a circuit to its other termination
|
||||||
elif isinstance(termination, CircuitTermination):
|
elif isinstance(termination, CircuitTermination) and follow_circuits:
|
||||||
peer_termination = termination.get_peer_termination()
|
peer_termination = termination.get_peer_termination()
|
||||||
if peer_termination is None:
|
if peer_termination is None:
|
||||||
return None, None
|
return None, None
|
||||||
@ -133,7 +133,7 @@ class CableTermination(models.Model):
|
|||||||
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
|
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
|
||||||
path = [(self, self.cable, far_end)]
|
path = [(self, self.cable, far_end)]
|
||||||
|
|
||||||
peer_port, position = get_peer_port(far_end, position)
|
peer_port, position = get_peer_port(far_end, position, follow_circuits)
|
||||||
if peer_port is None:
|
if peer_port is None:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@ -1704,7 +1704,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
connection_status = models.NullBooleanField(
|
connection_status = models.NullBooleanField(
|
||||||
choices=CONNECTION_STATUS_CHOICES,
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
default=CONNECTION_STATUS_CONNECTED
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
@ -1792,7 +1792,7 @@ class PowerPort(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
connection_status = models.NullBooleanField(
|
connection_status = models.NullBooleanField(
|
||||||
choices=CONNECTION_STATUS_CHOICES,
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
default=CONNECTION_STATUS_CONNECTED
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
@ -1897,7 +1897,7 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
connection_status = models.NullBooleanField(
|
connection_status = models.NullBooleanField(
|
||||||
choices=CONNECTION_STATUS_CHOICES,
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
default=CONNECTION_STATUS_CONNECTED
|
blank=True
|
||||||
)
|
)
|
||||||
lag = models.ForeignKey(
|
lag = models.ForeignKey(
|
||||||
to='self',
|
to='self',
|
||||||
@ -2554,7 +2554,7 @@ class Cable(ChangeLoggedModel):
|
|||||||
))
|
))
|
||||||
|
|
||||||
# Virtual interfaces cannot be connected
|
# Virtual interfaces cannot be connected
|
||||||
endpoint_a, endpoint_b = self.get_path_endpoints()
|
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
|
||||||
if (
|
if (
|
||||||
(
|
(
|
||||||
isinstance(endpoint_a, Interface) and
|
isinstance(endpoint_a, Interface) and
|
||||||
@ -2600,42 +2600,18 @@ class Cable(ChangeLoggedModel):
|
|||||||
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
|
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
|
||||||
None.
|
None.
|
||||||
"""
|
"""
|
||||||
def trace_cable(termination, position=1):
|
a_path = self.termination_a.trace()
|
||||||
|
b_path = self.termination_b.trace()
|
||||||
|
|
||||||
# Given a front port, follow the cable connected to the corresponding rear port/position
|
# Determine overall path status (connected or planned)
|
||||||
if isinstance(termination, FrontPort):
|
cables = [segment[1] for segment in a_path + b_path]
|
||||||
peer_port = termination.rear_port
|
if all(cables) and all([c.status for c in cables]):
|
||||||
position = termination.rear_port_position
|
path_status = CONNECTION_STATUS_CONNECTED
|
||||||
|
else:
|
||||||
|
path_status = CONNECTION_STATUS_PLANNED
|
||||||
|
|
||||||
# Given a rear port/position, follow the cable connected to the corresponding front port
|
# (A path end, B path end, connected/planned)
|
||||||
elif isinstance(termination, RearPort):
|
return a_path[-1][2], b_path[-1][2], path_status
|
||||||
if position not in range(1, termination.positions + 1):
|
|
||||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
|
||||||
termination, termination.positions, position
|
|
||||||
))
|
|
||||||
peer_port = FrontPort.objects.get(
|
|
||||||
rear_port=termination,
|
|
||||||
rear_port_position=position,
|
|
||||||
)
|
|
||||||
position = 1
|
|
||||||
|
|
||||||
# Termination is not a pass-through port, so we've reached the end of the path
|
|
||||||
else:
|
|
||||||
return termination
|
|
||||||
|
|
||||||
# Find the cable (if any) attached to the peer port
|
|
||||||
next_cable = peer_port.cable
|
|
||||||
|
|
||||||
# If no cable exists, return None
|
|
||||||
if next_cable is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
far_end = next_cable.termination_b if next_cable.termination_a == peer_port else next_cable.termination_a
|
|
||||||
|
|
||||||
# Return the far side termination of the cable
|
|
||||||
return trace_cable(far_end, position)
|
|
||||||
|
|
||||||
return trace_cable(self.termination_a), trace_cable(self.termination_b)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -23,26 +23,35 @@ def clear_virtualchassis_members(instance, **kwargs):
|
|||||||
|
|
||||||
@receiver(post_save, sender=Cable)
|
@receiver(post_save, sender=Cable)
|
||||||
def update_connected_endpoints(instance, **kwargs):
|
def update_connected_endpoints(instance, **kwargs):
|
||||||
|
"""
|
||||||
|
When a Cable is saved, check for and update its two connected endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
# Cache the Cable on its two termination points
|
# Cache the Cable on its two termination points
|
||||||
instance.termination_a.cable = instance
|
if instance.termination_a.cable != instance:
|
||||||
instance.termination_a.save()
|
instance.termination_a.cable = instance
|
||||||
instance.termination_b.cable = instance
|
instance.termination_a.save()
|
||||||
instance.termination_b.save()
|
if instance.termination_b.cable != instance:
|
||||||
|
instance.termination_b.cable = instance
|
||||||
|
instance.termination_b.save()
|
||||||
|
|
||||||
# Check if this Cable has formed a complete path. If so, update both endpoints.
|
# Check if this Cable has formed a complete path. If so, update both endpoints.
|
||||||
endpoint_a, endpoint_b = instance.get_path_endpoints()
|
endpoint_a, endpoint_b, path_status = instance.get_path_endpoints()
|
||||||
if endpoint_a is not None and endpoint_b is not None:
|
if endpoint_a is not None and endpoint_b is not None:
|
||||||
endpoint_a.connected_endpoint = endpoint_b
|
endpoint_a.connected_endpoint = endpoint_b
|
||||||
endpoint_a.connection_status = True
|
endpoint_a.connection_status = path_status
|
||||||
endpoint_a.save()
|
endpoint_a.save()
|
||||||
endpoint_b.connected_endpoint = endpoint_a
|
endpoint_b.connected_endpoint = endpoint_a
|
||||||
endpoint_b.connection_status = True
|
endpoint_b.connection_status = path_status
|
||||||
endpoint_b.save()
|
endpoint_b.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=Cable)
|
@receiver(pre_delete, sender=Cable)
|
||||||
def nullify_connected_endpoints(instance, **kwargs):
|
def nullify_connected_endpoints(instance, **kwargs):
|
||||||
|
"""
|
||||||
|
When a Cable is deleted, check for and update its two connected endpoints
|
||||||
|
"""
|
||||||
|
endpoint_a, endpoint_b, _ = instance.get_path_endpoints()
|
||||||
|
|
||||||
# Disassociate the Cable from its termination points
|
# Disassociate the Cable from its termination points
|
||||||
if instance.termination_a is not None:
|
if instance.termination_a is not None:
|
||||||
@ -53,7 +62,6 @@ def nullify_connected_endpoints(instance, **kwargs):
|
|||||||
instance.termination_b.save()
|
instance.termination_b.save()
|
||||||
|
|
||||||
# If this Cable was part of a complete path, tear it down
|
# If this Cable was part of a complete path, tear it down
|
||||||
endpoint_a, endpoint_b = instance.get_path_endpoints()
|
|
||||||
if endpoint_a is not None and endpoint_b is not None:
|
if endpoint_a is not None and endpoint_b is not None:
|
||||||
endpoint_a.connected_endpoint = None
|
endpoint_a.connected_endpoint = None
|
||||||
endpoint_a.connection_status = None
|
endpoint_a.connection_status = None
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
|
|
||||||
|
|
||||||
@ -252,3 +253,95 @@ class CableTestCase(TestCase):
|
|||||||
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
|
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cable.clean()
|
cable.clean()
|
||||||
|
|
||||||
|
|
||||||
|
class CablePathTestCase(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='Test Device 1', site=site
|
||||||
|
)
|
||||||
|
self.device2 = Device.objects.create(
|
||||||
|
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=site
|
||||||
|
)
|
||||||
|
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
||||||
|
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||||
|
self.panel1 = Device.objects.create(
|
||||||
|
device_type=devicetype, device_role=devicerole, name='Test Panel 1', site=site
|
||||||
|
)
|
||||||
|
self.panel2 = Device.objects.create(
|
||||||
|
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
|
||||||
|
)
|
||||||
|
self.rear_port1 = RearPort.objects.create(
|
||||||
|
device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C
|
||||||
|
)
|
||||||
|
self.front_port1 = FrontPort.objects.create(
|
||||||
|
device=self.panel1, name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=self.rear_port1
|
||||||
|
)
|
||||||
|
self.rear_port2 = RearPort.objects.create(
|
||||||
|
device=self.panel2, name='Rear Port 2', type=PORT_TYPE_8P8C
|
||||||
|
)
|
||||||
|
self.front_port2 = FrontPort.objects.create(
|
||||||
|
device=self.panel2, name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=self.rear_port2
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_path_completion(self):
|
||||||
|
|
||||||
|
# First segment
|
||||||
|
cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1)
|
||||||
|
cable1.save()
|
||||||
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
|
self.assertIsNone(interface1.connected_endpoint)
|
||||||
|
self.assertIsNone(interface1.connection_status)
|
||||||
|
|
||||||
|
# Second segment
|
||||||
|
cable2 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2)
|
||||||
|
cable2.save()
|
||||||
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
|
self.assertIsNone(interface1.connected_endpoint)
|
||||||
|
self.assertIsNone(interface1.connection_status)
|
||||||
|
|
||||||
|
# Third segment
|
||||||
|
cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2, status=CONNECTION_STATUS_PLANNED)
|
||||||
|
cable3.save()
|
||||||
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
|
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||||
|
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_PLANNED)
|
||||||
|
|
||||||
|
# Switch third segment from planned to connected
|
||||||
|
cable3.status = CONNECTION_STATUS_CONNECTED
|
||||||
|
cable3.save()
|
||||||
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
|
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||||
|
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED)
|
||||||
|
|
||||||
|
def test_path_teardown(self):
|
||||||
|
|
||||||
|
# Build the path
|
||||||
|
cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1)
|
||||||
|
cable1.save()
|
||||||
|
cable2 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2)
|
||||||
|
cable2.save()
|
||||||
|
cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2)
|
||||||
|
cable3.save()
|
||||||
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
|
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||||
|
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED)
|
||||||
|
|
||||||
|
# Remove a cable
|
||||||
|
cable2.delete()
|
||||||
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
|
self.assertIsNone(interface1.connected_endpoint)
|
||||||
|
self.assertIsNone(interface1.connection_status)
|
||||||
|
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||||
|
self.assertIsNone(interface2.connected_endpoint)
|
||||||
|
self.assertIsNone(interface2.connection_status)
|
||||||
|
@ -1625,7 +1625,7 @@ class CableTraceView(View):
|
|||||||
|
|
||||||
return render(request, 'dcim/cable_trace.html', {
|
return render(request, 'dcim/cable_trace.html', {
|
||||||
'obj': obj,
|
'obj': obj,
|
||||||
'trace': obj.trace(),
|
'trace': obj.trace(follow_circuits=True),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user