Structural diff library for network device configurations
Project description
diffnc(DIFF for Network device Configurations)
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 eth1appearing more than once) are merged at parse time - Only sections where order carries meaning emit order diffs (Junos
firewall filter/policy-statementterms, Ciscoaccess-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 setformat)
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, 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 andno <line>for deletes (withno no foocollapsed tofoo, sono shutdown↔shutdowntoggles correctly). - Junos hierarchical: emits flat
set <path>anddelete <path>lines. - Junos set: emits
<line>verbatim for adds anddelete <path>(with theset/activate/deactivateprefix stripped) for deletes. - Order-sensitive sections (ACL,
policy-map, Junosfirewall filter/policy-statementterms): 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
no description uplink
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;
}
}
}
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 --extra dev
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) -> ConfigTreeformat(tree) -> list[str]render_open(node, depth) -> strrender_close(node, depth) -> str | Nonerender_leaf(node, depth) -> stris_order_sensitive(path) -> bool(optional; treated as alwaysFalseif not implemented. See the "How order is handled" section.)render_reconcile(events) -> Iterator[str](optional; required only to supportreconcile. Receives a sequence ofReconcileAdd/ReconcileDelete/ReconcileRecreateevents fromdiffnc.reconcileand yields the corresponding CLI lines.)
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file diffnc-0.0.1.tar.gz.
File metadata
- Download URL: diffnc-0.0.1.tar.gz
- Upload date:
- Size: 51.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
343cf90cb931d8f03ab95fe6fd8c42f8777b16ee02cc7c06db46dc5e0fc4830f
|
|
| MD5 |
c96094813da3e9ebbd5d37a96b035cbe
|
|
| BLAKE2b-256 |
c90b819c50739a2f628b69d381bca1d4960cf5d1b79018df1a95cff236fc2cb3
|
Provenance
The following attestation bundles were made for diffnc-0.0.1.tar.gz:
Publisher:
publish.yml on minefuto/diffnc
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
diffnc-0.0.1.tar.gz -
Subject digest:
343cf90cb931d8f03ab95fe6fd8c42f8777b16ee02cc7c06db46dc5e0fc4830f - Sigstore transparency entry: 1670455665
- Sigstore integration time:
-
Permalink:
minefuto/diffnc@79504776bc791373a93495816508be9ace8dd125 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/minefuto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@79504776bc791373a93495816508be9ace8dd125 -
Trigger Event:
push
-
Statement type:
File details
Details for the file diffnc-0.0.1-py3-none-any.whl.
File metadata
- Download URL: diffnc-0.0.1-py3-none-any.whl
- Upload date:
- Size: 30.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20bd57a30ae52c561cd33cd36046bd55eb2a8eaae52dc0dd468286d36c8e525e
|
|
| MD5 |
77a3194a5b48d155c7baf7e2b3ea63ac
|
|
| BLAKE2b-256 |
a2f4bfa7f2724307777083af76952aca0b8eb96af700acc4ac0f7ac38ffb42cd
|
Provenance
The following attestation bundles were made for diffnc-0.0.1-py3-none-any.whl:
Publisher:
publish.yml on minefuto/diffnc
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
diffnc-0.0.1-py3-none-any.whl -
Subject digest:
20bd57a30ae52c561cd33cd36046bd55eb2a8eaae52dc0dd468286d36c8e525e - Sigstore transparency entry: 1670455843
- Sigstore integration time:
-
Permalink:
minefuto/diffnc@79504776bc791373a93495816508be9ace8dd125 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/minefuto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@79504776bc791373a93495816508be9ace8dd125 -
Trigger Event:
push
-
Statement type: