confgraph 0.3.8__tar.gz → 0.3.9__tar.gz
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.
- {confgraph-0.3.8 → confgraph-0.3.9}/PKG-INFO +14 -1
- {confgraph-0.3.8 → confgraph-0.3.9}/README.md +13 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/topology.py +12 -1
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/__init__.py +62 -1
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/iosxr_parser.py +169 -17
- confgraph-0.3.9/confgraph/parsers/junos_deletions.py +308 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/junos_hierarchy.py +133 -11
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/junos_parser.py +89 -12
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/nxos_parser.py +44 -14
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/utils/ospf.py +30 -0
- confgraph-0.3.9/dist-0.3.8/confgraph-0.3.8-py3-none-any.whl +0 -0
- confgraph-0.3.9/dist-0.3.8/confgraph-0.3.8.tar.gz +0 -0
- confgraph-0.3.9/dist-0.3.9/.gitignore +1 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/pyproject.toml +1 -1
- {confgraph-0.3.8 → confgraph-0.3.9}/uv.lock +1 -1
- {confgraph-0.3.8 → confgraph-0.3.9}/.github/workflows/publish.yml +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/.github/workflows/redos-scan.yml +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/.gitignore +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/.python-version +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/CHANGELOG.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/CLAUDE.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/INTERFACE.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/LICENSE +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/analysis/__init__.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/analysis/dependency_resolver.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/change_ir.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/cli.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/__init__.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/assets/cytoscape-dagre.min.js +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/assets/cytoscape.min.js +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/assets/dagre.min.js +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/builder.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/exporters/__init__.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/exporters/base.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/exporters/html.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/graph/exporters/json.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/loader.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/__init__.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/aaa.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/acl.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/banner.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/base.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/bfd.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/bgp.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/cdp.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/community_list.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/crypto.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/dhcp.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/dns.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/eem.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/eigrp.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/evpn.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/interface.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/ipsla.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/isis.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/line.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/lldp.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/logging_config.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/mpls.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/multicast.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/nat.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/netflow.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/ntp.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/object_group.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/object_tracking.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/ospf.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/panos_zone.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/parsed_config.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/prefix_list.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/qos.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/rip.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/route_map.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/snmp.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/static_route.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/stp.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/vlan.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/vpc.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/vrf.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/models/vxlan.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/base.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/eos_parser.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/ios_parser.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/junos_groups.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/panos_parser.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/parsers/panos_xml.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/tombstones.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/topology/__init__.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/topology/exporters.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/topology/graph.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/topology/ingest.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/utils/__init__.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/utils/escaping.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/confgraph/utils/interface.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/dist-0.3.7/.gitignore +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/dist-0.3.7/confgraph-0.3.7-py3-none-any.whl +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/dist-0.3.7/confgraph-0.3.7.tar.gz +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/dist-0.3.8/.gitignore +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/ADDING_NEW_OS_SUPPORT.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/ARCHITECTURE.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/CLI_USAGE.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/EOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/IOSXR_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/IOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/JUNOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/NXOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/PANOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/PARSER_SUPPORT_MATRIX.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/bgp_cluster_view.png +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/default_view.png +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/specific_node_highlight.png +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/docs/supported_os_types.md +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/main.py +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/complex_router.ios +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/eos.txt +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/ios.txt +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/ios01.txt +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/ios_xe.txt +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/ios_xr.txt +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/iosxr_test.cfg +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/junos_test.cfg +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/nxos.txt +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/samples/panos_sample.xml +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/tools/redos_baseline.json +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/tools/redos_scan.json +0 -0
- {confgraph-0.3.8 → confgraph-0.3.9}/tools/redos_scan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: confgraph
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Local-first network config dependency mapper — parse, visualize, and lint vendor configs
|
|
5
5
|
Project-URL: Homepage, https://github.com/verigraphs/confgraph
|
|
6
6
|
Project-URL: Repository, https://github.com/verigraphs/confgraph
|
|
@@ -131,6 +131,19 @@ See [`docs/CLI_USAGE.md`](docs/CLI_USAGE.md) for all commands, options, and exam
|
|
|
131
131
|
|
|
132
132
|
Contributions welcome — new parsers, bug fixes, additional protocol coverage. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) to get started.
|
|
133
133
|
|
|
134
|
+
### Running the tests
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
uv run --extra dev pytest
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**The `--extra dev` is not optional.** `pytest` is an optional dependency (see
|
|
141
|
+
`[project.optional-dependencies]`), so a bare `uv run pytest` falls back to
|
|
142
|
+
whatever `pytest` is on your `PATH` — which runs the suite against a different
|
|
143
|
+
environment and fails well over a thousand tests for reasons that have nothing
|
|
144
|
+
to do with your change. There is no error message saying so; it just looks like
|
|
145
|
+
you broke everything.
|
|
146
|
+
|
|
134
147
|
## License
|
|
135
148
|
|
|
136
149
|
Apache 2.0
|
|
@@ -101,6 +101,19 @@ See [`docs/CLI_USAGE.md`](docs/CLI_USAGE.md) for all commands, options, and exam
|
|
|
101
101
|
|
|
102
102
|
Contributions welcome — new parsers, bug fixes, additional protocol coverage. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) to get started.
|
|
103
103
|
|
|
104
|
+
### Running the tests
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uv run --extra dev pytest
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**The `--extra dev` is not optional.** `pytest` is an optional dependency (see
|
|
111
|
+
`[project.optional-dependencies]`), so a bare `uv run pytest` falls back to
|
|
112
|
+
whatever `pytest` is on your `PATH` — which runs the suite against a different
|
|
113
|
+
environment and fails well over a thousand tests for reasons that have nothing
|
|
114
|
+
to do with your change. There is no error message saying so; it just looks like
|
|
115
|
+
you broke everything.
|
|
116
|
+
|
|
104
117
|
## License
|
|
105
118
|
|
|
106
119
|
Apache 2.0
|
|
@@ -28,13 +28,24 @@ class PhysicalLink:
|
|
|
28
28
|
"cdp" — from CDP neighbor data
|
|
29
29
|
"lldp" — from LLDP neighbor data
|
|
30
30
|
"mac-arp" — validated via MAC-ARP correlation (supplementary only)
|
|
31
|
+
"upload" — supplied by the operator (an export from their source
|
|
32
|
+
of truth / NMS / IPAM, or a topology file)
|
|
33
|
+
"synth" — INFERRED, not observed: synthesized from two devices
|
|
34
|
+
having interfaces on a shared L3 subnet
|
|
35
|
+
|
|
36
|
+
The "synth" value is load-bearing rather than merely informative, so a
|
|
37
|
+
producer must label an inferred link honestly. Consumers distinguish
|
|
38
|
+
supplied cabling from inference on this field alone — the simulator's
|
|
39
|
+
link-vs-config consistency checks deliberately say nothing about a "synth"
|
|
40
|
+
link, because a warning citing one would be the engine disputing its own
|
|
41
|
+
inference rather than disclosing an input it cannot trust.
|
|
31
42
|
"""
|
|
32
43
|
|
|
33
44
|
device_a: str
|
|
34
45
|
port_a: str # canonical long-form logical interface on device_a
|
|
35
46
|
device_b: str
|
|
36
47
|
port_b: str # canonical long-form logical interface on device_b
|
|
37
|
-
source: str # "cdp" | "lldp" | "mac-arp"
|
|
48
|
+
source: str # "cdp" | "lldp" | "mac-arp" | "upload" | "synth"
|
|
38
49
|
member_count: int = 1
|
|
39
50
|
|
|
40
51
|
|
|
@@ -145,7 +145,68 @@ from confgraph.parsers.panos_parser import PANOSParser
|
|
|
145
145
|
# this module's header is per-commit and not optional for a qualifying
|
|
146
146
|
# change, and deciding otherwise would mean re-deriving "was the predecessor
|
|
147
147
|
# released?" at every review.
|
|
148
|
-
|
|
148
|
+
# 14 (CCR-0217): the JunOS brace branch of ``unsupported_lines`` is fail-closed
|
|
149
|
+
# on the parse OUTCOME — a document routed to the brace tokenizer that yields
|
|
150
|
+
# an EMPTY canonical tree while carrying at least one non-blank
|
|
151
|
+
# comment-stripped line discloses those lines as UnrecognizedBlocks instead of
|
|
152
|
+
# returning silence. The hole was the document with no JunOS idiom in it at all
|
|
153
|
+
# (Cisco ``no`` commands, an IOS ``interface`` block): zero set-family lines
|
|
154
|
+
# routes it brace-ward, every statement is discarded at EOF for want of a ``;``
|
|
155
|
+
# or ``{``, and the result read "no recognized configuration change" for text
|
|
156
|
+
# that plainly requested one — the last silent arm of CCR-0210's fail-closed
|
|
157
|
+
# story, modulo the CCR-0236 mixed-document residual. Observable parse output
|
|
158
|
+
# moves for every such document, with NO ParsedConfig JSON-schema change — the
|
|
159
|
+
# sharp case this constant exists for. Comment-only and whitespace-only
|
|
160
|
+
# documents carry no content and stay silent; ``inactive:``-only documents
|
|
161
|
+
# parse non-empty and keep their existing tag disclosure.
|
|
162
|
+
# 15 (CCR-0214): JunOS ``delete`` / ``deactivate`` statements on MODELLED
|
|
163
|
+
# paths compile to native change ops instead of being disclosed. JunOS emitted
|
|
164
|
+
# no deletion artifact of any kind before this — no tombstones (the grammar is
|
|
165
|
+
# Cisco syntax throughout) and no ops — so a withdrawal, the most outage-prone
|
|
166
|
+
# change class on the OS, could only ever be reported as content the engine
|
|
167
|
+
# could not read. The modelled set is one declarative table
|
|
168
|
+
# (``parsers/junos_deletions.py``): a BGP neighbour, a whole BGP group, a
|
|
169
|
+
# static route, an interface unit. Every path outside it stays on the CCR-0210
|
|
170
|
+
# disclosure channel, so nothing became silent and nothing is half-applied;
|
|
171
|
+
# ``deactivate`` compiles to the same op as ``delete`` because an inactive
|
|
172
|
+
# subtree makes no contribution to the running configuration, which is the
|
|
173
|
+
# only configuration the model holds. Observable parse output moves for any
|
|
174
|
+
# JunOS document carrying one of those statements —
|
|
175
|
+
# ``native_change_ops`` gains entries and ``unrecognized_blocks`` loses the
|
|
176
|
+
# matching lines — with NO ParsedConfig JSON-schema change. No BASELINE moves:
|
|
177
|
+
# a saved configuration states no deletion verb (the brace idiom has none),
|
|
178
|
+
# and the ``inactive:`` statement tag is untouched and still disclosed.
|
|
179
|
+
# 16 (CCR-0214, validation round 1 finding R1): a JunOS removal that the SAME
|
|
180
|
+
# document later re-states no longer compiles — it falls back to the CCR-0210
|
|
181
|
+
# disclosure channel. JunOS applies candidate statements in order, so
|
|
182
|
+
# ``delete protocols bgp group RR`` followed by ``set protocols bgp group RR …``
|
|
183
|
+
# rebuilds the group rather than removing it, and the same idiom moves a static
|
|
184
|
+
# route to a new next-hop. Those are the two commonest JunOS rewrite idioms, and
|
|
185
|
+
# compiling the deletion alone reported a confident teardown of something the
|
|
186
|
+
# device would still have — measured in validation as a CRITICAL full-teardown
|
|
187
|
+
# verdict on a group rebuild. The guard is order-aware (``set`` strictly AFTER
|
|
188
|
+
# the deletion line, at an equal or deeper path): the reverse order really is a
|
|
189
|
+
# removal on a device, so it still compiles. Observable parse output moves for
|
|
190
|
+
# any JunOS document mixing a removal with a later restatement of the same
|
|
191
|
+
# path — ``native_change_ops`` loses that op and ``unrecognized_blocks`` gains
|
|
192
|
+
# the line back — with NO ParsedConfig JSON-schema change, and no baseline
|
|
193
|
+
# motion (a saved configuration states no deletion verb).
|
|
194
|
+
# 17 (CCR-0239 + CCR-0240, bump landed late — caught at the v0.3.9 release
|
|
195
|
+
# audit, 2026-11-06): both landings changed parse output without moving this
|
|
196
|
+
# constant, so PBV-16 parse-cache blobs minted before them could be served as
|
|
197
|
+
# hits carrying the pre-fix models. CCR-0239: NX-OS named OSPF instance tags
|
|
198
|
+
# (``ip router ospf CORE area …``) enroll their interfaces — previously the
|
|
199
|
+
# named-instance spelling bound nothing, so those interfaces were
|
|
200
|
+
# OSPF-invisible. CCR-0240: IOS-XR OSPF passive resolves at all four scopes
|
|
201
|
+
# (process default, area, interface ``passive enable`` AND ``passive
|
|
202
|
+
# disable``) — previously the process-scope default was hardwired False and
|
|
203
|
+
# ``passive disable`` was not in the vocabulary, so suppressed interfaces
|
|
204
|
+
# formed phantom adjacencies while exempted uplinks read as suppressed.
|
|
205
|
+
# Observable parse output moves for any NX-OS config using named instance
|
|
206
|
+
# tags and any IOS-XR config using process/area-scope passive — BASELINES
|
|
207
|
+
# included (both spellings exist in saved configurations). No ParsedConfig
|
|
208
|
+
# JSON-schema change.
|
|
209
|
+
PARSER_BEHAVIOR_VERSION: int = 17
|
|
149
210
|
|
|
150
211
|
__all__ = [
|
|
151
212
|
"BaseParser",
|
|
@@ -78,6 +78,26 @@ _AF_HEADER_RE = re.compile(r"^\s*address-family\s+\S+")
|
|
|
78
78
|
_AF_SPLICE_RE = re.compile(r"^\s*address-family\s+ipv4\s+unicast\b")
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
# CCR-0240. The ONE spelling of the IOS-XR OSPF `passive` line, shared by the
|
|
82
|
+
# scope reader (_read_iosxr_passive) and the `area > interface` body vocabulary
|
|
83
|
+
# table (_OSPF_AREA_IFACE_PATTERNS) so the two cannot drift into disagreeing
|
|
84
|
+
# about which lines count.
|
|
85
|
+
#
|
|
86
|
+
# `disable` is a FIRST-CLASS VALUE, not the absence of `enable`: its whole job is
|
|
87
|
+
# to override a passive inherited from an enclosing scope, which is how an XR
|
|
88
|
+
# estate hardened with a process-wide `passive enable` exempts its uplinks. A
|
|
89
|
+
# model that encoded `disable` as absence could not express that exemption.
|
|
90
|
+
#
|
|
91
|
+
# The optional keyword is deliberate tolerance. Cisco's command reference marks
|
|
92
|
+
# both `disable` and `enable` "(Optional)" and its own example TYPES a bare
|
|
93
|
+
# `passive`, while the running-config emits the keyword
|
|
94
|
+
# (syntax-corpus/iosxr/ospf.yaml -> `passive`, status doc-only: `passive disable`
|
|
95
|
+
# is witnessed in a device transcript, the literal `passive enable` is not). A
|
|
96
|
+
# bare `passive` means passive ON, matching the typed form's documented meaning.
|
|
97
|
+
_IOSXR_PASSIVE_PATTERN = r"^\s+passive(?:\s+(?P<val>enable|disable))?\s*$"
|
|
98
|
+
_IOSXR_PASSIVE_RE = re.compile(_IOSXR_PASSIVE_PATTERN)
|
|
99
|
+
|
|
100
|
+
|
|
81
101
|
class _AFTransparentBlock:
|
|
82
102
|
"""A read-through view of an IOS-XR block that reads *through* its
|
|
83
103
|
``address-family ipv4 unicast`` sub-block — and only that one.
|
|
@@ -785,6 +805,41 @@ class IOSXRParser(IOSParser):
|
|
|
785
805
|
# OSPF — interfaces nested under area blocks
|
|
786
806
|
# -----------------------------------------------------------------------
|
|
787
807
|
|
|
808
|
+
@staticmethod
|
|
809
|
+
def _read_iosxr_passive(obj) -> bool | None:
|
|
810
|
+
"""Scope-local OSPF ``passive`` as a TRI-STATE (CCR-0240).
|
|
811
|
+
|
|
812
|
+
``True`` = this scope says passive, ``False`` = this scope says
|
|
813
|
+
explicitly NOT passive (``passive disable``), ``None`` = this scope said
|
|
814
|
+
nothing, so the question belongs to the enclosing scope.
|
|
815
|
+
|
|
816
|
+
The tri-state is the whole mechanism. IOS-XR makes ``passive``
|
|
817
|
+
inheritable at four scopes — router (process), VRF, area and interface —
|
|
818
|
+
and the vendor states the chain outright: *"If this command is not
|
|
819
|
+
specified in interface configuration mode, then the interface adopts the
|
|
820
|
+
passive parameter specified by the area. If this command is not
|
|
821
|
+
specified in area configuration mode, then the interface adopts the
|
|
822
|
+
passive parameter specified for the process. If this command is not
|
|
823
|
+
specified at any level, then the passive parameter is disabled"*
|
|
824
|
+
(ASR9000 routing command reference, "passive (OSPF)"; Command Modes:
|
|
825
|
+
Interface / Area / Router / VRF). A two-valued read cannot tell "not
|
|
826
|
+
specified here" from "specified off", and collapsing the two is exactly
|
|
827
|
+
the bug: the exempted uplink becomes indistinguishable from a
|
|
828
|
+
suppressed one.
|
|
829
|
+
|
|
830
|
+
Direct children only. ``obj.children`` rather than
|
|
831
|
+
``find_child_objects`` because an ``area N`` block's descendants include
|
|
832
|
+
its ``interface X`` sub-blocks, whose ``passive`` belongs to the
|
|
833
|
+
interface and must not be read as the area's. Last line wins, which is
|
|
834
|
+
what a device does with a repeated statement.
|
|
835
|
+
"""
|
|
836
|
+
state: bool | None = None
|
|
837
|
+
for child in obj.children:
|
|
838
|
+
m = _IOSXR_PASSIVE_RE.match(child.text)
|
|
839
|
+
if m:
|
|
840
|
+
state = m.group("val") != "disable"
|
|
841
|
+
return state
|
|
842
|
+
|
|
788
843
|
def parse_ospf(self) -> list[OSPFConfig]:
|
|
789
844
|
"""Parse OSPF configurations for IOS-XR.
|
|
790
845
|
|
|
@@ -828,13 +883,27 @@ class IOSXRParser(IOSParser):
|
|
|
828
883
|
if v:
|
|
829
884
|
auto_cost_ref_bw = int(v)
|
|
830
885
|
|
|
831
|
-
# Passive
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
886
|
+
# Passive, at PROCESS scope (CCR-0240). `passive enable` / `passive
|
|
887
|
+
# disable` as a direct child of `router ospf N` is the default every
|
|
888
|
+
# area and interface inherits; it used to be hardwired False, so the
|
|
889
|
+
# standard XR hardening idiom ("suppress everywhere, re-enable per
|
|
890
|
+
# uplink") read as nothing suppressed anywhere.
|
|
891
|
+
#
|
|
892
|
+
# `passive_interface_default` carries the PROCESS-scope answer only —
|
|
893
|
+
# a tri-state None means nothing was specified at this level, which
|
|
894
|
+
# the vendor defines as not passive. Area- and interface-scope
|
|
895
|
+
# overrides are resolved per interface into the two lists below, so
|
|
896
|
+
# the engine's resolver gets the right answer under either branch of
|
|
897
|
+
# `passive_interface_default`.
|
|
898
|
+
process_passive = self._read_iosxr_passive(ospf_obj)
|
|
899
|
+
passive_interface_default = process_passive is True
|
|
835
900
|
|
|
836
901
|
# Parse areas — IOS-XR has "area N" stanzas with nested interfaces
|
|
837
|
-
areas, passive_interfaces =
|
|
902
|
+
areas, passive_interfaces, non_passive_interfaces = (
|
|
903
|
+
self._parse_ospf_areas_iosxr(
|
|
904
|
+
ospf_obj, process_id, passive_default=process_passive
|
|
905
|
+
)
|
|
906
|
+
)
|
|
838
907
|
|
|
839
908
|
# Redistribution
|
|
840
909
|
redistribute = self._parse_ospf_redistribute_iosxr(ospf_obj)
|
|
@@ -1008,7 +1077,27 @@ class IOSXRParser(IOSParser):
|
|
|
1008
1077
|
except ValueError:
|
|
1009
1078
|
pass
|
|
1010
1079
|
|
|
1011
|
-
|
|
1080
|
+
# CCR-0240. `passive` is legal in VRF configuration mode as well as
|
|
1081
|
+
# router/area/interface mode, and a VRF sub-block sits INSIDE the process
|
|
1082
|
+
# block — so the inherited default here is the VRF's own setting if it has
|
|
1083
|
+
# one, else the enclosing `router ospf N`'s. Threading the process scope
|
|
1084
|
+
# through is not optional: without it a process-wide `passive enable`
|
|
1085
|
+
# would apply to the global instance's interfaces and silently vanish for
|
|
1086
|
+
# the VRF's, which is the same defect in a second place.
|
|
1087
|
+
parent = getattr(vrf_obj, "parent", None)
|
|
1088
|
+
process_passive = (
|
|
1089
|
+
self._read_iosxr_passive(parent)
|
|
1090
|
+
if parent is not None and parent is not vrf_obj
|
|
1091
|
+
else None
|
|
1092
|
+
)
|
|
1093
|
+
vrf_passive = self._read_iosxr_passive(vrf_obj)
|
|
1094
|
+
passive_default = vrf_passive if vrf_passive is not None else process_passive
|
|
1095
|
+
|
|
1096
|
+
areas, passive_interfaces, non_passive_interfaces = (
|
|
1097
|
+
self._parse_ospf_areas_iosxr(
|
|
1098
|
+
vrf_obj, process_id, passive_default=passive_default
|
|
1099
|
+
)
|
|
1100
|
+
)
|
|
1012
1101
|
redistribute = self._parse_ospf_redistribute_iosxr(vrf_obj)
|
|
1013
1102
|
|
|
1014
1103
|
return OSPFConfig(
|
|
@@ -1020,20 +1109,36 @@ class IOSXRParser(IOSParser):
|
|
|
1020
1109
|
vrf=vrf_name,
|
|
1021
1110
|
router_id=router_id,
|
|
1022
1111
|
areas=areas,
|
|
1112
|
+
passive_interface_default=passive_default is True,
|
|
1023
1113
|
passive_interfaces=passive_interfaces,
|
|
1114
|
+
non_passive_interfaces=non_passive_interfaces,
|
|
1024
1115
|
redistribute=redistribute,
|
|
1025
1116
|
)
|
|
1026
1117
|
|
|
1027
1118
|
def _parse_ospf_areas_iosxr(
|
|
1028
|
-
self,
|
|
1029
|
-
|
|
1119
|
+
self,
|
|
1120
|
+
ospf_obj,
|
|
1121
|
+
process_id: int | str | None = None,
|
|
1122
|
+
*,
|
|
1123
|
+
passive_default: bool | None = None,
|
|
1124
|
+
) -> tuple[list[OSPFArea], list[str], list[str]]:
|
|
1030
1125
|
"""Parse OSPF areas with nested interface blocks (IOS-XR style).
|
|
1031
1126
|
|
|
1032
|
-
|
|
1127
|
+
``passive_default`` is the ``passive`` state inherited from the enclosing
|
|
1128
|
+
scope as a tri-state (``None`` = nothing specified above), already
|
|
1129
|
+
resolved by the caller down the process → VRF chain.
|
|
1130
|
+
|
|
1131
|
+
Returns a tuple of (areas, passive_interfaces, non_passive_interfaces).
|
|
1132
|
+
Both lists are populated from the RESOLVED per-interface state, so the
|
|
1133
|
+
engine's resolver is correct under either branch of
|
|
1134
|
+
``passive_interface_default`` — it asks ``name not in
|
|
1135
|
+
non_passive_interfaces`` when the default is passive and ``name in
|
|
1136
|
+
passive_interfaces`` when it is not (CCR-0240).
|
|
1033
1137
|
"""
|
|
1034
1138
|
areas: list[OSPFArea] = []
|
|
1035
1139
|
area_dict: dict[str, dict] = {}
|
|
1036
1140
|
passive_interfaces: list[str] = []
|
|
1141
|
+
non_passive_interfaces: list[str] = []
|
|
1037
1142
|
|
|
1038
1143
|
area_children = ospf_obj.find_child_objects(r"^\s+area\s+(\S+)")
|
|
1039
1144
|
for area_child in area_children:
|
|
@@ -1136,6 +1241,12 @@ class IOSXRParser(IOSParser):
|
|
|
1136
1241
|
except ValueError:
|
|
1137
1242
|
pass
|
|
1138
1243
|
|
|
1244
|
+
# Passive, at AREA scope (CCR-0240) — overrides the process/VRF
|
|
1245
|
+
# default for this area's interfaces, and is itself overridden by an
|
|
1246
|
+
# interface-scope statement.
|
|
1247
|
+
area_passive = self._read_iosxr_passive(area_child)
|
|
1248
|
+
area_default = area_passive if area_passive is not None else passive_default
|
|
1249
|
+
|
|
1139
1250
|
# Interfaces nested under area
|
|
1140
1251
|
for intf_child in area_child.find_child_objects(r"^\s+interface\s+(\S+)"):
|
|
1141
1252
|
intf_name = self._extract_match(intf_child.text, r"^\s+interface\s+(\S+)")
|
|
@@ -1143,24 +1254,39 @@ class IOSXRParser(IOSParser):
|
|
|
1143
1254
|
continue
|
|
1144
1255
|
if intf_name not in area_dict[area_id]["interfaces"]:
|
|
1145
1256
|
area_dict[area_id]["interfaces"].append(intf_name)
|
|
1146
|
-
|
|
1147
|
-
|
|
1257
|
+
|
|
1258
|
+
# Resolve the inheritance chain: interface ?? area ?? VRF ??
|
|
1259
|
+
# process. `passive disable` here is the uplink exemption and
|
|
1260
|
+
# resolves to an explicit False, which is a different fact from
|
|
1261
|
+
# "nobody said" (None) — the first lands in
|
|
1262
|
+
# non_passive_interfaces so the engine can un-gate the uplink
|
|
1263
|
+
# against a passive default, the second lands in neither list
|
|
1264
|
+
# because no scope made a claim.
|
|
1265
|
+
intf_passive = self._read_iosxr_passive(intf_child)
|
|
1266
|
+
resolved = intf_passive if intf_passive is not None else area_default
|
|
1267
|
+
if resolved is True:
|
|
1148
1268
|
if intf_name not in passive_interfaces:
|
|
1149
1269
|
passive_interfaces.append(intf_name)
|
|
1270
|
+
elif resolved is False:
|
|
1271
|
+
if intf_name not in non_passive_interfaces:
|
|
1272
|
+
non_passive_interfaces.append(intf_name)
|
|
1273
|
+
|
|
1150
1274
|
area_dict[area_id]["interface_settings"][intf_name] = (
|
|
1151
1275
|
self._parse_ospf_area_interface(
|
|
1152
|
-
intf_child, intf_name, area_id, process_id
|
|
1276
|
+
intf_child, intf_name, area_id, process_id,
|
|
1277
|
+
passive=resolved,
|
|
1153
1278
|
)
|
|
1154
1279
|
)
|
|
1155
1280
|
|
|
1156
1281
|
for area_data in area_dict.values():
|
|
1157
1282
|
areas.append(OSPFArea(**area_data))
|
|
1158
1283
|
|
|
1159
|
-
return areas, passive_interfaces
|
|
1284
|
+
return areas, passive_interfaces, non_passive_interfaces
|
|
1160
1285
|
|
|
1161
1286
|
# IOS-XR OSPF `area > interface` body → OSPFInterfaceConfig field. One row per
|
|
1162
|
-
# setting; the regex's `val` group is the value
|
|
1163
|
-
#
|
|
1287
|
+
# setting; the regex's `val` group is the value — cast by _OSPF_INT_FIELDS or
|
|
1288
|
+
# read as an enable/disable keyword by _OSPF_ENABLE_DISABLE_FIELDS — or the row
|
|
1289
|
+
# is a bare flag (no group) whose presence means True.
|
|
1164
1290
|
#
|
|
1165
1291
|
# This is the extraction half of CCR-0038 Theme 2. The ATTRIBUTION half — the
|
|
1166
1292
|
# part that used to be missing entirely on IOS-XR, and duplicated per-OS on
|
|
@@ -1177,10 +1303,12 @@ class IOSXRParser(IOSParser):
|
|
|
1177
1303
|
("authentication", r"^\s+authentication\s+(?P<val>message-digest|null)\s*$"),
|
|
1178
1304
|
("bfd_interval", r"^\s+bfd\s+minimum-interval\s+(?P<val>\d+)\s*$"),
|
|
1179
1305
|
("bfd_multiplier", r"^\s+bfd\s+multiplier\s+(?P<val>\d+)\s*$"),
|
|
1306
|
+
# `<setting> enable | disable` — the keyword IS the value, so `disable`
|
|
1307
|
+
# is parsed as False rather than dropped on the floor (CCR-0240).
|
|
1308
|
+
("passive", _IOSXR_PASSIVE_PATTERN),
|
|
1180
1309
|
# Bare flags — presence is the value.
|
|
1181
1310
|
("bfd", r"^\s+bfd\s+fast-detect\s*$"),
|
|
1182
1311
|
("mtu_ignore", r"^\s+mtu-ignore\s+enable\s*$"),
|
|
1183
|
-
("passive", r"^\s+passive\s+enable\s*$"),
|
|
1184
1312
|
)
|
|
1185
1313
|
|
|
1186
1314
|
_OSPF_INT_FIELDS = frozenset({
|
|
@@ -1188,9 +1316,18 @@ class IOSXRParser(IOSParser):
|
|
|
1188
1316
|
"bfd_interval", "bfd_multiplier",
|
|
1189
1317
|
})
|
|
1190
1318
|
|
|
1319
|
+
# Rows whose `val` group is an IOS-XR `enable`/`disable` keyword rather than a
|
|
1320
|
+
# scalar: `enable` (or the keyword's absence, which is legal when typed) means
|
|
1321
|
+
# True and `disable` means False. A sibling of _OSPF_INT_FIELDS, so the
|
|
1322
|
+
# (N+1)th enable/disable-valued setting is one row in the table plus one entry
|
|
1323
|
+
# here — not a branch in the loop. [[CCR-0240]]
|
|
1324
|
+
_OSPF_ENABLE_DISABLE_FIELDS = frozenset({"passive"})
|
|
1325
|
+
|
|
1191
1326
|
def _parse_ospf_area_interface(
|
|
1192
1327
|
self, intf_child, intf_name: str, area_id: str,
|
|
1193
1328
|
process_id: int | str | None = None,
|
|
1329
|
+
*,
|
|
1330
|
+
passive: bool | None = None,
|
|
1194
1331
|
) -> OSPFInterfaceConfig:
|
|
1195
1332
|
"""One ``area N > interface NAME`` block → OSPFInterfaceConfig.
|
|
1196
1333
|
|
|
@@ -1202,6 +1339,15 @@ class IOSXRParser(IOSParser):
|
|
|
1202
1339
|
settings = OSPFInterfaceConfig(
|
|
1203
1340
|
name=intf_name, area_id=area_id, process_id=process_id
|
|
1204
1341
|
)
|
|
1342
|
+
# CCR-0240: `passive` is seeded with the RESOLVED state the caller worked
|
|
1343
|
+
# out down the process → VRF → area → interface chain, before the body is
|
|
1344
|
+
# read. An interface-scope `passive` line then overwrites it with the
|
|
1345
|
+
# same answer (the caller derived `passive` from that very line), so the
|
|
1346
|
+
# seed is what makes an INHERITED passive visible on an interface whose
|
|
1347
|
+
# own block is silent — the model field is a plain bool and has no way to
|
|
1348
|
+
# say "ask my area".
|
|
1349
|
+
if passive is not None:
|
|
1350
|
+
settings.passive = passive
|
|
1205
1351
|
for child in intf_child.children:
|
|
1206
1352
|
text = child.text
|
|
1207
1353
|
for field, pattern in self._OSPF_AREA_IFACE_PATTERNS:
|
|
@@ -1210,7 +1356,13 @@ class IOSXRParser(IOSParser):
|
|
|
1210
1356
|
continue
|
|
1211
1357
|
if "val" in m.groupdict():
|
|
1212
1358
|
raw = m.group("val")
|
|
1213
|
-
|
|
1359
|
+
if field in self._OSPF_ENABLE_DISABLE_FIELDS:
|
|
1360
|
+
# `enable`, or the keyword omitted entirely, means True.
|
|
1361
|
+
value: object = raw != "disable"
|
|
1362
|
+
elif field in self._OSPF_INT_FIELDS:
|
|
1363
|
+
value = int(raw)
|
|
1364
|
+
else:
|
|
1365
|
+
value = raw
|
|
1214
1366
|
else:
|
|
1215
1367
|
value = True # bare flag
|
|
1216
1368
|
setattr(settings, field, value)
|