From 0c5efa243defd2695c6b2e884bbe4d48c3fa69fb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 13:45:47 -0400 Subject: [PATCH] Handle traces which split at a RearPort --- netbox/dcim/exceptions.py | 9 ---- netbox/dcim/models/device_components.py | 8 ++-- netbox/dcim/tests/test_cablepaths.py | 57 +++++++++++++++++++++++++ netbox/dcim/utils.py | 5 +-- netbox/templates/dcim/cable_trace.html | 32 ++++++++------ 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/netbox/dcim/exceptions.py b/netbox/dcim/exceptions.py index 18e42318b..e788c9b5f 100644 --- a/netbox/dcim/exceptions.py +++ b/netbox/dcim/exceptions.py @@ -3,12 +3,3 @@ class LoopDetected(Exception): A loop has been detected while tracing a cable path. """ pass - - -class CableTraceSplit(Exception): - """ - A cable trace cannot be completed because a RearPort maps to multiple FrontPorts and - we don't know which one to follow. - """ - def __init__(self, termination, *args, **kwargs): - self.termination = termination diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 72bd453c5..2ea9b88b6 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -172,9 +172,11 @@ class PathEndpoint(models.Model): return [] # Construct the complete path - path = [self, *[path_node_to_object(obj) for obj in self._path.path], self._path.destination] - assert not len(path) % 3,\ - f"Invalid path length for CablePath #{self.pk}: {len(self._path.path)} elements in path" + path = [self, *[path_node_to_object(obj) for obj in self._path.path]] + while (len(path) + 1) % 3: + # Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort) + path.append(None) + path.append(self._path.destination) # Return the path as a list of three-tuples (A termination, cable, B termination) return list(zip(*[iter(path)] * 3)) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index cfe63929d..95a8e0d30 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -721,6 +721,63 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) + def test_206_unidirectional_split_paths(self): + """ + [IF1] --C1-- [RP1] [FP1:1] --C2-- [IF2] + [FP1:2] --C3-- [IF3] + """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.interface3.refresh_from_db() + + # Create cables 1 + cable1 = Cable(termination_a=self.interface1, termination_b=self.rear_port1) + cable1.save() + self.assertPathExists( + origin=self.interface1, + destination=None, + path=(cable1, self.rear_port1), + is_active=False + ) + self.assertEqual(CablePath.objects.count(), 1) + + # Create cables 2-3 + cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_1) + cable2.save() + cable3 = Cable(termination_a=self.interface3, termination_b=self.front_port1_2) + cable3.save() + self.assertPathExists( + origin=self.interface2, + destination=self.interface1, + path=(cable2, self.front_port1_1, self.rear_port1, cable1), + is_active=True + ) + self.assertPathExists( + origin=self.interface3, + destination=self.interface1, + path=(cable3, self.front_port1_2, self.rear_port1, cable1), + is_active=True + ) + self.assertEqual(CablePath.objects.count(), 3) + + # Delete cable 1 + cable1.delete() + + # Check that the partial path was deleted and the two complete paths are now partial + self.assertPathExists( + origin=self.interface2, + destination=None, + path=(cable2, self.front_port1_1, self.rear_port1), + is_active=False + ) + self.assertPathExists( + origin=self.interface3, + destination=None, + path=(cable3, self.front_port1_2, self.rear_port1), + is_active=False + ) + self.assertEqual(CablePath.objects.count(), 2) + def test_301_create_path_via_existing_cable(self): """ [IF1] --C1-- [FP5] [RP5] --C2-- [RP6] [FP6] --C3-- [IF2] diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 52b0a4232..b82dd58d2 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -1,7 +1,6 @@ from django.contrib.contenttypes.models import ContentType from .choices import CableStatusChoices -from .exceptions import CableTraceSplit def compile_path_node(ct_id, object_id): @@ -69,8 +68,8 @@ def trace_path(node): node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position) path.append(object_to_path_node(node)) else: - # No position indicated: path has split (probably invalid?) - raise CableTraceSplit(peer_termination) + # No position indicated: path has split, so we stop at the RearPort + break # Anything else marks the end of the path else: diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 7df39caca..a328f2052 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -19,16 +19,17 @@ {% elif near_end.circuit %} {% include 'dcim/trace/circuit.html' with circuit=near_end.circuit %} {% include 'dcim/trace/termination.html' with termination=near_end %} + {% else %} +

Split Paths!

+ {# TODO: Present the user with successive paths to choose from #} {% endif %} {# Cable #} -
- {% if cable %} + {% if cable %} +
{% include 'dcim/trace/cable.html' %} - {% else %} -

No cable

- {% endif %} -
+
+ {% endif %} {# Far end #} {% if far_end.device %} @@ -45,15 +46,18 @@ {% endif %} {% endif %} + {% if forloop.last and far_end %} +
+
+

Trace completed!

+ {% if total_length %} +
Total length: {{ total_length|floatformat:"-2" }} Meters
+ {% endif %} +
+
+ {% endif %} + {% endfor %} -
-
-

Trace completed!

- {% if total_length %} -
Total length: {{ total_length|floatformat:"-2" }} Meters
- {% endif %} -
-