Jelajahi Sumber

Introduce CableTermination model & migrate data

jeremystretch 3 tahun lalu
induk
melakukan
1f4ad444ae

+ 15 - 0
netbox/dcim/choices.py

@@ -1204,6 +1204,21 @@ class CableLengthUnitChoices(ChoiceSet):
     )
     )
 
 
 
 
+#
+# CableTerminations
+#
+
+class CableEndChoices(ChoiceSet):
+
+    SIDE_A = 'A'
+    SIDE_B = 'B'
+
+    CHOICES = (
+        (SIDE_A, 'A'),
+        (SIDE_B, 'B')
+    )
+
+
 #
 #
 # PowerFeeds
 # PowerFeeds
 #
 #

+ 5 - 5
netbox/dcim/filtersets.py

@@ -1498,10 +1498,10 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
 
 
 
 
 class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
 class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
-    termination_a_type = ContentTypeFilter()
-    termination_a_id = MultiValueNumberFilter()
-    termination_b_type = ContentTypeFilter()
-    termination_b_id = MultiValueNumberFilter()
+    # termination_a_type = ContentTypeFilter()
+    # termination_a_id = MultiValueNumberFilter()
+    # termination_b_type = ContentTypeFilter()
+    # termination_b_id = MultiValueNumberFilter()
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=CableTypeChoices
         choices=CableTypeChoices
     )
     )
@@ -1537,7 +1537,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = Cable
         model = Cable
-        fields = ['id', 'label', 'length', 'length_unit', 'termination_a_id', 'termination_b_id']
+        fields = ['id', 'label', 'length', 'length_unit']
 
 
     def search(self, queryset, name, value):
     def search(self, queryset, name, value):
         if not value.strip():
         if not value.strip():

+ 30 - 0
netbox/dcim/migrations/0154_cabletermination.py

@@ -0,0 +1,30 @@
+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': ['pk'],
+            },
+        ),
+        migrations.AddConstraint(
+            model_name='cabletermination',
+            constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='unique_termination'),
+        ),
+    ]

+ 51 - 0
netbox/dcim/migrations/0155_populate_cable_terminations.py

@@ -0,0 +1,51 @@
+from django.db import migrations
+
+
+def populate_cable_terminations(apps, schema_editor):
+    """
+    Replicate terminations from the Cable model into CableTermination instances.
+    """
+    Cable = apps.get_model('dcim', 'Cable')
+    CableTermination = apps.get_model('dcim', 'CableTermination')
+
+    # Retrieve the necessary data from Cable objects
+    cables = Cable.objects.values(
+        'id', 'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id'
+    )
+
+    # Queue CableTerminations to be created
+    cable_terminations = []
+    for i, cable in enumerate(cables, start=1):
+        cable_terminations.append(
+            CableTermination(
+                cable_id=cable['id'],
+                cable_end='A',
+                termination_type_id=cable['termination_a_type'],
+                termination_id=cable['termination_a_id']
+            )
+        )
+        cable_terminations.append(
+            CableTermination(
+                cable_id=cable['id'],
+                cable_end='B',
+                termination_type_id=cable['termination_b_type'],
+                termination_id=cable['termination_b_id']
+            )
+        )
+
+    # Bulk create the termination objects
+    CableTermination.objects.bulk_create(cable_terminations, batch_size=100)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0154_cabletermination'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            code=populate_cable_terminations,
+            reverse_code=migrations.RunPython.noop
+        ),
+    ]

+ 37 - 0
netbox/dcim/migrations/0156_cable_remove_terminations.py

@@ -0,0 +1,37 @@
+# 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',
+        ),
+    ]

+ 94 - 151
netbox/dcim/models/cables.py

@@ -2,7 +2,6 @@ from collections import defaultdict
 
 
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.postgres.fields import ArrayField
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.db import models
 from django.db import models
 from django.db.models import Sum
 from django.db.models import Sum
@@ -22,6 +21,7 @@ from .device_components import FrontPort, RearPort
 __all__ = (
 __all__ = (
     'Cable',
     'Cable',
     'CablePath',
     'CablePath',
+    'CableTermination',
 )
 )
 
 
 
 
@@ -33,28 +33,6 @@ class Cable(NetBoxModel):
     """
     """
     A physical connection between two endpoints.
     A physical connection between two endpoints.
     """
     """
-    termination_a_type = models.ForeignKey(
-        to=ContentType,
-        limit_choices_to=CABLE_TERMINATION_MODELS,
-        on_delete=models.PROTECT,
-        related_name='+'
-    )
-    termination_a_id = models.PositiveBigIntegerField()
-    termination_a = GenericForeignKey(
-        ct_field='termination_a_type',
-        fk_field='termination_a_id'
-    )
-    termination_b_type = models.ForeignKey(
-        to=ContentType,
-        limit_choices_to=CABLE_TERMINATION_MODELS,
-        on_delete=models.PROTECT,
-        related_name='+'
-    )
-    termination_b_id = models.PositiveBigIntegerField()
-    termination_b = GenericForeignKey(
-        ct_field='termination_b_type',
-        fk_field='termination_b_id'
-    )
     type = models.CharField(
     type = models.CharField(
         max_length=50,
         max_length=50,
         choices=CableTypeChoices,
         choices=CableTypeChoices,
@@ -115,11 +93,7 @@ class Cable(NetBoxModel):
     )
     )
 
 
     class Meta:
     class Meta:
-        ordering = ['pk']
-        unique_together = (
-            ('termination_a_type', 'termination_a_id'),
-            ('termination_b_type', 'termination_b_id'),
-        )
+        ordering = ('pk',)
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
@@ -130,19 +104,19 @@ class Cable(NetBoxModel):
         # Cache the original status so we can check later if it's been changed
         # Cache the original status so we can check later if it's been changed
         self._orig_status = self.status
         self._orig_status = self.status
 
 
-    @classmethod
-    def from_db(cls, db, field_names, values):
-        """
-        Cache the original A and B terminations of existing Cable instances for later reference inside clean().
-        """
-        instance = super().from_db(db, field_names, values)
-
-        instance._orig_termination_a_type_id = instance.termination_a_type_id
-        instance._orig_termination_a_ids = instance.termination_a_ids
-        instance._orig_termination_b_type_id = instance.termination_b_type_id
-        instance._orig_termination_b_ids = instance.termination_b_ids
-
-        return instance
+    # @classmethod
+    # def from_db(cls, db, field_names, values):
+    #     """
+    #     Cache the original A and B terminations of existing Cable instances for later reference inside clean().
+    #     """
+    #     instance = super().from_db(db, field_names, values)
+    #
+    #     instance._orig_termination_a_type_id = instance.termination_a_type_id
+    #     instance._orig_termination_a_ids = instance.termination_a_ids
+    #     instance._orig_termination_b_type_id = instance.termination_b_type_id
+    #     instance._orig_termination_b_ids = instance.termination_b_ids
+    #
+    #     return instance
 
 
     def __str__(self):
     def __str__(self):
         pk = self.pk or self._pk
         pk = self.pk or self._pk
@@ -151,82 +125,9 @@ class Cable(NetBoxModel):
     def get_absolute_url(self):
     def get_absolute_url(self):
         return reverse('dcim:cable', args=[self.pk])
         return reverse('dcim:cable', args=[self.pk])
 
 
-    @property
-    def termination_a(self):
-        if not hasattr(self, 'termination_a_type') or not self.termination_a_ids:
-            return []
-        return list(self.termination_a_type.model_class().objects.filter(pk__in=self.termination_a_ids))
-
-    @property
-    def termination_b(self):
-        if not hasattr(self, 'termination_b_type') or not self.termination_b_ids:
-            return []
-        return list(self.termination_b_type.model_class().objects.filter(pk__in=self.termination_b_ids))
-
     def clean(self):
     def clean(self):
-        from circuits.models import CircuitTermination
-
         super().clean()
         super().clean()
 
 
-        # Validate that termination A exists
-        if not hasattr(self, 'termination_a_type'):
-            raise ValidationError('Termination A type has not been specified')
-        model = self.termination_a_type.model_class()
-        if model.objects.filter(pk__in=self.termination_a_ids).count() != len(self.termination_a_ids):
-            raise ValidationError({
-                'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type)
-            })
-
-        # Validate that termination B exists
-        if not hasattr(self, 'termination_b_type'):
-            raise ValidationError('Termination B type has not been specified')
-        model = self.termination_a_type.model_class()
-        if model.objects.filter(pk__in=self.termination_b_ids).count() != len(self.termination_b_ids):
-            raise ValidationError({
-                'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
-            })
-
-        # If editing an existing Cable instance, check that neither termination has been modified.
-        if self.pk:
-            err_msg = 'Cable termination points may not be modified. Delete and recreate the cable instead.'
-            if (
-                self.termination_a_type_id != self._orig_termination_a_type_id or
-                set(self.termination_a_ids) != set(self._orig_termination_a_ids)
-            ):
-                raise ValidationError({
-                    'termination_a': err_msg
-                })
-            if (
-                self.termination_b_type_id != self._orig_termination_b_type_id or
-                set(self.termination_b_ids) != set(self._orig_termination_b_ids)
-            ):
-                raise ValidationError({
-                    'termination_b': err_msg
-                })
-
-        type_a = self.termination_a_type.model
-        type_b = self.termination_b_type.model
-
-        # Validate interface types
-        if type_a == 'interface':
-            for term in self.termination_a:
-                if term.type in NONCONNECTABLE_IFACE_TYPES:
-                    raise ValidationError({
-                        'termination_a_id': f'Cables cannot be terminated to {term.get_type_display()} interfaces'
-                    })
-        if type_a == 'interface':
-            for term in self.termination_b:
-                if term.type in NONCONNECTABLE_IFACE_TYPES:
-                    raise ValidationError({
-                        'termination_b_id': f'Cables cannot be terminated to {term.get_type_display()} interfaces'
-                    })
-
-        # Check that termination types are compatible
-        if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
-            raise ValidationError(
-                f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
-            )
-
         # TODO: Is this validation still necessary?
         # TODO: Is this validation still necessary?
         # # Check that two connected RearPorts have the same number of positions (if both are >1)
         # # Check that two connected RearPorts have the same number of positions (if both are >1)
         # if isinstance(self.termination_a, RearPort) and isinstance(self.termination_b, RearPort):
         # if isinstance(self.termination_a, RearPort) and isinstance(self.termination_b, RearPort):
@@ -238,38 +139,6 @@ class Cable(NetBoxModel):
         #                 f"Both terminations must have the same number of positions (if greater than one)."
         #                 f"Both terminations must have the same number of positions (if greater than one)."
         #             )
         #             )
 
 
-        # A termination point cannot be connected to itself
-        if set(self.termination_a).intersection(self.termination_b):
-            raise ValidationError(f"Cannot connect {self.termination_a_type} to itself")
-
-        # TODO
-        # # A front port cannot be connected to its corresponding rear port
-        # if (
-        #     type_a in ['frontport', 'rearport'] and
-        #     type_b in ['frontport', 'rearport'] and
-        #     (
-        #         getattr(self.termination_a, 'rear_port', None) == self.termination_b or
-        #         getattr(self.termination_b, 'rear_port', None) == self.termination_a
-        #     )
-        # ):
-        #     raise ValidationError("A front port cannot be connected to it corresponding rear port")
-
-        # TODO
-        # # A CircuitTermination attached to a ProviderNetwork cannot have a Cable
-        # if isinstance(self.termination_a, CircuitTermination) and self.termination_a.provider_network is not None:
-        #     raise ValidationError({
-        #         'termination_a_id': "Circuit terminations attached to a provider network may not be cabled."
-        #     })
-        # if isinstance(self.termination_b, CircuitTermination) and self.termination_b.provider_network is not None:
-        #     raise ValidationError({
-        #         'termination_b_id': "Circuit terminations attached to a provider network may not be cabled."
-        #     })
-
-        # Check for an existing Cable connected to either termination object
-        for term in [*self.termination_a, *self.termination_b]:
-            if term.cable not in (None, self):
-                raise ValidationError(f'{term} already has a cable attached (#{term.cable_id})')
-
         # Validate length and length_unit
         # Validate length and length_unit
         if self.length is not None and not self.length_unit:
         if self.length is not None and not self.length_unit:
             raise ValidationError("Must specify a unit when setting a cable length")
             raise ValidationError("Must specify a unit when setting a cable length")
@@ -284,11 +153,12 @@ class Cable(NetBoxModel):
         else:
         else:
             self._abs_length = None
             self._abs_length = None
 
 
-        # Store the parent Device for the A and B terminations (if applicable) to enable filtering
-        if hasattr(self.termination_a[0], 'device'):
-            self._termination_a_device = self.termination_a[0].device
-        if hasattr(self.termination_b[0], 'device'):
-            self._termination_b_device = self.termination_b[0].device
+        # TODO: Move to CableTermination
+        # # Store the parent Device for the A and B terminations (if applicable) to enable filtering
+        # if hasattr(self.termination_a[0], 'device'):
+        #     self._termination_a_device = self.termination_a[0].device
+        # if hasattr(self.termination_b[0], 'device'):
+        #     self._termination_b_device = self.termination_b[0].device
 
 
         super().save(*args, **kwargs)
         super().save(*args, **kwargs)
 
 
@@ -299,6 +169,79 @@ class Cable(NetBoxModel):
         return LinkStatusChoices.colors.get(self.status)
         return LinkStatusChoices.colors.get(self.status)
 
 
 
 
+class CableTermination(models.Model):
+    """
+    A mapping between side A or B of a Cable and a terminating object (e.g. an Interface or CircuitTermination).
+    """
+    cable = models.ForeignKey(
+        to='dcim.Cable',
+        on_delete=models.CASCADE,
+        related_name='terminations'
+    )
+    cable_end = models.CharField(
+        max_length=1,
+        choices=CableEndChoices,
+        verbose_name='End'
+    )
+    termination_type = models.ForeignKey(
+        to=ContentType,
+        limit_choices_to=CABLE_TERMINATION_MODELS,
+        on_delete=models.PROTECT,
+        related_name='+'
+    )
+    termination_id = models.PositiveBigIntegerField()
+    termination = GenericForeignKey(
+        ct_field='termination_type',
+        fk_field='termination_id'
+    )
+
+    class Meta:
+        ordering = ['pk']
+        constraints = (
+            models.UniqueConstraint(
+                fields=('termination_type', 'termination_id'),
+                name='unique_termination'
+            ),
+        )
+
+    def __str__(self):
+        return f'Cable {self.cable} to {self.termination}'
+
+    def clean(self):
+        super().clean()
+
+        # Validate interface type (if applicable)
+        if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
+            raise ValidationError({
+                'termination': f'Cables cannot be terminated to {self.termination.get_type_display()} interfaces'
+            })
+
+        # A CircuitTermination attached to a ProviderNetwork cannot have a Cable
+        if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
+            raise ValidationError({
+                'termination': "Circuit terminations attached to a provider network may not be cabled."
+            })
+
+        # TODO
+        # # A front port cannot be connected to its corresponding rear port
+        # if (
+        #     type_a in ['frontport', 'rearport'] and
+        #     type_b in ['frontport', 'rearport'] and
+        #     (
+        #         getattr(self.termination_a, 'rear_port', None) == self.termination_b or
+        #         getattr(self.termination_b, 'rear_port', None) == self.termination_a
+        #     )
+        # ):
+        #     raise ValidationError("A front port cannot be connected to it corresponding rear port")
+
+        # TODO
+        # # Check that termination types are compatible
+        # if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
+        #     raise ValidationError(
+        #         f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
+        #     )
+
+
 class CablePath(models.Model):
 class CablePath(models.Model):
     """
     """
     A CablePath instance represents the physical path from an origin to a destination, including all intermediate
     A CablePath instance represents the physical path from an origin to a destination, including all intermediate

+ 52 - 41
netbox/dcim/tables/cables.py

@@ -4,55 +4,67 @@ from django_tables2.utils import Accessor
 from dcim.models import Cable
 from dcim.models import Cable
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
 from tenancy.tables import TenantColumn
 from tenancy.tables import TenantColumn
-from .template_code import CABLE_LENGTH, CABLE_TERMINATION, CABLE_TERMINATION_PARENT
+from .template_code import CABLE_LENGTH
 
 
 __all__ = (
 __all__ = (
     'CableTable',
     'CableTable',
 )
 )
 
 
 
 
+class CableTerminationColumn(tables.TemplateColumn):
+
+    def __init__(self, cable_end, *args, **kwargs):
+        template_code = """
+        {% for term in value.all %}
+          {% if term.cable_end == '""" + cable_end + """' %}
+            <a href="{{ term.termination.get_absolute_url }}">{{ term.termination }}</a>
+          {% endif %}
+        {% endfor %}
+        """
+        super().__init__(template_code=template_code, *args, **kwargs)
+
+    def value(self, value):
+        return ', '.join(value.all())
+
+
 #
 #
 # Cables
 # Cables
 #
 #
 
 
 class CableTable(NetBoxTable):
 class CableTable(NetBoxTable):
-    termination_a_parent = tables.TemplateColumn(
-        template_code=CABLE_TERMINATION_PARENT,
-        accessor=Accessor('termination_a'),
-        orderable=False,
-        verbose_name='Side A'
-    )
-    rack_a = tables.Column(
-        accessor=Accessor('termination_a__device__rack'),
-        orderable=False,
-        linkify=True,
-        verbose_name='Rack A'
-    )
-    termination_a = tables.TemplateColumn(
-        template_code=CABLE_TERMINATION,
-        accessor=Accessor('termination_a'),
-        orderable=False,
-        linkify=True,
-        verbose_name='Termination A'
-    )
-    termination_b_parent = tables.TemplateColumn(
-        template_code=CABLE_TERMINATION_PARENT,
-        accessor=Accessor('termination_b'),
-        orderable=False,
-        verbose_name='Side B'
-    )
-    rack_b = tables.Column(
-        accessor=Accessor('termination_b__device__rack'),
-        orderable=False,
-        linkify=True,
-        verbose_name='Rack B'
+    # termination_a_parent = tables.TemplateColumn(
+    #     template_code=CABLE_TERMINATION_PARENT,
+    #     accessor=Accessor('termination_a'),
+    #     orderable=False,
+    #     verbose_name='Side A'
+    # )
+    # rack_a = tables.Column(
+    #     accessor=Accessor('termination_a__device__rack'),
+    #     orderable=False,
+    #     linkify=True,
+    #     verbose_name='Rack A'
+    # )
+    # termination_b_parent = tables.TemplateColumn(
+    #     template_code=CABLE_TERMINATION_PARENT,
+    #     accessor=Accessor('termination_b'),
+    #     orderable=False,
+    #     verbose_name='Side B'
+    # )
+    # rack_b = tables.Column(
+    #     accessor=Accessor('termination_b__device__rack'),
+    #     orderable=False,
+    #     linkify=True,
+    #     verbose_name='Rack B'
+    # )
+    a_terminations = CableTerminationColumn(
+        cable_end='A',
+        accessor=Accessor('terminations'),
+        orderable=False
     )
     )
-    termination_b = tables.TemplateColumn(
-        template_code=CABLE_TERMINATION,
-        accessor=Accessor('termination_b'),
-        orderable=False,
-        linkify=True,
-        verbose_name='Termination B'
+    b_terminations = CableTerminationColumn(
+        cable_end='B',
+        accessor=Accessor('terminations'),
+        orderable=False
     )
     )
     status = columns.ChoiceFieldColumn()
     status = columns.ChoiceFieldColumn()
     tenant = TenantColumn()
     tenant = TenantColumn()
@@ -68,10 +80,9 @@ class CableTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Cable
         model = Cable
         fields = (
         fields = (
-            'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
-            'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
+            'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'status', 'type', 'tenant', 'color', 'length',
+            'tags', 'created', 'last_updated',
         )
         )
         default_columns = (
         default_columns = (
-            'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
-            'status', 'type',
+            'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'status', 'type',
         )
         )

+ 2 - 2
netbox/dcim/views.py

@@ -12,7 +12,7 @@ from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from django.views.generic import View
 from django.views.generic import View
 
 
-from circuits.models import Circuit, CircuitTermination
+from circuits.models import Circuit
 from extras.views import ObjectConfigContextView
 from extras.views import ObjectConfigContextView
 from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
 from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
 from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
 from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
@@ -2738,7 +2738,7 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView):
 #
 #
 
 
 class CableListView(generic.ObjectListView):
 class CableListView(generic.ObjectListView):
-    queryset = Cable.objects.all()
+    queryset = Cable.objects.prefetch_related('terminations__termination')
     filterset = filtersets.CableFilterSet
     filterset = filtersets.CableFilterSet
     filterset_form = forms.CableFilterForm
     filterset_form = forms.CableFilterForm
     table = tables.CableTable
     table = tables.CableTable