diffnc 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffnc/__init__.py +37 -0
- diffnc/__main__.py +8 -0
- diffnc/cli.py +140 -0
- diffnc/detect.py +170 -0
- diffnc/diff.py +353 -0
- diffnc/errors.py +22 -0
- diffnc/ir.py +58 -0
- diffnc/py.typed +0 -0
- diffnc/reconcile.py +132 -0
- diffnc/vendors/__init__.py +58 -0
- diffnc/vendors/_cisco_like.py +203 -0
- diffnc/vendors/base.py +88 -0
- diffnc/vendors/eos.py +19 -0
- diffnc/vendors/ios.py +19 -0
- diffnc/vendors/iosxe.py +20 -0
- diffnc/vendors/iosxr.py +19 -0
- diffnc/vendors/junos.py +175 -0
- diffnc/vendors/junos_set.py +158 -0
- diffnc/vendors/nxos.py +22 -0
- diffnc-0.0.1.dist-info/METADATA +283 -0
- diffnc-0.0.1.dist-info/RECORD +24 -0
- diffnc-0.0.1.dist-info/WHEEL +4 -0
- diffnc-0.0.1.dist-info/entry_points.txt +2 -0
- diffnc-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Junos set-form vendor parser.
|
|
2
|
+
|
|
3
|
+
Handles the flat ``set ...`` form produced by ``display set``. Documents are modelled as a
|
|
4
|
+
flat list of leaves (one per logical ``set`` statement); duplicate statements collapse to
|
|
5
|
+
one. ``activate <path>`` and ``deactivate <path>`` are treated as a tri-state per path
|
|
6
|
+
(activated / deactivated / unspecified), applied in input order — the last toggle wins and
|
|
7
|
+
only one state node per path remains in the tree. ``delete <path>`` is applied in-order:
|
|
8
|
+
it removes any earlier ``set``/``activate``/``deactivate`` whose path equals or starts
|
|
9
|
+
with ``<path>`` at a token boundary, mirroring the Junos CLI semantics. Subsequent ``set``
|
|
10
|
+
statements may re-add state.
|
|
11
|
+
|
|
12
|
+
Line comments (``#``, ``//`` to end of line) are stripped during parsing.
|
|
13
|
+
|
|
14
|
+
The hierarchical (``show configuration``) form is handled by a separate vendor, see
|
|
15
|
+
:mod:`diffnc.vendors.junos`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import Iterable, Iterator
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from diffnc.errors import ParseError
|
|
25
|
+
from diffnc.ir import ConfigNode, ConfigTree
|
|
26
|
+
from diffnc.vendors.base import VendorParser
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from diffnc.reconcile import ReconcileEvent
|
|
30
|
+
|
|
31
|
+
_SET_PREFIXES = ("set ", "deactivate ", "activate ")
|
|
32
|
+
_DELETE_PREFIX = "delete "
|
|
33
|
+
_TOGGLE_PREFIXES = ("activate ", "deactivate ")
|
|
34
|
+
_VALID_SET_PREFIXES = (*_SET_PREFIXES, _DELETE_PREFIX)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class _JunosSetParser:
|
|
39
|
+
name: str = "junos_set"
|
|
40
|
+
|
|
41
|
+
def parse(self, text: str) -> ConfigTree:
|
|
42
|
+
tree = ConfigTree.empty(vendor=self.name)
|
|
43
|
+
for lineno, raw in enumerate(text.splitlines(), start=1):
|
|
44
|
+
stripped = _strip_line_comment(raw).strip()
|
|
45
|
+
if not stripped:
|
|
46
|
+
continue
|
|
47
|
+
if not stripped.startswith(_VALID_SET_PREFIXES):
|
|
48
|
+
raise ParseError(
|
|
49
|
+
f"line {lineno}: expected 'set'/'activate'/'deactivate'/'delete' line, "
|
|
50
|
+
f"got {stripped!r}"
|
|
51
|
+
)
|
|
52
|
+
if stripped.startswith(_DELETE_PREFIX):
|
|
53
|
+
path = stripped[len(_DELETE_PREFIX) :].strip()
|
|
54
|
+
if not path:
|
|
55
|
+
raise ParseError(f"line {lineno}: 'delete' requires a path")
|
|
56
|
+
_apply_delete(tree.root, path)
|
|
57
|
+
continue
|
|
58
|
+
if stripped.startswith(_TOGGLE_PREFIXES):
|
|
59
|
+
op, _, path = stripped.partition(" ")
|
|
60
|
+
path = path.strip()
|
|
61
|
+
if not path:
|
|
62
|
+
raise ParseError(f"line {lineno}: {op!r} requires a path")
|
|
63
|
+
_apply_state_toggle(tree.root, path, stripped)
|
|
64
|
+
continue
|
|
65
|
+
tree.root.add_child(ConfigNode(line=stripped))
|
|
66
|
+
return tree
|
|
67
|
+
|
|
68
|
+
def format(self, tree: ConfigTree) -> list[str]:
|
|
69
|
+
return [child.line for child in tree.root.children]
|
|
70
|
+
|
|
71
|
+
def render_open(self, node: ConfigNode, depth: int) -> str:
|
|
72
|
+
# Set form has no sections, so this should never be invoked by the diff engine.
|
|
73
|
+
raise ParseError("render_open is not applicable to Junos set form")
|
|
74
|
+
|
|
75
|
+
def render_close(self, node: ConfigNode, depth: int) -> str | None:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def render_leaf(self, node: ConfigNode, depth: int) -> str:
|
|
79
|
+
return node.line
|
|
80
|
+
|
|
81
|
+
def is_order_sensitive(self, path: tuple[str, ...]) -> bool:
|
|
82
|
+
"""Set form has no semantically ordered children.
|
|
83
|
+
|
|
84
|
+
:meth:`parse` already replays the activate/deactivate/delete operations to a
|
|
85
|
+
canonical final state, so the IR's residual order is just whatever the input
|
|
86
|
+
happened to leave behind — never load-bearing.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def render_reconcile(self, events: Iterable[ReconcileEvent]) -> Iterator[str]:
|
|
92
|
+
from diffnc.reconcile import ReconcileAdd, ReconcileDelete
|
|
93
|
+
|
|
94
|
+
for ev in events:
|
|
95
|
+
if isinstance(ev, ReconcileAdd):
|
|
96
|
+
yield ev.node.line
|
|
97
|
+
elif isinstance(ev, ReconcileDelete):
|
|
98
|
+
yield f"delete {_strip_set_prefix(ev.node.line)}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _apply_state_toggle(root: ConfigNode, path: str, new_line: str) -> None:
|
|
102
|
+
"""Toggle the activate/deactivate state for ``path``.
|
|
103
|
+
|
|
104
|
+
Replaces any existing ``activate <path>`` / ``deactivate <path>`` child line in place
|
|
105
|
+
(preserving first-occurrence position), or appends a new state node when none exists.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
activate_line = f"activate {path}"
|
|
109
|
+
deactivate_line = f"deactivate {path}"
|
|
110
|
+
for child in root.children:
|
|
111
|
+
if child.line == activate_line or child.line == deactivate_line:
|
|
112
|
+
child.line = new_line
|
|
113
|
+
return
|
|
114
|
+
root.children.append(ConfigNode(line=new_line))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _strip_set_prefix(line: str) -> str:
|
|
118
|
+
for prefix in _SET_PREFIXES:
|
|
119
|
+
if line.startswith(prefix):
|
|
120
|
+
return line[len(prefix) :]
|
|
121
|
+
return line
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _apply_delete(root: ConfigNode, delete_path: str) -> None:
|
|
125
|
+
"""Drop children whose set-form path equals or token-prefix-matches ``delete_path``."""
|
|
126
|
+
|
|
127
|
+
boundary = delete_path + " "
|
|
128
|
+
surviving: list[ConfigNode] = []
|
|
129
|
+
for child in root.children:
|
|
130
|
+
path = _strip_set_prefix(child.line)
|
|
131
|
+
if path == delete_path or path.startswith(boundary):
|
|
132
|
+
continue
|
|
133
|
+
surviving.append(child)
|
|
134
|
+
root.children = surviving
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _strip_line_comment(line: str) -> str:
|
|
138
|
+
"""Truncate at the first ``#`` or ``//`` that lies outside a double-quoted string."""
|
|
139
|
+
|
|
140
|
+
in_quote = False
|
|
141
|
+
i = 0
|
|
142
|
+
n = len(line)
|
|
143
|
+
while i < n:
|
|
144
|
+
ch = line[i]
|
|
145
|
+
if ch == '"':
|
|
146
|
+
in_quote = not in_quote
|
|
147
|
+
i += 1
|
|
148
|
+
continue
|
|
149
|
+
if not in_quote:
|
|
150
|
+
if ch == "#":
|
|
151
|
+
return line[:i]
|
|
152
|
+
if ch == "/" and i + 1 < n and line[i + 1] == "/":
|
|
153
|
+
return line[:i]
|
|
154
|
+
i += 1
|
|
155
|
+
return line
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
PARSER: VendorParser = _JunosSetParser()
|
diffnc/vendors/nxos.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""NX-OS vendor parser.
|
|
2
|
+
|
|
3
|
+
NX-OS uses significant indentation (2-space unit) under section heads such as
|
|
4
|
+
``interface Ethernet1/1`` or ``vrf context FOO``. Shutdown toggling, ``default``
|
|
5
|
+
semantics, and ``!``/``#`` comment handling are shared with the other Cisco-style
|
|
6
|
+
vendors via :mod:`diffnc.vendors._cisco_like`.
|
|
7
|
+
|
|
8
|
+
Unlike IOS, saved NX-OS configurations do not typically include ``end`` / ``exit``
|
|
9
|
+
terminator lines, so we leave the ``terminators`` set empty here to keep the existing
|
|
10
|
+
behaviour where such lines would be parsed as ordinary config statements if present.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from diffnc.vendors._cisco_like import CiscoLikeParser
|
|
16
|
+
from diffnc.vendors.base import VendorParser
|
|
17
|
+
|
|
18
|
+
PARSER: VendorParser = CiscoLikeParser(
|
|
19
|
+
name="nxos",
|
|
20
|
+
indent_unit=2,
|
|
21
|
+
terminators=frozenset(),
|
|
22
|
+
)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diffnc
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Structural diff library for network device configurations
|
|
5
|
+
Author-email: minefuto <46558834+minefuto@users.noreply.github.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 minefuto
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
33
|
+
Requires-Python: >=3.11
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# diffnc(DIFF for Network device Configurations)
|
|
37
|
+
|
|
38
|
+
A Python library and CLI that diffs network device configurations **with structural awareness**, exposed through a `difflib`-like API.
|
|
39
|
+
|
|
40
|
+
* Duplicate same-name blocks (e.g. `interface eth1` appearing more than once) are merged at parse time
|
|
41
|
+
* **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
|
|
42
|
+
* Vendor is auto-detected. Diffing across vendors raises an error
|
|
43
|
+
* Supported vendors: **Cisco NX-OS**, **Cisco IOS**, **Cisco IOS-XE**, **Cisco IOS-XR**, **Arista EOS**, **Junos** (hierarchical), **Junos set** (`display set` format)
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install diffnc
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For development:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv sync
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Library usage
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from diffnc import unified_diff, ndiff
|
|
61
|
+
|
|
62
|
+
with open("router-before.conf") as f:
|
|
63
|
+
a = f.read()
|
|
64
|
+
with open("router-after.conf") as f:
|
|
65
|
+
b = f.read()
|
|
66
|
+
|
|
67
|
+
# Structural unified diff (shows changed lines and their parent sections only)
|
|
68
|
+
for line in unified_diff(a, b, fromfile="before", tofile="after"):
|
|
69
|
+
print(line, end="")
|
|
70
|
+
|
|
71
|
+
# Full ndiff
|
|
72
|
+
for line in ndiff(a, b):
|
|
73
|
+
print(line, end="")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
To force a specific vendor:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
unified_diff(a, b, vendor="junos_set")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
To only run detection:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from diffnc import detect_vendor
|
|
86
|
+
detect_vendor(open("config.conf").read()) # -> "nxos"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `reconcile` (experimental)
|
|
90
|
+
|
|
91
|
+
> **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.
|
|
92
|
+
|
|
93
|
+
`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*.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from diffnc import reconcile
|
|
97
|
+
|
|
98
|
+
for line in reconcile(a, b):
|
|
99
|
+
print(line)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Output is config-mode commands only — no `configure terminal` / `end` / `commit` wrappers, no indentation. Pipe through your own session manager.
|
|
103
|
+
|
|
104
|
+
* **Cisco-like (NX-OS / IOS / IOS-XE / IOS-XR / EOS):** emits section navigation plus `<line>` for adds and `no <line>` for deletes (with `no no foo` collapsed to `foo`, so `no shutdown` ↔ `shutdown` toggles correctly).
|
|
105
|
+
* **Junos hierarchical:** emits flat `set <path>` and `delete <path>` lines.
|
|
106
|
+
* **Junos set:** emits `<line>` verbatim for adds and `delete <path>` (with the `set ` / `activate ` / `deactivate ` prefix stripped) for deletes.
|
|
107
|
+
* **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.
|
|
108
|
+
|
|
109
|
+
Exceptions:
|
|
110
|
+
|
|
111
|
+
| Exception | When it is raised |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `VendorMismatchError` | The two configs are detected as different vendors (e.g. Junos set vs. Junos hierarchical is also rejected here) |
|
|
114
|
+
| `ParseError` | Vendor detection failed, syntax error, etc. |
|
|
115
|
+
|
|
116
|
+
## CLI
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
diffnc [OPTIONS] FILE_A FILE_B
|
|
120
|
+
|
|
121
|
+
-u, --unified Structural unified diff (default)
|
|
122
|
+
-n, --ndiff Full ndiff output
|
|
123
|
+
-r, --reconcile Emit config-mode commands that transform FILE_A into FILE_B (experimental)
|
|
124
|
+
--vendor {junos,junos_set,nxos,ios,iosxe,iosxr,eos}
|
|
125
|
+
Skip auto-detection and use the given vendor
|
|
126
|
+
--color {auto,always,never}
|
|
127
|
+
Colorize +/- lines (auto = tty detection)
|
|
128
|
+
--version
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Exit codes follow `diff(1)`: `0` = no differences, `1` = differences found, `2` = error.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
$ diffnc before.conf after.conf
|
|
137
|
+
--- before.conf
|
|
138
|
+
+++ after.conf
|
|
139
|
+
+feature ospf
|
|
140
|
+
interface Ethernet1/1
|
|
141
|
+
- description uplink
|
|
142
|
+
+ description uplink-to-spine
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Or, in reconcile mode (**experimental**):
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
$ diffnc before.conf after.conf -r
|
|
149
|
+
interface Ethernet1/1
|
|
150
|
+
no description uplink
|
|
151
|
+
description uplink-to-spine
|
|
152
|
+
feature ospf
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Example: normalizing duplicate blocks
|
|
156
|
+
|
|
157
|
+
Input A:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
interface eth1
|
|
161
|
+
no shut
|
|
162
|
+
ip address 1.1.1.1/24
|
|
163
|
+
stp
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Input B (the same `interface eth1` appears twice):
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
interface eth1
|
|
170
|
+
shut
|
|
171
|
+
ip address 1.1.1.1/24
|
|
172
|
+
|
|
173
|
+
interface eth1
|
|
174
|
+
stp
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`ndiff` output:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
interface eth1
|
|
181
|
+
- no shut
|
|
182
|
+
+ shut
|
|
183
|
+
ip address 1.1.1.1/24
|
|
184
|
+
stp
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## How order is handled
|
|
188
|
+
|
|
189
|
+
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**.
|
|
190
|
+
|
|
191
|
+
### Order-insensitive (reorder ≠ diff)
|
|
192
|
+
|
|
193
|
+
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.
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
# A
|
|
197
|
+
system {
|
|
198
|
+
host-name foo;
|
|
199
|
+
domain-name example.com;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# B
|
|
203
|
+
system {
|
|
204
|
+
domain-name example.com;
|
|
205
|
+
host-name foo;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
$ diffnc a.conf b.conf # → no diff, exit 0
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Order-sensitive (reorder = diff)
|
|
212
|
+
|
|
213
|
+
The paths below are evaluated in declaration order by the device, so swapping term/ACE/class order produces diff output.
|
|
214
|
+
|
|
215
|
+
| Vendor | Parent path | Children |
|
|
216
|
+
| --- | --- | --- |
|
|
217
|
+
| Junos | `firewall.filter <name>` | `term <name>` |
|
|
218
|
+
| Junos | `firewall.family <fam>.filter <name>` | `term <name>` |
|
|
219
|
+
| Junos | `policy-options.policy-statement <name>` | `term <name>` |
|
|
220
|
+
| Cisco-like (IOS / IOS-XE / IOS-XR / NX-OS / EOS) | `ip access-list <name>`, `ipv6 access-list <name>`, `mac access-list <name>` | ACE lines |
|
|
221
|
+
| Cisco-like (same as above) | `policy-map <name>` | `class <name>` blocks |
|
|
222
|
+
|
|
223
|
+
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.
|
|
224
|
+
|
|
225
|
+
Example: swapping two byte-identical terms inside a Junos firewall filter
|
|
226
|
+
|
|
227
|
+
```diff
|
|
228
|
+
firewall {
|
|
229
|
+
filter F {
|
|
230
|
+
! term B {
|
|
231
|
+
! then discard;
|
|
232
|
+
! }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Example: a reorder of one term plus a content change in another term
|
|
238
|
+
|
|
239
|
+
```diff
|
|
240
|
+
firewall {
|
|
241
|
+
filter F {
|
|
242
|
+
! term A {
|
|
243
|
+
! then accept;
|
|
244
|
+
! }
|
|
245
|
+
term B {
|
|
246
|
+
- then discard;
|
|
247
|
+
+ then reject;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Customizing the behavior for a new vendor
|
|
254
|
+
|
|
255
|
+
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(...)`.
|
|
256
|
+
|
|
257
|
+
## Development
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
uv sync --extra dev
|
|
261
|
+
uv run pytest # tests
|
|
262
|
+
uv run ruff check . # lint
|
|
263
|
+
uv run ruff format . # format
|
|
264
|
+
uv run ty check # type check
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Adding a new vendor
|
|
268
|
+
|
|
269
|
+
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`.
|
|
270
|
+
|
|
271
|
+
`VendorParser` requires the following methods:
|
|
272
|
+
|
|
273
|
+
* `parse(text) -> ConfigTree`
|
|
274
|
+
* `format(tree) -> list[str]`
|
|
275
|
+
* `render_open(node, depth) -> str`
|
|
276
|
+
* `render_close(node, depth) -> str | None`
|
|
277
|
+
* `render_leaf(node, depth) -> str`
|
|
278
|
+
* `is_order_sensitive(path) -> bool` (optional; treated as always `False` if not implemented. See the "How order is handled" section.)
|
|
279
|
+
* `render_reconcile(events) -> Iterator[str]` (optional; required only to support `reconcile`. Receives a sequence of `ReconcileAdd` / `ReconcileDelete` / `ReconcileRecreate` events from `diffnc.reconcile` and yields the corresponding CLI lines.)
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
diffnc/__init__.py,sha256=yavFeCIll2bGmhWcWVto-7okLVvIbjwHh7b8nCgxWFA,943
|
|
2
|
+
diffnc/__main__.py,sha256=hMk6DO5C-rRNRXCLgWgounKjoKZFY-z3e8RGksfkCqo,179
|
|
3
|
+
diffnc/cli.py,sha256=3deyuQqb0-vQf6QuhuI7OyTbps-sk8NF9LGxUYwMvys,4006
|
|
4
|
+
diffnc/detect.py,sha256=Bg1bRDsNvzEEC8kABL8aNCDk16N2uH5DyV84I49iW1s,5191
|
|
5
|
+
diffnc/diff.py,sha256=pnR62dCgN1dLUtu1laUx0noHh4ewsHF3rr6IaWFqd4E,12082
|
|
6
|
+
diffnc/errors.py,sha256=PfEvBZAEA7jeYzoyCNdZpvo1jhCF8OoH_dZZdPT-b9k,648
|
|
7
|
+
diffnc/ir.py,sha256=gN06VRvIGArvMPOOpeNZbQ6zEbvrfRfcowUtU0k-eWE,1979
|
|
8
|
+
diffnc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
diffnc/reconcile.py,sha256=kGlRBiFpAfd6bgflrM2oZCFwv89Uo7jRMaiLf5OhEo4,4714
|
|
10
|
+
diffnc/vendors/__init__.py,sha256=s5db9NNpV337_ov11PWlmGoyGp8iqtnm1i8JE3q7kuo,1726
|
|
11
|
+
diffnc/vendors/_cisco_like.py,sha256=jCS3g1b_jYLHgMWC0bJG8-qsb8UA2p9E-vL8VtTzk_I,7250
|
|
12
|
+
diffnc/vendors/base.py,sha256=DxHcmYd4Tj2VbI2xnPwAtLzH56Z2wOMrG7_FiwpN-Nw,3305
|
|
13
|
+
diffnc/vendors/eos.py,sha256=DizzGfb5Qcytj3CEYMPPw3uG78n428RuTQBLZs2ZOpU,704
|
|
14
|
+
diffnc/vendors/ios.py,sha256=jEvlUlV3XOJ04Q5K_oJtcmflBvnBi3gjOS4-Q4PFzXk,694
|
|
15
|
+
diffnc/vendors/iosxe.py,sha256=xsdyjALeJi66Zp17U2grdbBxQfiP6yzB60APakHbRNo,757
|
|
16
|
+
diffnc/vendors/iosxr.py,sha256=ZoKXFYmnM0wIDPxqMZAQ5XXIdrluOTAfIvx63_WqrX4,734
|
|
17
|
+
diffnc/vendors/junos.py,sha256=rufK-hmz8kvRa30OeU4LxYRtan95xIpRvrQ5x9jxINo,5896
|
|
18
|
+
diffnc/vendors/junos_set.py,sha256=y-_NaK4_fjRJbcjQlEXVRWMa_dg4j2EMi6-IGxvghSM,5807
|
|
19
|
+
diffnc/vendors/nxos.py,sha256=Kt9sSSf_5vfVWgq9GipcZSgghdut6yK0J23wLCx4h2s,817
|
|
20
|
+
diffnc-0.0.1.dist-info/METADATA,sha256=2--nghNCTvJJFsusR8XBKUn6xVpRzr1MdRdD6cxEXEk,9748
|
|
21
|
+
diffnc-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
22
|
+
diffnc-0.0.1.dist-info/entry_points.txt,sha256=F79q2jhkvDItSS2e4LhCFhPRy7w6PZ-F2aR9sg9LXvs,43
|
|
23
|
+
diffnc-0.0.1.dist-info/licenses/LICENSE,sha256=nQ7lrLsasMOfMg78sASYIcvRRhqCvGUYV7_1dXBbsBc,1065
|
|
24
|
+
diffnc-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 minefuto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|