nac-artifacts
A Python library to discover and layer the schema, rules and test artifacts
packaged with Network-as-Code modules and bundles. It is shared by
nac-validate and
nac-test.
Status: early development; the API may still change.
Overview
Schema, validation rules and test templates can come from three places. They are layered, highest priority first:
- Local: files and paths configured in the project itself
- Bundle: an archive (or directory) dropped into the project, e.g. a set of rules and tests
- Module: the
nac/directory shipped inside the installed Terraform module, however the module was sourced (registry, git, archive, local path)
nac-artifacts resolves the bundle and module layers. The tools decide how to
merge what they find: for example, nac-validate replaces rules by rule ID and
nac-test replaces templates by relative path.
Artifact layout
Modules and bundles use the same layout below their nac/ directory:
nac/
├── schema.yaml # schema for nac-validate
├── rules/ # semantic validation rules for nac-validate
└── tests/
├── templates/ # test templates for nac-test
├── jinja_filters/ # Jinja filters for nac-test
└── jinja_tests/ # Jinja tests for nac-test (optional)
Bundles
A bundle is a .zip or .tar.gz archive (or an already extracted directory)
with a manifest.yaml at its root next to the nac/ directory. An archive
made by zipping a single folder is accepted as well.
# manifest.yaml
name: acme-base # required; identifies the bundle in messages, independent of the file name
version: "1.2.0" # must be a string: quote it, an unquoted 1.10 is read as 1.1
module: # optional
versions: ">=0.3,<0.4" # PEP 440 specifier
- Compatibility: if
module.versionsis set, it is checked against the one installed module that has anac/directory, whichever artifacts the caller asked for. A mismatch raisesBundleError. The check is skipped, with a log message, when no module is installed, several are, or the module's version is unknown (local and git sources have none). - Location: any
.zip,.tar.gzor.tgzfile directly in.nac/, or an extracted.nac/bundle/directory. The file name is free (it can carry a version or architecture, such asacme-nxos-1.2.0.zip) and is never parsed: identity comes from the manifestname. - Several bundles: all bundles in
.nac/(or all passed explicitly) are peers in one priority tier, above the module. They may coexist as long as they provide different artifacts, which allows splitting content (for example rules in one bundle and tests in another). If two bundles provide the same artifact, the tool reports an error naming both. A bundle'soverrides.yamlaffects the module but not its peer bundles. - Same name twice: two bundles with the same manifest
name(typically an old and a new version left in.nac/) are an error, because a forgotten old version would otherwise keep providing artifacts the new one dropped or renamed. - Bundles for other tools: a bundle found in
.nac/that provides nothing the calling tool uses is skipped (logged at INFO). A bundle passed explicitly that provides nothing is an error. A bundle with anoverrides.yamlthat disables something always counts, even without any artifacts of its own, so a bundle can consist of nothing but a manifest and anoverrides.yaml. - Safety: archives are checked before extraction (no absolute or
..paths, no symlinks or special files, limits on archive size, extracted size and file count). Extraction is atomic and cached by the archive's sha256; cache contents are trusted without being verified again. Cache entries not used for 30 days (typically old bundle versions) are removed. - Trust: bundles usually contain code that the tools execute (validation rules, Jinja filters), so a bundle needs the same trust as code in your own repository.
Disabling artifacts of lower layers
A layer can add or replace artifacts of the layers below it. To remove some, list
them in an overrides.yaml:
disable:
rules: ["102"] # nac-validate: by rule ID (exact match)
templates: ["config/*.robot"] # nac-test: by path relative to the templates directory
The same format is read from two places:
.nac/overrides.yamlin the project: disables artifacts of the bundle and the moduleoverrides.yamlat the root of a bundle (next tomanifest.yaml): disables artifacts of the module
Rules:
- A layer's entries only affect layers below it. Its own artifacts and the local layer (files and paths you pass to the tools) are never affected; to get a disabled artifact back, add a copy to the local layer.
- Entries are declarative: they state what must not be active. An entry that matches
nothing (a typo, a module that no longer has the artifact, no bundle installed) is
not an error. The tools show which entries matched in
--list-artifacts. - The structure is validated: unknown keys, wrong types, empty entries and absolute or
..template paths are errors (OverridesError). Rule IDs may be written unquoted (102). - Template patterns are globs (
fnmatch;*also matches/, solib/*covers nested files) and are case-sensitive. - Bundle overrides can switch validations off, so treat a bundle like code you run. Each tool logs a summary at INFO level and shows what was disabled and by which layer.
Usage
from pathlib import Path
from nac_artifacts import resolve_artifact_layers
layers = resolve_artifact_layers(
Path.cwd(),
provides=("schema.yaml", "rules"), # paths below nac/ this tool needs
)
for layer in layers: # bundles (peers) first, then the module
print(layer.label, layer.version, layer.nac_dir)
# Entries to disable per layer (same order as layers), from .nac/overrides.yaml and bundles
from nac_artifacts import DisableMatcher, layer_disables
for layer, disabled in zip(layers, layer_disables(Path.cwd(), layers)):
matcher = DisableMatcher(disabled.rules) # exact; use glob=True for templates
if matcher.match("102"):
... # rule 102 is disabled for this layer
print(matcher.unmatched()) # entries that matched nothing, for reporting
- A layer is used only if it has at least one of the
providespaths (or, for a bundle, anoverrides.yamlthat disables something), so each tool picks the layers relevant to it. A layer may therefore lack some or all of the paths.providesmust be a non-empty sequence of paths relative tonac/(a plain string is rejected). - Modules are found through the
modules.jsonthatterraform initandtofu initwrite (honouringTF_DATA_DIR). Only the root module and the modules it calls directly are considered. If several provide the requested paths,AmbiguousModuleErrorlists them; passmodule_dirto select one. - Explicit
module_dirandbundlespaths are returned as absolute paths. Relative ones are resolved against the current working directory (as for command line arguments), not againstproject_dir. - Bundles are found in
.nac/, or passbundles.Noneand an empty sequence both mean auto-detection, so forwarding an empty list of command line options keeps the default. Archives are extracted once into.nac/cache/, which ignores itself in git. - Failures raise
ArtifactErrorsubclasses (ModuleDiscoveryError,AmbiguousModuleError,BundleError).
nac_artifacts.testing provides builders for tests of tools that use this
library: installed modules with a modules.json, and bundles as zip or tar.gz.
Development
uv sync --extra dev
uv run pytest
uv run pre-commit run --all-files
License
MPL-2.0
Metadata
Release files for nac-artifacts 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nac_artifacts-0.1.0.tar.gz | 44.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nac_artifacts-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 73.6 kB
Release files / nac_artifacts-0.1.0.tar.gz
| Download URL | nac_artifacts-0.1.0.tar.gz |
|---|---|
| Size | 44.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ccf843a36d878fcc4088e9bf30ffdc63a6c4fe9b24f1354dff4c48236873b9bb
|
|
BLAKE2b-256 checksum How to use checksums |
371b971498e079007d11c9e3aa296e50b0c4de37d71ec570f538b7bfc0ab6b31
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / nac_artifacts-0.1.0-py3-none-any.whl
| Download URL | nac_artifacts-0.1.0-py3-none-any.whl |
|---|---|
| Size | 29.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ff56ca318e118683992f393c211aaf55369d056245c147440882ff00832134a1
|
|
BLAKE2b-256 checksum How to use checksums |
2d7e4e6db179b68ae185b7611e7863e8cb8cd7529091e119c91233ef57f8c84d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|