Parcourir la source

Move Cable and CablePath to cables.py

Jeremy Stretch il y a 5 ans
Parent
commit
e7d26ca5dc
3 fichiers modifiés avec 392 ajouts et 371 suppressions
  1. 1 0
      netbox/dcim/models/__init__.py
  2. 387 0
      netbox/dcim/models/cables.py
  3. 4 371
      netbox/dcim/models/devices.py

+ 1 - 0
netbox/dcim/models/__init__.py

@@ -1,3 +1,4 @@
+from .cables import *
 from .device_component_templates import *
 from .device_component_templates import *
 from .device_components import *
 from .device_components import *
 from .devices import *
 from .devices import *

+ 387 - 0
netbox/dcim/models/cables.py

@@ -0,0 +1,387 @@
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.db import models
+from django.db.models import Sum
+from django.urls import reverse
+from taggit.managers import TaggableManager
+
+from dcim.choices import *
+from dcim.constants import *
+from dcim.fields import PathField
+from dcim.utils import decompile_path_node, path_node_to_object
+from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
+from extras.utils import extras_features
+from utilities.fields import ColorField
+from utilities.querysets import RestrictedQuerySet
+from utilities.utils import to_meters
+from .devices import Device
+from .device_components import FrontPort, RearPort
+
+
+__all__ = (
+    'Cable',
+    'CablePath',
+)
+
+
+#
+# Cables
+#
+
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+class Cable(ChangeLoggedModel, CustomFieldModel):
+    """
+    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.PositiveIntegerField()
+    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.PositiveIntegerField()
+    termination_b = GenericForeignKey(
+        ct_field='termination_b_type',
+        fk_field='termination_b_id'
+    )
+    type = models.CharField(
+        max_length=50,
+        choices=CableTypeChoices,
+        blank=True
+    )
+    status = models.CharField(
+        max_length=50,
+        choices=CableStatusChoices,
+        default=CableStatusChoices.STATUS_CONNECTED
+    )
+    label = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    color = ColorField(
+        blank=True
+    )
+    length = models.PositiveSmallIntegerField(
+        blank=True,
+        null=True
+    )
+    length_unit = models.CharField(
+        max_length=50,
+        choices=CableLengthUnitChoices,
+        blank=True,
+    )
+    # Stores the normalized length (in meters) for database ordering
+    _abs_length = models.DecimalField(
+        max_digits=10,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+    # Cache the associated device (where applicable) for the A and B terminations. This enables filtering of Cables by
+    # their associated Devices.
+    _termination_a_device = models.ForeignKey(
+        to=Device,
+        on_delete=models.CASCADE,
+        related_name='+',
+        blank=True,
+        null=True
+    )
+    _termination_b_device = models.ForeignKey(
+        to=Device,
+        on_delete=models.CASCADE,
+        related_name='+',
+        blank=True,
+        null=True
+    )
+    tags = TaggableManager(through=TaggedItem)
+
+    objects = RestrictedQuerySet.as_manager()
+
+    csv_headers = [
+        'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id', 'type', 'status', 'label',
+        'color', 'length', 'length_unit',
+    ]
+
+    class Meta:
+        ordering = ['pk']
+        unique_together = (
+            ('termination_a_type', 'termination_a_id'),
+            ('termination_b_type', 'termination_b_id'),
+        )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # A copy of the PK to be used by __str__ in case the object is deleted
+        self._pk = self.pk
+
+        # Cache the original status so we can check later if it's been changed
+        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_id = instance.termination_a_id
+        instance._orig_termination_b_type_id = instance.termination_b_type_id
+        instance._orig_termination_b_id = instance.termination_b_id
+
+        return instance
+
+    def __str__(self):
+        return self.label or '#{}'.format(self._pk)
+
+    def get_absolute_url(self):
+        return reverse('dcim:cable', args=[self.pk])
+
+    def clean(self):
+        from circuits.models import CircuitTermination
+
+        # Validate that termination A exists
+        if not hasattr(self, 'termination_a_type'):
+            raise ValidationError('Termination A type has not been specified')
+        try:
+            self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
+        except ObjectDoesNotExist:
+            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')
+        try:
+            self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
+        except ObjectDoesNotExist:
+            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
+                self.termination_a_id != self._orig_termination_a_id
+            ):
+                raise ValidationError({
+                    'termination_a': err_msg
+                })
+            if (
+                self.termination_b_type_id != self._orig_termination_b_type_id or
+                self.termination_b_id != self._orig_termination_b_id
+            ):
+                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' and self.termination_a.type in NONCONNECTABLE_IFACE_TYPES:
+            raise ValidationError({
+                'termination_a_id': 'Cables cannot be terminated to {} interfaces'.format(
+                    self.termination_a.get_type_display()
+                )
+            })
+        if type_b == 'interface' and self.termination_b.type in NONCONNECTABLE_IFACE_TYPES:
+            raise ValidationError({
+                'termination_b_id': 'Cables cannot be terminated to {} interfaces'.format(
+                    self.termination_b.get_type_display()
+                )
+            })
+
+        # 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}"
+            )
+
+        # Check that a RearPort with multiple positions isn't connected to an endpoint
+        # or a RearPort with a different number of positions.
+        for term_a, term_b in [
+            (self.termination_a, self.termination_b),
+            (self.termination_b, self.termination_a)
+        ]:
+            if isinstance(term_a, RearPort) and term_a.positions > 1:
+                if not isinstance(term_b, (FrontPort, RearPort, CircuitTermination)):
+                    raise ValidationError(
+                        "Rear ports with multiple positions may only be connected to other pass-through ports"
+                    )
+                if isinstance(term_b, RearPort) and term_b.positions > 1 and term_a.positions != term_b.positions:
+                    raise ValidationError(
+                        f"{term_a} of {term_a.device} has {term_a.positions} position(s) but "
+                        f"{term_b} of {term_b.device} has {term_b.positions}. "
+                        f"Both terminations must have the same number of positions."
+                    )
+
+        # A termination point cannot be connected to itself
+        if self.termination_a == self.termination_b:
+            raise ValidationError(f"Cannot connect {self.termination_a_type} to itself")
+
+        # 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")
+
+        # Check for an existing Cable connected to either termination object
+        if self.termination_a.cable not in (None, self):
+            raise ValidationError("{} already has a cable attached (#{})".format(
+                self.termination_a, self.termination_a.cable_id
+            ))
+        if self.termination_b.cable not in (None, self):
+            raise ValidationError("{} already has a cable attached (#{})".format(
+                self.termination_b, self.termination_b.cable_id
+            ))
+
+        # Validate length and length_unit
+        if self.length is not None and not self.length_unit:
+            raise ValidationError("Must specify a unit when setting a cable length")
+        elif self.length is None:
+            self.length_unit = ''
+
+    def save(self, *args, **kwargs):
+
+        # Store the given length (if any) in meters for use in database ordering
+        if self.length and self.length_unit:
+            self._abs_length = to_meters(self.length, self.length_unit)
+        else:
+            self._abs_length = None
+
+        # Store the parent Device for the A and B terminations (if applicable) to enable filtering
+        if hasattr(self.termination_a, 'device'):
+            self._termination_a_device = self.termination_a.device
+        if hasattr(self.termination_b, 'device'):
+            self._termination_b_device = self.termination_b.device
+
+        super().save(*args, **kwargs)
+
+        # Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
+        self._pk = self.pk
+
+    def to_csv(self):
+        return (
+            '{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model),
+            self.termination_a_id,
+            '{}.{}'.format(self.termination_b_type.app_label, self.termination_b_type.model),
+            self.termination_b_id,
+            self.get_type_display(),
+            self.get_status_display(),
+            self.label,
+            self.color,
+            self.length,
+            self.length_unit,
+        )
+
+    def get_status_class(self):
+        return CableStatusChoices.CSS_CLASSES.get(self.status)
+
+    def get_compatible_types(self):
+        """
+        Return all termination types compatible with termination A.
+        """
+        if self.termination_a is None:
+            return
+        return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
+
+
+class CablePath(models.Model):
+    """
+    A CablePath instance represents the physical path from an origin to a destination, including all intermediate
+    elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do
+    not terminate on a PathEndpoint).
+
+    `path` contains a list of nodes within the path, each represented by a tuple of (type, ID). The first element in the
+    path must be a Cable instance, followed by a pair of pass-through ports. For example, consider the following
+    topology:
+
+                     1                              2                              3
+        Interface A --- Front Port A | Rear Port A --- Rear Port B | Front Port B --- Interface B
+
+    This path would be expressed as:
+
+    CablePath(
+        origin = Interface A
+        destination = Interface B
+        path = [Cable 1, Front Port A, Rear Port A, Cable 2, Rear Port B, Front Port B, Cable 3]
+    )
+
+    `is_active` is set to True only if 1) `destination` is not null, and 2) every Cable within the path has a status of
+    "connected".
+    """
+    origin_type = models.ForeignKey(
+        to=ContentType,
+        on_delete=models.CASCADE,
+        related_name='+'
+    )
+    origin_id = models.PositiveIntegerField()
+    origin = GenericForeignKey(
+        ct_field='origin_type',
+        fk_field='origin_id'
+    )
+    destination_type = models.ForeignKey(
+        to=ContentType,
+        on_delete=models.CASCADE,
+        related_name='+',
+        blank=True,
+        null=True
+    )
+    destination_id = models.PositiveIntegerField(
+        blank=True,
+        null=True
+    )
+    destination = GenericForeignKey(
+        ct_field='destination_type',
+        fk_field='destination_id'
+    )
+    path = PathField()
+    is_active = models.BooleanField(
+        default=False
+    )
+
+    class Meta:
+        unique_together = ('origin_type', 'origin_id')
+
+    def __str__(self):
+        path = ', '.join([str(path_node_to_object(node)) for node in self.path])
+        return f"Path #{self.pk}: {self.origin} to {self.destination} via ({path})"
+
+    def save(self, *args, **kwargs):
+        super().save(*args, **kwargs)
+
+        # Record a direct reference to this CablePath on its originating object
+        model = self.origin._meta.model
+        model.objects.filter(pk=self.origin.pk).update(_path=self.pk)
+
+    def get_total_length(self):
+        """
+        Return the sum of the length of each cable in the path.
+        """
+        cable_ids = [
+            # Starting from the first element, every third element in the path should be a Cable
+            decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
+        ]
+        return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total']

+ 4 - 371
netbox/dcim/models/devices.py

@@ -2,32 +2,26 @@ from collections import OrderedDict
 
 
 import yaml
 import yaml
 from django.conf import settings
 from django.conf import settings
-from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.contrib.contenttypes.fields import GenericRelation
+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 F, ProtectedError, Sum
+from django.db.models import F, ProtectedError
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 
 
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
-from dcim.fields import PathField
-from dcim.utils import decompile_path_node, path_node_to_object
 from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
 from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
 from extras.utils import extras_features
 from extras.utils import extras_features
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
-from utilities.utils import to_meters
 from .device_components import *
 from .device_components import *
 
 
 
 
 __all__ = (
 __all__ = (
-    'Cable',
-    'CablePath',
     'Device',
     'Device',
     'DeviceRole',
     'DeviceRole',
     'DeviceType',
     'DeviceType',
@@ -856,6 +850,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         """
         """
         Return a QuerySet or PK list matching all Cables connected to a component of this Device.
         Return a QuerySet or PK list matching all Cables connected to a component of this Device.
         """
         """
+        from .cables import Cable
         cable_pks = []
         cable_pks = []
         for component_model in [
         for component_model in [
             ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort
             ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort
@@ -877,368 +872,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         return DeviceStatusChoices.CSS_CLASSES.get(self.status)
         return DeviceStatusChoices.CSS_CLASSES.get(self.status)
 
 
 
 
-#
-# Cables
-#
-
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
-class Cable(ChangeLoggedModel, CustomFieldModel):
-    """
-    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.PositiveIntegerField()
-    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.PositiveIntegerField()
-    termination_b = GenericForeignKey(
-        ct_field='termination_b_type',
-        fk_field='termination_b_id'
-    )
-    type = models.CharField(
-        max_length=50,
-        choices=CableTypeChoices,
-        blank=True
-    )
-    status = models.CharField(
-        max_length=50,
-        choices=CableStatusChoices,
-        default=CableStatusChoices.STATUS_CONNECTED
-    )
-    label = models.CharField(
-        max_length=100,
-        blank=True
-    )
-    color = ColorField(
-        blank=True
-    )
-    length = models.PositiveSmallIntegerField(
-        blank=True,
-        null=True
-    )
-    length_unit = models.CharField(
-        max_length=50,
-        choices=CableLengthUnitChoices,
-        blank=True,
-    )
-    # Stores the normalized length (in meters) for database ordering
-    _abs_length = models.DecimalField(
-        max_digits=10,
-        decimal_places=4,
-        blank=True,
-        null=True
-    )
-    # Cache the associated device (where applicable) for the A and B terminations. This enables filtering of Cables by
-    # their associated Devices.
-    _termination_a_device = models.ForeignKey(
-        to=Device,
-        on_delete=models.CASCADE,
-        related_name='+',
-        blank=True,
-        null=True
-    )
-    _termination_b_device = models.ForeignKey(
-        to=Device,
-        on_delete=models.CASCADE,
-        related_name='+',
-        blank=True,
-        null=True
-    )
-    tags = TaggableManager(through=TaggedItem)
-
-    objects = RestrictedQuerySet.as_manager()
-
-    csv_headers = [
-        'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id', 'type', 'status', 'label',
-        'color', 'length', 'length_unit',
-    ]
-
-    class Meta:
-        ordering = ['pk']
-        unique_together = (
-            ('termination_a_type', 'termination_a_id'),
-            ('termination_b_type', 'termination_b_id'),
-        )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        # A copy of the PK to be used by __str__ in case the object is deleted
-        self._pk = self.pk
-
-        # Cache the original status so we can check later if it's been changed
-        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_id = instance.termination_a_id
-        instance._orig_termination_b_type_id = instance.termination_b_type_id
-        instance._orig_termination_b_id = instance.termination_b_id
-
-        return instance
-
-    def __str__(self):
-        return self.label or '#{}'.format(self._pk)
-
-    def get_absolute_url(self):
-        return reverse('dcim:cable', args=[self.pk])
-
-    def clean(self):
-        from circuits.models import CircuitTermination
-
-        # Validate that termination A exists
-        if not hasattr(self, 'termination_a_type'):
-            raise ValidationError('Termination A type has not been specified')
-        try:
-            self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
-        except ObjectDoesNotExist:
-            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')
-        try:
-            self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
-        except ObjectDoesNotExist:
-            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
-                self.termination_a_id != self._orig_termination_a_id
-            ):
-                raise ValidationError({
-                    'termination_a': err_msg
-                })
-            if (
-                self.termination_b_type_id != self._orig_termination_b_type_id or
-                self.termination_b_id != self._orig_termination_b_id
-            ):
-                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' and self.termination_a.type in NONCONNECTABLE_IFACE_TYPES:
-            raise ValidationError({
-                'termination_a_id': 'Cables cannot be terminated to {} interfaces'.format(
-                    self.termination_a.get_type_display()
-                )
-            })
-        if type_b == 'interface' and self.termination_b.type in NONCONNECTABLE_IFACE_TYPES:
-            raise ValidationError({
-                'termination_b_id': 'Cables cannot be terminated to {} interfaces'.format(
-                    self.termination_b.get_type_display()
-                )
-            })
-
-        # 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}"
-            )
-
-        # Check that a RearPort with multiple positions isn't connected to an endpoint
-        # or a RearPort with a different number of positions.
-        for term_a, term_b in [
-            (self.termination_a, self.termination_b),
-            (self.termination_b, self.termination_a)
-        ]:
-            if isinstance(term_a, RearPort) and term_a.positions > 1:
-                if not isinstance(term_b, (FrontPort, RearPort, CircuitTermination)):
-                    raise ValidationError(
-                        "Rear ports with multiple positions may only be connected to other pass-through ports"
-                    )
-                if isinstance(term_b, RearPort) and term_b.positions > 1 and term_a.positions != term_b.positions:
-                    raise ValidationError(
-                        f"{term_a} of {term_a.device} has {term_a.positions} position(s) but "
-                        f"{term_b} of {term_b.device} has {term_b.positions}. "
-                        f"Both terminations must have the same number of positions."
-                    )
-
-        # A termination point cannot be connected to itself
-        if self.termination_a == self.termination_b:
-            raise ValidationError(f"Cannot connect {self.termination_a_type} to itself")
-
-        # 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")
-
-        # Check for an existing Cable connected to either termination object
-        if self.termination_a.cable not in (None, self):
-            raise ValidationError("{} already has a cable attached (#{})".format(
-                self.termination_a, self.termination_a.cable_id
-            ))
-        if self.termination_b.cable not in (None, self):
-            raise ValidationError("{} already has a cable attached (#{})".format(
-                self.termination_b, self.termination_b.cable_id
-            ))
-
-        # Validate length and length_unit
-        if self.length is not None and not self.length_unit:
-            raise ValidationError("Must specify a unit when setting a cable length")
-        elif self.length is None:
-            self.length_unit = ''
-
-    def save(self, *args, **kwargs):
-
-        # Store the given length (if any) in meters for use in database ordering
-        if self.length and self.length_unit:
-            self._abs_length = to_meters(self.length, self.length_unit)
-        else:
-            self._abs_length = None
-
-        # Store the parent Device for the A and B terminations (if applicable) to enable filtering
-        if hasattr(self.termination_a, 'device'):
-            self._termination_a_device = self.termination_a.device
-        if hasattr(self.termination_b, 'device'):
-            self._termination_b_device = self.termination_b.device
-
-        super().save(*args, **kwargs)
-
-        # Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
-        self._pk = self.pk
-
-    def to_csv(self):
-        return (
-            '{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model),
-            self.termination_a_id,
-            '{}.{}'.format(self.termination_b_type.app_label, self.termination_b_type.model),
-            self.termination_b_id,
-            self.get_type_display(),
-            self.get_status_display(),
-            self.label,
-            self.color,
-            self.length,
-            self.length_unit,
-        )
-
-    def get_status_class(self):
-        return CableStatusChoices.CSS_CLASSES.get(self.status)
-
-    def get_compatible_types(self):
-        """
-        Return all termination types compatible with termination A.
-        """
-        if self.termination_a is None:
-            return
-        return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
-
-
-class CablePath(models.Model):
-    """
-    A CablePath instance represents the physical path from an origin to a destination, including all intermediate
-    elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do
-    not terminate on a PathEndpoint).
-
-    `path` contains a list of nodes within the path, each represented by a tuple of (type, ID). The first element in the
-    path must be a Cable instance, followed by a pair of pass-through ports. For example, consider the following
-    topology:
-
-                     1                              2                              3
-        Interface A --- Front Port A | Rear Port A --- Rear Port B | Front Port B --- Interface B
-
-    This path would be expressed as:
-
-    CablePath(
-        origin = Interface A
-        destination = Interface B
-        path = [Cable 1, Front Port A, Rear Port A, Cable 2, Rear Port B, Front Port B, Cable 3]
-    )
-
-    `is_active` is set to True only if 1) `destination` is not null, and 2) every Cable within the path has a status of
-    "connected".
-    """
-    origin_type = models.ForeignKey(
-        to=ContentType,
-        on_delete=models.CASCADE,
-        related_name='+'
-    )
-    origin_id = models.PositiveIntegerField()
-    origin = GenericForeignKey(
-        ct_field='origin_type',
-        fk_field='origin_id'
-    )
-    destination_type = models.ForeignKey(
-        to=ContentType,
-        on_delete=models.CASCADE,
-        related_name='+',
-        blank=True,
-        null=True
-    )
-    destination_id = models.PositiveIntegerField(
-        blank=True,
-        null=True
-    )
-    destination = GenericForeignKey(
-        ct_field='destination_type',
-        fk_field='destination_id'
-    )
-    path = PathField()
-    is_active = models.BooleanField(
-        default=False
-    )
-
-    class Meta:
-        unique_together = ('origin_type', 'origin_id')
-
-    def __str__(self):
-        path = ', '.join([str(path_node_to_object(node)) for node in self.path])
-        return f"Path #{self.pk}: {self.origin} to {self.destination} via ({path})"
-
-    def save(self, *args, **kwargs):
-        super().save(*args, **kwargs)
-
-        # Record a direct reference to this CablePath on its originating object
-        model = self.origin._meta.model
-        model.objects.filter(pk=self.origin.pk).update(_path=self.pk)
-
-    def get_total_length(self):
-        """
-        Return the sum of the length of each cable in the path.
-        """
-        cable_ids = [
-            # Starting from the first element, every third element in the path should be a Cable
-            decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
-        ]
-        return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total']
-
-
 #
 #
 # Virtual chassis
 # Virtual chassis
 #
 #