Why niwaki — a comparison with cobra¶
Cisco ships an official Python SDK for ACI:
cobra (acicobra + acimodel). It is the
authoritative binding of the APIC object model, and this page treats it with
the respect it deserves — every claim below comes from its
official documentation. But cobra was
designed in the Python 2 era, and its model of the world is the APIC’s, not
the operator’s. niwaki exists to close that gap.
Note
cobra in this documentation — three pages, three jobs: this page is the argument; Side by side — cobra and niwaki shows the same tasks side by side; Migrate from cobra is the migration how-to.
Distribution and lifecycle¶
cobra is not on PyPI. Per its
install guide, you
download two wheels from a running APIC
(https://<apic>/cobra/_downloads/), install acicobra first and
acimodel second, and the wheel filenames encode the firmware they match
(acicobra-4.2_2j-…). The documented Python support is “Python 2.7 or
Python 3.6”, with a dependency on future.
Consequences in practice: your automation environment is coupled to a firmware version, upgrading the fabric means re-provisioning every virtualenv, and dependency resolution knows nothing about it.
niwaki is one standard wheel with PEP 621/639 metadata, py.typed, and no
runtime dependency on any APIC: the 2,222 generated models ship inside the
package. A single environment can talk to any 6.x fabric; the schema
version is a property of the codegen, tracked in the changelog, not of your
venv.
The writing model¶
cobra is imperative. From its examples, verbatim:
from cobra.model.fv import Tenant, Ctx, BD, RsCtx, Ap, AEPg, RsBd
from cobra.mit.request import ConfigRequest
fvTenantMo = Tenant(uniMo, 'ExampleCorp')
Ctx(fvTenantMo, 'private-net1')
fvBDMo = BD(fvTenantMo, 'bridge-domain1')
RsCtx(fvBDMo, tnFvCtxName='private-net1')
fvApMo = Ap(fvTenantMo, 'WebApp')
fvAEPgMo = AEPg(fvApMo, 'WebEPG')
RsBd(fvAEPgMo, tnFvBDName='bridge-domain1')
configReq = ConfigRequest()
configReq.addMo(fvTenantMo)
moDir.commit(configReq)
To write this you must know: the ACI class names (fv.Tenant, fv.AEPg),
the relation classes (RsCtx, RsBd) and which side owns them, the
wire attribute carrying each target (tnFvCtxName, tnFvBDName), and the
parent-MO plumbing. Nothing checks that 'private-net1' matches the Ctx
you created three lines earlier — a typo becomes an APIC error after the
POST, or worse, a dangling reference.
The same configuration in niwaki:
from niwaki import Niwaki
from niwaki.design import tenant
config = (
tenant("ExampleCorp")
.vrf("private-net1")
.bd("bridge-domain1").bind(vrf="private-net1")
.app("WebApp")
.epg("WebEPG").bind(bd="bridge-domain1")
) # fmt: skip
with Niwaki("https://apic.example.com", "admin", "secret") as aci:
config.push(aci)
No relation class, no tn*Name, no parent plumbing: bind(vrf=...) is
resolved at push time against the objects declared in the design —
closed world — and bind(vrf="private-net2") fails before any
request with no fvCtx named 'private-net2' is declared in this design. Declared: private-net1. Did you mean 'private-net1'?.
Three push modes complete the model: strict (one atomic POST of the whole
design — all or nothing), staged (per-object waves, parents before
children), and plan (a dry-run diff against the live fabric). cobra has
one: commit(). There is no dry run; the closest workflow is committing
and reading back.
The language of the code¶
cobra exposes the model as the APIC stores it: wire attribute names
(arpFlood, unicastRoute), abbreviation-heavy class names, and query
filters written as raw strings — from the
getting-started guide:
moDir.lookupByClass("fvTenant", propFilter='and(eq(fvTenant.name, "Tenant1"))')
Because acimodel is generated without type annotations, an IDE cannot
autocomplete attributes or catch a misspelled one; the documentation is an
API dump of the model.
niwaki spends its entire codegen budget on this problem: human-readable
field names with the wire name as alias (arp_flooding ↔ arpFlood),
676 real enums, typed cursors per position so autocompletion knows that
.node_block() under a leaf selector differs from the one under a spine
selector, and eager Pydantic validation at the call site. The
DSL reference is generated from the
same tables the runtime uses — documentation that cannot drift from the
code.
Observation¶
Both SDKs read anything. cobra queries with DnQuery/ClassQuery and
string filters; niwaki with a typed builder (where, under, include,
with_faults/health/stats, streaming pagination) that still accepts any of
the ~15,000 class names as plain strings for operational classes. niwaki
adds a first-class async mirror (AsyncNiwaki, TaskGroup gather),
proactive token refresh, typed exceptions and retries with backoff — cobra
predates asyncio and leaves session expiry and retries to the caller.
Where cobra remains the right tool¶
Honesty matters in a comparison:
Guaranteed firmware parity. cobra’s wheels come from your APIC: every class and attribute of that exact firmware is writable. niwaki’s models are generated from one schema release (currently APIC 6.0) and its curated design vocabulary covers the common operational surface — the escape hatches (
.mo(),bind_dn(), string-class queries) cover the rest, but a brand-new class in tomorrow’s firmware reaches cobra first.Official support. cobra is Cisco’s; a TAC case can reference it.
Python 2 estates. If you are still running Python 2.7 automation, cobra is your only option — and your bigger problem.
If your goal is configuration as code — reviewable, diffable, converging, typed — that is the use case niwaki was built for.
Sources¶
cobra installation guide (APIC-hosted wheels, firmware matching, “Python 2.7 or Python 3.6”)
cobra getting started (LoginSession/MoDirectory, ConfigRequest, propFilter strings)
cobra examples (tenant/BD/EPG example quoted above)
PyPI:
acicobraandacimodelare not published (checked 2026-07-11)