mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -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:
|
||||
model = CablePath
|
||||
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)
|
||||
|
@ -19,6 +19,7 @@ class Migration(migrations.Migration):
|
||||
('destination_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)),
|
||||
('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')),
|
||||
('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.constants import *
|
||||
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.utils import extras_features
|
||||
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}"
|
||||
)
|
||||
|
||||
# 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."
|
||||
)
|
||||
# Check that two connected RearPorts have the same number of positions
|
||||
if isinstance(self.termination_a, RearPort) and isinstance(self.termination_b, RearPort):
|
||||
if self.termination_a.positions != self.termination_b.positions:
|
||||
raise ValidationError(
|
||||
f"{self.termination_a} has {self.termination_a.positions} position(s) but "
|
||||
f"{self.termination_b} has {self.termination_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:
|
||||
@ -365,12 +356,16 @@ class CablePath(models.Model):
|
||||
is_active = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
is_split = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('origin_type', 'origin_id')
|
||||
|
||||
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):
|
||||
super().save(*args, **kwargs)
|
||||
@ -384,6 +379,68 @@ class CablePath(models.Model):
|
||||
total_length = 1 + len(self.path) + (1 if self.destination else 0)
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
]
|
||||
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 .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis
|
||||
from .utils import trace_path
|
||||
|
||||
|
||||
def create_cablepath(node):
|
||||
"""
|
||||
Create CablePaths for all paths originating from the specified node.
|
||||
"""
|
||||
path, destination, is_active = trace_path(node)
|
||||
if path:
|
||||
cp = CablePath(origin=node, path=path, destination=destination, is_active=is_active)
|
||||
cp.save()
|
||||
cp = CablePath.from_origin(node)
|
||||
if cp:
|
||||
try:
|
||||
cp.save()
|
||||
except Exception as e:
|
||||
print(node, node.pk)
|
||||
raise e
|
||||
|
||||
|
||||
def rebuild_paths(obj):
|
||||
@ -116,13 +118,14 @@ def nullify_connected_endpoints(instance, **kwargs):
|
||||
|
||||
# Delete and retrace any dependent cable paths
|
||||
for cablepath in CablePath.objects.filter(path__contains=instance):
|
||||
path, destination, is_active = trace_path(cablepath.origin)
|
||||
if path:
|
||||
cp = CablePath.from_origin(cablepath.origin)
|
||||
if cp:
|
||||
CablePath.objects.filter(pk=cablepath.pk).update(
|
||||
path=path,
|
||||
destination_type=ContentType.objects.get_for_model(destination) if destination else None,
|
||||
destination_id=destination.pk if destination else None,
|
||||
is_active=is_active
|
||||
path=cp.path,
|
||||
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,
|
||||
is_split=cp.is_split
|
||||
)
|
||||
else:
|
||||
cablepath.delete()
|
||||
|
@ -1,7 +1,5 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from .choices import CableStatusChoices
|
||||
|
||||
|
||||
def compile_path_node(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)
|
||||
|
||||
|
||||
def trace_path(node):
|
||||
from .models import FrontPort, RearPort
|
||||
|
||||
destination = None
|
||||
path = []
|
||||
position_stack = []
|
||||
is_active = True
|
||||
|
||||
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
|
||||
def path_node_to_object(repr):
|
||||
"""
|
||||
Given the string representation of a path node, return the corresponding instance.
|
||||
"""
|
||||
ct_id, object_id = decompile_path_node(repr)
|
||||
ct = ContentType.objects.get_for_id(ct_id)
|
||||
return ct.model_class().objects.get(pk=object_id)
|
||||
|
@ -22,9 +22,6 @@
|
||||
{% elif near_end.circuit %}
|
||||
{% include 'dcim/trace/circuit.html' with circuit=near_end.circuit %}
|
||||
{% 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 %}
|
||||
|
||||
{# Cable #}
|
||||
@ -49,17 +46,36 @@
|
||||
{% endif %}
|
||||
|
||||
{% if forloop.last %}
|
||||
<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>
|
||||
{% if path.is_split %}
|
||||
<div class="trace-end">
|
||||
<h3 class="text-danger">Path split!</h3>
|
||||
<p>Select a node below to continue:</p>
|
||||
<ul class="text-left">
|
||||
{% for next_node in path.get_split_nodes %}
|
||||
{% if next_node.cable %}
|
||||
<li>
|
||||
<a href="{% url 'dcim:frontport_trace' pk=next_node.pk %}">{{ next_node }}</a>
|
||||
(Cable <a href="{{ next_node.cable.get_absolute_url }}">{{ next_node.cable }}</a>)
|
||||
</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 %}
|
||||
|
||||
{% endfor %}
|
||||
|
Loading…
Reference in New Issue
Block a user