Explorar el Código

Refactor cable tracing logic

Jeremy Stretch hace 5 años
padre
commit
5205c4963f

+ 9 - 0
netbox/dcim/exceptions.py

@@ -3,3 +3,12 @@ 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

+ 0 - 23
netbox/dcim/models/__init__.py

@@ -2205,26 +2205,3 @@ class Cable(ChangeLoggedModel):
         if self.termination_a is None:
             return
         return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
-
-    def get_path_endpoints(self):
-        """
-        Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
-        None.
-        """
-        a_path = self.termination_b.trace()
-        b_path = self.termination_a.trace()
-
-        # Determine overall path status (connected or planned)
-        if self.status == CableStatusChoices.STATUS_CONNECTED:
-            path_status = True
-            for segment in a_path[1:] + b_path[1:]:
-                if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
-                    path_status = False
-                    break
-        else:
-            path_status = False
-
-        a_endpoint = a_path[-1][2]
-        b_endpoint = b_path[-1][2]
-
-        return a_endpoint, b_endpoint, path_status

+ 21 - 4
netbox/dcim/models/device_components.py

@@ -10,6 +10,7 @@ from taggit.managers import TaggableManager
 
 from dcim.choices import *
 from dcim.constants import *
+from dcim.exceptions import CableTraceSplit
 from dcim.fields import MACAddressField
 from extras.models import ObjectChange, TaggedItem
 from extras.utils import extras_features
@@ -117,10 +118,7 @@ class CableTermination(models.Model):
 
                 # Can't map to a FrontPort without a position
                 if not position_stack:
-                    # TODO: This behavior is broken. We need a mechanism by which to return all FrontPorts mapped
-                    # to a given RearPort so that we can update end-to-end paths when a cable is created/deleted.
-                    # For now, we're maintaining the current behavior of tracing only to the first FrontPort.
-                    position_stack.append(1)
+                    raise CableTraceSplit(termination)
 
                 position = position_stack.pop()
 
@@ -186,6 +184,25 @@ class CableTermination(models.Model):
         if self._cabled_as_b.exists():
             return self.cable.termination_a
 
+    def get_path_endpoints(self):
+        """
+        Return all endpoints of paths which traverse this object.
+        """
+        endpoints = []
+
+        # Get the far end of the last path segment
+        try:
+            endpoint = self.trace()[-1][2]
+            if endpoint is not None:
+                endpoints.append(endpoint)
+
+        # We've hit a RearPort mapped to multiple FrontPorts. Recurse to trace each of them individually.
+        except CableTraceSplit as e:
+            for frontport in e.termination.frontports.all():
+                endpoints.extend(frontport.get_path_endpoints())
+
+        return endpoints
+
 
 #
 # Console ports

+ 31 - 20
netbox/dcim/signals.py

@@ -3,6 +3,7 @@ import logging
 from django.db.models.signals import post_save, pre_delete
 from django.dispatch import receiver
 
+from .choices import CableStatusChoices
 from .models import Cable, Device, VirtualChassis
 
 
@@ -48,16 +49,28 @@ def update_connected_endpoints(instance, **kwargs):
         instance.termination_b.cable = instance
         instance.termination_b.save()
 
-    # Check if this Cable has formed a complete path. If so, update both endpoints.
-    endpoint_a, endpoint_b, path_status = instance.get_path_endpoints()
-    if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
-        logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
-        endpoint_a.connected_endpoint = endpoint_b
-        endpoint_a.connection_status = path_status
-        endpoint_a.save()
-        endpoint_b.connected_endpoint = endpoint_a
-        endpoint_b.connection_status = path_status
-        endpoint_b.save()
+    # Update any endpoints for this Cable.
+    endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
+    for endpoint in endpoints:
+        path = endpoint.trace()
+        # Determine overall path status (connected or planned)
+        path_status = True
+        for segment in path:
+            if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
+                path_status = False
+                break
+
+        endpoint_a = path[0][0]
+        endpoint_b = path[-1][2]
+
+        if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
+            logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
+            endpoint_a.connected_endpoint = endpoint_b
+            endpoint_a.connection_status = path_status
+            endpoint_a.save()
+            endpoint_b.connected_endpoint = endpoint_a
+            endpoint_b.connection_status = path_status
+            endpoint_b.save()
 
 
 @receiver(pre_delete, sender=Cable)
@@ -67,7 +80,7 @@ def nullify_connected_endpoints(instance, **kwargs):
     """
     logger = logging.getLogger('netbox.dcim.cable')
 
-    endpoint_a, endpoint_b, _ = instance.get_path_endpoints()
+    endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
 
     # Disassociate the Cable from its termination points
     if instance.termination_a is not None:
@@ -79,12 +92,10 @@ def nullify_connected_endpoints(instance, **kwargs):
         instance.termination_b.cable = None
         instance.termination_b.save()
 
-    # If this Cable was part of a complete path, tear it down
-    if hasattr(endpoint_a, 'connected_endpoint') and hasattr(endpoint_b, 'connected_endpoint'):
-        logger.debug("Tearing down path ({} <---> {})".format(endpoint_a, endpoint_b))
-        endpoint_a.connected_endpoint = None
-        endpoint_a.connection_status = None
-        endpoint_a.save()
-        endpoint_b.connected_endpoint = None
-        endpoint_b.connection_status = None
-        endpoint_b.save()
+    # If this Cable was part of any complete end-to-end paths, tear them down.
+    for endpoint in endpoints:
+        logger.debug(f"Removing path information for {endpoint}")
+        if hasattr(endpoint, 'connected_endpoint'):
+            endpoint.connected_endpoint = None
+            endpoint.connection_status = None
+            endpoint.save()