Refuse breakouts that claim other ports - #2560
Merged
Merged
Conversation
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The
real_port_configfixture computesrepo_rootviaPath(__file__).resolve().parents[5], which is fairly brittle to directory layout changes; consider deriving the path from a more stable anchor (e.g. usingimportlib.resourcesor a smaller fixed relative path from the test module) to make the tests more robust. - The logic to build
childrenport-name lists is duplicated across the NetBox-format, SONiC 400G, and standard SONiC paths; extracting a small helper (e.g._breakout_children_from_group(...)) would reduce the chance of those paths diverging subtly in future changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `real_port_config` fixture computes `repo_root` via `Path(__file__).resolve().parents[5]`, which is fairly brittle to directory layout changes; consider deriving the path from a more stable anchor (e.g. using `importlib.resources` or a smaller fixed relative path from the test module) to make the tests more robust.
- The logic to build `children` port-name lists is duplicated across the NetBox-format, SONiC 400G, and standard SONiC paths; extracting a small helper (e.g. `_breakout_children_from_group(...)`) would reduce the chance of those paths diverging subtly in future changes.
## Individual Comments
### Comment 1
<location path="tests/unit/tasks/conductor/sonic/test_breakout_detection.py" line_range="579-588" />
<code_context>
+# ---------------------------------------------------------------------------
+
+
+@pytest.fixture
+def real_port_config(monkeypatch):
+ """Load a port_config from the .ini files actually shipped in this repo.
+
+ The helpers above build port_configs with one or two entries, which cannot
+ express a child slot already occupied by another port -- the one shape that
+ makes a breakout unsafe, and the reason this class of bug went unnoticed.
+ These tests need the real file.
+ """
+
+ def _load(hwsku):
+ repo_root = pathlib.Path(__file__).resolve().parents[5]
+ monkeypatch.setattr(
+ interface_module,
+ "PORT_CONFIG_PATH",
+ str(repo_root / "files" / "sonic" / "port_config"),
+ )
+ interface_module.clear_port_config_cache()
+ return interface_module.get_port_config(hwsku)
+
</code_context>
<issue_to_address>
**suggestion:** The real_port_config fixture relies on a hard-coded parents[5] path, which may be brittle if the test file is moved or the repo layout changes.
Consider resolving the repo root in a layout-independent way, e.g., walking up parents until you hit a known marker (pyproject.toml/setup.cfg/tests) or the `files/sonic/port_config` directory itself. This avoids relying on a fixed parents[5] depth and keeps the fixture working if the file is moved or the tree structure changes.
Suggested implementation:
```python
import pathlib
from types import SimpleNamespace
import pytest
@pytest.fixture
def real_port_config(monkeypatch):
"""Load a port_config from the .ini files actually shipped in this repo.
The helpers above build port_configs with one or two entries, which cannot
express a child slot already occupied by another port -- the one shape that
makes a breakout unsafe, and the reason this class of bug went unnoticed.
These tests need the real file.
"""
def _find_repo_root(start: pathlib.Path) -> pathlib.Path:
"""Walk up parents until we find a known repo marker or port_config dir."""
current = start
while True:
port_config_dir = current / "files" / "sonic" / "port_config"
if port_config_dir.exists():
return current
# Common project-root markers
if any(
(current / marker).exists()
for marker in ("pyproject.toml", "setup.cfg", "setup.py", "tests")
):
return current
if current.parent == current:
raise RuntimeError("Could not locate repository root for tests")
current = current.parent
def _load(hwsku):
repo_root = _find_repo_root(pathlib.Path(__file__).resolve())
monkeypatch.setattr(
interface_module,
"PORT_CONFIG_PATH",
str(repo_root / "files" / "sonic" / "port_config"),
)
interface_module.clear_port_config_cache()
return interface_module.get_port_config(hwsku)
return _load
```
1. Ensure `interface_module` is imported or otherwise available in this test module; if it is not yet imported, add an appropriate import (e.g., `from <module> import interface_module`) alongside the other imports.
2. If your repo layout uses a different root marker than `pyproject.toml`, `setup.cfg`, `setup.py`, or `tests`, add that marker to the list in `_find_repo_root`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Comment on lines
+579
to
+588
| @pytest.fixture | ||
| def real_port_config(monkeypatch): | ||
| """Load a port_config from the .ini files actually shipped in this repo. | ||
|
|
||
| The helpers above build port_configs with one or two entries, which cannot | ||
| express a child slot already occupied by another port -- the one shape that | ||
| makes a breakout unsafe, and the reason this class of bug went unnoticed. | ||
| These tests need the real file. | ||
| """ | ||
|
|
There was a problem hiding this comment.
suggestion: The real_port_config fixture relies on a hard-coded parents[5] path, which may be brittle if the test file is moved or the repo layout changes.
Consider resolving the repo root in a layout-independent way, e.g., walking up parents until you hit a known marker (pyproject.toml/setup.cfg/tests) or the files/sonic/port_config directory itself. This avoids relying on a fixed parents[5] depth and keeps the fixture working if the file is moved or the tree structure changes.
Suggested implementation:
import pathlib
from types import SimpleNamespace
import pytest
@pytest.fixture
def real_port_config(monkeypatch):
"""Load a port_config from the .ini files actually shipped in this repo.
The helpers above build port_configs with one or two entries, which cannot
express a child slot already occupied by another port -- the one shape that
makes a breakout unsafe, and the reason this class of bug went unnoticed.
These tests need the real file.
"""
def _find_repo_root(start: pathlib.Path) -> pathlib.Path:
"""Walk up parents until we find a known repo marker or port_config dir."""
current = start
while True:
port_config_dir = current / "files" / "sonic" / "port_config"
if port_config_dir.exists():
return current
# Common project-root markers
if any(
(current / marker).exists()
for marker in ("pyproject.toml", "setup.cfg", "setup.py", "tests")
):
return current
if current.parent == current:
raise RuntimeError("Could not locate repository root for tests")
current = current.parent
def _load(hwsku):
repo_root = _find_repo_root(pathlib.Path(__file__).resolve())
monkeypatch.setattr(
interface_module,
"PORT_CONFIG_PATH",
str(repo_root / "files" / "sonic" / "port_config"),
)
interface_module.clear_port_config_cache()
return interface_module.get_port_config(hwsku)
return _load- Ensure
interface_moduleis imported or otherwise available in this test module; if it is not yet imported, add an appropriate import (e.g.,from <module> import interface_module) alongside the other imports. - If your repo layout uses a different root marker than
pyproject.toml,setup.cfg,setup.py, ortests, add that marker to the list in_find_repo_root.
A detected breakout names its children after the master's lane offsets
-- Ethernet<base + n*lanes_per_child> -- which assumes every slot below
the next master is unused. That holds for every port of every bundled
HWSKU except one: on Accton-AS7726-32X the last 100G port, Ethernet124,
has four lanes, but Ethernet125 and Ethernet126 are independent 10G SFP+
ports occupying two of its four child slots.
Claiming them as children silently reconfigures two working ports. A
breakout_ports entry is authoritative for a port's lanes and speed, and
the PORT table is built from every entry in port_config rather than only
the interfaces present in NetBox, so both ports are rewritten on any
device with that HWSKU: lanes 129 and 128 become 126 and 127, speed
10000 becomes 25000, and the aliases Eth1/33 and Eth1/34 become
Eth1/33/1 and Eth1/34/1, presenting them as breakout sub-ports.
Add _breakout_child_collisions() and refuse before mutating anything, so
the group is dropped whole rather than half-applied and the master keeps
its own configuration. Two of the three detection paths needed it:
- the Eth<module>/<port>/<subport> path, which computes child names
from the master offset and had no check at all;
- the SONiC-name 400G grouping path, likewise.
The SONiC-name standard grouping path already refuses these groups. Its
topology gate skips a group whose intermediate slots are ports in
port_config, which is the same condition this collision test applies. A
comment now records that the gate is load-bearing for correctness and
not only for the native-port misdetection it was written for.
The collision test is that a child name other than the master is itself
a key in port_config. Swept across every master of all nine bundled
HWSKUs at 1x/2x/4x/8x, it flags exactly the three real cases on that one
HWSKU and nothing else, so it needs no per-HWSKU exception list.
The new tests load the .ini files shipped in this repo instead of
building a port_config inline. That is the point: the existing
breakout tests use one- or two-entry port_configs, which cannot
express an occupied child slot, so this class of bug was
structurally unreachable by the suite. The 400G case keeps a
synthetic port_config because no bundled HWSKU has an 8-lane master
with an occupied child slot.
The tests locate the repo root by walking up for the setup.cfg
marker rather than a fixed parent depth, matching how
tests/integration/conftest.py finds it; a hard-coded depth breaks
silently when a test module moves.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
ideaship
force-pushed
the
fix/sonic-breakout-child-collisions
branch
from
August 5, 2026 11:55
2e99e86 to
8018af5
Compare
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A detected breakout names its children after the master's lane offsets —
Ethernet<base + n*lanes_per_child>— which assumes every slot below the nextmaster is free. That holds for every port of every bundled HWSKU except one.
On Accton-AS7726-32X the last 100G port has four lanes, but the next two
port names are already taken by independent 10G SFP+ ports:
A 4x breakout of
Ethernet124generates childrenEthernet124/125/126/127, soit claims
Ethernet125andEthernet126as sub-ports of a cage they are notpart of.
What that does to the generated config
Ethernet125lanes=129 speed=10000 alias=Eth1/33lanes=126 speed=25000 alias=Eth1/33/1Ethernet126lanes=128 speed=10000 alias=Eth1/34lanes=127 speed=25000 alias=Eth1/34/1All three fields are wrong, and both ports are re-presented as breakout
sub-ports. Two things make this reach a real switch rather than staying
theoretical:
breakout_portsentry is authoritative for a port's lanes and speed —the declared path stashes them directly, the inferred path recomputes them
from the master's lane list via
_calculate_breakout_port_lane();PORTtable is built from every entry inport_config, not only theinterfaces present in NetBox, so no NetBox modelling of the SFP ports is
needed for them to be emitted and corrupted. Every generated config for that
HWSKU already contains all 34 ports.
2x50Gcollides the same way, onEthernet126alone.The fix
_breakout_child_collisions()returns any child name that is a port of thisHWSKU in its own right. Both affected paths now check it before mutating
anything, so the group is dropped whole rather than half-applied and the
master keeps its own configuration:
Eth<module>/<port>/<subport>path, which computes child names from themaster offset and had no check at all;
The SONiC-name standard grouping path needed no change. Its existing
topology gate already skips a group whose intermediate slots are ports in
port_config— the same condition — so only a comment was added there,recording that the gate is load-bearing for correctness and not only for the
native-port misdetection it was written to prevent.
The collision test is simply that a child name other than the master is itself a
key in
port_config. Swept across every master of all nine bundled HWSKUs at1x/2x/4x/8x, it flags exactly the three real cases on that one HWSKU and nothing
else, so it needs no per-HWSKU exception list and no false positives to
suppress.
Why the existing tests could not have caught this
Every breakout test builds its
port_configfrom a helper that returns asingle entry (occasionally two). A one-port
port_configcannot express achild slot occupied by another port, so this class of bug was structurally
unreachable by the suite — the fixtures encoded the same assumption as the code.
The new tests therefore load the
.inifiles shipped in this repo. The 400G casekeeps a synthetic
port_config, because no bundled HWSKU has an 8-lane masterwith an occupied child slot.
Note on the underlying port config
Worth flagging for whoever owns those files, though it is deliberately not
changed here. Upstream
sonic-buildimageships 32 ports for this HWSKU —Ethernet125/Ethernet126do not exist there — and its Broadcom config carriesthem only as commented-out entries (
#portmap_130=128:10:m,#portmap_66=129:10:m). Lane 128 is muxed between the last 100G port's fourthlane and a 10G port, so the vendor ships the 10G ports disabled to leave the
100G port all four lanes.
Accton-AS7326-56X.inihas the same lane-128 overlap.So these files declare port pairs the hardware cannot run simultaneously, which
is a data question separate from this change. Refusing to silently claim a port
that exists is correct either way, which is why the guard goes in regardless of
how that is settled.
Verification
tests/unit/tasks/conductor/sonicat 701 passed.two corresponding tests fail, while the topology-gate test and the
breakout-still-works control keep passing.
flake8andblackclean.