From a0f4d481dce4ef7bb69f63e271e46cd6504f462c Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Thu, 7 May 2020 02:12:08 +0200 Subject: [PATCH 01/21] make single front/rear port work when between panels --- netbox/dcim/models/device_components.py | 30 +++-- netbox/dcim/tests/test_models.py | 162 +++++++++++++++++++++++- 2 files changed, 176 insertions(+), 16 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 4005d41a4..d854fdc6e 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -115,26 +115,32 @@ class CableTermination(models.Model): # Map a front port to its corresponding rear port 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 peer_port = RearPort.objects.get(pk=termination.rear_port.pk) + + # Don't use the stack for 1-on-1 ports, they don't have to come in pairs + if peer_port.positions > 1: + position_stack.append(termination.rear_port_position) + return peer_port # Map a rear port/position to its corresponding front port 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 - if termination.positions > 1 and not position_stack: - raise CableTraceSplit(termination) + position = position_stack.pop() - # We can assume position 1 if the RearPort has only one position - position = position_stack.pop() if position_stack else 1 - - # Validate the position - if position not in range(1, termination.positions + 1): - raise Exception("Invalid position for {} ({} positions): {})".format( - termination, termination.positions, position - )) + # Validate the position + if position not in range(1, termination.positions + 1): + raise Exception("Invalid position for {} ({} positions): {})".format( + termination, termination.positions, position + )) + else: + # Don't use the stack for 1-on-1 ports, they don't have to come in pairs + position = 1 try: peer_port = FrontPort.objects.get( diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 6db938732..62167b601 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -512,6 +512,11 @@ class CablePathTestCase(TestCase): FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C), )) + # Create a 1-on-1 patch panel + patch_panel = Device.objects.create(device_type=devicetype, device_role=devicerole, name='Panel 5', site=site) + 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): """ Test a direct connection between two interfaces. @@ -554,17 +559,17 @@ class CablePathTestCase(TestCase): Test a connection which passes through a single front/rear port pair. 1 2 - [Device 1] ----- [Panel 1] ----- [Device 2] + [Device 1] ----- [Panel 5] ----- [Device 2] Iface1 FP1 RP1 Iface1 """ - # Create cables + # Create cables (FP first, RP 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') + termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1') ) cable1.save() cable2 = Cable( - termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), + termination_b=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'), termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') ) cable2.save() @@ -592,6 +597,155 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_b.connection_status) + # Recreate cable 1 to test creating the cables in reverse order (RP first, FP second) + cable1.save() + + # Refresh endpoints + endpoint_a.refresh_from_db() + endpoint_b.refresh_from_db() + + # Validate connections + self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) + self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) + self.assertTrue(endpoint_a.connection_status) + self.assertTrue(endpoint_b.connection_status) + + # Delete cable 2 + cable2.delete() + + # Refresh endpoints + endpoint_a.refresh_from_db() + endpoint_b.refresh_from_db() + + # Check that connections have been nullified + self.assertIsNone(endpoint_a.connected_endpoint) + self.assertIsNone(endpoint_b.connected_endpoint) + self.assertIsNone(endpoint_a.connection_status) + self.assertIsNone(endpoint_b.connection_status) + + def test_connection_via_nested_single_rear_port(self): + """ + Test a connection which passes through a single front/rear port pair between two multi-port MUXes. + + 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.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.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.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.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.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.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) + + # Recreate cable 3 to test reverse order (Panel 5 FP first, RP second) + cable3.save() + + # Refresh endpoints + endpoint_a.refresh_from_db() + endpoint_b.refresh_from_db() + endpoint_c.refresh_from_db() + endpoint_d.refresh_from_db() + + # 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 4 + cable4.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): """ Test two connections via patched rear ports: From 112dfb865bd748af3dc5d6a9a4e8742d1f9dfb76 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Fri, 8 May 2020 01:24:30 +0200 Subject: [PATCH 02/21] Integrate patch panel building into one list --- netbox/dcim/tests/test_models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 62167b601..ab541e550 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -501,9 +501,12 @@ class CablePathTestCase(TestCase): 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 4', site=site), + Device(device_type=devicetype, device_role=devicerole, name='Panel 5', site=site), ) 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) FrontPort.objects.bulk_create(( FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C), @@ -513,9 +516,9 @@ class CablePathTestCase(TestCase): )) # Create a 1-on-1 patch panel - patch_panel = Device.objects.create(device_type=devicetype, device_role=devicerole, name='Panel 5', site=site) - 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) + 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): """ From 3278cc8cc0e26a26fa7b62f160327fdb62a73634 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Fri, 8 May 2020 01:28:36 +0200 Subject: [PATCH 03/21] Recreate the model instance instead of re-saving a deleted model Same end result, but easier to read --- netbox/dcim/tests/test_models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index ab541e550..3920e990e 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -601,6 +601,10 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_b.connection_status) # Recreate cable 1 to test creating the cables in reverse order (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 5', name='Front Port 1') + ) cable1.save() # Refresh endpoints @@ -712,6 +716,10 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_d.connection_status) # Recreate cable 3 to test reverse order (Panel 5 FP first, RP second) + 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.save() # Refresh endpoints From 56898f7e3797b20f6c9a2c92972b377abc2abe6b Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Fri, 8 May 2020 01:42:17 +0200 Subject: [PATCH 04/21] Restore original test_connection_via_single_rear_port test and make separate test for one-on-one panels --- netbox/dcim/tests/test_models.py | 47 +++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 3920e990e..9a816ce30 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -561,6 +561,51 @@ class CablePathTestCase(TestCase): """ Test a connection which passes through a single front/rear port pair. + 1 2 + [Device 1] ----- [Panel 1] ----- [Device 2] + Iface1 FP1 RP1 Iface1 + + TODO: Panel 1's rear port has multiple front ports. Should this even work? + """ + # Create cables (FP first, RP 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.save() + cable2 = Cable( + termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), + termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') + ) + cable2.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') + + # Validate connections + self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) + self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) + self.assertTrue(endpoint_a.connection_status) + self.assertTrue(endpoint_b.connection_status) + + # Delete cable 1 + cable1.delete() + + # Refresh endpoints + endpoint_a.refresh_from_db() + endpoint_b.refresh_from_db() + + # Check that connections have been nullified + self.assertIsNone(endpoint_a.connected_endpoint) + self.assertIsNone(endpoint_b.connected_endpoint) + self.assertIsNone(endpoint_a.connection_status) + self.assertIsNone(endpoint_b.connection_status) + + def test_connection_via_one_on_one_port(self): + """ + Test a connection which passes through a rear port with exactly one front port. + 1 2 [Device 1] ----- [Panel 5] ----- [Device 2] Iface1 FP1 RP1 Iface1 @@ -630,7 +675,7 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_b.connection_status) - def test_connection_via_nested_single_rear_port(self): + def test_connection_via_nested_one_on_one_port(self): """ Test a connection which passes through a single front/rear port pair between two multi-port MUXes. From 1d33d7d2059b4e9216fd4e56aaa2789836469166 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 8 May 2020 10:44:06 -0400 Subject: [PATCH 05/21] Call full_clean() when saving Cable instances --- netbox/dcim/tests/test_models.py | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 9a816ce30..ceefbe87e 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -572,11 +572,13 @@ class CablePathTestCase(TestCase): 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=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') ) + cable2.full_clean() cable2.save() # Retrieve endpoints @@ -615,11 +617,13 @@ class CablePathTestCase(TestCase): termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1') ) + cable1.full_clean() cable1.save() cable2 = Cable( termination_b=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'), termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') ) + cable2.full_clean() cable2.save() # Retrieve endpoints @@ -650,6 +654,7 @@ class CablePathTestCase(TestCase): termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1') ) + cable1.full_clean() cable1.save() # Refresh endpoints @@ -698,31 +703,37 @@ class CablePathTestCase(TestCase): 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 @@ -765,6 +776,7 @@ class CablePathTestCase(TestCase): 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() # Refresh endpoints @@ -823,28 +835,33 @@ class CablePathTestCase(TestCase): 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_a=Interface.objects.get(device__name='Device 2', name='Interface 1'), termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1') ) + cable2.full_clean() cable2.save() cable3 = Cable( 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') ) + cable3.full_clean() cable3.save() cable4 = Cable( 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') ) + cable4.full_clean() cable4.save() cable5 = Cable( 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') ) + cable5.full_clean() cable5.save() # Retrieve endpoints @@ -903,43 +920,51 @@ class CablePathTestCase(TestCase): 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_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') ) + cable2.full_clean() cable2.save() cable3 = Cable( 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') ) + cable3.full_clean() cable3.save() cable4 = Cable( 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') ) + cable4.full_clean() cable4.save() cable5 = Cable( 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') ) + cable5.full_clean() cable5.save() cable6 = Cable( 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') ) + cable6.full_clean() cable6.save() cable7 = Cable( 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') ) + cable7.full_clean() cable7.save() cable8 = Cable( 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') ) + cable8.full_clean() cable8.save() # Retrieve endpoints @@ -999,38 +1024,45 @@ class CablePathTestCase(TestCase): 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_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'), termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') ) + cable2.full_clean() cable2.save() cable3 = Cable( 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') ) + cable3.full_clean() cable3.save() cable4 = Cable( 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') ) + cable4.full_clean() cable4.save() cable5 = Cable( 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') ) + cable5.full_clean() cable5.save() cable6 = Cable( 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') ) + cable6.full_clean() cable6.save() cable7 = Cable( 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') ) + cable7.full_clean() cable7.save() # Retrieve endpoints @@ -1080,11 +1112,13 @@ class CablePathTestCase(TestCase): termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_b=CircuitTermination.objects.get(term_side='A') ) + cable1.full_clean() cable1.save() cable2 = Cable( termination_a=CircuitTermination.objects.get(term_side='Z'), termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') ) + cable2.full_clean() cable2.save() # Retrieve endpoints @@ -1122,21 +1156,25 @@ class CablePathTestCase(TestCase): 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_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), termination_b=CircuitTermination.objects.get(term_side='A') ) + cable2.full_clean() cable2.save() cable3 = Cable( termination_a=CircuitTermination.objects.get(term_side='Z'), termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1') ) + cable3.full_clean() cable3.save() cable4 = Cable( termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'), termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') ) + cable4.full_clean() cable4.save() # Retrieve endpoints From 6fc7c6a7d0915756ca7fe5cbedc5a39aff65053e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 8 May 2020 11:17:09 -0400 Subject: [PATCH 06/21] Update path validation tests for single-position rear port scenarios --- netbox/dcim/tests/test_models.py | 59 ++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index ceefbe87e..6ee556df6 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -370,9 +370,13 @@ class CableTestCase(TestCase): self.patch_pannel = Device.objects.create( 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.front_port = FrontPort.objects.create( - device=self.patch_pannel, name='F1', type=1000, rear_port=self.rear_port + self.rear_port1 = RearPort.objects.create(device=self.patch_pannel, name='RP1', type='8p8c') + self.front_port1 = FrontPort.objects.create( + 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 ) def test_cable_creation(self): @@ -426,7 +430,7 @@ class CableTestCase(TestCase): """ 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): cable.clean() @@ -439,6 +443,23 @@ class CableTestCase(TestCase): with self.assertRaises(ValidationError): cable.clean() + def test_multipos_rearport_connections(self): + """ + A RearPort with more than one position can only be connected to another RearPort with the same number of + positions. + """ + with self.assertRaises( + ValidationError, + msg='Connecting a single-position RearPort to a multi-position RearPort should fail' + ): + Cable(termination_a=self.rear_port1, termination_b=self.rear_port2).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.interface1).full_clean() + def test_cable_cannot_terminate_to_a_virtual_inteface(self): """ A cable cannot terminate to a virtual interface @@ -502,6 +523,7 @@ class CablePathTestCase(TestCase): 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 5', site=site), + Device(device_type=devicetype, device_role=devicerole, name='Panel 6', site=site), ) Device.objects.bulk_create(patch_panels) @@ -557,27 +579,26 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_b.connection_status) - def test_connection_via_single_rear_port(self): + def test_connection_rear_port_to_interface(self): """ - Test a connection which passes through a single front/rear port pair. + Test a connection which passes through a single front/rear port pair, where the rear port has a single position. 1 2 - [Device 1] ----- [Panel 1] ----- [Device 2] + [Device 1] ----- [Panel 5] ----- [Device 2] Iface1 FP1 RP1 Iface1 - - TODO: Panel 1's rear port has multiple front ports. Should this even work? """ - # Create cables (FP first, RP second) + # Create cables 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') + termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1') ) cable1.full_clean() cable1.save() cable2 = Cable( - termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), - termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') + termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 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() @@ -682,7 +703,7 @@ class CablePathTestCase(TestCase): def test_connection_via_nested_one_on_one_port(self): """ - Test a connection which passes through a single front/rear port pair between two multi-port MUXes. + 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 @@ -1147,31 +1168,31 @@ class CablePathTestCase(TestCase): def test_connection_via_patched_circuit(self): """ 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 """ # Create cables 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') + termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1') ) cable1.full_clean() cable1.save() 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') ) cable2.full_clean() cable2.save() cable3 = Cable( 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() 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') ) cable4.full_clean() From 2fe4656db49d4c08d222351d4a91cd38082254e1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 8 May 2020 11:23:36 -0400 Subject: [PATCH 07/21] Permit connection of a multi-position RearPort to a FrontPort --- netbox/dcim/models/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 1f6478119..ca87bc8d9 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -2191,17 +2191,18 @@ class Cable(ChangeLoggedModel): 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 + # A RearPort with multiple positions must be connected to a RearPort with an equal number of positions, or a + # FrontPort 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, RearPort): + if not isinstance(term_b, (FrontPort, RearPort)): 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_a.positions != term_b.positions: raise ValidationError( f"{term_a} has {term_a.positions} position(s) but {term_b} has {term_b.positions}. " f"Both terminations must have the same number of positions." From 2479b8a57f645571747fb17282c7dd3c48d9a23e Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 2 Jun 2020 13:11:35 +0200 Subject: [PATCH 08/21] Validate against is_path_endpoint instead of specific classes, and only when positions > 1 --- netbox/dcim/models/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index ca87bc8d9..6a0b6c087 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -2191,20 +2191,20 @@ class Cable(ChangeLoggedModel): 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, or a - # FrontPort + # Check that a RearPort isn't connected to something silly 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)): + if term_b.is_path_endpoint: raise ValidationError( "Rear ports with multiple positions may only be connected to other pass-through ports" ) - if isinstance(term_b, RearPort) and 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( - 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." ) From 81a322eaaf796025458b8f84e4655921974a91ab Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 2 Jun 2020 13:13:10 +0200 Subject: [PATCH 09/21] Add position_stack to returned values from trace() --- netbox/dcim/models/device_components.py | 18 ++++++++++-------- netbox/dcim/signals.py | 2 +- netbox/dcim/views.py | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index d854fdc6e..d344546ef 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -93,9 +93,11 @@ class CableTermination(models.Model): def trace(self): """ - Return two items: the traceable portion of a cable path, and the termination points where it splits (if any). - This occurs when the trace is initiated from a midpoint 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. + Return three items: the traceable portion of a cable path, the termination points where it splits (if any), and + the remaining positions on the position stack (if any). Splits occur when the trace is initiated from a midpoint + 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 three-tuple: @@ -171,12 +173,12 @@ class CableTermination(models.Model): if not endpoint.cable: path.append((endpoint, None, None)) logger.debug("No cable connected") - return path, None + return path, None, position_stack # Check for loops if endpoint.cable in [segment[1] for segment in path]: logger.debug("Loop detected!") - return path, None + return path, None, position_stack # Record the current segment in the path far_end = endpoint.get_cable_peer() @@ -189,10 +191,10 @@ class CableTermination(models.Model): try: endpoint = get_peer_port(far_end) except CableTraceSplit as e: - return path, e.termination.frontports.all() + return path, e.termination.frontports.all(), position_stack if endpoint is None: - return path, None + return path, None, position_stack def get_cable_peer(self): if self.cable is None: @@ -209,7 +211,7 @@ class CableTermination(models.Model): endpoints = [] # 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] if split_ends is not None: for termination in split_ends: diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index c94ecf61e..1eb6605d1 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -52,7 +52,7 @@ def update_connected_endpoints(instance, **kwargs): # Update any endpoints for this Cable. endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints() for endpoint in endpoints: - path, split_ends = endpoint.trace() + path, split_ends, position_stack = endpoint.trace() # Determine overall path status (connected or planned) path_status = True for segment in path: diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d141f93c6..3cf28634b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2057,7 +2057,7 @@ class CableTraceView(PermissionRequiredMixin, View): def get(self, request, model, pk): obj = get_object_or_404(model, pk=pk) - path, split_ends = obj.trace() + path, split_ends, position_stack = obj.trace() total_length = sum( [entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length] ) From 34ae57dfa361ad7b1e63a28de3b18ad444dce35c Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 2 Jun 2020 13:13:41 +0200 Subject: [PATCH 10/21] Show warning when position stack is not empty after trace --- netbox/dcim/views.py | 1 + netbox/templates/dcim/cable_trace.html | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 3cf28634b..68359fc05 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2066,6 +2066,7 @@ class CableTraceView(PermissionRequiredMixin, View): 'obj': obj, 'trace': path, 'split_ends': split_ends, + 'position_stack': position_stack, 'total_length': total_length, }) diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 1e7210e9a..64c4a22d9 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -88,6 +88,10 @@ + {% elif position_stack %} +
+

Multiple possible paths end at this point. No connection established!

+
{% else %}

Trace completed!

From 8bd9b460cbad0392832abc92580444820a21e9c1 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 2 Jun 2020 13:14:38 +0200 Subject: [PATCH 11/21] Only complete path when there are not split_ends or position_stack --- netbox/dcim/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 1eb6605d1..7cae1a39c 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -61,7 +61,7 @@ def update_connected_endpoints(instance, **kwargs): break 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): logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b)) From 886b59f400da13ada217b7c7b2bec46cae9b44d5 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 2 Jun 2020 13:14:51 +0200 Subject: [PATCH 12/21] Update tests for cables --- netbox/dcim/tests/test_models.py | 86 ++++++++++++-------------------- 1 file changed, 33 insertions(+), 53 deletions(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 6ee556df6..f3b890102 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -363,6 +363,7 @@ class CableTestCase(TestCase): ) self.interface1 = Interface.objects.create(device=self.device1, 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.save() @@ -378,6 +379,15 @@ class CableTestCase(TestCase): 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.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): """ @@ -409,7 +419,7 @@ class CableTestCase(TestCase): cable = Cable.objects.filter(pk=self.cable.pk).first() 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 """ @@ -443,6 +453,17 @@ class CableTestCase(TestCase): with self.assertRaises(ValidationError): cable.clean() + def test_singlepos_rearport_connections(self): + """ + A RearPort with one position can be connected to anything as it is just a + cable extender. + """ + # 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() + def test_multipos_rearport_connections(self): """ A RearPort with more than one position can only be connected to another RearPort with the same number of @@ -450,15 +471,18 @@ class CableTestCase(TestCase): """ with self.assertRaises( ValidationError, - msg='Connecting a single-position RearPort to a multi-position RearPort should fail' + msg='Connecting a 2-position RearPort to a 3-position RearPort should fail' ): - Cable(termination_a=self.rear_port1, termination_b=self.rear_port2).full_clean() + 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.interface1).full_clean() + Cable(termination_a=self.rear_port2, termination_b=self.interface3).full_clean() + + # Connecting a multi-position RearPort to a CircuitTermination should be ok + Cable(termination_a=self.rear_port1, termination_b=self.circuittermination1).full_clean() def test_cable_cannot_terminate_to_a_virtual_inteface(self): """ @@ -537,7 +561,7 @@ class CablePathTestCase(TestCase): FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C), )) - # Create a 1-on-1 patch panel + # 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) @@ -554,6 +578,7 @@ class CablePathTestCase(TestCase): termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') ) + cable.full_clean() cable.save() # Retrieve endpoints @@ -579,52 +604,6 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_b.connection_status) - def test_connection_rear_port_to_interface(self): - """ - Test a connection which passes through a single front/rear port pair, where the rear port has a single position. - - 1 2 - [Device 1] ----- [Panel 5] ----- [Device 2] - Iface1 FP1 RP1 Iface1 - """ - # Create cables - cable1 = Cable( - termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), - termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1') - ) - cable1.full_clean() - cable1.save() - cable2 = Cable( - termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 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() - - # 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') - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - - # Delete cable 1 - cable1.delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - def test_connection_via_one_on_one_port(self): """ Test a connection which passes through a rear port with exactly one front port. @@ -641,9 +620,10 @@ class CablePathTestCase(TestCase): cable1.full_clean() cable1.save() cable2 = Cable( - termination_b=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'), - termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1') + termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 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() From cafecb091dc11046d3942a00287d0713f0a5fba2 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 16 Jun 2020 21:46:16 +0200 Subject: [PATCH 13/21] Replace temporary comment with proper one --- netbox/dcim/models/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 6a0b6c087..87077ea19 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -2191,7 +2191,8 @@ class Cable(ChangeLoggedModel): f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}" ) - # Check that a RearPort isn't connected to something silly + # 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) From 4a11800d9e8bb6aee612ee09c6d1c02f55102c08 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 16 Jun 2020 21:47:10 +0200 Subject: [PATCH 14/21] Better comments --- netbox/dcim/models/device_components.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index d344546ef..b81e395c4 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -120,7 +120,9 @@ class CableTermination(models.Model): # 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) - # Don't use the stack for 1-on-1 ports, they don't have to come in pairs + # 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.rear_port_position) @@ -141,7 +143,7 @@ class CableTermination(models.Model): termination, termination.positions, position )) else: - # Don't use the stack for 1-on-1 ports, they don't have to come in pairs + # Don't use the stack for RearPorts with a single position. The only possible position is 1. position = 1 try: From abaf0daa6eff0e2ed5085c0f057e840b120f56c4 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 16 Jun 2020 21:47:37 +0200 Subject: [PATCH 15/21] Store the front ports on the position_stack so we can provide better feedback to the user --- netbox/dcim/models/device_components.py | 5 +++-- netbox/templates/dcim/cable_trace.html | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index b81e395c4..4fa9c37b5 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -124,7 +124,7 @@ class CableTermination(models.Model): # 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.rear_port_position) + position_stack.append(termination) return peer_port @@ -135,7 +135,8 @@ class CableTermination(models.Model): if not position_stack: raise CableTraceSplit(termination) - position = position_stack.pop() + front_port = position_stack.pop() + position = front_port.rear_port_position # Validate the position if position not in range(1, termination.positions + 1): diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 64c4a22d9..df484609a 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -90,7 +90,13 @@
{% elif position_stack %}
-

Multiple possible paths end at this point. No connection established!

+

+ {% with last_position=position_stack|last %} + Trace completed, but there is no Front Port corresponding to + {{ last_position.device }} {{ last_position }}.
+ Therefore no end-to-end connection can be established. + {% endwith %} +

{% else %}
From f075339c5f3e7d21ec88d82766a6791d48e5b91c Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 16 Jun 2020 21:48:26 +0200 Subject: [PATCH 16/21] Improve test comments and remove over-enthusiastic tests --- netbox/dcim/tests/test_models.py | 160 ++++++++++++++----------------- 1 file changed, 73 insertions(+), 87 deletions(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index f3b890102..c55d099c9 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -383,6 +383,10 @@ class CableTestCase(TestCase): 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') @@ -453,10 +457,21 @@ class CableTestCase(TestCase): with self.assertRaises(ValidationError): cable.clean() - def test_singlepos_rearport_connections(self): + def test_connection_via_single_position_rearport(self): """ - A RearPort with one position can be connected to anything as it is just a - cable extender. + 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() @@ -464,11 +479,59 @@ class CableTestCase(TestCase): # Connecting a single-position RearPort to an Interface is ok Cable(termination_a=self.rear_port1, termination_b=self.interface3).full_clean() - def test_multipos_rearport_connections(self): + # 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 more than one position can only be connected to another RearPort with the same number of - positions. + 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' @@ -481,10 +544,7 @@ class CableTestCase(TestCase): ): Cable(termination_a=self.rear_port2, termination_b=self.interface3).full_clean() - # Connecting a multi-position RearPort to a CircuitTermination should be ok - Cable(termination_a=self.rear_port1, termination_b=self.circuittermination1).full_clean() - - def test_cable_cannot_terminate_to_a_virtual_inteface(self): + def test_cable_cannot_terminate_to_a_virtual_interface(self): """ A cable cannot terminate to a virtual interface """ @@ -493,7 +553,7 @@ class CableTestCase(TestCase): with self.assertRaises(ValidationError): 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 """ @@ -604,7 +664,7 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_b.connection_status) - def test_connection_via_one_on_one_port(self): + def test_connection_via_single_rear_port(self): """ Test a connection which passes through a rear port with exactly one front port. @@ -650,38 +710,7 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_b.connection_status) - # Recreate cable 1 to test creating the cables in reverse order (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 5', name='Front Port 1') - ) - cable1.full_clean() - cable1.save() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - - # Validate connections - self.assertEqual(endpoint_a.connected_endpoint, endpoint_b) - self.assertEqual(endpoint_b.connected_endpoint, endpoint_a) - self.assertTrue(endpoint_a.connection_status) - self.assertTrue(endpoint_b.connection_status) - - # Delete cable 2 - cable2.delete() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - - # Check that connections have been nullified - self.assertIsNone(endpoint_a.connected_endpoint) - self.assertIsNone(endpoint_b.connected_endpoint) - self.assertIsNone(endpoint_a.connection_status) - self.assertIsNone(endpoint_b.connection_status) - - def test_connection_via_nested_one_on_one_port(self): + 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. @@ -772,49 +801,6 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_c.connection_status) self.assertIsNone(endpoint_d.connection_status) - # Recreate cable 3 to test reverse order (Panel 5 FP first, RP second) - 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() - - # Refresh endpoints - endpoint_a.refresh_from_db() - endpoint_b.refresh_from_db() - endpoint_c.refresh_from_db() - endpoint_d.refresh_from_db() - - # 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 4 - cable4.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): """ Test two connections via patched rear ports: From 3876efe494f5840248d29c0ce6b29ddee243ee44 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Tue, 16 Jun 2020 21:56:46 +0200 Subject: [PATCH 17/21] Fix `is_path_endpoint` flag on CableTermination --- netbox/circuits/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 57d41a994..2bad69789 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -300,6 +300,9 @@ class CircuitTermination(CableTermination): blank=True ) + # Paths do not end on cable terminations, they continue at the other end of the circuit + is_path_endpoint = False + class Meta: ordering = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side'] From 715ddc6b02de1725d63d586d085b32d12ecf40e9 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Wed, 17 Jun 2020 17:11:28 +0200 Subject: [PATCH 18/21] Define `is_path_endpoint` and `is_connected_endpoint` separately, as a CableTermination is a possible connected endpoint but not always the end of the path. --- netbox/circuits/models.py | 3 +++ netbox/dcim/models/device_components.py | 14 ++++++++++++++ netbox/dcim/signals.py | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 2bad69789..a51a7039a 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -303,6 +303,9 @@ class CircuitTermination(CableTermination): # Paths do not end on cable terminations, they continue at the other end of the circuit is_path_endpoint = False + # But they are a possible connected endpoint + is_connected_endpoint = True + class Meta: ordering = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side'] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 4fa9c37b5..4fb3a9f05 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -86,8 +86,12 @@ class CableTermination(models.Model): object_id_field='termination_b_id' ) + # Whether this class is always an endpoint for cable traces is_path_endpoint = True + # Whether this class can be a connected endpoint + is_connected_endpoint = True + class Meta: abstract = True @@ -895,8 +899,13 @@ class FrontPort(CableTermination, ComponentModel): tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] + + # Whether this class is always an endpoint for cable traces is_path_endpoint = False + # Whether this class can be a connected endpoint + is_connected_endpoint = False + class Meta: ordering = ('device', '_name') unique_together = ( @@ -963,8 +972,13 @@ class RearPort(CableTermination, ComponentModel): tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'type', 'positions', 'description'] + + # Whether this class is always an endpoint for cable traces is_path_endpoint = False + # Whether this class can be a connected endpoint + is_connected_endpoint = False + class Meta: ordering = ('device', '_name') unique_together = ('device', 'name') diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 7cae1a39c..f5bf43c70 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -63,7 +63,7 @@ def update_connected_endpoints(instance, **kwargs): endpoint_a = path[0][0] 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): + if getattr(endpoint_a, 'is_connected_endpoint', False) and getattr(endpoint_b, 'is_connected_endpoint', False): logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b)) endpoint_a.connected_endpoint = endpoint_b endpoint_a.connection_status = path_status From 3fdc8e7d3d28a50acbd42ed0ea0965414b1bed56 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Fri, 26 Jun 2020 17:25:07 +0200 Subject: [PATCH 19/21] =?UTF-8?q?Replace=20`is=5Fpath=5Fendpoint`=20with?= =?UTF-8?q?=20simple=20`isinstance`=20check=20It=20was=20only=20used=20in?= =?UTF-8?q?=20a=20single=20location=20anyway=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netbox/circuits/models.py | 3 --- netbox/dcim/models/__init__.py | 3 ++- netbox/dcim/models/device_components.py | 9 --------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index a51a7039a..ca792ae2c 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -300,9 +300,6 @@ class CircuitTermination(CableTermination): blank=True ) - # Paths do not end on cable terminations, they continue at the other end of the circuit - is_path_endpoint = False - # But they are a possible connected endpoint is_connected_endpoint = True diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 87077ea19..0e10e97c8 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -2129,6 +2129,7 @@ class Cable(ChangeLoggedModel): return reverse('dcim:cable', args=[self.pk]) def clean(self): + from circuits.models import CircuitTermination # Validate that termination A exists if not hasattr(self, 'termination_a_type'): @@ -2198,7 +2199,7 @@ class Cable(ChangeLoggedModel): (self.termination_b, self.termination_a) ]: if isinstance(term_a, RearPort) and term_a.positions > 1: - if term_b.is_path_endpoint: + if not isinstance(term_b, (FrontPort, RearPort, CircuitTermination)): raise ValidationError( "Rear ports with multiple positions may only be connected to other pass-through ports" ) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 4fb3a9f05..1d6623117 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -86,9 +86,6 @@ class CableTermination(models.Model): object_id_field='termination_b_id' ) - # Whether this class is always an endpoint for cable traces - is_path_endpoint = True - # Whether this class can be a connected endpoint is_connected_endpoint = True @@ -900,9 +897,6 @@ class FrontPort(CableTermination, ComponentModel): csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] - # Whether this class is always an endpoint for cable traces - is_path_endpoint = False - # Whether this class can be a connected endpoint is_connected_endpoint = False @@ -973,9 +967,6 @@ class RearPort(CableTermination, ComponentModel): csv_headers = ['device', 'name', 'type', 'positions', 'description'] - # Whether this class is always an endpoint for cable traces - is_path_endpoint = False - # Whether this class can be a connected endpoint is_connected_endpoint = False From 25926e32f0b70556a3c4990c2cb0cc0c64ba893f Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Fri, 26 Jun 2020 17:30:59 +0200 Subject: [PATCH 20/21] =?UTF-8?q?Replace=20`is=5Fconnected=5Fendpoint`=20w?= =?UTF-8?q?ith=20simple=20`isinstance`=20check=20It=20was=20only=20used=20?= =?UTF-8?q?in=20a=20single=20location=20anyway=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netbox/circuits/models.py | 3 --- netbox/dcim/models/device_components.py | 9 --------- netbox/dcim/signals.py | 5 +++-- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index ca792ae2c..57d41a994 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -300,9 +300,6 @@ class CircuitTermination(CableTermination): blank=True ) - # But they are a possible connected endpoint - is_connected_endpoint = True - class Meta: ordering = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side'] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 1d6623117..41ebc1cad 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -86,9 +86,6 @@ class CableTermination(models.Model): object_id_field='termination_b_id' ) - # Whether this class can be a connected endpoint - is_connected_endpoint = True - class Meta: abstract = True @@ -897,9 +894,6 @@ class FrontPort(CableTermination, ComponentModel): csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] - # Whether this class can be a connected endpoint - is_connected_endpoint = False - class Meta: ordering = ('device', '_name') unique_together = ( @@ -967,9 +961,6 @@ class RearPort(CableTermination, ComponentModel): csv_headers = ['device', 'name', 'type', 'positions', 'description'] - # Whether this class can be a connected endpoint - is_connected_endpoint = False - class Meta: ordering = ('device', '_name') unique_together = ('device', 'name') diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index f5bf43c70..2d0f3aa45 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -4,7 +4,7 @@ from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from .choices import CableStatusChoices -from .models import Cable, Device, VirtualChassis +from .models import Cable, Device, FrontPort, RearPort, VirtualChassis @receiver(post_save, sender=VirtualChassis) @@ -63,7 +63,8 @@ def update_connected_endpoints(instance, **kwargs): endpoint_a = path[0][0] endpoint_b = path[-1][2] if not split_ends and not position_stack else None - if getattr(endpoint_a, 'is_connected_endpoint', False) and getattr(endpoint_b, 'is_connected_endpoint', False): + # Patch panel ports are not connected endpoints, everything else is + if not isinstance(endpoint_a, (FrontPort, RearPort)) and not isinstance(endpoint_b, (FrontPort, RearPort)): logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b)) endpoint_a.connected_endpoint = endpoint_b endpoint_a.connection_status = path_status From 045594759746da5135666819d916f8172962a650 Mon Sep 17 00:00:00 2001 From: Sander Steffann Date: Fri, 26 Jun 2020 18:24:04 +0200 Subject: [PATCH 21/21] Make sure that the endpoint is actually a CableTermination --- netbox/dcim/signals.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 2d0f3aa45..8c8ac67bc 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -4,7 +4,7 @@ from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from .choices import CableStatusChoices -from .models import Cable, Device, FrontPort, RearPort, VirtualChassis +from .models import Cable, CableTermination, Device, FrontPort, RearPort, VirtualChassis @receiver(post_save, sender=VirtualChassis) @@ -63,8 +63,9 @@ def update_connected_endpoints(instance, **kwargs): endpoint_a = path[0][0] endpoint_b = path[-1][2] if not split_ends and not position_stack else None - # Patch panel ports are not connected endpoints, everything else is - if not isinstance(endpoint_a, (FrontPort, RearPort)) and not isinstance(endpoint_b, (FrontPort, RearPort)): + # 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)) endpoint_a.connected_endpoint = endpoint_b endpoint_a.connection_status = path_status