diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index cbf8358bf..9ed878a7f 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -12,6 +12,7 @@ * [#4527](https://github.com/netbox-community/netbox/issues/4527) - Fix assignment of certain tags to config contexts * [#4545](https://github.com/netbox-community/netbox/issues/4545) - Removed all squashed schema migrations to allow direct upgrades from very old releases +* [#4548](https://github.com/netbox-community/netbox/issues/4548) - Fix tracing cables through a single RearPort * [#4549](https://github.com/netbox-community/netbox/issues/4549) - Fix encoding unicode webhook body data * [#4556](https://github.com/netbox-community/netbox/issues/4556) - Update form for adding devices to clusters diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 8c79d89d8..3b61f80ba 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -123,11 +123,12 @@ class CableTermination(models.Model): # Map a rear port/position to its corresponding front port elif isinstance(termination, RearPort): - # Can't map to a FrontPort without a position - if not position_stack: + # 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): diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 7be9ef6e4..6db938732 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -514,10 +514,10 @@ class CablePathTestCase(TestCase): def test_direct_connection(self): """ + Test a direct connection between two interfaces. [Device 1] ----- [Device 2] Iface1 Iface1 - """ # Create cable cable = Cable( @@ -549,6 +549,49 @@ class CablePathTestCase(TestCase): self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_b.connection_status) + def test_connection_via_single_rear_port(self): + """ + Test a connection which passes through a single front/rear port pair. + + 1 2 + [Device 1] ----- [Panel 1] ----- [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 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_connections_via_patch(self): """ Test two connections via patched rear ports: