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 3acd0d4a1f..23b9b7fd53 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 @@ def get_prep_value(self, value): 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 0000000000..903a6feac5 --- /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 0000000000..4e36c31d07 --- /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 e50fa2eda2..fdd4d1bf57 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -8,6 +8,7 @@ __all__ = ( 'BaseInterface', 'Cable', + 'CablePath', 'CableTermination', 'ConsolePort', 'ConsolePortTemplate', diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 2cc4c8b604..12bb224fd9 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 @@ 'FrontPort', 'Interface', 'InventoryItem', + 'PathEndpoint', 'PowerOutlet', 'PowerPort', 'RearPort', @@ -250,12 +252,23 @@ def get_path_endpoints(self): 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 @@ def to_csv(self): # @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 @@ def to_csv(self): # @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 @@ def get_power_draw(self): # @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 Meta: @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 463b1a3e33..162dcb8313 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -14,6 +14,9 @@ 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 @@ __all__ = ( 'Cable', + 'CablePath', 'Device', 'DeviceRole', 'DeviceType', @@ -1154,6 +1158,44 @@ def get_compatible_types(self): 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 172c366b51..4e6cadb283 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 0000000000..2baa916220 --- /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 a317dc937d..706801dd1f 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 0000000000..efd3fcbee5 --- /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 | 201 ++++++++++++++++++++++----- 1 file changed, 167 insertions(+), 34 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index efd3fcbee5..401c38ca57 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -120,91 +120,224 @@ def test_01_interface_to_interface(self): # 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) + 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 2 - cable2 = Cable(termination_a=self.rear_ports[0], termination_b=self.interfaces[1]) - cable2.save() + # 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[1], - path=(cable1, self.front_ports[0], self.rear_ports[0], cable2) + 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=(cable2, self.rear_ports[0], self.front_ports[0], cable1) + 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(), 5) # Two complete + three partial paths + self.assertEqual(CablePath.objects.count(), 6) # Four complete + two partial paths - # Delete cable 1 - cable1.delete() + # 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.rear_ports[0], self.front_ports[0]) + path=(cable2, self.front_ports[1], self.rear_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): + # 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] --C2-- [RP2] [FP2:1] --C3-- [IF2] + [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [IF3] + [IF2] --C2-- [FP1:2] [FP2:2] --C5-- [IF4] """ - # Create cable 1 + # 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.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], + origin=self.interfaces[1], destination=None, - path=(cable1, self.front_ports[0], self.rear_ports[0], cable2, self.rear_ports[1], self.front_ports[4]) + path=(cable2, self.front_ports[1], self.rear_ports[0]) ) - self.assertEqual(CablePath.objects.count(), 1) + self.assertEqual(CablePath.objects.count(), 2) # Create cable 3 - cable3 = Cable(termination_a=self.front_ports[4], termination_b=self.interfaces[1]) + 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=( - cable1, self.front_ports[0], self.rear_ports[0], cable2, self.rear_ports[1], self.front_ports[4], - cable3, + 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=( - cable3, self.front_ports[4], self.rear_ports[1], cable2, self.rear_ports[0], self.front_ports[0], + 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.assertEqual(CablePath.objects.count(), 2) + 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() - # Delete cable 2 - cable2.delete() - self.assertEqual(CablePath.objects.count(), 2) # Two partial paths from IF1 and IF2 + # 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 2baa916220..75029cacc5 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 401c38ca57..767b34594a 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -45,17 +45,36 @@ def setUpTestData(cls): ] 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 @@ def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): # 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 767b34594a..3d4efae8e2 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -18,27 +18,7 @@ def setUpTestData(cls): 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 | 22 ++++++++----- 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, 51 insertions(+), 88 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 12bb224fd9..56e8f6fc41 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -252,15 +252,19 @@ def get_path_endpoints(self): 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 - ) +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' + ) + + class Meta: + abstract = True # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ce204bee01..58be5d213f 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -32,7 +32,7 @@ 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 @@ def get(self, request, pk): # 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 6fa5e8b912..dc0ff384c9 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 fca1fa5f46..0af64b4c14 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 0000000000..07d73a534f --- /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 706801dd1f..ae1363dbac 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 5800f4b48b..39af6828d4 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 b30fc8456f..4ec1b786eb 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 0000000000..e69de29bb2 diff --git a/netbox/dcim/management/commands/__init__.py b/netbox/dcim/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/netbox/dcim/management/commands/retrace_paths.py b/netbox/dcim/management/commands/retrace_paths.py new file mode 100644 index 0000000000..76c29e89c8 --- /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 408a53c3c7..686ab92191 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -4,7 +4,7 @@ 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 @@ def termination_z(self): 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 76c29e89c8..b60537b374 100644 --- a/netbox/dcim/management/commands/retrace_paths.py +++ b/netbox/dcim/management/commands/retrace_paths.py @@ -12,6 +12,7 @@ '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 f55d077a44..caa22e74ab 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -10,7 +10,7 @@ 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 @@ def clean(self): @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 b60537b374..a27833d625 100644 --- a/netbox/dcim/management/commands/retrace_paths.py +++ b/netbox/dcim/management/commands/retrace_paths.py @@ -4,7 +4,7 @@ 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 @@ 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) + 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 | 60 ++++++------------- 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, 62 insertions(+), 93 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 7256393212..cc8e6df1fd 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 @@ def get_connected_endpoint(self, obj): """ 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 a27833d625..d11a85417e 100644 --- a/netbox/dcim/management/commands/retrace_paths.py +++ b/netbox/dcim/management/commands/retrace_paths.py @@ -5,7 +5,7 @@ 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 @@ def handle(self, *model_names, **options): 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 56e8f6fc41..6bf2ac77af 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 4e6cadb283..46a2cf1d3b 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -6,14 +6,15 @@ 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 3d4efae8e2..68121e6716 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -123,69 +123,43 @@ def test_01_interface_to_interface(self): # 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]) - 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]) + path=(cable1, self.front_ports[16], self.rear_ports[4]) ) - self.assertEqual(CablePath.objects.count(), 2) + self.assertEqual(CablePath.objects.count(), 1) - # Create cable 3 - cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.interfaces[2]) - cable3.save() + # 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=self.interfaces[2], - path=(cable1, self.front_ports[0], self.rear_ports[0], cable3) + destination=self.interfaces[1], + path=(cable1, self.front_ports[16], self.rear_ports[4], cable2) ) 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) + path=(cable2, self.rear_ports[4], self.front_ports[16], 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 + self.assertEqual(CablePath.objects.count(), 2) - # 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]) - ) - self.assertPathExists( - origin=self.interfaces[1], - destination=None, - path=(cable2, self.front_ports[1], self.rear_ports[0]) + path=(cable1, self.front_ports[16], self.rear_ports[4]) ) - - # 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 75029cacc5..59ca59bfc4 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 58be5d213f..96e6615e8b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1018,7 +1018,7 @@ def get(self, request, pk): # 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 @@ def get(self, request, pk): 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 dc0ff384c9..912404be3f 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 0af64b4c14..b7a5c6b568 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 07d73a534f..1c25a0e284 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 ae1363dbac..1595511924 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 39af6828d4..b3e003e99f 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 4ec1b786eb..c65b685d77 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 cc8e6df1fd..8078f88190 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -30,7 +30,7 @@ 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 @@ def get_connected_endpoint(self, obj): 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 4e36c31d07..2cb8376b7f 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 162dcb8313..3b13b1f739 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -992,6 +992,8 @@ def from_db(cls, db, field_names, values): 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 46a2cf1d3b..0c5da61600 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -5,6 +5,7 @@ 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 59ca59bfc4..f97a1e8f00 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 68121e6716..529935eb3f 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -376,3 +376,43 @@ def test_05_interfaces_to_interfaces_via_patched_pass_throughs(self): # 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 3b13b1f739..0cb1ea9704 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -980,6 +980,9 @@ def __init__(self, *args, **kwargs): # 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 @@ def from_db(cls, db, field_names, values): 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 0c5da61600..9b0493e345 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 529935eb3f..362b3804b9 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2,6 +2,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 @@ -70,13 +71,14 @@ def setUpTestData(cls): ] 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 @@ def assertPathExists(self, origin, destination, path=None, msg=None): 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 @@ def test_01_interface_to_interface(self): 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 @@ def test_02_interface_to_interface_via_pass_through(self): 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 @@ def test_02_interface_to_interface_via_pass_through(self): 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 @@ def test_02_interface_to_interface_via_pass_through(self): 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 @@ def test_03_interfaces_to_interfaces_via_pass_through(self): 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 @@ def test_03_interfaces_to_interfaces_via_pass_through(self): 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 @@ def test_03_interfaces_to_interfaces_via_pass_through(self): 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 @@ def test_03_interfaces_to_interfaces_via_pass_through(self): 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 @@ def test_03_interfaces_to_interfaces_via_pass_through(self): 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 @@ def test_03_interfaces_to_interfaces_via_pass_through(self): 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 @@ def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): 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 @@ def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): 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 @@ def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): 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 @@ def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): 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 @@ def test_05_interfaces_to_interfaces_via_patched_pass_throughs(self): 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 @@ def test_05_interfaces_to_interfaces_via_patched_pass_throughs(self): 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 @@ def test_05_interfaces_to_interfaces_via_patched_pass_throughs(self): 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 @@ def test_05_interfaces_to_interfaces_via_patched_pass_throughs(self): 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 @@ def test_06_interface_to_interface_via_existing_cable(self): 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 @@ def test_06_interface_to_interface_via_existing_cable(self): 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 @@ def test_06_interface_to_interface_via_existing_cable(self): 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 f97a1e8f00..16d0753ba9 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 362b3804b9..a851a010b0 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -337,7 +337,81 @@ def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): 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 @@ def test_05_interfaces_to_interfaces_via_patched_pass_throughs(self): 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 @@ def test_06_interface_to_interface_via_existing_cable(self): ) 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 c55d099c91..83438a609d 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -561,628 +561,3 @@ def test_cable_cannot_terminate_to_a_wireless_interface(self): 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 | 377 ++++++++++++++------------- 1 file changed, 203 insertions(+), 174 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index a851a010b0..53ce82403f 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -19,44 +19,73 @@ def setUpTestData(cls): 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 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 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 four circuits with two terminations (A and Z) each (8 total) provider = Provider.objects.create(name='Provider', slug='provider') @@ -107,17 +136,17 @@ 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 = 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 @@ def test_02_interface_to_interface_via_pass_through(self): [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 @@ def test_02_interface_to_interface_via_pass_through(self): # 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 @@ def test_03_interfaces_to_interfaces_via_pass_through(self): [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 @@ def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): [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 @@ def test_05_interfaces_to_interfaces_via_multiple_pass_throughs(self): [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 @@ def test_06_interfaces_to_interfaces_via_patched_pass_throughs(self): [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 @@ def test_07_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 = 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 @@ def test_08_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 = 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 @@ def test_08_change_cable_status(self): 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 @@ def test_08_change_cable_status(self): 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 | 163 ++++++++++++++++++++++++--- 1 file changed, 147 insertions(+), 16 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 53ce82403f..18c15fd161 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -8,7 +8,14 @@ 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 @@ def setUpTestData(cls): 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 @@ def setUpTestData(cls): 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 @@ def assertPathExists(self, origin, destination, path=None, is_connected=None, ms 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 @@ def test_01_interface_to_interface(self): # 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 @@ def test_02_interface_to_interface_via_pass_through(self): ) 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 @@ def test_03_interfaces_to_interfaces_via_pass_through(self): 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 @@ def test_04_interfaces_to_interfaces_via_nested_pass_throughs(self): 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 @@ def test_05_interfaces_to_interfaces_via_multiple_pass_throughs(self): 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 @@ def test_06_interfaces_to_interfaces_via_patched_pass_throughs(self): 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 @@ def test_07_interface_to_interface_via_existing_cable(self): ) 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 512d7919cc..528301f8f4 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1453,377 +1453,6 @@ def setUpTestData(cls): ] -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 2cb8376b7f..f3448e7474 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 0cb1ea9704..52627dc7da 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 206dcc3058..ebc0d0ec1a 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 @@ def search(self, queryset, name, value): ).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 457483273d..c76bd3b876 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 @@ def search(self, queryset, name, value): ) -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 @@ def search(self, queryset, name, value): 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 0a2794f01f..c399e1a92c 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -1514,9 +1514,8 @@ def test_description(self): 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 @@ def test_description(self): 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 @@ def test_allocated_draw(self): 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 @@ def test_feed_leg(self): 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 @@ def test_name(self): 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 86ea55fa86..d757fd90df 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 @@ 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 0583d4e56d..edbdfb7be3 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -45,7 +45,7 @@ def get_view_name(self): # Mixins -class CableTraceMixin(object): +class PathEndpointMixin(object): @action(detail=True, url_path='trace') def trace(self, request, pk): @@ -57,7 +57,7 @@ def trace(self, request, pk): # 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 @@ def napalm(self, request, pk): # 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 6bf2ac77af..b714c662a6 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 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 @@ def get_cable_peer(self): 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 aa0453bafb..ba58cf67e1 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -207,7 +207,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 16d0753ba9..4ef902dc76 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -2,7 +2,6 @@ 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 96e6615e8b..ac30461b03 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1961,9 +1961,9 @@ def get(self, request, pk): }) -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 @@ def dispatch(self, request, *args, **kwargs): 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 @@ def get(self, request, pk): 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 df484609ac..2f54f94eea 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -51,57 +51,8 @@

No Cable


{% 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 ba58cf67e1..90f6b5ef24 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -383,6 +383,7 @@ 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 7af030a036..7ab08eae46 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -812,13 +812,15 @@ class Meta(BaseTable.Meta): # 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 Meta(BaseTable.Meta): 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 0000000000..fabf71798f --- /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 f3448e7474..0d2199e311 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 b714c662a6..65032f5290 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -126,38 +126,30 @@ def get_cable_peer(self): 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 52627dc7da..ea7332b3d0 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1204,6 +1204,13 @@ 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})" + 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 9b0493e345..13653c7d43 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 18c15fd161..52b37e3bd6 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -132,6 +132,8 @@ def assertPathExists(self, origin, destination, path=None, is_connected=None, ms :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 @@ def assertPathExists(self, origin, destination, path=None, is_connected=None, ms 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 @@ def test_101_interface_to_interface(self): # 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 @@ def test_101_interface_to_interface(self): # 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 @@ def test_103_consoleport_to_consoleserverport(self): # 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 @@ def test_104_powerport_to_poweroutlet(self): # 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 @@ def test_105_powerport_to_powerfeed(self): # 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 @@ def test_201_single_path_via_pass_through(self): """ [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 @@ def test_201_single_path_via_pass_through(self): # 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 @@ def test_202_multiple_paths_via_pass_through(self): 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 @@ def test_202_multiple_paths_via_pass_through(self): ), is_connected=True ) - self.assertPathExists( + path2 = self.assertPathExists( origin=self.interface2, destination=self.interface4, path=( @@ -395,7 +456,7 @@ def test_202_multiple_paths_via_pass_through(self): ), is_connected=True ) - self.assertPathExists( + path3 = self.assertPathExists( origin=self.interface3, destination=self.interface1, path=( @@ -404,7 +465,7 @@ def test_202_multiple_paths_via_pass_through(self): ), is_connected=True ) - self.assertPathExists( + path4 = self.assertPathExists( origin=self.interface4, destination=self.interface2, path=( @@ -421,12 +482,25 @@ def test_202_multiple_paths_via_pass_through(self): # 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 @@ 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] """ + 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 @@ 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] """ + 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 @@ def test_301_create_path_via_existing_cable(self): """ [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 @@ def test_302_update_path_on_cable_status_change(self): """ [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 7ab08eae46..57cd0b6ebc 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -67,8 +67,8 @@ {% endfor %} """ -CONNECTION_STATUS = """ -{{ record.get_connection_status_display }} +PATH_STATUS = """ +{% if value %}Connected{% else %}Not Connected{% endif %} """ @@ -813,13 +813,13 @@ class Meta(BaseTable.Meta): 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 ac30461b03..87bd383090 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 @@ def queryset_to_csv(self): ] 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): ] 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): ] 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 cd73a614dc..1c8ad69e45 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 0000000000..59647dd2bc --- /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 686ab92191..746a71b3cf 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 e5da5100fd..968c887777 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -131,7 +131,7 @@ def get(self, request, pk): 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 @@ def get(self, request, pk): 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 8078f88190..0cf78fafc6 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 @@ def get_connected_endpoint(self, obj): 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 edbdfb7be3..6ed50324ec 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -470,37 +470,31 @@ def napalm(self, request, pk): # 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 @@ def list(self, request): 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 c76bd3b876..aa5c62552c 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1171,18 +1171,19 @@ class Meta: 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 Meta: 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 Meta: 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 0000000000..f05cfdd1a5 --- /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 65032f5290..72edfc46e2 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -150,6 +150,15 @@ def trace(self): # 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 @@ def to_csv(self): 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 @@ def save(self, *args, **kwargs): 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 caa22e74ab..ec1480a7ef 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 87bd383090..7a1028dece 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 @@ def get(self, request, pk): 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 b2bee0f96a..161bfda743 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 7fcf6ab0a9..12b9918e94 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 59647dd2bc..e5540b44d1 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 746a71b3cf..725fe4b3f8 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -2,7 +2,6 @@ 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 40b03ada6f..159540ece8 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 Meta: 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 0cf78fafc6..b591ae36a0 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 @@ def get_connected_endpoint(self, obj): """ 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 Meta: 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 aa5c62552c..d16f5d1aa4 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 f05cfdd1a5..9404ecf53e 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 72edfc46e2..863b39e626 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 ec1480a7ef..f869a3af43 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 528301f8f4..5c13b51229 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -977,7 +977,7 @@ def test_unique_name_per_site_constraint(self): 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 @@ def setUpTestData(cls): 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 @@ def setUpTestData(cls): 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 @@ def setUpTestData(cls): 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 @@ def setUpTestData(cls): 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 @@ def setUpTestData(cls): ] -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 @@ def setUpTestData(cls): ] -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 4ef902dc76..40896f97bb 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 7a1028dece..9290a2b4af 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1018,36 +1018,31 @@ def get(self, request, pk): # 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 ef693a9de6..c49283d14f 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 b591ae36a0..144e764dcd 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -52,7 +52,7 @@ def get_connected_endpoint(self, obj): @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 d16f5d1aa4..74b5903a66 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 0d2199e311..2b58f119a0 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 ea7332b3d0..1f9db09866 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 13653c7d43..6079417c1a 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 57cd0b6ebc..1ae5282f34 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 52b37e3bd6..65de412cff 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -123,14 +123,14 @@ def setUpTestData(cls): 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 @@ def assertPathExists(self, origin, destination, path=None, is_connected=None, ms 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 @@ def test_101_interface_to_interface(self): 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 @@ def test_102_consoleport_to_consoleserverport(self): 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 @@ def test_103_powerport_to_poweroutlet(self): 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 @@ def test_104_powerport_to_powerfeed(self): 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 @@ def test_105_interface_to_circuittermination(self): 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 @@ def test_201_single_path_via_pass_through(self): 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 @@ def test_201_single_path_via_pass_through(self): 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 @@ def test_201_single_path_via_pass_through(self): 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 @@ def test_202_multiple_paths_via_pass_through(self): 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 @@ def test_202_multiple_paths_via_pass_through(self): 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 @@ def test_202_multiple_paths_via_pass_through(self): 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 @@ def test_202_multiple_paths_via_pass_through(self): 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 @@ def test_202_multiple_paths_via_pass_through(self): 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 @@ def test_202_multiple_paths_via_pass_through(self): 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 @@ def test_203_multiple_paths_via_nested_pass_throughs(self): 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 @@ def test_203_multiple_paths_via_nested_pass_throughs(self): 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 @@ def test_203_multiple_paths_via_nested_pass_throughs(self): 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 @@ def test_203_multiple_paths_via_nested_pass_throughs(self): 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 @@ def test_204_multiple_paths_via_multiple_pass_throughs(self): 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 @@ def test_204_multiple_paths_via_multiple_pass_throughs(self): 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 @@ def test_204_multiple_paths_via_multiple_pass_throughs(self): 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 @@ def test_204_multiple_paths_via_multiple_pass_throughs(self): 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 @@ def test_205_multiple_paths_via_patched_pass_throughs(self): 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 @@ def test_205_multiple_paths_via_patched_pass_throughs(self): 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 @@ def test_205_multiple_paths_via_patched_pass_throughs(self): 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 @@ def test_205_multiple_paths_via_patched_pass_throughs(self): 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 @@ def test_301_create_path_via_existing_cable(self): 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 @@ def test_301_create_path_via_existing_cable(self): 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 @@ def test_301_create_path_via_existing_cable(self): 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 @@ def test_302_update_path_on_cable_status_change(self): 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 @@ def test_302_update_path_on_cable_status_change(self): 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 @@ def test_302_update_path_on_cable_status_change(self): 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 40896f97bb..186ea72e51 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 9290a2b4af..a2850fc46f 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2088,7 +2088,7 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 c49283d14f..b43662a3ea 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 10ae5e5ee6..03c9012af9 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -77,5 +77,5 @@ class Meta: 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 144e764dcd..42018c0466 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -30,7 +30,7 @@ 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 @@ def get_connected_endpoint(self, obj): 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 Meta: 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 Meta: 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 Meta: 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 Meta: 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 Meta: 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 961c458e0e..804e5be037 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -59,12 +59,6 @@ # 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 83d8841df0..f3d9421129 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1714,11 +1714,6 @@ def setUpTestData(cls): '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 1d5c9c0a05..063d300162 100644 --- a/netbox/utilities/custom_inspectors.py +++ b/netbox/utilities/custom_inspectors.py @@ -62,8 +62,8 @@ def field_to_swagger_object(self, field, swagger_object_type, use_references, ** 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 b43662a3ea..ee49f223f7 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 42018c0466..031ce575b8 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -760,7 +760,7 @@ class Meta: 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 Meta: 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 fabf71798f..b416d2e9f4 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 e5540b44d1..0000000000 --- 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 2b58f119a0..dd3c4ed197 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 9404ecf53e..0000000000 --- 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 | 68 +++++++++++------------------------------- netbox/dcim/tables.py | 25 ++++++---------- netbox/dcim/views.py | 6 ++-- 3 files changed, 30 insertions(+), 69 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 74b5903a66..aeccc341bb 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1150,7 +1150,20 @@ def filter_device(self, queryset, name, value): 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 Meta: 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 Meta: 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 Meta: 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 1ae5282f34..b1aa6e57db 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -67,10 +67,6 @@ {% 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 a2850fc46f..d7ddc18557 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2088,7 +2088,7 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 863b39e626..2ab3ce7c85 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -150,6 +150,10 @@ def trace(self): # 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 90f6b5ef24..884941c9dc 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -385,5 +385,6 @@ 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 3f4d7b3069..2b79b2b1cc 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 77d17fe8a7..ae164634de 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 12b9918e94..2a3435f330 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 017a430ee6..9f0a45fcd1 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -123,11 +123,6 @@

{% block title %}{{ powerfeed }}{% endblock %}

Cable + {{ instance.cable }} + + + +
Cable - {{ instance.cable }} - - - -
Connection StatusPath 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 @@

{% block title %}{{ powerfeed }}{% endblock %}

+ {% 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 2ea70972b4..82782be09a 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 3f3d288994..52c9c0e954 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 d7ddc18557..c82b9e6ac2 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1018,31 +1018,31 @@ def get(self, request, pk): # 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 96b61ea47e..6c7b1c9715 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -479,7 +479,7 @@

{{ device }}

-
+
{% 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 c82b9e6ac2..3608b37928 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -34,10 +34,11 @@ 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 @@ def dispatch(self, request, *args, **kwargs): 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 2f54f94eea..4b3f7f2d48 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -7,52 +7,70 @@

{% block title %}Cable Trace for {{ obj }}{% endblock %}

{% 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 }}

-
-
- {% 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 %} +
+ +
+
+

Near End

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

Far End

+
-
- {% 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 23b9b7fd53..21af2ed145 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 0000000000..03acc478a3 --- /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 6079417c1a..ee006c9d75 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -7,7 +7,7 @@ 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 65de412cff..cfe63929dd 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -4,7 +4,7 @@ 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 @@ def assertPathExists(self, origin, destination, path=None, is_active=None, msg=N 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 186ea72e51..d36cb1ad32 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 3608b37928..63711a8639 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -38,7 +38,6 @@ 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 @@ def get(self, request, pk): 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 1c25a0e284..3169d2ffc1 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 f267479f38..d362b60034 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 c1e5482d06..ce6edc8837 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 1c8ad69e45..7b147412e1 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 6ed50324ec..a804ad0b68 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -470,31 +470,31 @@ def napalm(self, request, pk): # 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 0000000000..9a470a3c28 --- /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 0000000000..aeb89c5d3e --- /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 2ab3ce7c85..1bd577cdf9 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 Meta: 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 ee006c9d75..5e5915313a 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 83438a609d..01829d7bc7 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -398,9 +398,11 @@ def test_cable_creation(self): 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 @@ def test_cable_deletion(self): 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 6c7b1c9715..d66213a781 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -506,6 +506,7 @@

{{ device }}

MTU Mode Cable + Cable Termination Connection @@ -566,7 +567,7 @@

{{ device }}

Position Description Cable - Connection + Cable Termination @@ -623,7 +624,7 @@

{{ device }}

Positions Description Cable - Connection + Cable Termination @@ -679,6 +680,7 @@

{{ device }}

Type Description Cable + Cable Termination Connection @@ -732,6 +734,7 @@

{{ device }}

Type Description Cable + Cable Termination Connection @@ -789,6 +792,7 @@

{{ device }}

Draw Description Cable + Cable Termination Connection @@ -843,6 +847,7 @@

{{ device }}

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 0000000000..e4b28fbcf1 --- /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 912404be3f..ace09cfe2c 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 b7a5c6b568..025b0bf028 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 d362b60034..91374cb1eb 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 1595511924..efaed7ecf4 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 b3e003e99f..38eb7b8a60 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 c65b685d77..c5c18c093c 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 ce6edc8837..fd5ee620c6 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 b1aa6e57db..437daaf296 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -67,6 +67,19 @@ {% 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 Meta(BaseTable.Meta): 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 d66213a781..c06f86bf0c 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -792,7 +792,6 @@

{{ device }}

Draw Description Cable - Cable Termination Connection @@ -847,7 +846,6 @@

{{ device }}

Input/Leg Description Cable - Cable Termination Connection diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 38eb7b8a60..a6a0dd03e4 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 c5c18c093c..125bc54459 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 3cad8b5b30..46b386cf39 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -58,7 +58,7 @@

{% block title %}{{ powerpanel }}{% endblock %}

{% block content %}
-
+
Power Panel @@ -82,17 +82,17 @@

{% block title %}{{ powerpanel }}{% endblock %}

- {% 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 ee49f223f7..124bbd645d 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 03c9012af9..6bbb0cef52 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -3,7 +3,7 @@ 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 Meta: ] -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 031ce575b8..803b64fbb6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -27,6 +27,27 @@ 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 Meta: 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 Meta: 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 Meta: 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 Meta: 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 Meta: 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 @@ def validate(self, data): 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 Meta: 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 Meta: 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 Meta: 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 a804ad0b68..14d6177bb4 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -470,31 +470,35 @@ def napalm(self, request, pk): # 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 903a6feac5..0000000000 --- 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 1f9db09866..e6c6134c6c 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -15,7 +15,6 @@ 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 6bbb0cef52..9bc95f065f 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -78,4 +78,5 @@ class Meta: 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 803b64fbb6..11e286132f 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -487,7 +487,7 @@ class Meta: 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 Meta: 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 Meta: 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 Meta: 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 Meta: 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 Meta: 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 Meta: 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 1bd577cdf9..6e93dd7687 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -39,6 +39,9 @@ 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 @@ def parent(self): 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 @@ def get_cable_peer(self): 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 e6c6134c6c..e4ea551b9c 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1162,7 +1162,27 @@ def get_compatible_types(self): 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 9756c320b0..b0861a7c00 100644 --- a/netbox/circuits/tests/test_filters.py +++ b/netbox/circuits/tests/test_filters.py @@ -3,7 +3,7 @@ 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 @@ def setUpTestData(cls): )) 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 @@ def test_site(self): 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 63711a8639..696b7f3569 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 @@ def queryset_to_csv(self): 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 124bbd645d..253ed06120 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 d36cb1ad32..ccc849aa56 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -5,12 +5,20 @@ 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 253ed06120..fa3f3aea8f 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 e4ea551b9c..b44146b994 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -7,7 +7,7 @@ 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 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 @@ def save(self, *args, **kwargs): 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 ccc849aa56..52b0a42323 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -4,20 +4,29 @@ 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 696b7f3569..ff4ec1ef6a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1984,15 +1984,11 @@ def get(self, request, pk): 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 | 84 ++++++++++++--------------- netbox/dcim/tests/test_filters.py | 54 +++++++++++++---- 4 files changed, 90 insertions(+), 62 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index ebc0d0ec1a..c573603c64 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 @@ def search(self, queryset, name, value): ).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 b0861a7c00..73701be038 100644 --- a/netbox/circuits/tests/test_filters.py +++ b/netbox/circuits/tests/test_filters.py @@ -316,6 +316,12 @@ def test_site(self): 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 aeccc341bb..9690ee195a 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -24,6 +24,7 @@ __all__ = ( 'CableFilterSet', + 'CableTerminationFilterSet', 'ConsoleConnectionFilterSet', 'ConsolePortFilterSet', 'ConsolePortTemplateFilterSet', @@ -41,6 +42,7 @@ 'InterfaceTemplateFilterSet', 'InventoryItemFilterSet', 'ManufacturerFilterSet', + 'PathEndpointFilterSet', 'PlatformFilterSet', 'PowerConnectionFilterSet', 'PowerFeedFilterSet', @@ -753,81 +755,76 @@ def search(self, queryset, name, value): ) +class CableTerminationFilterSet(django_filters.FilterSet): + cabled = django_filters.BooleanFilter( + field_name='cable', + lookup_expr='isnull', + exclude=True + ) + + class PathEndpointFilterSet(django_filters.FilterSet): - is_connected = django_filters.BooleanFilter( - method='filter_is_connected', - label='Search', + connected = django_filters.BooleanFilter( + method='filter_connected' ) - def filter_is_connected(self, queryset, name, value): - return queryset.filter(_path__is_active=True) + 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, PathEndpointFilterSet): +class ConsolePortFilterSet(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 = 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 @@ def filter_kind(self, queryset, name, value): }.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 @@ def search(self, queryset, name, value): 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 c399e1a92c..f209cd1f48 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -1514,9 +1514,11 @@ def test_description(self): 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 @@ def test_description(self): 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 @@ def test_allocated_draw(self): 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 @@ def test_feed_leg(self): 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 @@ def test_name(self): 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 @@ def setUpTestData(cls): ) 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 @@ def test_rack_id(self): 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 9a470a3c28..630c3b4ece 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 b416d2e9f4..4a5b26efa6 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 @@ 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 aeb89c5d3e..c45d033968 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 dd3c4ed197..737e59b326 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 d11a85417e..0000000000 --- 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 0000000000..47636a943e --- /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 66ba7b39f1..468f189b33 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 | 28 ++++++++++++++----------- netbox/templates/dcim/rack.html | 12 +++++++---- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 6e93dd7687..72bd453c5b 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -320,8 +320,12 @@ def get_power_draw(self): """ # 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 @@ def get_power_draw(self): } # 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 409db14c5f..78c72f5038 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -3,6 +3,7 @@ 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.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 @@ def get_power_utilization(self): """ 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 + + 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 - 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 + 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 4cf3b9018f..c44b75be1b 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -150,10 +150,14 @@

{% block title %}Rack {{ rack }}{% endblock %}

{{ 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 6073c06ee1..71b7c56ef3 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 4b3f7f2d48..7df39caca1 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -7,66 +7,65 @@

{% block title %}Cable Trace for {{ obj }}{% endblock %}

{% 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 71b7c56ef3..0000000000 --- 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 0000000000..2cb5ffed64 --- /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 0000000000..ef1ed05bc5 --- /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 0000000000..c3ed109d70 --- /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 0000000000..a439aff27a --- /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 0000000000..dedb562ea7 --- /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 11e286132f..d599e7461b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -7,12 +7,13 @@ 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 Meta: ] +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 14d6177bb4..50dc82c9d8 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -17,7 +17,7 @@ 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 @@ def trace(self, request, pk): 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 d599e7461b..f9e17b12cb 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -786,17 +786,23 @@ def get_path(self, obj): 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 9bc95f065f..ad5e609e44 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -77,6 +77,6 @@ class Meta: 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 f9e17b12cb..d6da5a5e32 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 Meta: 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 Meta: 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 fa3f3aea8f..33c3bd1b6f 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 18e42318b9..e788c9b5fb 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 72bd453c5b..2ea9b88b6b 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -172,9 +172,11 @@ def trace(self): 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 cfe63929dd..95a8e0d30c 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -721,6 +721,63 @@ def test_205_multiple_paths_via_patched_pass_throughs(self): 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 52b0a42323..b82dd58d2b 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 7df39caca1..a328f20520 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -19,16 +19,17 @@

{% block title %}Cable Trace for {{ obj }}{% endblock %}

{% 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 @@

No cable

{% 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 33c3bd1b6f..0e636fc45d 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 7b147412e1..5168319832 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -3,6 +3,7 @@ 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 50dc82c9d8..c45879dbe2 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 c45879dbe2..b14c67e652 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -58,6 +58,9 @@ def trace(self, request, pk): 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 | 704 ++++++++++++++------------- 1 file changed, 365 insertions(+), 339 deletions(-) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 95a8e0d30c..5699b3b886 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') - - # 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') - 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 + cls.device = Device.objects.create(site=cls.site, device_type=device_type, device_role=device_role, name='Test Device') + + cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel') + 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 @@ def test_101_interface_to_interface(self): """ [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 @@ def test_102_consoleport_to_consoleserverport(self): """ [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 @@ def test_103_powerport_to_poweroutlet(self): """ [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 @@ def test_104_powerport_to_powerfeed(self): """ [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 @@ def test_104_powerport_to_powerfeed(self): 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 @@ def test_105_interface_to_circuittermination(self): 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 @@ def test_201_single_path_via_pass_through(self): # 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 @@ def test_202_multiple_paths_via_pass_through(self): # 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 @@ 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] """ - 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 @@ def test_204_multiple_paths_via_multiple_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] + [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 @@ 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() + 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 @@ def test_206_unidirectional_split_paths(self): # 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 @@ def test_302_update_path_on_cable_status_change(self): 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 @@ def test_302_update_path_on_cable_status_change(self): 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 e788c9b5fb..0000000000 --- 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