confgraph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- confgraph/analysis/__init__.py +15 -0
- confgraph/analysis/dependency_resolver.py +661 -0
- confgraph/cli.py +348 -0
- confgraph/graph/__init__.py +15 -0
- confgraph/graph/assets/cytoscape-dagre.min.js +397 -0
- confgraph/graph/assets/cytoscape.min.js +32 -0
- confgraph/graph/assets/dagre.min.js +3809 -0
- confgraph/graph/builder.py +307 -0
- confgraph/graph/exporters/__init__.py +7 -0
- confgraph/graph/exporters/base.py +25 -0
- confgraph/graph/exporters/html.py +999 -0
- confgraph/graph/exporters/json.py +53 -0
- confgraph/models/__init__.py +76 -0
- confgraph/models/acl.py +141 -0
- confgraph/models/banner.py +16 -0
- confgraph/models/base.py +67 -0
- confgraph/models/bfd.py +42 -0
- confgraph/models/bgp.py +385 -0
- confgraph/models/community_list.py +68 -0
- confgraph/models/crypto.py +98 -0
- confgraph/models/eem.py +33 -0
- confgraph/models/eigrp.py +56 -0
- confgraph/models/interface.py +277 -0
- confgraph/models/ipsla.py +47 -0
- confgraph/models/isis.py +140 -0
- confgraph/models/line.py +44 -0
- confgraph/models/logging_config.py +38 -0
- confgraph/models/multicast.py +45 -0
- confgraph/models/nat.py +66 -0
- confgraph/models/ntp.py +46 -0
- confgraph/models/object_tracking.py +31 -0
- confgraph/models/ospf.py +268 -0
- confgraph/models/parsed_config.py +211 -0
- confgraph/models/prefix_list.py +51 -0
- confgraph/models/qos.py +81 -0
- confgraph/models/rip.py +43 -0
- confgraph/models/route_map.py +72 -0
- confgraph/models/snmp.py +79 -0
- confgraph/models/static_route.py +50 -0
- confgraph/models/vrf.py +55 -0
- confgraph/parsers/__init__.py +16 -0
- confgraph/parsers/base.py +536 -0
- confgraph/parsers/eos_parser.py +889 -0
- confgraph/parsers/ios_parser.py +4167 -0
- confgraph/parsers/iosxr_parser.py +1235 -0
- confgraph/parsers/nxos_parser.py +359 -0
- confgraph-0.1.0.dist-info/METADATA +108 -0
- confgraph-0.1.0.dist-info/RECORD +52 -0
- confgraph-0.1.0.dist-info/WHEEL +5 -0
- confgraph-0.1.0.dist-info/entry_points.txt +2 -0
- confgraph-0.1.0.dist-info/licenses/LICENSE +201 -0
- confgraph-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Configuration analysis tools — dependency resolution, orphan detection, linting."""
|
|
2
|
+
|
|
3
|
+
from confgraph.analysis.dependency_resolver import (
|
|
4
|
+
DependencyLink,
|
|
5
|
+
DependencyReport,
|
|
6
|
+
DependencyResolver,
|
|
7
|
+
OrphanedObject,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"DependencyLink",
|
|
12
|
+
"DependencyReport",
|
|
13
|
+
"DependencyResolver",
|
|
14
|
+
"OrphanedObject",
|
|
15
|
+
]
|
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
"""Dependency resolution for parsed network configurations.
|
|
2
|
+
|
|
3
|
+
Resolves all string-based cross-references between parsed objects
|
|
4
|
+
(e.g. BGP neighbor → RouteMapConfig) and identifies:
|
|
5
|
+
|
|
6
|
+
- Dangling references: a name is referenced but no matching object exists
|
|
7
|
+
- Orphaned objects: an object is defined but never referenced by anything
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from confgraph.analysis import DependencyResolver
|
|
12
|
+
|
|
13
|
+
report = DependencyResolver(parsed_config).resolve()
|
|
14
|
+
report.dangling_refs # list[DependencyLink] where resolved=False
|
|
15
|
+
report.orphaned # list[OrphanedObject]
|
|
16
|
+
report.has_issues # bool
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
|
|
23
|
+
from confgraph.models.parsed_config import ParsedConfig
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Output models
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
class DependencyLink(BaseModel):
|
|
31
|
+
"""A single cross-reference between two config objects."""
|
|
32
|
+
|
|
33
|
+
source_type: str = Field(description="Kind of object that holds the reference")
|
|
34
|
+
source_id: str = Field(description="Identifier of the source object")
|
|
35
|
+
source_field: str = Field(description="Field name that holds the reference")
|
|
36
|
+
ref_type: str = Field(description="Kind of object being referenced")
|
|
37
|
+
ref_name: str = Field(description="Name of the referenced object")
|
|
38
|
+
resolved: bool = Field(description="True if the target exists in ParsedConfig")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OrphanedObject(BaseModel):
|
|
42
|
+
"""An object that is defined but never referenced by any other object."""
|
|
43
|
+
|
|
44
|
+
object_type: str = Field(description="Kind of object (e.g. 'route_map', 'prefix_list')")
|
|
45
|
+
name: str = Field(description="Name of the orphaned object")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DependencyReport(BaseModel):
|
|
49
|
+
"""Full dependency analysis result for a ParsedConfig."""
|
|
50
|
+
|
|
51
|
+
links: list[DependencyLink] = Field(default_factory=list)
|
|
52
|
+
orphaned: list[OrphanedObject] = Field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def dangling_refs(self) -> list[DependencyLink]:
|
|
56
|
+
"""References that point to objects not found in ParsedConfig."""
|
|
57
|
+
return [l for l in self.links if not l.resolved]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def has_issues(self) -> bool:
|
|
61
|
+
return bool(self.dangling_refs or self.orphaned)
|
|
62
|
+
|
|
63
|
+
def summary(self) -> str:
|
|
64
|
+
total = len(self.links)
|
|
65
|
+
resolved = sum(1 for l in self.links if l.resolved)
|
|
66
|
+
return (
|
|
67
|
+
f"Links: {total} total, {resolved} resolved, {len(self.dangling_refs)} dangling | "
|
|
68
|
+
f"Orphans: {len(self.orphaned)}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Resolver
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
class DependencyResolver:
|
|
77
|
+
"""Resolves all cross-references in a ParsedConfig.
|
|
78
|
+
|
|
79
|
+
Builds name indexes on construction, then walks every object to emit
|
|
80
|
+
DependencyLinks. Tracks which named objects are referenced for orphan
|
|
81
|
+
detection.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, config: ParsedConfig) -> None:
|
|
85
|
+
self._config = config
|
|
86
|
+
|
|
87
|
+
# Name indexes for O(1) resolution
|
|
88
|
+
self._route_maps = {rm.name: rm for rm in config.route_maps}
|
|
89
|
+
self._prefix_lists = {pl.name: pl for pl in config.prefix_lists}
|
|
90
|
+
self._community_lists = {cl.name: cl for cl in config.community_lists}
|
|
91
|
+
self._as_path_lists = {ap.name: ap for ap in config.as_path_lists}
|
|
92
|
+
self._acls = {acl.name: acl for acl in config.acls}
|
|
93
|
+
self._vrfs = {vrf.name: vrf for vrf in config.vrfs}
|
|
94
|
+
self._interfaces = {iface.name: iface for iface in config.interfaces}
|
|
95
|
+
self._class_maps = {cm.name: cm for cm in config.class_maps}
|
|
96
|
+
self._policy_maps = {pm.name: pm for pm in config.policy_maps}
|
|
97
|
+
self._ip_sla_ops = {op.sla_id: op for op in config.ip_sla_operations}
|
|
98
|
+
|
|
99
|
+
# Track which named objects have been referenced (for orphan detection)
|
|
100
|
+
self._referenced: dict[str, set[str]] = {
|
|
101
|
+
"route_map": set(),
|
|
102
|
+
"prefix_list": set(),
|
|
103
|
+
"community_list": set(),
|
|
104
|
+
"as_path_list": set(),
|
|
105
|
+
"acl": set(),
|
|
106
|
+
"class_map": set(),
|
|
107
|
+
"policy_map": set(),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# BGP peer group orphans collected during BGP resolution
|
|
111
|
+
self._pg_orphans: list[OrphanedObject] = []
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Public
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def resolve(self) -> DependencyReport:
|
|
118
|
+
links: list[DependencyLink] = []
|
|
119
|
+
links.extend(self._resolve_bgp())
|
|
120
|
+
links.extend(self._resolve_ospf())
|
|
121
|
+
links.extend(self._resolve_eigrp())
|
|
122
|
+
links.extend(self._resolve_rip())
|
|
123
|
+
links.extend(self._resolve_interfaces())
|
|
124
|
+
links.extend(self._resolve_route_maps())
|
|
125
|
+
links.extend(self._resolve_static_routes())
|
|
126
|
+
links.extend(self._resolve_ntp())
|
|
127
|
+
links.extend(self._resolve_snmp())
|
|
128
|
+
links.extend(self._resolve_lines())
|
|
129
|
+
links.extend(self._resolve_qos())
|
|
130
|
+
links.extend(self._resolve_nat())
|
|
131
|
+
links.extend(self._resolve_crypto())
|
|
132
|
+
links.extend(self._resolve_ipsla())
|
|
133
|
+
links.extend(self._resolve_object_tracking())
|
|
134
|
+
links.extend(self._resolve_multicast())
|
|
135
|
+
|
|
136
|
+
orphaned = self._find_orphans()
|
|
137
|
+
return DependencyReport(links=links, orphaned=orphaned)
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# BGP
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _resolve_bgp(self) -> list[DependencyLink]:
|
|
144
|
+
links: list[DependencyLink] = []
|
|
145
|
+
|
|
146
|
+
for bgp in self._config.bgp_instances:
|
|
147
|
+
# Node ID must match GraphBuilder's naming: "{asn}" or "{asn} vrf {vrf}"
|
|
148
|
+
bgp_node_id = f"{bgp.asn}" + (f" vrf {bgp.vrf}" if bgp.vrf else "")
|
|
149
|
+
|
|
150
|
+
# BGP instance VRF
|
|
151
|
+
if bgp.vrf:
|
|
152
|
+
links.append(self._link("bgp_instance", bgp_node_id, "vrf", "vrf", bgp.vrf))
|
|
153
|
+
|
|
154
|
+
# Global network statements → collapse to bgp_instance
|
|
155
|
+
for net in bgp.networks:
|
|
156
|
+
if net.route_map:
|
|
157
|
+
links.append(self._link(
|
|
158
|
+
"bgp_instance", bgp_node_id, "network_route_map", "route_map", net.route_map,
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
# Global redistribute → collapse to bgp_instance
|
|
162
|
+
for redist in bgp.redistribute:
|
|
163
|
+
if redist.route_map:
|
|
164
|
+
links.append(self._link(
|
|
165
|
+
"bgp_instance", bgp_node_id,
|
|
166
|
+
f"redistribute_{redist.protocol}_route_map", "route_map", redist.route_map,
|
|
167
|
+
))
|
|
168
|
+
|
|
169
|
+
# Peer groups → collapse to bgp_instance
|
|
170
|
+
pg_names_defined = {pg.name for pg in bgp.peer_groups}
|
|
171
|
+
pg_names_referenced: set[str] = set()
|
|
172
|
+
|
|
173
|
+
for pg in bgp.peer_groups:
|
|
174
|
+
links.extend(self._resolve_policy_holder("bgp_instance", bgp_node_id, pg))
|
|
175
|
+
|
|
176
|
+
# Neighbors → collapse to bgp_instance
|
|
177
|
+
for nb in bgp.neighbors:
|
|
178
|
+
if nb.peer_group:
|
|
179
|
+
pg_names_referenced.add(nb.peer_group)
|
|
180
|
+
|
|
181
|
+
links.extend(self._resolve_policy_holder("bgp_instance", bgp_node_id, nb))
|
|
182
|
+
|
|
183
|
+
# Address families → collapse to bgp_instance
|
|
184
|
+
for af in bgp.address_families:
|
|
185
|
+
if af.vrf:
|
|
186
|
+
links.append(self._link("bgp_instance", bgp_node_id, "vrf", "vrf", af.vrf))
|
|
187
|
+
|
|
188
|
+
for net in af.networks:
|
|
189
|
+
if net.route_map:
|
|
190
|
+
links.append(self._link(
|
|
191
|
+
"bgp_instance", bgp_node_id, "network_route_map", "route_map", net.route_map,
|
|
192
|
+
))
|
|
193
|
+
|
|
194
|
+
for redist in af.redistribute:
|
|
195
|
+
if redist.route_map:
|
|
196
|
+
links.append(self._link(
|
|
197
|
+
"bgp_instance", bgp_node_id,
|
|
198
|
+
f"redistribute_{redist.protocol}_route_map", "route_map", redist.route_map,
|
|
199
|
+
))
|
|
200
|
+
|
|
201
|
+
for agg in af.aggregate_addresses:
|
|
202
|
+
for field in ("attribute_map", "advertise_map", "suppress_map"):
|
|
203
|
+
val = getattr(agg, field)
|
|
204
|
+
if val:
|
|
205
|
+
links.append(self._link(
|
|
206
|
+
"bgp_instance", bgp_node_id, field, "route_map", val,
|
|
207
|
+
))
|
|
208
|
+
|
|
209
|
+
# Peer group orphan detection (scoped to this BGP instance)
|
|
210
|
+
for pg_name in pg_names_defined:
|
|
211
|
+
if pg_name not in pg_names_referenced:
|
|
212
|
+
self._pg_orphans.append(OrphanedObject(
|
|
213
|
+
object_type="bgp_peer_group", name=f"{bgp_node_id}:{pg_name}",
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
return links
|
|
217
|
+
|
|
218
|
+
def _resolve_policy_holder(
|
|
219
|
+
self, source_type: str, source_id: str, obj: object,
|
|
220
|
+
) -> list[DependencyLink]:
|
|
221
|
+
"""Resolve route-map / prefix-list / filter-list / update-source refs
|
|
222
|
+
from a BGPNeighbor or BGPPeerGroup, including per-AF fields.
|
|
223
|
+
All links are emitted from source_type/source_id (the parent node)."""
|
|
224
|
+
links: list[DependencyLink] = []
|
|
225
|
+
|
|
226
|
+
_DIRECT_FIELDS: list[tuple[str, str]] = [
|
|
227
|
+
("route_map_in", "route_map"),
|
|
228
|
+
("route_map_out", "route_map"),
|
|
229
|
+
("prefix_list_in", "prefix_list"),
|
|
230
|
+
("prefix_list_out", "prefix_list"),
|
|
231
|
+
("filter_list_in", "as_path_list"),
|
|
232
|
+
("filter_list_out", "as_path_list"),
|
|
233
|
+
]
|
|
234
|
+
for field, ref_type in _DIRECT_FIELDS:
|
|
235
|
+
val = getattr(obj, field, None)
|
|
236
|
+
if val:
|
|
237
|
+
links.append(self._link(source_type, source_id, field, ref_type, val))
|
|
238
|
+
|
|
239
|
+
if hasattr(obj, "update_source") and obj.update_source:
|
|
240
|
+
links.append(self._link(
|
|
241
|
+
source_type, source_id, "update_source", "interface", obj.update_source,
|
|
242
|
+
))
|
|
243
|
+
|
|
244
|
+
# Per address-family — collapse to same parent node
|
|
245
|
+
_AF_FIELDS: list[tuple[str, str]] = [
|
|
246
|
+
("route_map_in", "route_map"),
|
|
247
|
+
("route_map_out", "route_map"),
|
|
248
|
+
("prefix_list_in", "prefix_list"),
|
|
249
|
+
("prefix_list_out", "prefix_list"),
|
|
250
|
+
("filter_list_in", "as_path_list"),
|
|
251
|
+
("filter_list_out", "as_path_list"),
|
|
252
|
+
("default_originate_route_map", "route_map"),
|
|
253
|
+
]
|
|
254
|
+
for af in getattr(obj, "address_families", []):
|
|
255
|
+
for field, ref_type in _AF_FIELDS:
|
|
256
|
+
val = getattr(af, field, None)
|
|
257
|
+
if val:
|
|
258
|
+
links.append(self._link(source_type, source_id, field, ref_type, val))
|
|
259
|
+
|
|
260
|
+
return links
|
|
261
|
+
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
# OSPF
|
|
264
|
+
# ------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def _resolve_ospf(self) -> list[DependencyLink]:
|
|
267
|
+
links: list[DependencyLink] = []
|
|
268
|
+
|
|
269
|
+
for ospf in self._config.ospf_instances:
|
|
270
|
+
# Node ID must match GraphBuilder's naming: "{process_id}" or "{process_id} vrf {vrf}"
|
|
271
|
+
ospf_node_id = f"{ospf.process_id}" + (f" vrf {ospf.vrf}" if ospf.vrf else "")
|
|
272
|
+
|
|
273
|
+
if ospf.vrf:
|
|
274
|
+
links.append(self._link("ospf_instance", ospf_node_id, "vrf", "vrf", ospf.vrf))
|
|
275
|
+
|
|
276
|
+
for redist in ospf.redistribute:
|
|
277
|
+
if redist.route_map:
|
|
278
|
+
links.append(self._link(
|
|
279
|
+
"ospf_instance", ospf_node_id,
|
|
280
|
+
f"redistribute_{redist.protocol}_route_map", "route_map", redist.route_map,
|
|
281
|
+
))
|
|
282
|
+
|
|
283
|
+
if ospf.default_information_originate_route_map:
|
|
284
|
+
links.append(self._link(
|
|
285
|
+
"ospf_instance", ospf_node_id,
|
|
286
|
+
"default_information_originate_route_map",
|
|
287
|
+
"route_map", ospf.default_information_originate_route_map,
|
|
288
|
+
))
|
|
289
|
+
|
|
290
|
+
return links
|
|
291
|
+
|
|
292
|
+
# ------------------------------------------------------------------
|
|
293
|
+
# Interfaces
|
|
294
|
+
# ------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
def _resolve_interfaces(self) -> list[DependencyLink]:
|
|
297
|
+
links: list[DependencyLink] = []
|
|
298
|
+
for iface in self._config.interfaces:
|
|
299
|
+
if iface.vrf:
|
|
300
|
+
links.append(self._link("interface", iface.name, "vrf", "vrf", iface.vrf))
|
|
301
|
+
if iface.unnumbered_source:
|
|
302
|
+
links.append(self._link(
|
|
303
|
+
"interface", iface.name, "unnumbered_source", "interface", iface.unnumbered_source,
|
|
304
|
+
))
|
|
305
|
+
if iface.acl_in:
|
|
306
|
+
links.append(self._link("interface", iface.name, "acl_in", "acl", iface.acl_in))
|
|
307
|
+
if iface.acl_out:
|
|
308
|
+
links.append(self._link("interface", iface.name, "acl_out", "acl", iface.acl_out))
|
|
309
|
+
return links
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
# Route-maps (match clause references)
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
def _resolve_route_maps(self) -> list[DependencyLink]:
|
|
316
|
+
links: list[DependencyLink] = []
|
|
317
|
+
for rm in self._config.route_maps:
|
|
318
|
+
for seq in rm.sequences:
|
|
319
|
+
for match in seq.match_clauses:
|
|
320
|
+
ref_type = self._infer_match_ref_type(match.match_type)
|
|
321
|
+
if ref_type is None:
|
|
322
|
+
continue
|
|
323
|
+
for val in match.values:
|
|
324
|
+
links.append(self._link(
|
|
325
|
+
"route_map",
|
|
326
|
+
rm.name,
|
|
327
|
+
match.match_type,
|
|
328
|
+
ref_type,
|
|
329
|
+
val,
|
|
330
|
+
))
|
|
331
|
+
return links
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def _infer_match_ref_type(match_type: str) -> str | None:
|
|
335
|
+
"""Map a route-map match_type string to its referenced object type."""
|
|
336
|
+
mt = match_type.lower()
|
|
337
|
+
if "prefix-list" in mt:
|
|
338
|
+
return "prefix_list"
|
|
339
|
+
if "as-path" in mt:
|
|
340
|
+
return "as_path_list"
|
|
341
|
+
if "community" in mt and "extcommunity" not in mt:
|
|
342
|
+
return "community_list"
|
|
343
|
+
if any(kw in mt for kw in ("ip address", "ip next-hop", "ip route-source")):
|
|
344
|
+
return "acl"
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------------
|
|
348
|
+
# Static routes
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
def _resolve_static_routes(self) -> list[DependencyLink]:
|
|
352
|
+
links: list[DependencyLink] = []
|
|
353
|
+
for sr in self._config.static_routes:
|
|
354
|
+
if sr.vrf:
|
|
355
|
+
links.append(self._link(
|
|
356
|
+
"static_route", str(sr.destination), "vrf", "vrf", sr.vrf,
|
|
357
|
+
))
|
|
358
|
+
if sr.next_hop_interface:
|
|
359
|
+
links.append(self._link(
|
|
360
|
+
"static_route", str(sr.destination),
|
|
361
|
+
"next_hop_interface", "interface", sr.next_hop_interface,
|
|
362
|
+
))
|
|
363
|
+
return links
|
|
364
|
+
|
|
365
|
+
# ------------------------------------------------------------------
|
|
366
|
+
# Orphan detection
|
|
367
|
+
# ------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
def _find_orphans(self) -> list[OrphanedObject]:
|
|
370
|
+
orphaned: list[OrphanedObject] = list(self._pg_orphans)
|
|
371
|
+
|
|
372
|
+
_INDEXES: list[tuple[str, dict]] = [
|
|
373
|
+
("route_map", self._route_maps),
|
|
374
|
+
("prefix_list", self._prefix_lists),
|
|
375
|
+
("community_list", self._community_lists),
|
|
376
|
+
("as_path_list", self._as_path_lists),
|
|
377
|
+
("acl", self._acls),
|
|
378
|
+
("class_map", self._class_maps),
|
|
379
|
+
("policy_map", self._policy_maps),
|
|
380
|
+
]
|
|
381
|
+
for ref_type, index in _INDEXES:
|
|
382
|
+
referenced = self._referenced[ref_type]
|
|
383
|
+
for name in index:
|
|
384
|
+
if name not in referenced:
|
|
385
|
+
orphaned.append(OrphanedObject(object_type=ref_type, name=name))
|
|
386
|
+
|
|
387
|
+
return orphaned
|
|
388
|
+
|
|
389
|
+
# ------------------------------------------------------------------
|
|
390
|
+
# Helpers
|
|
391
|
+
# ------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
def _link(
|
|
394
|
+
self,
|
|
395
|
+
source_type: str,
|
|
396
|
+
source_id: str,
|
|
397
|
+
source_field: str,
|
|
398
|
+
ref_type: str,
|
|
399
|
+
ref_name: str,
|
|
400
|
+
) -> DependencyLink:
|
|
401
|
+
"""Create a DependencyLink and update orphan-tracking state."""
|
|
402
|
+
# Mark as referenced for orphan tracking (even if dangling)
|
|
403
|
+
if ref_type in self._referenced:
|
|
404
|
+
self._referenced[ref_type].add(ref_name)
|
|
405
|
+
|
|
406
|
+
resolved = self._is_resolved(ref_type, ref_name)
|
|
407
|
+
return DependencyLink(
|
|
408
|
+
source_type=source_type,
|
|
409
|
+
source_id=source_id,
|
|
410
|
+
source_field=source_field,
|
|
411
|
+
ref_type=ref_type,
|
|
412
|
+
ref_name=ref_name,
|
|
413
|
+
resolved=resolved,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def _is_resolved(self, ref_type: str, ref_name: str) -> bool:
|
|
417
|
+
index: dict = {
|
|
418
|
+
"route_map": self._route_maps,
|
|
419
|
+
"prefix_list": self._prefix_lists,
|
|
420
|
+
"community_list": self._community_lists,
|
|
421
|
+
"as_path_list": self._as_path_lists,
|
|
422
|
+
"acl": self._acls,
|
|
423
|
+
"vrf": self._vrfs,
|
|
424
|
+
"interface": self._interfaces,
|
|
425
|
+
"class_map": self._class_maps,
|
|
426
|
+
"policy_map": self._policy_maps,
|
|
427
|
+
}.get(ref_type, {})
|
|
428
|
+
return ref_name in index
|
|
429
|
+
|
|
430
|
+
# ------------------------------------------------------------------
|
|
431
|
+
# EIGRP
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
def _resolve_eigrp(self) -> list[DependencyLink]:
|
|
435
|
+
links: list[DependencyLink] = []
|
|
436
|
+
for eigrp in self._config.eigrp_instances:
|
|
437
|
+
# Node ID must match GraphBuilder's naming: str(as_number)
|
|
438
|
+
eigrp_node_id = str(eigrp.as_number)
|
|
439
|
+
if eigrp.vrf:
|
|
440
|
+
links.append(self._link("eigrp_instance", eigrp_node_id, "vrf", "vrf", eigrp.vrf))
|
|
441
|
+
for redist in eigrp.redistribute:
|
|
442
|
+
if redist.route_map:
|
|
443
|
+
links.append(self._link(
|
|
444
|
+
"eigrp_instance", eigrp_node_id,
|
|
445
|
+
f"redistribute_{redist.protocol}_route_map", "route_map", redist.route_map,
|
|
446
|
+
))
|
|
447
|
+
return links
|
|
448
|
+
|
|
449
|
+
# ------------------------------------------------------------------
|
|
450
|
+
# RIP
|
|
451
|
+
# ------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
def _resolve_rip(self) -> list[DependencyLink]:
|
|
454
|
+
links: list[DependencyLink] = []
|
|
455
|
+
for rip in self._config.rip_instances:
|
|
456
|
+
rip_id = "rip"
|
|
457
|
+
if rip.vrf:
|
|
458
|
+
links.append(self._link("rip_instance", rip_id, "vrf", "vrf", rip.vrf))
|
|
459
|
+
for redist in rip.redistribute:
|
|
460
|
+
if redist.route_map:
|
|
461
|
+
links.append(self._link(
|
|
462
|
+
"rip_instance", rip_id,
|
|
463
|
+
f"redistribute_{redist.protocol}_route_map", "route_map", redist.route_map,
|
|
464
|
+
))
|
|
465
|
+
return links
|
|
466
|
+
|
|
467
|
+
# ------------------------------------------------------------------
|
|
468
|
+
# NTP
|
|
469
|
+
# ------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
def _resolve_ntp(self) -> list[DependencyLink]:
|
|
472
|
+
links: list[DependencyLink] = []
|
|
473
|
+
ntp = self._config.ntp
|
|
474
|
+
if not ntp:
|
|
475
|
+
return links
|
|
476
|
+
for server in ntp.servers + ntp.peers:
|
|
477
|
+
if server.vrf:
|
|
478
|
+
links.append(self._link("ntp", "ntp", "server_vrf", "vrf", server.vrf))
|
|
479
|
+
if server.source:
|
|
480
|
+
links.append(self._link("ntp", "ntp", "server_source", "interface", server.source))
|
|
481
|
+
if ntp.source_interface:
|
|
482
|
+
links.append(self._link("ntp", "ntp", "source_interface", "interface", ntp.source_interface))
|
|
483
|
+
for ag in filter(None, [ntp.access_group_query_only, ntp.access_group_serve_only,
|
|
484
|
+
ntp.access_group_serve, ntp.access_group_peer]):
|
|
485
|
+
links.append(self._link("ntp", "ntp", "access_group", "acl", ag))
|
|
486
|
+
return links
|
|
487
|
+
|
|
488
|
+
# ------------------------------------------------------------------
|
|
489
|
+
# SNMP
|
|
490
|
+
# ------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
def _resolve_snmp(self) -> list[DependencyLink]:
|
|
493
|
+
links: list[DependencyLink] = []
|
|
494
|
+
snmp = self._config.snmp
|
|
495
|
+
if not snmp:
|
|
496
|
+
return links
|
|
497
|
+
for host in snmp.hosts:
|
|
498
|
+
if host.vrf:
|
|
499
|
+
links.append(self._link("snmp", "snmp", "host_vrf", "vrf", host.vrf))
|
|
500
|
+
if snmp.source_interface:
|
|
501
|
+
links.append(self._link("snmp", "snmp", "source_interface", "interface", snmp.source_interface))
|
|
502
|
+
if snmp.trap_source:
|
|
503
|
+
links.append(self._link("snmp", "snmp", "trap_source", "interface", snmp.trap_source))
|
|
504
|
+
return links
|
|
505
|
+
|
|
506
|
+
# ------------------------------------------------------------------
|
|
507
|
+
# Lines
|
|
508
|
+
# ------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
def _resolve_lines(self) -> list[DependencyLink]:
|
|
511
|
+
links: list[DependencyLink] = []
|
|
512
|
+
for line in self._config.lines:
|
|
513
|
+
if line.access_class_in:
|
|
514
|
+
links.append(self._link("lines", "lines", "access_class_in", "acl", line.access_class_in))
|
|
515
|
+
if line.access_class_out:
|
|
516
|
+
links.append(self._link("lines", "lines", "access_class_out", "acl", line.access_class_out))
|
|
517
|
+
return links
|
|
518
|
+
|
|
519
|
+
# ------------------------------------------------------------------
|
|
520
|
+
# QoS
|
|
521
|
+
# ------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
def _resolve_qos(self) -> list[DependencyLink]:
|
|
524
|
+
links: list[DependencyLink] = []
|
|
525
|
+
for cm in self._config.class_maps:
|
|
526
|
+
for match in cm.matches:
|
|
527
|
+
if "access-group" in match.match_type and match.values:
|
|
528
|
+
# "match access-group name <acl>" has "name" as a keyword, not an ACL name
|
|
529
|
+
acl_values = match.values[1:] if match.values[0] == "name" else match.values
|
|
530
|
+
for acl_name in acl_values:
|
|
531
|
+
links.append(self._link("class_map", cm.name, "match_acl", "acl", acl_name))
|
|
532
|
+
for pm in self._config.policy_maps:
|
|
533
|
+
for cls in pm.classes:
|
|
534
|
+
if cls.class_name != "class-default":
|
|
535
|
+
links.append(self._link("policy_map", pm.name, "class", "class_map", cls.class_name))
|
|
536
|
+
if cls.service_policy:
|
|
537
|
+
links.append(self._link(
|
|
538
|
+
"policy_map", pm.name,
|
|
539
|
+
"service_policy", "policy_map", cls.service_policy,
|
|
540
|
+
))
|
|
541
|
+
for iface in self._config.interfaces:
|
|
542
|
+
if iface.service_policy_input:
|
|
543
|
+
links.append(self._link(
|
|
544
|
+
"interface", iface.name, "service_policy_input",
|
|
545
|
+
"policy_map", iface.service_policy_input,
|
|
546
|
+
))
|
|
547
|
+
if iface.service_policy_output:
|
|
548
|
+
links.append(self._link(
|
|
549
|
+
"interface", iface.name, "service_policy_output",
|
|
550
|
+
"policy_map", iface.service_policy_output,
|
|
551
|
+
))
|
|
552
|
+
return links
|
|
553
|
+
|
|
554
|
+
# ------------------------------------------------------------------
|
|
555
|
+
# NAT
|
|
556
|
+
# ------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
def _resolve_nat(self) -> list[DependencyLink]:
|
|
559
|
+
links: list[DependencyLink] = []
|
|
560
|
+
nat = self._config.nat
|
|
561
|
+
if not nat:
|
|
562
|
+
return links
|
|
563
|
+
for de in nat.dynamic_entries:
|
|
564
|
+
links.append(self._link("nat", "nat", "acl", "acl", de.acl))
|
|
565
|
+
if de.interface:
|
|
566
|
+
links.append(self._link("nat", "nat", "interface", "interface", de.interface))
|
|
567
|
+
if de.vrf:
|
|
568
|
+
links.append(self._link("nat", "nat", "vrf", "vrf", de.vrf))
|
|
569
|
+
for se in nat.static_entries:
|
|
570
|
+
if se.vrf:
|
|
571
|
+
links.append(self._link("nat", "nat", "vrf", "vrf", se.vrf))
|
|
572
|
+
return links
|
|
573
|
+
|
|
574
|
+
# ------------------------------------------------------------------
|
|
575
|
+
# Crypto
|
|
576
|
+
# ------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
def _resolve_crypto(self) -> list[DependencyLink]:
|
|
579
|
+
links: list[DependencyLink] = []
|
|
580
|
+
crypto = self._config.crypto
|
|
581
|
+
if not crypto:
|
|
582
|
+
return links
|
|
583
|
+
for cmap in crypto.crypto_maps:
|
|
584
|
+
for entry in cmap.entries:
|
|
585
|
+
if entry.acl:
|
|
586
|
+
links.append(self._link(
|
|
587
|
+
"crypto", "crypto",
|
|
588
|
+
"match_acl", "acl", entry.acl,
|
|
589
|
+
))
|
|
590
|
+
return links
|
|
591
|
+
|
|
592
|
+
# ------------------------------------------------------------------
|
|
593
|
+
# IP SLA
|
|
594
|
+
# ------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
def _resolve_ipsla(self) -> list[DependencyLink]:
|
|
597
|
+
links: list[DependencyLink] = []
|
|
598
|
+
for op in self._config.ip_sla_operations:
|
|
599
|
+
sla_id_str = str(op.sla_id)
|
|
600
|
+
if op.vrf:
|
|
601
|
+
links.append(self._link("ip_sla", sla_id_str, "vrf", "vrf", op.vrf))
|
|
602
|
+
if op.source_interface:
|
|
603
|
+
links.append(self._link("ip_sla", sla_id_str, "source_interface", "interface", op.source_interface))
|
|
604
|
+
return links
|
|
605
|
+
|
|
606
|
+
# ------------------------------------------------------------------
|
|
607
|
+
# Object tracking
|
|
608
|
+
# ------------------------------------------------------------------
|
|
609
|
+
|
|
610
|
+
def _resolve_object_tracking(self) -> list[DependencyLink]:
|
|
611
|
+
links: list[DependencyLink] = []
|
|
612
|
+
for track in self._config.object_tracks:
|
|
613
|
+
track_id_str = str(track.track_id)
|
|
614
|
+
if track.tracked_interface:
|
|
615
|
+
links.append(self._link(
|
|
616
|
+
"object_track", track_id_str,
|
|
617
|
+
"tracked_interface", "interface", track.tracked_interface,
|
|
618
|
+
))
|
|
619
|
+
if track.tracked_sla_id is not None:
|
|
620
|
+
resolved = track.tracked_sla_id in self._ip_sla_ops
|
|
621
|
+
links.append(DependencyLink(
|
|
622
|
+
source_type="object_track",
|
|
623
|
+
source_id=track_id_str,
|
|
624
|
+
source_field="tracked_sla_id",
|
|
625
|
+
ref_type="ip_sla",
|
|
626
|
+
ref_name=str(track.tracked_sla_id),
|
|
627
|
+
resolved=resolved,
|
|
628
|
+
))
|
|
629
|
+
return links
|
|
630
|
+
|
|
631
|
+
# ------------------------------------------------------------------
|
|
632
|
+
# Multicast
|
|
633
|
+
# ------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
def _resolve_multicast(self) -> list[DependencyLink]:
|
|
636
|
+
links: list[DependencyLink] = []
|
|
637
|
+
mc = self._config.multicast
|
|
638
|
+
if not mc:
|
|
639
|
+
return links
|
|
640
|
+
for rp in mc.pim_rp_addresses:
|
|
641
|
+
if rp.acl:
|
|
642
|
+
links.append(self._link("multicast", "multicast", "pim_rp_acl", "acl", rp.acl))
|
|
643
|
+
if mc.pim_ssm_range:
|
|
644
|
+
links.append(self._link("multicast", "multicast", "pim_ssm_range", "acl", mc.pim_ssm_range))
|
|
645
|
+
for peer in mc.msdp_peers:
|
|
646
|
+
if peer.connect_source:
|
|
647
|
+
links.append(self._link(
|
|
648
|
+
"multicast", "multicast",
|
|
649
|
+
"msdp_connect_source", "interface", peer.connect_source,
|
|
650
|
+
))
|
|
651
|
+
if peer.sa_filter_in:
|
|
652
|
+
links.append(self._link(
|
|
653
|
+
"multicast", "multicast", "msdp_sa_filter_in", "acl", peer.sa_filter_in,
|
|
654
|
+
))
|
|
655
|
+
if peer.sa_filter_out:
|
|
656
|
+
links.append(self._link(
|
|
657
|
+
"multicast", "multicast", "msdp_sa_filter_out", "acl", peer.sa_filter_out,
|
|
658
|
+
))
|
|
659
|
+
if mc.vrf:
|
|
660
|
+
links.append(self._link("multicast", "multicast", "vrf", "vrf", mc.vrf))
|
|
661
|
+
return links
|