The design DSL¶
Niwaki design DSL — declarative, IDE-friendly ACI provisioning.
Build a detached design tree with operator vocabulary (no APIC class names,
no session), then validate and push it in one call. The DSL covers the whole
uni subtree — tenants, access policies (infra), fabric policies
(fabric), and controller policies — through one uniform vocabulary:
from niwaki import Niwaki
from niwaki.design import tenant
config = (
tenant("prod")
.app("prod")
.epg("frontend").bind(bd="frontend")
.bd("frontend")
.set(unicast_routing=True)
.bind(vrf="prod")
.subnet("10.0.1.1/24")
.vrf("prod")
)
with Niwaki("https://apic.example.com", "admin", "secret") as aci:
config.push(aci, mode="strict") # one atomic POST, all-or-nothing
Every design is rooted at polUni; design() starts an empty
multi-domain design, while tenant() / infra() / fabric() /
controller() are shorthands that declare the first domain and return its
cursor — sibling domains stay one maker call away:
cfg = design()
cfg.fabric().datetime_policy("prod-ntp")
cfg.infra().vlan_pool("prod", "static").range("vlan-100", "vlan-199")
cfg.tenant("prod").vrf("main")
Core rules (see the Design-first architecture page in the documentation):
Structure is literal: every maker maps 1:1 to a real APIC child class; nothing is silently created or hidden.
Verbatim is translated: names and parameters use operator vocabulary (
entry("http", tcp=80),scope="vrf").References are lazy:
bind()/provide()/consume()resolve at push time — forward references allowed, closed-world validated.No I/O during construction: transport is injected at
push()only.
Root factories¶
Each root opens a curated, fully typed surface: the makers, their keyword
arguments, the bind() aliases and the verbs available at every position are
generated and documented in the DSL reference.
- niwaki.design.design()[source]¶
Create an empty detached design rooted at
polUni(uni).The uniform entry point for multi-domain designs: every other root factory (
tenant(),infra(),fabric(),controller()) is sugar fordesign().<maker>(...). No session and no I/O — the tree is built in memory and pushed later viapush().- Returns:
Root
UniCursorof a new empty design.- Return type:
UniCursor
Example:
cfg = design() cfg.fabric().datetime_policy("prod-ntp") cfg.infra().vlan_pool("prod", "static") cfg.tenant("prod").vrf("main") payload = cfg.to_payload() # one atomic polUni envelope
- niwaki.design.tenant(
- name,
- *,
- annotation=None,
- description=None,
- display_name=None,
- owner_key=None,
- owner_tag=None,
- userdom=None,
Create a detached design and declare its
fvTenantroot.Shorthand for
design().tenant(...)— the returned cursor sits on the declared object; the design root is the enclosingpolUninode, so sibling domains stay reachable through the implicit-pop makers.- Returns:
TenantCursoron the newfvTenantnode.- Return type:
TenantCursor
- niwaki.design.infra(*, annotation=None, display_name=None, owner_key=None, owner_tag=None, userdom=None)[source]¶
Create a detached design and declare its
infraInfraroot.Shorthand for
design().infra(...)— the returned cursor sits on the declared object; the design root is the enclosingpolUninode, so sibling domains stay reachable through the implicit-pop makers.- Returns:
InfraCursoron the newinfraInfranode.- Return type:
InfraCursor
- niwaki.design.fabric(
- *,
- annotation=None,
- name=None,
- display_name=None,
- owner_key=None,
- owner_tag=None,
- userdom=None,
Create a detached design and declare its
fabricInstroot.Shorthand for
design().fabric(...)— the returned cursor sits on the declared object; the design root is the enclosingpolUninode, so sibling domains stay reachable through the implicit-pop makers.- Returns:
FabricCursoron the newfabricInstnode.- Return type:
FabricCursor
- niwaki.design.controller(
- *,
- annotation=None,
- name=None,
- display_name=None,
- owner_key=None,
- owner_tag=None,
- userdom=None,
Create a detached design and declare its
ctrlrInstroot.Shorthand for
design().controller(...)— the returned cursor sits on the declared object; the design root is the enclosingpolUninode, so sibling domains stay reachable through the implicit-pop makers.- Returns:
ControllerCursoron the newctrlrInstnode.- Return type:
ControllerCursor
References that carry configuration¶
A bind(), a bind_dn() or a verb usually takes a plain name; wrap the target
in ref() when the relationship object itself carries configuration (a domain
attachment’s immediacy, a subject filter’s log directive, a node’s management
address).
- niwaki.design.ref(target, **attrs)[source]¶
Reference target and configure the relationship itself.
- Parameters:
- Returns:
A
Refusable anywhere a plain name is –bind(),bind_dn()and the contract verbs.- Return type:
Example
>>> from niwaki.design import ref, tenant >>> cfg = tenant("prod") >>> cfg.filter("http").entry("tcp-80", tcp=80) <Cursor ...> >>> subject = cfg.contract("web").subject("http") >>> _ = subject.bind(filter=ref("http", directives="log"))
- class niwaki.design.Ref(target, attrs)[source]¶
Bases:
objectA reference that carries configuration of its own.
Most relationships in the MIT are pure edges — the Rs object stores the target and nothing else, so naming the target is the whole reference:
epg.bind(bd="web")
Some carry configuration: an EPG-to-domain attachment holds the static encap and the resolution immediacy, a filter attached to a subject holds its directives, a route-control profile attachment holds its direction. Wrap the target in
ref()to set them:epg.bind(domain=ref("vmm-prod", encap="vlan-201", untagged=False)) subject.bind(filter=ref("http", directives="log"))
The attributes are validated against the relationship class at declaration time, like every other field in a design.
Cursor¶
Every position is a typed cursor subclass of Cursor — the makers and the
set() / bind() signatures are generated per position (see the
DSL reference). The base class below is the
behaviour they all share.
- class niwaki.design.Cursor(node)[source]¶
Bases:
objectA position in a design tree, exposing the curated build vocabulary.
Do not instantiate directly — obtain the root cursor from
niwaki.design.tenant()and children from maker calls.- property design_node: DesignNode¶
The underlying design node (read-only structural handle).
Deliberately verbose: short names on the cursor belong to the curated vocabulary (
.node()is a maker under a route reflector or a vPC pair) — the generator enforces that no maker shadows the base cursor API.
- property dn: str¶
Distinguished Name this node will occupy once pushed.
- Returns:
DN string rooted at
uni(e.g."uni/tn-prod/BD-web").
- set(**attrs)[source]¶
Set scalar attributes on the current object.
Values are merged with previously set attributes (last call wins) and the full attribute set is re-validated through the Pydantic model immediately — constraint violations raise at the call site, before any push.
- Parameters:
**attrs (Any) – Field values using the human-readable Python names (e.g.
unicast_routing=True). Sugar applies where defined (e.g.scope="vrf"on a contract).- Returns:
This cursor, for chaining.
- Raises:
DesignError – Unknown attribute name, ACI wire name used instead of the Python field name, or attempt to change a naming prop.
pydantic.ValidationError – A value violates the model constraints.
- Return type:
- bind(**targets)[source]¶
Declare lazy Rs relationships by target vocabulary and name.
Each
alias=namepair records a reference resolved at push time — forward references are allowed, and the relationship class, its flavor (name vs DN) and the side it lives on are derived fromREFERENCE_MAP, so.vrf("prod").bind(l3out="prod")works even though the Rs object lives on the L3Out side.The alias must be curated on the current level — binds do not climb to an ancestor. Declare each alias on the object that owns it (
.bd("b").bind(vrf="v")), so a relation can never land silently on a parent.- Parameters:
**targets (Any) – One or more
alias=namepairs (e.g.vrf="prod").- Returns:
This cursor, for chaining.
- Raises:
DesignError – An alias is not bindable at any level of the path.
- Return type:
- bind_dn(**targets)[source]¶
Reference objects outside the design by raw DN (escape hatch).
Same aliases as
bind(), but the value is a full DN and no closed-world lookup happens — the DN is trusted as-is and the APIC is the one to reject a dangling reference. Only aliases whose Rs class targets by DN qualify; name-flavored aliases must go throughbind()(the Rs object physically stores a name, not a DN).- Parameters:
**targets (str | Ref) – One or more
alias=dnpairs (e.g.vlan_pool="uni/infra/vlanns-[shared]-static"), or aref()when the relation itself carries configuration (ref(dn, immediacy="immediate")).- Returns:
This cursor, for chaining.
- Raises:
DesignError – The alias is unknown, its relation lives on the target side, or it targets by name rather than by DN.
- Return type:
- intra_epg(contract)[source]¶
Declare contract between the endpoints of this EPG (
fvRsIntraEpg).The APIC applies it to traffic that stays inside the group — the EPG must be intra-EPG isolated for it to bite.
- mo(cls, **kwargs)[source]¶
Declare a child of an arbitrary generated class (escape hatch).
For classes outside the curated vocabulary. Naming props are picked from
kwargsby name; the remainder are scalar attributes.- Parameters:
cls (type[ManagedObject]) – Generated
ManagedObjectsubclass.**kwargs (Any) – Naming props and attributes.
- Returns:
Cursor on the new child node.
- Raises:
DesignError – cls is not a valid APIC child of this object.
- Return type:
- to_payload()[source]¶
Validate, resolve references, and return the atomic push payload.
Follows the
Query.build()house pattern: full inspection without execution. The returned dict is exactly whatpush(mode="strict")POSTs to/api/mo/uni.json.- Returns:
polUnienvelope dict wrapping the whole design.- Raises:
UnresolvedReferenceError – A reference target is not in the design.
AmbiguousBindError – A bind edge has no Rs class in either direction.
- Return type:
- push( ) PushReport[source]¶
- push(client: Niwaki, *, mode: Literal['plan']) PlanResult
- push(
- client: AsyncNiwaki,
- *,
- mode: Literal['strict', 'staged'] = 'strict',
- push(
- client: AsyncNiwaki,
- *,
- mode: Literal['plan'],
Validate the design and push it through client.
Always operates on the whole design tree regardless of which cursor it is called on. Construction never touches the network — transport is injected here and only here.
- Modes:
"strict"(default): closed-world validation (every reference must resolve inside the design), then one atomic nested POST to/api/mo/uni.json— all or nothing."staged": compile to per-object operations executed in DN-depth waves (parents before children) with a detailed report; a failing wave stops the remaining ones."plan": dry run — read the current APIC state and report what would be created or changed, pushing nothing.
- Parameters:
client (Niwaki | AsyncNiwaki) – A connected
Niwaki(returns the result directly) orAsyncNiwaki(returns an awaitable).mode (PushMode) –
"strict"|"staged"|"plan".
- Returns:
PushReportfor write modes,PlanResultfor"plan"(wrapped in a coroutine for async clients).- Raises:
UnresolvedReferenceError – Closed-world validation failed.
AmbiguousBindError – A bind edge has no Rs class.
APIError – The APIC rejected a write (strict mode).
StagedPushError – One or more staged operations failed — carries the partial report and the failed/skipped DNs.
- Return type:
PushReport | PlanResult | _Coroutine[Any, Any, PushReport | PlanResult]
Push results¶
- class niwaki.design.PushReport(mode, dns, request_count)[source]¶
Bases:
objectSummary of a successful
strictorstagedpush.
- class niwaki.design.PlanResult(creates, updates, unchanged)[source]¶
Bases:
objectDry-run report of what a push would change (
planmode).Deletions are out of scope by design: a plan never proposes removing objects that exist on the APIC but not in the design.
- updates¶
Per-DN field changes as
{field: (current, desired)}.