packet-tracer-skill 0.2.1 → 0.2.3

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 (43) hide show
  1. package/CHANGELOG.md +53 -2
  2. package/README.md +443 -124
  3. package/docs/automation-controller-proof.md +35 -0
  4. package/docs/campus-donor-proof.md +103 -0
  5. package/docs/curated-donor-registry.md +37 -0
  6. package/docs/generate-ready-pilot-design.md +30 -0
  7. package/docs/github-launch-ops-0.2.1.md +45 -0
  8. package/docs/github-launch-ops-0.2.2.md +42 -0
  9. package/docs/github-metadata.md +23 -6
  10. package/docs/home-iot-donor-proof.md +64 -0
  11. package/docs/industrial-programming-proof.md +48 -0
  12. package/docs/ipv4-routing-management-proof.md +37 -0
  13. package/docs/l2-resiliency-bgp-proof.md +60 -0
  14. package/docs/l2-security-qos-proof.md +59 -0
  15. package/docs/launch-announcement-0.2.2.md +15 -0
  16. package/docs/packet-tracer-feature-gap-atlas.md +244 -0
  17. package/docs/post-launch-follow-up.md +39 -0
  18. package/docs/publish-preview-roadmap.md +20 -17
  19. package/docs/release-checklist.md +32 -17
  20. package/docs/release-notes-0.2.1.md +7 -6
  21. package/docs/release-notes-0.2.2.md +26 -0
  22. package/docs/release-notes-0.2.3.md +59 -0
  23. package/docs/runtime-truth.md +31 -0
  24. package/docs/security-edge-deepening-proof.md +65 -0
  25. package/docs/voice-collaboration-proof.md +38 -0
  26. package/docs/wan-security-donor-proof.md +58 -0
  27. package/docs/wireless-advanced-proof.md +60 -0
  28. package/examples/README.md +11 -8
  29. package/examples/gallery.md +9 -3
  30. package/examples/index.json +3 -3
  31. package/package.json +20 -1
  32. package/references/curated-donor-registry.json +5 -2
  33. package/references/packettracer-feature-atlas.json +183 -0
  34. package/references/scenario-fixture-corpus.json +1 -0
  35. package/scripts/build_examples_index.py +26 -15
  36. package/scripts/coverage_matrix.py +1054 -25
  37. package/scripts/feature_atlas.py +293 -0
  38. package/scripts/generate_pkt.py +491 -34
  39. package/scripts/intent_parser.py +798 -8
  40. package/scripts/pkt_editor.py +651 -0
  41. package/scripts/remote_search.py +197 -21
  42. package/scripts/runtime_doctor.py +83 -10
  43. package/scripts/sample_catalog.py +266 -6
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import copy
4
+ import hashlib
4
5
  import json
5
6
  import re
6
7
  from pathlib import Path
@@ -189,6 +190,88 @@ def inventory_iot(root: ET.Element) -> dict[str, dict[str, object]]:
189
190
  return result
190
191
 
191
192
 
193
+ def _script_language(app_name: str, file_name: str, content: str) -> str:
194
+ lowered = " ".join([app_name.lower(), file_name.lower(), content[:500].lower()])
195
+ if file_name.lower().endswith(".py") or "python" in lowered:
196
+ return "python"
197
+ if file_name.lower().endswith(".js") or "javascript" in lowered:
198
+ return "javascript"
199
+ if file_name.lower().endswith(".visual") or "<xml" in lowered or "blockly" in lowered:
200
+ return "visual"
201
+ return "unknown"
202
+
203
+
204
+ def _script_feature_tags(app_name: str, file_name: str, content: str) -> list[str]:
205
+ lowered = " ".join([app_name.lower(), file_name.lower(), content.lower()])
206
+ tags: set[str] = set()
207
+ if "mqtt" in lowered:
208
+ tags.add("mqtt")
209
+ if "realhttp" in lowered or "real http" in lowered:
210
+ tags.add("real_http")
211
+ if "realws" in lowered or "websocket" in lowered:
212
+ tags.add("real_websocket")
213
+ language = _script_language(app_name, file_name, content)
214
+ if language == "python":
215
+ tags.add("python_programming")
216
+ if language == "javascript":
217
+ tags.add("javascript_programming")
218
+ if language == "visual":
219
+ tags.add("blockly_programming")
220
+ if "tcp" in lowered or "udp" in lowered:
221
+ tags.add("tcp_udp_app")
222
+ return sorted(tags)
223
+
224
+
225
+ def _file_text(file_node: ET.Element) -> str:
226
+ return file_node.findtext("./FILE_CONTENT/TEXT", default="")
227
+
228
+
229
+ def inventory_programming(root: ET.Element) -> dict[str, dict[str, object]]:
230
+ result: dict[str, dict[str, object]] = {}
231
+ for device in root.findall(".//DEVICES/DEVICE"):
232
+ device_name = device.findtext("./ENGINE/NAME", default="")
233
+ apps: list[dict[str, object]] = []
234
+ for directory in device.findall(".//FILE[@class='CDirectory']"):
235
+ app_name = directory.findtext("NAME", default="").strip()
236
+ if not app_name:
237
+ continue
238
+ files: list[dict[str, object]] = []
239
+ app_tags: set[str] = set()
240
+ for file_node in directory.findall(".//FILE[@class='CFile']"):
241
+ file_name = file_node.findtext("NAME", default="").strip()
242
+ content = _file_text(file_node)
243
+ if not file_name or not content:
244
+ continue
245
+ language = _script_language(app_name, file_name, content)
246
+ feature_tags = _script_feature_tags(app_name, file_name, content)
247
+ app_tags.update(feature_tags)
248
+ files.append(
249
+ {
250
+ "name": file_name,
251
+ "language": language,
252
+ "content_length": len(content),
253
+ "content_sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
254
+ "feature_tags": feature_tags,
255
+ }
256
+ )
257
+ if files:
258
+ apps.append(
259
+ {
260
+ "app_name": app_name,
261
+ "file_count": len(files),
262
+ "feature_tags": sorted(app_tags),
263
+ "files": files,
264
+ }
265
+ )
266
+ if apps:
267
+ result[device_name] = {
268
+ "app_count": len(apps),
269
+ "feature_tags": sorted({tag for app in apps for tag in list(app.get("feature_tags", []))}),
270
+ "apps": apps,
271
+ }
272
+ return result
273
+
274
+
192
275
  def inventory_vlans(root: ET.Element) -> dict[str, list[dict[str, str]]]:
193
276
  result: dict[str, list[dict[str, str]]] = {}
194
277
  for device in root.findall(".//DEVICES/DEVICE"):
@@ -277,6 +360,184 @@ def inventory_management(root: ET.Element) -> dict[str, dict[str, object]]:
277
360
  return result
278
361
 
279
362
 
363
+ def inventory_routing(root: ET.Element) -> dict[str, dict[str, object]]:
364
+ result: dict[str, dict[str, object]] = {}
365
+ for device in root.findall(".//DEVICES/DEVICE"):
366
+ name = device.findtext("./ENGINE/NAME", default="")
367
+ running = "\n".join(line.text or "" for line in device.findall("./ENGINE/RUNNINGCONFIG/LINE"))
368
+ if not running:
369
+ continue
370
+ capabilities: set[str] = set()
371
+ if re.search(r"(?mi)^\s*ipv6\s+unicast-routing\s*$", running) or re.search(r"(?mi)^\s*ipv6\s+(?:enable|address)\b", running):
372
+ capabilities.add("ipv6_slaac")
373
+ if re.search(r"(?mi)^\s*ipv6\s+dhcp\s+pool\b", running) or re.search(r"(?mi)^\s*ipv6\s+dhcp\s+server\b", running):
374
+ capabilities.add("dhcpv6_stateful")
375
+ if re.search(r"(?mi)^\s*ipv6\s+ospf\b", running) or re.search(r"(?mi)^\s*ipv6\s+router\s+ospf\b", running):
376
+ capabilities.add("ospfv3")
377
+ if re.search(r"(?mi)^\s*ipv6\s+eigrp\b", running) or re.search(r"(?mi)^\s*ipv6\s+router\s+eigrp\b", running):
378
+ capabilities.add("eigrp_ipv6")
379
+ if re.search(r"(?mi)^\s*ipv6\s+rip\b", running):
380
+ capabilities.add("ripng")
381
+ if re.search(r"(?mi)^\s*standby\s+\d+\s+ipv6\b", running):
382
+ capabilities.add("hsrp")
383
+ if re.search(r"(?mi)^\s*interface\s+Tunnel\d+\b", running) or re.search(r"(?mi)^\s*tunnel\s+(?:source|destination|mode\s+gre)\b", running):
384
+ capabilities.add("gre")
385
+ if re.search(r"(?mi)^\s*encapsulation\s+ppp\b", running):
386
+ capabilities.add("ppp")
387
+ if re.search(r"(?mi)^\s*crypto\s+ipsec\s+transform-set\b", running) or re.search(r"(?mi)^\s*crypto\s+map\b", running):
388
+ capabilities.add("ipsec")
389
+ if re.search(r"(?mi)^\s*crypto\s+map\b", running):
390
+ capabilities.add("vpn")
391
+ if re.search(r"(?mi)^\s*ip\s+inspect\s+name\b", running):
392
+ capabilities.add("cbac")
393
+ if (
394
+ re.search(r"(?mi)^zone\s+security\b", running)
395
+ or re.search(r"(?mi)^zone-pair\s+security\b", running)
396
+ or re.search(r"(?mi)^class-map\s+type\s+inspect\b", running)
397
+ or re.search(r"(?mi)^policy-map\s+type\s+inspect\b", running)
398
+ ):
399
+ capabilities.add("zfw")
400
+ if capabilities:
401
+ result[name] = {"capabilities": sorted(capabilities)}
402
+ return result
403
+
404
+
405
+ def inventory_ipv4_routing_management(root: ET.Element) -> dict[str, dict[str, object]]:
406
+ result: dict[str, dict[str, object]] = {}
407
+ for device in root.findall(".//DEVICES/DEVICE"):
408
+ name = device.findtext("./ENGINE/NAME", default="")
409
+ running = "\n".join(line.text or "" for line in device.findall("./ENGINE/RUNNINGCONFIG/LINE"))
410
+ if not running:
411
+ continue
412
+ capabilities: set[str] = set()
413
+ if re.search(r"(?mi)^\s*router\s+ospf\s+\d+\s*$", running):
414
+ capabilities.add("ospfv2")
415
+ if re.search(r"(?mi)^\s*router\s+eigrp\s+\d+\s*$", running):
416
+ capabilities.add("eigrp_ipv4")
417
+ if re.search(r"(?mi)^\s*router\s+rip\s*$", running):
418
+ capabilities.add("ripv2")
419
+ if re.search(r"(?mi)^\s*ip\s+route\s+\S+\s+\S+\s+\S+", running):
420
+ capabilities.add("static_route")
421
+ if re.search(r"(?mi)^\s*ip\s+route\s+0\.0\.0\.0\s+0\.0\.0\.0\s+\S+", running):
422
+ capabilities.add("default_route")
423
+ if re.search(r"(?mi)^\s*ip\s+helper-address\s+\d+\.\d+\.\d+\.\d+\s*$", running):
424
+ capabilities.add("dhcp_relay")
425
+ if re.search(r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+static\b", running):
426
+ capabilities.update({"nat_static", "nat"})
427
+ if re.search(r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+(?:list|route-map)\b", running):
428
+ capabilities.update({"nat_dynamic", "nat"})
429
+ if re.search(r"(?mi)^\s*ip\s+nat\s+inside\s+source\s+list\s+\S+\s+interface\s+\S+\s+overload\b", running):
430
+ capabilities.update({"pat", "nat"})
431
+ if re.search(r"(?mi)^\s*ip\s+ssh\b", running) or re.search(r"(?mi)^\s*crypto\s+key\s+generate\s+rsa\b", running):
432
+ capabilities.add("ssh_ios")
433
+ if re.search(r"(?mi)^\s*ntp\s+server\s+\d+\.\d+\.\d+\.\d+\s*$", running):
434
+ capabilities.add("ntp_ios")
435
+ if re.search(r"(?mi)^\s*logging\s+host\s+\d+\.\d+\.\d+\.\d+\s*$", running):
436
+ capabilities.add("syslog_ios")
437
+ if capabilities:
438
+ result[name] = {"capabilities": sorted(capabilities)}
439
+ return result
440
+
441
+
442
+ def inventory_l2_security_monitoring(root: ET.Element) -> dict[str, dict[str, object]]:
443
+ result: dict[str, dict[str, object]] = {}
444
+ for device in root.findall(".//DEVICES/DEVICE"):
445
+ name = device.findtext("./ENGINE/NAME", default="")
446
+ running = "\n".join(line.text or "" for line in device.findall("./ENGINE/RUNNINGCONFIG/LINE"))
447
+ if not running:
448
+ continue
449
+ capabilities: set[str] = set()
450
+ if re.search(r"(?mi)^\s*ip\s+dhcp\s+snooping\b", running):
451
+ capabilities.add("dhcp_snooping")
452
+ if re.search(r"(?mi)^\s*ip\s+arp\s+inspection\b", running):
453
+ capabilities.add("dai")
454
+ if re.search(r"(?mi)^\s*(?:aaa\s+new-model|radius-server\s+host|authentication\s+port-control|dot1x\b)", running):
455
+ capabilities.add("dot1x")
456
+ if re.search(r"(?mi)^\s*lldp\s+run\b", running):
457
+ capabilities.add("lldp")
458
+ if re.search(r"(?mi)^\s*rep\s+segment\b", running):
459
+ capabilities.add("rep")
460
+ if re.search(r"(?mi)^\s*snmp-server\b", running):
461
+ capabilities.add("snmp")
462
+ if re.search(r"(?mi)^\s*ip\s+flow-export\b", running) or re.search(r"(?mi)^\s*ip\s+flow\s+(?:ingress|egress)\b", running):
463
+ capabilities.add("netflow")
464
+ if re.search(r"(?mi)^\s*monitor\s+session\b", running):
465
+ capabilities.add("span")
466
+ if re.search(r"(?mi)^\s*(?:mls\s+qos|auto\s+qos|class-map|policy-map|service-policy)\b", running):
467
+ capabilities.add("qos")
468
+ if re.search(r"(?mi)^\s*switchport\s+port-security\b", running):
469
+ capabilities.add("port_security")
470
+ if capabilities:
471
+ result[name] = {"capabilities": sorted(capabilities)}
472
+ return result
473
+
474
+
475
+ def inventory_l2_resiliency_routing(root: ET.Element) -> dict[str, dict[str, object]]:
476
+ result: dict[str, dict[str, object]] = {}
477
+ for device in root.findall(".//DEVICES/DEVICE"):
478
+ name = device.findtext("./ENGINE/NAME", default="")
479
+ running = "\n".join(line.text or "" for line in device.findall("./ENGINE/RUNNINGCONFIG/LINE"))
480
+ if not running:
481
+ continue
482
+ capabilities: set[str] = set()
483
+ if re.search(r"(?mi)^\s*router\s+bgp\s+\d+\s*$", running) or re.search(r"(?mi)^\s*neighbor\s+\S+\s+remote-as\s+\d+\s*$", running):
484
+ capabilities.add("bgp")
485
+ if re.search(r"(?mi)^\s*spanning-tree\b", running):
486
+ capabilities.add("stp")
487
+ if re.search(r"(?mi)^\s*spanning-tree\s+mode\s+(?:rapid-pvst|rstp)\b", running) or re.search(r"(?mi)\brapid-pvst\b", running):
488
+ capabilities.add("rstp")
489
+ if re.search(r"(?mi)^\s*interface\s+Port-channel\d+\b", running) or re.search(r"(?mi)^\s*channel-group\s+\d+\s+mode\b", running):
490
+ capabilities.add("etherchannel")
491
+ if re.search(r"(?mi)^\s*channel-group\s+\d+\s+mode\s+(?:active|passive)\b", running):
492
+ capabilities.add("lacp")
493
+ if re.search(r"(?mi)^\s*channel-group\s+\d+\s+mode\s+(?:desirable|auto)\b", running):
494
+ capabilities.add("pagp")
495
+ if re.search(r"(?mi)^\s*vtp\s+(?:domain|mode|version)\b", running):
496
+ capabilities.add("vtp")
497
+ if re.search(r"(?mi)^\s*switchport\s+mode\s+dynamic\s+(?:desirable|auto)\b", running) or re.search(r"(?mi)^\s*switchport\s+nonegotiate\b", running):
498
+ capabilities.add("dtp")
499
+ if capabilities:
500
+ result[name] = {"capabilities": sorted(capabilities)}
501
+ return result
502
+
503
+
504
+ def inventory_voice(root: ET.Element) -> dict[str, dict[str, object]]:
505
+ result: dict[str, dict[str, object]] = {}
506
+ for device in root.findall(".//DEVICES/DEVICE"):
507
+ name = device.findtext("./ENGINE/NAME", default="")
508
+ device_type = _device_type(device)
509
+ model = device.find("./ENGINE/TYPE").get("model", "") if device.find("./ENGINE/TYPE") is not None else ""
510
+ running = "\n".join(line.text or "" for line in device.findall("./ENGINE/RUNNINGCONFIG/LINE"))
511
+ capabilities: set[str] = set()
512
+ details: dict[str, object] = {"device_type": device_type, "model": model}
513
+ if device_type in {"IpPhone", "HomeVoip", "AnalogPhone"}:
514
+ capabilities.add("voip")
515
+ if device_type == "IpPhone":
516
+ capabilities.add("ip_phone")
517
+ if running:
518
+ extensions = sorted(dict.fromkeys(re.findall(r"(?mi)^\s*number\s+([A-Za-z0-9*+#.-]+)\s*$", running)))
519
+ ephones = sorted(dict.fromkeys(re.findall(r"(?mi)^ephone\s+(\d+)\s*$", running)), key=int)
520
+ dial_peers = sorted(dict.fromkeys(re.findall(r"(?mi)^dial-peer\s+voice\s+(\d+)\b", running)), key=int)
521
+ source_match = re.search(r"(?mi)^\s*ip\s+source-address\s+(\d+\.\d+\.\d+\.\d+)\s+port\s+(\d+)\s*$", running)
522
+ if re.search(r"(?mi)^telephony-service\s*$", running) or extensions or ephones:
523
+ capabilities.update({"voip", "call_manager", "ip_phone"})
524
+ if dial_peers:
525
+ capabilities.add("voip")
526
+ if extensions:
527
+ details["extensions"] = extensions
528
+ if ephones:
529
+ details["ephones"] = ephones
530
+ if dial_peers:
531
+ details["dial_peers"] = dial_peers
532
+ if source_match:
533
+ details["source_address"] = source_match.group(1)
534
+ details["source_port"] = int(source_match.group(2))
535
+ if capabilities:
536
+ details["capabilities"] = sorted(capabilities)
537
+ result[name] = details
538
+ return result
539
+
540
+
280
541
  def inventory_topology_summary(root: ET.Element) -> dict[str, object]:
281
542
  devices = inventory_devices(root)
282
543
  counts: dict[str, int] = {}
@@ -303,6 +564,12 @@ def inventory_root(root: ET.Element) -> dict[str, object]:
303
564
  "dhcp_pools": inventory_dhcp_pools(root),
304
565
  "acl_names": inventory_acl_names(root),
305
566
  "management": inventory_management(root),
567
+ "routing": inventory_routing(root),
568
+ "ipv4_routing_management": inventory_ipv4_routing_management(root),
569
+ "l2_security_monitoring": inventory_l2_security_monitoring(root),
570
+ "l2_resiliency_routing": inventory_l2_resiliency_routing(root),
571
+ "voice": inventory_voice(root),
572
+ "programming": inventory_programming(root),
306
573
  "topology_summary": inventory_topology_summary(root),
307
574
  }
308
575
 
@@ -702,6 +969,105 @@ def _apply_switch_op(device: ET.Element, operation: dict[str, object]) -> None:
702
969
  for target in _config_targets(device):
703
970
  _append_config_block(target, f"interface {operation['port']}", body)
704
971
  return
972
+ elif operation["op"] == "set_dhcp_snooping":
973
+ for target in _config_targets(device):
974
+ _append_unique_config_lines(target, ["ip dhcp snooping", f"ip dhcp snooping vlan {operation['vlan']}"])
975
+ if operation.get("trust_port"):
976
+ _append_config_block(target, f"interface {operation['trust_port']}", [" ip dhcp snooping trust"])
977
+ return
978
+ elif operation["op"] == "set_dai":
979
+ for target in _config_targets(device):
980
+ _append_unique_config_lines(target, ["ip dhcp snooping", f"ip dhcp snooping vlan {operation['vlan']}", f"ip arp inspection vlan {operation['vlan']}"])
981
+ if operation.get("trust_port"):
982
+ _append_config_block(target, f"interface {operation['trust_port']}", [" ip arp inspection trust", " ip dhcp snooping trust"])
983
+ return
984
+ elif operation["op"] == "set_port_security":
985
+ body = [" switchport mode access", " switchport port-security"]
986
+ if operation.get("maximum"):
987
+ body.append(f" switchport port-security maximum {operation['maximum']}")
988
+ if operation.get("violation"):
989
+ body.append(f" switchport port-security violation {operation['violation']}")
990
+ for target in _config_targets(device):
991
+ _append_config_block(target, f"interface {operation['port']}", body)
992
+ return
993
+ elif operation["op"] == "set_lldp":
994
+ for target in _config_targets(device):
995
+ _append_unique_config_lines(target, ["lldp run"])
996
+ return
997
+ elif operation["op"] == "set_rep":
998
+ for target in _config_targets(device):
999
+ _append_config_block(target, f"interface {operation['interface']}", [f" rep segment {operation['segment']}"])
1000
+ return
1001
+ elif operation["op"] == "set_span":
1002
+ for target in _config_targets(device):
1003
+ _append_unique_config_lines(
1004
+ target,
1005
+ [
1006
+ f"monitor session {operation['session']} source interface {operation['source']}",
1007
+ f"monitor session {operation['session']} destination interface {operation['destination']}",
1008
+ ],
1009
+ )
1010
+ return
1011
+ elif operation["op"] == "set_dot1x":
1012
+ global_lines = ["aaa new-model", "dot1x system-auth-control"]
1013
+ if operation.get("radius_host") and operation.get("radius_key"):
1014
+ global_lines.append(f"radius-server host {operation['radius_host']} key {operation['radius_key']}")
1015
+ mode = str(operation.get("mode") or "auto")
1016
+ for target in _config_targets(device):
1017
+ _append_unique_config_lines(target, global_lines)
1018
+ _append_config_block(
1019
+ target,
1020
+ f"interface {operation['interface']}",
1021
+ [f" authentication port-control {mode}", " dot1x pae authenticator"],
1022
+ )
1023
+ return
1024
+ elif operation["op"] == "set_qos_policy":
1025
+ action = str(operation.get("action") or "priority")
1026
+ for target in _config_targets(device):
1027
+ _append_unique_config_lines(target, ["mls qos"])
1028
+ _append_config_block(
1029
+ target,
1030
+ f"class-map match-any {operation['class_map']}",
1031
+ [f" match {operation['match']}"],
1032
+ )
1033
+ _append_config_block(
1034
+ target,
1035
+ f"policy-map {operation['policy_map']}",
1036
+ [f" class {operation['class_map']}", f" {action}"],
1037
+ )
1038
+ _append_config_block(
1039
+ target,
1040
+ f"interface {operation['interface']}",
1041
+ [f" service-policy {operation['direction']} {operation['policy_map']}"],
1042
+ )
1043
+ return
1044
+ elif operation["op"] == "set_stp":
1045
+ lines = [f"spanning-tree mode {operation['mode']}"]
1046
+ if operation.get("vlan") and operation.get("root"):
1047
+ lines.append(f"spanning-tree vlan {operation['vlan']} root {operation['root']}")
1048
+ for target in _config_targets(device):
1049
+ _append_unique_config_lines(target, lines)
1050
+ return
1051
+ elif operation["op"] == "set_etherchannel":
1052
+ channel = int(operation["channel"])
1053
+ mode = str(operation.get("mode") or "active")
1054
+ interfaces = [str(interface) for interface in operation.get("interfaces", []) if str(interface).strip()]
1055
+ for target in _config_targets(device):
1056
+ for interface_name in interfaces:
1057
+ _append_config_block(target, f"interface {interface_name}", [f" channel-group {channel} mode {mode}"])
1058
+ _append_config_block(target, f"interface Port-channel{channel}", [" no shutdown"])
1059
+ return
1060
+ elif operation["op"] == "set_vtp":
1061
+ lines = [f"vtp domain {operation['domain']}", f"vtp mode {operation['mode']}"]
1062
+ if operation.get("version"):
1063
+ lines.append(f"vtp version {operation['version']}")
1064
+ for target in _config_targets(device):
1065
+ _append_unique_config_lines(target, lines)
1066
+ return
1067
+ elif operation["op"] == "set_dtp":
1068
+ for target in _config_targets(device):
1069
+ _append_config_block(target, f"interface {operation['interface']}", [f" switchport mode {operation['mode']}"])
1070
+ return
705
1071
  else:
706
1072
  return
707
1073
 
@@ -742,6 +1108,257 @@ def _apply_router_op(device: ET.Element, operation: dict[str, object]) -> None:
742
1108
  for target in _config_targets(device):
743
1109
  _append_config_block(target, f"interface {operation['interface']}", [f" ip access-group {operation['acl_name']} {operation['direction']}"])
744
1110
  return
1111
+ elif operation["op"] == "enable_ipv6_unicast_routing":
1112
+ for target in _config_targets(device):
1113
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
1114
+ return
1115
+ elif operation["op"] == "set_ipv6_address":
1116
+ for target in _config_targets(device):
1117
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
1118
+ _append_config_block(
1119
+ target,
1120
+ f"interface {operation['interface']}",
1121
+ [f" ipv6 address {operation['address']}/{operation['prefix']}", " no shutdown"],
1122
+ )
1123
+ return
1124
+ elif operation["op"] == "set_ipv6_slaac":
1125
+ body = [" ipv6 enable", " no shutdown"]
1126
+ if operation.get("prefix") and operation.get("prefix_len"):
1127
+ body.insert(1, f" ipv6 nd prefix {operation['prefix']}/{operation['prefix_len']}")
1128
+ for target in _config_targets(device):
1129
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
1130
+ _append_config_block(target, f"interface {operation['interface']}", body)
1131
+ return
1132
+ elif operation["op"] == "set_dhcpv6_pool":
1133
+ pool_body = [f" address prefix {operation['prefix']}/{operation['prefix_len']}"]
1134
+ if operation.get("dns"):
1135
+ pool_body.append(f" dns-server {operation['dns']}")
1136
+ if operation.get("domain"):
1137
+ pool_body.append(f" domain-name {operation['domain']}")
1138
+ interface_body = [f" ipv6 dhcp server {operation['name']}", " ipv6 nd managed-config-flag", " no shutdown"]
1139
+ for target in _config_targets(device):
1140
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
1141
+ _append_config_block(target, f"ipv6 dhcp pool {operation['name']}", pool_body)
1142
+ _append_config_block(target, f"interface {operation['interface']}", interface_body)
1143
+ return
1144
+ elif operation["op"] == "set_ospfv3_interface":
1145
+ process_id = operation["process_id"]
1146
+ for target in _config_targets(device):
1147
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
1148
+ _append_config_block(target, f"interface {operation['interface']}", [f" ipv6 ospf {process_id} area {operation['area']}", " no shutdown"])
1149
+ _append_config_block(target, f"ipv6 router ospf {process_id}", [])
1150
+ return
1151
+ elif operation["op"] == "set_eigrp_ipv6_interface":
1152
+ asn = operation["asn"]
1153
+ for target in _config_targets(device):
1154
+ _append_unique_config_lines(target, ["ipv6 unicast-routing", "no ipv6 cef"])
1155
+ _append_config_block(target, f"interface {operation['interface']}", [f" ipv6 eigrp {asn}", " no shutdown"])
1156
+ _append_config_block(target, f"ipv6 router eigrp {asn}", [" no shutdown"])
1157
+ return
1158
+ elif operation["op"] == "set_ripng_interface":
1159
+ for target in _config_targets(device):
1160
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
1161
+ _append_config_block(target, f"interface {operation['interface']}", [f" ipv6 rip {operation['process_name']} enable", " no shutdown"])
1162
+ return
1163
+ elif operation["op"] == "set_hsrp_ipv6":
1164
+ body = [" standby version 2", f" standby {operation['group']} ipv6 {operation['virtual_ipv6']}"]
1165
+ if operation.get("priority"):
1166
+ body.append(f" standby {operation['group']} priority {operation['priority']}")
1167
+ body.append(f" standby {operation['group']} preempt")
1168
+ for target in _config_targets(device):
1169
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
1170
+ _append_config_block(target, f"interface {operation['interface']}", body)
1171
+ return
1172
+ elif operation["op"] == "set_ospfv2_network":
1173
+ for target in _config_targets(device):
1174
+ _append_config_block(
1175
+ target,
1176
+ f"router ospf {operation['process_id']}",
1177
+ [f" network {operation['network']} {operation['wildcard']} area {operation['area']}"],
1178
+ )
1179
+ return
1180
+ elif operation["op"] == "set_eigrp_ipv4_network":
1181
+ body = [f" network {operation['network']} {operation['wildcard']}"]
1182
+ if operation.get("no_auto_summary"):
1183
+ body.append(" no auto-summary")
1184
+ for target in _config_targets(device):
1185
+ _append_config_block(target, f"router eigrp {operation['asn']}", body)
1186
+ return
1187
+ elif operation["op"] == "set_ripv2_network":
1188
+ body = [" version 2", f" network {operation['network']}"]
1189
+ if operation.get("no_auto_summary"):
1190
+ body.append(" no auto-summary")
1191
+ for target in _config_targets(device):
1192
+ _append_config_block(target, "router rip", body)
1193
+ return
1194
+ elif operation["op"] == "set_static_route":
1195
+ for target in _config_targets(device):
1196
+ _append_unique_config_lines(target, [f"ip route {operation['network']} {_prefix_to_mask(int(operation['prefix']))} {operation['next_hop']}"])
1197
+ return
1198
+ elif operation["op"] == "set_dhcp_relay":
1199
+ for target in _config_targets(device):
1200
+ _append_config_block(target, f"interface {operation['interface']}", [f" ip helper-address {operation['helper']}"])
1201
+ return
1202
+ elif operation["op"] == "set_nat_interface":
1203
+ for target in _config_targets(device):
1204
+ _append_config_block(target, f"interface {operation['interface']}", [f" ip nat {operation['role']}"])
1205
+ return
1206
+ elif operation["op"] == "set_nat_static":
1207
+ for target in _config_targets(device):
1208
+ _append_unique_config_lines(target, [f"ip nat inside source static {operation['inside_local']} {operation['inside_global']}"])
1209
+ return
1210
+ elif operation["op"] == "set_pat_overload":
1211
+ suffix = " overload" if operation.get("overload") else ""
1212
+ for target in _config_targets(device):
1213
+ _append_unique_config_lines(target, [f"ip nat inside source list {operation['acl']} interface {operation['interface']}{suffix}"])
1214
+ return
1215
+ elif operation["op"] == "set_ssh_ios":
1216
+ lines = [
1217
+ f"ip domain-name {operation['domain']}",
1218
+ f"username {operation['username']} password {operation['password']}",
1219
+ f"crypto key generate rsa modulus {operation['modulus']}",
1220
+ "ip ssh version 2",
1221
+ ]
1222
+ for target in _config_targets(device):
1223
+ _append_unique_config_lines(target, lines)
1224
+ return
1225
+ elif operation["op"] == "set_ntp_server":
1226
+ for target in _config_targets(device):
1227
+ _append_unique_config_lines(target, [f"ntp server {operation['server']}"])
1228
+ return
1229
+ elif operation["op"] == "set_syslog_server":
1230
+ for target in _config_targets(device):
1231
+ _append_unique_config_lines(target, [f"logging host {operation['server']}"])
1232
+ return
1233
+ elif operation["op"] == "set_bgp_neighbor":
1234
+ body = [f" neighbor {operation['neighbor']} remote-as {operation['remote_as']}"]
1235
+ if operation.get("network") and operation.get("mask"):
1236
+ body.append(f" network {operation['network']} mask {operation['mask']}")
1237
+ for target in _config_targets(device):
1238
+ _append_config_block(target, f"router bgp {operation['asn']}", body)
1239
+ return
1240
+ elif operation["op"] == "set_snmp_community":
1241
+ for target in _config_targets(device):
1242
+ _append_unique_config_lines(target, [f"snmp-server community {operation['community']} {operation['mode']}"])
1243
+ return
1244
+ elif operation["op"] == "set_netflow":
1245
+ global_lines = [
1246
+ f"ip flow-export destination {operation['destination']} {operation['port']}",
1247
+ f"ip flow-export version {operation['version']}",
1248
+ ]
1249
+ for target in _config_targets(device):
1250
+ _append_unique_config_lines(target, global_lines)
1251
+ if operation.get("interface") and operation.get("direction"):
1252
+ _append_config_block(target, f"interface {operation['interface']}", [f" ip flow {operation['direction']}"])
1253
+ return
1254
+ elif operation["op"] == "set_gre_tunnel":
1255
+ body = []
1256
+ if operation.get("ip") and operation.get("prefix"):
1257
+ body.append(f" ip address {operation['ip']} {_prefix_to_mask(int(operation['prefix']))}")
1258
+ body.extend(
1259
+ [
1260
+ f" tunnel source {operation['source']}",
1261
+ f" tunnel destination {operation['destination']}",
1262
+ " tunnel mode gre ip",
1263
+ " no shutdown",
1264
+ ]
1265
+ )
1266
+ for target in _config_targets(device):
1267
+ _append_config_block(target, f"interface {operation['interface']}", body)
1268
+ return
1269
+ elif operation["op"] == "set_ppp_interface":
1270
+ body = [" encapsulation ppp"]
1271
+ if operation.get("authentication"):
1272
+ body.append(f" ppp authentication {operation['authentication']}")
1273
+ body.append(" no shutdown")
1274
+ for target in _config_targets(device):
1275
+ _append_config_block(target, f"interface {operation['interface']}", body)
1276
+ return
1277
+ elif operation["op"] == "set_ipsec_transform_set":
1278
+ for target in _config_targets(device):
1279
+ _append_unique_config_lines(
1280
+ target,
1281
+ [f"crypto ipsec transform-set {operation['name']} {operation['encryption']} {operation['integrity']}"],
1282
+ )
1283
+ return
1284
+ elif operation["op"] == "set_crypto_map":
1285
+ for target in _config_targets(device):
1286
+ _append_config_block(
1287
+ target,
1288
+ f"crypto map {operation['map_name']} {operation['sequence']} ipsec-isakmp",
1289
+ [
1290
+ f" set peer {operation['peer']}",
1291
+ f" set transform-set {operation['transform_set']}",
1292
+ f" match address {operation['acl_name']}",
1293
+ ],
1294
+ )
1295
+ if operation.get("interface"):
1296
+ _append_config_block(target, f"interface {operation['interface']}", [f" crypto map {operation['map_name']}"])
1297
+ return
1298
+ elif operation["op"] == "set_cbac_inspect":
1299
+ for target in _config_targets(device):
1300
+ _append_unique_config_lines(target, [f"ip inspect name {operation['name']} {operation['protocol']}"])
1301
+ _append_config_block(target, f"interface {operation['interface']}", [f" ip inspect {operation['name']} {operation['direction']}"])
1302
+ return
1303
+ elif operation["op"] == "set_zfw_zone_interface":
1304
+ for target in _config_targets(device):
1305
+ _append_config_block(target, f"zone security {operation['zone']}", [])
1306
+ _append_config_block(target, f"interface {operation['interface']}", [f" zone-member security {operation['zone']}"])
1307
+ return
1308
+ elif operation["op"] == "set_zfw_zone_pair":
1309
+ for target in _config_targets(device):
1310
+ _append_config_block(
1311
+ target,
1312
+ f"zone-pair security {operation['pair_name']} source {operation['source']} destination {operation['destination']}",
1313
+ [f" service-policy type inspect {operation['policy']}"],
1314
+ )
1315
+ return
1316
+ elif operation["op"] == "set_zfw_policy":
1317
+ for target in _config_targets(device):
1318
+ _append_config_block(
1319
+ target,
1320
+ f"class-map type inspect match-any {operation['class_map']}",
1321
+ [f" match protocol {operation['protocol']}"],
1322
+ )
1323
+ _append_config_block(
1324
+ target,
1325
+ f"policy-map type inspect {operation['policy_map']}",
1326
+ [f" class type inspect {operation['class_map']}", f" {operation['action']}"],
1327
+ )
1328
+ return
1329
+ elif operation["op"] == "set_telephony_service":
1330
+ body = [" no auto-reg-ephone"]
1331
+ if operation.get("max_ephones"):
1332
+ body.append(f" max-ephones {operation['max_ephones']}")
1333
+ if operation.get("max_dn"):
1334
+ body.append(f" max-dn {operation['max_dn']}")
1335
+ body.append(f" ip source-address {operation['source_address']} port {operation['port']}")
1336
+ for target in _config_targets(device):
1337
+ _append_config_block(target, "telephony-service", body)
1338
+ return
1339
+ elif operation["op"] == "set_ephone_dn":
1340
+ for target in _config_targets(device):
1341
+ _append_config_block(target, f"ephone-dn {operation['dn_id']}", [f" number {operation['number']}"])
1342
+ return
1343
+ elif operation["op"] == "set_ephone":
1344
+ for target in _config_targets(device):
1345
+ _append_config_block(
1346
+ target,
1347
+ f"ephone {operation['ephone_id']}",
1348
+ [f" mac-address {operation['mac']}", f" button {operation['button']}"],
1349
+ )
1350
+ return
1351
+ elif operation["op"] == "set_dial_peer_voice":
1352
+ for target in _config_targets(device):
1353
+ _append_config_block(
1354
+ target,
1355
+ f"dial-peer voice {operation['peer_id']} voip",
1356
+ [
1357
+ f" destination-pattern {operation['destination_pattern']}",
1358
+ f" session target ipv4:{operation['session_target']}",
1359
+ ],
1360
+ )
1361
+ return
745
1362
  else:
746
1363
  return
747
1364
  for target in _config_targets(device):
@@ -977,6 +1594,39 @@ def _apply_iot_op(device: ET.Element, operation: dict[str, object]) -> None:
977
1594
  return
978
1595
 
979
1596
 
1597
+ def _find_script_file(device: ET.Element, app_name: str, file_name: str) -> ET.Element:
1598
+ app_matches = [
1599
+ directory
1600
+ for directory in device.findall(".//FILE[@class='CDirectory']")
1601
+ if directory.findtext("NAME", default="").strip() == app_name
1602
+ ]
1603
+ if not app_matches:
1604
+ device_name = device.findtext("./ENGINE/NAME", default="")
1605
+ raise ValueError(f"Script app {app_name!r} was not found on device {device_name!r}.")
1606
+ file_matches: list[ET.Element] = []
1607
+ for directory in app_matches:
1608
+ file_matches.extend(
1609
+ file_node
1610
+ for file_node in directory.findall(".//FILE[@class='CFile']")
1611
+ if file_node.findtext("NAME", default="").strip() == file_name
1612
+ )
1613
+ if not file_matches:
1614
+ raise ValueError(f"Script file {file_name!r} was not found in app {app_name!r}.")
1615
+ if len(file_matches) > 1:
1616
+ raise ValueError(f"Script file {file_name!r} in app {app_name!r} is ambiguous.")
1617
+ return file_matches[0]
1618
+
1619
+
1620
+ def _apply_programming_op(device: ET.Element, operation: dict[str, object]) -> None:
1621
+ if operation["op"] != "set_script_file_content":
1622
+ return
1623
+ file_node = _find_script_file(device, str(operation["app_name"]), str(operation["file_name"]))
1624
+ content_node = file_node.find("FILE_CONTENT")
1625
+ if content_node is None:
1626
+ content_node = ET.SubElement(file_node, "FILE_CONTENT", {"class": "CTextFileContent"})
1627
+ _ensure_text(content_node, "TEXT", str(operation["content"]))
1628
+
1629
+
980
1630
  def apply_plan_operations(root: ET.Element, plan: IntentPlan) -> ET.Element:
981
1631
  updated = copy.deepcopy(root)
982
1632
  port_mem_map = _link_port_mem_map(updated)
@@ -1025,6 +1675,7 @@ def apply_plan_operations(root: ET.Element, plan: IntentPlan) -> ET.Element:
1025
1675
  (plan.end_device_ops, _apply_end_device_op),
1026
1676
  (plan.management_ops, _apply_management_op),
1027
1677
  (plan.iot_ops, _apply_iot_op),
1678
+ (plan.programming_ops, _apply_programming_op),
1028
1679
  ]:
1029
1680
  for operation in bucket:
1030
1681
  device = _find_device(updated, str(operation["device"]))