diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 725639321..cc8e6df1f 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -33,11 +33,9 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) def get_connected_endpoint_type(self, obj): - if hasattr(obj, 'connected_endpoint') and obj.connected_endpoint is not None: - return '{}.{}'.format( - obj.connected_endpoint._meta.app_label, - obj.connected_endpoint._meta.model_name - ) + if obj.path is not None: + destination = obj.path.destination + return f'{destination._meta.app_label}.{destination._meta.model_name}' return None @swagger_serializer_method(serializer_or_field=serializers.DictField) @@ -45,14 +43,11 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): """ Return the appropriate serializer for the type of connected object. """ - if getattr(obj, 'connected_endpoint', None) is None: - return None - - serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested') - context = {'request': self.context['request']} - data = serializer(obj.connected_endpoint, context=context).data - - return data + if obj.path is not None: + serializer = get_serializer_for_model(obj.path.destination, prefix='Nested') + context = {'request': self.context['request']} + return serializer(obj.path.destination, context=context).data + return None # diff --git a/netbox/dcim/management/commands/retrace_paths.py b/netbox/dcim/management/commands/retrace_paths.py index a27833d62..d11a85417 100644 --- a/netbox/dcim/management/commands/retrace_paths.py +++ b/netbox/dcim/management/commands/retrace_paths.py @@ -5,7 +5,7 @@ from django.db import connection from django.db.models import Q from dcim.models import CablePath -from dcim.signals import create_cablepaths +from dcim.signals import create_cablepath ENDPOINT_MODELS = ( 'circuits.CircuitTermination', @@ -60,7 +60,7 @@ class Command(BaseCommand): print(f'Retracing {origins.count()} cabled {model._meta.verbose_name_plural}...') i = 0 for i, obj in enumerate(origins, start=1): - create_cablepaths(obj) + create_cablepath(obj) if not i % 1000: self.stdout.write(f' {i}') self.stdout.write(self.style.SUCCESS(f' Retraced {i} {model._meta.verbose_name_plural}')) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 56e8f6fc4..6bf2ac77a 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1,7 +1,6 @@ import logging from django.contrib.contenttypes.fields import GenericRelation -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -256,7 +255,7 @@ class PathEndpoint(models.Model): """ Any object which may serve as either endpoint of a CablePath. """ - paths = GenericRelation( + _paths = GenericRelation( to='dcim.CablePath', content_type_field='origin_type', object_id_field='origin_id', @@ -266,6 +265,15 @@ class PathEndpoint(models.Model): class Meta: abstract = True + @property + def path(self): + """ + Return the _complete_ CablePath associated with this origin point, if any. + """ + if not hasattr(self, '_path'): + self._path = self._paths.filter(destination_id__isnull=False).first() + return self._path + # # Console ports diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 4e6cadb28..46a2cf1d3 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -6,14 +6,15 @@ from django.db import transaction from django.dispatch import receiver from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis -from .utils import object_to_path_node, trace_paths +from .utils import object_to_path_node, trace_path -def create_cablepaths(node): +def create_cablepath(node): """ Create CablePaths for all paths originating from the specified node. """ - for path, destination in trace_paths(node): + path, destination = trace_path(node) + if path: cp = CablePath(origin=node, path=path, destination=destination) cp.save() @@ -28,7 +29,7 @@ def rebuild_paths(obj): with transaction.atomic(): for cp in cable_paths: cp.delete() - create_cablepaths(cp.origin) + create_cablepath(cp.origin) @receiver(post_save, sender=VirtualChassis) @@ -76,7 +77,7 @@ def update_connected_endpoints(instance, created, **kwargs): if created: for termination in (instance.termination_a, instance.termination_b): if isinstance(termination, PathEndpoint): - create_cablepaths(termination) + create_cablepath(termination) else: rebuild_paths(termination) else: @@ -116,4 +117,4 @@ def nullify_connected_endpoints(instance, **kwargs): origin_type=ContentType.objects.get_for_model(origin), origin_id=origin.pk ).delete() - create_cablepaths(origin) + create_cablepath(origin) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 3d4efae8e..68121e671 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -123,69 +123,43 @@ class CablePathTestCase(TestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_02_interfaces_to_interface_via_pass_through(self): + def test_02_interface_to_interface_via_pass_through(self): """ - [IF1] --C1-- [FP1:1] [RP1] --C3-- [IF3] - [IF2] --C2-- [FP1:2] + [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] """ - # Create cables 1 and 2 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + # Create cable 1 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[16]) cable1.save() - cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) + self.assertPathExists( + origin=self.interfaces[0], + destination=None, + path=(cable1, self.front_ports[16], self.rear_ports[4]) + ) + self.assertEqual(CablePath.objects.count(), 1) + + # Create cable 2 + cable2 = Cable(termination_a=self.rear_ports[4], termination_b=self.interfaces[1]) cable2.save() self.assertPathExists( origin=self.interfaces[0], - destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0]) + destination=self.interfaces[1], + path=(cable1, self.front_ports[16], self.rear_ports[4], cable2) ) self.assertPathExists( origin=self.interfaces[1], - destination=None, - path=(cable2, self.front_ports[1], self.rear_ports[0]) + destination=self.interfaces[0], + path=(cable2, self.rear_ports[4], self.front_ports[16], cable1) ) self.assertEqual(CablePath.objects.count(), 2) - # Create cable 3 - cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.interfaces[2]) - cable3.save() - self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[2], - path=(cable1, self.front_ports[0], self.rear_ports[0], cable3) - ) - self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[2], - path=(cable2, self.front_ports[1], self.rear_ports[0], cable3) - ) - self.assertPathExists( - origin=self.interfaces[2], - destination=self.interfaces[0], - path=(cable3, self.rear_ports[0], self.front_ports[0], cable1) - ) - self.assertPathExists( - origin=self.interfaces[2], - destination=self.interfaces[1], - path=(cable3, self.rear_ports[0], self.front_ports[1], cable2) - ) - self.assertEqual(CablePath.objects.count(), 6) # Four complete + two partial paths - - # Delete cable 3 - cable3.delete() + # Delete cable 2 + cable2.delete() self.assertPathExists( origin=self.interfaces[0], destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0]) + path=(cable1, self.front_ports[16], self.rear_ports[4]) ) - self.assertPathExists( - origin=self.interfaces[1], - destination=None, - path=(cable2, self.front_ports[1], self.rear_ports[0]) - ) - - # Check for two partial paths from IF1 and IF2 - self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 2) - self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) + self.assertEqual(CablePath.objects.count(), 1) def test_03_interfaces_to_interfaces_via_pass_through(self): """ diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 75029cacc..59ca59bfc 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -1,5 +1,6 @@ from django.contrib.contenttypes.models import ContentType +from .exceptions import CableTraceSplit from .models import FrontPort, RearPort @@ -17,13 +18,13 @@ def path_node_to_object(repr): return model_class.objects.get(pk=int(object_id)) -def trace_paths(node): +def trace_path(node): destination = None path = [] position_stack = [] if node.cable is None: - return [] + return [], None while node.cable is not None: @@ -50,20 +51,12 @@ def trace_paths(node): node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position) path.append(object_to_path_node(node)) else: - # No position indicated, so we have to trace _all_ peer FrontPorts - paths = [] - for frontport in FrontPort.objects.filter(rear_port=peer_termination): - branches = trace_paths(frontport) - if branches: - for branch, destination in branches: - paths.append(([*path, object_to_path_node(frontport), *branch], destination)) - else: - paths.append(([*path, object_to_path_node(frontport)], None)) - return paths + # No position indicated: path has split (probably invalid?) + raise CableTraceSplit(peer_termination) # Anything else marks the end of the path else: destination = peer_termination break - return [(path, destination)] + return path, destination diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 58be5d213..96e6615e8 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1018,7 +1018,7 @@ class DeviceView(ObjectView): # Console ports consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), + Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), 'cable', ) @@ -1026,25 +1026,25 @@ class DeviceView(ObjectView): consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter( device=device ).prefetch_related( - Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), + Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), 'cable', ) # Power ports powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), + Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), 'cable', ) # Power outlets poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), + Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), 'cable', 'power_port', ) # Interfaces interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related( - Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), + Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)), Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)), 'lag', 'cable', 'tags', diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index dc0ff384c..912404be3 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -36,7 +36,7 @@ {# Connection #} - {% include 'dcim/inc/endpoint_connection.html' with paths=cp.paths.all %} + {% include 'dcim/inc/endpoint_connection.html' with path=cp.path %} {# Actions #} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index 0af64b4c1..b7a5c6b56 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -38,7 +38,7 @@ {# Connection #} - {% include 'dcim/inc/endpoint_connection.html' with paths=csp.paths.all %} + {% include 'dcim/inc/endpoint_connection.html' with path=csp.path %} {# Actions #} diff --git a/netbox/templates/dcim/inc/endpoint_connection.html b/netbox/templates/dcim/inc/endpoint_connection.html index 07d73a534..1c25a0e28 100644 --- a/netbox/templates/dcim/inc/endpoint_connection.html +++ b/netbox/templates/dcim/inc/endpoint_connection.html @@ -1,7 +1,5 @@ -{% if paths|length > 1 %} - Multiple connections -{% elif paths %} - {% with endpoint=paths.0.destination %} +{% if path %} + {% with endpoint=path.destination %} {{ endpoint.parent }} {{ endpoint }} {% endwith %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index ae1363dba..159551192 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -76,7 +76,7 @@ {% elif iface.is_wireless %} Wireless interface {% else %} - {% include 'dcim/inc/endpoint_connection.html' with paths=iface.paths.all %} + {% include 'dcim/inc/endpoint_connection.html' with path=iface.path %} {% endif %} {# Buttons #} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 39af6828d..b3e003e99 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -49,7 +49,7 @@ {# Connection #} - {% with paths=po.paths.all %} + {% with path=po.path %} {% include 'dcim/inc/endpoint_connection.html' %} {% if paths|length == 1 %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index 4ec1b786e..c65b685d7 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -45,7 +45,7 @@ {# Connection #} - {% include 'dcim/inc/endpoint_connection.html' with paths=pp.paths.all %} + {% include 'dcim/inc/endpoint_connection.html' with path=pp.path %} {# Actions #}