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.
- package/CHANGELOG.md +53 -2
- package/README.md +443 -124
- package/docs/automation-controller-proof.md +35 -0
- package/docs/campus-donor-proof.md +103 -0
- package/docs/curated-donor-registry.md +37 -0
- package/docs/generate-ready-pilot-design.md +30 -0
- package/docs/github-launch-ops-0.2.1.md +45 -0
- package/docs/github-launch-ops-0.2.2.md +42 -0
- package/docs/github-metadata.md +23 -6
- package/docs/home-iot-donor-proof.md +64 -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/launch-announcement-0.2.2.md +15 -0
- package/docs/packet-tracer-feature-gap-atlas.md +244 -0
- package/docs/post-launch-follow-up.md +39 -0
- package/docs/publish-preview-roadmap.md +20 -17
- package/docs/release-checklist.md +32 -17
- package/docs/release-notes-0.2.1.md +7 -6
- package/docs/release-notes-0.2.2.md +26 -0
- package/docs/release-notes-0.2.3.md +59 -0
- package/docs/runtime-truth.md +31 -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 +58 -0
- package/docs/wireless-advanced-proof.md +60 -0
- package/examples/README.md +11 -8
- package/examples/gallery.md +9 -3
- package/examples/index.json +3 -3
- package/package.json +20 -1
- package/references/curated-donor-registry.json +5 -2
- package/references/packettracer-feature-atlas.json +183 -0
- package/references/scenario-fixture-corpus.json +1 -0
- package/scripts/build_examples_index.py +26 -15
- package/scripts/coverage_matrix.py +1054 -25
- package/scripts/feature_atlas.py +293 -0
- package/scripts/generate_pkt.py +491 -34
- package/scripts/intent_parser.py +798 -8
- package/scripts/pkt_editor.py +651 -0
- package/scripts/remote_search.py +197 -21
- package/scripts/runtime_doctor.py +83 -10
- package/scripts/sample_catalog.py +266 -6
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"):
|
|
@@ -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"]))
|