From 3362bc3106095748d83aa908fa65f79e42c57717 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 1 Jun 2022 16:48:56 -0400 Subject: [PATCH] Remove link peer fields from cable termination models --- netbox/circuits/api/serializers.py | 2 +- netbox/circuits/api/views.py | 2 +- netbox/circuits/filtersets.py | 2 +- .../migrations/0036_new_cabling_models.py | 16 +++ .../migrations/0037_cabling_cleanup.py | 20 +++ netbox/dcim/api/serializers.py | 52 +++++--- netbox/dcim/api/views.py | 18 +-- netbox/dcim/choices.py | 3 +- netbox/dcim/filtersets.py | 18 +-- .../dcim/migrations/0154_cabletermination.py | 30 ----- .../migrations/0154_new_cabling_models.py | 91 +++++++++++++ .../0155_populate_cable_terminations.py | 2 +- .../0156_cable_remove_terminations.py | 37 ----- ...e_path.py => 0156_populate_cable_paths.py} | 2 +- netbox/dcim/migrations/0157_cablepath.py | 28 ---- .../migrations/0157_populate_cable_ends.py | 42 ++++++ .../dcim/migrations/0158_cabling_cleanup.py | 126 ++++++++++++++++++ ...159_cablepath_remove_origin_destination.py | 33 ----- netbox/dcim/models/cables.py | 5 +- netbox/dcim/models/device_components.py | 39 +++--- netbox/dcim/signals.py | 2 +- netbox/dcim/tests/test_models.py | 12 +- .../circuits/inc/circuit_termination.html | 9 +- netbox/wireless/signals.py | 14 +- 24 files changed, 389 insertions(+), 216 deletions(-) create mode 100644 netbox/circuits/migrations/0036_new_cabling_models.py create mode 100644 netbox/circuits/migrations/0037_cabling_cleanup.py delete mode 100644 netbox/dcim/migrations/0154_cabletermination.py create mode 100644 netbox/dcim/migrations/0154_new_cabling_models.py delete mode 100644 netbox/dcim/migrations/0156_cable_remove_terminations.py rename netbox/dcim/migrations/{0158_cablepath_populate_path.py => 0156_populate_cable_paths.py} (96%) delete mode 100644 netbox/dcim/migrations/0157_cablepath.py create mode 100644 netbox/dcim/migrations/0157_populate_cable_ends.py create mode 100644 netbox/dcim/migrations/0158_cabling_cleanup.py delete mode 100644 netbox/dcim/migrations/0159_cablepath_remove_origin_destination.py diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 19570f067..eac411261 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -109,6 +109,6 @@ class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSeri model = CircuitTermination fields = [ 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', - 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', + 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peers', 'link_peers_type', '_occupied', 'created', 'last_updated', ] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 3573c05e3..f5f3f0fab 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -58,7 +58,7 @@ class CircuitViewSet(NetBoxModelViewSet): class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet): queryset = CircuitTermination.objects.prefetch_related( - 'circuit', 'site', 'provider_network', 'cable' + 'circuit', 'site', 'provider_network', 'cable__terminations' ) serializer_class = serializers.CircuitTerminationSerializer filterset_class = filtersets.CircuitTerminationFilterSet diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 2087d90d2..4985d0529 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -224,7 +224,7 @@ class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CabledObjectFilter class Meta: model = CircuitTermination - fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description'] + fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'cable_end'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/circuits/migrations/0036_new_cabling_models.py b/netbox/circuits/migrations/0036_new_cabling_models.py new file mode 100644 index 000000000..7e5a89093 --- /dev/null +++ b/netbox/circuits/migrations/0036_new_cabling_models.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0035_provider_asns'), + ] + + operations = [ + migrations.AddField( + model_name='circuittermination', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + ] diff --git a/netbox/circuits/migrations/0037_cabling_cleanup.py b/netbox/circuits/migrations/0037_cabling_cleanup.py new file mode 100644 index 000000000..a7f550749 --- /dev/null +++ b/netbox/circuits/migrations/0037_cabling_cleanup.py @@ -0,0 +1,20 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0036_new_cabling_models'), + ('dcim', '0157_populate_cable_ends'), + ] + + operations = [ + migrations.RemoveField( + model_name='circuittermination', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='circuittermination', + name='_link_peer_type', + ), + ] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 53f5d1327..fe7c0ba2f 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -27,25 +27,37 @@ from .nested_serializers import * class LinkTerminationSerializer(serializers.ModelSerializer): - link_peer_type = serializers.SerializerMethodField(read_only=True) - link_peer = serializers.SerializerMethodField(read_only=True) + link_peers_type = serializers.SerializerMethodField(read_only=True) + link_peers = serializers.SerializerMethodField(read_only=True) _occupied = serializers.SerializerMethodField(read_only=True) - def get_link_peer_type(self, obj): - if obj._link_peer is not None: - return f'{obj._link_peer._meta.app_label}.{obj._link_peer._meta.model_name}' + def get_link_peers_type(self, obj): + """ + Return the type of the peer link terminations, or None. + """ + if not obj.cable: + return None + + if obj.link_peers: + return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}' + return None - @swagger_serializer_method(serializer_or_field=serializers.DictField) - def get_link_peer(self, obj): + @swagger_serializer_method(serializer_or_field=serializers.ListField) + def get_link_peers(self, obj): """ Return the appropriate serializer for the link termination model. """ - if obj._link_peer is not None: - serializer = get_serializer_for_model(obj._link_peer, prefix='Nested') + if not obj.cable: + 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_peer, context=context).data - return None + return serializer(obj.link_peers, context=context, many=True).data + + return [] @swagger_serializer_method(serializer_or_field=serializers.BooleanField) def get__occupied(self, obj): @@ -96,7 +108,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer): if endpoints: return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}' - @swagger_serializer_method(serializer_or_field=serializers.DictField) + @swagger_serializer_method(serializer_or_field=serializers.ListField) def get_connected_endpoints(self, obj): """ Return the appropriate serializer for the type of connected object. @@ -715,7 +727,7 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializ model = ConsoleServerPort fields = [ 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', - 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoints', 'connected_endpoints_type', + 'mark_connected', 'cable', 'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] @@ -743,7 +755,7 @@ class ConsolePortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Co model = ConsolePort fields = [ 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', - 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoints', 'connected_endpoints_type', + 'mark_connected', 'cable', 'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] @@ -777,7 +789,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Co model = PowerOutlet fields = [ 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', - 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoints', + 'description', 'mark_connected', 'cable', 'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] @@ -801,7 +813,7 @@ class PowerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn model = PowerPort fields = [ 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', - 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoints', + 'description', 'mark_connected', 'cable', 'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] @@ -847,7 +859,7 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', - 'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', 'vrf', 'connected_endpoints', + 'cable', 'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', ] @@ -880,7 +892,7 @@ class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer): model = RearPort fields = [ 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', - 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created', + 'mark_connected', 'cable', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] @@ -911,7 +923,7 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer): model = FrontPort fields = [ 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', + 'rear_port_position', 'description', 'mark_connected', 'cable', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] @@ -1193,7 +1205,7 @@ class PowerFeedSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn model = PowerFeed fields = [ 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', - 'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', + 'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 363523819..ef549caa6 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -546,7 +546,7 @@ class ModuleViewSet(NetBoxModelViewSet): class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet): queryset = ConsolePort.objects.prefetch_related( - 'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags' + 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags' ) serializer_class = serializers.ConsolePortSerializer filterset_class = filtersets.ConsolePortFilterSet @@ -555,7 +555,7 @@ class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet): class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet): queryset = ConsoleServerPort.objects.prefetch_related( - 'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags' + 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags' ) serializer_class = serializers.ConsoleServerPortSerializer filterset_class = filtersets.ConsoleServerPortFilterSet @@ -564,7 +564,7 @@ class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet): class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet): queryset = PowerPort.objects.prefetch_related( - 'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags' + 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags' ) serializer_class = serializers.PowerPortSerializer filterset_class = filtersets.PowerPortFilterSet @@ -573,7 +573,7 @@ class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet): class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet): queryset = PowerOutlet.objects.prefetch_related( - 'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags' + 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags' ) serializer_class = serializers.PowerOutletSerializer filterset_class = filtersets.PowerOutletFilterSet @@ -582,8 +582,8 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet): class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet): queryset = Interface.objects.prefetch_related( - 'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable', '_link_peer', - 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags' + 'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans', + 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags' ) serializer_class = serializers.InterfaceSerializer filterset_class = filtersets.InterfaceFilterSet @@ -592,7 +592,7 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet): class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet): queryset = FrontPort.objects.prefetch_related( - 'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags' + 'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable__terminations', 'tags' ) serializer_class = serializers.FrontPortSerializer filterset_class = filtersets.FrontPortFilterSet @@ -601,7 +601,7 @@ class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet): class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet): queryset = RearPort.objects.prefetch_related( - 'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags' + 'device__device_type__manufacturer', 'module__module_bay', 'cable__terminations', 'tags' ) serializer_class = serializers.RearPortSerializer filterset_class = filtersets.RearPortFilterSet @@ -691,7 +691,7 @@ class PowerPanelViewSet(NetBoxModelViewSet): class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet): queryset = PowerFeed.objects.prefetch_related( - 'power_panel', 'rack', '_path', 'cable', '_link_peer', 'tags' + 'power_panel', 'rack', '_path', 'cable__terminations', 'tags' ) serializer_class = serializers.PowerFeedSerializer filterset_class = filtersets.PowerFeedFilterSet diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 5a351bb23..4b0dd6c48 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1226,7 +1226,8 @@ class CableEndChoices(ChoiceSet): CHOICES = ( (SIDE_A, 'A'), - (SIDE_B, 'B') + (SIDE_B, 'B'), + ('', ''), ) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index a80062526..6bb95a981 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1141,7 +1141,7 @@ class ConsolePortFilterSet( class Meta: model = ConsolePort - fields = ['id', 'name', 'label', 'description'] + fields = ['id', 'name', 'label', 'description', 'cable_end'] class ConsoleServerPortFilterSet( @@ -1157,7 +1157,7 @@ class ConsoleServerPortFilterSet( class Meta: model = ConsoleServerPort - fields = ['id', 'name', 'label', 'description'] + fields = ['id', 'name', 'label', 'description', 'cable_end'] class PowerPortFilterSet( @@ -1173,7 +1173,7 @@ class PowerPortFilterSet( class Meta: model = PowerPort - fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description'] + fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'cable_end'] class PowerOutletFilterSet( @@ -1193,7 +1193,7 @@ class PowerOutletFilterSet( class Meta: model = PowerOutlet - fields = ['id', 'name', 'label', 'feed_leg', 'description'] + fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end'] class InterfaceFilterSet( @@ -1273,7 +1273,7 @@ class InterfaceFilterSet( model = Interface fields = [ 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel', - 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', + 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end', ] def filter_device(self, queryset, name, value): @@ -1336,7 +1336,7 @@ class FrontPortFilterSet( class Meta: model = FrontPort - fields = ['id', 'name', 'label', 'type', 'color', 'description'] + fields = ['id', 'name', 'label', 'type', 'color', 'description', 'cable_end'] class RearPortFilterSet( @@ -1351,7 +1351,7 @@ class RearPortFilterSet( class Meta: model = RearPort - fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description'] + fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description', 'cable_end'] class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): @@ -1679,7 +1679,9 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi class Meta: model = PowerFeed - fields = ['id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'] + fields = [ + 'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end', + ] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/migrations/0154_cabletermination.py b/netbox/dcim/migrations/0154_cabletermination.py deleted file mode 100644 index 88245d483..000000000 --- a/netbox/dcim/migrations/0154_cabletermination.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('dcim', '0153_created_datetimefield'), - ] - - operations = [ - migrations.CreateModel( - name='CableTermination', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('cable_end', models.CharField(max_length=1)), - ('termination_id', models.PositiveBigIntegerField()), - ('cable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='dcim.cable')), - ('termination_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), - ], - options={ - 'ordering': ('cable', 'cable_end', 'pk'), - }, - ), - migrations.AddConstraint( - model_name='cabletermination', - constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='unique_termination'), - ), - ] diff --git a/netbox/dcim/migrations/0154_new_cabling_models.py b/netbox/dcim/migrations/0154_new_cabling_models.py new file mode 100644 index 000000000..6d8bd9ab6 --- /dev/null +++ b/netbox/dcim/migrations/0154_new_cabling_models.py @@ -0,0 +1,91 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('dcim', '0153_created_datetimefield'), + ] + + operations = [ + + # Create CableTermination model + migrations.CreateModel( + name='CableTermination', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('cable_end', models.CharField(max_length=1)), + ('termination_id', models.PositiveBigIntegerField()), + ('cable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='dcim.cable')), + ('termination_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')), + ], + options={ + 'ordering': ('cable', 'cable_end', 'pk'), + }, + ), + migrations.AddConstraint( + model_name='cabletermination', + constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='unique_termination'), + ), + + # Update CablePath model + migrations.RenameField( + model_name='cablepath', + old_name='path', + new_name='_nodes', + ), + migrations.AddField( + model_name='cablepath', + name='path', + field=models.JSONField(default=list), + ), + migrations.AddField( + model_name='cablepath', + name='is_complete', + field=models.BooleanField(default=False), + ), + + # Add cable_end field to cable termination models + migrations.AddField( + model_name='consoleport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='consoleserverport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='frontport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='interface', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='powerfeed', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='poweroutlet', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='powerport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + migrations.AddField( + model_name='rearport', + name='cable_end', + field=models.CharField(blank=True, max_length=1), + ), + ] diff --git a/netbox/dcim/migrations/0155_populate_cable_terminations.py b/netbox/dcim/migrations/0155_populate_cable_terminations.py index c570f240b..64d0caf6c 100644 --- a/netbox/dcim/migrations/0155_populate_cable_terminations.py +++ b/netbox/dcim/migrations/0155_populate_cable_terminations.py @@ -40,7 +40,7 @@ def populate_cable_terminations(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('dcim', '0154_cabletermination'), + ('dcim', '0154_new_cabling_models'), ] operations = [ diff --git a/netbox/dcim/migrations/0156_cable_remove_terminations.py b/netbox/dcim/migrations/0156_cable_remove_terminations.py deleted file mode 100644 index 268224717..000000000 --- a/netbox/dcim/migrations/0156_cable_remove_terminations.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.0.4 on 2022-04-29 14:05 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0155_populate_cable_terminations'), - ] - - operations = [ - migrations.AlterModelOptions( - name='cable', - options={'ordering': ('pk',)}, - ), - migrations.AlterUniqueTogether( - name='cable', - unique_together=set(), - ), - migrations.RemoveField( - model_name='cable', - name='termination_a_id', - ), - migrations.RemoveField( - model_name='cable', - name='termination_a_type', - ), - migrations.RemoveField( - model_name='cable', - name='termination_b_id', - ), - migrations.RemoveField( - model_name='cable', - name='termination_b_type', - ), - ] diff --git a/netbox/dcim/migrations/0158_cablepath_populate_path.py b/netbox/dcim/migrations/0156_populate_cable_paths.py similarity index 96% rename from netbox/dcim/migrations/0158_cablepath_populate_path.py rename to netbox/dcim/migrations/0156_populate_cable_paths.py index 47113513f..efc0ab514 100644 --- a/netbox/dcim/migrations/0158_cablepath_populate_path.py +++ b/netbox/dcim/migrations/0156_populate_cable_paths.py @@ -39,7 +39,7 @@ def populate_cable_paths(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('dcim', '0157_cablepath'), + ('dcim', '0155_populate_cable_terminations'), ] operations = [ diff --git a/netbox/dcim/migrations/0157_cablepath.py b/netbox/dcim/migrations/0157_cablepath.py deleted file mode 100644 index a40f6a10e..000000000 --- a/netbox/dcim/migrations/0157_cablepath.py +++ /dev/null @@ -1,28 +0,0 @@ -import dcim.fields -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0156_cable_remove_terminations'), - ] - - operations = [ - migrations.RenameField( - model_name='cablepath', - old_name='path', - new_name='_nodes', - ), - migrations.AddField( - model_name='cablepath', - name='path', - field=models.JSONField(default=list), - ), - migrations.AddField( - model_name='cablepath', - name='is_complete', - field=models.BooleanField(default=False), - ), - ] diff --git a/netbox/dcim/migrations/0157_populate_cable_ends.py b/netbox/dcim/migrations/0157_populate_cable_ends.py new file mode 100644 index 000000000..3bff31a1d --- /dev/null +++ b/netbox/dcim/migrations/0157_populate_cable_ends.py @@ -0,0 +1,42 @@ +from django.db import migrations + + +def populate_cable_terminations(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + ContentType = apps.get_model('contenttypes', 'ContentType') + + cable_termination_models = ( + apps.get_model('dcim', 'ConsolePort'), + apps.get_model('dcim', 'ConsoleServerPort'), + apps.get_model('dcim', 'PowerPort'), + apps.get_model('dcim', 'PowerOutlet'), + apps.get_model('dcim', 'Interface'), + apps.get_model('dcim', 'FrontPort'), + apps.get_model('dcim', 'RearPort'), + apps.get_model('dcim', 'PowerFeed'), + apps.get_model('circuits', 'CircuitTermination'), + ) + + for model in cable_termination_models: + ct = ContentType.objects.get_for_model(model) + model.objects.filter( + id__in=Cable.objects.filter(termination_a_type=ct).values_list('termination_a_id', flat=True) + ).update(cable_end='A') + model.objects.filter( + id__in=Cable.objects.filter(termination_b_type=ct).values_list('termination_b_id', flat=True) + ).update(cable_end='B') + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0036_new_cabling_models'), + ('dcim', '0156_populate_cable_paths'), + ] + + operations = [ + migrations.RunPython( + code=populate_cable_terminations, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/migrations/0158_cabling_cleanup.py b/netbox/dcim/migrations/0158_cabling_cleanup.py new file mode 100644 index 000000000..51c4b6a42 --- /dev/null +++ b/netbox/dcim/migrations/0158_cabling_cleanup.py @@ -0,0 +1,126 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0157_populate_cable_ends'), + ] + + operations = [ + + # Remove old fields from Cable + migrations.AlterModelOptions( + name='cable', + options={'ordering': ('pk',)}, + ), + migrations.AlterUniqueTogether( + name='cable', + unique_together=set(), + ), + migrations.RemoveField( + model_name='cable', + name='termination_a_id', + ), + migrations.RemoveField( + model_name='cable', + name='termination_a_type', + ), + migrations.RemoveField( + model_name='cable', + name='termination_b_id', + ), + migrations.RemoveField( + model_name='cable', + name='termination_b_type', + ), + + # Remove old fields from CablePath + migrations.AlterUniqueTogether( + name='cablepath', + unique_together=set(), + ), + migrations.RemoveField( + model_name='cablepath', + name='destination_id', + ), + migrations.RemoveField( + model_name='cablepath', + name='destination_type', + ), + migrations.RemoveField( + model_name='cablepath', + name='origin_id', + ), + migrations.RemoveField( + model_name='cablepath', + name='origin_type', + ), + + # Remove link peer type/ID fields from cable termination models + migrations.RemoveField( + model_name='consoleport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='consoleport', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='consoleserverport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='consoleserverport', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='frontport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='frontport', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='interface', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='interface', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='powerfeed', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='powerfeed', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='poweroutlet', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='poweroutlet', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='powerport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='powerport', + name='_link_peer_type', + ), + migrations.RemoveField( + model_name='rearport', + name='_link_peer_id', + ), + migrations.RemoveField( + model_name='rearport', + name='_link_peer_type', + ), + + ] diff --git a/netbox/dcim/migrations/0159_cablepath_remove_origin_destination.py b/netbox/dcim/migrations/0159_cablepath_remove_origin_destination.py deleted file mode 100644 index 12d585bf0..000000000 --- a/netbox/dcim/migrations/0159_cablepath_remove_origin_destination.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-03 14:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0158_cablepath_populate_path'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='cablepath', - unique_together=set(), - ), - migrations.RemoveField( - model_name='cablepath', - name='destination_id', - ), - migrations.RemoveField( - model_name='cablepath', - name='destination_type', - ), - migrations.RemoveField( - model_name='cablepath', - name='origin_id', - ), - migrations.RemoveField( - model_name='cablepath', - name='origin_type', - ), - ] diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index a1d9d3ea9..32250b8bd 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -307,7 +307,10 @@ class CableTermination(models.Model): # Set the cable on the terminating object termination_model = self.termination._meta.model - termination_model.objects.filter(pk=self.termination_id).update(cable=self.cable) + termination_model.objects.filter(pk=self.termination_id).update( + cable=self.cable, + cable_end=self.cable_end + ) def delete(self, *args, **kwargs): diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 9bc444203..895614f32 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1,6 +1,6 @@ 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.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Sum @@ -105,12 +105,7 @@ class ModularComponentModel(ComponentModel): class LinkTermination(models.Model): """ - An abstract model inherited by all models to which a Cable, WirelessLink, or other such link can terminate. Examples - include most device components, CircuitTerminations, and PowerFeeds. The `cable` and `wireless_link` fields - reference the attached Cable or WirelessLink instance, respectively. - - `_link_peer` is a GenericForeignKey used to cache the far-end LinkTermination on the local instance; this is a - shortcut to referencing `instance.link.termination_b`, for example. + An abstract model inherited by all models to which a Cable can terminate. """ cable = models.ForeignKey( to='dcim.Cable', @@ -119,20 +114,10 @@ class LinkTermination(models.Model): blank=True, null=True ) - _link_peer_type = models.ForeignKey( - to=ContentType, - on_delete=models.SET_NULL, - related_name='+', + cable_end = models.CharField( + max_length=1, blank=True, - null=True - ) - _link_peer_id = models.PositiveBigIntegerField( - blank=True, - null=True - ) - _link_peer = GenericForeignKey( - ct_field='_link_peer_type', - fk_field='_link_peer_id' + choices=CableEndChoices ) mark_connected = models.BooleanField( default=False, @@ -145,13 +130,23 @@ class LinkTermination(models.Model): def clean(self): super().clean() + if self.cable and not self.cable_end: + raise ValidationError({ + "cable_end": "Must specify cable end (A or B) when attaching a cable." + }) + if self.mark_connected and self.cable_id: raise ValidationError({ "mark_connected": "Cannot mark as connected with a cable attached." }) - def get_link_peer(self): - return self._link_peer + @property + def link_peers(self): + # TODO: Support WirelessLinks + if not self.cable: + return [] + peer_terminations = self.cable.terminations.exclude(cable_end=self.cable_end).prefetch_related('termination') + return [ct.termination for ct in peer_terminations] @property def _occupied(self): diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 9ea8dd9f8..9d249a93e 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -132,4 +132,4 @@ def nullify_connected_endpoints(instance, **kwargs): Disassociate the Cable from the termination object. """ model = instance.termination_type.model_class() - model.objects.filter(pk=instance.termination_id).update(_link_peer_type=None, _link_peer_id=None) + model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='') diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index e3d775b23..84347c4f2 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -503,8 +503,12 @@ class CableTestCase(TestCase): """ self.interface1.refresh_from_db() self.interface2.refresh_from_db() - self.assertEqual(self.interface1._link_peer, self.interface2) - self.assertEqual(self.interface2._link_peer, self.interface1) + self.assertEqual(self.interface1.cable, self.cable) + self.assertEqual(self.interface2.cable, self.cable) + self.assertEqual(self.interface1.cable_end, 'A') + self.assertEqual(self.interface2.cable_end, 'B') + self.assertEqual(self.interface1.link_peers, [self.interface2]) + self.assertEqual(self.interface2.link_peers, [self.interface1]) def test_cable_deletion(self): """ @@ -516,10 +520,10 @@ class CableTestCase(TestCase): self.assertNotEqual(str(self.cable), '#None') interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertIsNone(interface1.cable) - self.assertIsNone(interface1._link_peer) + self.assertListEqual(interface1.link_peers, []) interface2 = Interface.objects.get(pk=self.interface2.pk) self.assertIsNone(interface2.cable) - self.assertIsNone(interface2._link_peer) + self.assertListEqual(interface2.link_peers, []) def test_cable_validates_same_parent_object(self): """ diff --git a/netbox/templates/circuits/inc/circuit_termination.html b/netbox/templates/circuits/inc/circuit_termination.html index 6cfcac05c..0614dc49e 100644 --- a/netbox/templates/circuits/inc/circuit_termination.html +++ b/netbox/templates/circuits/inc/circuit_termination.html @@ -44,16 +44,15 @@ Marked as connected {% elif termination.cable %} - {{ termination.cable }} - {% with peer=termination.get_link_peer %} - to + {{ termination.cable }} to + {% for peer in termination.link_peers %} {% if peer.device %} {{ peer.device|linkify }}
{% elif peer.circuit %} {{ peer.circuit|linkify }}
{% endif %} - {{ peer|linkify }} - {% endwith %} + {{ peer|linkify }}{% if not forloop.last %},{% endif %} + {% endfor %}
Trace diff --git a/netbox/wireless/signals.py b/netbox/wireless/signals.py index 015428807..ff7b1229c 100644 --- a/netbox/wireless/signals.py +++ b/netbox/wireless/signals.py @@ -25,12 +25,10 @@ def update_connected_interfaces(instance, created, raw=False, **kwargs): if instance.interface_a.wireless_link != instance: logger.debug(f"Updating interface A for wireless link {instance}") instance.interface_a.wireless_link = instance - instance.interface_a._link_peer = instance.interface_b instance.interface_a.save() if instance.interface_b.cable != instance: logger.debug(f"Updating interface B for wireless link {instance}") instance.interface_b.wireless_link = instance - instance.interface_b._link_peer = instance.interface_a instance.interface_b.save() # Create/update cable paths @@ -48,18 +46,10 @@ def nullify_connected_interfaces(instance, **kwargs): if instance.interface_a is not None: logger.debug(f"Nullifying interface A for wireless link {instance}") - Interface.objects.filter(pk=instance.interface_a.pk).update( - wireless_link=None, - _link_peer_type=None, - _link_peer_id=None - ) + Interface.objects.filter(pk=instance.interface_a.pk).update(wireless_link=None) if instance.interface_b is not None: logger.debug(f"Nullifying interface B for wireless link {instance}") - Interface.objects.filter(pk=instance.interface_b.pk).update( - wireless_link=None, - _link_peer_type=None, - _link_peer_id=None - ) + Interface.objects.filter(pk=instance.interface_b.pk).update(wireless_link=None) # Delete and retrace any dependent cable paths for cablepath in CablePath.objects.filter(_nodes__contains=instance):