mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Drop support for split paths
This commit is contained in:
@ -33,11 +33,9 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
|
|||||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||||
|
|
||||||
def get_connected_endpoint_type(self, obj):
|
def get_connected_endpoint_type(self, obj):
|
||||||
if hasattr(obj, 'connected_endpoint') and obj.connected_endpoint is not None:
|
if obj.path is not None:
|
||||||
return '{}.{}'.format(
|
destination = obj.path.destination
|
||||||
obj.connected_endpoint._meta.app_label,
|
return f'{destination._meta.app_label}.{destination._meta.model_name}'
|
||||||
obj.connected_endpoint._meta.model_name
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
@ -45,14 +43,11 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
|
|||||||
"""
|
"""
|
||||||
Return the appropriate serializer for the type of connected object.
|
Return the appropriate serializer for the type of connected object.
|
||||||
"""
|
"""
|
||||||
if getattr(obj, 'connected_endpoint', None) is None:
|
if obj.path is not None:
|
||||||
return None
|
serializer = get_serializer_for_model(obj.path.destination, prefix='Nested')
|
||||||
|
context = {'request': self.context['request']}
|
||||||
serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested')
|
return serializer(obj.path.destination, context=context).data
|
||||||
context = {'request': self.context['request']}
|
return None
|
||||||
data = serializer(obj.connected_endpoint, context=context).data
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -5,7 +5,7 @@ from django.db import connection
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import CablePath
|
from dcim.models import CablePath
|
||||||
from dcim.signals import create_cablepaths
|
from dcim.signals import create_cablepath
|
||||||
|
|
||||||
ENDPOINT_MODELS = (
|
ENDPOINT_MODELS = (
|
||||||
'circuits.CircuitTermination',
|
'circuits.CircuitTermination',
|
||||||
@ -60,7 +60,7 @@ class Command(BaseCommand):
|
|||||||
print(f'Retracing {origins.count()} cabled {model._meta.verbose_name_plural}...')
|
print(f'Retracing {origins.count()} cabled {model._meta.verbose_name_plural}...')
|
||||||
i = 0
|
i = 0
|
||||||
for i, obj in enumerate(origins, start=1):
|
for i, obj in enumerate(origins, start=1):
|
||||||
create_cablepaths(obj)
|
create_cablepath(obj)
|
||||||
if not i % 1000:
|
if not i % 1000:
|
||||||
self.stdout.write(f' {i}')
|
self.stdout.write(f' {i}')
|
||||||
self.stdout.write(self.style.SUCCESS(f' Retraced {i} {model._meta.verbose_name_plural}'))
|
self.stdout.write(self.style.SUCCESS(f' Retraced {i} {model._meta.verbose_name_plural}'))
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -256,7 +255,7 @@ class PathEndpoint(models.Model):
|
|||||||
"""
|
"""
|
||||||
Any object which may serve as either endpoint of a CablePath.
|
Any object which may serve as either endpoint of a CablePath.
|
||||||
"""
|
"""
|
||||||
paths = GenericRelation(
|
_paths = GenericRelation(
|
||||||
to='dcim.CablePath',
|
to='dcim.CablePath',
|
||||||
content_type_field='origin_type',
|
content_type_field='origin_type',
|
||||||
object_id_field='origin_id',
|
object_id_field='origin_id',
|
||||||
@ -266,6 +265,15 @@ class PathEndpoint(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
"""
|
||||||
|
Return the _complete_ CablePath associated with this origin point, if any.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, '_path'):
|
||||||
|
self._path = self._paths.filter(destination_id__isnull=False).first()
|
||||||
|
return self._path
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console ports
|
# Console ports
|
||||||
|
@ -6,14 +6,15 @@ from django.db import transaction
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis
|
from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis
|
||||||
from .utils import object_to_path_node, trace_paths
|
from .utils import object_to_path_node, trace_path
|
||||||
|
|
||||||
|
|
||||||
def create_cablepaths(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.
|
||||||
"""
|
"""
|
||||||
for path, destination in trace_paths(node):
|
path, destination = trace_path(node)
|
||||||
|
if path:
|
||||||
cp = CablePath(origin=node, path=path, destination=destination)
|
cp = CablePath(origin=node, path=path, destination=destination)
|
||||||
cp.save()
|
cp.save()
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ def rebuild_paths(obj):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for cp in cable_paths:
|
for cp in cable_paths:
|
||||||
cp.delete()
|
cp.delete()
|
||||||
create_cablepaths(cp.origin)
|
create_cablepath(cp.origin)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=VirtualChassis)
|
@receiver(post_save, sender=VirtualChassis)
|
||||||
@ -76,7 +77,7 @@ def update_connected_endpoints(instance, created, **kwargs):
|
|||||||
if created:
|
if created:
|
||||||
for termination in (instance.termination_a, instance.termination_b):
|
for termination in (instance.termination_a, instance.termination_b):
|
||||||
if isinstance(termination, PathEndpoint):
|
if isinstance(termination, PathEndpoint):
|
||||||
create_cablepaths(termination)
|
create_cablepath(termination)
|
||||||
else:
|
else:
|
||||||
rebuild_paths(termination)
|
rebuild_paths(termination)
|
||||||
else:
|
else:
|
||||||
@ -116,4 +117,4 @@ def nullify_connected_endpoints(instance, **kwargs):
|
|||||||
origin_type=ContentType.objects.get_for_model(origin),
|
origin_type=ContentType.objects.get_for_model(origin),
|
||||||
origin_id=origin.pk
|
origin_id=origin.pk
|
||||||
).delete()
|
).delete()
|
||||||
create_cablepaths(origin)
|
create_cablepath(origin)
|
||||||
|
@ -123,69 +123,43 @@ class CablePathTestCase(TestCase):
|
|||||||
# Check that all CablePaths have been deleted
|
# Check that all CablePaths have been deleted
|
||||||
self.assertEqual(CablePath.objects.count(), 0)
|
self.assertEqual(CablePath.objects.count(), 0)
|
||||||
|
|
||||||
def test_02_interfaces_to_interface_via_pass_through(self):
|
def test_02_interface_to_interface_via_pass_through(self):
|
||||||
"""
|
"""
|
||||||
[IF1] --C1-- [FP1:1] [RP1] --C3-- [IF3]
|
[IF1] --C1-- [FP5] [RP5] --C2-- [IF2]
|
||||||
[IF2] --C2-- [FP1:2]
|
|
||||||
"""
|
"""
|
||||||
# Create cables 1 and 2
|
# Create cable 1
|
||||||
cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[0])
|
cable1 = Cable(termination_a=self.interfaces[0], termination_b=self.front_ports[16])
|
||||||
cable1.save()
|
cable1.save()
|
||||||
cable2 = Cable(termination_a=self.interfaces[1], termination_b=self.front_ports[1])
|
self.assertPathExists(
|
||||||
|
origin=self.interfaces[0],
|
||||||
|
destination=None,
|
||||||
|
path=(cable1, self.front_ports[16], self.rear_ports[4])
|
||||||
|
)
|
||||||
|
self.assertEqual(CablePath.objects.count(), 1)
|
||||||
|
|
||||||
|
# Create cable 2
|
||||||
|
cable2 = Cable(termination_a=self.rear_ports[4], termination_b=self.interfaces[1])
|
||||||
cable2.save()
|
cable2.save()
|
||||||
self.assertPathExists(
|
self.assertPathExists(
|
||||||
origin=self.interfaces[0],
|
origin=self.interfaces[0],
|
||||||
destination=None,
|
destination=self.interfaces[1],
|
||||||
path=(cable1, self.front_ports[0], self.rear_ports[0])
|
path=(cable1, self.front_ports[16], self.rear_ports[4], cable2)
|
||||||
)
|
)
|
||||||
self.assertPathExists(
|
self.assertPathExists(
|
||||||
origin=self.interfaces[1],
|
origin=self.interfaces[1],
|
||||||
destination=None,
|
destination=self.interfaces[0],
|
||||||
path=(cable2, self.front_ports[1], self.rear_ports[0])
|
path=(cable2, self.rear_ports[4], self.front_ports[16], cable1)
|
||||||
)
|
)
|
||||||
self.assertEqual(CablePath.objects.count(), 2)
|
self.assertEqual(CablePath.objects.count(), 2)
|
||||||
|
|
||||||
# Create cable 3
|
# Delete cable 2
|
||||||
cable3 = Cable(termination_a=self.rear_ports[0], termination_b=self.interfaces[2])
|
cable2.delete()
|
||||||
cable3.save()
|
|
||||||
self.assertPathExists(
|
|
||||||
origin=self.interfaces[0],
|
|
||||||
destination=self.interfaces[2],
|
|
||||||
path=(cable1, self.front_ports[0], self.rear_ports[0], cable3)
|
|
||||||
)
|
|
||||||
self.assertPathExists(
|
|
||||||
origin=self.interfaces[1],
|
|
||||||
destination=self.interfaces[2],
|
|
||||||
path=(cable2, self.front_ports[1], self.rear_ports[0], cable3)
|
|
||||||
)
|
|
||||||
self.assertPathExists(
|
|
||||||
origin=self.interfaces[2],
|
|
||||||
destination=self.interfaces[0],
|
|
||||||
path=(cable3, self.rear_ports[0], self.front_ports[0], cable1)
|
|
||||||
)
|
|
||||||
self.assertPathExists(
|
|
||||||
origin=self.interfaces[2],
|
|
||||||
destination=self.interfaces[1],
|
|
||||||
path=(cable3, self.rear_ports[0], self.front_ports[1], cable2)
|
|
||||||
)
|
|
||||||
self.assertEqual(CablePath.objects.count(), 6) # Four complete + two partial paths
|
|
||||||
|
|
||||||
# Delete cable 3
|
|
||||||
cable3.delete()
|
|
||||||
self.assertPathExists(
|
self.assertPathExists(
|
||||||
origin=self.interfaces[0],
|
origin=self.interfaces[0],
|
||||||
destination=None,
|
destination=None,
|
||||||
path=(cable1, self.front_ports[0], self.rear_ports[0])
|
path=(cable1, self.front_ports[16], self.rear_ports[4])
|
||||||
)
|
)
|
||||||
self.assertPathExists(
|
self.assertEqual(CablePath.objects.count(), 1)
|
||||||
origin=self.interfaces[1],
|
|
||||||
destination=None,
|
|
||||||
path=(cable2, self.front_ports[1], self.rear_ports[0])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for two partial paths from IF1 and IF2
|
|
||||||
self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 2)
|
|
||||||
self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0)
|
|
||||||
|
|
||||||
def test_03_interfaces_to_interfaces_via_pass_through(self):
|
def test_03_interfaces_to_interfaces_via_pass_through(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from .exceptions import CableTraceSplit
|
||||||
from .models import FrontPort, RearPort
|
from .models import FrontPort, RearPort
|
||||||
|
|
||||||
|
|
||||||
@ -17,13 +18,13 @@ def path_node_to_object(repr):
|
|||||||
return model_class.objects.get(pk=int(object_id))
|
return model_class.objects.get(pk=int(object_id))
|
||||||
|
|
||||||
|
|
||||||
def trace_paths(node):
|
def trace_path(node):
|
||||||
destination = None
|
destination = None
|
||||||
path = []
|
path = []
|
||||||
position_stack = []
|
position_stack = []
|
||||||
|
|
||||||
if node.cable is None:
|
if node.cable is None:
|
||||||
return []
|
return [], None
|
||||||
|
|
||||||
while node.cable is not None:
|
while node.cable is not None:
|
||||||
|
|
||||||
@ -50,20 +51,12 @@ def trace_paths(node):
|
|||||||
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position)
|
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position)
|
||||||
path.append(object_to_path_node(node))
|
path.append(object_to_path_node(node))
|
||||||
else:
|
else:
|
||||||
# No position indicated, so we have to trace _all_ peer FrontPorts
|
# No position indicated: path has split (probably invalid?)
|
||||||
paths = []
|
raise CableTraceSplit(peer_termination)
|
||||||
for frontport in FrontPort.objects.filter(rear_port=peer_termination):
|
|
||||||
branches = trace_paths(frontport)
|
|
||||||
if branches:
|
|
||||||
for branch, destination in branches:
|
|
||||||
paths.append(([*path, object_to_path_node(frontport), *branch], destination))
|
|
||||||
else:
|
|
||||||
paths.append(([*path, object_to_path_node(frontport)], None))
|
|
||||||
return paths
|
|
||||||
|
|
||||||
# Anything else marks the end of the path
|
# Anything else marks the end of the path
|
||||||
else:
|
else:
|
||||||
destination = peer_termination
|
destination = peer_termination
|
||||||
break
|
break
|
||||||
|
|
||||||
return [(path, destination)]
|
return path, destination
|
||||||
|
@ -1018,7 +1018,7 @@ class DeviceView(ObjectView):
|
|||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||||
Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
||||||
'cable',
|
'cable',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1026,25 +1026,25 @@ class DeviceView(ObjectView):
|
|||||||
consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter(
|
consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter(
|
||||||
device=device
|
device=device
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
||||||
'cable',
|
'cable',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Power ports
|
# Power ports
|
||||||
powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||||
Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
||||||
'cable',
|
'cable',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Power outlets
|
# Power outlets
|
||||||
poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||||
Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
||||||
'cable', 'power_port',
|
'cable', 'power_port',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related(
|
interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related(
|
||||||
Prefetch('paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
Prefetch('_paths', queryset=CablePath.objects.filter(destination_id__isnull=False)),
|
||||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||||
'lag', 'cable', 'tags',
|
'lag', 'cable', 'tags',
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{# Connection #}
|
{# Connection #}
|
||||||
{% include 'dcim/inc/endpoint_connection.html' with paths=cp.paths.all %}
|
{% include 'dcim/inc/endpoint_connection.html' with path=cp.path %}
|
||||||
|
|
||||||
{# Actions #}
|
{# Actions #}
|
||||||
<td class="text-right noprint">
|
<td class="text-right noprint">
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{# Connection #}
|
{# Connection #}
|
||||||
{% include 'dcim/inc/endpoint_connection.html' with paths=csp.paths.all %}
|
{% include 'dcim/inc/endpoint_connection.html' with path=csp.path %}
|
||||||
|
|
||||||
{# Actions #}
|
{# Actions #}
|
||||||
<td class="text-right noprint">
|
<td class="text-right noprint">
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
{% if paths|length > 1 %}
|
{% if path %}
|
||||||
<td colspan="2">Multiple connections</td>
|
{% with endpoint=path.destination %}
|
||||||
{% elif paths %}
|
|
||||||
{% with endpoint=paths.0.destination %}
|
|
||||||
<td><a href="{{ endpoint.parent.get_absolute_url }}">{{ endpoint.parent }}</a></td>
|
<td><a href="{{ endpoint.parent.get_absolute_url }}">{{ endpoint.parent }}</a></td>
|
||||||
<td><a href="{{ endpoint.get_absolute_url }}">{{ endpoint }}</a></td>
|
<td><a href="{{ endpoint.get_absolute_url }}">{{ endpoint }}</a></td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
{% elif iface.is_wireless %}
|
{% elif iface.is_wireless %}
|
||||||
<td colspan="2" class="text-muted">Wireless interface</td>
|
<td colspan="2" class="text-muted">Wireless interface</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include 'dcim/inc/endpoint_connection.html' with paths=iface.paths.all %}
|
{% include 'dcim/inc/endpoint_connection.html' with path=iface.path %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Buttons #}
|
{# Buttons #}
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{# Connection #}
|
{# Connection #}
|
||||||
{% with paths=po.paths.all %}
|
{% with path=po.path %}
|
||||||
{% include 'dcim/inc/endpoint_connection.html' %}
|
{% include 'dcim/inc/endpoint_connection.html' %}
|
||||||
<td>
|
<td>
|
||||||
{% if paths|length == 1 %}
|
{% if paths|length == 1 %}
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{# Connection #}
|
{# Connection #}
|
||||||
{% include 'dcim/inc/endpoint_connection.html' with paths=pp.paths.all %}
|
{% include 'dcim/inc/endpoint_connection.html' with path=pp.path %}
|
||||||
|
|
||||||
{# Actions #}
|
{# Actions #}
|
||||||
<td class="text-right noprint">
|
<td class="text-right noprint">
|
||||||
|
Reference in New Issue
Block a user