packet-tracer-skill 0.2.2 → 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.
- package/CHANGELOG.md +32 -1
- package/README.md +221 -51
- package/docs/automation-controller-proof.md +35 -0
- package/docs/curated-donor-registry.md +11 -0
- package/docs/generate-ready-pilot-design.md +30 -0
- package/docs/industrial-programming-proof.md +48 -0
- package/docs/ipv4-routing-management-proof.md +37 -0
- package/docs/l2-resiliency-bgp-proof.md +60 -0
- package/docs/l2-security-qos-proof.md +59 -0
- package/docs/packet-tracer-feature-gap-atlas.md +172 -15
- package/docs/release-checklist.md +13 -8
- package/docs/release-notes-0.2.2.md +1 -1
- package/docs/release-notes-0.2.3.md +59 -0
- package/docs/security-edge-deepening-proof.md +65 -0
- package/docs/voice-collaboration-proof.md +38 -0
- package/docs/wan-security-donor-proof.md +20 -3
- package/package.json +10 -1
- package/references/packettracer-feature-atlas.json +67 -17
- package/scripts/coverage_matrix.py +505 -12
- package/scripts/feature_atlas.py +65 -1
- package/scripts/generate_pkt.py +161 -3
- package/scripts/intent_parser.py +521 -2
- package/scripts/pkt_editor.py +477 -0
- package/scripts/remote_search.py +197 -21
- package/scripts/sample_catalog.py +57 -2
package/scripts/pkt_editor.py
CHANGED
|
@@ -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"):
|
|
@@ -297,6 +380,60 @@ def inventory_routing(root: ET.Element) -> dict[str, dict[str, object]]:
|
|
|
297
380
|
capabilities.add("ripng")
|
|
298
381
|
if re.search(r"(?mi)^\s*standby\s+\d+\s+ipv6\b", running):
|
|
299
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")
|
|
300
437
|
if capabilities:
|
|
301
438
|
result[name] = {"capabilities": sorted(capabilities)}
|
|
302
439
|
return result
|
|
@@ -335,6 +472,72 @@ def inventory_l2_security_monitoring(root: ET.Element) -> dict[str, dict[str, ob
|
|
|
335
472
|
return result
|
|
336
473
|
|
|
337
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
|
+
|
|
338
541
|
def inventory_topology_summary(root: ET.Element) -> dict[str, object]:
|
|
339
542
|
devices = inventory_devices(root)
|
|
340
543
|
counts: dict[str, int] = {}
|
|
@@ -362,7 +565,11 @@ def inventory_root(root: ET.Element) -> dict[str, object]:
|
|
|
362
565
|
"acl_names": inventory_acl_names(root),
|
|
363
566
|
"management": inventory_management(root),
|
|
364
567
|
"routing": inventory_routing(root),
|
|
568
|
+
"ipv4_routing_management": inventory_ipv4_routing_management(root),
|
|
365
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),
|
|
366
573
|
"topology_summary": inventory_topology_summary(root),
|
|
367
574
|
}
|
|
368
575
|
|
|
@@ -801,6 +1008,66 @@ def _apply_switch_op(device: ET.Element, operation: dict[str, object]) -> None:
|
|
|
801
1008
|
],
|
|
802
1009
|
)
|
|
803
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
|
|
804
1071
|
else:
|
|
805
1072
|
return
|
|
806
1073
|
|
|
@@ -902,6 +1169,74 @@ def _apply_router_op(device: ET.Element, operation: dict[str, object]) -> None:
|
|
|
902
1169
|
_append_unique_config_lines(target, ["ipv6 unicast-routing"])
|
|
903
1170
|
_append_config_block(target, f"interface {operation['interface']}", body)
|
|
904
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
|
|
905
1240
|
elif operation["op"] == "set_snmp_community":
|
|
906
1241
|
for target in _config_targets(device):
|
|
907
1242
|
_append_unique_config_lines(target, [f"snmp-server community {operation['community']} {operation['mode']}"])
|
|
@@ -916,6 +1251,114 @@ def _apply_router_op(device: ET.Element, operation: dict[str, object]) -> None:
|
|
|
916
1251
|
if operation.get("interface") and operation.get("direction"):
|
|
917
1252
|
_append_config_block(target, f"interface {operation['interface']}", [f" ip flow {operation['direction']}"])
|
|
918
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
|
|
919
1362
|
else:
|
|
920
1363
|
return
|
|
921
1364
|
for target in _config_targets(device):
|
|
@@ -1151,6 +1594,39 @@ def _apply_iot_op(device: ET.Element, operation: dict[str, object]) -> None:
|
|
|
1151
1594
|
return
|
|
1152
1595
|
|
|
1153
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
|
+
|
|
1154
1630
|
def apply_plan_operations(root: ET.Element, plan: IntentPlan) -> ET.Element:
|
|
1155
1631
|
updated = copy.deepcopy(root)
|
|
1156
1632
|
port_mem_map = _link_port_mem_map(updated)
|
|
@@ -1199,6 +1675,7 @@ def apply_plan_operations(root: ET.Element, plan: IntentPlan) -> ET.Element:
|
|
|
1199
1675
|
(plan.end_device_ops, _apply_end_device_op),
|
|
1200
1676
|
(plan.management_ops, _apply_management_op),
|
|
1201
1677
|
(plan.iot_ops, _apply_iot_op),
|
|
1678
|
+
(plan.programming_ops, _apply_programming_op),
|
|
1202
1679
|
]:
|
|
1203
1680
|
for operation in bucket:
|
|
1204
1681
|
device = _find_device(updated, str(operation["device"]))
|