Remove link peer fields from cable termination models

This commit is contained in:
jeremystretch 2022-06-01 16:48:56 -04:00
parent 6befd2155a
commit 3362bc3106
24 changed files with 389 additions and 216 deletions

View File

@ -109,6 +109,6 @@ class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSeri
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', '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', '_occupied', 'created', 'last_updated',
] ]

View File

@ -58,7 +58,7 @@ class CircuitViewSet(NetBoxModelViewSet):
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet): class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = CircuitTermination.objects.prefetch_related( queryset = CircuitTermination.objects.prefetch_related(
'circuit', 'site', 'provider_network', 'cable' 'circuit', 'site', 'provider_network', 'cable__terminations'
) )
serializer_class = serializers.CircuitTerminationSerializer serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filtersets.CircuitTerminationFilterSet filterset_class = filtersets.CircuitTerminationFilterSet

View File

@ -224,7 +224,7 @@ class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CabledObjectFilter
class Meta: class Meta:
model = CircuitTermination 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): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -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),
),
]

View File

@ -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',
),
]

View File

@ -27,25 +27,37 @@ from .nested_serializers import *
class LinkTerminationSerializer(serializers.ModelSerializer): class LinkTerminationSerializer(serializers.ModelSerializer):
link_peer_type = serializers.SerializerMethodField(read_only=True) link_peers_type = serializers.SerializerMethodField(read_only=True)
link_peer = serializers.SerializerMethodField(read_only=True) link_peers = serializers.SerializerMethodField(read_only=True)
_occupied = serializers.SerializerMethodField(read_only=True) _occupied = serializers.SerializerMethodField(read_only=True)
def get_link_peer_type(self, obj): def get_link_peers_type(self, obj):
if obj._link_peer is not None: """
return f'{obj._link_peer._meta.app_label}.{obj._link_peer._meta.model_name}' Return the type of the peer link terminations, or None.
"""
if not obj.cable:
return None return None
@swagger_serializer_method(serializer_or_field=serializers.DictField) if obj.link_peers:
def get_link_peer(self, obj): 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.ListField)
def get_link_peers(self, obj):
""" """
Return the appropriate serializer for the link termination model. Return the appropriate serializer for the link termination model.
""" """
if obj._link_peer is not None: if not obj.cable:
serializer = get_serializer_for_model(obj._link_peer, prefix='Nested') 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']} context = {'request': self.context['request']}
return serializer(obj._link_peer, context=context).data return serializer(obj.link_peers, context=context, many=True).data
return None
return []
@swagger_serializer_method(serializer_or_field=serializers.BooleanField) @swagger_serializer_method(serializer_or_field=serializers.BooleanField)
def get__occupied(self, obj): def get__occupied(self, obj):
@ -96,7 +108,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
if endpoints: if endpoints:
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}' 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): def get_connected_endpoints(self, obj):
""" """
Return the appropriate serializer for the type of connected object. Return the appropriate serializer for the type of connected object.
@ -715,7 +727,7 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializ
model = ConsoleServerPort model = ConsoleServerPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', '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', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
@ -743,7 +755,7 @@ class ConsolePortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Co
model = ConsolePort model = ConsolePort
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', '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', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
@ -777,7 +789,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Co
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', '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', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied', 'last_updated', '_occupied',
] ]
@ -801,7 +813,7 @@ class PowerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', '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', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied', 'last_updated', '_occupied',
] ]
@ -847,7 +859,7 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', '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', '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', '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', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
] ]
@ -880,7 +892,7 @@ class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
model = RearPort model = RearPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', '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', 'last_updated', '_occupied',
] ]
@ -911,7 +923,7 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
model = FrontPort model = FrontPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', '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', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
@ -1193,7 +1205,7 @@ class PowerFeedSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
model = PowerFeed model = PowerFeed
fields = [ fields = [
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', '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', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied', 'created', 'last_updated', '_occupied',
] ]

View File

@ -546,7 +546,7 @@ class ModuleViewSet(NetBoxModelViewSet):
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet): class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsolePort.objects.prefetch_related( 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 serializer_class = serializers.ConsolePortSerializer
filterset_class = filtersets.ConsolePortFilterSet filterset_class = filtersets.ConsolePortFilterSet
@ -555,7 +555,7 @@ class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet): class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related( 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 serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filtersets.ConsoleServerPortFilterSet filterset_class = filtersets.ConsoleServerPortFilterSet
@ -564,7 +564,7 @@ class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet): class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerPort.objects.prefetch_related( 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 serializer_class = serializers.PowerPortSerializer
filterset_class = filtersets.PowerPortFilterSet filterset_class = filtersets.PowerPortFilterSet
@ -573,7 +573,7 @@ class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet): class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerOutlet.objects.prefetch_related( 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 serializer_class = serializers.PowerOutletSerializer
filterset_class = filtersets.PowerOutletFilterSet filterset_class = filtersets.PowerOutletFilterSet
@ -582,8 +582,8 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet): class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable', '_link_peer', 'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags' 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filtersets.InterfaceFilterSet filterset_class = filtersets.InterfaceFilterSet
@ -592,7 +592,7 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet): class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = FrontPort.objects.prefetch_related( 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 serializer_class = serializers.FrontPortSerializer
filterset_class = filtersets.FrontPortFilterSet filterset_class = filtersets.FrontPortFilterSet
@ -601,7 +601,7 @@ class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet): class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = RearPort.objects.prefetch_related( 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 serializer_class = serializers.RearPortSerializer
filterset_class = filtersets.RearPortFilterSet filterset_class = filtersets.RearPortFilterSet
@ -691,7 +691,7 @@ class PowerPanelViewSet(NetBoxModelViewSet):
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet): class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerFeed.objects.prefetch_related( queryset = PowerFeed.objects.prefetch_related(
'power_panel', 'rack', '_path', 'cable', '_link_peer', 'tags' 'power_panel', 'rack', '_path', 'cable__terminations', 'tags'
) )
serializer_class = serializers.PowerFeedSerializer serializer_class = serializers.PowerFeedSerializer
filterset_class = filtersets.PowerFeedFilterSet filterset_class = filtersets.PowerFeedFilterSet

View File

@ -1226,7 +1226,8 @@ class CableEndChoices(ChoiceSet):
CHOICES = ( CHOICES = (
(SIDE_A, 'A'), (SIDE_A, 'A'),
(SIDE_B, 'B') (SIDE_B, 'B'),
('', ''),
) )

View File

@ -1141,7 +1141,7 @@ class ConsolePortFilterSet(
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ['id', 'name', 'label', 'description'] fields = ['id', 'name', 'label', 'description', 'cable_end']
class ConsoleServerPortFilterSet( class ConsoleServerPortFilterSet(
@ -1157,7 +1157,7 @@ class ConsoleServerPortFilterSet(
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = ['id', 'name', 'label', 'description'] fields = ['id', 'name', 'label', 'description', 'cable_end']
class PowerPortFilterSet( class PowerPortFilterSet(
@ -1173,7 +1173,7 @@ class PowerPortFilterSet(
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description'] fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'cable_end']
class PowerOutletFilterSet( class PowerOutletFilterSet(
@ -1193,7 +1193,7 @@ class PowerOutletFilterSet(
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ['id', 'name', 'label', 'feed_leg', 'description'] fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end']
class InterfaceFilterSet( class InterfaceFilterSet(
@ -1273,7 +1273,7 @@ class InterfaceFilterSet(
model = Interface model = Interface
fields = [ fields = [
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel', '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): def filter_device(self, queryset, name, value):
@ -1336,7 +1336,7 @@ class FrontPortFilterSet(
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = ['id', 'name', 'label', 'type', 'color', 'description'] fields = ['id', 'name', 'label', 'type', 'color', 'description', 'cable_end']
class RearPortFilterSet( class RearPortFilterSet(
@ -1351,7 +1351,7 @@ class RearPortFilterSet(
class Meta: class Meta:
model = RearPort model = RearPort
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description'] fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description', 'cable_end']
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
@ -1679,7 +1679,9 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
class Meta: class Meta:
model = PowerFeed 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): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -40,7 +40,7 @@ def populate_cable_terminations(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dcim', '0154_cabletermination'), ('dcim', '0154_new_cabling_models'),
] ]
operations = [ operations = [

View File

@ -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',
),
]

View File

@ -39,7 +39,7 @@ def populate_cable_paths(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dcim', '0157_cablepath'), ('dcim', '0155_populate_cable_terminations'),
] ]
operations = [ operations = [

View File

@ -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),
),
]

View File

@ -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
),
]

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -307,7 +307,10 @@ class CableTermination(models.Model):
# Set the cable on the terminating object # Set the cable on the terminating object
termination_model = self.termination._meta.model 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): def delete(self, *args, **kwargs):

View File

@ -1,6 +1,6 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType 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.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
@ -105,12 +105,7 @@ class ModularComponentModel(ComponentModel):
class LinkTermination(models.Model): class LinkTermination(models.Model):
""" """
An abstract model inherited by all models to which a Cable, WirelessLink, or other such link can terminate. Examples An abstract model inherited by all models to which a Cable can terminate.
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.
""" """
cable = models.ForeignKey( cable = models.ForeignKey(
to='dcim.Cable', to='dcim.Cable',
@ -119,20 +114,10 @@ class LinkTermination(models.Model):
blank=True, blank=True,
null=True null=True
) )
_link_peer_type = models.ForeignKey( cable_end = models.CharField(
to=ContentType, max_length=1,
on_delete=models.SET_NULL,
related_name='+',
blank=True, blank=True,
null=True choices=CableEndChoices
)
_link_peer_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
_link_peer = GenericForeignKey(
ct_field='_link_peer_type',
fk_field='_link_peer_id'
) )
mark_connected = models.BooleanField( mark_connected = models.BooleanField(
default=False, default=False,
@ -145,13 +130,23 @@ class LinkTermination(models.Model):
def clean(self): def clean(self):
super().clean() 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: if self.mark_connected and self.cable_id:
raise ValidationError({ raise ValidationError({
"mark_connected": "Cannot mark as connected with a cable attached." "mark_connected": "Cannot mark as connected with a cable attached."
}) })
def get_link_peer(self): @property
return self._link_peer 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 @property
def _occupied(self): def _occupied(self):

View File

@ -132,4 +132,4 @@ def nullify_connected_endpoints(instance, **kwargs):
Disassociate the Cable from the termination object. Disassociate the Cable from the termination object.
""" """
model = instance.termination_type.model_class() 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='')

View File

@ -503,8 +503,12 @@ class CableTestCase(TestCase):
""" """
self.interface1.refresh_from_db() self.interface1.refresh_from_db()
self.interface2.refresh_from_db() self.interface2.refresh_from_db()
self.assertEqual(self.interface1._link_peer, self.interface2) self.assertEqual(self.interface1.cable, self.cable)
self.assertEqual(self.interface2._link_peer, self.interface1) 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): def test_cable_deletion(self):
""" """
@ -516,10 +520,10 @@ class CableTestCase(TestCase):
self.assertNotEqual(str(self.cable), '#None') self.assertNotEqual(str(self.cable), '#None')
interface1 = Interface.objects.get(pk=self.interface1.pk) interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertIsNone(interface1.cable) self.assertIsNone(interface1.cable)
self.assertIsNone(interface1._link_peer) self.assertListEqual(interface1.link_peers, [])
interface2 = Interface.objects.get(pk=self.interface2.pk) interface2 = Interface.objects.get(pk=self.interface2.pk)
self.assertIsNone(interface2.cable) self.assertIsNone(interface2.cable)
self.assertIsNone(interface2._link_peer) self.assertListEqual(interface2.link_peers, [])
def test_cable_validates_same_parent_object(self): def test_cable_validates_same_parent_object(self):
""" """

View File

@ -44,16 +44,15 @@
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> <span class="text-success"><i class="mdi mdi-check-bold"></i></span>
<span class="text-muted">Marked as connected</span> <span class="text-muted">Marked as connected</span>
{% elif termination.cable %} {% elif termination.cable %}
<a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a> <a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a> to
{% with peer=termination.get_link_peer %} {% for peer in termination.link_peers %}
to
{% if peer.device %} {% if peer.device %}
{{ peer.device|linkify }}<br/> {{ peer.device|linkify }}<br/>
{% elif peer.circuit %} {% elif peer.circuit %}
{{ peer.circuit|linkify }}<br/> {{ peer.circuit|linkify }}<br/>
{% endif %} {% endif %}
{{ peer|linkify }} {{ peer|linkify }}{% if not forloop.last %},{% endif %}
{% endwith %} {% endfor %}
<div class="mt-1"> <div class="mt-1">
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace"> <a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> Trace <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> Trace

View File

@ -25,12 +25,10 @@ def update_connected_interfaces(instance, created, raw=False, **kwargs):
if instance.interface_a.wireless_link != instance: if instance.interface_a.wireless_link != instance:
logger.debug(f"Updating interface A for wireless link {instance}") logger.debug(f"Updating interface A for wireless link {instance}")
instance.interface_a.wireless_link = instance instance.interface_a.wireless_link = instance
instance.interface_a._link_peer = instance.interface_b
instance.interface_a.save() instance.interface_a.save()
if instance.interface_b.cable != instance: if instance.interface_b.cable != instance:
logger.debug(f"Updating interface B for wireless link {instance}") logger.debug(f"Updating interface B for wireless link {instance}")
instance.interface_b.wireless_link = instance instance.interface_b.wireless_link = instance
instance.interface_b._link_peer = instance.interface_a
instance.interface_b.save() instance.interface_b.save()
# Create/update cable paths # Create/update cable paths
@ -48,18 +46,10 @@ def nullify_connected_interfaces(instance, **kwargs):
if instance.interface_a is not None: if instance.interface_a is not None:
logger.debug(f"Nullifying interface A for wireless link {instance}") logger.debug(f"Nullifying interface A for wireless link {instance}")
Interface.objects.filter(pk=instance.interface_a.pk).update( Interface.objects.filter(pk=instance.interface_a.pk).update(wireless_link=None)
wireless_link=None,
_link_peer_type=None,
_link_peer_id=None
)
if instance.interface_b is not None: if instance.interface_b is not None:
logger.debug(f"Nullifying interface B for wireless link {instance}") logger.debug(f"Nullifying interface B for wireless link {instance}")
Interface.objects.filter(pk=instance.interface_b.pk).update( Interface.objects.filter(pk=instance.interface_b.pk).update(wireless_link=None)
wireless_link=None,
_link_peer_type=None,
_link_peer_id=None
)
# Delete and retrace any dependent cable paths # Delete and retrace any dependent cable paths
for cablepath in CablePath.objects.filter(_nodes__contains=instance): for cablepath in CablePath.objects.filter(_nodes__contains=instance):