Răsfoiți Sursa

Handle traces which split at a RearPort

Jeremy Stretch 5 ani în urmă
părinte
comite
0c5efa243d

+ 0 - 9
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

+ 5 - 3
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))

+ 57 - 0
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]

+ 2 - 3
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:

+ 18 - 14
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 %}
+                    <h3 class="text-danger text-center">Split Paths!</h3>
+                    {# TODO: Present the user with successive paths to choose from #}
                 {% endif %}
 
                 {# Cable #}
-                <div class="row">
-                    {% if cable %}
+                {% if cable %}
+                    <div class="row">
                         {% include 'dcim/trace/cable.html' %}
-                    {% else %}
-                        <h4>No cable</h4>
-                    {% endif %}
-                </div>
+                    </div>
+                {% endif %}
 
                 {# Far end #}
                 {% if far_end.device %}
@@ -45,15 +46,18 @@
                     {% endif %}
                 {% endif %}
 
+                {% if forloop.last and far_end %}
+                    <div class="row">
+                        <div class="text-center">
+                            <h3 class="text-success text-center">Trace completed!</h3>
+                            {% if total_length %}
+                                <h5>Total length: {{ total_length|floatformat:"-2" }} Meters<h5>
+                            {% endif %}
+                        </div>
+                    </div>
+                {% endif %}
+
             {% endfor %}
-            <div class="row">
-                <div class="text-center">
-                    <h3 class="text-success text-center">Trace completed!</h3>
-                    {% if total_length %}
-                        <h5>Total length: {{ total_length|floatformat:"-2" }} Meters<h5>
-                    {% endif %}
-                </div>
-            </div>
 
         </div>
         <div class="col-md-7 col-sm-12">