Inside the DSL¶
The design DSL is niwaki’s core value: the layer that lets an operator write
their own vocabulary — .bd("web").bind(vrf="prod"), .bgp_peer(...),
.external_epg(...) — and get exact, validated APIC payloads out. The
The design DSL teaches how to use it; this page explains what it
is made of, how it is implemented in this repository, and how to help it
grow — the vocabulary is where contribution is most welcome.
A design is a detached, typed description that compiles to the exact wire payload:
from niwaki.design import tenant
config = tenant("shop")
config.bd("web", arp_flooding=True).bind(vrf="prod")
config.vrf("prod")
payload = config.to_payload()
bd = payload["polUni"]["children"][0]["fvTenant"]["children"][0]["fvBD"]
assert bd["attributes"] == {"name": "web", "arpFlood": "true"}
assert bd["children"] == [{"fvRsCtx": {"attributes": {"tnFvCtxName": "prod"}}}]
Three translations happened, none of them magic: the maker .bd() became
the real APIC child class fvBD, the readable arp_flooding became the
wire attribute arpFlood, and the lazy bind(vrf=...) was resolved
closed-world into the fvRsCtx relation with its tnFvCtxName target
property. Everything the DSL knows comes from two places: one curated
YAML file and the APIC schemas.
One source of truth: vocabulary.yaml¶
Every word of the DSL is a line in
src/niwaki/domain/vocabulary.yaml
— 927 curated positions and counting. Six sections, each with one job:
jargon: # ACI class → operator short name
fvAEPg: epg
makers: # parent class → {maker name → child class}
fvTenant:
bd: fvBD
l3out: l3extOut
binds: # cursor class → {bind alias → target class}
fvBD:
vrf: fvCtx # the Rs class and direction come from the schemas
verbs: # references whose relation class is named upfront
fvAEPg:
provide: {rs: fvRsProv, target: vzBrCP}
qosRequirement: # two relations to one class — ingress and egress
ingress_dpp: {rs: qosRsIngressDppPol, target: qosDppPol}
sugar: # typed convenience parameters
vzEntry:
tcp: "int | str | tuple[int, int] | None"
atomic: # subtrees the APIC validates as a unit
- fabricExplicitGEp
The rule behind every line: structure is literal, vocabulary is
translated. A maker never invents an object — it names a real APIC child
class in the words an operator uses. Curation is deliberate
(Design-first architecture explains why 927 hand-reviewed positions beat 2,222
auto-generated ones); the escape hatches .mo(AnyClass, ...) and
bind_dn(alias=dn) keep the rest of the schema one call away.
binds and verbs differ in one thing only: who names the relation class.
A bind lets the schemas decide (REFERENCE_MAP resolves owner → target to a
single Rs class and its flavor); a verb names it upfront, which is what lets
it reach a target the automatic resolution cannot — two relations to the same
class, like provide versus consume, or ingress versus egress policing.
Either way, when the relation carries configuration of its own, ref() sets
it: bind(domain=ref("prod-phys", resolution_immediacy="immediate")).
Generated, never hand-written¶
Three generators in src/niwaki/_codegen/ turn that YAML plus the APIC
schemas into everything the DSL ships. Their outputs are committed,
regenerable, and drift-guarded: a test fails if any generated artifact
does not match a fresh regeneration.
Generator |
Output |
What it derives |
|---|---|---|
|
|
|
|
|
one typed cursor class per position (a maker path, not a class — |
|
|
the DSL reference — one page per position with every keyword argument, the enums, and the coverage matrix; documentation that cannot drift from the code |
The cursor package is built to scale: each position’s makers live in one mixin, and a cursor inherits its ancestor chain — the method resolution order reproduces the runtime’s nearest-wins name resolution exactly, and the generated code stays around 75 lines per position. Signatures are introspected from the generated Pydantic models, so field names and enums can never disagree with validation.
The documentation is generated from the same source: Cisco’s schema
comments flow into every model field’s description, into per-value enum
docstrings, and into each maker’s Args section — hovering
ospf_interface_policy( in an IDE shows Cisco’s definition of every
parameter, its allowed values, and its default.
The generated classes add types only. The runtime is a thin, stable
core: design/_cursor.py dispatches makers dynamically, design/_resolver.py
resolves references closed-world (name flavor for tn* properties, DN
flavor for tDn, abstract targets matched against their concrete
subclasses), and the push engine compiles the tree to strict/staged/
plan executions (Push modes).
How the vocabulary grows¶
Coverage grows in reviewed batches, with a tool doing the heavy lifting and a human owning every name that becomes public API:
uv run python -m niwaki._codegen.propose_vocabulary l3extOut --wave my-wave
propose_vocabulary walks a schema subtree and emits a candidate YAML block
shaped exactly like vocabulary.yaml: maker names taken from the facade’s
navigation names (the jargon table — the DSL and the facade always agree),
bind aliases derived from
REFERENCE_MAP, contract verbs detected from the Rs*Prov/Rs*Cons pairs
— and a # REVIEW: comment on every line that deserves a human eye
(collision-resolved names, over-long labels, abstract targets). The
reviewed block is merged by hand into vocabulary.yaml; candidates are
never program input.
The safety net then takes over, automatically: every merged entry is validated by parametrized tests — real schema containment, resolvable references, navigation-name agreement — the cursors and the book regenerate under drift guards, and new positions are exercised live against a lab APIC before they ship.
Contribute here¶
The vocabulary is the part of niwaki that grows best with many hands — every network team has corners of ACI it knows intimately:
Missing vocabulary? Open a vocabulary request — name the ACI classes and the words your team uses for them. The coverage matrix shows what is curated today.
Want to propose the entries yourself? A vocabulary PR is a few YAML lines plus the regenerated artifacts (
uv run python -m niwaki._codegen.generate_domain && uv run python -m niwaki._codegen.generate_design && uv run python -m niwaki._codegen.generate_docs). The parametrized guards intests/design/test_core_yaml.pytell you immediately whether an entry is valid — you do not need a fabric to contribute.The workflow and review model are described in CONTRIBUTING.
Reading further¶
The design DSL — using the DSL
Design-first architecture — why one write path, why curation
Vocabulary book — every position, maker, keyword and alias
Side by side — cobra and niwaki — the same tasks in cobra and niwaki