confgraph 0.3.7__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.7 → confgraph-0.3.9}/PKG-INFO +14 -1
- {confgraph-0.3.7 → confgraph-0.3.9}/README.md +13 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/interface.py +28 -5
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/ospf.py +33 -3
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/topology.py +12 -1
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/__init__.py +99 -1
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/base.py +50 -9
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/eos_parser.py +9 -2
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/ios_parser.py +73 -10
- {confgraph-0.3.7 → 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.7 → confgraph-0.3.9}/confgraph/parsers/junos_hierarchy.py +133 -11
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/junos_parser.py +89 -12
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/nxos_parser.py +48 -13
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/tombstones.py +15 -0
- confgraph-0.3.9/confgraph/utils/ospf.py +73 -0
- confgraph-0.3.9/dist-0.3.7/confgraph-0.3.7-py3-none-any.whl +0 -0
- confgraph-0.3.9/dist-0.3.7/confgraph-0.3.7.tar.gz +0 -0
- confgraph-0.3.9/dist-0.3.8/.gitignore +1 -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.7 → confgraph-0.3.9}/pyproject.toml +1 -1
- {confgraph-0.3.7 → confgraph-0.3.9}/uv.lock +1 -1
- {confgraph-0.3.7 → confgraph-0.3.9}/.github/workflows/publish.yml +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/.github/workflows/redos-scan.yml +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/.gitignore +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/.python-version +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/CHANGELOG.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/CLAUDE.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/INTERFACE.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/LICENSE +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/analysis/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/analysis/dependency_resolver.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/change_ir.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/cli.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/assets/cytoscape-dagre.min.js +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/assets/cytoscape.min.js +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/assets/dagre.min.js +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/builder.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/exporters/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/exporters/base.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/exporters/html.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/graph/exporters/json.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/loader.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/aaa.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/acl.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/banner.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/base.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/bfd.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/bgp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/cdp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/community_list.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/crypto.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/dhcp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/dns.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/eem.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/eigrp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/evpn.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/ipsla.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/isis.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/line.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/lldp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/logging_config.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/mpls.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/multicast.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/nat.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/netflow.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/ntp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/object_group.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/object_tracking.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/panos_zone.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/parsed_config.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/prefix_list.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/qos.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/rip.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/route_map.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/snmp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/static_route.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/stp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/vlan.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/vpc.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/vrf.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/models/vxlan.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/junos_groups.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/panos_parser.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/parsers/panos_xml.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/topology/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/topology/exporters.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/topology/graph.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/topology/ingest.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/utils/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/utils/escaping.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/confgraph/utils/interface.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/dist-0.3.7/.gitignore +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/ADDING_NEW_OS_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/ARCHITECTURE.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/CLI_USAGE.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/EOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/IOSXR_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/IOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/JUNOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/NXOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/PANOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/PARSER_SUPPORT_MATRIX.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/bgp_cluster_view.png +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/default_view.png +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/specific_node_highlight.png +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/docs/supported_os_types.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/main.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/complex_router.ios +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/eos.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/ios.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/ios01.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/ios_xe.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/ios_xr.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/iosxr_test.cfg +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/junos_test.cfg +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/nxos.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/samples/panos_sample.xml +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/tools/redos_baseline.json +0 -0
- {confgraph-0.3.7 → confgraph-0.3.9}/tools/redos_scan.json +0 -0
- {confgraph-0.3.7 → 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
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from ipaddress import IPv4Address, IPv4Interface, IPv6Interface
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
6
|
from confgraph.models.base import BaseConfigObject
|
|
7
|
+
from confgraph.utils.ospf import canonical_area_id
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class InterfaceType(str, Enum):
|
|
@@ -355,7 +356,11 @@ class InterfaceConfig(BaseConfigObject):
|
|
|
355
356
|
)
|
|
356
357
|
ospf_area: str | None = Field(
|
|
357
358
|
default=None,
|
|
358
|
-
description=
|
|
359
|
+
description=(
|
|
360
|
+
"OSPF area ID, CANONICALIZED to the decimal spelling (CCR-0227) "
|
|
361
|
+
"so a '0.0.0.0' interface and a '0' interface are in the same "
|
|
362
|
+
"area here, as they are on hardware."
|
|
363
|
+
),
|
|
359
364
|
)
|
|
360
365
|
ospf_cost: int | None = Field(
|
|
361
366
|
default=None,
|
|
@@ -377,9 +382,22 @@ class InterfaceConfig(BaseConfigObject):
|
|
|
377
382
|
default=None,
|
|
378
383
|
description="OSPF network type ('point-to-point', 'broadcast', etc.)",
|
|
379
384
|
)
|
|
380
|
-
ospf_passive: bool = Field(
|
|
381
|
-
default=
|
|
382
|
-
description=
|
|
385
|
+
ospf_passive: bool | None = Field(
|
|
386
|
+
default=None,
|
|
387
|
+
description=(
|
|
388
|
+
"OSPF passive interface. TRI-STATE (CCR-0222, the CCR-0165 "
|
|
389
|
+
"``activate`` precedent): True = this interface is passive "
|
|
390
|
+
"(an interface-level ``ip ospf passive-interface``, or the "
|
|
391
|
+
"process-level passive config back-filled by the M3 walk); "
|
|
392
|
+
"False = EXPLICITLY non-passive (``no ip ospf passive-interface`` "
|
|
393
|
+
"— the per-interface override of a process-level "
|
|
394
|
+
"``passive-interface default``); None = NOT STATED on the "
|
|
395
|
+
"interface, so the consumer falls back to the process-level "
|
|
396
|
+
"passive config. A bool default of False could not express the "
|
|
397
|
+
"override at all: the M3 back-fill clobbered it, a field-level "
|
|
398
|
+
"merge could never overlay it (the CCR-0211 disease), and no "
|
|
399
|
+
"Change-IR SET op was emitted for it."
|
|
400
|
+
),
|
|
383
401
|
)
|
|
384
402
|
ospf_authentication: str | None = Field(
|
|
385
403
|
default=None,
|
|
@@ -646,6 +664,11 @@ class InterfaceConfig(BaseConfigObject):
|
|
|
646
664
|
or self.dynamic_address
|
|
647
665
|
)
|
|
648
666
|
|
|
667
|
+
# CCR-0227: area-ID storage point 4 of 4.
|
|
668
|
+
_canonical_area = field_validator("ospf_area", mode="before")(
|
|
669
|
+
canonical_area_id
|
|
670
|
+
)
|
|
671
|
+
|
|
649
672
|
class Config:
|
|
650
673
|
"""Pydantic model configuration."""
|
|
651
674
|
use_enum_values = True
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from ipaddress import IPv4Address, IPv4Network
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
6
|
from confgraph.models.base import BaseConfigObject
|
|
7
|
+
from confgraph.utils.ospf import canonical_area_id
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class OSPFAreaType(str, Enum):
|
|
@@ -159,12 +160,24 @@ class OSPFInterfaceConfig(BaseModel):
|
|
|
159
160
|
default=None, description="BFD detection multiplier"
|
|
160
161
|
)
|
|
161
162
|
|
|
163
|
+
# CCR-0227: area-ID storage point 1 of 4.
|
|
164
|
+
_canonical_area = field_validator("area_id", mode="before")(
|
|
165
|
+
canonical_area_id
|
|
166
|
+
)
|
|
167
|
+
|
|
162
168
|
|
|
163
169
|
class OSPFArea(BaseModel):
|
|
164
170
|
"""OSPF area configuration."""
|
|
165
171
|
|
|
166
172
|
area_id: str = Field(
|
|
167
|
-
...,
|
|
173
|
+
...,
|
|
174
|
+
description=(
|
|
175
|
+
"Area ID, CANONICALIZED to the decimal spelling (CCR-0227): the "
|
|
176
|
+
"NX-OS/JunOS '0.0.0.0' and the IOS/IOS-XR '0' are the same area on "
|
|
177
|
+
"hardware and are the same key here. Dotted quads convert to their "
|
|
178
|
+
"32-bit decimal ('0.0.0.51' -> '51'); anything else is stored as "
|
|
179
|
+
"written."
|
|
180
|
+
),
|
|
168
181
|
)
|
|
169
182
|
area_type: OSPFAreaType = Field(
|
|
170
183
|
default=OSPFAreaType.NORMAL, description="Area type"
|
|
@@ -220,6 +233,11 @@ class OSPFArea(BaseModel):
|
|
|
220
233
|
description="Prefix-list name filtering Type-3 LSAs sent out of this area (outbound from area)",
|
|
221
234
|
)
|
|
222
235
|
|
|
236
|
+
# CCR-0227: area-ID storage point 2 of 4.
|
|
237
|
+
_canonical_area = field_validator("area_id", mode="before")(
|
|
238
|
+
canonical_area_id
|
|
239
|
+
)
|
|
240
|
+
|
|
223
241
|
class Config:
|
|
224
242
|
"""Pydantic model configuration."""
|
|
225
243
|
use_enum_values = True
|
|
@@ -326,7 +344,11 @@ class OSPFConfig(BaseConfigObject):
|
|
|
326
344
|
)
|
|
327
345
|
network_statements: list[tuple[IPv4Network, str]] = Field(
|
|
328
346
|
default_factory=list,
|
|
329
|
-
description=
|
|
347
|
+
description=(
|
|
348
|
+
"Network statements (network, area) - IOS style. The area half is "
|
|
349
|
+
"canonicalized exactly like every other area-ID storage point "
|
|
350
|
+
"(CCR-0227)."
|
|
351
|
+
),
|
|
330
352
|
)
|
|
331
353
|
shutdown: bool = Field(
|
|
332
354
|
default=False, description="OSPF process shutdown"
|
|
@@ -341,6 +363,14 @@ class OSPFConfig(BaseConfigObject):
|
|
|
341
363
|
default=False, description="BFD enabled on all OSPF interfaces"
|
|
342
364
|
)
|
|
343
365
|
|
|
366
|
+
# CCR-0227: area-ID storage point 3 of 4 — the area half of each
|
|
367
|
+
# statement. ``mode="after"`` so the tuples are already built; the
|
|
368
|
+
# network half is untouched.
|
|
369
|
+
@field_validator("network_statements", mode="after")
|
|
370
|
+
@classmethod
|
|
371
|
+
def _canonical_statement_areas(cls, value):
|
|
372
|
+
return [(net, canonical_area_id(area)) for net, area in value]
|
|
373
|
+
|
|
344
374
|
class Config:
|
|
345
375
|
"""Pydantic model configuration."""
|
|
346
376
|
use_enum_values = True
|
|
@@ -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
|
|
|
@@ -108,7 +108,105 @@ from confgraph.parsers.panos_parser import PANOSParser
|
|
|
108
108
|
# Persisted baseline snapshots are invalidated by the pbv move itself (it is a
|
|
109
109
|
# digest-key component), which subsumes the Tier-A digest shift these fields
|
|
110
110
|
# cause.
|
|
111
|
-
|
|
111
|
+
# 11 (CCR-0222): ``InterfaceConfig.ospf_passive`` becomes tri-state
|
|
112
|
+
# (``bool | None``, default None) and the shared interface walk extracts BOTH
|
|
113
|
+
# polarities of the per-interface ``[no] ip ospf passive-interface`` override
|
|
114
|
+
# (NX-OS/EOS inherit it). Observable parse output moves three ways: every
|
|
115
|
+
# interface that says nothing now reads None rather than False; an interface
|
|
116
|
+
# exempted from a process-level ``passive-interface default`` reads an explicit
|
|
117
|
+
# False and gains a native SET op for it (False != the new default); and the
|
|
118
|
+
# ``no`` line it was emitting as an UnrecognizedBlock disappears, claimed by
|
|
119
|
+
# that op's line provenance (CCR-0203 ledger). No ParsedConfig JSON-schema
|
|
120
|
+
# change beyond the field's own type, so pbv is what invalidates the platform
|
|
121
|
+
# parse cache and entrp's digest keys.
|
|
122
|
+
# 12 (CCR-0227): OSPF area IDs are CANONICALIZED at parse to the decimal
|
|
123
|
+
# spelling, by one shared ``canonical_area_id`` applied via model validators on
|
|
124
|
+
# all four storage points (OSPFArea.area_id, OSPFInterfaceConfig.area_id,
|
|
125
|
+
# InterfaceConfig.ospf_area, the area half of OSPFConfig.network_statements).
|
|
126
|
+
# `0.0.0.0` and `0` are the same area on hardware; stored raw they were two
|
|
127
|
+
# keys, so every mixed-vendor estate (NX-OS/JunOS dotted, IOS/IOS-XR decimal)
|
|
128
|
+
# modelled one real area as two — no adjacency across the spellings and wrong
|
|
129
|
+
# ABR/inter-area computation. Observable parse output moves for every config
|
|
130
|
+
# that spells an area as a dotted quad, with NO ParsedConfig JSON-schema
|
|
131
|
+
# change — the sharp case this constant exists for. Decimal is canonical
|
|
132
|
+
# precisely so the existing tombstone/golden keys (`ospf:1:area:0:...`) do not
|
|
133
|
+
# move.
|
|
134
|
+
# 13 (CCR-0227 completion): the two OPS-ONLY area-scoped removals —
|
|
135
|
+
# ``no network A W area <aid>`` (and the EOS ``no network A/M area <aid>``
|
|
136
|
+
# twin) and ``no area <aid> range A M`` — build their LIST_REMOVE key
|
|
137
|
+
# directly in the parser rather than through the tombstone registry, and were
|
|
138
|
+
# left spelling the area as the proposal wrote it while 12 made storage
|
|
139
|
+
# canonical. A dotted withdrawal therefore addressed an area that no longer
|
|
140
|
+
# existed under that spelling and removed NOTHING, silently — including the
|
|
141
|
+
# dotted-baseline/dotted-proposal pair that worked before 12. The removal key
|
|
142
|
+
# and the refresh-suppression map on both sides of each walk are now
|
|
143
|
+
# canonical. Observable parse output moves (those op paths) with no schema
|
|
144
|
+
# change. Bumped rather than riding 12 — unreleased though 12 is, the rule in
|
|
145
|
+
# this module's header is per-commit and not optional for a qualifying
|
|
146
|
+
# change, and deciding otherwise would mean re-deriving "was the predecessor
|
|
147
|
+
# released?" at every review.
|
|
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
|
|
112
210
|
|
|
113
211
|
__all__ = [
|
|
114
212
|
"BaseParser",
|
|
@@ -956,16 +956,23 @@ class BaseParser(ABC):
|
|
|
956
956
|
# (explicitly named in passive_interfaces or non_passive_interfaces,
|
|
957
957
|
# or whose ospf_process_id matches). Never infer passive for L2 ports
|
|
958
958
|
# or interfaces not in any OSPF process — correctness over coverage.
|
|
959
|
+
#
|
|
960
|
+
# CCR-0222: every stamp here is guarded on ``ospf_passive is None``.
|
|
961
|
+
# The field is tri-state, and a non-None value means the INTERFACE
|
|
962
|
+
# block itself spoke (`[no] ip ospf passive-interface`) — which on
|
|
963
|
+
# hardware overrides anything the process says. Without the guard
|
|
964
|
+
# this walk clobbers the explicit False and the override vanishes
|
|
965
|
+
# again, one layer down.
|
|
959
966
|
if pc.ospf_instances and pc.interfaces:
|
|
960
967
|
intf_by_name = {i.name: i for i in pc.interfaces}
|
|
961
968
|
for ospf in pc.ospf_instances:
|
|
962
969
|
for name in ospf.passive_interfaces:
|
|
963
970
|
intf = intf_by_name.get(name)
|
|
964
|
-
if intf:
|
|
971
|
+
if intf and intf.ospf_passive is None:
|
|
965
972
|
intf.ospf_passive = True
|
|
966
973
|
for name in ospf.non_passive_interfaces:
|
|
967
974
|
intf = intf_by_name.get(name)
|
|
968
|
-
if intf:
|
|
975
|
+
if intf and intf.ospf_passive is None:
|
|
969
976
|
intf.ospf_passive = False
|
|
970
977
|
# For default-passive, only mark interfaces that belong to
|
|
971
978
|
# this process (ospf_process_id matches or listed in an area)
|
|
@@ -975,6 +982,8 @@ class BaseParser(ABC):
|
|
|
975
982
|
area_intfs.update(area.interfaces)
|
|
976
983
|
non_passive_set = set(ospf.non_passive_interfaces)
|
|
977
984
|
for intf in pc.interfaces:
|
|
985
|
+
if intf.ospf_passive is not None:
|
|
986
|
+
continue # the interface block already decided
|
|
978
987
|
if intf.name in non_passive_set:
|
|
979
988
|
continue # already handled above
|
|
980
989
|
if intf.name in area_intfs or intf.ospf_process_id == ospf.process_id:
|
|
@@ -1036,6 +1045,27 @@ class BaseParser(ABC):
|
|
|
1036
1045
|
("bfd_multiplier", "bfd_multiplier"),
|
|
1037
1046
|
)
|
|
1038
1047
|
|
|
1048
|
+
@staticmethod
|
|
1049
|
+
def _is_model_default(obj, field_name: str, value) -> bool:
|
|
1050
|
+
"""True when *value* is still *field_name*'s model-declared default.
|
|
1051
|
+
|
|
1052
|
+
The single "has this field been set?" test for the back-fill walks
|
|
1053
|
+
(CCR-0222). Identity first so the singletons (``None``, ``False``)
|
|
1054
|
+
match exactly; equality only within the same type, so an explicitly
|
|
1055
|
+
parsed ``0`` is never read as a default ``False``.
|
|
1056
|
+
"""
|
|
1057
|
+
field_info = type(obj).model_fields.get(field_name)
|
|
1058
|
+
if field_info is None:
|
|
1059
|
+
return value is None
|
|
1060
|
+
default = (
|
|
1061
|
+
field_info.default_factory()
|
|
1062
|
+
if field_info.default_factory is not None
|
|
1063
|
+
else field_info.default
|
|
1064
|
+
)
|
|
1065
|
+
if value is default:
|
|
1066
|
+
return True
|
|
1067
|
+
return type(value) is type(default) and value == default
|
|
1068
|
+
|
|
1039
1069
|
def _backfill_ospf_interface_settings(self, pc: ParsedConfig) -> None:
|
|
1040
1070
|
"""Carry ``area > interface`` OSPF settings out to ``InterfaceConfig``.
|
|
1041
1071
|
|
|
@@ -1077,14 +1107,25 @@ class BaseParser(ABC):
|
|
|
1077
1107
|
continue
|
|
1078
1108
|
# Never clobber a value the interface block itself set.
|
|
1079
1109
|
#
|
|
1080
|
-
#
|
|
1081
|
-
#
|
|
1082
|
-
#
|
|
1083
|
-
#
|
|
1084
|
-
#
|
|
1085
|
-
#
|
|
1110
|
+
# "Unset" means "still holds the field's MODEL-DECLARED
|
|
1111
|
+
# default", read off the model rather than hard-coded as
|
|
1112
|
+
# None-or-False (CCR-0222): `ospf_passive` is tri-state
|
|
1113
|
+
# with default None, so a False there is an EXPLICIT
|
|
1114
|
+
# `no ip ospf passive-interface` and must survive, while
|
|
1115
|
+
# a False in a default-False field (`ospf_mtu_ignore`)
|
|
1116
|
+
# is still unset. Behaviour-identical for every other
|
|
1117
|
+
# row — no default-None field in the table can hold
|
|
1118
|
+
# False, and no default-False field can hold None.
|
|
1119
|
+
#
|
|
1120
|
+
# Compared with `is` before `==`, not `in (None, False)`:
|
|
1121
|
+
# in Python `0 == False`, so a plain equality test would
|
|
1122
|
+
# read an explicitly parsed ZERO as "unset" and overwrite
|
|
1123
|
+
# it. `ip ospf priority 0` is a real, meaningful value —
|
|
1124
|
+
# it says "never become DR" — and silently promoting it
|
|
1125
|
+
# to a back-filled 33 would invert the operator's intent.
|
|
1126
|
+
# The type check keeps `0` distinct from `False`.
|
|
1086
1127
|
current = getattr(intf, dst_field, None)
|
|
1087
|
-
if
|
|
1128
|
+
if not self._is_model_default(intf, dst_field, current):
|
|
1088
1129
|
continue
|
|
1089
1130
|
setattr(intf, dst_field, value)
|
|
1090
1131
|
|
|
@@ -14,6 +14,7 @@ from confgraph.parsers.base import (
|
|
|
14
14
|
from confgraph.models.base import OSType, UnrecognizedBlock
|
|
15
15
|
from confgraph.models.line import LineType
|
|
16
16
|
from confgraph.models.ospf import OSPFConfig
|
|
17
|
+
from confgraph.utils.ospf import canonical_area_id
|
|
17
18
|
from confgraph.models.bgp import BGPConfig, BGPNeighborAF
|
|
18
19
|
from confgraph.models.prefix_list import PrefixListConfig, PrefixListEntry
|
|
19
20
|
from confgraph.models.static_route import StaticRoute
|
|
@@ -306,7 +307,10 @@ class EOSParser(IOSParser):
|
|
|
306
307
|
ospf_area_children[0].text,
|
|
307
308
|
)
|
|
308
309
|
if am:
|
|
309
|
-
|
|
310
|
+
# CCR-0227: assignment to an already-built model skips
|
|
311
|
+
# pydantic validation — canonicalize as the field
|
|
312
|
+
# validator would.
|
|
313
|
+
intf_cfg.ospf_area = canonical_area_id(am.group(1))
|
|
310
314
|
|
|
311
315
|
# VARP: "ip virtual-router address <ip>". A device emits ONE LINE
|
|
312
316
|
# PER ADDRESS, so accumulate rather than overwrite
|
|
@@ -397,7 +401,10 @@ class EOSParser(IOSParser):
|
|
|
397
401
|
m = re.match(r"^\s+network\s+(\S+)\s+area\s+(\S+)\s*$", nc.text)
|
|
398
402
|
if not m:
|
|
399
403
|
continue
|
|
400
|
-
|
|
404
|
+
# CCR-0227: this appends into a LIVE model's list, which runs
|
|
405
|
+
# no field validator — canonicalize here as the validator on
|
|
406
|
+
# OSPFConfig.network_statements would.
|
|
407
|
+
prefix_str, area_id = m.group(1), canonical_area_id(m.group(2))
|
|
401
408
|
try:
|
|
402
409
|
net = IPv4Network(prefix_str, strict=False)
|
|
403
410
|
except ValueError:
|
|
@@ -10,6 +10,7 @@ from confgraph.utils.interface import normalize_interface_name
|
|
|
10
10
|
# confgraph imports, so this cannot cycle (the other change_ir names in this file
|
|
11
11
|
# stay lazily imported at their call sites, unchanged).
|
|
12
12
|
from confgraph.change_ir import _native_deletion_path
|
|
13
|
+
from confgraph.utils.ospf import canonical_area_id
|
|
13
14
|
from confgraph.models.base import OSType
|
|
14
15
|
from confgraph.models.vrf import VRFConfig
|
|
15
16
|
from confgraph.models.interface import (
|
|
@@ -1266,7 +1267,7 @@ class IOSParser(BaseParser):
|
|
|
1266
1267
|
ospf_hello_interval = None
|
|
1267
1268
|
ospf_dead_interval = None
|
|
1268
1269
|
ospf_network_type = None
|
|
1269
|
-
ospf_passive =
|
|
1270
|
+
ospf_passive = None
|
|
1270
1271
|
ospf_authentication = None
|
|
1271
1272
|
ospf_authentication_key = None
|
|
1272
1273
|
ospf_message_digest_keys = {}
|
|
@@ -1380,6 +1381,28 @@ class IOSParser(BaseParser):
|
|
|
1380
1381
|
if intf_obj.find_child_objects(r"^\s+ip\s+ospf\s+mtu-ignore"):
|
|
1381
1382
|
ospf_mtu_ignore = True
|
|
1382
1383
|
|
|
1384
|
+
# ip ospf passive-interface / no ip ospf passive-interface
|
|
1385
|
+
#
|
|
1386
|
+
# CCR-0222: BOTH polarities are value-bearing state, so the
|
|
1387
|
+
# negation parses to an explicit False — it is the per-interface
|
|
1388
|
+
# override of a process-level `passive-interface default`, the
|
|
1389
|
+
# NX-OS hardening pattern, and on hardware the interface-level
|
|
1390
|
+
# word is final. Hence the tri-state field: None means the
|
|
1391
|
+
# interface said nothing and the process-level config decides.
|
|
1392
|
+
# `$`-anchored both ways (disjoint from any operand-bearing
|
|
1393
|
+
# spelling) with last-line-wins, exactly like every sibling
|
|
1394
|
+
# `ip ospf *` scalar above. This is the SHARED walk — NX-OS and
|
|
1395
|
+
# EOS inherit it through super().parse_interfaces(); the
|
|
1396
|
+
# process-level `passive-interface` spelling is parsed by the
|
|
1397
|
+
# OSPF-process walks and is untouched by this.
|
|
1398
|
+
ospf_passive_children = intf_obj.find_child_objects(
|
|
1399
|
+
r"^\s+(?:no\s+)?ip\s+ospf\s+passive-interface\s*$"
|
|
1400
|
+
)
|
|
1401
|
+
if ospf_passive_children:
|
|
1402
|
+
ospf_passive = not re.match(
|
|
1403
|
+
r"^\s*no\s+", ospf_passive_children[-1].text
|
|
1404
|
+
)
|
|
1405
|
+
|
|
1383
1406
|
# Tunnel attributes
|
|
1384
1407
|
tunnel_source = None
|
|
1385
1408
|
tunnel_destination = None
|
|
@@ -1893,6 +1916,11 @@ class IOSParser(BaseParser):
|
|
|
1893
1916
|
"ospf_dead_interval": r"^\s+ip\s+ospf\s+dead-interval\s+",
|
|
1894
1917
|
"ospf_network_type": r"^\s+ip\s+ospf\s+network\s+",
|
|
1895
1918
|
"ospf_mtu_ignore": r"^\s+ip\s+ospf\s+mtu-ignore\s*$",
|
|
1919
|
+
# CCR-0222: both polarities — the SET op for an explicit False must
|
|
1920
|
+
# carry the `no ip ospf passive-interface` line, which is also what
|
|
1921
|
+
# claims that negation for the CCR-0203 ledger (consumed, not
|
|
1922
|
+
# disclosed).
|
|
1923
|
+
"ospf_passive": r"^\s+(?:no\s+)?ip\s+ospf\s+passive-interface\s*$",
|
|
1896
1924
|
"cdp_enabled": r"^\s+(?:no\s+)?cdp\s+enable\s*$",
|
|
1897
1925
|
"lldp_transmit": r"^\s+(?:no\s+)?lldp\s+transmit\s*$",
|
|
1898
1926
|
"lldp_receive": r"^\s+(?:no\s+)?lldp\s+receive\s*$",
|
|
@@ -4952,6 +4980,18 @@ class IOSParser(BaseParser):
|
|
|
4952
4980
|
"""
|
|
4953
4981
|
from confgraph.change_ir import ChangeOp, Verb
|
|
4954
4982
|
|
|
4983
|
+
# CCR-0227: areas are STORED canonical, so both sides of this walk must
|
|
4984
|
+
# speak the canonical spelling — the removal key, which the merger
|
|
4985
|
+
# compares against stored ``network_statements``, and the caller's
|
|
4986
|
+
# positive map, whose keys come from its own walk (IOS builds them raw
|
|
4987
|
+
# from the regex, EOS canonical). Normalizing both HERE, where
|
|
4988
|
+
# everything the two spellings must never disagree on already lives,
|
|
4989
|
+
# keeps suppression and removal from splitting on the spelling.
|
|
4990
|
+
positive_net_last_line = {
|
|
4991
|
+
(net, canonical_area_id(area)): line
|
|
4992
|
+
for (net, area), line in positive_net_last_line.items()
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4955
4995
|
for nnc in ospf_obj.find_child_objects(r"^\s+no\s+network\s+"):
|
|
4956
4996
|
nnm = re.match(child_pattern, nnc.text)
|
|
4957
4997
|
if not nnm:
|
|
@@ -4960,7 +5000,7 @@ class IOSParser(BaseParser):
|
|
|
4960
5000
|
if extracted is None:
|
|
4961
5001
|
continue
|
|
4962
5002
|
rem_net, rem_area = extracted
|
|
4963
|
-
rem_key = (str(rem_net), rem_area)
|
|
5003
|
+
rem_key = (str(rem_net), canonical_area_id(rem_area))
|
|
4964
5004
|
if positive_net_last_line.get(rem_key, -1) > nnc.linenum:
|
|
4965
5005
|
# CCR-0203: suppressed-by-refresh is CONSUMED (understood and
|
|
4966
5006
|
# deliberately dropped), not blind — claim it or the fallback
|
|
@@ -5186,16 +5226,28 @@ class IOSParser(BaseParser):
|
|
|
5186
5226
|
# (area_id, prefix) reappears as a positive range line LATER in
|
|
5187
5227
|
# the block (refresh → re-add wins → device truth and legacy
|
|
5188
5228
|
# parity).
|
|
5229
|
+
#
|
|
5230
|
+
# ONE key builder for both walks (CCR-0227): the area half is
|
|
5231
|
+
# canonicalized, because the merger looks the area up by its
|
|
5232
|
+
# STORED id, and the suppression map and the removal must agree
|
|
5233
|
+
# on the spelling or a refresh stops suppressing its own removal.
|
|
5234
|
+
# Returns None on an unparseable mask — dropped on BOTH walks, as
|
|
5235
|
+
# before.
|
|
5236
|
+
def _range_key(m) -> "tuple[str, str] | None":
|
|
5237
|
+
try:
|
|
5238
|
+
prefix = IPv4Network(f"{m.group(2)}/{m.group(3)}")
|
|
5239
|
+
except ValueError:
|
|
5240
|
+
return None
|
|
5241
|
+
return canonical_area_id(m.group(1)), str(prefix)
|
|
5242
|
+
|
|
5189
5243
|
positive_range_last_line: dict[tuple[str, str], int] = {}
|
|
5190
5244
|
for rc in ospf_obj.find_child_objects(r"^\s+area\s+\S+\s+range\s+"):
|
|
5191
5245
|
rm = re.match(r"^\s+area\s+(\S+)\s+range\s+(\S+)\s+(\S+)", rc.text)
|
|
5192
5246
|
if not rm:
|
|
5193
5247
|
continue
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
except ValueError:
|
|
5248
|
+
rkey = _range_key(rm)
|
|
5249
|
+
if rkey is None:
|
|
5197
5250
|
continue
|
|
5198
|
-
rkey = (rm.group(1), str(rng_pfx))
|
|
5199
5251
|
positive_range_last_line[rkey] = max(
|
|
5200
5252
|
positive_range_last_line.get(rkey, -1), rc.linenum
|
|
5201
5253
|
)
|
|
@@ -5205,11 +5257,9 @@ class IOSParser(BaseParser):
|
|
|
5205
5257
|
)
|
|
5206
5258
|
if not nrm:
|
|
5207
5259
|
continue
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
except ValueError:
|
|
5260
|
+
rkey = _range_key(nrm)
|
|
5261
|
+
if rkey is None:
|
|
5211
5262
|
continue
|
|
5212
|
-
rkey = (nrm.group(1), str(rng_pfx))
|
|
5213
5263
|
if positive_range_last_line.get(rkey, -1) > nrc.linenum:
|
|
5214
5264
|
self._claim_negation(nrc) # CCR-0203: suppressed == consumed
|
|
5215
5265
|
continue # re-added later in the block — removal suppressed
|
|
@@ -5383,6 +5433,19 @@ class IOSParser(BaseParser):
|
|
|
5383
5433
|
def _queue_ospf_negation(verb, path_tail, child):
|
|
5384
5434
|
# CCR-0110 Phase E4/E5: queue the native op only; op-primary
|
|
5385
5435
|
# parsers no longer persist the deprecated no_commands string.
|
|
5436
|
+
#
|
|
5437
|
+
# CCR-0227: the area segment of an area-scoped path comes from
|
|
5438
|
+
# the proposal TEXT, while the positive member ops key off the
|
|
5439
|
+
# stored (canonical) ``area.area_id`` — so canonicalize it
|
|
5440
|
+
# here, at the one point every OSPF negation op is built. A
|
|
5441
|
+
# `no area 0.0.0.1 stub` must address the same area a
|
|
5442
|
+
# `no area 1 stub` does, because the device does.
|
|
5443
|
+
if len(path_tail) > 1 and path_tail[0] == "area":
|
|
5444
|
+
path_tail = (
|
|
5445
|
+
"area",
|
|
5446
|
+
canonical_area_id(path_tail[1]),
|
|
5447
|
+
*path_tail[2:],
|
|
5448
|
+
)
|
|
5386
5449
|
self._pending_native_ospf_ops.append(
|
|
5387
5450
|
ChangeOp(
|
|
5388
5451
|
verb=verb,
|