NetStacksNetStacks

Config Diff & Version History

Enterprise

Diff 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_sync without 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.

Enterprise controller feature

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.

Note

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}/diff

The 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.

Drift then diff

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

  1. Open the device in the controller and go to its config history.
  2. Each stored version is listed with its version number, capture time, source, and hash. Note the two version numbers you want to compare.
  3. Programmatically, list versions with GET /api/admin/config/devices/{id}/configs and read the version and config_hash fields.

Diff Two Versions

  1. In the UI, select two versions from the history and open the diff view.
  2. The diff shows added lines (prefixed +) and removed lines (prefixed -), with addition and deletion counts.
  3. Via the API, call GET /api/admin/config/devices/{id}/diff-versions?old_version=…&new_version=….

Check for Drift Against the Live Device

  1. Ensure the device has a working gNMI or NETCONF transport and credentials configured.
  2. Call POST /api/admin/config/devices/{id}/diff. NetStacks pulls the live config and compares its hash to the latest stored version.
  3. If in_sync is false, pull a fresh version (config pull), then diff the new version against the prior one to see the exact changes.
Post-maintenance verification

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

  1. Render the intended config from a template (see Templates & Rendering).
  2. Pull a fresh version from the device so the latest stored version reflects reality.
  3. Fetch the rendered template text and the latest version's config_text via the API, then compare them with your own tooling (for example diff -u or git 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-versions.shbash
# 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

diff-versions.shbash
# 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

version-diff-response.jsonjson
{
  "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

drift-check.shbash
# 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

drift-response.jsonjson
{
  "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:

config-diff-example.txttext
 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 synchronous

Detect Changes Across the Version Timeline

detect-changes.shbash
# 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-prefixed diff string plus additions and deletions counts.
Q: How do I check whether a device has drifted from its stored config?
A: Call POST /api/admin/config/devices/{device_id}/diff with 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_text via GET /api/admin/config/devices/{id}/configs/latest and diff it against your rendered output with your own tooling.
Q: What diff format is used?
A: A line-based text diff (the Rust similar crate). 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_hash on 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_text via GET /api/admin/config/devices/{id}/configs/latest and 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 flip in_sync to false even with no real change.
  • Counters or uptime fields — some structured pulls include dynamic operational data; restrict the pull path to 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:

check-versions.shbash
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:

large-diff.shbash
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.json

Config diff works alongside snapshot and template features for full configuration lifecycle management: