Skip to content

Commit df1acf4

Browse files
committed
tests/e2e: provision NetBox with docker compose
Add the infrastructure side of the SONiC config-generation E2E golden test: tests/e2e/compose.yaml, tests/e2e/deploy_netbox.sh and tests/e2e/sonic_golden_test.sh, plus the Makefile target and .gitignore entry that drive them. compose.yaml defines three services (NetBox, its Postgres database and Redis) -- no Kubernetes, so there is no kind/kubectl dependency and no cluster bring-up latency. NetBox is configured entirely through the environment variables its own baked-in configuration.py already reads; nothing is templated or mounted over it. API_TOKEN_PEPPERS is deliberately left unset so that a plain v1 API token can be minted for the test run instead of the peppered v2 format. NETBOX_PORT is the only variable interpolated into the compose file, because compose re-interpolates the whole file on every subcommand (up, ps, down, ...): keeping the rest of the configuration in the container environment avoids re-resolving values on each invocation. deploy_netbox.sh brings the stack up, waits for the healthchecks and mints/prints the API token. sonic_golden_test.sh is the harness entrypoint: it provisions NetBox via deploy_netbox.sh, then seeds it by installing netbox-manager into a dedicated venv (so its package pins never mutate this project's venv) and running it against the fixtures under tests/e2e/scenario/ -- netbox-manager's own bundled example/ data is not used. Concurrent seeding is disabled by default (SEED_PARALLEL=1) because seeding files in the same numeric group take KEY SHARE locks on shared parent dcim_device rows and can deadlock; the script documents the observed error and the condition that still causes it (200-fabric.yml cabling both leaves to a shared spine). Finally it runs sync_sonic() via generate.py and compares the result against tests/e2e/golden/. make sonic-e2e wires the above together; make sonic-e2e-regen adds --regenerate to rewrite the goldens. --regenerate refuses to run against a reused stack (CREATED_STACK==0) unless ALLOW_WARM_REGEN=1 is set: applying the fixtures as an UPDATE over whatever is already in a reused database can produce goldens a fresh stack -- the only kind CI ever uses -- would not reproduce, and nothing would otherwise say so. sonic_golden_test.sh also now accepts --allow-coverage-loss and forwards it to tests.e2e.compare, so an intentional fixture removal can get a green regen without editing the script by hand. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Roger Luethi <luethi@osism.tech>
1 parent 572bb79 commit df1acf4

5 files changed

Lines changed: 423 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
*.egg-info
2+
.venv-sonic-e2e/
23
*.pyc
34
*.swp
45
__pycache__

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
NETBOX_MANAGER_DIR ?= $(abspath ../netbox-manager)
2+
3+
# SONiC config-generation E2E golden test (see tests/e2e/sonic_golden_test.sh).
4+
5+
# Full cycle: start the NetBox compose stack (an existing stack is reused
6+
# and left in place), seed, generate, compare against tests/e2e/golden/.
7+
sonic-e2e:
8+
NETBOX_MANAGER_DIR=$(NETBOX_MANAGER_DIR) tests/e2e/sonic_golden_test.sh
9+
10+
# Regenerate the golden files after an intentional generator change,
11+
# then review and commit the diff.
12+
sonic-e2e-regen:
13+
NETBOX_MANAGER_DIR=$(NETBOX_MANAGER_DIR) tests/e2e/sonic_golden_test.sh --regenerate
14+
15+
# Start the NetBox stack and leave it running for debugging. Export a
16+
# NETBOX_TOKEN beforehand to get a known API token minted.
17+
sonic-e2e-up:
18+
tests/e2e/deploy_netbox.sh
19+
20+
# Stop the NetBox stack and remove its volumes.
21+
sonic-e2e-down:
22+
docker compose -f tests/e2e/compose.yaml down --volumes --remove-orphans
23+
24+
.PHONY: sonic-e2e sonic-e2e-regen sonic-e2e-up sonic-e2e-down

tests/e2e/compose.yaml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
# NetBox fixture for the SONiC config-generation E2E golden test.
3+
#
4+
# Three services, no Kubernetes: the test only needs a NetBox REST API,
5+
# its PostgreSQL and its Valkey. See
6+
# docs/superpowers/specs/2026-07-29-sonic-e2e-compose-design.md.
7+
#
8+
# NetBox is configured through the environment variables its own baked
9+
# /etc/netbox/config/configuration.py reads, so no configuration file is
10+
# mounted. API_TOKEN_PEPPERS is deliberately NOT set: the image's
11+
# super_user.py only creates a token when a pepper is configured, and then
12+
# only a v2 one, which pynetbox / netbox.netbox cannot use. deploy_netbox.sh
13+
# mints a v1 token instead. ("No API token will be created" in the netbox
14+
# log is therefore expected, not an error.)
15+
#
16+
# The secret key and superuser password are fixed literals, not
17+
# interpolated variables. This stack is ephemeral, published on loopback
18+
# only, and thrown away after each run, so they are not credentials. Keeping
19+
# them out of ${...} matters because docker compose interpolates the whole
20+
# file on *every* subcommand -- a `${VAR:?}` here would make plain
21+
# `docker compose down` fail whenever the caller's shell lacked the value.
22+
#
23+
# The NetBox version is pinned because the golden files were generated
24+
# against it -- changing it can change generated configs. postgres and
25+
# valkey are pinned to a patch level; they are tags, not digests, so this
26+
# bounds rather than freezes them.
27+
28+
name: sonic-e2e
29+
30+
services:
31+
postgres:
32+
image: postgres:17.10-alpine
33+
environment:
34+
# Must match netbox's DB_NAME / DB_USER / DB_PASSWORD below. The
35+
# official image mandates POSTGRES_PASSWORD and would otherwise
36+
# default the database and role to "postgres".
37+
POSTGRES_DB: netbox
38+
POSTGRES_USER: netbox
39+
POSTGRES_PASSWORD: netbox
40+
healthcheck:
41+
test: ["CMD-SHELL", "pg_isready -U netbox -d netbox"]
42+
interval: 5s
43+
timeout: 5s
44+
retries: 24
45+
46+
valkey:
47+
image: valkey/valkey:8.1-alpine
48+
# No authentication: valkey is reachable only on the compose-private
49+
# network and publishes no port, so netbox needs no REDIS_PASSWORD.
50+
healthcheck:
51+
test: ["CMD", "valkey-cli", "ping"]
52+
interval: 5s
53+
timeout: 5s
54+
retries: 24
55+
56+
netbox:
57+
image: ghcr.io/netbox-community/netbox:v4.5.10
58+
depends_on:
59+
postgres:
60+
condition: service_healthy
61+
valkey:
62+
condition: service_healthy
63+
environment:
64+
DB_HOST: postgres
65+
DB_NAME: netbox
66+
DB_USER: netbox
67+
DB_PASSWORD: netbox
68+
# NetBox needs two logical Redis databases; one valkey serves both.
69+
REDIS_HOST: valkey
70+
REDIS_PORT: "6379"
71+
REDIS_DATABASE: "0"
72+
REDIS_CACHE_HOST: valkey
73+
REDIS_CACHE_PORT: "6379"
74+
REDIS_CACHE_DATABASE: "1"
75+
# Django wants >= 50 characters. Not a credential -- see the header.
76+
SECRET_KEY: "insecure-sonic-e2e-secret-key-not-used-outside-tests"
77+
SUPERUSER_NAME: admin
78+
SUPERUSER_EMAIL: admin@example.com
79+
SUPERUSER_PASSWORD: "insecure-sonic-e2e-admin-password"
80+
ALLOWED_HOSTS: "*"
81+
ports:
82+
# Published on loopback only; the seeding and generation phases talk
83+
# to http://127.0.0.1:${NETBOX_PORT}. No port-forward involved.
84+
- "127.0.0.1:${NETBOX_PORT:-8080}:8080"
85+
healthcheck:
86+
# First boot runs the full migration set, which takes minutes; the
87+
# start_period covers that without the container being marked
88+
# unhealthy. Subsequent boots skip migrations and come up in ~1 min.
89+
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://localhost:8080/login/"]
90+
interval: 10s
91+
timeout: 5s
92+
retries: 3
93+
start_period: 600s

tests/e2e/deploy_netbox.sh

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Provision NetBox for the SONiC E2E golden test (phase 1), using docker
4+
# compose. See docs/superpowers/specs/2026-07-29-sonic-e2e-compose-design.md.
5+
#
6+
# This script starts the stack and mints the API token. It deliberately
7+
# installs NO teardown trap: sonic_golden_test.sh owns the lifecycle, and a trap here
8+
# would fire when this script exits -- before seeding and generation.
9+
#
10+
# Safe to run standalone for debugging (`make sonic-e2e-up`), which leaves
11+
# the stack running.
12+
#
13+
# The NetBox secret key and superuser password are fixed literals in
14+
# compose.yaml (ephemeral loopback-only fixture); only the API token and the
15+
# published port are parameterised here.
16+
#
17+
# Environment overrides:
18+
# NETBOX_TOKEN v1 API token to mint (default: random)
19+
# NETBOX_PORT host port for the NetBox API (default: 8080)
20+
# PRINT_NETBOX_TOKEN=0 suppress echoing the token (set by sonic_golden_test.sh)
21+
22+
set -euo pipefail
23+
24+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25+
COMPOSE_FILE="${HERE}/compose.yaml"
26+
27+
# Only generated when unset, so sonic_golden_test.sh's value wins when it calls us.
28+
export NETBOX_TOKEN="${NETBOX_TOKEN:-$(openssl rand -hex 20)}"
29+
# Guard against two failure modes of a caller-supplied token: it is
30+
# interpolated into a Python string literal in the heredoc below, so a
31+
# quote or newline would break out of that literal; and a token that is
32+
# not 40 hex characters is otherwise accepted here but silently rejected
33+
# by NetBox much later, far from this, the actual cause.
34+
[[ "${NETBOX_TOKEN}" =~ ^[0-9a-f]{40}$ ]] || {
35+
echo "error: NETBOX_TOKEN must be 40 hex characters" >&2
36+
exit 2
37+
}
38+
export NETBOX_PORT="${NETBOX_PORT:-8080}"
39+
40+
compose() { docker compose -f "${COMPOSE_FILE}" "$@"; }
41+
42+
echo ">>> Starting the NetBox stack (postgres, valkey, netbox)"
43+
# --wait blocks until every service is healthy. First boot runs the full
44+
# NetBox migration set, hence the generous timeout.
45+
compose up --detach --wait --wait-timeout 900
46+
47+
# Mint a deterministic v1 API token for the superuser. NetBox 4.5 introduced
48+
# peppered "v2" API tokens; the image's bootstrap only ever creates a v2 one,
49+
# and only when API_TOKEN_PEPPERS is set (compose.yaml leaves it unset, so it
50+
# creates none). pynetbox / netbox.netbox authenticate with
51+
# `Authorization: Token <key>`, i.e. a v1 token, which NetBox accepts through
52+
# v4.6 -- legacy v1 support is removed in v4.7, so re-check this on a bump.
53+
#
54+
# The delete-then-create makes this idempotent: re-running against a reused
55+
# stack replaces the old token instead of colliding with it. Keep the delete.
56+
#
57+
# The script is fed over stdin (not `shell -c`) so the token never lands on a
58+
# command line inside the container.
59+
echo ">>> Creating a deterministic v1 API token for the superuser"
60+
compose exec -T netbox /opt/netbox/netbox/manage.py shell --interface python <<PYEOF
61+
from django.contrib.auth import get_user_model
62+
from users.models import Token
63+
from users.choices import TokenVersionChoices
64+
user = get_user_model().objects.get(username="admin")
65+
Token.objects.filter(user=user).delete()
66+
Token.objects.create(user=user, version=TokenVersionChoices.V1, token="${NETBOX_TOKEN}")
67+
print("Created v1 token for", user.username)
68+
PYEOF
69+
70+
echo
71+
echo "NetBox is deployed."
72+
echo
73+
echo " API : http://127.0.0.1:${NETBOX_PORT}"
74+
# Echo the token only for the standalone debug workflow; sonic_golden_test.sh sets
75+
# PRINT_NETBOX_TOKEN=0 to keep it out of CI logs that may be retained.
76+
if [[ "${PRINT_NETBOX_TOKEN:-1}" != "0" ]]; then
77+
echo " API token : ${NETBOX_TOKEN}"
78+
fi
79+
echo

0 commit comments

Comments
 (0)