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