Skip to main content

Structural diff library for network device configurations

Project description

diffnc(DIFF for Network device Configurations)

PyPI - Python Version PyPI - Version GitHub License

A Python library and CLI that diffs network device configurations with structural awareness, exposed through a difflib-like API.

  • Duplicate same-name blocks (e.g. interface eth1 appearing more than once) are merged at parse time
  • Only sections where order carries meaning emit order diffs (Junos firewall filter / policy-statement terms, Cisco access-list / policy-map, etc.). Everywhere else, reordering alone produces no diff
  • Vendor is auto-detected. Diffing across vendors raises an error
  • Supported vendors: Cisco NX-OS, Cisco IOS, Cisco IOS-XE, Cisco IOS-XR, Arista EOS, Junos (hierarchical), Junos set (display set format)

Installation

pip install diffnc

For development:

uv sync

Library usage

from diffnc import unified_diff, ndiff

with open("router-before.conf") as f:
    a = f.read()
with open("router-after.conf") as f:
    b = f.read()

# Structural unified diff (shows changed lines and their parent sections only)
for line in unified_diff(a, b, fromfile="before", tofile="after"):
    print(line, end="")

# Full ndiff
for line in ndiff(a, b):
    print(line, end="")

To force a specific vendor:

unified_diff(a, b, vendor="junos_set")

To only run detection:

from diffnc import detect_vendor
detect_vendor(open("config.conf").read())  # -> "nxos"

reconcile (experimental)

Experimental. The output shape and exact command sequences may change in future releases. Always review the generated commands before applying them to a live device.

reconcile(a, b) returns the bare config-mode command lines that, when entered on a device currently running config A, bring it to the state described by config B.

from diffnc import reconcile

for line in reconcile(a, b):
    print(line)

Output is config-mode commands only — no configure terminal / end / commit wrappers. Lines are indented to mirror the section hierarchy (using each vendor's own indent unit), so the result reads like the source config; flat vendors such as Junos set form emit no indentation. Pipe through your own session manager.

  • Cisco-like (NX-OS / IOS / IOS-XE / IOS-XR / EOS): emits section navigation plus <line> for adds and no <line> for deletes. X / no X toggles are tracked as state: a transition (e.g. no shutdownshutdown) emits just the new value, while a removed toggle resets to default — default <cmd> on vendors that support it (IOS / IOS-XE / NX-OS / EOS) and the inverted no on IOS-XR.
  • Junos hierarchical: emits flat set <path> and delete <path> lines. A node whose only change is its inactive state (inactive: <line>) emits activate <path> / deactivate <path> instead of a delete/set pair.
  • Junos set: emits <line> verbatim for adds and delete <path> (with the set prefix stripped) for deletes. activate / deactivate are tracked as state: a removed toggle inverts to its counterpart (deactivate Xactivate X) rather than deleting the underlying set.
  • Order-sensitive sections (ACL, policy-map, Junos firewall filter / policy-statement terms): on any change, the entire section is deleted and recreated from B — partial in-place edits are not attempted.

Exceptions:

Exception When it is raised
VendorMismatchError The two configs are detected as different vendors (e.g. Junos set vs. Junos hierarchical is also rejected here)
ParseError Vendor detection failed, syntax error, etc.

CLI

diffnc [OPTIONS] FILE_A FILE_B

  -u, --unified           Structural unified diff (default)
  -n, --ndiff             Full ndiff output
  -r, --reconcile         Emit config-mode commands that transform FILE_A into FILE_B (experimental)
  --vendor {junos,junos_set,nxos,ios,iosxe,iosxr,eos}
                          Skip auto-detection and use the given vendor
  --color {auto,always,never}
                          Colorize +/- lines (auto = tty detection)
  --version

Exit codes follow diff(1): 0 = no differences, 1 = differences found, 2 = error.

Example:

$ diffnc before.conf after.conf
--- before.conf
+++ after.conf
+feature ospf
 interface Ethernet1/1
-  description uplink
+  description uplink-to-spine

Or, in reconcile mode (experimental):

$ diffnc before.conf after.conf -r
interface Ethernet1/1
  description uplink-to-spine
feature ospf

Example: normalizing duplicate blocks

Input A:

interface eth1
  no shut
  ip address 1.1.1.1/24
  stp

Input B (the same interface eth1 appears twice):

interface eth1
  shut
  ip address 1.1.1.1/24

interface eth1
  stp

ndiff output:

  interface eth1
-   no shut
+   shut
    ip address 1.1.1.1/24
    stp

How order is handled

Network device configurations mix "sections whose semantics don't depend on order" with "sections where order determines behavior." diffnc diffs order-insensitively by default and only does position-based comparison for parent paths where order carries meaning.

Order-insensitive (reorder ≠ diff)

Most containers fall into this bucket. Examples: system, interfaces, routing-options, vrf context, top-level interface ..., route-map FOO permit <seq>, and so on. Reshuffling the children alone produces an empty diff.

# A
system {
    host-name foo;
    domain-name example.com;
}

# B
system {
    domain-name example.com;
    host-name foo;
}

$ diffnc a.conf b.conf   # → no diff, exit 0

Order-sensitive (reorder = diff)

The paths below are evaluated in declaration order by the device, so swapping term/ACE/class order produces diff output.

Vendor Parent path Children
Junos firewall.filter <name> term <name>
Junos firewall.family <fam>.filter <name> term <name>
Junos policy-options.policy-statement <name> term <name>
Cisco-like (IOS / IOS-XE / IOS-XR / NX-OS / EOS) ip access-list <name>, ipv6 access-list <name>, mac access-list <name> ACE lines
Cisco-like (same as above) policy-map <name> class <name> blocks

Pure reorders (children whose rendered subtree is byte-identical on both sides, just in a different position) are surfaced with a ! marker, once per moved subtree. Children whose contents also changed continue to use - / + pairs.

Example: swapping two byte-identical terms inside a Junos firewall filter

 firewall {
     filter F {
!        term B {
!            then discard;
!        }
     }
 }

Example: a reorder of one term plus a content change in another term

 firewall {
     filter F {
!        term A {
!            then accept;
!        }
         term B {
-            then discard;
+            then reject;
         }
     }
 }

Junos inactive/active toggles

When a Junos hierarchical node only flips its inactive: state (the node itself and its subtree are otherwise identical), the diff does not re-emit the whole subtree as a -/+ pair. Instead it pairs the two states as one node and marks the new state with a !:

  • A deactivated node keeps its literal inactive: prefix.
  • A reactivated node has no marker on the B side, so a synthetic activate: is shown to make the direction visible.
 system {
!    inactive: host-name foo;
 }

If the toggled section also has real changes inside it, the section header is marked ! and expanded so the inner -/+ (or nested !) lines still surface:

 protocols {
!    inactive: ospf {
         area 0.0.0.0 {
!            activate: interface ge-0/0/0.0;
         }
     }
 }

In reconcile, the same toggles emit activate <path> / deactivate <path> (see the reconcile section above).

Customizing the behavior for a new vendor

The VendorParser protocol exposes is_order_sensitive(path: tuple[str, ...]) -> bool. path is the tuple of line values from the root down to "the parent node whose children are being compared." Returning True makes the children compared positionally via SequenceMatcher; returning False (the default) falls back to set-style key comparison. If you're subclassing the Cisco family, the shortest path is to pass order_sensitive_predicate to CiscoLikeParser(...).

Development

uv sync
uv run pytest          # tests
uv run ruff check .    # lint
uv run ruff format .   # format
uv run ty check        # type check

Adding a new vendor

Create a new module under src/diffnc/vendors/, expose an implementation of the VendorParser protocol (src/diffnc/vendors/base.py) as PARSER, call register(_yourvendor.PARSER) from src/diffnc/vendors/__init__.py, and add the corresponding case to the detection logic in src/diffnc/detect.py.

VendorParser requires the following methods:

  • parse(text) -> ConfigTree
  • format(tree) -> list[str]
  • render_open(node, depth) -> str
  • render_close(node, depth) -> str | None
  • render_leaf(node, depth) -> str
  • is_order_sensitive(path) -> bool (optional; treated as always False if not implemented. See the "How order is handled" section.)
  • render_reconcile(events) -> Iterator[str] (optional; required only to support reconcile. Receives a sequence of ReconcileAdd / ReconcileDelete / ReconcileRecreate / ReconcileToggle events from diffnc.reconcile and yields the corresponding CLI lines.)
  • toggle_partners(a, b) -> bool / is_toggle_state(line) -> bool (optional; both default to False. Let reconcile recognise paired toggle states — e.g. Xno X, activatedeactivate — so a transition emits only the new value and a removed toggle is excluded from value-change matching.)
  • base_identity(line) -> str / render_toggle_line(b_line, depth, *, became_active, kind) -> str (optional; only needed for vendors with an inactive-state prefix such as Junos hierarchical inactive:. base_identity strips the prefix so a (de)activated node matches its active form as one node in two states; when that match's lines still differ, the diff/reconcile engines treat it as a toggle and call render_toggle_line to render the !-marked line — kind is "leaf", "section_open" or "section_collapsed" — or emit a ReconcileToggle.)

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

diffnc-0.0.5.tar.gz (63.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

diffnc-0.0.5-py3-none-any.whl (40.3 kB view details)

Uploaded Python 3

File details

Details for the file diffnc-0.0.5.tar.gz.

File metadata

  • Download URL: diffnc-0.0.5.tar.gz
  • Upload date:
  • Size: 63.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for diffnc-0.0.5.tar.gz
Algorithm Hash digest
SHA256 6ff8565224fda688c674994cdcb151d0d9a4dd30c6df0e8ce7206da55a8c5b5d
MD5 4dfcd14161c653f7382d8b9f4c90f1d6
BLAKE2b-256 8ee861dd24d489419f3999d39593fcc068ae47cb6f4d1ebeec0cffbc66dd0b62

See more details on using hashes here.

Provenance

The following attestation bundles were made for diffnc-0.0.5.tar.gz:

Publisher: publish.yml on minefuto/diffnc

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file diffnc-0.0.5-py3-none-any.whl.

File metadata

  • Download URL: diffnc-0.0.5-py3-none-any.whl
  • Upload date:
  • Size: 40.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for diffnc-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 bcbc09f73aef2dba34c62d0180a081d4b5262e3e4210c49c05c23f5fb1a1ab96
MD5 2c29a15c3c868a804e972e4f03ba8ebf
BLAKE2b-256 98816a73dae26eb3d09d4d70202c7b8bfe0ad87039d503566d1d1ff636de1996

See more details on using hashes here.

Provenance

The following attestation bundles were made for diffnc-0.0.5-py3-none-any.whl:

Publisher: publish.yml on minefuto/diffnc

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page