Просмотр исходного кода

Migrate CablePath to use two-dimensional array

jeremystretch 3 лет назад
Родитель
Сommit
82706eb3a6

+ 6 - 6
netbox/dcim/api/views.py

@@ -546,7 +546,7 @@ class ModuleViewSet(NetBoxModelViewSet):
 
 class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = ConsolePort.objects.prefetch_related(
-        'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
+        'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags'
     )
     serializer_class = serializers.ConsolePortSerializer
     filterset_class = filtersets.ConsolePortFilterSet
@@ -555,7 +555,7 @@ class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
 
 class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = ConsoleServerPort.objects.prefetch_related(
-        'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
+        'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags'
     )
     serializer_class = serializers.ConsoleServerPortSerializer
     filterset_class = filtersets.ConsoleServerPortFilterSet
@@ -564,7 +564,7 @@ class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
 
 class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = PowerPort.objects.prefetch_related(
-        'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
+        'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags'
     )
     serializer_class = serializers.PowerPortSerializer
     filterset_class = filtersets.PowerPortFilterSet
@@ -573,7 +573,7 @@ class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
 
 class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = PowerOutlet.objects.prefetch_related(
-        'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
+        'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags'
     )
     serializer_class = serializers.PowerOutletSerializer
     filterset_class = filtersets.PowerOutletFilterSet
@@ -582,7 +582,7 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
 
 class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = Interface.objects.prefetch_related(
-        'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
+        'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable', '_link_peer',
         'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
     )
     serializer_class = serializers.InterfaceSerializer
@@ -685,7 +685,7 @@ class PowerPanelViewSet(NetBoxModelViewSet):
 
 class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = PowerFeed.objects.prefetch_related(
-        'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
+        'power_panel', 'rack', '_path', 'cable', '_link_peer', 'tags'
     )
     serializer_class = serializers.PowerFeedSerializer
     filterset_class = filtersets.PowerFeedFilterSet

+ 13 - 0
netbox/dcim/fields.py

@@ -10,6 +10,7 @@ from .lookups import PathContains
 __all__ = (
     'ASNField',
     'MACAddressField',
+    'MultiNodePathField',
     'PathField',
     'WWNField',
 )
@@ -104,4 +105,16 @@ class PathField(ArrayField):
         super().__init__(**kwargs)
 
 
+class MultiNodePathField(ArrayField):
+    """
+    A two-dimensional ArrayField which represents a path, with one or more nodes at each hop. Each node is
+    identified by a (type, ID) tuple.
+    """
+    def __init__(self, **kwargs):
+        kwargs['base_field'] = ArrayField(
+            base_field=models.CharField(max_length=40)
+        )
+        super().__init__(**kwargs)
+
+
 PathField.register_lookup(PathContains)

+ 29 - 0
netbox/dcim/migrations/0157_cablepath.py

@@ -0,0 +1,29 @@
+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=dcim.fields.MultiNodePathField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=40), size=None), default=[], size=None),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='cablepath',
+            name='is_complete',
+            field=models.BooleanField(default=False),
+        ),
+    ]

+ 50 - 0
netbox/dcim/migrations/0158_cablepath_populate_path.py

@@ -0,0 +1,50 @@
+from django.db import migrations
+
+from dcim.utils import compile_path_node
+
+
+def populate_cable_paths(apps, schema_editor):
+    """
+    Replicate terminations from the Cable model into CableTermination instances.
+    """
+    CablePath = apps.get_model('dcim', 'CablePath')
+
+    # Construct the new two-dimensional path, and add the origin & destination objects to the nodes list
+    cable_paths = []
+    for cablepath in CablePath.objects.all():
+
+        # Origin
+        origin = compile_path_node(cablepath.origin_type_id, cablepath.origin_id)
+        cablepath.path.append([origin])
+        cablepath._nodes.insert(0, origin)
+
+        # Transit nodes
+        cablepath.path.extend([
+            [node] for node in cablepath._nodes[1:]
+        ])
+
+        # Destination
+        if cablepath.destination_id:
+            destination = compile_path_node(cablepath.destination_type_id, cablepath.destination_id)
+            cablepath.path.append([destination])
+            cablepath._nodes.append(destination)
+            cablepath.is_complete = True
+
+        cable_paths.append(cablepath)
+
+    # Bulk create the termination objects
+    CablePath.objects.bulk_update(cable_paths, fields=('path', 'is_complete'), batch_size=100)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0157_cablepath'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            code=populate_cable_paths,
+            reverse_code=migrations.RunPython.noop
+        ),
+    ]

+ 33 - 0
netbox/dcim/migrations/0159_cablepath_remove_origin_destination.py

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

+ 36 - 42
netbox/dcim/models/cables.py

@@ -9,8 +9,8 @@ from django.urls import reverse
 
 from dcim.choices import *
 from dcim.constants import *
-from dcim.fields import PathField
-from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
+from dcim.fields import MultiNodePathField, PathField
+from dcim.utils import decompile_path_node, flatten_path, object_to_path_node, path_node_to_object
 from netbox.models import NetBoxModel
 from utilities.fields import ColorField
 from utilities.utils import to_meters
@@ -276,57 +276,39 @@ class CablePath(models.Model):
     `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.PositiveBigIntegerField()
-    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.PositiveBigIntegerField(
-        blank=True,
-        null=True
-    )
-    destination = GenericForeignKey(
-        ct_field='destination_type',
-        fk_field='destination_id'
-    )
-    path = PathField()
+    path = MultiNodePathField()
     is_active = models.BooleanField(
         default=False
     )
+    is_complete = models.BooleanField(
+        default=False
+    )
     is_split = models.BooleanField(
         default=False
     )
+    _nodes = PathField()
 
     class Meta:
-        unique_together = ('origin_type', 'origin_id')
+        pass
 
     def __str__(self):
         status = ' (active)' if self.is_active else ' (split)' if self.is_split else ''
-        return f"Path #{self.pk}: {self.origin} to {self.destination} via {len(self.path)} nodes{status}"
+        return f"Path #{self.pk}: {len(self.path)} nodes{status}"
 
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
 
+        # Save the flattened nodes list
+        self._nodes = flatten_path(self.path)
+
+        # TODO
         # 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)
+        # model = self.origin._meta.model
+        # model.objects.filter(pk=self.origin.pk).update(_path=self.pk)
 
     @property
     def segment_count(self):
-        total_length = 1 + len(self.path) + (1 if self.destination else 0)
-        return int(total_length / 3)
+        return int(len(self.path) / 3)
 
     @classmethod
     def from_origin(cls, origin):
@@ -421,7 +403,7 @@ class CablePath(models.Model):
         """
         # Compile a list of IDs to prefetch for each type of model in the path
         to_prefetch = defaultdict(list)
-        for node in self.path:
+        for node in self._nodes:
             ct_id, object_id = decompile_path_node(node)
             to_prefetch[ct_id].append(object_id)
 
@@ -438,18 +420,30 @@ class CablePath(models.Model):
 
         # Replicate the path using the prefetched objects.
         path = []
-        for node in self.path:
-            ct_id, object_id = decompile_path_node(node)
-            path.append(prefetched[ct_id][object_id])
+        for step in self.path:
+            nodes = []
+            for node in step:
+                ct_id, object_id = decompile_path_node(node)
+                nodes.append(prefetched[ct_id][object_id])
+            path.append(nodes)
 
         return path
 
+    def get_destination(self):
+        if not self.is_complete:
+            return None
+        return [
+            path_node_to_object(node) for node in self.path[-1]
+        ]
+
     @property
-    def last_node(self):
+    def last_nodes(self):
         """
         Return either the destination or the last node within the path.
         """
-        return self.destination or path_node_to_object(self.path[-1])
+        return [
+            path_node_to_object(node) for node in self.path[-1]
+        ]
 
     def get_cable_ids(self):
         """
@@ -458,7 +452,7 @@ class CablePath(models.Model):
         cable_ct = ContentType.objects.get_for_model(Cable).pk
         cable_ids = []
 
-        for node in self.path:
+        for node in self._nodes:
             ct, id = decompile_path_node(node)
             if ct == cable_ct:
                 cable_ids.append(id)
@@ -481,6 +475,6 @@ class CablePath(models.Model):
         """
         Return all available next segments in a split cable path.
         """
-        rearport = path_node_to_object(self.path[-1])
+        rearport = path_node_to_object(self._nodes[-1])
 
         return FrontPort.objects.filter(rear_port=rearport)

+ 1 - 1
netbox/dcim/models/device_components.py

@@ -229,7 +229,7 @@ class PathEndpoint(models.Model):
         Caching accessor for the attached CablePath's destination (if any)
         """
         if not hasattr(self, '_connected_endpoint'):
-            self._connected_endpoint = self._path.destination if self._path else None
+            self._connected_endpoint = self._path.get_destination()
         return self._connected_endpoint
 
 

+ 1 - 1
netbox/dcim/signals.py

@@ -133,7 +133,7 @@ def nullify_connected_endpoints(instance, **kwargs):
         cp = CablePath.from_origin(cablepath.origin)
         if cp:
             CablePath.objects.filter(pk=cablepath.pk).update(
-                path=cp.path,
+                _nodes=cp._nodes,
                 destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None,
                 destination_id=cp.destination.pk if cp.destination else None,
                 is_active=cp.is_active,

+ 10 - 0
netbox/dcim/utils.py

@@ -29,6 +29,16 @@ def path_node_to_object(repr):
     return ct.model_class().objects.get(pk=object_id)
 
 
+def flatten_path(path):
+    """
+    Flatten a two-dimensional array (list of lists) into a flat list.
+    """
+    ret = []
+    for step in path:
+        ret.extend(step)
+    return ret
+
+
 def create_cablepath(node):
     """
     Create CablePaths for all paths originating from the specified node.

+ 1 - 1
netbox/dcim/views.py

@@ -1711,7 +1711,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
 
     def get_extra_context(self, request, instance):
         interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
-            '_path__destination'
+            '_path'
         ).exclude(
             type__in=NONCONNECTABLE_IFACE_TYPES
         )

+ 3 - 4
netbox/netbox/views/__init__.py

@@ -37,14 +37,13 @@ class HomeView(View):
             return redirect("login")
 
         connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
-            _path__destination_id__isnull=False
+            _path__is_active=True
         )
         connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
-            _path__destination_id__isnull=False
+            _path__is_active=True
         )
         connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
-            _path__destination_id__isnull=False,
-            pk__lt=F('_path__destination_id')
+            _path__is_active=True
         )
 
         def build_stats():