1
0
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:
Jeremy Stretch
2020-10-01 14:16:43 -04:00
parent cd398b15d8
commit 610420c020
13 changed files with 66 additions and 97 deletions

View File

@ -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
# #

View File

@ -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}'))

View File

@ -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

View File

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

View File

@ -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):
""" """

View File

@ -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

View File

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

View File

@ -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">

View File

@ -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">

View File

@ -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 %}

View File

@ -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 #}

View File

@ -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 %}

View File

@ -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">