mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-19 09:46:33 +00:00
aci-sim is a stateful REST simulator of a 2-site Cisco ACI fabric (per-site APIC + ND/NDO management planes) — for testing ACI automation (Ansible cisco.aci / cisco.mso, aci-py, custom REST clients) and CI gates without real hardware. Highlights: APIC + NDO REST surface served from one in-memory MIT built from a declarative topology.yaml; port mode + sandbox (real per-device IPs on :443) run modes; LLDP/CDP neighbor visibility; NDO->APIC deploy mirror (multi-site templates materialize onto the target sites' APIC stores); real-APIC fvBD default attributes; file-backed state save/restore; an `aci-sim` CLI (validate/show/graph/run/new/init/lldp); and an 888-test suite. History squashed for the public release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1058 lines
56 KiB
Markdown
1058 lines
56 KiB
Markdown
# aci-sim
|
||
|
||
A stateful, dependency-free REST simulator of a 2-site Cisco ACI/NDO fabric —
|
||
run Ansible `cisco.aci` playbooks, ACI SDKs, or an AI agent against it like
|
||
real gear, with no hardware required.
|
||
|
||
[](https://github.com/dtzp555-max/aci-sim/actions/workflows/ci.yml)
|
||
[](LICENSE)
|
||
[](pyproject.toml)
|
||
|
||
A faithful REST **simulator of a 2-site Cisco ACI fabric** — a per-site APIC
|
||
plus a Nexus Dashboard Orchestrator (ND/NDO) plane — with no physical or
|
||
virtual ACI hardware required.
|
||
|
||
The sim answers the same URLs, response envelopes, and query semantics as a
|
||
real APIC / NDO, so anything that speaks the ACI/NDO REST API — Ansible's
|
||
`cisco.aci` collection, custom ACI clients/SDKs, CI pipelines, or an AI
|
||
coding agent poking around a "fabric" — runs against it unmodified. The
|
||
entire fabric — nodes, tenants, VRFs/BDs/EPGs, contracts, L3Outs, faults,
|
||
endpoints, and the whole NDO schema tree — is generated deterministically
|
||
from a single `topology.yaml`.
|
||
|
||
---
|
||
|
||
## 1. Purpose
|
||
|
||
**What it simulates:** a 2-site Cisco ACI fabric (spines, leaves, border
|
||
leaves in vPC pairs, an inter-site ISN/EVPN mesh), one APIC REST API per
|
||
site, and one NDO/MSO REST plane tying the two sites together. All three
|
||
planes are derived from the same topology definition, so tenant/VRF/BD names
|
||
and IDs are guaranteed consistent between a site's APIC and NDO's view of it
|
||
— matching how a real multi-site deployment behaves.
|
||
|
||
The simulator implements a **99-class object catalog** (`docs/CONTRACT.md`
|
||
§6) spanning fabric/topology, faults, interfaces, underlay neighbors,
|
||
BGP-EVPN overlay, COOP/EPM, routing tables, tenant policy, contracts, L3Out,
|
||
access policy, and capacity/backup classes — the subset of the real ACI
|
||
object model that REST clients and Ansible's `cisco.aci` modules actually
|
||
read and write.
|
||
|
||
**Why it exists:**
|
||
|
||
- **Test Ansible `cisco.aci` playbooks** (tenant/VRF/BD/AP/EPG create,
|
||
idempotency, teardown) without booking lab time on real or virtual ACI
|
||
hardware. See `examples/ansible/`.
|
||
- **Develop and exercise ACI REST clients/SDKs** against faithful envelope
|
||
shapes, filter grammar, pagination, and error codes — including edge cases
|
||
(malformed filters, expired sessions, atomic write validation) that are
|
||
tedious to provoke on a real controller.
|
||
- **CI gates** — spin the sim up in a GitHub Actions job, run integration
|
||
tests against real HTTP, tear it down; no external dependency, no
|
||
credentials to manage, fully deterministic. See `examples/ci/` for a
|
||
copy-pasteable GitHub Actions gate + `assert_mit.py`, a tiny MIT-end-state
|
||
assertion helper that fails the build when a change stops producing the
|
||
objects it should.
|
||
- **AI-agent sandboxes** — a safe, disposable target for agents that need to
|
||
"drive" an ACI/NDO-shaped API (query, write, verify) without touching
|
||
production infrastructure. The `/_sim` control API (snapshot / restore /
|
||
reset) makes a clean act-observe-reset eval harness; seeded faults let you
|
||
test an agent's diagnosis path.
|
||
- **Training / demos** — a full 2-site fabric with faults, endpoints, and
|
||
contracts to explore, with no hardware cost.
|
||
|
||
It was originally built to exercise a companion project, autoACI, end-to-end
|
||
without a lab; that contract is preserved (`docs/CONTRACT.md`), but the
|
||
simulator's REST surface is generic ACI/NDO — nothing in it is autoACI-
|
||
specific.
|
||
|
||
### More applications
|
||
|
||
The same "stateful management-plane, real REST, resettable" core supports a
|
||
few more scenarios — with an honest note on what each needs today:
|
||
|
||
| Use case | Status |
|
||
|---|---|
|
||
| **SDK / provider testing** (cobra, acitoolkit, terraform-provider-aci, custom Go/Python clients) | Ready — faithful envelope/DN/filter semantics |
|
||
| **Change pre-flight** — model a proposed change (new tenant, VLAN migration) against a sim seeded to mirror production, catch errors before touching real gear | Ready — this is what the production-var validation runs exercise |
|
||
| **REST-contract / regression testing** — pin the APIC/NDO REST behaviour your tooling depends on to a git-versioned target | Ready |
|
||
| **Multi-tool interop** — verify Ansible + Terraform + a Python script converge on the same MIT | Ready |
|
||
| **Monitoring/observability integration** — test a fault→alert pipeline or exporter against seeded faults/health | Ready — polling AND websocket **query subscriptions** (`?subscription=yes` + `/socket<token>` push-on-change) both work, see §10a |
|
||
| **Client scale/perf testing** — hammer pagination / large-MIT / concurrent queries | Needs a large-topology seed (extension) |
|
||
|
||
**Out of scope** (management-plane simulator, by design): the data plane
|
||
(packet forwarding, real VXLAN), real APIC software internals (upgrades,
|
||
cluster quorum), and live event-driven telemetry — operational objects
|
||
(faults, health, endpoints, routes) are synthesised from the topology at
|
||
build time, not generated from real events.
|
||
|
||
---
|
||
|
||
## 2. Architecture
|
||
|
||
### Module map
|
||
|
||
```
|
||
topology.yaml
|
||
│ load_topology() topology/loader.py + schema.py (Pydantic v2)
|
||
▼
|
||
Topology
|
||
│
|
||
├─ build_all(topo) ──► {site_name: MITStore} build/orchestrator.py
|
||
│ builders: fabric, cabling, underlay, overlay, interfaces,
|
||
│ tenants, access, l3out, endpoints, routing,
|
||
│ zoning, health_faults
|
||
│
|
||
└─ build_ndo_model(topo) ──► NdoState ndo/model.py
|
||
(names derived from the SAME topo → APIC/NDO consistency)
|
||
|
||
make_apic_app(ApicSiteState) ──► FastAPI (×2, one per site) rest_aci/app.py
|
||
├─ query engine (class / mo / node-scoped) query/engine.py
|
||
├─ write handler (upsert + fabricNodeIdentP reaction) rest_aci/writes.py
|
||
└─ /_sim control router control/admin.py
|
||
|
||
make_ndo_app(NdoState) ──► FastAPI ndo/app.py
|
||
|
||
runtime/supervisor.py → three uvicorn servers over TLS (asyncio.gather)
|
||
```
|
||
|
||
- **`mit/`** — the object store. `MO` (dn-keyed managed object with
|
||
attrs/children) + `MITStore` (dn→MO map, parent→children index,
|
||
deterministic insertion order). No dependencies — the foundation every
|
||
other layer builds on.
|
||
- **`query/`** — the ACI query engine: `filters.py` parses
|
||
`eq/wcard/and/or/gt/lt/ge/le/bw(...)` filter expressions into predicates;
|
||
`engine.py` implements the three query shapes (class, MO/DN, node-scoped)
|
||
plus both subtree modes (legacy `query-target=subtree` flat, modern
|
||
`rsp-subtree=children|full` nested) and pagination.
|
||
- **`topology/`** — Pydantic v2 schema + loader for `topology.yaml`. Fails
|
||
loudly (all errors at once) on bad cross-references (BD→VRF, EPG→BD,
|
||
L3Out→VRF, CSW peering mismatches, IP pools outside builder-derived
|
||
ranges).
|
||
- **`build/`** — one pure `build(topo, site, store)` function per concern
|
||
(fabric, cabling, underlay, overlay/ISN, interfaces, tenants, access,
|
||
l3out, endpoints, routing, zoning, health_faults); each only *adds* MOs.
|
||
`orchestrator.py` runs them in dependency order and returns a populated
|
||
`MITStore` per site.
|
||
- **`rest_aci/`** — the APIC FastAPI app: `auth.py` (aaaLogin/aaaRefresh/
|
||
aaaLogout, cookie/session lifecycle, cert accept-mode), `app.py` (routes +
|
||
query-param parsing → query engine → envelope + error mapping),
|
||
`writes.py` (POST/DELETE body → MIT upsert, atomic validate-then-commit,
|
||
the `fabricNodeIdentP` node-registration reaction).
|
||
- **`ndo/`** — `model.py` builds the NDO schema/tenant/site model from the
|
||
same `Topology` object; `app.py` serves the NDO/MSO REST surface.
|
||
- **`runtime/`** — `config.py` (env-driven ports/binds/paths),
|
||
`supervisor.py` (loads topology, builds all stores, starts the three TLS
|
||
servers, keeps a baseline snapshot for `/_sim/reset`).
|
||
- **`control/`** — the `/_sim/*` test-orchestration router mounted on every
|
||
APIC app (reset/snapshot/restore/reload/add-leaf/remove-leaf) — not part
|
||
of the real ACI API surface.
|
||
|
||
### Data flow
|
||
|
||
`topology.yaml` → `load_topology()` validates and produces a typed
|
||
`Topology` → `build_all()` runs each site's builders in dependency order
|
||
(cabling before underlay/overlay; endpoints before tenants/routing/zoning,
|
||
so those can read back real endpoint ports) → one `MITStore` per site →
|
||
`make_apic_app()` wraps each store in a FastAPI app implementing the query
|
||
engine + write path → `build_ndo_model()` derives the NDO schema from the
|
||
same `Topology` → `make_ndo_app()` serves it. `runtime/supervisor.py` starts
|
||
all three apps as uvicorn servers over TLS, and keeps a baseline copy of
|
||
each store so `/_sim/reset` can restore it.
|
||
|
||
### Two run modes
|
||
|
||
This is the key differentiator versus "just run three dev servers":
|
||
|
||
| | **Port mode** (default) | **Sandbox mode** |
|
||
| --- | --- | --- |
|
||
| Addressing | one shared bind address (`SIM_BIND`, default `127.0.0.1`), three distinct ports: `8443` (APIC site A), `8444` (APIC site B), `8445` (NDO) | one **loopback IP alias per device**, all on the standard HTTPS port **`:443`** — no port numbers, exactly like real gear |
|
||
| Started by | `scripts/run.sh` | `scripts/sandbox-up.sh` (root required) |
|
||
| Use case | everyday development, Ansible playbooks, CI, direct APIC login | NDO **auto-discovery** ("Connect All Sites"), which connects to each site's APIC by IP on port 443 and cannot be redirected to a `host:port` form |
|
||
| Platform handling | n/a | branches on `uname -s`: macOS uses `ifconfig lo0 alias`, Linux uses `ip addr add … dev lo`; the client under test is never modified |
|
||
|
||
Sandbox mode exists because some NDO/multi-site-orchestration clients
|
||
hardcode port 443 for auto-discovered sites and have no field to enter an
|
||
alternate port — the only way to satisfy that client without patching it is
|
||
to give each simulated controller a real, individually addressable IP. The
|
||
IPs come from `topology.yaml` (`fabric.ndo_mgmt_ip` + each site's `mgmt_ip`)
|
||
and are added/removed as loopback aliases by `sandbox-up.sh`/`sandbox-down.sh`,
|
||
which also verify the previous instance is a genuine
|
||
`aci_sim.runtime.supervisor` process before killing it, and confirm
|
||
the new servers actually answer on `:443` before declaring success.
|
||
|
||
---
|
||
|
||
## 3. Dependencies
|
||
|
||
From `requirements.txt`:
|
||
|
||
| Package | Purpose |
|
||
| --- | --- |
|
||
| `fastapi>=0.110` | REST app framework for the APIC/NDO planes |
|
||
| `uvicorn[standard]>=0.29` | ASGI server (TLS termination, `asyncio` event loop) |
|
||
| `pydantic>=2.6` | Topology schema validation |
|
||
| `pyyaml>=6.0` | `topology.yaml` parsing |
|
||
| `httpx>=0.27` | Used by FastAPI's `TestClient` in tests |
|
||
| `pytest>=8.0` | Test runner |
|
||
| `pytest-asyncio>=0.23` | Async test support (`asyncio_mode = "auto"`) |
|
||
|
||
Python **3.11+** (see `pyproject.toml`, `requires-python = ">=3.11"`).
|
||
No database, no external services — everything is in-memory.
|
||
|
||
---
|
||
|
||
## 4. Usage / Quickstart
|
||
|
||
### Install
|
||
|
||
Direct from GitHub (simplest — no clone needed):
|
||
|
||
```bash
|
||
pip install git+https://github.com/dtzp555-max/aci-sim.git
|
||
```
|
||
|
||
From source (dev — editable install, test/lint extras):
|
||
|
||
```bash
|
||
git clone https://github.com/dtzp555-max/aci-sim.git
|
||
cd aci-sim
|
||
python -m venv .venv
|
||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||
pip install -e '.[dev]' # installs aci_sim + fastapi/uvicorn/pydantic/pyyaml + httpx/pytest
|
||
```
|
||
|
||
`pip install aci-sim` will work once the project is published to PyPI (not
|
||
yet published — use one of the two forms above until then).
|
||
|
||
### Port mode (default)
|
||
|
||
```bash
|
||
bash scripts/run.sh
|
||
# or, equivalently, via the aci-sim CLI (see §5):
|
||
aci-sim run
|
||
```
|
||
|
||
Generates a self-signed cert on first run (`scripts/gen_certs.sh`) and
|
||
starts:
|
||
|
||
```
|
||
[sim] APIC LAB1 (asn 65001) → https://127.0.0.1:8443
|
||
[sim] APIC LAB2 (asn 65002) → https://127.0.0.1:8444
|
||
[sim] NDO → https://127.0.0.1:8445
|
||
```
|
||
|
||
Smoke test:
|
||
|
||
```bash
|
||
curl -sk https://127.0.0.1:8443/api/class/fabricNode.json | python -m json.tool
|
||
```
|
||
|
||
### Sandbox mode (real per-device IPs on :443)
|
||
|
||
Needed for NDO "Connect All Sites" auto-discovery, which addresses each site
|
||
by IP on the standard HTTPS port with no port field.
|
||
|
||
**macOS:**
|
||
|
||
```bash
|
||
sudo bash scripts/sandbox-up.sh # aliases 10.192.0.10/.11 + 10.192.128.11 on lo0, serves :443
|
||
# point your NDO client at 10.192.0.10 to exercise auto-discovery
|
||
sudo bash scripts/sandbox-down.sh # stop the sim + remove the aliases
|
||
```
|
||
|
||
**Linux:** the same two scripts detect `uname -s == Linux` and use
|
||
`ip addr add/del … dev lo` instead of `ifconfig lo0 alias`; invocation is
|
||
identical (`sudo bash scripts/sandbox-up.sh` / `sandbox-down.sh`).
|
||
|
||
The IPs live in `topology.yaml` — edit `fabric.ndo_mgmt_ip` and each site's
|
||
`mgmt_ip` if they clash with your network.
|
||
|
||
### Ansible quickstart
|
||
|
||
`examples/ansible/` contains a validated `cisco.aci` playbook set — see
|
||
`examples/ansible/README.md` for exact run instructions against either run
|
||
mode.
|
||
|
||
```bash
|
||
cd examples/ansible
|
||
ansible-playbook -i inventory.ini smoke.yml # create + idempotency check
|
||
ansible-playbook -i inventory.ini teardown.yml # state=absent cleanup
|
||
```
|
||
|
||
### Running as a background service
|
||
|
||
For a standing deployment (e.g. a lab host reachable from other machines),
|
||
see `deploy/README.md` for a systemd `--user` unit (Linux) and a launchd
|
||
plist sketch (macOS).
|
||
|
||
---
|
||
|
||
## 5. CLI
|
||
|
||
`pip install -e .` installs an `aci-sim` console script (backed by
|
||
`aci_sim/cli.py`) that makes `topology.yaml` easier to author,
|
||
validate, preview, and run — **without** a web UI and **without** changing
|
||
the config format. `topology.yaml` remains the single source of truth;
|
||
`aci-sim` is a thin, purely-additive convenience layer around
|
||
`topology.loader.load_topology()` and the existing supervisor.
|
||
|
||
### `aci-sim validate [TOPOLOGY]`
|
||
|
||
Loads and validates a topology file (default `./topology.yaml`) — the same
|
||
Pydantic v2 validation (`Topology.normalize_and_validate`) the sim always
|
||
runs at startup, wrapped in a friendly pass/fail summary. This is the CI
|
||
gate: exit 0 + summary on success, exit non-zero + readable error list on
|
||
failure.
|
||
|
||
```console
|
||
$ aci-sim validate
|
||
OK: 2 site(s), 3 tenant(s)
|
||
- LAB1: 2 leaves, 2 spines, 2 border leaves
|
||
- LAB2: 2 leaves, 2 spines, 2 border leaves
|
||
```
|
||
|
||
### `aci-sim show [TOPOLOGY]`
|
||
|
||
Prints the **derived** inventory you need before pointing a client at the
|
||
sim: each site's APIC host/mgmt IP/ASN/pod/fabric name/OOB gateway, every
|
||
node's role, model, version, and derived loopback/OOB IP (via the same
|
||
`build/fabric.py::loopback_ip`/`oob_ip` the builders use), the NDO mgmt IP,
|
||
and the port/IP each controller will actually listen on (port mode vs
|
||
sandbox mode, read from `runtime/config.py`). Add `--json` for
|
||
machine-readable output (e.g. to feed inventory into another tool).
|
||
|
||
```console
|
||
$ aci-sim show
|
||
Fabric: LAB-IT-ACI (NDO mgmt IP: 10.192.0.10)
|
||
tep_pool=10.0.0.0/16 infra_vlan=3967 gipo_pool=225.0.0.0/15
|
||
|
||
Site LAB1: APIC host=127.0.0.1:8443 mgmt_ip=10.192.0.11 asn=65001 pod=1 controllers=1 fabric_name=LAB1-IT-ACI controller_ips=10.192.0.11 oob_gateway=-
|
||
ID NAME ROLE MODEL VERSION SERIAL LOOPBACK OOB IP
|
||
101 LAB1-ACI-LF101 leaf N9K-C9332C n9000-14.2(7f) SAL10101 10.1.101.1 192.168.1.101
|
||
...
|
||
|
||
Port bindings:
|
||
LAB1 (apic) -> https://127.0.0.1:8443
|
||
LAB2 (apic) -> https://127.0.0.1:8444
|
||
NDO (ndo ) -> https://127.0.0.1:8445
|
||
|
||
$ aci-sim show --json | jq '.sites[0].nodes[0]'
|
||
{
|
||
"id": 101,
|
||
"name": "LAB1-ACI-LF101",
|
||
"role": "leaf",
|
||
"model": "N9K-C9332C",
|
||
"version": "n9000-14.2(7f)",
|
||
"serial": "SAL10101",
|
||
"loopback_ip": "10.1.101.1",
|
||
"oob_ip": "192.168.1.101"
|
||
}
|
||
```
|
||
|
||
### `aci-sim lldp [TOPOLOGY]`
|
||
|
||
Builds the topology fresh (same builders the sim boots with) and prints a
|
||
**`show lldp neighbors`-style table** of every `lldpAdjEp` adjacency —
|
||
i.e. the neighbor relationships the cabling in `topology.yaml` implies
|
||
(spine<->leaf links, APIC<->leaf attachment), materialized exactly the way
|
||
they'd appear on a running sim (§10's LLDP/CDP fidelity). Pass `--cdp` to
|
||
read `cdpAdjEp` instead. This does not start any server — it's a static,
|
||
read-only view of one freshly-built site (or all sites).
|
||
|
||
```console
|
||
$ aci-sim lldp
|
||
Site LAB1 — node-101 (LAB1-ACI-LF101)
|
||
Local Port Neighbor Neighbor Port Mgmt IP Platform
|
||
eth1/1 LAB1-ACI-APIC01 eth1/1 10.192.0.11 Cisco APIC
|
||
eth1/49 LAB1-ACI-SP01 eth1/1 192.168.1.201 N9K-C9332C
|
||
eth1/50 LAB1-ACI-SP02 eth1/1 192.168.1.202 N9K-C9332C
|
||
|
||
Site LAB2 — node-301 (LAB2-ACI-LF301)
|
||
Local Port Neighbor Neighbor Port Mgmt IP Platform
|
||
eth1/1 LAB2-ACI-APIC01 eth1/1 10.192.128.11 Cisco APIC
|
||
eth1/49 LAB2-ACI-SP01 eth1/1 192.169.1.145 N9K-C9332C
|
||
...
|
||
|
||
$ aci-sim lldp --cdp --site LAB1 --node 101 --json
|
||
[
|
||
{
|
||
"local_node_id": 101,
|
||
"local_node_name": "LAB1-ACI-LF101",
|
||
"local_port": "eth1/1",
|
||
"neighbor": "LAB1-ACI-APIC01",
|
||
"neighbor_port": "eth1/1",
|
||
"mgmt_ip": "10.192.0.11",
|
||
"platform": "APIC-SERVER-M3",
|
||
"site": "LAB1"
|
||
}
|
||
]
|
||
```
|
||
|
||
Flags: `TOPOLOGY` positional (default `./topology.yaml`), `--cdp` (show
|
||
`cdpAdjEp` neighbors instead of `lldpAdjEp`), `--site NAME` (filter to one
|
||
site; default all sites), `--node ID` (filter to one local node id; default
|
||
all nodes), `--json` (machine-readable output instead of the grouped text
|
||
table).
|
||
|
||
### `aci-sim graph [TOPOLOGY] -o FILE.html`
|
||
|
||
Renders a **self-contained** visual topology diagram of the built fabric —
|
||
spines, leaves, border leaves (with vPC pairs), per-site controllers, and (for
|
||
multi-site topologies) the ISN cloud connecting each site's spines — as
|
||
either an `.html` file (inline SVG + inline CSS, ready to double-click and
|
||
open in any browser) or a raw `.svg` file. Output format is inferred from the
|
||
`-o` extension. There is no CDN dependency, no local web server, and no npm
|
||
build step: the file is plain hand-generated SVG markup, so it opens directly
|
||
from disk. It complements `aci-sim show` (`show` = text inventory, `graph` =
|
||
visual diagram) and visually mirrors autoACI's topology-view color language
|
||
(spine = blue, leaf/border-leaf = green, controller = amber, ISN cloud =
|
||
slate) — a standalone Python reimplementation, not a code import.
|
||
|
||
```console
|
||
$ aci-sim graph -o topology.html
|
||
[aci-sim] wrote topology diagram: topology.html
|
||
|
||
$ aci-sim graph lab3.topology.yaml -o lab3.svg
|
||
[aci-sim] wrote topology diagram: lab3.svg
|
||
```
|
||
|
||
Flags: `TOPOLOGY` positional (default `./topology.yaml`), `-o/--output`
|
||
(default `topology.html`; `.html`/`.htm` writes an HTML wrapper, `.svg`
|
||
writes raw SVG).
|
||
|
||
### `aci-sim run [TOPOLOGY]`
|
||
|
||
A thin wrapper around `python -m aci_sim.runtime.supervisor` — it
|
||
validates the topology up front (a friendly error instead of a raw
|
||
traceback), sets `TOPOLOGY_PATH` to the given file, resolves + injects the
|
||
admin credentials the APIC plane will enforce (see "Admin account /
|
||
credentials" below), passes through `SIM_BIND`/`SIM_SANDBOX`/etc. from the
|
||
current environment unmodified, and execs the supervisor (so it prints the
|
||
same `[sim] APIC ... -> https://...` lines you'd see from `scripts/run.sh`).
|
||
It does not reimplement the supervisor — `scripts/run.sh` keeps working
|
||
unchanged.
|
||
|
||
```console
|
||
$ aci-sim run
|
||
[aci-sim] Auth: APIC admin 'admin' (from topology.yaml's auth: section)
|
||
[aci-sim] running supervisor with TOPOLOGY_PATH=topology.yaml
|
||
[sim] APIC LAB1 (asn 65001) → https://127.0.0.1:8443
|
||
[sim] APIC LAB2 (asn 65002) → https://127.0.0.1:8444
|
||
[sim] NDO → https://127.0.0.1:8445
|
||
|
||
$ SIM_BIND=0.0.0.0 aci-sim run lab2.topology.yaml
|
||
```
|
||
|
||
### `aci-sim new`
|
||
|
||
Scaffolds a fresh, minimal, **valid** `topology.yaml` to stdout (or `-o
|
||
FILE`), using the same node-ID scheme documented in `topology/schema.py`
|
||
(leaves: base 101, +200/site; spines: base 201, +200/site; border leaves:
|
||
placed outside both ranges, in vPC pairs). The output always passes
|
||
`aci-sim validate` and boots.
|
||
|
||
```console
|
||
$ aci-sim new --sites 3 --leaves-per-site 4 -o lab3.topology.yaml
|
||
Wrote lab3.topology.yaml
|
||
$ aci-sim validate lab3.topology.yaml
|
||
OK: 3 site(s), 3 tenant(s)
|
||
- LABA: 4 leaves, 2 spines, 2 border leaves
|
||
- LABB: 4 leaves, 2 spines, 2 border leaves
|
||
- LABC: 4 leaves, 2 spines, 2 border leaves
|
||
```
|
||
|
||
Flags: `--sites N` (default 2), `--leaves-per-site N` (default 2, max 100 —
|
||
see "Leaf/spine count elasticity" note below), `--spines-per-site N`
|
||
(default 2, max 100), `--border-pairs N` (default 1, = 2 border leaves per
|
||
site; combined with `--leaves-per-site`/`--spines-per-site` is also subject
|
||
to the same per-site node-ID budget), `--asn A,B,...` (per-site fabric ASN;
|
||
default 65001,65002,...), `--fabric-name-prefix` (default `LAB`), `--ndo-ip`
|
||
(default `10.192.0.10`), `--apic-ip-base` (default `10.192`),
|
||
`--controllers N` (APIC cluster size per site; default **1**, range 1-5 —
|
||
single-APIC by design, see §6a below; set to 3+ for multi-controller/
|
||
cluster-health testing), `--tep-pool CIDR`
|
||
(default `10.0.0.0/16`), `--infra-vlan N` (default 3967), `--gipo-pool CIDR`
|
||
(default `225.0.0.0/15`), `--isn-ospf-area` (default `0.0.0.0`; Tier-2, PR-19),
|
||
`--isn-mtu N` (default 9150, range 576-9216; Tier-2, PR-19),
|
||
`--isn-ospf-hello N` (default 10; Tier-3, PR-20), `--isn-ospf-dead N`
|
||
(default 40; Tier-3, PR-20), `--isn-bfd-min-rx/--isn-bfd-min-tx N` (default
|
||
50/50; Tier-3, PR-20, store-only), `--isn-bfd-multiplier N` (default 3,
|
||
range 1-50; Tier-3, PR-20, store-only), `--default-bd-mac MAC` (default
|
||
`00:22:BD:F8:19:FF`, the real ACI default; Tier-3, PR-20), `-o/--output`
|
||
(default: stdout).
|
||
|
||
**Leaf/spine count elasticity (PR-19):** `--leaves-per-site`/
|
||
`--spines-per-site` work at any count up to 100 (e.g.
|
||
`--leaves-per-site 8 --spines-per-site 4`), and `--border-pairs` works for
|
||
more than 1 pair — all generate collision-free node IDs. Beyond count 100
|
||
per block (or a `--border-pairs` count that would push the border block
|
||
past the next site's leaf range), `aci-sim new` now **rejects the request
|
||
with a clear error naming the offending flag** instead of silently emitting
|
||
a `topology.yaml` that fails deep inside `Topology.normalize_and_validate`
|
||
with a generic "node ids collide" message — this is a hard ceiling of the
|
||
node-ID scheme itself (`site_offset=200` per `topology/schema.py`'s
|
||
docstring), not a bug fixable without a breaking change to node-ID
|
||
semantics for existing files.
|
||
|
||
### `aci-sim init [-o topology.yaml]`
|
||
|
||
An interactive Q&A wizard modeled on the real APIC's first-boot **setup
|
||
dialog** — it prompts for each field with a suggested default shown in
|
||
`[brackets]`; press ENTER to accept it, or type a custom value. It is a
|
||
thin front-end over the SAME `generate_topology()` function `aci-sim new`
|
||
calls (extended, PR-22, to also accept per-site overrides and per-site
|
||
node counts) — it does not duplicate topology-emission logic, and its
|
||
output always passes `aci-sim validate` and boots, same as `new`.
|
||
|
||
Fields asked, mirroring real APIC's dialog plus this sim's multi-site
|
||
extras:
|
||
|
||
- **Step 0 — admin account**: admin username (default `admin`), admin
|
||
password (default `cisco`), then `NDO uses the same admin account as
|
||
APIC?` (default yes). Answering "no" additionally asks for a separate NDO
|
||
username/password. See "Admin account / credentials" below for what this
|
||
account actually controls.
|
||
- **Step 1 — deployment type**: `1) Single fabric` or `2) Multi-site (2+
|
||
fabrics via ISN+NDO)` (default 2). Real APIC's dialog also offers
|
||
"multi-pod" — this sim doesn't build true multi-pod fabrics (no per-pod
|
||
spine/leaf partitioning or IPN), so that choice is deliberately not
|
||
offered; a single fabric can still set a non-default **Pod ID** per-site.
|
||
- **Step 2 — per site** (looped once per site; multi-site additionally asks
|
||
"Number of sites"): fabric/site name, site ID, BGP AS, Pod ID, number of
|
||
APIC controllers (1-5), APIC OOB mgmt IP, then — **if controllers > 1** —
|
||
one prompt per additional controller, each suggesting **the previous
|
||
controller's IP + 1** (accept with ENTER or type a custom IP; maps to
|
||
PR-21's `Site.controller_ips`), then the OOB gateway (asked once per
|
||
site; maps to `Site.oob_gateway` — see §6c — and lands on the real
|
||
`mgmtRsOoBStNode.gw` MO for every node at that site, build/mgmt.py), TEP
|
||
pool, infra VLAN, multicast/GIPo pool, OOB mgmt subnet, number of
|
||
spines/leaves/border-leaf pairs.
|
||
- **Step 3 — multi-site only**: NDO/ND mgmt IP, ISN OSPF area, ISN MTU
|
||
(inter-site EVPN uses each fabric's own BGP AS from Step 2 — there is no
|
||
separate ISN AS).
|
||
- **Step 4 — confirm + write**: prints a summary, asks `Write to
|
||
topology.yaml? [yes]`, writes the file, then **auto-runs `aci-sim
|
||
validate`** on the result and prints OK (or the errors).
|
||
|
||
```console
|
||
$ aci-sim init
|
||
This wizard collects the same setup information as a real APIC's
|
||
first-boot dialog, then generates a topology.yaml for aci-sim.
|
||
Press ENTER to accept a suggested default shown in [brackets].
|
||
|
||
Step 0: Admin account
|
||
Admin username [admin]:
|
||
Admin password [cisco]:
|
||
NDO uses the same admin account as APIC? [yes]:
|
||
|
||
Step 1: Deployment type
|
||
Deployment type: 1) Single fabric 2) Multi-site (2+ fabrics via ISN+NDO) [2]: 1
|
||
|
||
Step 2: Site 1 of 1
|
||
Fabric/site name [Site1-IT-ACI]: MyLab-IT-ACI
|
||
Site ID [1]:
|
||
BGP AS (fabric) [65001]:
|
||
Pod ID [1]:
|
||
Number of APIC controllers [1]: 3
|
||
APIC OOB mgmt IP [10.192.0.11]:
|
||
APIC-2 OOB IP [10.192.0.12]:
|
||
APIC-3 OOB IP [10.192.0.13]: 10.192.0.20
|
||
APIC OOB gateway (shared by all controllers at this site) [10.192.0.1]:
|
||
TEP pool [10.0.0.0/16]:
|
||
Infra VLAN (1-4094) [3967]:
|
||
Multicast/GIPo pool [225.0.0.0/15]:
|
||
OOB mgmt subnet [192.168.0.0/16]:
|
||
Number of spines [2]:
|
||
Number of leaves [2]:
|
||
Number of border-leaf pairs [1]:
|
||
|
||
Summary:
|
||
Admin account: username=admin password=c*** (NDO: same account — lenient by design, accepts any credential)
|
||
Fabric: MyLab-IT-ACI
|
||
ISN/multi-site enabled: False
|
||
APIC OOB gateway (site 1): 10.192.0.1
|
||
Site MyLab-IT-ACI (id=1, asn=65001, pod=1): 3 controller(s) ['10.192.0.11', '10.192.0.12', '10.192.0.20'], oob_gateway=10.192.0.1, 2 spines, 2 leaves, 2 border leaves (1 pair(s))
|
||
|
||
Write to topology.yaml? [yes]:
|
||
Wrote topology.yaml
|
||
OK: 1 site(s), 1 tenant(s)
|
||
- MyLab-IT-ACI: 2 leaves, 2 spines, 2 border leaves
|
||
```
|
||
|
||
This transcript was generated with `aci-sim init -o topology.yaml --answers` (see the "Admin account / credentials" section below for the exact command + real output); the prompt lines shown here mirror `run_wizard`'s literal prompt text.
|
||
|
||
Note the third controller's IP was typed as a custom value (`10.192.0.20`)
|
||
overriding the suggested sequential default (`10.192.0.13` = controller
|
||
2's IP + 1) — pressing ENTER at any prompt always accepts the bracketed
|
||
suggestion instead.
|
||
|
||
**Non-interactive use** (CI/scripting — never blocks waiting for input):
|
||
|
||
- `--defaults` — accept every suggested default without prompting.
|
||
Auto-enabled whenever stdin is not a TTY (e.g. piped/redirected/
|
||
`subprocess` with `stdin=DEVNULL`), so `aci-sim init -o topology.yaml` in
|
||
a script or CI job completes immediately with the wizard's defaults even
|
||
without the flag.
|
||
- `--answers FILE` — a YAML or JSON file of pre-filled field-name -> value
|
||
answers (e.g. `{"deployment_type": "2", "site1_controllers": "3"}`).
|
||
Listed fields are used verbatim (no prompt); unlisted fields fall back to
|
||
prompting/defaults as usual. Field names: `admin_username`,
|
||
`admin_password`, `ndo_same_account` (`yes`/`no`), `ndo_username`/
|
||
`ndo_password` (only consulted if `ndo_same_account` is `no`);
|
||
`deployment_type`, `num_sites` (multi-site only), then per site index `N`:
|
||
`siteN_name`, `siteN_id`, `siteN_asn`, `siteN_pod`, `siteN_controllers`,
|
||
`siteN_apicC_ip` (C = 1..controllers), `siteN_gateway`, `siteN_tep_pool`,
|
||
`siteN_infra_vlan`, `siteN_gipo_pool`, `siteN_oob_subnet`, `siteN_spines`,
|
||
`siteN_leaves`, `siteN_border_pairs`; multi-site only: `ndo_mgmt_ip`,
|
||
`isn_ospf_area`, `isn_mtu`; and `confirm_write` (`yes`/`no`) to skip the
|
||
final confirm prompt too.
|
||
|
||
```console
|
||
$ aci-sim init --defaults -o lab.topology.yaml
|
||
...
|
||
Wrote lab.topology.yaml
|
||
OK: 2 site(s), 2 tenant(s)
|
||
- Site1-IT-ACI: 2 leaves, 2 spines, 2 border leaves
|
||
- Site2-IT-ACI: 2 leaves, 2 spines, 2 border leaves
|
||
```
|
||
|
||
`-o/--output` (default: `./topology.yaml`).
|
||
|
||
### Admin account / credentials
|
||
|
||
`aci-sim init`'s **Step 0** is where you set the fabric admin account — it
|
||
ends up as an `auth:` section in `topology.yaml`:
|
||
|
||
```yaml
|
||
auth:
|
||
username: admin
|
||
password: cisco
|
||
```
|
||
|
||
**The APIC plane ENFORCES this account** — `aci-sim run` resolves it and
|
||
injects `SIM_USERNAME`/`SIM_PASSWORD` before execing the supervisor, and
|
||
`rest_aci/auth.py`'s `aaaLogin` 401s on any mismatch (see §7 below). **NDO
|
||
shares the same account by default** (the wizard's `ndo_same_account: yes`
|
||
default) but stays **lenient by design regardless** — `ndo/app.py`'s
|
||
`/api/v1/auth/login`/`/login` accept any credential and never validate a
|
||
token, matching this sim's long-standing NDO client-compat behavior (see
|
||
§8 "Known limitations"). Answering "no" to `ndo_same_account` only changes
|
||
what NDO is *told* (`auth.ndo_username`/`auth.ndo_password`, informational),
|
||
never what it *checks*.
|
||
|
||
**Precedence `aci-sim run` uses to resolve `SIM_USERNAME`/`SIM_PASSWORD`**
|
||
(highest first):
|
||
|
||
1. `SIM_USERNAME` already set in your shell environment — an explicit env
|
||
override always wins (preserves existing CI/env-override workflows);
|
||
`topology.yaml`'s `auth:` is ignored even if present.
|
||
2. `topology.yaml` has an `auth:` section (written by the wizard) — its
|
||
`username`/`password` are injected.
|
||
3. Neither — nothing is injected; `rest_aci/auth.py` falls back to its own
|
||
`SIM_USERNAME`/`SIM_PASSWORD`-env-or-`admin`/`cisco` default, unchanged.
|
||
|
||
`aci-sim run` always prints which source won, e.g.:
|
||
|
||
```console
|
||
$ aci-sim run
|
||
[aci-sim] Auth: APIC admin 'admin' (from topology.yaml's auth: section)
|
||
```
|
||
|
||
An existing `topology.yaml` with no `auth:` section at all (every file that
|
||
predates this feature) keeps validating and behaves exactly as before —
|
||
`Topology.auth` is `Optional`, defaulting to `None`.
|
||
|
||
---
|
||
|
||
## 6. Configuration
|
||
|
||
All configuration is via environment variables, read at process start
|
||
(`aci_sim/runtime/config.py`, `aci_sim/rest_aci/auth.py`).
|
||
|
||
| Variable | Default | Effect |
|
||
| --- | --- | --- |
|
||
| `SIM_BIND` | `127.0.0.1` | Bind address for port-mode servers. Defaults to loopback-only because the `/_sim` control plane is unauthenticated — set `SIM_BIND=0.0.0.0` explicitly for LAN deployments. Ignored in sandbox mode (always binds each device's own `mgmt_ip`). |
|
||
| `SIM_USERNAME` | `admin` | Username checked by `aaaLogin`. An explicit env value here always wins over a topology `auth:` section — see "Admin account / credentials" above. |
|
||
| `SIM_PASSWORD` | `cisco` | Password checked by `aaaLogin`; a mismatch returns a 401 APIC error envelope. Same env-wins-over-topology precedence as `SIM_USERNAME`. |
|
||
| `APIC_A_PORT` | `8443` | Port-mode port for APIC site A. |
|
||
| `APIC_B_PORT` | `8444` | Port-mode port for APIC site B. |
|
||
| `NDO_PORT` | `8445` | Port-mode port for the NDO plane. |
|
||
| `SIM_SANDBOX` | unset (`0`) | Set to `1`/`true`/`yes`/`on` to enable sandbox mode (bind each device to its own `mgmt_ip`). Set by `scripts/sandbox-up.sh`; not normally set by hand. |
|
||
| `SIM_SANDBOX_PORT` | `443` | Port sandbox-mode servers bind to on each device's own IP. |
|
||
| `CERT_FILE` | `certs/sim.crt` | TLS certificate path (self-signed, all three apps). |
|
||
| `KEY_FILE` | `certs/sim.key` | TLS private key path. |
|
||
| `TOPOLOGY_PATH` | `topology.yaml` | Path to the topology definition loaded at startup and by `/_sim/reload`. |
|
||
| `SIM_STATE_DIR` | `~/.aci-sim/state` | Directory for `/_sim/save`/`/_sim/load` state files (§10). |
|
||
| `SIM_CERT_STRICT` | `false` | **Placeholder, currently a no-op.** Reserved for a future mode that would verify `cisco.aci` certificate-signature auth (`APIC-Request-Signature`) against a registered public key. Today, certificate-auth requests are accepted based on the claimed identity alone (see §Error handling below) regardless of this flag. |
|
||
|
||
### 6a. Configuration Reference — Tier-1 fabric parameters (topology.yaml)
|
||
|
||
Unlike the environment variables above, these are `topology.yaml` fields
|
||
(`aci_sim/topology/schema.py`). All are **optional** with defaults
|
||
matching the pre-existing hardcoded behavior, so an existing `topology.yaml`
|
||
that omits them keeps validating and building byte-identical output
|
||
(verified: the repo's own `topology.yaml` is unchanged in value and still
|
||
passes `aci-sim validate` + the full test suite). See `docs/DESIGN.md`'s
|
||
"PR-18 — Tier-1 fabric configuration parameters" section for the full
|
||
wiring detail and exactly which fields are store-only vs. builder-wired.
|
||
|
||
**Single-APIC-per-fabric/site is DELIBERATE (PR-21).** `aci-sim`
|
||
defaults every site to `controllers: 1` because APIC cluster size does not
|
||
affect Ansible playbook deployment testing — a playbook connects to and
|
||
pushes configuration through exactly **one** APIC endpoint, regardless of
|
||
how many controllers sit behind it in real ACI. Cluster size only matters
|
||
for cluster-health/appliance-vector tooling, such as autoACI's
|
||
`health_score.py`, which reads `infraWiNode` across every controller in the
|
||
cluster. Set `controllers: 3` (+ optionally `controller_ips: [...]`) on a
|
||
site **only if** you specifically need to exercise multi-controller
|
||
`infraWiNode`/cluster-health semantics — the sim still binds exactly one
|
||
REST endpoint per site either way (it does not run N separate APIC
|
||
processes); `controllers` only changes how many controller-role
|
||
`fabricNode`/`topSystem`/`infraWiNode` entries are built. Valid range: 1-5.
|
||
|
||
| Field | Scope | Default | Wired into builders? |
|
||
| --- | --- | --- | --- |
|
||
| `site.controllers` | per-site | **`1`** (PR-21; was `3` pre-PR-21) | Yes — APIC cluster size, range **1-5**; drives the `fabricNode`/`topSystem`/`firmwareCtrlrRunning` controller loop, the `infraWiNode` appliance-vector (viewer × member), `health_faults.py`, and `endpoints.py`'s APIC-reserved-port accounting in `build/fabric.py` and friends. |
|
||
| `site.controller_ips` | per-site | unset (PR-21) | Yes, when set — one OOB mgmt IP per controller, in order (`controller_ips[0]` = controller 1's `topSystem.oobMgmtAddr`, etc.); length must equal `controllers`. When unset, IPs are auto-derived **sequentially from `site.mgmt_ip`** (controller 1 = `mgmt_ip`, controller 2 = `mgmt_ip`+1, ...). If `mgmt_ip` is also unset, falls back further to the legacy per-node `oob_ip(pod, cid)` scheme (pre-PR-21 behavior, unchanged). |
|
||
| `site.pod` | per-site | `1` | Yes — every `topology/pod-{N}/...` DN across all builders reads `site.pod`; a `pod: 2` site builds all its DNs under `pod-2`. |
|
||
| `fabric.tep_pool` | fabric-wide | `"10.0.0.0/16"` | Store + CIDR-validate + surface only (`aci-sim show`/`new`). No builder derives a per-node address from it — `loopback_ip()`/`oob_ip()` remain the actual address source. |
|
||
| `fabric.infra_vlan` | fabric-wide | `3967` | Store + range-validate (1-4094) + surface only. No MIT class in the current catalog carries a distinct infra-VLAN attribute. |
|
||
| `fabric.gipo_pool` | fabric-wide | `"225.0.0.0/15"` | Store + CIDR-validate + surface only. |
|
||
| Auto-generated node `serial` | per-node | `f"SAL{{site_id}}{{node_id:04d}}"` | Yes — `build/fabric.py`'s `default_serial()` fallback whenever a `Node.serial` is empty (used by `fabricNode.serial` and `fabricNodeIdentP.serial`, which `cisco.aci.aci_fabric_node` keys registration on). An explicit `serial:` in the YAML always overrides. |
|
||
|
||
**N-site note:** `aci-sim new --sites N` and the node-ID/pod/controller
|
||
wiring above work for arbitrary N with zero collisions. **ISN inter-site
|
||
peering and stretched-tenant HOME/REMOTE endpoint placement do not** — both
|
||
rely on `Topology.other_site()`, which resolves to "the first other site in
|
||
YAML order", not "every other site" (pre-existing, not introduced by
|
||
PR-18). Practical effect: with 3+ sites, ISN is not a true full mesh (each
|
||
site only peers with one other), even though `isn.peer_mesh: full` is set.
|
||
See `docs/DESIGN.md` PR-18 section for the exact verification and why
|
||
fixing it is out of scope here.
|
||
|
||
### 6b. Configuration Reference — Tier-2 topology-elasticity parameters (topology.yaml)
|
||
|
||
Same backward-compat guarantee as §6a: all fields below are **optional**
|
||
with defaults matching pre-existing behavior. See `docs/DESIGN.md`'s
|
||
"PR-19 — Tier-2 topology-elasticity parameters" section for the full
|
||
wiring detail.
|
||
|
||
| Field | Scope | Default | Wired into builders? |
|
||
| --- | --- | --- | --- |
|
||
| `isn.ospf_area` | fabric-wide | `"0.0.0.0"` | Yes — `build/underlay.py`'s `ospfIf.area`/`ospfAdjEp.area` (was a hardcoded `"0.0.0.0"` literal). Accepts either dotted-decimal (`"0.0.0.0"`) or decimal-integer (`"0"`) OSPF area notation. |
|
||
| `isn.mtu` | fabric-wide | `9150` | Yes — `build/interfaces.py`'s dedicated ISN/IPN spine uplink `l1PhysIf.mtu` (was hardcoded to the general fabric `9216` value, same as every other port). Only the ISN uplink port changes; ordinary intra-fabric ports keep `9216`. Range-validated 576-9216. |
|
||
| `isn.peer_mesh` | fabric-wide | `"full"` | Validate-and-reject for any value other than `"full"` (unchanged from PR-17/18). `"partial"` mesh has no sensible default partition without additional per-site config the schema doesn't carry (which spine pairs with which isn't inferable from spine count alone) — `build/overlay.py` fails fast rather than silently building full-mesh or an undefined subset. |
|
||
| `access.vmm_domains[].vcenter_ip` | per-domain | `""` (unset) | Yes, when set — `build/access.py` emits a `vmmCtrlrP` child of the domain's `vmmDomP` with `hostOrIp=vcenter_ip`. Unset → no `vmmCtrlrP`/`vmmUsrAccP` is built (just a bare `vmmDomP`). |
|
||
| `access.vmm_domains[].datacenter` | per-domain | `""` (unset) | Yes, when `vcenter_ip` is set — `vmmCtrlrP.rootContName`; defaults to the domain's own `name` if unset. |
|
||
| `access.vmm_domains[].vcenter_dvs` | per-domain | `""` (unset) | Yes, when set — `vmmCtrlrP.dvsName`. |
|
||
| `access.vmm_domains[].vlan_pool` | per-domain | `""` (unset) | Yes, when set — binds an `infraRsVlanNs` under the `vmmDomP` to the referenced `access.vlan_pools[]` entry (same pattern phys/l3 domains already use). Cross-referenced against `access.vlan_pools[].name` at validation time. |
|
||
|
||
**VMM domain is a pure addition, not a rewire (PR-19 design note):** no
|
||
existing builder or the real production chains (autoACI sandbox e2e / aci-py
|
||
MS-TN1) read a `vmmDomP`/`vmmCtrlrP` MIT object today. The real
|
||
`bind_epg_to_vmm_domain` playbook (verified read-only against
|
||
`aci-py`'s `playbooks.py` + `content/templates/bind_epg_to_vmm_domain.j2.yml`
|
||
on the hardware test bed) writes an **NDO schema `domainAssociations` entry**
|
||
referencing the VMM domain purely by DN string
|
||
(`uni/vmmp-VMware/dom-<name>`) — it never touches an APIC-side `vmmDomP`
|
||
object, so `access.vmm_domains` defaults to an **empty list** (unlike
|
||
`phys_domains`/`l3_domains`/`aaeps`, which each default to one seeded
|
||
entry) and adding it changes zero existing builder output.
|
||
`bind_epg_to_physical_domain`, by contrast, DOES read a real `physDomP`,
|
||
which is why that one already existed pre-PR-19.
|
||
|
||
### 6c. Configuration Reference — Tier-3 fine-tuning parameters (topology.yaml)
|
||
|
||
Same backward-compat guarantee as §6a/§6b: all fields below are **optional**
|
||
with defaults matching pre-existing behavior (or the real ACI factory
|
||
default, for fields that had no prior value at all). This is the last
|
||
planned param tier. See `docs/DESIGN.md`'s "PR-20 — Tier-3 fine-tuning
|
||
parameters" section for the full wiring detail.
|
||
|
||
| Field | Scope | Default | Wired into builders? |
|
||
| --- | --- | --- | --- |
|
||
| `bd.mac` | per-BD | unset (falls back to `fabric.default_bd_mac`) | Yes — `build/tenants.py`'s `fvBD.mac`. |
|
||
| `fabric.default_bd_mac` | fabric-wide | `"00:22:BD:F8:19:FF"` (real ACI default) | Yes — same `fvBD.mac`, used when a BD doesn't set its own `bd.mac`. This is the exact literal `build/tenants.py` already hardcoded for every BD pre-PR-20, so the default produces byte-identical output. |
|
||
| `l3out.csw_peer.keepalive` / `.hold` | per-L3Out-peer | `60` / `180` (real ACI defaults) | Yes — `build/l3out.py`'s `bgpPeerP.keepAliveIntvl`/`holdIntvl`. `hold` must exceed `keepalive`. |
|
||
| `isn.ospf_hello` / `isn.ospf_dead` | fabric-wide | `10` / `40` (real ACI defaults) | Yes — `build/underlay.py`'s `ospfIf.helloIntvl`/`deadIntvl`. `ospf_dead` must exceed `ospf_hello`. |
|
||
| `isn.bfd_min_rx` / `isn.bfd_min_tx` / `isn.bfd_multiplier` | fabric-wide | `50` / `50` / `3` (real ACI defaults) | **No** — store + range-validate + surface only (`aci-sim show`/`new`). This sim builds no `bfdIfP`-style MO anywhere; adding a faithful one would additionally require a believable oper-state tree with no verified consumer to anchor its shape against. |
|
||
| `fabric.oob_subnet` | fabric-wide | unset | **No** — store + CIDR/range-validate (must fall within `192.168.0.0/16`) + surface only. `build/fabric.py`'s `oob_ip()` derives every node's OOB address from `(pod, node_id)` only, same documented limitation as `fabric.oob_pool` itself. |
|
||
| `fabric.inb_subnet` | fabric-wide | unset | **No** — store + CIDR/range-validate (must fall within a private RFC1918 range) + surface only. This sim has no in-band mgmt MO (`mgmtInB`/`inbMgmtAddr`) anywhere; adding one would require a new `mgmt`-tenant builder, out of scope for this PR. |
|
||
| `fabric.default_apic_version` | fabric-wide | unset (falls back to each site's own `site.apic_version`) | Yes — `build/fabric.py`'s controller `fabricNode.version`/`topSystem.version`/`firmwareCtrlrRunning.version`, taking precedence over `site.apic_version` when set. `site.apic_version` (PR-18 lineage) is already independently settable from switch-node `Node.version` (PR-18); this field adds a fabric-wide override so one value covers every site instead of repeating `apic_version:` per site. |
|
||
| `site.oob_gateway` | per-site | unset (`None`) | **Yes** — `build/mgmt.py`'s `mgmtRsOoBStNode.gw`, one per node (switches + controllers) at this site, under the new mgmt-tenant scaffolding `uni/tn-mgmt/mgmtp-default/oob-default` (`fvTenant`/`mgmtMgmtP`/`mgmtOoB`). This is the OOB-gateway PR: `aci-sim init`'s wizard already asked this question but discarded the answer (no schema field, no MO); it now writes it here. Unset → the same MOs still build, just with an empty `gw` (no gateway invented/derived — see build/mgmt.py). |
|
||
|
||
**BD MAC default is a genuine backward-compat guarantee, not just a
|
||
plausible-looking default:** `build/tenants.py`'s `_build_bd` already
|
||
hardcoded the literal `"00:22:BD:F8:19:FF"` (the real ACI default gateway
|
||
MAC) for every BD before this PR — `fabric.default_bd_mac`'s schema default
|
||
reproduces that exact value, so omitting both `bd.mac` and
|
||
`fabric.default_bd_mac` from `topology.yaml` produces byte-identical
|
||
`fvBD.mac` output to pre-PR-20.
|
||
|
||
**APIC version is already fully wired, independent of switch version
|
||
(PR-18/PR-9 lineage):** `site.apic_version` has driven the controller
|
||
`fabricNode`/`topSystem`/`firmwareCtrlrRunning` since PR-9/PR-18, completely
|
||
independently of the per-node switch `Node.version` field. PR-20 does not
|
||
change that wiring — it adds `fabric.default_apic_version` as a fabric-wide
|
||
override on top, and surfaces the *effective* per-site APIC version (after
|
||
resolving that override) in `aci-sim show`, which previously showed no APIC
|
||
version at all.
|
||
|
||
---
|
||
|
||
## 7. Error handling
|
||
|
||
The simulator follows APIC's real error-envelope shape and status-code
|
||
semantics rather than generic REST conventions — this is load-bearing for
|
||
any client (like `cisco.aci`) that branches on APIC's exact codes.
|
||
|
||
**Error envelope** (all non-2xx responses):
|
||
|
||
```json
|
||
{"imdata":[{"error":{"attributes":{"text":"<message>","code":"<code>"}}}]}
|
||
```
|
||
|
||
**Auth semantics:**
|
||
|
||
- `POST /api/aaaLogin.json` — credentials checked against
|
||
`SIM_USERNAME`/`SIM_PASSWORD`. Mismatch → **401**
|
||
(`"Authentication failed: invalid username or password"`). Malformed body
|
||
(wrong shape / non-JSON) → **400** APIC envelope — never FastAPI's raw
|
||
`{"detail": [...]}` validation-error shape.
|
||
- Every `/api/class/*`, `/api/mo/*`, and `/api/node/class/*` route requires
|
||
a live, unexpired `APIC-cookie` (minted by `aaaLogin`, renewed by
|
||
`aaaRefresh`, expiring after `refreshTimeoutSeconds` — 600s by default).
|
||
Missing/unknown/expired cookie → **403**
|
||
(`"Token was invalid (Error: Token timeout)"`).
|
||
- Certificate-signature auth (`cisco.aci`'s password-less `private_key`
|
||
mode) is accepted in **accept-mode**: the four `APIC-Certificate-*`/
|
||
`APIC-Request-Signature` cookies are checked for presence and that the
|
||
claimed username matches `SIM_USERNAME` — the RSA-SHA256 signature bytes
|
||
are never verified. See `docs/DESIGN.md`'s "Certificate accept-mode trust
|
||
model" section. **Do not expose this to anything internet-reachable or
|
||
multi-tenant** — anyone who can reach the sim can authenticate as
|
||
`SIM_USERNAME` with four unsigned cookies.
|
||
- The `/_sim/*` control router and `/api/aaaLogin.json`/`aaaLogout.json`
|
||
are intentionally exempt from the session-cookie gate (bootstrapping +
|
||
test tooling depend on them being reachable pre-auth).
|
||
|
||
**Query/write error codes:**
|
||
|
||
- **400** — malformed `query-target-filter` expression (`code="107"`),
|
||
out-of-range/non-numeric `page`/`page-size` (`code="107"`), malformed
|
||
`aaaLogin` body, or a POST body with a malformed node anywhere in the
|
||
tree (atomic validation — the whole write is rejected, store untouched).
|
||
- **200 + empty envelope**, not 404, for a `GET /api/mo/{dn}.json` on a
|
||
well-formed but nonexistent DN (`{"imdata":[],"totalCount":"0"}`) — this
|
||
matches real APIC and is required for `cisco.aci`'s `state=present`
|
||
modules, which GET-before-write and treat any non-200 GET as fatal.
|
||
`/api/class/*` and `/api/node/class/*` already return 200-empty for a
|
||
query matching nothing.
|
||
- **DELETE `/api/mo/{dn}.json`** (`state=absent`) is server-side idempotent
|
||
— 200-empty whether the DN existed or not.
|
||
|
||
**Never-500 contract:** every documented input shape (malformed filters,
|
||
bad pagination, atomic write validation failures, expired sessions) maps to
|
||
a **400/401/403 APIC error envelope**, never a bare 500 or an unhandled
|
||
exception. See `docs/CONTRACT.md` §5 for the full mapping and
|
||
`docs/DESIGN.md` for the write-path atomicity design.
|
||
|
||
---
|
||
|
||
## 8. Known limitations
|
||
|
||
- **ISN/stretched-tenant correctness is verified for exactly 2 sites** —
|
||
`Topology.other_site()` resolves to "the first other site in YAML order",
|
||
not "every other site". `aci-sim new --sites N` (N>2) builds without
|
||
node-ID collisions, but ISN inter-site eBGP-EVPN peering and stretched-
|
||
tenant HOME/REMOTE endpoint placement only reach one other site, so ISN
|
||
is not a true full mesh across 3+ sites even with `isn.peer_mesh: full`.
|
||
See §6a and `docs/DESIGN.md`'s PR-18 section for the verified detail.
|
||
- **Certificate signatures are not verified** — `cisco.aci` certificate
|
||
auth is accepted based on claimed identity alone ("accept-mode"), not a
|
||
real RSA-SHA256 signature check. `SIM_CERT_STRICT` is a documented,
|
||
currently-inert placeholder for a future verifying mode. Do not point
|
||
this at anything internet-reachable or multi-tenant.
|
||
- **NDO surface is partial** — the sim implements the NDO/MSO endpoints a
|
||
REST client typically needs (auth, sites, tenants, schemas/templates,
|
||
fabric-connectivity, policy-states, template summaries, audit records,
|
||
schema/deploy writes), not the entire NDO API. See `docs/CONTRACT.md` §7
|
||
for the exact endpoint list. **`POST /mso/api/v1/task` (deploy) is no
|
||
longer a no-op on the APIC side** — see §10 "NDO → APIC deploy mirror";
|
||
what remains partial is breadth of *endpoint* coverage, not the
|
||
deploy-materialization gap this used to describe.
|
||
- **Single-user auth model** — one username/password pair
|
||
(`SIM_USERNAME`/`SIM_PASSWORD`) per process; there is no per-user RBAC,
|
||
multiple local users, or AAA/TACACS+ integration.
|
||
- **Websocket subscriptions are class/DN-scoped, not filter-scoped** —
|
||
`?subscription=yes` + `/socket<token>` push-on-change is implemented
|
||
(§10a), matching real APIC's class- and DN/subtree-scoped subscriptions.
|
||
Not implemented: subscribing to a *filtered* query (e.g. only
|
||
`faultInst` rows matching a `query-target-filter`) and getting pushes
|
||
scoped to just that filter — a filtered subscription still fires for
|
||
every change to the class/DN, same as an unfiltered one.
|
||
- **`/_sim/*` is a sim-only control plane** — `reset`, `snapshot`/`restore`,
|
||
`reload`, `add-leaf`/`remove-leaf` do not exist on a real APIC. It is
|
||
unauthenticated by design (test-orchestration convenience) and is exactly
|
||
why `SIM_BIND` defaults to loopback-only.
|
||
- **Static, not computed, control plane** — BGP/EVPN, COOP, IS-IS/OSPF
|
||
adjacencies, and forwarding state are represented as fixed, internally-
|
||
consistent MOs (peers always show `established`/`formed`), not the
|
||
output of a running protocol stack. There is no convergence, flapping, or
|
||
liveness engine.
|
||
- **Writes are a MIT upsert, not policy resolution** — a POST stores the
|
||
object (plus a small set of modeled reactions: `fabricNodeIdentP` →
|
||
`fabricNode` + supporting MOs, and — since the NDO → APIC deploy mirror,
|
||
§10 — an NDO template deploy materializing its objects into the target
|
||
sites' APIC stores); it does not cascade *arbitrary* dependent policy or
|
||
run the full validation a real APIC's policy engine would.
|
||
- **Faults are seeded, not generated** — `faults.seed` in `topology.yaml`
|
||
is the only source; the sim never raises new faults on its own.
|
||
- **Self-signed TLS only**, intended for local/lab use — not hardened for
|
||
production or internet exposure.
|
||
- **NX-OS SSH / N5K/N7K migration surface is out of scope** — the sim
|
||
covers the ACI/NDO **REST** API only.
|
||
- **Object coverage tracks real consumer needs**, not the full ACI object
|
||
model — see `docs/CONTRACT.md` §6 for the exact 99-class catalog. Classes
|
||
no known consumer queries may be absent; add a builder + topology knob if
|
||
you need more.
|
||
|
||
This is a **test fixture**, not a Cisco product, and is not affiliated with
|
||
or endorsed by Cisco.
|
||
|
||
---
|
||
|
||
## 9. Editing the topology
|
||
|
||
`topology.yaml` is the single source of truth for the whole simulated
|
||
fabric. Edit it and restart (or call `POST /_sim/reload`) to change it. Key
|
||
knobs: sites (nodes, ASN, pod, mgmt IP), border-leaf vPC pairs + CSW
|
||
peering, tenants (VRFs/BDs/APs/EPGs/contracts/L3Outs), ISN, seeded faults,
|
||
endpoint generation count, and access-policy defaults (VLAN pools, physical/
|
||
L3 domains, AAEPs). The loader validates the whole file at once (Pydantic
|
||
v2) and reports every cross-reference error together, so a typo'd BD/VRF
|
||
name fails fast rather than surfacing as a confusing runtime 404 later.
|
||
|
||
## 10. The `/_sim` control API
|
||
|
||
Mounted on each APIC app at `/_sim` for test orchestration (does not exist
|
||
on a real APIC):
|
||
|
||
| Method + path | Effect |
|
||
| --- | --- |
|
||
| `POST /_sim/reset` | Restore the store to its build-time baseline. |
|
||
| `POST /_sim/snapshot/{name}` | Deep-copy the current store under `{name}`. |
|
||
| `POST /_sim/restore/{name}` | Restore a previously taken snapshot. |
|
||
| `POST /_sim/reload` | Re-read `topology.yaml` and rebuild the site from scratch. |
|
||
| `POST /_sim/add-leaf` | Add a `fabricNode` (`{"id": 105}` or auto-numbered). |
|
||
| `POST /_sim/remove-leaf` | Mark a `fabricNode` deleted (`{"id": 105}`). |
|
||
| `POST /_sim/save/{name}` | Persist this plane's store to disk under `{name}` (`SIM_STATE_DIR`). |
|
||
| `POST /_sim/load/{name}` | Restore this plane's store from a prior `save/{name}`. |
|
||
|
||
Typical pattern: `snapshot` a clean state → mutate via writes → assert →
|
||
`restore` (or `reset`) between test cases.
|
||
|
||
**`snapshot`/`restore` vs. `save`/`load`:** `snapshot`/`restore` are
|
||
**in-memory only** — a deep-copy kept in the running process, gone the
|
||
moment the sim restarts; they're the fast within-run rollback used between
|
||
test cases. `save`/`load` are **on-disk** — they serialize the store to a
|
||
JSON file under `SIM_STATE_DIR` (default `~/.aci-sim/state`) and survive a
|
||
full sim restart. Both `save` and `load` exist on **every plane** — each
|
||
APIC site's app and the NDO app all mount their own `/_sim/save/{name}` and
|
||
`/_sim/load/{name}`. `scripts/sim-state.sh {save|restore} <name>` is the
|
||
whole-fabric wrapper: it hits every plane's endpoint in one command (both
|
||
APIC sites' stores + the NDO state), so a full fabric state can be
|
||
snapshotted to disk and restored later in one call instead of one curl per
|
||
plane.
|
||
|
||
**NDO → APIC deploy mirror:** `POST /mso/api/v1/task` (an NDO template
|
||
deploy, the request `cisco.mso.ndo_schema_template_deploy` sends) now
|
||
materializes the deployed multi-site template's VRFs/BDs/ANPs/EPGs and
|
||
domain/static-port binds into the **target sites' APIC stores** — previously
|
||
a pure acknowledgment no-op with no APIC-side effect. A multi-site tenant
|
||
created purely through NDO is now readable back from each target site's
|
||
APIC (including a per-site `fvTenant` shadow, so the tenant itself — not
|
||
just its child objects — shows up), matching how a real NDO-driven deploy
|
||
pushes policy down to the fabrics it targets. See
|
||
`aci_sim/ndo/deploy_mirror.py` for the mirroring logic.
|
||
|
||
## 10a. Query subscriptions + websocket push-on-change
|
||
|
||
The sim implements real APIC's subscription mechanism — the piece the
|
||
"Monitoring/observability integration" use case (§1) needed to move from
|
||
polling-only to event-driven.
|
||
|
||
**1. Subscribe** — add `?subscription=yes` to any of the three query shapes:
|
||
`GET /api/class/{cls}.json`, `GET /api/mo/{dn}.json` (incl. the
|
||
`/api/node/mo/{dn}` alias), and `GET /api/node/class/{...}`. The response is
|
||
the **normal query result, unchanged**, plus a top-level `subscriptionId`:
|
||
|
||
```json
|
||
{"imdata":[...], "totalCount":"3", "subscriptionId":"1"}
|
||
```
|
||
|
||
Without `?subscription=yes`, the response has no `subscriptionId` key at
|
||
all — byte-identical to every query before this feature existed. This is a
|
||
strict backward-compat guarantee, not just a default: nothing about a plain
|
||
query's shape or behavior changed.
|
||
|
||
A class-query subscription (`/api/class/{cls}.json`) watches every MO of
|
||
that class fabric-wide. A DN-scoped subscription (`/api/mo/{dn}.json`,
|
||
node-scoped `/api/node/class/{dn}/{cls}.json`) watches that DN **and its
|
||
entire subtree** — a write to a child DN still notifies a parent-DN
|
||
subscriber, matching real APIC's subtree-subscription behavior.
|
||
|
||
**2. Open the websocket** — `GET /socket<token>`, where `<token>` is the
|
||
same `APIC-cookie` token minted by `aaaLogin` (no separator between
|
||
`/socket` and the token — this matches real APIC's literal path shape). An
|
||
unknown/expired token is rejected before accept. One live connection per
|
||
token; all of that session's subscriptions push over this one socket.
|
||
|
||
**3. Push on change** — after a write (`POST /api/mo/{dn}.json` create/
|
||
update, or `DELETE /api/mo/{dn}.json`) commits, every subscription whose
|
||
scope matches the changed MO(s) gets an event on its owning token's socket:
|
||
|
||
```json
|
||
{"subscriptionId":["1"], "imdata":[{"fvTenant":{"attributes":{"dn":"uni/tn-Corp","status":"created"}}}]}
|
||
```
|
||
|
||
`status` is `created` (new DN), `modified` (existing DN updated), or
|
||
`deleted` (DELETE, or a POST body with `status="deleted"`) — matching real
|
||
APIC's subscription payload shape. A DELETE on a DN with children emits one
|
||
event per removed MO in the subtree, so both class-scoped and DN-scoped
|
||
subscribers see everything that was actually removed.
|
||
|
||
**4. Refresh** — `GET /api/subscriptionRefresh.json?id=<id>` keeps a
|
||
subscription alive past its TTL (90s default, matching real APIC); always
|
||
200, even for an unknown/expired id (real APIC does not error a stale
|
||
refresh — the caller just re-subscribes on its next query).
|
||
|
||
**Lifecycle**: disconnecting the websocket drops that connection's
|
||
subscriptions immediately — a later change that would have matched is
|
||
silently skipped (not an error), same as a client that never subscribed.
|
||
|
||
See `docs/CONTRACT.md`'s subscription section for the exact response shapes
|
||
and `aci_sim/rest_aci/subscriptions.py` for the registry + the sync-
|
||
write-to-async-websocket bridge design (write handlers stay synchronous;
|
||
each websocket connection owns an `asyncio.Queue` that the write path enqueues
|
||
onto via `put_nowait`, decoupling "a write happened" from "bytes went out on
|
||
a socket").
|
||
|
||
## 11. Running the test suite
|
||
|
||
```bash
|
||
.venv/bin/python -m pytest tests/ -q
|
||
```
|
||
|
||
`tests/verify_autoaci.py` is a verification harness replaying real ACI/NDO
|
||
query patterns end-to-end against a running sim instance
|
||
(`scripts/verify.sh`); the rest of `tests/` are unit/integration tests run
|
||
via plain pytest.
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
MIT — see `LICENSE`.
|