NornFlow has three test tiers. Most contributors and CI only need the first two.
| Tier | Location | When it runs | Needs live lab |
|---|---|---|---|
| Unit | tests/unit/ |
Default pytest, CI |
No |
| Layer 1 integration | tests/integration/catalog_namespaces/ |
Default pytest, CI |
No (mocked Nornir) |
| Layer 2 integration | tests/integration/containerlab/ |
Opt-in; maintainers run before merging your PR | Yes (reachable cEOS lab) |
pytest
# or
pytest tests/This runs unit tests and Layer 1 integration tests. Containerlab tests are excluded by default (addopts = "-m 'not containerlab'" in pyproject.toml).
GitHub Actions uses the same default: no lab, credentials, or Arista images required.
Layer 2 tests talk to real Arista cEOS devices over eAPI. The lab can be physical or virtual. What matters is that your environment matches the expected topology below and that management IPs are reachable from the machine running pytest.
The suite assumes a four-node spine–leaf lab with these hostnames and management addresses (defaults):
| Host | Mgmt IP (default) | Role | Nornir groups |
|---|---|---|---|
| spine1 | 172.29.163.101 | Spine | eos, spines |
| spine2 | 172.29.163.102 | Spine | eos, spines |
| leaf1 | 172.29.163.103 | Leaf | eos, leafs |
| leaf2 | 172.29.163.104 | Leaf | eos, leafs |
Additional expectations:
- eAPI enabled on each node:
http://<mgmt-ip>/command-apion port 80 (HTTP, not HTTPS). - Credentials shared across all four hosts (injected at test runtime via env vars).
- Data-plane links between nodes are not asserted, but the local blueprint runs
get_lldp_neighbors; a cabled spine–leaf topology will exercise that task meaningfully.
A common setup is a containerized Containerlab deployment with virtual cEOS images (spine1/2, leaf1/2). Any other lab that reproduces the same hostnames, reachability, and eAPI settings works equally well: bare metal, VM, cloud, or a different container runtime.
You are responsible for:
- Deploying and keeping the lab running.
- Ensuring the pytest host has L3 reachability to each management IP.
- Providing valid eAPI credentials via environment variables (never commit secrets).
All tunable defaults for Layer 2 tests live in tests/integration/containerlab/constants.py. Edit that file to match your environment; do not scatter changes across lab_project.py or test modules.
| Constant | Purpose |
|---|---|
NORNFLOW_ARISTA_VERSION |
PyPI pin for nornflow-arista installed in the lab venv |
NORNFLOW_ARISTA_PACKAGE |
Package namespace used in catalog assertions |
LAB_HOSTS |
Hostnames, management IPs (or DNS), and Nornir groups |
LAB_EAPI_TRANSPORT / LAB_EAPI_PORT |
eAPI settings written into generated inventory |
LAB_INTEGRATION_WORKFLOW / LAB_READONLY_BLUEPRINT |
Generated asset filenames |
LAB_PREFLIGHT_HOST |
Which LAB_HOSTS key README preflight examples use |
Example: point tests at your management subnet and bump the package pin:
NORNFLOW_ARISTA_VERSION = "0.1.0"
LAB_HOSTS: dict[str, LabHostSpec] = {
"spine1": {"hostname": "10.0.0.11", "groups": ["eos", "spines"]},
"spine2": {"hostname": "10.0.0.12", "groups": ["eos", "spines"]},
"leaf1": {"hostname": "10.0.0.13", "groups": ["eos", "leafs"]},
"leaf2": {"hostname": "10.0.0.14", "groups": ["eos", "leafs"]},
}To point tests at your lab:
- Edit
hostnamevalues inLAB_HOSTSto match your management IPs or DNS names. - Optionally rename keys (e.g.
spine1→my-spine-a). Workflows usehost.namefrom Nornir, so renames are fine as long as all four roles exist and eAPI works. UpdateLAB_PREFLIGHT_HOSTif you rename the default preflight leaf. - Adjust
groupsonly if you change inventory structure; the generated project expectseos,spines, andleafs. - Keep
tests/integration/containerlab/inventory/hosts.yaml.examplein sync for manual preflight reference.
Tests do not read hosts.yaml.example at runtime.
export NORNFLOW_LAB=1
export NORNFLOW_LAB_USER=your_user
export NORNFLOW_LAB_PASSWORD=your_password| Variable | Required | Purpose |
|---|---|---|
NORNFLOW_LAB |
Yes | Must be 1 to run Layer 2 tests |
NORNFLOW_LAB_USER |
Yes when NORNFLOW_LAB=1 |
eAPI username (injected into generated inventory) |
NORNFLOW_LAB_PASSWORD |
Yes when NORNFLOW_LAB=1 |
eAPI password (never commit) |
If NORNFLOW_LAB is unset or not 1, containerlab tests skip (they do not fail).
If NORNFLOW_LAB=1 but credentials are missing, tests fail immediately.
pytest tests/integration/containerlab \
--override-ini addopts= \
-m containerlab \
-s \
-v-s disables pytest output capture so NornFlow CLI output (workflow overview, per-host task results) prints live.
--override-ini addopts= clears the default -m 'not containerlab' filter so the lab tests are collected.
The Layer 2 suite runs in phases. Each phase builds on the previous one:
| Phase | What | Device I/O |
|---|---|---|
| A | Provision isolated venv + temp NornFlow project | No |
| B | Catalog load + nornflow show smoke tests |
No |
| C | nornflow run workflows against live cEOS |
Yes |
| D,E... | Not implemented (see Out of scope) | — |
Phase A: provision (session fixture, once per pytest run):
- Creates a temp directory with its own virtualenv.
- Installs editable nornflow from your checkout and pinned
nornflow-aristafrom PyPI (version fromconstants.py). - Runs
nornflow initin the temp project, merges static fixture assets fromtests/integration/fixtures/common/andtests/integration/containerlab/fixtures/overlay/, patchesnornflow.yaml(e.g.packages), and writes hosts.yaml with credentials from env vars.
Review workflows, blueprints, vars, and j2 filters in those fixture directories. They are copied verbatim into the temp project (not built from Python strings).
Implemented in provision_lab_environment() (lab_project.py), triggered by the lab_environment session fixture.
Phase B: catalog smoke tests (no device I/O):
- In-process catalog validation via
lab_runner.py phase-b(test_catalog_and_package.py). nornflow showCLI checks for tasks, filters, workflows, blueprints, j2-filters, and hooks (test_nornflow_show.py).nornflow validatestatic workflow checks — success and failure fixtures (test_validate.py).nornflow run vars_all_levels.yaml: env, global, domain, workflow, CLI, and runtime vars plus j2 filters (test_vars_all_levels.py, echo-only; file underworkflows/lab/for domain scoping).
Phase C: live workflow runs (requires reachable lab):
nornflow run lab_integration.yaml: workflow vars, package j2 filters, hooks (if,single,store_as), local blueprint, read-onlyget_facts(test_readonly_getters.py).nornflow run lab_store_as_failure.yaml: failure-pathstore_as+ conditional echo (test_store_as_failure_path.py).
Teardown: deletes the temp tree when the session finishes (pass or fail).
Before pytest, confirm reachability and eAPI from the same machine that will run the tests. Replace the IP with one of your leaf management addresses (default: leaf1):
ping -c 2 172.29.163.103
curl -s -u "$NORNFLOW_LAB_USER:$NORNFLOW_LAB_PASSWORD" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"runCmds","id":1,"params":{"version":1,"format":"json","cmds":["show version"]}}' \
http://172.29.163.103/command-api | headReference inventory (no secrets): tests/integration/containerlab/inventory/hosts.yaml.example.
NORNFLOW_LABis not1: expected for normalpytest/ CI.- You ran without
--override-ini addopts=and-m containerlab: default pytest excludes thecontainerlabmarker.
uv venv/uv pip installerrors: check network access to PyPI and thatuvis on your PATH.- PyPI or version pin issue installing
nornflow-aristaat the version inconstants.py. - Failure writing the temp project under the session temp directory (permissions, disk space).
- Package load or catalog resolution error: run with
-vand inspect stderr from the lab venv subprocess. - Missing expected catalog keys in
test_catalog_and_package.pyortest_nornflow_show.py: often a breaking change in builtin ornornflow-aristaasset names.
- No route to the lab management subnet from the pytest host: fix routing, VPN, or port forwarding for your environment.
- Lab not running or nodes still booting: confirm containers/VMs are up and eAPI is enabled.
- Wrong credentials: verify with the curl preflight above.
- IP mismatch: if your lab uses different addresses, update
LAB_HOSTSinconstants.py(see Customizing the lab).
- Deploying or destroying Containerlab (or any lab) inside pytest
- Public CI with cEOS images (licensing)
- Workflow dry-run tests (Phase D)
- Live config mutation tests (Phase E)
- Nautobot / dynamic inventory
- SSH / Netmiko checks
Values below match LAB_HOSTS in tests/integration/containerlab/constants.py.
| Host | Mgmt IP | Role |
|---|---|---|
| spine1 | 172.29.163.101 | Spine |
| spine2 | 172.29.163.102 | Spine |
| leaf1 | 172.29.163.103 | Leaf |
| leaf2 | 172.29.163.104 | Leaf |
eAPI: http://<ip>/command-api (port 80, not HTTPS).