When a push fails

Problem — a push failed, or you do not trust it yet. This is the diagnostic ladder, cheapest rung first: inspect the payload offline, plan, read the typed error, recover from a partial staged push. Most “why did the APIC reject this?” questions are answered before you ever reach the wire.

Rung 1 — to_payload(): see what would be sent

No session, no I/O — the exact strict-mode payload, with resolution and validation already run:

import json

from niwaki.design import tenant

config = tenant("commerce")
config.vrf("prod")
config.bd("bd-web", unicast_routing=True).bind(vrf="prod").subnet("10.30.10.1/24")

print(json.dumps(config.to_payload(), indent=2))

Reading the envelope answers most rejection questions — a missing relation, or a wire value that is not what you assumed.

Warning

The payload (and plan output) is your fabric’s configuration — treat it like configuration when pasting into tickets and chats.

Rung 2 — resolution errors: read the message

Everything checkable before the wire fails before the wire, with the declared world named in the message (Errors & recovery):

from niwaki.exceptions import DesignError

probe = tenant("commerce")
probe.bd("bd-web").bind(vrf="prd")            # typo — no such VRF declared

try:
    probe.to_payload()
except DesignError as exc:
    print(exc)     # ...no fvCtx named 'prd' is declared... (with a did-you-mean)

to_payload() triggers the same resolution as push(), so it is the offline way to shake out reference errors.

Rung 3 — plan: what would actually change?

from niwaki import Niwaki

aci = Niwaki.connect("https://apic.example.com", "admin", "secret")

plan = config.push(aci, mode="plan")
print("creates  :", plan.creates)
print("updates  :", plan.updates)
print("converged:", not plan.has_changes)

The with form closes the session for you; connect() is used here so the rest of the page can share one client — see Connection & transport.

A plan that proposes more than you expected usually means a default you set explicitly somewhere (every set() field is compared), or a name that does not match the existing object (so it creates instead of updating). The inverse surprise: write-only attributes never read back, so a rotated password plans as “unchanged” — expected (Push modes).

Rung 4 — the APIC said no

On-wire rejections surface as typed exceptions carrying the APIC’s own message. Log status_code and apic_message — the latter names the offending attribute more often than not:

from niwaki.exceptions import APIError

try:
    config.push(aci)
except APIError as exc:
    print(exc.status_code, exc.apic_message)

With strict mode there is nothing to clean up after a rejection: the POST was atomic, the fabric did not change.

Rung 5 — a staged push stopped midway

staged mode can partially apply; the exception is the recovery map (Errors & recovery has the full playbook):

from niwaki.exceptions import StagedPushError

try:
    config.push(aci, mode="staged")
except StagedPushError as exc:
    print("applied :", exc.report.dns)
    print("failed  :", [dn for dn, _ in exc.failures])
    print("skipped :", exc.not_run)

Fix the design, push again — upserts converge, and nothing needs undoing because the applied objects are exactly what the design describes.

When it is not the push

  • Auth / TLS / timeouts — the exception class tells you which; the knobs are in When you cannot connect.

  • Pushed fine, not working — the fabric took the config; now audit it: faults on the new objects (aci.query("faultInst").where(severity="critical")), then health (Observe and verify a fabric).

  • It worked yesterday — re-run yesterday’s design with mode="plan": the diff between intent and fabric is the incident summary (Day-2 changes, the declarative way).