Config Diff & Version History
EnterpriseDiff two stored config versions of a device, check whether the stored config is in sync with the live device, and walk the version-by-version change history.
Overview
Every time NetStacks pulls a configuration from a device, it stores the full text as a numbered version with a SHA-256 hash. The diff features let you compare those versions and check whether the most-recently-stored config still matches what is actually running on the device.
NetStacks provides two distinct config comparison operations:
- Version diff — Compare any two stored config versions of the same device and get a line-by-line text diff plus addition/deletion counts.
- Stored-vs-live drift check — Pull the current live config from the device and compare its hash to the latest stored version. The result tells you whether the device is
in_syncwithout returning the full configs.
Version history itself is just the list of stored versions: each one records its version number, format, hash, source, and timestamp, so you can walk the timeline and diff any two points in it.
Config versioning and diffing run in the NetStacks controller. All endpoints below live under /api/admin/config and require an operator-level permission. The standalone terminal app does not store config history.
Diffs are computed on demand from stored config text — NetStacks does not persist diff results. Each request re-reads the stored versions and recomputes the comparison, so results always reflect the actual stored configurations.
How It Works
Diff Algorithm
The diff is a plain line-based text comparison built on the Rust similar crate (TextDiff::from_lines). It does not normalize whitespace or strip timestamp lines — the configs are compared exactly as stored. Each output line is tagged:
- Added lines are prefixed with
+— present in the new version but not the old. - Removed lines are prefixed with
-— present in the old version but not the new. - Unchanged lines are prefixed with a single space.
The version-diff response also reports additions and deletions counts and an identical boolean (true when the two versions share the same hash, in which case the diff string is empty).
Version Diff (Two Stored Versions)
To compare two stored versions, NetStacks loads each version's full config text by version number, compares them, and returns the result. Both versions must belong to the same device. The endpoint is:
GET /api/admin/config/devices/{device_id}/diff-versions?old_version=<int>&new_version=<int>Note that the query parameters are integer version numbers (1, 2, 3…), not backup IDs. You can list a device's versions first to find the numbers you want to compare.
Stored-vs-Live Drift Check
The drift check connects to the device over its configured transport (gNMI or NETCONF), pulls the live config, hashes it, and compares that hash to the latest stored version. It returns a small JSON summary — not a line diff:
POST /api/admin/config/devices/{device_id}/diffThe response is { in_sync, latest_version, latest_hash, live_hash }. When in_sync is false, the live device has drifted from the stored config. This endpoint does not accept a pasted config body — it always reads the live config from the device itself.
A common workflow is to run the drift check to detect that a device is out of sync, then pull a fresh config version and run a version diff between the new version and the previous one to see exactly what changed on the box.
Version History
Listing a device's versions returns summaries (no full text) ordered by version. Each summary carries the config_hash, so consecutive versions with identical hashes represent no-change captures, while a hash change marks a real configuration change you can drill into with a version diff.
Step-by-Step Guide
Find the Versions to Compare
- Open the device in the controller and go to its config history.
- Each stored version is listed with its version number, capture time, source, and hash. Note the two version numbers you want to compare.
- Programmatically, list versions with
GET /api/admin/config/devices/{id}/configsand read theversionandconfig_hashfields.
Diff Two Versions
- In the UI, select two versions from the history and open the diff view.
- The diff shows added lines (prefixed
+) and removed lines (prefixed-), with addition and deletion counts. - Via the API, call
GET /api/admin/config/devices/{id}/diff-versions?old_version=…&new_version=….
Check for Drift Against the Live Device
- Ensure the device has a working gNMI or NETCONF transport and credentials configured.
- Call
POST /api/admin/config/devices/{id}/diff. NetStacks pulls the live config and compares its hash to the latest stored version. - If
in_syncisfalse, pull a fresh version (config pull), then diff the new version against the prior one to see the exact changes.
After a maintenance window, pull a fresh config version and diff it against the pre-maintenance version. The diff should show only the changes you intended. Any unexpected additions or deletions indicate unplanned modifications that need investigation.
Compare Against an Intended Template
- Render the intended config from a template (see Templates & Rendering).
- Pull a fresh version from the device so the latest stored version reflects reality.
- Fetch the rendered template text and the latest version's
config_textvia the API, then compare them with your own tooling (for examplediff -uorgit diff). The built-in version diff compares two stored versions, so template drift comparison is done client-side against the rendered output.
Code Examples
List Stored Config Versions
# List config versions (summaries, newest first) for a device
curl -s "http://localhost:8080/api/admin/config/devices/${DEVICE_ID}/configs?limit=20" \
-H "Authorization: Bearer ${TOKEN}" | jq '.configs[] | {version, config_hash, source, created_at}'Diff Two Stored Versions
# Compare version 7 against version 8 of the same device
curl -s "http://localhost:8080/api/admin/config/devices/${DEVICE_ID}/diff-versions?old_version=7&new_version=8" \
-H "Authorization: Bearer ${TOKEN}" | jq '.'Version Diff Response Shape
{
"old_version": 7,
"new_version": 8,
"old_format": "cli",
"new_format": "cli",
"identical": false,
"additions": 6,
"deletions": 2,
"diff": " ip access-list extended MGMT-ACCESS\n permit tcp 10.0.0.0 0.0.0.255 any eq 22\n+ permit tcp 10.0.1.0 0.0.0.255 any eq 22\n- deny ip any\n+ deny ip any any log\n"
}Check Drift: Stored vs. Live
# Pull the live config and compare its hash to the latest stored version.
# No request body is needed. credential_id / path are optional query params.
curl -s -X POST "http://localhost:8080/api/admin/config/devices/${DEVICE_ID}/diff" \
-H "Authorization: Bearer ${TOKEN}" | jq '.'Drift Check Response Shape
{
"in_sync": false,
"latest_version": 8,
"latest_hash": "9f1c0e3a2b...",
"live_hash": "4d77ab12cc..."
}Example Diff Output
The diff field is a single string of line-prefixed text. Pretty printed, a maintenance-window change to a Cisco IOS router (an ACL update and a new VLAN interface) looks like this:
ip access-list extended MGMT-ACCESS
permit tcp 10.0.0.0 0.0.0.255 any eq 22
permit tcp 10.0.0.0 0.0.0.255 any eq 443
+ permit tcp 10.0.1.0 0.0.0.255 any eq 22
+ permit tcp 10.0.1.0 0.0.0.255 any eq 443
deny ip any any log
!
+interface Vlan200
+ description Server-VLAN-200
+ ip address 10.1.200.1 255.255.255.0
+ ip helper-address 10.0.0.53
+ no shutdown
+!
line con 0
logging synchronousDetect Changes Across the Version Timeline
# List versions and keep only those whose hash differs from the previous one.
# Versions are returned newest-first, so reverse before walking the timeline.
curl -s "http://localhost:8080/api/admin/config/devices/${DEVICE_ID}/configs?limit=50" \
-H "Authorization: Bearer ${TOKEN}" | \
jq '[.configs[] | {version, config_hash, created_at}] | reverse |
to_entries |
map(select(.key == 0 or .value.config_hash != .[.key-1].value.config_hash)) |
map(.value)'Questions & Answers
- Q: How do I diff two configurations of a device?
- A: Use the version-diff endpoint with the two version numbers:
GET /api/admin/config/devices/{device_id}/diff-versions?old_version=…&new_version=…. The parameters are integer version numbers (not IDs). The response returns a line-prefixeddiffstring plusadditionsanddeletionscounts. - Q: How do I check whether a device has drifted from its stored config?
- A: Call
POST /api/admin/config/devices/{device_id}/diffwith no body. NetStacks pulls the live config over gNMI/NETCONF, hashes it, and compares to the latest stored version. The response{ in_sync, latest_version, latest_hash, live_hash }tells you whether they match. - Q: Can I paste a config or template and diff it against the device?
- A: Not directly. The drift endpoint always reads the live config from the device and has no pasted-text input. To compare against a rendered template, fetch the latest version's
config_textviaGET /api/admin/config/devices/{id}/configs/latestand diff it against your rendered output with your own tooling. - Q: What diff format is used?
- A: A line-based text diff (the Rust
similarcrate). Added lines are prefixed with+, removed lines with-, and unchanged lines with a single space. It is not a hunk-header unified diff and contains no@@markers. - Q: How far back does version history go?
- A: As far back as your stored versions. Every config pull adds a new numbered version, so the history covers every capture. Use the
config_hashon each version summary to spot where real changes occurred without reading every version in full. - Q: Can I compare configs between two different devices?
- A: The built-in version diff compares versions of the same device. To compare two devices, fetch each device's latest version
config_textviaGET /api/admin/config/devices/{id}/configs/latestand diff them externally — useful for verifying redundant pairs match.
Troubleshooting
Drift check reports out-of-sync after every poll
The drift check compares exact hashes, with no whitespace normalization or timestamp filtering. Configs that contain volatile lines will hash differently on every pull. Common causes:
- Timestamps in the config — lines like
! Last configuration change at …change every capture and will flipin_synctofalseeven with no real change. - Counters or uptime fields — some structured pulls include dynamic operational data; restrict the pull
pathto configuration subtrees where possible.
Run a version diff between two pulls to see exactly which lines differ and confirm whether the change is real or just a volatile line.
Version diff returns identical when you expected changes
If identical is true, the two versions share the same hash and the diff string is empty. Verify you passed the correct old_version and new_version numbers — consecutive captures of an unchanged device produce versions with the same hash.
404 Version not found
diff-versions returns a not-found error if either old_version or new_version does not exist for the device. List the device's versions first to confirm the numbers:
curl -s "http://localhost:8080/api/admin/config/devices/${DEVICE_ID}/configs" \
-H "Authorization: Bearer ${TOKEN}" | jq '.configs[].version'Large diffs are slow
Devices with very large configurations produce large diff strings. Fetch the diff via the API and write it to a file rather than rendering it inline:
curl -s "http://localhost:8080/api/admin/config/devices/${DEVICE_ID}/diff-versions?old_version=${OLD}&new_version=${NEW}" \
-H "Authorization: Bearer ${TOKEN}" > diff-output.jsonRelated Features
Config diff works alongside snapshot and template features for full configuration lifecycle management:
- Config Snapshots — Capture the config versions that diffs compare
- Templates & Rendering — Generate intended configs to compare against stored versions
- Template Versioning — Track template changes the same way device configs are versioned
- Adding Devices — Devices must exist before configs can be pulled and versioned
- Scheduled Tasks — Automate periodic config pulls that build up the version history
- Devices API — Reference for the device and config endpoints used above