From e9da84f91aaf80aced3b369d4205165572a62dc5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 14:54:16 -0400 Subject: [PATCH] Replace legacy trace() method --- netbox/circuits/urls.py | 4 +- netbox/dcim/api/views.py | 18 +-- netbox/dcim/models/device_components.py | 140 ++---------------------- netbox/dcim/urls.py | 14 +-- netbox/dcim/utils.py | 3 +- netbox/dcim/views.py | 8 +- netbox/templates/dcim/cable_trace.html | 55 +--------- 7 files changed, 38 insertions(+), 204 deletions(-) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 86ea55fa8..d757fd90d 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from dcim.views import CableCreateView, CableTraceView +from dcim.views import CableCreateView, PathTraceView from extras.views import ObjectChangeLogView from . import views from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -45,6 +45,6 @@ urlpatterns = [ path('circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), path('circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), path('circuit-terminations//connect//', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), - path('circuit-terminations//trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), + path('circuit-terminations//trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 0583d4e56..edbdfb7be 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -45,7 +45,7 @@ class DCIMRootView(APIRootView): # Mixins -class CableTraceMixin(object): +class PathEndpointMixin(object): @action(detail=True, url_path='trace') def trace(self, request, pk): @@ -57,7 +57,7 @@ class CableTraceMixin(object): # Initialize the path array path = [] - for near_end, cable, far_end in obj.trace()[0]: + for near_end, cable, far_end in obj.trace(): # Serialize each object serializer_a = get_serializer_for_model(near_end, prefix='Nested') @@ -469,19 +469,19 @@ class DeviceViewSet(CustomFieldModelViewSet): # Device components # -class ConsolePortViewSet(CableTraceMixin, ModelViewSet): +class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.ConsolePortSerializer filterset_class = filters.ConsolePortFilterSet -class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet): +class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.ConsoleServerPortSerializer filterset_class = filters.ConsoleServerPortFilterSet -class PowerPortViewSet(CableTraceMixin, ModelViewSet): +class PowerPortViewSet(PathEndpointMixin, ModelViewSet): queryset = PowerPort.objects.prefetch_related( 'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags' ) @@ -489,13 +489,13 @@ class PowerPortViewSet(CableTraceMixin, ModelViewSet): filterset_class = filters.PowerPortFilterSet -class PowerOutletViewSet(CableTraceMixin, ModelViewSet): +class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.PowerOutletSerializer filterset_class = filters.PowerOutletFilterSet -class InterfaceViewSet(CableTraceMixin, ModelViewSet): +class InterfaceViewSet(PathEndpointMixin, ModelViewSet): queryset = Interface.objects.prefetch_related( 'device', '_connected_interface', '_connected_circuittermination', 'cable', 'ip_addresses', 'tags' ).filter( @@ -505,13 +505,13 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): filterset_class = filters.InterfaceFilterSet -class FrontPortViewSet(CableTraceMixin, ModelViewSet): +class FrontPortViewSet(ModelViewSet): queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') serializer_class = serializers.FrontPortSerializer filterset_class = filters.FrontPortFilterSet -class RearPortViewSet(CableTraceMixin, ModelViewSet): +class RearPortViewSet(ModelViewSet): queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') serializer_class = serializers.RearPortSerializer filterset_class = filters.RearPortFilterSet diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 6bf2ac77a..b714c662a 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1,5 +1,3 @@ -import logging - from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator @@ -11,8 +9,8 @@ from taggit.managers import TaggableManager from dcim.choices import * from dcim.constants import * -from dcim.exceptions import CableTraceSplit from dcim.fields import MACAddressField +from dcim.utils import path_node_to_object from extras.models import ObjectChange, TaggedItem from extras.utils import extras_features from utilities.fields import NaturalOrderingField @@ -117,114 +115,6 @@ class CableTermination(models.Model): class Meta: abstract = True - def trace(self): - """ - Return three items: the traceable portion of a cable path, the termination points where it splits (if any), and - the remaining positions on the position stack (if any). Splits occur when the trace is initiated from a midpoint - along a path which traverses a RearPort. In cases where the originating endpoint is unknown, it is not possible - to know which corresponding FrontPort to follow. Remaining positions occur when tracing a path that traverses - a FrontPort without traversing a RearPort again. - - The path is a list representing a complete cable path, with each individual segment represented as a - three-tuple: - - [ - (termination A, cable, termination B), - (termination C, cable, termination D), - (termination E, cable, termination F) - ] - """ - endpoint = self - path = [] - position_stack = [] - - def get_peer_port(termination): - from circuits.models import CircuitTermination - - # Map a front port to its corresponding rear port - if isinstance(termination, FrontPort): - # Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance - peer_port = RearPort.objects.get(pk=termination.rear_port.pk) - - # Don't use the stack for RearPorts with a single position. Only remember the position at - # many-to-one points so we can select the correct FrontPort when we reach the corresponding - # one-to-many point. - if peer_port.positions > 1: - position_stack.append(termination) - - return peer_port - - # Map a rear port/position to its corresponding front port - elif isinstance(termination, RearPort): - if termination.positions > 1: - # Can't map to a FrontPort without a position if there are multiple options - if not position_stack: - raise CableTraceSplit(termination) - - front_port = position_stack.pop() - position = front_port.rear_port_position - - # Validate the position - if position not in range(1, termination.positions + 1): - raise Exception("Invalid position for {} ({} positions): {})".format( - termination, termination.positions, position - )) - else: - # Don't use the stack for RearPorts with a single position. The only possible position is 1. - position = 1 - - try: - peer_port = FrontPort.objects.get( - rear_port=termination, - rear_port_position=position, - ) - return peer_port - except ObjectDoesNotExist: - return None - - # Follow a circuit to its other termination - elif isinstance(termination, CircuitTermination): - peer_termination = termination.get_peer_termination() - if peer_termination is None: - return None - return peer_termination - - # Termination is not a pass-through port - else: - return None - - logger = logging.getLogger('netbox.dcim.cable.trace') - logger.debug("Tracing cable from {} {}".format(self.parent, self)) - - while endpoint is not None: - - # No cable connected; nothing to trace - if not endpoint.cable: - path.append((endpoint, None, None)) - logger.debug("No cable connected") - return path, None, position_stack - - # Check for loops - if endpoint.cable in [segment[1] for segment in path]: - logger.debug("Loop detected!") - return path, None, position_stack - - # Record the current segment in the path - far_end = endpoint.get_cable_peer() - path.append((endpoint, endpoint.cable, far_end)) - logger.debug("{}[{}] --- Cable {} ---> {}[{}]".format( - endpoint.parent, endpoint, endpoint.cable.pk, far_end.parent, far_end - )) - - # Get the peer port of the far end termination - try: - endpoint = get_peer_port(far_end) - except CableTraceSplit as e: - return path, e.termination.frontports.all(), position_stack - - if endpoint is None: - return path, None, position_stack - def get_cable_peer(self): if self.cable is None: return None @@ -233,23 +123,6 @@ 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 - path, split_ends, position_stack = self.trace() - endpoint = path[-1][2] - if split_ends is not None: - for termination in split_ends: - endpoints.extend(termination.get_path_endpoints()) - elif endpoint is not None: - endpoints.append(endpoint) - - return endpoints - class PathEndpoint(models.Model): """ @@ -265,6 +138,17 @@ class PathEndpoint(models.Model): class Meta: abstract = True + def trace(self): + if self.path is None: + 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)} elements in path" + + # Return the path as a list of three-tuples (A termination, cable, B termination) + return list(zip(*[iter(path)] * 3)) + @property def path(self): """ diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index aa0453baf..ba58cf67e 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -207,7 +207,7 @@ urlpatterns = [ path('console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path('console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path('console-ports//changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}), - path('console-ports//trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), + path('console-ports//trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), path('console-ports//connect//', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), @@ -223,7 +223,7 @@ urlpatterns = [ path('console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path('console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path('console-server-ports//changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}), - path('console-server-ports//trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), + path('console-server-ports//trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), path('console-server-ports//connect//', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), @@ -239,7 +239,7 @@ urlpatterns = [ path('power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path('power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path('power-ports//changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}), - path('power-ports//trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), + path('power-ports//trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), path('power-ports//connect//', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), @@ -255,7 +255,7 @@ urlpatterns = [ path('power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path('power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path('power-outlets//changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}), - path('power-outlets//trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), + path('power-outlets//trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), path('power-outlets//connect//', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), @@ -271,7 +271,7 @@ urlpatterns = [ path('interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path('interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), path('interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), - path('interfaces//trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), + path('interfaces//trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), path('interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), @@ -287,7 +287,7 @@ urlpatterns = [ path('front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path('front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path('front-ports//changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}), - path('front-ports//trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), + path('front-ports//trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), path('front-ports//connect//', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), @@ -303,7 +303,7 @@ urlpatterns = [ path('rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path('rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path('rear-ports//changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}), - path('rear-ports//trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), + path('rear-ports//trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path('rear-ports//connect//', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 16d0753ba..4ef902dc7 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -2,7 +2,6 @@ from django.contrib.contenttypes.models import ContentType from .choices import CableStatusChoices from .exceptions import CableTraceSplit -from .models import FrontPort, RearPort def object_to_path_node(obj): @@ -20,6 +19,8 @@ def path_node_to_object(repr): def trace_path(node): + from .models import FrontPort, RearPort + destination = None path = [] position_stack = [] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 96e6615e8..ac30461b0 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1961,9 +1961,9 @@ class CableView(ObjectView): }) -class CableTraceView(ObjectView): +class PathTraceView(ObjectView): """ - Trace a cable path beginning from the given termination. + Trace a cable path beginning from the given path endpoint (origin). """ additional_permissions = ['dcim.view_cable'] @@ -1976,7 +1976,7 @@ class CableTraceView(ObjectView): def get(self, request, pk): obj = get_object_or_404(self.queryset, pk=pk) - path, split_ends, position_stack = obj.trace() + path = obj.trace() total_length = sum( [entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length] ) @@ -1984,8 +1984,6 @@ class CableTraceView(ObjectView): return render(request, 'dcim/cable_trace.html', { 'obj': obj, 'trace': path, - 'split_ends': split_ends, - 'position_stack': position_stack, 'total_length': total_length, }) diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index df484609a..2f54f94ee 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -51,57 +51,8 @@
{% endfor %}
- {% if split_ends %} -
-
-
- Trace Split -
-
- There are multiple possible paths from this point. Select a port to continue. -
-
-
- - - - - - - - - - {% for termination in split_ends %} - - - - - - - {% endfor %} -
PortConnectedTypeDescription
{{ termination }} - {% if termination.cable %} - - {% else %} - - {% endif %} - {{ termination.get_type_display }}{{ termination.description|placeholder }}
-
-
- {% elif position_stack %} -
-

- {% with last_position=position_stack|last %} - Trace completed, but there is no Front Port corresponding to - {{ last_position.device }} {{ last_position }}.
- Therefore no end-to-end connection can be established. - {% endwith %} -

-
- {% else %} -
-

Trace completed!

-
- {% endif %} +
+

Trace completed!

+
{% endblock %}