F5 fidelity gap: cisco.mso mso_schema/mso_tenant state=absent send
DELETE /mso/api/v1/schemas/{id} and DELETE /mso/api/v1/tenants/{id};
both were unrouted (405), blocking E2E baseline cleanup.
- DELETE schemas/{id}: removes detail + summary (seeded or extra) +
policy-states; 404 on unknown id. No cascade into the APIC deploy
mirror — real NDO orphans deployed objects on schema delete.
- DELETE tenants/{id} (+ bare /api/v1 alias, matching the GET pair):
400 while any schema template tenantId references the tenant,
200 once dereferenced, 404 on unknown id.
- tests/test_ndo_delete.py: 8 tests incl. no-cascade mirror check.
Full suite: 922 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zdUTqU9fvCCvF3uVGZsu1
70 KiB
Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning (pre-1.0: minor bumps may include breaking changes to the sim's behavior).
[0.22.0] - 2026-07-08
Added
- NDO DELETE endpoints — schemas and tenants (F5):
DELETE /mso/api/v1/schemas/{id}removes a schema (detail, summary — seeded or runtime-POSTed — and policy-states record; unknown id → 404) andDELETE /mso/api/v1/tenants/{id}(+ bare/api/v1/tenants/{id}alias, same dual-shape pair as GET /tenants) removes a tenant.cisco.mso.mso_schema/mso_tenantwithstate=absentsend exactly these requests and previously got 405, blocking E2E baseline cleanup. Real-NDO guard: a tenant still referenced by any schema template'stenantIdis refused with a 400 ("referenced by schema(s) …") until the referencing schemas are deleted. Schema deletion deliberately does NOT cascade into the APIC deploy mirror — real NDO does not undeploy on schema delete, so already-deployed objects stay orphaned on the sites' APICs unless the caller undeploys first (POST /mso/api/v1/taskwithundeploy).
[0.21.0] - 2026-07-07
Added
- Write validation — fail like real gear:
POST /api/monow rejects malformed property values with an APIC-style 400 instead of absorbing them into the MIT:fvSubnet/l3extSubnetipmust be a valid address[/prefix],vnsRedirectDestipa valid IP. (Surfaced by a TN2 push whose var file was missing the firewall block: the unguarded J2 template renderedip=".1/24"andip="."and the sim accepted both.) Deletes are exempt — cleanup only needs the DN. query-target=childrenon MO/class queries (real-APIC semantics: the DIRECT children of each matched root as flat imdata, root excluded,target-subtree-classhonored). Previously unsupported and silently answered with empty imdata — a verification false-negative.- Deploy mirror: contract/filter/service-graph shadows — an NDO deploy
now also materializes
vzFilter/vzEntry,vzBrCP/vzSubj(+vzRsSubjFiltAtt, andvzRsSubjGraphAttwhen the NDO contract carries a serviceGraphRelationship) andvnsAbsGraphon every target site's APIC. Previously the mirrored EPGs'fvRsProv/fvRsConswere dangling references (GET /api/class/vzBrCPreturned 0 fabric-wide) — same family as the F1 fvTenant-root gap. Undeploy tears the shadows down.
[0.20.5] - 2026-07-07
Docs
- README: macOS note — the bundled
python3is often 3.9 (below the 3.11 floor); install 3.11+ from python.org or Homebrew first. Verified fresh installs from the public repo on macOS 13 / Intel / Python 3.14 (908 tests green) in addition to Apple Silicon / Python 3.13; classifiers now list Python 3.13/3.14.
[0.20.4] - 2026-07-07
Docs
- README: added a Platforms section — Linux (x86-64/ARM incl. Raspberry Pi OS, Debian, Ubuntu) and macOS (Apple Silicon/Intel), Python 3.11+; both verified with the full test suite (macOS re-verified at v0.20.3: fresh clone from the public repo, 908 tests green). Sandbox mode is Linux/macOS-only (root); Windows untested.
[0.20.3] - 2026-07-07
Fixed
aci-sim graph: border leaves now share the LEAF row (placed to its right), matching autoACI's topology view — the previous separate border-leaf tier under the leaves read as a bogus three-tier "leaf under leaf" hierarchy. README diagram regenerated.
[0.20.2] - 2026-07-07
Fixed
aci-sim graph: the single-row legend (role swatches + link-type samples) clipped off the right edge on narrow diagrams (few nodes) — the SVG width now accounts for the legend's minimum width.
Docs
- README: regenerated the embedded topology diagram with the v0.20 renderer (solid ISN uplinks, link-type legend, physical-links table with /31 point-to-point ISN addressing) + caption updated.
[0.20.1] - 2026-07-07
Docs
- README: added a "What it simulates (and what it doesn't)" positioning note — aci-sim is a management-plane simulator; protocol operational state is materialized from configuration, not computed by running protocols.
[0.20.0] - 2026-07-07
Changed
- Spine-to-ISN underlay is now /31 point-to-point (per the Cisco ACI
Multi-Site design guide) — each spine's ISN-facing VLAN-4 sub-interface
gets its own /31 pair (
172.16.{site}.{2*si}/31, IPN peer.{2*si+1}) instead of the previous shared/24LAN segment with a single.254IPN router.ospfIf.addrand eachospfAdjEppeer reflect the per-link pair; the ISN CSW's LLDP mgmt address is unchanged (a device-level management IP, not an interface address). aci-sim graphnow mirrors the autoACI topology view logic: ISN uplinks are drawn solid (they are physical backbone links, dashes are for overlays), the legend gains link-type entries (fabric link / ISN uplink / vPC peer-link), and a Physical links table is rendered under the diagram — one row per fabric link and per spine ISN uplink, the ISN rows carrying the /31 local/remote IPs.
[0.19.1] - 2026-07-07
Fixed
- ISN sub-interface naming conventions flipped to match real gear (owner
review of the v0.19.0 demo, verified against Cisco docs/real spine CLI):
the ACI spine names its VLAN-4 sub-if after the PORT (
eth1/49.49, cf.Eth1/29.29-style output inshow ip ospf neighbors vrf overlay-1), while the ISN Nexus names its dot1q-4 sub-if after the VLAN (Ethernet1/1.4). v0.19.0 had it backwards (spine.4, CSW bare physical):ospfIf/ospfAdjEpids/dns now use the port-named form and the ISN-CSWlldpAdjEp.portIdVgained the.4suffix.
[0.19.0] - 2026-07-07
Changed
- Spine ISN-facing OSPF interface is now a routed VLAN-4 sub-interface
(
build/underlay.py) — real ACI multi-site spines carry the OSPF address toward the IPN/ISN on a routed sub-interface (e.g.eth1/49.4), not the bare physical port.ospfIf.id/dn now end in.4and carry a newaddrattribute (172.16.{site.id}.{si+1}/24, same/24as the existing172.16.{site.id}.254peer);ospfAdjEpnests under the new sub-if segment. The underlying physical port (eth1/{49+si}) and itsl1PhysIf/ethpmPhysIfare unchanged — LLDP/physical state stays there, matching real hardware. - ISN CSW LLDP neighbor (
build/fabric.py) — each spine's ISN uplink physical port now gets a simulated LLDP neighbor (lldpAdjEp/cdpAdjEp, via the sharedadd_adjacencyhelper) representing the inter-site network switch:sysName=ISN-CSW{site.id},portIdV=Ethernet1/{si+1}, modelN9K-C9364C. Together with the sub-interface change above, this closes the gap where autoACI's Topology "Fabric (physical)" view showedLocal IP=-/Neighbor Port=-for spine ISN uplinks — it now derives a complete row fromospfIf(port + local IP),ospfAdjEp(remote IP), and thislldpAdjEp(CSW-side port). Multi-site fabrics only; single-site fabrics build none of this (unchanged: no OSPF/LLDP toward an ISN that doesn't exist). New/updated assertions intests/test_pr19_tier2.py,tests/test_pr3b_batch1.py,tests/test_pr5_topology_consistency.py.
[0.18.1] - 2026-07-07
Changed
- License: MIT -> PolyForm Noncommercial 1.0.0 — aci-sim stays free for
personal, lab, research, and other noncommercial use; commercial use now
requires the author's permission. (
LICENSE,pyproject.tomlSPDX field, README badge + License section.) - README: added a Buy-Me-a-Coffee badge/link.
[0.18.0] - 2026-07-06
Added
- Runtime tenant-L3Out eBGP session materialization — a tenant L3Out
pushed live via
POST /api/mo(cisco.aci playbooks / aci-py) landsbgpPeerP(config) through the generic write path, which never produced the operational session MOs.writes.pynow reacts to every runtimebgpPeerPby materializingbgpPeer/bgpPeerEntry(operSt=established)/bgpPeerAfEntryunder adom-{tenant}:{vrf}BGP domain on the L3Out's own node — the peer ASN derived from the nestedbgpAsP, the VRF from the L3Out'sl3extRsEctx.tnFvCtxName(skips cleanly when no VRF is bound, or onstatus=deleted). This mirrors whatbuild/l3out.pyalready does for topology-defined L3Outs, and makes per-tenant L3Out BGP session views in API clients (e.g. autoACI's "L3Out BGP" topology table) populate for runtime-pushed tenants. New regression tests intests/test_l3out_bgp_session.py(903 tests total).
[0.17.0] - 2026-07-05
Added
aci-sim query CLASS— a zero-friction, stdlib-only authenticated MIT query helper (aci_sim/cli.py'scmd_query), closing a clean-room onboarding gap: the README's smoke test (curl .../class/fabricNode.json) returned an APIC 403 error envelope, not the fabric, because — like real APIC — the sim requiresaaaLoginbefore any query.aci-sim querydoes theaaaLogin->APIC-cookie->GET /api/class|mo/...dance for you (viaurllib.request/ssl/http.cookies, no new dependency — httpx stays dev-only) and prints a compact table (--jsonfor the raw envelope). Supports--dn DNfor a single-MO query,--host/--user/--passwordoverrides, and a friendly hint ("is the sim running? start it withaci-sim run") on a connection error instead of a traceback.
Fixed
scripts/run.shfailed withModuleNotFoundError: uvicornoutside an activated venv. It invoked barepython(whatever that resolves to on$PATH), unlikescripts/sandbox-up.shwhich already resolves./.venv/bin/python. Nowscripts/run.shuses the samePY="./.venv/bin/python"(falling back topython3) pattern, sobash scripts/run.shworks from a fresh, non-activated shell right afterpip install -e '.[dev]'— nosource .venv/bin/activaterequired.- README quickstart + smoke test now copy-paste-correct. Added a
60-second quickstart (
pip install->aci-sim run->aci-sim query fabricNode) verified end-to-end in a clean-room test; the old smoke test (curlwith no login) is replaced withaci-sim query fabricNode(or the full authenticated curl, shown as an alternative). Also embeds a renderedaci-sim graphtopology screenshot (docs/images/topology.png) near the top and in theaci-sim graphCLI section, and documentsaci-sim queryin §5 with a live sample transcript.
[0.16.0] - 2026-07-05
Changed
- Project renamed
aci-fabric-sim->aci-sim. Import packageaci_fabric_sim->aci_sim, PyPI distribution name, GitHub repo, and theaci-simCLI entry point are now all unified under one name (previously the package and CLI wereaci-sim/aci_simwhile the repo/distribution metadata still saidaci-fabric-simin places). No behavior change — every module path underaci_sim/and every documented command are unchanged; this is a naming-consistency cleanup ahead of the repo going public.
[0.15.0] - 2026-07-05
Added
- NDO -> APIC deploy mirror.
POST /mso/api/v1/task(the deploy/undeploy requestcisco.mso.ndo_schema_template_deploysends) was a pure acknowledgment no-op — it never materialized the deployed schema template's VRFs/BDs/ANPs/EPGs/binds into the target sites' APIC stores, so a multi-site tenant created purely through NDO never appeared on the APIC side.mirror_template_to_sites()(aci_sim/ndo/deploy_mirror.py) closes this gap: given the NDO state, a template name, and a site_id -> ApicSiteState map, it walks the owning schema's template plus each associated SITE entry (host-based-routing overlay, domain associations, static ports) and upserts the equivalentfvCtx/fvBD/fvAp/fvAEPg(+ children) MOs into every target site'sMITStore, reusing the same DN/attribute conventions the boot-time builders and the direct POST-write path already use. Also creates a per-sitefvTenantshadow so the tenant itself — not just its child objects — is visible on each target site's APIC after an NDO-driven deploy (previously only the site passed via--apicin ad hoc testing got the tenant; the other site'sfvTenantwas missing entirely). fvBDcreate-time default attributes. A POSTed BD now carries the same default attribute set a real APIC assigns at creation (epMoveDetectMode="garp",hostBasedRouting="no",mtu="9000", …) instead of only the attributes the caller explicitly set — so host-based-routing and EP-move-detection now show up correctly in the APIC BD view for a BD created via a plain POST, matching real-APIC behavior.
[0.14.0] - 2026-07-05
Added
- File-backed state persistence. New
POST /_sim/save/{name}andPOST /_sim/load/{name}routes on both the APIC plane (aci_sim/control/admin.py) and the NDO plane (aci_sim/ndo/app.py) serialize/restore each plane's store to/from disk underSIM_STATE_DIR(default~/.aci-sim/state) — unlike the existing in-memorysnapshot/restore, this state survives a full sim restart.scripts/sim-state.sh {save|restore} <name>is the whole-fabric wrapper: it derives each plane's address the same wayscripts/sandbox-up.shdoes and hits every plane's endpoint (both APIC sites + NDO) in one command. New moduleaci_sim/control/persist.pyimplements the serialization: aMITStoreflattens to a JSON list of{class, attrs}dicts (MOs stored in the MIT are always childless — the parent/child hierarchy lives only in the store's own index — so replaying them throughMITStore.addfaithfully rebuilds it);NdoState's persisted fields are deep-copied to a JSON-friendly dict and reapplied onto the same live object viasetattr(the running NDO app closes over that object's identity, so a reload must mutate in place rather than swap in a new instance).
[0.13.2] - 2026-07-04
Fixed
- NDO
serviceNodeRefresolved to a string ref (was a dict). The service-graph -> contract bind path storedserviceNodeRefas a dict-form ref while every other mirror/lookup path expects the canonical NDO string ref shape (/schemas/{id}/templates/{tmpl}/{category}/{name}); re-running the bind against an already-bound contract was not idempotent because the two representations never compared equal. Fixed to resolve/store the string form, matching the rest of the NDO ref conventions.
[0.13.1] - 2026-07-04
Fixed
pip installwas completely broken — now fixed (critical, packaging). A from-scratch clean-venv E2E revealed thatpip install aci-sim/pip install .produced an unusable package: (1) zero runtime dependencies were declared to pip —pyproject.tomlhaddynamic = ["dependencies"]with no[tool.setuptools.dynamic]source, so fastapi/uvicorn/pydantic/pyyaml were never installed (the CLI crashed on the first third-party import,import yaml); and (2) every subpackage was omitted —packages = ["aci_sim"]shipped only the top-level module, droppingtopology/build/rest_aci/ndo/mit/runtime/query/control. The dev workflow (pip install -e .+pip install -r requirements.txt) masked both. Fix: static[project.dependencies](fastapi/uvicorn[standard]/pydantic/pyyaml), a[project.optional-dependencies].devextra (httpx/pytest/pytest-asyncio), and[tool.setuptools.packages.find] include = ["aci_sim*"]to ship every subpackage. Verified end-to-end: a fresh venvpip install .now yields a workingaci-simCLI + importable package.
[0.13.0] - 2026-07-04
Added
- Admin account in the setup wizard +
aci-sim runcredential enforcement.aci-sim init's new Step 0: Admin account prompts for the fabric admin username/password (defaultadmin/cisco) and whether NDO shares the APIC account (default yes), writing them into a new topology.yamlauth:section (schema.Auth:username/password+ optionalndo_username/ndo_password).aci-sim runresolves these intoSIM_USERNAME/SIM_PASSWORDfor the supervisor via the newcli.resolve_admin_credentials(), so the APIC plane then ENFORCES them (aaaLoginreturns 401 on a username/password mismatch). Precedence (highest first): an explicitSIM_USERNAME/SIM_PASSWORDalready in the environment wins (the CI/override escape hatch), else the topologyauth:section, elseauth.py's ownadmin/ciscodefault.aci-sim runprints a one-line auth-source banner at startup (the username + where it came from, never the password). - The NDO plane stays lenient by design —
/loginand/api/v1/auth/loginaccept any credential and never validate the bearer token (cisco.mso/aci-py client compatibility). The shared account is recorded/reported but not enforced there; a splitndo_username/ndo_passwordis informational.
Notes
- Purely additive / backward-compatible: a topology.yaml with no
auth:section still validates (Topology.auth: Auth | None = None) andaci-sim runinjects nothing — behavior is identical to 0.12.0. - 24 new tests (
tests/test_admin_account.py) cover the wizard'sauth:output (defaults / custom / split-NDO),resolve_admin_credentialsprecedence,build_run_env_argvinjection + filesystem purity, end-to-endaaaLoginenforcement of a rotated password (incl. the stale default now failing) with NDO still lenient, and no-auth:backward-compat.
[0.12.0] - 2026-07-04
Added
- LLDP/CDP neighbor fidelity — enriched adjacencies + materialized container MOs + CLI.
lldpAdjEp/cdpAdjEppreviously carried onlysysName/mgmtIp; both are now enriched with real-APIC-shaped fields —lldpAdjEpgainsportIdV(the NEIGHBOR's own port),portIdT,chassisIdT,chassisIdV(a deterministic per-node MAC via newbuild/neighbors.py:node_mac),sysDesc("{model} running {version}"for a switch neighbor,"Cisco APIC"for a controller),capability,id,ttl;cdpAdjEpgainsdevId(==sysName),portId,platId,ver,cap. Purely additive —sysName/mgmtIpare unchanged, so every pre-existing consumer (autoACI, this repo's own tests) keeps working untouched. New container classeslldpInst/lldpIf/cdpInst/cdpIfare now materialized (one*Instper switch node with any adjacency, one*Ifper adjacency-bearing local port), fixing a bug where a deep-root subtree query (GET /api/mo/topology/pod-1/node-101/sys/lldp/inst.json? query-target=subtree&target-subtree-class=lldpAdjEp) returnedtotalCount=0— the query engine'srun_mo_queryshort-circuits to([], 0)when the root DN has no MO of its own, and.../sys/lldp/instpreviously had none./api/class/lldpInst.json/lldpIf.json/cdpInst.json/cdpIf.jsonnow also return real results. All three adjacency-construction sites (build/cabling.pyspine<->leaf links,build/fabric.pyAPIC<->leaf attachment,rest_aci/writes.py's dynamic node-registration reaction) now share one helper module,build/neighbors.py(add_adjacency/add_switch_adjacency+ensure_lldp_containers/ensure_cdp_containers), so a dynamically-registered node (via POSTfabricNodeIdentPor/_sim/add-leaf) gets the identical enriched/materialized treatment a boot-time-seeded node gets — verified bytests/test_lldp_cdp_fidelity.py's dynamic-path tests. New CLI subcommandaci-sim lldp [topology] [--site NAME] [--node ID] [--cdp] [--json]prints ashow lldp neighbors-style grouped table (or CDP with--cdp, or structured JSON with--json).tests/test_lldp_cdp_fidelity.py(20 tests): container materialization, the subtree-query regression fix, bidirectionalportIdVconsistency across a spine<->leaf link,cdpAdjEp.devId == sysName, backward-compat field presence, the dynamic-registration path, and the CLI (text/--json/--cdp/a graceful non-existent node id).
[0.11.0] - 2026-07-04
Added
- APIC query subscriptions + websocket push-on-change. Real APIC's subscription mechanism,
the piece the "Monitoring/observability integration" use case (README §1) needed to move past
polling-only.
GET /api/class/{cls}.json,GET /api/mo/{dn}.json(+/api/node/mo/{dn}alias), andGET /api/node/class/{...}all now accept?subscription=yes, returning the unchanged query result plus a top-levelsubscriptionIdstring (opt-in only — a plain query with no?subscription=yesis byte-identical to before this feature, no key added, no behavior change).GET /socket<token>(real APIC's literal path shape,<token>= theAPIC-cookiesession token) opens a websocket; when a subscribed class or DN/subtree changes viaPOST/DELETE, a{"subscriptionId":[...],"imdata":[{<class>:{"attributes":{...,"status":"created|modified| deleted","dn":...}}}]}event pushes over that connection.GET /api/subscriptionRefresh.json? id=<id>keeps a subscription alive past its 90s TTL (matching real APIC), always 200 even for an unknown id. Disconnecting the websocket drops that connection's subscriptions. New moduleaci_sim/rest_aci/subscriptions.py(in-memory registry + notify — module-level state, same pattern asauth.py's session store); write handlers stay fully synchronous and simply callnotify(), which enqueues onto each live connection's ownasyncio.Queue— the websocket route drains that queue independently, so a write request never awaits network I/O on some other client's socket.tests/test_subscriptions.py(12 tests): subscribe returnssubscriptionId; a plain query has zero shape change; websocket created/modified/deleted push for a subscribed class; an unsubscribed class does not push; DN-scoped subscriptions match subtree writes; refresh returns 200 (known and unknown id); an unknown-token websocket is rejected; disconnect cleans up subscriptions. See README §10a anddocs/CONTRACT.md§2a for the full contract.
[0.10.0] - 2026-07-04
Added
- OOB management gateway — a real MO instead of a discarded value. Every node already got
topSystem.oobMgmtAddr(the OOB IP), but no gateway was modeled anywhere;aci-sim init's wizard even asked for one per site, but discarded the answer (no schema field, no MO — just an informational summary line). Real APIC models the OOB address + gateway in the specialmgmttenant (uni/tn-mgmt/mgmtp-default/oob-default, amgmtOoBEPG) with onemgmtRsOoBStNodechild per node. NewSite.oob_gateway: Optional[str]schema field (optional, defaultNone, validated as a real IP address) plus a new builderaci_sim/build/mgmt.py(registered inbuild/orchestrator.py) that emits the minimal faithful scaffolding for every site:fvTenant(mgmt) →mgmtMgmtP(default) →mgmtOoB(default) → onemgmtRsOoBStNode(tDn,addr,gw) per node (switches AND controllers),addr= the node's OOB IP + mask (mask fromfabric.oob_subnetif set, else/24by default — deliberately notfabric.oob_pool's wider/16, which spans multiple pods/sites),gw=site.oob_gateway(empty string, never an invented default, when unset). aci-sim init's wizard now WRITES each site's answered OOB gateway intotopology_dict["sites"][i]["oob_gateway"]instead of discarding it;aci-sim showsurfacesoob_gateway=<value|->per site.tests/test_oob_gateway.py(25 tests): schema default/validation, builder MO shape (mgmtRsOoBStNode per node, addr/gw correctness, mask derivation, empty-gw-when-unset), wizard write-through (defaults run, custom answers, multi-site per-site gateways), and backward compatibility (repotopology.yamlbuilds unchanged, existingtopSystem/fabricNodeattrs untouched, new mgmt-tenant scaffolding is purely additive).
Changed
docs/CONTRACT.md: new "Mgmt tenant — OOB management" entry catalogingfvTenant(mgmt)/mgmtMgmtP/mgmtOoB/mgmtRsOoBStNode.README.md§6c (Tier-3 config reference) documentssite.oob_gateway; §5aci-sim show/aci-sim initsample output updated to includeoob_gateway=....
[0.9.0] - 2026-07-04
Added
aci-sim graph— self-contained SVG/HTML topology diagram. Newaci_sim/graph.pyrenders the built fabric (spines, leaves, border leaves with vPC-pair grouping, per-site controllers, and — for multi-site topologies withisn.enabled: true— a single ISN cloud node with spine→ISN uplinks) as hand-generated, dependency-free SVG: no CDN, no local server, no npm build step.-o FILE.htmlwraps the SVG in a minimal inline-CSS HTML shell (double-click to open in any browser);-o FILE.svgwrites raw SVG. Layout is a hand-computed tiered ACI hierarchy (spine row → leaf row → border-leaf row → controller row per site, sites placed side by side, ISN cloud centered above/between them for multi-site) plus a role→color legend and a title bar (fabric name, site count, node count). Purely additive: no change totopology/schema.pyor anybuild/*.pybuilder —graph.pyonly reads the validatedTopologymodel and re-derives the same spine×leaf meshbuild/cabling.py::cabling_links()produces, so the diagram always matches the fabric that would actually be built.- Visual language matched to autoACI's topology view (studied read-only on autoACI's
frontend/src/composables/useTopologyStyles.js+backend/routers/topology.py; NOT imported — autoACI's implementation is Vue+cytoscape, this is a standalone Python reimplementation of the same colors/hierarchy): spine =#2563ebblue, leaf/border-leaf =#16a34agreen, controller =#d97706amber, ISN cloud =#475569slate; rounded-rect nodes with a subtle drop shadow; cabling links solid gray, ISN links dashed orange, vPC-peer links dotted slate. - Complements
aci-sim show(text inventory) with a visual counterpart —showfor machine/human text detail,graphfor at-a-glance topology shape. tests/test_graph.py(18 tests): node/link counts against the repo's default 2-site topology, ISN-cloud presence for multi-site and absence for a generated 1-site topology, a generated 3-site topology rendering all 3 sites, role-color presence, well-formed-XML checks viaxml.etree, andaci-sim graphCLI coverage (HTML/SVG output, default paths, missing-file and bad-extension errors) — no browser required anywhere in the suite.
[0.8.1] - 2026-07-04
Added
- CI gate example + Use Cases docs. New
examples/ci/:assert_mit.py(a tiny MIT-end-state assertion helper —class:/mo:/absent:/attr:checks, exit code = failed-check count, works against the sim OR a real APIC) andgithub-actions-aci-gate.yml(copy-pasteable start-sim → run-automation → assert workflow). README §1 gains a "More applications" table (SDK/provider testing, change pre-flight, REST-contract testing, multi-tool interop, monitoring, scale) with honest ready/partial/extension status, plus an explicit out-of-scope note.
[0.8.0] - 2026-07-04
Added
aci-sim init— interactive APIC-setup-style topology wizard (PR-22). Modeled on the real APIC first-boot setup dialog: prompts each field with a suggested default shown in[brackets](ENTER accepts it, or type a custom value), then writes atopology.yamland auto-runsaci-sim validateon the result. Flow: Step 0 deployment type (1) Single fabric/2) Multi-site— real APIC's "multi-pod" choice is deliberately NOT offered, since this sim builds no true multi-pod fabric); Step 1 per-site fields (name, site ID, BGP AS, pod ID, controller count 1-5, APIC OOB mgmt IP, then — if controllers > 1 — one prompt per additional controller each suggesting the previous IP + 1 viaipaddress.ip_address(prev) + 1, mapping to PR-21'sSite.controller_ips; OOB gateway asked once per site; TEP pool/infra VLAN/GIPo pool/OOB subnet/spine-leaf-border counts); Step 2 multi-site-only (NDO mgmt IP, ISN OSPF area/MTU); Step 3 summary + confirm + write + auto-validate. Reusesgenerate_topology()— the SAME generatoraci-sim newcalls — as a thin interactive front-end; no topology-emission logic is duplicated.- Non-interactive wizard modes:
--defaultsaccepts every suggested default without prompting, and is also auto-enabled whenever stdin is not a TTY (soaci-sim initnever hangs in CI/scripts even without the flag).--answers FILEaccepts a YAML or JSON file of pre-filled field-name -> value answers; listed fields are used verbatim, unlisted fields still prompt/default as usual — enables fully scripted, reproducible topology generation (e.g.site1_controllers: "3"to script a specific cluster size). generate_topology()widened (PR-22, additive, backward compatible): new optionalsite_overridesparameter — alist[dict](one entry per site) shallow-merged onto each auto-derived site definition (name/id/apic_host/mgmt_ip/fabric_name/asn/pod/controllers/controller_ips) — lets a caller (the wizard) express per-site heterogeneity, including PR-21'scontroller_ips, which the existing flat--controllers/--apic-ip-baseflags cannot express per-site.leaves_per_site/spines_per_site/border_pairsnow also accept alist[int](one count per site) in addition to the pre-existing singleintapplied uniformly to every site — needed because the wizard asks spine/leaf/border-pair counts per site. Both widenings are fully backward compatible: every pre-PR-22 caller (includingaci-sim newand every existing test) passes a plainint/omitssite_overrides, and gets byte-identical output to before.
Changed
- CLI help/argparse: new
initsubcommand registered alongsidevalidate/show/run/new.
[0.7.0] - 2026-07-04
Changed
- BREAKING (node counts):
Site.controllersdefault 3 → 1 — single-APIC-per-site by design (PR-21). APIC cluster size is irrelevant to Ansible playbook deployment testing (a playbook targets exactly one APIC endpoint); cluster size only matters for cluster-health/appliance-vector tooling such as autoACI'shealth_score.py.controllersremains fully configurable, now range-validated 1-5 (Topology.normalize_and_validate) — setcontrollers: 3on a site to restore the old default / exercise multi-controllerinfraWiNode/cluster-health semantics. This changes every site's generatedfabricNode/topSystemcount (−2 per site at the new default vs. the old one),fabricLink/lldpAdjEp/cdpAdjEpcounts for the APIC↔leaf attachment, andinfraWiNodecount (controllers²). All existing test assertions and e2e scripts encoding the old counts were recomputed and updated — seedocs/DESIGN.md's "PR-21" section for the full before/after accounting. aci-sim new's--controllersflag andgenerate_topology()'scontrollersparameter both default to1now (was3), matching the newSite.controllersschema default.- The shipped root
topology.yaml's commented# controllers: Nexample line now shows the current default (1) instead of the pre-PR-21 default (3); a new commented block shows thecontrollers: 3+controller_ips: [...]opt-in shape for multi-controller topologies.
Added
Site.controller_ips: Optional[list[str]]— explicit per-controller OOB mgmt IPs (PR-21). When set,controller_ips[i]is controlleri+1'stopSystem.oobMgmtAddrverbatim (length must equalcontrollers, each entry a valid IP — validated). When unset (default), OOB IPs are auto-derived sequentially fromsite.mgmt_ip: controller 1 =mgmt_ip, controller 2 =mgmt_ip+1, controller 3 =mgmt_ip+2, etc., via the newSite.controller_oob_ips()helper (topology/schema.py). Falls back to the legacy per-nodeoob_ip(pod, cid)scheme whenmgmt_ipis also unset, so a topology setting neither field keeps byte-identical controller OOB addresses to pre-PR-21. Wired intobuild/fabric.py's controllertopSystem.oobMgmtAddrand the leaf-sidelldpAdjEp/cdpAdjEp.mgmtIp(kept consistent — both read the same per-call derived list). Surfaced inaci-sim show/show --jsonascontroller_ips. The sim's REST server still binds to exactly one address per site (mgmt_ip, controller-1 / the cluster VIP) regardless ofcontrollers— it never runs N separate APIC processes;controllers/controller_ipsonly affect the simulated fabricNode/topSystem/infraWiNode data.- README "Configuration" section (§6a) now documents the single-APIC design intent explicitly, and a new
docs/DESIGN.md"PR-21" section covers the full before/after node-count accounting and verification.
[0.6.0] - 2026-07-04
Added
- Tier-3 fine-tuning parameters (PR-20) — the last planned param tier.
New optional
topology.yamlfields — all defaulted to pre-existing hardcoded behavior (or the real ACI factory default where no prior hardcoded value existed), so the existing repotopology.yamlvalidates and builds unchanged:bd.mac(per-BD, optional) andfabric.default_bd_mac(fabric-wide, default"00:22:BD:F8:19:FF"— the exact literalbuild/tenants.pyalready hardcoded for every BD pre-PR-20) — now threaded intofvBD.mac, replacing that hardcode. Resolution order:bd.mac or fabric.default_bd_mac. Byte-identical output when both are unset.l3out.csw_peer.keepalive/.hold(defaults60/180, real ACI defaults) — threaded intobuild/l3out.py'sbgpPeerP.keepAliveIntvl/holdIntvl(previously not carried on that MO at all).holdmust exceedkeepalive.isn.ospf_hello/.ospf_dead(defaults10/40, real ACI defaults) — threaded intobuild/underlay.py'sospfIf.helloIntvl/deadIntvl(previously not carried on that MO at all).ospf_deadmust exceedospf_hello.isn.bfd_min_rx/.bfd_min_tx/.bfd_multiplier(defaults50/50/3, real ACI defaults) — store + range-validate + surface only. This sim builds nobfdIfP-style MO anywhere (greppedbuild/*.py: zero matches for "bfd"), so these are explicitly store-only, documented as such inaci-sim show's own output.fabric.oob_subnet(validate-only; must fall within192.168.0.0/16) andfabric.inb_subnet(store-only; must fall within a private RFC1918 range) —oob_ip()still derives every node's OOB address from(pod, node_id)only (documented pre-existing limitation, unchanged); this sim has no in-band mgmt MO at all, soinb_subnetis explicitly store-only.fabric.default_apic_version(fabric-wide override; defaultNone= unchanged behavior) — takes precedence over each site's ownsite.apic_version(PR-9/PR-18 lineage, already fully wired and already independent of switch-nodeNode.version) inbuild/fabric.py's controllerfabricNode/topSystem/firmwareCtrlrRunning. Finding while implementing this PR:site.apic_versionalready drove those MOs independently of switch version before this PR — the actual gap closed here is (1) a fabric-wide override knob and (2)aci-sim shownever surfacedapic_versionat all pre-PR-20 (now shown per-site as the resolved effective value).aci-sim newgained--isn-ospf-hello,--isn-ospf-dead,--isn-bfd-min-rx,--isn-bfd-min-tx,--isn-bfd-multiplier,--default-bd-macflags;aci-sim shownow printsoob_subnet/inb_subnet(labeled store-only)/default_bd_mac,default_apic_version(when set), ISNospf_hello/ospf_dead, ISN BFD timers (labeled store-only), and each site's effectiveapic_version.- New tests:
tests/test_pr20_tier3.py(41 tests). Full suite: 713 passed (672 pre-existing + 41 new), zero regressions. - See
docs/DESIGN.md's "PR-20 — Tier-3 fine-tuning parameters" section for the full design rationale and an explicit statement of which backward-compat verification was performed in this sandboxed development environment vs. deferred to the main thread's independent production-chain re-run (no SSH access to the hardware test host from this environment).
[0.5.0] - 2026-07-04
Added
- Tier-2 topology-elasticity parameters (PR-19). New optional
topology.yamlfields — all defaulted to pre-existing hardcoded behavior, so the existing repotopology.yamlvalidates and builds unchanged:isn.ospf_area(default"0.0.0.0", accepts dotted-decimal or decimal-integer notation) andisn.mtu(default9150, range 576-9216) — now threaded intobuild/underlay.py'sospfIf/ospfAdjEpareaandbuild/interfaces.py's dedicated ISN/IPN spine uplinkl1PhysIf.mtu, replacing hardcoded"0.0.0.0"/9216literals at those specific call sites. Ordinary fabric ports keep their existing 9216 MTU.access.vmm_domains[](default: empty list) — a VMM domain model (vcenter_ip,vcenter_dvs,vlan_pool,datacenter) that builds avmmDomP(+vmmCtrlrP/vmmUsrAccPwhenvcenter_ipis set, +infraRsVlanNswhenvlan_poolis set) inbuild/access.py. Verified (read-only, hardware test host) that this is a pure addition: the real aci-pybind_epg_to_vmm_domainMS-TN1 chain writes an NDO schemadomainAssociationsentry referencing the domain by DN string only — no existing builder or production chain reads avmmDomP/vmmCtrlrPobject today, hence the empty-list default (unlikephys_domains/l3_domains/aaeps, which each seed one entry). Seedocs/DESIGN.mdPR-19 section for the full verification trail.- Leaf/spine count elasticity bug fix:
aci-sim new's node-ID generator (generate_topology()) had no guard against--leaves-per-site/--spines-per-siteexceeding the node-ID scheme's documented ~100-per-site ceiling, or--border-pairspushing the border block past the next site's leaf range — all three previously produced atopology.yamldict that failed deep insideTopology.normalize_and_validatewith a generic "node ids collide" error. Now rejected at generation time with a clear message naming the offending flag. Verified:--leaves-per-site 8 --spines-per-site 4(and--border-pairs> 1) validate and build clean; the exact boundary (100 leaves/spines, 49 border-pairs at default counts) still works; 101/50 respectively are cleanly rejected. isn.peer_mesh: partialre-examined and kept as a documented validate-and-reject (unchanged decision from PR-17/18) — no sensible default partition exists without additional per-site config the schema doesn't carry.aci-sim newgained--isn-ospf-area,--isn-mtuflags;aci-sim shownow prints ISN enabled/peer_mesh/ospf_area/mtu and any configured VMM domains.- New tests:
tests/test_pr19_tier2.py(37 tests) + 6 additions totests/test_cli.py.
[0.4.0] - 2026-07-04
Added
- Tier-1 fabric configuration parameters (PR-18). New optional
topology.yamlfields — all defaulted to the pre-existing hardcoded behavior, so the existing repotopology.yamlvalidates and builds unchanged:fabric.tep_pool(default"10.0.0.0/16"),fabric.infra_vlan(default3967, range 1-4094),fabric.gipo_pool(default"225.0.0.0/15") — stored, CIDR/range-validated, and surfaced viaaci-sim show/aci-sim new. No builder currently derives a per-node address or VLAN attribute from these (seedocs/DESIGN.mdPR-18 section) — store-only until a future PR wires them into a builder.site.controllers(APIC cluster size, default 3) andsite.pod(default 1) already existed and were already fully wired through every relevant builder (fabricNode/topSystem/infraWiNode/firmwareCtrlrRunning/health/endpoints); PR-18 verified this end-to-end and addedaci-sim new --controllers N.- Deterministic non-empty node serials:
build/fabric.py's newdefault_serial(site_id, node_id)(f"SAL{site_id}{node_id:04d}") replaces the previous inlineSIM{node.id:08d}fallback used whenever an auto-generatedNode.serialis empty, sofabricNode.serial/fabricNodeIdentP.serialalways carry a real, deterministic serial forcisco.aci.aci_fabric_nodeto register against. Explicit YAMLserial:values still override. aci-sim newgained--controllers,--tep-pool,--infra-vlan,--gipo-poolflags;aci-sim shownow prints each node's resolved serial and each site's controller count, plus the fabric-wide tep_pool/infra_vlan/gipo_pool.- Verified N-site behavior: a 3-site scaffold (
aci-sim new --sites 3) builds with zero node-ID collisions and per-siteinfraWiNodecounts matching the configured controller count. Documented limit: ISN inter-site peering and stretched-tenant HOME/REMOTE endpoint placement still rely onTopology.other_site(), which is 2-site-only semantics (pre-existing, not introduced by this PR) — with 3+ sites, ISN is not a true full mesh. See README §6a / §8 anddocs/DESIGN.mdPR-18 section. - New tests:
tests/test_pr18_tier1.py.
[0.3.0] - 2026-07-04
Added
aci-simtopology CLI (PR-17). A new console script (aci_sim/cli.py,[project.scripts] aci-sim) that makestopology.yamleasier to author, validate, preview, and run — purely additive: it does not change the topology schema, builders, or NDO/REST code, andtopology.yamlremains the single source of truth. Four subcommands:aci-sim validate [TOPOLOGY]— runs the sameload_topology()/Pydantic validation the sim already performs at startup, printing a concise pass/fail summary (sites, leaves/spines/border-leaves per site, tenant count) instead of a raw traceback. This is the CI gate.aci-sim show [TOPOLOGY]— prints the derived inventory (not just an echo of the YAML): per-site APIC host/mgmt IP/ASN/pod/fabric name, a table of every node with its derived loopback/OOB IP (viabuild/fabric.py'sloopback_ip/oob_ip), the NDO mgmt IP, and the actual host:port each controller will listen on (port mode or sandbox mode, read fromruntime/config.py).--jsonfor machine-readable output.aci-sim run [TOPOLOGY]— a thin wrapper aroundpython -m aci_sim.runtime.supervisor: validates the topology up front, generates self-signed certs on first run (mirrorsscripts/gen_certs.sh), setsTOPOLOGY_PATH, passes throughSIM_BIND/SIM_SANDBOX/etc. from the environment, and execs the supervisor —scripts/run.shis unchanged and keeps working.aci-sim new— scaffolds a fresh, minimal, validtopology.yaml(to stdout or-o FILE) from flags (--sites,--leaves-per-site,--spines-per-site,--border-pairs,--asn,--fabric-name-prefix,--ndo-ip,--apic-ip-base), using the same node-ID scheme documented intopology/schema.py(leaves base 101 +200/site, spines base 201 +200/site, border leaves placed outside both ranges in vPC pairs). The generated file always passesaci-sim validateand boots.- New
tests/test_cli.py(20 tests): validate positive/negative (duplicate node id, bad CIDR), show text +--jsonasserting derived IPs for a known node,new --sites 3 --leaves-per-site 4round-tripped through the real Pydantic model, andrun's env/argv construction via a purebuild_run_env_argvhelper (no live socket binding in tests). - README gets a new "## 5. CLI" section (subsequent sections renumbered
6-11);
deploy/README.md/Quickstart noteaci-sim runas the preferred entrypoint alongsidescripts/run.sh.
[0.2.7] - 2026-07-04
Fixed
- NDO site-local BD duplication — mirror vs site-module dedup (PR-16).
aci_sim/ndo/patch.py::_mirror_template_bd_to_sites(PR-15) auto-creates a site-local BD shadow with a full-path STRINGbdRefthe moment a template BD is added. Separately,mso_schema_site_bd.py's "BD does not exist yet" branch PATCHesadd /sites/{siteId}-{template}/ bds/-(the RFC-6902 append form) with a DICT-shapedbdRef. The append branch ofapply_json_patchnever consulted_find_by_name/ref resolution before appending, and_find_by_name's own ref-lookup only recognized the string ref shape — so the two writers never converged on one entry, andsites[].bds[]ended up with two rows for the same template BD (one empty mirror, one carrying the real subnet) — confirmed against a reference NDO schema (App1-Net-LAB0, Acme App1-Net tenant) aftercreate_tenant+create_bd. Fixed by deduping a nameless site-local shadow strictly by its single IDENTITY ref (bds→bdRef,anps→anpRef,epgs→epgRef,contracts→contractRef;_IDENTITY_REF_BY_COLLECTION), resolved in both string and dict shapes via a new_bare_ref_name()helper. A new_find_shadow_by_identity()is the append-path dedup;_find_by_name's ref-lookup now fires only for nameless items and only over identity refs. A NAMED object (a template-level BD add) is matched only by name/displayName and always appends. IMPORTANT — the first cut of this fix iterated all*Refkeys (includingvrfRef), which collapsed every template's BDs into a single entry because distinct BDs share onevrfRef(vrf-L3_LAB0); an independent full-chain Acme run caught it (create_bdfailed"Provided BD 'bd-App2-Net-...' does not exist"). The shipped fix restricts ref-matching to identity refs of nameless shadows only — guarded bytest_distinct_template_bds_sharing_vrfref_do_not_collapse. Verified end-to-end on real ACI hardware: Acme App1-Netcreate_tenant.yml/create_bd.ymlbothfailed=0; live NDO schemaApp1-Net-LAB0shows template BD countsLAB1-LAB2=6,LAB1=14,LAB2=10(30, no collapse) and exactly one site-local BD entry per template BD (no duplicate). Covers the analogous ANP/contract site-mirror paths too — no regression against the PR-12/PR-13 mirror suites. 9 tests intests/test_pr16_bd_mirror_dedup.py.
[0.2.6] - 2026-07-04
Added
- Domain-qualified
aaaLoginname + template-BD site mirroring for aci-py (PR-15). Two confirmed sim gaps from a realaci-py(pure-Python Ansible-to-ACI compiler) run against the hardware sandbox:- APIC login rejects a domain-qualified login name.
aci-pypostsPOST /api/aaaLogin.jsonwithaaaUser.attributes.name = "apic:local\admin"— a valid real-APIC login format (apic:<loginDomain>\<username>). The sim'saaaLoginhandler (aci_sim/rest_aci/auth.py) exact-matched the rawnameattribute againstSIM_USERNAME, so every domain-qualified login 401'd even with the correct password — confirmed live against the sandbox (name="apic:local\admin"-> 401,name="admin"-> 200 before this fix). Added_bare_username(), which strips an optionalapic:<domain>\/<domain>\prefix before the credential compare, so both the plain and domain-qualified forms authenticate identically; a wrong username in either form still 401s. - NDO schema PATCH 400 mid
create_tenant/create_bd.aci-py'smso_schema_site_bdshim always PATCHes a site-local BD shadow withop: replace(neveradd), because real NDO 4.x auto-creates that shadow ("BDDelta") the moment a template-level BD is added to a template that already has sites attached — a freshaddthere 409s on real hardware, so the shim does a GET-then-replace instead (its own docstring documents this real-hardware behavior). The sim never mirrored a template BDaddintosites[].bds[](unlike ANPs/EPGs since PR-12 and contracts since PR-13 — the same "auto-mirror on template add" pattern, just missing the BD collection), so the site-BDreplace400'd with"replace: 'bd-FW_LAB0' not found at '/sites/1-LAB1-LAB2/bds/bd-FW_LAB0'"— confirmed verbatim against a real hardware aci-py MS-TN2 run before this fix. Added_mirror_template_bd_to_sites(aci_sim/ndo/patch.py, PATCH-time, parallel to_mirror_template_anp_to_sites/_mirror_template_contract_to_sites) and the equivalent boot-time mirror inbuild_ndo_model(aci_sim/ndo/model.py), so a topology.yaml tenant that ships with BDs already configured also boots with the site shadow pre-mirrored.
- VERDICT: both gaps are genuine sim fidelity gaps, not aci-py bugs — aci-py's domain-qualified login name and its GET-then-replace site-BD shim both mirror documented real-APIC/real-NDO behavior.
- Confirmed end-to-end on real ACI hardware: aci-py's
live_chain.pydriver againstenvironments/lab-multisite-simnow reaches MS-TN2: 13/13 steps rc=0 (was 9 ok / 4 fail before this PR — the 4 failures were exactly the login 401s and thereplace: 'bd-*' not found400) and MS-TN1: 11/11 rc=0 (1 skip, no regression).
- APIC login rejects a domain-qualified login name.
- 13 new tests in
tests/test_pr15_acipy_gaps.pycovering both plain and domain-qualified (apic:local\adminandlocal\admin) aaaLogin forms, a domain-qualified wrong-user/wrong-password still-401 check, the template-BD-add-mirrors-into-site PATCH behavior (single site, multiple sites, idempotency, non-associated-site isolation), the full add-then-replace sequence aci-py actually emits, and the boot-time mirror for topology.yaml tenants that ship with BDs already configured.
[0.2.5] - 2026-07-04
Added
- Classic ND bare
/login+/logoutendpoints (aci-py, cisco.nd) (PR-14). aci-py's NDO connector and thecisco.ndhttpapi plugin POST to the classic Nexus Dashboard bare/login(body{userName, userPasswd, domain}, JWT intoken/jwttoken) alongside the newer/api/v1/auth/login. The sim only had the latter, so aci-py 404'd on every NDO login. Mirrored the existing auth handler at bare/login+/logout.
[0.2.4] - 2026-07-04
Added
- NDO tenant-policy-template DHCP surface + schema serviceGraphs for
cisco.mso (PR-13). Two confirmed sim gaps from a real aci-ansible run
against ACI hardware, both the last NDO write surfaces blocking the multi-site
(MS) suite:
- Tenant-policy-template CREATE flow.
prereq_tenantpol.yml'scisco.mso.ndo_templatetask (delegate_to: localhost) 404'd onGET /api/v1/templates/summaries— a BARE path with no/msoprefix. Root cause:delegate_to: localhostforcesansible-mso'sMSOModuledown its direct-HTTP branch instead of the persistentansible.netcommon.httpapiconnection every other task on the same playbook host uses, and that branch never adds the/msoprefix. Added a full generalized template store (POST/GET/PATCH/DELETE /templates{,/summaries,/objects,/{id}}, both bare and/mso-prefixed) sondo_templatecan create a tenant-policy template that persists,cisco.mso.ndo_dhcp_relay_policy(create_dhcp_relay) can add a DHCP relay policy against it (schema-template EPGs now carry auuidfield for provider refs — previously absent), andcisco.mso.ndo_schema_template_bd_dhcp_policy(bind_dhcp_relay_to_bd) can resolve/bind it to a BD via the newtemplates/objects?type= dhcpRelay&{uuid|name}=...cross-template lookup route. Confirmed end-to-end on a real hardware run: MS-TN1/MS-TN2create_dhcp_relay+bind_dhcp_relay_to_bdnow reachfailed=0. - Schema serviceGraphs surface. The mso-model role's "Create service
graph" task (
custom_mso_schema_service_graph.py, a legacy pre-MSOTemplate/MSOSchemamodule) bare-subscriptsschema_obj.get('sites')[site_idx]['serviceGraphs']— the TEMPLATE-levelserviceGraphsbare-subscript was already covered by PR-11, but the SITE-level sibling was not, hittingKeyError: 'serviceGraphs'on a real hardware MS-TN2create_tenantpass-2 run (-e automate_contract_graph=true). AddedSITE_TOP_LEVEL_DEFAULTS(aci_sim/ndo/patch.py) to backfillserviceGraphs(andcontracts, see below) on every schemasites[]entry;GET /mso/api/v1/schemasnow returns full nested schema detail (matching real NDO) instead of a trimmed{name}-only summary, since this same legacy module bare-subscripts straight into that response; addedGET schemas/service-node-typesand atenantIdfield on every schema template (both required by the same module). Chasing this to green on real ACI hardware surfaced a DEEPER gap: once theserviceGraphsKeyError was fixed, the mso-model role's rawcisco.mso.mso_rest-driven "Atomic PATCH — bind service-graph redirect on ALL fabrics" task addresses a SITE-LOCAL contract by bare name (/sites/{siteId}-{t}/contracts/{c}/serviceGraphRelationship) — the sim never mirrored template-level contracts intosites[].contracts[]at all (unlike ANPs/EPGs, mirrored since PR-12), hittingPatchError: path segment 'con-Firewall_LAB0' not found in list. Added_mirror_template_contract_to_sites(PATCH-time) and the equivalent boot-time mirror inbuild_ndo_model. Confirmed end-to-end on a real hardware run: MS-TN2create_tenantpass-2 (01b) now reachesfailed=0.
- Tenant-policy-template CREATE flow.
- 26 new tests in
tests/test_pr13_ndo_dhcp_svcgraph.pycovering the bare/mso-prefixed template routes, the fullndo_templatecreate→lookup round trip, DHCP relay policy add/read viatemplates/objects,bind_dhcp_relay_to_bd's full PATCH sequence, site-levelserviceGraphsnormalization, the full schema-detailGET /schemasshape, and the site-local contracts mirroring + atomic redirect PATCH.
[0.2.3] - 2026-07-03
Added
-
NDO site-local ANP/EPG + static-port schema surface. PR-11 closed the schema write round-trip for template-level objects but left the last MS-TN1 gap:
bind_epg_to_static_port(cisco.mso.mso_schema_site_anp_epg_staticport) crashed client-side with'NoneType' object has no attribute 'details'. Two compounding root causes, both fixed:- Real NDO stores a self-referencing
anpRefstring on every template-level ANP object itself (andepgRefon every EPG) — confirmed becauseansible-mso'smso_schema_template_anp.py/_anp_epg.pyexplicitly strip it client-side before an idempotency comparison (if "anpRef" in mso.previous: del mso.previous["anpRef"]— dead code unless the server sends one back).MSOSchema. set_site_anp()/set_site_anp_epg()(ansible-mso'smodule_utils/schema.py) key their site-local lookup off exactly this field; without it the lookup always compares againstNone.normalize_template()(aci_sim/ndo/patch.py) now backfillsanpRef/epgRefon everyanps[]/epgs[]entry (takes an optionalschema_idparam to build them);build_ndo_model()(aci_sim/ndo/model.py) calls it at boot too. - Real NDO 4.x auto-populates a schema's site-local
sites[].anps[].epgs[]the moment a template-level ANP/EPG is added to a template that already has a site attached — the sim never did this mirroring, soset_site_anp()/set_site_anp_epg()always returnedNoneand the module's own dead-on-4.x fallback path (its own source comment: "Coverage misses this two conditionals when testing on 4.x and above") crashed instead.apply_json_patchnow auto-mirrors:add /templates/{t}/anps/-creates a matching{anpRef, epgs:[]}site-anp in every associated site;add /templates/{t}/anps/{a}/epgs/-creates a matching full-shaped site-EPG{epgRef, subnets:[], staticPorts:[], staticLeafs:[], domainAssociations:[]}under it. The full child-collection shape is mandatory — every siblingmso_schema_site_anp_epg_*module bare-subscripts its own array (a missing key is a hardKeyError, not a 4xx):_domainreadsdomainAssociations,_staticleafreadsstaticLeafs,_staticportreadsstaticPorts,_subnetreadssubnets. (An earlier iteration that omitteddomainAssociationsregressedbind_epg_to_physical_domain/_vmm_domainon ACI hardware withKeyError: 'domainAssociations'.) Single source of truth for the shape isSITE_OBJECT_DEFAULTS["epgs"], consumed by_new_site_epg()(mirror path) andnormalize_site()(backfill path).build_ndo_model()performs the same mirroring at boot for topology-seeded schemas. Both idempotent.
epgRef's canonical string form uniquely nests two name segments (/schemas/{id}/templates/{t}/anps/{a}/epgs/{e}, confirmed againstansible-mso'smodule_utils/mso.pyepg_ref()) — handled by a dedicated_stringify_refsbranch since the existing single-category_REF_NAME_FIELDStable only fits one name field._REF_LOOKUP_KEYSgainedepgRefso a site-epg (ref-only, nonameof its own) resolves by its bare EPG name the same way site-local bds/anps already did (PR-11). Verified end-to-end against a real-hardware multi-site E2E run: the full MS-TN1 chain (create_tenant→create_bd→create_application→bind_epg_to_static_port) now reachesfailed=0on every host, closing the CONTRACT.md §7a "known gap" note. Newtests/test_pr12_site_local.py. - Real NDO stores a self-referencing
[0.2.2] - 2026-07-03
Added
- NDO schema write round-trip (POST/PATCH). PR-10's schema surface was
read-only (a static per-tenant seed plus a POST that stored a schema no
PATCH could reach). PR-11 makes the schema store fully writable, matching
the exact request sequence every
cisco.msoschema-object module performs (verified againstansible-mso'splugins/module_utils/mso.py+schema.pyand confirmed on a real multi-site aci-ansible E2E run against ACI hardware):GET schemas/list-identity(displayName→id, already existed) →GET schemas/{id}(already existed) →PATCH schemas/{id}with a JSON-Patch-shaped op list ({"op":"add"/"replace"/"remove","path":..., "value":...}). Newaci_sim/ndo/patch.pyimplements a general applier (apply_json_patch) supporting three path-addressing conventions cisco.mso modules use: numeric index /"-"append (RFC 6902), a named segment resolved against aname/displayNamefield (/templates/LAB1/bds/bd-App1_LAB1), a synthetic composite key"{siteId}-{templateName}"unique to the top-levelsites[]array (/sites/1-LAB1/bds/-— everymso_schema_site_*module builds this literally), and a*Ref-name lookup for site-local child objects that carry nonameof their own (/sites/1-LAB1/bds/bd-App1_LAB1/subnets/-resolvesbd-App1_LAB1against the site-local BD'sbdRefstring).normalize_template()/normalize_site()backfill every collection-default key (vrfs/bds/anps.epgs/contracts/filters/externalEpgs/intersiteL3outs/serviceGraphs/subnets/...) real NDO defaults server-side but severalcisco.msocreate/add payloads omit — a missing key there is a hard crash client-side ('NoneType' object is not iterable, bareKeyError, orTypeError: sequence item 0: expected str instance, dict foundwhen a sibling module", ".join()s a*Reffield the sim had stored as a dict instead of NDO's canonical string form/schemas/{id}/templates/{tmpl}/{category}/{name}— all three confirmed on real hardware create_tenant/create_bd runs before the fix). GET /api/config/class/remoteusers+/localusers.cisco.mso.mso_ tenant's tenant-create flow always resolves the tenant'suserslist vialookup_users(), which on ND platforms falls back to these legacy/api/config/class/*routes when the modern/nexus/infra/api/aaa/v4/*endpoints aren't available. The sim only served the modern route's absence as a 404, sond_request()had no"code"/"messages"in the error body and aborted with"ND Error: Unknown error no error code in decoded payload"— verified on a real hardware create_tenant run. Now seeded with a localadminuser in ND's{"items":[{"spec":{...}}]}aggregate shape.- Bare
GET /api/v1/sites.cisco.mso.ndo_template's TenantPol prereq lookup hits this un-prefixed path (no/mso); the sim only served the/mso/api/v1/sitesform. Same data, added alongside PR-10's bare/api/v1/tenants. PUT/POST /mso/api/v1/tenants{,/{id}}.cisco.mso.mso_tenantupdates a tenant in place (PUT .../tenants/{id}) whenever the tenant already exists inGET /mso/api/v1/tenants— true for every tenant this sim seeds from the topology. Previously unimplemented (404).GET /mso/api/v1/schemas/{id}/validate,execute|status/schema/{id}/ template/{name},POST /mso/api/v1/task. The full schema-template deploy surface:cisco.mso.mso_schema_template_deploy(legacy) validates then callsGET execute|status/schema/.../template/...; the module aci-ansible'smso-modelrole actually invokes,cisco.mso.ndo_schema_template_deploy, instead validates thenPOST /mso/api/v1/taskwith{schemaId, templateName, isRedeploy}— confirmed against the collection installed on real ACI hardware (differs from the upstreammaster-branch module source). All three were 404 before this PR.
Fixed
GET /mso/api/v1/schemas/list-identityno longer double-lists (or silently drops) a schema after it's mutated byPATCH— the summary projection is now kept in sync in place rather than appended as a duplicate "extra" entry.
Verified against a real-hardware E2E run
MS-TN1 (multi-site tenant) now reaches failed=0 for create_tenant,
create_bd, create_application, bind_epg_to_physical_domain,
bind_epg_to_vmm_domain, and bind_epg_to_secure_application. Remaining
known gap: bind_epg_to_static_port needs site-local ANP/EPG object
creation (mso_schema_site_anp/_anp_epg), not implemented in this PR —
tracked for PR-12 (see docs/CONTRACT.md §7 for the precise failure
signature).
Tests
- New
tests/test_pr11_ndo_write.py(31 tests):apply_json_patchop semantics (add/replace/remove, index/name/composite-key/ref-name resolution, auto-vivification safety),normalize_template/normalize_sitecollection-default backfilling,*Refdict→string normalization, and full schema create→PATCH→GET round-trips through the FastAPI routes (including the exact site-local BD + BD-subnet sequence a real E2E run exercises).
[0.2.1] - 2026-07-03
Fixed
/api/node/mo/{dn}alias (11 E2E failures). Real APIC serves/api/node/mo/{dn}.jsonas a full equivalent of/api/mo/{dn}.json(the form the APIC GUI's API Inspector and somecisco.acicall paths emit). The sim only registered/api/mo/*, so an alias request fell through to FastAPI's default 404{"detail":"Not Found"}— a shapecisco.acicannot parse (surfaces as"APIC Error None: None",status=-1). GET, POST, and DELETE are now all registered on both paths via stacked route decorators sharing one handler, so behavior can never drift between the canonical and alias forms.- APIC-shaped 400 for unmatched
/api/*paths. Any request under/api/*that doesn't match a real route now returns an APIC error envelope (imdata/error/code) instead of FastAPI's{"detail":...}— real APIC never emits that shape. Implemented as a Starlette 404 exception handler scoped to the/api/*prefix, so it only fires once routing has already failed and can never shadow a real route. - NDO site names now use the fabric name, not the short topology name.
GET /mso/api/v1/sitespreviously reported the topology's internal short site name ("LAB1"/"LAB2"); real NDO registers sites under their fabric name ("LAB1-IT-ACI"/"LAB2-IT-ACI"), andcisco.mso.mso_tenantfailed with"Site 'LAB1-IT-ACI' is not a valid site name"against the short form. Verified against a real-gearcisco.mso.mso_tenantrun.aci_sim/ndo/model.py's site builder now exposesSite.fabric_name(falling back to the short name if unset); internal site-id joins (tenant.sites, schema site associations) are unaffected since they key off the topology's short name, never the NDO-facing display name. - NDO schema
list-identity+ bare/api/v1/tenants. AddedGET /mso/api/v1/schemas/list-identity— the lightweight schema-enumeration endpoint everycisco.msoschema module calls first to resolve a schemadisplayNameto itsid(verified againstansible-mso'splugins/module_utils/mso.py:query_objs("schemas/list-identity", key="schemas", displayName=schema), which readsjson["schemas"][]entries with at leastid+displayName). Declared before the parameterizedGET /mso/api/v1/schemas/{schema_id}route so"list-identity"is never matched as a schema id (previously would 404 with{"detail":"Schema 'list-identity' not found"}). Also addedGET /api/v1/tenants, backingcisco.mso.ndo_template's tenant prereq lookup, returning the same tenant list as the existing/mso/api/v1/tenants. Both are read-only additions — the schema/template write surface (PATCHdeploy) is an intentional follow-up. - New regression suite
tests/test_pr10_ansible_gaps.py(9 tests) covers all of the above;tests/test_ndo.py's site-name assertions updated to join by fabric name.
[0.2.0] - 2026-07-03
Added
- Public-release preparation: rewritten README (purpose, architecture, both
run modes, configuration reference, error-handling contract, known
limitations), MIT
LICENSE, thisCHANGELOG.md, GitHub Actions CI (ci.yml), a validatedexamples/ansible/playbook set (smoke + teardown), and a project-rootCLAUDE.mdrelease-kit overlay.
Changed
- Hygiene pass: removed a real internal hostname/username pair from
docs/DESIGN.md's batch-2/3 verification note and genericized an example path indeploy/aci-sim.service; no functional/behavioral change.
[0.1.12] - 2026-07-03
Added
- Ansible
cisco.acifidelity:DELETE /api/mo/{dn}.json(state=absent, server-side idempotent),GETon a nonexistent-but-well-formed DN now returns 200-empty instead of 404 (unblocksstate=presentGET-before-write playbooks),status="deleted"cascade regression coverage,annotation/ownerKey/ownerTag/nameAliaspassthrough regression coverage, and certificate-signature auth in accept-mode (claimed-identity trust, no signature verification — documented trust-model caveat).
[0.1.11] - 2026-07-03
Added
- Default loopback bind (
SIM_BIND, defaults to127.0.0.1) so the unauthenticated/_simcontrol plane is never LAN-exposed by accident. - Linux sandbox-mode support (
ip addr add/del … dev lo) alongside the existing macOSlo0 aliaspath insandbox-up.sh/sandbox-down.sh. - Effective-port environment overrides and topology config validation.
- systemd
--userunit and launchd plist deployment docs (deploy/).
[0.1.10] - 2026-07-03
Added
- Mutation-killing numeric-comparison test coverage and minimum-row assertions on HTTP fault-path tests, closing gaps a mutation-testing pass identified in the query engine's comparison operators.
[0.1.9] - 2026-07-03
Fixed
/_sim/reloadnow rebuilds against the freshly-loadedSiteobject (matched by id, falling back to name) instead of a stale pre-reload reference, so topology edits actually take effect.- The
fabricNodeIdentPnode-registration reaction now materializes an enriched MO set (fabricNode + topSystem + healthInst + fabricLink/ lldpAdjEp/cdpAdjEp cabling + port inventory), not just a bare fabricNode. GET /api/node/class/{class}.json(no DN prefix) now correctly resolves as the fabric-wide equivalent of/api/class/{class}.json.
[0.1.8] - 2026-07-03
Added
- Seeded batch-2/3 contract classes: port-channel/vPC member relations
(
pcAggrIf,pcRsMbrIfs), transceiver/optics inventory (ethpmFcot), APIC cluster health (infraWiNode), zoning-rule/pcTag plumbing (actrlRule,fvEpP), EVPN VPN routes (bgpVpnRoute,bgpPath), and boot-seeded node registration (fabricNodeIdentP). Deliberately did NOT buildinfraNodeIdentP(no real consumer; a different concept fromfabricNodeIdentPdespite the similar name).
[0.1.7] - 2026-07-03
Added
- Seeded batch-1 previously-catalogued-but-unbuilt contract classes:
access-policy cluster (
infraAccPortP/infraHPortS/infraPortBlk/infraRsAccBaseGrp/infraAccBndlGrp), route-control cluster (rtctrlProfile/rtctrlCtxP/rtctrlSubjP/rtctrlMatchRtDest/rtctrlSetComm/rtctrlSetPref), static routes (ipRouteP/ipNexthopP), RIB/ARP (uribv4Route/uribv4Nexthop/arpAdjEp),ospfIf, and static bindings (fvRsPathAtt,vzRsSubjGraphAtt).
[0.1.6] - 2026-07-03
Fixed
- Topology-consistency findings: real ports built for L3Out sub-interfaces
and spine OSPF/ISN uplinks (previously referenced but never built),
endpoint/APIC port separation on shared leaves, correct multi-site
endpoint semantics (home-site front-panel attachment vs. remote tunnel-
learned), BD
unicastRoutenow reflects actual subnet presence, and controller health (healthInst) seeded for every controller node.
[0.1.5] - 2026-07-03
Fixed
- Auth is now enforced end-to-end: token validation, credential checking
against
SIM_USERNAME/SIM_PASSWORD, session expiry (refreshTimeoutSeconds), and APIC error envelopes (401/403) on every query/write route instead of accepting any request.
[0.1.4] - 2026-07-03
Fixed
- Valid IPv4 derivation for node IDs greater than 255 (previous scheme
overflowed the last octet). Seeded the
userprofile/aaaUserDomainlogin-probe MO so the CONTRACT §1 subtree query resolves.
[0.1.3] - 2026-07-03
Fixed
- Write path is now atomic (validate-then-commit): a malformed node
anywhere in a POST body's tree rejects the whole write with a 400
envelope and leaves the store untouched. Canonical RN synthesis for
dn-less children (e.g.
fvBD→BD-{name}) instead of a generic{class}-{name}guess, avoiding orphan duplicate DNs.
[0.1.2] - 2026-07-03
Fixed
- Query engine: subtree-scope filtering now applies
query-target-filterto the whole scoped set (root + descendants) rather than pre-filtering the root out of scope;page/page-sizeparameter validation (400 on non-numeric/out-of-range values instead of silent truncation); stricter filter-expression parsing (malformed/truncated expressions → 400 instead of a 500 or silent no-op).
[0.1.1] - 2026-07-02
Fixed
rsp-subtree=fullnesting (recursive children, not one level).- Comparison filter operators (
gt/lt/ge/le/bw) with numeric-vs- lexical comparison semantics.
[0.1.0] - 2026-07-01
Added
- Initial project scaffold: MIT store + query engine (P1/P2), topology
schema + default
topology.yaml, fabric/tenant builders (P3/P4), the APIC REST app + runtime supervisor +/_simcontrol API (P5), the NDO plane (P6), and an end-to-end verification harness (P7) — seedocs/CONTRACT.mdanddocs/DESIGN.mdfor the full design record.