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.
Files changed (52) hide show
  1. confgraph/analysis/__init__.py +15 -0
  2. confgraph/analysis/dependency_resolver.py +661 -0
  3. confgraph/cli.py +348 -0
  4. confgraph/graph/__init__.py +15 -0
  5. confgraph/graph/assets/cytoscape-dagre.min.js +397 -0
  6. confgraph/graph/assets/cytoscape.min.js +32 -0
  7. confgraph/graph/assets/dagre.min.js +3809 -0
  8. confgraph/graph/builder.py +307 -0
  9. confgraph/graph/exporters/__init__.py +7 -0
  10. confgraph/graph/exporters/base.py +25 -0
  11. confgraph/graph/exporters/html.py +999 -0
  12. confgraph/graph/exporters/json.py +53 -0
  13. confgraph/models/__init__.py +76 -0
  14. confgraph/models/acl.py +141 -0
  15. confgraph/models/banner.py +16 -0
  16. confgraph/models/base.py +67 -0
  17. confgraph/models/bfd.py +42 -0
  18. confgraph/models/bgp.py +385 -0
  19. confgraph/models/community_list.py +68 -0
  20. confgraph/models/crypto.py +98 -0
  21. confgraph/models/eem.py +33 -0
  22. confgraph/models/eigrp.py +56 -0
  23. confgraph/models/interface.py +277 -0
  24. confgraph/models/ipsla.py +47 -0
  25. confgraph/models/isis.py +140 -0
  26. confgraph/models/line.py +44 -0
  27. confgraph/models/logging_config.py +38 -0
  28. confgraph/models/multicast.py +45 -0
  29. confgraph/models/nat.py +66 -0
  30. confgraph/models/ntp.py +46 -0
  31. confgraph/models/object_tracking.py +31 -0
  32. confgraph/models/ospf.py +268 -0
  33. confgraph/models/parsed_config.py +211 -0
  34. confgraph/models/prefix_list.py +51 -0
  35. confgraph/models/qos.py +81 -0
  36. confgraph/models/rip.py +43 -0
  37. confgraph/models/route_map.py +72 -0
  38. confgraph/models/snmp.py +79 -0
  39. confgraph/models/static_route.py +50 -0
  40. confgraph/models/vrf.py +55 -0
  41. confgraph/parsers/__init__.py +16 -0
  42. confgraph/parsers/base.py +536 -0
  43. confgraph/parsers/eos_parser.py +889 -0
  44. confgraph/parsers/ios_parser.py +4167 -0
  45. confgraph/parsers/iosxr_parser.py +1235 -0
  46. confgraph/parsers/nxos_parser.py +359 -0
  47. confgraph-0.1.0.dist-info/METADATA +108 -0
  48. confgraph-0.1.0.dist-info/RECORD +52 -0
  49. confgraph-0.1.0.dist-info/WHEEL +5 -0
  50. confgraph-0.1.0.dist-info/entry_points.txt +2 -0
  51. confgraph-0.1.0.dist-info/licenses/LICENSE +201 -0
  52. 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