Merge pull request #4706 from netbox-community/4604_check_position_stack

4708: more flexible checks on RearPort usage
This commit is contained in:
Jeremy Stretch 2020-07-01 10:59:17 -04:00 committed by GitHub
commit 56ec4a6360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 311 additions and 53 deletions

View File

@ -2129,6 +2129,7 @@ class Cable(ChangeLoggedModel):
return reverse('dcim:cable', args=[self.pk]) return reverse('dcim:cable', args=[self.pk])
def clean(self): def clean(self):
from circuits.models import CircuitTermination
# Validate that termination A exists # Validate that termination A exists
if not hasattr(self, 'termination_a_type'): if not hasattr(self, 'termination_a_type'):
@ -2191,19 +2192,21 @@ class Cable(ChangeLoggedModel):
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}"
) )
# A RearPort with multiple positions must be connected to a RearPort with an equal number of positions # 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 [ for term_a, term_b in [
(self.termination_a, self.termination_b), (self.termination_a, self.termination_b),
(self.termination_b, self.termination_a) (self.termination_b, self.termination_a)
]: ]:
if isinstance(term_a, RearPort) and term_a.positions > 1: if isinstance(term_a, RearPort) and term_a.positions > 1:
if not isinstance(term_b, RearPort): if not isinstance(term_b, (FrontPort, RearPort, CircuitTermination)):
raise ValidationError( raise ValidationError(
"Rear ports with multiple positions may only be connected to other rear ports" "Rear ports with multiple positions may only be connected to other pass-through ports"
) )
elif term_a.positions != term_b.positions: if isinstance(term_b, RearPort) and term_b.positions > 1 and term_a.positions != term_b.positions:
raise ValidationError( raise ValidationError(
f"{term_a} has {term_a.positions} position(s) but {term_b} has {term_b.positions}. " 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." f"Both terminations must have the same number of positions."
) )

View File

@ -89,16 +89,16 @@ class CableTermination(models.Model):
object_id_field='termination_b_id' object_id_field='termination_b_id'
) )
is_path_endpoint = True
class Meta: class Meta:
abstract = True abstract = True
def trace(self): def trace(self):
""" """
Return two items: the traceable portion of a cable path, and the termination points where it splits (if any). Return three items: the traceable portion of a cable path, the termination points where it splits (if any), and
This occurs when the trace is initiated from a midpoint along a path which traverses a RearPort. In cases where the remaining positions on the position stack (if any). Splits occur when the trace is initiated from a midpoint
the originating endpoint is unknown, it is not possible to know which corresponding FrontPort to follow. along a path which traverses a RearPort. In cases where the originating endpoint is unknown, it is not possible
to know which corresponding FrontPort to follow. Remaining positions occur when tracing a path that traverses
a FrontPort without traversing a RearPort again.
The path is a list representing a complete cable path, with each individual segment represented as a The path is a list representing a complete cable path, with each individual segment represented as a
three-tuple: three-tuple:
@ -118,26 +118,35 @@ class CableTermination(models.Model):
# Map a front port to its corresponding rear port # Map a front port to its corresponding rear port
if isinstance(termination, FrontPort): if isinstance(termination, FrontPort):
position_stack.append(termination.rear_port_position)
# Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance # Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance
peer_port = RearPort.objects.get(pk=termination.rear_port.pk) peer_port = RearPort.objects.get(pk=termination.rear_port.pk)
# Don't use the stack for RearPorts with a single position. Only remember the position at
# many-to-one points so we can select the correct FrontPort when we reach the corresponding
# one-to-many point.
if peer_port.positions > 1:
position_stack.append(termination)
return peer_port return peer_port
# Map a rear port/position to its corresponding front port # Map a rear port/position to its corresponding front port
elif isinstance(termination, RearPort): elif isinstance(termination, RearPort):
if termination.positions > 1:
# Can't map to a FrontPort without a position if there are multiple options
if not position_stack:
raise CableTraceSplit(termination)
# Can't map to a FrontPort without a position if there are multiple options front_port = position_stack.pop()
if termination.positions > 1 and not position_stack: position = front_port.rear_port_position
raise CableTraceSplit(termination)
# We can assume position 1 if the RearPort has only one position # Validate the position
position = position_stack.pop() if position_stack else 1 if position not in range(1, termination.positions + 1):
raise Exception("Invalid position for {} ({} positions): {})".format(
# Validate the position termination, termination.positions, position
if position not in range(1, termination.positions + 1): ))
raise Exception("Invalid position for {} ({} positions): {})".format( else:
termination, termination.positions, position # Don't use the stack for RearPorts with a single position. The only possible position is 1.
)) position = 1
try: try:
peer_port = FrontPort.objects.get( peer_port = FrontPort.objects.get(
@ -168,12 +177,12 @@ class CableTermination(models.Model):
if not endpoint.cable: if not endpoint.cable:
path.append((endpoint, None, None)) path.append((endpoint, None, None))
logger.debug("No cable connected") logger.debug("No cable connected")
return path, None return path, None, position_stack
# Check for loops # Check for loops
if endpoint.cable in [segment[1] for segment in path]: if endpoint.cable in [segment[1] for segment in path]:
logger.debug("Loop detected!") logger.debug("Loop detected!")
return path, None return path, None, position_stack
# Record the current segment in the path # Record the current segment in the path
far_end = endpoint.get_cable_peer() far_end = endpoint.get_cable_peer()
@ -186,10 +195,10 @@ class CableTermination(models.Model):
try: try:
endpoint = get_peer_port(far_end) endpoint = get_peer_port(far_end)
except CableTraceSplit as e: except CableTraceSplit as e:
return path, e.termination.frontports.all() return path, e.termination.frontports.all(), position_stack
if endpoint is None: if endpoint is None:
return path, None return path, None, position_stack
def get_cable_peer(self): def get_cable_peer(self):
if self.cable is None: if self.cable is None:
@ -206,7 +215,7 @@ class CableTermination(models.Model):
endpoints = [] endpoints = []
# Get the far end of the last path segment # Get the far end of the last path segment
path, split_ends = self.trace() path, split_ends, position_stack = self.trace()
endpoint = path[-1][2] endpoint = path[-1][2]
if split_ends is not None: if split_ends is not None:
for termination in split_ends: for termination in split_ends:
@ -872,7 +881,6 @@ class FrontPort(CableTermination, ComponentModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
is_path_endpoint = False
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -937,7 +945,6 @@ class RearPort(CableTermination, ComponentModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'positions', 'description'] csv_headers = ['device', 'name', 'type', 'positions', 'description']
is_path_endpoint = False
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')

View File

@ -4,7 +4,7 @@ from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from .choices import CableStatusChoices from .choices import CableStatusChoices
from .models import Cable, Device, VirtualChassis from .models import Cable, CableTermination, Device, FrontPort, RearPort, VirtualChassis
@receiver(post_save, sender=VirtualChassis) @receiver(post_save, sender=VirtualChassis)
@ -52,7 +52,7 @@ def update_connected_endpoints(instance, **kwargs):
# Update any endpoints for this Cable. # Update any endpoints for this Cable.
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints() endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
for endpoint in endpoints: for endpoint in endpoints:
path, split_ends = endpoint.trace() path, split_ends, position_stack = endpoint.trace()
# Determine overall path status (connected or planned) # Determine overall path status (connected or planned)
path_status = True path_status = True
for segment in path: for segment in path:
@ -61,9 +61,11 @@ def update_connected_endpoints(instance, **kwargs):
break break
endpoint_a = path[0][0] endpoint_a = path[0][0]
endpoint_b = path[-1][2] endpoint_b = path[-1][2] if not split_ends and not position_stack else None
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False): # Patch panel ports are not connected endpoints, all other cable terminations are
if isinstance(endpoint_a, CableTermination) and not isinstance(endpoint_a, (FrontPort, RearPort)) and \
isinstance(endpoint_b, CableTermination) and not isinstance(endpoint_b, (FrontPort, RearPort)):
logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b)) logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
endpoint_a.connected_endpoint = endpoint_b endpoint_a.connected_endpoint = endpoint_b
endpoint_a.connection_status = path_status endpoint_a.connection_status = path_status

View File

@ -363,6 +363,7 @@ class CableTestCase(TestCase):
) )
self.interface1 = Interface.objects.create(device=self.device1, name='eth0') self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
self.interface2 = Interface.objects.create(device=self.device2, name='eth0') self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
self.interface3 = Interface.objects.create(device=self.device2, name='eth1')
self.cable = Cable(termination_a=self.interface1, termination_b=self.interface2) self.cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
self.cable.save() self.cable.save()
@ -370,10 +371,27 @@ class CableTestCase(TestCase):
self.patch_pannel = Device.objects.create( self.patch_pannel = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site
) )
self.rear_port = RearPort.objects.create(device=self.patch_pannel, name='R1', type=1000) self.rear_port1 = RearPort.objects.create(device=self.patch_pannel, name='RP1', type='8p8c')
self.front_port = FrontPort.objects.create( self.front_port1 = FrontPort.objects.create(
device=self.patch_pannel, name='F1', type=1000, rear_port=self.rear_port device=self.patch_pannel, name='FP1', type='8p8c', rear_port=self.rear_port1, rear_port_position=1
) )
self.rear_port2 = RearPort.objects.create(device=self.patch_pannel, name='RP2', type='8p8c', positions=2)
self.front_port2 = FrontPort.objects.create(
device=self.patch_pannel, name='FP2', type='8p8c', rear_port=self.rear_port2, rear_port_position=1
)
self.rear_port3 = RearPort.objects.create(device=self.patch_pannel, name='RP3', type='8p8c', positions=3)
self.front_port3 = FrontPort.objects.create(
device=self.patch_pannel, name='FP3', type='8p8c', rear_port=self.rear_port3, rear_port_position=1
)
self.rear_port4 = RearPort.objects.create(device=self.patch_pannel, name='RP4', type='8p8c', positions=3)
self.front_port4 = FrontPort.objects.create(
device=self.patch_pannel, name='FP4', type='8p8c', rear_port=self.rear_port4, rear_port_position=1
)
self.provider = Provider.objects.create(name='Provider 1', slug='provider-1')
self.circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
self.circuit = Circuit.objects.create(provider=self.provider, type=self.circuittype, cid='1')
self.circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=site, term_side='A', port_speed=1000)
self.circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site, term_side='Z', port_speed=1000)
def test_cable_creation(self): def test_cable_creation(self):
""" """
@ -405,7 +423,7 @@ class CableTestCase(TestCase):
cable = Cable.objects.filter(pk=self.cable.pk).first() cable = Cable.objects.filter(pk=self.cable.pk).first()
self.assertIsNone(cable) self.assertIsNone(cable)
def test_cable_validates_compatibale_types(self): def test_cable_validates_compatible_types(self):
""" """
The clean method should have a check to ensure only compatible port types can be connected by a cable The clean method should have a check to ensure only compatible port types can be connected by a cable
""" """
@ -426,7 +444,7 @@ class CableTestCase(TestCase):
""" """
A cable cannot connect a front port to its corresponding rear port A cable cannot connect a front port to its corresponding rear port
""" """
cable = Cable(termination_a=self.front_port, termination_b=self.rear_port) cable = Cable(termination_a=self.front_port1, termination_b=self.rear_port1)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() cable.clean()
@ -439,7 +457,94 @@ class CableTestCase(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() cable.clean()
def test_cable_cannot_terminate_to_a_virtual_inteface(self): def test_connection_via_single_position_rearport(self):
"""
A RearPort with one position can be connected to anything.
[CableTermination X]---[RP(pos=1) FP]---[CableTermination Y]
is allowed anywhere
[CableTermination X]---[CableTermination Y]
is allowed.
A RearPort with multiple positions may not be directly connected to a path endpoint or another RearPort
with a different number of positions. RearPorts with a single position on the other hand may be connected
to such CableTerminations. Check that this is indeed allowed.
"""
# Connecting a single-position RearPort to a multi-position RearPort is ok
Cable(termination_a=self.rear_port1, termination_b=self.rear_port2).full_clean()
# Connecting a single-position RearPort to an Interface is ok
Cable(termination_a=self.rear_port1, termination_b=self.interface3).full_clean()
# Connecting a single-position RearPort to a CircuitTermination is ok
Cable(termination_a=self.rear_port1, termination_b=self.circuittermination1).full_clean()
def test_connection_via_multi_position_rearport(self):
"""
A RearPort with multiple positions may not be directly connected to a path endpoint or another RearPort
with a different number of positions.
The following scenario's are allowed (with x>1):
~----------+ +---------~
| |
RP2(pos=x)|---|RP(pos=x)
| |
~----------+ +---------~
~----------+ +---------~
| |
RP2(pos=x)|---|RP(pos=1)
| |
~----------+ +---------~
~----------+ +------------------~
| |
RP2(pos=x)|---|CircuitTermination
| |
~----------+ +------------------~
These scenarios are NOT allowed (with x>1):
~----------+ +----------~
| |
RP2(pos=x)|---|RP(pos!=x)
| |
~----------+ +----------~
~----------+ +----------~
| |
RP2(pos=x)|---|Interface
| |
~----------+ +----------~
These scenarios are tested in this order below.
"""
# Connecting a multi-position RearPort to another RearPort with the same number of positions is ok
Cable(termination_a=self.rear_port3, termination_b=self.rear_port4).full_clean()
# Connecting a multi-position RearPort to a single-position RearPort is ok
Cable(termination_a=self.rear_port2, termination_b=self.rear_port1).full_clean()
# Connecting a multi-position RearPort to a CircuitTermination is ok
Cable(termination_a=self.rear_port2, termination_b=self.circuittermination1).full_clean()
with self.assertRaises(
ValidationError,
msg='Connecting a 2-position RearPort to a 3-position RearPort should fail'
):
Cable(termination_a=self.rear_port2, termination_b=self.rear_port3).full_clean()
with self.assertRaises(
ValidationError,
msg='Connecting a multi-position RearPort to an Interface should fail'
):
Cable(termination_a=self.rear_port2, termination_b=self.interface3).full_clean()
def test_cable_cannot_terminate_to_a_virtual_interface(self):
""" """
A cable cannot terminate to a virtual interface A cable cannot terminate to a virtual interface
""" """
@ -448,7 +553,7 @@ class CableTestCase(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() cable.clean()
def test_cable_cannot_terminate_to_a_wireless_inteface(self): def test_cable_cannot_terminate_to_a_wireless_interface(self):
""" """
A cable cannot terminate to a wireless interface A cable cannot terminate to a wireless interface
""" """
@ -501,9 +606,13 @@ class CablePathTestCase(TestCase):
Device(device_type=devicetype, device_role=devicerole, name='Panel 2', site=site), Device(device_type=devicetype, device_role=devicerole, name='Panel 2', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 3', site=site), Device(device_type=devicetype, device_role=devicerole, name='Panel 3', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 4', site=site), Device(device_type=devicetype, device_role=devicerole, name='Panel 4', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 5', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 6', site=site),
) )
Device.objects.bulk_create(patch_panels) Device.objects.bulk_create(patch_panels)
for patch_panel in patch_panels:
# Create patch panels with 4 positions
for patch_panel in patch_panels[:4]:
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C) rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C)
FrontPort.objects.bulk_create(( FrontPort.objects.bulk_create((
FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C), FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C),
@ -512,6 +621,11 @@ class CablePathTestCase(TestCase):
FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C), FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C),
)) ))
# Create 1-on-1 patch panels
for patch_panel in patch_panels[4:]:
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=1, type=PortTypeChoices.TYPE_8P8C)
FrontPort.objects.create(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C)
def test_direct_connection(self): def test_direct_connection(self):
""" """
Test a direct connection between two interfaces. Test a direct connection between two interfaces.
@ -524,6 +638,7 @@ class CablePathTestCase(TestCase):
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable.full_clean()
cable.save() cable.save()
# Retrieve endpoints # Retrieve endpoints
@ -551,22 +666,25 @@ class CablePathTestCase(TestCase):
def test_connection_via_single_rear_port(self): def test_connection_via_single_rear_port(self):
""" """
Test a connection which passes through a single front/rear port pair. Test a connection which passes through a rear port with exactly one front port.
1 2 1 2
[Device 1] ----- [Panel 1] ----- [Device 2] [Device 1] ----- [Panel 5] ----- [Device 2]
Iface1 FP1 RP1 Iface1 Iface1 FP1 RP1 Iface1
""" """
# Create cables # Create cables (FP first, RP second)
cable1 = Cable( cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'),
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
self.assertEqual(cable2.termination_a.positions, 1) # Sanity check
cable2.full_clean()
cable2.save() cable2.save()
# Retrieve endpoints # Retrieve endpoints
@ -592,6 +710,97 @@ class CablePathTestCase(TestCase):
self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status) self.assertIsNone(endpoint_b.connection_status)
def test_connections_via_nested_single_position_rearport(self):
"""
Test a connection which passes through a single front/rear port pair between two multi-position rear ports.
Test two connections via patched rear ports:
Device 1 <---> Device 2
Device 3 <---> Device 4
1 2
[Device 1] -----------+ +----------- [Device 2]
Iface1 | | Iface1
FP1 | 3 4 | FP1
[Panel 1] ----- [Panel 5] ----- [Panel 2]
FP2 | RP1 RP1 FP1 RP1 | FP2
Iface1 | | Iface1
[Device 3] -----------+ +----------- [Device 4]
5 6
"""
# Create cables (Panel 5 RP first, FP second)
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.full_clean()
cable1.save()
cable2 = Cable(
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable2.full_clean()
cable2.save()
cable3 = Cable(
termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1')
)
cable3.full_clean()
cable3.save()
cable4 = Cable(
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1'),
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
)
cable4.full_clean()
cable4.save()
cable5 = Cable(
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2'),
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1')
)
cable5.full_clean()
cable5.save()
cable6 = Cable(
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1')
)
cable6.full_clean()
cable6.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
self.assertTrue(endpoint_c.connection_status)
self.assertTrue(endpoint_d.connection_status)
# Delete cable 3
cable3.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
endpoint_c.refresh_from_db()
endpoint_d.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_c.connected_endpoint)
self.assertIsNone(endpoint_d.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
self.assertIsNone(endpoint_c.connection_status)
self.assertIsNone(endpoint_d.connection_status)
def test_connections_via_patch(self): def test_connections_via_patch(self):
""" """
Test two connections via patched rear ports: Test two connections via patched rear ports:
@ -613,28 +822,33 @@ class CablePathTestCase(TestCase):
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1') termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
) )
cable2.full_clean()
cable2.save() cable2.save()
cable3 = Cable( cable3 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1') termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
) )
cable3.full_clean()
cable3.save() cable3.save()
cable4 = Cable( cable4 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
) )
cable4.full_clean()
cable4.save() cable4.save()
cable5 = Cable( cable5 = Cable(
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2') termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2')
) )
cable5.full_clean()
cable5.save() cable5.save()
# Retrieve endpoints # Retrieve endpoints
@ -693,43 +907,51 @@ class CablePathTestCase(TestCase):
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'), termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1') termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1')
) )
cable2.full_clean()
cable2.save() cable2.save()
cable3 = Cable( cable3 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'), termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable3.full_clean()
cable3.save() cable3.save()
cable4 = Cable( cable4 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1') termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
) )
cable4.full_clean()
cable4.save() cable4.save()
cable5 = Cable( cable5 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'), termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1') termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
) )
cable5.full_clean()
cable5.save() cable5.save()
cable6 = Cable( cable6 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
) )
cable6.full_clean()
cable6.save() cable6.save()
cable7 = Cable( cable7 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'), termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2') termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2')
) )
cable7.full_clean()
cable7.save() cable7.save()
cable8 = Cable( cable8 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'), termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
) )
cable8.full_clean()
cable8.save() cable8.save()
# Retrieve endpoints # Retrieve endpoints
@ -789,38 +1011,45 @@ class CablePathTestCase(TestCase):
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'), termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable2.full_clean()
cable2.save() cable2.save()
cable3 = Cable( cable3 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1') termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
) )
cable3.full_clean()
cable3.save() cable3.save()
cable4 = Cable( cable4 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'), termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1') termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1')
) )
cable4.full_clean()
cable4.save() cable4.save()
cable5 = Cable( cable5 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'), termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1') termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
) )
cable5.full_clean()
cable5.save() cable5.save()
cable6 = Cable( cable6 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
) )
cable6.full_clean()
cable6.save() cable6.save()
cable7 = Cable( cable7 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'), termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
) )
cable7.full_clean()
cable7.save() cable7.save()
# Retrieve endpoints # Retrieve endpoints
@ -870,11 +1099,13 @@ class CablePathTestCase(TestCase):
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=CircuitTermination.objects.get(term_side='A') termination_b=CircuitTermination.objects.get(term_side='A')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=CircuitTermination.objects.get(term_side='Z'), termination_a=CircuitTermination.objects.get(term_side='Z'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable2.full_clean()
cable2.save() cable2.save()
# Retrieve endpoints # Retrieve endpoints
@ -903,30 +1134,34 @@ class CablePathTestCase(TestCase):
def test_connection_via_patched_circuit(self): def test_connection_via_patched_circuit(self):
""" """
1 2 3 4 1 2 3 4
[Device 1] ----- [Panel 1] ----- [Circuit] ----- [Panel 2] ----- [Device 2] [Device 1] ----- [Panel 5] ----- [Circuit] ----- [Panel 6] ----- [Device 2]
Iface1 FP1 RP1 A Z RP1 FP1 Iface1 Iface1 FP1 RP1 A Z RP1 FP1 Iface1
""" """
# Create cables # Create cables
cable1 = Cable( cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1') termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'),
termination_b=CircuitTermination.objects.get(term_side='A') termination_b=CircuitTermination.objects.get(term_side='A')
) )
cable2.full_clean()
cable2.save() cable2.save()
cable3 = Cable( cable3 = Cable(
termination_a=CircuitTermination.objects.get(term_side='Z'), termination_a=CircuitTermination.objects.get(term_side='Z'),
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1') termination_b=RearPort.objects.get(device__name='Panel 6', name='Rear Port 1')
) )
cable3.full_clean()
cable3.save() cable3.save()
cable4 = Cable( cable4 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'), termination_a=FrontPort.objects.get(device__name='Panel 6', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable4.full_clean()
cable4.save() cable4.save()
# Retrieve endpoints # Retrieve endpoints

View File

@ -2057,7 +2057,7 @@ class CableTraceView(PermissionRequiredMixin, View):
def get(self, request, model, pk): def get(self, request, model, pk):
obj = get_object_or_404(model, pk=pk) obj = get_object_or_404(model, pk=pk)
path, split_ends = obj.trace() path, split_ends, position_stack = obj.trace()
total_length = sum( total_length = sum(
[entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length] [entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length]
) )
@ -2066,6 +2066,7 @@ class CableTraceView(PermissionRequiredMixin, View):
'obj': obj, 'obj': obj,
'trace': path, 'trace': path,
'split_ends': split_ends, 'split_ends': split_ends,
'position_stack': position_stack,
'total_length': total_length, 'total_length': total_length,
}) })

View File

@ -88,6 +88,16 @@
</table> </table>
</div> </div>
</div> </div>
{% elif position_stack %}
<div class="col-md-11 col-md-offset-1">
<h3 class="text-warning text-center">
{% with last_position=position_stack|last %}
Trace completed, but there is no Front Port corresponding to
<a href="{{ last_position.device.get_absolute_url }}">{{ last_position.device }}</a> {{ last_position }}.<br>
Therefore no end-to-end connection can be established.
{% endwith %}
</h3>
</div>
{% else %} {% else %}
<div class="col-md-11 col-md-offset-1"> <div class="col-md-11 col-md-offset-1">
<h3 class="text-success text-center">Trace completed!</h3> <h3 class="text-success text-center">Trace completed!</h3>