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
- niwaki.design.aaa(
- *,
- name=None,
- annotation=None,
- description=None,
- display_name=None,
- owner_key=None,
- owner_tag=None,
- password_strength_check=None,
- userdom=None,
Create a detached design and declare its
aaaUserEproot.Shorthand for
design().aaa(...)— 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:
AaaCursoron the newaaaUserEpnode.- Return type:
AaaCursor
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:
- raw(aci_class, **wire_attrs)[source]¶
Declare a child by ACI class name with wire attributes.
The escape hatch for classes outside the generated model set (or when only the wire spelling is at hand — a reverse import). Identity and validity come from the shipped catalogue: the class must exist, be a valid child of this node, and every naming prop of its RN format must be present in wire_attrs. Unknown wire properties fail loudly.
- Parameters:
- Returns:
Cursor on the new child node (base cursor — children of a raw node dispatch through
raw()/mo(), not curated makers).- Raises:
DesignError – Unknown class, containment violation, missing naming prop, or unknown wire property.
DuplicateDeclarationError – Same class + RN already declared here.
- Return type:
- raw_set(**wire_attrs)[source]¶
Set wire-named attributes on the current node (escape hatch).
For properties outside the node’s typed surface — a prop newer than the generated models, or a reverse-imported attribute with only its wire spelling at hand. The values join the emitted attributes at the wire boundary in every push mode; the strict typed model never sees the keys. Each property must exist for this class in the shipped catalogue — silence is never an option.
- Returns:
selffor chaining.- Raises:
DesignError – A property the catalogue does not know for this class.
- Return type:
- view()[source]¶
Project the whole design into a frozen, walkable view.
Like
push()andto_payload(), this operates on the full design tree regardless of which cursor it is called on. The view is a snapshot of the design at call time — later cursor edits do not change it; take a new one.- Returns:
A
DesignView– iterate it parents-first, look nodes up by DN (view["uni/tn-prod/BD-web"]), or filter by class (view.by_class("fvBD")).- Return type:
Example
>>> from niwaki.design import tenant >>> cfg = tenant("prod") >>> _ = cfg.bd("web") >>> [node.aci_class for node in cfg.view()] ['polUni', 'fvTenant', 'fvBD']
- slice(dn)[source]¶
Carve out a fresh design holding the subtree at dn alone.
The flagship brownfield move: “import the fabric, carve out tenant X, replay it in staging”. The result is a new, independent design — the source is never mutated — containing the subtree at dn in full, hung off its ancestor chain rebuilt as attribute-less Day-2 upserts (pushing the slice never touches an ancestor’s attributes).
References follow the wire-footprint rule (see
niwaki.design._compose): a reference whose Rs object lands inside the subtree is kept — as-is when its target is inside too, pinned without the closed world otherwise (bind_dnfor a DN-flavored relation, an explicit Rs child carrying the exacttn*name for a name-flavored one). An inverse edge whose Rs materialises outside the subtree is not part of this slice’s wire and is not carried.- Parameters:
dn (str) – DN of the subtree to carve (
"uni/tn-prod");"uni"copies the whole design.- Returns:
The root
Cursorof the new design.- Raises:
DesignError – The design declares nothing at dn.
- Return type:
Example
>>> from niwaki.design import design >>> cfg = design() >>> _ = cfg.tenant("prod").bd("web") >>> _ = cfg.tenant("dev") >>> staged = cfg.slice("uni/tn-prod") >>> [n.aci_class for n in staged.view()] ['polUni', 'fvTenant', 'fvBD']
- 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:
- external_refs()[source]¶
Enumerate the design’s external DN references — no transport needed.
The same enumeration
push(verify_refs=True)verifies: raw-DNbind_dntargets and literal-DN makers (static_path,path_attachment, …), minus everything the design itself declares. Inspection only, liketo_payload()— nothing is read or written.- Returns:
The references, sorted by
(dn, referencing class).- Return type:
- push(
- client: Niwaki,
- *,
- mode: Literal['strict', 'staged'] = 'strict',
- verify_refs: bool = False,
- max_concurrent: int | None = None,
- push(
- client: Niwaki,
- *,
- mode: Literal['plan'],
- verify_refs: bool = False,
- max_concurrent: int | None = None,
- push(
- client: AsyncNiwaki,
- *,
- mode: Literal['strict', 'staged'] = 'strict',
- verify_refs: bool = False,
- max_concurrent: int | None = None,
- push(
- client: AsyncNiwaki,
- *,
- mode: Literal['plan'],
- verify_refs: bool = False,
- max_concurrent: int | None = None,
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".verify_refs (bool) – Verify the design’s external references (raw-DN
bind_dntargets and literal-DN makers such asstatic_path) against the live APIC before anything is written — reads only. Instrict/stagedmode a missing or wrong-class target raisesDanglingReferenceErrorwith the complete failure list; inplanmode the per-reference statuses land onexternal_refsand nothing raises. DefaultFalse— the wire behavior without the flag is byte-identical to previous releases.max_concurrent (int | None) – Upper bound on how many operations of one
stagedwave are in flight at once — async clients only. It throttles down and never up: the effective bound is the smaller of this value and the client’s ownmax_concurrent(default10), sopush(..., max_concurrent=50)against a default client still runs ten at a time — raise the limit on the client to go wider. Omitted, the push inherits the client’s limit, which is byte-identical to previous releases. With a sync client it is accepted and inert: the sync engine writes one object at a time. Must be>= 1, elseValueErrorbefore any I/O.
- Returns:
PushReportfor write modes,PlanResultfor"plan"(wrapped in a coroutine for async clients).- Raises:
UnresolvedReferenceError – Closed-world validation failed.
DanglingReferenceError –
verify_refs=Truein a write mode and at least one external reference cannot be honored by the APIC — nothing was written.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.- dns¶
The DNs this push accounts for, including the Rs objects the resolver materialises. The order is deterministic and derived from the design, never from the order in which the controller answered: a parent always precedes its descendants.
The set is mode-dependent, so never diff one mode’s report against another’s.
strictwalks the whole design tree, so every declared object appears.stagedlists one entry per operation, and two curated kinds of class do not map one-to-one onto operations: a class marked atomic ships its whole subtree in a single request, so its children have no entry of their own, and a carrier — a path-only class the APIC materialises when a child posts beneath it — gets no operation and so no entry at all.
- class niwaki.design.PlanResult(creates, updates, unchanged, external_refs=<factory>)[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)}.
- class niwaki.design.ExternalRef(dn, rs_class, declared_at)[source]¶
Bases:
objectOne DN a design references without declaring it.
- rs_class¶
The referencing class (an Rs class for
bind_dnextras, the node’s own class for literal-DN makers) — the source of the expected-class accept-set.- Type:
- class niwaki.design.RefCheck(ref, status, expected, found, detail='', apic_code=None)[source]¶
Bases:
objectThe verification outcome for one external reference.
- ref¶
The reference that was checked.
- status¶
ok(target exists and matches the accept-set),missing(no object at the DN),wrong_class(an object exists but its class is outside the referencing class’s accept-set),unverifiable(the expected class is a carrier — a GET answers empty even for a valid path), orerror(the read itself failed; seedetail).- Type:
Literal[‘ok’, ‘missing’, ‘wrong_class’, ‘unverifiable’, ‘error’]
- expected¶
The accept-set the target was checked against (empty when unknown — existence-only check).
- apic_code¶
The APIC’s own error code behind a
status="error", when the failure came from the controller and carried one.Nonefor every other status, and for a read that failed without an APIC error envelope — the HTML page a loaded controller answers instead of JSON, say. (A client-side timeout is not one of these: it raises aTransportError, which this pass does not catch, so it aborts the verification rather than becoming a row.)detailstays the human text — this is the machine-readable half, so a caller can tell “the DN is malformed” from “I am not allowed to look”.- Type:
str | None
Reverse import¶
The inverse of push(): rebuild a design from a
Snapshot — the whole fabric or any scope under
uni — or from a raw payload envelope, preferring the curated vocabulary
(makers, bind(), the contract verbs) and falling back to the wire-name
escape hatches so the design compiles to the same wire payload the source
describes.
- niwaki.design.to_design(snapshot, *, on_unknown='raise', redacted='raise')[source]¶
Rebuild a design from a snapshot — the inverse of
push.Walks the snapshot tree and re-expresses every captured object in the design DSL, preferring the curated vocabulary (makers,
bind(), the contract verbs) and falling back to the wire-name escape hatches (raw()/raw_set) whenever a curated inversion cannot be made provably equivalent. The returned design compiles to the same wire payload the snapshot describes: pushing it inmode="plan"against the fabric the snapshot was taken from reports no changes.A live snapshot carries every configurable property at its current wire value, unset markers included; the importer normalises them so the strict typed models never see what the fabric spells as “not configured”: empty strings drop (the design layer cannot emit
""by contract), a model-refused value equal to the property’s schema default drops (vmac="not-applicable",vrfIndex="0"), and any other model-refused value rides the wire channel verbatim (raw_set— the APIC served it, the wire is authoritative).Objects whose DN itself carries a secret (listed in
warnings) import unchanged — their identity cannot be redacted; review the snapshot’s warnings before sharing anything derived from it.- Parameters:
snapshot (Snapshot) – A
Snapshot— the whole configuration (scope"uni") or any narrower config scope under it ("uni/tn-prod","uni/infra", …). For a scoped snapshot the ancestors of the captured object are rebuilt from the scope DN as attribute-less Day-2 upserts: pushing the design never touches an attribute the capture does not carry.on_unknown (Literal['raise', 'raw']) – Policy for classes/properties outside this SDK’s schema baseline (a snapshot from a newer firmware).
"raise"(default) collects every occurrence into oneSnapshotImportError;"raw"carries them verbatim on the wire-attribute channel instead.redacted (Literal['raise', 'skip']) – Policy for values holding the
REDACTEDsentinel (curated secrets are elided at capture)."raise"(default) collects them — a design pushing the sentinel literally would be wrong;"skip"drops those values from the design. A relation whose target value was redacted imports as an unformed relation under"skip"— re-declare it with the real target before pushing anywhere.
- Returns:
The root
Cursorof the rebuilt design (apolUninode), ready forto_payload()/push().- Raises:
DesignError – The snapshot’s scope is outside
uni, its tree is empty, a"uni"snapshot is not rooted onpolUni, or an ancestor segment of a scoped snapshot cannot be resolved.SnapshotImportError – One or more items could not be imported — collected over the whole tree, never first-fail. The partial design is discarded.
- Return type:
Example:
from niwaki import snapshot from niwaki.design import to_design snap = snapshot.Snapshot.from_json(Path("fabric.json").read_text()) config = to_design(snap) result = config.push(aci, mode="plan") assert not result.has_changes # the design IS the fabric
- niwaki.design.from_payload(payload, *, on_unknown='raise', redacted='raise')[source]¶
Rebuild a design from a raw APIC payload — the inverse of
to_payload.Accepts the atomic
polUnienvelope shape ({"polUni": {"attributes": …, "children": […]}}) — whatto_payload()emits, what a strict push POSTs, and what configuration JSON exported from other tooling commonly looks like. The payload is converted to the snapshot tree shape (RNs recomputed from the catalogue, fail-loud on a missing naming value or astatuswrite directive) and handed to the same importer asto_design()— one inversion engine, two doors.- Parameters:
on_unknown (Literal['raise', 'raw']) – See
to_design().redacted (Literal['raise', 'skip']) – See
to_design().
- Returns:
The root
Cursorof the rebuilt design.- Raises:
DesignError – The envelope is malformed — not rooted on
polUni, several class keys in one envelope, an unknown class, a missing naming value, or astatusdirective.SnapshotImportError – See
to_design().
- Return type:
Example:
from niwaki.design import from_payload, tenant original = tenant("prod") original.bd("web") clone = from_payload(original.to_payload()) assert clone.to_payload() == original.to_payload()
- class niwaki.design.ImportProblem(dn, kind, detail)[source]¶
Bases:
objectOne snapshot item
to_design()cannot import.- kind¶
Problem family —
"unknown-class","unknown-property","redacted-value","invalid-value"or"structure".- Type:
Literal[‘unknown-class’, ‘unknown-property’, ‘redacted-value’, ‘invalid-value’, ‘structure’]
Walking a design¶
- class niwaki.design.DesignView(root)[source]¶
Bases:
objectA whole design, frozen and walkable.
Obtain one from
niwaki.design.Cursor.view(). Iteration yields every node parents-first in declaration order (thepolUniroot included); lookups are by DN.Example:
from niwaki.design import tenant cfg = tenant("prod") cfg.bd("web").bind(vrf="main") cfg.vrf("main") view = cfg.view() [n.aci_class for n in view] # → ['polUni', 'fvTenant', 'fvBD', 'fvCtx'] view["uni/tn-prod/BD-web"].binds[0].alias # → 'vrf' [n.dn for n in view.by_class("fvCtx")] # → ['uni/tn-prod/ctx-main']
- property root: DesignViewNode¶
The
polUniroot node of the design.
- class niwaki.design.DesignViewNode(dn, aci_class, label, position, naming, attrs, raw_attrs, binds, children)[source]¶
Bases:
objectOne declared object of a design, projected read-only.
- label¶
The maker that created the node (
"bd"), or the class name for escape-hatch nodes (.mo()/.raw()).- Type:
- position¶
Dotted maker path from the
polUniroot ("tenant.bd"),""for the root itself,Nonefor nodes outside the curated vocabulary.- Type:
str | None
- naming¶
Naming property values. Readable names for typed nodes; wire names for catalogue-served
raw()nodes (their whole surface is wire-spelled).
- attrs¶
Non-naming attributes exactly as declared (readable names, pre-coercion values).
- raw_attrs¶
Wire-channel escapes (
raw_set()and the reverse importer’s escapes) — wire names, wire string values.
- binds¶
The references declared on this node, in declaration order.
- Type:
- children¶
Child nodes, in declaration order.
- Type:
- class niwaki.design.DesignViewBind(kind, alias, target_class, target, attrs)[source]¶
Bases:
objectOne reference declared on a design node, as the caller declared it.
- kind¶
"bind"(closed-world alias),"bind_dn"(raw-DN escape) or"verb"(curated contract verb such asprovide).- Type:
Literal[‘bind’, ‘bind_dn’, ‘verb’]
- target_class¶
ACI class the reference points at — possibly abstract for curated aliases (
"infraDomP").- Type:
Composition¶
Carving is a cursor method — Cursor.slice("uni/tn-prod") returns a fresh
design holding that subtree over an attribute-less ancestor chain;
recombining is a function:
- niwaki.design.merge(*designs)[source]¶
Combine several designs into one — fail-loud on any contradiction.
The union is by DN: an object declared in one source is carried whole; an object declared in several must agree — same class, and no attribute (typed or wire-channel) set to two different values. References concatenate, identical duplicates collapse (the resolver already treats two identical relations as one). Every contradiction across the whole merge is collected before raising, never first-fail.
- Parameters:
*designs (Cursor) – Two or more design cursors (any cursor of each design — the whole tree is taken, like
push). Sources are never mutated.- Returns:
The root
Cursorof a fresh merged design.- Raises:
DesignError – Fewer than two designs given.
MergeConflictError – At least one contradiction — carries every conflicting
(dn, what, values)triple.
- Return type:
Example:
from niwaki.design import merge, tenant base = tenant("prod") base.bd("web") overlay = tenant("prod") overlay.bd("web").set(unicast_routing=True) combined = merge(base, overlay)
Emitting code¶
- niwaki.design.to_code(source, *, var='cfg')[source]¶
Emit the Python DSL source that replays source — the code emitter.
The inverse of executing a design script: any design — hand-built, reverse-imported (
to_design()), composed (slice()/merge()) — renders as reviewable, replayable Python. Executing the emitted source yields a design whose payload is canonically byte-identical to the source’s; the acceptance suite pins exactly that round trip.- Parameters:
source (Cursor | DesignView) – A design cursor (any cursor — the whole design is taken, like
push) or an already-takenDesignView.var (str) – Name of the root variable in the emitted source (default
"cfg") — the emitted script ends with that variable holding the root cursor.
- Returns:
Python source text – imports first, then the declarations in the design’s own order. Curated positions render as their makers and references; everything outside the curated vocabulary renders through the wire-name doors (
raw()/raw_set()) — including the tag/annotation objects that fabrics touched by other tooling always carry.- Return type:
Example:
from niwaki.design import tenant, to_code cfg = tenant("prod") cfg.bd("web").bind(vrf="main") cfg.vrf("main") print(to_code(cfg)) # cfg = design() # tenant_prod = cfg.tenant('prod') # tenant_prod.bd('web').bind(vrf='main') # tenant_prod.vrf('main')
Reconciliation¶
The other half of drift, beside plan: what the fabric carries that the
design does not declare. Read-only — nothing is ever proposed for deletion.
- niwaki.design.reconcile(source, client)[source]¶
Report what the fabric carries under this design’s domains and the design does not declare — the other half of drift, beside
plan.One snapshot capture of the whole configuration (the same 15-or-so requests a backup costs, whatever the design’s size) is walked against the design’s own DNs, resolved references included. Only the domains the design declares (its direct children of
polUni, carriers included) are accounted — a domain the design does not claim is nobody’s drift.Reads only; nothing is ever written, and nothing is proposed for deletion — a design never removes what it does not declare. Sync only, like
niwaki.snapshot.take()which it reuses.- Parameters:
- Returns:
A
Reconciliation.cleanisTruewhen the fabric holds nothing created beyond the design; fabric-materialised objects (default relations, minted containers — non-creatable classes) report separately underimplicit.- Return type:
Example:
from niwaki import snapshot from niwaki.design import reconcile, to_design cfg = to_design(snapshot.take(aci, "uni"), redacted="skip") report = reconcile(cfg, aci) assert report.clean # an imported design covers its own fabric # A partial hand design instead names what it does not own: for dn in report.orphan_subtrees: print("not mine:", dn)
- class niwaki.design.Reconciliation(extra, implicit, orphan_subtrees)[source]¶
Bases:
objectThe fabric-side half of drift — objects the design does not declare.
- extra¶
Every live, creatable object under the design’s declared domains that the design does not declare, as sorted
(dn, aci_class)pairs — things someone created.
- implicit¶
The undeclared objects whose class the schema marks non-creatable — fabric-materialised state (default relations, minted containers), never an operator’s leftover. Reported for completeness, excluded from
clean.
- orphan_subtrees¶
The minimal roots of the
extraregions — the extra objects whose parent is declared (or implicit, or a domain root). The operator-granularity answer to “which whole subtrees are not mine?”.