fix(dcim): Resolve link peers for cable profile connectors

Add `_get_profile_link_peers()` method to handle connector-to-connector
mappings when cables use profiles. Includes regression test for
TRUNK_4C1P profile ensuring correct peer resolution between interfaces
and rear ports.

Fixes #21917
This commit is contained in:
Martin Hauser 2026-04-27 14:42:19 +02:00 committed by Jeremy Stretch
parent 5abcebb67b
commit aa14e1d322
2 changed files with 99 additions and 7 deletions

View File

@ -254,13 +254,37 @@ class CabledObjectModel(models.Model):
@cached_property
def link_peers(self):
if self.cable:
return [
peer.termination
for peer in self.cable.terminations.all()
if peer.cable_end != self.cable_end
]
return []
if not self.cable:
return []
if self.cable.profile:
return self._get_profile_link_peers()
return [peer.termination for peer in self.cable.terminations.all() if peer.cable_end != self.cable_end]
def _get_profile_link_peers(self):
if self.cable_end is None or self.cable_connector is None or not self.cable_positions:
return []
profile = self.cable.profile_class()
peer_terminations = {
(peer.connector, position): peer.termination
for peer in self.cable.terminations.all()
if peer.cable_end == self.opposite_cable_end and peer.connector is not None
for position in peer.positions or []
}
link_peers = []
for position in self.cable_positions:
mapped_position = profile.get_mapped_position(self.cable_end, self.cable_connector, position)
if mapped_position is None:
continue
peer = peer_terminations.get(mapped_position)
if peer is not None and peer not in link_peers:
link_peers.append(peer)
return link_peers
@property
def _occupied(self):

View File

@ -0,0 +1,68 @@
from django.test import tag
from dcim.choices import CableProfileChoices
from dcim.models import Cable, Interface, RearPort
from dcim.tests.utils import CablePathTestCase
class CableProfileLinkPeerTests(CablePathTestCase):
"""
Tests for link peer resolution with cable profiles.
"""
@tag('regression') # #21917
def test_trunk_4c1p_link_peers(self):
"""
Link peers for trunk profile cables should honor connector mappings.
"""
interfaces = [Interface.objects.create(device=self.device, name=f'Interface {i}') for i in range(1, 5)]
rear_ports = [
RearPort.objects.create(device=self.device, name=f'Rear Port {i}', positions=1) for i in range(1, 5)
]
cable = Cable(
profile=CableProfileChoices.TRUNK_4C1P,
a_terminations=interfaces,
b_terminations=rear_ports,
)
cable.clean()
cable.save()
for interface, rear_port in zip(interfaces, rear_ports):
interface.refresh_from_db()
rear_port.refresh_from_db()
self.assertEqual(interface.link_peers, [rear_port])
self.assertEqual(rear_port.link_peers, [interface])
@tag('regression') # #21917
def test_breakout_shuffle_link_peers(self):
"""
Link peers for asymmetric breakout profiles should honor mapped connectors.
"""
rear_ports = [
RearPort.objects.create(device=self.device, name=f'Rear Port {i}', positions=4) for i in range(1, 3)
]
interfaces = [Interface.objects.create(device=self.device, name=f'Interface {i}') for i in range(1, 9)]
cable = Cable(
profile=CableProfileChoices.BREAKOUT_2C4P_8C1P_SHUFFLE,
a_terminations=rear_ports,
b_terminations=interfaces,
)
cable.clean()
cable.save()
for rear_port in rear_ports:
rear_port.refresh_from_db()
for interface in interfaces:
interface.refresh_from_db()
self.assertEqual(rear_ports[0].link_peers, [interfaces[0], interfaces[1], interfaces[4], interfaces[5]])
self.assertEqual(rear_ports[1].link_peers, [interfaces[2], interfaces[3], interfaces[6], interfaces[7]])
for interface in interfaces[0:2] + interfaces[4:6]:
self.assertEqual(interface.link_peers, [rear_ports[0]])
for interface in interfaces[2:4] + interfaces[6:8]:
self.assertEqual(interface.link_peers, [rear_ports[1]])