From 587e6fcf72fe05a7597b8e565b782f659aacefb1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 30 Sep 2020 15:07:56 -0400 Subject: [PATCH 01/67] Initial work on cable paths (WIP) --- netbox/dcim/fields.py | 11 +++ netbox/dcim/managers.py | 8 ++ netbox/dcim/migrations/0120_cablepath.py | 27 +++++++ netbox/dcim/models/__init__.py | 1 + netbox/dcim/models/device_components.py | 23 ++++-- netbox/dcim/models/devices.py | 42 ++++++++++ netbox/dcim/signals.py | 97 ++++++++++++++---------- netbox/dcim/utils.py | 65 ++++++++++++++++ netbox/templates/dcim/inc/interface.html | 66 +++------------- 9 files changed, 239 insertions(+), 101 deletions(-) create mode 100644 netbox/dcim/managers.py create mode 100644 netbox/dcim/migrations/0120_cablepath.py create mode 100644 netbox/dcim/utils.py diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index 3acd0d4a1..23b9b7fd5 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -1,3 +1,5 @@ +from django.contrib.postgres.fields import ArrayField +from django.contrib.postgres.validators import ArrayMaxLengthValidator from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models @@ -50,3 +52,12 @@ class MACAddressField(models.Field): if not value: return None return str(self.to_python(value)) + + +class PathField(ArrayField): + """ + An ArrayField which holds a set of objects, each identified by a (type, ID) tuple. + """ + def __init__(self, **kwargs): + kwargs['base_field'] = models.CharField(max_length=40) + super().__init__(**kwargs) diff --git a/netbox/dcim/managers.py b/netbox/dcim/managers.py new file mode 100644 index 000000000..903a6feac --- /dev/null +++ b/netbox/dcim/managers.py @@ -0,0 +1,8 @@ +from django.contrib.contenttypes.models import ContentType +from django.db.models import Manager + + +class CablePathManager(Manager): + + def create_for_endpoint(self, endpoint): + ct = ContentType.objects.get_for_model(endpoint) diff --git a/netbox/dcim/migrations/0120_cablepath.py b/netbox/dcim/migrations/0120_cablepath.py new file mode 100644 index 000000000..4e36c31d0 --- /dev/null +++ b/netbox/dcim/migrations/0120_cablepath.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1 on 2020-09-30 18:09 + +import dcim.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('dcim', '0119_inventoryitem_mptt_rebuild'), + ] + + operations = [ + migrations.CreateModel( + name='CablePath', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('origin_id', models.PositiveIntegerField()), + ('destination_id', models.PositiveIntegerField(blank=True, null=True)), + ('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)), + ('destination_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), + ('origin_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), + ], + ), + ] diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index e50fa2eda..fdd4d1bf5 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -8,6 +8,7 @@ from .sites import * __all__ = ( 'BaseInterface', 'Cable', + 'CablePath', 'CableTermination', 'ConsolePort', 'ConsolePortTemplate', diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 2cc4c8b60..12bb224fd 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1,6 +1,7 @@ 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 @@ -32,6 +33,7 @@ __all__ = ( 'FrontPort', 'Interface', 'InventoryItem', + 'PathEndpoint', 'PowerOutlet', 'PowerPort', 'RearPort', @@ -250,12 +252,23 @@ class CableTermination(models.Model): return endpoints +class PathEndpoint: + + def get_connections(self): + from dcim.models import CablePath + return CablePath.objects.filter( + origin_type=ContentType.objects.get_for_model(self), + origin_id=self.pk, + destination_id__isnull=False + ) + + # # Console ports # @extras_features('export_templates', 'webhooks') -class ConsolePort(CableTermination, ComponentModel): +class ConsolePort(CableTermination, PathEndpoint, ComponentModel): """ A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. """ @@ -303,7 +316,7 @@ class ConsolePort(CableTermination, ComponentModel): # @extras_features('webhooks') -class ConsoleServerPort(CableTermination, ComponentModel): +class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel): """ A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. """ @@ -344,7 +357,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): # @extras_features('export_templates', 'webhooks') -class PowerPort(CableTermination, ComponentModel): +class PowerPort(CableTermination, PathEndpoint, ComponentModel): """ A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. """ @@ -493,7 +506,7 @@ class PowerPort(CableTermination, ComponentModel): # @extras_features('webhooks') -class PowerOutlet(CableTermination, ComponentModel): +class PowerOutlet(CableTermination, PathEndpoint, ComponentModel): """ A physical power outlet (output) within a Device which provides power to a PowerPort. """ @@ -585,7 +598,7 @@ class BaseInterface(models.Model): @extras_features('export_templates', 'webhooks') -class Interface(CableTermination, ComponentModel, BaseInterface): +class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): """ A network interface within a Device. A physical Interface can connect to exactly one other Interface. """ diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 463b1a3e3..162dcb831 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -14,6 +14,9 @@ from taggit.managers import TaggableManager from dcim.choices import * from dcim.constants import * +from dcim.fields import PathField +from dcim.managers import CablePathManager +from dcim.utils import path_node_to_object from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem from extras.utils import extras_features from utilities.choices import ColorChoices @@ -25,6 +28,7 @@ from .device_components import * __all__ = ( 'Cable', + 'CablePath', 'Device', 'DeviceRole', 'DeviceType', @@ -1154,6 +1158,44 @@ class Cable(ChangeLoggedModel, CustomFieldModel): return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name] +class CablePath(models.Model): + """ + An array of objects conveying the end-to-end path of one or more Cables. + """ + origin_type = models.ForeignKey( + to=ContentType, + on_delete=models.CASCADE, + related_name='+' + ) + origin_id = models.PositiveIntegerField() + origin = GenericForeignKey( + ct_field='origin_type', + fk_field='origin_id' + ) + destination_type = models.ForeignKey( + to=ContentType, + on_delete=models.CASCADE, + related_name='+', + blank=True, + null=True + ) + destination_id = models.PositiveIntegerField( + blank=True, + null=True + ) + destination = GenericForeignKey( + ct_field='destination_type', + fk_field='destination_id' + ) + path = PathField() + + objects = CablePathManager() + + def __str__(self): + path = ', '.join([str(path_node_to_object(node)) for node in self.path]) + return f"Path #{self.pk}: {self.origin} to {self.destination} via ({path})" + + # # Virtual chassis # diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 172c366b5..4e6cadb28 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -1,10 +1,34 @@ import logging +from django.contrib.contenttypes.models import ContentType from django.db.models.signals import post_save, pre_delete +from django.db import transaction from django.dispatch import receiver -from .choices import CableStatusChoices -from .models import Cable, CableTermination, Device, FrontPort, RearPort, VirtualChassis +from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis +from .utils import object_to_path_node, trace_paths + + +def create_cablepaths(node): + """ + Create CablePaths for all paths originating from the specified node. + """ + for path, destination in trace_paths(node): + cp = CablePath(origin=node, path=path, destination=destination) + cp.save() + + +def rebuild_paths(obj): + """ + Rebuild all CablePaths which traverse the specified node + """ + node = object_to_path_node(obj) + cable_paths = CablePath.objects.filter(path__contains=[node]) + + with transaction.atomic(): + for cp in cable_paths: + cp.delete() + create_cablepaths(cp.origin) @receiver(post_save, sender=VirtualChassis) @@ -32,7 +56,7 @@ def clear_virtualchassis_members(instance, **kwargs): @receiver(post_save, sender=Cable) -def update_connected_endpoints(instance, **kwargs): +def update_connected_endpoints(instance, created, **kwargs): """ When a Cable is saved, check for and update its two connected endpoints """ @@ -40,38 +64,25 @@ def update_connected_endpoints(instance, **kwargs): # Cache the Cable on its two termination points if instance.termination_a.cable != instance: - logger.debug("Updating termination A for cable {}".format(instance)) + logger.debug(f"Updating termination A for cable {instance}") instance.termination_a.cable = instance instance.termination_a.save() if instance.termination_b.cable != instance: - logger.debug("Updating termination B for cable {}".format(instance)) + logger.debug(f"Updating termination B for cable {instance}") instance.termination_b.cable = instance instance.termination_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, split_ends, position_stack = 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 not split_ends and not position_stack else None - - # Patch panel ports are not connected endpoints, all other cable terminations are - if isinstance(endpoint_a, CableTermination) and not isinstance(endpoint_a, (FrontPort, RearPort)) and \ - isinstance(endpoint_b, CableTermination) and not isinstance(endpoint_b, (FrontPort, RearPort)): - 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() + # Create/update cable paths + if created: + for termination in (instance.termination_a, instance.termination_b): + if isinstance(termination, PathEndpoint): + create_cablepaths(termination) + else: + rebuild_paths(termination) + else: + # We currently don't support modifying either termination of an existing Cable. This + # may change in the future. + pass @receiver(pre_delete, sender=Cable) @@ -81,22 +92,28 @@ def nullify_connected_endpoints(instance, **kwargs): """ logger = logging.getLogger('netbox.dcim.cable') - 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: - logger.debug("Nullifying termination A for cable {}".format(instance)) + logger.debug(f"Nullifying termination A for cable {instance}") instance.termination_a.cable = None instance.termination_a.save() if instance.termination_b is not None: - logger.debug("Nullifying termination B for cable {}".format(instance)) + logger.debug(f"Nullifying termination B for cable {instance}") instance.termination_b.cable = None instance.termination_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() + # Delete any dependent cable paths + cable_paths = CablePath.objects.filter(path__contains=[object_to_path_node(instance)]) + retrace_queue = [cp.origin for cp in cable_paths] + deleted, _ = cable_paths.delete() + logger.info(f'Deleted {deleted} cable paths') + + # Retrace cable paths from the origins of deleted paths + for origin in retrace_queue: + # Delete and recreate all CablePaths for this origin point + # TODO: We can probably be smarter about skipping unchanged paths + CablePath.objects.filter( + origin_type=ContentType.objects.get_for_model(origin), + origin_id=origin.pk + ).delete() + create_cablepaths(origin) diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py new file mode 100644 index 000000000..2baa91622 --- /dev/null +++ b/netbox/dcim/utils.py @@ -0,0 +1,65 @@ +from django.contrib.contenttypes.models import ContentType + +from .models import FrontPort, RearPort + + +def object_to_path_node(obj): + return f'{obj._meta.model_name}:{obj.pk}' + + +def objects_to_path(*obj_list): + return [object_to_path_node(obj) for obj in obj_list] + + +def path_node_to_object(repr): + model_name, object_id = repr.split(':') + model_class = ContentType.objects.get(model=model_name).model_class() + return model_class.objects.get(pk=int(object_id)) + + +def trace_paths(node): + destination = None + path = [] + position_stack = [] + + if node.cable is None: + return [] + + while node.cable is not None: + + # Follow the cable to its far-end termination + path.append(object_to_path_node(node.cable)) + peer_termination = node.get_cable_peer() + + # Follow a FrontPort to its corresponding RearPort + if isinstance(peer_termination, FrontPort): + path.append(object_to_path_node(peer_termination)) + position_stack.append(peer_termination.rear_port_position) + node = peer_termination.rear_port + path.append(object_to_path_node(node)) + + # Follow a RearPort to its corresponding FrontPort + elif isinstance(peer_termination, RearPort): + path.append(object_to_path_node(peer_termination)) + if position_stack: + position = position_stack.pop() + 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 + + # Anything else marks the end of the path + else: + destination = peer_termination + break + + return [(path, destination)] diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index a317dc937..706801dd1 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -75,65 +75,19 @@ Virtual interface {% elif iface.is_wireless %} Wireless interface - {% elif iface.connected_endpoint.name %} - {# Connected to an Interface #} - - - {{ iface.connected_endpoint.device }} - - - - - - {{ iface.connected_endpoint }} - - - - {% elif iface.connected_endpoint.term_side %} - {# Connected to a CircuitTermination #} - {% with iface.connected_endpoint.get_peer_termination as peer_termination %} - {% if peer_termination %} - {% if peer_termination.connected_endpoint %} - - - {{ peer_termination.connected_endpoint.device }} -
- via - - {{ iface.connected_endpoint.circuit.provider }} - {{ iface.connected_endpoint.circuit }} - - - - - {{ peer_termination.connected_endpoint }} - - {% else %} - - - {{ peer_termination.site }} - - via - - {{ iface.connected_endpoint.circuit.provider }} - {{ iface.connected_endpoint.circuit }} - - - {% endif %} + {% else %} + {% with path_count=iface.get_connections.count %} + {% if path_count > 1 %} + Multiple connections + {% elif path_count %} + {% with endpoint=iface.get_connections.first.destination %} + {{ endpoint.parent }} + {{ endpoint }} + {% endwith %} {% else %} - - - - {{ iface.connected_endpoint.circuit.provider }} - {{ iface.connected_endpoint.circuit }} - - + Not connected {% endif %} {% endwith %} - {% else %} - - Not connected - {% endif %} {# Buttons #} From 985197788b0039976299de1fc56be25d90756dd8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 30 Sep 2020 15:13:33 -0400 Subject: [PATCH 02/67] Add initial tests --- netbox/dcim/tests/test_cablepaths.py | 210 +++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 netbox/dcim/tests/test_cablepaths.py diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py new file mode 100644 index 000000000..efd3fcbee --- /dev/null +++ b/netbox/dcim/tests/test_cablepaths.py @@ -0,0 +1,210 @@ +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from circuits.models import * +from dcim.models import * +from dcim.utils import objects_to_path + + +class CablePathTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + + # Create a single device that will hold all components + site = Site.objects.create(name='Site', slug='site') + manufacturer = Manufacturer.objects.create(name='Generic', slug='generic') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Test Device') + device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') + device = Device.objects.create(site=site, device_type=device_type, device_role=device_role, name='Test Device') + + # Create 16 instances of each type of path-terminating component + cls.console_ports = [ + ConsolePort(device=device, name=f'Console Port {i}') + for i in range(1, 17) + ] + ConsolePort.objects.bulk_create(cls.console_ports) + cls.console_server_ports = [ + ConsoleServerPort(device=device, name=f'Console Server Port {i}') + for i in range(1, 17) + ] + ConsoleServerPort.objects.bulk_create(cls.console_server_ports) + cls.power_ports = [ + PowerPort(device=device, name=f'Power Port {i}') + for i in range(1, 17) + ] + PowerPort.objects.bulk_create(cls.power_ports) + cls.power_outlets = [ + PowerOutlet(device=device, name=f'Power Outlet {i}') + for i in range(1, 17) + ] + PowerOutlet.objects.bulk_create(cls.power_outlets) + cls.interfaces = [ + Interface(device=device, name=f'Interface {i}') + for i in range(1, 17) + ] + Interface.objects.bulk_create(cls.interfaces) + + # Create four RearPorts with four FrontPorts each + cls.rear_ports = [ + RearPort(device=device, name=f'RP{i}', positions=4) for i in range(1, 5) + ] + RearPort.objects.bulk_create(cls.rear_ports) + cls.front_ports = [] + for i, rear_port in enumerate(cls.rear_ports, start=1): + cls.front_ports.extend( + FrontPort(device=device, name=f'FP{i}:{j}', rear_port=rear_port, rear_port_position=j) + for j in range(1, 5) + ) + FrontPort.objects.bulk_create(cls.front_ports) + + # Create four circuits with two terminations (A and Z) each (8 total) + provider = Provider.objects.create(name='Provider', slug='provider') + circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type') + circuits = [ + Circuit(provider=provider, type=circuit_type, cid=f'Circuit {i}') for i in range(1, 5) + ] + Circuit.objects.bulk_create(circuits) + cls.circuit_terminations = [ + *[CircuitTermination(circuit=circuit, site=site, term_side='A', port_speed=1000) for circuit in circuits], + *[CircuitTermination(circuit=circuit, site=site, term_side='Z', port_speed=1000) for circuit in circuits], + ] + CircuitTermination.objects.bulk_create(cls.circuit_terminations) + + def assertPathExists(self, origin, destination, path=None, msg=None): + """ + Assert that a CablePath from origin to destination with a specific intermediate path exists. + + :param origin: Originating endpoint + :param destination: Terminating endpoint, or None + :param path: Sequence of objects comprising the intermediate path (optional) + :param msg: Custom failure message (optional) + """ + kwargs = { + 'origin_type': ContentType.objects.get_for_model(origin), + 'origin_id': origin.pk, + } + if destination is not None: + kwargs['destination_type'] = ContentType.objects.get_for_model(destination) + kwargs['destination_id'] = destination.pk + else: + kwargs['destination_type__isnull'] = True + kwargs['destination_id__isnull'] = True + if path is not None: + kwargs['path'] = objects_to_path(*path) + if msg is None: + if destination is not None: + msg = f"Missing path from {origin} to {destination}" + else: + msg = f"Missing partial path originating from {origin}" + self.assertEqual(CablePath.objects.filter(**kwargs).count(), 1, msg=msg) + + def test_01_interface_to_interface(self): + """ + [IF1] --C1-- [IF2] + """ + # Create cable 1 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.interfaces[1]) + cable1.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[1], + path=(cable1,) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[0], + path=(cable1,) + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Delete cable 1 + cable1.delete() + self.assertEqual(CablePath.objects.count(), 0) + + def test_02_interface_to_interface_via_single_frontport(self): + """ + [IF1] --C1-- [FP1:1] [RP1] --C2-- [IF2] + """ + # Create cable 1 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + cable1.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=None, + path=(cable1, self.front_ports[0], self.rear_ports[0]) + ) + self.assertEqual(CablePath.objects.count(), 1) + + # Create cable 2 + cable2 = Cable(termination_a=self.rear_ports[0], termination_b=self.interfaces[1]) + cable2.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[1], + path=(cable1, self.front_ports[0], self.rear_ports[0], cable2) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[0], + path=(cable2, self.rear_ports[0], self.front_ports[0], cable1) + ) + self.assertEqual(CablePath.objects.count(), 5) # Two complete + three partial paths + + # Delete cable 1 + cable1.delete() + self.assertPathExists( + origin=self.interfaces[1], + destination=None, + path=(cable2, self.rear_ports[0], self.front_ports[0]) + ) + self.assertEqual(CablePath.objects.count(), 4) # Four partial paths from IF2 to FP1:[1-4] + + def test_03_interface_to_interface_via_rearport_pair(self): + """ + [IF1] --C1-- [FP1:1] [RP1] --C2-- [RP2] [FP2:1] --C3-- [IF2] + """ + # Create cable 1 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + cable1.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=None, + path=(cable1, self.front_ports[0], self.rear_ports[0]) + ) + self.assertEqual(CablePath.objects.count(), 1) + + # Create cable 2 + cable2 = Cable(termination_a=self.rear_ports[0], termination_b=self.rear_ports[1]) + cable2.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=None, + path=(cable1, self.front_ports[0], self.rear_ports[0], cable2, self.rear_ports[1], self.front_ports[4]) + ) + self.assertEqual(CablePath.objects.count(), 1) + + # Create cable 3 + cable3 = Cable(termination_a=self.front_ports[4], termination_b=self.interfaces[1]) + cable3.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[1], + path=( + cable1, self.front_ports[0], self.rear_ports[0], cable2, self.rear_ports[1], self.front_ports[4], + cable3, + ) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[0], + path=( + cable3, self.front_ports[4], self.rear_ports[1], cable2, self.rear_ports[0], self.front_ports[0], + cable1 + ) + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Delete cable 2 + cable2.delete() + self.assertEqual(CablePath.objects.count(), 2) # Two partial paths from IF1 and IF2 From 319329e2b230a65122031130d861816cf3f75160 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 30 Sep 2020 16:17:22 -0400 Subject: [PATCH 03/67] Extend cable path tests --- netbox/dcim/tests/test_cablepaths.py | 275 ++++++++++++++++++++------- 1 file changed, 204 insertions(+), 71 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index efd3fcbee..401c38ca5 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -120,91 +120,224 @@ class CablePathTestCase(TestCase): # Delete cable 1 cable1.delete() + + # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_02_interface_to_interface_via_single_frontport(self): + def test_02_interfaces_to_interface_via_pass_through(self): """ - [IF1] --C1-- [FP1:1] [RP1] --C2-- [IF2] + [IF1] --C1-- [FP1:1] [RP1] --C3-- [IF3] + [IF2] --C2-- [FP1:2] """ - # Create cable 1 + # Create cables 1 and 2 cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) cable1.save() + cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) + cable2.save() self.assertPathExists( origin=self.interfaces[0], destination=None, path=(cable1, self.front_ports[0], self.rear_ports[0]) ) - self.assertEqual(CablePath.objects.count(), 1) - - # Create cable 2 - cable2 = Cable(termination_a=self.rear_ports[0], termination_b=self.interfaces[1]) - cable2.save() - self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[1], - path=(cable1, self.front_ports[0], self.rear_ports[0], cable2) - ) - self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[0], - path=(cable2, self.rear_ports[0], self.front_ports[0], cable1) - ) - self.assertEqual(CablePath.objects.count(), 5) # Two complete + three partial paths - - # Delete cable 1 - cable1.delete() self.assertPathExists( origin=self.interfaces[1], destination=None, - path=(cable2, self.rear_ports[0], self.front_ports[0]) - ) - self.assertEqual(CablePath.objects.count(), 4) # Four partial paths from IF2 to FP1:[1-4] - - def test_03_interface_to_interface_via_rearport_pair(self): - """ - [IF1] --C1-- [FP1:1] [RP1] --C2-- [RP2] [FP2:1] --C3-- [IF2] - """ - # Create cable 1 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) - cable1.save() - self.assertPathExists( - origin=self.interfaces[0], - destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0]) - ) - self.assertEqual(CablePath.objects.count(), 1) - - # Create cable 2 - cable2 = Cable(termination_a=self.rear_ports[0], termination_b=self.rear_ports[1]) - cable2.save() - self.assertPathExists( - origin=self.interfaces[0], - destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0], cable2, self.rear_ports[1], self.front_ports[4]) - ) - self.assertEqual(CablePath.objects.count(), 1) - - # Create cable 3 - cable3 = Cable(termination_a=self.front_ports[4], termination_b=self.interfaces[1]) - cable3.save() - self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[1], - path=( - cable1, self.front_ports[0], self.rear_ports[0], cable2, self.rear_ports[1], self.front_ports[4], - cable3, - ) - ) - self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[0], - path=( - cable3, self.front_ports[4], self.rear_ports[1], cable2, self.rear_ports[0], self.front_ports[0], - cable1 - ) + path=(cable2, self.front_ports[1], self.rear_ports[0]) ) self.assertEqual(CablePath.objects.count(), 2) - # Delete cable 2 - cable2.delete() - self.assertEqual(CablePath.objects.count(), 2) # Two partial paths from IF1 and IF2 + # 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() + self.assertPathExists( + origin=self.interfaces[0], + destination=None, + path=(cable1, self.front_ports[0], self.rear_ports[0]) + ) + 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) + + def test_03_interfaces_to_interfaces_via_pass_through(self): + """ + [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [IF3] + [IF2] --C2-- [FP1:2] [FP2:2] --C5-- [IF4] + """ + # Create cables 1-2 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + cable1.save() + cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) + cable2.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=None, + path=(cable1, self.front_ports[0], self.rear_ports[0]) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=None, + path=(cable2, self.front_ports[1], self.rear_ports[0]) + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Create cable 3 + cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.rear_ports[1]) + cable3.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=None, + path=(cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4]) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=None, + path=(cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5]) + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Create cables 4-5 + cable4 = Cable(termination_a=self.front_ports[4], termination_b=self.interfaces[2]) + cable4.save() + cable5 = Cable(termination_a=self.front_ports[5], termination_b=self.interfaces[3]) + cable5.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[2], + path=( + cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4], + cable4, + ) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[3], + path=( + cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5], + cable5, + ) + ) + self.assertPathExists( + origin=self.interfaces[2], + destination=self.interfaces[0], + path=( + cable4, self.front_ports[4], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[0], + cable1 + ) + ) + self.assertPathExists( + origin=self.interfaces[3], + destination=self.interfaces[1], + path=( + cable5, self.front_ports[5], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[1], + cable2 + ) + ) + self.assertEqual(CablePath.objects.count(), 4) + + # Delete cable 3 + cable3.delete() + + # Check for four partial paths; one from each interface + self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) + self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) + + def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): + """ + [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP2:1] [RP2] --C4-- [RP3] [FP3:1] --C5-- [RP4] [FP4:1] --C6-- [IF3] + [IF2] --C2-- [FP1:2] [FP4:2] --C7-- [IF4] + """ + # Create cables 1-2, 6-7 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + cable1.save() + cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) + cable2.save() + cable6 = Cable(termination_a=self.interfaces[2], termination_b=self.front_ports[12]) + cable6.save() + cable7 = Cable(termination_a=self.interfaces[3], termination_b=self.front_ports[13]) + cable7.save() + self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface + + # Create cables 3 and 5 + cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.front_ports[4]) + cable3.save() + cable5 = Cable(termination_a=self.rear_ports[3], termination_b=self.front_ports[8]) + cable5.save() + self.assertEqual(CablePath.objects.count(), 4) # Four (longer) partial paths; one from each interface + + # Create cable 4 + cable4 = Cable(termination_a=self.rear_ports[1], termination_b=self.rear_ports[2]) + cable4.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[2], + path=( + cable1, self.front_ports[0], self.rear_ports[0], cable3, self.front_ports[4], self.rear_ports[1], + cable4, self.rear_ports[2], self.front_ports[8], cable5, self.rear_ports[3], self.front_ports[12], + cable6 + ) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[3], + path=( + cable2, self.front_ports[1], self.rear_ports[0], cable3, self.front_ports[4], self.rear_ports[1], + cable4, self.rear_ports[2], self.front_ports[8], cable5, self.rear_ports[3], self.front_ports[13], + cable7 + ) + ) + self.assertPathExists( + origin=self.interfaces[2], + destination=self.interfaces[0], + path=( + cable6, self.front_ports[12], self.rear_ports[3], cable5, self.front_ports[8], self.rear_ports[2], + cable4, self.rear_ports[1], self.front_ports[4], cable3, self.rear_ports[0], self.front_ports[0], + cable1 + ) + ) + self.assertPathExists( + origin=self.interfaces[3], + destination=self.interfaces[1], + path=( + cable7, self.front_ports[13], self.rear_ports[3], cable5, self.front_ports[8], self.rear_ports[2], + cable4, self.rear_ports[1], self.front_ports[4], cable3, self.rear_ports[0], self.front_ports[1], + cable2 + ) + ) + self.assertEqual(CablePath.objects.count(), 4) + + # Delete cable 3 + cable3.delete() + + # Check for four partial paths; one from each interface + self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) + self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) From cd7179937376ea7c7b81deedf37509717ed85143 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 30 Sep 2020 17:09:39 -0400 Subject: [PATCH 04/67] Ignore the position stack when traversing single-position rear ports --- netbox/dcim/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 2baa91622..75029cacc 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -34,14 +34,18 @@ def trace_paths(node): # Follow a FrontPort to its corresponding RearPort if isinstance(peer_termination, FrontPort): path.append(object_to_path_node(peer_termination)) - position_stack.append(peer_termination.rear_port_position) node = peer_termination.rear_port + if node.positions > 1: + position_stack.append(peer_termination.rear_port_position) path.append(object_to_path_node(node)) # Follow a RearPort to its corresponding FrontPort elif isinstance(peer_termination, RearPort): path.append(object_to_path_node(peer_termination)) - if position_stack: + if peer_termination.positions == 1: + node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=1) + path.append(object_to_path_node(node)) + elif position_stack: position = position_stack.pop() node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position) path.append(object_to_path_node(node)) From e53ae1d584267cd2a40e0ae701e150334364e10d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 30 Sep 2020 17:10:22 -0400 Subject: [PATCH 05/67] Extend cable path tests --- netbox/dcim/tests/test_cablepaths.py | 97 +++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 401c38ca5..767b34594 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -45,17 +45,36 @@ class CablePathTestCase(TestCase): ] Interface.objects.bulk_create(cls.interfaces) - # Create four RearPorts with four FrontPorts each + # Create four RearPorts with four FrontPorts each, and two with only one position cls.rear_ports = [ - RearPort(device=device, name=f'RP{i}', positions=4) for i in range(1, 5) + RearPort(device=device, name=f'RP1', positions=4), + RearPort(device=device, name=f'RP2', positions=4), + RearPort(device=device, name=f'RP3', positions=4), + RearPort(device=device, name=f'RP4', positions=4), + RearPort(device=device, name=f'RP5', positions=1), + RearPort(device=device, name=f'RP6', positions=1), ] RearPort.objects.bulk_create(cls.rear_ports) - cls.front_ports = [] - for i, rear_port in enumerate(cls.rear_ports, start=1): - cls.front_ports.extend( - FrontPort(device=device, name=f'FP{i}:{j}', rear_port=rear_port, rear_port_position=j) - for j in range(1, 5) - ) + cls.front_ports = [ + FrontPort(device=device, name=f'FP1:1', rear_port=cls.rear_ports[0], rear_port_position=1), + FrontPort(device=device, name=f'FP1:2', rear_port=cls.rear_ports[0], rear_port_position=2), + FrontPort(device=device, name=f'FP1:3', rear_port=cls.rear_ports[0], rear_port_position=3), + FrontPort(device=device, name=f'FP1:4', rear_port=cls.rear_ports[0], rear_port_position=4), + FrontPort(device=device, name=f'FP2:1', rear_port=cls.rear_ports[1], rear_port_position=1), + FrontPort(device=device, name=f'FP2:2', rear_port=cls.rear_ports[1], rear_port_position=2), + FrontPort(device=device, name=f'FP2:3', rear_port=cls.rear_ports[1], rear_port_position=3), + FrontPort(device=device, name=f'FP2:4', rear_port=cls.rear_ports[1], rear_port_position=4), + FrontPort(device=device, name=f'FP3:1', rear_port=cls.rear_ports[2], rear_port_position=1), + FrontPort(device=device, name=f'FP3:2', rear_port=cls.rear_ports[2], rear_port_position=2), + FrontPort(device=device, name=f'FP3:3', rear_port=cls.rear_ports[2], rear_port_position=3), + FrontPort(device=device, name=f'FP3:4', rear_port=cls.rear_ports[2], rear_port_position=4), + FrontPort(device=device, name=f'FP4:1', rear_port=cls.rear_ports[3], rear_port_position=1), + FrontPort(device=device, name=f'FP4:2', rear_port=cls.rear_ports[3], rear_port_position=2), + FrontPort(device=device, name=f'FP4:3', rear_port=cls.rear_ports[3], rear_port_position=3), + FrontPort(device=device, name=f'FP4:4', rear_port=cls.rear_ports[3], rear_port_position=4), + FrontPort(device=device, name=f'FP5', rear_port=cls.rear_ports[4], rear_port_position=1), + FrontPort(device=device, name=f'FP6', rear_port=cls.rear_ports[5], rear_port_position=1), + ] FrontPort.objects.bulk_create(cls.front_ports) # Create four circuits with two terminations (A and Z) each (8 total) @@ -341,3 +360,65 @@ class CablePathTestCase(TestCase): # Check for four partial paths; one from each interface self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) + + def test_05_interfaces_to_interfaces_via_patched_pass_throughs(self): + """ + [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP5] [RP5] --C4-- [RP2] [FP2:1] --C5-- [IF3] + [IF2] --C2-- [FP1:2] [FP2:2] --C6-- [IF4] + """ + # Create cables 1-2, 5-6 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) # IF1 -> FP1:1 + cable1.save() + cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) # IF2 -> FP1:2 + cable2.save() + cable5 = Cable(termination_a=self.interfaces[2], termination_b=self.front_ports[4]) # IF3 -> FP2:1 + cable5.save() + cable6 = Cable(termination_a=self.interfaces[3], termination_b=self.front_ports[5]) # IF4 -> FP2:2 + cable6.save() + self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface + + # Create cables 3-4 + cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.front_ports[16]) # RP1 -> FP5 + cable3.save() + cable4 = Cable(termination_a=self.rear_ports[4], termination_b=self.rear_ports[1]) # RP5 -> RP2 + cable4.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[2], + path=( + cable1, self.front_ports[0], self.rear_ports[0], cable3, self.front_ports[16], self.rear_ports[4], + cable4, self.rear_ports[1], self.front_ports[4], cable5 + ) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[3], + path=( + cable2, self.front_ports[1], self.rear_ports[0], cable3, self.front_ports[16], self.rear_ports[4], + cable4, self.rear_ports[1], self.front_ports[5], cable6 + ) + ) + self.assertPathExists( + origin=self.interfaces[2], + destination=self.interfaces[0], + path=( + cable5, self.front_ports[4], self.rear_ports[1], cable4, self.rear_ports[4], self.front_ports[16], + cable3, self.rear_ports[0], self.front_ports[0], cable1 + ) + ) + self.assertPathExists( + origin=self.interfaces[3], + destination=self.interfaces[1], + path=( + cable6, self.front_ports[5], self.rear_ports[1], cable4, self.rear_ports[4], self.front_ports[16], + cable3, self.rear_ports[0], self.front_ports[1], cable2 + ) + ) + self.assertEqual(CablePath.objects.count(), 4) + + # Delete cable 3 + cable3.delete() + + # Check for four partial paths; one from each interface + self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) + self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) From 46df5a97b22bfd4f3d5bb24525c346f78194f3c6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 30 Sep 2020 17:12:38 -0400 Subject: [PATCH 06/67] Remove extraneous test objects --- netbox/dcim/tests/test_cablepaths.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 767b34594..3d4efae8e 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -18,27 +18,7 @@ class CablePathTestCase(TestCase): device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') device = Device.objects.create(site=site, device_type=device_type, device_role=device_role, name='Test Device') - # Create 16 instances of each type of path-terminating component - cls.console_ports = [ - ConsolePort(device=device, name=f'Console Port {i}') - for i in range(1, 17) - ] - ConsolePort.objects.bulk_create(cls.console_ports) - cls.console_server_ports = [ - ConsoleServerPort(device=device, name=f'Console Server Port {i}') - for i in range(1, 17) - ] - ConsoleServerPort.objects.bulk_create(cls.console_server_ports) - cls.power_ports = [ - PowerPort(device=device, name=f'Power Port {i}') - for i in range(1, 17) - ] - PowerPort.objects.bulk_create(cls.power_ports) - cls.power_outlets = [ - PowerOutlet(device=device, name=f'Power Outlet {i}') - for i in range(1, 17) - ] - PowerOutlet.objects.bulk_create(cls.power_outlets) + # Create 16 interfaces for testing cls.interfaces = [ Interface(device=device, name=f'Interface {i}') for i in range(1, 17) From 19a3a4d4ef2b6d5c974c66a7a6babcacf8ce7fce Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Oct 2020 11:30:03 -0400 Subject: [PATCH 07/67] Add GenericRelation to originating cable paths on PathEndpoint --- netbox/dcim/models/device_components.py | 20 ++++++----- netbox/dcim/views.py | 18 ++++++---- netbox/templates/dcim/inc/consoleport.html | 13 +------- .../templates/dcim/inc/consoleserverport.html | 13 +------- .../dcim/inc/endpoint_connection.html | 10 ++++++ netbox/templates/dcim/inc/interface.html | 13 +------- netbox/templates/dcim/inc/poweroutlet.html | 33 ++++++++----------- netbox/templates/dcim/inc/powerport.html | 17 +--------- 8 files changed, 50 insertions(+), 87 deletions(-) create mode 100644 netbox/templates/dcim/inc/endpoint_connection.html diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 12bb224fd..56e8f6fc4 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -252,15 +252,19 @@ class CableTermination(models.Model): return endpoints -class PathEndpoint: +class PathEndpoint(models.Model): + """ + Any object which may serve as either endpoint of a CablePath. + """ + paths = GenericRelation( + to='dcim.CablePath', + content_type_field='origin_type', + object_id_field='origin_id', + related_query_name='%(class)s' + ) - def get_connections(self): - from dcim.models import CablePath - return CablePath.objects.filter( - origin_type=ContentType.objects.get_for_model(self), - origin_id=self.pk, - destination_id__isnull=False - ) + class Meta: + abstract = True # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ce204bee0..58be5d213 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -32,7 +32,7 @@ from . import filters, forms, tables from .choices import DeviceFaceChoices from .constants import NONCONNECTABLE_IFACE_TYPES from .models import ( - Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, + Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, @@ -1018,32 +1018,36 @@ class DeviceView(ObjectView): # Console ports consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - 'connected_endpoint__device', 'cable', + Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), + 'cable', ) # Console server ports consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter( device=device ).prefetch_related( - 'connected_endpoint__device', 'cable', + 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( - '_connected_poweroutlet__device', 'cable', + 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( - 'connected_endpoint__device', 'cable', 'power_port', + 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('ip_addresses', queryset=IPAddress.objects.restrict(request.user)), Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)), - 'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable', - 'cable__termination_a', 'cable__termination_b', 'tags' + 'lag', 'cable', 'tags', ) # Front ports diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index 6fa5e8b91..dc0ff384c 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -36,18 +36,7 @@ {# Connection #} - {% if cp.connected_endpoint %} - - {{ cp.connected_endpoint.device }} - - - {{ cp.connected_endpoint }} - - {% else %} - - Not connected - - {% endif %} + {% include 'dcim/inc/endpoint_connection.html' with paths=cp.paths.all %} {# Actions #} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index fca1fa5f4..0af64b4c1 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -38,18 +38,7 @@ {# Connection #} - {% if csp.connected_endpoint %} - - {{ csp.connected_endpoint.device }} - - - {{ csp.connected_endpoint }} - - {% else %} - - Not connected - - {% endif %} + {% include 'dcim/inc/endpoint_connection.html' with paths=csp.paths.all %} {# Actions #} diff --git a/netbox/templates/dcim/inc/endpoint_connection.html b/netbox/templates/dcim/inc/endpoint_connection.html new file mode 100644 index 000000000..07d73a534 --- /dev/null +++ b/netbox/templates/dcim/inc/endpoint_connection.html @@ -0,0 +1,10 @@ +{% if paths|length > 1 %} + Multiple connections +{% elif paths %} + {% with endpoint=paths.0.destination %} + {{ endpoint.parent }} + {{ endpoint }} + {% endwith %} +{% else %} + Not connected +{% endif %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 706801dd1..ae1363dba 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -76,18 +76,7 @@ {% elif iface.is_wireless %} Wireless interface {% else %} - {% with path_count=iface.get_connections.count %} - {% if path_count > 1 %} - Multiple connections - {% elif path_count %} - {% with endpoint=iface.get_connections.first.destination %} - {{ endpoint.parent }} - {{ endpoint }} - {% endwith %} - {% else %} - Not connected - {% endif %} - {% endwith %} + {% include 'dcim/inc/endpoint_connection.html' with paths=iface.paths.all %} {% endif %} {# Buttons #} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 5800f4b48..39af6828d 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -49,27 +49,20 @@ {# Connection #} - {% if po.connected_endpoint %} - {% with pp=po.connected_endpoint %} - - {{ pp.device }} - - - {{ pp }} - - - {% if pp.allocated_draw %} - {{ pp.allocated_draw }}W{% if pp.maximum_draw %} ({{ pp.maximum_draw }}W max){% endif %} - {% elif pp.maximum_draw %} - {{ pp.maximum_draw }}W - {% endif %} - - {% endwith %} - {% else %} - - Not connected + {% with paths=po.paths.all %} + {% include 'dcim/inc/endpoint_connection.html' %} + + {% if paths|length == 1 %} + {% with pp=paths.0.destination %} + {% if pp.allocated_draw %} + {{ pp.allocated_draw }}W{% if pp.maximum_draw %} ({{ pp.maximum_draw }}W max){% endif %} + {% elif pp.maximum_draw %} + {{ pp.maximum_draw }}W + {% endif %} + {% endwith %} + {% endif %} - {% endif %} + {% endwith %} {# Actions #} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index b30fc8456..4ec1b786e 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -45,22 +45,7 @@ {# Connection #} - {% if pp.connected_endpoint.device %} - - {{ pp.connected_endpoint.device }} - - - {{ pp.connected_endpoint }} - - {% elif pp.connected_endpoint %} - - {{ pp.connected_endpoint }} - - {% else %} - - Not connected - - {% endif %} + {% include 'dcim/inc/endpoint_connection.html' with paths=pp.paths.all %} {# Actions #} From 105c0fd3d282bee4ab1602fc72190fc764811465 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Oct 2020 12:18:42 -0400 Subject: [PATCH 08/67] Introduce retrace_paths management command --- netbox/dcim/management/__init__.py | 0 netbox/dcim/management/commands/__init__.py | 0 .../dcim/management/commands/retrace_paths.py | 67 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 netbox/dcim/management/__init__.py create mode 100644 netbox/dcim/management/commands/__init__.py create mode 100644 netbox/dcim/management/commands/retrace_paths.py diff --git a/netbox/dcim/management/__init__.py b/netbox/dcim/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/dcim/management/commands/__init__.py b/netbox/dcim/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/dcim/management/commands/retrace_paths.py b/netbox/dcim/management/commands/retrace_paths.py new file mode 100644 index 000000000..76c29e89c --- /dev/null +++ b/netbox/dcim/management/commands/retrace_paths.py @@ -0,0 +1,67 @@ +from django.contrib.contenttypes.models import ContentType +from django.core.management.base import BaseCommand +from django.core.management.color import no_style +from django.db import connection +from django.db.models import Q + +from dcim.models import CablePath, Interface +from dcim.signals import create_cablepaths + +ENDPOINT_MODELS = ( + 'circuits.CircuitTermination', + 'dcim.ConsolePort', + 'dcim.ConsoleServerPort', + 'dcim.Interface', + 'dcim.PowerOutlet', + 'dcim.PowerPort', +) + + +class Command(BaseCommand): + help = "Recalculate natural ordering values for the specified models" + + def add_arguments(self, parser): + parser.add_argument( + 'args', metavar='app_label.ModelName', nargs='*', + help='One or more specific models (each prefixed with its app_label) to retrace', + ) + + def _get_content_types(self, model_names): + q = Q() + for model_name in model_names: + app_label, model = model_name.split('.') + q |= Q(app_label=app_label, model=model) + return ContentType.objects.filter(q) + + def handle(self, *model_names, **options): + # Determine the models for which we're retracing all paths + origin_types = self._get_content_types(model_names or ENDPOINT_MODELS) + self.stdout.write(f"Retracing paths for models: {', '.join([str(ct) for ct in origin_types])}") + + # Delete all existing CablePath instances + self.stdout.write(f"Deleting existing cable paths...") + deleted_count, _ = CablePath.objects.filter(origin_type__in=origin_types).delete() + self.stdout.write((self.style.SUCCESS(f' Deleted {deleted_count} paths'))) + + # Reset the SQL sequence. Can do this only if deleting _all_ CablePaths. + if not CablePath.objects.count(): + self.stdout.write(f'Resetting database sequence for CablePath...') + sequence_sql = connection.ops.sequence_reset_sql(no_style(), [CablePath]) + with connection.cursor() as cursor: + for sql in sequence_sql: + cursor.execute(sql) + self.stdout.write(self.style.SUCCESS(' Success.')) + + # Retrace interfaces + for ct in origin_types: + model = ct.model_class() + origins = model.objects.filter(cable__isnull=False) + print(f'Retracing {origins.count()} cabled {model._meta.verbose_name_plural}...') + i = 0 + for i, obj in enumerate(origins, start=1): + create_cablepaths(obj) + if not i % 1000: + self.stdout.write(f' {i}') + self.stdout.write(self.style.SUCCESS(f' Retraced {i} {model._meta.verbose_name_plural}')) + + self.stdout.write(self.style.SUCCESS('Finished.')) From 8abc05544cb2fbcbbf919b9546af2400b6123dde Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Oct 2020 13:05:00 -0400 Subject: [PATCH 09/67] CircuitTermination and PowerFeed are path endpoints --- netbox/circuits/models.py | 4 ++-- netbox/dcim/management/commands/retrace_paths.py | 1 + netbox/dcim/models/power.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 408a53c3c..686ab9219 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -4,7 +4,7 @@ from taggit.managers import TaggableManager from dcim.constants import CONNECTION_STATUS_CHOICES from dcim.fields import ASNField -from dcim.models import CableTermination +from dcim.models import CableTermination, PathEndpoint from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem from extras.utils import extras_features from utilities.querysets import RestrictedQuerySet @@ -232,7 +232,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): return self._get_termination('Z') -class CircuitTermination(CableTermination): +class CircuitTermination(PathEndpoint, CableTermination): circuit = models.ForeignKey( to='circuits.Circuit', on_delete=models.CASCADE, diff --git a/netbox/dcim/management/commands/retrace_paths.py b/netbox/dcim/management/commands/retrace_paths.py index 76c29e89c..b60537b37 100644 --- a/netbox/dcim/management/commands/retrace_paths.py +++ b/netbox/dcim/management/commands/retrace_paths.py @@ -12,6 +12,7 @@ ENDPOINT_MODELS = ( 'dcim.ConsolePort', 'dcim.ConsoleServerPort', 'dcim.Interface', + 'dcim.PowerFeed', 'dcim.PowerOutlet', 'dcim.PowerPort', ) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index f55d077a4..caa22e74a 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -10,7 +10,7 @@ from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem from extras.utils import extras_features from utilities.querysets import RestrictedQuerySet from utilities.validators import ExclusionValidator -from .device_components import CableTermination +from .device_components import CableTermination, PathEndpoint __all__ = ( 'PowerFeed', @@ -73,7 +73,7 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel): @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') -class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): +class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldModel): """ An electrical circuit delivered from a PowerPanel. """ From cd398b15d83107ba5ee6f296d566f1b9d7a21622 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Oct 2020 13:09:29 -0400 Subject: [PATCH 10/67] retrace_paths should ignore case in model names --- netbox/dcim/management/commands/retrace_paths.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/management/commands/retrace_paths.py b/netbox/dcim/management/commands/retrace_paths.py index b60537b37..a27833d62 100644 --- a/netbox/dcim/management/commands/retrace_paths.py +++ b/netbox/dcim/management/commands/retrace_paths.py @@ -4,7 +4,7 @@ from django.core.management.color import no_style from django.db import connection from django.db.models import Q -from dcim.models import CablePath, Interface +from dcim.models import CablePath from dcim.signals import create_cablepaths ENDPOINT_MODELS = ( @@ -31,7 +31,7 @@ class Command(BaseCommand): q = Q() for model_name in model_names: app_label, model = model_name.split('.') - q |= Q(app_label=app_label, model=model) + q |= Q(app_label__iexact=app_label, model__iexact=model) return ContentType.objects.filter(q) def handle(self, *model_names, **options): From 610420c0205089b424d5073cdecb87c40262e6f9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Oct 2020 14:16:43 -0400 Subject: [PATCH 11/67] Drop support for split paths --- netbox/dcim/api/serializers.py | 21 +++--- .../dcim/management/commands/retrace_paths.py | 4 +- netbox/dcim/models/device_components.py | 12 +++- netbox/dcim/signals.py | 13 ++-- netbox/dcim/tests/test_cablepaths.py | 68 ++++++------------- netbox/dcim/utils.py | 19 ++---- netbox/dcim/views.py | 10 +-- netbox/templates/dcim/inc/consoleport.html | 2 +- .../templates/dcim/inc/consoleserverport.html | 2 +- .../dcim/inc/endpoint_connection.html | 6 +- netbox/templates/dcim/inc/interface.html | 2 +- netbox/templates/dcim/inc/poweroutlet.html | 2 +- netbox/templates/dcim/inc/powerport.html | 2 +- 13 files changed, 66 insertions(+), 97 deletions(-) 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 #} From c974c5687c4ebe90f6f15baa32692f6620bf073a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Oct 2020 16:42:57 -0400 Subject: [PATCH 12/67] Capture path end-to-end status in CablePath --- netbox/dcim/api/serializers.py | 9 ++++++++- netbox/dcim/migrations/0120_cablepath.py | 3 +-- netbox/dcim/models/devices.py | 5 +++++ netbox/dcim/signals.py | 17 +++++++++++------ netbox/dcim/utils.py | 8 ++++++-- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index cc8e6df1f..8078f8819 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -30,7 +30,7 @@ from .nested_serializers import * class ConnectedEndpointSerializer(ValidatedModelSerializer): connected_endpoint_type = serializers.SerializerMethodField(read_only=True) connected_endpoint = serializers.SerializerMethodField(read_only=True) - connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) + connection_status = serializers.SerializerMethodField(read_only=True) def get_connected_endpoint_type(self, obj): if obj.path is not None: @@ -49,6 +49,13 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): return serializer(obj.path.destination, context=context).data return None + # TODO: Tweak the representation for this field + @swagger_serializer_method(serializer_or_field=serializers.BooleanField) + def get_connection_status(self, obj): + if obj.path is not None: + return obj.path.is_connected + return None + # # Regions/sites diff --git a/netbox/dcim/migrations/0120_cablepath.py b/netbox/dcim/migrations/0120_cablepath.py index 4e36c31d0..2cb8376b7 100644 --- a/netbox/dcim/migrations/0120_cablepath.py +++ b/netbox/dcim/migrations/0120_cablepath.py @@ -1,5 +1,3 @@ -# Generated by Django 3.1 on 2020-09-30 18:09 - import dcim.fields from django.db import migrations, models import django.db.models.deletion @@ -22,6 +20,7 @@ class Migration(migrations.Migration): ('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)), ('destination_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), ('origin_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), + ('is_connected', models.BooleanField(default=False)), ], ), ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 162dcb831..3b13b1f73 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -992,6 +992,8 @@ class Cable(ChangeLoggedModel, CustomFieldModel): instance._orig_termination_b_type_id = instance.termination_b_type_id instance._orig_termination_b_id = instance.termination_b_id + instance._orig_status = instance.status + return instance def __str__(self): @@ -1188,6 +1190,9 @@ class CablePath(models.Model): fk_field='destination_id' ) path = PathField() + is_connected = models.BooleanField( + default=False + ) objects = CablePathManager() diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 46a2cf1d3..0c5da6160 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -5,6 +5,7 @@ from django.db.models.signals import post_save, pre_delete from django.db import transaction from django.dispatch import receiver +from .choices import CableStatusChoices from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis from .utils import object_to_path_node, trace_path @@ -13,9 +14,9 @@ def create_cablepath(node): """ Create CablePaths for all paths originating from the specified node. """ - path, destination = trace_path(node) + path, destination, is_connected = trace_path(node) if path: - cp = CablePath(origin=node, path=path, destination=destination) + cp = CablePath(origin=node, path=path, destination=destination, is_connected=is_connected) cp.save() @@ -80,10 +81,14 @@ def update_connected_endpoints(instance, created, **kwargs): create_cablepath(termination) else: rebuild_paths(termination) - else: - # We currently don't support modifying either termination of an existing Cable. This - # may change in the future. - pass + elif instance.status != instance._orig_status: + # We currently don't support modifying either termination of an existing Cable. (This + # may change in the future.) However, we do need to capture status changes and update + # any CablePaths accordingly. + if instance.status != CableStatusChoices.STATUS_CONNECTED: + CablePath.objects.filter(path__contains=object_to_path_node(instance)).update(is_connected=False) + else: + rebuild_paths(instance) @receiver(pre_delete, sender=Cable) diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 59ca59bfc..f97a1e8f0 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -1,5 +1,6 @@ from django.contrib.contenttypes.models import ContentType +from .choices import CableStatusChoices from .exceptions import CableTraceSplit from .models import FrontPort, RearPort @@ -22,11 +23,14 @@ def trace_path(node): destination = None path = [] position_stack = [] + is_connected = True if node.cable is None: - return [], None + return [], None, False while node.cable is not None: + if node.cable.status != CableStatusChoices.STATUS_CONNECTED: + is_connected = False # Follow the cable to its far-end termination path.append(object_to_path_node(node.cable)) @@ -59,4 +63,4 @@ def trace_path(node): destination = peer_termination break - return path, destination + return path, destination, is_connected From 0d07b0346b05064b30c472e75bf152f6f7395699 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Oct 2020 16:53:13 -0400 Subject: [PATCH 13/67] Add test for connecting cables out of order --- netbox/dcim/tests/test_cablepaths.py | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 68121e671..529935eb3 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -376,3 +376,43 @@ class CablePathTestCase(TestCase): # Check for four partial paths; one from each interface self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) + + def test_06_interface_to_interface_via_existing_cable(self): + """ + [IF1] --C1-- [FP5] [RP5] --C2-- [RP6] [FP6] --C3-- [IF2] + """ + # Create cable 2 + cable2 = Cable(termination_a=self.rear_ports[4], termination_b=self.rear_ports[5]) + cable2.save() + self.assertEqual(CablePath.objects.count(), 0) + + # Create cable1 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[16]) + cable1.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=None, + path=(cable1, self.front_ports[16], self.rear_ports[4], cable2, self.rear_ports[5], self.front_ports[17]) + ) + self.assertEqual(CablePath.objects.count(), 1) + + # Create cable 3 + cable3 = Cable(termination_a=self.front_ports[17], termination_b=self.interfaces[1]) + cable3.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[1], + path=( + cable1, self.front_ports[16], self.rear_ports[4], cable2, self.rear_ports[5], self.front_ports[17], + cable3, + ) + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[0], + path=( + cable3, self.front_ports[17], self.rear_ports[5], cable2, self.rear_ports[4], self.front_ports[16], + cable1, + ) + ) + self.assertEqual(CablePath.objects.count(), 2) From 3b0a75edf85d4b161f5a174a7ad82109cd8979c5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Oct 2020 17:25:44 -0400 Subject: [PATCH 14/67] Add test for updated paths on cable status change --- netbox/dcim/models/devices.py | 5 +- netbox/dcim/signals.py | 2 +- netbox/dcim/tests/test_cablepaths.py | 128 +++++++++++++++++++++------ netbox/dcim/utils.py | 3 + 4 files changed, 109 insertions(+), 29 deletions(-) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 3b13b1f73..0cb1ea970 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -980,6 +980,9 @@ class Cable(ChangeLoggedModel, CustomFieldModel): # A copy of the PK to be used by __str__ in case the object is deleted self._pk = self.pk + # Cache the original status so we can check later if it's been changed + self._orig_status = self.status + @classmethod def from_db(cls, db, field_names, values): """ @@ -992,8 +995,6 @@ class Cable(ChangeLoggedModel, CustomFieldModel): instance._orig_termination_b_type_id = instance.termination_b_type_id instance._orig_termination_b_id = instance.termination_b_id - instance._orig_status = instance.status - return instance def __str__(self): diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 0c5da6160..9b0493e34 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -86,7 +86,7 @@ def update_connected_endpoints(instance, created, **kwargs): # may change in the future.) However, we do need to capture status changes and update # any CablePaths accordingly. if instance.status != CableStatusChoices.STATUS_CONNECTED: - CablePath.objects.filter(path__contains=object_to_path_node(instance)).update(is_connected=False) + CablePath.objects.filter(path__contains=[object_to_path_node(instance)]).update(is_connected=False) else: rebuild_paths(instance) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 529935eb3..362b3804b 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2,6 +2,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from circuits.models import * +from dcim.choices import CableStatusChoices from dcim.models import * from dcim.utils import objects_to_path @@ -70,13 +71,14 @@ class CablePathTestCase(TestCase): ] CircuitTermination.objects.bulk_create(cls.circuit_terminations) - def assertPathExists(self, origin, destination, path=None, msg=None): + def assertPathExists(self, origin, destination, path=None, is_connected=None, msg=None): """ Assert that a CablePath from origin to destination with a specific intermediate path exists. :param origin: Originating endpoint :param destination: Terminating endpoint, or None :param path: Sequence of objects comprising the intermediate path (optional) + :param is_connected: Boolean indicating whether the end-to-end path is complete and active (optional) :param msg: Custom failure message (optional) """ kwargs = { @@ -91,6 +93,8 @@ class CablePathTestCase(TestCase): kwargs['destination_id__isnull'] = True if path is not None: kwargs['path'] = objects_to_path(*path) + if is_connected is not None: + kwargs['is_connected'] = is_connected if msg is None: if destination is not None: msg = f"Missing path from {origin} to {destination}" @@ -108,12 +112,14 @@ class CablePathTestCase(TestCase): self.assertPathExists( origin=self.interfaces[0], destination=self.interfaces[1], - path=(cable1,) + path=(cable1,), + is_connected=True ) self.assertPathExists( origin=self.interfaces[1], destination=self.interfaces[0], - path=(cable1,) + path=(cable1,), + is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) @@ -133,7 +139,8 @@ class CablePathTestCase(TestCase): self.assertPathExists( origin=self.interfaces[0], destination=None, - path=(cable1, self.front_ports[16], self.rear_ports[4]) + path=(cable1, self.front_ports[16], self.rear_ports[4]), + is_connected=False ) self.assertEqual(CablePath.objects.count(), 1) @@ -143,12 +150,14 @@ class CablePathTestCase(TestCase): self.assertPathExists( origin=self.interfaces[0], destination=self.interfaces[1], - path=(cable1, self.front_ports[16], self.rear_ports[4], cable2) + path=(cable1, self.front_ports[16], self.rear_ports[4], cable2), + is_connected=True ) self.assertPathExists( origin=self.interfaces[1], destination=self.interfaces[0], - path=(cable2, self.rear_ports[4], self.front_ports[16], cable1) + path=(cable2, self.rear_ports[4], self.front_ports[16], cable1), + is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) @@ -157,7 +166,8 @@ class CablePathTestCase(TestCase): self.assertPathExists( origin=self.interfaces[0], destination=None, - path=(cable1, self.front_ports[16], self.rear_ports[4]) + path=(cable1, self.front_ports[16], self.rear_ports[4]), + is_connected=False ) self.assertEqual(CablePath.objects.count(), 1) @@ -174,12 +184,14 @@ class CablePathTestCase(TestCase): self.assertPathExists( origin=self.interfaces[0], destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0]) + path=(cable1, self.front_ports[0], self.rear_ports[0]), + is_connected=False ) self.assertPathExists( origin=self.interfaces[1], destination=None, - path=(cable2, self.front_ports[1], self.rear_ports[0]) + path=(cable2, self.front_ports[1], self.rear_ports[0]), + is_connected=False ) self.assertEqual(CablePath.objects.count(), 2) @@ -189,12 +201,14 @@ class CablePathTestCase(TestCase): self.assertPathExists( origin=self.interfaces[0], destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4]) + path=(cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4]), + is_connected=False ) self.assertPathExists( origin=self.interfaces[1], destination=None, - path=(cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5]) + path=(cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5]), + is_connected=False ) self.assertEqual(CablePath.objects.count(), 2) @@ -209,7 +223,8 @@ class CablePathTestCase(TestCase): path=( cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4], cable4, - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[1], @@ -217,7 +232,8 @@ class CablePathTestCase(TestCase): path=( cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5], cable5, - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[2], @@ -225,7 +241,8 @@ class CablePathTestCase(TestCase): path=( cable4, self.front_ports[4], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[0], cable1 - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[3], @@ -233,7 +250,8 @@ class CablePathTestCase(TestCase): path=( cable5, self.front_ports[5], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[1], cable2 - ) + ), + is_connected=True ) self.assertEqual(CablePath.objects.count(), 4) @@ -277,7 +295,8 @@ class CablePathTestCase(TestCase): cable1, self.front_ports[0], self.rear_ports[0], cable3, self.front_ports[4], self.rear_ports[1], cable4, self.rear_ports[2], self.front_ports[8], cable5, self.rear_ports[3], self.front_ports[12], cable6 - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[1], @@ -286,7 +305,8 @@ class CablePathTestCase(TestCase): cable2, self.front_ports[1], self.rear_ports[0], cable3, self.front_ports[4], self.rear_ports[1], cable4, self.rear_ports[2], self.front_ports[8], cable5, self.rear_ports[3], self.front_ports[13], cable7 - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[2], @@ -295,7 +315,8 @@ class CablePathTestCase(TestCase): cable6, self.front_ports[12], self.rear_ports[3], cable5, self.front_ports[8], self.rear_ports[2], cable4, self.rear_ports[1], self.front_ports[4], cable3, self.rear_ports[0], self.front_ports[0], cable1 - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[3], @@ -304,7 +325,8 @@ class CablePathTestCase(TestCase): cable7, self.front_ports[13], self.rear_ports[3], cable5, self.front_ports[8], self.rear_ports[2], cable4, self.rear_ports[1], self.front_ports[4], cable3, self.rear_ports[0], self.front_ports[1], cable2 - ) + ), + is_connected=True ) self.assertEqual(CablePath.objects.count(), 4) @@ -342,7 +364,8 @@ class CablePathTestCase(TestCase): path=( cable1, self.front_ports[0], self.rear_ports[0], cable3, self.front_ports[16], self.rear_ports[4], cable4, self.rear_ports[1], self.front_ports[4], cable5 - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[1], @@ -350,7 +373,8 @@ class CablePathTestCase(TestCase): path=( cable2, self.front_ports[1], self.rear_ports[0], cable3, self.front_ports[16], self.rear_ports[4], cable4, self.rear_ports[1], self.front_ports[5], cable6 - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[2], @@ -358,7 +382,8 @@ class CablePathTestCase(TestCase): path=( cable5, self.front_ports[4], self.rear_ports[1], cable4, self.rear_ports[4], self.front_ports[16], cable3, self.rear_ports[0], self.front_ports[0], cable1 - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[3], @@ -366,7 +391,8 @@ class CablePathTestCase(TestCase): path=( cable6, self.front_ports[5], self.rear_ports[1], cable4, self.rear_ports[4], self.front_ports[16], cable3, self.rear_ports[0], self.front_ports[1], cable2 - ) + ), + is_connected=True ) self.assertEqual(CablePath.objects.count(), 4) @@ -392,7 +418,8 @@ class CablePathTestCase(TestCase): self.assertPathExists( origin=self.interfaces[0], destination=None, - path=(cable1, self.front_ports[16], self.rear_ports[4], cable2, self.rear_ports[5], self.front_ports[17]) + path=(cable1, self.front_ports[16], self.rear_ports[4], cable2, self.rear_ports[5], self.front_ports[17]), + is_connected=False ) self.assertEqual(CablePath.objects.count(), 1) @@ -405,7 +432,8 @@ class CablePathTestCase(TestCase): path=( cable1, self.front_ports[16], self.rear_ports[4], cable2, self.rear_ports[5], self.front_ports[17], cable3, - ) + ), + is_connected=True ) self.assertPathExists( origin=self.interfaces[1], @@ -413,6 +441,54 @@ class CablePathTestCase(TestCase): path=( cable3, self.front_ports[17], self.rear_ports[5], cable2, self.rear_ports[4], self.front_ports[16], cable1, - ) + ), + is_connected=True + ) + self.assertEqual(CablePath.objects.count(), 2) + + def test_07_change_cable_status(self): + """ + [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] + """ + # Create cables 1 and 2 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[16]) + cable1.save() + cable2 = Cable(termination_a=self.rear_ports[4], termination_b=self.interfaces[1]) + cable2.save() + self.assertEqual(CablePath.objects.filter(is_connected=True).count(), 2) + self.assertEqual(CablePath.objects.count(), 2) + + # Change cable 2's status to "planned" + cable2.status = CableStatusChoices.STATUS_PLANNED + cable2.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[1], + path=(cable1, self.front_ports[16], self.rear_ports[4], cable2), + is_connected=False + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[0], + path=(cable2, self.rear_ports[4], self.front_ports[16], cable1), + is_connected=False + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Change cable 2's status to "connected" + cable2 = Cable.objects.get(pk=cable2.pk) + cable2.status = CableStatusChoices.STATUS_CONNECTED + cable2.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[1], + path=(cable1, self.front_ports[16], self.rear_ports[4], cable2), + is_connected=True + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[0], + path=(cable2, self.rear_ports[4], self.front_ports[16], cable1), + is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index f97a1e8f0..16d0753ba 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -63,4 +63,7 @@ def trace_path(node): destination = peer_termination break + if destination is None: + is_connected = False + return path, destination, is_connected From d50a0d94be887effd3f60faaa1b703759afca778 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 09:54:12 -0400 Subject: [PATCH 15/67] Add tests for multiple pass-through breakouts --- netbox/dcim/tests/test_cablepaths.py | 80 ++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 362b3804b..a851a010b 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -337,7 +337,81 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) - def test_05_interfaces_to_interfaces_via_patched_pass_throughs(self): + def test_05_interfaces_to_interfaces_via_multiple_pass_throughs(self): + """ + [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [FP3:1] [RP3] --C6-- [RP4] [FP4:1] --C7-- [IF3] + [IF2] --C2-- [FP1:2] [FP2:1] --C5-- [FP3:1] [FP4:2] --C8-- [IF4] + """ + # Create cables 1-3, 6-8 + cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + cable1.save() + cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) + cable2.save() + cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.rear_ports[1]) + cable3.save() + cable6 = Cable(termination_a=self.rear_ports[2], termination_b=self.rear_ports[3]) + cable6.save() + cable7 = Cable(termination_a=self.interfaces[2], termination_b=self.front_ports[12]) + cable7.save() + cable8 = Cable(termination_a=self.interfaces[3], termination_b=self.front_ports[13]) + cable8.save() + self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface + + # Create cables 4 and 5 + cable4 = Cable(termination_a=self.front_ports[4], termination_b=self.front_ports[8]) + cable4.save() + cable5 = Cable(termination_a=self.front_ports[5], termination_b=self.front_ports[9]) + cable5.save() + self.assertPathExists( + origin=self.interfaces[0], + destination=self.interfaces[2], + path=( + cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4], + cable4, self.front_ports[8], self.rear_ports[2], cable6, self.rear_ports[3], self.front_ports[12], + cable7 + ), + is_connected=True + ) + self.assertPathExists( + origin=self.interfaces[1], + destination=self.interfaces[3], + path=( + cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5], + cable5, self.front_ports[9], self.rear_ports[2], cable6, self.rear_ports[3], self.front_ports[13], + cable8 + ), + is_connected=True + ) + self.assertPathExists( + origin=self.interfaces[2], + destination=self.interfaces[0], + path=( + cable7, self.front_ports[12], self.rear_ports[3], cable6, self.rear_ports[2], self.front_ports[8], + cable4, self.front_ports[4], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[0], + cable1 + ), + is_connected=True + ) + self.assertPathExists( + origin=self.interfaces[3], + destination=self.interfaces[1], + path=( + cable8, self.front_ports[13], self.rear_ports[3], cable6, self.rear_ports[2], self.front_ports[9], + cable5, self.front_ports[5], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[1], + cable2 + ), + is_connected=True + ) + self.assertEqual(CablePath.objects.count(), 4) + + # Delete cable 5 + cable5.delete() + + # Check for two complete paths (IF1 <--> IF2) and two partial (IF3 <--> IF4) + self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 2) + self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 2) + + def test_06_interfaces_to_interfaces_via_patched_pass_throughs(self): """ [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP5] [RP5] --C4-- [RP2] [FP2:1] --C5-- [IF3] [IF2] --C2-- [FP1:2] [FP2:2] --C6-- [IF4] @@ -403,7 +477,7 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) - def test_06_interface_to_interface_via_existing_cable(self): + def test_07_interface_to_interface_via_existing_cable(self): """ [IF1] --C1-- [FP5] [RP5] --C2-- [RP6] [FP6] --C3-- [IF2] """ @@ -446,7 +520,7 @@ class CablePathTestCase(TestCase): ) self.assertEqual(CablePath.objects.count(), 2) - def test_07_change_cable_status(self): + def test_08_change_cable_status(self): """ [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] """ From 9d10c57dc9a892ad8c6bebfb6ff881d58720ed43 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 09:55:28 -0400 Subject: [PATCH 16/67] Remove legacy CablePathTestCase --- netbox/dcim/tests/test_models.py | 625 ------------------------------- 1 file changed, 625 deletions(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index c55d099c9..83438a609 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -561,628 +561,3 @@ class CableTestCase(TestCase): cable = Cable(termination_a=self.interface2, termination_b=wireless_interface) with self.assertRaises(ValidationError): cable.clean() - - -class CablePathTestCase(TestCase): - - @classmethod - def setUpTestData(cls): - - site = Site.objects.create(name='Site 1', slug='site-1') - manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetype = DeviceType.objects.create( - manufacturer=manufacturer, model='Device Type 1', slug='device-type-1' - ) - devicerole = DeviceRole.objects.create( - name='Device Role 1', slug='device-role-1', color='ff0000' - ) - provider = Provider.objects.create(name='Provider 1', slug='provider-1') - circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') - circuit = Circuit.objects.create(provider=provider, type=circuittype, cid='1') - CircuitTermination.objects.bulk_create(( - CircuitTermination(circuit=circuit, site=site, term_side='A', port_speed=1000), - CircuitTermination(circuit=circuit, site=site, term_side='Z', port_speed=1000), - )) - - # Create four network devices with four interfaces each - devices = ( - Device(device_type=devicetype, device_role=devicerole, name='Device 1', site=site), - Device(device_type=devicetype, device_role=devicerole, name='Device 2', site=site), - Device(device_type=devicetype, device_role=devicerole, name='Device 3', site=site), - Device(device_type=devicetype, device_role=devicerole, name='Device 4', site=site), - ) - Device.objects.bulk_create(devices) - for device in devices: - Interface.objects.bulk_create(( - Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), - Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), - Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), - Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED), - )) - - # Create four patch panels, each with one rear port and four front ports - patch_panels = ( - Device(device_type=devicetype, device_role=devicerole, name='Panel 1', site=site), - Device(device_type=devicetype, device_role=devicerole, name='Panel 2', site=site), - Device(device_type=devicetype, device_role=devicerole, name='Panel 3', site=site), - Device(device_type=devicetype, device_role=devicerole, name='Panel 4', site=site), - Device(device_type=devicetype, device_role=devicerole, name='Panel 5', site=site), - Device(device_type=devicetype, device_role=devicerole, name='Panel 6', site=site), - ) - Device.objects.bulk_create(patch_panels) - - # Create patch panels with 4 positions - for patch_panel in patch_panels[:4]: - rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C) - FrontPort.objects.bulk_create(( - FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C), - FrontPort(device=patch_panel, name='Front Port 2', rear_port=rearport, rear_port_position=2, type=PortTypeChoices.TYPE_8P8C), - FrontPort(device=patch_panel, name='Front Port 3', rear_port=rearport, rear_port_position=3, type=PortTypeChoices.TYPE_8P8C), - FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C), - )) - - # Create 1-on-1 patch panels - for patch_panel in patch_panels[4:]: - rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=1, type=PortTypeChoices.TYPE_8P8C) - FrontPort.objects.create(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C) - - def test_direct_connection(self): - """ - Test a direct connection between two interfaces. - - [Device 1] ----- [Device 2] - Iface1 Iface1 - """ - # Create cable - cable = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') - ) - cable.full_clean() - cable.save() - - # Retrieve endpoints - endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') - endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - - # Delete cable - cable.delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - - def test_connection_via_single_rear_port(self): - """ - Test a connection which passes through a rear port with exactly one front port. - - 1 2 - [Device 1] ----- [Panel 5] ----- [Device 2] - Iface1 FP1 RP1 Iface1 - """ - # Create cables (FP first, RP second) - cable1 = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1') - ) - cable1.full_clean() - cable1.save() - cable2 = Cable( - termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'), - termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') - ) - self.assertEqual(cable2.termination_a.positions, 1) # Sanity check - cable2.full_clean() - cable2.save() - - # Retrieve endpoints - endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') - endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - - # Delete cable 1 - cable1.delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - - def test_connections_via_nested_single_position_rearport(self): - """ - Test a connection which passes through a single front/rear port pair between two multi-position rear ports. - - Test two connections via patched rear ports: - Device 1 <---> Device 2 - Device 3 <---> Device 4 - - 1 2 - [Device 1] -----------+ +----------- [Device 2] - Iface1 | | Iface1 - FP1 | 3 4 | FP1 - [Panel 1] ----- [Panel 5] ----- [Panel 2] - FP2 | RP1 RP1 FP1 RP1 | FP2 - Iface1 | | Iface1 - [Device 3] -----------+ +----------- [Device 4] - 5 6 - """ - # Create cables (Panel 5 RP first, FP second) - cable1 = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') - ) - cable1.full_clean() - cable1.save() - cable2 = Cable( - termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'), - termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') - ) - cable2.full_clean() - cable2.save() - cable3 = Cable( - termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), - termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1') - ) - cable3.full_clean() - cable3.save() - cable4 = Cable( - termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1'), - termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1') - ) - cable4.full_clean() - cable4.save() - cable5 = Cable( - termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2'), - termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1') - ) - cable5.full_clean() - cable5.save() - cable6 = Cable( - termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'), - termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1') - ) - cable6.full_clean() - cable6.save() - - # Retrieve endpoints - endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') - endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') - endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1') - endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertEqual(endpoint_c.connected_endpoint, endpoint_d) - self.assertEqual(endpoint_d.connected_endpoint, endpoint_c) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - self.assertTrue(endpoint_c.connection_status) - self.assertTrue(endpoint_d.connection_status) - - # Delete cable 3 - cable3.delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - endpoint_c.refresh_from_db() - endpoint_d.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_c.connected_endpoint) - self.assertIsNone(endpoint_d.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - self.assertIsNone(endpoint_c.connection_status) - self.assertIsNone(endpoint_d.connection_status) - - def test_connections_via_patch(self): - """ - Test two connections via patched rear ports: - Device 1 <---> Device 2 - Device 3 <---> Device 4 - - 1 2 - [Device 1] -----------+ +----------- [Device 2] - Iface1 | | Iface1 - FP1 | 3 | FP1 - [Panel 1] ----- [Panel 2] - FP2 | RP1 RP1 | FP2 - Iface1 | | Iface1 - [Device 3] -----------+ +----------- [Device 4] - 4 5 - """ - # Create cables - cable1 = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') - ) - cable1.full_clean() - cable1.save() - cable2 = Cable( - termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1') - ) - cable2.full_clean() - cable2.save() - - cable3 = Cable( - termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), - termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1') - ) - cable3.full_clean() - cable3.save() - - cable4 = Cable( - termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2') - ) - cable4.full_clean() - cable4.save() - cable5 = Cable( - termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2') - ) - cable5.full_clean() - cable5.save() - - # Retrieve endpoints - endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') - endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') - endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1') - endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertEqual(endpoint_c.connected_endpoint, endpoint_d) - self.assertEqual(endpoint_d.connected_endpoint, endpoint_c) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - self.assertTrue(endpoint_c.connection_status) - self.assertTrue(endpoint_d.connection_status) - - # Delete cable 3 - cable3.delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - endpoint_c.refresh_from_db() - endpoint_d.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_c.connected_endpoint) - self.assertIsNone(endpoint_d.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - self.assertIsNone(endpoint_c.connection_status) - self.assertIsNone(endpoint_d.connection_status) - - def test_connections_via_multiple_patches(self): - """ - Test two connections via patched rear ports: - Device 1 <---> Device 2 - Device 3 <---> Device 4 - - 1 2 3 - [Device 1] -----------+ +---------------+ +----------- [Device 2] - Iface1 | | | | Iface1 - FP1 | 4 | FP1 FP1 | 5 | FP1 - [Panel 1] ----- [Panel 2] [Panel 3] ----- [Panel 4] - FP2 | RP1 RP1 | FP2 FP2 | RP1 RP1 | FP2 - Iface1 | | | | Iface1 - [Device 3] -----------+ +---------------+ +----------- [Device 4] - 6 7 8 - """ - # Create cables - cable1 = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') - ) - cable1.full_clean() - cable1.save() - cable2 = Cable( - termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'), - termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1') - ) - cable2.full_clean() - cable2.save() - cable3 = Cable( - termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'), - termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') - ) - cable3.full_clean() - cable3.save() - - cable4 = Cable( - termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), - termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1') - ) - cable4.full_clean() - cable4.save() - cable5 = Cable( - termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'), - termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1') - ) - cable5.full_clean() - cable5.save() - - cable6 = Cable( - termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2') - ) - cable6.full_clean() - cable6.save() - cable7 = Cable( - termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'), - termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2') - ) - cable7.full_clean() - cable7.save() - cable8 = Cable( - termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'), - termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1') - ) - cable8.full_clean() - cable8.save() - - # Retrieve endpoints - endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') - endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') - endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1') - endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertEqual(endpoint_c.connected_endpoint, endpoint_d) - self.assertEqual(endpoint_d.connected_endpoint, endpoint_c) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - self.assertTrue(endpoint_c.connection_status) - self.assertTrue(endpoint_d.connection_status) - - # Delete cables 4 and 5 - cable4.delete() - cable5.delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - endpoint_c.refresh_from_db() - endpoint_d.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_c.connected_endpoint) - self.assertIsNone(endpoint_d.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - self.assertIsNone(endpoint_c.connection_status) - self.assertIsNone(endpoint_d.connection_status) - - def test_connections_via_nested_rear_ports(self): - """ - Test two connections via nested rear ports: - Device 1 <---> Device 2 - Device 3 <---> Device 4 - - 1 2 - [Device 1] -----------+ +----------- [Device 2] - Iface1 | | Iface1 - FP1 | 3 4 5 | FP1 - [Panel 1] ----- [Panel 2] ----- [Panel 3] ----- [Panel 4] - FP2 | RP1 FP1 RP1 RP1 FP1 RP1 | FP2 - Iface1 | | Iface1 - [Device 3] -----------+ +----------- [Device 4] - 6 7 - """ - # Create cables - cable1 = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') - ) - cable1.full_clean() - cable1.save() - cable2 = Cable( - termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'), - termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') - ) - cable2.full_clean() - cable2.save() - - cable3 = Cable( - termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), - termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1') - ) - cable3.full_clean() - cable3.save() - cable4 = Cable( - termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'), - termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1') - ) - cable4.full_clean() - cable4.save() - cable5 = Cable( - termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'), - termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1') - ) - cable5.full_clean() - cable5.save() - - cable6 = Cable( - termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2') - ) - cable6.full_clean() - cable6.save() - cable7 = Cable( - termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'), - termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1') - ) - cable7.full_clean() - cable7.save() - - # Retrieve endpoints - endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') - endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') - endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1') - endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertEqual(endpoint_c.connected_endpoint, endpoint_d) - self.assertEqual(endpoint_d.connected_endpoint, endpoint_c) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - self.assertTrue(endpoint_c.connection_status) - self.assertTrue(endpoint_d.connection_status) - - # Delete cable 4 - cable4.delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - endpoint_c.refresh_from_db() - endpoint_d.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_c.connected_endpoint) - self.assertIsNone(endpoint_d.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - self.assertIsNone(endpoint_c.connection_status) - self.assertIsNone(endpoint_d.connection_status) - - def test_connection_via_circuit(self): - """ - 1 2 - [Device 1] ----- [Circuit] ----- [Device 2] - Iface1 A Z Iface1 - - """ - # Create cables - cable1 = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=CircuitTermination.objects.get(term_side='A') - ) - cable1.full_clean() - cable1.save() - cable2 = Cable( - termination_a=CircuitTermination.objects.get(term_side='Z'), - termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') - ) - cable2.full_clean() - cable2.save() - - # Retrieve endpoints - endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') - endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - - # Delete circuit - circuit = Circuit.objects.first().delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - - def test_connection_via_patched_circuit(self): - """ - 1 2 3 4 - [Device 1] ----- [Panel 5] ----- [Circuit] ----- [Panel 6] ----- [Device 2] - Iface1 FP1 RP1 A Z RP1 FP1 Iface1 - - """ - # Create cables - cable1 = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1') - ) - cable1.full_clean() - cable1.save() - cable2 = Cable( - termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'), - termination_b=CircuitTermination.objects.get(term_side='A') - ) - cable2.full_clean() - cable2.save() - cable3 = Cable( - termination_a=CircuitTermination.objects.get(term_side='Z'), - termination_b=RearPort.objects.get(device__name='Panel 6', name='Rear Port 1') - ) - cable3.full_clean() - cable3.save() - cable4 = Cable( - termination_a=FrontPort.objects.get(device__name='Panel 6', name='Front Port 1'), - termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') - ) - cable4.full_clean() - cable4.save() - - # Retrieve endpoints - endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1') - endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - - # Delete circuit - circuit = Circuit.objects.first().delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) From 9f242216e6e3ea770a68f904e378c00506cb6398 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 10:14:52 -0400 Subject: [PATCH 17/67] Rename test elements to be more readable --- netbox/dcim/tests/test_cablepaths.py | 375 +++++++++++++++------------ 1 file changed, 202 insertions(+), 173 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index a851a010b..53ce82403 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -19,44 +19,73 @@ class CablePathTestCase(TestCase): device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') device = Device.objects.create(site=site, device_type=device_type, device_role=device_role, name='Test Device') - # Create 16 interfaces for testing - cls.interfaces = [ - Interface(device=device, name=f'Interface {i}') - for i in range(1, 17) - ] - Interface.objects.bulk_create(cls.interfaces) + # Create 4 interfaces for testing + cls.interface1 = Interface(device=device, name=f'Interface 1') + cls.interface2 = Interface(device=device, name=f'Interface 2') + cls.interface3 = Interface(device=device, name=f'Interface 3') + cls.interface4 = Interface(device=device, name=f'Interface 4') + Interface.objects.bulk_create([ + cls.interface1, + cls.interface2, + cls.interface3, + cls.interface4 + ]) - # Create four RearPorts with four FrontPorts each, and two with only one position - cls.rear_ports = [ - RearPort(device=device, name=f'RP1', positions=4), - RearPort(device=device, name=f'RP2', positions=4), - RearPort(device=device, name=f'RP3', positions=4), - RearPort(device=device, name=f'RP4', positions=4), - RearPort(device=device, name=f'RP5', positions=1), - RearPort(device=device, name=f'RP6', positions=1), - ] - RearPort.objects.bulk_create(cls.rear_ports) - cls.front_ports = [ - FrontPort(device=device, name=f'FP1:1', rear_port=cls.rear_ports[0], rear_port_position=1), - FrontPort(device=device, name=f'FP1:2', rear_port=cls.rear_ports[0], rear_port_position=2), - FrontPort(device=device, name=f'FP1:3', rear_port=cls.rear_ports[0], rear_port_position=3), - FrontPort(device=device, name=f'FP1:4', rear_port=cls.rear_ports[0], rear_port_position=4), - FrontPort(device=device, name=f'FP2:1', rear_port=cls.rear_ports[1], rear_port_position=1), - FrontPort(device=device, name=f'FP2:2', rear_port=cls.rear_ports[1], rear_port_position=2), - FrontPort(device=device, name=f'FP2:3', rear_port=cls.rear_ports[1], rear_port_position=3), - FrontPort(device=device, name=f'FP2:4', rear_port=cls.rear_ports[1], rear_port_position=4), - FrontPort(device=device, name=f'FP3:1', rear_port=cls.rear_ports[2], rear_port_position=1), - FrontPort(device=device, name=f'FP3:2', rear_port=cls.rear_ports[2], rear_port_position=2), - FrontPort(device=device, name=f'FP3:3', rear_port=cls.rear_ports[2], rear_port_position=3), - FrontPort(device=device, name=f'FP3:4', rear_port=cls.rear_ports[2], rear_port_position=4), - FrontPort(device=device, name=f'FP4:1', rear_port=cls.rear_ports[3], rear_port_position=1), - FrontPort(device=device, name=f'FP4:2', rear_port=cls.rear_ports[3], rear_port_position=2), - FrontPort(device=device, name=f'FP4:3', rear_port=cls.rear_ports[3], rear_port_position=3), - FrontPort(device=device, name=f'FP4:4', rear_port=cls.rear_ports[3], rear_port_position=4), - FrontPort(device=device, name=f'FP5', rear_port=cls.rear_ports[4], rear_port_position=1), - FrontPort(device=device, name=f'FP6', rear_port=cls.rear_ports[5], rear_port_position=1), - ] - FrontPort.objects.bulk_create(cls.front_ports) + # Create four RearPorts with four positions each, and two with only one position + cls.rear_port1 = RearPort(device=device, name=f'RP1', positions=4) + cls.rear_port2 = RearPort(device=device, name=f'RP2', positions=4) + cls.rear_port3 = RearPort(device=device, name=f'RP3', positions=4) + cls.rear_port4 = RearPort(device=device, name=f'RP4', positions=4) + cls.rear_port5 = RearPort(device=device, name=f'RP5', positions=1) + cls.rear_port6 = RearPort(device=device, name=f'RP6', positions=1) + RearPort.objects.bulk_create([ + cls.rear_port1, + cls.rear_port2, + cls.rear_port3, + cls.rear_port4, + cls.rear_port5, + cls.rear_port6 + ]) + + # Create FrontPorts to match RearPorts (4x4 + 2x1) + cls.front_port1_1 = FrontPort(device=device, name=f'FP1:1', rear_port=cls.rear_port1, rear_port_position=1) + cls.front_port1_2 = FrontPort(device=device, name=f'FP1:2', rear_port=cls.rear_port1, rear_port_position=2) + cls.front_port1_3 = FrontPort(device=device, name=f'FP1:3', rear_port=cls.rear_port1, rear_port_position=3) + cls.front_port1_4 = FrontPort(device=device, name=f'FP1:4', rear_port=cls.rear_port1, rear_port_position=4) + cls.front_port2_1 = FrontPort(device=device, name=f'FP2:1', rear_port=cls.rear_port2, rear_port_position=1) + cls.front_port2_2 = FrontPort(device=device, name=f'FP2:2', rear_port=cls.rear_port2, rear_port_position=2) + cls.front_port2_3 = FrontPort(device=device, name=f'FP2:3', rear_port=cls.rear_port2, rear_port_position=3) + cls.front_port2_4 = FrontPort(device=device, name=f'FP2:4', rear_port=cls.rear_port2, rear_port_position=4) + cls.front_port3_1 = FrontPort(device=device, name=f'FP3:1', rear_port=cls.rear_port3, rear_port_position=1) + cls.front_port3_2 = FrontPort(device=device, name=f'FP3:2', rear_port=cls.rear_port3, rear_port_position=2) + cls.front_port3_3 = FrontPort(device=device, name=f'FP3:3', rear_port=cls.rear_port3, rear_port_position=3) + cls.front_port3_4 = FrontPort(device=device, name=f'FP3:4', rear_port=cls.rear_port3, rear_port_position=4) + cls.front_port4_1 = FrontPort(device=device, name=f'FP4:1', rear_port=cls.rear_port4, rear_port_position=1) + cls.front_port4_2 = FrontPort(device=device, name=f'FP4:2', rear_port=cls.rear_port4, rear_port_position=2) + cls.front_port4_3 = FrontPort(device=device, name=f'FP4:3', rear_port=cls.rear_port4, rear_port_position=3) + cls.front_port4_4 = FrontPort(device=device, name=f'FP4:4', rear_port=cls.rear_port4, rear_port_position=4) + cls.front_port5_1 = FrontPort(device=device, name=f'FP5:1', rear_port=cls.rear_port5, rear_port_position=1) + cls.front_port6_1 = FrontPort(device=device, name=f'FP6:1', rear_port=cls.rear_port6, rear_port_position=1) + FrontPort.objects.bulk_create([ + cls.front_port1_1, + cls.front_port1_2, + cls.front_port1_3, + cls.front_port1_4, + cls.front_port2_1, + cls.front_port2_2, + cls.front_port2_3, + cls.front_port2_4, + cls.front_port3_1, + cls.front_port3_2, + cls.front_port3_3, + cls.front_port3_4, + cls.front_port4_1, + cls.front_port4_2, + cls.front_port4_3, + cls.front_port4_4, + cls.front_port5_1, + cls.front_port6_1, + ]) # Create four circuits with two terminations (A and Z) each (8 total) provider = Provider.objects.create(name='Provider', slug='provider') @@ -107,17 +136,17 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [IF2] """ # Create cable 1 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.interfaces[1]) + cable1 = Cable(termination_a=self.interface1, termination_b=self.interface2) cable1.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[1], + origin=self.interface1, + destination=self.interface2, path=(cable1,), is_connected=True ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[0], + origin=self.interface2, + destination=self.interface1, path=(cable1,), is_connected=True ) @@ -134,29 +163,29 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] """ # Create cable 1 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[16]) + cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port5_1) cable1.save() self.assertPathExists( - origin=self.interfaces[0], + origin=self.interface1, destination=None, - path=(cable1, self.front_ports[16], self.rear_ports[4]), + path=(cable1, self.front_port5_1, self.rear_port5), is_connected=False ) self.assertEqual(CablePath.objects.count(), 1) # Create cable 2 - cable2 = Cable(termination_a=self.rear_ports[4], termination_b=self.interfaces[1]) + cable2 = Cable(termination_a=self.rear_port5, termination_b=self.interface2) cable2.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[1], - path=(cable1, self.front_ports[16], self.rear_ports[4], cable2), + origin=self.interface1, + destination=self.interface2, + path=(cable1, self.front_port5_1, self.rear_port5, cable2), is_connected=True ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[0], - path=(cable2, self.rear_ports[4], self.front_ports[16], cable1), + origin=self.interface2, + destination=self.interface1, + path=(cable2, self.rear_port5, self.front_port5_1, cable1), is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) @@ -164,9 +193,9 @@ class CablePathTestCase(TestCase): # Delete cable 2 cable2.delete() self.assertPathExists( - origin=self.interfaces[0], + origin=self.interface1, destination=None, - path=(cable1, self.front_ports[16], self.rear_ports[4]), + path=(cable1, self.front_port5_1, self.rear_port5), is_connected=False ) self.assertEqual(CablePath.objects.count(), 1) @@ -177,78 +206,78 @@ class CablePathTestCase(TestCase): [IF2] --C2-- [FP1:2] [FP2:2] --C5-- [IF4] """ # Create cables 1-2 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) cable1.save() - cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) + cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_2) cable2.save() self.assertPathExists( - origin=self.interfaces[0], + origin=self.interface1, destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0]), + path=(cable1, self.front_port1_1, self.rear_port1), is_connected=False ) self.assertPathExists( - origin=self.interfaces[1], + origin=self.interface2, destination=None, - path=(cable2, self.front_ports[1], self.rear_ports[0]), + path=(cable2, self.front_port1_2, self.rear_port1), is_connected=False ) self.assertEqual(CablePath.objects.count(), 2) # Create cable 3 - cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.rear_ports[1]) + cable3 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2) cable3.save() self.assertPathExists( - origin=self.interfaces[0], + origin=self.interface1, destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4]), + path=(cable1, self.front_port1_1, self.rear_port1, cable3, self.rear_port2, self.front_port2_1), is_connected=False ) self.assertPathExists( - origin=self.interfaces[1], + origin=self.interface2, destination=None, - path=(cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5]), + path=(cable2, self.front_port1_2, self.rear_port1, cable3, self.rear_port2, self.front_port2_2), is_connected=False ) self.assertEqual(CablePath.objects.count(), 2) # Create cables 4-5 - cable4 = Cable(termination_a=self.front_ports[4], termination_b=self.interfaces[2]) + cable4 = Cable(termination_a=self.front_port2_1, termination_b=self.interface3) cable4.save() - cable5 = Cable(termination_a=self.front_ports[5], termination_b=self.interfaces[3]) + cable5 = Cable(termination_a=self.front_port2_2, termination_b=self.interface4) cable5.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[2], + origin=self.interface1, + destination=self.interface3, path=( - cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4], + cable1, self.front_port1_1, self.rear_port1, cable3, self.rear_port2, self.front_port2_1, cable4, ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[3], + origin=self.interface2, + destination=self.interface4, path=( - cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5], + cable2, self.front_port1_2, self.rear_port1, cable3, self.rear_port2, self.front_port2_2, cable5, ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[2], - destination=self.interfaces[0], + origin=self.interface3, + destination=self.interface1, path=( - cable4, self.front_ports[4], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[0], + cable4, self.front_port2_1, self.rear_port2, cable3, self.rear_port1, self.front_port1_1, cable1 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[3], - destination=self.interfaces[1], + origin=self.interface4, + destination=self.interface2, path=( - cable5, self.front_ports[5], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[1], + cable5, self.front_port2_2, self.rear_port2, cable3, self.rear_port1, self.front_port1_2, cable2 ), is_connected=True @@ -268,62 +297,62 @@ class CablePathTestCase(TestCase): [IF2] --C2-- [FP1:2] [FP4:2] --C7-- [IF4] """ # Create cables 1-2, 6-7 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) cable1.save() - cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) + cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_2) cable2.save() - cable6 = Cable(termination_a=self.interfaces[2], termination_b=self.front_ports[12]) + cable6 = Cable(termination_a=self.interface3, termination_b=self.front_port4_1) cable6.save() - cable7 = Cable(termination_a=self.interfaces[3], termination_b=self.front_ports[13]) + cable7 = Cable(termination_a=self.interface4, termination_b=self.front_port4_2) cable7.save() self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface # Create cables 3 and 5 - cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.front_ports[4]) + cable3 = Cable(termination_a=self.rear_port1, termination_b=self.front_port2_1) cable3.save() - cable5 = Cable(termination_a=self.rear_ports[3], termination_b=self.front_ports[8]) + cable5 = Cable(termination_a=self.rear_port4, termination_b=self.front_port3_1) cable5.save() self.assertEqual(CablePath.objects.count(), 4) # Four (longer) partial paths; one from each interface # Create cable 4 - cable4 = Cable(termination_a=self.rear_ports[1], termination_b=self.rear_ports[2]) + cable4 = Cable(termination_a=self.rear_port2, termination_b=self.rear_port3) cable4.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[2], + origin=self.interface1, + destination=self.interface3, path=( - cable1, self.front_ports[0], self.rear_ports[0], cable3, self.front_ports[4], self.rear_ports[1], - cable4, self.rear_ports[2], self.front_ports[8], cable5, self.rear_ports[3], self.front_ports[12], + cable1, self.front_port1_1, self.rear_port1, cable3, self.front_port2_1, self.rear_port2, + cable4, self.rear_port3, self.front_port3_1, cable5, self.rear_port4, self.front_port4_1, cable6 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[3], + origin=self.interface2, + destination=self.interface4, path=( - cable2, self.front_ports[1], self.rear_ports[0], cable3, self.front_ports[4], self.rear_ports[1], - cable4, self.rear_ports[2], self.front_ports[8], cable5, self.rear_ports[3], self.front_ports[13], + cable2, self.front_port1_2, self.rear_port1, cable3, self.front_port2_1, self.rear_port2, + cable4, self.rear_port3, self.front_port3_1, cable5, self.rear_port4, self.front_port4_2, cable7 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[2], - destination=self.interfaces[0], + origin=self.interface3, + destination=self.interface1, path=( - cable6, self.front_ports[12], self.rear_ports[3], cable5, self.front_ports[8], self.rear_ports[2], - cable4, self.rear_ports[1], self.front_ports[4], cable3, self.rear_ports[0], self.front_ports[0], + cable6, self.front_port4_1, self.rear_port4, cable5, self.front_port3_1, self.rear_port3, + cable4, self.rear_port2, self.front_port2_1, cable3, self.rear_port1, self.front_port1_1, cable1 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[3], - destination=self.interfaces[1], + origin=self.interface4, + destination=self.interface2, path=( - cable7, self.front_ports[13], self.rear_ports[3], cable5, self.front_ports[8], self.rear_ports[2], - cable4, self.rear_ports[1], self.front_ports[4], cable3, self.rear_ports[0], self.front_ports[1], + cable7, self.front_port4_2, self.rear_port4, cable5, self.front_port3_1, self.rear_port3, + cable4, self.rear_port2, self.front_port2_1, cable3, self.rear_port1, self.front_port1_2, cable2 ), is_connected=True @@ -343,61 +372,61 @@ class CablePathTestCase(TestCase): [IF2] --C2-- [FP1:2] [FP2:1] --C5-- [FP3:1] [FP4:2] --C8-- [IF4] """ # Create cables 1-3, 6-8 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) + cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) cable1.save() - cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) + cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_2) cable2.save() - cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.rear_ports[1]) + cable3 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2) cable3.save() - cable6 = Cable(termination_a=self.rear_ports[2], termination_b=self.rear_ports[3]) + cable6 = Cable(termination_a=self.rear_port3, termination_b=self.rear_port4) cable6.save() - cable7 = Cable(termination_a=self.interfaces[2], termination_b=self.front_ports[12]) + cable7 = Cable(termination_a=self.interface3, termination_b=self.front_port4_1) cable7.save() - cable8 = Cable(termination_a=self.interfaces[3], termination_b=self.front_ports[13]) + cable8 = Cable(termination_a=self.interface4, termination_b=self.front_port4_2) cable8.save() self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface # Create cables 4 and 5 - cable4 = Cable(termination_a=self.front_ports[4], termination_b=self.front_ports[8]) + cable4 = Cable(termination_a=self.front_port2_1, termination_b=self.front_port3_1) cable4.save() - cable5 = Cable(termination_a=self.front_ports[5], termination_b=self.front_ports[9]) + cable5 = Cable(termination_a=self.front_port2_2, termination_b=self.front_port3_2) cable5.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[2], + origin=self.interface1, + destination=self.interface3, path=( - cable1, self.front_ports[0], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[4], - cable4, self.front_ports[8], self.rear_ports[2], cable6, self.rear_ports[3], self.front_ports[12], + cable1, self.front_port1_1, self.rear_port1, cable3, self.rear_port2, self.front_port2_1, + cable4, self.front_port3_1, self.rear_port3, cable6, self.rear_port4, self.front_port4_1, cable7 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[3], + origin=self.interface2, + destination=self.interface4, path=( - cable2, self.front_ports[1], self.rear_ports[0], cable3, self.rear_ports[1], self.front_ports[5], - cable5, self.front_ports[9], self.rear_ports[2], cable6, self.rear_ports[3], self.front_ports[13], + cable2, self.front_port1_2, self.rear_port1, cable3, self.rear_port2, self.front_port2_2, + cable5, self.front_port3_2, self.rear_port3, cable6, self.rear_port4, self.front_port4_2, cable8 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[2], - destination=self.interfaces[0], + origin=self.interface3, + destination=self.interface1, path=( - cable7, self.front_ports[12], self.rear_ports[3], cable6, self.rear_ports[2], self.front_ports[8], - cable4, self.front_ports[4], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[0], + cable7, self.front_port4_1, self.rear_port4, cable6, self.rear_port3, self.front_port3_1, + cable4, self.front_port2_1, self.rear_port2, cable3, self.rear_port1, self.front_port1_1, cable1 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[3], - destination=self.interfaces[1], + origin=self.interface4, + destination=self.interface2, path=( - cable8, self.front_ports[13], self.rear_ports[3], cable6, self.rear_ports[2], self.front_ports[9], - cable5, self.front_ports[5], self.rear_ports[1], cable3, self.rear_ports[0], self.front_ports[1], + cable8, self.front_port4_2, self.rear_port4, cable6, self.rear_port3, self.front_port3_2, + cable5, self.front_port2_2, self.rear_port2, cable3, self.rear_port1, self.front_port1_2, cable2 ), is_connected=True @@ -417,54 +446,54 @@ class CablePathTestCase(TestCase): [IF2] --C2-- [FP1:2] [FP2:2] --C6-- [IF4] """ # Create cables 1-2, 5-6 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0]) # IF1 -> FP1:1 + cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) # IF1 -> FP1:1 cable1.save() - cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1]) # IF2 -> FP1:2 + cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_2) # IF2 -> FP1:2 cable2.save() - cable5 = Cable(termination_a=self.interfaces[2], termination_b=self.front_ports[4]) # IF3 -> FP2:1 + cable5 = Cable(termination_a=self.interface3, termination_b=self.front_port2_1) # IF3 -> FP2:1 cable5.save() - cable6 = Cable(termination_a=self.interfaces[3], termination_b=self.front_ports[5]) # IF4 -> FP2:2 + cable6 = Cable(termination_a=self.interface4, termination_b=self.front_port2_2) # IF4 -> FP2:2 cable6.save() self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface # Create cables 3-4 - cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.front_ports[16]) # RP1 -> FP5 + cable3 = Cable(termination_a=self.rear_port1, termination_b=self.front_port5_1) # RP1 -> FP5 cable3.save() - cable4 = Cable(termination_a=self.rear_ports[4], termination_b=self.rear_ports[1]) # RP5 -> RP2 + cable4 = Cable(termination_a=self.rear_port5, termination_b=self.rear_port2) # RP5 -> RP2 cable4.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[2], + origin=self.interface1, + destination=self.interface3, path=( - cable1, self.front_ports[0], self.rear_ports[0], cable3, self.front_ports[16], self.rear_ports[4], - cable4, self.rear_ports[1], self.front_ports[4], cable5 + cable1, self.front_port1_1, self.rear_port1, cable3, self.front_port5_1, self.rear_port5, + cable4, self.rear_port2, self.front_port2_1, cable5 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[3], + origin=self.interface2, + destination=self.interface4, path=( - cable2, self.front_ports[1], self.rear_ports[0], cable3, self.front_ports[16], self.rear_ports[4], - cable4, self.rear_ports[1], self.front_ports[5], cable6 + cable2, self.front_port1_2, self.rear_port1, cable3, self.front_port5_1, self.rear_port5, + cable4, self.rear_port2, self.front_port2_2, cable6 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[2], - destination=self.interfaces[0], + origin=self.interface3, + destination=self.interface1, path=( - cable5, self.front_ports[4], self.rear_ports[1], cable4, self.rear_ports[4], self.front_ports[16], - cable3, self.rear_ports[0], self.front_ports[0], cable1 + cable5, self.front_port2_1, self.rear_port2, cable4, self.rear_port5, self.front_port5_1, + cable3, self.rear_port1, self.front_port1_1, cable1 ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[3], - destination=self.interfaces[1], + origin=self.interface4, + destination=self.interface2, path=( - cable6, self.front_ports[5], self.rear_ports[1], cable4, self.rear_ports[4], self.front_ports[16], - cable3, self.rear_ports[0], self.front_ports[1], cable2 + cable6, self.front_port2_2, self.rear_port2, cable4, self.rear_port5, self.front_port5_1, + cable3, self.rear_port1, self.front_port1_2, cable2 ), is_connected=True ) @@ -482,38 +511,38 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [FP5] [RP5] --C2-- [RP6] [FP6] --C3-- [IF2] """ # Create cable 2 - cable2 = Cable(termination_a=self.rear_ports[4], termination_b=self.rear_ports[5]) + cable2 = Cable(termination_a=self.rear_port5, termination_b=self.rear_port6) cable2.save() self.assertEqual(CablePath.objects.count(), 0) # Create cable1 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[16]) + cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port5_1) cable1.save() self.assertPathExists( - origin=self.interfaces[0], + origin=self.interface1, destination=None, - path=(cable1, self.front_ports[16], self.rear_ports[4], cable2, self.rear_ports[5], self.front_ports[17]), + path=(cable1, self.front_port5_1, self.rear_port5, cable2, self.rear_port6, self.front_port6_1), is_connected=False ) self.assertEqual(CablePath.objects.count(), 1) # Create cable 3 - cable3 = Cable(termination_a=self.front_ports[17], termination_b=self.interfaces[1]) + cable3 = Cable(termination_a=self.front_port6_1, termination_b=self.interface2) cable3.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[1], + origin=self.interface1, + destination=self.interface2, path=( - cable1, self.front_ports[16], self.rear_ports[4], cable2, self.rear_ports[5], self.front_ports[17], + cable1, self.front_port5_1, self.rear_port5, cable2, self.rear_port6, self.front_port6_1, cable3, ), is_connected=True ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[0], + origin=self.interface2, + destination=self.interface1, path=( - cable3, self.front_ports[17], self.rear_ports[5], cable2, self.rear_ports[4], self.front_ports[16], + cable3, self.front_port6_1, self.rear_port6, cable2, self.rear_port5, self.front_port5_1, cable1, ), is_connected=True @@ -525,9 +554,9 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] """ # Create cables 1 and 2 - cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[16]) + cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port5_1) cable1.save() - cable2 = Cable(termination_a=self.rear_ports[4], termination_b=self.interfaces[1]) + cable2 = Cable(termination_a=self.rear_port5, termination_b=self.interface2) cable2.save() self.assertEqual(CablePath.objects.filter(is_connected=True).count(), 2) self.assertEqual(CablePath.objects.count(), 2) @@ -536,15 +565,15 @@ class CablePathTestCase(TestCase): cable2.status = CableStatusChoices.STATUS_PLANNED cable2.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[1], - path=(cable1, self.front_ports[16], self.rear_ports[4], cable2), + origin=self.interface1, + destination=self.interface2, + path=(cable1, self.front_port5_1, self.rear_port5, cable2), is_connected=False ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[0], - path=(cable2, self.rear_ports[4], self.front_ports[16], cable1), + origin=self.interface2, + destination=self.interface1, + path=(cable2, self.rear_port5, self.front_port5_1, cable1), is_connected=False ) self.assertEqual(CablePath.objects.count(), 2) @@ -554,15 +583,15 @@ class CablePathTestCase(TestCase): cable2.status = CableStatusChoices.STATUS_CONNECTED cable2.save() self.assertPathExists( - origin=self.interfaces[0], - destination=self.interfaces[1], - path=(cable1, self.front_ports[16], self.rear_ports[4], cable2), + origin=self.interface1, + destination=self.interface2, + path=(cable1, self.front_port5_1, self.rear_port5, cable2), is_connected=True ) self.assertPathExists( - origin=self.interfaces[1], - destination=self.interfaces[0], - path=(cable2, self.rear_ports[4], self.front_ports[16], cable1), + origin=self.interface2, + destination=self.interface1, + path=(cable2, self.rear_port5, self.front_port5_1, cable1), is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) From 4fd12198144f2da3fad3422493a365bb12cfb331 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 11:35:17 -0400 Subject: [PATCH 18/67] Add tests for all PathEndpoint classes --- netbox/dcim/tests/test_cablepaths.py | 161 ++++++++++++++++++++++++--- 1 file changed, 146 insertions(+), 15 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 53ce82403..18c15fd16 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -8,7 +8,14 @@ from dcim.utils import objects_to_path class CablePathTestCase(TestCase): + """ + Test NetBox's ability to trace and retrace CablePaths in response to data model changes. Tests are numbered + as follows: + 1XX: Test direct connections between different endpoint types + 2XX: Test different cable topologies + 3XX: Test responses to changes in existing objects + """ @classmethod def setUpTestData(cls): @@ -19,6 +26,12 @@ class CablePathTestCase(TestCase): device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') device = Device.objects.create(site=site, device_type=device_type, device_role=device_role, name='Test Device') + # Create console/power components for testing + cls.consoleport1 = ConsolePort.objects.create(device=device, name='Console Port 1') + cls.consoleserverport1 = ConsoleServerPort.objects.create(device=device, name='Console Server Port 1') + cls.powerport1 = PowerPort.objects.create(device=device, name='Power Port 1') + cls.poweroutlet1 = PowerPort.objects.create(device=device, name='Power Outlet 1') + # Create 4 interfaces for testing cls.interface1 = Interface(device=device, name=f'Interface 1') cls.interface2 = Interface(device=device, name=f'Interface 2') @@ -87,18 +100,28 @@ class CablePathTestCase(TestCase): cls.front_port6_1, ]) - # Create four circuits with two terminations (A and Z) each (8 total) + # Create a PowerFeed for testing + powerpanel = PowerPanel.objects.create(site=site, name='Power Panel') + cls.powerfeed1 = PowerFeed.objects.create(power_panel=powerpanel, name='Power Feed 1') + + # Create four CircuitTerminations for testing provider = Provider.objects.create(name='Provider', slug='provider') circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type') circuits = [ - Circuit(provider=provider, type=circuit_type, cid=f'Circuit {i}') for i in range(1, 5) + Circuit(provider=provider, type=circuit_type, cid='Circuit 1'), + Circuit(provider=provider, type=circuit_type, cid='Circuit 2'), ] Circuit.objects.bulk_create(circuits) - cls.circuit_terminations = [ - *[CircuitTermination(circuit=circuit, site=site, term_side='A', port_speed=1000) for circuit in circuits], - *[CircuitTermination(circuit=circuit, site=site, term_side='Z', port_speed=1000) for circuit in circuits], - ] - CircuitTermination.objects.bulk_create(cls.circuit_terminations) + cls.circuittermination1_A = CircuitTermination(circuit=circuits[0], site=site, term_side='A', port_speed=1000) + cls.circuittermination1_Z = CircuitTermination(circuit=circuits[0], site=site, term_side='Z', port_speed=1000) + cls.circuittermination2_A = CircuitTermination(circuit=circuits[1], site=site, term_side='A', port_speed=1000) + cls.circuittermination2_Z = CircuitTermination(circuit=circuits[1], site=site, term_side='Z', port_speed=1000) + CircuitTermination.objects.bulk_create([ + cls.circuittermination1_A, + cls.circuittermination1_Z, + cls.circuittermination2_A, + cls.circuittermination2_Z, + ]) def assertPathExists(self, origin, destination, path=None, is_connected=None, msg=None): """ @@ -131,7 +154,7 @@ class CablePathTestCase(TestCase): msg = f"Missing partial path originating from {origin}" self.assertEqual(CablePath.objects.filter(**kwargs).count(), 1, msg=msg) - def test_01_interface_to_interface(self): + def test_101_interface_to_interface(self): """ [IF1] --C1-- [IF2] """ @@ -158,7 +181,115 @@ class CablePathTestCase(TestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_02_interface_to_interface_via_pass_through(self): + def test_103_consoleport_to_consoleserverport(self): + """ + [CP1] --C1-- [CSP1] + """ + # Create cable 1 + cable1 = Cable(termination_a=self.consoleport1, termination_b=self.consoleserverport1) + cable1.save() + self.assertPathExists( + origin=self.consoleport1, + destination=self.consoleserverport1, + path=(cable1,), + is_connected=True + ) + self.assertPathExists( + origin=self.consoleserverport1, + destination=self.consoleport1, + path=(cable1,), + is_connected=True + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Delete cable 1 + cable1.delete() + + # Check that all CablePaths have been deleted + self.assertEqual(CablePath.objects.count(), 0) + + def test_104_powerport_to_poweroutlet(self): + """ + [PP1] --C1-- [PO1] + """ + # Create cable 1 + cable1 = Cable(termination_a=self.powerport1, termination_b=self.poweroutlet1) + cable1.save() + self.assertPathExists( + origin=self.powerport1, + destination=self.poweroutlet1, + path=(cable1,), + is_connected=True + ) + self.assertPathExists( + origin=self.poweroutlet1, + destination=self.powerport1, + path=(cable1,), + is_connected=True + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Delete cable 1 + cable1.delete() + + # Check that all CablePaths have been deleted + self.assertEqual(CablePath.objects.count(), 0) + + def test_105_powerport_to_powerfeed(self): + """ + [PP1] --C1-- [PF1] + """ + # Create cable 1 + cable1 = Cable(termination_a=self.powerport1, termination_b=self.powerfeed1) + cable1.save() + self.assertPathExists( + origin=self.powerport1, + destination=self.powerfeed1, + path=(cable1,), + is_connected=True + ) + self.assertPathExists( + origin=self.powerfeed1, + destination=self.powerport1, + path=(cable1,), + is_connected=True + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Delete cable 1 + cable1.delete() + + # Check that all CablePaths have been deleted + self.assertEqual(CablePath.objects.count(), 0) + + def test_106_interface_to_circuittermination(self): + """ + [PP1] --C1-- [CT1A] + """ + # Create cable 1 + cable1 = Cable(termination_a=self.interface1, termination_b=self.circuittermination1_A) + cable1.save() + self.assertPathExists( + origin=self.interface1, + destination=self.circuittermination1_A, + path=(cable1,), + is_connected=True + ) + self.assertPathExists( + origin=self.circuittermination1_A, + destination=self.interface1, + path=(cable1,), + is_connected=True + ) + self.assertEqual(CablePath.objects.count(), 2) + + # Delete cable 1 + cable1.delete() + + # Check that all CablePaths have been deleted + self.assertEqual(CablePath.objects.count(), 0) + + def test_201_single_path_via_pass_through(self): """ [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] """ @@ -200,7 +331,7 @@ class CablePathTestCase(TestCase): ) self.assertEqual(CablePath.objects.count(), 1) - def test_03_interfaces_to_interfaces_via_pass_through(self): + def test_202_multiple_paths_via_pass_through(self): """ [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [IF3] [IF2] --C2-- [FP1:2] [FP2:2] --C5-- [IF4] @@ -291,7 +422,7 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) - def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): + def test_203_multiple_paths_via_nested_pass_throughs(self): """ [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP2:1] [RP2] --C4-- [RP3] [FP3:1] --C5-- [RP4] [FP4:1] --C6-- [IF3] [IF2] --C2-- [FP1:2] [FP4:2] --C7-- [IF4] @@ -366,7 +497,7 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) - def test_05_interfaces_to_interfaces_via_multiple_pass_throughs(self): + def test_204_multiple_paths_via_multiple_pass_throughs(self): """ [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [FP3:1] [RP3] --C6-- [RP4] [FP4:1] --C7-- [IF3] [IF2] --C2-- [FP1:2] [FP2:1] --C5-- [FP3:1] [FP4:2] --C8-- [IF4] @@ -440,7 +571,7 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 2) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 2) - def test_06_interfaces_to_interfaces_via_patched_pass_throughs(self): + def test_205_multiple_paths_via_patched_pass_throughs(self): """ [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP5] [RP5] --C4-- [RP2] [FP2:1] --C5-- [IF3] [IF2] --C2-- [FP1:2] [FP2:2] --C6-- [IF4] @@ -506,7 +637,7 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) - def test_07_interface_to_interface_via_existing_cable(self): + def test_301_create_path_via_existing_cable(self): """ [IF1] --C1-- [FP5] [RP5] --C2-- [RP6] [FP6] --C3-- [IF2] """ @@ -549,7 +680,7 @@ class CablePathTestCase(TestCase): ) self.assertEqual(CablePath.objects.count(), 2) - def test_08_change_cable_status(self): + def test_302_update_path_on_cable_status_change(self): """ [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] """ From e0abd7ef3ec52c163ed86a68141ff23800731873 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 11:45:42 -0400 Subject: [PATCH 19/67] Remove dcim.tests.test_api.ConnectionTest --- netbox/dcim/tests/test_api.py | 371 ---------------------------------- 1 file changed, 371 deletions(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 512d7919c..528301f8f 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1453,377 +1453,6 @@ class CableTest(APIViewTestCases.APIViewTestCase): ] -class ConnectionTest(APITestCase): - - def setUp(self): - - super().setUp() - - self.site = Site.objects.create( - name='Test Site 1', slug='test-site-1' - ) - manufacturer = Manufacturer.objects.create( - name='Test Manufacturer 1', slug='test-manufacturer-1' - ) - devicetype = DeviceType.objects.create( - manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' - ) - devicerole = DeviceRole.objects.create( - name='Test Device Role 1', slug='test-device-role-1', color='ff0000' - ) - self.device1 = Device.objects.create( - device_type=devicetype, device_role=devicerole, name='Test Device 1', site=self.site - ) - self.device2 = Device.objects.create( - device_type=devicetype, device_role=devicerole, name='Test Device 2', site=self.site - ) - self.panel1 = Device.objects.create( - device_type=devicetype, device_role=devicerole, name='Test Panel 1', site=self.site - ) - self.panel2 = Device.objects.create( - device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=self.site - ) - - def test_create_direct_console_connection(self): - - consoleport1 = ConsolePort.objects.create( - device=self.device1, name='Test Console Port 1' - ) - consoleserverport1 = ConsoleServerPort.objects.create( - device=self.device2, name='Test Console Server Port 1' - ) - - data = { - 'termination_a_type': 'dcim.consoleport', - 'termination_a_id': consoleport1.pk, - 'termination_b_type': 'dcim.consoleserverport', - 'termination_b_id': consoleserverport1.pk, - } - - self.add_permissions('dcim.add_cable') - url = reverse('dcim-api:cable-list') - response = self.client.post(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(Cable.objects.count(), 1) - - cable = Cable.objects.get(pk=response.data['id']) - consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk) - consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk) - - self.assertEqual(cable.termination_a, consoleport1) - self.assertEqual(cable.termination_b, consoleserverport1) - self.assertEqual(consoleport1.cable, cable) - self.assertEqual(consoleserverport1.cable, cable) - self.assertEqual(consoleport1.connected_endpoint, consoleserverport1) - self.assertEqual(consoleserverport1.connected_endpoint, consoleport1) - - def test_create_patched_console_connection(self): - - consoleport1 = ConsolePort.objects.create( - device=self.device1, name='Test Console Port 1' - ) - consoleserverport1 = ConsoleServerPort.objects.create( - device=self.device2, name='Test Console Server Port 1' - ) - rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C - ) - frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 - ) - rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C - ) - frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 - ) - - self.add_permissions('dcim.add_cable') - url = reverse('dcim-api:cable-list') - cables = [ - # Console port to panel1 front - { - 'termination_a_type': 'dcim.consoleport', - 'termination_a_id': consoleport1.pk, - 'termination_b_type': 'dcim.frontport', - 'termination_b_id': frontport1.pk, - }, - # Panel1 rear to panel2 rear - { - 'termination_a_type': 'dcim.rearport', - 'termination_a_id': rearport1.pk, - 'termination_b_type': 'dcim.rearport', - 'termination_b_id': rearport2.pk, - }, - # Panel2 front to console server port - { - 'termination_a_type': 'dcim.frontport', - 'termination_a_id': frontport2.pk, - 'termination_b_type': 'dcim.consoleserverport', - 'termination_b_id': consoleserverport1.pk, - }, - ] - - for data in cables: - - response = self.client.post(url, data, format='json', **self.header) - self.assertHttpStatus(response, status.HTTP_201_CREATED) - - cable = Cable.objects.get(pk=response.data['id']) - self.assertEqual(cable.termination_a.cable, cable) - self.assertEqual(cable.termination_b.cable, cable) - - consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk) - consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk) - self.assertEqual(consoleport1.connected_endpoint, consoleserverport1) - self.assertEqual(consoleserverport1.connected_endpoint, consoleport1) - - def test_create_direct_power_connection(self): - - powerport1 = PowerPort.objects.create( - device=self.device1, name='Test Power Port 1' - ) - poweroutlet1 = PowerOutlet.objects.create( - device=self.device2, name='Test Power Outlet 1' - ) - - data = { - 'termination_a_type': 'dcim.powerport', - 'termination_a_id': powerport1.pk, - 'termination_b_type': 'dcim.poweroutlet', - 'termination_b_id': poweroutlet1.pk, - } - - self.add_permissions('dcim.add_cable') - url = reverse('dcim-api:cable-list') - response = self.client.post(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(Cable.objects.count(), 1) - - cable = Cable.objects.get(pk=response.data['id']) - powerport1 = PowerPort.objects.get(pk=powerport1.pk) - poweroutlet1 = PowerOutlet.objects.get(pk=poweroutlet1.pk) - - self.assertEqual(cable.termination_a, powerport1) - self.assertEqual(cable.termination_b, poweroutlet1) - self.assertEqual(powerport1.cable, cable) - self.assertEqual(poweroutlet1.cable, cable) - self.assertEqual(powerport1.connected_endpoint, poweroutlet1) - self.assertEqual(poweroutlet1.connected_endpoint, powerport1) - - # Note: Power connections via patch ports are not supported. - - def test_create_direct_interface_connection(self): - - interface1 = Interface.objects.create( - device=self.device1, name='Test Interface 1' - ) - interface2 = Interface.objects.create( - device=self.device2, name='Test Interface 2' - ) - - data = { - 'termination_a_type': 'dcim.interface', - 'termination_a_id': interface1.pk, - 'termination_b_type': 'dcim.interface', - 'termination_b_id': interface2.pk, - } - - self.add_permissions('dcim.add_cable') - url = reverse('dcim-api:cable-list') - response = self.client.post(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(Cable.objects.count(), 1) - - cable = Cable.objects.get(pk=response.data['id']) - interface1 = Interface.objects.get(pk=interface1.pk) - interface2 = Interface.objects.get(pk=interface2.pk) - - self.assertEqual(cable.termination_a, interface1) - self.assertEqual(cable.termination_b, interface2) - self.assertEqual(interface1.cable, cable) - self.assertEqual(interface2.cable, cable) - self.assertEqual(interface1.connected_endpoint, interface2) - self.assertEqual(interface2.connected_endpoint, interface1) - - def test_create_patched_interface_connection(self): - - interface1 = Interface.objects.create( - device=self.device1, name='Test Interface 1' - ) - interface2 = Interface.objects.create( - device=self.device2, name='Test Interface 2' - ) - rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C - ) - frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 - ) - rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C - ) - frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 - ) - - self.add_permissions('dcim.add_cable') - url = reverse('dcim-api:cable-list') - cables = [ - # Interface1 to panel1 front - { - 'termination_a_type': 'dcim.interface', - 'termination_a_id': interface1.pk, - 'termination_b_type': 'dcim.frontport', - 'termination_b_id': frontport1.pk, - }, - # Panel1 rear to panel2 rear - { - 'termination_a_type': 'dcim.rearport', - 'termination_a_id': rearport1.pk, - 'termination_b_type': 'dcim.rearport', - 'termination_b_id': rearport2.pk, - }, - # Panel2 front to interface2 - { - 'termination_a_type': 'dcim.frontport', - 'termination_a_id': frontport2.pk, - 'termination_b_type': 'dcim.interface', - 'termination_b_id': interface2.pk, - }, - ] - - for data in cables: - - response = self.client.post(url, data, format='json', **self.header) - self.assertHttpStatus(response, status.HTTP_201_CREATED) - - cable = Cable.objects.get(pk=response.data['id']) - self.assertEqual(cable.termination_a.cable, cable) - self.assertEqual(cable.termination_b.cable, cable) - - interface1 = Interface.objects.get(pk=interface1.pk) - interface2 = Interface.objects.get(pk=interface2.pk) - self.assertEqual(interface1.connected_endpoint, interface2) - self.assertEqual(interface2.connected_endpoint, interface1) - - def test_create_direct_circuittermination_connection(self): - - provider = Provider.objects.create( - name='Test Provider 1', slug='test-provider-1' - ) - circuittype = CircuitType.objects.create( - name='Test Circuit Type 1', slug='test-circuit-type-1' - ) - circuit = Circuit.objects.create( - provider=provider, type=circuittype, cid='Test Circuit 1' - ) - interface1 = Interface.objects.create( - device=self.device1, name='Test Interface 1' - ) - circuittermination1 = CircuitTermination.objects.create( - circuit=circuit, term_side='A', site=self.site, port_speed=10000 - ) - - data = { - 'termination_a_type': 'dcim.interface', - 'termination_a_id': interface1.pk, - 'termination_b_type': 'circuits.circuittermination', - 'termination_b_id': circuittermination1.pk, - } - - self.add_permissions('dcim.add_cable') - url = reverse('dcim-api:cable-list') - response = self.client.post(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(Cable.objects.count(), 1) - - cable = Cable.objects.get(pk=response.data['id']) - interface1 = Interface.objects.get(pk=interface1.pk) - circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk) - - self.assertEqual(cable.termination_a, interface1) - self.assertEqual(cable.termination_b, circuittermination1) - self.assertEqual(interface1.cable, cable) - self.assertEqual(circuittermination1.cable, cable) - self.assertEqual(interface1.connected_endpoint, circuittermination1) - self.assertEqual(circuittermination1.connected_endpoint, interface1) - - def test_create_patched_circuittermination_connection(self): - - provider = Provider.objects.create( - name='Test Provider 1', slug='test-provider-1' - ) - circuittype = CircuitType.objects.create( - name='Test Circuit Type 1', slug='test-circuit-type-1' - ) - circuit = Circuit.objects.create( - provider=provider, type=circuittype, cid='Test Circuit 1' - ) - interface1 = Interface.objects.create( - device=self.device1, name='Test Interface 1' - ) - circuittermination1 = CircuitTermination.objects.create( - circuit=circuit, term_side='A', site=self.site, port_speed=10000 - ) - rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C - ) - frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 - ) - rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C - ) - frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 - ) - - self.add_permissions('dcim.add_cable') - url = reverse('dcim-api:cable-list') - cables = [ - # Interface to panel1 front - { - 'termination_a_type': 'dcim.interface', - 'termination_a_id': interface1.pk, - 'termination_b_type': 'dcim.frontport', - 'termination_b_id': frontport1.pk, - }, - # Panel1 rear to panel2 rear - { - 'termination_a_type': 'dcim.rearport', - 'termination_a_id': rearport1.pk, - 'termination_b_type': 'dcim.rearport', - 'termination_b_id': rearport2.pk, - }, - # Panel2 front to circuit termination - { - 'termination_a_type': 'dcim.frontport', - 'termination_a_id': frontport2.pk, - 'termination_b_type': 'circuits.circuittermination', - 'termination_b_id': circuittermination1.pk, - }, - ] - - for data in cables: - - response = self.client.post(url, data, format='json', **self.header) - self.assertHttpStatus(response, status.HTTP_201_CREATED) - - cable = Cable.objects.get(pk=response.data['id']) - self.assertEqual(cable.termination_a.cable, cable) - self.assertEqual(cable.termination_b.cable, cable) - - interface1 = Interface.objects.get(pk=interface1.pk) - circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk) - self.assertEqual(interface1.connected_endpoint, circuittermination1) - self.assertEqual(circuittermination1.connected_endpoint, interface1) - - class ConnectedDeviceTest(APITestCase): def setUp(self): From 66355da04c87ab35b7b0a4f53039e5e542a871bd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 11:51:23 -0400 Subject: [PATCH 20/67] CablePath.origin should be unique --- netbox/dcim/migrations/0120_cablepath.py | 7 ++++++- netbox/dcim/models/devices.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/migrations/0120_cablepath.py b/netbox/dcim/migrations/0120_cablepath.py index 2cb8376b7..f3448e747 100644 --- a/netbox/dcim/migrations/0120_cablepath.py +++ b/netbox/dcim/migrations/0120_cablepath.py @@ -1,3 +1,5 @@ +# Generated by Django 3.1 on 2020-10-02 15:49 + import dcim.fields from django.db import migrations, models import django.db.models.deletion @@ -18,9 +20,12 @@ class Migration(migrations.Migration): ('origin_id', models.PositiveIntegerField()), ('destination_id', models.PositiveIntegerField(blank=True, null=True)), ('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)), + ('is_connected', models.BooleanField(default=False)), ('destination_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), ('origin_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), - ('is_connected', models.BooleanField(default=False)), ], + options={ + 'unique_together': {('origin_type', 'origin_id')}, + }, ), ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 0cb1ea970..52627dc7d 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1197,6 +1197,9 @@ class CablePath(models.Model): objects = CablePathManager() + class Meta: + unique_together = ('origin_type', 'origin_id') + def __str__(self): path = ', '.join([str(path_node_to_object(node)) for node in self.path]) return f"Path #{self.pk}: {self.origin} to {self.destination} via ({path})" From aa0d4c4145f9ffd74711fb7aadcdbbb36a4c3bea Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 12:25:31 -0400 Subject: [PATCH 21/67] Replace connection_status filter with is_connected --- netbox/circuits/filters.py | 3 ++- netbox/dcim/filters.py | 37 ++++++++++++++++++++++--------- netbox/dcim/tests/test_filters.py | 25 +++++++++------------ 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 206dcc305..ebc0d0ec1 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -1,6 +1,7 @@ import django_filters from django.db.models import Q +from dcim.filters import PathEndpointFilterSet from dcim.models import Region, Site from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from tenancy.filters import TenancyFilterSet @@ -144,7 +145,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr ).distinct() -class CircuitTerminationFilterSet(BaseFilterSet): +class CircuitTerminationFilterSet(BaseFilterSet, PathEndpointFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 457483273..c76bd3b87 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,5 +1,6 @@ import django_filters from django.contrib.auth.models import User +from django.db.models import Count from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet from tenancy.filters import TenancyFilterSet @@ -752,7 +753,21 @@ class DeviceComponentFilterSet(django_filters.FilterSet): ) -class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet): +class PathEndpointFilterSet(django_filters.FilterSet): + is_connected = django_filters.BooleanFilter( + method='filter_is_connected', + label='Search', + ) + + def filter_is_connected(self, queryset, name, value): + kwargs = {'connected_paths': 1 if value else 0} + # TODO: Boolean rather than Count()? + return queryset.annotate( + connected_paths=Count('_paths', filter=Q(_paths__is_connected=True)) + ).filter(**kwargs) + + +class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None @@ -765,10 +780,10 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet): class Meta: model = ConsolePort - fields = ['id', 'name', 'description', 'connection_status'] + fields = ['id', 'name', 'description'] -class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): +class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None @@ -781,10 +796,10 @@ class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): class Meta: model = ConsoleServerPort - fields = ['id', 'name', 'description', 'connection_status'] + fields = ['id', 'name', 'description'] -class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): +class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerPortTypeChoices, null_value=None @@ -797,10 +812,10 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): class Meta: model = PowerPort - fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status'] + fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description'] -class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet): +class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerOutletTypeChoices, null_value=None @@ -813,10 +828,10 @@ class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet): class Meta: model = PowerOutlet - fields = ['id', 'name', 'feed_leg', 'description', 'connection_status'] + fields = ['id', 'name', 'feed_leg', 'description'] -class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet): +class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -864,7 +879,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet): class Meta: model = Interface - fields = ['id', 'name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] + fields = ['id', 'name', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] def filter_device(self, queryset, name, value): try: @@ -1284,7 +1299,7 @@ class PowerPanelFilterSet(BaseFilterSet): return queryset.filter(qs_filter) -class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class PowerFeedFilterSet(BaseFilterSet, PathEndpointFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index 0a2794f01..c399e1a92 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -1514,9 +1514,8 @@ class ConsolePortTestCase(TestCase): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - # TODO: Fix boolean value - def test_connection_status(self): - params = {'connection_status': 'True'} + def test_is_connected(self): + params = {'is_connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_region(self): @@ -1609,9 +1608,8 @@ class ConsoleServerPortTestCase(TestCase): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - # TODO: Fix boolean value - def test_connection_status(self): - params = {'connection_status': 'True'} + def test_is_connected(self): + params = {'is_connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_region(self): @@ -1712,9 +1710,8 @@ class PowerPortTestCase(TestCase): params = {'allocated_draw': [50, 100]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - # TODO: Fix boolean value - def test_connection_status(self): - params = {'connection_status': 'True'} + def test_is_connected(self): + params = {'is_connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_region(self): @@ -1812,9 +1809,8 @@ class PowerOutletTestCase(TestCase): params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - # TODO: Fix boolean value - def test_connection_status(self): - params = {'connection_status': 'True'} + def test_is_connected(self): + params = {'is_connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_region(self): @@ -1900,9 +1896,8 @@ class InterfaceTestCase(TestCase): params = {'name': ['Interface 1', 'Interface 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - # TODO: Fix boolean value - def test_connection_status(self): - params = {'connection_status': 'True'} + def test_is_connected(self): + params = {'is_connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_enabled(self): From e9da84f91aaf80aced3b369d4205165572a62dc5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 14:54:16 -0400 Subject: [PATCH 22/67] 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 %} From 7ff247c57ff683676b6ecc52b52f755c440dbe22 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 14:57:50 -0400 Subject: [PATCH 23/67] Add trace view for PowerFeed --- netbox/dcim/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index ba58cf67e..90f6b5ef2 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -383,6 +383,7 @@ urlpatterns = [ path('power-feeds//', views.PowerFeedView.as_view(), name='powerfeed'), path('power-feeds//edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), path('power-feeds//delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), + path('power-feeds//trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}), path('power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), ] From 8cb636bed2c5f6355003aa8509ab70e1b52f4519 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 15:10:49 -0400 Subject: [PATCH 24/67] Update console/power/interface connection tables --- netbox/dcim/tables.py | 58 ++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 7af030a03..7ab08eae4 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -812,13 +812,15 @@ class CableTable(BaseTable): # class ConsoleConnectionTable(BaseTable): - console_server = tables.LinkColumn( - viewname='dcim:device', - accessor=Accessor('connected_endpoint__device'), - args=[Accessor('connected_endpoint__device__pk')], + console_server = tables.Column( + accessor=Accessor('path__destination__device'), + orderable=False, + linkify=True, verbose_name='Console Server' ) - connected_endpoint = tables.Column( + console_server_port = tables.Column( + accessor=Accessor('path__destination'), + orderable=False, linkify=True, verbose_name='Port' ) @@ -830,25 +832,27 @@ class ConsoleConnectionTable(BaseTable): verbose_name='Console Port' ) connection_status = tables.TemplateColumn( + accessor=Accessor('path__is_connected'), + orderable=False, template_code=CONNECTION_STATUS, verbose_name='Status' ) class Meta(BaseTable.Meta): model = ConsolePort - fields = ('console_server', 'connected_endpoint', 'device', 'name', 'connection_status') + fields = ('console_server', 'console_server_port', 'device', 'name', 'connection_status') class PowerConnectionTable(BaseTable): - pdu = tables.LinkColumn( - viewname='dcim:device', - accessor=Accessor('connected_endpoint__device'), - args=[Accessor('connected_endpoint__device__pk')], - order_by='_connected_poweroutlet__device', + pdu = tables.Column( + accessor=Accessor('path__destination__device'), + orderable=False, + linkify=True, verbose_name='PDU' ) outlet = tables.Column( - accessor=Accessor('_connected_poweroutlet'), + accessor=Accessor('path__destination'), + orderable=False, linkify=True, verbose_name='Outlet' ) @@ -860,6 +864,8 @@ class PowerConnectionTable(BaseTable): verbose_name='Power Port' ) connection_status = tables.TemplateColumn( + accessor=Accessor('path__is_connected'), + orderable=False, template_code=CONNECTION_STATUS, verbose_name='Status' ) @@ -870,31 +876,31 @@ class PowerConnectionTable(BaseTable): class InterfaceConnectionTable(BaseTable): - device_a = tables.LinkColumn( - viewname='dcim:device', + device_a = tables.Column( accessor=Accessor('device'), - args=[Accessor('device__pk')], + linkify=True, verbose_name='Device A' ) - interface_a = tables.LinkColumn( - viewname='dcim:interface', + interface_a = tables.Column( accessor=Accessor('name'), - args=[Accessor('pk')], + linkify=True, verbose_name='Interface A' ) - device_b = tables.LinkColumn( - viewname='dcim:device', - accessor=Accessor('_connected_interface__device'), - args=[Accessor('_connected_interface__device__pk')], + device_b = tables.Column( + accessor=Accessor('path__destination__device'), + orderable=False, + linkify=True, verbose_name='Device B' ) - interface_b = tables.LinkColumn( - viewname='dcim:interface', - accessor=Accessor('_connected_interface'), - args=[Accessor('_connected_interface__pk')], + interface_b = tables.Column( + accessor=Accessor('path__destination'), + orderable=False, + linkify=True, verbose_name='Interface B' ) connection_status = tables.TemplateColumn( + accessor=Accessor('path__is_connected'), + orderable=False, template_code=CONNECTION_STATUS, verbose_name='Status' ) From 5737f6fca0b443d47087469519773d2519b83b1d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 17:16:43 -0400 Subject: [PATCH 25/67] Cache each CablePath on its originating endpoint --- netbox/circuits/migrations/0021_cablepath.py | 20 +++ netbox/dcim/migrations/0120_cablepath.py | 32 ++++- netbox/dcim/models/device_components.py | 26 ++-- netbox/dcim/models/devices.py | 7 + netbox/dcim/signals.py | 31 ++--- netbox/dcim/tests/test_cablepaths.py | 130 ++++++++++++++++--- 6 files changed, 191 insertions(+), 55 deletions(-) create mode 100644 netbox/circuits/migrations/0021_cablepath.py diff --git a/netbox/circuits/migrations/0021_cablepath.py b/netbox/circuits/migrations/0021_cablepath.py new file mode 100644 index 000000000..fabf71798 --- /dev/null +++ b/netbox/circuits/migrations/0021_cablepath.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1 on 2020-10-02 19:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0120_cablepath'), + ('circuits', '0020_custom_field_data'), + ] + + operations = [ + migrations.AddField( + model_name='circuittermination', + name='_path', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), + ), + ] diff --git a/netbox/dcim/migrations/0120_cablepath.py b/netbox/dcim/migrations/0120_cablepath.py index f3448e747..0d2199e31 100644 --- a/netbox/dcim/migrations/0120_cablepath.py +++ b/netbox/dcim/migrations/0120_cablepath.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1 on 2020-10-02 15:49 +# Generated by Django 3.1 on 2020-10-02 19:43 import dcim.fields from django.db import migrations, models @@ -28,4 +28,34 @@ class Migration(migrations.Migration): 'unique_together': {('origin_type', 'origin_id')}, }, ), + migrations.AddField( + model_name='consoleport', + name='_path', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), + ), + migrations.AddField( + model_name='consoleserverport', + name='_path', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), + ), + migrations.AddField( + model_name='interface', + name='_path', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), + ), + migrations.AddField( + model_name='powerfeed', + name='_path', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), + ), + migrations.AddField( + model_name='poweroutlet', + name='_path', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), + ), + migrations.AddField( + model_name='powerport', + name='_path', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), + ), ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index b714c662a..65032f529 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -126,38 +126,30 @@ class CableTermination(models.Model): class PathEndpoint(models.Model): """ - Any object which may serve as either endpoint of a CablePath. + Any object which may serve as the originating endpoint of a CablePath. """ - _paths = GenericRelation( + _path = models.ForeignKey( to='dcim.CablePath', - content_type_field='origin_type', - object_id_field='origin_id', - related_query_name='%(class)s' + on_delete=models.SET_NULL, + null=True, + blank=True ) class Meta: abstract = True def trace(self): - if self.path is None: + 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" + 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.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): - """ - 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/models/devices.py b/netbox/dcim/models/devices.py index 52627dc7d..ea7332b3d 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1204,6 +1204,13 @@ class CablePath(models.Model): path = ', '.join([str(path_node_to_object(node)) for node in self.path]) return f"Path #{self.pk}: {self.origin} to {self.destination} via ({path})" + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + # Record a direct reference to this CablePath on its originating object + model = self.origin._meta.model + model.objects.filter(pk=self.origin.pk).update(_path=self.pk) + # # Virtual chassis diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 9b0493e34..13653c7d4 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -1,7 +1,7 @@ import logging from django.contrib.contenttypes.models import ContentType -from django.db.models.signals import post_save, pre_delete +from django.db.models.signals import post_save, post_delete, pre_delete from django.db import transaction from django.dispatch import receiver @@ -91,7 +91,7 @@ def update_connected_endpoints(instance, created, **kwargs): rebuild_paths(instance) -@receiver(pre_delete, sender=Cable) +@receiver(post_delete, sender=Cable) def nullify_connected_endpoints(instance, **kwargs): """ When a Cable is deleted, check for and update its two connected endpoints @@ -108,18 +108,15 @@ def nullify_connected_endpoints(instance, **kwargs): instance.termination_b.cable = None instance.termination_b.save() - # Delete any dependent cable paths - cable_paths = CablePath.objects.filter(path__contains=[object_to_path_node(instance)]) - retrace_queue = [cp.origin for cp in cable_paths] - deleted, _ = cable_paths.delete() - logger.info(f'Deleted {deleted} cable paths') - - # Retrace cable paths from the origins of deleted paths - for origin in retrace_queue: - # Delete and recreate all CablePaths for this origin point - # TODO: We can probably be smarter about skipping unchanged paths - CablePath.objects.filter( - origin_type=ContentType.objects.get_for_model(origin), - origin_id=origin.pk - ).delete() - create_cablepath(origin) + # Delete and retrace any dependent cable paths + for cablepath in CablePath.objects.filter(path__contains=[object_to_path_node(instance)]): + path, destination, is_connected = trace_path(cablepath.origin) + if path: + CablePath.objects.filter(pk=cablepath.pk).update( + path=path, + destination_type=ContentType.objects.get_for_model(destination) if destination else None, + destination_id=destination.pk if destination else None, + is_connected=is_connected + ) + else: + cablepath.delete() diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 18c15fd16..52b37e3bd 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -132,6 +132,8 @@ class CablePathTestCase(TestCase): :param path: Sequence of objects comprising the intermediate path (optional) :param is_connected: Boolean indicating whether the end-to-end path is complete and active (optional) :param msg: Custom failure message (optional) + + :return: The matching CablePath (if any) """ kwargs = { 'origin_type': ContentType.objects.get_for_model(origin), @@ -152,7 +154,34 @@ class CablePathTestCase(TestCase): msg = f"Missing path from {origin} to {destination}" else: msg = f"Missing partial path originating from {origin}" - self.assertEqual(CablePath.objects.filter(**kwargs).count(), 1, msg=msg) + + cablepath = CablePath.objects.filter(**kwargs).first() + self.assertIsNotNone(cablepath, msg=msg) + + return cablepath + + def assertPathIsSet(self, origin, cablepath, msg=None): + """ + Assert that a specific CablePath instance is set as the path on the origin. + + :param origin: The originating path endpoint + :param cablepath: The CablePath instance originating from this endpoint + :param msg: Custom failure message (optional) + """ + if msg is None: + msg = f"Path #{cablepath.pk} not set on originating endpoint {origin}" + self.assertEqual(origin._path_id, cablepath.pk, msg=msg) + + def assertPathIsNotSet(self, origin, msg=None): + """ + Assert that a specific CablePath instance is set as the path on the origin. + + :param origin: The originating path endpoint + :param msg: Custom failure message (optional) + """ + if msg is None: + msg = f"Path #{origin._path_id} set as origin on {origin}; should be None!" + self.assertIsNone(origin._path_id, msg=msg) def test_101_interface_to_interface(self): """ @@ -161,19 +190,23 @@ class CablePathTestCase(TestCase): # Create cable 1 cable1 = Cable(termination_a=self.interface1, termination_b=self.interface2) cable1.save() - self.assertPathExists( + path1 = self.assertPathExists( origin=self.interface1, destination=self.interface2, path=(cable1,), is_connected=True ) - self.assertPathExists( + path2 = self.assertPathExists( origin=self.interface2, destination=self.interface1, path=(cable1,), is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.assertPathIsSet(self.interface1, path1) + self.assertPathIsSet(self.interface2, path2) # Delete cable 1 cable1.delete() @@ -181,26 +214,30 @@ class CablePathTestCase(TestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_103_consoleport_to_consoleserverport(self): + def test_102_consoleport_to_consoleserverport(self): """ [CP1] --C1-- [CSP1] """ # Create cable 1 cable1 = Cable(termination_a=self.consoleport1, termination_b=self.consoleserverport1) cable1.save() - self.assertPathExists( + path1 = self.assertPathExists( origin=self.consoleport1, destination=self.consoleserverport1, path=(cable1,), is_connected=True ) - self.assertPathExists( + path2 = self.assertPathExists( origin=self.consoleserverport1, destination=self.consoleport1, path=(cable1,), is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) + self.consoleport1.refresh_from_db() + self.consoleserverport1.refresh_from_db() + self.assertPathIsSet(self.consoleport1, path1) + self.assertPathIsSet(self.consoleserverport1, path2) # Delete cable 1 cable1.delete() @@ -208,26 +245,30 @@ class CablePathTestCase(TestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_104_powerport_to_poweroutlet(self): + def test_103_powerport_to_poweroutlet(self): """ [PP1] --C1-- [PO1] """ # Create cable 1 cable1 = Cable(termination_a=self.powerport1, termination_b=self.poweroutlet1) cable1.save() - self.assertPathExists( + path1 = self.assertPathExists( origin=self.powerport1, destination=self.poweroutlet1, path=(cable1,), is_connected=True ) - self.assertPathExists( + path2 = self.assertPathExists( origin=self.poweroutlet1, destination=self.powerport1, path=(cable1,), is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) + self.powerport1.refresh_from_db() + self.poweroutlet1.refresh_from_db() + self.assertPathIsSet(self.powerport1, path1) + self.assertPathIsSet(self.poweroutlet1, path2) # Delete cable 1 cable1.delete() @@ -235,26 +276,30 @@ class CablePathTestCase(TestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_105_powerport_to_powerfeed(self): + def test_104_powerport_to_powerfeed(self): """ [PP1] --C1-- [PF1] """ # Create cable 1 cable1 = Cable(termination_a=self.powerport1, termination_b=self.powerfeed1) cable1.save() - self.assertPathExists( + path1 = self.assertPathExists( origin=self.powerport1, destination=self.powerfeed1, path=(cable1,), is_connected=True ) - self.assertPathExists( + path2 = self.assertPathExists( origin=self.powerfeed1, destination=self.powerport1, path=(cable1,), is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) + self.powerport1.refresh_from_db() + self.powerfeed1.refresh_from_db() + self.assertPathIsSet(self.powerport1, path1) + self.assertPathIsSet(self.powerfeed1, path2) # Delete cable 1 cable1.delete() @@ -262,26 +307,30 @@ class CablePathTestCase(TestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_106_interface_to_circuittermination(self): + def test_105_interface_to_circuittermination(self): """ [PP1] --C1-- [CT1A] """ # Create cable 1 cable1 = Cable(termination_a=self.interface1, termination_b=self.circuittermination1_A) cable1.save() - self.assertPathExists( + path1 = self.assertPathExists( origin=self.interface1, destination=self.circuittermination1_A, path=(cable1,), is_connected=True ) - self.assertPathExists( + path2 = self.assertPathExists( origin=self.circuittermination1_A, destination=self.interface1, path=(cable1,), is_connected=True ) self.assertEqual(CablePath.objects.count(), 2) + self.interface1.refresh_from_db() + self.circuittermination1_A.refresh_from_db() + self.assertPathIsSet(self.interface1, path1) + self.assertPathIsSet(self.circuittermination1_A, path2) # Delete cable 1 cable1.delete() @@ -293,6 +342,9 @@ class CablePathTestCase(TestCase): """ [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + # Create cable 1 cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port5_1) cable1.save() @@ -323,19 +375,28 @@ class CablePathTestCase(TestCase): # Delete cable 2 cable2.delete() - self.assertPathExists( + path1 = self.assertPathExists( origin=self.interface1, destination=None, path=(cable1, self.front_port5_1, self.rear_port5), is_connected=False ) self.assertEqual(CablePath.objects.count(), 1) + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.assertPathIsSet(self.interface1, path1) + self.assertPathIsNotSet(self.interface2) def test_202_multiple_paths_via_pass_through(self): """ [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [IF3] [IF2] --C2-- [FP1:2] [FP2:2] --C5-- [IF4] """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.interface3.refresh_from_db() + self.interface4.refresh_from_db() + # Create cables 1-2 cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) cable1.save() @@ -377,7 +438,7 @@ class CablePathTestCase(TestCase): cable4.save() cable5 = Cable(termination_a=self.front_port2_2, termination_b=self.interface4) cable5.save() - self.assertPathExists( + path1 = self.assertPathExists( origin=self.interface1, destination=self.interface3, path=( @@ -386,7 +447,7 @@ class CablePathTestCase(TestCase): ), is_connected=True ) - self.assertPathExists( + path2 = self.assertPathExists( origin=self.interface2, destination=self.interface4, path=( @@ -395,7 +456,7 @@ class CablePathTestCase(TestCase): ), is_connected=True ) - self.assertPathExists( + path3 = self.assertPathExists( origin=self.interface3, destination=self.interface1, path=( @@ -404,7 +465,7 @@ class CablePathTestCase(TestCase): ), is_connected=True ) - self.assertPathExists( + path4 = self.assertPathExists( origin=self.interface4, destination=self.interface2, path=( @@ -421,12 +482,25 @@ class CablePathTestCase(TestCase): # Check for four partial paths; one from each interface self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.interface3.refresh_from_db() + self.interface4.refresh_from_db() + self.assertPathIsSet(self.interface1, path1) + self.assertPathIsSet(self.interface2, path2) + self.assertPathIsSet(self.interface3, path3) + self.assertPathIsSet(self.interface4, path4) def test_203_multiple_paths_via_nested_pass_throughs(self): """ [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP2:1] [RP2] --C4-- [RP3] [FP3:1] --C5-- [RP4] [FP4:1] --C6-- [IF3] [IF2] --C2-- [FP1:2] [FP4:2] --C7-- [IF4] """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.interface3.refresh_from_db() + self.interface4.refresh_from_db() + # Create cables 1-2, 6-7 cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) cable1.save() @@ -502,6 +576,11 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [FP3:1] [RP3] --C6-- [RP4] [FP4:1] --C7-- [IF3] [IF2] --C2-- [FP1:2] [FP2:1] --C5-- [FP3:1] [FP4:2] --C8-- [IF4] """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.interface3.refresh_from_db() + self.interface4.refresh_from_db() + # Create cables 1-3, 6-8 cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) cable1.save() @@ -576,6 +655,11 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP5] [RP5] --C4-- [RP2] [FP2:1] --C5-- [IF3] [IF2] --C2-- [FP1:2] [FP2:2] --C6-- [IF4] """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.interface3.refresh_from_db() + self.interface4.refresh_from_db() + # Create cables 1-2, 5-6 cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) # IF1 -> FP1:1 cable1.save() @@ -641,6 +725,9 @@ class CablePathTestCase(TestCase): """ [IF1] --C1-- [FP5] [RP5] --C2-- [RP6] [FP6] --C3-- [IF2] """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + # Create cable 2 cable2 = Cable(termination_a=self.rear_port5, termination_b=self.rear_port6) cable2.save() @@ -684,6 +771,9 @@ class CablePathTestCase(TestCase): """ [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + # Create cables 1 and 2 cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port5_1) cable1.save() From f8800b8303a188c2a7c62ce8f4f38b5e0f99e397 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 2 Oct 2020 21:39:55 -0400 Subject: [PATCH 26/67] Optimize console/power/interface connection lists --- netbox/dcim/tables.py | 57 ++++++++++++++++++++++--------------------- netbox/dcim/views.py | 42 ++++++++++++------------------- 2 files changed, 45 insertions(+), 54 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 7ab08eae4..57cd0b6eb 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -67,8 +67,8 @@ INTERFACE_TAGGED_VLANS = """ {% endfor %} """ -CONNECTION_STATUS = """ -{{ record.get_connection_status_display }} +PATH_STATUS = """ +{% if value %}Connected{% else %}Not Connected{% endif %} """ @@ -813,13 +813,13 @@ class CableTable(BaseTable): class ConsoleConnectionTable(BaseTable): console_server = tables.Column( - accessor=Accessor('path__destination__device'), + accessor=Accessor('_path__destination__device'), orderable=False, linkify=True, verbose_name='Console Server' ) console_server_port = tables.Column( - accessor=Accessor('path__destination'), + accessor=Accessor('_path__destination'), orderable=False, linkify=True, verbose_name='Port' @@ -831,27 +831,28 @@ class ConsoleConnectionTable(BaseTable): linkify=True, verbose_name='Console Port' ) - connection_status = tables.TemplateColumn( - accessor=Accessor('path__is_connected'), - orderable=False, - template_code=CONNECTION_STATUS, - verbose_name='Status' + path_status = tables.TemplateColumn( + accessor=Accessor('_path__is_connected'), + template_code=PATH_STATUS, + verbose_name='Path Status' ) + add_prefetch = False + class Meta(BaseTable.Meta): model = ConsolePort - fields = ('console_server', 'console_server_port', 'device', 'name', 'connection_status') + fields = ('console_server', 'console_server_port', 'device', 'name', 'path_status') class PowerConnectionTable(BaseTable): pdu = tables.Column( - accessor=Accessor('path__destination__device'), + accessor=Accessor('_path__destination__device'), orderable=False, linkify=True, verbose_name='PDU' ) outlet = tables.Column( - accessor=Accessor('path__destination'), + accessor=Accessor('_path__destination'), orderable=False, linkify=True, verbose_name='Outlet' @@ -863,16 +864,17 @@ class PowerConnectionTable(BaseTable): linkify=True, verbose_name='Power Port' ) - connection_status = tables.TemplateColumn( - accessor=Accessor('path__is_connected'), - orderable=False, - template_code=CONNECTION_STATUS, - verbose_name='Status' + path_status = tables.TemplateColumn( + accessor=Accessor('_path__is_connected'), + template_code=PATH_STATUS, + verbose_name='Path Status' ) + add_prefetch = False + class Meta(BaseTable.Meta): model = PowerPort - fields = ('pdu', 'outlet', 'device', 'name', 'connection_status') + fields = ('pdu', 'outlet', 'device', 'name', 'path_status') class InterfaceConnectionTable(BaseTable): @@ -887,29 +889,28 @@ class InterfaceConnectionTable(BaseTable): verbose_name='Interface A' ) device_b = tables.Column( - accessor=Accessor('path__destination__device'), + accessor=Accessor('_path__destination__device'), orderable=False, linkify=True, verbose_name='Device B' ) interface_b = tables.Column( - accessor=Accessor('path__destination'), + accessor=Accessor('_path__destination'), orderable=False, linkify=True, verbose_name='Interface B' ) - connection_status = tables.TemplateColumn( - accessor=Accessor('path__is_connected'), - orderable=False, - template_code=CONNECTION_STATUS, - verbose_name='Status' + path_status = tables.TemplateColumn( + accessor=Accessor('_path__is_connected'), + template_code=PATH_STATUS, + verbose_name='Path Status' ) + add_prefetch = False + class Meta(BaseTable.Meta): model = Interface - fields = ( - 'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status', - ) + fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'path_status') # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ac30461b0..87bd38309 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2079,12 +2079,8 @@ class CableBulkDeleteView(BulkDeleteView): class ConsoleConnectionsListView(ObjectListView): queryset = ConsolePort.objects.prefetch_related( - 'device', 'connected_endpoint__device' - ).filter( - connected_endpoint__isnull=False - ).order_by( - 'cable', 'connected_endpoint__device__name', 'connected_endpoint__name' - ) + 'device', '_path__destination__device' + ).filter(_path__isnull=False).order_by('device') filterset = filters.ConsoleConnectionFilterSet filterset_form = forms.ConsoleConnectionFilterForm table = tables.ConsoleConnectionTable @@ -2097,11 +2093,11 @@ class ConsoleConnectionsListView(ObjectListView): ] for obj in self.queryset: csv = csv_format([ - obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, - obj.connected_endpoint.name if obj.connected_endpoint else None, + obj._path.destination.device.identifier if obj._path.destination else None, + obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - obj.get_connection_status_display(), + 'Connected' if obj._path.is_connected else 'Not Connected', ]) csv_data.append(csv) @@ -2110,12 +2106,8 @@ class ConsoleConnectionsListView(ObjectListView): class PowerConnectionsListView(ObjectListView): queryset = PowerPort.objects.prefetch_related( - 'device', '_connected_poweroutlet__device' - ).filter( - _connected_poweroutlet__isnull=False - ).order_by( - 'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name' - ) + 'device', '_path__destination__device' + ).filter(_path__isnull=False).order_by('device') filterset = filters.PowerConnectionFilterSet filterset_form = forms.PowerConnectionFilterForm table = tables.PowerConnectionTable @@ -2128,11 +2120,11 @@ class PowerConnectionsListView(ObjectListView): ] for obj in self.queryset: csv = csv_format([ - obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, - obj.connected_endpoint.name if obj.connected_endpoint else None, + obj._path.destination.device.identifier if obj._path.destination else None, + obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - obj.get_connection_status_display(), + 'Connected' if obj._path.is_connected else 'Not Connected', ]) csv_data.append(csv) @@ -2141,14 +2133,12 @@ class PowerConnectionsListView(ObjectListView): class InterfaceConnectionsListView(ObjectListView): queryset = Interface.objects.prefetch_related( - 'device', 'cable', '_connected_interface__device' + 'device', '_path__destination__device' ).filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair - _connected_interface__isnull=False, + _path__isnull=False, pk__lt=F('_connected_interface') - ).order_by( - 'device' - ) + ).order_by('device') filterset = filters.InterfaceConnectionFilterSet filterset_form = forms.InterfaceConnectionFilterForm table = tables.InterfaceConnectionTable @@ -2163,11 +2153,11 @@ class InterfaceConnectionsListView(ObjectListView): ] for obj in self.queryset: csv = csv_format([ - obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, - obj.connected_endpoint.name if obj.connected_endpoint else None, + obj._path.destination.device.identifier if obj._path.destination else None, + obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - obj.get_connection_status_display(), + 'Connected' if obj._path.is_connected else 'Not Connected', ]) csv_data.append(csv) From 079c42291c132e46d61125eef46e90d59403b739 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 09:56:46 -0400 Subject: [PATCH 27/67] Remove legacy connected endpoint fields --- netbox/circuits/api/views.py | 6 +- .../0022_drop_connected_endpoint.py | 17 +++ netbox/circuits/models.py | 7 -- netbox/circuits/views.py | 4 +- netbox/dcim/api/serializers.py | 5 +- netbox/dcim/api/views.py | 38 ++---- netbox/dcim/filters.py | 81 ++++++------ .../0121_drop_connected_endpoint.py | 37 ++++++ netbox/dcim/models/device_components.py | 117 ++---------------- netbox/dcim/models/power.py | 7 -- netbox/dcim/views.py | 8 +- netbox/netbox/views.py | 14 +-- netbox/templates/dcim/interface.html | 112 +++++++++-------- 13 files changed, 190 insertions(+), 263 deletions(-) create mode 100644 netbox/circuits/migrations/0022_drop_connected_endpoint.py create mode 100644 netbox/dcim/migrations/0121_drop_connected_endpoint.py diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index cd73a614d..1c8ad69e4 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -46,9 +46,7 @@ class CircuitTypeViewSet(ModelViewSet): class CircuitViewSet(CustomFieldModelViewSet): queryset = Circuit.objects.prefetch_related( - Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related( - 'site', 'connected_endpoint__device' - )), + Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related('site')), 'type', 'tenant', 'provider', ).prefetch_related('tags') serializer_class = serializers.CircuitSerializer @@ -61,7 +59,7 @@ class CircuitViewSet(CustomFieldModelViewSet): class CircuitTerminationViewSet(ModelViewSet): queryset = CircuitTermination.objects.prefetch_related( - 'circuit', 'site', 'connected_endpoint__device', 'cable' + 'circuit', 'site', 'cable' ) serializer_class = serializers.CircuitTerminationSerializer filterset_class = filters.CircuitTerminationFilterSet diff --git a/netbox/circuits/migrations/0022_drop_connected_endpoint.py b/netbox/circuits/migrations/0022_drop_connected_endpoint.py new file mode 100644 index 000000000..59647dd2b --- /dev/null +++ b/netbox/circuits/migrations/0022_drop_connected_endpoint.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-10-05 13:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0021_cablepath'), + ] + + operations = [ + migrations.RemoveField( + model_name='circuittermination', + name='connected_endpoint', + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 686ab9219..746a71b3c 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -248,13 +248,6 @@ class CircuitTermination(PathEndpoint, CableTermination): on_delete=models.PROTECT, related_name='circuit_terminations' ) - connected_endpoint = models.OneToOneField( - to='dcim.Interface', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index e5da5100f..968c88777 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -131,7 +131,7 @@ class CircuitView(ObjectView): circuit = get_object_or_404(self.queryset, pk=pk) termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related( - 'site__region', 'connected_endpoint__device' + 'site__region' ).filter( circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A ).first() @@ -139,7 +139,7 @@ class CircuitView(ObjectView): termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view') termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related( - 'site__region', 'connected_endpoint__device' + 'site__region' ).filter( circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z ).first() diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 8078f8819..0cf78fafc 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -34,8 +34,7 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): def get_connected_endpoint_type(self, obj): if obj.path is not None: - destination = obj.path.destination - return f'{destination._meta.app_label}.{destination._meta.model_name}' + return f'{obj.connected_endpoint._meta.app_label}.{obj.connected_endpoint._meta.model_name}' return None @swagger_serializer_method(serializer_or_field=serializers.DictField) @@ -44,7 +43,7 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): Return the appropriate serializer for the type of connected object. """ if obj.path is not None: - serializer = get_serializer_for_model(obj.path.destination, prefix='Nested') + serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested') context = {'request': self.context['request']} return serializer(obj.path.destination, context=context).data return None diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index edbdfb7be..6ed50324e 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -470,37 +470,31 @@ class DeviceViewSet(CustomFieldModelViewSet): # class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') + queryset = ConsolePort.objects.prefetch_related('device', '_path', 'cable', 'tags') serializer_class = serializers.ConsolePortSerializer filterset_class = filters.ConsolePortFilterSet class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') + queryset = ConsoleServerPort.objects.prefetch_related('device', '_path', 'cable', 'tags') serializer_class = serializers.ConsoleServerPortSerializer filterset_class = filters.ConsoleServerPortFilterSet class PowerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerPort.objects.prefetch_related( - 'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags' - ) + queryset = PowerPort.objects.prefetch_related('device', '_path', 'cable', 'tags') serializer_class = serializers.PowerPortSerializer filterset_class = filters.PowerPortFilterSet class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') + queryset = PowerOutlet.objects.prefetch_related('device', '_path', 'cable', 'tags') serializer_class = serializers.PowerOutletSerializer filterset_class = filters.PowerOutletFilterSet class InterfaceViewSet(PathEndpointMixin, ModelViewSet): - queryset = Interface.objects.prefetch_related( - 'device', '_connected_interface', '_connected_circuittermination', 'cable', 'ip_addresses', 'tags' - ).filter( - device__isnull=False - ) + queryset = Interface.objects.prefetch_related('device', '_path', 'cable', 'ip_addresses', 'tags') serializer_class = serializers.InterfaceSerializer filterset_class = filters.InterfaceFilterSet @@ -534,32 +528,26 @@ class InventoryItemViewSet(ModelViewSet): # class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet): - queryset = ConsolePort.objects.prefetch_related( - 'device', 'connected_endpoint__device' - ).filter( - connected_endpoint__isnull=False + queryset = ConsolePort.objects.prefetch_related('device', '_path').filter( + _path__destination_id__isnull=False ) serializer_class = serializers.ConsolePortSerializer filterset_class = filters.ConsoleConnectionFilterSet class PowerConnectionViewSet(ListModelMixin, GenericViewSet): - queryset = PowerPort.objects.prefetch_related( - 'device', 'connected_endpoint__device' - ).filter( - _connected_poweroutlet__isnull=False + queryset = PowerPort.objects.prefetch_related('device', '_path').filter( + _path__destination_id__isnull=False ) serializer_class = serializers.PowerPortSerializer filterset_class = filters.PowerConnectionFilterSet class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): - queryset = Interface.objects.prefetch_related( - 'device', '_connected_interface__device' - ).filter( + queryset = Interface.objects.prefetch_related('device', '_path').filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair - _connected_interface__isnull=False, - pk__lt=F('_connected_interface') + _path__destination_id__isnull=False, + pk__lt=F('_path__destination_id') ) serializer_class = serializers.InterfaceConnectionSerializer filterset_class = filters.InterfaceConnectionFilterSet @@ -664,7 +652,7 @@ class ConnectedDeviceViewSet(ViewSet): device__name=peer_device_name, name=peer_interface_name ) - local_interface = peer_interface._connected_interface + local_interface = peer_interface.connected_endpoint if local_interface is None: return Response() diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index c76bd3b87..aa5c62552 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1171,18 +1171,19 @@ class ConsoleConnectionFilterSet(BaseFilterSet): model = ConsolePort fields = ['name', 'connection_status'] - def filter_site(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter(connected_endpoint__device__site__slug=value) - - def filter_device(self, queryset, name, value): - if not value: - return queryset - return queryset.filter( - Q(**{'{}__in'.format(name): value}) | - Q(**{'connected_endpoint__{}__in'.format(name): value}) - ) + # TODO: Fix filters + # def filter_site(self, queryset, name, value): + # if not value.strip(): + # return queryset + # return queryset.filter(connected_endpoint__device__site__slug=value) + # + # def filter_device(self, queryset, name, value): + # if not value: + # return queryset + # return queryset.filter( + # Q(**{'{}__in'.format(name): value}) | + # Q(**{'connected_endpoint__{}__in'.format(name): value}) + # ) class PowerConnectionFilterSet(BaseFilterSet): @@ -1202,18 +1203,19 @@ class PowerConnectionFilterSet(BaseFilterSet): model = PowerPort fields = ['name', 'connection_status'] - def filter_site(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter(_connected_poweroutlet__device__site__slug=value) - - def filter_device(self, queryset, name, value): - if not value: - return queryset - return queryset.filter( - Q(**{'{}__in'.format(name): value}) | - Q(**{'_connected_poweroutlet__{}__in'.format(name): value}) - ) + # TODO: Fix filters + # def filter_site(self, queryset, name, value): + # if not value.strip(): + # return queryset + # return queryset.filter(_connected_poweroutlet__device__site__slug=value) + # + # def filter_device(self, queryset, name, value): + # if not value: + # return queryset + # return queryset.filter( + # Q(**{'{}__in'.format(name): value}) | + # Q(**{'_connected_poweroutlet__{}__in'.format(name): value}) + # ) class InterfaceConnectionFilterSet(BaseFilterSet): @@ -1233,21 +1235,22 @@ class InterfaceConnectionFilterSet(BaseFilterSet): model = Interface fields = ['connection_status'] - def filter_site(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(device__site__slug=value) | - Q(_connected_interface__device__site__slug=value) - ) - - def filter_device(self, queryset, name, value): - if not value: - return queryset - return queryset.filter( - Q(**{'{}__in'.format(name): value}) | - Q(**{'_connected_interface__{}__in'.format(name): value}) - ) + # TODO: Fix filters + # def filter_site(self, queryset, name, value): + # if not value.strip(): + # return queryset + # return queryset.filter( + # Q(device__site__slug=value) | + # Q(_connected_interface__device__site__slug=value) + # ) + # + # def filter_device(self, queryset, name, value): + # if not value: + # return queryset + # return queryset.filter( + # Q(**{'{}__in'.format(name): value}) | + # Q(**{'_connected_interface__{}__in'.format(name): value}) + # ) class PowerPanelFilterSet(BaseFilterSet): diff --git a/netbox/dcim/migrations/0121_drop_connected_endpoint.py b/netbox/dcim/migrations/0121_drop_connected_endpoint.py new file mode 100644 index 000000000..f05cfdd1a --- /dev/null +++ b/netbox/dcim/migrations/0121_drop_connected_endpoint.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1 on 2020-10-05 13:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0120_cablepath'), + ] + + operations = [ + migrations.RemoveField( + model_name='consoleport', + name='connected_endpoint', + ), + migrations.RemoveField( + model_name='interface', + name='_connected_circuittermination', + ), + migrations.RemoveField( + model_name='interface', + name='_connected_interface', + ), + migrations.RemoveField( + model_name='powerfeed', + name='connected_endpoint', + ), + migrations.RemoveField( + model_name='powerport', + name='_connected_powerfeed', + ), + migrations.RemoveField( + model_name='powerport', + name='_connected_poweroutlet', + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 65032f529..72edfc46e 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -150,6 +150,15 @@ class PathEndpoint(models.Model): # Return the path as a list of three-tuples (A termination, cable, B termination) return list(zip(*[iter(path)] * 3)) + @property + def connected_endpoint(self): + """ + Caching accessor for the attached CablePath's destination (if any) + """ + if not hasattr(self, '_connected_endpoint'): + self._connected_endpoint = self._path.destination if self._path else None + return self._connected_endpoint + # # Console ports @@ -166,13 +175,6 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel): blank=True, help_text='Physical port type' ) - connected_endpoint = models.OneToOneField( - to='dcim.ConsoleServerPort', - on_delete=models.SET_NULL, - related_name='connected_endpoint', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, @@ -267,20 +269,6 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): validators=[MinValueValidator(1)], help_text="Allocated power draw (watts)" ) - _connected_poweroutlet = models.OneToOneField( - to='dcim.PowerOutlet', - on_delete=models.SET_NULL, - related_name='connected_endpoint', - blank=True, - null=True - ) - _connected_powerfeed = models.OneToOneField( - to='dcim.PowerFeed', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, @@ -308,43 +296,6 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): self.description, ) - @property - def connected_endpoint(self): - """ - Return the connected PowerOutlet, if it exists, or the connected PowerFeed, if it exists. We have to check for - ObjectDoesNotExist in case the referenced object has been deleted from the database. - """ - try: - if self._connected_poweroutlet: - return self._connected_poweroutlet - except ObjectDoesNotExist: - pass - try: - if self._connected_powerfeed: - return self._connected_powerfeed - except ObjectDoesNotExist: - pass - return None - - @connected_endpoint.setter - def connected_endpoint(self, value): - # TODO: Fix circular import - from . import PowerFeed - - if value is None: - self._connected_poweroutlet = None - self._connected_powerfeed = None - elif isinstance(value, PowerOutlet): - self._connected_poweroutlet = value - self._connected_powerfeed = None - elif isinstance(value, PowerFeed): - self._connected_poweroutlet = None - self._connected_powerfeed = value - else: - raise ValueError( - "Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value)) - ) - def get_power_draw(self): """ Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort. @@ -497,20 +448,6 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): max_length=100, blank=True ) - _connected_interface = models.OneToOneField( - to='self', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - _connected_circuittermination = models.OneToOneField( - to='circuits.CircuitTermination', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, @@ -631,42 +568,6 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): return super().save(*args, **kwargs) - @property - def connected_endpoint(self): - """ - Return the connected Interface, if it exists, or the connected CircuitTermination, if it exists. We have to - check for ObjectDoesNotExist in case the referenced object has been deleted from the database. - """ - try: - if self._connected_interface: - return self._connected_interface - except ObjectDoesNotExist: - pass - try: - if self._connected_circuittermination: - return self._connected_circuittermination - except ObjectDoesNotExist: - pass - return None - - @connected_endpoint.setter - def connected_endpoint(self, value): - from circuits.models import CircuitTermination - - if value is None: - self._connected_interface = None - self._connected_circuittermination = None - elif isinstance(value, Interface): - self._connected_interface = value - self._connected_circuittermination = None - elif isinstance(value, CircuitTermination): - self._connected_interface = None - self._connected_circuittermination = value - else: - raise ValueError( - "Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value)) - ) - @property def parent(self): return self.device diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index caa22e74a..ec1480a7e 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -88,13 +88,6 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo blank=True, null=True ) - connected_endpoint = models.OneToOneField( - to='dcim.PowerPort', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 87bd38309..7a1028dec 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1122,10 +1122,8 @@ class DeviceLLDPNeighborsView(ObjectView): def get(self, request, pk): device = get_object_or_404(self.queryset, pk=pk) - interfaces = device.vc_interfaces.restrict(request.user, 'view').exclude( + interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related('_path').exclude( type__in=NONCONNECTABLE_IFACE_TYPES - ).prefetch_related( - '_connected_interface__device' ) return render(request, 'dcim/device_lldp_neighbors.html', { @@ -1483,8 +1481,6 @@ class InterfaceView(ObjectView): return render(request, 'dcim/interface.html', { 'instance': interface, - 'connected_interface': interface._connected_interface, - 'connected_circuittermination': interface._connected_circuittermination, 'ipaddress_table': ipaddress_table, 'vlan_table': vlan_table, }) @@ -2137,7 +2133,7 @@ class InterfaceConnectionsListView(ObjectListView): ).filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair _path__isnull=False, - pk__lt=F('_connected_interface') + pk__lt=F('_path__destination_id') ).order_by('device') filterset = filters.InterfaceConnectionFilterSet filterset_form = forms.InterfaceConnectionFilterForm diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index b2bee0f96..161bfda74 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -190,15 +190,15 @@ class HomeView(View): def get(self, request): - connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').filter( - connected_endpoint__isnull=False + connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( + _path__destination_id__isnull=False ) - connected_powerports = PowerPort.objects.restrict(request.user, 'view').filter( - _connected_poweroutlet__isnull=False + connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( + _path__destination_id__isnull=False ) - connected_interfaces = Interface.objects.restrict(request.user, 'view').filter( - _connected_interface__isnull=False, - pk__lt=F('_connected_interface') + connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter( + _path__destination_id__isnull=False, + pk__lt=F('_path__destination_id') ) # Report Results diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 7fcf6ab0a..12b9918e9 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -77,61 +77,63 @@ {% if instance.cable %} - {% if connected_interface %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% elif connected_circuittermination %} - {% with ct=connected_circuittermination %} + {% if instance.connected_endpoint.device %} + {% with iface=instance.connected_endpoint %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endwith %} + {% elif instance.connected_endpoint.circuit %} + {% with ct=instance.connected_endpoint %} From df737371289928bbcbef4bfec3ede8335f0ec630 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 10:08:16 -0400 Subject: [PATCH 28/67] Remove legacy connection_status fields --- .../0022_drop_connected_endpoint.py | 6 ++++- netbox/circuits/models.py | 6 ----- netbox/dcim/api/nested_serializers.py | 18 +++++-------- netbox/dcim/api/serializers.py | 16 ++++++------ netbox/dcim/filters.py | 12 +++------ .../0121_drop_connected_endpoint.py | 26 ++++++++++++++++++- netbox/dcim/models/device_components.py | 25 ------------------ netbox/dcim/models/power.py | 5 ---- netbox/dcim/tests/test_api.py | 14 +++++----- netbox/dcim/utils.py | 2 +- netbox/dcim/views.py | 15 ++++------- 11 files changed, 61 insertions(+), 84 deletions(-) diff --git a/netbox/circuits/migrations/0022_drop_connected_endpoint.py b/netbox/circuits/migrations/0022_drop_connected_endpoint.py index 59647dd2b..e5540b44d 100644 --- a/netbox/circuits/migrations/0022_drop_connected_endpoint.py +++ b/netbox/circuits/migrations/0022_drop_connected_endpoint.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1 on 2020-10-05 13:56 +# Generated by Django 3.1 on 2020-10-05 14:07 from django.db import migrations @@ -14,4 +14,8 @@ class Migration(migrations.Migration): model_name='circuittermination', name='connected_endpoint', ), + migrations.RemoveField( + model_name='circuittermination', + name='connection_status', + ), ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 746a71b3c..725fe4b3f 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -2,7 +2,6 @@ from django.db import models from django.urls import reverse from taggit.managers import TaggableManager -from dcim.constants import CONNECTION_STATUS_CHOICES from dcim.fields import ASNField from dcim.models import CableTermination, PathEndpoint from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem @@ -248,11 +247,6 @@ class CircuitTermination(PathEndpoint, CableTermination): on_delete=models.PROTECT, related_name='circuit_terminations' ) - connection_status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True, - null=True - ) port_speed = models.PositiveIntegerField( verbose_name='Port speed (Kbps)' ) diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 40b03ada6..159540ece 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -1,8 +1,7 @@ from rest_framework import serializers -from dcim.constants import CONNECTION_STATUS_CHOICES from dcim import models -from utilities.api import ChoiceField, WritableNestedSerializer +from utilities.api import WritableNestedSerializer __all__ = [ 'NestedCableSerializer', @@ -228,51 +227,46 @@ class NestedDeviceSerializer(WritableNestedSerializer): class NestedConsoleServerPortSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') device = NestedDeviceSerializer(read_only=True) - connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) class Meta: model = models.ConsoleServerPort - fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] + fields = ['id', 'url', 'device', 'name', 'cable'] class NestedConsolePortSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') device = NestedDeviceSerializer(read_only=True) - connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) class Meta: model = models.ConsolePort - fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] + fields = ['id', 'url', 'device', 'name', 'cable'] class NestedPowerOutletSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') device = NestedDeviceSerializer(read_only=True) - connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) class Meta: model = models.PowerOutlet - fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] + fields = ['id', 'url', 'device', 'name', 'cable'] class NestedPowerPortSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') device = NestedDeviceSerializer(read_only=True) - connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) class Meta: model = models.PowerPort - fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] + fields = ['id', 'url', 'device', 'name', 'cable'] class NestedInterfaceSerializer(WritableNestedSerializer): device = NestedDeviceSerializer(read_only=True) url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') - connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) class Meta: model = models.Interface - fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] + fields = ['id', 'url', 'device', 'name', 'cable'] class NestedRearPortSerializer(WritableNestedSerializer): diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 0cf78fafc..b591ae36a 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -33,8 +33,8 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): connection_status = serializers.SerializerMethodField(read_only=True) def get_connected_endpoint_type(self, obj): - if obj.path is not None: - return f'{obj.connected_endpoint._meta.app_label}.{obj.connected_endpoint._meta.model_name}' + if obj._path is not None and obj._path.destination is not None: + return f'{obj._path.destination._meta.app_label}.{obj._path.destination._meta.model_name}' return None @swagger_serializer_method(serializer_or_field=serializers.DictField) @@ -42,17 +42,17 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): """ Return the appropriate serializer for the type of connected object. """ - if obj.path is not None: - serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested') + if obj._path is not None and obj._path.destination 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 serializer(obj._path.destination, context=context).data return None # TODO: Tweak the representation for this field @swagger_serializer_method(serializer_or_field=serializers.BooleanField) def get_connection_status(self, obj): - if obj.path is not None: - return obj.path.is_connected + if obj._path is not None: + return obj._path.is_connected return None @@ -716,7 +716,7 @@ class TracedCableSerializer(serializers.ModelSerializer): class InterfaceConnectionSerializer(ValidatedModelSerializer): interface_a = serializers.SerializerMethodField() interface_b = NestedInterfaceSerializer(source='connected_endpoint') - connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) + # connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) class Meta: model = Interface diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index aa5c62552..d16f5d1aa 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -760,11 +760,7 @@ class PathEndpointFilterSet(django_filters.FilterSet): ) def filter_is_connected(self, queryset, name, value): - kwargs = {'connected_paths': 1 if value else 0} - # TODO: Boolean rather than Count()? - return queryset.annotate( - connected_paths=Count('_paths', filter=Q(_paths__is_connected=True)) - ).filter(**kwargs) + return queryset.filter(_path__is_connected=True) class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): @@ -1169,7 +1165,7 @@ class ConsoleConnectionFilterSet(BaseFilterSet): class Meta: model = ConsolePort - fields = ['name', 'connection_status'] + fields = ['name'] # TODO: Fix filters # def filter_site(self, queryset, name, value): @@ -1201,7 +1197,7 @@ class PowerConnectionFilterSet(BaseFilterSet): class Meta: model = PowerPort - fields = ['name', 'connection_status'] + fields = ['name'] # TODO: Fix filters # def filter_site(self, queryset, name, value): @@ -1233,7 +1229,7 @@ class InterfaceConnectionFilterSet(BaseFilterSet): class Meta: model = Interface - fields = ['connection_status'] + fields = [] # TODO: Fix filters # def filter_site(self, queryset, name, value): diff --git a/netbox/dcim/migrations/0121_drop_connected_endpoint.py b/netbox/dcim/migrations/0121_drop_connected_endpoint.py index f05cfdd1a..9404ecf53 100644 --- a/netbox/dcim/migrations/0121_drop_connected_endpoint.py +++ b/netbox/dcim/migrations/0121_drop_connected_endpoint.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1 on 2020-10-05 13:56 +# Generated by Django 3.1 on 2020-10-05 14:07 from django.db import migrations @@ -14,6 +14,14 @@ class Migration(migrations.Migration): model_name='consoleport', name='connected_endpoint', ), + migrations.RemoveField( + model_name='consoleport', + name='connection_status', + ), + migrations.RemoveField( + model_name='consoleserverport', + name='connection_status', + ), migrations.RemoveField( model_name='interface', name='_connected_circuittermination', @@ -22,10 +30,22 @@ class Migration(migrations.Migration): model_name='interface', name='_connected_interface', ), + migrations.RemoveField( + model_name='interface', + name='connection_status', + ), migrations.RemoveField( model_name='powerfeed', name='connected_endpoint', ), + migrations.RemoveField( + model_name='powerfeed', + name='connection_status', + ), + migrations.RemoveField( + model_name='poweroutlet', + name='connection_status', + ), migrations.RemoveField( model_name='powerport', name='_connected_powerfeed', @@ -34,4 +54,8 @@ class Migration(migrations.Migration): model_name='powerport', name='_connected_poweroutlet', ), + migrations.RemoveField( + model_name='powerport', + name='connection_status', + ), ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 72edfc46e..863b39e62 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -175,11 +175,6 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel): blank=True, help_text='Physical port type' ) - connection_status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True, - null=True - ) tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'label', 'type', 'description'] @@ -216,11 +211,6 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel): blank=True, help_text='Physical port type' ) - connection_status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True, - null=True - ) tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'label', 'type', 'description'] @@ -269,11 +259,6 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): validators=[MinValueValidator(1)], help_text="Allocated power draw (watts)" ) - connection_status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True, - null=True - ) tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description'] @@ -368,11 +353,6 @@ class PowerOutlet(CableTermination, PathEndpoint, ComponentModel): blank=True, help_text="Phase (for three-phase feeds)" ) - connection_status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True, - null=True - ) tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'] @@ -448,11 +428,6 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): max_length=100, blank=True ) - connection_status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True, - null=True - ) lag = models.ForeignKey( to='self', on_delete=models.SET_NULL, diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index ec1480a7e..f869a3af4 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -88,11 +88,6 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo blank=True, null=True ) - connection_status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True, - null=True - ) name = models.CharField( max_length=50 ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 528301f8f..5c13b5122 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -977,7 +977,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase): class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = ConsolePort - brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] + brief_fields = ['cable', 'device', 'id', 'name', 'url'] bulk_update_data = { 'description': 'New description', } @@ -1016,7 +1016,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = ConsoleServerPort - brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] + brief_fields = ['cable', 'device', 'id', 'name', 'url'] bulk_update_data = { 'description': 'New description', } @@ -1055,7 +1055,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = PowerPort - brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] + brief_fields = ['cable', 'device', 'id', 'name', 'url'] bulk_update_data = { 'description': 'New description', } @@ -1094,7 +1094,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = PowerOutlet - brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] + brief_fields = ['cable', 'device', 'id', 'name', 'url'] bulk_update_data = { 'description': 'New description', } @@ -1133,7 +1133,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = Interface - brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] + brief_fields = ['cable', 'device', 'id', 'name', 'url'] bulk_update_data = { 'description': 'New description', } @@ -1189,7 +1189,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase ] -class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): +class FrontPortTest(APIViewTestCases.APIViewTestCase): model = FrontPort brief_fields = ['cable', 'device', 'id', 'name', 'url'] bulk_update_data = { @@ -1247,7 +1247,7 @@ class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase ] -class RearPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): +class RearPortTest(APIViewTestCases.APIViewTestCase): model = RearPort brief_fields = ['cable', 'device', 'id', 'name', 'url'] bulk_update_data = { diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 4ef902dc7..40896f97b 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -26,7 +26,7 @@ def trace_path(node): position_stack = [] is_connected = True - if node.cable is None: + if node is None or node.cable is None: return [], None, False while node.cable is not None: diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 7a1028dec..9290a2b4a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1018,36 +1018,31 @@ 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)), - 'cable', + 'cable', '_path', ) # Console server ports consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter( device=device ).prefetch_related( - Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), - 'cable', + 'cable', '_path', ) # Power ports powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), - 'cable', + 'cable', '_path', ) # Power outlets poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)), - 'cable', 'power_port', + 'cable', 'power_port', '_path', ) # Interfaces interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related( - 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', + 'lag', 'cable', '_path', 'tags', ) # Front ports From 13db22d39264d91982a6ba758e995e9fe53d3830 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 11:07:03 -0400 Subject: [PATCH 29/67] Initial changelog notes for #4900 --- docs/release-notes/version-2.10.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index ef693a9de..c49283d14 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -34,6 +34,12 @@ http://netbox/api/dcim/sites/ \ --data '[{"id": 10, "description": "Foo"}, {"id": 11, "description": "Bar"}]' ``` +#### Improved Cable Trace Performance ([#4900](https://github.com/netbox-community/netbox/issues/4900)) + +All end-to-end cable paths are now cached using the new CablePath model. This allows NetBox to now immediately return the complete path originating from any endpoint directly from the database, rather than having to trace each cable recursively. It also resolves some systemic validation issues with the original implementation. + +**Note:** As part of this change, cable traces will no longer traverse circuits: A circuit termination will be considered the origin or destination of an end-to-end path. + ### Enhancements * [#1503](https://github.com/netbox-community/netbox/issues/1503) - Allow assigment of secrets to virtual machines @@ -56,9 +62,11 @@ http://netbox/api/dcim/sites/ \ * Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints * dcim.Cable: Added `custom_fields` +* dcim.FrontPort: Removed the `trace` endpoint * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning * dcim.PowerPanel: Added `custom_fields` * dcim.RackReservation: Added `custom_fields` +* dcim.RearPort: Removed the `trace` endpoint * dcim.VirtualChassis: Added `custom_fields` * extras.ExportTemplate: The `template_language` field has been removed * extras.Graph: This API endpoint has been removed (see #4349) From 3d34f1cdcb55d82bb484c5141df5788f4afa0c6a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 11:13:33 -0400 Subject: [PATCH 30/67] Rename CablePath.is_connected to is_active --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/filters.py | 2 +- netbox/dcim/migrations/0120_cablepath.py | 2 +- netbox/dcim/models/devices.py | 2 +- netbox/dcim/signals.py | 10 +-- netbox/dcim/tables.py | 6 +- netbox/dcim/tests/test_cablepaths.py | 92 ++++++++++++------------ netbox/dcim/utils.py | 8 +-- netbox/dcim/views.py | 6 +- 9 files changed, 65 insertions(+), 65 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index b591ae36a..144e764dc 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -52,7 +52,7 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): @swagger_serializer_method(serializer_or_field=serializers.BooleanField) def get_connection_status(self, obj): if obj._path is not None: - return obj._path.is_connected + return obj._path.is_active return None diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index d16f5d1aa..74b5903a6 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -760,7 +760,7 @@ class PathEndpointFilterSet(django_filters.FilterSet): ) def filter_is_connected(self, queryset, name, value): - return queryset.filter(_path__is_connected=True) + return queryset.filter(_path__is_active=True) class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): diff --git a/netbox/dcim/migrations/0120_cablepath.py b/netbox/dcim/migrations/0120_cablepath.py index 0d2199e31..2b58f119a 100644 --- a/netbox/dcim/migrations/0120_cablepath.py +++ b/netbox/dcim/migrations/0120_cablepath.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): ('origin_id', models.PositiveIntegerField()), ('destination_id', models.PositiveIntegerField(blank=True, null=True)), ('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)), - ('is_connected', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=False)), ('destination_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), ('origin_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), ], diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index ea7332b3d..1f9db0986 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1191,7 +1191,7 @@ class CablePath(models.Model): fk_field='destination_id' ) path = PathField() - is_connected = models.BooleanField( + is_active = models.BooleanField( default=False ) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 13653c7d4..6079417c1 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -14,9 +14,9 @@ def create_cablepath(node): """ Create CablePaths for all paths originating from the specified node. """ - path, destination, is_connected = trace_path(node) + path, destination, is_active = trace_path(node) if path: - cp = CablePath(origin=node, path=path, destination=destination, is_connected=is_connected) + cp = CablePath(origin=node, path=path, destination=destination, is_active=is_active) cp.save() @@ -86,7 +86,7 @@ def update_connected_endpoints(instance, created, **kwargs): # may change in the future.) However, we do need to capture status changes and update # any CablePaths accordingly. if instance.status != CableStatusChoices.STATUS_CONNECTED: - CablePath.objects.filter(path__contains=[object_to_path_node(instance)]).update(is_connected=False) + CablePath.objects.filter(path__contains=[object_to_path_node(instance)]).update(is_active=False) else: rebuild_paths(instance) @@ -110,13 +110,13 @@ def nullify_connected_endpoints(instance, **kwargs): # Delete and retrace any dependent cable paths for cablepath in CablePath.objects.filter(path__contains=[object_to_path_node(instance)]): - path, destination, is_connected = trace_path(cablepath.origin) + path, destination, is_active = trace_path(cablepath.origin) if path: CablePath.objects.filter(pk=cablepath.pk).update( path=path, destination_type=ContentType.objects.get_for_model(destination) if destination else None, destination_id=destination.pk if destination else None, - is_connected=is_connected + is_active=is_active ) else: cablepath.delete() diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 57cd0b6eb..1ae5282f3 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -832,7 +832,7 @@ class ConsoleConnectionTable(BaseTable): verbose_name='Console Port' ) path_status = tables.TemplateColumn( - accessor=Accessor('_path__is_connected'), + accessor=Accessor('_path__is_active'), template_code=PATH_STATUS, verbose_name='Path Status' ) @@ -865,7 +865,7 @@ class PowerConnectionTable(BaseTable): verbose_name='Power Port' ) path_status = tables.TemplateColumn( - accessor=Accessor('_path__is_connected'), + accessor=Accessor('_path__is_active'), template_code=PATH_STATUS, verbose_name='Path Status' ) @@ -901,7 +901,7 @@ class InterfaceConnectionTable(BaseTable): verbose_name='Interface B' ) path_status = tables.TemplateColumn( - accessor=Accessor('_path__is_connected'), + accessor=Accessor('_path__is_active'), template_code=PATH_STATUS, verbose_name='Path Status' ) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 52b37e3bd..65de412cf 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -123,14 +123,14 @@ class CablePathTestCase(TestCase): cls.circuittermination2_Z, ]) - def assertPathExists(self, origin, destination, path=None, is_connected=None, msg=None): + def assertPathExists(self, origin, destination, path=None, is_active=None, msg=None): """ Assert that a CablePath from origin to destination with a specific intermediate path exists. :param origin: Originating endpoint :param destination: Terminating endpoint, or None :param path: Sequence of objects comprising the intermediate path (optional) - :param is_connected: Boolean indicating whether the end-to-end path is complete and active (optional) + :param is_active: Boolean indicating whether the end-to-end path is complete and active (optional) :param msg: Custom failure message (optional) :return: The matching CablePath (if any) @@ -147,8 +147,8 @@ class CablePathTestCase(TestCase): kwargs['destination_id__isnull'] = True if path is not None: kwargs['path'] = objects_to_path(*path) - if is_connected is not None: - kwargs['is_connected'] = is_connected + if is_active is not None: + kwargs['is_active'] = is_active if msg is None: if destination is not None: msg = f"Missing path from {origin} to {destination}" @@ -194,13 +194,13 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=self.interface2, path=(cable1,), - is_connected=True + is_active=True ) path2 = self.assertPathExists( origin=self.interface2, destination=self.interface1, path=(cable1,), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 2) self.interface1.refresh_from_db() @@ -225,13 +225,13 @@ class CablePathTestCase(TestCase): origin=self.consoleport1, destination=self.consoleserverport1, path=(cable1,), - is_connected=True + is_active=True ) path2 = self.assertPathExists( origin=self.consoleserverport1, destination=self.consoleport1, path=(cable1,), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 2) self.consoleport1.refresh_from_db() @@ -256,13 +256,13 @@ class CablePathTestCase(TestCase): origin=self.powerport1, destination=self.poweroutlet1, path=(cable1,), - is_connected=True + is_active=True ) path2 = self.assertPathExists( origin=self.poweroutlet1, destination=self.powerport1, path=(cable1,), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 2) self.powerport1.refresh_from_db() @@ -287,13 +287,13 @@ class CablePathTestCase(TestCase): origin=self.powerport1, destination=self.powerfeed1, path=(cable1,), - is_connected=True + is_active=True ) path2 = self.assertPathExists( origin=self.powerfeed1, destination=self.powerport1, path=(cable1,), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 2) self.powerport1.refresh_from_db() @@ -318,13 +318,13 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=self.circuittermination1_A, path=(cable1,), - is_connected=True + is_active=True ) path2 = self.assertPathExists( origin=self.circuittermination1_A, destination=self.interface1, path=(cable1,), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 2) self.interface1.refresh_from_db() @@ -352,7 +352,7 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=None, path=(cable1, self.front_port5_1, self.rear_port5), - is_connected=False + is_active=False ) self.assertEqual(CablePath.objects.count(), 1) @@ -363,13 +363,13 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=self.interface2, path=(cable1, self.front_port5_1, self.rear_port5, cable2), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface2, destination=self.interface1, path=(cable2, self.rear_port5, self.front_port5_1, cable1), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 2) @@ -379,7 +379,7 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=None, path=(cable1, self.front_port5_1, self.rear_port5), - is_connected=False + is_active=False ) self.assertEqual(CablePath.objects.count(), 1) self.interface1.refresh_from_db() @@ -406,13 +406,13 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=None, path=(cable1, self.front_port1_1, self.rear_port1), - is_connected=False + is_active=False ) self.assertPathExists( origin=self.interface2, destination=None, path=(cable2, self.front_port1_2, self.rear_port1), - is_connected=False + is_active=False ) self.assertEqual(CablePath.objects.count(), 2) @@ -423,13 +423,13 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=None, path=(cable1, self.front_port1_1, self.rear_port1, cable3, self.rear_port2, self.front_port2_1), - is_connected=False + is_active=False ) self.assertPathExists( origin=self.interface2, destination=None, path=(cable2, self.front_port1_2, self.rear_port1, cable3, self.rear_port2, self.front_port2_2), - is_connected=False + is_active=False ) self.assertEqual(CablePath.objects.count(), 2) @@ -445,7 +445,7 @@ class CablePathTestCase(TestCase): cable1, self.front_port1_1, self.rear_port1, cable3, self.rear_port2, self.front_port2_1, cable4, ), - is_connected=True + is_active=True ) path2 = self.assertPathExists( origin=self.interface2, @@ -454,7 +454,7 @@ class CablePathTestCase(TestCase): cable2, self.front_port1_2, self.rear_port1, cable3, self.rear_port2, self.front_port2_2, cable5, ), - is_connected=True + is_active=True ) path3 = self.assertPathExists( origin=self.interface3, @@ -463,7 +463,7 @@ class CablePathTestCase(TestCase): cable4, self.front_port2_1, self.rear_port2, cable3, self.rear_port1, self.front_port1_1, cable1 ), - is_connected=True + is_active=True ) path4 = self.assertPathExists( origin=self.interface4, @@ -472,7 +472,7 @@ class CablePathTestCase(TestCase): cable5, self.front_port2_2, self.rear_port2, cable3, self.rear_port1, self.front_port1_2, cable2 ), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 4) @@ -530,7 +530,7 @@ class CablePathTestCase(TestCase): cable4, self.rear_port3, self.front_port3_1, cable5, self.rear_port4, self.front_port4_1, cable6 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface2, @@ -540,7 +540,7 @@ class CablePathTestCase(TestCase): cable4, self.rear_port3, self.front_port3_1, cable5, self.rear_port4, self.front_port4_2, cable7 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface3, @@ -550,7 +550,7 @@ class CablePathTestCase(TestCase): cable4, self.rear_port2, self.front_port2_1, cable3, self.rear_port1, self.front_port1_1, cable1 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface4, @@ -560,7 +560,7 @@ class CablePathTestCase(TestCase): cable4, self.rear_port2, self.front_port2_1, cable3, self.rear_port1, self.front_port1_2, cable2 ), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 4) @@ -609,7 +609,7 @@ class CablePathTestCase(TestCase): cable4, self.front_port3_1, self.rear_port3, cable6, self.rear_port4, self.front_port4_1, cable7 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface2, @@ -619,7 +619,7 @@ class CablePathTestCase(TestCase): cable5, self.front_port3_2, self.rear_port3, cable6, self.rear_port4, self.front_port4_2, cable8 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface3, @@ -629,7 +629,7 @@ class CablePathTestCase(TestCase): cable4, self.front_port2_1, self.rear_port2, cable3, self.rear_port1, self.front_port1_1, cable1 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface4, @@ -639,7 +639,7 @@ class CablePathTestCase(TestCase): cable5, self.front_port2_2, self.rear_port2, cable3, self.rear_port1, self.front_port1_2, cable2 ), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 4) @@ -683,7 +683,7 @@ class CablePathTestCase(TestCase): cable1, self.front_port1_1, self.rear_port1, cable3, self.front_port5_1, self.rear_port5, cable4, self.rear_port2, self.front_port2_1, cable5 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface2, @@ -692,7 +692,7 @@ class CablePathTestCase(TestCase): cable2, self.front_port1_2, self.rear_port1, cable3, self.front_port5_1, self.rear_port5, cable4, self.rear_port2, self.front_port2_2, cable6 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface3, @@ -701,7 +701,7 @@ class CablePathTestCase(TestCase): cable5, self.front_port2_1, self.rear_port2, cable4, self.rear_port5, self.front_port5_1, cable3, self.rear_port1, self.front_port1_1, cable1 ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface4, @@ -710,7 +710,7 @@ class CablePathTestCase(TestCase): cable6, self.front_port2_2, self.rear_port2, cable4, self.rear_port5, self.front_port5_1, cable3, self.rear_port1, self.front_port1_2, cable2 ), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 4) @@ -740,7 +740,7 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=None, path=(cable1, self.front_port5_1, self.rear_port5, cable2, self.rear_port6, self.front_port6_1), - is_connected=False + is_active=False ) self.assertEqual(CablePath.objects.count(), 1) @@ -754,7 +754,7 @@ class CablePathTestCase(TestCase): cable1, self.front_port5_1, self.rear_port5, cable2, self.rear_port6, self.front_port6_1, cable3, ), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface2, @@ -763,7 +763,7 @@ class CablePathTestCase(TestCase): cable3, self.front_port6_1, self.rear_port6, cable2, self.rear_port5, self.front_port5_1, cable1, ), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 2) @@ -779,7 +779,7 @@ class CablePathTestCase(TestCase): cable1.save() cable2 = Cable(termination_a=self.rear_port5, termination_b=self.interface2) cable2.save() - self.assertEqual(CablePath.objects.filter(is_connected=True).count(), 2) + self.assertEqual(CablePath.objects.filter(is_active=True).count(), 2) self.assertEqual(CablePath.objects.count(), 2) # Change cable 2's status to "planned" @@ -789,13 +789,13 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=self.interface2, path=(cable1, self.front_port5_1, self.rear_port5, cable2), - is_connected=False + is_active=False ) self.assertPathExists( origin=self.interface2, destination=self.interface1, path=(cable2, self.rear_port5, self.front_port5_1, cable1), - is_connected=False + is_active=False ) self.assertEqual(CablePath.objects.count(), 2) @@ -807,12 +807,12 @@ class CablePathTestCase(TestCase): origin=self.interface1, destination=self.interface2, path=(cable1, self.front_port5_1, self.rear_port5, cable2), - is_connected=True + is_active=True ) self.assertPathExists( origin=self.interface2, destination=self.interface1, path=(cable2, self.rear_port5, self.front_port5_1, cable1), - is_connected=True + is_active=True ) self.assertEqual(CablePath.objects.count(), 2) diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 40896f97b..186ea72e5 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -24,14 +24,14 @@ def trace_path(node): destination = None path = [] position_stack = [] - is_connected = True + is_active = True if node is None or node.cable is None: return [], None, False while node.cable is not None: if node.cable.status != CableStatusChoices.STATUS_CONNECTED: - is_connected = False + is_active = False # Follow the cable to its far-end termination path.append(object_to_path_node(node.cable)) @@ -65,6 +65,6 @@ def trace_path(node): break if destination is None: - is_connected = False + is_active = False - return path, destination, is_connected + return path, destination, is_active diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9290a2b4a..a2850fc46 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2088,7 +2088,7 @@ class ConsoleConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Connected' if obj._path.is_connected else 'Not Connected', + 'Connected' if obj._path.is_active else 'Not Connected', ]) csv_data.append(csv) @@ -2115,7 +2115,7 @@ class PowerConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Connected' if obj._path.is_connected else 'Not Connected', + 'Connected' if obj._path.is_active else 'Not Connected', ]) csv_data.append(csv) @@ -2148,7 +2148,7 @@ class InterfaceConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Connected' if obj._path.is_connected else 'Not Connected', + 'Connected' if obj._path.is_active else 'Not Connected', ]) csv_data.append(csv) From b846f631a40d8d3162254ac883b2435270c56bb0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 11:32:39 -0400 Subject: [PATCH 31/67] Rename connection_status to connected_endpoint_reachable --- docs/release-notes/version-2.10.md | 6 ++++++ netbox/circuits/api/serializers.py | 2 +- netbox/dcim/api/serializers.py | 19 +++++++++---------- netbox/dcim/constants.py | 6 ------ netbox/dcim/tests/test_views.py | 5 ----- netbox/utilities/custom_inspectors.py | 4 ++-- 6 files changed, 18 insertions(+), 24 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index c49283d14..b43662a3e 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -61,10 +61,16 @@ All end-to-end cable paths are now cached using the new CablePath model. This al ### REST API Changes * Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints +* circuits.CircuitTermination: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * dcim.Cable: Added `custom_fields` +* dcim.ConsolePort: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) +* dcim.ConsoleServerPort: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * dcim.FrontPort: Removed the `trace` endpoint +* dcim.Interface: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning +* dcim.PowerOutlet: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * dcim.PowerPanel: Added `custom_fields` +* dcim.PowerPort: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * dcim.RackReservation: Added `custom_fields` * dcim.RearPort: Removed the `trace` endpoint * dcim.VirtualChassis: Added `custom_fields` diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 10ae5e5ee..03c9012af 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -77,5 +77,5 @@ class CircuitTerminationSerializer(ConnectedEndpointSerializer): model = CircuitTermination fields = [ 'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', - 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', + 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', ] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 144e764dc..42018c046 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -30,7 +30,7 @@ from .nested_serializers import * class ConnectedEndpointSerializer(ValidatedModelSerializer): connected_endpoint_type = serializers.SerializerMethodField(read_only=True) connected_endpoint = serializers.SerializerMethodField(read_only=True) - connection_status = serializers.SerializerMethodField(read_only=True) + connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True) def get_connected_endpoint_type(self, obj): if obj._path is not None and obj._path.destination is not None: @@ -48,9 +48,8 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): return serializer(obj._path.destination, context=context).data return None - # TODO: Tweak the representation for this field @swagger_serializer_method(serializer_or_field=serializers.BooleanField) - def get_connection_status(self, obj): + def get_connected_endpoint_reachable(self, obj): if obj._path is not None: return obj._path.is_active return None @@ -467,7 +466,7 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSeria model = ConsoleServerPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', - 'connected_endpoint', 'connection_status', 'cable', 'tags', + 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'tags', ] @@ -485,7 +484,7 @@ class ConsolePortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer) model = ConsolePort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', - 'connected_endpoint', 'connection_status', 'cable', 'tags', + 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'tags', ] @@ -513,7 +512,7 @@ class PowerOutletSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer) model = PowerOutlet fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', - 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags', + 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'tags', ] @@ -531,7 +530,7 @@ class PowerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): model = PowerPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', - 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags', + 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'tags', ] @@ -555,8 +554,8 @@ class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): model = Interface fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', - 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', - 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', + 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', + 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', ] # TODO: This validation should be handled by Interface.clean() @@ -720,7 +719,7 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer): class Meta: model = Interface - fields = ['interface_a', 'interface_b', 'connection_status'] + fields = ['interface_a', 'interface_b'] @swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer) def get_interface_a(self, obj): diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 961c458e0..804e5be03 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -59,12 +59,6 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage # Cabling and connections # -# Console/power/interface connection statuses -CONNECTION_STATUS_CHOICES = [ - [False, 'Not Connected'], - [True, 'Connected'], -] - # Cable endpoint types CABLE_TERMINATION_MODELS = Q( Q(app_label='circuits', model__in=( diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 83d8841df..f3d942112 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1714,11 +1714,6 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'max_utilization': 50, 'comments': 'New comments', 'tags': [t.pk for t in tags], - - # Connection - 'cable': None, - 'connected_endpoint': None, - 'connection_status': None, } cls.csv_data = ( diff --git a/netbox/utilities/custom_inspectors.py b/netbox/utilities/custom_inspectors.py index 1d5c9c0a0..063d30016 100644 --- a/netbox/utilities/custom_inspectors.py +++ b/netbox/utilities/custom_inspectors.py @@ -62,8 +62,8 @@ class ChoiceFieldInspector(FieldInspector): value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value) if set([None] + choice_value) == {None, True, False}: - # DeviceType.subdevice_role, Device.face and InterfaceConnection.connection_status all need to be - # differentiated since they each have subtly different values in their choice keys. + # DeviceType.subdevice_role and Device.face need to be differentiated since they each have + # subtly different values in their choice keys. # - subdevice_role and connection_status are booleans, although subdevice_role includes None # - face is an integer set {0, 1} which is easily confused with {False, True} schema_type = openapi.TYPE_STRING From 32aa2daea634b7dfd901a27402655844f0666024 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 11:39:17 -0400 Subject: [PATCH 32/67] PowerFeedSerializer should subclass ConnectedEndpointSerializer --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/api/serializers.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index b43662a3e..ee49f223f 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -68,6 +68,7 @@ All end-to-end cable paths are now cached using the new CablePath model. This al * dcim.FrontPort: Removed the `trace` endpoint * dcim.Interface: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning +* dcim.PowerFeed: Add fields `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` * dcim.PowerOutlet: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * dcim.PowerPanel: Added `custom_fields` * dcim.PowerPort: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 42018c046..031ce575b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -760,7 +760,7 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count'] -class PowerFeedSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): +class PowerFeedSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') power_panel = NestedPowerPanelSerializer() rack = NestedRackSerializer( @@ -790,5 +790,6 @@ class PowerFeedSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): model = PowerFeed fields = [ 'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', - 'max_utilization', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'cable', + 'max_utilization', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', ] From b2066bc4b728e5d2a3b9103eb29aea911a096524 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 11:47:24 -0400 Subject: [PATCH 33/67] Merge schema migrations --- netbox/circuits/migrations/0021_cablepath.py | 8 +++ .../0022_drop_connected_endpoint.py | 21 ------- netbox/dcim/migrations/0120_cablepath.py | 48 +++++++++++++++ .../0121_drop_connected_endpoint.py | 61 ------------------- 4 files changed, 56 insertions(+), 82 deletions(-) delete mode 100644 netbox/circuits/migrations/0022_drop_connected_endpoint.py delete mode 100644 netbox/dcim/migrations/0121_drop_connected_endpoint.py diff --git a/netbox/circuits/migrations/0021_cablepath.py b/netbox/circuits/migrations/0021_cablepath.py index fabf71798..b416d2e9f 100644 --- a/netbox/circuits/migrations/0021_cablepath.py +++ b/netbox/circuits/migrations/0021_cablepath.py @@ -17,4 +17,12 @@ class Migration(migrations.Migration): name='_path', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), ), + migrations.RemoveField( + model_name='circuittermination', + name='connected_endpoint', + ), + migrations.RemoveField( + model_name='circuittermination', + name='connection_status', + ), ] diff --git a/netbox/circuits/migrations/0022_drop_connected_endpoint.py b/netbox/circuits/migrations/0022_drop_connected_endpoint.py deleted file mode 100644 index e5540b44d..000000000 --- a/netbox/circuits/migrations/0022_drop_connected_endpoint.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.1 on 2020-10-05 14:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('circuits', '0021_cablepath'), - ] - - operations = [ - migrations.RemoveField( - model_name='circuittermination', - name='connected_endpoint', - ), - migrations.RemoveField( - model_name='circuittermination', - name='connection_status', - ), - ] diff --git a/netbox/dcim/migrations/0120_cablepath.py b/netbox/dcim/migrations/0120_cablepath.py index 2b58f119a..dd3c4ed19 100644 --- a/netbox/dcim/migrations/0120_cablepath.py +++ b/netbox/dcim/migrations/0120_cablepath.py @@ -58,4 +58,52 @@ class Migration(migrations.Migration): name='_path', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'), ), + migrations.RemoveField( + model_name='consoleport', + name='connected_endpoint', + ), + migrations.RemoveField( + model_name='consoleport', + name='connection_status', + ), + migrations.RemoveField( + model_name='consoleserverport', + name='connection_status', + ), + migrations.RemoveField( + model_name='interface', + name='_connected_circuittermination', + ), + migrations.RemoveField( + model_name='interface', + name='_connected_interface', + ), + migrations.RemoveField( + model_name='interface', + name='connection_status', + ), + migrations.RemoveField( + model_name='powerfeed', + name='connected_endpoint', + ), + migrations.RemoveField( + model_name='powerfeed', + name='connection_status', + ), + migrations.RemoveField( + model_name='poweroutlet', + name='connection_status', + ), + migrations.RemoveField( + model_name='powerport', + name='_connected_powerfeed', + ), + migrations.RemoveField( + model_name='powerport', + name='_connected_poweroutlet', + ), + migrations.RemoveField( + model_name='powerport', + name='connection_status', + ), ] diff --git a/netbox/dcim/migrations/0121_drop_connected_endpoint.py b/netbox/dcim/migrations/0121_drop_connected_endpoint.py deleted file mode 100644 index 9404ecf53..000000000 --- a/netbox/dcim/migrations/0121_drop_connected_endpoint.py +++ /dev/null @@ -1,61 +0,0 @@ -# Generated by Django 3.1 on 2020-10-05 14:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0120_cablepath'), - ] - - operations = [ - migrations.RemoveField( - model_name='consoleport', - name='connected_endpoint', - ), - migrations.RemoveField( - model_name='consoleport', - name='connection_status', - ), - migrations.RemoveField( - model_name='consoleserverport', - name='connection_status', - ), - migrations.RemoveField( - model_name='interface', - name='_connected_circuittermination', - ), - migrations.RemoveField( - model_name='interface', - name='_connected_interface', - ), - migrations.RemoveField( - model_name='interface', - name='connection_status', - ), - migrations.RemoveField( - model_name='powerfeed', - name='connected_endpoint', - ), - migrations.RemoveField( - model_name='powerfeed', - name='connection_status', - ), - migrations.RemoveField( - model_name='poweroutlet', - name='connection_status', - ), - migrations.RemoveField( - model_name='powerport', - name='_connected_powerfeed', - ), - migrations.RemoveField( - model_name='powerport', - name='_connected_poweroutlet', - ), - migrations.RemoveField( - model_name='powerport', - name='connection_status', - ), - ] From 50aecd02f45236479751e915de89895d2e5c1d3b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 12:05:29 -0400 Subject: [PATCH 34/67] Fix up connection lists (pending additional work) --- netbox/dcim/filters.py | 64 +++++++++++------------------------------- netbox/dcim/tables.py | 25 ++++++----------- netbox/dcim/views.py | 6 ++-- 3 files changed, 28 insertions(+), 67 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 74b5903a6..aeccc341b 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1150,7 +1150,20 @@ class CableFilterSet(BaseFilterSet): return queryset -class ConsoleConnectionFilterSet(BaseFilterSet): +class ConnectionFilterSet: + + def filter_site(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter(device__site__slug=value) + + def filter_device(self, queryset, name, value): + if not value: + return queryset + return queryset.filter(device_id__in=value) + + +class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1167,22 +1180,8 @@ class ConsoleConnectionFilterSet(BaseFilterSet): model = ConsolePort fields = ['name'] - # TODO: Fix filters - # def filter_site(self, queryset, name, value): - # if not value.strip(): - # return queryset - # return queryset.filter(connected_endpoint__device__site__slug=value) - # - # def filter_device(self, queryset, name, value): - # if not value: - # return queryset - # return queryset.filter( - # Q(**{'{}__in'.format(name): value}) | - # Q(**{'connected_endpoint__{}__in'.format(name): value}) - # ) - -class PowerConnectionFilterSet(BaseFilterSet): +class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1199,22 +1198,8 @@ class PowerConnectionFilterSet(BaseFilterSet): model = PowerPort fields = ['name'] - # TODO: Fix filters - # def filter_site(self, queryset, name, value): - # if not value.strip(): - # return queryset - # return queryset.filter(_connected_poweroutlet__device__site__slug=value) - # - # def filter_device(self, queryset, name, value): - # if not value: - # return queryset - # return queryset.filter( - # Q(**{'{}__in'.format(name): value}) | - # Q(**{'_connected_poweroutlet__{}__in'.format(name): value}) - # ) - -class InterfaceConnectionFilterSet(BaseFilterSet): +class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1231,23 +1216,6 @@ class InterfaceConnectionFilterSet(BaseFilterSet): model = Interface fields = [] - # TODO: Fix filters - # def filter_site(self, queryset, name, value): - # if not value.strip(): - # return queryset - # return queryset.filter( - # Q(device__site__slug=value) | - # Q(_connected_interface__device__site__slug=value) - # ) - # - # def filter_device(self, queryset, name, value): - # if not value: - # return queryset - # return queryset.filter( - # Q(**{'{}__in'.format(name): value}) | - # Q(**{'_connected_interface__{}__in'.format(name): value}) - # ) - class PowerPanelFilterSet(BaseFilterSet): q = django_filters.CharFilter( diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 1ae5282f3..b1aa6e57d 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -67,10 +67,6 @@ INTERFACE_TAGGED_VLANS = """ {% endfor %} """ -PATH_STATUS = """ -{% if value %}Connected{% else %}Not Connected{% endif %} -""" - # # Regions @@ -831,17 +827,16 @@ class ConsoleConnectionTable(BaseTable): linkify=True, verbose_name='Console Port' ) - path_status = tables.TemplateColumn( + reachable = BooleanColumn( accessor=Accessor('_path__is_active'), - template_code=PATH_STATUS, - verbose_name='Path Status' + verbose_name='Reachable' ) add_prefetch = False class Meta(BaseTable.Meta): model = ConsolePort - fields = ('console_server', 'console_server_port', 'device', 'name', 'path_status') + fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable') class PowerConnectionTable(BaseTable): @@ -864,17 +859,16 @@ class PowerConnectionTable(BaseTable): linkify=True, verbose_name='Power Port' ) - path_status = tables.TemplateColumn( + reachable = BooleanColumn( accessor=Accessor('_path__is_active'), - template_code=PATH_STATUS, - verbose_name='Path Status' + verbose_name='Reachable' ) add_prefetch = False class Meta(BaseTable.Meta): model = PowerPort - fields = ('pdu', 'outlet', 'device', 'name', 'path_status') + fields = ('device', 'name', 'pdu', 'outlet', 'reachable') class InterfaceConnectionTable(BaseTable): @@ -900,17 +894,16 @@ class InterfaceConnectionTable(BaseTable): linkify=True, verbose_name='Interface B' ) - path_status = tables.TemplateColumn( + reachable = BooleanColumn( accessor=Accessor('_path__is_active'), - template_code=PATH_STATUS, - verbose_name='Path Status' + verbose_name='Reachable' ) add_prefetch = False class Meta(BaseTable.Meta): model = Interface - fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'path_status') + fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable') # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index a2850fc46..d7ddc1855 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2088,7 +2088,7 @@ class ConsoleConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Connected' if obj._path.is_active else 'Not Connected', + 'Reachable' if obj._path.is_active else 'Not Reachable', ]) csv_data.append(csv) @@ -2115,7 +2115,7 @@ class PowerConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Connected' if obj._path.is_active else 'Not Connected', + 'Reachable' if obj._path.is_active else 'Not Reachable', ]) csv_data.append(csv) @@ -2148,7 +2148,7 @@ class InterfaceConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Connected' if obj._path.is_active else 'Not Connected', + 'Reachable' if obj._path.is_active else 'Not Reachable', ]) csv_data.append(csv) From 32b8148da1e9a5c0c606bb66f363ea26b0732e77 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 13:23:55 -0400 Subject: [PATCH 35/67] Standardize path endpoint templates --- netbox/dcim/models/device_components.py | 4 ++ netbox/dcim/urls.py | 1 + netbox/templates/dcim/consoleport.html | 38 +++++------ netbox/templates/dcim/consoleserverport.html | 38 +++++------ netbox/templates/dcim/interface.html | 26 ++++---- netbox/templates/dcim/powerfeed.html | 69 ++++++++++++++++++-- netbox/templates/dcim/poweroutlet.html | 38 +++++------ netbox/templates/dcim/powerport.html | 38 +++++------ 8 files changed, 158 insertions(+), 94 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 863b39e62..2ab3ce7c8 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -150,6 +150,10 @@ class PathEndpoint(models.Model): # Return the path as a list of three-tuples (A termination, cable, B termination) return list(zip(*[iter(path)] * 3)) + @property + def path(self): + return self._path + @property def connected_endpoint(self): """ diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 90f6b5ef2..884941c9d 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -385,5 +385,6 @@ urlpatterns = [ path('power-feeds//delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), path('power-feeds//trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}), path('power-feeds//changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), + path('power-feeds//connect//', views.CableCreateView.as_view(), name='powerfeed_connect', kwargs={'termination_a_type': PowerFeed}), ] diff --git a/netbox/templates/dcim/consoleport.html b/netbox/templates/dcim/consoleport.html index 3f4d7b306..2b79b2b1c 100644 --- a/netbox/templates/dcim/consoleport.html +++ b/netbox/templates/dcim/consoleport.html @@ -44,6 +44,15 @@ {% if instance.cable %}
Device - {{ connected_interface.device }} -
Name - {{ connected_interface.name }} -
Type{{ connected_interface.get_type_display }}
Enabled - {% if connected_interface.enabled %} - - {% else %} - - {% endif %} -
LAG - {% if connected_interface.lag%} - {{ connected_interface.lag }} - {% else %} - None - {% endif %} -
Description{{ connected_interface.description|placeholder }}
MTU{{ connected_interface.mtu|placeholder }}
MAC Address{{ connected_interface.mac_address|placeholder }}
802.1Q Mode{{ connected_interface.get_mode_display }}
Device + {{ iface.device }} +
Name + {{ iface.name }} +
Type{{ iface.get_type_display }}
Enabled + {% if iface.enabled %} + + {% else %} + + {% endif %} +
LAG + {% if iface.lag%} + {{ iface.lag }} + {% else %} + None + {% endif %} +
Description{{ iface.description|placeholder }}
MTU{{ iface.mtu|placeholder }}
MAC Address{{ iface.mac_address|placeholder }}
802.1Q Mode{{ iface.get_mode_display }}
Provider {{ ct.circuit.provider }}
+ + + + {% if instance.connected_endpoint %} @@ -65,26 +74,17 @@ + + + + {% endif %} - - - - - - - -
Cable + {{ instance.cable }} + + + +
DeviceDescription {{ instance.connected_endpoint.description|placeholder }}
Path Status + {% if instance.path.is_active %} + Reachable + {% else %} + Not Reachable + {% endif %} +
Cable - {{ instance.cable }} - - - -
Connection Status - {% if instance.connection_status %} - {{ instance.get_connection_status_display }} - {% else %} - {{ instance.get_connection_status_display }} - {% endif %} -
{% else %}
diff --git a/netbox/templates/dcim/consoleserverport.html b/netbox/templates/dcim/consoleserverport.html index 77d17fe8a..ae164634d 100644 --- a/netbox/templates/dcim/consoleserverport.html +++ b/netbox/templates/dcim/consoleserverport.html @@ -44,6 +44,15 @@
{% if instance.cable %} + + + + {% if instance.connected_endpoint %} @@ -65,26 +74,17 @@ + + + + {% endif %} - - - - - - - -
Cable + {{ instance.cable }} + + + +
DeviceDescription {{ instance.connected_endpoint.description|placeholder }}
Path Status + {% if instance.path.is_active %} + Reachable + {% else %} + Not Reachable + {% endif %} +
Cable - {{ instance.cable }} - - - -
Connection Status - {% if instance.connection_status %} - {{ instance.get_connection_status_display }} - {% else %} - {{ instance.get_connection_status_display }} - {% endif %} -
{% else %}
diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 12b9918e9..2a3435f33 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -77,6 +77,15 @@
{% if instance.cable %} + + + + {% if instance.connected_endpoint.device %} {% with iface=instance.connected_endpoint %} @@ -149,21 +158,12 @@ {% endwith %} {% endif %} - + - - - - diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index 017a430ee..9f0a45fcd 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -123,11 +123,6 @@
Cable + {{ instance.cable }} + + + +
CablePath Status - {{ instance.cable }} - - - -
Connection Status - {% if instance.connection_status %} - {{ instance.get_connection_status_display }} + {% if instance.path.is_active %} + Reachable {% else %} - {{ instance.get_connection_status_display }} + Not Reachable {% endif %}
- {% include 'inc/custom_fields_panel.html' with obj=powerfeed %} - {% include 'extras/inc/tags_panel.html' with tags=powerfeed.tags.all url='dcim:powerfeed_list' %} - {% plugin_left_page powerfeed %} - -
Electrical Characteristics @@ -155,6 +150,70 @@
+ {% include 'inc/custom_fields_panel.html' with obj=powerfeed %} + {% include 'extras/inc/tags_panel.html' with tags=powerfeed.tags.all url='dcim:powerfeed_list' %} + {% plugin_left_page powerfeed %} +
+
+
+
+ Connection +
+ {% if powerfeed.cable %} + + + + + + {% if powerfeed.connected_endpoint %} + + + + + + + + + + + + + + + + + + + + + {% endif %} +
Cable + {{ powerfeed.cable }} + + + +
Device + {{ powerfeed.connected_endpoint.device }} +
Name + {{ powerfeed.connected_endpoint.name }} +
Type{{ powerfeed.connected_endpoint.get_type_display|placeholder }}
Description{{ powerfeed.connected_endpoint.description|placeholder }}
Path Status + {% if powerfeed.path.is_active %} + Reachable + {% else %} + Not Reachable + {% endif %} +
+ {% else %} +
+ {% if perms.dcim.add_cable %} + + Connect + + {% endif %} + Not connected +
+ {% endif %} +
Comments diff --git a/netbox/templates/dcim/poweroutlet.html b/netbox/templates/dcim/poweroutlet.html index 2ea70972b..82782be09 100644 --- a/netbox/templates/dcim/poweroutlet.html +++ b/netbox/templates/dcim/poweroutlet.html @@ -52,6 +52,15 @@
{% if instance.cable %} + + + + {% if instance.connected_endpoint %} @@ -73,26 +82,17 @@ + + + + {% endif %} - - - - - - - -
Cable + {{ instance.cable }} + + + +
DeviceDescription {{ instance.connected_endpoint.description|placeholder }}
Path Status + {% if instance.path.is_active %} + Reachable + {% else %} + Not Reachable + {% endif %} +
Cable - {{ instance.cable }} - - - -
Connection Status - {% if instance.connection_status %} - {{ instance.get_connection_status_display }} - {% else %} - {{ instance.get_connection_status_display }} - {% endif %} -
{% else %}
diff --git a/netbox/templates/dcim/powerport.html b/netbox/templates/dcim/powerport.html index 3f3d28899..52c9c0e95 100644 --- a/netbox/templates/dcim/powerport.html +++ b/netbox/templates/dcim/powerport.html @@ -52,6 +52,15 @@
{% if instance.cable %} + + + + {% if instance.connected_endpoint %} @@ -73,26 +82,17 @@ + + + + {% endif %} - - - - - - - -
Cable + {{ instance.cable }} + + + +
DeviceDescription {{ instance.connected_endpoint.description|placeholder }}
Path Status + {% if instance.path.is_active %} + Reachable + {% else %} + Not Reachable + {% endif %} +
Cable - {{ instance.cable }} - - - -
Connection Status - {% if instance.connection_status %} - {{ instance.get_connection_status_display }} - {% else %} - {{ instance.get_connection_status_display }} - {% endif %} -
{% else %}
From d5d6b0e856cc370ff58d33663174e0d9377cf933 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 14:47:21 -0400 Subject: [PATCH 36/67] Optimize path prefetching --- netbox/dcim/views.py | 10 +++++----- netbox/templates/dcim/device.html | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d7ddc1855..c82b9e6ac 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1018,31 +1018,31 @@ class DeviceView(ObjectView): # Console ports consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - 'cable', '_path', + 'cable', '_path__destination', ) # Console server ports consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter( device=device ).prefetch_related( - 'cable', '_path', + 'cable', '_path__destination', ) # Power ports powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - 'cable', '_path', + 'cable', '_path__destination', ) # Power outlets poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( - 'cable', 'power_port', '_path', + 'cable', 'power_port', '_path__destination', ) # Interfaces interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related( Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)), Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)), - 'lag', 'cable', '_path', 'tags', + 'lag', 'cable', '_path__destination', 'tags', ) # Front ports diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 96b61ea47..6c7b1c971 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -479,7 +479,7 @@
-
+
{% csrf_token %}
From 19430ddeb5ff02641e1c0f70e7630bb19cdd0c72 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Oct 2020 16:03:30 -0400 Subject: [PATCH 37/67] Extend cable trace view to show related paths --- netbox/dcim/views.py | 38 ++++++--- netbox/templates/dcim/cable_trace.html | 104 +++++++++++++++---------- 2 files changed, 90 insertions(+), 52 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c82b9e6ac..3608b3792 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -34,10 +34,11 @@ from .constants import NONCONNECTABLE_IFACE_TYPES from .models import ( Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, + InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, + PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) +from .utils import object_to_path_node class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): @@ -1965,17 +1966,36 @@ class PathTraceView(ObjectView): return super().dispatch(request, *args, **kwargs) def get(self, request, pk): - obj = get_object_or_404(self.queryset, pk=pk) - path = obj.trace() - total_length = sum( - [entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length] - ) + related_paths = [] + + # If tracing a PathEndpoint, locate the CablePath (if one exists) by its origin + if isinstance(obj, PathEndpoint): + path = obj._path + # Otherwise, find all CablePaths which traverse the specified object + else: + related_paths = CablePath.objects.filter( + path__contains=[object_to_path_node(obj)] + ).prefetch_related('origin') + # Check for specification of a particular path (when tracing pass-through ports) + try: + path_id = int(request.GET.get('cablepath_id')) + except TypeError: + path_id = None + if path_id in list(related_paths.values_list('pk', flat=True)): + path = CablePath.objects.get(pk=path_id) + else: + path = related_paths.first() + + # total_length = sum( + # [entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length] + # ) return render(request, 'dcim/cable_trace.html', { 'obj': obj, - 'trace': path, - 'total_length': total_length, + 'path': path, + 'related_paths': related_paths, + # 'total_length': total_length, }) diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 2f54f94ee..4b3f7f2d4 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -7,52 +7,70 @@ {% block content %}
-
-

Near End

-
-
- {% if total_length %}
Total length: {{ total_length|floatformat:"-2" }} Meters
{% endif %} -
-
-

Far End

-
-
- {% for near_end, cable, far_end in trace %} -
-
-

{{ forloop.counter }}

+
+ +
+
+

Near End

+
+
+ {% if total_length %}
Total length: {{ total_length|floatformat:"-2" }} Meters
{% endif %} +
+
+

Far End

+
-
- {% include 'dcim/inc/cable_trace_end.html' with end=near_end %} -
-
- {% if cable %} -

- - {% if cable.label %}{{ cable.label }}{% else %}Cable #{{ cable.pk }}{% endif %} - -

-

{{ cable.get_status_display }}

-

{{ cable.get_type_display|default:"" }}

- {% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %} - {% if cable.color %} -   - {% endif %} - {% else %} -

No Cable

- {% endif %} -
-
- {% if far_end %} - {% include 'dcim/inc/cable_trace_end.html' with end=far_end %} - {% endif %} + {% for near_end, cable, far_end in path.origin.trace %} +
+
+

{{ forloop.counter }}

+
+
+ {% include 'dcim/inc/cable_trace_end.html' with end=near_end %} +
+
+ {% if cable %} +

+ + {% if cable.label %}{{ cable.label }}{% else %}Cable #{{ cable.pk }}{% endif %} + +

+

{{ cable.get_status_display }}

+

{{ cable.get_type_display|default:"" }}

+ {% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %} + {% if cable.color %} +   + {% endif %} + {% else %} +

No Cable

+ {% endif %} +
+
+ {% if far_end %} + {% include 'dcim/inc/cable_trace_end.html' with end=far_end %} + {% endif %} +
+
+
+ {% endfor %} +
+
+

Trace completed!

+
+
-
- {% endfor %} -
-
-

Trace completed!

+
+ +

Related Paths

+ +
{% endblock %} From 56ee425227f384cef315c0e689b4fec52644cb92 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 09:41:45 -0400 Subject: [PATCH 38/67] Introduce PathContains lookup to allow filtering against objects in path directly --- netbox/dcim/fields.py | 5 ++++- netbox/dcim/lookups.py | 10 ++++++++++ netbox/dcim/signals.py | 9 ++++----- netbox/dcim/tests/test_cablepaths.py | 4 ++-- netbox/dcim/utils.py | 4 ---- netbox/dcim/views.py | 5 +---- 6 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 netbox/dcim/lookups.py diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index 23b9b7fd5..21af2ed14 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -1,11 +1,11 @@ from django.contrib.postgres.fields import ArrayField -from django.contrib.postgres.validators import ArrayMaxLengthValidator from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from netaddr import AddrFormatError, EUI, mac_unix_expanded from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN +from .lookups import PathContains class ASNField(models.BigIntegerField): @@ -61,3 +61,6 @@ class PathField(ArrayField): def __init__(self, **kwargs): kwargs['base_field'] = models.CharField(max_length=40) super().__init__(**kwargs) + + +PathField.register_lookup(PathContains) diff --git a/netbox/dcim/lookups.py b/netbox/dcim/lookups.py new file mode 100644 index 000000000..03acc478a --- /dev/null +++ b/netbox/dcim/lookups.py @@ -0,0 +1,10 @@ +from django.contrib.postgres.fields.array import ArrayContains + +from dcim.utils import object_to_path_node + + +class PathContains(ArrayContains): + + def get_prep_lookup(self): + self.rhs = [object_to_path_node(self.rhs)] + return super().get_prep_lookup() diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 6079417c1..ee006c9d7 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -7,7 +7,7 @@ from django.dispatch import receiver from .choices import CableStatusChoices from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis -from .utils import object_to_path_node, trace_path +from .utils import trace_path def create_cablepath(node): @@ -24,8 +24,7 @@ def rebuild_paths(obj): """ Rebuild all CablePaths which traverse the specified node """ - node = object_to_path_node(obj) - cable_paths = CablePath.objects.filter(path__contains=[node]) + cable_paths = CablePath.objects.filter(path__contains=obj) with transaction.atomic(): for cp in cable_paths: @@ -86,7 +85,7 @@ def update_connected_endpoints(instance, created, **kwargs): # may change in the future.) However, we do need to capture status changes and update # any CablePaths accordingly. if instance.status != CableStatusChoices.STATUS_CONNECTED: - CablePath.objects.filter(path__contains=[object_to_path_node(instance)]).update(is_active=False) + CablePath.objects.filter(path__contains=instance).update(is_active=False) else: rebuild_paths(instance) @@ -109,7 +108,7 @@ def nullify_connected_endpoints(instance, **kwargs): instance.termination_b.save() # Delete and retrace any dependent cable paths - for cablepath in CablePath.objects.filter(path__contains=[object_to_path_node(instance)]): + for cablepath in CablePath.objects.filter(path__contains=instance): path, destination, is_active = trace_path(cablepath.origin) if path: CablePath.objects.filter(pk=cablepath.pk).update( diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 65de412cf..cfe63929d 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -4,7 +4,7 @@ from django.test import TestCase from circuits.models import * from dcim.choices import CableStatusChoices from dcim.models import * -from dcim.utils import objects_to_path +from dcim.utils import object_to_path_node class CablePathTestCase(TestCase): @@ -146,7 +146,7 @@ class CablePathTestCase(TestCase): kwargs['destination_type__isnull'] = True kwargs['destination_id__isnull'] = True if path is not None: - kwargs['path'] = objects_to_path(*path) + kwargs['path'] = [object_to_path_node(obj) for obj in path] if is_active is not None: kwargs['is_active'] = is_active if msg is None: diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 186ea72e5..d36cb1ad3 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -8,10 +8,6 @@ def object_to_path_node(obj): return f'{obj._meta.model_name}:{obj.pk}' -def objects_to_path(*obj_list): - return [object_to_path_node(obj) for obj in obj_list] - - def path_node_to_object(repr): model_name, object_id = repr.split(':') model_class = ContentType.objects.get(model=model_name).model_class() diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 3608b3792..63711a863 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -38,7 +38,6 @@ from .models import ( PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) -from .utils import object_to_path_node class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): @@ -1974,9 +1973,7 @@ class PathTraceView(ObjectView): path = obj._path # Otherwise, find all CablePaths which traverse the specified object else: - related_paths = CablePath.objects.filter( - path__contains=[object_to_path_node(obj)] - ).prefetch_related('origin') + related_paths = CablePath.objects.filter(path__contains=obj).prefetch_related('origin') # Check for specification of a particular path (when tracing pass-through ports) try: path_id = int(request.GET.get('cablepath_id')) From ffdf5514aeba55ecc084d1b638128dd399a0a6cd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 10:37:59 -0400 Subject: [PATCH 39/67] Tweak component templates --- netbox/templates/dcim/inc/endpoint_connection.html | 2 +- netbox/templates/dcim/inc/frontport.html | 2 +- netbox/templates/dcim/inc/rearport.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/templates/dcim/inc/endpoint_connection.html b/netbox/templates/dcim/inc/endpoint_connection.html index 1c25a0e28..3169d2ffc 100644 --- a/netbox/templates/dcim/inc/endpoint_connection.html +++ b/netbox/templates/dcim/inc/endpoint_connection.html @@ -1,4 +1,4 @@ -{% if path %} +{% if path.destination_id %} {% with endpoint=path.destination %} {{ endpoint.parent }} {{ endpoint }} diff --git a/netbox/templates/dcim/inc/frontport.html b/netbox/templates/dcim/inc/frontport.html index f267479f3..d362b6003 100644 --- a/netbox/templates/dcim/inc/frontport.html +++ b/netbox/templates/dcim/inc/frontport.html @@ -24,7 +24,7 @@ {# Description #} {{ frontport.description|placeholder }} - {# Cable/connection #} + {# Cable #} {% if frontport.cable %} {{ frontport.cable }} diff --git a/netbox/templates/dcim/inc/rearport.html b/netbox/templates/dcim/inc/rearport.html index c1e5482d0..ce6edc883 100644 --- a/netbox/templates/dcim/inc/rearport.html +++ b/netbox/templates/dcim/inc/rearport.html @@ -23,7 +23,7 @@ {# Description #} {{ rearport.description|placeholder }} - {# Cable/connection #} + {# Cable #} {% if rearport.cable %} {{ rearport.cable }} From 6275c8c67d0e4e80f407cfee5d200a6f925d6c8f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 10:41:52 -0400 Subject: [PATCH 40/67] Prefetch path & destination for API views --- netbox/circuits/api/views.py | 2 +- netbox/dcim/api/views.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 1c8ad69e4..7b147412e 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -59,7 +59,7 @@ class CircuitViewSet(CustomFieldModelViewSet): class CircuitTerminationViewSet(ModelViewSet): queryset = CircuitTermination.objects.prefetch_related( - 'circuit', 'site', 'cable' + 'circuit', 'site', '_path__destination', 'cable' ) serializer_class = serializers.CircuitTerminationSerializer filterset_class = filters.CircuitTerminationFilterSet diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 6ed50324e..a804ad0b6 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -470,31 +470,31 @@ class DeviceViewSet(CustomFieldModelViewSet): # class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsolePort.objects.prefetch_related('device', '_path', 'cable', 'tags') + queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', 'tags') serializer_class = serializers.ConsolePortSerializer filterset_class = filters.ConsolePortFilterSet class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsoleServerPort.objects.prefetch_related('device', '_path', 'cable', 'tags') + queryset = ConsoleServerPort.objects.prefetch_related('device', '_path__destination', 'cable', 'tags') serializer_class = serializers.ConsoleServerPortSerializer filterset_class = filters.ConsoleServerPortFilterSet class PowerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerPort.objects.prefetch_related('device', '_path', 'cable', 'tags') + queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', 'tags') serializer_class = serializers.PowerPortSerializer filterset_class = filters.PowerPortFilterSet class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerOutlet.objects.prefetch_related('device', '_path', 'cable', 'tags') + queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', 'tags') serializer_class = serializers.PowerOutletSerializer filterset_class = filters.PowerOutletFilterSet class InterfaceViewSet(PathEndpointMixin, ModelViewSet): - queryset = Interface.objects.prefetch_related('device', '_path', 'cable', 'ip_addresses', 'tags') + queryset = Interface.objects.prefetch_related('device', '_path__destination', 'cable', 'ip_addresses', 'tags') serializer_class = serializers.InterfaceSerializer filterset_class = filters.InterfaceFilterSet @@ -597,7 +597,7 @@ class PowerPanelViewSet(ModelViewSet): # class PowerFeedViewSet(CustomFieldModelViewSet): - queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags') + queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', '_path__destination', 'cable', 'tags') serializer_class = serializers.PowerFeedSerializer filterset_class = filters.PowerFeedFilterSet From d59f0891e4d6c0b7ebca57b958d93d63329b367f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 12:10:12 -0400 Subject: [PATCH 41/67] Cache peer termination on CableTerminations --- .../migrations/0022_cache_cable_peer.py | 49 ++++++ .../dcim/migrations/0121_cache_cable_peer.py | 141 ++++++++++++++++++ netbox/dcim/models/device_components.py | 25 +++- netbox/dcim/signals.py | 4 + netbox/dcim/tests/test_models.py | 6 +- 5 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 netbox/circuits/migrations/0022_cache_cable_peer.py create mode 100644 netbox/dcim/migrations/0121_cache_cable_peer.py diff --git a/netbox/circuits/migrations/0022_cache_cable_peer.py b/netbox/circuits/migrations/0022_cache_cable_peer.py new file mode 100644 index 000000000..9a470a3c2 --- /dev/null +++ b/netbox/circuits/migrations/0022_cache_cable_peer.py @@ -0,0 +1,49 @@ +import sys + +from django.db import migrations, models +import django.db.models.deletion + + +def cache_cable_peers(apps, schema_editor): + ContentType = apps.get_model('contenttypes', 'ContentType') + Cable = apps.get_model('dcim', 'Cable') + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + + if 'test' not in sys.argv: + print(f"\n Updating circuit termination cable peers...", flush=True) + ct = ContentType.objects.get_for_model(CircuitTermination) + for cable in Cable.objects.filter(termination_a_type=ct): + CircuitTermination.objects.filter(pk=cable.termination_a_id).update( + _cable_peer_type_id=cable.termination_b_type_id, + _cable_peer_id=cable.termination_b_id + ) + for cable in Cable.objects.filter(termination_b_type=ct): + CircuitTermination.objects.filter(pk=cable.termination_b_id).update( + _cable_peer_type_id=cable.termination_a_type_id, + _cable_peer_id=cable.termination_a_id + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('circuits', '0021_cablepath'), + ] + + operations = [ + migrations.AddField( + model_name='circuittermination', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='circuittermination', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.RunPython( + code=cache_cable_peers, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/migrations/0121_cache_cable_peer.py b/netbox/dcim/migrations/0121_cache_cable_peer.py new file mode 100644 index 000000000..aeb89c5d3 --- /dev/null +++ b/netbox/dcim/migrations/0121_cache_cable_peer.py @@ -0,0 +1,141 @@ +import sys + +from django.db import migrations, models +import django.db.models.deletion + + +def cache_cable_peers(apps, schema_editor): + ContentType = apps.get_model('contenttypes', 'ContentType') + Cable = apps.get_model('dcim', 'Cable') + ConsolePort = apps.get_model('dcim', 'ConsolePort') + ConsoleServerPort = apps.get_model('dcim', 'ConsoleServerPort') + PowerPort = apps.get_model('dcim', 'PowerPort') + PowerOutlet = apps.get_model('dcim', 'PowerOutlet') + Interface = apps.get_model('dcim', 'Interface') + FrontPort = apps.get_model('dcim', 'FrontPort') + RearPort = apps.get_model('dcim', 'RearPort') + PowerFeed = apps.get_model('dcim', 'PowerFeed') + + models = ( + ConsolePort, + ConsoleServerPort, + PowerPort, + PowerOutlet, + Interface, + FrontPort, + RearPort, + PowerFeed + ) + + if 'test' not in sys.argv: + print("\n", end="") + + for model in models: + if 'test' not in sys.argv: + print(f" Updating {model._meta.verbose_name} cable peers...", flush=True) + ct = ContentType.objects.get_for_model(model) + for cable in Cable.objects.filter(termination_a_type=ct): + model.objects.filter(pk=cable.termination_a_id).update( + _cable_peer_type_id=cable.termination_b_type_id, + _cable_peer_id=cable.termination_b_id + ) + for cable in Cable.objects.filter(termination_b_type=ct): + model.objects.filter(pk=cable.termination_b_id).update( + _cable_peer_type_id=cable.termination_a_type_id, + _cable_peer_id=cable.termination_a_id + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('dcim', '0120_cablepath'), + ] + + operations = [ + migrations.AddField( + model_name='consoleport', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='consoleport', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='consoleserverport', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='consoleserverport', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='frontport', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='frontport', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='interface', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='interface', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='powerfeed', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='powerfeed', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='poweroutlet', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='poweroutlet', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='powerport', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='powerport', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='rearport', + name='_cable_peer_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rearport', + name='_cable_peer_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'), + ), + migrations.RunPython( + code=cache_cable_peers, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 2ab3ce7c8..1bd577cdf 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1,4 +1,5 @@ -from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.contenttypes.fields import GenericForeignKey, 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 @@ -99,6 +100,21 @@ class CableTermination(models.Model): blank=True, null=True ) + _cable_peer_type = models.ForeignKey( + to=ContentType, + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + _cable_peer_id = models.PositiveIntegerField( + blank=True, + null=True + ) + _cable_peer = GenericForeignKey( + ct_field='_cable_peer_type', + fk_field='_cable_peer_id' + ) # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted. _cabled_as_a = GenericRelation( @@ -116,12 +132,7 @@ class CableTermination(models.Model): abstract = True def get_cable_peer(self): - if self.cable is None: - return None - if self._cabled_as_a.exists(): - return self.cable.termination_b - if self._cabled_as_b.exists(): - return self.cable.termination_a + return self._cable_peer class PathEndpoint(models.Model): diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index ee006c9d7..5e5915313 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -67,10 +67,12 @@ def update_connected_endpoints(instance, created, **kwargs): if instance.termination_a.cable != instance: logger.debug(f"Updating termination A for cable {instance}") instance.termination_a.cable = instance + instance.termination_a._cable_peer = instance.termination_b instance.termination_a.save() if instance.termination_b.cable != instance: logger.debug(f"Updating termination B for cable {instance}") instance.termination_b.cable = instance + instance.termination_b._cable_peer = instance.termination_a instance.termination_b.save() # Create/update cable paths @@ -101,10 +103,12 @@ def nullify_connected_endpoints(instance, **kwargs): if instance.termination_a is not None: logger.debug(f"Nullifying termination A for cable {instance}") instance.termination_a.cable = None + instance.termination_a._cable_peer = None instance.termination_a.save() if instance.termination_b is not None: logger.debug(f"Nullifying termination B for cable {instance}") instance.termination_b.cable = None + instance.termination_b._cable_peer = None instance.termination_b.save() # Delete and retrace any dependent cable paths diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 83438a609..01829d7bc 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -398,9 +398,11 @@ class CableTestCase(TestCase): When a new Cable is created, it must be cached on either termination point. """ interface1 = Interface.objects.get(pk=self.interface1.pk) - self.assertEqual(self.cable.termination_a, interface1) interface2 = Interface.objects.get(pk=self.interface2.pk) + self.assertEqual(self.cable.termination_a, interface1) + self.assertEqual(interface1._cable_peer, interface2) self.assertEqual(self.cable.termination_b, interface2) + self.assertEqual(interface2._cable_peer, interface1) def test_cable_deletion(self): """ @@ -412,8 +414,10 @@ class CableTestCase(TestCase): self.assertNotEqual(str(self.cable), '#None') interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertIsNone(interface1.cable) + self.assertIsNone(interface1._cable_peer) interface2 = Interface.objects.get(pk=self.interface2.pk) self.assertIsNone(interface2.cable) + self.assertIsNone(interface2._cable_peer) def test_cabletermination_deletion(self): """ From d984dbd83b50fd776a3dbc7e56b0d80689cda534 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 13:08:41 -0400 Subject: [PATCH 42/67] Extend device view to show local cable termination for all components --- netbox/templates/dcim/device.html | 9 +++++++-- .../templates/dcim/inc/cabletermination.html | 14 ++++++++++++++ netbox/templates/dcim/inc/consoleport.html | 15 +++++++++------ .../templates/dcim/inc/consoleserverport.html | 15 +++++++++------ netbox/templates/dcim/inc/frontport.html | 17 +---------------- netbox/templates/dcim/inc/interface.html | 18 +++++++++--------- netbox/templates/dcim/inc/poweroutlet.html | 15 +++++++++------ netbox/templates/dcim/inc/powerport.html | 15 +++++++++------ netbox/templates/dcim/inc/rearport.html | 17 +---------------- 9 files changed, 68 insertions(+), 67 deletions(-) create mode 100644 netbox/templates/dcim/inc/cabletermination.html diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 6c7b1c971..d66213a78 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -506,6 +506,7 @@ MTU Mode Cable + Cable Termination Connection @@ -566,7 +567,7 @@ Position Description Cable - Connection + Cable Termination @@ -623,7 +624,7 @@ Positions Description Cable - Connection + Cable Termination @@ -679,6 +680,7 @@ Type Description Cable + Cable Termination Connection @@ -732,6 +734,7 @@ Type Description Cable + Cable Termination Connection @@ -789,6 +792,7 @@ Draw Description Cable + Cable Termination Connection @@ -843,6 +847,7 @@ Input/Leg Description Cable + Cable Termination Connection diff --git a/netbox/templates/dcim/inc/cabletermination.html b/netbox/templates/dcim/inc/cabletermination.html new file mode 100644 index 000000000..e4b28fbcf --- /dev/null +++ b/netbox/templates/dcim/inc/cabletermination.html @@ -0,0 +1,14 @@ + + {% if termination.parent.provider %} + + + {{ termination.parent.provider }} + {{ termination.parent }} + + {% else %} + {{ termination.parent }} + {% endif %} + + + {{ termination }} + diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index 912404be3..ace09cfe2 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -24,16 +24,19 @@ {# Cable #} - - {% if cp.cable %} + {% if cp.cable %} + {{ cp.cable }} - {% else %} - — - {% endif %} - + + {% include 'dcim/inc/cabletermination.html' with termination=cp.get_cable_peer %} + {% else %} + + Not connected + + {% endif %} {# Connection #} {% include 'dcim/inc/endpoint_connection.html' with path=cp.path %} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index b7a5c6b56..025b0bf02 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -26,16 +26,19 @@ {# Cable #} - - {% if csp.cable %} + {% if csp.cable %} + {{ csp.cable }} - {% else %} - - {% endif %} - + + {% include 'dcim/inc/cabletermination.html' with termination=csp.get_cable_peer %} + {% else %} + + Not connected + + {% endif %} {# Connection #} {% include 'dcim/inc/endpoint_connection.html' with path=csp.path %} diff --git a/netbox/templates/dcim/inc/frontport.html b/netbox/templates/dcim/inc/frontport.html index d362b6003..91374cb1e 100644 --- a/netbox/templates/dcim/inc/frontport.html +++ b/netbox/templates/dcim/inc/frontport.html @@ -32,22 +32,7 @@ - {% with far_end=frontport.get_cable_peer %} - - {% if far_end.parent.provider %} - - - {{ far_end.parent.provider }} - {{ far_end.parent }} - - {% else %} - - {{ far_end.parent }} - - {% endif %} - - {{ far_end }} - {% endwith %} + {% include 'dcim/inc/cabletermination.html' with termination=frontport.get_cable_peer %} {% else %} Not connected diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 159551192..efaed7ecf 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -45,19 +45,19 @@ {{ iface.get_mode_display|default:"—" }} {# Cable #} - - {% if iface.cable %} + {% if iface.cable %} + {{ iface.cable }} - {% if iface.cable.color %} -   - {% endif %} - {% else %} - - {% endif %} - + + {% include 'dcim/inc/cabletermination.html' with termination=iface.get_cable_peer %} + {% else %} + + Not connected + + {% endif %} {# Connection or type #} {% if iface.is_lag %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index b3e003e99..38eb7b8a6 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -37,16 +37,19 @@ {# Cable #} - - {% if po.cable %} + {% if po.cable %} + {{ po.cable }} - {% else %} - - {% endif %} - + + {% include 'dcim/inc/cabletermination.html' with termination=po.get_cable_peer %} + {% else %} + + Not connected + + {% endif %} {# Connection #} {% with path=po.path %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index c65b685d7..c5c18c093 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -33,16 +33,19 @@ {# Cable #} - - {% if pp.cable %} + {% if pp.cable %} + {{ pp.cable }} - {% else %} - — - {% endif %} - + + {% include 'dcim/inc/cabletermination.html' with termination=pp.get_cable_peer %} + {% else %} + + Not connected + + {% endif %} {# Connection #} {% include 'dcim/inc/endpoint_connection.html' with path=pp.path %} diff --git a/netbox/templates/dcim/inc/rearport.html b/netbox/templates/dcim/inc/rearport.html index ce6edc883..fd5ee620c 100644 --- a/netbox/templates/dcim/inc/rearport.html +++ b/netbox/templates/dcim/inc/rearport.html @@ -31,22 +31,7 @@ - {% with far_end=rearport.get_cable_peer %} - - {% if far_end.parent.provider %} - - - {{ far_end.parent.provider }} - {{ far_end.parent }} - - {% else %} - - {{ far_end.parent }} - - {% endif %} - - {{ far_end }} - {% endwith %} + {% include 'dcim/inc/cabletermination.html' with termination=rearport.get_cable_peer %} {% else %} Not connected From c813ae4f0494a72571e8fbf5bd5f33c0db6573d3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 13:30:28 -0400 Subject: [PATCH 43/67] Clean up power connection tables --- netbox/dcim/tables.py | 27 ++++++++++++++++++++-- netbox/templates/dcim/device.html | 2 -- netbox/templates/dcim/inc/poweroutlet.html | 5 +--- netbox/templates/dcim/inc/powerport.html | 5 +--- netbox/templates/dcim/powerpanel.html | 10 ++++---- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index b1aa6e57d..437daaf29 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -67,6 +67,19 @@ INTERFACE_TAGGED_VLANS = """ {% endfor %} """ +POWERFEED_CABLE = """ +{{ value }} + + + +""" + +POWERFEED_CABLETERMINATION = """ +{{ value.parent }} + +{{ value }} +""" + # # Regions @@ -977,6 +990,15 @@ class PowerFeedTable(BaseTable): max_utilization = tables.TemplateColumn( template_code="{{ value }}%" ) + cable = tables.TemplateColumn( + template_code=POWERFEED_CABLE, + orderable=False + ) + connection = tables.TemplateColumn( + accessor='get_cable_peer', + template_code=POWERFEED_CABLETERMINATION, + orderable=False + ) available_power = tables.Column( verbose_name='Available power (VA)' ) @@ -988,8 +1010,9 @@ class PowerFeedTable(BaseTable): model = PowerFeed fields = ( 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', - 'max_utilization', 'available_power', 'tags', + 'max_utilization', 'cable', 'connection', 'available_power', 'tags', ) default_columns = ( - 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', + 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable', + 'connection', ) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index d66213a78..c06f86bf0 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -792,7 +792,6 @@ Draw Description Cable - Cable Termination Connection @@ -847,7 +846,6 @@ Input/Leg Description Cable - Cable Termination Connection diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 38eb7b8a6..a6a0dd03e 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -44,11 +44,8 @@ - {% include 'dcim/inc/cabletermination.html' with termination=po.get_cable_peer %} {% else %} - - Not connected - + Not connected {% endif %} {# Connection #} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index c5c18c093..125bc5445 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -40,11 +40,8 @@ - {% include 'dcim/inc/cabletermination.html' with termination=pp.get_cable_peer %} {% else %} - - Not connected - + Not connected {% endif %} {# Connection #} diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index 3cad8b5b3..46b386cf3 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -58,7 +58,7 @@ {% block content %}
-
+
Power Panel @@ -82,17 +82,17 @@
- {% include 'inc/custom_fields_panel.html' with obj=powerpanel %} - {% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %} {% plugin_left_page powerpanel %}
-
- {% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %} +
+ {% include 'inc/custom_fields_panel.html' with obj=powerpanel %} + {% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %} {% plugin_right_page powerpanel %}
+ {% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %} {% plugin_full_width_page powerpanel %}
From 23cde6d1b8a48cb55a3b76d99ccbadf11e782be9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 14:30:46 -0400 Subject: [PATCH 44/67] Include cable_peer on CableTermination serializers --- docs/release-notes/version-2.10.md | 35 ++++++++++++----- netbox/circuits/api/serializers.py | 4 +- netbox/dcim/api/serializers.py | 60 ++++++++++++++++++++++-------- netbox/dcim/api/views.py | 18 ++++++--- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index ee49f223f..124bbd645 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -60,20 +60,35 @@ All end-to-end cable paths are now cached using the new CablePath model. This al ### REST API Changes -* Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints -* circuits.CircuitTermination: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) +* Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints (bulk update and delete) +* circuits.CircuitTermination: + * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) + * Added `cable_peer` * dcim.Cable: Added `custom_fields` -* dcim.ConsolePort: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) -* dcim.ConsoleServerPort: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) -* dcim.FrontPort: Removed the `trace` endpoint -* dcim.Interface: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) +* dcim.ConsolePort: + * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) + * Added `cable_peer` +* dcim.ConsoleServerPort: + * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) + * Added `cable_peer` +* dcim.FrontPort: + * Removed the `trace` endpoint + * Added `cable_peer` +* dcim.Interface: + * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) + * Added `cable_peer` * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning -* dcim.PowerFeed: Add fields `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` -* dcim.PowerOutlet: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) +* dcim.PowerFeed: Add fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, and `cable_peer` +* dcim.PowerOutlet: + * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) + * Added `cable_peer` * dcim.PowerPanel: Added `custom_fields` -* dcim.PowerPort: Replaced `connection_status` with `connected_endpoint_reachable` (boolean) +* dcim.PowerPort + * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) + * Added `cable_peer` * dcim.RackReservation: Added `custom_fields` -* dcim.RearPort: Removed the `trace` endpoint +* dcim.RearPort: + * Removed the `trace` endpoint * dcim.VirtualChassis: Added `custom_fields` * extras.ExportTemplate: The `template_language` field has been removed * extras.Graph: This API endpoint has been removed (see #4349) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 03c9012af..6bbb0cef5 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from circuits.choices import CircuitStatusChoices from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer -from dcim.api.serializers import ConnectedEndpointSerializer +from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer from extras.api.customfields import CustomFieldModelSerializer from extras.api.serializers import TaggedObjectSerializer from tenancy.api.nested_serializers import NestedTenantSerializer @@ -67,7 +67,7 @@ class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): ] -class CircuitTerminationSerializer(ConnectedEndpointSerializer): +class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') circuit = NestedCircuitSerializer() site = NestedSiteSerializer() diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 031ce575b..803b64fbb 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -27,6 +27,27 @@ from virtualization.api.nested_serializers import NestedClusterSerializer from .nested_serializers import * +class CableTerminationSerializer(serializers.ModelSerializer): + cable_peer_type = serializers.SerializerMethodField(read_only=True) + cable_peer = serializers.SerializerMethodField(read_only=True) + + def get_cable_peer_type(self, obj): + if obj._cable_peer is not None: + return f'{obj._cable_peer._meta.app_label}.{obj._cable_peer._meta.model_name}' + return None + + @swagger_serializer_method(serializer_or_field=serializers.DictField) + def get_cable_peer(self, obj): + """ + Return the appropriate serializer for the cable termination model. + """ + if obj._cable_peer is not None: + serializer = get_serializer_for_model(obj._cable_peer, prefix='Nested') + context = {'request': self.context['request']} + return serializer(obj._cable_peer, context=context).data + return None + + class ConnectedEndpointSerializer(ValidatedModelSerializer): connected_endpoint_type = serializers.SerializerMethodField(read_only=True) connected_endpoint = serializers.SerializerMethodField(read_only=True) @@ -452,7 +473,7 @@ class DeviceNAPALMSerializer(serializers.Serializer): method = serializers.DictField() -class ConsoleServerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): +class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') device = NestedDeviceSerializer() type = ChoiceField( @@ -466,11 +487,11 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSeria model = ConsoleServerPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', - 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'tags', + 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', 'tags', ] -class ConsolePortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): +class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') device = NestedDeviceSerializer() type = ChoiceField( @@ -484,11 +505,11 @@ class ConsolePortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer) model = ConsolePort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', - 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'tags', + 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', 'tags', ] -class PowerOutletSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): +class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') device = NestedDeviceSerializer() type = ChoiceField( @@ -512,11 +533,12 @@ class PowerOutletSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer) model = PowerOutlet fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', - 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'tags', + 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', + 'tags', ] -class PowerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): +class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') device = NestedDeviceSerializer() type = ChoiceField( @@ -530,11 +552,12 @@ class PowerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): model = PowerPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', - 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'tags', + 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', + 'tags', ] -class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): +class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') device = NestedDeviceSerializer() type = ChoiceField(choices=InterfaceTypeChoices) @@ -555,7 +578,7 @@ class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', - 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', + 'cable_peer', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', ] # TODO: This validation should be handled by Interface.clean() @@ -579,7 +602,7 @@ class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): return super().validate(data) -class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer): +class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') device = NestedDeviceSerializer() type = ChoiceField(choices=PortTypeChoices) @@ -587,7 +610,9 @@ class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class Meta: model = RearPort - fields = ['id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'tags'] + fields = [ + 'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'tags', + ] class FrontPortRearPortSerializer(WritableNestedSerializer): @@ -601,7 +626,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer): fields = ['id', 'url', 'name', 'label'] -class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer): +class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') device = NestedDeviceSerializer() type = ChoiceField(choices=PortTypeChoices) @@ -612,7 +637,7 @@ class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer): model = FrontPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', - 'tags', + 'cable_peer', 'tags', ] @@ -760,7 +785,12 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count'] -class PowerFeedSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer, CustomFieldModelSerializer): +class PowerFeedSerializer( + TaggedObjectSerializer, + CableTerminationSerializer, + ConnectedEndpointSerializer, + CustomFieldModelSerializer +): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') power_panel = NestedPowerPanelSerializer() rack = NestedRackSerializer( diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index a804ad0b6..14d6177bb 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -470,31 +470,35 @@ class DeviceViewSet(CustomFieldModelViewSet): # class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', 'tags') + queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.ConsolePortSerializer filterset_class = filters.ConsolePortFilterSet class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsoleServerPort.objects.prefetch_related('device', '_path__destination', 'cable', 'tags') + queryset = ConsoleServerPort.objects.prefetch_related( + 'device', '_path__destination', 'cable', '_cable_peer', 'tags' + ) serializer_class = serializers.ConsoleServerPortSerializer filterset_class = filters.ConsoleServerPortFilterSet class PowerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', 'tags') + queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.PowerPortSerializer filterset_class = filters.PowerPortFilterSet class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', 'tags') + queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.PowerOutletSerializer filterset_class = filters.PowerOutletFilterSet class InterfaceViewSet(PathEndpointMixin, ModelViewSet): - queryset = Interface.objects.prefetch_related('device', '_path__destination', 'cable', 'ip_addresses', 'tags') + queryset = Interface.objects.prefetch_related( + 'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags' + ) serializer_class = serializers.InterfaceSerializer filterset_class = filters.InterfaceFilterSet @@ -597,7 +601,9 @@ class PowerPanelViewSet(ModelViewSet): # class PowerFeedViewSet(CustomFieldModelViewSet): - queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', '_path__destination', 'cable', 'tags') + queryset = PowerFeed.objects.prefetch_related( + 'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags' + ) serializer_class = serializers.PowerFeedSerializer filterset_class = filters.PowerFeedFilterSet From 3870f5d2466a69481830025bf8df16c040f2c2f0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 15:26:59 -0400 Subject: [PATCH 45/67] Remove unused CablePathManager --- netbox/dcim/managers.py | 8 -------- netbox/dcim/models/devices.py | 3 --- 2 files changed, 11 deletions(-) delete mode 100644 netbox/dcim/managers.py diff --git a/netbox/dcim/managers.py b/netbox/dcim/managers.py deleted file mode 100644 index 903a6feac..000000000 --- a/netbox/dcim/managers.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.db.models import Manager - - -class CablePathManager(Manager): - - def create_for_endpoint(self, endpoint): - ct = ContentType.objects.get_for_model(endpoint) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 1f9db0986..e6c6134c6 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -15,7 +15,6 @@ from taggit.managers import TaggableManager from dcim.choices import * from dcim.constants import * from dcim.fields import PathField -from dcim.managers import CablePathManager from dcim.utils import path_node_to_object from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem from extras.utils import extras_features @@ -1195,8 +1194,6 @@ class CablePath(models.Model): default=False ) - objects = CablePathManager() - class Meta: unique_together = ('origin_type', 'origin_id') From 52ec35b94f25a2a9764f10b3d0868a1023c34411 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 15:27:40 -0400 Subject: [PATCH 46/67] Correct serializer field lists --- netbox/circuits/api/serializers.py | 1 + netbox/dcim/api/serializers.py | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 6bbb0cef5..9bc95f065 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -78,4 +78,5 @@ class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpoint fields = [ 'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', + 'cable_peer', 'cable_peer_type', ] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 803b64fbb..11e286132 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -487,7 +487,7 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial model = ConsoleServerPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', - 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', 'tags', + 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', 'cable_peer_type', 'tags', ] @@ -505,7 +505,7 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, model = ConsolePort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', - 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', 'tags', + 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', 'cable_peer_type', 'tags', ] @@ -534,7 +534,7 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', - 'tags', + 'cable_peer_type', 'tags', ] @@ -553,7 +553,7 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', - 'tags', + 'cable_peer_type', 'tags', ] @@ -578,7 +578,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', - 'cable_peer', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', + 'cable_peer', 'cable_peer_type', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', ] # TODO: This validation should be handled by Interface.clean() @@ -611,7 +611,8 @@ class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Val class Meta: model = RearPort fields = [ - 'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'tags', + 'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', + 'cable_peer_type', 'tags', ] @@ -637,7 +638,7 @@ class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Va model = FrontPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', - 'cable_peer', 'tags', + 'cable_peer', 'cable_peer_type', 'tags', ] @@ -821,5 +822,6 @@ class PowerFeedSerializer( fields = [ 'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', + 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', + 'cable_peer_type', ] From 534364a30fc00e6bb927c15113ed4495ca86e70f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 15:48:52 -0400 Subject: [PATCH 47/67] Improve model docstrings --- netbox/dcim/models/device_components.py | 20 +++++++++++++++++++- netbox/dcim/models/devices.py | 22 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 1bd577cdf..6e93dd768 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -39,6 +39,9 @@ __all__ = ( class ComponentModel(models.Model): + """ + An abstract model inherited by any model which has a parent Device. + """ device = models.ForeignKey( to='dcim.Device', on_delete=models.CASCADE, @@ -93,6 +96,14 @@ class ComponentModel(models.Model): class CableTermination(models.Model): + """ + An abstract model inherited by all models to which a Cable can terminate (certain device components, PowerFeed, and + CircuitTermination instances). The `cable` field indicates the Cable instance which is terminated to this instance. + + `_cable_peer` is a GenericForeignKey used to cache the far-end CableTermination on the local instance; this is a + shortcut to referencing `cable.termination_b`, for example. `_cable_peer` is set or cleared by the receivers in + dcim.signals when a Cable instance is created or deleted, respectively. + """ cable = models.ForeignKey( to='dcim.Cable', on_delete=models.SET_NULL, @@ -137,7 +148,14 @@ class CableTermination(models.Model): class PathEndpoint(models.Model): """ - Any object which may serve as the originating endpoint of a CablePath. + An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically, + these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, PowerFeed, and CircuitTermination. + + `_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in + dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the + CablePath model. `_path` should not be accessed directly; rather, use the `path` property. + + `connected_endpoint()` is a convenience method for returning the destination of the associated CablePath, if any. """ _path = models.ForeignKey( to='dcim.CablePath', diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index e6c6134c6..e4ea551b9 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1162,7 +1162,27 @@ class Cable(ChangeLoggedModel, CustomFieldModel): class CablePath(models.Model): """ - An array of objects conveying the end-to-end path of one or more Cables. + A CablePath instance represents the physical path from an origin to a destination, including all intermediate + elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do + not terminate on a PathEndpoint). + + `path` contains a list of nodes within the path, each represented by a tuple of (type, ID). The first element in the + path must be a Cable instance, followed by a pair of pass-through ports. For example, consider the following + topology: + + 1 2 3 + Interface A --- Front Port A | Rear Port A --- Rear Port B | Front Port B --- Interface B + + This path would be expressed as: + + CablePath( + origin = Interface A + destination = Interface B + path = [Cable 1, Front Port A, Rear Port A, Cable 2, Rear Port B, Front Port B, Cable 3] + ) + + `is_active` is set to True only if 1) `destination` is not null, and 2) every Cable within the path has a status of + "connected". """ origin_type = models.ForeignKey( to=ContentType, From 6b3a1998c83f50bbda7379da685ccf6a85cdddf9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 15:59:21 -0400 Subject: [PATCH 48/67] Add test_is_connected to CircuitTerminationTestCase --- netbox/circuits/tests/test_filters.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/circuits/tests/test_filters.py b/netbox/circuits/tests/test_filters.py index 9756c320b..b0861a7c0 100644 --- a/netbox/circuits/tests/test_filters.py +++ b/netbox/circuits/tests/test_filters.py @@ -3,7 +3,7 @@ from django.test import TestCase from circuits.choices import * from circuits.filters import * from circuits.models import Circuit, CircuitTermination, CircuitType, Provider -from dcim.models import Region, Site +from dcim.models import Cable, Region, Site from tenancy.models import Tenant, TenantGroup @@ -286,6 +286,8 @@ class CircuitTerminationTestCase(TestCase): )) CircuitTermination.objects.bulk_create(circuit_terminations) + Cable(termination_a=circuit_terminations[0], termination_b=circuit_terminations[1]).save() + def test_term_side(self): params = {'term_side': 'A'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) @@ -313,3 +315,7 @@ class CircuitTerminationTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_is_connected(self): + params = {'is_connected': True} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) From a6e0ef8cd84e0ec08b60425b8178b065146cdf52 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 16:15:18 -0400 Subject: [PATCH 49/67] Clean up console/power/interface connections views --- netbox/dcim/views.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 63711a863..696b7f356 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1117,7 +1117,7 @@ class DeviceLLDPNeighborsView(ObjectView): def get(self, request, pk): device = get_object_or_404(self.queryset, pk=pk) - interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related('_path').exclude( + interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related('_path__destination').exclude( type__in=NONCONNECTABLE_IFACE_TYPES ) @@ -2087,7 +2087,7 @@ class CableBulkDeleteView(BulkDeleteView): class ConsoleConnectionsListView(ObjectListView): queryset = ConsolePort.objects.prefetch_related( - 'device', '_path__destination__device' + 'device', '_path__destination' ).filter(_path__isnull=False).order_by('device') filterset = filters.ConsoleConnectionFilterSet filterset_form = forms.ConsoleConnectionFilterForm @@ -2097,7 +2097,7 @@ class ConsoleConnectionsListView(ObjectListView): def queryset_to_csv(self): csv_data = [ # Headers - ','.join(['console_server', 'port', 'device', 'console_port', 'connection_status']) + ','.join(['console_server', 'port', 'device', 'console_port', 'reachable']) ] for obj in self.queryset: csv = csv_format([ @@ -2105,7 +2105,7 @@ class ConsoleConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Reachable' if obj._path.is_active else 'Not Reachable', + obj._path.is_active ]) csv_data.append(csv) @@ -2114,7 +2114,7 @@ class ConsoleConnectionsListView(ObjectListView): class PowerConnectionsListView(ObjectListView): queryset = PowerPort.objects.prefetch_related( - 'device', '_path__destination__device' + 'device', '_path__destination' ).filter(_path__isnull=False).order_by('device') filterset = filters.PowerConnectionFilterSet filterset_form = forms.PowerConnectionFilterForm @@ -2124,7 +2124,7 @@ class PowerConnectionsListView(ObjectListView): def queryset_to_csv(self): csv_data = [ # Headers - ','.join(['pdu', 'outlet', 'device', 'power_port', 'connection_status']) + ','.join(['pdu', 'outlet', 'device', 'power_port', 'reachable']) ] for obj in self.queryset: csv = csv_format([ @@ -2132,7 +2132,7 @@ class PowerConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Reachable' if obj._path.is_active else 'Not Reachable', + obj._path.is_active ]) csv_data.append(csv) @@ -2141,7 +2141,7 @@ class PowerConnectionsListView(ObjectListView): class InterfaceConnectionsListView(ObjectListView): queryset = Interface.objects.prefetch_related( - 'device', '_path__destination__device' + 'device', '_path__destination' ).filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair _path__isnull=False, @@ -2156,7 +2156,7 @@ class InterfaceConnectionsListView(ObjectListView): csv_data = [ # Headers ','.join([ - 'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status' + 'device_a', 'interface_a', 'device_b', 'interface_b', 'reachable' ]) ] for obj in self.queryset: @@ -2165,7 +2165,7 @@ class InterfaceConnectionsListView(ObjectListView): obj._path.destination.name if obj._path.destination else None, obj.device.identifier, obj.name, - 'Reachable' if obj._path.is_active else 'Not Reachable', + obj._path.is_active ]) csv_data.append(csv) From a072d4059420a13b7dadd3184a3d22536ec3c8ad Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 16:16:08 -0400 Subject: [PATCH 50/67] Update v2.10 changelog --- docs/release-notes/version-2.10.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 124bbd645..253ed0612 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -63,32 +63,29 @@ All end-to-end cable paths are now cached using the new CablePath model. This al * Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints (bulk update and delete) * circuits.CircuitTermination: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) - * Added `cable_peer` + * Added `cable_peer` and `cable_peer_type` * dcim.Cable: Added `custom_fields` * dcim.ConsolePort: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) - * Added `cable_peer` + * Added `cable_peer` and `cable_peer_type` * dcim.ConsoleServerPort: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) - * Added `cable_peer` -* dcim.FrontPort: - * Removed the `trace` endpoint - * Added `cable_peer` + * Added `cable_peer` and `cable_peer_type` +* dcim.FrontPort: Added `cable_peer` and `cable_peer_type` * dcim.Interface: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) - * Added `cable_peer` + * Added `cable_peer` and `cable_peer_type` * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning -* dcim.PowerFeed: Add fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, and `cable_peer` +* dcim.PowerFeed: Add fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, `cable_peer`, and `cable_peer_type` * dcim.PowerOutlet: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) - * Added `cable_peer` + * Added `cable_peer` and `cable_peer_type` * dcim.PowerPanel: Added `custom_fields` * dcim.PowerPort * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) - * Added `cable_peer` + * Added `cable_peer` and `cable_peer_type` * dcim.RackReservation: Added `custom_fields` -* dcim.RearPort: - * Removed the `trace` endpoint +* dcim.RearPort: Added `cable_peer` and `cable_peer_type` * dcim.VirtualChassis: Added `custom_fields` * extras.ExportTemplate: The `template_language` field has been removed * extras.Graph: This API endpoint has been removed (see #4349) From 2c9ae60dec31732d5db77ad585eb662580c126b4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 16:34:03 -0400 Subject: [PATCH 51/67] Optimize path node representations --- netbox/dcim/utils.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index d36cb1ad3..ccc849aa5 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -5,12 +5,20 @@ from .exceptions import CableTraceSplit def object_to_path_node(obj): - return f'{obj._meta.model_name}:{obj.pk}' + """ + Return a representation of an object suitable for inclusion in a CablePath path. Node representation is in the + form :. + """ + ct = ContentType.objects.get_for_model(obj) + return f'{ct.pk}:{obj.pk}' def path_node_to_object(repr): - model_name, object_id = repr.split(':') - model_class = ContentType.objects.get(model=model_name).model_class() + """ + Given a path node representation, return the corresponding object. + """ + ct_id, object_id = repr.split(':') + model_class = ContentType.objects.get(pk=ct_id).model_class() return model_class.objects.get(pk=int(object_id)) From 44b842592ae803b4cd6d1bbda6d11170035f4f9a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 16:58:11 -0400 Subject: [PATCH 52/67] Restore total length count on trace view --- docs/release-notes/version-2.10.md | 8 ++++++-- netbox/dcim/models/devices.py | 14 ++++++++++++-- netbox/dcim/utils.py | 13 +++++++++++-- netbox/dcim/views.py | 6 +----- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 253ed0612..fa3f3aea8 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -71,7 +71,9 @@ All end-to-end cable paths are now cached using the new CablePath model. This al * dcim.ConsoleServerPort: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` -* dcim.FrontPort: Added `cable_peer` and `cable_peer_type` +* dcim.FrontPort: + * Removed the `/trace/` endpoint + * Added `cable_peer` and `cable_peer_type` * dcim.Interface: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` @@ -85,7 +87,9 @@ All end-to-end cable paths are now cached using the new CablePath model. This al * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` * dcim.RackReservation: Added `custom_fields` -* dcim.RearPort: Added `cable_peer` and `cable_peer_type` +* dcim.RearPort: + * Removed the `/trace/` endpoint + * Added `cable_peer` and `cable_peer_type` * dcim.VirtualChassis: Added `custom_fields` * extras.ExportTemplate: The `template_language` field has been removed * extras.Graph: This API endpoint has been removed (see #4349) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index e4ea551b9..b44146b99 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -7,7 +7,7 @@ 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 -from django.db.models import F, ProtectedError +from django.db.models import F, ProtectedError, Sum from django.urls import reverse from django.utils.safestring import mark_safe from taggit.managers import TaggableManager @@ -15,7 +15,7 @@ from taggit.managers import TaggableManager from dcim.choices import * from dcim.constants import * from dcim.fields import PathField -from dcim.utils import path_node_to_object +from dcim.utils import decompile_path_node, path_node_to_object from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem from extras.utils import extras_features from utilities.choices import ColorChoices @@ -1228,6 +1228,16 @@ class CablePath(models.Model): model = self.origin._meta.model model.objects.filter(pk=self.origin.pk).update(_path=self.pk) + def get_total_length(self): + """ + Return the sum of the length of each cable in the path. + """ + cable_ids = [ + # Starting from the first element, every third element in the path should be a Cable + decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3) + ] + return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total'] + # # Virtual chassis diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index ccc849aa5..52b0a4232 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -4,20 +4,29 @@ from .choices import CableStatusChoices from .exceptions import CableTraceSplit +def compile_path_node(ct_id, object_id): + return f'{ct_id}:{object_id}' + + +def decompile_path_node(repr): + ct_id, object_id = repr.split(':') + return int(ct_id), int(object_id) + + def object_to_path_node(obj): """ Return a representation of an object suitable for inclusion in a CablePath path. Node representation is in the form :. """ ct = ContentType.objects.get_for_model(obj) - return f'{ct.pk}:{obj.pk}' + return compile_path_node(ct.pk, obj.pk) def path_node_to_object(repr): """ Given a path node representation, return the corresponding object. """ - ct_id, object_id = repr.split(':') + ct_id, object_id = decompile_path_node(repr) model_class = ContentType.objects.get(pk=ct_id).model_class() return model_class.objects.get(pk=int(object_id)) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 696b7f356..ff4ec1ef6 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1984,15 +1984,11 @@ class PathTraceView(ObjectView): else: path = related_paths.first() - # total_length = sum( - # [entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length] - # ) - return render(request, 'dcim/cable_trace.html', { 'obj': obj, 'path': path, 'related_paths': related_paths, - # 'total_length': total_length, + 'total_length': path.get_total_length(), }) From c7c66626b6806c6da17f5bb7ad61d6743ca51f00 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Oct 2020 17:28:25 -0400 Subject: [PATCH 53/67] Standardize 'cabled' and 'connected' filters; complete tests --- netbox/circuits/filters.py | 4 +- netbox/circuits/tests/test_filters.py | 10 ++- netbox/dcim/filters.py | 92 ++++++++++++--------------- netbox/dcim/tests/test_filters.py | 54 +++++++++++++--- 4 files changed, 94 insertions(+), 66 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index ebc0d0ec1..c573603c6 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -1,7 +1,7 @@ import django_filters from django.db.models import Q -from dcim.filters import PathEndpointFilterSet +from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet from dcim.models import Region, Site from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from tenancy.filters import TenancyFilterSet @@ -145,7 +145,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr ).distinct() -class CircuitTerminationFilterSet(BaseFilterSet, PathEndpointFilterSet): +class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/circuits/tests/test_filters.py b/netbox/circuits/tests/test_filters.py index b0861a7c0..73701be03 100644 --- a/netbox/circuits/tests/test_filters.py +++ b/netbox/circuits/tests/test_filters.py @@ -316,6 +316,12 @@ class CircuitTerminationTestCase(TestCase): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) - def test_is_connected(self): - params = {'is_connected': True} + def test_cabled(self): + params = {'cabled': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_connected(self): + params = {'connected': True} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'connected': False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index aeccc341b..9690ee195 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -24,6 +24,7 @@ from .models import ( __all__ = ( 'CableFilterSet', + 'CableTerminationFilterSet', 'ConsoleConnectionFilterSet', 'ConsolePortFilterSet', 'ConsolePortTemplateFilterSet', @@ -41,6 +42,7 @@ __all__ = ( 'InterfaceTemplateFilterSet', 'InventoryItemFilterSet', 'ManufacturerFilterSet', + 'PathEndpointFilterSet', 'PlatformFilterSet', 'PowerConnectionFilterSet', 'PowerFeedFilterSet', @@ -753,81 +755,76 @@ class DeviceComponentFilterSet(django_filters.FilterSet): ) -class PathEndpointFilterSet(django_filters.FilterSet): - is_connected = django_filters.BooleanFilter( - method='filter_is_connected', - label='Search', - ) - - def filter_is_connected(self, queryset, name, value): - return queryset.filter(_path__is_active=True) - - -class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): - type = django_filters.MultipleChoiceFilter( - choices=ConsolePortTypeChoices, - null_value=None - ) +class CableTerminationFilterSet(django_filters.FilterSet): cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', exclude=True ) + +class PathEndpointFilterSet(django_filters.FilterSet): + connected = django_filters.BooleanFilter( + method='filter_connected' + ) + + def filter_connected(self, queryset, name, value): + if value: + return queryset.filter(_path__is_active=True) + else: + return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False)) + + +class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=ConsolePortTypeChoices, + null_value=None + ) + class Meta: model = ConsolePort fields = ['id', 'name', 'description'] -class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): +class ConsoleServerPortFilterSet( + BaseFilterSet, + DeviceComponentFilterSet, + CableTerminationFilterSet, + PathEndpointFilterSet +): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None ) - cabled = django_filters.BooleanFilter( - field_name='cable', - lookup_expr='isnull', - exclude=True - ) class Meta: model = ConsoleServerPort fields = ['id', 'name', 'description'] -class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): +class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerPortTypeChoices, null_value=None ) - cabled = django_filters.BooleanFilter( - field_name='cable', - lookup_expr='isnull', - exclude=True - ) class Meta: model = PowerPort fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description'] -class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): +class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerOutletTypeChoices, null_value=None ) - cabled = django_filters.BooleanFilter( - field_name='cable', - lookup_expr='isnull', - exclude=True - ) class Meta: model = PowerOutlet fields = ['id', 'name', 'feed_leg', 'description'] -class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFilterSet): +class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -844,11 +841,6 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFi field_name='pk', label='Device (ID)', ) - cabled = django_filters.BooleanFilter( - field_name='cable', - lookup_expr='isnull', - exclude=True - ) kind = django_filters.CharFilter( method='filter_kind', label='Kind of interface', @@ -925,24 +917,14 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, PathEndpointFi }.get(value, queryset.none()) -class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): - cabled = django_filters.BooleanFilter( - field_name='cable', - lookup_expr='isnull', - exclude=True - ) +class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class Meta: model = FrontPort fields = ['id', 'name', 'type', 'description'] -class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): - cabled = django_filters.BooleanFilter( - field_name='cable', - lookup_expr='isnull', - exclude=True - ) +class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class Meta: model = RearPort @@ -1266,7 +1248,13 @@ class PowerPanelFilterSet(BaseFilterSet): return queryset.filter(qs_filter) -class PowerFeedFilterSet(BaseFilterSet, PathEndpointFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class PowerFeedFilterSet( + BaseFilterSet, + CableTerminationFilterSet, + PathEndpointFilterSet, + CustomFieldFilterSet, + CreatedUpdatedFilterSet +): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index c399e1a92..f209cd1f4 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -1514,9 +1514,11 @@ class ConsolePortTestCase(TestCase): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_is_connected(self): - params = {'is_connected': True} + def test_connected(self): + params = {'connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'connected': False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_region(self): regions = Region.objects.all()[:2] @@ -1608,9 +1610,11 @@ class ConsoleServerPortTestCase(TestCase): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_is_connected(self): - params = {'is_connected': True} + def test_connected(self): + params = {'connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'connected': False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_region(self): regions = Region.objects.all()[:2] @@ -1710,9 +1714,11 @@ class PowerPortTestCase(TestCase): params = {'allocated_draw': [50, 100]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_is_connected(self): - params = {'is_connected': True} + def test_connected(self): + params = {'connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'connected': False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_region(self): regions = Region.objects.all()[:2] @@ -1809,9 +1815,11 @@ class PowerOutletTestCase(TestCase): params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - def test_is_connected(self): - params = {'is_connected': True} + def test_connected(self): + params = {'connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'connected': False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_region(self): regions = Region.objects.all()[:2] @@ -1896,9 +1904,11 @@ class InterfaceTestCase(TestCase): params = {'name': ['Interface 1', 'Interface 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_is_connected(self): - params = {'is_connected': True} + def test_connected(self): + params = {'connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'connected': False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_enabled(self): params = {'enabled': 'true'} @@ -2657,6 +2667,18 @@ class PowerFeedTestCase(TestCase): ) PowerFeed.objects.bulk_create(power_feeds) + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model', slug='model') + device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') + device = Device.objects.create(name='Device', device_type=device_type, device_role=device_role, site=sites[0]) + power_ports = [ + PowerPort(device=device, name='Power Port 1'), + PowerPort(device=device, name='Power Port 2'), + ] + PowerPort.objects.bulk_create(power_ports) + Cable(termination_a=power_feeds[0], termination_b=power_ports[0]).save() + Cable(termination_a=power_feeds[1], termination_b=power_ports[1]).save() + def test_id(self): params = {'id': self.queryset.values_list('pk', flat=True)[:2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2718,5 +2740,17 @@ class PowerFeedTestCase(TestCase): params = {'rack_id': [racks[0].pk, racks[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_cabled(self): + params = {'cabled': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cabled': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_connected(self): + params = {'connected': True} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'connected': False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + # TODO: Connection filters From 6db3c65bcc335640cc792632b043e993b685069e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Oct 2020 09:50:12 -0400 Subject: [PATCH 54/67] Swap order of cabling migrations --- .../{0022_cache_cable_peer.py => 0021_cache_cable_peer.py} | 2 +- .../migrations/{0021_cablepath.py => 0022_cablepath.py} | 6 ++---- .../{0121_cache_cable_peer.py => 0120_cache_cable_peer.py} | 2 +- .../migrations/{0120_cablepath.py => 0121_cablepath.py} | 4 +--- 4 files changed, 5 insertions(+), 9 deletions(-) rename netbox/circuits/migrations/{0022_cache_cable_peer.py => 0021_cache_cable_peer.py} (97%) rename netbox/circuits/migrations/{0021_cablepath.py => 0022_cablepath.py} (83%) rename netbox/dcim/migrations/{0121_cache_cable_peer.py => 0120_cache_cable_peer.py} (99%) rename netbox/dcim/migrations/{0120_cablepath.py => 0121_cablepath.py} (97%) diff --git a/netbox/circuits/migrations/0022_cache_cable_peer.py b/netbox/circuits/migrations/0021_cache_cable_peer.py similarity index 97% rename from netbox/circuits/migrations/0022_cache_cable_peer.py rename to netbox/circuits/migrations/0021_cache_cable_peer.py index 9a470a3c2..630c3b4ec 100644 --- a/netbox/circuits/migrations/0022_cache_cable_peer.py +++ b/netbox/circuits/migrations/0021_cache_cable_peer.py @@ -28,7 +28,7 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), - ('circuits', '0021_cablepath'), + ('circuits', '0020_custom_field_data'), ] operations = [ diff --git a/netbox/circuits/migrations/0021_cablepath.py b/netbox/circuits/migrations/0022_cablepath.py similarity index 83% rename from netbox/circuits/migrations/0021_cablepath.py rename to netbox/circuits/migrations/0022_cablepath.py index b416d2e9f..4a5b26efa 100644 --- a/netbox/circuits/migrations/0021_cablepath.py +++ b/netbox/circuits/migrations/0022_cablepath.py @@ -1,5 +1,3 @@ -# Generated by Django 3.1 on 2020-10-02 19:43 - from django.db import migrations, models import django.db.models.deletion @@ -7,8 +5,8 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('dcim', '0120_cablepath'), - ('circuits', '0020_custom_field_data'), + ('dcim', '0121_cablepath'), + ('circuits', '0021_cache_cable_peer'), ] operations = [ diff --git a/netbox/dcim/migrations/0121_cache_cable_peer.py b/netbox/dcim/migrations/0120_cache_cable_peer.py similarity index 99% rename from netbox/dcim/migrations/0121_cache_cable_peer.py rename to netbox/dcim/migrations/0120_cache_cable_peer.py index aeb89c5d3..c45d03396 100644 --- a/netbox/dcim/migrations/0121_cache_cable_peer.py +++ b/netbox/dcim/migrations/0120_cache_cable_peer.py @@ -50,7 +50,7 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), - ('dcim', '0120_cablepath'), + ('dcim', '0119_inventoryitem_mptt_rebuild'), ] operations = [ diff --git a/netbox/dcim/migrations/0120_cablepath.py b/netbox/dcim/migrations/0121_cablepath.py similarity index 97% rename from netbox/dcim/migrations/0120_cablepath.py rename to netbox/dcim/migrations/0121_cablepath.py index dd3c4ed19..737e59b32 100644 --- a/netbox/dcim/migrations/0120_cablepath.py +++ b/netbox/dcim/migrations/0121_cablepath.py @@ -1,5 +1,3 @@ -# Generated by Django 3.1 on 2020-10-02 19:43 - import dcim.fields from django.db import migrations, models import django.db.models.deletion @@ -9,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), - ('dcim', '0119_inventoryitem_mptt_rebuild'), + ('dcim', '0120_cache_cable_peer'), ] operations = [ From f560693748d1f35e79ad9d006a8a9b75ef5ae37b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Oct 2020 10:23:15 -0400 Subject: [PATCH 55/67] Rewrite trace_paths management command and call in upgrade.sh --- .../dcim/management/commands/retrace_paths.py | 68 ---------------- .../dcim/management/commands/trace_paths.py | 81 +++++++++++++++++++ upgrade.sh | 5 ++ 3 files changed, 86 insertions(+), 68 deletions(-) delete mode 100644 netbox/dcim/management/commands/retrace_paths.py create mode 100644 netbox/dcim/management/commands/trace_paths.py diff --git a/netbox/dcim/management/commands/retrace_paths.py b/netbox/dcim/management/commands/retrace_paths.py deleted file mode 100644 index d11a85417..000000000 --- a/netbox/dcim/management/commands/retrace_paths.py +++ /dev/null @@ -1,68 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.core.management.base import BaseCommand -from django.core.management.color import no_style -from django.db import connection -from django.db.models import Q - -from dcim.models import CablePath -from dcim.signals import create_cablepath - -ENDPOINT_MODELS = ( - 'circuits.CircuitTermination', - 'dcim.ConsolePort', - 'dcim.ConsoleServerPort', - 'dcim.Interface', - 'dcim.PowerFeed', - 'dcim.PowerOutlet', - 'dcim.PowerPort', -) - - -class Command(BaseCommand): - help = "Recalculate natural ordering values for the specified models" - - def add_arguments(self, parser): - parser.add_argument( - 'args', metavar='app_label.ModelName', nargs='*', - help='One or more specific models (each prefixed with its app_label) to retrace', - ) - - def _get_content_types(self, model_names): - q = Q() - for model_name in model_names: - app_label, model = model_name.split('.') - q |= Q(app_label__iexact=app_label, model__iexact=model) - return ContentType.objects.filter(q) - - def handle(self, *model_names, **options): - # Determine the models for which we're retracing all paths - origin_types = self._get_content_types(model_names or ENDPOINT_MODELS) - self.stdout.write(f"Retracing paths for models: {', '.join([str(ct) for ct in origin_types])}") - - # Delete all existing CablePath instances - self.stdout.write(f"Deleting existing cable paths...") - deleted_count, _ = CablePath.objects.filter(origin_type__in=origin_types).delete() - self.stdout.write((self.style.SUCCESS(f' Deleted {deleted_count} paths'))) - - # Reset the SQL sequence. Can do this only if deleting _all_ CablePaths. - if not CablePath.objects.count(): - self.stdout.write(f'Resetting database sequence for CablePath...') - sequence_sql = connection.ops.sequence_reset_sql(no_style(), [CablePath]) - with connection.cursor() as cursor: - for sql in sequence_sql: - cursor.execute(sql) - self.stdout.write(self.style.SUCCESS(' Success.')) - - # Retrace interfaces - for ct in origin_types: - model = ct.model_class() - origins = model.objects.filter(cable__isnull=False) - print(f'Retracing {origins.count()} cabled {model._meta.verbose_name_plural}...') - i = 0 - for i, obj in enumerate(origins, start=1): - 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}')) - - self.stdout.write(self.style.SUCCESS('Finished.')) diff --git a/netbox/dcim/management/commands/trace_paths.py b/netbox/dcim/management/commands/trace_paths.py new file mode 100644 index 000000000..47636a943 --- /dev/null +++ b/netbox/dcim/management/commands/trace_paths.py @@ -0,0 +1,81 @@ +from django.core.management.base import BaseCommand +from django.core.management.color import no_style +from django.db import connection + +from circuits.models import CircuitTermination +from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort +from dcim.signals import create_cablepath + +ENDPOINT_MODELS = ( + CircuitTermination, + ConsolePort, + ConsoleServerPort, + Interface, + PowerFeed, + PowerOutlet, + PowerPort +) + + +class Command(BaseCommand): + help = "Generate any missing cable paths among all cable termination objects in NetBox" + + def add_arguments(self, parser): + parser.add_argument( + "--force", action='store_true', dest='force', + help="Force recalculation of all existing cable paths" + ) + parser.add_argument( + "--no-input", action='store_true', dest='no_input', + help="Do not prompt user for any input/confirmation" + ) + + def handle(self, *model_names, **options): + + # If --force was passed, first delete all existing CablePaths + if options['force']: + cable_paths = CablePath.objects.all() + paths_count = cable_paths.count() + + # Prompt the user to confirm recalculation of all paths + if paths_count and not options['no_input']: + self.stdout.write(self.style.ERROR("WARNING: Forcing recalculation of all cable paths.")) + self.stdout.write( + f"This will delete and recalculate all {paths_count} existing cable paths. Are you sure?" + ) + confirmation = input("Type yes to confirm: ") + if confirmation != 'yes': + self.stdout.write(self.style.SUCCESS("Aborting")) + return + + # Delete all existing CablePath instances + self.stdout.write(f"Deleting {paths_count} existing cable paths...") + deleted_count, _ = CablePath.objects.all().delete() + self.stdout.write((self.style.SUCCESS(f' Deleted {deleted_count} paths'))) + + # Reinitialize the model's PK sequence + self.stdout.write(f'Resetting database sequence for CablePath model') + sequence_sql = connection.ops.sequence_reset_sql(no_style(), [CablePath]) + with connection.cursor() as cursor: + for sql in sequence_sql: + cursor.execute(sql) + + # Retrace paths + for model in ENDPOINT_MODELS: + origins = model.objects.filter(cable__isnull=False) + if not options['force']: + origins = origins.filter(_path__isnull=True) + origins_count = origins.count() + if not origins_count: + print(f'Found no missing {model._meta.verbose_name} paths; skipping') + continue + print(f'Retracing {origins_count} cabled {model._meta.verbose_name_plural}...') + i = 0 + for i, obj in enumerate(origins, start=1): + create_cablepath(obj) + # TODO: Come up with a better progress indicator + if not i % 1000: + self.stdout.write(f' {i}') + self.stdout.write(self.style.SUCCESS(f' Retraced {i} {model._meta.verbose_name_plural}')) + + self.stdout.write(self.style.SUCCESS('Finished.')) diff --git a/upgrade.sh b/upgrade.sh index 66ba7b39f..468f189b3 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -55,6 +55,11 @@ COMMAND="python3 netbox/manage.py migrate" echo "Applying database migrations ($COMMAND)..." eval $COMMAND || exit 1 +# Trace any missing cable paths (not typically needed) +COMMAND="python3 netbox/manage.py trace_paths --no-input" +echo "Checking for missing cable paths ($COMMAND)..." +eval $COMMAND || exit 1 + # Collect static files COMMAND="python3 netbox/manage.py collectstatic --no-input" echo "Collecting static files ($COMMAND)..." From eaf8d95ce54fb9d4b3de239a193792cfc1a255e0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Oct 2020 11:14:16 -0400 Subject: [PATCH 56/67] Clean up power utilization logic --- netbox/dcim/models/device_components.py | 13 ++++++++--- netbox/dcim/models/racks.py | 30 ++++++++++++++----------- netbox/templates/dcim/rack.html | 12 ++++++---- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 6e93dd768..72bd453c5 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -320,8 +320,12 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): """ # Calculate aggregate draw of all child power outlets if no numbers have been defined manually if self.allocated_draw is None and self.maximum_draw is None: + poweroutlet_ct = ContentType.objects.get_for_model(PowerOutlet) outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True) - utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( + utilization = PowerPort.objects.filter( + _cable_peer_type=poweroutlet_ct, + _cable_peer_id__in=outlet_ids + ).aggregate( maximum_draw_total=Sum('maximum_draw'), allocated_draw_total=Sum('allocated_draw'), ) @@ -333,10 +337,13 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): } # Calculate per-leg aggregates for three-phase feeds - if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE: + if getattr(self._cable_peer, 'phase', None) == PowerFeedPhaseChoices.PHASE_3PHASE: for leg, leg_name in PowerOutletFeedLegChoices: outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True) - utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( + utilization = PowerPort.objects.filter( + _cable_peer_type=poweroutlet_ct, + _cable_peer_id__in=outlet_ids + ).aggregate( maximum_draw_total=Sum('maximum_draw'), allocated_draw_total=Sum('allocated_draw'), ) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 409db14c5..78c72f503 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -3,6 +3,7 @@ from collections import OrderedDict from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator @@ -22,6 +23,7 @@ from utilities.fields import ColorField, NaturalOrderingField from utilities.querysets import RestrictedQuerySet from utilities.mptt import TreeManager from utilities.utils import array_to_string, serialize_object +from .device_components import PowerOutlet, PowerPort from .devices import Device from .power import PowerFeed @@ -536,20 +538,22 @@ class Rack(ChangeLoggedModel, CustomFieldModel): """ Determine the utilization rate of power in the rack and return it as a percentage. """ - power_stats = PowerFeed.objects.filter( - rack=self - ).annotate( - allocated_draw_total=Sum('connected_endpoint__poweroutlets__connected_endpoint__allocated_draw'), - ).values( - 'allocated_draw_total', - 'available_power' - ) + powerfeeds = PowerFeed.objects.filter(rack=self) + available_power_total = sum(pf.available_power for pf in powerfeeds) + if not available_power_total: + return 0 - if power_stats: - allocated_draw_total = sum(x['allocated_draw_total'] or 0 for x in power_stats) - available_power_total = sum(x['available_power'] for x in power_stats) - return int(allocated_draw_total / available_power_total * 100) or 0 - return 0 + pf_powerports = PowerPort.objects.filter( + _cable_peer_type=ContentType.objects.get_for_model(PowerFeed), + _cable_peer_id__in=powerfeeds.values_list('id', flat=True) + ) + poweroutlets = PowerOutlet.objects.filter(power_port_id__in=pf_powerports) + allocated_draw_total = PowerPort.objects.filter( + _cable_peer_type=ContentType.objects.get_for_model(PowerOutlet), + _cable_peer_id__in=poweroutlets.values_list('id', flat=True) + ).aggregate(Sum('allocated_draw'))['allocated_draw__sum'] or 0 + + return int(allocated_draw_total / available_power_total * 100) @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 4cf3b9018..c44b75be1 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -150,10 +150,14 @@ {{ device_count }} - - Utilization - {% utilization_graph rack.get_utilization %} - + + Space Utilization + {% utilization_graph rack.get_utilization %} + + + Power Utilization + {% utilization_graph rack.get_power_utilization %} +
From 85439fd952a85bc86c7e38e43f414017fe601e14 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Oct 2020 11:33:47 -0400 Subject: [PATCH 57/67] Fix PowerFeed display in cable traces --- .../templates/dcim/inc/cable_trace_end.html | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/netbox/templates/dcim/inc/cable_trace_end.html b/netbox/templates/dcim/inc/cable_trace_end.html index 6073c06ee..71b7c56ef 100644 --- a/netbox/templates/dcim/inc/cable_trace_end.html +++ b/netbox/templates/dcim/inc/cable_trace_end.html @@ -10,25 +10,34 @@ / {{ end.device.rack }} {% endif %} - {% else %} + {% elif end.circuit %} {{ end.circuit.provider }} + {% elif end.power_panel %} + {{ end.power_panel }}
+ + {{ end.power_panel.site }} + {% endif %}
{% if end.device %} {# Device component #} {% with model=end|meta:"verbose_name" %} - {{ model|bettertitle }} {{ end }}
- {% if model == 'interface' %} - {{ end.get_type_display }} - {% elif model == 'front port' or model == 'rear port' %} + {{ model|bettertitle }} {{ end }}
+ {% if model == 'interface' or model == 'front port' or model == 'rear port' %} {{ end.get_type_display }} {% endif %} {% endwith %} - {% else %} - {# Circuit termination #} + {% elif end.circuit %} + {# CircuitTermination #} {{ end.circuit }}
{{ end }} + {% elif end.power_panel %} + {# PowerFeed #} + Power Feed {{ end }}
+ {% if end.rack %} + {{ end.rack }} + {% endif %} {% endif %}
From 35759fdb70cdeee50b98e96c9fd297070ad185fa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Oct 2020 16:39:15 -0400 Subject: [PATCH 58/67] Redo the cable trace UI (WIP) --- netbox/templates/dcim/cable_trace.html | 89 +++++++++---------- .../templates/dcim/inc/cable_trace_end.html | 43 --------- netbox/templates/dcim/trace/cable.html | 15 ++++ netbox/templates/dcim/trace/circuit.html | 6 ++ netbox/templates/dcim/trace/device.html | 9 ++ netbox/templates/dcim/trace/powerfeed.html | 9 ++ netbox/templates/dcim/trace/termination.html | 9 ++ 7 files changed, 92 insertions(+), 88 deletions(-) delete mode 100644 netbox/templates/dcim/inc/cable_trace_end.html create mode 100644 netbox/templates/dcim/trace/cable.html create mode 100644 netbox/templates/dcim/trace/circuit.html create mode 100644 netbox/templates/dcim/trace/device.html create mode 100644 netbox/templates/dcim/trace/powerfeed.html create mode 100644 netbox/templates/dcim/trace/termination.html diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 4b3f7f2d4..7df39caca 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -7,66 +7,65 @@ {% block content %}
-
- -
-
-

Near End

-
-
- {% if total_length %}
Total length: {{ total_length|floatformat:"-2" }} Meters
{% endif %} -
-
-

Far End

-
-
+
{% for near_end, cable, far_end in path.origin.trace %} + + {# Near end #} + {% if near_end.device %} + {% include 'dcim/trace/device.html' with device=near_end.device %} + {% include 'dcim/trace/termination.html' with termination=near_end %} + {% elif near_end.power_panel %} + {% include 'dcim/trace/powerfeed.html' with powerfeed=near_end %} + {% elif near_end.circuit %} + {% include 'dcim/trace/circuit.html' with circuit=near_end.circuit %} + {% include 'dcim/trace/termination.html' with termination=near_end %} + {% endif %} + + {# Cable #}
-
-

{{ forloop.counter }}

-
-
- {% include 'dcim/inc/cable_trace_end.html' with end=near_end %} -
-
- {% if cable %} -

- - {% if cable.label %}{{ cable.label }}{% else %}Cable #{{ cable.pk }}{% endif %} - -

-

{{ cable.get_status_display }}

-

{{ cable.get_type_display|default:"" }}

- {% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %} - {% if cable.color %} -   - {% endif %} - {% else %} -

No Cable

- {% endif %} -
-
- {% if far_end %} - {% include 'dcim/inc/cable_trace_end.html' with end=far_end %} - {% endif %} -
+ {% if cable %} + {% include 'dcim/trace/cable.html' %} + {% else %} +

No cable

+ {% endif %}
-
+ + {# Far end #} + {% if far_end.device %} + {% include 'dcim/trace/termination.html' with termination=far_end %} + {% if forloop.last %} + {% include 'dcim/trace/device.html' with device=far_end.device %} + {% endif %} + {% elif far_end.power_panel %} + {% include 'dcim/trace/powerfeed.html' with powerfeed=far_end %} + {% elif far_end.circuit %} + {% include 'dcim/trace/termination.html' with termination=far_end %} + {% if forloop.last %} + {% include 'dcim/trace/circuit.html' with circuit=far_end.circuit %} + {% endif %} + {% endif %} + {% endfor %}
-
+

Trace completed!

+ {% if total_length %} +
Total length: {{ total_length|floatformat:"-2" }} Meters
+ {% endif %}
-
+

Related Paths

diff --git a/netbox/templates/dcim/inc/cable_trace_end.html b/netbox/templates/dcim/inc/cable_trace_end.html deleted file mode 100644 index 71b7c56ef..000000000 --- a/netbox/templates/dcim/inc/cable_trace_end.html +++ /dev/null @@ -1,43 +0,0 @@ -{% load helpers %} - -
-
- {% if end.device %} - {{ end.device }}
- - {{ end.device.site }} - {% if end.device.rack %} - / {{ end.device.rack }} - {% endif %} - - {% elif end.circuit %} - {{ end.circuit.provider }} - {% elif end.power_panel %} - {{ end.power_panel }}
- - {{ end.power_panel.site }} - - {% endif %} -
-
- {% if end.device %} - {# Device component #} - {% with model=end|meta:"verbose_name" %} - {{ model|bettertitle }} {{ end }}
- {% if model == 'interface' or model == 'front port' or model == 'rear port' %} - {{ end.get_type_display }} - {% endif %} - {% endwith %} - {% elif end.circuit %} - {# CircuitTermination #} - {{ end.circuit }}
- {{ end }} - {% elif end.power_panel %} - {# PowerFeed #} - Power Feed {{ end }}
- {% if end.rack %} - {{ end.rack }} - {% endif %} - {% endif %} -
-
diff --git a/netbox/templates/dcim/trace/cable.html b/netbox/templates/dcim/trace/cable.html new file mode 100644 index 000000000..2cb5ffed6 --- /dev/null +++ b/netbox/templates/dcim/trace/cable.html @@ -0,0 +1,15 @@ +
+

+

+ + {% if cable.label %}{{ cable.label }}{% else %}Cable #{{ cable.pk }}{% endif %} + +

+

{{ cable.get_status_display }}

+

{{ cable.get_type_display|default:"" }}

+ {% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %} + {% if cable.color %} +   + {% endif %} +

+
diff --git a/netbox/templates/dcim/trace/circuit.html b/netbox/templates/dcim/trace/circuit.html new file mode 100644 index 000000000..ef1ed05bc --- /dev/null +++ b/netbox/templates/dcim/trace/circuit.html @@ -0,0 +1,6 @@ + diff --git a/netbox/templates/dcim/trace/device.html b/netbox/templates/dcim/trace/device.html new file mode 100644 index 000000000..c3ed109d7 --- /dev/null +++ b/netbox/templates/dcim/trace/device.html @@ -0,0 +1,9 @@ +
+
+ Device {{ device }}
+ {{ device.site }} + {% if device.rack %} + / {{ device.rack }} + {% endif %} +
+
diff --git a/netbox/templates/dcim/trace/powerfeed.html b/netbox/templates/dcim/trace/powerfeed.html new file mode 100644 index 000000000..a439aff27 --- /dev/null +++ b/netbox/templates/dcim/trace/powerfeed.html @@ -0,0 +1,9 @@ +
+
+ Power Feed {{ powerfeed }}
+ {{ powerfeed.power_panel }} + {% if powerfeed.rack %} + / {{ powerfeed.rack }} + {% endif %} +
+
diff --git a/netbox/templates/dcim/trace/termination.html b/netbox/templates/dcim/trace/termination.html new file mode 100644 index 000000000..dedb562ea --- /dev/null +++ b/netbox/templates/dcim/trace/termination.html @@ -0,0 +1,9 @@ +{% load helpers %} +
+
+ {{ termination|meta:"verbose_name"|bettertitle }} {{ termination }} + {% if termination.type %} +
{{ termination.get_type_display }} + {% endif %} +
+
\ No newline at end of file From aa0ee2720bb34615b9ec59c0282f285133143be6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 10:32:17 -0400 Subject: [PATCH 59/67] Add cable paths API detail view for pass-through ports --- netbox/dcim/api/serializers.py | 47 +++++++++++++++++++++++++++++++++- netbox/dcim/api/views.py | 20 ++++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 11e286132..d599e7461 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -7,12 +7,13 @@ from rest_framework.validators import UniqueTogetherValidator from dcim.choices import * from dcim.constants import * from dcim.models import ( - Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, + Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) +from dcim.utils import decompile_path_node from extras.api.customfields import CustomFieldModelSerializer from extras.api.serializers import TaggedObjectSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer @@ -734,6 +735,50 @@ class TracedCableSerializer(serializers.ModelSerializer): ] +class CablePathSerializer(serializers.ModelSerializer): + origin_type = ContentTypeField(read_only=True) + origin = serializers.SerializerMethodField(read_only=True) + destination_type = ContentTypeField(read_only=True) + destination = serializers.SerializerMethodField(read_only=True) + path = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = CablePath + fields = [ + 'id', 'origin_type', 'origin', 'destination_type', 'destination', 'path', 'is_active', + ] + + @swagger_serializer_method(serializer_or_field=serializers.DictField) + def get_origin(self, obj): + """ + Return the appropriate serializer for the origin. + """ + serializer = get_serializer_for_model(obj.origin, prefix='Nested') + context = {'request': self.context['request']} + return serializer(obj.origin, context=context).data + + @swagger_serializer_method(serializer_or_field=serializers.DictField) + def get_destination(self, obj): + """ + Return the appropriate serializer for the destination, if any. + """ + if obj.destination_id is not None: + serializer = get_serializer_for_model(obj.destination, prefix='Nested') + context = {'request': self.context['request']} + return serializer(obj.destination, context=context).data + return None + + @swagger_serializer_method(serializer_or_field=serializers.ListField) + def get_path(self, obj): + ret = [] + for node in obj.path: + ct_id, object_id = decompile_path_node(node) + ct = ContentType.objects.get_for_id(ct_id) + # TODO: Return the object URL + ret.append(f'{ct.app_label}.{ct.model}:{object_id}') + return ret + + # # Interface connections # diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 14d6177bb..50dc82c9d 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -17,7 +17,7 @@ from rest_framework.viewsets import GenericViewSet, ViewSet from circuits.models import Circuit from dcim import filters from dcim.models import ( - Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, + Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, @@ -77,6 +77,20 @@ class PathEndpointMixin(object): return Response(path) +class PassThroughPortMixin(object): + + @action(detail=True, url_path='paths') + def paths(self, request, pk): + """ + Return all CablePaths which traverse a given pass-through port. + """ + obj = get_object_or_404(self.queryset, pk=pk) + cablepaths = CablePath.objects.filter(path__contains=obj).prefetch_related('origin', 'destination') + serializer = serializers.CablePathSerializer(cablepaths, context={'request': request}, many=True) + + return Response(serializer.data) + + # # Regions # @@ -503,13 +517,13 @@ class InterfaceViewSet(PathEndpointMixin, ModelViewSet): filterset_class = filters.InterfaceFilterSet -class FrontPortViewSet(ModelViewSet): +class FrontPortViewSet(PassThroughPortMixin, ModelViewSet): queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') serializer_class = serializers.FrontPortSerializer filterset_class = filters.FrontPortFilterSet -class RearPortViewSet(ModelViewSet): +class RearPortViewSet(PassThroughPortMixin, ModelViewSet): queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') serializer_class = serializers.RearPortSerializer filterset_class = filters.RearPortFilterSet From 55268c90c8173088607b62bdf1d08935296c4e04 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 11:15:09 -0400 Subject: [PATCH 60/67] Replace connection_status with connected_endpoint_reachable on InterfaceConnectionSerializer --- netbox/dcim/api/serializers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d599e7461..f9e17b12c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -786,17 +786,23 @@ class CablePathSerializer(serializers.ModelSerializer): class InterfaceConnectionSerializer(ValidatedModelSerializer): interface_a = serializers.SerializerMethodField() interface_b = NestedInterfaceSerializer(source='connected_endpoint') - # connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) + connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True) class Meta: model = Interface - fields = ['interface_a', 'interface_b'] + fields = ['interface_a', 'interface_b', 'connected_endpoint_reachable'] @swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer) def get_interface_a(self, obj): context = {'request': self.context['request']} return NestedInterfaceSerializer(instance=obj, context=context).data + @swagger_serializer_method(serializer_or_field=serializers.BooleanField) + def get_connected_endpoint_reachable(self, obj): + if obj._path is not None: + return obj._path.is_active + return None + # # Virtual chassis From ae1ceb26b92809176a9cd3be6ea704a61eed15b4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 11:23:24 -0400 Subject: [PATCH 61/67] Standardize cable/connection field ordering --- netbox/circuits/api/serializers.py | 4 ++-- netbox/dcim/api/serializers.py | 31 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 9bc95f065..ad5e609e4 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -77,6 +77,6 @@ class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpoint model = CircuitTermination fields = [ 'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', - 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', - 'cable_peer', 'cable_peer_type', + 'description', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', + 'connected_endpoint_reachable' ] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index f9e17b12c..d6da5a5e3 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -487,8 +487,8 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial class Meta: model = ConsoleServerPort fields = [ - 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', - 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', 'cable_peer_type', 'tags', + 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type', + 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', ] @@ -505,8 +505,8 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, class Meta: model = ConsolePort fields = [ - 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', - 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', 'cable_peer_type', 'tags', + 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type', + 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', ] @@ -533,9 +533,9 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, class Meta: model = PowerOutlet fields = [ - 'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', - 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', - 'cable_peer_type', 'tags', + 'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', + 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', + 'connected_endpoint_reachable', 'tags', ] @@ -552,9 +552,9 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co class Meta: model = PowerPort fields = [ - 'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', - 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', - 'cable_peer_type', 'tags', + 'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', + 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', + 'connected_endpoint_reachable', 'tags', ] @@ -578,8 +578,9 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co model = Interface fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', - 'description', 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', - 'cable_peer', 'cable_peer_type', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', + 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'cable', 'cable_peer', 'cable_peer_type', + 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', + 'count_ipaddresses', ] # TODO: This validation should be handled by Interface.clean() @@ -872,7 +873,7 @@ class PowerFeedSerializer( model = PowerFeed fields = [ 'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', - 'max_utilization', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'connected_endpoint_type', 'connected_endpoint', 'connected_endpoint_reachable', 'cable', 'cable_peer', - 'cable_peer_type', + 'max_utilization', 'comments', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', + 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', ] From 29eebf9fbe43321a43597fff1f2d99cf6766ed53 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 11:26:02 -0400 Subject: [PATCH 62/67] Update REST API changes --- docs/release-notes/version-2.10.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index fa3f3aea8..33c3bd1b6 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -68,27 +68,32 @@ All end-to-end cable paths are now cached using the new CablePath model. This al * dcim.ConsolePort: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` + * Removed `connection_status` from nested serializer * dcim.ConsoleServerPort: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` + * Removed `connection_status` from nested serializer * dcim.FrontPort: - * Removed the `/trace/` endpoint + * Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths * Added `cable_peer` and `cable_peer_type` * dcim.Interface: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` + * Removed `connection_status` from nested serializer * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning * dcim.PowerFeed: Add fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, `cable_peer`, and `cable_peer_type` * dcim.PowerOutlet: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` + * Removed `connection_status` from nested serializer * dcim.PowerPanel: Added `custom_fields` * dcim.PowerPort * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` + * Removed `connection_status` from nested serializer * dcim.RackReservation: Added `custom_fields` * dcim.RearPort: - * Removed the `/trace/` endpoint + * Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths * Added `cable_peer` and `cable_peer_type` * dcim.VirtualChassis: Added `custom_fields` * extras.ExportTemplate: The `template_language` field has been removed From 0c5efa243defd2695c6b2e884bbe4d48c3fa69fb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 13:45:47 -0400 Subject: [PATCH 63/67] Handle traces which split at a RearPort --- netbox/dcim/exceptions.py | 9 ---- netbox/dcim/models/device_components.py | 8 ++-- netbox/dcim/tests/test_cablepaths.py | 57 +++++++++++++++++++++++++ netbox/dcim/utils.py | 5 +-- netbox/templates/dcim/cable_trace.html | 32 ++++++++------ 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/netbox/dcim/exceptions.py b/netbox/dcim/exceptions.py index 18e42318b..e788c9b5f 100644 --- a/netbox/dcim/exceptions.py +++ b/netbox/dcim/exceptions.py @@ -3,12 +3,3 @@ 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/device_components.py b/netbox/dcim/models/device_components.py index 72bd453c5..2ea9b88b6 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -172,9 +172,11 @@ class PathEndpoint(models.Model): 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.path)} elements in path" + path = [self, *[path_node_to_object(obj) for obj in self._path.path]] + while (len(path) + 1) % 3: + # Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort) + path.append(None) + path.append(self._path.destination) # Return the path as a list of three-tuples (A termination, cable, B termination) return list(zip(*[iter(path)] * 3)) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index cfe63929d..95a8e0d30 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -721,6 +721,63 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) + def test_206_unidirectional_split_paths(self): + """ + [IF1] --C1-- [RP1] [FP1:1] --C2-- [IF2] + [FP1:2] --C3-- [IF3] + """ + self.interface1.refresh_from_db() + self.interface2.refresh_from_db() + self.interface3.refresh_from_db() + + # Create cables 1 + cable1 = Cable(termination_a=self.interface1, termination_b=self.rear_port1) + cable1.save() + self.assertPathExists( + origin=self.interface1, + destination=None, + path=(cable1, self.rear_port1), + is_active=False + ) + self.assertEqual(CablePath.objects.count(), 1) + + # Create cables 2-3 + cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_1) + cable2.save() + cable3 = Cable(termination_a=self.interface3, termination_b=self.front_port1_2) + cable3.save() + self.assertPathExists( + origin=self.interface2, + destination=self.interface1, + path=(cable2, self.front_port1_1, self.rear_port1, cable1), + is_active=True + ) + self.assertPathExists( + origin=self.interface3, + destination=self.interface1, + path=(cable3, self.front_port1_2, self.rear_port1, cable1), + is_active=True + ) + self.assertEqual(CablePath.objects.count(), 3) + + # Delete cable 1 + cable1.delete() + + # Check that the partial path was deleted and the two complete paths are now partial + self.assertPathExists( + origin=self.interface2, + destination=None, + path=(cable2, self.front_port1_1, self.rear_port1), + is_active=False + ) + self.assertPathExists( + origin=self.interface3, + destination=None, + path=(cable3, self.front_port1_2, self.rear_port1), + is_active=False + ) + self.assertEqual(CablePath.objects.count(), 2) + def test_301_create_path_via_existing_cable(self): """ [IF1] --C1-- [FP5] [RP5] --C2-- [RP6] [FP6] --C3-- [IF2] diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 52b0a4232..b82dd58d2 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -1,7 +1,6 @@ from django.contrib.contenttypes.models import ContentType from .choices import CableStatusChoices -from .exceptions import CableTraceSplit def compile_path_node(ct_id, object_id): @@ -69,8 +68,8 @@ def trace_path(node): node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position) path.append(object_to_path_node(node)) else: - # No position indicated: path has split (probably invalid?) - raise CableTraceSplit(peer_termination) + # No position indicated: path has split, so we stop at the RearPort + break # Anything else marks the end of the path else: diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 7df39caca..a328f2052 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -19,16 +19,17 @@ {% elif near_end.circuit %} {% include 'dcim/trace/circuit.html' with circuit=near_end.circuit %} {% include 'dcim/trace/termination.html' with termination=near_end %} + {% else %} +

Split Paths!

+ {# TODO: Present the user with successive paths to choose from #} {% endif %} {# Cable #} -
- {% if cable %} + {% if cable %} +
{% include 'dcim/trace/cable.html' %} - {% else %} -

No cable

- {% endif %} -
+
+ {% endif %} {# Far end #} {% if far_end.device %} @@ -45,15 +46,18 @@ {% endif %} {% endif %} + {% if forloop.last and far_end %} +
+
+

Trace completed!

+ {% if total_length %} +
Total length: {{ total_length|floatformat:"-2" }} Meters
+ {% endif %} +
+
+ {% endif %} + {% endfor %} -
-
-

Trace completed!

- {% if total_length %} -
Total length: {{ total_length|floatformat:"-2" }} Meters
- {% endif %} -
-
From 0e41bc48b7a9a248862c3bbda0ba99f11b8334bd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 13:55:29 -0400 Subject: [PATCH 64/67] Add /trace API endpoints for CircuitTermination and PowerFeed --- docs/release-notes/version-2.10.md | 5 ++++- netbox/circuits/api/views.py | 3 ++- netbox/dcim/api/views.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 33c3bd1b6..0e636fc45 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -62,6 +62,7 @@ All end-to-end cable paths are now cached using the new CablePath model. This al * Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints (bulk update and delete) * circuits.CircuitTermination: + * Added the `/trace/` endpoint * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` * dcim.Cable: Added `custom_fields` @@ -81,7 +82,9 @@ All end-to-end cable paths are now cached using the new CablePath model. This al * Added `cable_peer` and `cable_peer_type` * Removed `connection_status` from nested serializer * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning -* dcim.PowerFeed: Add fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, `cable_peer`, and `cable_peer_type` +* dcim.PowerFeed: + * Added the `/trace/` endpoint + * Added fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, `cable_peer`, and `cable_peer_type` * dcim.PowerOutlet: * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) * Added `cable_peer` and `cable_peer_type` diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 7b147412e..516831983 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -3,6 +3,7 @@ from rest_framework.routers import APIRootView from circuits import filters from circuits.models import Provider, CircuitTermination, CircuitType, Circuit +from dcim.api.views import PathEndpointMixin from extras.api.views import CustomFieldModelViewSet from utilities.api import ModelViewSet from . import serializers @@ -57,7 +58,7 @@ class CircuitViewSet(CustomFieldModelViewSet): # Circuit Terminations # -class CircuitTerminationViewSet(ModelViewSet): +class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet): queryset = CircuitTermination.objects.prefetch_related( 'circuit', 'site', '_path__destination', 'cable' ) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 50dc82c9d..c45879dbe 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -614,7 +614,7 @@ class PowerPanelViewSet(ModelViewSet): # Power feeds # -class PowerFeedViewSet(CustomFieldModelViewSet): +class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet): queryset = PowerFeed.objects.prefetch_related( 'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags' ) From 75ddc6346637f335001fbecded8f7baf41d95d6a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 14:01:47 -0400 Subject: [PATCH 65/67] Handle split paths --- netbox/dcim/api/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index c45879dbe..b14c67e65 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -58,6 +58,9 @@ class PathEndpointMixin(object): path = [] for near_end, cable, far_end in obj.trace(): + if near_end is None: + # Split paths + break # Serialize each object serializer_a = get_serializer_for_model(near_end, prefix='Nested') From a716ca705c4f4911718d50af6b7953bf24631439 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 14:55:13 -0400 Subject: [PATCH 66/67] Rewrite cablepath tests to create components within each test --- netbox/dcim/tests/test_cablepaths.py | 700 ++++++++++++++------------- 1 file changed, 363 insertions(+), 337 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 95a8e0d30..5699b3b88 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -20,108 +20,18 @@ class CablePathTestCase(TestCase): def setUpTestData(cls): # Create a single device that will hold all components - site = Site.objects.create(name='Site', slug='site') + cls.site = Site.objects.create(name='Site', slug='site') + manufacturer = Manufacturer.objects.create(name='Generic', slug='generic') device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Test Device') device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') - device = Device.objects.create(site=site, device_type=device_type, device_role=device_role, name='Test Device') + cls.device = Device.objects.create(site=cls.site, device_type=device_type, device_role=device_role, name='Test Device') - # Create console/power components for testing - cls.consoleport1 = ConsolePort.objects.create(device=device, name='Console Port 1') - cls.consoleserverport1 = ConsoleServerPort.objects.create(device=device, name='Console Server Port 1') - cls.powerport1 = PowerPort.objects.create(device=device, name='Power Port 1') - cls.poweroutlet1 = PowerPort.objects.create(device=device, name='Power Outlet 1') + cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel') - # Create 4 interfaces for testing - cls.interface1 = Interface(device=device, name=f'Interface 1') - cls.interface2 = Interface(device=device, name=f'Interface 2') - cls.interface3 = Interface(device=device, name=f'Interface 3') - cls.interface4 = Interface(device=device, name=f'Interface 4') - Interface.objects.bulk_create([ - cls.interface1, - cls.interface2, - cls.interface3, - cls.interface4 - ]) - - # Create four RearPorts with four positions each, and two with only one position - cls.rear_port1 = RearPort(device=device, name=f'RP1', positions=4) - cls.rear_port2 = RearPort(device=device, name=f'RP2', positions=4) - cls.rear_port3 = RearPort(device=device, name=f'RP3', positions=4) - cls.rear_port4 = RearPort(device=device, name=f'RP4', positions=4) - cls.rear_port5 = RearPort(device=device, name=f'RP5', positions=1) - cls.rear_port6 = RearPort(device=device, name=f'RP6', positions=1) - RearPort.objects.bulk_create([ - cls.rear_port1, - cls.rear_port2, - cls.rear_port3, - cls.rear_port4, - cls.rear_port5, - cls.rear_port6 - ]) - - # Create FrontPorts to match RearPorts (4x4 + 2x1) - cls.front_port1_1 = FrontPort(device=device, name=f'FP1:1', rear_port=cls.rear_port1, rear_port_position=1) - cls.front_port1_2 = FrontPort(device=device, name=f'FP1:2', rear_port=cls.rear_port1, rear_port_position=2) - cls.front_port1_3 = FrontPort(device=device, name=f'FP1:3', rear_port=cls.rear_port1, rear_port_position=3) - cls.front_port1_4 = FrontPort(device=device, name=f'FP1:4', rear_port=cls.rear_port1, rear_port_position=4) - cls.front_port2_1 = FrontPort(device=device, name=f'FP2:1', rear_port=cls.rear_port2, rear_port_position=1) - cls.front_port2_2 = FrontPort(device=device, name=f'FP2:2', rear_port=cls.rear_port2, rear_port_position=2) - cls.front_port2_3 = FrontPort(device=device, name=f'FP2:3', rear_port=cls.rear_port2, rear_port_position=3) - cls.front_port2_4 = FrontPort(device=device, name=f'FP2:4', rear_port=cls.rear_port2, rear_port_position=4) - cls.front_port3_1 = FrontPort(device=device, name=f'FP3:1', rear_port=cls.rear_port3, rear_port_position=1) - cls.front_port3_2 = FrontPort(device=device, name=f'FP3:2', rear_port=cls.rear_port3, rear_port_position=2) - cls.front_port3_3 = FrontPort(device=device, name=f'FP3:3', rear_port=cls.rear_port3, rear_port_position=3) - cls.front_port3_4 = FrontPort(device=device, name=f'FP3:4', rear_port=cls.rear_port3, rear_port_position=4) - cls.front_port4_1 = FrontPort(device=device, name=f'FP4:1', rear_port=cls.rear_port4, rear_port_position=1) - cls.front_port4_2 = FrontPort(device=device, name=f'FP4:2', rear_port=cls.rear_port4, rear_port_position=2) - cls.front_port4_3 = FrontPort(device=device, name=f'FP4:3', rear_port=cls.rear_port4, rear_port_position=3) - cls.front_port4_4 = FrontPort(device=device, name=f'FP4:4', rear_port=cls.rear_port4, rear_port_position=4) - cls.front_port5_1 = FrontPort(device=device, name=f'FP5:1', rear_port=cls.rear_port5, rear_port_position=1) - cls.front_port6_1 = FrontPort(device=device, name=f'FP6:1', rear_port=cls.rear_port6, rear_port_position=1) - FrontPort.objects.bulk_create([ - cls.front_port1_1, - cls.front_port1_2, - cls.front_port1_3, - cls.front_port1_4, - cls.front_port2_1, - cls.front_port2_2, - cls.front_port2_3, - cls.front_port2_4, - cls.front_port3_1, - cls.front_port3_2, - cls.front_port3_3, - cls.front_port3_4, - cls.front_port4_1, - cls.front_port4_2, - cls.front_port4_3, - cls.front_port4_4, - cls.front_port5_1, - cls.front_port6_1, - ]) - - # Create a PowerFeed for testing - powerpanel = PowerPanel.objects.create(site=site, name='Power Panel') - cls.powerfeed1 = PowerFeed.objects.create(power_panel=powerpanel, name='Power Feed 1') - - # Create four CircuitTerminations for testing provider = Provider.objects.create(name='Provider', slug='provider') circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type') - circuits = [ - Circuit(provider=provider, type=circuit_type, cid='Circuit 1'), - Circuit(provider=provider, type=circuit_type, cid='Circuit 2'), - ] - Circuit.objects.bulk_create(circuits) - cls.circuittermination1_A = CircuitTermination(circuit=circuits[0], site=site, term_side='A', port_speed=1000) - cls.circuittermination1_Z = CircuitTermination(circuit=circuits[0], site=site, term_side='Z', port_speed=1000) - cls.circuittermination2_A = CircuitTermination(circuit=circuits[1], site=site, term_side='A', port_speed=1000) - cls.circuittermination2_Z = CircuitTermination(circuit=circuits[1], site=site, term_side='Z', port_speed=1000) - CircuitTermination.objects.bulk_create([ - cls.circuittermination1_A, - cls.circuittermination1_Z, - cls.circuittermination2_A, - cls.circuittermination2_Z, - ]) + cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1') def assertPathExists(self, origin, destination, path=None, is_active=None, msg=None): """ @@ -187,26 +97,29 @@ class CablePathTestCase(TestCase): """ [IF1] --C1-- [IF2] """ + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + # Create cable 1 - cable1 = Cable(termination_a=self.interface1, termination_b=self.interface2) + cable1 = Cable(termination_a=interface1, termination_b=interface2) cable1.save() path1 = self.assertPathExists( - origin=self.interface1, - destination=self.interface2, + origin=interface1, + destination=interface2, path=(cable1,), is_active=True ) path2 = self.assertPathExists( - origin=self.interface2, - destination=self.interface1, + origin=interface2, + destination=interface1, path=(cable1,), is_active=True ) self.assertEqual(CablePath.objects.count(), 2) - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() - self.assertPathIsSet(self.interface1, path1) - self.assertPathIsSet(self.interface2, path2) + interface1.refresh_from_db() + interface2.refresh_from_db() + self.assertPathIsSet(interface1, path1) + self.assertPathIsSet(interface2, path2) # Delete cable 1 cable1.delete() @@ -218,26 +131,29 @@ class CablePathTestCase(TestCase): """ [CP1] --C1-- [CSP1] """ + consoleport1 = ConsolePort.objects.create(device=self.device, name='Console Port 1') + consoleserverport1 = ConsoleServerPort.objects.create(device=self.device, name='Console Server Port 1') + # Create cable 1 - cable1 = Cable(termination_a=self.consoleport1, termination_b=self.consoleserverport1) + cable1 = Cable(termination_a=consoleport1, termination_b=consoleserverport1) cable1.save() path1 = self.assertPathExists( - origin=self.consoleport1, - destination=self.consoleserverport1, + origin=consoleport1, + destination=consoleserverport1, path=(cable1,), is_active=True ) path2 = self.assertPathExists( - origin=self.consoleserverport1, - destination=self.consoleport1, + origin=consoleserverport1, + destination=consoleport1, path=(cable1,), is_active=True ) self.assertEqual(CablePath.objects.count(), 2) - self.consoleport1.refresh_from_db() - self.consoleserverport1.refresh_from_db() - self.assertPathIsSet(self.consoleport1, path1) - self.assertPathIsSet(self.consoleserverport1, path2) + consoleport1.refresh_from_db() + consoleserverport1.refresh_from_db() + self.assertPathIsSet(consoleport1, path1) + self.assertPathIsSet(consoleserverport1, path2) # Delete cable 1 cable1.delete() @@ -249,26 +165,29 @@ class CablePathTestCase(TestCase): """ [PP1] --C1-- [PO1] """ + powerport1 = PowerPort.objects.create(device=self.device, name='Power Port 1') + poweroutlet1 = PowerOutlet.objects.create(device=self.device, name='Power Outlet 1') + # Create cable 1 - cable1 = Cable(termination_a=self.powerport1, termination_b=self.poweroutlet1) + cable1 = Cable(termination_a=powerport1, termination_b=poweroutlet1) cable1.save() path1 = self.assertPathExists( - origin=self.powerport1, - destination=self.poweroutlet1, + origin=powerport1, + destination=poweroutlet1, path=(cable1,), is_active=True ) path2 = self.assertPathExists( - origin=self.poweroutlet1, - destination=self.powerport1, + origin=poweroutlet1, + destination=powerport1, path=(cable1,), is_active=True ) self.assertEqual(CablePath.objects.count(), 2) - self.powerport1.refresh_from_db() - self.poweroutlet1.refresh_from_db() - self.assertPathIsSet(self.powerport1, path1) - self.assertPathIsSet(self.poweroutlet1, path2) + powerport1.refresh_from_db() + poweroutlet1.refresh_from_db() + self.assertPathIsSet(powerport1, path1) + self.assertPathIsSet(poweroutlet1, path2) # Delete cable 1 cable1.delete() @@ -280,26 +199,29 @@ class CablePathTestCase(TestCase): """ [PP1] --C1-- [PF1] """ + powerport1 = PowerPort.objects.create(device=self.device, name='Power Port 1') + powerfeed1 = PowerFeed.objects.create(power_panel=self.powerpanel, name='Power Feed 1') + # Create cable 1 - cable1 = Cable(termination_a=self.powerport1, termination_b=self.powerfeed1) + cable1 = Cable(termination_a=powerport1, termination_b=powerfeed1) cable1.save() path1 = self.assertPathExists( - origin=self.powerport1, - destination=self.powerfeed1, + origin=powerport1, + destination=powerfeed1, path=(cable1,), is_active=True ) path2 = self.assertPathExists( - origin=self.powerfeed1, - destination=self.powerport1, + origin=powerfeed1, + destination=powerport1, path=(cable1,), is_active=True ) self.assertEqual(CablePath.objects.count(), 2) - self.powerport1.refresh_from_db() - self.powerfeed1.refresh_from_db() - self.assertPathIsSet(self.powerport1, path1) - self.assertPathIsSet(self.powerfeed1, path2) + powerport1.refresh_from_db() + powerfeed1.refresh_from_db() + self.assertPathIsSet(powerport1, path1) + self.assertPathIsSet(powerfeed1, path2) # Delete cable 1 cable1.delete() @@ -309,28 +231,33 @@ class CablePathTestCase(TestCase): def test_105_interface_to_circuittermination(self): """ - [PP1] --C1-- [CT1A] + [IF1] --C1-- [CT1A] """ + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + circuittermination1 = CircuitTermination.objects.create( + circuit=self.circuit, site=self.site, term_side='A', port_speed=1000 + ) + # Create cable 1 - cable1 = Cable(termination_a=self.interface1, termination_b=self.circuittermination1_A) + cable1 = Cable(termination_a=interface1, termination_b=circuittermination1) cable1.save() path1 = self.assertPathExists( - origin=self.interface1, - destination=self.circuittermination1_A, + origin=interface1, + destination=circuittermination1, path=(cable1,), is_active=True ) path2 = self.assertPathExists( - origin=self.circuittermination1_A, - destination=self.interface1, + origin=circuittermination1, + destination=interface1, path=(cable1,), is_active=True ) self.assertEqual(CablePath.objects.count(), 2) - self.interface1.refresh_from_db() - self.circuittermination1_A.refresh_from_db() - self.assertPathIsSet(self.interface1, path1) - self.assertPathIsSet(self.circuittermination1_A, path2) + interface1.refresh_from_db() + circuittermination1.refresh_from_db() + self.assertPathIsSet(interface1, path1) + self.assertPathIsSet(circuittermination1, path2) # Delete cable 1 cable1.delete() @@ -340,35 +267,39 @@ class CablePathTestCase(TestCase): def test_201_single_path_via_pass_through(self): """ - [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] + [IF1] --C1-- [FP1] [RP1] --C2-- [IF2] """ - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) + frontport1 = FrontPort.objects.create( + device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 + ) # Create cable 1 - cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port5_1) + cable1 = Cable(termination_a=interface1, termination_b=frontport1) cable1.save() self.assertPathExists( - origin=self.interface1, + origin=interface1, destination=None, - path=(cable1, self.front_port5_1, self.rear_port5), + path=(cable1, frontport1, rearport1), is_active=False ) self.assertEqual(CablePath.objects.count(), 1) # Create cable 2 - cable2 = Cable(termination_a=self.rear_port5, termination_b=self.interface2) + cable2 = Cable(termination_a=rearport1, termination_b=interface2) cable2.save() self.assertPathExists( - origin=self.interface1, - destination=self.interface2, - path=(cable1, self.front_port5_1, self.rear_port5, cable2), + origin=interface1, + destination=interface2, + path=(cable1, frontport1, rearport1, cable2), is_active=True ) self.assertPathExists( - origin=self.interface2, - destination=self.interface1, - path=(cable2, self.rear_port5, self.front_port5_1, cable1), + origin=interface2, + destination=interface1, + path=(cable2, rearport1, frontport1, cable1), is_active=True ) self.assertEqual(CablePath.objects.count(), 2) @@ -376,100 +307,114 @@ class CablePathTestCase(TestCase): # Delete cable 2 cable2.delete() path1 = self.assertPathExists( - origin=self.interface1, + origin=interface1, destination=None, - path=(cable1, self.front_port5_1, self.rear_port5), + path=(cable1, frontport1, rearport1), is_active=False ) self.assertEqual(CablePath.objects.count(), 1) - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() - self.assertPathIsSet(self.interface1, path1) - self.assertPathIsNotSet(self.interface2) + interface1.refresh_from_db() + interface2.refresh_from_db() + self.assertPathIsSet(interface1, path1) + self.assertPathIsNotSet(interface2) def test_202_multiple_paths_via_pass_through(self): """ [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [IF3] [IF2] --C2-- [FP1:2] [FP2:2] --C5-- [IF4] """ - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() - self.interface3.refresh_from_db() - self.interface4.refresh_from_db() + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + interface3 = Interface.objects.create(device=self.device, name='Interface 3') + interface4 = Interface.objects.create(device=self.device, name='Interface 4') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) + frontport1_1 = FrontPort.objects.create( + device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 + ) + frontport1_2 = FrontPort.objects.create( + device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 + ) + frontport2_1 = FrontPort.objects.create( + device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 + ) + frontport2_2 = FrontPort.objects.create( + device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 + ) # Create cables 1-2 - cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) + cable1 = Cable(termination_a=interface1, termination_b=frontport1_1) cable1.save() - cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_2) + cable2 = Cable(termination_a=interface2, termination_b=frontport1_2) cable2.save() self.assertPathExists( - origin=self.interface1, + origin=interface1, destination=None, - path=(cable1, self.front_port1_1, self.rear_port1), + path=(cable1, frontport1_1, rearport1), is_active=False ) self.assertPathExists( - origin=self.interface2, + origin=interface2, destination=None, - path=(cable2, self.front_port1_2, self.rear_port1), + path=(cable2, frontport1_2, rearport1), is_active=False ) self.assertEqual(CablePath.objects.count(), 2) # Create cable 3 - cable3 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2) + cable3 = Cable(termination_a=rearport1, termination_b=rearport2) cable3.save() self.assertPathExists( - origin=self.interface1, + origin=interface1, destination=None, - path=(cable1, self.front_port1_1, self.rear_port1, cable3, self.rear_port2, self.front_port2_1), + path=(cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1), is_active=False ) self.assertPathExists( - origin=self.interface2, + origin=interface2, destination=None, - path=(cable2, self.front_port1_2, self.rear_port1, cable3, self.rear_port2, self.front_port2_2), + path=(cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2), is_active=False ) self.assertEqual(CablePath.objects.count(), 2) # Create cables 4-5 - cable4 = Cable(termination_a=self.front_port2_1, termination_b=self.interface3) + cable4 = Cable(termination_a=frontport2_1, termination_b=interface3) cable4.save() - cable5 = Cable(termination_a=self.front_port2_2, termination_b=self.interface4) + cable5 = Cable(termination_a=frontport2_2, termination_b=interface4) cable5.save() path1 = self.assertPathExists( - origin=self.interface1, - destination=self.interface3, + origin=interface1, + destination=interface3, path=( - cable1, self.front_port1_1, self.rear_port1, cable3, self.rear_port2, self.front_port2_1, + cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1, cable4, ), is_active=True ) path2 = self.assertPathExists( - origin=self.interface2, - destination=self.interface4, + origin=interface2, + destination=interface4, path=( - cable2, self.front_port1_2, self.rear_port1, cable3, self.rear_port2, self.front_port2_2, + cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2, cable5, ), is_active=True ) path3 = self.assertPathExists( - origin=self.interface3, - destination=self.interface1, + origin=interface3, + destination=interface1, path=( - cable4, self.front_port2_1, self.rear_port2, cable3, self.rear_port1, self.front_port1_1, + cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1, cable1 ), is_active=True ) path4 = self.assertPathExists( - origin=self.interface4, - destination=self.interface2, + origin=interface4, + destination=interface2, path=( - cable5, self.front_port2_2, self.rear_port2, cable3, self.rear_port1, self.front_port1_2, + cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2, cable2 ), is_active=True @@ -482,82 +427,104 @@ class CablePathTestCase(TestCase): # Check for four partial paths; one from each interface self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4) self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0) - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() - self.interface3.refresh_from_db() - self.interface4.refresh_from_db() - self.assertPathIsSet(self.interface1, path1) - self.assertPathIsSet(self.interface2, path2) - self.assertPathIsSet(self.interface3, path3) - self.assertPathIsSet(self.interface4, path4) + interface1.refresh_from_db() + interface2.refresh_from_db() + interface3.refresh_from_db() + interface4.refresh_from_db() + self.assertPathIsSet(interface1, path1) + self.assertPathIsSet(interface2, path2) + self.assertPathIsSet(interface3, path3) + self.assertPathIsSet(interface4, path4) def test_203_multiple_paths_via_nested_pass_throughs(self): """ - [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP2:1] [RP2] --C4-- [RP3] [FP3:1] --C5-- [RP4] [FP4:1] --C6-- [IF3] - [IF2] --C2-- [FP1:2] [FP4:2] --C7-- [IF4] + [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP2] [RP2] --C4-- [RP3] [FP3] --C5-- [RP4] [FP4:1] --C6-- [IF3] + [IF2] --C2-- [FP1:2] [FP4:2] --C7-- [IF4] """ - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() - self.interface3.refresh_from_db() - self.interface4.refresh_from_db() + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + interface3 = Interface.objects.create(device=self.device, name='Interface 3') + interface4 = Interface.objects.create(device=self.device, name='Interface 4') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1) + rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=4) + frontport1_1 = FrontPort.objects.create( + device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 + ) + frontport1_2 = FrontPort.objects.create( + device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 + ) + frontport2 = FrontPort.objects.create( + device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 + ) + frontport3 = FrontPort.objects.create( + device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1 + ) + frontport4_1 = FrontPort.objects.create( + device=self.device, name='Front Port 4:1', rear_port=rearport4, rear_port_position=1 + ) + frontport4_2 = FrontPort.objects.create( + device=self.device, name='Front Port 4:2', rear_port=rearport4, rear_port_position=2 + ) # Create cables 1-2, 6-7 - cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) + cable1 = Cable(termination_a=interface1, termination_b=frontport1_1) cable1.save() - cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_2) + cable2 = Cable(termination_a=interface2, termination_b=frontport1_2) cable2.save() - cable6 = Cable(termination_a=self.interface3, termination_b=self.front_port4_1) + cable6 = Cable(termination_a=interface3, termination_b=frontport4_1) cable6.save() - cable7 = Cable(termination_a=self.interface4, termination_b=self.front_port4_2) + cable7 = Cable(termination_a=interface4, termination_b=frontport4_2) cable7.save() self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface # Create cables 3 and 5 - cable3 = Cable(termination_a=self.rear_port1, termination_b=self.front_port2_1) + cable3 = Cable(termination_a=rearport1, termination_b=frontport2) cable3.save() - cable5 = Cable(termination_a=self.rear_port4, termination_b=self.front_port3_1) + cable5 = Cable(termination_a=rearport4, termination_b=frontport3) cable5.save() self.assertEqual(CablePath.objects.count(), 4) # Four (longer) partial paths; one from each interface # Create cable 4 - cable4 = Cable(termination_a=self.rear_port2, termination_b=self.rear_port3) + cable4 = Cable(termination_a=rearport2, termination_b=rearport3) cable4.save() self.assertPathExists( - origin=self.interface1, - destination=self.interface3, + origin=interface1, + destination=interface3, path=( - cable1, self.front_port1_1, self.rear_port1, cable3, self.front_port2_1, self.rear_port2, - cable4, self.rear_port3, self.front_port3_1, cable5, self.rear_port4, self.front_port4_1, + cable1, frontport1_1, rearport1, cable3, frontport2, rearport2, + cable4, rearport3, frontport3, cable5, rearport4, frontport4_1, cable6 ), is_active=True ) self.assertPathExists( - origin=self.interface2, - destination=self.interface4, + origin=interface2, + destination=interface4, path=( - cable2, self.front_port1_2, self.rear_port1, cable3, self.front_port2_1, self.rear_port2, - cable4, self.rear_port3, self.front_port3_1, cable5, self.rear_port4, self.front_port4_2, + cable2, frontport1_2, rearport1, cable3, frontport2, rearport2, + cable4, rearport3, frontport3, cable5, rearport4, frontport4_2, cable7 ), is_active=True ) self.assertPathExists( - origin=self.interface3, - destination=self.interface1, + origin=interface3, + destination=interface1, path=( - cable6, self.front_port4_1, self.rear_port4, cable5, self.front_port3_1, self.rear_port3, - cable4, self.rear_port2, self.front_port2_1, cable3, self.rear_port1, self.front_port1_1, + cable6, frontport4_1, rearport4, cable5, frontport3, rearport3, + cable4, rearport2, frontport2, cable3, rearport1, frontport1_1, cable1 ), is_active=True ) self.assertPathExists( - origin=self.interface4, - destination=self.interface2, + origin=interface4, + destination=interface2, path=( - cable7, self.front_port4_2, self.rear_port4, cable5, self.front_port3_1, self.rear_port3, - cable4, self.rear_port2, self.front_port2_1, cable3, self.rear_port1, self.front_port1_2, + cable7, frontport4_2, rearport4, cable5, frontport3, rearport3, + cable4, rearport2, frontport2, cable3, rearport1, frontport1_2, cable2 ), is_active=True @@ -576,67 +543,95 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [FP3:1] [RP3] --C6-- [RP4] [FP4:1] --C7-- [IF3] [IF2] --C2-- [FP1:2] [FP2:1] --C5-- [FP3:1] [FP4:2] --C8-- [IF4] """ - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() - self.interface3.refresh_from_db() - self.interface4.refresh_from_db() + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + interface3 = Interface.objects.create(device=self.device, name='Interface 3') + interface4 = Interface.objects.create(device=self.device, name='Interface 4') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=4) + rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=4) + frontport1_1 = FrontPort.objects.create( + device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 + ) + frontport1_2 = FrontPort.objects.create( + device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 + ) + frontport2_1 = FrontPort.objects.create( + device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 + ) + frontport2_2 = FrontPort.objects.create( + device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 + ) + frontport3_1 = FrontPort.objects.create( + device=self.device, name='Front Port 3:1', rear_port=rearport3, rear_port_position=1 + ) + frontport3_2 = FrontPort.objects.create( + device=self.device, name='Front Port 3:2', rear_port=rearport3, rear_port_position=2 + ) + frontport4_1 = FrontPort.objects.create( + device=self.device, name='Front Port 4:1', rear_port=rearport4, rear_port_position=1 + ) + frontport4_2 = FrontPort.objects.create( + device=self.device, name='Front Port 4:2', rear_port=rearport4, rear_port_position=2 + ) # Create cables 1-3, 6-8 - cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) + cable1 = Cable(termination_a=interface1, termination_b=frontport1_1) cable1.save() - cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_2) + cable2 = Cable(termination_a=interface2, termination_b=frontport1_2) cable2.save() - cable3 = Cable(termination_a=self.rear_port1, termination_b=self.rear_port2) + cable3 = Cable(termination_a=rearport1, termination_b=rearport2) cable3.save() - cable6 = Cable(termination_a=self.rear_port3, termination_b=self.rear_port4) + cable6 = Cable(termination_a=rearport3, termination_b=rearport4) cable6.save() - cable7 = Cable(termination_a=self.interface3, termination_b=self.front_port4_1) + cable7 = Cable(termination_a=interface3, termination_b=frontport4_1) cable7.save() - cable8 = Cable(termination_a=self.interface4, termination_b=self.front_port4_2) + cable8 = Cable(termination_a=interface4, termination_b=frontport4_2) cable8.save() self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface # Create cables 4 and 5 - cable4 = Cable(termination_a=self.front_port2_1, termination_b=self.front_port3_1) + cable4 = Cable(termination_a=frontport2_1, termination_b=frontport3_1) cable4.save() - cable5 = Cable(termination_a=self.front_port2_2, termination_b=self.front_port3_2) + cable5 = Cable(termination_a=frontport2_2, termination_b=frontport3_2) cable5.save() self.assertPathExists( - origin=self.interface1, - destination=self.interface3, + origin=interface1, + destination=interface3, path=( - cable1, self.front_port1_1, self.rear_port1, cable3, self.rear_port2, self.front_port2_1, - cable4, self.front_port3_1, self.rear_port3, cable6, self.rear_port4, self.front_port4_1, + cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1, + cable4, frontport3_1, rearport3, cable6, rearport4, frontport4_1, cable7 ), is_active=True ) self.assertPathExists( - origin=self.interface2, - destination=self.interface4, + origin=interface2, + destination=interface4, path=( - cable2, self.front_port1_2, self.rear_port1, cable3, self.rear_port2, self.front_port2_2, - cable5, self.front_port3_2, self.rear_port3, cable6, self.rear_port4, self.front_port4_2, + cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2, + cable5, frontport3_2, rearport3, cable6, rearport4, frontport4_2, cable8 ), is_active=True ) self.assertPathExists( - origin=self.interface3, - destination=self.interface1, + origin=interface3, + destination=interface1, path=( - cable7, self.front_port4_1, self.rear_port4, cable6, self.rear_port3, self.front_port3_1, - cable4, self.front_port2_1, self.rear_port2, cable3, self.rear_port1, self.front_port1_1, + cable7, frontport4_1, rearport4, cable6, rearport3, frontport3_1, + cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1, cable1 ), is_active=True ) self.assertPathExists( - origin=self.interface4, - destination=self.interface2, + origin=interface4, + destination=interface2, path=( - cable8, self.front_port4_2, self.rear_port4, cable6, self.rear_port3, self.front_port3_2, - cable5, self.front_port2_2, self.rear_port2, cable3, self.rear_port1, self.front_port1_2, + cable8, frontport4_2, rearport4, cable6, rearport3, frontport3_2, + cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2, cable2 ), is_active=True @@ -652,63 +647,81 @@ class CablePathTestCase(TestCase): def test_205_multiple_paths_via_patched_pass_throughs(self): """ - [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP5] [RP5] --C4-- [RP2] [FP2:1] --C5-- [IF3] - [IF2] --C2-- [FP1:2] [FP2:2] --C6-- [IF4] + [IF1] --C1-- [FP1:1] [RP1] --C3-- [FP2] [RP2] --C4-- [RP3] [FP3:1] --C5-- [IF3] + [IF2] --C2-- [FP1:2] [FP3:2] --C6-- [IF4] """ - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() - self.interface3.refresh_from_db() - self.interface4.refresh_from_db() + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + interface3 = Interface.objects.create(device=self.device, name='Interface 3') + interface4 = Interface.objects.create(device=self.device, name='Interface 4') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 5', positions=1) + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) + frontport1_1 = FrontPort.objects.create( + device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 + ) + frontport1_2 = FrontPort.objects.create( + device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 + ) + frontport2 = FrontPort.objects.create( + device=self.device, name='Front Port 5', rear_port=rearport2, rear_port_position=1 + ) + frontport3_1 = FrontPort.objects.create( + device=self.device, name='Front Port 2:1', rear_port=rearport3, rear_port_position=1 + ) + frontport3_2 = FrontPort.objects.create( + device=self.device, name='Front Port 2:2', rear_port=rearport3, rear_port_position=2 + ) # Create cables 1-2, 5-6 - cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port1_1) # IF1 -> FP1:1 + cable1 = Cable(termination_a=interface1, termination_b=frontport1_1) # IF1 -> FP1:1 cable1.save() - cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_2) # IF2 -> FP1:2 + cable2 = Cable(termination_a=interface2, termination_b=frontport1_2) # IF2 -> FP1:2 cable2.save() - cable5 = Cable(termination_a=self.interface3, termination_b=self.front_port2_1) # IF3 -> FP2:1 + cable5 = Cable(termination_a=interface3, termination_b=frontport3_1) # IF3 -> FP3:1 cable5.save() - cable6 = Cable(termination_a=self.interface4, termination_b=self.front_port2_2) # IF4 -> FP2:2 + cable6 = Cable(termination_a=interface4, termination_b=frontport3_2) # IF4 -> FP3:2 cable6.save() self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface # Create cables 3-4 - cable3 = Cable(termination_a=self.rear_port1, termination_b=self.front_port5_1) # RP1 -> FP5 + cable3 = Cable(termination_a=rearport1, termination_b=frontport2) # RP1 -> FP2 cable3.save() - cable4 = Cable(termination_a=self.rear_port5, termination_b=self.rear_port2) # RP5 -> RP2 + cable4 = Cable(termination_a=rearport2, termination_b=rearport3) # RP2 -> RP3 cable4.save() self.assertPathExists( - origin=self.interface1, - destination=self.interface3, + origin=interface1, + destination=interface3, path=( - cable1, self.front_port1_1, self.rear_port1, cable3, self.front_port5_1, self.rear_port5, - cable4, self.rear_port2, self.front_port2_1, cable5 + cable1, frontport1_1, rearport1, cable3, frontport2, rearport2, + cable4, rearport3, frontport3_1, cable5 ), is_active=True ) self.assertPathExists( - origin=self.interface2, - destination=self.interface4, + origin=interface2, + destination=interface4, path=( - cable2, self.front_port1_2, self.rear_port1, cable3, self.front_port5_1, self.rear_port5, - cable4, self.rear_port2, self.front_port2_2, cable6 + cable2, frontport1_2, rearport1, cable3, frontport2, rearport2, + cable4, rearport3, frontport3_2, cable6 ), is_active=True ) self.assertPathExists( - origin=self.interface3, - destination=self.interface1, + origin=interface3, + destination=interface1, path=( - cable5, self.front_port2_1, self.rear_port2, cable4, self.rear_port5, self.front_port5_1, - cable3, self.rear_port1, self.front_port1_1, cable1 + cable5, frontport3_1, rearport3, cable4, rearport2, frontport2, + cable3, rearport1, frontport1_1, cable1 ), is_active=True ) self.assertPathExists( - origin=self.interface4, - destination=self.interface2, + origin=interface4, + destination=interface2, path=( - cable6, self.front_port2_2, self.rear_port2, cable4, self.rear_port5, self.front_port5_1, - cable3, self.rear_port1, self.front_port1_2, cable2 + cable6, frontport3_2, rearport3, cable4, rearport2, frontport2, + cable3, rearport1, frontport1_2, cable2 ), is_active=True ) @@ -726,36 +739,43 @@ class CablePathTestCase(TestCase): [IF1] --C1-- [RP1] [FP1:1] --C2-- [IF2] [FP1:2] --C3-- [IF3] """ - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() - self.interface3.refresh_from_db() + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + interface3 = Interface.objects.create(device=self.device, name='Interface 3') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) + frontport1_1 = FrontPort.objects.create( + device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 + ) + frontport1_2 = FrontPort.objects.create( + device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 + ) # Create cables 1 - cable1 = Cable(termination_a=self.interface1, termination_b=self.rear_port1) + cable1 = Cable(termination_a=interface1, termination_b=rearport1) cable1.save() self.assertPathExists( - origin=self.interface1, + origin=interface1, destination=None, - path=(cable1, self.rear_port1), + path=(cable1, rearport1), is_active=False ) self.assertEqual(CablePath.objects.count(), 1) # Create cables 2-3 - cable2 = Cable(termination_a=self.interface2, termination_b=self.front_port1_1) + cable2 = Cable(termination_a=interface2, termination_b=frontport1_1) cable2.save() - cable3 = Cable(termination_a=self.interface3, termination_b=self.front_port1_2) + cable3 = Cable(termination_a=interface3, termination_b=frontport1_2) cable3.save() self.assertPathExists( - origin=self.interface2, - destination=self.interface1, - path=(cable2, self.front_port1_1, self.rear_port1, cable1), + origin=interface2, + destination=interface1, + path=(cable2, frontport1_1, rearport1, cable1), is_active=True ) self.assertPathExists( - origin=self.interface3, - destination=self.interface1, - path=(cable3, self.front_port1_2, self.rear_port1, cable1), + origin=interface3, + destination=interface1, + path=(cable3, frontport1_2, rearport1, cable1), is_active=True ) self.assertEqual(CablePath.objects.count(), 3) @@ -765,76 +785,82 @@ class CablePathTestCase(TestCase): # Check that the partial path was deleted and the two complete paths are now partial self.assertPathExists( - origin=self.interface2, + origin=interface2, destination=None, - path=(cable2, self.front_port1_1, self.rear_port1), + path=(cable2, frontport1_1, rearport1), is_active=False ) self.assertPathExists( - origin=self.interface3, + origin=interface3, destination=None, - path=(cable3, self.front_port1_2, self.rear_port1), + path=(cable3, frontport1_2, rearport1), is_active=False ) self.assertEqual(CablePath.objects.count(), 2) def test_301_create_path_via_existing_cable(self): """ - [IF1] --C1-- [FP5] [RP5] --C2-- [RP6] [FP6] --C3-- [IF2] + [IF1] --C1-- [FP1] [RP2] --C2-- [RP2] [FP2] --C3-- [IF2] """ - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) + frontport1 = FrontPort.objects.create( + device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 + ) + frontport2 = FrontPort.objects.create( + device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 + ) # Create cable 2 - cable2 = Cable(termination_a=self.rear_port5, termination_b=self.rear_port6) + cable2 = Cable(termination_a=rearport1, termination_b=rearport2) cable2.save() self.assertEqual(CablePath.objects.count(), 0) # Create cable1 - cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port5_1) + cable1 = Cable(termination_a=interface1, termination_b=frontport1) cable1.save() self.assertPathExists( - origin=self.interface1, + origin=interface1, destination=None, - path=(cable1, self.front_port5_1, self.rear_port5, cable2, self.rear_port6, self.front_port6_1), + path=(cable1, frontport1, rearport1, cable2, rearport2, frontport2), is_active=False ) self.assertEqual(CablePath.objects.count(), 1) # Create cable 3 - cable3 = Cable(termination_a=self.front_port6_1, termination_b=self.interface2) + cable3 = Cable(termination_a=frontport2, termination_b=interface2) cable3.save() self.assertPathExists( - origin=self.interface1, - destination=self.interface2, - path=( - cable1, self.front_port5_1, self.rear_port5, cable2, self.rear_port6, self.front_port6_1, - cable3, - ), + origin=interface1, + destination=interface2, + path=(cable1, frontport1, rearport1, cable2, rearport2, frontport2, cable3), is_active=True ) self.assertPathExists( - origin=self.interface2, - destination=self.interface1, - path=( - cable3, self.front_port6_1, self.rear_port6, cable2, self.rear_port5, self.front_port5_1, - cable1, - ), + origin=interface2, + destination=interface1, + path=(cable3, frontport2, rearport2, cable2, rearport1, frontport1, cable1), is_active=True ) self.assertEqual(CablePath.objects.count(), 2) def test_302_update_path_on_cable_status_change(self): """ - [IF1] --C1-- [FP5] [RP5] --C2-- [IF2] + [IF1] --C1-- [FP1] [RP1] --C2-- [IF2] """ - self.interface1.refresh_from_db() - self.interface2.refresh_from_db() + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) + frontport1 = FrontPort.objects.create( + device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 + ) # Create cables 1 and 2 - cable1 = Cable(termination_a=self.interface1, termination_b=self.front_port5_1) + cable1 = Cable(termination_a=interface1, termination_b=frontport1) cable1.save() - cable2 = Cable(termination_a=self.rear_port5, termination_b=self.interface2) + cable2 = Cable(termination_a=rearport1, termination_b=interface2) cable2.save() self.assertEqual(CablePath.objects.filter(is_active=True).count(), 2) self.assertEqual(CablePath.objects.count(), 2) @@ -843,15 +869,15 @@ class CablePathTestCase(TestCase): cable2.status = CableStatusChoices.STATUS_PLANNED cable2.save() self.assertPathExists( - origin=self.interface1, - destination=self.interface2, - path=(cable1, self.front_port5_1, self.rear_port5, cable2), + origin=interface1, + destination=interface2, + path=(cable1, frontport1, rearport1, cable2), is_active=False ) self.assertPathExists( - origin=self.interface2, - destination=self.interface1, - path=(cable2, self.rear_port5, self.front_port5_1, cable1), + origin=interface2, + destination=interface1, + path=(cable2, rearport1, frontport1, cable1), is_active=False ) self.assertEqual(CablePath.objects.count(), 2) @@ -861,15 +887,15 @@ class CablePathTestCase(TestCase): cable2.status = CableStatusChoices.STATUS_CONNECTED cable2.save() self.assertPathExists( - origin=self.interface1, - destination=self.interface2, - path=(cable1, self.front_port5_1, self.rear_port5, cable2), + origin=interface1, + destination=interface2, + path=(cable1, frontport1, rearport1, cable2), is_active=True ) self.assertPathExists( - origin=self.interface2, - destination=self.interface1, - path=(cable2, self.rear_port5, self.front_port5_1, cable1), + origin=interface2, + destination=interface1, + path=(cable2, rearport1, frontport1, cable1), is_active=True ) self.assertEqual(CablePath.objects.count(), 2) From 44caa402d0342a0e631690d610b749468dadb5d3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Oct 2020 15:01:55 -0400 Subject: [PATCH 67/67] Delete obsolete LoopDetected exception --- netbox/dcim/exceptions.py | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 netbox/dcim/exceptions.py diff --git a/netbox/dcim/exceptions.py b/netbox/dcim/exceptions.py deleted file mode 100644 index e788c9b5f..000000000 --- a/netbox/dcim/exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ -class LoopDetected(Exception): - """ - A loop has been detected while tracing a cable path. - """ - pass