Skip to content

Make targets

The Makefile is the operator contract. Everything below exists in it today.

Scenarios

The lab holds more than one network. SCENARIO selects which one every target below acts on — see Scenarios for the mechanism.

Target What it does
make scenarios Lists every scenario from its scenario.json: name, device count, supported flavours, title and summary.
make scenario-resolve Materialises scenarios/_base + scenarios/$(SCENARIO) into generated/$(SCENARIO)/scenario/, the one flat directory the containers mount and the scripts read. A prerequisite of local-env-init, local-env-up, local-lab-up, local-lab-discover, apply-cms-config, kind-lab-up, kind-lab-discover and kind-lab-cms-config, so you rarely call it directly.
make generate scenario-resolve, then ./gen_clab_topology — rebuilds the containerlab topology and the committed scenarios/$(SCENARIO)/workflow-execution-parameters/*.json.
Variable Default Purpose
SCENARIO wan-and-fabric Which lab to act on. The Makefile holds the only default and exports it
SCENARIO_SRC scenarios/$(SCENARIO) The scenario’s source assets
SCENARIO_DIR generated/$(SCENARIO)/scenario The resolved overlay
GEN_DIR generated/$(SCENARIO) Where the generators write
make scenarios
make local-lab-up SCENARIO=frr-only

Every other consumer fails on an unset SCENARIO

The four generators, resolve_scenario, apply_cms_config and both compose files read SCENARIO and refuse to run without it. A second default is how a scenario gets applied half from one lab and half from another — invisible until discovery produces a confusing result. Drive docker compose through make, or set SCENARIO yourself.

Only one scenario runs at a time: they share lab-net, the 172.30.0.0/24 subnet and the compose project. The containerlab lab name stays constant across scenarios on purpose, so containerlab deploy --reconfigure replaces the running one.

Lab lifecycle

Target What it does
make local-lab-up Depends on build-docker, lab-jwt, lab-env and scenario-resolve. Generates the containerlab topology + device configs from the scenario’s topology.json, refreshes the worker image, starts the CMS and waits for it to be healthy, runs apply_cms_config, mints an engine token, brings up the base stack plus the worker and lab_bootstrap (creating lab-net), waits for workflow registration, ./containerlab deploy --reconfigures the scenario’s devices (re-runnable), mints a fresh token, waits for the worker’s function blocks, then waits for every device’s SSH. Refuses to run without cms_api_key.env.
make local-lab-discover Runs apply_cms_config, mints one engine token, waits for the discovery function block and for device SSH, then POSTs a workflow execution and polls to a terminal state (15-minute ceiling). Override DISCOVER_PARAMS to change targeting.
make local-lab-logs docker compose logs -f worker lab_bootstrap.
make local-lab-down ./containerlab destroy --cleanup (removes the devices and generated/$(SCENARIO)/clab/clab-neops-lab/), then docker compose down. Volumes survive.
make clab-suid For the CLAB_NATIVE (host binary) path: sets the SUID bit on the containerlab binary so the lab targets can deploy without sudo. Idempotent — it only prompts for a password when the bit is missing — and warns if you are not in clab_admins. Re-run after every containerlab upgrade. See Prerequisites.

The first four export COMPOSE_FILE=docker-compose.yml:docker-compose.worker.yml, so they see the worker overlay; clab-suid touches no containers, and the local-env-* targets do not get the overlay either.

Variables

Variable Default Purpose
DISCOVER_PARAMS $(SCENARIO_SRC)/workflow-execution-parameters/discover-params.json Which parameter file local-lab-discover sends
PROFILE operator Grant profile lab-grant applies: author, operator or admin
NEOPS_ENGINE_TOKEN (env) (minted per target) Engine access token; export one to reuse it across targets. local-lab-up mints a fresh one after the containerlab deploy
DISCOVER_FB fb.base.neops.io/global_discover_network:0.1.0 The function block wait_ready blocks on. Shared with the Kubernetes flavour
DISCOVER_WORKFLOW wf.lab.neops.io/simple_lab_discovery:1.2.0 The definition that is executed; must match the version in the workflow YAML. Shared with the Kubernetes flavour
CLAB_TOPO $(GEN_DIR)/clab/neops-lab.clab.json The generated containerlab topology
CONTAINERLAB ./containerlab The containerlab launcher
CLAB_IMAGE (env) ghcr.io/srl-labs/clab:0.78.2 containerlab version the launcher runs
WAIT_READY_TIMEOUT 180 Seconds wait_ready waits for an online worker
WAIT_DEVICES_TIMEOUT 240 Seconds wait_devices waits for device SSH

These are make/environment variables (make local-lab-up WAIT_READY_TIMEOUT=600); the Makefile does not read .env — only docker compose does.

make local-lab-discover \
  DISCOVER_PARAMS=scenarios/wan-and-fabric/workflow-execution-parameters/discover-params-autodetect.json

Kubernetes lab lifecycle

A separate flavour that runs the FRR devices as pods in a local KIND cluster. It needs neither containerlab nor the compose stack — see Kubernetes lab for the full picture.

Target What it does
make kind-lab-up Depends on build-docker-frr and scenario-resolve. kind loads the image onto the cluster nodes, generates generated/$(SCENARIO)/kind/lab.yaml from the scenario’s topology.json, applies it, waits for every Deployment to become Available, then prints each device’s in-cluster DNS name. Renders vendor: "frr" devices only, so the scenario must declare the kind flavour.
make kind-lab-status kubectl get pods,svc in the lab namespace.
make kind-lab-down Deletes the namespace. The cluster itself is never touched.
make kind-lab-cms-config Runs apply_cms_config against a Kubernetes-deployed CMS: grants the neops user the lab-admin role and seeds the Global scope, without which the web client shows no scope at all. Idempotent.
make kind-lab-discover Publishes the workflow definition with bootstrap/register.py, generates the per-device targets with ./gen_kind_discover_params, waits for the discovery function block with ./wait_ready, then executes it with ./run_workflow. Depends on build-docker-bootstrap, and on the engine being reachable at KIND_ENGINE_URL.

kind-lab-cms-config opens a short-lived kubectl port-forward to the CMS and reads the API key from a cluster Secret; it touches nothing else in the product namespace. Getting the devices into the CMS is discovery’s job, and kind-lab-discover does it by driving the same workflow, the same function block and the same four scripts the containerlab flavour uses — it needs a worker image that actually carries the base function blocks. See Kubernetes lab.

Variable Default Purpose
KIND_CLUSTER kind Cluster the image is loaded into
KIND_LAB_NAMESPACE neops-lab Namespace the devices live in
KIND_LAB_TIMEOUT 180 Seconds kubectl wait allows for the pods
KIND_MANIFEST $(GEN_DIR)/kind/lab.yaml The generated manifest
KIND_ENGINE_URL http://engine.neops.local How the host and the bootstrap container reach the workflow engine. The offline ingress name, so run make -C neops-helm config-hosts once
KIND_DISCOVER_PARAMS $(GEN_DIR)/kind/discover-params.json The generated discovery parameters
KIND_DISCOVER_TIMEOUT 600 Seconds run_workflow polls for a terminal state
NEOPS_NAMESPACE neops Namespace the product stack runs in
NEOPS_CMS_DEPLOYMENT neops-neops-core Deployment apply_cms_config runs manage.py in
NEOPS_CMS_SERVICE / NEOPS_CMS_PORT neops-neops-core / 8000 What the port-forward targets
NEOPS_CMS_TOKEN_SECRET neops-neops-workflow-engine Secret the CMS API key is read from
CMS_PORT 18000 Local end of the port-forward
CMS_EXEC the kubectl exec form How apply_cms_config runs manage.py; the script’s own default is the compose lab

Environment lifecycle

Target What it does
make doctor Host preflight (./doctor): docker + RAM, amd64 emulation on Apple Silicon, mount round-trip, subnet overlaps, the clab image, python3. Read-only apart from pulling two images; the fix is printed per failure.
make lab-jwt Mints cms/jwt/{private,public}.pem with openssl genpkey if absent. Idempotent. A prerequisite of local-env-init, local-env-up and local-lab-up.
make lab-env Copies .env.example to .env if there is no .env yet, so a fresh clone has one to edit. Never overwrites an existing file. Every value in the example is commented out, so it changes no behaviour on its own. A prerequisite of local-lab-up.
make local-env-init One-time per environment: pull (--policy always) + start the base stack, resolve the neops user and mint the CMS API key into cms_api_key.env (fails on an empty key), run apply_cms_config, then force-recreate the engine so it picks up the token.
make local-env-up Start the base stack again later. Fails with a clear message if cms_api_key.env is missing.
make local-env-down docker compose down — stops the base stack, keeps the volumes.
make local-env-prune docker compose down -v — the true reset; drops the Elasticsearch and Postgres volumes.
make apply-cms-config Runs ./apply_cms_config on its own. Idempotent, and worth re-running after a CMS restart (see below).
make lab-grant make lab-grant ROLE=<role> [PROFILE=operator] — applies one workflow grant profile to one role, the same manage.py grant_workflow_permissions call apply_cms_config makes per role in the scenario’s cms/permissions.json. Grants only widen — see Authorization.

Re-run apply-cms-config after a CMS restart

The CMS image seeds a scope named Global on every startup with always_update_on_restart=True, so a restart resets its columns and filters to the image defaults. dashboard_configuration is not reset — init_scopes never writes it. Re-run make apply-cms-config to reapply the lab’s columns and drill-down.

What apply_cms_config does

It grants the neops user a full-permission role (lab-admin, default_permission=7) and applies the scenario’s cms/permissions.json — every declared role gets default_permission=7 and a RoleScope on Global, every declared user gets its roles and a password equal to its username, and each role’s workflow grant profile is applied through the CMS’s own manage.py grant_workflow_permissions. See Authorization.

It also configures the Global scope from the JSON files under generated/$(SCENARIO)/scenario/scope/Global/ — table columns for devices, interfaces, clients and groups, a location drill-down schema, and a dashboard configuration. Those files, and cms/permissions.json, come from scenarios/_base/ unless the scenario overrides one; edit them there, not in generated/.

It is bash, not Python, and it uses manage.py shell rather than GraphQL for the seeding steps: roleUpsert / scopesUpsert / roleScopeUpsert all gate on permissions the freshly-bootstrapped neops user does not yet have. Table columns and drill-down are written through GraphQL, deliberately last, because the CMS’s init_scopes runs on every manage.py invocation and would otherwise overwrite a shell write.

If you reorder anything in that script, read its header comment first.

Images

Target What it does
make build-docker Builds both local-only images — neops-lab-frr:latest and neops-lab-bootstrap:latest. Nothing is pushed. A prerequisite of local-lab-up.
make build-docker-frr Just neops-lab-frr:latest from devices/frr/. A prerequisite of kind-lab-up, which needs no bootstrap image.
make build-docker-bootstrap Just neops-lab-bootstrap:latest from bootstrap/.

DOCKER_BUILD_FLAGS is passed to every docker build; set it to --network=host on a host whose docker bridge cannot resolve DNS. See Images.

Quality gates

Target What it runs
make lint ruff format --check . then ruff check .
make format ruff format . then ruff check --fix .
make typeCheck pyrefly check
make test pytest tests — the generator unit tests (parametrised over every scenario), the scenario-manifest checks, and the repo invariants (subnets, the one-SCENARIO-default rule)
make py39-check Imports the host scripts under Python 3.9 (the stock macOS python3)
make shell-syntax bash -n over apply_cms_config, containerlab, doctor
make check lint typeCheck test py39-check shell-syntax — the one to run before pushing

Documentation

Contributed by the vendored mkdocs-documentation tooling in .make_scripts/mkdocs-documentation/:

Target What it does
make doc-serve Live-preview server; regenerates mkdocs.yml when mkdocs_custom.yml or mkdocs_base.yml change
make doc-build Regenerate mkdocs.yml, mkdocs build, then clean the site dir
make doc-build-docker / make doc-run-docker Build and run the docs as an NGINX image
make doc-create-mkdocs Regenerate mkdocs.yml and refresh the docs CI workflow
make doc-update-assets Pull the latest tooling release into .make_scripts/

mkdocs.yml is autogenerated — never edit it

It is produced by setup_documentation.py from this repo’s mkdocs_custom.yml deep-merged over the shared mkdocs_base.yml. Edit mkdocs_custom.yml. Note that dicts merge but lists concatenate, so re-declaring hooks: or plugins: entries that the base already has registers them twice.

Release and infrastructure

Contributed by the vendored release-management and project-infrastructure tooling:

Target What it does
make tag-patch / tag-minor / tag-major (+ -beta) Create a local annotated SemVer tag
make tag-latest-beta, make check-for-releases, make hard-reset-tags, make tag-major-minor-ruleset Tag helpers
make sync-release-assets, make sync-infrastructure-assets Re-vendor the tooling scripts (destructive by design — they rm -rf the vendored dir first)
make github-set-branch-protections, make github-set-default-branch, make github-autodelete-merged-branches Repository settings

Tagging this repo publishes nothing

neops-lab is not a distributable package — no wheel, no npm package, no registry image. make tag-* creates a local annotated tag; the separate git push --tags is what triggers downstream pipelines in repos that have them. This one has none.

Full reset

make local-lab-down local-env-prune
make local-env-init && make local-lab-up && make local-lab-discover