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)
pip install diffncFor development:
uv syncfrom 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"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. |
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-spineOr, in reconcile mode (experimental):
$ diffnc before.conf after.conf -r
interface Ethernet1/1
no description uplink
description uplink-to-spine
feature ospfInput 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
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.
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
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;
}
}
}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(...).
uv sync --extra dev
uv run pytest # tests
uv run ruff check . # lint
uv run ruff format . # format
uv run ty check # type checkCreate 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.)
MIT