packet-tracer-skill 0.1.0
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/LICENSE +21 -0
- package/LICENSES/LICENSE.Twofish-BSD-3-Clause.txt +29 -0
- package/README.md +687 -0
- package/SKILL.md +221 -0
- package/bin/packet-tracer-skill.js +635 -0
- package/examples/blueprint_minimal.json +46 -0
- package/package.json +42 -0
- package/references/packettracer-sample-catalog.json +21410 -0
- package/references/packettracer-sample-catalog.md +1124 -0
- package/references/pkt-format.md +57 -0
- package/references/xml-skeleton-notes.md +44 -0
- package/requirements-dev.txt +1 -0
- package/requirements.txt +6 -0
- package/scripts/build_sample_catalog.py +65 -0
- package/scripts/donor_diagnostics.py +35 -0
- package/scripts/generate_pkt.py +1264 -0
- package/scripts/install_skill.py +71 -0
- package/scripts/intent_parser.py +712 -0
- package/scripts/packet_tracer_env.py +278 -0
- package/scripts/pkt_builder.py +15 -0
- package/scripts/pkt_codec.py +181 -0
- package/scripts/pkt_editor.py +752 -0
- package/scripts/pkt_transformer.py +541 -0
- package/scripts/sample_catalog.py +385 -0
- package/scripts/sample_selector.py +156 -0
- package/scripts/setup.ps1 +26 -0
- package/scripts/twofish_diagnostics.py +91 -0
- package/scripts/vendor/README.md +46 -0
- package/scripts/vendor/twofish.py +81 -0
- package/scripts/workspace_repair.py +441 -0
- package/templates/pt900/base_empty.xml +21 -0
- package/templates/pt900/device_library/pc.xml +20 -0
- package/templates/pt900/device_library/printer.xml +432 -0
- package/templates/pt900/device_library/router.xml +16 -0
- package/templates/pt900/device_library/switch.xml +38 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
import re
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from packet_tracer_env import get_packet_tracer_compatibility_donor, get_packet_tracer_target_version, require_packet_tracer_compatibility_donor, resolve_sample_path
|
|
11
|
+
from pkt_codec import decode_pkt_modern
|
|
12
|
+
from sample_catalog import SampleDescriptor, load_catalog, normalize_device_type
|
|
13
|
+
from workspace_repair import sanitize_generated_physical_workspace, validate_workspace_integrity
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
16
|
+
SKILL_ROOT = SCRIPT_DIR.parent
|
|
17
|
+
TEMPLATE_DIR = SKILL_ROOT / "templates" / "pt900" / "device_library"
|
|
18
|
+
FALLBACK_PROTOTYPE_SAMPLE = r"01 Networking\FTP\FTP.pkt"
|
|
19
|
+
DEVICE_TEMPLATE_FILES = {
|
|
20
|
+
"Printer": "printer.xml",
|
|
21
|
+
}
|
|
22
|
+
GENERIC_COPPER_HOST_TYPES = {"PC", "Server", "Printer", "Laptop"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_sample_root(sample_path: str | Path) -> ET.Element:
|
|
26
|
+
return copy.deepcopy(_load_sample_root_cached(str(sample_path)))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@lru_cache(maxsize=512)
|
|
30
|
+
def _load_sample_root_cached(sample_path: str) -> ET.Element:
|
|
31
|
+
xml_bytes = decode_pkt_modern(Path(sample_path).read_bytes())
|
|
32
|
+
return ET.fromstring(xml_bytes)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@lru_cache(maxsize=1)
|
|
36
|
+
def generation_root_sample() -> str:
|
|
37
|
+
donor = get_packet_tracer_compatibility_donor()
|
|
38
|
+
if donor is not None:
|
|
39
|
+
return str(donor)
|
|
40
|
+
return str(resolve_sample_path(FALLBACK_PROTOTYPE_SAMPLE))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def generation_root_version() -> str:
|
|
44
|
+
donor = get_packet_tracer_compatibility_donor()
|
|
45
|
+
if donor is not None:
|
|
46
|
+
root = _load_sample_root_cached(str(donor))
|
|
47
|
+
return root.findtext("./VERSION", default=get_packet_tracer_target_version())
|
|
48
|
+
return get_packet_tracer_target_version()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def strict_compatibility_mode() -> bool:
|
|
52
|
+
return generation_root_version().startswith("9.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _device_type(device: ET.Element) -> str:
|
|
56
|
+
return normalize_device_type(device.findtext("./ENGINE/TYPE", default=""))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _device_model(device: ET.Element) -> str:
|
|
60
|
+
node = device.find("./ENGINE/TYPE")
|
|
61
|
+
return node.get("model", "") if node is not None else ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _logical_position(device: ET.Element) -> tuple[int, int]:
|
|
65
|
+
x_node = device.find("./WORKSPACE/LOGICAL/X")
|
|
66
|
+
y_node = device.find("./WORKSPACE/LOGICAL/Y")
|
|
67
|
+
x_val = float(x_node.text) if x_node is not None and x_node.text else 200.0
|
|
68
|
+
y_val = float(y_node.text) if y_node is not None and y_node.text else 200.0
|
|
69
|
+
return int(round(x_val)), int(round(y_val))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _set_device_name(device: ET.Element, new_name: str) -> None:
|
|
73
|
+
node = device.find("./ENGINE/NAME")
|
|
74
|
+
if node is not None:
|
|
75
|
+
node.text = new_name
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _set_device_model(device: ET.Element, model: str) -> None:
|
|
79
|
+
node = device.find("./ENGINE/TYPE")
|
|
80
|
+
if node is not None:
|
|
81
|
+
node.set("model", model)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _set_position(device: ET.Element, x_pos: int, y_pos: int) -> None:
|
|
85
|
+
x_node = device.find("./WORKSPACE/LOGICAL/X")
|
|
86
|
+
y_node = device.find("./WORKSPACE/LOGICAL/Y")
|
|
87
|
+
if x_node is not None:
|
|
88
|
+
x_node.text = str(x_pos)
|
|
89
|
+
if y_node is not None:
|
|
90
|
+
y_node.text = str(y_pos)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _set_workspace_ids(device: ET.Element, index: int) -> None:
|
|
94
|
+
mem_node = device.find("./WORKSPACE/LOGICAL/MEM_ADDR")
|
|
95
|
+
dev_node = device.find("./WORKSPACE/LOGICAL/DEV_ADDR")
|
|
96
|
+
if mem_node is not None:
|
|
97
|
+
mem_node.text = str(3_000_000_000_000 + index * 32)
|
|
98
|
+
if dev_node is not None:
|
|
99
|
+
dev_node.text = str(3_100_000_000_000 + index * 32)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _set_runtime_ids(device: ET.Element, index: int) -> None:
|
|
103
|
+
save_ref = device.find("./ENGINE/SAVE_REF_ID")
|
|
104
|
+
serial = device.find("./ENGINE/SERIALNUMBER")
|
|
105
|
+
start_time = device.find("./ENGINE/STARTTIME")
|
|
106
|
+
coord_x = device.find("./ENGINE/COORD_SETTINGS/X_COORD")
|
|
107
|
+
coord_y = device.find("./ENGINE/COORD_SETTINGS/Y_COORD")
|
|
108
|
+
logical_x = device.findtext("./WORKSPACE/LOGICAL/X", default="")
|
|
109
|
+
logical_y = device.findtext("./WORKSPACE/LOGICAL/Y", default="")
|
|
110
|
+
if save_ref is not None:
|
|
111
|
+
save_ref.text = f"save-ref-id:{9_000_000_000_000_000_000 + index}"
|
|
112
|
+
if serial is not None:
|
|
113
|
+
prefix = (serial.text or "AUTO").split("-")[0][:8] or "AUTO"
|
|
114
|
+
serial.text = f"{prefix}{index:04d}-"
|
|
115
|
+
if start_time is not None:
|
|
116
|
+
start_time.text = str(800_000_000_000 + index)
|
|
117
|
+
if coord_x is not None and logical_x:
|
|
118
|
+
coord_x.text = logical_x
|
|
119
|
+
if coord_y is not None and logical_y:
|
|
120
|
+
coord_y.text = logical_y
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _build_workspace_reference_library(root: ET.Element) -> dict[str, list[ET.Element]]:
|
|
124
|
+
buckets: dict[str, list[ET.Element]] = {}
|
|
125
|
+
for device in root.findall(".//DEVICES/DEVICE"):
|
|
126
|
+
workspace = device.find("./WORKSPACE")
|
|
127
|
+
if workspace is None:
|
|
128
|
+
continue
|
|
129
|
+
buckets.setdefault(_device_type(device), []).append(workspace)
|
|
130
|
+
return buckets
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _normalize_workspace_physical(device: ET.Element, requested_type: str, reference_workspaces: dict[str, list[ET.Element]], usage_index: int) -> None:
|
|
134
|
+
candidates = reference_workspaces.get(requested_type, [])
|
|
135
|
+
if not candidates:
|
|
136
|
+
return
|
|
137
|
+
reference = copy.deepcopy(candidates[(usage_index - 1) % len(candidates)])
|
|
138
|
+
target_workspace = device.find("./WORKSPACE")
|
|
139
|
+
if target_workspace is None:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
logical = target_workspace.find("./LOGICAL")
|
|
143
|
+
logical_copy = copy.deepcopy(logical) if logical is not None else None
|
|
144
|
+
target_workspace.clear()
|
|
145
|
+
for child in list(reference):
|
|
146
|
+
target_workspace.append(copy.deepcopy(child))
|
|
147
|
+
if logical_copy is not None:
|
|
148
|
+
logical_target = target_workspace.find("./LOGICAL")
|
|
149
|
+
if logical_target is not None:
|
|
150
|
+
target_workspace.remove(logical_target)
|
|
151
|
+
target_workspace.insert(0, logical_copy)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _find_switch_config(device: ET.Element) -> ET.Element | None:
|
|
155
|
+
for node in device.findall(".//FILE_CONTENT/CONFIG"):
|
|
156
|
+
rendered = "\n".join(line.text or "" for line in node.findall("./LINE"))
|
|
157
|
+
if "interface FastEthernet0/1" in rendered:
|
|
158
|
+
return node
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def apply_host_ip(device: ET.Element, config: dict[str, Any]) -> None:
|
|
163
|
+
port = device.find("./ENGINE/MODULE/SLOT/MODULE/PORT")
|
|
164
|
+
if port is None:
|
|
165
|
+
return
|
|
166
|
+
for source_key, xml_key in [("ip", "IP"), ("mask", "SUBNET")]:
|
|
167
|
+
if source_key in config:
|
|
168
|
+
node = port.find(xml_key)
|
|
169
|
+
if node is not None:
|
|
170
|
+
node.text = str(config[source_key])
|
|
171
|
+
if "gw" in config:
|
|
172
|
+
node = device.find("./ENGINE/GATEWAY")
|
|
173
|
+
if node is not None:
|
|
174
|
+
node.text = str(config["gw"])
|
|
175
|
+
if "dns" in config:
|
|
176
|
+
node = device.find("./ENGINE/DNS_CLIENT/SERVER_IP")
|
|
177
|
+
if node is not None:
|
|
178
|
+
node.text = str(config["dns"])
|
|
179
|
+
port_dns = port.find("PORT_DNS")
|
|
180
|
+
if port_dns is not None:
|
|
181
|
+
port_dns.text = str(config["dns"])
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def apply_router_config(device: ET.Element, lines: list[str]) -> None:
|
|
185
|
+
for target in [device.find("./ENGINE/RUNNINGCONFIG"), device.find("./ENGINE/STARTUPCONFIG")]:
|
|
186
|
+
if target is None:
|
|
187
|
+
continue
|
|
188
|
+
target.clear()
|
|
189
|
+
final_lines = [str(line) for line in lines]
|
|
190
|
+
if final_lines and final_lines[-1].strip().lower() != "end":
|
|
191
|
+
final_lines.append("end")
|
|
192
|
+
for line in final_lines:
|
|
193
|
+
node = ET.SubElement(target, "LINE")
|
|
194
|
+
node.text = line
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def apply_switch_config(device: ET.Element, lines: list[str]) -> None:
|
|
198
|
+
target = _find_switch_config(device)
|
|
199
|
+
if target is None:
|
|
200
|
+
return
|
|
201
|
+
target.clear()
|
|
202
|
+
final_lines = [str(line) for line in lines]
|
|
203
|
+
if final_lines and final_lines[-1].strip().lower() != "end":
|
|
204
|
+
final_lines.append("end")
|
|
205
|
+
for line in final_lines:
|
|
206
|
+
node = ET.SubElement(target, "LINE")
|
|
207
|
+
node.text = line
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_device_library(source_root: ET.Element) -> dict[str, list[ET.Element]]:
|
|
211
|
+
buckets: dict[str, list[ET.Element]] = {}
|
|
212
|
+
for device in source_root.findall(".//DEVICES/DEVICE"):
|
|
213
|
+
buckets.setdefault(_device_type(device), []).append(device)
|
|
214
|
+
return buckets
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _load_device_template(device_type: str, model: str | None = None) -> ET.Element | None:
|
|
218
|
+
normalized = normalize_device_type(device_type)
|
|
219
|
+
template_name = DEVICE_TEMPLATE_FILES.get(normalized)
|
|
220
|
+
if not template_name:
|
|
221
|
+
return None
|
|
222
|
+
template_path = TEMPLATE_DIR / template_name
|
|
223
|
+
if not template_path.exists():
|
|
224
|
+
return None
|
|
225
|
+
root = ET.fromstring(template_path.read_text(encoding="utf-8"))
|
|
226
|
+
actual_type = _device_type(root)
|
|
227
|
+
actual_model = _device_model(root)
|
|
228
|
+
if actual_type != normalized:
|
|
229
|
+
return None
|
|
230
|
+
if model and actual_model.lower() != str(model).lower():
|
|
231
|
+
return None
|
|
232
|
+
return root
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def find_device_prototype(device_type: str, model: str | None, preferred_sample: SampleDescriptor) -> ET.Element:
|
|
236
|
+
return ET.fromstring(_find_device_prototype_xml(device_type, model, preferred_sample.path))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@lru_cache(maxsize=512)
|
|
240
|
+
def _find_device_prototype_xml(device_type: str, model: str | None, preferred_sample_path: str) -> str:
|
|
241
|
+
compatibility_root = generation_root_sample()
|
|
242
|
+
visited_paths: set[str] = set()
|
|
243
|
+
normalized_target = normalize_device_type(device_type)
|
|
244
|
+
wanted_model = (model or "").lower()
|
|
245
|
+
if strict_compatibility_mode():
|
|
246
|
+
candidate_paths = [compatibility_root]
|
|
247
|
+
else:
|
|
248
|
+
candidate_paths = [compatibility_root, preferred_sample_path]
|
|
249
|
+
candidate_paths.extend(sample.path for sample in load_catalog())
|
|
250
|
+
for candidate_path in candidate_paths:
|
|
251
|
+
if candidate_path in visited_paths:
|
|
252
|
+
continue
|
|
253
|
+
visited_paths.add(candidate_path)
|
|
254
|
+
root = _load_sample_root_cached(candidate_path)
|
|
255
|
+
for device in root.findall(".//DEVICES/DEVICE"):
|
|
256
|
+
if _device_type(device) != normalized_target:
|
|
257
|
+
continue
|
|
258
|
+
actual_model = _device_model(device).lower()
|
|
259
|
+
if wanted_model and actual_model != wanted_model:
|
|
260
|
+
continue
|
|
261
|
+
return ET.tostring(device, encoding="unicode")
|
|
262
|
+
if strict_compatibility_mode():
|
|
263
|
+
donor = require_packet_tracer_compatibility_donor()
|
|
264
|
+
target = f"{device_type} with model {model}" if model else device_type
|
|
265
|
+
raise ValueError(f"Strict 9.0 donor {donor} does not contain a prototype for {target}")
|
|
266
|
+
template = _load_device_template(normalized_target, model)
|
|
267
|
+
if template is not None:
|
|
268
|
+
return ET.tostring(template, encoding="unicode")
|
|
269
|
+
if model:
|
|
270
|
+
raise ValueError(f"No prototype found for device type {device_type} with model {model}")
|
|
271
|
+
raise ValueError(f"No prototype found for device type {device_type}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def infer_default_links(devices: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
275
|
+
routers = [device for device in devices if device["type"] == "Router"]
|
|
276
|
+
switches = [device for device in devices if device["type"] == "Switch"]
|
|
277
|
+
hosts = [device for device in devices if device["type"] in {"PC", "Server"}]
|
|
278
|
+
links: list[dict[str, Any]] = []
|
|
279
|
+
if switches:
|
|
280
|
+
uplink_switch = switches[0]
|
|
281
|
+
for index, host in enumerate(hosts, start=1):
|
|
282
|
+
links.append({"a": {"dev": host["name"], "port": "FastEthernet0"}, "b": {"dev": uplink_switch["name"], "port": f"FastEthernet0/{min(index, 24)}"}, "media": "copper"})
|
|
283
|
+
if routers:
|
|
284
|
+
links.append({"a": {"dev": uplink_switch["name"], "port": "FastEthernet0/24"}, "b": {"dev": routers[0]["name"], "port": "FastEthernet0/0"}, "media": "copper"})
|
|
285
|
+
for prev, nxt in zip(switches, switches[1:]):
|
|
286
|
+
links.append({"a": {"dev": prev["name"], "port": "FastEthernet0/23"}, "b": {"dev": nxt["name"], "port": "FastEthernet0/24"}, "media": "copper"})
|
|
287
|
+
elif len(routers) >= 2:
|
|
288
|
+
for prev, nxt in zip(routers, routers[1:]):
|
|
289
|
+
links.append({"a": {"dev": prev["name"], "port": "FastEthernet0/0"}, "b": {"dev": nxt["name"], "port": "FastEthernet0/1"}, "media": "copper"})
|
|
290
|
+
return links
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _effective_link_type(device_type: str) -> str:
|
|
294
|
+
normalized = normalize_device_type(device_type)
|
|
295
|
+
if normalized in GENERIC_COPPER_HOST_TYPES:
|
|
296
|
+
return "PC"
|
|
297
|
+
return normalized
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _prototype_link_by_pair(source_sample_path: str | Path, left_type: str, right_type: str) -> ET.Element:
|
|
301
|
+
return ET.fromstring(_prototype_link_xml(str(source_sample_path), left_type, right_type))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@lru_cache(maxsize=512)
|
|
305
|
+
def _prototype_link_xml(source_sample_path: str, left_type: str, right_type: str) -> str:
|
|
306
|
+
compatibility_root = generation_root_sample()
|
|
307
|
+
if strict_compatibility_mode():
|
|
308
|
+
candidate_paths = [str(compatibility_root)]
|
|
309
|
+
else:
|
|
310
|
+
candidate_paths = [str(compatibility_root), str(source_sample_path), str(resolve_sample_path(FALLBACK_PROTOTYPE_SAMPLE))]
|
|
311
|
+
candidate_paths.extend(sample.path for sample in load_catalog())
|
|
312
|
+
visited_paths: set[str] = set()
|
|
313
|
+
wanted_left = _effective_link_type(left_type)
|
|
314
|
+
wanted_right = _effective_link_type(right_type)
|
|
315
|
+
for candidate in candidate_paths:
|
|
316
|
+
root_key = str(candidate)
|
|
317
|
+
if root_key in visited_paths:
|
|
318
|
+
continue
|
|
319
|
+
visited_paths.add(root_key)
|
|
320
|
+
root = _load_sample_root_cached(root_key)
|
|
321
|
+
devices = root.findall(".//DEVICES/DEVICE")
|
|
322
|
+
index_to_type = {str(index): _effective_link_type(_device_type(device)) for index, device in enumerate(devices)}
|
|
323
|
+
save_ref_to_type = {
|
|
324
|
+
device.findtext("./ENGINE/SAVE_REF_ID", default=""): _effective_link_type(_device_type(device))
|
|
325
|
+
for device in devices
|
|
326
|
+
if device.findtext("./ENGINE/SAVE_REF_ID", default="")
|
|
327
|
+
}
|
|
328
|
+
for link in root.findall(".//LINKS/LINK"):
|
|
329
|
+
cable = link.find("./CABLE")
|
|
330
|
+
if cable is None:
|
|
331
|
+
continue
|
|
332
|
+
from_ref = cable.findtext("FROM", default="")
|
|
333
|
+
to_ref = cable.findtext("TO", default="")
|
|
334
|
+
from_type = save_ref_to_type.get(from_ref) or index_to_type.get(from_ref)
|
|
335
|
+
to_type = save_ref_to_type.get(to_ref) or index_to_type.get(to_ref)
|
|
336
|
+
if from_type == wanted_left and to_type == wanted_right:
|
|
337
|
+
return ET.tostring(link, encoding="unicode")
|
|
338
|
+
if from_type == wanted_right and to_type == wanted_left:
|
|
339
|
+
return ET.tostring(link, encoding="unicode")
|
|
340
|
+
if strict_compatibility_mode():
|
|
341
|
+
donor = require_packet_tracer_compatibility_donor()
|
|
342
|
+
raise ValueError(f"Strict 9.0 donor {donor} does not contain a prototype link for {left_type} <-> {right_type}")
|
|
343
|
+
raise ValueError(f"No prototype link found for type pair {left_type} <-> {right_type}")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def apply_cable_type(cable: ET.Element, media: str) -> None:
|
|
347
|
+
media_key = media.lower()
|
|
348
|
+
node = cable.find("TYPE")
|
|
349
|
+
if node is None:
|
|
350
|
+
return
|
|
351
|
+
mapping = {
|
|
352
|
+
"copper": "eStraightThrough",
|
|
353
|
+
"straight-through": "eStraightThrough",
|
|
354
|
+
"straight": "eStraightThrough",
|
|
355
|
+
"crossover": "eCrossOver",
|
|
356
|
+
"cross-over": "eCrossOver",
|
|
357
|
+
"serial": "eSerialDCE",
|
|
358
|
+
"fiber": "eFiber",
|
|
359
|
+
}
|
|
360
|
+
node.text = mapping.get(media_key, node.text or "eStraightThrough")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _port_nodes(device: ET.Element) -> list[ET.Element]:
|
|
364
|
+
return [port for port in device.findall(".//PORT") if port.findtext("TYPE", "").startswith("eCopper")]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _parse_port_index(port_name: str) -> int | None:
|
|
368
|
+
match = re.search(r"(\d+)$", port_name)
|
|
369
|
+
return int(match.group(1)) if match else None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _canonical_port_name(port_name: str) -> str:
|
|
373
|
+
lowered = port_name.strip()
|
|
374
|
+
if lowered.lower().startswith("fa"):
|
|
375
|
+
return "FastEthernet" + lowered[2:]
|
|
376
|
+
if lowered.lower().startswith("gi"):
|
|
377
|
+
return "GigabitEthernet" + lowered[2:]
|
|
378
|
+
return lowered
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _port_address_for_name(device: ET.Element, port_name: str) -> str | None:
|
|
382
|
+
canonical = _canonical_port_name(port_name)
|
|
383
|
+
fast_nodes = [port for port in _port_nodes(device) if "FastEthernet" in port.findtext("TYPE", "")]
|
|
384
|
+
gig_nodes = [port for port in _port_nodes(device) if "GigabitEthernet" in port.findtext("TYPE", "")]
|
|
385
|
+
target: ET.Element | None = None
|
|
386
|
+
if canonical == "FastEthernet0":
|
|
387
|
+
target = fast_nodes[0] if fast_nodes else None
|
|
388
|
+
elif canonical.startswith("FastEthernet"):
|
|
389
|
+
index = _parse_port_index(canonical)
|
|
390
|
+
if index is not None and fast_nodes:
|
|
391
|
+
if _device_type(device) == "Router":
|
|
392
|
+
target = fast_nodes[index] if index < len(fast_nodes) else None
|
|
393
|
+
else:
|
|
394
|
+
target = fast_nodes[index - 1] if 0 < index <= len(fast_nodes) else None
|
|
395
|
+
elif canonical.startswith("GigabitEthernet"):
|
|
396
|
+
index = _parse_port_index(canonical)
|
|
397
|
+
if index is not None and gig_nodes:
|
|
398
|
+
if _device_type(device) == "Router" and "/" in canonical and canonical.count("/") == 1:
|
|
399
|
+
target = gig_nodes[index] if index < len(gig_nodes) else None
|
|
400
|
+
else:
|
|
401
|
+
target = gig_nodes[index - 1] if 0 < index <= len(gig_nodes) else None
|
|
402
|
+
if target is None:
|
|
403
|
+
return None
|
|
404
|
+
return target.findtext("MEM_ADDR")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _sanitize_generated_runtime_sections(root: ET.Element) -> None:
|
|
408
|
+
scenario_set = root.find("./SCENARIOSET")
|
|
409
|
+
if scenario_set is not None:
|
|
410
|
+
scenario_set.clear()
|
|
411
|
+
scenario = ET.SubElement(scenario_set, "SCENARIO")
|
|
412
|
+
name = ET.SubElement(scenario, "NAME")
|
|
413
|
+
name.set("translate", "true")
|
|
414
|
+
name.text = "Scenario 0"
|
|
415
|
+
description = ET.SubElement(scenario, "DESCRIPTION")
|
|
416
|
+
description.set("translate", "true")
|
|
417
|
+
command_logs = root.find("./COMMAND_LOGS")
|
|
418
|
+
if command_logs is not None:
|
|
419
|
+
command_logs.clear()
|
|
420
|
+
ceps = root.find("./CEPS")
|
|
421
|
+
if ceps is not None:
|
|
422
|
+
ceps.clear()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def transform_from_blueprint(blueprint: dict[str, Any], sample: SampleDescriptor) -> bytes:
|
|
426
|
+
source_root = load_sample_root(sample.path)
|
|
427
|
+
root_sample_path = generation_root_sample()
|
|
428
|
+
target_root = copy.deepcopy(load_sample_root(root_sample_path))
|
|
429
|
+
version_node = target_root.find("./VERSION")
|
|
430
|
+
if version_node is not None:
|
|
431
|
+
version_node.text = generation_root_version()
|
|
432
|
+
devices_parent = target_root.find(".//DEVICES")
|
|
433
|
+
links_parent = target_root.find(".//LINKS")
|
|
434
|
+
if devices_parent is None or links_parent is None:
|
|
435
|
+
raise ValueError("Sample XML is missing DEVICES or LINKS")
|
|
436
|
+
devices_parent.clear()
|
|
437
|
+
links_parent.clear()
|
|
438
|
+
for tag in ["LINES", "RECTANGLES", "ELLIPSES", "POLYGONS", "GEOVIEW_GRAPHICSITEMS", "NOTES"]:
|
|
439
|
+
node = target_root.find(tag)
|
|
440
|
+
if node is not None:
|
|
441
|
+
node.clear()
|
|
442
|
+
compatibility_mode = strict_compatibility_mode()
|
|
443
|
+
library_source_root = load_sample_root(root_sample_path) if compatibility_mode else source_root
|
|
444
|
+
reference_workspaces = _build_workspace_reference_library(load_sample_root(root_sample_path))
|
|
445
|
+
|
|
446
|
+
device_library = build_device_library(library_source_root)
|
|
447
|
+
requested_devices = blueprint.get("devices", [])
|
|
448
|
+
if not requested_devices:
|
|
449
|
+
raise ValueError("Blueprint must include devices")
|
|
450
|
+
requested_links = blueprint.get("links") or infer_default_links(requested_devices)
|
|
451
|
+
configs = blueprint.get("configs", {})
|
|
452
|
+
|
|
453
|
+
built_devices: list[ET.Element] = []
|
|
454
|
+
name_to_device: dict[str, ET.Element] = {}
|
|
455
|
+
name_to_index: dict[str, int] = {}
|
|
456
|
+
type_usage: dict[str, int] = {}
|
|
457
|
+
workspace_usage: dict[str, int] = {}
|
|
458
|
+
|
|
459
|
+
for requested in requested_devices:
|
|
460
|
+
requested_type = normalize_device_type(str(requested["type"]))
|
|
461
|
+
requested_model = requested.get("model")
|
|
462
|
+
prototypes = device_library.get(requested_type, [])
|
|
463
|
+
use_index = type_usage.get(requested_type, 0)
|
|
464
|
+
workspace_index = workspace_usage.get(requested_type, 0) + 1
|
|
465
|
+
workspace_usage[requested_type] = workspace_index
|
|
466
|
+
matching_prototypes = prototypes
|
|
467
|
+
if requested_model:
|
|
468
|
+
matching_prototypes = [prototype for prototype in prototypes if _device_model(prototype).lower() == str(requested_model).lower()]
|
|
469
|
+
if use_index < len(matching_prototypes):
|
|
470
|
+
built = copy.deepcopy(matching_prototypes[use_index])
|
|
471
|
+
type_usage[requested_type] = use_index + 1
|
|
472
|
+
else:
|
|
473
|
+
if compatibility_mode:
|
|
474
|
+
donor = require_packet_tracer_compatibility_donor()
|
|
475
|
+
target = f"{requested_type} with model {requested_model}" if requested_model else requested_type
|
|
476
|
+
raise ValueError(
|
|
477
|
+
f"Strict 9.0 donor {donor} has only {len(matching_prototypes)} prototype(s) for {target}; requested more devices than donor supports."
|
|
478
|
+
)
|
|
479
|
+
if requested_model:
|
|
480
|
+
built = copy.deepcopy(find_device_prototype(requested_type, str(requested_model), sample))
|
|
481
|
+
else:
|
|
482
|
+
built = copy.deepcopy(find_device_prototype(requested_type, None, sample))
|
|
483
|
+
name = str(requested["name"])
|
|
484
|
+
_set_device_name(built, name)
|
|
485
|
+
if requested_model:
|
|
486
|
+
_set_device_model(built, str(requested_model))
|
|
487
|
+
default_x, default_y = _logical_position(built)
|
|
488
|
+
_set_position(built, int(requested.get("x", default_x)), int(requested.get("y", default_y)))
|
|
489
|
+
if not compatibility_mode:
|
|
490
|
+
_normalize_workspace_physical(built, requested_type, reference_workspaces, workspace_index)
|
|
491
|
+
_set_workspace_ids(built, len(built_devices) + 1)
|
|
492
|
+
_set_runtime_ids(built, len(built_devices) + 1)
|
|
493
|
+
device_config = configs.get(name, {})
|
|
494
|
+
if requested_type in {"PC", "Server"} and isinstance(device_config, dict):
|
|
495
|
+
apply_host_ip(built, device_config)
|
|
496
|
+
elif requested_type == "Router" and isinstance(device_config, list):
|
|
497
|
+
apply_router_config(built, device_config)
|
|
498
|
+
elif requested_type == "Switch" and isinstance(device_config, list):
|
|
499
|
+
apply_switch_config(built, device_config)
|
|
500
|
+
name_to_index[name] = len(built_devices)
|
|
501
|
+
name_to_device[name] = built
|
|
502
|
+
built_devices.append(built)
|
|
503
|
+
devices_parent.append(built)
|
|
504
|
+
|
|
505
|
+
requested_lookup = {str(device["name"]): normalize_device_type(str(device["type"])) for device in requested_devices}
|
|
506
|
+
for requested_link in requested_links:
|
|
507
|
+
left_name = str(requested_link["a"]["dev"])
|
|
508
|
+
right_name = str(requested_link["b"]["dev"])
|
|
509
|
+
prototype = copy.deepcopy(_prototype_link_by_pair(sample.path, requested_lookup[left_name], requested_lookup[right_name]))
|
|
510
|
+
cable = prototype.find("./CABLE")
|
|
511
|
+
if cable is None:
|
|
512
|
+
continue
|
|
513
|
+
from_device = name_to_device[left_name]
|
|
514
|
+
to_device = name_to_device[right_name]
|
|
515
|
+
if compatibility_mode:
|
|
516
|
+
cable.find("FROM").text = from_device.findtext("./ENGINE/SAVE_REF_ID", default=str(name_to_index[left_name]))
|
|
517
|
+
cable.find("TO").text = to_device.findtext("./ENGINE/SAVE_REF_ID", default=str(name_to_index[right_name]))
|
|
518
|
+
else:
|
|
519
|
+
cable.find("FROM").text = str(name_to_index[left_name])
|
|
520
|
+
cable.find("TO").text = str(name_to_index[right_name])
|
|
521
|
+
ports = cable.findall("PORT")
|
|
522
|
+
if len(ports) >= 2:
|
|
523
|
+
ports[0].text = str(requested_link["a"]["port"])
|
|
524
|
+
ports[1].text = str(requested_link["b"]["port"])
|
|
525
|
+
mappings = [
|
|
526
|
+
("FROM_DEVICE_MEM_ADDR", from_device.findtext("./WORKSPACE/LOGICAL/MEM_ADDR")),
|
|
527
|
+
("TO_DEVICE_MEM_ADDR", to_device.findtext("./WORKSPACE/LOGICAL/MEM_ADDR")),
|
|
528
|
+
("FROM_PORT_MEM_ADDR", _port_address_for_name(from_device, str(requested_link["a"]["port"]))),
|
|
529
|
+
("TO_PORT_MEM_ADDR", _port_address_for_name(to_device, str(requested_link["b"]["port"]))),
|
|
530
|
+
]
|
|
531
|
+
for node_name, value in mappings:
|
|
532
|
+
node = cable.find(node_name)
|
|
533
|
+
if node is not None and value:
|
|
534
|
+
node.text = value
|
|
535
|
+
apply_cable_type(cable, str(requested_link.get("media", "copper")))
|
|
536
|
+
links_parent.append(prototype)
|
|
537
|
+
|
|
538
|
+
_sanitize_generated_runtime_sections(target_root)
|
|
539
|
+
sanitize_generated_physical_workspace(target_root)
|
|
540
|
+
validate_workspace_integrity(target_root)
|
|
541
|
+
return ET.tostring(target_root, encoding="utf-8", xml_declaration=False)
|