1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Migrate CablePath to use two-dimensional array

This commit is contained in:
jeremystretch
2022-05-03 11:32:52 -04:00
parent c22007939b
commit 82706eb3a6
11 changed files with 183 additions and 55 deletions

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__destination', 'cable', '_link_peer', 'tags' 'device', 'module__module_bay', '_path', 'cable', '_link_peer', '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__destination', 'cable', '_link_peer', 'tags' 'device', 'module__module_bay', '_path', 'cable', '_link_peer', '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__destination', 'cable', '_link_peer', 'tags' 'device', 'module__module_bay', '_path', 'cable', '_link_peer', '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__destination', 'cable', '_link_peer', 'tags' 'device', 'module__module_bay', '_path', 'cable', '_link_peer', 'tags'
) )
serializer_class = serializers.PowerOutletSerializer serializer_class = serializers.PowerOutletSerializer
filterset_class = filtersets.PowerOutletFilterSet filterset_class = filtersets.PowerOutletFilterSet
@ -582,7 +582,7 @@ 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__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' 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
@ -685,7 +685,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__destination', 'cable', '_link_peer', 'tags' 'power_panel', 'rack', '_path', 'cable', '_link_peer', 'tags'
) )
serializer_class = serializers.PowerFeedSerializer serializer_class = serializers.PowerFeedSerializer
filterset_class = filtersets.PowerFeedFilterSet filterset_class = filtersets.PowerFeedFilterSet

View File

@ -10,6 +10,7 @@ from .lookups import PathContains
__all__ = ( __all__ = (
'ASNField', 'ASNField',
'MACAddressField', 'MACAddressField',
'MultiNodePathField',
'PathField', 'PathField',
'WWNField', 'WWNField',
) )
@ -104,4 +105,16 @@ class PathField(ArrayField):
super().__init__(**kwargs) 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) PathField.register_lookup(PathContains)

View File

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

View File

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

View File

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

View File

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

View File

@ -229,7 +229,7 @@ class PathEndpoint(models.Model):
Caching accessor for the attached CablePath's destination (if any) Caching accessor for the attached CablePath's destination (if any)
""" """
if not hasattr(self, '_connected_endpoint'): 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 return self._connected_endpoint

View File

@ -133,7 +133,7 @@ def nullify_connected_endpoints(instance, **kwargs):
cp = CablePath.from_origin(cablepath.origin) cp = CablePath.from_origin(cablepath.origin)
if cp: if cp:
CablePath.objects.filter(pk=cablepath.pk).update( 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_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None,
destination_id=cp.destination.pk if cp.destination else None, destination_id=cp.destination.pk if cp.destination else None,
is_active=cp.is_active, is_active=cp.is_active,

View File

@ -29,6 +29,16 @@ def path_node_to_object(repr):
return ct.model_class().objects.get(pk=object_id) 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): def create_cablepath(node):
""" """
Create CablePaths for all paths originating from the specified node. Create CablePaths for all paths originating from the specified node.

View File

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

View File

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