An umbrella CLI for Network-as-Code workflows, driven by a single
nac.yaml config file.
Network-as-Code repos run the same workflow regardless of architecture:
validate the data, plan, apply, test. nac is one CLI for all of it. Instead
of juggling terraform/tofu, nac-validate and nac-test with their own
flags, you describe the repo once in nac.yaml and run nac validate,
nac plan, nac apply, nac test. The same commands work on a laptop and
in CI.
Install nac itself and there's nothing else to set up: it resolves and
runs the rest of the toolchain for you (via uvx, falling back to a local
install if one's already on PATH), so there's no separate
nac-validate/nac-test install or version-pinning to manage yourself.
The package is published on PyPI as nac-cli; the command it installs is
nac. Install uv
first if you don't have it, then:
uv tool install nac-cliUpgrade or uninstall with:
uv tool upgrade nac-cli
uv tool uninstall nac-cliPin a specific version with uv tool install nac-cli==0.2.0.
Warning
Don't pip install nac or run uvx nac -- that is a different, unrelated
package.
uv— always required;nacuses it to runnac-validate/nac-testviauvx, so you never install those two yourself.terraformortofuonPATH— if neither is installed,nac setupcan bootstrap-install OpenTofu for you (see Tool/version management).tenv— only needed if you pintools.terraform.versioninnac.yaml.
Run nac setup to verify all of the above (plus any env.required
variables from your config) before running anything else.
A nac.yaml at the repo root is optional — every field defaults, so an
empty or missing file is perfectly valid for a repo that sticks to the
data//tests/ conventions and needs no env vars checked. The most
common lean config just declares the environment variables the wrapped tools
expect, so nac setup has something to check:
env:
required: [ACI_URL, ACI_USERNAME, ACI_PASSWORD]Then, from the repo root:
nac setup # verify prerequisites, report resolved tool versions
nac init # terraform/tofu init
nac validate # nac-validate against the configured data paths
nac plan # terraform/tofu plan -> plan.tfplan
nac apply # apply the saved plan (or interactively, without one)
nac test # nac-test against the configured data/templates/output
nac destroy # destroy the managed infrastructure (interactive unless --auto-approve)Every field not shown above falls back to a sensible default — see
nac.yaml reference below for the full set of
defaults and a worked example for repos that render a model.yaml
before validating (e.g. NX-OS).
Global options, available on every subcommand:
| Option | Env var | Default | Description |
|---|---|---|---|
--config |
NAC_CONFIG |
./nac.yaml |
Path to the config file. |
-v, --verbosity |
NAC_VERBOSITY |
WARNING |
DEBUG|INFO|WARNING|ERROR|CRITICAL. |
--no-color |
NO_COLOR |
off | Disable colored output. |
Every subcommand except setup forwards extra arguments to the wrapped tool
(e.g. nac plan -var-file=prod.tfvars), and --help shows nac's help
followed by the wrapped tool's own options. The wrapped tool's exit code is
returned. --artifacts, available on the same subcommands, also writes the
command's output to <command>.txt in working_dir (e.g. validate.txt).
Verifies prerequisites and reports resolved tool versions — a pre-flight for
CI and local use. Checks uv is on PATH; checks tenv only if
tools.terraform.version is set; resolves the terraform/tofu binary
(explicit tools.terraform.engine, or auto-detected: tofu then
terraform); checks every env.required variable is set (never prints
values); reports resolved terraform/tofu, nac-validate, and nac-test
versions, and warns if nac-validate/nac-test are outside the module's
tested versions. Fails fast with a non-zero exit on any
missing prerequisite (tested-versions warnings never fail it).
| Flag | Description |
|---|---|
--yes, --install |
Skip the confirmation prompt for the OpenTofu bootstrap install. |
--prewarm |
Pre-warm the uvx cache for nac-validate/nac-test — skipped for a tool that already resolves to a local install. |
Streams terraform init (or tofu init) in working_dir.
| Flag | Description |
|---|---|
--artifacts |
Also write the output to init.txt. |
If render.target is set, first streams a targeted
terraform apply -target=<target> -auto-approve to produce render.output,
then streams nac-validate against the resolved data paths (render.output
if a render just happened, else data.paths). The render step is fully
implied by config, never a separate opt-in.
| Flag | Description |
|---|---|
--artifacts |
Also write the output (render step included) to validate.txt. |
Streams terraform plan -out=plan.tfplan.
| Flag | Description |
|---|---|
--artifacts |
Also stream terraform show twice to emit plan.txt and plan.json alongside the saved plan. |
If a saved plan.tfplan exists in working_dir, streams
terraform apply plan.tfplan (and deletes the plan file after a successful
apply); otherwise runs terraform apply interactively (stdin is inherited,
so the normal confirmation prompt works).
| Flag | Description |
|---|---|
--auto-approve |
Pass -auto-approve when applying without a saved plan file. |
--artifacts |
Also write the output to apply.txt. |
Streams terraform destroy (interactive unless --auto-approve, matching
terraform destroy's own default confirmation prompt).
| Flag | Description |
|---|---|
--auto-approve |
Pass -auto-approve when destroying. |
--artifacts |
Also write the output to destroy.txt. |
Streams nac-test against the resolved data paths (render.output if
render is configured, else data.paths, plus data.defaults whenever
it's configured) and the templates/filters/output from the test:
config block.
| Flag | Description |
|---|---|
--artifacts |
Also write the output to test.txt. |
A single nac.yaml at the repo root (path overridable with --config)
drives every subcommand. Every field is optional, and so is the file itself
— a missing nac.yaml is treated the same as an empty one, with every
field falling back to its default. That's a legitimate config for a repo
that needs no env.required checks and follows the data//tests/
conventions, not just a fallback for ad hoc smoke-testing.
Data resolution rules (no boolean toggles anywhere — both follow
automatically from whether render/render.target/data.defaults are
configured):
- validate data =
render.outputifrender.targetis set, elsedata.paths. - test data = (
render.outputifrenderis configured at all, elsedata.paths) +data.defaults(appended whenever it's configured).
Field reference:
| Field | Default | Notes |
|---|---|---|
working_dir |
. |
Directory terraform/tofu runs in. |
data.paths |
[data/] |
Raw data source(s) for validate/test when no render applies. |
data.defaults |
defaults.yaml if it exists in working_dir, else unset |
Appended to test data whenever configured. |
render.target |
unset | Terraform target for a pre-validation render, e.g. module.nxos.local_sensitive_file.model. |
render.output |
unset | Rendered file path, e.g. model.yaml; required if render.target is set. |
validate.schema |
unset → flag omitted | Passed to nac-validate -s; omitted lets nac-validate use its own default. |
validate.rules |
unset → flag omitted | Passed to nac-validate -r; omitted lets nac-validate use its own default. |
test.templates |
tests/templates if that directory exists in working_dir, else unset → flag omitted |
Passed to nac-test -t; if omitted, nac-test uses templates provided by a module or bundle. |
test.filters |
tests/filters if that directory exists in working_dir, else unset |
Passed to nac-test -f. |
test.output |
tests/results |
Passed to nac-test -o. |
tools.terraform.engine |
auto-detected: tofu if on PATH, else terraform |
Explicit value always wins over detection. |
tools.terraform.version |
unset | Only set if you need tenv to pin/install a specific version. |
tools.nac_validate |
unset (latest compatible with the module's tested version; without a manifest: any local install, else latest via uvx) |
Version/spec a local install must satisfy, else passed to uvx --from nac-validate<spec>; e.g. 1.2.0 or >=0.9,<1.0. |
tools.nac_test |
unset (latest compatible with the module's tested version; without a manifest: any local install, else latest via uvx) |
Version/spec a local install must satisfy, else passed to uvx --from nac-test<spec>; e.g. 2.0.0 or >=2.0,<3.0. |
env.required |
[] |
Variable names nac setup checks are present (values are never printed). |
Worked examples. Lean NX-OS (render + validate + test all use the
rendered model.yaml):
tools:
terraform:
engine: tofu
render:
target: module.nxos.local_sensitive_file.model
output: model.yaml
env:
required: [NXOS_USERNAME, NXOS_PASSWORD]Lean ACI (no render at all; test appends data.defaults since
defaults.yaml exists at the repo root):
env:
required: [ACI_URL, ACI_USERNAME, ACI_PASSWORD]Fully-expanded NX-OS config, with every field made explicit — useful when a repo genuinely deviates from convention:
working_dir: .
data:
paths: [data/]
defaults: null
render:
target: module.nxos.local_sensitive_file.model
output: model.yaml
validate:
schema: .schema.yaml
rules: [.rules]
test:
templates: tests/templates
filters: tests/filters
output: tests/results
tools:
terraform:
engine: tofu
version: "1.9.5"
nac_validate: ">=0.9,<1.0"
nac_test: ">=2.0,<3.0"
env:
required: [NXOS_USERNAME, NXOS_PASSWORD]terraform/tofu: with notools.terraform.versionset,nacresolves the binary directly viaPATH—tofufirst,terraformas fallback (or whichevertools.terraform.enginenames explicitly). If neither is found,nac setup(interactively, or with--yes/--install) offers to download and cache a verified OpenTofu binary itself — no package manager ortenvrequired. Settingtools.terraform.versionswitches totenvfor version pinning/auto-install (TOFUENV_TOFU_VERSION/TFENV_TERRAFORM_VERSION+ the matching auto-install env var).nac-validate/nac-test: prefers a local install already onPATHif it satisfiestools.nac_validate/tools.nac_test(unset means any local version is acceptable) — same PATH-first philosophy asterraform/tofuabove. Otherwise falls back touvx(uv tool run) --uvx --from nac-validate==<version> nac-validate .... Omittools.nac_validate/tools.nac_testto letuvxresolve the latest published release when no usable local install exists -- or, if the module ships a manifest, the latest release compatible with the tested one.nac setupreports which source (localoruvx) each tool actually resolved to.
A Network-as-Code Terraform module can ship a nac/manifest.yaml --
alongside its other nac-specific content (schema, rules, test templates) --
listing the nac-validate/nac-test versions it was tested with:
schema: 1
tools:
nac-validate: "2.0.0"
nac-test: "2.0.0"terraform/tofu and provider versions don't belong here: the module's
versions.tf (required_version, required_providers) already constrains
them, and Terraform enforces that itself.
After nac init has downloaded the module, nac finds the manifest via
.terraform/modules/modules.json (the shallowest module that ships one,
so a local wrapper around the module works too). It is purely a hint --
nothing is ever enforced, and without a manifest (e.g. before the first
init) nothing changes:
- Defaults: with
tools.nac_validate/tools.nac_testunset,nac-validate/nac-testresolve to the latest release compatible with the tested version instead of the latest overall -- a local install is used if it's in that range, otherwiseuvxfetches it. - Warnings: an explicit
tools.nac_validate/tools.nac_testvalue always wins, but if it resolves to a version outside the tested, semver-compatible range,nac validate/nac test(andnac setup) print a warning (to stderr, and into the--artifactsfile), plus a short note after the output if the command then fails. Exit codes are never changed. A range resolved throughuvx(e.g.>=1.0) isn't checked, since the versionuvxpicks isn't known up front; an exact pin or a local install is.
"Compatible" means the tested version or newer, up to the next major --
or the next minor for 0.x versions, where a minor bump may be breaking.
A tested 2.0.0 allows >=2.0.0,<3, a tested 0.13.1 allows
>=0.13.1,<0.14. Newer-but-compatible versions are only logged at INFO.