Snapshot — deterministic configuration captures

Deterministic, git-diffable snapshots of a fabric’s configuration.

A snapshot is a fact: every instance of every exportable class under a scope DN, read through the sharded, paginated bulk reader, normalised by the catalogue’s own metadata, and serialised so that the same fabric state always produces the same bytes. Two snapshots of an unchanged fabric git diff empty; a config change diffs as exactly that change.

Three catalogue-driven decisions, none of them a hand-maintained list:

  • Which objects: classes the schema marks isExportable — Cisco’s own definition of what a configuration export contains (measured on 6.0(9c): exportable is a strict subset of configurable; the difference is runtime state such as login sessions, which a backup must not carry). The curated DSL vocabulary plays no role here: an object the DSL cannot express is still configuration, and ignoring it would make the backup lie.

  • Which properties: those the schema marks isConfigurable — the operational halo (timestamps, status, computed backpointers) falls away data-driven, never through a denylist.

  • What never ships: the curated secret policy. Values the schema flags secure never read back from an APIC in the first place; the curated positions the flag misses are redacted to "<redacted>"; and an object whose DN carries a secret (an SNMP community string names its own object) cannot be redacted in place — it is reported in Snapshot.warnings so the caller decides, instead of publishing a secret to git silently.

The serialised form is wire-format (APIC class and property names), so a snapshot survives any renaming of the SDK’s readable surface unchanged.

Constants

niwaki.snapshot.REDACTED = '<redacted>'

The stable sentinel written in place of a curated secret value.

Functions

niwaki.snapshot.take(aci, scope='uni')[source]

Capture the configuration under scope into a Snapshot.

Reads every exportable class under the scope through the sharded, paginated bulk reader (one request in the common case, several when the class list or the result set demands it), then normalises:

Parameters:
  • aci (Any) – A connected niwaki.Niwaki client (observation only — a snapshot never writes).

  • scope (str) – The subtree to capture. "uni" for the whole configuration, "uni/tn-<name>" for one tenant, any config DN for narrower.

Returns:

The Snapshot. tree is None when nothing exists at scope.

Return type:

Snapshot

Example:

snap = snapshot.take(aci, "uni/tn-prod")
Path("tn-prod.json").write_text(snap.to_json())
niwaki.snapshot.diff(a, b)[source]

Compare two snapshots structurally — the drift detector.

Works on any two snapshots of the same scope: two moments of one fabric (config drift), or the same scope on two fabrics (divergence between a reference fabric and a replica).

Parameters:
  • a (Snapshot) – The reference snapshot (“before”).

  • b (Snapshot) – The other snapshot (“after”).

Returns:

A SnapshotDiff. Empty (has_changes false) when the two captures describe identical configuration.

Raises:

ValueError – The snapshots cover different scopes — comparing a tenant to a whole fabric is a mistake, not a diff.

Return type:

SnapshotDiff

Example:

before = snapshot.take(aci, "uni/tn-prod")
...
after = snapshot.take(aci, "uni/tn-prod")
delta = snapshot.diff(before, after)
for dn in delta.added:
    print("new object:", dn)

Result types

class niwaki.snapshot.Snapshot(scope, tree, coverage=<factory>, warnings=())[source]

Bases: object

One deterministic capture of a scope’s configuration.

scope

The DN the snapshot was rooted at (e.g. "uni", "uni/tn-prod").

Type:

str

tree

The captured configuration as nested plain dicts, wire-format:

{"class": "fvTenant", "rn": "tn-prod",
 "attributes": {"name": "prod", ...},
 "children": [...]}

Children are sorted by (class, rn) and attributes by name — the determinism lives in the data, not in the serialiser.

Type:

dict[str, Any] | None

coverage

Instance count per class — the interpretation beside the fact: what the capture actually contains, at a glance.

Type:

dict[str, int]

warnings

Human-readable notices the caller must not ignore, one per object whose DN itself carries a secret (an SNMP community profile is named by its community string).

Type:

tuple[str, …]

to_json()[source]

Serialise deterministically — same state, same bytes.

One attribute per line, sorted keys, stable child order: the format is chosen for git diff, not for compactness.

classmethod from_json(text)[source]

Rehydrate a snapshot serialised by to_json().

Raises:

KeyError – The text is not a niwaki snapshot document.

class niwaki.snapshot.SnapshotDiff(added, removed, changed)[source]

Bases: object

The structural difference between two snapshots.

added

DNs present in b but not in a, sorted.

Type:

tuple[str, …]

removed

DNs present in a but not in b, sorted.

Type:

tuple[str, …]

changed

Per-DN attribute deltas, {dn: {wire: (a_value, b_value)}} — only attributes that differ; an attribute present on one side only appears with None on the other.

Type:

dict[str, dict[str, tuple[Any, Any]]]

property has_changes: bool

True when the two snapshots describe different configuration.