confgraph 0.3.7__tar.gz → 0.3.8__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.8}/PKG-INFO +1 -1
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/interface.py +28 -5
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/ospf.py +33 -3
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/__init__.py +38 -1
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/base.py +50 -9
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/eos_parser.py +9 -2
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/ios_parser.py +73 -10
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/nxos_parser.py +6 -1
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/tombstones.py +15 -0
- confgraph-0.3.8/confgraph/utils/ospf.py +43 -0
- confgraph-0.3.8/dist-0.3.7/confgraph-0.3.7-py3-none-any.whl +0 -0
- confgraph-0.3.8/dist-0.3.7/confgraph-0.3.7.tar.gz +0 -0
- confgraph-0.3.8/dist-0.3.8/.gitignore +1 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/pyproject.toml +1 -1
- {confgraph-0.3.7 → confgraph-0.3.8}/uv.lock +1 -1
- {confgraph-0.3.7 → confgraph-0.3.8}/.github/workflows/publish.yml +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/.github/workflows/redos-scan.yml +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/.gitignore +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/.python-version +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/CHANGELOG.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/CLAUDE.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/INTERFACE.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/LICENSE +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/README.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/analysis/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/analysis/dependency_resolver.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/change_ir.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/cli.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/assets/cytoscape-dagre.min.js +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/assets/cytoscape.min.js +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/assets/dagre.min.js +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/builder.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/exporters/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/exporters/base.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/exporters/html.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/graph/exporters/json.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/loader.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/aaa.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/acl.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/banner.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/base.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/bfd.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/bgp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/cdp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/community_list.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/crypto.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/dhcp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/dns.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/eem.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/eigrp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/evpn.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/ipsla.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/isis.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/line.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/lldp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/logging_config.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/mpls.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/multicast.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/nat.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/netflow.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/ntp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/object_group.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/object_tracking.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/panos_zone.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/parsed_config.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/prefix_list.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/qos.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/rip.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/route_map.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/snmp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/static_route.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/stp.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/topology.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/vlan.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/vpc.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/vrf.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/models/vxlan.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/iosxr_parser.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/junos_groups.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/junos_hierarchy.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/junos_parser.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/panos_parser.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/parsers/panos_xml.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/topology/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/topology/exporters.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/topology/graph.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/topology/ingest.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/utils/__init__.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/utils/escaping.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/confgraph/utils/interface.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/dist-0.3.7/.gitignore +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/ADDING_NEW_OS_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/ARCHITECTURE.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/CLI_USAGE.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/EOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/IOSXR_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/IOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/JUNOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/NXOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/PANOS_PARSER_SUPPORT.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/PARSER_SUPPORT_MATRIX.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/bgp_cluster_view.png +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/default_view.png +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/specific_node_highlight.png +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/docs/supported_os_types.md +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/main.py +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/complex_router.ios +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/eos.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/ios.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/ios01.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/ios_xe.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/ios_xr.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/iosxr_test.cfg +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/junos_test.cfg +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/nxos.txt +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/samples/panos_sample.xml +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/tools/redos_baseline.json +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/tools/redos_scan.json +0 -0
- {confgraph-0.3.7 → confgraph-0.3.8}/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.8
|
|
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
|
|
@@ -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
|
|
@@ -108,7 +108,44 @@ 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
|
+
PARSER_BEHAVIOR_VERSION: int = 13
|
|
112
149
|
|
|
113
150
|
__all__ = [
|
|
114
151
|
"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,
|
|
@@ -13,6 +13,7 @@ from confgraph.models.bgp import (
|
|
|
13
13
|
BGPBestpathOptions,
|
|
14
14
|
)
|
|
15
15
|
from confgraph.models.ospf import OSPFConfig
|
|
16
|
+
from confgraph.utils.ospf import canonical_area_id
|
|
16
17
|
from confgraph.models.qos import ControlPlaneConfig
|
|
17
18
|
from confgraph.models.interface import InterfaceFlowMonitor, StormControlLevel, VRRPGroup
|
|
18
19
|
from confgraph.models.netflow import (
|
|
@@ -488,7 +489,11 @@ class NXOSParser(IOSParser):
|
|
|
488
489
|
)
|
|
489
490
|
if m:
|
|
490
491
|
intf_cfg.ospf_process_id = int(m.group(1))
|
|
491
|
-
|
|
492
|
+
# CCR-0227: assignment to an already-built model skips
|
|
493
|
+
# pydantic validation, so this post-patch calls the
|
|
494
|
+
# canonicalizer the field validator would have — NX-OS is
|
|
495
|
+
# the dotted-quad vendor that makes it matter.
|
|
496
|
+
intf_cfg.ospf_area = canonical_area_id(m.group(2))
|
|
492
497
|
|
|
493
498
|
# NX-OS storm-control: "storm-control {broadcast|multicast|unicast}
|
|
494
499
|
# level <threshold>". The threshold is a bandwidth percentage by
|
|
@@ -86,6 +86,19 @@ def _derive_secondary_cidr(ctx: dict) -> "dict[str, Any] | None":
|
|
|
86
86
|
return None
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def _derive_area_id(ctx: dict) -> "dict[str, Any] | None":
|
|
90
|
+
"""Canonical OSPF area-ID key for an area-scoped tombstone (CCR-0227).
|
|
91
|
+
|
|
92
|
+
Areas are stored canonical (``0.0.0.1`` -> ``1``), and the POSITIVE
|
|
93
|
+
member ops read the stored value, so a deletion key built from the raw
|
|
94
|
+
proposal text would address an area that no longer exists under that
|
|
95
|
+
spelling. One normalizer, the same one the models use.
|
|
96
|
+
"""
|
|
97
|
+
from confgraph.utils.ospf import canonical_area_id
|
|
98
|
+
|
|
99
|
+
return {"area_id": canonical_area_id(ctx["area_id"])}
|
|
100
|
+
|
|
101
|
+
|
|
89
102
|
def _derive_line_type(ctx: dict) -> "dict[str, Any] | None":
|
|
90
103
|
"""Canonical ``LineType`` value for a line-block tombstone key.
|
|
91
104
|
|
|
@@ -162,6 +175,7 @@ NESTED_DELETION_RULES: list[NestedDeletionRule] = [
|
|
|
162
175
|
child_pattern=r"^no\s+area\s+(\S+)\s+stub",
|
|
163
176
|
child_groups=["area_id"],
|
|
164
177
|
template="ospf:{pid}:area:{area_id}:stub_reset",
|
|
178
|
+
derive=_derive_area_id,
|
|
165
179
|
),
|
|
166
180
|
# OSPF area type reset — 'no area N nssa' / 'no area N nssa no-summary'
|
|
167
181
|
# Proposal: ``no area 2 nssa`` inside ``router ospf 1``
|
|
@@ -173,6 +187,7 @@ NESTED_DELETION_RULES: list[NestedDeletionRule] = [
|
|
|
173
187
|
child_pattern=r"^no\s+area\s+(\S+)\s+nssa",
|
|
174
188
|
child_groups=["area_id"],
|
|
175
189
|
template="ospf:{pid}:area:{area_id}:nssa_reset",
|
|
190
|
+
derive=_derive_area_id,
|
|
176
191
|
),
|
|
177
192
|
# VRF route-target removals (CCR confgraph_vrf_rt_removal_tombstones.md).
|
|
178
193
|
# The WI-4 merge made route_target_* lists ADDITIVE (device-faithful), so
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""OSPF value normalization shared by the models and every consumer.
|
|
2
|
+
|
|
3
|
+
CCR-0227: an OSPF area ID is a 32-bit number that vendors print two ways.
|
|
4
|
+
NX-OS and JunOS spell the backbone ``0.0.0.0``; IOS and IOS-XR spell it ``0``.
|
|
5
|
+
On hardware they are the same area and the adjacency forms. Stored as raw
|
|
6
|
+
strings they are two different keys, so the model partitions one real area in
|
|
7
|
+
two across a mixed-vendor estate: the adjacency gate refuses the neighbor,
|
|
8
|
+
and every area-KEYED structure (ABR detection, inter-area filtering) computes
|
|
9
|
+
against half the area.
|
|
10
|
+
|
|
11
|
+
Canonical form is the DECIMAL spelling — chosen over dotted-quad because it
|
|
12
|
+
leaves existing tombstone and golden keys (``ospf:1:area:0:stub_reset``)
|
|
13
|
+
byte-identical.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
# Exactly four dotted octets. [0-9] not \d — \d is Unicode-aware, and a
|
|
19
|
+
# Devanagari digit is not an area ID.
|
|
20
|
+
_DOTTED_QUAD = re.compile(r"^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def canonical_area_id(value):
|
|
24
|
+
"""Return *value*'s canonical OSPF area ID — the single normalizer.
|
|
25
|
+
|
|
26
|
+
``"0.0.0.0"`` -> ``"0"``; any dotted quad -> the decimal spelling of its
|
|
27
|
+
32-bit value (``"0.0.0.51"`` -> ``"51"``). Everything else — decimal
|
|
28
|
+
strings, None, non-strings — passes through UNCHANGED: this normalizes a
|
|
29
|
+
spelling, it never rejects or invents one. Deliberately not a parser of
|
|
30
|
+
out-of-range quads either (``"0.0.0.999"`` is not an area ID in any
|
|
31
|
+
spelling, so it passes through for the caller to see as written).
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(value, str):
|
|
34
|
+
return value
|
|
35
|
+
match = _DOTTED_QUAD.match(value.strip())
|
|
36
|
+
if match is None:
|
|
37
|
+
return value
|
|
38
|
+
octets = [int(group) for group in match.groups()]
|
|
39
|
+
if any(octet > 255 for octet in octets):
|
|
40
|
+
return value
|
|
41
|
+
return str(
|
|
42
|
+
(octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
|
|
43
|
+
)
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "confgraph"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.8"
|
|
8
8
|
description = "Local-first network config dependency mapper — parse, visualize, and lint vendor configs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|