Skip to content

Add E2E golden tests for SONiC config generation#2483

Draft
ideaship wants to merge 9 commits into
mainfrom
sonic-e2e-harness
Draft

Add E2E golden tests for SONiC config generation#2483
ideaship wants to merge 9 commits into
mainfrom
sonic-e2e-harness

Conversation

@ideaship

Copy link
Copy Markdown
Contributor

Summary

Adds an end-to-end golden test for the SONiC config generator: provision NetBox on a local kind cluster, seed it with the netbox-manager example data, run sync_sonic(), and compare every exported config_db.json against golden files committed under tests/e2e/golden/. Any behavior change in the generator becomes a reviewable golden diff in the same PR.

Motivation

Unit tests verify the generator against fixture values that encode the author's assumptions, so a class of regressions — such as mixed kbps/Mbps interface speeds — can pass the unit suite while producing broken switch configurations. Nothing exercised the full pipeline (real NetBox data in, complete config_db out) until now:

Changes

  • SONIC_PORT_CONFIG_PATH setting so the generator can read the in-repo files/sonic/port_config/ outside the container image
  • tests/e2e/generate.py — drives sync_sonic(); since it returns only a device→config dict and swallows per-device failures, the driver asserts success itself (expected device set derived from golden filenames, empty-config detection, a loguru sink that fails the run on any ERROR record)
  • tests/e2e/compare.py — exact file-set equality (missing / extra / mismatched reported separately), structural diff paths like PORT|Ethernet4.speed, canonical unified diff, --regenerate for intentional output changes
  • tests/e2e/run.sh + Makefile targets sonic-e2e, sonic-e2e-regen, sonic-e2e-up, sonic-e2e-down; netbox-manager is installed from a checkout (sibling dir or NETBOX_MANAGER_DIR) into a dedicated venv
  • Golden files for the five example-testbed switches, cross-checked against their independent sources (seed YAML, port_config .ini speeds, the ASN derivation rule) and reproduced byte-for-byte by a second seed-and-generate cycle
  • Zuul job python-osism-sonic-e2e in check (restricted by a files matcher to changes that can alter generator output) and periodic-daily (unconditional, bounds seed-data drift); netbox-manager comes from required-projects, so Depends-On is honored for its code and seed data
  • Drive-by fix: _add_snmp_configuration no longer builds the Redis-backed Ansible vault for devices without encrypted secrets — it logged three ERRORs per device in any environment without vault infrastructure (found by the new error guard)

Verification

  • 2043 unit tests pass; the comparator and driver logic are unit-tested (tests/unit/e2e/)
  • Full local E2E cycle run twice against kind + NetBox 4.5.10: golden bootstrap, then a compare-mode rerun against a fresh re-seed reproduced all five files exactly

Follow-up work

  • Scenario seed data for the known regression shapes (derived-speed breakout subports, explicit kbps speeds, 4x10G fallback) plus VLAN coverage — the base example data exercises neither breakouts nor data-port VLANs, so the paths that broke in [bug] Generation of SONiC Config Breakout Port speed from Port Type fails #2478 are pinned only once that overlay lands
  • Factor the kind/kubectl/helm pre-run into a shared zuul-jobs role instead of the copy adapted from netbox-manager's pre-e2e.yml

Opened as draft so the new Zuul job can validate itself on this PR (the files matcher includes .zuul.yaml, tests/e2e/, and the playbooks).

🤖 Generated with Claude Code

ideaship added 6 commits July 17, 2026 15:17
The directory holding the per-HWSKU port_config .ini files was
hardcoded to /etc/sonic/port_config, which only exists inside the
container image (the Dockerfile copies files/sonic/port_config there).
Running the config generator outside a container -- for local
development or an E2E test harness that points it at the in-repo
files -- required root to plant files under /etc/sonic.

Introduce a SONIC_PORT_CONFIG_PATH setting in osism.settings,
following the existing SONIC_* environment variable convention, and
wire constants.PORT_CONFIG_PATH to it. The default is unchanged, and
the value is read at import time like every other setting, so the
container behavior is identical.

Tests cover the default, the environment override, and the
settings-to-constants wiring.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
Unit tests verify the SONiC config generator against fixture values
that encode the author's assumptions, so a class of regressions --
such as mixed kbps/Mbps interface speeds -- can pass the unit suite
while producing broken switch configurations. Nothing exercised the
full pipeline: real NetBox data in, complete config_db.json out.

Add a golden-file E2E harness that provisions NetBox on a local kind
cluster (reusing the deploy script from a netbox-manager checkout),
seeds it with the bundled example data, runs sync_sonic(), and
compares every exported config_db file against goldens committed
under tests/e2e/golden/. Any generator behavior change then surfaces
as a reviewable golden diff instead of shipping silently.

tests/e2e/generate.py drives the generation. sync_sonic() returns
only a device-to-config dict and logs-and-swallows per-device
failures, so the driver asserts success itself: the returned device
set must match the expectation derived from the golden file names,
empty configs fail, and a loguru sink turns any ERROR-level record
into a failure. --no-expect skips the device-set check for the
initial golden bootstrap.

tests/e2e/compare.py enforces exact file-set equality (missing,
extra, and mismatched exports are reported as distinct categories)
and reports mismatches both as structural paths such as
PORT|Ethernet4.speed and as a unified diff of the canonicalized
JSON. Canonicalization sorts dictionary keys only; array order is
part of the compared contract. --regenerate rewrites the goldens
canonically for review. Both modules are pure logic and covered by
the regular unit suite under tests/unit/e2e/.

tests/e2e/run.sh and the Makefile targets sonic-e2e, sonic-e2e-regen,
sonic-e2e-up and sonic-e2e-down tie the phases together. The exports
use hostname identifiers (the seed devices carry no serial numbers)
and the generator reads the in-repo port_config files via
SONIC_PORT_CONFIG_PATH. netbox-manager is installed from the checkout
rather than PyPI so a coordinated cross-repository change is tested
against the changed code and data.

Golden files are not yet committed; the initial set is bootstrapped
with make sonic-e2e-regen and reviewed by hand. A Zuul job and seed
data for known regression scenarios are follow-up work.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
_add_snmp_configuration created a VaultLib instance for every device
before looking at the secrets custom field. get_vault() reads the key
file at /share/ansible_vault_password.key and fetches the encrypted
vault password from Redis, so outside a worker container -- local
development, or an E2E run against a plain NetBox -- it fails and
logs three ERROR-level messages per synced device, even though there
was nothing to decrypt in the first place.

Defer vault construction until the device actually carries a
non-empty secrets custom field. Devices with encrypted secrets are
decrypted exactly as before; devices without secrets no longer touch
the key file or Redis at all.

Found by the E2E harness error guard, which treats any ERROR-level
log record during config generation as a failure.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
The harness previously installed netbox-manager into the project
venv. netbox-manager pins its own versions of packages that
python-osism pins exactly -- the install mutated five of them,
including pynetbox -- so a local E2E run silently left the developer
venv diverged from Pipfile.lock.

Install netbox-manager into a dedicated venv (.venv-sonic-e2e,
gitignored, reused across runs, overridable via SEED_VENV) instead.
The venv's bin directory is prepended to PATH because netbox-manager
drives Ansible through ansible-runner, which resolves ansible-playbook
via PATH rather than relative to its own interpreter.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
Bootstrap the golden set from the netbox-manager example data via
make sonic-e2e-regen: the four testbed switches plus the OOB switch,
generated by sync_sonic() against a seeded NetBox 4.5.10.

The files pin current generator behavior as a change detector; they
were cross-checked against their independent sources rather than
reviewed line by line:

- hostname, hwsku, management IP, loopback addresses (v4/v6) and the
  eth0 MAC match the seed resources for each device
- all 34 PORT entries of the AS7726-32X devices carry exactly the
  speeds of files/sonic/port_config/Accton-AS7726-32X.ini (the seed
  sets no NetBox speed overrides)
- BGP ASNs follow the documented rule (4200 prefix + loopback-derived
  suffix, e.g. 192.168.16.27 -> 4200016027) and the interconnected
  spine pair shares the group minimum 4200016029
- a second seed-and-generate cycle against a fresh NetBox deployment
  reproduced all five files byte-for-byte (determinism)

The VLAN tables are empty on every device: the example data assigns
VLANs only to the switches' own management interfaces, never to data
ports, so the generator's VLAN paths are not yet exercised. Coverage
for those (and for the breakout regression scenarios) comes with the
planned scenario seed data.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
Wire the E2E golden test into CI as python-osism-sonic-e2e: provision
NetBox on a kind cluster, seed it with the netbox-manager example
data, run sync_sonic() and compare the exported config_db files
against tests/e2e/golden/ via tests/e2e/run.sh.

The netbox-manager checkout comes from required-projects, so the job
consumes its seed data and CLI at tip-of-main and a Depends-On change
is tested against the changed code and data. The pre-run playbook is
adapted from netbox-manager's pre-e2e.yml (same pinned kind, kubectl
and Helm versions with SHA256-verified downloads, same IPv6 accept_ra
workaround for SLAAC-only CI nodes) and must stay in sync with it
until the setup is factored into a shared zuul-jobs role. On top of
that it installs curl, openssl and python3-venv for the harness
script and its seeding venv.

In check the job carries a files matcher so it only runs for changes
that can alter the generated output: conductor code, settings, the
port_config files, the harness itself, the CI playbooks, and the
dependency pins. periodic-daily runs it unconditionally, bounding
silent drift of the netbox-manager seed data to a day.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
ideaship added 3 commits July 17, 2026 19:24
The CI run of python-osism-sonic-e2e failed at the generation step:
importing sync_sonic pulls in the whole conductor package, whose
utils module does `from ansible import constants` at import time, and
ansible-core was not present in the job's venv.

The gap is easy to miss because every other environment masks it: the
unit tests stub the ansible modules in tests/unit/conftest.py (so the
unit job and a local pytest run stay green without ansible-core), and
the container image installs requirements.ansible.txt explicitly.
The E2E driver is the first consumer of the real import chain in a
plain pipenv environment.

Have run.sh install the project's own [ansible] extra (which resolves
to requirements.ansible.txt, the same pin the container uses) into the
project venv before generating. The step is idempotent and covers
local runs from a fresh venv as well as CI.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
The bundled netbox-manager example models no breakout ports and sets
no explicit interface speeds, so breakout detection and the
kbps-to-Mbps speed normalisation are never exercised by the base
golden test -- exactly the generation logic behind the recent
customer-visible breakout speed regression.

Add a scenario overlay applied as a second seeding pass. It defines a
minimal synthetic device type (hwsku Accton-AS9726-32D) and two
devices that each break the 8-lane 400G master Eth1/1 into a 4x100G
group. The three non-master sub-ports are absent from the port_config
.ini and so are generated by _add_missing_breakout_ports. The two
devices differ only in where the sub-port speed comes from:

  e2e-breakout-derived   speed derived from the interface type; no
                         explicit NetBox speed. This is how real
                         deployments model breakouts.
  e2e-breakout-explicit  the same sub-ports with an explicit NetBox
                         speed in kbps -- the other unit the
                         collection step must normalise.

Both must yield sub-port speed 100000. A tagged-VLAN port is added on
the derived device for VLAN/VLAN_MEMBER coverage the base example
lacks.

The seed data is synthetic; real customer NetBox configs were used
only as a modelling reference for interface naming and speed
conventions, with no customer identifiers copied.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
Goldens for the two breakout scenario devices, generated against a
seeded NetBox. Both devices produce sub-port speed 100000 for the
4x100G group -- the same value whether the speed is derived from the
interface type or set explicitly in kbps.

Verified the guard by reverting the fix locally against these goldens:

  - Restoring the mixed-unit collection (explicit speed left in kbps)
    together with the downstream division turns the derived golden red
    (Ethernet0/2/4/6 speed 100000 -> 100, the customer-visible
    symptom) while the explicit golden stays green.
  - Dropping the division instead turns the explicit golden red
    (speed 100000 -> 100000000, raw kbps) while the derived golden
    stays green.

The current code keeps both green, so either regression would now
fail the E2E test with a speed diff on the affected device.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants