From 9a7f3f8c1a11ba429cf733b0f441df94dc14fdf9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 7 Jul 2022 12:48:44 -0400 Subject: [PATCH] Cleanup for #9102 --- netbox/circuits/graphql/types.py | 3 +- netbox/dcim/api/serializers.py | 35 +++--- netbox/dcim/api/views.py | 34 +++--- netbox/dcim/choices.py | 2 +- netbox/dcim/graphql/mixins.py | 5 + netbox/dcim/graphql/types.py | 17 +-- .../migrations/0157_new_cabling_models.py | 2 +- netbox/dcim/models/cables.py | 103 ++++++++++-------- netbox/dcim/models/device_components.py | 32 +++--- netbox/dcim/models/racks.py | 6 +- netbox/dcim/signals.py | 4 +- netbox/dcim/tables/template_code.py | 26 ++--- netbox/dcim/tests/test_cablepaths.py | 13 ++- netbox/dcim/utils.py | 14 +-- netbox/dcim/views.py | 32 +++--- netbox/netbox/middleware.py | 4 +- netbox/netbox/views/__init__.py | 7 +- 17 files changed, 169 insertions(+), 170 deletions(-) create mode 100644 netbox/dcim/graphql/mixins.py diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 094b78d07..e96fe98a5 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,4 +1,5 @@ from circuits import filtersets, models +from dcim.graphql.mixins import CabledObjectMixin from extras.graphql.mixins import CustomFieldsMixin, TagsMixin from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType @@ -11,7 +12,7 @@ __all__ = ( ) -class CircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): +class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): class Meta: model = models.CircuitTermination diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9938bb2e9..53205e162 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -52,16 +52,13 @@ class CabledObjectSerializer(serializers.ModelSerializer): """ Return the appropriate serializer for the link termination model. """ - if not obj.cable: + if not obj.link_peers: return [] # Return serialized peer termination objects - if obj.link_peers: - serializer = get_serializer_for_model(obj.link_peers[0], prefix='Nested') - context = {'request': self.context['request']} - return serializer(obj.link_peers, context=context, many=True).data - - return [] + serializer = get_serializer_for_model(obj.link_peers[0], prefix='Nested') + context = {'request': self.context['request']} + return serializer(obj.link_peers, context=context, many=True).data @swagger_serializer_method(serializer_or_field=serializers.BooleanField) def get__occupied(self, obj): @@ -77,8 +74,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer): connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True) def get_connected_endpoints_type(self, obj): - endpoints = obj.connected_endpoints - if endpoints: + if endpoints := obj.connected_endpoints: return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}' @swagger_serializer_method(serializer_or_field=serializers.ListField) @@ -86,8 +82,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer): """ Return the appropriate serializer for the type of connected object. """ - endpoints = obj.connected_endpoints - if endpoints: + if endpoints := obj.connected_endpoints: serializer = get_serializer_for_model(endpoints[0], prefix='Nested') context = {'request': self.context['request']} return serializer(endpoints, many=True, context=context).data @@ -1016,15 +1011,15 @@ class CableSerializer(NetBoxModelSerializer): ] def _get_terminations_type(self, obj, side): - assert side.lower() in ('a', 'b') - terms = [t.termination for t in obj.terminations.all() if t.cable_end == side.upper()] + assert side in CableEndChoices.values() + terms = getattr(obj, f'get_{side.lower()}_terminations')() if terms: ct = ContentType.objects.get_for_model(terms[0]) return f"{ct.app_label}.{ct.model}" def _get_terminations(self, obj, side): - assert side.lower() in ('a', 'b') - terms = [t.termination for t in obj.terminations.all() if t.cable_end == side.upper()] + assert side in CableEndChoices.values() + terms = getattr(obj, f'get_{side.lower()}_terminations')() if not terms: return [] @@ -1037,19 +1032,19 @@ class CableSerializer(NetBoxModelSerializer): @swagger_serializer_method(serializer_or_field=serializers.CharField) def get_a_terminations_type(self, obj): - return self._get_terminations_type(obj, 'a') + return self._get_terminations_type(obj, CableEndChoices.SIDE_A) @swagger_serializer_method(serializer_or_field=serializers.CharField) def get_b_terminations_type(self, obj): - return self._get_terminations_type(obj, 'b') + return self._get_terminations_type(obj, CableEndChoices.SIDE_B) @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_a_terminations(self, obj): - return self._get_terminations(obj, 'a') + return self._get_terminations(obj, CableEndChoices.SIDE_A) @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_b_terminations(self, obj): - return self._get_terminations(obj, 'b') + return self._get_terminations(obj, CableEndChoices.SIDE_B) class TracedCableSerializer(serializers.ModelSerializer): @@ -1066,7 +1061,7 @@ class TracedCableSerializer(serializers.ModelSerializer): class CableTerminationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail') termination_type = ContentTypeField( queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) ) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index e6f7605ef..e71a6500d 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -15,6 +15,7 @@ from circuits.models import Circuit from dcim import filtersets from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH from dcim.models import * +from dcim.svg import CableTraceSVG from extras.api.views import ConfigContextQuerySetMixin from ipam.models import Prefix, VLAN from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired @@ -52,37 +53,30 @@ class PathEndpointMixin(object): # Initialize the path array path = [] + # Render SVG image if requested if request.GET.get('render', None) == 'svg': - # Render SVG try: width = int(request.GET.get('width', CABLE_TRACE_SVG_DEFAULT_WIDTH)) except (ValueError, TypeError): width = CABLE_TRACE_SVG_DEFAULT_WIDTH - drawing = obj.get_trace_svg( - base_url=request.build_absolute_uri('/'), - width=width - ) - return HttpResponse(drawing.tostring(), content_type='image/svg+xml') + drawing = CableTraceSVG(self, base_url=request.build_absolute_uri('/'), width=width) + return HttpResponse(drawing.render().tostring(), content_type='image/svg+xml') + # Serialize path objects, iterating over each three-tuple in the 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[0], prefix='Nested') - x = serializer_a(near_end, many=True, context={'request': request}).data - if cable is not None: - y = serializers.TracedCableSerializer(cable[0], context={'request': request}).data + if near_end is not None: + serializer_a = get_serializer_for_model(near_end[0], prefix='Nested') + near_end = serializer_a(near_end, many=True, context={'request': request}).data else: - y = None + # Path is split; stop here + break + if cable is not None: + cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data if far_end is not None: serializer_b = get_serializer_for_model(far_end[0], prefix='Nested') - z = serializer_b(far_end, many=True, context={'request': request}).data - else: - z = None + far_end = serializer_b(far_end, many=True, context={'request': request}).data - path.append((x, y, z)) + path.append((near_end, cable, far_end)) return Response(path) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index dbbc60a93..1a66312da 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1294,7 +1294,7 @@ class CableEndChoices(ChoiceSet): CHOICES = ( (SIDE_A, 'A'), (SIDE_B, 'B'), - ('', ''), + # ('', ''), ) diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py new file mode 100644 index 000000000..d8488aa5f --- /dev/null +++ b/netbox/dcim/graphql/mixins.py @@ -0,0 +1,5 @@ +class CabledObjectMixin: + + def resolve_cable_end(self, info): + # Handle empty values + return self.cable_end or None diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 31589dae3..a43b293a4 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -7,6 +7,7 @@ from extras.graphql.mixins import ( from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType +from .mixins import CabledObjectMixin __all__ = ( 'CableType', @@ -107,7 +108,7 @@ class CableTerminationType(NetBoxObjectType): filterset_class = filtersets.CableTerminationFilterSet -class ConsolePortType(ComponentObjectType): +class ConsolePortType(ComponentObjectType, CabledObjectMixin): class Meta: model = models.ConsolePort @@ -129,7 +130,7 @@ class ConsolePortTemplateType(ComponentTemplateObjectType): return self.type or None -class ConsoleServerPortType(ComponentObjectType): +class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin): class Meta: model = models.ConsoleServerPort @@ -211,7 +212,7 @@ class DeviceTypeType(NetBoxObjectType): return self.airflow or None -class FrontPortType(ComponentObjectType): +class FrontPortType(ComponentObjectType, CabledObjectMixin): class Meta: model = models.FrontPort @@ -227,7 +228,7 @@ class FrontPortTemplateType(ComponentTemplateObjectType): filterset_class = filtersets.FrontPortTemplateFilterSet -class InterfaceType(IPAddressesMixin, ComponentObjectType): +class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin): class Meta: model = models.Interface @@ -330,7 +331,7 @@ class PlatformType(OrganizationalObjectType): filterset_class = filtersets.PlatformFilterSet -class PowerFeedType(NetBoxObjectType): +class PowerFeedType(NetBoxObjectType, CabledObjectMixin): class Meta: model = models.PowerFeed @@ -338,7 +339,7 @@ class PowerFeedType(NetBoxObjectType): filterset_class = filtersets.PowerFeedFilterSet -class PowerOutletType(ComponentObjectType): +class PowerOutletType(ComponentObjectType, CabledObjectMixin): class Meta: model = models.PowerOutlet @@ -374,7 +375,7 @@ class PowerPanelType(NetBoxObjectType): filterset_class = filtersets.PowerPanelFilterSet -class PowerPortType(ComponentObjectType): +class PowerPortType(ComponentObjectType, CabledObjectMixin): class Meta: model = models.PowerPort @@ -426,7 +427,7 @@ class RackRoleType(OrganizationalObjectType): filterset_class = filtersets.RackRoleFilterSet -class RearPortType(ComponentObjectType): +class RearPortType(ComponentObjectType, CabledObjectMixin): class Meta: model = models.RearPort diff --git a/netbox/dcim/migrations/0157_new_cabling_models.py b/netbox/dcim/migrations/0157_new_cabling_models.py index def63d8bd..e7c55997c 100644 --- a/netbox/dcim/migrations/0157_new_cabling_models.py +++ b/netbox/dcim/migrations/0157_new_cabling_models.py @@ -27,7 +27,7 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='cabletermination', - constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='unique_termination'), + constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='dcim_cable_termination_unique_termination'), ), # Update CablePath model diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 8f31c77f2..cf9f6064d 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -1,3 +1,4 @@ +import itertools from collections import defaultdict from django.contrib.contenttypes.fields import GenericForeignKey @@ -11,15 +12,14 @@ from django.urls import reverse from dcim.choices import * from dcim.constants import * from dcim.fields import PathField -from dcim.utils import decompile_path_node, flatten_path, object_to_path_node, path_node_to_object +from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object from netbox.models import NetBoxModel from utilities.fields import ColorField from utilities.querysets import RestrictedQuerySet from utilities.utils import to_meters from wireless.models import WirelessLink -from .devices import Device from .device_components import FrontPort, RearPort - +from .devices import Device __all__ = ( 'Cable', @@ -110,7 +110,8 @@ class Cable(NetBoxModel): # Cache the original status so we can check later if it's been changed self._orig_status = self.status - # Assign associated CableTerminations (if any) + # Assign any *new* CableTerminations for the instance. These will replace any existing + # terminations on save(). if a_terminations is not None: self.a_terminations = a_terminations if b_terminations is not None: @@ -133,28 +134,25 @@ class Cable(NetBoxModel): self.length_unit = '' a_terminations = [ - CableTermination(cable=self, cable_end='A', termination=t) for t in getattr(self, 'a_terminations', []) + CableTermination(cable=self, cable_end='A', termination=t) + for t in getattr(self, 'a_terminations', []) ] b_terminations = [ - CableTermination(cable=self, cable_end='B', termination=t) for t in getattr(self, 'b_terminations', []) + CableTermination(cable=self, cable_end='B', termination=t) + for t in getattr(self, 'b_terminations', []) ] # Check that all termination objects for either end are of the same type for terms in (a_terminations, b_terminations): - if terms and len(terms) > 1: - if not all(t.termination_type == terms[0].termination_type for t in terms[1:]): - raise ValidationError( - "Cannot connect different termination types to same end of cable." - ) + if len(terms) > 1 and not all(t.termination_type == terms[0].termination_type for t in terms[1:]): + raise ValidationError("Cannot connect different termination types to same end of cable.") # Check that termination types are compatible if a_terminations and b_terminations: a_type = a_terminations[0].termination_type.model b_type = b_terminations[0].termination_type.model if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type): - raise ValidationError( - f"Incompatible termination types: {a_type} and {b_type}" - ) + raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}") # Run clean() on any new CableTerminations for cabletermination in [*a_terminations, *b_terminations]: @@ -169,6 +167,7 @@ class Cable(NetBoxModel): else: self._abs_length = None + # TODO: Need to come with a proper solution for filtering by termination parent # Store the parent Device for the A and B terminations (if applicable) to enable filtering if hasattr(self, 'a_terminations'): self._termination_a_device = getattr(self.a_terminations[0], 'device', None) @@ -210,13 +209,15 @@ class Cable(NetBoxModel): return LinkStatusChoices.colors.get(self.status) def get_a_terminations(self): + # Query self.terminations.all() to leverage cached results return [ - term.termination for term in CableTermination.objects.filter(cable=self, cable_end='A') + ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_A ] def get_b_terminations(self): + # Query self.terminations.all() to leverage cached results return [ - term.termination for term in CableTermination.objects.filter(cable=self, cable_end='B') + ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_B ] @@ -253,7 +254,7 @@ class CableTermination(models.Model): constraints = ( models.UniqueConstraint( fields=('termination_type', 'termination_id'), - name='unique_termination' + name='dcim_cable_termination_unique_termination' ), ) @@ -289,34 +290,48 @@ class CableTermination(models.Model): # Delete the cable association on the terminating object termination_model = self.termination._meta.model - termination_model.objects.filter(pk=self.termination_id).update(cable=None, cable_end='', _path=None) + termination_model.objects.filter(pk=self.termination_id).update( + cable=None, + cable_end='' + ) super().delete(*args, **kwargs) class CablePath(models.Model): """ - 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). + A CablePath instance represents the physical path from a set of origin nodes to a set of destination nodes, + including all intermediate elements. - `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 + `path` contains the ordered set of nodes, arranged in lists of (type, ID) tuples. (Each cable in the path can + terminate to one or more objects.) 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 + A B C + Interface 1 --- Front Port 1 | Rear Port 1 --- Rear Port 2 | Front Port 3 --- Interface 2 + Front Port 2 Front Port 4 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] + path = [ + [Interface 1], + [Cable A], + [Front Port 1, Front Port 2], + [Rear Port 1], + [Cable B], + [Rear Port 2], + [Front Port 3, Front Port 4], + [Cable C], + [Interface 2], + ] ) - `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". + `is_active` is set to True only if every Cable within the path has a status of "connected". `is_complete` is True + if the instance represents a complete end-to-end path from origin(s) to destination(s). `is_split` is True if the + path diverges across multiple cables. + + `_nodes` retains a flattened list of all nodes within the path to enable simple filtering. """ path = models.JSONField( default=list @@ -332,36 +347,32 @@ class CablePath(models.Model): ) _nodes = PathField() - class Meta: - pass - def __str__(self): - status = ' (active)' if self.is_active else ' (split)' if self.is_split else '' - return f"Path #{self.pk}: {len(self.path)} nodes{status}" + return f"Path #{self.pk}: {len(self.path)} hops" def save(self, *args, **kwargs): # Save the flattened nodes list - self._nodes = flatten_path(self.path) + self._nodes = list(itertools.chain(*self.path)) super().save(*args, **kwargs) # Record a direct reference to this CablePath on its originating object(s) - origin_model = self.origins[0]._meta.model - origin_ids = [o.id for o in self.origins] + origin_model = self.origin_type.model_class() + origin_ids = [decompile_path_node(node)[1] for node in self.path[0]] origin_model.objects.filter(pk__in=origin_ids).update(_path=self.pk) @property def origin_type(self): - ct_id, _ = decompile_path_node(self.path[0][0]) - return ContentType.objects.get_for_id(ct_id) + if self.path: + ct_id, _ = decompile_path_node(self.path[0][0]) + return ContentType.objects.get_for_id(ct_id) @property def destination_type(self): - if not self.is_complete: - return None - ct_id, _ = decompile_path_node(self.path[-1][0]) - return ContentType.objects.get_for_id(ct_id) + if self.is_complete: + ct_id, _ = decompile_path_node(self.path[-1][0]) + return ContentType.objects.get_for_id(ct_id) @property def path_objects(self): @@ -375,7 +386,7 @@ class CablePath(models.Model): @property def origins(self): """ - Return the list of originating objects (from cache, if available). + Return the list of originating objects. """ if hasattr(self, '_path_objects'): return self.path_objects[0] @@ -386,7 +397,7 @@ class CablePath(models.Model): @property def destinations(self): """ - Return the list of destination objects (from cache, if available), if the path is complete. + Return the list of destination objects, if the path is complete. """ if not self.is_complete: return [] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index d54ad8384..d1bdd757e 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -10,7 +10,6 @@ from mptt.models import MPTTModel, TreeForeignKey from dcim.choices import * from dcim.constants import * from dcim.fields import MACAddressField, WWNField -from dcim.svg import CableTraceSVG from netbox.models import OrganizationalModel, NetBoxModel from utilities.choices import ColorChoices from utilities.fields import ColorField, NaturalOrderingField @@ -105,7 +104,8 @@ class ModularComponentModel(ComponentModel): class CabledObjectModel(models.Model): """ - An abstract model inherited by all models to which a Cable can terminate. + An abstract model inherited by all models to which a Cable can terminate. Provides the `cable` and `cable_end` + fields for caching cable associations, as well as `mark_connected` to designate "fake" connections. """ cable = models.ForeignKey( to='dcim.Cable', @@ -134,8 +134,11 @@ class CabledObjectModel(models.Model): raise ValidationError({ "cable_end": "Must specify cable end (A or B) when attaching a cable." }) - - if self.mark_connected and self.cable_id: + if self.cable_end and not self.cable: + raise ValidationError({ + "cable_end": "Cable end must not be set without a cable." + }) + if self.mark_connected and self.cable: raise ValidationError({ "mark_connected": "Cannot mark as connected with a cable attached." }) @@ -167,12 +170,13 @@ class CabledObjectModel(models.Model): """ Generic wrapper for a Cable, WirelessLink, or some other relation to a connected termination. """ + # TODO: Support WirelessLinks return self.cable class PathEndpoint(models.Model): """ - An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically, + An abstract model inherited by any CabledObjectModel subclass which represents the end of a CablePath; specifically, these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, and PowerFeed. `_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in @@ -215,14 +219,6 @@ class PathEndpoint(models.Model): # Return the path as a list of three-tuples (A termination(s), cable(s), B termination(s)) return list(zip(*[iter(path)] * 3)) - def get_trace_svg(self, base_url=None, width=CABLE_TRACE_SVG_DEFAULT_WIDTH): - trace = CableTraceSVG(self, base_url=base_url, width=width) - return trace.render() - - @property - def path(self): - return self._path - @property def connected_endpoints(self): """ @@ -338,7 +334,15 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint): def get_downstream_powerports(self, leg=None): """ - Return a queryset of all PowerPorts connected via cable to a child PowerOutlet. + Return a queryset of all PowerPorts connected via cable to a child PowerOutlet. For example, in the topology + below, PP1.get_downstream_powerports() would return PP2-4. + + ---- PO1 <---> PP2 + / + PP1 ------- PO2 <---> PP3 + \ + ---- PO3 <---> PP4 + """ poweroutlets = self.poweroutlets.filter(cable__isnull=False) if leg: diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 3bbf38b6a..2039def09 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -438,9 +438,9 @@ class Rack(NetBoxModel): peer for peer in powerfeed.link_peers if isinstance(peer, PowerPort) ]) - allocated_draw = 0 - for powerport in powerports: - allocated_draw += powerport.get_power_draw()['allocated'] + allocated_draw = sum([ + powerport.get_power_draw()['allocated'] for powerport in powerports + ]) return int(allocated_draw / available_power_total * 100) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index cb6b32de3..7cfdc823d 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -3,7 +3,7 @@ import logging from django.db.models.signals import post_save, post_delete, pre_delete from django.dispatch import receiver -from .choices import LinkStatusChoices +from .choices import CableEndChoices, LinkStatusChoices from .models import Cable, CablePath, CableTermination, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis from .models.cables import trace_paths from .utils import create_cablepath, rebuild_paths @@ -83,7 +83,7 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs): a_terminations = [] b_terminations = [] for t in instance.terminations.all(): - if t.cable_end == 'A': + if t.cable_end == CableEndChoices.SIDE_A: a_terminations.append(t.termination) else: b_terminations.append(t.termination) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 39f40c816..a07186973 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -13,21 +13,17 @@ CABLE_LENGTH = """ {% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %} """ -CABLE_TERMINATION = """ -{{ value|join:", " }} -""" - -CABLE_TERMINATION_PARENT = """ -{% with value.0 as termination %} - {% if termination.device %} - {{ termination.device }} - {% elif termination.circuit %} - {{ termination.circuit }} - {% elif termination.power_panel %} - {{ termination.power_panel }} - {% endif %} -{% endwith %} -""" +# CABLE_TERMINATION_PARENT = """ +# {% with value.0 as termination %} +# {% if termination.device %} +# {{ termination.device }} +# {% elif termination.circuit %} +# {{ termination.circuit }} +# {% elif termination.power_panel %} +# {{ termination.power_panel }} +# {% endif %} +# {% endwith %} +# """ DEVICE_LINK = """ diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index e5b441c51..cfbbbc63b 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -3,6 +3,7 @@ from django.test import TestCase from circuits.models import * from dcim.choices import LinkStatusChoices from dcim.models import * +from dcim.svg import CableTraceSVG from dcim.utils import object_to_path_node @@ -107,7 +108,7 @@ class CablePathTestCase(TestCase): self.assertPathIsSet(interface2, path2) # Test SVG generation - interface1.get_trace_svg() + CableTraceSVG(interface1).render() # Delete cable 1 cable1.delete() @@ -146,7 +147,7 @@ class CablePathTestCase(TestCase): self.assertPathIsSet(consoleserverport1, path2) # Test SVG generation - consoleport1.get_trace_svg() + CableTraceSVG(consoleport1).render() # Delete cable 1 cable1.delete() @@ -185,7 +186,7 @@ class CablePathTestCase(TestCase): self.assertPathIsSet(poweroutlet1, path2) # Test SVG generation - powerport1.get_trace_svg() + CableTraceSVG(powerport1).render() # Delete cable 1 cable1.delete() @@ -224,7 +225,7 @@ class CablePathTestCase(TestCase): self.assertPathIsSet(powerfeed1, path2) # Test SVG generation - powerport1.get_trace_svg() + CableTraceSVG(powerport1).render() # Delete cable 1 cable1.delete() @@ -267,7 +268,7 @@ class CablePathTestCase(TestCase): self.assertPathIsSet(interface3, path2) # Test SVG generation - interface1.get_trace_svg() + CableTraceSVG(interface1).render() # Delete cable 1 cable1.delete() @@ -319,7 +320,7 @@ class CablePathTestCase(TestCase): self.assertPathIsSet(interface4, path2) # Test SVG generation - interface1.get_trace_svg() + CableTraceSVG(interface1).render() # Delete cable 1 cable1.delete() diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index e773dacc0..26b6e2e25 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -1,3 +1,5 @@ +import itertools + from django.contrib.contenttypes.models import ContentType from django.db import transaction @@ -29,16 +31,6 @@ def path_node_to_object(repr): return ct.model_class().objects.get(pk=object_id) -def flatten_path(path): - """ - Flatten a two-dimensional array (list of lists) into a flat list. - """ - ret = [] - for step in path: - ret.extend(step) - return ret - - def create_cablepath(terminations): """ Create CablePaths for all paths originating from the specified set of nodes. @@ -54,7 +46,7 @@ def create_cablepath(terminations): def rebuild_paths(terminations): """ - Rebuild all CablePaths which traverse the specified node + Rebuild all CablePaths which traverse the specified nodes. """ from dcim.models import CablePath diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index f6262c789..5619329ac 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -28,6 +28,18 @@ from .choices import DeviceFaceChoices from .constants import NONCONNECTABLE_IFACE_TYPES from .models import * +CABLE_TERMINATION_TYPES = { + 'dcim.consoleport': ConsolePort, + 'dcim.consoleserverport': ConsoleServerPort, + 'dcim.powerport': PowerPort, + 'dcim.poweroutlet': PowerOutlet, + 'dcim.interface': Interface, + 'dcim.frontport': FrontPort, + 'dcim.rearport': RearPort, + 'dcim.powerfeed': PowerFeed, + 'circuits.circuittermination': CircuitTermination, +} + class DeviceComponentsView(generic.ObjectChildrenView): queryset = Device.objects.all() @@ -2818,22 +2830,10 @@ class CableEditView(generic.ObjectEditView): # If creating a new Cable, initialize the form class using URL query params if 'pk' not in kwargs: - termination_types = { - 'dcim.consoleport': ConsolePort, - 'dcim.consoleserverport': ConsoleServerPort, - 'dcim.powerport': PowerPort, - 'dcim.poweroutlet': PowerOutlet, - 'dcim.interface': Interface, - 'dcim.frontport': FrontPort, - 'dcim.rearport': RearPort, - 'dcim.powerfeed': PowerFeed, - 'circuits.circuittermination': CircuitTermination, - } - - a_type = termination_types.get(request.GET.get('a_terminations_type')) - b_type = termination_types.get(request.GET.get('b_terminations_type')) - - self.form = forms.get_cable_form(a_type, b_type) + self.form = forms.get_cable_form( + a_type=CABLE_TERMINATION_TYPES.get(request.GET.get('a_terminations_type')), + b_type=CABLE_TERMINATION_TYPES.get(request.GET.get('b_terminations_type')) + ) return super().dispatch(request, *args, **kwargs) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index cc768cbdc..5c4b2813d 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -179,8 +179,8 @@ class ExceptionHandlingMiddleware: def process_exception(self, request, exception): # Handle exceptions that occur from REST API requests - if is_api_request(request): - return rest_api_server_error(request) + # if is_api_request(request): + # return rest_api_server_error(request) # Don't catch exceptions when in debug mode if settings.DEBUG: diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index 5528faec2..666e3d28a 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -3,7 +3,6 @@ import sys from django.conf import settings from django.core.cache import cache -from django.db.models import F from django.http import HttpResponseServerError from django.shortcuts import redirect, render from django.template import loader @@ -37,13 +36,13 @@ class HomeView(View): return redirect("login") connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( - _path__is_active=True + _path__is_complete=True ) connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( - _path__is_active=True + _path__is_complete=True ) connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter( - _path__is_active=True + _path__is_complete=True ) def build_stats():