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
This commit is contained in:
Martin Hauser 2026-06-26 16:02:39 +02:00 committed by Jeremy Stretch
parent 1077ff7169
commit 4da65854e1
4 changed files with 89 additions and 0 deletions

View File

@ -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,

View File

@ -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)')),
),
),

View File

@ -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)

View File

@ -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