From a19a73543a56757ab81c63890e5ebc7e303f233b Mon Sep 17 00:00:00 2001 From: Roger Luethi Date: Mon, 20 Jul 2026 22:03:06 +0200 Subject: [PATCH] Gate SONiC EthernetN breakout on port topology The loose SONiC-name breakout path treats any four consecutive EthernetN interfaces at <=50G as a 4xNG breakout without consulting the port_config topology. Unifying breakout mode selection widened this path to also emit 4x10G (via BREAKOUT_MODE_BY_SPEED), so a switch whose native low-speed ports happen to be named Ethernet0..3 is now misdetected as a breakout -- e.g. AS5835-54T (native 10G) or DellEMC-S5212f / AS7326-56X (native 25G) fabricate a 4x10G/4x25G cage that does not exist. Add a topology gate on this path: accept the group as a breakout only when the port_config confirms a single multi-lane master (Ethernet {base}) whose intermediate lanes Ethernet{base+1..base+3} are absent -- they materialize only when the cage is actually broken out. Four separate single-lane entries are native ports and are now skipped. Unlike the EthX/Y/Z path, a bare EthernetN name carries no explicit breakout signal, so the port_config is the only local evidence. This is a necessary-not-sufficient negative filter: it removes the known native-port false positives but cannot by itself prove breakout intent. A follow-up will add an explicit per-port breakout_mode declaration as the authoritative signal. Add a regression test covering four native single-lane 25G ports. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Roger Luethi --- osism/tasks/conductor/sonic/interface.py | 27 ++++++++++++++++++ .../sonic/test_breakout_detection.py | 28 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/osism/tasks/conductor/sonic/interface.py b/osism/tasks/conductor/sonic/interface.py index abb8c02eb..2ee32feaa 100644 --- a/osism/tasks/conductor/sonic/interface.py +++ b/osism/tasks/conductor/sonic/interface.py @@ -915,6 +915,33 @@ def detect_breakout_ports(device): if len(sonic_breakout_group) == 4: master_port = f"Ethernet{base_port}" + # Topology gate: only treat consecutive EthernetN ports as a + # breakout when the port_config confirms a single multi-lane + # master whose intermediate lanes are absent — they only + # materialize when the cage is broken out. Four separate + # single-lane entries are native ports, not a breakout cage. + # This prevents native low-speed switches (e.g. AS5835-54T + # 10G, DellEMC-S5212f / AS7326-56X 25G) from being + # misdetected as a 4xNG breakout. Unlike EthX/Y/Z, the bare + # EthernetN name carries no explicit breakout signal, so the + # port_config topology is the only local evidence available. + master_cfg = port_config.get(master_port) + if not master_cfg or "," not in master_cfg.get("lanes", ""): + logger.debug( + f"Skipping SONiC breakout for {master_port}: master " + f"is not a multi-lane port in port_config (native port)" + ) + continue + if any( + f"Ethernet{base_port + offset}" in port_config + for offset in (1, 2, 3) + ): + logger.debug( + f"Skipping SONiC breakout for {master_port}: " + f"intermediate ports present in port_config (native ports)" + ) + continue + # Determine breakout mode based on speed; the membership # filter above (<= 50G) limits this path to 4x10G, 4x25G # and 4x50G diff --git a/tests/unit/tasks/conductor/sonic/test_breakout_detection.py b/tests/unit/tasks/conductor/sonic/test_breakout_detection.py index b68b575bc..95198aac5 100644 --- a/tests/unit/tasks/conductor/sonic/test_breakout_detection.py +++ b/tests/unit/tasks/conductor/sonic/test_breakout_detection.py @@ -526,6 +526,34 @@ def test_detect_breakout_ports_sonic_standard_only_three_interfaces( } +def test_detect_breakout_ports_sonic_standard_native_ports_not_breakout( + patch_breakout_helpers, +): + # Four consecutive single-lane 25G ports that are each their own + # port_config entry are NATIVE ports, not a breakout cage, and must not be + # misdetected as a 4x25G breakout. A real breakout is a single multi-lane + # master with the intermediate lanes absent (they materialize only when the + # cage is broken out). This guards native low-speed switches such as + # DellEMC-S5212f / AS7326-56X (25G) and AS5835-54T (10G). + device = _make_sonic_device() + interfaces = [_make_iface(f"Ethernet{n}", speed=25_000_000) for n in range(4)] + port_config = { + f"Ethernet{n}": { + "lanes": str(29 + n), # one lane each -> native ports + "alias": f"twentyfiveGigE1/{n + 1}", + "index": str(n + 1), + "speed": "25000", + } + for n in range(4) + } + patch_breakout_helpers(interfaces=interfaces, port_config=port_config) + + assert detect_breakout_ports(device) == { + "breakout_cfgs": {}, + "breakout_ports": {}, + } + + def test_detect_breakout_ports_sonic_standard_speed_resolved_from_port_type( patch_breakout_helpers, ):