Jeremy Stretch 5 лет назад
Родитель
Сommit
e9da84f91a

+ 2 - 2
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/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
     path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
     path('circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
-    path('circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
+    path('circuit-terminations/<int:pk>/trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
 
 ]

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

+ 12 - 128
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):
         """

+ 7 - 7
netbox/dcim/urls.py

@@ -207,7 +207,7 @@ urlpatterns = [
     path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
     path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
     path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
-    path('console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
+    path('console-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
     path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', 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/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
     path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
     path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
-    path('console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
+    path('console-server-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
     path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', 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/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
     path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
     path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
-    path('power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
+    path('power-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
     path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', 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/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
     path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
     path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
-    path('power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
+    path('power-outlets/<int:pk>/trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
     path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', 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/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
     path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
     path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
-    path('interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
+    path('interfaces/<int:pk>/trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
     path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', 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/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
     path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
     path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
-    path('front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
+    path('front-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
     path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', 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/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
     path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
     path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
-    path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
+    path('rear-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
     path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', 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'),
 

+ 2 - 1
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 = []

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

+ 3 - 52
netbox/templates/dcim/cable_trace.html

@@ -51,57 +51,8 @@
         <hr />
     {% endfor %}
     <div class="row">
-        {% if split_ends %}
-            <div class="col-md-7 col-md-offset-3">
-                <div class="panel panel-warning">
-                    <div class="panel-heading">
-                        <strong><i class="fa fa-warning"></i> Trace Split</strong>
-                    </div>
-                    <div class="panel-body">
-                        There are multiple possible paths from this point. Select a port to continue.
-                    </div>
-                </div>
-                <div class="panel panel-default">
-                    <table class="panel-body table">
-                        <thead>
-                            <tr class="table-headings">
-                                <th>Port</th>
-                                <th>Connected</th>
-                                <th>Type</th>
-                                <th>Description</th>
-                            </tr>
-                        </thead>
-                        {% for termination in split_ends %}
-                            <tr>
-                                <td><a href="{% url 'dcim:frontport_trace' pk=termination.pk %}">{{ termination }}</a></td>
-                                <td>
-                                    {% if termination.cable %}
-                                        <i class="fa fa-check text-success" title="Yes"></i>
-                                    {% else %}
-                                        <i class="fa fa-times text-danger" title="No"></i>
-                                    {% endif %}
-                                </td>
-                                <td>{{ termination.get_type_display }}</td>
-                                <td>{{ termination.description|placeholder }}</td>
-                            </tr>
-                        {% endfor %}
-                    </table>
-                </div>
-            </div>
-        {% elif position_stack %}
-            <div class="col-md-11 col-md-offset-1">
-                <h3 class="text-warning text-center">
-                    {% with last_position=position_stack|last %}
-                        Trace completed, but there is no Front Port corresponding to
-                        <a href="{{ last_position.device.get_absolute_url }}">{{ last_position.device }}</a> {{ last_position }}.<br>
-                        Therefore no end-to-end connection can be established.
-                    {% endwith %}
-                </h3>
-            </div>
-        {% else %}
-            <div class="col-md-11 col-md-offset-1">
-                <h3 class="text-success text-center">Trace completed!</h3>
-            </div>
-        {% endif %}
+        <div class="col-md-11 col-md-offset-1">
+            <h3 class="text-success text-center">Trace completed!</h3>
+        </div>
     </div>
 {% endblock %}