Przeglądaj źródła

feat(dcim): Add 1C8P:8C1P breakout cable profile

Introduce Breakout1C8Px8C1PCableProfile to map a single 8-position
connector to eight single-position connectors. Add profile choice,
registration, and bidirectional link peer tests for the new breakout.

Fixes #22279
Martin Hauser 1 tydzień temu
rodzic
commit
4da65854e1

+ 33 - 0
netbox/dcim/cable_profiles.py

@@ -409,6 +409,39 @@ class Breakout1C6Px6C1PCableProfile(BaseCableProfile):
     }
 
 
+class Breakout1C8Px8C1PCableProfile(BaseCableProfile):
+    a_connectors = {
+        1: 8,
+    }
+    b_connectors = {
+        1: 1,
+        2: 1,
+        3: 1,
+        4: 1,
+        5: 1,
+        6: 1,
+        7: 1,
+        8: 1,
+    }
+    _mapping = {
+        (1, 1): (1, 1),
+        (1, 2): (2, 1),
+        (1, 3): (3, 1),
+        (1, 4): (4, 1),
+        (1, 5): (5, 1),
+        (1, 6): (6, 1),
+        (1, 7): (7, 1),
+        (1, 8): (8, 1),
+        (2, 1): (1, 2),
+        (3, 1): (1, 3),
+        (4, 1): (1, 4),
+        (5, 1): (1, 5),
+        (6, 1): (1, 6),
+        (7, 1): (1, 7),
+        (8, 1): (1, 8),
+    }
+
+
 class Trunk2C4PShuffleCableProfile(BaseCableProfile):
     a_connectors = {
         1: 4,

+ 2 - 0
netbox/dcim/choices.py

@@ -1787,6 +1787,7 @@ class CableProfileChoices(ChoiceSet):
     BREAKOUT_1C2P_2C1P = 'breakout-1c2p-2c1p'
     BREAKOUT_1C4P_4C1P = 'breakout-1c4p-4c1p'
     BREAKOUT_1C6P_6C1P = 'breakout-1c6p-6c1p'
+    BREAKOUT_1C8P_8C1P = 'breakout-1c8p-8c1p'
     BREAKOUT_2C4P_8C1P_SHUFFLE = 'breakout-2c4p-8c1p-shuffle'
 
     CHOICES = (
@@ -1827,6 +1828,7 @@ class CableProfileChoices(ChoiceSet):
                 (BREAKOUT_1C2P_2C1P, _('1C2P:2C1P breakout')),
                 (BREAKOUT_1C4P_4C1P, _('1C4P:4C1P breakout')),
                 (BREAKOUT_1C6P_6C1P, _('1C6P:6C1P breakout')),
+                (BREAKOUT_1C8P_8C1P, _('1C8P:8C1P breakout')),
                 (BREAKOUT_2C4P_8C1P_SHUFFLE, _('2C4P:8C1P breakout (shuffle)')),
             ),
         ),

+ 1 - 0
netbox/dcim/models/cables.py

@@ -207,6 +207,7 @@ class Cable(PrimaryModel):
             CableProfileChoices.BREAKOUT_1C2P_2C1P: cable_profiles.Breakout1C2Px2C1PCableProfile,
             CableProfileChoices.BREAKOUT_1C4P_4C1P: cable_profiles.Breakout1C4Px4C1PCableProfile,
             CableProfileChoices.BREAKOUT_1C6P_6C1P: cable_profiles.Breakout1C6Px6C1PCableProfile,
+            CableProfileChoices.BREAKOUT_1C8P_8C1P: cable_profiles.Breakout1C8Px8C1PCableProfile,
             CableProfileChoices.BREAKOUT_2C4P_8C1P_SHUFFLE: cable_profiles.Breakout2C4Px8C1PShuffleCableProfile,
         }.get(self.profile)
 

+ 53 - 0
netbox/dcim/tests/test_cable_profiles.py

@@ -2,6 +2,7 @@ from django.test import tag
 
 from dcim.cable_profiles import (
     Breakout1C4Px4C1PCableProfile,
+    Breakout1C8Px8C1PCableProfile,
     Single1C1PCableProfile,
     Single1C4PCableProfile,
     Trunk2C2PCableProfile,
@@ -74,6 +75,32 @@ class CableProfileLinkPeerTestCase(BaseCablePathTestCase):
         for interface in interfaces[2:4] + interfaces[6:8]:
             self.assertEqual(interface.link_peers, [rear_ports[1]])
 
+    def test_breakout_1c8p_8c1p_link_peers(self):
+        """
+        Link peers for a 1C8P:8C1P breakout map the single A-side connector's
+        eight positions to the eight B-side connectors, and each back to it.
+        """
+        a_interface = Interface.objects.create(device=self.device, name='Interface A')
+        b_interfaces = [
+            Interface.objects.create(device=self.device, name=f'Interface B{i}') for i in range(1, 9)
+        ]
+
+        cable = Cable(
+            profile=CableProfileChoices.BREAKOUT_1C8P_8C1P,
+            a_terminations=[a_interface],
+            b_terminations=b_interfaces,
+        )
+        cable.clean()
+        cable.save()
+
+        a_interface.refresh_from_db()
+        for iface in b_interfaces:
+            iface.refresh_from_db()
+
+        self.assertEqual(a_interface.link_peers, b_interfaces)
+        for iface in b_interfaces:
+            self.assertEqual(iface.link_peers, [a_interface])
+
 
 class CableProfilePeerTerminationTestCase(BaseCablePathTestCase):
     """
@@ -228,6 +255,32 @@ class CableProfilePeerTerminationTestCase(BaseCablePathTestCase):
         b_pairs = [(iface, 1) for iface in self.interfaces[9:13]]
         self._assert_batch_matches_singular(profile, b_pairs)
 
+    def test_breakout_1c8p_profile(self):
+        """
+        Batch resolution on a 1C8P:8C1P breakout maps one A-side connector's
+        eight positions to eight B-side connectors.
+        """
+        cable = Cable(
+            profile=CableProfileChoices.BREAKOUT_1C8P_8C1P,
+            a_terminations=[self.interfaces[0]],
+            b_terminations=self.interfaces[1:9],
+        )
+        cable.clean()
+        cable.save()
+
+        self.interfaces[0].refresh_from_db()
+        for iface in self.interfaces[1:9]:
+            iface.refresh_from_db()
+        profile = Breakout1C8Px8C1PCableProfile()
+
+        # A→B direction (one connector, 8 positions → 8 connectors)
+        a_pairs = [(self.interfaces[0], pos) for pos in self.interfaces[0].cable_positions]
+        self._assert_batch_matches_singular(profile, a_pairs)
+
+        # B→A direction (8 connectors, 1 position each → one connector)
+        b_pairs = [(iface, 1) for iface in self.interfaces[1:9]]
+        self._assert_batch_matches_singular(profile, b_pairs)
+
     def test_multi_position_single_termination(self):
         """
         When a single-connector multi-position profile has only one termination