diff --git a/netbox/dcim/exceptions.py b/netbox/dcim/exceptions.py index e788c9b5f..18e42318b 100644 --- a/netbox/dcim/exceptions.py +++ b/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 diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 144bcc28a..2c1940296 100644 --- a/netbox/dcim/models/__init__.py +++ b/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 diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 3e615b283..58af8bc91 100644 --- a/netbox/dcim/models/device_components.py +++ b/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 diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 4ea09655f..2b922ebb5 100644 --- a/netbox/dcim/signals.py +++ b/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()