From ad8f3315bc080410241d96c6d7e4e56dc4694518 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sun, 14 Dec 2025 12:29:13 -0500 Subject: [PATCH] Flesh out cable profiles & tests --- netbox/dcim/cable_profiles.py | 160 ++++- netbox/dcim/choices.py | 88 ++- netbox/dcim/models/cables.py | 27 +- netbox/dcim/tests/test_cablepaths2.py | 801 ++++++++++++++++++++------ 4 files changed, 835 insertions(+), 241 deletions(-) diff --git a/netbox/dcim/cable_profiles.py b/netbox/dcim/cable_profiles.py index bad85bfaa..66cfa13a4 100644 --- a/netbox/dcim/cable_profiles.py +++ b/netbox/dcim/cable_profiles.py @@ -5,6 +5,12 @@ from dcim.models import CableTermination class BaseCableProfile: + """Base class for representing a cable profile.""" + + # Mappings of connectors to their available positions at either end of the cable. For example, a 12-strand MPO + # fiber cable would have one connector at either end with six positions (six bidirectional fiber pairs). + a_connectors = {} + b_connectors = {} def clean(self, cable): # Enforce maximum terminations limits @@ -61,35 +67,56 @@ class BaseCableProfile: return None, None -class Straight1C1PCableProfile(BaseCableProfile): +class Single1C1PCableProfile(BaseCableProfile): a_connectors = { 1: [1], } b_connectors = a_connectors -class Straight1C2PCableProfile(BaseCableProfile): +class Single1C2PCableProfile(BaseCableProfile): a_connectors = { 1: [1, 2], } b_connectors = a_connectors -class Straight1C4PCableProfile(BaseCableProfile): +class Single1C4PCableProfile(BaseCableProfile): a_connectors = { 1: [1, 2, 3, 4], } b_connectors = a_connectors -class Straight1C8PCableProfile(BaseCableProfile): +class Single1C6PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4, 5, 6], + } + b_connectors = a_connectors + + +class Single1C8PCableProfile(BaseCableProfile): a_connectors = { 1: [1, 2, 3, 4, 5, 6, 7, 8], } b_connectors = a_connectors -class Straight2C1PCableProfile(BaseCableProfile): +class Single1C12PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + } + b_connectors = a_connectors + + +class Single1C16PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16], + } + b_connectors = a_connectors + + +class Trunk2C1PCableProfile(BaseCableProfile): a_connectors = { 1: [1], 2: [1], @@ -97,7 +124,7 @@ class Straight2C1PCableProfile(BaseCableProfile): b_connectors = a_connectors -class Straight2C2PCableProfile(BaseCableProfile): +class Trunk2C2PCableProfile(BaseCableProfile): a_connectors = { 1: [1, 2], 2: [1, 2], @@ -105,7 +132,104 @@ class Straight2C2PCableProfile(BaseCableProfile): b_connectors = a_connectors +class Trunk2C4PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4], + 2: [1, 2, 3, 4], + } + b_connectors = a_connectors + + +class Trunk2C6PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4, 5, 6], + 2: [1, 2, 3, 4, 5, 6], + } + b_connectors = a_connectors + + +class Trunk2C8PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4, 5, 6, 7, 8], + 2: [1, 2, 3, 4, 5, 6, 7, 8], + } + b_connectors = a_connectors + + +class Trunk2C12PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + 2: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + } + b_connectors = a_connectors + + +class Trunk4C1PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1], + 2: [1], + 3: [1], + 4: [1], + } + b_connectors = a_connectors + + +class Trunk4C2PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2], + 2: [1, 2], + 3: [1, 2], + 4: [1, 2], + } + b_connectors = a_connectors + + +class Trunk4C4PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4], + 2: [1, 2, 3, 4], + 3: [1, 2, 3, 4], + 4: [1, 2, 3, 4], + } + b_connectors = a_connectors + + +class Trunk4C6PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4, 5, 6], + 2: [1, 2, 3, 4, 5, 6], + 3: [1, 2, 3, 4, 5, 6], + 4: [1, 2, 3, 4, 5, 6], + } + b_connectors = a_connectors + + +class Trunk4C8PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4, 5, 6, 7, 8], + 2: [1, 2, 3, 4, 5, 6, 7, 8], + 3: [1, 2, 3, 4, 5, 6, 7, 8], + 4: [1, 2, 3, 4, 5, 6, 7, 8], + } + b_connectors = a_connectors + + +class Trunk8C4PCableProfile(BaseCableProfile): + a_connectors = { + 1: [1, 2, 3, 4], + 2: [1, 2, 3, 4], + 3: [1, 2, 3, 4], + 4: [1, 2, 3, 4], + 5: [1, 2, 3, 4], + 6: [1, 2, 3, 4], + 7: [1, 2, 3, 4], + 8: [1, 2, 3, 4], + } + b_connectors = a_connectors + + class Breakout1x4CableProfile(BaseCableProfile): + """Breakout 1:4 to 4:1""" a_connectors = { 1: [1, 2, 3, 4], } @@ -129,30 +253,6 @@ class Breakout1x4CableProfile(BaseCableProfile): return self._mapping.get((connector, position)) -class MPOTrunk4x4CableProfile(BaseCableProfile): - a_connectors = { - 1: [1, 2, 3, 4], - 2: [1, 2, 3, 4], - 3: [1, 2, 3, 4], - 4: [1, 2, 3, 4], - } - b_connectors = a_connectors - - -class MPOTrunk8x8CableProfile(BaseCableProfile): - a_connectors = { - 1: [1, 2, 3, 4], - 2: [1, 2, 3, 4], - 3: [1, 2, 3, 4], - 4: [1, 2, 3, 4], - 5: [1, 2, 3, 4], - 6: [1, 2, 3, 4], - 7: [1, 2, 3, 4], - 8: [1, 2, 3, 4], - } - b_connectors = a_connectors - - class Shuffle2x2MPO8CableProfile(BaseCableProfile): a_connectors = { 1: [1, 2, 3, 4], diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 98225d276..8080005ea 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1722,32 +1722,78 @@ class PortTypeChoices(ChoiceSet): # class CableProfileChoices(ChoiceSet): - STRAIGHT_1C1P = 'straight-1c1p' - STRAIGHT_1C2P = 'straight-1c2p' - STRAIGHT_1C4P = 'straight-1c4p' - STRAIGHT_1C8P = 'straight-1c8p' - STRAIGHT_2C1P = 'straight-2c1p' - STRAIGHT_2C2P = 'straight-2c2p' + # Singles + SINGLE_1C1P = 'single-1c1p' + SINGLE_1C2P = 'single-1c2p' + SINGLE_1C4P = 'single-1c4p' + SINGLE_1C6P = 'single-1c6p' + SINGLE_1C8P = 'single-1c8p' + SINGLE_1C12P = 'single-1c12p' + SINGLE_1C16P = 'single-1c16p' + # Trunks + TRUNK_2C1P = 'trunk-2c1p' + TRUNK_2C2P = 'trunk-2c2p' + TRUNK_2C4P = 'trunk-2c4p' + TRUNK_2C6P = 'trunk-2c6p' + TRUNK_2C8P = 'trunk-2c8p' + TRUNK_2C12P = 'trunk-2c12p' + TRUNK_4C1P = 'trunk-4c1p' + TRUNK_4C2P = 'trunk-4c2p' + TRUNK_4C4P = 'trunk-4c4p' + TRUNK_4C6P = 'trunk-4c6p' + TRUNK_4C8P = 'trunk-4c8p' + TRUNK_8C4P = 'trunk-8c4p' + # Breakouts BREAKOUT_1X4 = 'breakout-1x4' - MPO_TRUNK_4X4 = 'mpo-trunk-4x4' - MPO_TRUNK_8X8 = 'mpo-trunk-8x8' + SHUFFLE_BREAKOUT_2X8 = 'shuffle-breakout-2x8' + # Shuffles SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8' SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8' - SHUFFLE_BREAKOUT_2X8 = 'shuffle-breakout-2x8' CHOICES = ( - (STRAIGHT_1C1P, _('Straight (1C1P)')), - (STRAIGHT_1C2P, _('Straight (1C2P)')), - (STRAIGHT_1C4P, _('Straight (1C4P)')), - (STRAIGHT_1C8P, _('Straight (1C8P)')), - (STRAIGHT_2C1P, _('Straight (2C1P)')), - (STRAIGHT_2C2P, _('Straight (2C2P)')), - (BREAKOUT_1X4, _('Breakout (1:4)')), - (MPO_TRUNK_4X4, _('MPO Trunk (4x4)')), - (MPO_TRUNK_8X8, _('MPO Trunk (8x8)')), - (SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')), - (SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')), - (SHUFFLE_BREAKOUT_2X8, _('Shuffle breakout (2x8)')), + ( + _('Single'), + ( + (SINGLE_1C1P, _('Single (1C1P)')), + (SINGLE_1C2P, _('Single (1C2P)')), + (SINGLE_1C4P, _('Single (1C4P)')), + (SINGLE_1C6P, _('Single (1C6P)')), + (SINGLE_1C8P, _('Single (1C8P)')), + (SINGLE_1C12P, _('Single (1C12P)')), + (SINGLE_1C16P, _('Single (1C16P)')), + ), + ), + ( + _('Trunk'), + ( + (TRUNK_2C1P, _('Trunk (2C1P)')), + (TRUNK_2C2P, _('Trunk (2C2P)')), + (TRUNK_2C4P, _('Trunk (2C4P)')), + (TRUNK_2C6P, _('Trunk (2C6P)')), + (TRUNK_2C8P, _('Trunk (2C8P)')), + (TRUNK_2C12P, _('Trunk (2C12P)')), + (TRUNK_4C1P, _('Trunk (4C1P)')), + (TRUNK_4C2P, _('Trunk (4C2P)')), + (TRUNK_4C4P, _('Trunk (4C4P)')), + (TRUNK_4C6P, _('Trunk (4C6P)')), + (TRUNK_4C8P, _('Trunk (4C8P)')), + (TRUNK_8C4P, _('Trunk (8C4P)')), + ), + ), + ( + _('Breakouts'), + ( + (BREAKOUT_1X4, _('Breakout (1:4)')), + ), + ), + ( + _('Shuffles'), + ( + (SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')), + (SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')), + (SHUFFLE_BREAKOUT_2X8, _('Shuffle breakout (2x8)')), + ), + ), ) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index cb1ddd583..b15185ede 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -137,15 +137,26 @@ class Cable(PrimaryModel): def profile_class(self): from dcim import cable_profiles return { - CableProfileChoices.STRAIGHT_1C1P: cable_profiles.Straight1C1PCableProfile, - CableProfileChoices.STRAIGHT_1C2P: cable_profiles.Straight1C2PCableProfile, - CableProfileChoices.STRAIGHT_1C4P: cable_profiles.Straight1C4PCableProfile, - CableProfileChoices.STRAIGHT_1C8P: cable_profiles.Straight1C8PCableProfile, - CableProfileChoices.STRAIGHT_2C1P: cable_profiles.Straight2C1PCableProfile, - CableProfileChoices.STRAIGHT_2C2P: cable_profiles.Straight2C2PCableProfile, + CableProfileChoices.SINGLE_1C1P: cable_profiles.Single1C1PCableProfile, + CableProfileChoices.SINGLE_1C2P: cable_profiles.Single1C2PCableProfile, + CableProfileChoices.SINGLE_1C4P: cable_profiles.Single1C4PCableProfile, + CableProfileChoices.SINGLE_1C6P: cable_profiles.Single1C6PCableProfile, + CableProfileChoices.SINGLE_1C8P: cable_profiles.Single1C8PCableProfile, + CableProfileChoices.SINGLE_1C12P: cable_profiles.Single1C12PCableProfile, + CableProfileChoices.SINGLE_1C16P: cable_profiles.Single1C16PCableProfile, + CableProfileChoices.TRUNK_2C1P: cable_profiles.Trunk2C1PCableProfile, + CableProfileChoices.TRUNK_2C2P: cable_profiles.Trunk2C2PCableProfile, + CableProfileChoices.TRUNK_2C4P: cable_profiles.Trunk2C4PCableProfile, + CableProfileChoices.TRUNK_2C6P: cable_profiles.Trunk2C6PCableProfile, + CableProfileChoices.TRUNK_2C8P: cable_profiles.Trunk2C8PCableProfile, + CableProfileChoices.TRUNK_2C12P: cable_profiles.Trunk2C12PCableProfile, + CableProfileChoices.TRUNK_4C1P: cable_profiles.Trunk4C1PCableProfile, + CableProfileChoices.TRUNK_4C2P: cable_profiles.Trunk4C2PCableProfile, + CableProfileChoices.TRUNK_4C4P: cable_profiles.Trunk4C4PCableProfile, + CableProfileChoices.TRUNK_4C6P: cable_profiles.Trunk4C6PCableProfile, + CableProfileChoices.TRUNK_4C8P: cable_profiles.Trunk4C8PCableProfile, + CableProfileChoices.TRUNK_8C4P: cable_profiles.Trunk8C4PCableProfile, CableProfileChoices.BREAKOUT_1X4: cable_profiles.Breakout1x4CableProfile, - CableProfileChoices.MPO_TRUNK_4X4: cable_profiles.MPOTrunk4x4CableProfile, - CableProfileChoices.MPO_TRUNK_8X8: cable_profiles.MPOTrunk8x8CableProfile, CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile, CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile, CableProfileChoices.SHUFFLE_BREAKOUT_2X8: cable_profiles.ShuffleBreakout2x8CableProfile, diff --git a/netbox/dcim/tests/test_cablepaths2.py b/netbox/dcim/tests/test_cablepaths2.py index 1aae6ddaf..ac9dd6d9d 100644 --- a/netbox/dcim/tests/test_cablepaths2.py +++ b/netbox/dcim/tests/test_cablepaths2.py @@ -16,11 +16,11 @@ class CablePathTests(CablePathTestCase): 2XX: Topology tests replicated from the legacy test case and adapted to use profiles """ - def test_101_cable_profile_straight_1c1p(self): + def test_101_cable_profile_single_1c1p(self): """ [IF1] --C1-- [IF2] - Cable profile: Straight 1C1P + Cable profile: Single connector, single position """ interfaces = [ Interface.objects.create(device=self.device, name='Interface 1'), @@ -29,7 +29,7 @@ class CablePathTests(CablePathTestCase): # Create cable 1 cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_1C1P, + profile=CableProfileChoices.SINGLE_1C1P, a_terminations=[interfaces[0]], b_terminations=[interfaces[1]], ) @@ -65,12 +65,148 @@ class CablePathTests(CablePathTestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_102_cable_profile_straight_2c1p(self): + def test_102_cable_profile_single_1c2p(self): + """ + [IF1] --C1-- [FP1][RP1] --C3-- [RP2][FP3] --C4-- [IF3] + [IF2] --C2-- [FP2] [FP4] --C5-- [IF4] + + Cable profile: Single connector, multiple positions + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=2), + RearPort.objects.create(device=self.device, name='Rear Port 2', positions=2), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), + ] + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=front_ports[0], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[1], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[2], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[3], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=2, + ), + ]) + + # Create cables + cable1 = Cable( + a_terminations=[interfaces[0]], + b_terminations=[front_ports[0]], + ) + cable1.clean() + cable1.save() + cable2 = Cable( + a_terminations=[interfaces[1]], + b_terminations=[front_ports[1]], + ) + cable2.clean() + cable2.save() + cable3 = Cable( + profile=CableProfileChoices.SINGLE_1C2P, + a_terminations=[rear_ports[0]], + b_terminations=[rear_ports[1]], + ) + cable3.clean() + cable3.save() + cable4 = Cable( + a_terminations=[interfaces[2]], + b_terminations=[front_ports[2]], + ) + cable4.clean() + cable4.save() + cable5 = Cable( + a_terminations=[interfaces[3]], + b_terminations=[front_ports[3]], + ) + cable5.clean() + cable5.save() + + path1 = self.assertPathExists( + ( + interfaces[0], cable1, front_ports[0], rear_ports[0], cable3, rear_ports[1], front_ports[2], cable4, + interfaces[2], + ), + is_complete=True, + is_active=True + ) + path2 = self.assertPathExists( + ( + interfaces[1], cable2, front_ports[1], rear_ports[0], cable3, rear_ports[1], front_ports[3], cable5, + interfaces[3], + ), + is_complete=True, + is_active=True + ) + path3 = self.assertPathExists( + ( + interfaces[2], cable4, front_ports[2], rear_ports[1], cable3, rear_ports[0], front_ports[0], cable1, + interfaces[0], + ), + is_complete=True, + is_active=True + ) + path4 = self.assertPathExists( + ( + interfaces[3], cable5, front_ports[3], rear_ports[1], cable3, rear_ports[0], front_ports[1], cable2, + interfaces[1], + ), + is_complete=True, + is_active=True + ) + self.assertEqual(CablePath.objects.count(), 4) + for iface in interfaces: + iface.refresh_from_db() + self.assertPathIsSet(interfaces[0], path1) + self.assertPathIsSet(interfaces[1], path2) + self.assertPathIsSet(interfaces[2], path3) + self.assertPathIsSet(interfaces[3], path4) + for rear_port in rear_ports: + rear_port.refresh_from_db() + self.assertEqual(rear_ports[0].cable_connector, 1) + self.assertEqual(rear_ports[0].cable_positions, [1, 2]) + self.assertEqual(rear_ports[1].cable_connector, 1) + self.assertEqual(rear_ports[1].cable_positions, [1, 2]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_103_cable_profile_trunk_2c1p(self): """ [IF1] --C1-- [IF3] [IF2] [IF4] - Cable profile: Straight 2C1P + Cable profile: Multiple connectors, single position """ interfaces = [ Interface.objects.create(device=self.device, name='Interface 1'), @@ -81,7 +217,7 @@ class CablePathTests(CablePathTestCase): # Create cable 1 cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_2C1P, + profile=CableProfileChoices.TRUNK_2C1P, a_terminations=[interfaces[0], interfaces[1]], b_terminations=[interfaces[2], interfaces[3]], ) @@ -134,17 +270,378 @@ class CablePathTests(CablePathTestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - @skip("Under development") - def test_103_cable_profile_shuffle_2x2_mpo8(self): + def test_104_cable_profile_trunk_2c2p(self): """ - [IF1:1] --C1-- [IF3:1] - [IF1:2] [IF3:2] - [IF1:3] [IF3:3] - [IF1:4] [IF3:4] - [IF2:1] [IF4:1] - [IF2:2] [IF4:2] - [IF2:3] [IF4:3] - [IF2:4] [IF4:4] + [IF1] --C1-- [FP1][RP1] --C9-- [RP3][FP5] --C5-- [IF5] + [IF2] --C2-- [FP2] [FP6] --C6-- [IF6] + [IF3] --C3-- [FP3][RP2] [RP4][FP7] --C7-- [IF7] + [IF4] --C4-- [FP4] [FP8] --C8-- [IF8] + + Cable profile: Multiple connectors, multiple positions + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + Interface.objects.create(device=self.device, name='Interface 5'), + Interface.objects.create(device=self.device, name='Interface 6'), + Interface.objects.create(device=self.device, name='Interface 7'), + Interface.objects.create(device=self.device, name='Interface 8'), + ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=2), + RearPort.objects.create(device=self.device, name='Rear Port 2', positions=2), + RearPort.objects.create(device=self.device, name='Rear Port 3', positions=2), + RearPort.objects.create(device=self.device, name='Rear Port 4', positions=2), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), + FrontPort.objects.create(device=self.device, name='Front Port 5'), + FrontPort.objects.create(device=self.device, name='Front Port 6'), + FrontPort.objects.create(device=self.device, name='Front Port 7'), + FrontPort.objects.create(device=self.device, name='Front Port 8'), + ] + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=front_ports[0], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[1], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[2], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[3], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[4], + front_port_position=1, + rear_port=rear_ports[2], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[5], + front_port_position=1, + rear_port=rear_ports[2], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[6], + front_port_position=1, + rear_port=rear_ports[3], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[7], + front_port_position=1, + rear_port=rear_ports[3], + rear_port_position=2, + ), + ]) + + # Create cables + cable1 = Cable(a_terminations=[interfaces[0]], b_terminations=[front_ports[0]]) + cable1.clean() + cable1.save() + cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[front_ports[1]]) + cable2.clean() + cable2.save() + cable3 = Cable(a_terminations=[interfaces[2]], b_terminations=[front_ports[2]]) + cable3.clean() + cable3.save() + cable4 = Cable(a_terminations=[interfaces[3]], b_terminations=[front_ports[3]]) + cable4.clean() + cable4.save() + cable5 = Cable(a_terminations=[interfaces[4]], b_terminations=[front_ports[4]]) + cable5.clean() + cable5.save() + cable6 = Cable(a_terminations=[interfaces[5]], b_terminations=[front_ports[5]]) + cable6.clean() + cable6.save() + cable7 = Cable(a_terminations=[interfaces[6]], b_terminations=[front_ports[6]]) + cable7.clean() + cable7.save() + cable8 = Cable(a_terminations=[interfaces[7]], b_terminations=[front_ports[7]]) + cable8.clean() + cable8.save() + cable9 = Cable( + profile=CableProfileChoices.TRUNK_2C2P, + a_terminations=[rear_ports[0], rear_ports[1]], + b_terminations=[rear_ports[2], rear_ports[3]] + ) + cable9.clean() + cable9.save() + + path1 = self.assertPathExists( + ( + interfaces[0], cable1, front_ports[0], rear_ports[0], cable9, rear_ports[2], front_ports[4], cable5, + interfaces[4], + ), + is_complete=True, + is_active=True + ) + path2 = self.assertPathExists( + ( + interfaces[1], cable2, front_ports[1], rear_ports[0], cable9, rear_ports[2], front_ports[5], cable6, + interfaces[5], + ), + is_complete=True, + is_active=True + ) + path3 = self.assertPathExists( + ( + interfaces[2], cable3, front_ports[2], rear_ports[1], cable9, rear_ports[3], front_ports[6], cable7, + interfaces[6], + ), + is_complete=True, + is_active=True + ) + path4 = self.assertPathExists( + ( + interfaces[3], cable4, front_ports[3], rear_ports[1], cable9, rear_ports[3], front_ports[7], cable8, + interfaces[7], + ), + is_complete=True, + is_active=True + ) + path5 = self.assertPathExists( + ( + interfaces[4], cable5, front_ports[4], rear_ports[2], cable9, rear_ports[0], front_ports[0], cable1, + interfaces[0], + ), + is_complete=True, + is_active=True + ) + path6 = self.assertPathExists( + ( + interfaces[5], cable6, front_ports[5], rear_ports[2], cable9, rear_ports[0], front_ports[1], cable2, + interfaces[1], + ), + is_complete=True, + is_active=True + ) + path7 = self.assertPathExists( + ( + interfaces[6], cable7, front_ports[6], rear_ports[3], cable9, rear_ports[1], front_ports[2], cable3, + interfaces[2], + ), + is_complete=True, + is_active=True + ) + path8 = self.assertPathExists( + ( + interfaces[7], cable8, front_ports[7], rear_ports[3], cable9, rear_ports[1], front_ports[3], cable4, + interfaces[3], + ), + is_complete=True, + is_active=True + ) + self.assertEqual(CablePath.objects.count(), 8) + for iface in interfaces: + iface.refresh_from_db() + self.assertPathIsSet(interfaces[0], path1) + self.assertPathIsSet(interfaces[1], path2) + self.assertPathIsSet(interfaces[2], path3) + self.assertPathIsSet(interfaces[3], path4) + self.assertPathIsSet(interfaces[4], path5) + self.assertPathIsSet(interfaces[5], path6) + self.assertPathIsSet(interfaces[6], path7) + self.assertPathIsSet(interfaces[7], path8) + for rear_port in rear_ports: + rear_port.refresh_from_db() + self.assertEqual(rear_ports[0].cable_connector, 1) + self.assertEqual(rear_ports[0].cable_positions, [1, 2]) + self.assertEqual(rear_ports[1].cable_connector, 2) + self.assertEqual(rear_ports[1].cable_positions, [1, 2]) + self.assertEqual(rear_ports[2].cable_connector, 1) + self.assertEqual(rear_ports[2].cable_positions, [1, 2]) + self.assertEqual(rear_ports[3].cable_connector, 2) + self.assertEqual(rear_ports[3].cable_positions, [1, 2]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_105_cable_profile_breakout(self): + """ + [IF1] --C1-- [FP1][RP1] --C2-- [IF5] + [IF2] --C3-- [FP2] [IF6] + [IF3] --C4-- [FP3] [IF7] + [IF4] --C5-- [FP4] [IF8] + + Cable profile: 1:4 breakout + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + Interface.objects.create(device=self.device, name='Interface 5'), + Interface.objects.create(device=self.device, name='Interface 6'), + Interface.objects.create(device=self.device, name='Interface 7'), + Interface.objects.create(device=self.device, name='Interface 8'), + ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), + ] + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=front_ports[0], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[1], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[2], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=3, + ), + PortMapping( + device=self.device, + front_port=front_ports[3], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=4, + ), + ]) + + # Create cables + cable1 = Cable(a_terminations=[interfaces[0]], b_terminations=[front_ports[0]]) + cable1.clean() + cable1.save() + cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[front_ports[1]]) + cable2.clean() + cable2.save() + cable3 = Cable(a_terminations=[interfaces[2]], b_terminations=[front_ports[2]]) + cable3.clean() + cable3.save() + cable4 = Cable(a_terminations=[interfaces[3]], b_terminations=[front_ports[3]]) + cable4.clean() + cable4.save() + cable5 = Cable( + profile=CableProfileChoices.BREAKOUT_1X4, + a_terminations=[rear_ports[0]], + b_terminations=interfaces[4:8], + ) + cable5.clean() + cable5.save() + + path1 = self.assertPathExists( + (interfaces[0], cable1, front_ports[0], rear_ports[0], cable5, interfaces[4]), + is_complete=True, + is_active=True + ) + path2 = self.assertPathExists( + (interfaces[1], cable2, front_ports[1], rear_ports[0], cable5, interfaces[5]), + is_complete=True, + is_active=True + ) + path3 = self.assertPathExists( + (interfaces[2], cable3, front_ports[2], rear_ports[0], cable5, interfaces[6]), + is_complete=True, + is_active=True + ) + path4 = self.assertPathExists( + (interfaces[3], cable4, front_ports[3], rear_ports[0], cable5, interfaces[7]), + is_complete=True, + is_active=True + ) + path5 = self.assertPathExists( + (interfaces[4], cable5, rear_ports[0], front_ports[0], cable1, interfaces[0]), + is_complete=True, + is_active=True + ) + path6 = self.assertPathExists( + (interfaces[5], cable5, rear_ports[0], front_ports[1], cable2, interfaces[1]), + is_complete=True, + is_active=True + ) + path7 = self.assertPathExists( + (interfaces[6], cable5, rear_ports[0], front_ports[2], cable3, interfaces[2]), + is_complete=True, + is_active=True + ) + path8 = self.assertPathExists( + (interfaces[7], cable5, rear_ports[0], front_ports[3], cable4, interfaces[3]), + is_complete=True, + is_active=True + ) + self.assertEqual(CablePath.objects.count(), 8) + for interface in interfaces: + interface.refresh_from_db() + self.assertPathIsSet(interfaces[0], path1) + self.assertPathIsSet(interfaces[1], path2) + self.assertPathIsSet(interfaces[2], path3) + self.assertPathIsSet(interfaces[3], path4) + self.assertPathIsSet(interfaces[4], path5) + self.assertPathIsSet(interfaces[5], path6) + self.assertPathIsSet(interfaces[6], path7) + self.assertPathIsSet(interfaces[7], path8) + self.assertEqual(interfaces[4].cable_connector, 1) + self.assertEqual(interfaces[4].cable_positions, [1]) + self.assertEqual(interfaces[5].cable_connector, 2) + self.assertEqual(interfaces[5].cable_positions, [1]) + self.assertEqual(interfaces[6].cable_connector, 3) + self.assertEqual(interfaces[6].cable_positions, [1]) + self.assertEqual(interfaces[7].cable_connector, 4) + self.assertEqual(interfaces[7].cable_positions, [1]) + rear_ports[0].refresh_from_db() + self.assertEqual(rear_ports[0].cable_connector, 1) + self.assertEqual(rear_ports[0].cable_positions, [1, 2, 3, 4]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_106_cable_profile_shuffle_2x2_mpo8(self): + """ + [IF1] --C1-- [FP1][RP1] --C17-- [RP3][FP9] --C9-- [IF9] + [IF2] --C2-- [FP2] [FP10] --C10-- [IF10] + [IF3] --C3-- [FP3] [FP11] --C11-- [IF11] + [IF4] --C4-- [FP4] [FP12] --C12-- [IF12] + [IF5] --C5-- [FP5][RP2] [RP4][FP13] --C13-- [IF9] + [IF6] --C6-- [FP6] [FP14] --C14-- [IF10] + [IF7] --C7-- [FP7] [FP15] --C15-- [IF11] + [IF8] --C8-- [FP8] [FP16] --C16-- [IF12] Cable profile: Shuffle (2x2 MPO8) """ @@ -168,198 +665,138 @@ class CablePathTests(CablePathTestCase): Interface.objects.create(device=self.device, name='Interface 4:3'), Interface.objects.create(device=self.device, name='Interface 4:4'), ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4), + RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4), + RearPort.objects.create(device=self.device, name='Rear Port 3', positions=4), + RearPort.objects.create(device=self.device, name='Rear Port 4', positions=4), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), + FrontPort.objects.create(device=self.device, name='Front Port 5'), + FrontPort.objects.create(device=self.device, name='Front Port 6'), + FrontPort.objects.create(device=self.device, name='Front Port 7'), + FrontPort.objects.create(device=self.device, name='Front Port 8'), + FrontPort.objects.create(device=self.device, name='Front Port 9'), + FrontPort.objects.create(device=self.device, name='Front Port 10'), + FrontPort.objects.create(device=self.device, name='Front Port 11'), + FrontPort.objects.create(device=self.device, name='Front Port 12'), + FrontPort.objects.create(device=self.device, name='Front Port 13'), + FrontPort.objects.create(device=self.device, name='Front Port 14'), + FrontPort.objects.create(device=self.device, name='Front Port 15'), + FrontPort.objects.create(device=self.device, name='Front Port 16'), + ] + port_mappings = [] + for i, front_port in enumerate(front_ports): + port_mappings.append( + PortMapping( + device=self.device, + front_port=front_ports[i], + front_port_position=1, + rear_port=rear_ports[int(i / 4)], + rear_port_position=(i % 4) + 1, + ), + ) + PortMapping.objects.bulk_create(port_mappings) - # Create cable 1 - cable1 = Cable( + # Create cables + cables = [] + for interface, front_port in zip(interfaces, front_ports): + cable = Cable(a_terminations=[interface], b_terminations=[front_port]) + cable.clean() + cable.save() + cables.append(cable) + shuffle_cable = Cable( profile=CableProfileChoices.SHUFFLE_2X2_MPO8, - a_terminations=interfaces[0:8], - b_terminations=interfaces[8:16], + a_terminations=rear_ports[0:2], + b_terminations=rear_ports[2:4], ) - cable1.clean() - cable1.save() + shuffle_cable.clean() + shuffle_cable.save() paths = [ # A-to-B paths self.assertPathExists( - (interfaces[0], cable1, interfaces[8]), is_complete=True, is_active=True + ( + interfaces[0], cables[0], front_ports[0], rear_ports[0], shuffle_cable, rear_ports[2], + front_ports[8], cables[8], interfaces[8], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[1], cable1, interfaces[9]), is_complete=True, is_active=True + ( + interfaces[1], cables[1], front_ports[1], rear_ports[0], shuffle_cable, rear_ports[2], + front_ports[9], cables[9], interfaces[9], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[2], cable1, interfaces[12]), is_complete=True, is_active=True + ( + interfaces[2], cables[2], front_ports[2], rear_ports[0], shuffle_cable, rear_ports[3], + front_ports[12], cables[12], interfaces[12], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[3], cable1, interfaces[13]), is_complete=True, is_active=True + ( + interfaces[3], cables[3], front_ports[3], rear_ports[0], shuffle_cable, rear_ports[3], + front_ports[13], cables[13], interfaces[13], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[4], cable1, interfaces[10]), is_complete=True, is_active=True + ( + interfaces[4], cables[4], front_ports[4], rear_ports[1], shuffle_cable, rear_ports[2], + front_ports[10], cables[10], interfaces[10], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[5], cable1, interfaces[11]), is_complete=True, is_active=True + ( + interfaces[5], cables[5], front_ports[5], rear_ports[1], shuffle_cable, rear_ports[2], + front_ports[11], cables[11], interfaces[11], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[6], cable1, interfaces[14]), is_complete=True, is_active=True + ( + interfaces[6], cables[6], front_ports[6], rear_ports[1], shuffle_cable, rear_ports[3], + front_ports[14], cables[14], interfaces[14], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[7], cable1, interfaces[15]), is_complete=True, is_active=True - ), - # B-to-A paths - self.assertPathExists( - (interfaces[8], cable1, interfaces[0]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[9], cable1, interfaces[1]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[10], cable1, interfaces[4]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[11], cable1, interfaces[5]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[12], cable1, interfaces[2]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[13], cable1, interfaces[3]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[14], cable1, interfaces[6]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[15], cable1, interfaces[7]), is_complete=True, is_active=True + ( + interfaces[7], cables[7], front_ports[7], rear_ports[1], shuffle_cable, rear_ports[3], + front_ports[15], cables[15], interfaces[15], + ), + is_complete=True, + is_active=True ), ] - self.assertEqual(CablePath.objects.count(), len(paths)) + self.assertEqual(CablePath.objects.count(), len(paths) * 2) for i, (interface, path) in enumerate(zip(interfaces, paths)): interface.refresh_from_db() self.assertPathIsSet(interface, path) - self.assertEqual(interface.cable_end, 'A' if i < 8 else 'B') - self.assertEqual(interface.cable_positions, [(i % 8) + 1]) + for i, rear_port in enumerate(rear_ports): + rear_port.refresh_from_db() + self.assertEqual(rear_port.cable_connector, (i % 2) + 1) + self.assertEqual(rear_port.cable_positions, [1, 2, 3, 4]) # Test SVG generation CableTraceSVG(interfaces[0]).render() - # Delete cable 1 - cable1.delete() - - # Check that all CablePaths have been deleted - self.assertEqual(CablePath.objects.count(), 0) - - @skip("Under development") - def test_104_cable_profile_4x4_mpo8(self): - """ - [IF1:1] --C1-- [IF3:1] - [IF1:2] [IF3:2] - [IF1:3] [IF3:3] - [IF1:4] [IF3:4] - [IF2:1] [IF4:1] - [IF2:2] [IF4:2] - [IF2:3] [IF4:3] - [IF2:4] [IF4:4] - - Cable profile: Shuffle (4x4 MPO8) - """ - interfaces = [ - # A side - Interface.objects.create(device=self.device, name='Interface 1:1'), - Interface.objects.create(device=self.device, name='Interface 1:2'), - Interface.objects.create(device=self.device, name='Interface 2:1'), - Interface.objects.create(device=self.device, name='Interface 2:2'), - Interface.objects.create(device=self.device, name='Interface 3:1'), - Interface.objects.create(device=self.device, name='Interface 3:2'), - Interface.objects.create(device=self.device, name='Interface 4:1'), - Interface.objects.create(device=self.device, name='Interface 4:2'), - # B side - Interface.objects.create(device=self.device, name='Interface 5:1'), - Interface.objects.create(device=self.device, name='Interface 5:2'), - Interface.objects.create(device=self.device, name='Interface 6:1'), - Interface.objects.create(device=self.device, name='Interface 6:2'), - Interface.objects.create(device=self.device, name='Interface 7:1'), - Interface.objects.create(device=self.device, name='Interface 7:2'), - Interface.objects.create(device=self.device, name='Interface 8:1'), - Interface.objects.create(device=self.device, name='Interface 8:2'), - ] - - # Create cable 1 - cable1 = Cable( - profile=CableProfileChoices.SHUFFLE_4X4_MPO8, - a_terminations=interfaces[0:8], - b_terminations=interfaces[8:16], - ) - cable1.clean() - cable1.save() - - paths = [ - # A-to-B paths - self.assertPathExists( - (interfaces[0], cable1, interfaces[8]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[1], cable1, interfaces[10]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[2], cable1, interfaces[12]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[3], cable1, interfaces[14]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[4], cable1, interfaces[9]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[5], cable1, interfaces[11]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[6], cable1, interfaces[13]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[7], cable1, interfaces[15]), is_complete=True, is_active=True - ), - # B-to-A paths - self.assertPathExists( - (interfaces[8], cable1, interfaces[0]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[9], cable1, interfaces[4]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[10], cable1, interfaces[1]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[11], cable1, interfaces[5]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[12], cable1, interfaces[2]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[13], cable1, interfaces[6]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[14], cable1, interfaces[3]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[15], cable1, interfaces[7]), is_complete=True, is_active=True - ), - ] - self.assertEqual(CablePath.objects.count(), len(paths)) - - for i, (interface, path) in enumerate(zip(interfaces, paths)): - interface.refresh_from_db() - self.assertPathIsSet(interface, path) - self.assertEqual(interface.cable_end, 'A' if i < 8 else 'B') - self.assertEqual(interface.cable_positions, [(i % 8) + 1]) - - # Test SVG generation - CableTraceSVG(interfaces[0]).render() - - # Delete cable 1 - cable1.delete() - - # Check that all CablePaths have been deleted - self.assertEqual(CablePath.objects.count(), 0) - def test_202_single_path_via_pass_through_with_breakouts(self): """ [IF1] --C1-- [FP1] [RP1] --C2-- [IF3] @@ -551,7 +988,7 @@ class CablePathTests(CablePathTestCase): cable2.clean() cable2.save() cable3 = Cable( - profile=CableProfileChoices.STRAIGHT_1C8P, + profile=CableProfileChoices.SINGLE_1C8P, a_terminations=[rearport1], b_terminations=[rearport2] ) @@ -783,7 +1220,7 @@ class CablePathTests(CablePathTestCase): # Create cables cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_2C1P, + profile=CableProfileChoices.SINGLE_2C1P, a_terminations=[interfaces[0]], b_terminations=[front_ports[0], front_ports[1]] ) @@ -796,7 +1233,7 @@ class CablePathTests(CablePathTestCase): cable2.clean() cable2.save() cable3 = Cable( - profile=CableProfileChoices.STRAIGHT_2C1P, + profile=CableProfileChoices.SINGLE_2C1P, a_terminations=[interfaces[1]], b_terminations=[front_ports[2], front_ports[3]] ) @@ -864,14 +1301,14 @@ class CablePathTests(CablePathTestCase): # Create cables cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_2C2P, + profile=CableProfileChoices.TRUNK_2C2P, a_terminations=[interfaces[0], interfaces[1]], b_terminations=[frontport1, frontport2] ) cable1.clean() cable1.save() cable2 = Cable( - profile=CableProfileChoices.STRAIGHT_2C2P, + profile=CableProfileChoices.TRUNK_2C2P, a_terminations=[rearport1, rearport2], b_terminations=[interfaces[2], interfaces[3]] )