diff --git a/netbox/dcim/cable_profiles.py b/netbox/dcim/cable_profiles.py index 9aa68c821..7a7f4953c 100644 --- a/netbox/dcim/cable_profiles.py +++ b/netbox/dcim/cable_profiles.py @@ -44,7 +44,12 @@ class BaseCableProfile: ) }) - def get_mapped_position(self, position): + def get_mapped_position(self, side, position): + """ + Return the mapped position for a given cable end and position. + + By default, assume all positions are symmetrical. + """ return position def get_peer_terminations(self, terminations, position_stack): @@ -65,7 +70,7 @@ class BaseCableProfile: cable_end=terminations[0].opposite_cable_end ) if position is not None: - qs = qs.filter(position=self.get_mapped_position(position)) + qs = qs.filter(position=self.get_mapped_position(local_end, position)) return qs @@ -93,11 +98,11 @@ class BToManyCableProfile(BaseCableProfile): b_side_numbered = False -class Shuffle2x2MPOCableProfile(BaseCableProfile): +class Shuffle2x2MPO8CableProfile(BaseCableProfile): a_max_connections = 8 b_max_connections = 8 - def get_mapped_position(self, position): + def get_mapped_position(self, side, position): return { 1: 1, 2: 2, @@ -108,3 +113,26 @@ class Shuffle2x2MPOCableProfile(BaseCableProfile): 7: 7, 8: 8, }.get(position) + + +class Shuffle4x4MPO8CableProfile(BaseCableProfile): + a_max_connections = 8 + b_max_connections = 8 + # A side to B side position mapping + _a_mapping = { + 1: 1, + 2: 3, + 3: 5, + 4: 7, + 5: 2, + 6: 4, + 7: 6, + 8: 8, + } + # B side to A side position mapping + _b_mapping = {v: k for k, v in _a_mapping.items()} + + def get_mapped_position(self, side, position): + if side.lower() == 'b': + return self._b_mapping.get(position) + return self._a_mapping.get(position) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index b3e04808a..e1c090093 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1722,7 +1722,8 @@ class CableProfileChoices(ChoiceSet): STRAIGHT_MULTI = 'straight-multi' A_TO_MANY = 'a-to-many' B_TO_MANY = 'b-to-many' - SHUFFLE_2X2_MPO = 'shuffle-2x2-mpo' + SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8' + SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8' CHOICES = ( (STRAIGHT_SINGLE, _('Straight (single position)')), @@ -1730,7 +1731,8 @@ class CableProfileChoices(ChoiceSet): # TODO: Better names for many-to-one profiles? (A_TO_MANY, _('A to many')), (B_TO_MANY, _('B to many')), - (SHUFFLE_2X2_MPO, _('Shuffle (2x2 MPO)')), + (SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')), + (SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')), ) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 3aa916c45..d7777870e 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -138,7 +138,8 @@ class Cable(PrimaryModel): CableProfileChoices.STRAIGHT_MULTI: cable_profiles.StraightMultiCableProfile, CableProfileChoices.A_TO_MANY: cable_profiles.AToManyCableProfile, CableProfileChoices.B_TO_MANY: cable_profiles.BToManyCableProfile, - CableProfileChoices.SHUFFLE_2X2_MPO: cable_profiles.Shuffle2x2MPOCableProfile, + CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile, + CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile, }.get(self.profile) def _get_x_terminations(self, side): diff --git a/netbox/dcim/tests/test_cablepaths2.py b/netbox/dcim/tests/test_cablepaths2.py index cb26bca6b..54da8c1ad 100644 --- a/netbox/dcim/tests/test_cablepaths2.py +++ b/netbox/dcim/tests/test_cablepaths2.py @@ -262,7 +262,7 @@ class CablePathTests(CablePathTestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_105_cable_profile_2x2_mpo(self): + def test_105_cable_profile_2x2_mpo8(self): """ [IF1:1] --C1-- [IF3:1] [IF1:2] [IF3:2] @@ -273,9 +273,10 @@ class CablePathTests(CablePathTestCase): [IF2:3] [IF4:3] [IF2:4] [IF4:4] - Cable profile: Shuffle (2x2 MPO) + Cable profile: Shuffle (2x2 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 1:3'), @@ -284,6 +285,7 @@ class CablePathTests(CablePathTestCase): Interface.objects.create(device=self.device, name='Interface 2:2'), Interface.objects.create(device=self.device, name='Interface 2:3'), Interface.objects.create(device=self.device, name='Interface 2:4'), + # B side 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 3:3'), @@ -296,7 +298,7 @@ class CablePathTests(CablePathTestCase): # Create cable 1 cable1 = Cable( - profile=CableProfileChoices.SHUFFLE_2X2_MPO, + profile=CableProfileChoices.SHUFFLE_2X2_MPO8, a_terminations=interfaces[0:8], b_terminations=interfaces[8:16], ) @@ -372,6 +374,118 @@ class CablePathTests(CablePathTestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) + def test_106_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_position, (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]