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:
@ -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
|
||||||
|
@ -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)
|
||||||
|
29
netbox/dcim/migrations/0157_cablepath.py
Normal file
29
netbox/dcim/migrations/0157_cablepath.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
50
netbox/dcim/migrations/0158_cablepath_populate_path.py
Normal file
50
netbox/dcim/migrations/0158_cablepath_populate_path.py
Normal 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
|
||||||
|
),
|
||||||
|
]
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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():
|
||||||
|
Reference in New Issue
Block a user