mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 09:28:38 -06:00
Add support for tracing split paths
This commit is contained in:
parent
ba2ff0acb8
commit
c559775135
@ -747,7 +747,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CablePath
|
model = CablePath
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'origin_type', 'origin', 'destination_type', 'destination', 'path', 'is_active',
|
'id', 'origin_type', 'origin', 'destination_type', 'destination', 'path', 'is_active', 'is_split',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
@ -19,6 +19,7 @@ class Migration(migrations.Migration):
|
|||||||
('destination_id', models.PositiveIntegerField(blank=True, null=True)),
|
('destination_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)),
|
('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)),
|
||||||
('is_active', models.BooleanField(default=False)),
|
('is_active', models.BooleanField(default=False)),
|
||||||
|
('is_split', models.BooleanField(default=False)),
|
||||||
('destination_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
|
('destination_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
|
||||||
('origin_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
|
('origin_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
|
||||||
],
|
],
|
||||||
|
@ -11,7 +11,7 @@ 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.fields import PathField
|
||||||
from dcim.utils import decompile_path_node
|
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
|
||||||
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
|
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField
|
||||||
@ -218,23 +218,14 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
|
|||||||
f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
|
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
|
# Check that two connected RearPorts have the same number of positions
|
||||||
# or a RearPort with a different number of positions.
|
if isinstance(self.termination_a, RearPort) and isinstance(self.termination_b, RearPort):
|
||||||
for term_a, term_b in [
|
if self.termination_a.positions != self.termination_b.positions:
|
||||||
(self.termination_a, self.termination_b),
|
raise ValidationError(
|
||||||
(self.termination_b, self.termination_a)
|
f"{self.termination_a} has {self.termination_a.positions} position(s) but "
|
||||||
]:
|
f"{self.termination_b} has {self.termination_b.positions}. "
|
||||||
if isinstance(term_a, RearPort) and term_a.positions > 1:
|
f"Both terminations must have the same number of positions."
|
||||||
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
|
# A termination point cannot be connected to itself
|
||||||
if self.termination_a == self.termination_b:
|
if self.termination_a == self.termination_b:
|
||||||
@ -365,12 +356,16 @@ class CablePath(models.Model):
|
|||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
is_split = models.BooleanField(
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('origin_type', 'origin_id')
|
unique_together = ('origin_type', 'origin_id')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Path #{self.pk}: {self.origin} to {self.destination} ({len(self.path)} nodes)"
|
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}"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
@ -384,6 +379,68 @@ class CablePath(models.Model):
|
|||||||
total_length = 1 + len(self.path) + (1 if self.destination else 0)
|
total_length = 1 + len(self.path) + (1 if self.destination else 0)
|
||||||
return int(total_length / 3)
|
return int(total_length / 3)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_origin(cls, origin):
|
||||||
|
"""
|
||||||
|
Create a new CablePath instance as traced from the given path origin.
|
||||||
|
"""
|
||||||
|
if origin is None or origin.cable is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
destination = None
|
||||||
|
path = []
|
||||||
|
position_stack = []
|
||||||
|
is_active = True
|
||||||
|
is_split = False
|
||||||
|
|
||||||
|
node = origin
|
||||||
|
while node.cable is not None:
|
||||||
|
if node.cable.status != CableStatusChoices.STATUS_CONNECTED:
|
||||||
|
is_active = False
|
||||||
|
|
||||||
|
# Follow the cable to its far-end termination
|
||||||
|
path.append(object_to_path_node(node.cable))
|
||||||
|
peer_termination = node.get_cable_peer()
|
||||||
|
|
||||||
|
# Follow a FrontPort to its corresponding RearPort
|
||||||
|
if isinstance(peer_termination, FrontPort):
|
||||||
|
path.append(object_to_path_node(peer_termination))
|
||||||
|
node = peer_termination.rear_port
|
||||||
|
if node.positions > 1:
|
||||||
|
position_stack.append(peer_termination.rear_port_position)
|
||||||
|
path.append(object_to_path_node(node))
|
||||||
|
|
||||||
|
# Follow a RearPort to its corresponding FrontPort
|
||||||
|
elif isinstance(peer_termination, RearPort):
|
||||||
|
path.append(object_to_path_node(peer_termination))
|
||||||
|
if peer_termination.positions == 1:
|
||||||
|
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=1)
|
||||||
|
path.append(object_to_path_node(node))
|
||||||
|
elif position_stack:
|
||||||
|
position = position_stack.pop()
|
||||||
|
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position)
|
||||||
|
path.append(object_to_path_node(node))
|
||||||
|
else:
|
||||||
|
# No position indicated: path has split, so we stop at the RearPort
|
||||||
|
is_split = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Anything else marks the end of the path
|
||||||
|
else:
|
||||||
|
destination = peer_termination
|
||||||
|
break
|
||||||
|
|
||||||
|
if destination is None:
|
||||||
|
is_active = False
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
origin=origin,
|
||||||
|
destination=destination,
|
||||||
|
path=path,
|
||||||
|
is_active=is_active,
|
||||||
|
is_split=is_split
|
||||||
|
)
|
||||||
|
|
||||||
def get_path(self):
|
def get_path(self):
|
||||||
"""
|
"""
|
||||||
Return the path as a list of prefetched objects.
|
Return the path as a list of prefetched objects.
|
||||||
@ -422,3 +479,11 @@ class CablePath(models.Model):
|
|||||||
decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
|
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']
|
return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total']
|
||||||
|
|
||||||
|
def get_split_nodes(self):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
rearport = path_node_to_object(self.path[-1])
|
||||||
|
return FrontPort.objects.filter(rear_port=rearport)
|
||||||
|
@ -7,17 +7,19 @@ from django.dispatch import receiver
|
|||||||
|
|
||||||
from .choices import CableStatusChoices
|
from .choices import CableStatusChoices
|
||||||
from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis
|
from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis
|
||||||
from .utils import trace_path
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
path, destination, is_active = trace_path(node)
|
cp = CablePath.from_origin(node)
|
||||||
if path:
|
if cp:
|
||||||
cp = CablePath(origin=node, path=path, destination=destination, is_active=is_active)
|
try:
|
||||||
cp.save()
|
cp.save()
|
||||||
|
except Exception as e:
|
||||||
|
print(node, node.pk)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def rebuild_paths(obj):
|
def rebuild_paths(obj):
|
||||||
@ -116,13 +118,14 @@ def nullify_connected_endpoints(instance, **kwargs):
|
|||||||
|
|
||||||
# Delete and retrace any dependent cable paths
|
# Delete and retrace any dependent cable paths
|
||||||
for cablepath in CablePath.objects.filter(path__contains=instance):
|
for cablepath in CablePath.objects.filter(path__contains=instance):
|
||||||
path, destination, is_active = trace_path(cablepath.origin)
|
cp = CablePath.from_origin(cablepath.origin)
|
||||||
if path:
|
if cp:
|
||||||
CablePath.objects.filter(pk=cablepath.pk).update(
|
CablePath.objects.filter(pk=cablepath.pk).update(
|
||||||
path=path,
|
path=cp.path,
|
||||||
destination_type=ContentType.objects.get_for_model(destination) if destination else None,
|
destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None,
|
||||||
destination_id=destination.pk if destination else None,
|
destination_id=cp.destination.pk if cp.destination else None,
|
||||||
is_active=is_active
|
is_active=cp.is_active,
|
||||||
|
is_split=cp.is_split
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cablepath.delete()
|
cablepath.delete()
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from .choices import CableStatusChoices
|
|
||||||
|
|
||||||
|
|
||||||
def compile_path_node(ct_id, object_id):
|
def compile_path_node(ct_id, object_id):
|
||||||
return f'{ct_id}:{object_id}'
|
return f'{ct_id}:{object_id}'
|
||||||
@ -21,53 +19,10 @@ def object_to_path_node(obj):
|
|||||||
return compile_path_node(ct.pk, obj.pk)
|
return compile_path_node(ct.pk, obj.pk)
|
||||||
|
|
||||||
|
|
||||||
def trace_path(node):
|
def path_node_to_object(repr):
|
||||||
from .models import FrontPort, RearPort
|
"""
|
||||||
|
Given the string representation of a path node, return the corresponding instance.
|
||||||
destination = None
|
"""
|
||||||
path = []
|
ct_id, object_id = decompile_path_node(repr)
|
||||||
position_stack = []
|
ct = ContentType.objects.get_for_id(ct_id)
|
||||||
is_active = True
|
return ct.model_class().objects.get(pk=object_id)
|
||||||
|
|
||||||
if node is None or node.cable is None:
|
|
||||||
return [], None, False
|
|
||||||
|
|
||||||
while node.cable is not None:
|
|
||||||
if node.cable.status != CableStatusChoices.STATUS_CONNECTED:
|
|
||||||
is_active = False
|
|
||||||
|
|
||||||
# Follow the cable to its far-end termination
|
|
||||||
path.append(object_to_path_node(node.cable))
|
|
||||||
peer_termination = node.get_cable_peer()
|
|
||||||
|
|
||||||
# Follow a FrontPort to its corresponding RearPort
|
|
||||||
if isinstance(peer_termination, FrontPort):
|
|
||||||
path.append(object_to_path_node(peer_termination))
|
|
||||||
node = peer_termination.rear_port
|
|
||||||
if node.positions > 1:
|
|
||||||
position_stack.append(peer_termination.rear_port_position)
|
|
||||||
path.append(object_to_path_node(node))
|
|
||||||
|
|
||||||
# Follow a RearPort to its corresponding FrontPort
|
|
||||||
elif isinstance(peer_termination, RearPort):
|
|
||||||
path.append(object_to_path_node(peer_termination))
|
|
||||||
if peer_termination.positions == 1:
|
|
||||||
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=1)
|
|
||||||
path.append(object_to_path_node(node))
|
|
||||||
elif position_stack:
|
|
||||||
position = position_stack.pop()
|
|
||||||
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position)
|
|
||||||
path.append(object_to_path_node(node))
|
|
||||||
else:
|
|
||||||
# No position indicated: path has split, so we stop at the RearPort
|
|
||||||
break
|
|
||||||
|
|
||||||
# Anything else marks the end of the path
|
|
||||||
else:
|
|
||||||
destination = peer_termination
|
|
||||||
break
|
|
||||||
|
|
||||||
if destination is None:
|
|
||||||
is_active = False
|
|
||||||
|
|
||||||
return path, destination, is_active
|
|
||||||
|
@ -22,9 +22,6 @@
|
|||||||
{% elif near_end.circuit %}
|
{% elif near_end.circuit %}
|
||||||
{% include 'dcim/trace/circuit.html' with circuit=near_end.circuit %}
|
{% include 'dcim/trace/circuit.html' with circuit=near_end.circuit %}
|
||||||
{% include 'dcim/trace/termination.html' with termination=near_end %}
|
{% include 'dcim/trace/termination.html' with termination=near_end %}
|
||||||
{% else %}
|
|
||||||
<h3 class="text-danger text-center">Split Paths!</h3>
|
|
||||||
{# TODO: Present the user with successive paths to choose from #}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Cable #}
|
{# Cable #}
|
||||||
@ -49,17 +46,36 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if forloop.last %}
|
{% if forloop.last %}
|
||||||
<div class="trace-end">
|
{% if path.is_split %}
|
||||||
<h3{% if far_end %} class="text-success"{% endif %}>Trace completed</h3>
|
<div class="trace-end">
|
||||||
<h5>Total segments: {{ traced_path|length }}</h5>
|
<h3 class="text-danger">Path split!</h3>
|
||||||
<h5>Total length:
|
<p>Select a node below to continue:</p>
|
||||||
{% if total_length %}
|
<ul class="text-left">
|
||||||
{{ total_length|floatformat:"-2" }} Meters
|
{% for next_node in path.get_split_nodes %}
|
||||||
{% else %}
|
{% if next_node.cable %}
|
||||||
<span class="text-muted">N/A</span>
|
<li>
|
||||||
{% endif %}
|
<a href="{% url 'dcim:frontport_trace' pk=next_node.pk %}">{{ next_node }}</a>
|
||||||
</h5>
|
(Cable <a href="{{ next_node.cable.get_absolute_url }}">{{ next_node.cable }}</a>)
|
||||||
</div>
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="text-muted">{{ next_node }}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="trace-end">
|
||||||
|
<h3{% if far_end %} class="text-success"{% endif %}>Trace completed</h3>
|
||||||
|
<h5>Total segments: {{ traced_path|length }}</h5>
|
||||||
|
<h5>Total length:
|
||||||
|
{% if total_length %}
|
||||||
|
{{ total_length|floatformat:"-2" }} Meters
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">N/A</span>
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
Loading…
Reference in New Issue
Block a user