The AVD project for the NTC + Arista "Modern Network Lifecycle" demo. It follows the layout of Arista's AVD 0-to-hero reference repo with one site, DC1, and one difference that matters:
sites/is generated by Nautobot. The DC1 Fabric design job renders inventory, group_vars, tenants, connected endpoints, and the clab topology, then opens a pull request here. Do not edit those files by hand; change Nautobot and rerun the design.config_contexts/is hand-edited. Nautobot syncs these two files as config contexts (AVD Fabric Settings,AVD Management Settings) and the design writes them back out assites/_global_vars/fabric_settings.ymlandmanagement.yml. Edit here, runinvoke sync-avd-repoin the workshop repo, rerun the design, review the PR. Nautobot stores the context as JSON, so the renderedsites/_global_vars/*.ymlfiles come back with keys in Nautobot's order, not the order in these files; the content is identical.
Open the repo in the devcontainer (AVD universal image) or run:
docker run --rm -it -v "$PWD":/workspace -w /workspace \
ghcr.io/aristanetworks/avd/universal:python3.13-avd-v6.3.0 make dc1-build
make help lists the targets. deploy and validate targets read CVAAS_SERVER, CVAAS_TOKEN,
ANTA_USERNAME, and ANTA_PASSWORD from the environment; none of those are committed. The
cvpadmin sha512 hash and the BGP peer password are lab values copied from Arista's public
reference repo, and they are committed here in the clear: the hash appears in
config_contexts/dc1_management.yml and in digital_twin/clab/init-configs/basic.cfg, and the
BGP peer password in config_contexts/dc1_fabric_settings.yml. Both are also carried into the
generated files under sites/. Replace them before pointing this repo at anything real.
Four GitHub workflows in .github/workflows/ carry a change from a branch to production. All four
run on the self-hosted runner labelled self-hosted, clab, in the AVD universal container
(ghcr.io/aristanetworks/avd/universal:python3.13-avd-v6.3.0, --user=root --network host). The
three event-driven workflows (feature_branches.yml, PR_test_digital_twin.yml,
main_branch.yml) are gated on a dorny/paths-filter check of sites/DC1/group_vars/** and
sites/_global_vars/** so unrelated commits do not touch the fabric. cvp_post_CC_validation.yml
has no such filter: a change control dispatch is an explicit request to validate now.
| Workflow | Trigger | What it does |
|---|---|---|
feature_branches.yml |
push to any branch but main |
make dc1-build, then commits the regenerated intended configs, documentation, twin configs, and amplification report back to the branch. |
PR_test_digital_twin.yml |
pull_request (opened, synchronize, reopened) |
Job deploy-digital-twin builds, finds the running twin, and pushes the twin configs over eAPI. Job Network-validation-digital-twin runs ANTA against the twin, commits the report, posts it as a sticky pull request comment, and fails the check if validation failed. The second job runs only if the deploy succeeded. |
main_branch.yml |
push to main |
Job deploy-prod deploys DC1 through CloudVision as a Service with cv_submit_workspace=true cv_run_change_control=true. Job Network-validation then runs ANTA against production and commits the report. The second job runs only if the deploy succeeded. |
cvp_post_CC_validation.yml |
repository_dispatch, type validation_trigger |
Runs the production ANTA validation on demand and commits the report. Post this dispatch when a CloudVision change control finishes. |
Workflow-generated commits are authored as github-actions[bot] and carry [skip ci], so a commit
back never retriggers the pipeline.
Four Actions secrets and one Actions variable:
| Secret | Used by |
|---|---|
CLAB_JWT_SECRET |
PR_test_digital_twin.yml, to mint a token for the containerlab API server |
CVAAS_SERVER, CVAAS_TOKEN |
main_branch.yml, read by playbooks/deploy.yml |
ANTA_PASSWORD |
main_branch.yml and cvp_post_CC_validation.yml, read by playbooks/validate.yml |
| Variable | Used by |
|---|---|
ANTA_USERNAME |
the same two workflows, as `${{ vars.ANTA_USERNAME |
ANTA_USERNAME is a variable and not a secret on purpose. It is a username, and its value is the
four-letter string anta, which as a secret GitHub would mask everywhere it appears in any log in
this repository. The workflows fall back to anta when the variable is unset, so the pipeline runs
with the variable missing.
The digital twin needs no credentials of its own: containerlab boots cEOS with admin/admin and
twin_inventory.py writes those into the generated inventory.
digital_twin/clab/build_twin_lab.py does not push configuration at the running twin: cEOS-lab
gates most routing configuration behind platform capabilities that never settle reliably for
runtime sessions, while startup configuration is applied ungated at boot. So the deploy REDEPLOYS
the running twin lab in place through the containerlab API server, with each node's built
configuration embedded as an inline startup-config: same lab name, same node names, static twin
management addresses that reuse each node's production last octet, and a boot wait until every
node's eAPI answers.
Each configuration is first rendered through digital_twin/clab/twin_config_filter.py. The filter
drops the management-plane stanzas, plus daemon TerminAttr (the twin never has the CloudVision
onboarding token the agent needs, so on the twin it could only restart in a loop) and sflow
(cEOS-lab rejects it), and appends a tail that restores admin/admin, interface Management1
with the node's own twin address in the default VRF, and eAPI over https in the default VRF.
vrf instance MGMT is deliberately kept: mlag configuration's heartbeat peer address, the name
servers, and the NTP servers all reference it, and an MGMT VRF with no interface in it cannot carry
a session. Everything the pull request is validating - BGP, VLANs, VRFs, Ethernet interfaces, MLAG,
route maps - passes through byte for byte, which the tests in
digital_twin/clab/tests/test_twin_config_filter.py assert. Filtering is idempotent: every stanza
the tail adds is a stanza the filter removes.
One consequence for reading twin ANTA results: the catalogs are generated from the production intended configs, so any check that asserts production's management addressing (Management1's address, the MGMT VRF default route) is an expected delta on the twin, not a regression. Read those rows against production, not against the twin.
PR_test_digital_twin.yml does not create the twin, it finds one. A twin lab for DC1 must already
be running on the containerlab host before the pull request is opened or updated; in the demo,
Nautobot's "Create & Deploy Digital Twin" job for DC1 starts it.
digital_twin/clab/twin_inventory.py asks the containerlab API server (http://localhost:8080,
reachable because the job container uses host networking on the runner's droplet) which labs are
up, ignores the production lab dc1, and writes twin_inventory.yml for the rest. If production
is the only lab running, the script exits 2 with a message saying which Nautobot job to launch, and
the pull request validation fails immediately rather than testing nothing.
Pull requests from forks are not validated. A pull_request run from a fork gets no secrets and a
read-only token, so it could neither reach the twin nor commit the report; both jobs in
PR_test_digital_twin.yml skip unless the head branch lives in this repository. pull_request_target
is deliberately not used anywhere.
main is the demo's starting point and is tagged baseline. invoke reset-demo in the workshop
repo closes every nautobot/* pull request, deletes those branches, moves main back to
baseline, and resets Nautobot to match. To change the starting point, commit to main and move
the tag: git tag -f baseline && git push -f origin baseline.