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, ):