packet-tracer-skill 0.2.3 → 0.3.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/CHANGELOG.md +424 -73
- package/README.md +557 -442
- package/SKILL.md +337 -262
- package/bin/packet-tracer-skill.js +29 -2
- package/docs/github-launch-ops-0.2.3.md +37 -0
- package/docs/github-metadata.md +6 -4
- package/docs/hero-demo-plan.md +1 -1
- package/docs/home-iot-donor-proof.md +4 -4
- package/docs/l2-security-qos-proof.md +1 -1
- package/docs/packet-tracer-feature-gap-atlas.md +4 -4
- package/docs/post-launch-follow-up.md +9 -5
- package/docs/proof-readiness-dashboard.md +69 -0
- package/docs/publish-preview-roadmap.md +6 -5
- package/docs/release-checklist.md +17 -8
- package/docs/release-notes-0.2.4.md +20 -0
- package/docs/runtime-truth.md +33 -8
- package/docs/security-edge-deepening-proof.md +1 -1
- package/examples/README.md +98 -69
- package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
- package/examples/gallery.md +94 -6
- package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
- package/examples/index.json +932 -4
- package/examples/local-sample-evidence.json +24 -0
- package/examples/proof-cards.json +117 -0
- package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
- package/package.json +60 -53
- package/pytest.ini +9 -0
- package/references/packettracer-sample-catalog.json +45287 -4525
- package/references/packettracer-sample-catalog.md +599 -259
- package/references/proof-readiness-candidates.json +352 -0
- package/scripts/build_examples_index.py +228 -35
- package/scripts/build_sample_catalog.py +24 -44
- package/scripts/corpus_runner.py +430 -0
- package/scripts/coverage_matrix.py +1842 -1812
- package/scripts/donor_cache.py +354 -0
- package/scripts/donor_diagnostics.py +3 -1
- package/scripts/generate_pkt.py +8762 -4228
- package/scripts/intent_parser.py +2242 -1657
- package/scripts/local_donors.py +340 -0
- package/scripts/packet_tracer_env.py +846 -391
- package/scripts/pkt_annotate.py +218 -0
- package/scripts/pkt_codec.py +420 -181
- package/scripts/pkt_editor.py +2405 -1703
- package/scripts/pkt_transformer.py +1072 -727
- package/scripts/pkt_verify.py +461 -0
- package/scripts/runtime_doctor.py +80 -29
- package/scripts/sample_catalog.py +1372 -1250
- package/scripts/twofish_diagnostics.py +48 -31
- package/scripts/usage_ledger.py +218 -0
- package/scripts/vendor/README.md +44 -37
- package/scripts/vendor/twofish_pure.py +321 -0
- package/scripts/workspace_repair.py +548 -508
- package/templates/pt900/donors/README.md +15 -0
package/scripts/intent_parser.py
CHANGED
|
@@ -1,1657 +1,2242 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"
|
|
229
|
-
"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
) -> str
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
)
|
|
541
|
-
for
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
)
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
)
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
)
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
)
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
)
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
"
|
|
914
|
-
"
|
|
915
|
-
"
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
for
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
for device,
|
|
1019
|
-
ops.append(
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
"
|
|
1032
|
-
"
|
|
1033
|
-
"
|
|
1034
|
-
"
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
for device,
|
|
1077
|
-
ops.append(
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
"
|
|
1084
|
-
"
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
"
|
|
1135
|
-
"
|
|
1136
|
-
"
|
|
1137
|
-
"
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
r"(
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
)
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
.
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
r
|
|
1287
|
-
flags=re.IGNORECASE
|
|
1288
|
-
)
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
re.
|
|
1326
|
-
|
|
1327
|
-
)
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
for
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
if
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import unicodedata
|
|
7
|
+
from dataclasses import asdict, dataclass, field
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CAPABILITY_PATTERNS = {
|
|
12
|
+
"vlan": [r"\bvlan(?:larda|lar|da|de|a|e)?\b", r"\btrunk\b", r"\baccess port\b", r"\brouter-on-a-stick\b", r"\bdot1q\b"],
|
|
13
|
+
"trunk": [r"\btrunk\b", r"\ballowed vlan\b", r"\bnative vlan\b"],
|
|
14
|
+
"access_port": [r"\baccess port\b", r"\baccess-port\b"],
|
|
15
|
+
"router_on_a_stick": [r"\brouter-on-a-stick\b", r"\bsubinterface\b", r"\bdot1q\b"],
|
|
16
|
+
"dhcp_pool": [r"\bdhcp\b", r"\bdhcp pool\b"],
|
|
17
|
+
"router_dhcp": [r"\bdhcp\b", r"\bdhcp pool\b", r"\bdefault-router\b"],
|
|
18
|
+
"server_dhcp": [r"\bserver dhcp\b", r"\bdhcp server\b"],
|
|
19
|
+
"dns": [r"\bdns\b", r"\ba record\b", r"\bcname\b"],
|
|
20
|
+
"server_dns": [r"\bdns\b", r"\ba record\b", r"\bcname\b"],
|
|
21
|
+
"server_http": [r"\bhttp\b", r"\bweb\b"],
|
|
22
|
+
"server_ftp": [r"\bftp\b", r"\btftp\b"],
|
|
23
|
+
"server_https": [r"\bhttps\b"],
|
|
24
|
+
"server_tftp": [r"\btftp\b"],
|
|
25
|
+
"server_email": [r"\bemail\b", r"\bsmtp\b", r"\bpop3\b"],
|
|
26
|
+
"server_syslog": [r"\bsyslog\b"],
|
|
27
|
+
"server_aaa": [r"\baaa\b", r"\bacs\b", r"\bradius\b"],
|
|
28
|
+
"iot": [r"\biot\b", r"\bhome gateway\b", r"\bsmart home\b", r"\bsensor\b", r"\bactuator\b", r"\bmcu\b"],
|
|
29
|
+
"iot_registration": [r"\biot registration\b", r"\bregister\b", r"\bregistration server\b"],
|
|
30
|
+
"iot_control": [r"\biot control\b", r"\bcontrol\b", r"\btrigger\b", r"\bturn on\b", r"\bturn off\b", r"\block\b", r"\bunlock\b"],
|
|
31
|
+
"management_vlan": [r"\bmanagement vlan\b", r"\bvlan 99\b", r"\bdefault-gateway\b"],
|
|
32
|
+
"telnet": [r"\btelnet\b", r"\bvty\b", r"\btransport input telnet\b"],
|
|
33
|
+
"wireless_ap": [r"\bssid\b", r"\bwifi\b", r"\bwpa\b", r"\bwpa2\b", r"\bwireless\b", r"\baccess point\b", r"\bap\b"],
|
|
34
|
+
"wireless_mutation": [r"\bset\s+[A-Za-z0-9_ -]+?\s+ssid\b", r"\bsecurity\s+(?:wpa|wpa2|wpa-enterprise|wpa2-enterprise|wep|open|802\.1x)\b", r"\bpassphrase\b", r"\bchannel\s+\d+\b"],
|
|
35
|
+
"wireless_client": [r"\btablet\b", r"\blaptop\b", r"\bsmartphone\b", r"\bassociate\b", r"\bjoin\b"],
|
|
36
|
+
"wireless_client_association": [r"\bassociate\b", r"\bjoin\b"],
|
|
37
|
+
"tablet": [r"\btablet\b"],
|
|
38
|
+
"printer": [r"\bprinter\b"],
|
|
39
|
+
"end_device_mutation": [r"\bset\s+[A-Za-z0-9_-]+\s+ip\b", r"\bset\s+[A-Za-z0-9_-]+\s+ipv4\s+dhcp\b", r"\bset\s+[A-Za-z0-9_-]+\s+dns\b"],
|
|
40
|
+
"verification": [r"\bshow vlan brief\b", r"\bshow interfaces trunk\b", r"\bshow ip interface brief\b", r"\bshow ip dhcp binding\b", r"\bping\b", r"\btelnet\b"],
|
|
41
|
+
"ospf": [r"\bospf\b"],
|
|
42
|
+
"eigrp": [r"\beigrp\b"],
|
|
43
|
+
"rip": [r"\brip\b"],
|
|
44
|
+
"ospfv2": [r"\bospfv2\b", r"\bospf v2\b", r"\bsingle-area ospf\b", r"\brouter ospf\b"],
|
|
45
|
+
"eigrp_ipv4": [r"\beigrp ipv4\b", r"\bipv4 eigrp\b", r"\brouter eigrp\b"],
|
|
46
|
+
"ripv2": [r"\bripv2\b", r"\brip v2\b", r"\brip version 2\b", r"\brouter rip\b"],
|
|
47
|
+
"static_route": [r"\bstatic route\b", r"\bstatic-route\b", r"\bip route\b"],
|
|
48
|
+
"default_route": [r"\bdefault route\b", r"\bdefault-route\b", r"\b0\.0\.0\.0/0\b"],
|
|
49
|
+
"dhcp_relay": [r"\bdhcp relay\b", r"\bdhcp-relay\b", r"\bhelper-address\b", r"\bip helper\b"],
|
|
50
|
+
"nat_static": [r"\bstatic nat\b", r"\bnat static\b"],
|
|
51
|
+
"nat_dynamic": [r"\bdynamic nat\b", r"\bnat dynamic\b"],
|
|
52
|
+
"pat": [r"\bpat\b", r"\boverload\b"],
|
|
53
|
+
"ssh_ios": [r"\bssh\b", r"\bip ssh\b", r"\bcrypto key\b"],
|
|
54
|
+
"ntp_ios": [r"\bntp\b", r"\bntp server\b"],
|
|
55
|
+
"syslog_ios": [r"\bsyslog\b", r"\blogging host\b"],
|
|
56
|
+
# Plain `ipv6` was only ever a network-style tag, never a capability, so
|
|
57
|
+
# "ipv6 olsun" produced nothing for the planner to act on.
|
|
58
|
+
"ipv6": [r"\bipv6\b", r"\bdual[- ]?stack\b"],
|
|
59
|
+
# Without this every OSPF lab was single-area, whatever the prompt asked.
|
|
60
|
+
"ospf_multiarea": [r"\bmulti[- ]?area\b", r"\barea\s+[1-9]\d*\b", r"\bcoxsaheli\b"],
|
|
61
|
+
"ipv6_slaac": [r"\bslaac\b", r"\bipv6\s+slaac\b"],
|
|
62
|
+
"dhcpv6_stateful": [r"\bdhcpv6\b", r"\bstateful dhcpv6\b", r"\bipv6 stateful\b"],
|
|
63
|
+
"dhcpv6_stateless": [r"\bstateless dhcpv6\b", r"\bipv6 stateless\b"],
|
|
64
|
+
"ipv6_prefix_delegation": [r"\bprefix delegation\b", r"\bipv6 prefix\b"],
|
|
65
|
+
"ipv6_dns_aaaa": [r"\baaaa\b", r"\bipv6 dns\b"],
|
|
66
|
+
"ipv6_tunneling": [r"\bipv6ip\b", r"\bipv6 tunnel\b", r"\bipv6 tunneling\b"],
|
|
67
|
+
"isatap": [r"\bisatap\b"],
|
|
68
|
+
"ospfv3": [r"\bospfv3\b", r"\bipv6 ospf\b"],
|
|
69
|
+
"eigrp_ipv6": [r"\bipv6 eigrp\b", r"\beigrp ipv6\b"],
|
|
70
|
+
"ripng": [r"\bripng\b", r"\bipv6 rip\b"],
|
|
71
|
+
"hsrp": [r"\bhsrp\b"],
|
|
72
|
+
"nat": [r"\bnat\b"],
|
|
73
|
+
"acl": [r"\bacl\b", r"\baccess-list\b"],
|
|
74
|
+
"dhcp_snooping": [r"\bdhcp snooping\b"],
|
|
75
|
+
"dai": [r"\bdai\b", r"\bdynamic arp inspection\b"],
|
|
76
|
+
"dot1x": [r"\bdot1x\b", r"\b802\.1x\b", r"\bnac\b"],
|
|
77
|
+
"lldp": [r"\blldp\b"],
|
|
78
|
+
"rep": [r"\brep\b"],
|
|
79
|
+
"snmp": [r"\bsnmp\b"],
|
|
80
|
+
"netflow": [r"\bnetflow\b"],
|
|
81
|
+
"span": [r"\bmonitor\s+session\b", r"\bspan\b", r"\brspan\b"],
|
|
82
|
+
"qos": [r"\bqos policy\b", r"\bqos class\b", r"\bclass-map\b", r"\bpolicy-map\b", r"\bquality of service\b"],
|
|
83
|
+
"port_security": [r"\bport security\b", r"\bport-security\b"],
|
|
84
|
+
"bgp": [r"\bbgp\b", r"\brouter bgp\b", r"\bremote-as\b"],
|
|
85
|
+
"stp": [r"\bstp\b", r"\bspanning tree\b", r"\bspanning-tree\b", r"\bpvst\b"],
|
|
86
|
+
"rstp": [r"\brstp\b", r"\brapid-pvst\b", r"\brapid pvst\b", r"\brapid spanning\b"],
|
|
87
|
+
"etherchannel": [r"\betherchannel\b", r"\bport-channel\b", r"\bchannel-group\b"],
|
|
88
|
+
"lacp": [r"\blacp\b"],
|
|
89
|
+
"pagp": [r"\bpagp\b"],
|
|
90
|
+
"vtp": [r"\bvtp\b"],
|
|
91
|
+
"dtp": [r"\bdtp\b", r"\bdynamic desirable\b", r"\bdynamic auto\b"],
|
|
92
|
+
"vpn": [r"\bvpn\b", r"\bsite-to-site\b", r"\bipsec\b", r"\btunnel\b"],
|
|
93
|
+
"ipsec": [r"\bipsec\b", r"\bike\b", r"\bphase 1\b", r"\bphase 2\b"],
|
|
94
|
+
"gre": [r"\bgre\b", r"\bgre tunnel\b"],
|
|
95
|
+
"ppp": [r"\bppp\b", r"\bchap\b", r"\bpap\b"],
|
|
96
|
+
# `_add_wan_link` gates the router-to-router link on a `wan` capability that
|
|
97
|
+
# nothing ever produced, so "routerler arasinda serial kabel olsun" came out
|
|
98
|
+
# with the two routers not connected to each other at all -- six copper
|
|
99
|
+
# links and no serial one. A prompt naming serial, WAN or a leased line is
|
|
100
|
+
# asking for exactly that link.
|
|
101
|
+
"wan": [
|
|
102
|
+
r"\bwan\b", r"\bserial\b", r"\bleased line\b",
|
|
103
|
+
r"\bpoint-to-point\b", r"\bnoqteden noqteye\b",
|
|
104
|
+
],
|
|
105
|
+
"security_edge": [r"\bsecurity edge\b", r"\bsecurity appliance\b", r"\bfirewall\b", r"\basa\b"],
|
|
106
|
+
"asa_acl_nat": [r"\basa acl\b", r"\basa nat\b", r"\basa acl nat\b"],
|
|
107
|
+
"asa_service_policy": [r"\basa service policy\b", r"\bservice policy\b"],
|
|
108
|
+
"clientless_vpn": [r"\bclientless vpn\b"],
|
|
109
|
+
"cbac": [r"\bcbac\b"],
|
|
110
|
+
"zfw": [r"\bzfw\b", r"\bzone based firewall\b", r"\bzone-based firewall\b"],
|
|
111
|
+
"sniffer": [r"\bsniffer\b"],
|
|
112
|
+
"multilayer_switching": [r"\bmultilayer switch\b", r"\blayer 3 switch\b", r"\b3560\b", r"\bsvi\b"],
|
|
113
|
+
"wlc": [r"\bwlc\b", r"\bwireless lan controller\b"],
|
|
114
|
+
"wpa_enterprise": [r"\bwpa enterprise\b", r"\bwpa2 enterprise\b", r"\bwpa-enterprise\b", r"\bwpa2-enterprise\b", r"\b802\.1x wireless\b"],
|
|
115
|
+
"wep": [r"\bwep\b"],
|
|
116
|
+
"guest_wifi": [r"\bguest wifi\b", r"\bguest wlan\b"],
|
|
117
|
+
"beamforming": [r"\bbeamforming\b"],
|
|
118
|
+
"meraki": [r"\bmeraki\b"],
|
|
119
|
+
"cellular_5g": [r"\b5g\b", r"\bcellular\b", r"\bcell tower\b"],
|
|
120
|
+
"bluetooth": [r"\bbluetooth\b"],
|
|
121
|
+
"network_controller": [r"\bnetwork controller\b", r"\bnetcon\b"],
|
|
122
|
+
"python_programming": [r"\bpython programming\b", r"\bpython app\b"],
|
|
123
|
+
"javascript_programming": [r"\bjavascript\b", r"\bjs app\b"],
|
|
124
|
+
"blockly_programming": [r"\bblockly\b"],
|
|
125
|
+
"tcp_udp_app": [r"\btcp test app\b", r"\budp test app\b", r"\btcp app\b", r"\budp app\b"],
|
|
126
|
+
"vm_iox": [r"\biox\b", r"\bvm\b", r"\bvirtual machine\b"],
|
|
127
|
+
"voip": [r"\bvoip\b"],
|
|
128
|
+
"ip_phone": [r"\bip phone\b", r"\bphone\b"],
|
|
129
|
+
"call_manager": [r"\bcall manager\b", r"\btelephony\b"],
|
|
130
|
+
"linksys_voice": [r"\blinksys voice\b"],
|
|
131
|
+
"mqtt": [r"\bmqtt\b"],
|
|
132
|
+
"real_http": [r"\breal http\b"],
|
|
133
|
+
"real_websocket": [r"\bwebsocket\b", r"\breal websocket\b"],
|
|
134
|
+
"visual_scripting": [r"\bvisual scripting\b"],
|
|
135
|
+
"ptp": [r"\bptp\b", r"\bprecision time protocol\b"],
|
|
136
|
+
"profinet": [r"\bprofinet\b"],
|
|
137
|
+
"l2nat": [r"\bl2nat\b", r"\bl2 nat\b"],
|
|
138
|
+
"cyberobserver": [r"\bcyberobserver\b"],
|
|
139
|
+
"industrial_firewall": [r"\bindustrial firewall\b", r"\bisa3000\b"],
|
|
140
|
+
"coaxial": [r"\bcoaxial\b"],
|
|
141
|
+
"cable_dsl": [r"\bcable modem\b", r"\bdsl\b", r"\bdsl modem\b"],
|
|
142
|
+
"central_office": [r"\bcentral office\b"],
|
|
143
|
+
"cell_tower": [r"\bcell tower\b"],
|
|
144
|
+
"power_distribution": [r"\bpower distribution\b"],
|
|
145
|
+
"hot_swappable": [r"\bhot swappable\b", r"\bhot-swappable\b"],
|
|
146
|
+
"ios_license": [r"\bios license\b", r"\blicense\b", r"\bios15\b", r"\bios 15\b"],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
DEVICE_SYNONYMS = {
|
|
150
|
+
"router": "Router",
|
|
151
|
+
"switch": "Switch",
|
|
152
|
+
"pc": "PC",
|
|
153
|
+
"server": "Server",
|
|
154
|
+
"access-point": "LightWeightAccessPoint",
|
|
155
|
+
"accesspoint": "LightWeightAccessPoint",
|
|
156
|
+
"ap": "LightWeightAccessPoint",
|
|
157
|
+
"wireless-router": "WirelessRouter",
|
|
158
|
+
"wirelessrouter": "WirelessRouter",
|
|
159
|
+
"home-router": "WirelessRouter",
|
|
160
|
+
"homerouter": "WirelessRouter",
|
|
161
|
+
"tablet": "Tablet",
|
|
162
|
+
"laptop": "Laptop",
|
|
163
|
+
"smartphone": "Smartphone",
|
|
164
|
+
"printer": "Printer",
|
|
165
|
+
"multilayer-switch": "MultiLayerSwitch",
|
|
166
|
+
"multilayerswitch": "MultiLayerSwitch",
|
|
167
|
+
"layer3-switch": "MultiLayerSwitch",
|
|
168
|
+
"layer-3-switch": "MultiLayerSwitch",
|
|
169
|
+
"cloud": "Cloud",
|
|
170
|
+
"cable-modem": "Cable Modem",
|
|
171
|
+
"cablemodem": "Cable Modem",
|
|
172
|
+
"dsl-modem": "Dsl Modem",
|
|
173
|
+
"dslmodem": "Dsl Modem",
|
|
174
|
+
"security-appliance": "Security Appliance",
|
|
175
|
+
"securityappliance": "Security Appliance",
|
|
176
|
+
"asa": "ASA",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
NATURAL_DEVICE_ALIASES = {
|
|
180
|
+
"Router": ["router", "routerler", "routerlerin"],
|
|
181
|
+
"Switch": ["switch", "switchler", "switchlerin", "komutator", "kommutator"],
|
|
182
|
+
"PC": ["pc", "pcs", "computer", "computers", "komputer", "komputerler", "kompyuter", "kompyuterler"],
|
|
183
|
+
"Server": ["server", "serverler"],
|
|
184
|
+
"WirelessRouter": ["wireless router", "wireless-router", "wirelessrouter", "home router", "home-router", "wrt"],
|
|
185
|
+
# `smartphone` belongs here rather than under a key of its own. Packet
|
|
186
|
+
# Tracer's palette lists SMARTPHONE-PT separately, but a lab saves it as
|
|
187
|
+
# `Pda`, which normalises to `Tablet` -- so a `Smartphone` key would be one
|
|
188
|
+
# the planner never produces, the same mistake `Cable Modem` made.
|
|
189
|
+
"Tablet": ["tablet", "tabletler", "pda", "handheld", "smartphone", "smartfon", "smart phone"],
|
|
190
|
+
"Laptop": ["laptop", "laptoplar"],
|
|
191
|
+
"Printer": ["printer", "printerler"],
|
|
192
|
+
"LightWeightAccessPoint": ["ap", "aps", "accesspoint", "access-point", "access point", "apler"],
|
|
193
|
+
"MultiLayerSwitch": ["multilayer switch", "multilayer-switch", "multilayerswitch", "layer 3 switch", "layer3 switch", "3560"],
|
|
194
|
+
"Cloud": ["cloud", "wan cloud"],
|
|
195
|
+
# Keyed as `normalize_device_type` spells them, not as English does. Written
|
|
196
|
+
# `Cable Modem` and `Dsl Modem`, these were keys the planner never produces,
|
|
197
|
+
# so both aliases resolved to nothing -- the same defect the `IoT` note below
|
|
198
|
+
# describes, repeated twice. Donors carry three cable modems and two DSL
|
|
199
|
+
# modems, none of which could be asked for.
|
|
200
|
+
"CableModem": ["cable modem", "cable-modem", "cablemodem", "kabel modem"],
|
|
201
|
+
"DslModem": ["dsl modem", "dsl-modem", "dslmodem"],
|
|
202
|
+
"ASA": ["asa", "security appliance", "security-appliance", "firewall"],
|
|
203
|
+
# Voice and IoT devices could not be asked for at all: "4 ip phone qur"
|
|
204
|
+
# parsed the count and then dropped it, because no alias matched.
|
|
205
|
+
"IpPhone": ["ip phone", "ipphone", "ip-phone", "telefon", "voip telefon"],
|
|
206
|
+
# Alias keys must be the *normalised* kind the planner works in, not the
|
|
207
|
+
# raw XML type. A device library stores IoT things as `MCUComponent`, which
|
|
208
|
+
# `normalize_device_type` folds to `IoT`; asking for `MCUComponent` looked
|
|
209
|
+
# for a kind the planner never produces, so a donor holding 112 of them
|
|
210
|
+
# still reported "no spare device".
|
|
211
|
+
"IoT": [
|
|
212
|
+
"iot device", "iot cihaz", "smart device", "agilli cihaz", "sensor",
|
|
213
|
+
"smart light", "smart door", "motion sensor", "temperature sensor",
|
|
214
|
+
"smoke detector", "fan", "lamp", "termostat", "thermostat",
|
|
215
|
+
],
|
|
216
|
+
"CCTVCamera": ["camera", "kamera", "cctv", "webcam"],
|
|
217
|
+
"Hub": ["hub", "konsentrator"],
|
|
218
|
+
"Repeater": ["repeater", "təkrarlayıcı", "tekrarlayici"],
|
|
219
|
+
# Added from what the donor pool actually holds, not from Packet Tracer's
|
|
220
|
+
# catalogue. Measured across 60 local donors: WirelessLanController appears
|
|
221
|
+
# in 3, Pda in 5, AnalogPhone and HomeVoip in 2 each. Patch panels, bridges,
|
|
222
|
+
# sniffers, Meraki gear, cell towers, TVs and coaxial splitters appear in
|
|
223
|
+
# none of them, so aliasing those would only turn a prompt into a refusal.
|
|
224
|
+
"WirelessLanController": [
|
|
225
|
+
"wlc", "wireless lan controller", "wireless controller",
|
|
226
|
+
"simsiz kontroller", "wlan controller",
|
|
227
|
+
],
|
|
228
|
+
"AnalogPhone": ["analog phone", "analog telefon", "analoq telefon"],
|
|
229
|
+
"HomeVoip": ["home voip", "home phone", "ev telefonu", "voip phone"],
|
|
230
|
+
# These exist in Packet Tracer's palette and in no local donor, so the
|
|
231
|
+
# earlier note here said aliasing them would only produce refusals. That was
|
|
232
|
+
# true of the donors that happened to be on disk, not of the tool: a donor
|
|
233
|
+
# carrying them was built through the live bridge and saved with the running
|
|
234
|
+
# build, and the kinds normalise to themselves, so they resolve.
|
|
235
|
+
"Patch Panel": ["patch panel", "patch-panel", "patchpanel", "kross panel"],
|
|
236
|
+
"Bridge": ["bridge", "kopru", "körpü"],
|
|
237
|
+
"Sniffer": ["sniffer", "paket analizatoru", "packet sniffer"],
|
|
238
|
+
"SecurityAppliance": ["meraki", "meraki mx", "security appliance mx"],
|
|
239
|
+
"TV": ["tv", "televizor", "smart tv"],
|
|
240
|
+
"CoAxialSplitter": ["coaxial splitter", "koaksial splitter", "splitter"],
|
|
241
|
+
"CellTower": ["cell tower", "mobil qulle", "base station", "bts"],
|
|
242
|
+
# Kinds the local donor pool actually holds and no prompt could name. Counted
|
|
243
|
+
# across 140 labs: CentralOfficeServer in 11, MerakiServer in 7, PLC in 6,
|
|
244
|
+
# NetworkController in 5, CyberObserver in 3, Power Distribution Device in 2,
|
|
245
|
+
# DataHistorian in 1. Each is listed here only because a donor carries it --
|
|
246
|
+
# an alias for a kind nobody has cannot do anything but turn a prompt into a
|
|
247
|
+
# refusal, which is what the note above this block was about.
|
|
248
|
+
#
|
|
249
|
+
# `meraki` alone stays with SecurityAppliance: the Meraki MX is the security
|
|
250
|
+
# appliance, and the server is the separate cloud-controller item.
|
|
251
|
+
"CentralOfficeServer": ["central office server", "central office", "co server"],
|
|
252
|
+
"MerakiServer": ["meraki server", "meraki cloud", "meraki dashboard"],
|
|
253
|
+
"NetworkController": ["network controller", "netcon", "sebeke kontrolleri"],
|
|
254
|
+
"PLC": ["plc", "programmable logic controller", "kontroller plc"],
|
|
255
|
+
"CyberObserver": ["cyber observer", "cyberobserver"],
|
|
256
|
+
"DataHistorian": ["data historian", "historian"],
|
|
257
|
+
"Power Distribution Device": [
|
|
258
|
+
"power distribution device", "power distribution", "guc paylayici",
|
|
259
|
+
],
|
|
260
|
+
# Added after building a donor that carries them, and checking what Packet
|
|
261
|
+
# Tracer actually writes for each. The palette lists more than these, and
|
|
262
|
+
# the saved types do not line up with it one for one:
|
|
263
|
+
#
|
|
264
|
+
# SMARTPHONE-PT saves as `Pda` -> folded into Tablet above
|
|
265
|
+
# Fiber Patch Panel saves as `Patch Panel` -> same kind as the copper one
|
|
266
|
+
# Copper Wall Mount saves as `Wall Mount` -> so does the fiber one
|
|
267
|
+
# Embedded-Server-PT Packet Tracer refused to create it at all
|
|
268
|
+
#
|
|
269
|
+
# So a palette of 74 entries is not 74 askable kinds. These four are the
|
|
270
|
+
# ones that survive as distinct types in a saved lab.
|
|
271
|
+
"SBC": ["sbc", "single board computer", "raspberry", "raspberry pi"],
|
|
272
|
+
"WiredEndDevice": ["wired end device", "simli son cihaz"],
|
|
273
|
+
"WirelessEndDevice": ["wireless end device", "simsiz son cihaz"],
|
|
274
|
+
"Wall Mount": ["wall mount", "wall-mount", "divar montaji"],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
NETWORK_STYLE_PATTERNS = {
|
|
278
|
+
"campus": [r"\bcampus\b", r"\bkampus\b", r"\bsobeli\b", r"\bdepart", r"\bdepartment\b"],
|
|
279
|
+
"branch": [r"\bbranch\b", r"\bfilial\b"],
|
|
280
|
+
"ipv6_routing": [r"\bipv6\b", r"\bslaac\b", r"\bdhcpv6\b", r"\bospfv3\b", r"\bhsrp\b"],
|
|
281
|
+
"ipv4_routing_management": [r"\bospfv2\b", r"\bospf v2\b", r"\beigrp ipv4\b", r"\bripv2\b", r"\brip version 2\b", r"\bstatic route\b", r"\bdefault route\b", r"\bdhcp relay\b", r"\bnat\b", r"\bpat\b", r"\bip ssh\b", r"\bntp server\b", r"\blogging host\b"],
|
|
282
|
+
"l2_security_monitoring": [r"\bdhcp snooping\b", r"\bdai\b", r"\bdot1x\b", r"\b802\.1x\b", r"\bsnmp\b", r"\bnetflow\b", r"\bspan\b", r"\brspan\b", r"\bquality of service\b", r"\blldp\b", r"\brep\b", r"\bport security\b", r"\bport-security\b"],
|
|
283
|
+
"l2_resiliency_routing": [r"\bbgp\b", r"\bstp\b", r"\brstp\b", r"\bspanning tree\b", r"\bspanning-tree\b", r"\betherchannel\b", r"\bport-channel\b", r"\bchannel-group\b", r"\blacp\b", r"\bpagp\b", r"\bvtp\b", r"\bdtp\b"],
|
|
284
|
+
"wireless_advanced": [r"\bwlc\b", r"\bwireless lan controller\b", r"\bmeraki\b", r"\bbluetooth\b", r"\b5g\b", r"\bcellular\b", r"\bwpa enterprise\b", r"\bwpa2 enterprise\b", r"\bwep\b", r"\bguest wifi\b", r"\bguest wlan\b", r"\bbeamforming\b"],
|
|
285
|
+
"automation_controller": [r"\bnetwork controller\b", r"\bnetcon\b", r"\bblockly\b", r"\bjavascript\b", r"\bpython programming\b", r"\biox\b"],
|
|
286
|
+
"voice_collaboration": [r"\bvoip\b", r"\bip phone\b", r"\bcall manager\b", r"\btelephony\b"],
|
|
287
|
+
"industrial_iot": [r"\bmqtt\b", r"\bwebsocket\b", r"\bprofinet\b", r"\bl2nat\b", r"\bptp\b", r"\bcyberobserver\b", r"\bindustrial\b"],
|
|
288
|
+
"physical_media_device": [r"\bcoaxial\b", r"\bcable modem\b", r"\bdsl modem\b", r"\bcentral office\b", r"\bcell tower\b", r"\bpower distribution\b"],
|
|
289
|
+
"small_office": [r"\bsmall office\b", r"\bhome\b", r"\bofis\b"],
|
|
290
|
+
"wireless_branch": [r"\bwireless\b", r"\bwifi\b", r"\bssid\b"],
|
|
291
|
+
"wan_security": [r"\bwan\b", r"\bgre\b", r"\bppp\b", r"\bipsec\b", r"\bvpn\b", r"\bsecurity edge\b", r"\bfirewall\b", r"\basa\b"],
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
PER_DEPARTMENT_DEVICE_ALIASES = {
|
|
295
|
+
"PC": ["pc", "komputer", "computer"],
|
|
296
|
+
"Printer": ["printer"],
|
|
297
|
+
"LightWeightAccessPoint": ["ap", "access point", "accesspoint"],
|
|
298
|
+
"Tablet": ["tablet"],
|
|
299
|
+
"Laptop": ["laptop"],
|
|
300
|
+
"Server": ["server"],
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
TOPOLOGY_HINT_WORDS = [
|
|
304
|
+
"switch",
|
|
305
|
+
"switchler",
|
|
306
|
+
"router",
|
|
307
|
+
"komputer",
|
|
308
|
+
"pc",
|
|
309
|
+
"server",
|
|
310
|
+
"printer",
|
|
311
|
+
"ap",
|
|
312
|
+
"tablet",
|
|
313
|
+
"qosul",
|
|
314
|
+
"qo",
|
|
315
|
+
"gig",
|
|
316
|
+
"fastethernet",
|
|
317
|
+
"vlan",
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
SECURITY_TO_AUTH = {
|
|
321
|
+
"open": ("0", "0"),
|
|
322
|
+
"wep": ("1", "1"),
|
|
323
|
+
"wpa-psk": ("3", "3"),
|
|
324
|
+
"wpa2-psk": ("4", "4"),
|
|
325
|
+
"wpa": ("2", "2"),
|
|
326
|
+
"wpa2": ("4", "4"),
|
|
327
|
+
"802.1x": ("5", "5"),
|
|
328
|
+
"wpa-enterprise": ("5", "5"),
|
|
329
|
+
"wpa2-enterprise": ("5", "5"),
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
TRANSLITERATION_TABLE = str.maketrans(
|
|
333
|
+
{
|
|
334
|
+
"ə": "e",
|
|
335
|
+
"Ə": "e",
|
|
336
|
+
"ş": "s",
|
|
337
|
+
"Ş": "s",
|
|
338
|
+
"ı": "i",
|
|
339
|
+
"İ": "i",
|
|
340
|
+
"ç": "c",
|
|
341
|
+
"Ç": "c",
|
|
342
|
+
"ö": "o",
|
|
343
|
+
"Ö": "o",
|
|
344
|
+
"ü": "u",
|
|
345
|
+
"Ü": "u",
|
|
346
|
+
"ğ": "g",
|
|
347
|
+
"Ğ": "g",
|
|
348
|
+
"â": "a",
|
|
349
|
+
"Â": "a",
|
|
350
|
+
"ê": "e",
|
|
351
|
+
"Ê": "e",
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _command_segments(prompt: str) -> list[str]:
|
|
357
|
+
parts = re.split(
|
|
358
|
+
r"(?=(?<!-)\b(?:device|connect|link|set|enable|associate|change|update|rename|apply)\b)",
|
|
359
|
+
prompt,
|
|
360
|
+
flags=re.IGNORECASE,
|
|
361
|
+
)
|
|
362
|
+
return [part.strip() for part in parts if part.strip()]
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _transliterate(prompt: str) -> str:
|
|
366
|
+
"""Fold Azerbaijani letters to ASCII while keeping capitalisation.
|
|
367
|
+
|
|
368
|
+
Device names carry their case into the generated lab, so the rename
|
|
369
|
+
patterns cannot run against the fully normalised prompt -- but they do need
|
|
370
|
+
`dəyiş` to read as `deyis`.
|
|
371
|
+
"""
|
|
372
|
+
text = prompt.translate(TRANSLITERATION_TABLE)
|
|
373
|
+
for source, target in _MOJIBAKE_REPLACEMENTS.items():
|
|
374
|
+
text = text.replace(source, target)
|
|
375
|
+
text = unicodedata.normalize("NFKD", text)
|
|
376
|
+
return text.encode("ascii", "ignore").decode("ascii")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
_MOJIBAKE_REPLACEMENTS = {
|
|
380
|
+
"É™": "e", "Æ": "e", "ÅŸ": "s", "Å": "s", "ı": "i", "İ": "i",
|
|
381
|
+
"ç": "c", "Ç": "c", "ö": "o", "Ö": "o", "ü": "u", "Ü": "u",
|
|
382
|
+
"ÄŸ": "g", "Ä": "g",
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _normalize_prompt(prompt: str) -> str:
|
|
387
|
+
text = prompt.translate(TRANSLITERATION_TABLE)
|
|
388
|
+
for source, target in {
|
|
389
|
+
"É™": "e",
|
|
390
|
+
"Æ": "e",
|
|
391
|
+
"ÅŸ": "s",
|
|
392
|
+
"Å": "s",
|
|
393
|
+
"ı": "i",
|
|
394
|
+
"İ": "i",
|
|
395
|
+
"ç": "c",
|
|
396
|
+
"Ç": "c",
|
|
397
|
+
"ö": "o",
|
|
398
|
+
"Ö": "o",
|
|
399
|
+
"ü": "u",
|
|
400
|
+
"Ü": "u",
|
|
401
|
+
"ÄŸ": "g",
|
|
402
|
+
"Ä": "g",
|
|
403
|
+
}.items():
|
|
404
|
+
text = text.replace(source, target)
|
|
405
|
+
text = unicodedata.normalize("NFKD", text)
|
|
406
|
+
text = text.encode("ascii", "ignore").decode("ascii")
|
|
407
|
+
text = text.lower()
|
|
408
|
+
text = re.sub(r"[^a-z0-9./\\,:_-]+", " ", text)
|
|
409
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
410
|
+
return _digits_for_number_words(text)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# Written after transliteration, so these are the folded forms: `uc` for uc,
|
|
414
|
+
# `dord` for dord, `bes` for bes, `sekkiz` for sekkiz.
|
|
415
|
+
#
|
|
416
|
+
# `on` -- ten in Azerbaijani -- is deliberately absent. Prompts here mix
|
|
417
|
+
# Azerbaijani and English, where `on` is a preposition sitting in front of the
|
|
418
|
+
# very words that identify a device: `dhcp on router`, `telnet on switch`. No
|
|
419
|
+
# lookahead separates the two readings, and guessing wrong silently orders ten
|
|
420
|
+
# routers instead of one. `10 komputer` still works; a misparse would not be
|
|
421
|
+
# noticed until Packet Tracer opened the wrong lab.
|
|
422
|
+
NUMBER_WORDS = {
|
|
423
|
+
"bir": 1, "iki": 2, "uc": 3, "dord": 4, "bes": 5,
|
|
424
|
+
"alti": 6, "yeddi": 7, "sekkiz": 8, "doqquz": 9,
|
|
425
|
+
"one": 1, "two": 2, "three": 3, "four": 4, "five": 5,
|
|
426
|
+
"six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10,
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
@lru_cache(maxsize=1)
|
|
430
|
+
def _number_word_pattern() -> re.Pattern[str]:
|
|
431
|
+
"""Match a spelled-out count only when a device word follows it.
|
|
432
|
+
|
|
433
|
+
The narrow lookahead is not optional. `on` is ten in Azerbaijani and a
|
|
434
|
+
preposition in English, so a bare substitution turns `dhcp on router` into
|
|
435
|
+
`dhcp 10 router` and silently orders ten routers. Requiring a device alias
|
|
436
|
+
next confines the rewrite to phrases that are actually counts.
|
|
437
|
+
"""
|
|
438
|
+
words = "|".join(sorted(NUMBER_WORDS, key=len, reverse=True))
|
|
439
|
+
return re.compile(
|
|
440
|
+
rf"\b({words})\b(?=\s+(?:dene|eded|tane)?\s*(?:{_any_device_alias_pattern()})\b)"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _digits_for_number_words(text: str) -> str:
|
|
445
|
+
"""Rewrite spelled-out counts as digits.
|
|
446
|
+
|
|
447
|
+
Every count extractor downstream matches `\\d+`, so `bir router iki switch
|
|
448
|
+
uc komputer` parsed as no devices at all -- a silent empty topology from a
|
|
449
|
+
perfectly ordinary Azerbaijani phrasing.
|
|
450
|
+
|
|
451
|
+
Converting here rather than in each extractor keeps one model of what a
|
|
452
|
+
count is.
|
|
453
|
+
"""
|
|
454
|
+
return _number_word_pattern().sub(lambda match: str(NUMBER_WORDS[match.group(1)]), text)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@dataclass
|
|
458
|
+
class IntentPlan:
|
|
459
|
+
goal: str
|
|
460
|
+
prompt: str
|
|
461
|
+
pkt_path: str | None = None
|
|
462
|
+
capabilities: list[str] = field(default_factory=list)
|
|
463
|
+
network_style: str | None = None
|
|
464
|
+
wireless_mode: str | None = None
|
|
465
|
+
wireless_settings: dict[str, object] = field(default_factory=dict)
|
|
466
|
+
requested_models: list[str] = field(default_factory=list)
|
|
467
|
+
device_requirements: dict[str, int] = field(default_factory=dict)
|
|
468
|
+
device_counts: dict[str, int] = field(default_factory=dict)
|
|
469
|
+
department_groups: list[dict[str, object]] = field(default_factory=list)
|
|
470
|
+
service_requirements: dict[str, object] = field(default_factory=dict)
|
|
471
|
+
topology_requirements: dict[str, object] = field(default_factory=dict)
|
|
472
|
+
vlan_ids: list[int] = field(default_factory=list)
|
|
473
|
+
uplink_intent: str | None = None
|
|
474
|
+
host_link_intent: str | None = None
|
|
475
|
+
host_vlan_assignment: dict[int, int] = field(default_factory=dict)
|
|
476
|
+
assumptions_used: list[str] = field(default_factory=list)
|
|
477
|
+
confidence_score: float = 0.0
|
|
478
|
+
parse_warnings: list[str] = field(default_factory=list)
|
|
479
|
+
blocking_gaps: list[str] = field(default_factory=list)
|
|
480
|
+
compatibility_profile: dict[str, object] = field(default_factory=dict)
|
|
481
|
+
unsafe_mutations_requested: list[str] = field(default_factory=list)
|
|
482
|
+
blocked_mutations: list[str] = field(default_factory=list)
|
|
483
|
+
acceptance_stage_plan: list[dict[str, object]] = field(default_factory=list)
|
|
484
|
+
capability_matrix_hits: list[dict[str, object]] = field(default_factory=list)
|
|
485
|
+
unsupported_capabilities: list[str] = field(default_factory=list)
|
|
486
|
+
coverage_gap_report: dict[str, object] = field(default_factory=dict)
|
|
487
|
+
blueprint_plan: dict[str, object] = field(default_factory=dict)
|
|
488
|
+
remote_search_results: list[dict[str, object]] = field(default_factory=list)
|
|
489
|
+
edit_operations: list[dict[str, object]] = field(default_factory=list)
|
|
490
|
+
devices: list[dict[str, object]] = field(default_factory=list)
|
|
491
|
+
links: list[dict[str, object]] = field(default_factory=list)
|
|
492
|
+
switch_ops: list[dict[str, object]] = field(default_factory=list)
|
|
493
|
+
router_ops: list[dict[str, object]] = field(default_factory=list)
|
|
494
|
+
server_ops: list[dict[str, object]] = field(default_factory=list)
|
|
495
|
+
wireless_ops: list[dict[str, object]] = field(default_factory=list)
|
|
496
|
+
end_device_ops: list[dict[str, object]] = field(default_factory=list)
|
|
497
|
+
management_ops: list[dict[str, object]] = field(default_factory=list)
|
|
498
|
+
verification_ops: list[dict[str, object]] = field(default_factory=list)
|
|
499
|
+
iot_ops: list[dict[str, object]] = field(default_factory=list)
|
|
500
|
+
programming_ops: list[dict[str, object]] = field(default_factory=list)
|
|
501
|
+
|
|
502
|
+
def all_operations(self) -> list[dict[str, object]]:
|
|
503
|
+
merged: list[dict[str, object]] = []
|
|
504
|
+
for bucket in [
|
|
505
|
+
self.edit_operations,
|
|
506
|
+
self.switch_ops,
|
|
507
|
+
self.router_ops,
|
|
508
|
+
self.server_ops,
|
|
509
|
+
self.wireless_ops,
|
|
510
|
+
self.end_device_ops,
|
|
511
|
+
self.management_ops,
|
|
512
|
+
self.verification_ops,
|
|
513
|
+
self.iot_ops,
|
|
514
|
+
self.programming_ops,
|
|
515
|
+
]:
|
|
516
|
+
merged.extend(bucket)
|
|
517
|
+
return merged
|
|
518
|
+
|
|
519
|
+
def to_dict(self) -> dict[str, object]:
|
|
520
|
+
data = asdict(self)
|
|
521
|
+
data["all_operations"] = self.all_operations()
|
|
522
|
+
return data
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _normalize_device_type(raw_type: str) -> str:
|
|
526
|
+
return DEVICE_SYNONYMS.get(raw_type.strip().lower(), raw_type.strip())
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# Plural forms were listed by hand, so the table disagreed with itself: `PC`
|
|
530
|
+
# carried `computers` and `pcs` while `Switch` and `Router` had no English
|
|
531
|
+
# plural at all. `2 switches 1 router 4 computers` therefore parsed as a router
|
|
532
|
+
# and four PCs -- the switches vanished silently, and the user got a topology
|
|
533
|
+
# with none. Matching the suffix instead of enumerating it keeps every device
|
|
534
|
+
# type on the same footing.
|
|
535
|
+
_PLURAL_SUFFIX = r"(?:s|es|ler|lar)?"
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _alias_alternation(aliases: list[str]) -> str:
|
|
539
|
+
"""Alternation over aliases, longest first so prefixes lose, plural-tolerant."""
|
|
540
|
+
ordered = sorted(set(aliases), key=len, reverse=True)
|
|
541
|
+
return "|".join(re.escape(alias) + _PLURAL_SUFFIX for alias in ordered)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _any_device_alias_pattern() -> str:
|
|
545
|
+
"""Alternation over every device alias, longest first so prefixes lose."""
|
|
546
|
+
return _alias_alternation(
|
|
547
|
+
[alias for alias_list in NATURAL_DEVICE_ALIASES.values() for alias in alias_list]
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# Cisco model designations sit right next to the device word -- `2911 router`,
|
|
552
|
+
# `2960 switch` -- exactly where a count goes. Reading one as a count made
|
|
553
|
+
# `2911 router qur` request two thousand nine hundred and eleven routers, which
|
|
554
|
+
# planned for minutes before failing.
|
|
555
|
+
KNOWN_DEVICE_MODELS = {
|
|
556
|
+
819, 829, 1240, 1941, 2504, 2620, 2621, 2811, 2901, 2911, 2950, 2960,
|
|
557
|
+
3560, 3650, 3702, 4321, 4331, 8200, 5505, 5506, 1841, 1921, 3945,
|
|
558
|
+
}
|
|
559
|
+
# A ceiling only to catch stray four-digit numbers that are neither counts nor
|
|
560
|
+
# known models. It was 200, which silently turned "500 komputer" -- an ordinary
|
|
561
|
+
# enterprise request -- into a single PC, because the bare-name fallback then
|
|
562
|
+
# supplied 1. Real labs do carry hundreds of devices, so the ceiling has to sit
|
|
563
|
+
# well above anything a person would actually ask for.
|
|
564
|
+
MAX_REASONABLE_DEVICE_COUNT = 2000
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _is_device_count(value: int) -> bool:
|
|
568
|
+
return 0 <= value <= MAX_REASONABLE_DEVICE_COUNT and value not in KNOWN_DEVICE_MODELS
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _extract_requested_models(normalized_prompt: str) -> list[str]:
|
|
572
|
+
"""Model designations the prompt names, e.g. `2911`, `ISR4331`, `2960-24TT`.
|
|
573
|
+
|
|
574
|
+
Recorded so the answer can be honest about them. A generated lab takes its
|
|
575
|
+
device models from whichever donor supplied the prototype, so asking for a
|
|
576
|
+
2911 and receiving a PT8200 is the normal outcome -- but it used to happen
|
|
577
|
+
silently, which reads as the request having been honoured.
|
|
578
|
+
"""
|
|
579
|
+
found: list[str] = []
|
|
580
|
+
for match in re.finditer(r"\b(isr\s?\d{3,4}|\d{3,4})(?:-[a-z0-9]+)?\b", normalized_prompt):
|
|
581
|
+
token = match.group(0).replace(" ", "").upper()
|
|
582
|
+
# Only the leading number identifies the family: `2960-24TT` is a 2960.
|
|
583
|
+
leading = re.match(r"[A-Z]*(\d{3,4})", token)
|
|
584
|
+
if leading and int(leading.group(1)) in KNOWN_DEVICE_MODELS and token not in found:
|
|
585
|
+
found.append(token)
|
|
586
|
+
return found
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _extract_natural_device_counts(normalized_prompt: str) -> dict[str, int]:
|
|
590
|
+
"""Count devices per type from a natural prompt.
|
|
591
|
+
|
|
592
|
+
Two phrasings are supported: `3 switch` (count first, the common form) and
|
|
593
|
+
`switch 3`. The trailing form used to swallow the *next* device's count —
|
|
594
|
+
"1 router 8 komputer" matched `router 8` and, because the two forms were
|
|
595
|
+
pooled through `max`, the router ended up with 8. Trailing counts are now
|
|
596
|
+
rejected when another device word follows, and the leading form wins
|
|
597
|
+
outright when both are present.
|
|
598
|
+
"""
|
|
599
|
+
counts: dict[str, int] = {}
|
|
600
|
+
any_alias = _any_device_alias_pattern()
|
|
601
|
+
for device_type, aliases in NATURAL_DEVICE_ALIASES.items():
|
|
602
|
+
alias_pattern = _alias_alternation(aliases)
|
|
603
|
+
leading = re.compile(
|
|
604
|
+
rf"(?<![,/])\b(\d+)\s*(?:dene|dene?|eded|eded|tane)?\s*(?:{alias_pattern})\b"
|
|
605
|
+
)
|
|
606
|
+
trailing = re.compile(
|
|
607
|
+
rf"\b(?:{alias_pattern})\s+(\d+)\b(?!\s*(?:dene|eded|tane)?\s*(?:{any_alias})\b)"
|
|
608
|
+
)
|
|
609
|
+
# Counted matches need the longer-alias guard too. The note on
|
|
610
|
+
# `_without_longer_aliases` claimed the digit anchors them, which holds
|
|
611
|
+
# only while the shorter alias sits at the *end* of the longer phrase:
|
|
612
|
+
# `1 wireless router` keeps `wireless ` between the digit and `router`,
|
|
613
|
+
# so nothing matches. `1 meraki server` does not -- `meraki` follows the
|
|
614
|
+
# digit directly -- and the prompt credited a security appliance and a
|
|
615
|
+
# Meraki server both.
|
|
616
|
+
counted_text = _without_longer_aliases(normalized_prompt, aliases)
|
|
617
|
+
leading_values = [
|
|
618
|
+
int(value) for value in leading.findall(counted_text) if _is_device_count(int(value))
|
|
619
|
+
]
|
|
620
|
+
if leading_values:
|
|
621
|
+
counts[device_type] = max(leading_values)
|
|
622
|
+
continue
|
|
623
|
+
trailing_values = [
|
|
624
|
+
int(value) for value in trailing.findall(counted_text) if _is_device_count(int(value))
|
|
625
|
+
]
|
|
626
|
+
if trailing_values:
|
|
627
|
+
counts[device_type] = max(trailing_values)
|
|
628
|
+
continue
|
|
629
|
+
# A device named with no number at all means one of it. `router switch pc
|
|
630
|
+
# qur` was refused with "this prompt does not describe a topology" -- a
|
|
631
|
+
# message contradicted by the prompt itself, which names three devices.
|
|
632
|
+
#
|
|
633
|
+
# Applied per device type and only when that type carries no count
|
|
634
|
+
# anywhere, so `2 switch ve router qur` still reads as two switches and
|
|
635
|
+
# one router rather than flattening either.
|
|
636
|
+
bare = re.compile(rf"\b(?:{alias_pattern})\b")
|
|
637
|
+
if bare.search(_without_longer_aliases(normalized_prompt, aliases)):
|
|
638
|
+
counts[device_type] = 1
|
|
639
|
+
return counts
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _without_longer_aliases(text: str, aliases: list[str]) -> str:
|
|
643
|
+
"""Blank out longer device names that merely contain one of `aliases`.
|
|
644
|
+
|
|
645
|
+
`wireless router` contains `router`, so a bare scan credited both a wireless
|
|
646
|
+
router and a plain one from `1 wireless router 2 laptop qur`. A counted
|
|
647
|
+
match never had this problem because the digit anchors it; only the
|
|
648
|
+
no-number fallback needs the guard.
|
|
649
|
+
|
|
650
|
+
A device type's *own* longer aliases are never masked. They are the same
|
|
651
|
+
device, not a different one, and blanking them deleted the only word the
|
|
652
|
+
scan had to go on: `switchler ve routerler qur` masked `switchler` because
|
|
653
|
+
it contains `switch`, masked `routerler` because it contains `router`, and
|
|
654
|
+
parsed as an empty plan -- no devices, no warning, nothing to build.
|
|
655
|
+
"""
|
|
656
|
+
own = set(aliases)
|
|
657
|
+
masked = text
|
|
658
|
+
for other in {alias for group in NATURAL_DEVICE_ALIASES.values() for alias in group}:
|
|
659
|
+
if other in own:
|
|
660
|
+
continue
|
|
661
|
+
if any(alias != other and alias in other for alias in aliases):
|
|
662
|
+
masked = re.sub(rf"\b{re.escape(other)}\b", " ", masked)
|
|
663
|
+
return masked
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _extract_vlan_ids(normalized_prompt: str) -> list[int]:
|
|
667
|
+
"""Every VLAN a prompt names, whether listed once or repeated.
|
|
668
|
+
|
|
669
|
+
The separators used to be a character class, `[0-9,\\s/veand]`, which
|
|
670
|
+
contains `v`. On `vlan 10 ve vlan 20` it swallowed the start of the second
|
|
671
|
+
keyword and the lab came out with one VLAN instead of two. A separator is
|
|
672
|
+
now only accepted between two numbers.
|
|
673
|
+
"""
|
|
674
|
+
vlan_ids: list[int] = []
|
|
675
|
+
pattern = r"\bvlan(?:larda|lar|da|de|a|e)?\s+((?:\d+(?:\s*(?:,|/|ve|and|-)\s*|\s+)?)+)"
|
|
676
|
+
for match in re.findall(pattern, normalized_prompt):
|
|
677
|
+
vlan_ids.extend(int(value) for value in re.findall(r"\d+", match))
|
|
678
|
+
return sorted(dict.fromkeys(vlan_ids))
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _extract_host_vlan_assignment(normalized_prompt: str) -> dict[int, int]:
|
|
682
|
+
"""Per-VLAN host counts, but only where the prompt really states one.
|
|
683
|
+
|
|
684
|
+
`4 komputer vlan 10 ve vlan 20 qur` reads as a host count next to a VLAN
|
|
685
|
+
*list*, not as "4 hosts in VLAN 10". Taken literally it produced
|
|
686
|
+
{10: 4}, which then suppressed the even split and left VLAN 20 empty.
|
|
687
|
+
|
|
688
|
+
A trailing `ve vlan 20` therefore disqualifies the match, while
|
|
689
|
+
`2 komputer vlan 10 ve 3 komputer vlan 20` -- a genuine pair of
|
|
690
|
+
assignments -- still parses, because there the separator is followed by a
|
|
691
|
+
count rather than by another VLAN.
|
|
692
|
+
"""
|
|
693
|
+
vlan_list_follows = r"(?!\s*(?:,|/|ve|and)\s*vlan\s*\d)"
|
|
694
|
+
assignments: dict[int, int] = {}
|
|
695
|
+
patterns = [
|
|
696
|
+
re.compile(r"\bvlan\s+(\d+)\s*(?:da|de|a|e)?\s+(\d+)\s+(?:pc|komputer|computer)\b"),
|
|
697
|
+
re.compile(
|
|
698
|
+
r"\b(\d+)\s+(?:pc|komputer|computer)\s+vlan(?:da|de|a|e)?\s+(\d+)\b" + vlan_list_follows
|
|
699
|
+
),
|
|
700
|
+
]
|
|
701
|
+
for pattern_index, pattern in enumerate(patterns):
|
|
702
|
+
for first, second in pattern.findall(normalized_prompt):
|
|
703
|
+
if pattern_index == 0:
|
|
704
|
+
vlan_id, count = int(first), int(second)
|
|
705
|
+
else:
|
|
706
|
+
count, vlan_id = int(first), int(second)
|
|
707
|
+
assignments[vlan_id] = assignments.get(vlan_id, 0) + count
|
|
708
|
+
return assignments
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _extract_uplink_intent(normalized_prompt: str) -> str | None:
|
|
712
|
+
if any(token in normalized_prompt for token in ["gig port", "gigabit", " gig ", "gi0", "routerle aralarinda gig", "switchlerin oz aralarinda"]):
|
|
713
|
+
return "gigabit"
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _extract_host_link_intent(normalized_prompt: str) -> str | None:
|
|
718
|
+
if any(token in normalized_prompt for token in ["fa port", "fastethernet", " fa ", "pc ler ise fa", "komputerler ise fa"]):
|
|
719
|
+
return "fastethernet"
|
|
720
|
+
return None
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _extract_network_style(normalized_prompt: str) -> str | None:
|
|
724
|
+
for style, patterns in NETWORK_STYLE_PATTERNS.items():
|
|
725
|
+
if any(re.search(pattern, normalized_prompt) for pattern in patterns):
|
|
726
|
+
return style
|
|
727
|
+
return None
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
# One vocabulary for "a repeated group of devices". The count pattern and the
|
|
731
|
+
# per-group pattern used to carry their own lists -- the first knew
|
|
732
|
+
# `department`, the second knew `departmentda` -- so a word added to one half
|
|
733
|
+
# worked in silence. Measured: `3 mertebe, her mertebede 6 komputer` produced
|
|
734
|
+
# six PCs on one switch, because neither list had heard of a floor, and no
|
|
735
|
+
# warning said so. The same prompt with `sobe` produced eighteen PCs on three.
|
|
736
|
+
#
|
|
737
|
+
# Stems are matched with optional Azerbaijani and English suffixes; locatives
|
|
738
|
+
# are spelled out because Azerbaijani vowel harmony picks -de or -da and
|
|
739
|
+
# guessing it in a regex would be worse than listing twelve words.
|
|
740
|
+
GROUP_NOUN_STEMS = (
|
|
741
|
+
"departament",
|
|
742
|
+
"department",
|
|
743
|
+
"building",
|
|
744
|
+
"mertebe",
|
|
745
|
+
"filial",
|
|
746
|
+
"branch",
|
|
747
|
+
"office",
|
|
748
|
+
"bolme",
|
|
749
|
+
"floor",
|
|
750
|
+
"sobe",
|
|
751
|
+
"ofis",
|
|
752
|
+
"bina",
|
|
753
|
+
"kat",
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
GROUP_NOUN_LOCATIVES = (
|
|
757
|
+
"departamentde",
|
|
758
|
+
"departmentda",
|
|
759
|
+
"mertebede",
|
|
760
|
+
"filialda",
|
|
761
|
+
"bolmede",
|
|
762
|
+
"sobede",
|
|
763
|
+
"ofisde",
|
|
764
|
+
"binada",
|
|
765
|
+
"katda",
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _group_noun_alternation(words: tuple[str, ...]) -> str:
|
|
770
|
+
"""Longest first, so `department` never matches inside `departament`."""
|
|
771
|
+
return "|".join(re.escape(word) for word in sorted(words, key=len, reverse=True))
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _extract_department_count(normalized_prompt: str) -> int:
|
|
775
|
+
stems = _group_noun_alternation(GROUP_NOUN_STEMS)
|
|
776
|
+
pattern = re.compile(rf"\b(\d+)\s+(?:{stems})(?:li|lu|ler|lar|s)?\b")
|
|
777
|
+
match = pattern.search(normalized_prompt)
|
|
778
|
+
return int(match.group(1)) if match else 0
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _extract_per_department_devices(normalized_prompt: str) -> dict[str, int]:
|
|
782
|
+
counts: dict[str, int] = {}
|
|
783
|
+
stems = _group_noun_alternation(GROUP_NOUN_STEMS)
|
|
784
|
+
locatives = _group_noun_alternation(GROUP_NOUN_LOCATIVES)
|
|
785
|
+
group_segment = ""
|
|
786
|
+
segment_match = re.search(
|
|
787
|
+
rf"\bher\s+(?:{locatives})\s+(.+?)(?=\b(?:router|dhcp|vlan|ssid|acl|telnet|ospf|eigrp|rip|nat)\b|$)",
|
|
788
|
+
normalized_prompt,
|
|
789
|
+
)
|
|
790
|
+
if segment_match:
|
|
791
|
+
group_segment = segment_match.group(1)
|
|
792
|
+
for device_type, aliases in PER_DEPARTMENT_DEVICE_ALIASES.items():
|
|
793
|
+
alias_pattern = "|".join(re.escape(alias) for alias in aliases)
|
|
794
|
+
# Each pattern says whether it may be read from the whole prompt or only
|
|
795
|
+
# from the per-group segment. That used to be inferred from whether the
|
|
796
|
+
# pattern text began with a digit group, which the reversed English form
|
|
797
|
+
# below would have tripped: `6 computers each` starts with one and must
|
|
798
|
+
# still be read from the whole prompt.
|
|
799
|
+
patterns: list[tuple[re.Pattern[str], bool]] = [
|
|
800
|
+
(re.compile(rf"\bher\s+(?:{locatives})\s+(\d+)\s+(?:dene\s+)?(?:{alias_pattern})s?\b"), False),
|
|
801
|
+
(re.compile(rf"\b(?:each|per)\s+(?:{stems}|group)\s+(\d+)\s+(?:{alias_pattern})s?\b"), False),
|
|
802
|
+
(re.compile(rf"\b(\d+)\s+(?:{alias_pattern})s?\s+(?:each|per\s+(?:{stems}))\b"), False),
|
|
803
|
+
(re.compile(rf"\b(\d+)\s+(?:dene\s+)?(?:{alias_pattern})s?\b"), True),
|
|
804
|
+
]
|
|
805
|
+
values: list[int] = []
|
|
806
|
+
for pattern, prefer_segment in patterns:
|
|
807
|
+
target_text = group_segment if prefer_segment and group_segment else normalized_prompt
|
|
808
|
+
values.extend(int(value) for value in pattern.findall(target_text))
|
|
809
|
+
if values:
|
|
810
|
+
counts[device_type] = max(values)
|
|
811
|
+
return counts
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _build_department_groups(
|
|
815
|
+
normalized_prompt: str,
|
|
816
|
+
department_count: int,
|
|
817
|
+
per_department_devices: dict[str, int],
|
|
818
|
+
vlan_ids: list[int],
|
|
819
|
+
) -> list[dict[str, object]]:
|
|
820
|
+
if department_count <= 0:
|
|
821
|
+
return []
|
|
822
|
+
groups: list[dict[str, object]] = []
|
|
823
|
+
for index in range(department_count):
|
|
824
|
+
group_name = f"DEPT{index + 1}"
|
|
825
|
+
groups.append(
|
|
826
|
+
{
|
|
827
|
+
"name": group_name,
|
|
828
|
+
"switch_name": f"{group_name}-SW",
|
|
829
|
+
"vlan_id": vlan_ids[index] if index < len(vlan_ids) else None,
|
|
830
|
+
"devices": dict(per_department_devices),
|
|
831
|
+
}
|
|
832
|
+
)
|
|
833
|
+
return groups
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _extract_service_requirements(capabilities: list[str], prompt: str) -> dict[str, object]:
|
|
837
|
+
lowered = prompt.lower()
|
|
838
|
+
requirements: dict[str, object] = {
|
|
839
|
+
"routing": next((cap for cap in ["ospf", "eigrp", "rip"] if cap in capabilities), None),
|
|
840
|
+
"services": [],
|
|
841
|
+
"security": [],
|
|
842
|
+
}
|
|
843
|
+
for service in ["dhcp", "dns", "http", "https", "ftp", "tftp", "ntp", "email", "syslog", "aaa"]:
|
|
844
|
+
if service in lowered:
|
|
845
|
+
requirements["services"].append(service)
|
|
846
|
+
for security in ["acl", "telnet", "wpa2", "wpa", "wep", "nat", "vpn", "ipsec", "gre", "ppp", "security_edge", "ssh"]:
|
|
847
|
+
if security in lowered:
|
|
848
|
+
requirements["security"].append(security)
|
|
849
|
+
return requirements
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _extract_wireless_mode(
|
|
853
|
+
normalized_prompt: str,
|
|
854
|
+
capabilities: list[str],
|
|
855
|
+
device_requirements: dict[str, int],
|
|
856
|
+
network_style: str | None,
|
|
857
|
+
) -> str | None:
|
|
858
|
+
has_wireless = any(cap in capabilities for cap in ["wireless_ap", "wireless_client"])
|
|
859
|
+
if not has_wireless and not device_requirements.get("WirelessRouter", 0):
|
|
860
|
+
return None
|
|
861
|
+
mentions_home_router = any(token in normalized_prompt for token in ["home router", "wireless router", "wirelessrouter", "wrt"])
|
|
862
|
+
campus_like = network_style in {"campus", "branch", "wireless_branch"} or "department" in normalized_prompt or "sobeli" in normalized_prompt
|
|
863
|
+
if mentions_home_router and campus_like:
|
|
864
|
+
return "mixed"
|
|
865
|
+
if mentions_home_router or "nat" in capabilities or network_style == "small_office" or device_requirements.get("WirelessRouter", 0):
|
|
866
|
+
return "home_router_edge"
|
|
867
|
+
return "ap_bridge"
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _estimate_confidence(
|
|
871
|
+
device_requirements: dict[str, int],
|
|
872
|
+
capabilities: list[str],
|
|
873
|
+
parse_warnings: list[str],
|
|
874
|
+
blocking_gaps: list[str],
|
|
875
|
+
explicit_devices: list[dict[str, object]],
|
|
876
|
+
links: list[dict[str, object]],
|
|
877
|
+
) -> float:
|
|
878
|
+
score = 0.2
|
|
879
|
+
if device_requirements or explicit_devices:
|
|
880
|
+
score += 0.25
|
|
881
|
+
if capabilities:
|
|
882
|
+
score += 0.2
|
|
883
|
+
if links:
|
|
884
|
+
score += 0.15
|
|
885
|
+
score -= min(len(parse_warnings) * 0.08, 0.16)
|
|
886
|
+
score -= min(len(blocking_gaps) * 0.2, 0.4)
|
|
887
|
+
return max(0.0, min(1.0, round(score, 2)))
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _extract_explicit_devices(prompt: str) -> list[dict[str, object]]:
|
|
891
|
+
devices: list[dict[str, object]] = []
|
|
892
|
+
pattern = re.compile(
|
|
893
|
+
r"(?:device\s+)?([A-Za-z][A-Za-z0-9_-]*)\s+type\s+([A-Za-z-]+)(?:\s+model\s+([A-Za-z0-9._-]+))?",
|
|
894
|
+
flags=re.IGNORECASE,
|
|
895
|
+
)
|
|
896
|
+
for segment in _command_segments(prompt):
|
|
897
|
+
for name, raw_type, model in pattern.findall(segment):
|
|
898
|
+
devices.append({"name": name, "type": _normalize_device_type(raw_type), "model": model or None})
|
|
899
|
+
return devices
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _extract_explicit_links(prompt: str) -> list[dict[str, object]]:
|
|
903
|
+
links: list[dict[str, object]] = []
|
|
904
|
+
pattern = re.compile(
|
|
905
|
+
r"(?:connect|link)\s+([A-Za-z][A-Za-z0-9_-]*):([A-Za-z0-9/._-]+)\s+(?:to|->)\s+([A-Za-z][A-Za-z0-9_-]*):([A-Za-z0-9/._-]+)"
|
|
906
|
+
r"(?:\s+with\s+([A-Za-z0-9._-]+))?",
|
|
907
|
+
flags=re.IGNORECASE,
|
|
908
|
+
)
|
|
909
|
+
for segment in _command_segments(prompt):
|
|
910
|
+
for left_dev, left_port, right_dev, right_port, cable in pattern.findall(segment):
|
|
911
|
+
links.append(
|
|
912
|
+
{
|
|
913
|
+
"a": {"dev": left_dev, "port": left_port},
|
|
914
|
+
"b": {"dev": right_dev, "port": right_port},
|
|
915
|
+
"media": (cable or "copper").lower(),
|
|
916
|
+
}
|
|
917
|
+
)
|
|
918
|
+
deduped: list[dict[str, object]] = []
|
|
919
|
+
seen: set[tuple[str, str, str, str, str]] = set()
|
|
920
|
+
for link in links:
|
|
921
|
+
key = (
|
|
922
|
+
str(link["a"]["dev"]),
|
|
923
|
+
str(link["a"]["port"]),
|
|
924
|
+
str(link["b"]["dev"]),
|
|
925
|
+
str(link["b"]["port"]),
|
|
926
|
+
str(link["media"]),
|
|
927
|
+
)
|
|
928
|
+
if key not in seen:
|
|
929
|
+
seen.add(key)
|
|
930
|
+
deduped.append(link)
|
|
931
|
+
return deduped
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _extract_link_edit_operations(links: list[dict[str, object]]) -> list[dict[str, object]]:
|
|
935
|
+
return [{"op": "set_link", **link} for link in links]
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _extract_natural_edit_operations(prompt: str) -> tuple[list[dict[str, object]], list[dict[str, object]]]:
|
|
939
|
+
"""Edit requests as an ordinary sentence phrases them.
|
|
940
|
+
|
|
941
|
+
`pkt_editor` implements every operation below already. Only the command form
|
|
942
|
+
was ever parsed -- `set SW1 vlan 20 name SALES` -- so nine of eleven
|
|
943
|
+
realistic requests produced no operations at all, and `--edit` wrote the
|
|
944
|
+
input back out unchanged.
|
|
945
|
+
|
|
946
|
+
Returns `(edit_operations, switch_operations)`; the editor routes those two
|
|
947
|
+
buckets differently.
|
|
948
|
+
"""
|
|
949
|
+
text = _transliterate(prompt)
|
|
950
|
+
edits: list[dict[str, object]] = []
|
|
951
|
+
switch_ops: list[dict[str, object]] = []
|
|
952
|
+
|
|
953
|
+
def add(bucket: list[dict[str, object]], operation: dict[str, object]) -> None:
|
|
954
|
+
if operation not in bucket:
|
|
955
|
+
bucket.append(operation)
|
|
956
|
+
|
|
957
|
+
# `SW1 de vlan 20 yarat` / `SW1 uzerinde vlan 20 ac`
|
|
958
|
+
for device, vlan in re.findall(
|
|
959
|
+
r"\b([A-Za-z0-9_-]+)\s*(?:de|da|uzerinde|ustunde)?\s*vlan\s+(\d+)\s*(?:yarat|ac|elave et|qur)",
|
|
960
|
+
text,
|
|
961
|
+
flags=re.IGNORECASE,
|
|
962
|
+
):
|
|
963
|
+
add(switch_ops, {"op": "set_vlan", "device": device, "vlan": int(vlan), "name": f"VLAN{vlan}"})
|
|
964
|
+
|
|
965
|
+
# `PC1 ve SW1 arasinda link qur` / `PC1 ile SW1 i birlesdir`
|
|
966
|
+
link_patterns = (
|
|
967
|
+
r"\b([A-Za-z0-9_-]+)\s+(?:ve|ile)\s+([A-Za-z0-9_-]+)\s*(?:i|ni|nu)?\s*aras[ıi]nda\s+(?:link|kabel|elaqe)\s*(?:qur|cek|yarat)",
|
|
968
|
+
r"\b([A-Za-z0-9_-]+)\s+(?:ve|ile)\s+([A-Za-z0-9_-]+)\s*(?:i|ni|nu)?\s*birlesdir",
|
|
969
|
+
)
|
|
970
|
+
for pattern in link_patterns:
|
|
971
|
+
for left, right in re.findall(pattern, text, flags=re.IGNORECASE):
|
|
972
|
+
# Ports are left unset on purpose: the planner picks free ones, and
|
|
973
|
+
# naming a port the device lacks makes Packet Tracer reject the file.
|
|
974
|
+
add(edits, {"op": "set_link", "a": {"dev": left, "port": ""}, "b": {"dev": right, "port": ""}, "media": "copper"})
|
|
975
|
+
|
|
976
|
+
# `R1 ve SW1 arasindaki linki sil`
|
|
977
|
+
for left, right in re.findall(
|
|
978
|
+
r"\b([A-Za-z0-9_-]+)\s+(?:ve|ile)\s+([A-Za-z0-9_-]+)\s*aras[ıi]ndak[ıi]\s+(?:link|kabel|elaqe)\w*\s+(?:sil|kes|qopar)",
|
|
979
|
+
text,
|
|
980
|
+
flags=re.IGNORECASE,
|
|
981
|
+
):
|
|
982
|
+
add(edits, {"op": "remove_link", "a": {"dev": left, "port": ""}, "b": {"dev": right, "port": ""}})
|
|
983
|
+
|
|
984
|
+
# `PC3 u sil` -- the trailing vowel is the Azerbaijani accusative suffix.
|
|
985
|
+
for device in re.findall(
|
|
986
|
+
r"\b([A-Za-z][A-Za-z0-9_-]*\d[A-Za-z0-9_-]*)\s*(?:i|u|ni|nu|nı|yi)?\s+(?:sil|cixart|kenar et)\b",
|
|
987
|
+
text,
|
|
988
|
+
flags=re.IGNORECASE,
|
|
989
|
+
):
|
|
990
|
+
add(edits, {"op": "prune_device", "device": device})
|
|
991
|
+
|
|
992
|
+
# `cli R1: <commands>` hands the user's own IOS straight through. Newlines
|
|
993
|
+
# keep their indentation, which is what marks a block body -- `ip dhcp pool
|
|
994
|
+
# X` followed by an indented `network ...` is a pool, and the same two lines
|
|
995
|
+
# unindented are two unrelated globals. A semicolon is a plain separator for
|
|
996
|
+
# one-liners, so every part it produces is top level.
|
|
997
|
+
cli_pattern = re.compile(
|
|
998
|
+
r"(?:^|\s)cli\s+([A-Za-z0-9_-]+)\s*:\s*(.+?)"
|
|
999
|
+
r"(?=(?:\r?\n)\s*cli\s+[A-Za-z0-9_-]+\s*:|$)",
|
|
1000
|
+
flags=re.IGNORECASE | re.DOTALL,
|
|
1001
|
+
)
|
|
1002
|
+
for device, body in cli_pattern.findall(prompt):
|
|
1003
|
+
lines: list[str] = []
|
|
1004
|
+
for raw_line in body.splitlines():
|
|
1005
|
+
if ";" in raw_line:
|
|
1006
|
+
lines.extend(part.strip() for part in raw_line.split(";") if part.strip())
|
|
1007
|
+
elif raw_line.strip():
|
|
1008
|
+
lines.append(raw_line.rstrip())
|
|
1009
|
+
if lines:
|
|
1010
|
+
add(edits, {"op": "apply_cli", "device": device, "lines": lines})
|
|
1011
|
+
|
|
1012
|
+
return edits, switch_ops
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def _extract_switch_ops(prompt: str) -> list[dict[str, object]]:
|
|
1016
|
+
ops: list[dict[str, object]] = []
|
|
1017
|
+
for segment in _command_segments(prompt):
|
|
1018
|
+
for device, vlan_id, name in re.findall(r"set\s+([A-Za-z0-9_-]+)\s+vlan\s+(\d+)\s+name\s+([A-Za-z0-9_-]+)", segment, flags=re.IGNORECASE):
|
|
1019
|
+
ops.append({"op": "set_vlan", "device": device, "vlan": int(vlan_id), "name": name})
|
|
1020
|
+
for device, port, vlan_id in re.findall(r"set\s+([A-Za-z0-9_-]+)\s+access-port\s+([A-Za-z0-9/._-]+)\s+vlan\s+(\d+)", segment, flags=re.IGNORECASE):
|
|
1021
|
+
ops.append({"op": "set_access_port", "device": device, "port": port, "vlan": int(vlan_id)})
|
|
1022
|
+
trunk_pattern = re.compile(
|
|
1023
|
+
r"set\s+([A-Za-z0-9_-]+)\s+trunk-port\s+([A-Za-z0-9/._-]+)\s+allowed\s+([0-9,\s]+?)(?:\s+native\s+(\d+))?(?=\s+set\s+|$)",
|
|
1024
|
+
flags=re.IGNORECASE,
|
|
1025
|
+
)
|
|
1026
|
+
for segment in _command_segments(prompt):
|
|
1027
|
+
for device, port, allowed, native in trunk_pattern.findall(segment):
|
|
1028
|
+
ops.append(
|
|
1029
|
+
{
|
|
1030
|
+
"op": "set_trunk_port",
|
|
1031
|
+
"device": device,
|
|
1032
|
+
"port": port,
|
|
1033
|
+
"allowed": [int(value) for value in re.findall(r"\d+", allowed)],
|
|
1034
|
+
"native": int(native) if native else None,
|
|
1035
|
+
}
|
|
1036
|
+
)
|
|
1037
|
+
dhcp_snooping_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+dhcp\s+snooping\s+vlan\s+(\d+)(?:\s+trust\s+([A-Za-z0-9/._-]+))?", flags=re.IGNORECASE)
|
|
1038
|
+
dai_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+dai\s+vlan\s+(\d+)(?:\s+trust\s+([A-Za-z0-9/._-]+))?", flags=re.IGNORECASE)
|
|
1039
|
+
port_security_pattern = re.compile(
|
|
1040
|
+
r"set\s+([A-Za-z0-9_-]+)\s+port-security\s+([A-Za-z0-9/._-]+)(?:\s+max\s+(\d+))?(?:\s+violation\s+(protect|restrict|shutdown))?",
|
|
1041
|
+
flags=re.IGNORECASE,
|
|
1042
|
+
)
|
|
1043
|
+
lldp_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+lldp(?:\s+(?:enable|run))?", flags=re.IGNORECASE)
|
|
1044
|
+
rep_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+rep\s+segment\s+(\d+)\s+interface\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
|
|
1045
|
+
span_pattern = re.compile(
|
|
1046
|
+
r"set\s+([A-Za-z0-9_-]+)\s+(?:r?span)\s+(\d+)\s+source\s+([A-Za-z0-9/._-]+)\s+destination\s+([A-Za-z0-9/._-]+)",
|
|
1047
|
+
flags=re.IGNORECASE,
|
|
1048
|
+
)
|
|
1049
|
+
dot1x_pattern = re.compile(
|
|
1050
|
+
r"set\s+([A-Za-z0-9_-]+)\s+dot1x\s+interface\s+([A-Za-z0-9/._-]+)\s+mode\s+(auto|force-authorized|force-unauthorized)"
|
|
1051
|
+
r"(?:\s+radius\s+(\d+\.\d+\.\d+\.\d+)\s+key\s+([A-Za-z0-9._-]+))?",
|
|
1052
|
+
flags=re.IGNORECASE,
|
|
1053
|
+
)
|
|
1054
|
+
qos_pattern = re.compile(
|
|
1055
|
+
r"set\s+([A-Za-z0-9_-]+)\s+qos\s+class-map\s+([A-Za-z0-9_-]+)\s+match\s+(.+?)\s+policy-map\s+([A-Za-z0-9_-]+)\s+class\s+\2\s+(priority|bandwidth\s+\d+|police\s+\d+)"
|
|
1056
|
+
r"\s+service-policy\s+(input|output)\s+([A-Za-z0-9/._-]+)(?=\s+set\s+|$)",
|
|
1057
|
+
flags=re.IGNORECASE,
|
|
1058
|
+
)
|
|
1059
|
+
stp_pattern = re.compile(
|
|
1060
|
+
r"set\s+([A-Za-z0-9_-]+)\s+stp\s+mode\s+([A-Za-z0-9_-]+)(?:\s+vlan\s+(\d+)\s+root\s+(primary|secondary))?",
|
|
1061
|
+
flags=re.IGNORECASE,
|
|
1062
|
+
)
|
|
1063
|
+
etherchannel_pattern = re.compile(
|
|
1064
|
+
r"set\s+([A-Za-z0-9_-]+)\s+etherchannel\s+(\d+)\s+mode\s+(active|passive|desirable|auto|on)\s+interfaces\s+(.+?)(?=\s+set\s+|$)",
|
|
1065
|
+
flags=re.IGNORECASE,
|
|
1066
|
+
)
|
|
1067
|
+
vtp_pattern = re.compile(
|
|
1068
|
+
r"set\s+([A-Za-z0-9_-]+)\s+vtp\s+domain\s+([A-Za-z0-9_.-]+)\s+mode\s+(server|client|transparent)(?:\s+version\s+(\d+))?",
|
|
1069
|
+
flags=re.IGNORECASE,
|
|
1070
|
+
)
|
|
1071
|
+
dtp_pattern = re.compile(
|
|
1072
|
+
r"set\s+([A-Za-z0-9_-]+)\s+dtp\s+interface\s+([A-Za-z0-9/._-]+)\s+mode\s+(dynamic\s+desirable|dynamic\s+auto|trunk|access)",
|
|
1073
|
+
flags=re.IGNORECASE,
|
|
1074
|
+
)
|
|
1075
|
+
for segment in _command_segments(prompt):
|
|
1076
|
+
for device, vlan_id, trust_port in dhcp_snooping_pattern.findall(segment):
|
|
1077
|
+
ops.append({"op": "set_dhcp_snooping", "device": device, "vlan": int(vlan_id), "trust_port": trust_port or None})
|
|
1078
|
+
for device, vlan_id, trust_port in dai_pattern.findall(segment):
|
|
1079
|
+
ops.append({"op": "set_dai", "device": device, "vlan": int(vlan_id), "trust_port": trust_port or None})
|
|
1080
|
+
for device, port, maximum, violation in port_security_pattern.findall(segment):
|
|
1081
|
+
ops.append(
|
|
1082
|
+
{
|
|
1083
|
+
"op": "set_port_security",
|
|
1084
|
+
"device": device,
|
|
1085
|
+
"port": port,
|
|
1086
|
+
"maximum": int(maximum) if maximum else None,
|
|
1087
|
+
"violation": violation.lower() if violation else None,
|
|
1088
|
+
}
|
|
1089
|
+
)
|
|
1090
|
+
for device in lldp_pattern.findall(segment):
|
|
1091
|
+
ops.append({"op": "set_lldp", "device": device})
|
|
1092
|
+
for device, segment_id, interface_name in rep_pattern.findall(segment):
|
|
1093
|
+
ops.append({"op": "set_rep", "device": device, "segment": int(segment_id), "interface": interface_name})
|
|
1094
|
+
for device, session, source, destination in span_pattern.findall(segment):
|
|
1095
|
+
ops.append({"op": "set_span", "device": device, "session": int(session), "source": source, "destination": destination})
|
|
1096
|
+
for device, interface_name, mode, radius_host, radius_key in dot1x_pattern.findall(segment):
|
|
1097
|
+
ops.append(
|
|
1098
|
+
{
|
|
1099
|
+
"op": "set_dot1x",
|
|
1100
|
+
"device": device,
|
|
1101
|
+
"interface": interface_name,
|
|
1102
|
+
"mode": mode.lower(),
|
|
1103
|
+
"radius_host": radius_host or None,
|
|
1104
|
+
"radius_key": radius_key or None,
|
|
1105
|
+
}
|
|
1106
|
+
)
|
|
1107
|
+
for device, class_map, match_expr, policy_map, action, direction, interface_name in qos_pattern.findall(segment):
|
|
1108
|
+
ops.append(
|
|
1109
|
+
{
|
|
1110
|
+
"op": "set_qos_policy",
|
|
1111
|
+
"device": device,
|
|
1112
|
+
"class_map": class_map,
|
|
1113
|
+
"match": match_expr.strip(),
|
|
1114
|
+
"policy_map": policy_map,
|
|
1115
|
+
"action": action.lower(),
|
|
1116
|
+
"direction": direction.lower(),
|
|
1117
|
+
"interface": interface_name,
|
|
1118
|
+
}
|
|
1119
|
+
)
|
|
1120
|
+
for device, mode, vlan_id, root_priority in stp_pattern.findall(segment):
|
|
1121
|
+
ops.append(
|
|
1122
|
+
{
|
|
1123
|
+
"op": "set_stp",
|
|
1124
|
+
"device": device,
|
|
1125
|
+
"mode": mode.lower(),
|
|
1126
|
+
"vlan": int(vlan_id) if vlan_id else None,
|
|
1127
|
+
"root": root_priority.lower() if root_priority else None,
|
|
1128
|
+
}
|
|
1129
|
+
)
|
|
1130
|
+
for device, channel, mode, interfaces_text in etherchannel_pattern.findall(segment):
|
|
1131
|
+
interfaces = [value for value in re.findall(r"[A-Za-z]+[A-Za-z]*[0-9/._-]+", interfaces_text) if value.lower() != "set"]
|
|
1132
|
+
ops.append(
|
|
1133
|
+
{
|
|
1134
|
+
"op": "set_etherchannel",
|
|
1135
|
+
"device": device,
|
|
1136
|
+
"channel": int(channel),
|
|
1137
|
+
"mode": mode.lower(),
|
|
1138
|
+
"interfaces": interfaces,
|
|
1139
|
+
}
|
|
1140
|
+
)
|
|
1141
|
+
for device, domain, mode, version in vtp_pattern.findall(segment):
|
|
1142
|
+
ops.append(
|
|
1143
|
+
{
|
|
1144
|
+
"op": "set_vtp",
|
|
1145
|
+
"device": device,
|
|
1146
|
+
"domain": domain,
|
|
1147
|
+
"mode": mode.lower(),
|
|
1148
|
+
"version": int(version) if version else None,
|
|
1149
|
+
}
|
|
1150
|
+
)
|
|
1151
|
+
for device, interface_name, mode in dtp_pattern.findall(segment):
|
|
1152
|
+
ops.append({"op": "set_dtp", "device": device, "interface": interface_name, "mode": mode.lower()})
|
|
1153
|
+
return ops
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def _extract_router_ops(prompt: str) -> list[dict[str, object]]:
|
|
1157
|
+
ops: list[dict[str, object]] = []
|
|
1158
|
+
subif_pattern = re.compile(
|
|
1159
|
+
r"set\s+([A-Za-z0-9_-]+)\s+subinterface\s+([A-Za-z0-9/._-]+)\s+encapsulation\s+dot1q\s+(\d+)\s+ip\s+(\d+\.\d+\.\d+\.\d+)/(\d+)",
|
|
1160
|
+
flags=re.IGNORECASE,
|
|
1161
|
+
)
|
|
1162
|
+
for segment in _command_segments(prompt):
|
|
1163
|
+
for device, subinterface, vlan_id, ip, prefix in subif_pattern.findall(segment):
|
|
1164
|
+
ops.append({"op": "set_subinterface", "device": device, "subinterface": subinterface, "vlan": int(vlan_id), "ip": ip, "prefix": int(prefix)})
|
|
1165
|
+
dhcp_pattern = re.compile(
|
|
1166
|
+
r"set\s+([A-Za-z0-9_-]+)\s+dhcp\s+pool\s+([A-Za-z0-9_-]+)\s+network\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+gateway\s+(\d+\.\d+\.\d+\.\d+)"
|
|
1167
|
+
r"(?:\s+dns\s+(\d+\.\d+\.\d+\.\d+))?(?:\s+start\s+(\d+\.\d+\.\d+\.\d+))?(?:\s+max\s+(\d+))?",
|
|
1168
|
+
flags=re.IGNORECASE,
|
|
1169
|
+
)
|
|
1170
|
+
for segment in _command_segments(prompt):
|
|
1171
|
+
for device, name, network, prefix, gateway, dns, start, max_users in dhcp_pattern.findall(segment):
|
|
1172
|
+
if str(device).strip().lower().startswith("server"):
|
|
1173
|
+
continue
|
|
1174
|
+
ops.append(
|
|
1175
|
+
{
|
|
1176
|
+
"op": "set_router_dhcp_pool",
|
|
1177
|
+
"device": device,
|
|
1178
|
+
"name": name,
|
|
1179
|
+
"network": network,
|
|
1180
|
+
"prefix": int(prefix),
|
|
1181
|
+
"gateway": gateway,
|
|
1182
|
+
"dns": dns or None,
|
|
1183
|
+
"start": start or None,
|
|
1184
|
+
"max_users": int(max_users) if max_users else None,
|
|
1185
|
+
}
|
|
1186
|
+
)
|
|
1187
|
+
acl_create_pattern = re.compile(r"set\s+([A-Za-z0-9_ -]+?)\s+acl\s+(standard|extended)\s+([A-Za-z0-9_-]+)", flags=re.IGNORECASE)
|
|
1188
|
+
for segment in _command_segments(prompt):
|
|
1189
|
+
for device, acl_kind, acl_name in acl_create_pattern.findall(segment):
|
|
1190
|
+
ops.append({"op": "set_acl", "device": device.strip(), "acl_kind": acl_kind.lower(), "acl_name": acl_name})
|
|
1191
|
+
acl_create_simple_pattern = re.compile(
|
|
1192
|
+
r"create\s+acl\s+([A-Za-z0-9_-]+)\s+on\s+([A-Za-z0-9_ -]+?)(?:\s+kind\s+(standard|extended))?(?=\s+(?:acl\b|apply\b|enable\b|set\b|associate\b|register\b|disable\b)|$)",
|
|
1193
|
+
flags=re.IGNORECASE,
|
|
1194
|
+
)
|
|
1195
|
+
for segment in _command_segments(prompt):
|
|
1196
|
+
for acl_name, device, acl_kind in acl_create_simple_pattern.findall(segment):
|
|
1197
|
+
ops.append(
|
|
1198
|
+
{
|
|
1199
|
+
"op": "set_acl",
|
|
1200
|
+
"device": device.strip(),
|
|
1201
|
+
"acl_kind": (acl_kind or "standard").lower(),
|
|
1202
|
+
"acl_name": acl_name,
|
|
1203
|
+
}
|
|
1204
|
+
)
|
|
1205
|
+
acl_rule_pattern = re.compile(
|
|
1206
|
+
r"acl\s+([A-Za-z0-9_-]+)\s+(permit|deny)\s+(host\s+\d+\.\d+\.\d+\.\d+|\d+\.\d+\.\d+\.\d+\s+\d+\.\d+\.\d+\.\d+|any)"
|
|
1207
|
+
r"(?:\s+(host\s+\d+\.\d+\.\d+\.\d+|\d+\.\d+\.\d+\.\d+\s+\d+\.\d+\.\d+\.\d+|any))?",
|
|
1208
|
+
flags=re.IGNORECASE,
|
|
1209
|
+
)
|
|
1210
|
+
for segment in _command_segments(prompt):
|
|
1211
|
+
for acl_name, action, source, destination in acl_rule_pattern.findall(segment):
|
|
1212
|
+
ops.append({"op": "add_acl_rule", "acl_name": acl_name, "action": action.lower(), "source": source.strip(), "destination": destination.strip() if destination else None})
|
|
1213
|
+
acl_apply_pattern = re.compile(
|
|
1214
|
+
r"apply\s+acl\s+([A-Za-z0-9_-]+)\s+(in|out)\s+on\s+([A-Za-z0-9_ -]+?)(?:\s+interface)?\s+([A-Za-z0-9/._-]+)",
|
|
1215
|
+
flags=re.IGNORECASE,
|
|
1216
|
+
)
|
|
1217
|
+
for segment in _command_segments(prompt):
|
|
1218
|
+
for acl_name, direction, device, interface_name in acl_apply_pattern.findall(segment):
|
|
1219
|
+
ops.append({"op": "apply_acl", "device": device.strip(), "acl_name": acl_name, "direction": direction.lower(), "interface": interface_name})
|
|
1220
|
+
ipv6_unicast_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ipv6\s+unicast-routing", flags=re.IGNORECASE)
|
|
1221
|
+
ipv6_address_pattern = re.compile(
|
|
1222
|
+
r"set\s+([A-Za-z0-9_-]+)\s+(?:interface\s+)?([A-Za-z0-9/._-]+)\s+ipv6\s+([0-9A-Fa-f:]+)/(\d+)",
|
|
1223
|
+
flags=re.IGNORECASE,
|
|
1224
|
+
)
|
|
1225
|
+
ipv6_slaac_pattern = re.compile(
|
|
1226
|
+
r"set\s+([A-Za-z0-9_-]+)\s+slaac\s+on\s+([A-Za-z0-9/._-]+)(?:\s+prefix\s+([0-9A-Fa-f:]+)/(\d+))?",
|
|
1227
|
+
flags=re.IGNORECASE,
|
|
1228
|
+
)
|
|
1229
|
+
dhcpv6_pattern = re.compile(
|
|
1230
|
+
r"set\s+([A-Za-z0-9_-]+)\s+dhcpv6\s+pool\s+([A-Za-z0-9_-]+)\s+prefix\s+([0-9A-Fa-f:]+)/(\d+)\s+interface\s+([A-Za-z0-9/._-]+)"
|
|
1231
|
+
r"(?:\s+dns\s+([0-9A-Fa-f:]+))?(?:\s+domain\s+([A-Za-z0-9._-]+))?",
|
|
1232
|
+
flags=re.IGNORECASE,
|
|
1233
|
+
)
|
|
1234
|
+
ospfv3_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ospfv3\s+(\d+)\s+area\s+(\d+)\s+interface\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
|
|
1235
|
+
eigrp_ipv6_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+eigrp\s+ipv6\s+(\d+)\s+interface\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
|
|
1236
|
+
ripng_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ripng\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
|
|
1237
|
+
hsrp_pattern = re.compile(
|
|
1238
|
+
r"set\s+([A-Za-z0-9_-]+)\s+hsrp\s+(\d+)\s+ipv6\s+([0-9A-Fa-f:]+)\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+priority\s+(\d+))?",
|
|
1239
|
+
flags=re.IGNORECASE,
|
|
1240
|
+
)
|
|
1241
|
+
ospfv2_pattern = re.compile(
|
|
1242
|
+
r"set\s+([A-Za-z0-9_-]+)\s+ospfv2\s+(\d+)\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+wildcard\s+(\d+\.\d+\.\d+\.\d+)\s+area\s+(\d+)",
|
|
1243
|
+
flags=re.IGNORECASE,
|
|
1244
|
+
)
|
|
1245
|
+
eigrp_ipv4_pattern = re.compile(
|
|
1246
|
+
r"set\s+([A-Za-z0-9_-]+)\s+eigrp\s+ipv4\s+(\d+)\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+wildcard\s+(\d+\.\d+\.\d+\.\d+)(?:\s+(no-auto-summary))?",
|
|
1247
|
+
flags=re.IGNORECASE,
|
|
1248
|
+
)
|
|
1249
|
+
ripv2_pattern = re.compile(
|
|
1250
|
+
r"set\s+([A-Za-z0-9_-]+)\s+rip\s+version\s+2\s+network\s+(\d+\.\d+\.\d+\.\d+)(?:\s+(no-auto-summary))?",
|
|
1251
|
+
flags=re.IGNORECASE,
|
|
1252
|
+
)
|
|
1253
|
+
static_route_pattern = re.compile(
|
|
1254
|
+
r"set\s+([A-Za-z0-9_-]+)\s+static-route\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+via\s+(\d+\.\d+\.\d+\.\d+)",
|
|
1255
|
+
flags=re.IGNORECASE,
|
|
1256
|
+
)
|
|
1257
|
+
dhcp_relay_pattern = re.compile(
|
|
1258
|
+
r"set\s+([A-Za-z0-9_-]+)\s+dhcp-relay\s+interface\s+([A-Za-z0-9/._-]+)\s+helper\s+(\d+\.\d+\.\d+\.\d+)",
|
|
1259
|
+
flags=re.IGNORECASE,
|
|
1260
|
+
)
|
|
1261
|
+
nat_inside_outside_pattern = re.compile(
|
|
1262
|
+
r"set\s+([A-Za-z0-9_-]+)\s+nat\s+(inside|outside)\s+interface\s+([A-Za-z0-9/._-]+)",
|
|
1263
|
+
flags=re.IGNORECASE,
|
|
1264
|
+
)
|
|
1265
|
+
nat_static_pattern = re.compile(
|
|
1266
|
+
r"set\s+([A-Za-z0-9_-]+)\s+nat\s+static\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)",
|
|
1267
|
+
flags=re.IGNORECASE,
|
|
1268
|
+
)
|
|
1269
|
+
pat_pattern = re.compile(
|
|
1270
|
+
r"set\s+([A-Za-z0-9_-]+)\s+pat\s+acl\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+(overload))?",
|
|
1271
|
+
flags=re.IGNORECASE,
|
|
1272
|
+
)
|
|
1273
|
+
ssh_ios_pattern = re.compile(
|
|
1274
|
+
r"set\s+([A-Za-z0-9_-]+)\s+ssh\s+domain\s+([A-Za-z0-9._-]+)\s+username\s+([A-Za-z0-9._-]+)\s+password\s+([A-Za-z0-9._-]+)(?:\s+modulus\s+(\d+))?",
|
|
1275
|
+
flags=re.IGNORECASE,
|
|
1276
|
+
)
|
|
1277
|
+
ntp_ios_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ntp\s+server\s+(\d+\.\d+\.\d+\.\d+)", flags=re.IGNORECASE)
|
|
1278
|
+
syslog_ios_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+syslog\s+server\s+(\d+\.\d+\.\d+\.\d+)", flags=re.IGNORECASE)
|
|
1279
|
+
bgp_pattern = re.compile(
|
|
1280
|
+
r"set\s+([A-Za-z0-9_-]+)\s+bgp\s+(\d+)\s+neighbor\s+(\d+\.\d+\.\d+\.\d+)\s+remote-as\s+(\d+)"
|
|
1281
|
+
r"(?:\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+mask\s+(\d+\.\d+\.\d+\.\d+))?",
|
|
1282
|
+
flags=re.IGNORECASE,
|
|
1283
|
+
)
|
|
1284
|
+
snmp_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+snmp\s+community\s+([A-Za-z0-9_-]+)\s+(ro|rw)", flags=re.IGNORECASE)
|
|
1285
|
+
netflow_pattern = re.compile(
|
|
1286
|
+
r"set\s+([A-Za-z0-9_-]+)\s+netflow\s+destination\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+)(?:\s+version\s+(\d+))?(?:\s+interface\s+([A-Za-z0-9/._-]+)\s+(ingress|egress))?",
|
|
1287
|
+
flags=re.IGNORECASE,
|
|
1288
|
+
)
|
|
1289
|
+
gre_tunnel_pattern = re.compile(
|
|
1290
|
+
r"set\s+([A-Za-z0-9_-]+)\s+gre\s+tunnel\s+([A-Za-z0-9/._-]+)\s+source\s+([A-Za-z0-9/._:-]+)\s+destination\s+([A-Za-z0-9/._:-]+)"
|
|
1291
|
+
r"(?:\s+ip\s+(\d+\.\d+\.\d+\.\d+)/(\d+))?",
|
|
1292
|
+
flags=re.IGNORECASE,
|
|
1293
|
+
)
|
|
1294
|
+
ppp_pattern = re.compile(
|
|
1295
|
+
r"set\s+([A-Za-z0-9_-]+)\s+ppp\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+authentication\s+(chap|pap))?",
|
|
1296
|
+
flags=re.IGNORECASE,
|
|
1297
|
+
)
|
|
1298
|
+
ipsec_transform_pattern = re.compile(
|
|
1299
|
+
r"set\s+([A-Za-z0-9_-]+)\s+ipsec\s+transform-set\s+([A-Za-z0-9_-]+)\s+([A-Za-z0-9_-]+)\s+([A-Za-z0-9_-]+)",
|
|
1300
|
+
flags=re.IGNORECASE,
|
|
1301
|
+
)
|
|
1302
|
+
crypto_map_pattern = re.compile(
|
|
1303
|
+
r"set\s+([A-Za-z0-9_-]+)\s+crypto\s+map\s+([A-Za-z0-9_-]+)\s+(\d+)\s+peer\s+(\d+\.\d+\.\d+\.\d+)\s+transform-set\s+([A-Za-z0-9_-]+)\s+match\s+([A-Za-z0-9_-]+)(?:\s+interface\s+([A-Za-z0-9/._-]+))?",
|
|
1304
|
+
flags=re.IGNORECASE,
|
|
1305
|
+
)
|
|
1306
|
+
cbac_pattern = re.compile(
|
|
1307
|
+
r"set\s+([A-Za-z0-9_-]+)\s+cbac\s+inspect\s+([A-Za-z0-9_-]+)\s+protocol\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)\s+direction\s+(in|out)",
|
|
1308
|
+
flags=re.IGNORECASE,
|
|
1309
|
+
)
|
|
1310
|
+
zfw_zone_interface_pattern = re.compile(
|
|
1311
|
+
r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+zone\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)",
|
|
1312
|
+
flags=re.IGNORECASE,
|
|
1313
|
+
)
|
|
1314
|
+
zfw_zone_pair_pattern = re.compile(
|
|
1315
|
+
r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+zone-pair\s+([A-Za-z0-9_-]+)\s+source\s+([A-Za-z0-9_-]+)\s+destination\s+([A-Za-z0-9_-]+)\s+policy\s+([A-Za-z0-9_-]+)",
|
|
1316
|
+
flags=re.IGNORECASE,
|
|
1317
|
+
)
|
|
1318
|
+
zfw_policy_pattern = re.compile(
|
|
1319
|
+
r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+class-map\s+([A-Za-z0-9_-]+)\s+match\s+protocol\s+([A-Za-z0-9_-]+)\s+policy-map\s+([A-Za-z0-9_-]+)\s+action\s+(inspect|pass|drop)",
|
|
1320
|
+
flags=re.IGNORECASE,
|
|
1321
|
+
)
|
|
1322
|
+
telephony_service_pattern = re.compile(
|
|
1323
|
+
r'set\s+"?([^"]+?)"?\s+telephony\s+service\s+source-address\s+(\d+\.\d+\.\d+\.\d+)\s+port\s+(\d+)'
|
|
1324
|
+
r"(?:\s+max-ephones\s+(\d+))?(?:\s+max-dn\s+(\d+))?",
|
|
1325
|
+
flags=re.IGNORECASE,
|
|
1326
|
+
)
|
|
1327
|
+
ephone_dn_pattern = re.compile(r'set\s+"?([^"]+?)"?\s+ephone-dn\s+(\d+)\s+number\s+([A-Za-z0-9*+#.-]+)', flags=re.IGNORECASE)
|
|
1328
|
+
ephone_pattern = re.compile(
|
|
1329
|
+
r'set\s+"?([^"]+?)"?\s+ephone\s+(\d+)\s+mac\s+([0-9A-Fa-f.:-]+)\s+button\s+([0-9:]+)',
|
|
1330
|
+
flags=re.IGNORECASE,
|
|
1331
|
+
)
|
|
1332
|
+
dial_peer_pattern = re.compile(
|
|
1333
|
+
r'set\s+"?([^"]+?)"?\s+dial-peer\s+voice\s+(\d+)\s+destination-pattern\s+(\S+)\s+session-target\s+ipv4:(\d+\.\d+\.\d+\.\d+)',
|
|
1334
|
+
flags=re.IGNORECASE,
|
|
1335
|
+
)
|
|
1336
|
+
for segment in _command_segments(prompt):
|
|
1337
|
+
for device in ipv6_unicast_pattern.findall(segment):
|
|
1338
|
+
ops.append({"op": "enable_ipv6_unicast_routing", "device": device})
|
|
1339
|
+
for device, interface_name, address, prefix in ipv6_address_pattern.findall(segment):
|
|
1340
|
+
ops.append({"op": "set_ipv6_address", "device": device, "interface": interface_name, "address": address, "prefix": int(prefix)})
|
|
1341
|
+
for device, interface_name, prefix_address, prefix_len in ipv6_slaac_pattern.findall(segment):
|
|
1342
|
+
ops.append(
|
|
1343
|
+
{
|
|
1344
|
+
"op": "set_ipv6_slaac",
|
|
1345
|
+
"device": device,
|
|
1346
|
+
"interface": interface_name,
|
|
1347
|
+
"prefix": prefix_address or None,
|
|
1348
|
+
"prefix_len": int(prefix_len) if prefix_len else None,
|
|
1349
|
+
}
|
|
1350
|
+
)
|
|
1351
|
+
for device, name, prefix_address, prefix_len, interface_name, dns, domain in dhcpv6_pattern.findall(segment):
|
|
1352
|
+
ops.append(
|
|
1353
|
+
{
|
|
1354
|
+
"op": "set_dhcpv6_pool",
|
|
1355
|
+
"device": device,
|
|
1356
|
+
"name": name,
|
|
1357
|
+
"prefix": prefix_address,
|
|
1358
|
+
"prefix_len": int(prefix_len),
|
|
1359
|
+
"interface": interface_name,
|
|
1360
|
+
"dns": dns or None,
|
|
1361
|
+
"domain": domain or None,
|
|
1362
|
+
}
|
|
1363
|
+
)
|
|
1364
|
+
for device, process_id, area, interface_name in ospfv3_pattern.findall(segment):
|
|
1365
|
+
ops.append({"op": "set_ospfv3_interface", "device": device, "process_id": int(process_id), "area": int(area), "interface": interface_name})
|
|
1366
|
+
for device, asn, interface_name in eigrp_ipv6_pattern.findall(segment):
|
|
1367
|
+
ops.append({"op": "set_eigrp_ipv6_interface", "device": device, "asn": int(asn), "interface": interface_name})
|
|
1368
|
+
for device, process_name, interface_name in ripng_pattern.findall(segment):
|
|
1369
|
+
ops.append({"op": "set_ripng_interface", "device": device, "process_name": process_name, "interface": interface_name})
|
|
1370
|
+
for device, group, virtual_ipv6, interface_name, priority in hsrp_pattern.findall(segment):
|
|
1371
|
+
ops.append(
|
|
1372
|
+
{
|
|
1373
|
+
"op": "set_hsrp_ipv6",
|
|
1374
|
+
"device": device,
|
|
1375
|
+
"group": int(group),
|
|
1376
|
+
"virtual_ipv6": virtual_ipv6,
|
|
1377
|
+
"interface": interface_name,
|
|
1378
|
+
"priority": int(priority) if priority else None,
|
|
1379
|
+
}
|
|
1380
|
+
)
|
|
1381
|
+
for device, process_id, network, wildcard, area in ospfv2_pattern.findall(segment):
|
|
1382
|
+
ops.append({"op": "set_ospfv2_network", "device": device, "process_id": int(process_id), "network": network, "wildcard": wildcard, "area": int(area)})
|
|
1383
|
+
for device, asn, network, wildcard, no_auto in eigrp_ipv4_pattern.findall(segment):
|
|
1384
|
+
ops.append({"op": "set_eigrp_ipv4_network", "device": device, "asn": int(asn), "network": network, "wildcard": wildcard, "no_auto_summary": bool(no_auto)})
|
|
1385
|
+
for device, network, no_auto in ripv2_pattern.findall(segment):
|
|
1386
|
+
ops.append({"op": "set_ripv2_network", "device": device, "network": network, "no_auto_summary": bool(no_auto)})
|
|
1387
|
+
for device, network, prefix, next_hop in static_route_pattern.findall(segment):
|
|
1388
|
+
ops.append({"op": "set_static_route", "device": device, "network": network, "prefix": int(prefix), "next_hop": next_hop})
|
|
1389
|
+
for device, interface_name, helper in dhcp_relay_pattern.findall(segment):
|
|
1390
|
+
ops.append({"op": "set_dhcp_relay", "device": device, "interface": interface_name, "helper": helper})
|
|
1391
|
+
for device, role, interface_name in nat_inside_outside_pattern.findall(segment):
|
|
1392
|
+
ops.append({"op": "set_nat_interface", "device": device, "role": role.lower(), "interface": interface_name})
|
|
1393
|
+
for device, inside_local, inside_global in nat_static_pattern.findall(segment):
|
|
1394
|
+
ops.append({"op": "set_nat_static", "device": device, "inside_local": inside_local, "inside_global": inside_global})
|
|
1395
|
+
for device, acl, interface_name, overload in pat_pattern.findall(segment):
|
|
1396
|
+
ops.append({"op": "set_pat_overload", "device": device, "acl": acl, "interface": interface_name, "overload": bool(overload)})
|
|
1397
|
+
for device, domain, username, password, modulus in ssh_ios_pattern.findall(segment):
|
|
1398
|
+
ops.append({"op": "set_ssh_ios", "device": device, "domain": domain, "username": username, "password": password, "modulus": int(modulus) if modulus else 1024})
|
|
1399
|
+
for device, server in ntp_ios_pattern.findall(segment):
|
|
1400
|
+
ops.append({"op": "set_ntp_server", "device": device, "server": server})
|
|
1401
|
+
for device, server in syslog_ios_pattern.findall(segment):
|
|
1402
|
+
ops.append({"op": "set_syslog_server", "device": device, "server": server})
|
|
1403
|
+
for device, asn, neighbor, remote_as, network, mask in bgp_pattern.findall(segment):
|
|
1404
|
+
ops.append(
|
|
1405
|
+
{
|
|
1406
|
+
"op": "set_bgp_neighbor",
|
|
1407
|
+
"device": device,
|
|
1408
|
+
"asn": int(asn),
|
|
1409
|
+
"neighbor": neighbor,
|
|
1410
|
+
"remote_as": int(remote_as),
|
|
1411
|
+
"network": network or None,
|
|
1412
|
+
"mask": mask or None,
|
|
1413
|
+
}
|
|
1414
|
+
)
|
|
1415
|
+
for device, community, mode in snmp_pattern.findall(segment):
|
|
1416
|
+
ops.append({"op": "set_snmp_community", "device": device, "community": community, "mode": mode.lower()})
|
|
1417
|
+
for device, destination, port, version, interface_name, direction in netflow_pattern.findall(segment):
|
|
1418
|
+
ops.append(
|
|
1419
|
+
{
|
|
1420
|
+
"op": "set_netflow",
|
|
1421
|
+
"device": device,
|
|
1422
|
+
"destination": destination,
|
|
1423
|
+
"port": int(port),
|
|
1424
|
+
"version": int(version) if version else 9,
|
|
1425
|
+
"interface": interface_name or None,
|
|
1426
|
+
"direction": direction.lower() if direction else None,
|
|
1427
|
+
}
|
|
1428
|
+
)
|
|
1429
|
+
for device, tunnel_interface, source, destination, ip, prefix in gre_tunnel_pattern.findall(segment):
|
|
1430
|
+
ops.append(
|
|
1431
|
+
{
|
|
1432
|
+
"op": "set_gre_tunnel",
|
|
1433
|
+
"device": device,
|
|
1434
|
+
"interface": tunnel_interface,
|
|
1435
|
+
"source": source,
|
|
1436
|
+
"destination": destination,
|
|
1437
|
+
"ip": ip or None,
|
|
1438
|
+
"prefix": int(prefix) if prefix else None,
|
|
1439
|
+
}
|
|
1440
|
+
)
|
|
1441
|
+
for device, interface_name, authentication in ppp_pattern.findall(segment):
|
|
1442
|
+
ops.append(
|
|
1443
|
+
{
|
|
1444
|
+
"op": "set_ppp_interface",
|
|
1445
|
+
"device": device,
|
|
1446
|
+
"interface": interface_name,
|
|
1447
|
+
"authentication": authentication.lower() if authentication else None,
|
|
1448
|
+
}
|
|
1449
|
+
)
|
|
1450
|
+
for device, name, encryption, integrity in ipsec_transform_pattern.findall(segment):
|
|
1451
|
+
ops.append(
|
|
1452
|
+
{
|
|
1453
|
+
"op": "set_ipsec_transform_set",
|
|
1454
|
+
"device": device,
|
|
1455
|
+
"name": name,
|
|
1456
|
+
"encryption": encryption,
|
|
1457
|
+
"integrity": integrity,
|
|
1458
|
+
}
|
|
1459
|
+
)
|
|
1460
|
+
for device, map_name, sequence, peer, transform_set, acl_name, interface_name in crypto_map_pattern.findall(segment):
|
|
1461
|
+
ops.append(
|
|
1462
|
+
{
|
|
1463
|
+
"op": "set_crypto_map",
|
|
1464
|
+
"device": device,
|
|
1465
|
+
"map_name": map_name,
|
|
1466
|
+
"sequence": int(sequence),
|
|
1467
|
+
"peer": peer,
|
|
1468
|
+
"transform_set": transform_set,
|
|
1469
|
+
"acl_name": acl_name,
|
|
1470
|
+
"interface": interface_name or None,
|
|
1471
|
+
}
|
|
1472
|
+
)
|
|
1473
|
+
for device, name, protocol, interface_name, direction in cbac_pattern.findall(segment):
|
|
1474
|
+
ops.append(
|
|
1475
|
+
{
|
|
1476
|
+
"op": "set_cbac_inspect",
|
|
1477
|
+
"device": device,
|
|
1478
|
+
"name": name,
|
|
1479
|
+
"protocol": protocol.lower(),
|
|
1480
|
+
"interface": interface_name,
|
|
1481
|
+
"direction": direction.lower(),
|
|
1482
|
+
}
|
|
1483
|
+
)
|
|
1484
|
+
for device, zone, interface_name in zfw_zone_interface_pattern.findall(segment):
|
|
1485
|
+
ops.append({"op": "set_zfw_zone_interface", "device": device, "zone": zone, "interface": interface_name})
|
|
1486
|
+
for device, pair_name, source, destination, policy in zfw_zone_pair_pattern.findall(segment):
|
|
1487
|
+
ops.append(
|
|
1488
|
+
{
|
|
1489
|
+
"op": "set_zfw_zone_pair",
|
|
1490
|
+
"device": device,
|
|
1491
|
+
"pair_name": pair_name,
|
|
1492
|
+
"source": source,
|
|
1493
|
+
"destination": destination,
|
|
1494
|
+
"policy": policy,
|
|
1495
|
+
}
|
|
1496
|
+
)
|
|
1497
|
+
for device, class_map, protocol, policy_map, action in zfw_policy_pattern.findall(segment):
|
|
1498
|
+
ops.append(
|
|
1499
|
+
{
|
|
1500
|
+
"op": "set_zfw_policy",
|
|
1501
|
+
"device": device,
|
|
1502
|
+
"class_map": class_map,
|
|
1503
|
+
"protocol": protocol.lower(),
|
|
1504
|
+
"policy_map": policy_map,
|
|
1505
|
+
"action": action.lower(),
|
|
1506
|
+
}
|
|
1507
|
+
)
|
|
1508
|
+
for device, source_address, port, max_ephones, max_dn in telephony_service_pattern.findall(segment):
|
|
1509
|
+
ops.append(
|
|
1510
|
+
{
|
|
1511
|
+
"op": "set_telephony_service",
|
|
1512
|
+
"device": device.strip(),
|
|
1513
|
+
"source_address": source_address,
|
|
1514
|
+
"port": int(port),
|
|
1515
|
+
"max_ephones": int(max_ephones) if max_ephones else None,
|
|
1516
|
+
"max_dn": int(max_dn) if max_dn else None,
|
|
1517
|
+
}
|
|
1518
|
+
)
|
|
1519
|
+
for device, dn_id, number in ephone_dn_pattern.findall(segment):
|
|
1520
|
+
ops.append({"op": "set_ephone_dn", "device": device.strip(), "dn_id": int(dn_id), "number": number})
|
|
1521
|
+
for device, ephone_id, mac, button in ephone_pattern.findall(segment):
|
|
1522
|
+
ops.append({"op": "set_ephone", "device": device.strip(), "ephone_id": int(ephone_id), "mac": mac.upper(), "button": button})
|
|
1523
|
+
for device, peer_id, destination_pattern, session_target in dial_peer_pattern.findall(segment):
|
|
1524
|
+
ops.append(
|
|
1525
|
+
{
|
|
1526
|
+
"op": "set_dial_peer_voice",
|
|
1527
|
+
"device": device.strip(),
|
|
1528
|
+
"peer_id": int(peer_id),
|
|
1529
|
+
"destination_pattern": destination_pattern,
|
|
1530
|
+
"session_target": session_target,
|
|
1531
|
+
}
|
|
1532
|
+
)
|
|
1533
|
+
return ops
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
def _extract_server_ops(prompt: str) -> list[dict[str, object]]:
|
|
1537
|
+
ops: list[dict[str, object]] = []
|
|
1538
|
+
dns_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+dns\s+(A|CNAME)\s+([A-Za-z0-9._-]+)\s+([A-Za-z0-9._-]+)", flags=re.IGNORECASE)
|
|
1539
|
+
for segment in _command_segments(prompt):
|
|
1540
|
+
for device, record_type, name, value in dns_pattern.findall(segment):
|
|
1541
|
+
ops.append({"op": "set_server_dns_record", "device": device, "record_type": record_type.upper(), "name": name, "value": value})
|
|
1542
|
+
email_domain_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+email\s+domain\s+([A-Za-z0-9._-]+)", flags=re.IGNORECASE)
|
|
1543
|
+
for segment in _command_segments(prompt):
|
|
1544
|
+
for device, domain in email_domain_pattern.findall(segment):
|
|
1545
|
+
ops.append({"op": "set_server_email_domain", "device": device, "domain": domain})
|
|
1546
|
+
aaa_auth_port_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+aaa\s+auth-port\s+(\d+)", flags=re.IGNORECASE)
|
|
1547
|
+
for segment in _command_segments(prompt):
|
|
1548
|
+
for device, port in aaa_auth_port_pattern.findall(segment):
|
|
1549
|
+
ops.append({"op": "set_server_aaa_auth_port", "device": device, "auth_port": int(port)})
|
|
1550
|
+
dhcp_pattern = re.compile(
|
|
1551
|
+
r"set\s+([A-Za-z0-9_-]+)\s+(server-dhcp|dhcp)\s+pool\s+([A-Za-z0-9_-]+)\s+network\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+gateway\s+(\d+\.\d+\.\d+\.\d+)"
|
|
1552
|
+
r"(?:\s+dns\s+(\d+\.\d+\.\d+\.\d+))?(?:\s+start\s+(\d+\.\d+\.\d+\.\d+))?(?:\s+max\s+(\d+))?",
|
|
1553
|
+
flags=re.IGNORECASE,
|
|
1554
|
+
)
|
|
1555
|
+
for segment in _command_segments(prompt):
|
|
1556
|
+
for device, mode, name, network, prefix, gateway, dns, start, max_users in dhcp_pattern.findall(segment):
|
|
1557
|
+
if mode.lower() != "server-dhcp" and not str(device).strip().lower().startswith("server"):
|
|
1558
|
+
continue
|
|
1559
|
+
ops.append(
|
|
1560
|
+
{
|
|
1561
|
+
"op": "set_server_dhcp_pool",
|
|
1562
|
+
"device": device,
|
|
1563
|
+
"name": name,
|
|
1564
|
+
"network": network,
|
|
1565
|
+
"prefix": int(prefix),
|
|
1566
|
+
"gateway": gateway,
|
|
1567
|
+
"dns": dns or None,
|
|
1568
|
+
"start": start or None,
|
|
1569
|
+
"max_users": int(max_users) if max_users else 0,
|
|
1570
|
+
}
|
|
1571
|
+
)
|
|
1572
|
+
for service, device in re.findall(r"enable\s+(dns|http|https|ftp|tftp|ntp|email|syslog|aaa)\s+on\s+([A-Za-z0-9_ -]+)", segment, flags=re.IGNORECASE):
|
|
1573
|
+
ops.append({"op": "enable_server_service", "device": device.strip(), "service": service.lower()})
|
|
1574
|
+
return ops
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def _extract_management_ops(prompt: str) -> list[dict[str, object]]:
|
|
1578
|
+
ops: list[dict[str, object]] = []
|
|
1579
|
+
mgmt_pattern = re.compile(r"set\s+([A-Za-z0-9_ -]+?)\s+management\s+vlan\s+(\d+)\s+ip\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+gateway\s+(\d+\.\d+\.\d+\.\d+)", flags=re.IGNORECASE)
|
|
1580
|
+
for segment in _command_segments(prompt):
|
|
1581
|
+
for device, vlan_id, ip, prefix, gateway in mgmt_pattern.findall(segment):
|
|
1582
|
+
ops.append({"op": "set_management_vlan", "device": device.strip(), "vlan": int(vlan_id), "ip": ip, "prefix": int(prefix), "gateway": gateway})
|
|
1583
|
+
telnet_pattern = re.compile(r"enable\s+telnet\s+on\s+([A-Za-z0-9_ -]+?)\s+username\s+([A-Za-z0-9._-]+)\s+password\s+([A-Za-z0-9._-]+)", flags=re.IGNORECASE)
|
|
1584
|
+
for segment in _command_segments(prompt):
|
|
1585
|
+
for device, username, password in telnet_pattern.findall(segment):
|
|
1586
|
+
ops.append({"op": "enable_telnet", "device": device.strip(), "username": username, "password": password})
|
|
1587
|
+
return ops
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
def _extract_wireless_settings(normalized_prompt: str) -> dict[str, object]:
|
|
1591
|
+
"""SSID, security and passphrase as an ordinary sentence names them.
|
|
1592
|
+
|
|
1593
|
+
`_extract_wireless_ops` only understands the command form
|
|
1594
|
+
`set AP1 ssid TEST security wpa2-psk passphrase test12345`. Nobody writing a
|
|
1595
|
+
prompt types that, so "ssid EvSebeke wpa2 sifre 12345678" produced no
|
|
1596
|
+
wireless operations at all -- the same shape as the DHCP and telnet gaps.
|
|
1597
|
+
"""
|
|
1598
|
+
settings: dict[str, object] = {}
|
|
1599
|
+
ssid = re.search(r"\bssid\s+(?:adi\s+)?([a-z0-9._-]{2,32})\b", normalized_prompt)
|
|
1600
|
+
# `olsun`/`ile`/`ve` are ordinary words that can follow `ssid`; reading one
|
|
1601
|
+
# as a network name would silently misname the network.
|
|
1602
|
+
if ssid and ssid.group(1) not in {"olsun", "ile", "ve", "adi", "ismi", "qur"}:
|
|
1603
|
+
settings["ssid"] = ssid.group(1)
|
|
1604
|
+
|
|
1605
|
+
for key in ("wpa2-enterprise", "wpa-enterprise", "wpa2-psk", "wpa-psk", "wpa2", "wpa", "wep", "open"):
|
|
1606
|
+
if re.search(rf"\b{re.escape(key)}\b", normalized_prompt):
|
|
1607
|
+
settings["security"] = key
|
|
1608
|
+
break
|
|
1609
|
+
|
|
1610
|
+
# `sifre`/`parol` are the Azerbaijani words; `password`/`passphrase` the English.
|
|
1611
|
+
secret = re.search(
|
|
1612
|
+
r"\b(?:sifre|sifresi|parol|parolu|password|passphrase|key)\s+([a-z0-9._-]{4,63})\b",
|
|
1613
|
+
normalized_prompt,
|
|
1614
|
+
)
|
|
1615
|
+
if secret:
|
|
1616
|
+
settings["passphrase"] = secret.group(1)
|
|
1617
|
+
|
|
1618
|
+
channel = re.search(r"\bchannel\s+(\d{1,2})\b", normalized_prompt)
|
|
1619
|
+
if channel:
|
|
1620
|
+
settings["channel"] = int(channel.group(1))
|
|
1621
|
+
return settings
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def _restore_wireless_casing(settings: dict[str, object], raw_prompt: str) -> dict[str, object]:
|
|
1625
|
+
"""Give the SSID and passphrase back the capitalisation the user typed.
|
|
1626
|
+
|
|
1627
|
+
Normalisation lowercases everything so the patterns can be simple, but a
|
|
1628
|
+
network name and a passphrase are user-visible strings -- `EvSebeke` must
|
|
1629
|
+
not reach Packet Tracer as `evsebeke`, and a lowercased passphrase would not
|
|
1630
|
+
even be the same secret.
|
|
1631
|
+
"""
|
|
1632
|
+
restored = dict(settings)
|
|
1633
|
+
for key in ("ssid", "passphrase"):
|
|
1634
|
+
value = str(restored.get(key) or "")
|
|
1635
|
+
if not value:
|
|
1636
|
+
continue
|
|
1637
|
+
match = re.search(rf"\b{re.escape(value)}\b", raw_prompt, flags=re.IGNORECASE)
|
|
1638
|
+
if match:
|
|
1639
|
+
restored[key] = match.group(0)
|
|
1640
|
+
return restored
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
def _extract_wireless_ops(prompt: str) -> list[dict[str, object]]:
|
|
1644
|
+
ops: list[dict[str, object]] = []
|
|
1645
|
+
ssid_pattern = re.compile(
|
|
1646
|
+
r"set\s+([A-Za-z0-9_ -]+?)\s+ssid\s+([A-Za-z0-9._-]+)"
|
|
1647
|
+
r"(?:\s+security\s+(wpa2?\s+enterprise|wpa2?-enterprise|wpa2?-psk|802\.1x|wep|open|wpa2?|wpa))?"
|
|
1648
|
+
r"(?:\s+passphrase\s+([A-Za-z0-9._-]+))?"
|
|
1649
|
+
r"(?:\s+radius\s+(\d+\.\d+\.\d+\.\d+)\s+secret\s+([A-Za-z0-9._-]+))?"
|
|
1650
|
+
r"(?:\s+channel\s+(\d+))?",
|
|
1651
|
+
flags=re.IGNORECASE,
|
|
1652
|
+
)
|
|
1653
|
+
for segment in _command_segments(prompt):
|
|
1654
|
+
for device, ssid, security, passphrase, radius_server, radius_secret, channel in ssid_pattern.findall(segment):
|
|
1655
|
+
security_key = (security or "open").lower().replace(" ", "-")
|
|
1656
|
+
auth_type, encrypt_type = SECURITY_TO_AUTH.get(security_key, ("0", "0"))
|
|
1657
|
+
operation = {
|
|
1658
|
+
"op": "set_wireless_ssid",
|
|
1659
|
+
"device": device.strip(),
|
|
1660
|
+
"ssid": ssid,
|
|
1661
|
+
"security": security_key,
|
|
1662
|
+
"auth_type": auth_type,
|
|
1663
|
+
"encrypt_type": encrypt_type,
|
|
1664
|
+
"passphrase": passphrase or "",
|
|
1665
|
+
"channel": int(channel) if channel else 1,
|
|
1666
|
+
}
|
|
1667
|
+
if radius_server:
|
|
1668
|
+
operation["radius_server"] = radius_server
|
|
1669
|
+
operation["radius_secret"] = radius_secret or ""
|
|
1670
|
+
ops.append(operation)
|
|
1671
|
+
assoc_pattern = re.compile(r"associate\s+([A-Za-z0-9_ -]+?)\s+to\s+([A-Za-z0-9_ -]+?)\s+ssid\s+([A-Za-z0-9._-]+)(?:\s+(dhcp|static))?", flags=re.IGNORECASE)
|
|
1672
|
+
for segment in _command_segments(prompt):
|
|
1673
|
+
for client, ap, ssid, ip_mode in assoc_pattern.findall(segment):
|
|
1674
|
+
ops.append({"op": "associate_wireless_client", "device": client.strip(), "ap": ap.strip(), "ssid": ssid, "ip_mode": (ip_mode or "dhcp").lower()})
|
|
1675
|
+
return ops
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def _extract_end_device_ops(prompt: str) -> list[dict[str, object]]:
|
|
1679
|
+
ops: list[dict[str, object]] = []
|
|
1680
|
+
ip_pattern = re.compile(
|
|
1681
|
+
r"(?:set|change|update)\s+([A-Za-z0-9_-]+)\s+ip\s+(\d+\.\d+\.\d+\.\d+)"
|
|
1682
|
+
r"(?:\s+mask\s+(\d+\.\d+\.\d+\.\d+))?"
|
|
1683
|
+
r"(?:\s+gw\s+(\d+\.\d+\.\d+\.\d+))?"
|
|
1684
|
+
r"(?:\s+dns\s+(\d+\.\d+\.\d+\.\d+))?",
|
|
1685
|
+
flags=re.IGNORECASE,
|
|
1686
|
+
)
|
|
1687
|
+
for segment in _command_segments(prompt):
|
|
1688
|
+
for device, ip, mask, gw, dns in ip_pattern.findall(segment):
|
|
1689
|
+
operation: dict[str, object] = {"op": "set_host_ip", "device": device, "ip": ip}
|
|
1690
|
+
if mask:
|
|
1691
|
+
operation["mask"] = mask
|
|
1692
|
+
if gw:
|
|
1693
|
+
operation["gw"] = gw
|
|
1694
|
+
if dns:
|
|
1695
|
+
operation["dns"] = dns
|
|
1696
|
+
ops.append(operation)
|
|
1697
|
+
for device in re.findall(r"set\s+([A-Za-z0-9_-]+)\s+ipv4\s+dhcp", segment, flags=re.IGNORECASE):
|
|
1698
|
+
ops.append({"op": "set_host_dhcp", "device": device})
|
|
1699
|
+
for device, dns in re.findall(r"set\s+([A-Za-z0-9_-]+)\s+dns\s+(\d+\.\d+\.\d+\.\d+)", segment, flags=re.IGNORECASE):
|
|
1700
|
+
ops.append({"op": "set_host_dns", "device": device, "dns": dns})
|
|
1701
|
+
return ops
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
def _extract_verification_ops(prompt: str) -> list[dict[str, object]]:
|
|
1705
|
+
ops: list[dict[str, object]] = []
|
|
1706
|
+
lowered = prompt.lower()
|
|
1707
|
+
for command in ["show vlan brief", "show interfaces trunk", "show ip interface brief", "show ip dhcp binding"]:
|
|
1708
|
+
if command in lowered:
|
|
1709
|
+
ops.append({"op": "verify_command", "command": command})
|
|
1710
|
+
if "ping" in lowered:
|
|
1711
|
+
ops.append({"op": "verify_ping"})
|
|
1712
|
+
if "telnet" in lowered:
|
|
1713
|
+
ops.append({"op": "verify_telnet"})
|
|
1714
|
+
return ops
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
def _extract_iot_ops(prompt: str) -> list[dict[str, object]]:
|
|
1718
|
+
ops: list[dict[str, object]] = []
|
|
1719
|
+
registration_pattern = re.compile(
|
|
1720
|
+
r"register\s+([A-Za-z0-9_ -]+?)\s+to\s+([A-Za-z0-9_ -]+?)(?=\s+(?:mode|server(?:-address)?|username|password)\b|$)"
|
|
1721
|
+
r"(?:\s+mode\s+(lan_server|remote_server))?"
|
|
1722
|
+
r"(?:\s+server(?:-address)?\s+(\d+\.\d+\.\d+\.\d+))?"
|
|
1723
|
+
r"(?:\s+username\s+([A-Za-z0-9._-]+))?"
|
|
1724
|
+
r"(?:\s+password\s+([A-Za-z0-9._-]+))?",
|
|
1725
|
+
flags=re.IGNORECASE,
|
|
1726
|
+
)
|
|
1727
|
+
for segment in _command_segments(prompt):
|
|
1728
|
+
for device, target, mode, server_address, username, password in registration_pattern.findall(segment):
|
|
1729
|
+
ops.append(
|
|
1730
|
+
{
|
|
1731
|
+
"op": "set_iot_registration",
|
|
1732
|
+
"device": device.strip(),
|
|
1733
|
+
"target": target.strip(),
|
|
1734
|
+
"mode": (mode or "").upper() or None,
|
|
1735
|
+
"server_address": server_address or None,
|
|
1736
|
+
"username": username or None,
|
|
1737
|
+
"password": password or None,
|
|
1738
|
+
}
|
|
1739
|
+
)
|
|
1740
|
+
rule_pattern = re.compile(
|
|
1741
|
+
r"(enable|disable)\s+iot\s+rule\s+(?:\"([^\"]+)\"|'([^']+)'|([A-Za-z0-9_ -]+?))\s+on\s+([A-Za-z0-9_ -]+)",
|
|
1742
|
+
flags=re.IGNORECASE,
|
|
1743
|
+
)
|
|
1744
|
+
for action, quoted_name, single_quoted_name, plain_name, device in rule_pattern.findall(segment):
|
|
1745
|
+
rule_name = quoted_name or single_quoted_name or plain_name
|
|
1746
|
+
ops.append(
|
|
1747
|
+
{
|
|
1748
|
+
"op": "set_iot_rule_state",
|
|
1749
|
+
"device": device.strip(),
|
|
1750
|
+
"rule_name": rule_name.strip(),
|
|
1751
|
+
"enabled": action.lower() == "enable",
|
|
1752
|
+
}
|
|
1753
|
+
)
|
|
1754
|
+
return ops
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def _decode_prompt_string(value: str) -> str:
|
|
1758
|
+
return (
|
|
1759
|
+
value.replace(r"\\", "\0")
|
|
1760
|
+
.replace(r"\"", '"')
|
|
1761
|
+
.replace(r"\n", "\n")
|
|
1762
|
+
.replace(r"\r", "\r")
|
|
1763
|
+
.replace(r"\t", "\t")
|
|
1764
|
+
.replace("\0", "\\")
|
|
1765
|
+
)
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def _extract_programming_ops(prompt: str) -> list[dict[str, object]]:
|
|
1769
|
+
ops: list[dict[str, object]] = []
|
|
1770
|
+
script_pattern = re.compile(
|
|
1771
|
+
r'set\s+"((?:\\.|[^"\\])*)"\s+script\s+app\s+"((?:\\.|[^"\\])*)"\s+file\s+"((?:\\.|[^"\\])*)"\s+content\s+"((?:\\.|[^"\\])*)"',
|
|
1772
|
+
flags=re.IGNORECASE | re.DOTALL,
|
|
1773
|
+
)
|
|
1774
|
+
for device, app_name, file_name, content in script_pattern.findall(prompt):
|
|
1775
|
+
ops.append(
|
|
1776
|
+
{
|
|
1777
|
+
"op": "set_script_file_content",
|
|
1778
|
+
"device": _decode_prompt_string(device).strip(),
|
|
1779
|
+
"app_name": _decode_prompt_string(app_name).strip(),
|
|
1780
|
+
"file_name": _decode_prompt_string(file_name).strip(),
|
|
1781
|
+
"content": _decode_prompt_string(content),
|
|
1782
|
+
}
|
|
1783
|
+
)
|
|
1784
|
+
return ops
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
def strict_vlan_assignment() -> bool:
|
|
1788
|
+
"""Refuse instead of defaulting host-to-VLAN distribution."""
|
|
1789
|
+
return (os.getenv("PACKET_TRACER_STRICT_VLAN_ASSIGNMENT") or "").strip().lower() in {"1", "true", "yes", "on"}
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
def distribute_hosts_across_vlans(host_count: int, vlan_ids: list[int]) -> dict[int, int]:
|
|
1793
|
+
"""Spread hosts across VLANs as evenly as possible, remainder to the first.
|
|
1794
|
+
|
|
1795
|
+
6 hosts over VLAN 10/20/30 gives 2/2/2; 7 gives 3/2/2. VLANs beyond the host
|
|
1796
|
+
count are still created, they just start empty — an empty VLAN is a valid
|
|
1797
|
+
configuration and more useful than a refusal.
|
|
1798
|
+
"""
|
|
1799
|
+
if host_count <= 0 or not vlan_ids:
|
|
1800
|
+
return {}
|
|
1801
|
+
base, remainder = divmod(host_count, len(vlan_ids))
|
|
1802
|
+
return {
|
|
1803
|
+
vlan_id: base + (1 if index < remainder else 0)
|
|
1804
|
+
for index, vlan_id in enumerate(vlan_ids)
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
def parse_intent(prompt: str) -> IntentPlan:
|
|
1809
|
+
pkt_path_match = re.search(r"([A-Za-z]:\\[^\"\n]+?\.pkt)\b", prompt, flags=re.IGNORECASE)
|
|
1810
|
+
pkt_path = pkt_path_match.group(1) if pkt_path_match else None
|
|
1811
|
+
lowered = prompt.lower()
|
|
1812
|
+
normalized_prompt = _normalize_prompt(prompt)
|
|
1813
|
+
|
|
1814
|
+
capability_set = {capability for capability, patterns in CAPABILITY_PATTERNS.items() if any(re.search(pattern, lowered) for pattern in patterns)}
|
|
1815
|
+
devices = _extract_explicit_devices(prompt)
|
|
1816
|
+
links = _extract_explicit_links(prompt)
|
|
1817
|
+
device_counts = _extract_natural_device_counts(normalized_prompt)
|
|
1818
|
+
vlan_ids = _extract_vlan_ids(normalized_prompt)
|
|
1819
|
+
uplink_intent = _extract_uplink_intent(normalized_prompt)
|
|
1820
|
+
host_link_intent = _extract_host_link_intent(normalized_prompt)
|
|
1821
|
+
host_vlan_assignment = _extract_host_vlan_assignment(normalized_prompt)
|
|
1822
|
+
network_style = _extract_network_style(normalized_prompt)
|
|
1823
|
+
if (
|
|
1824
|
+
network_style == "wireless_advanced"
|
|
1825
|
+
and "server_aaa" in capability_set
|
|
1826
|
+
and re.search(r"\bradius\b", lowered)
|
|
1827
|
+
and not re.search(r"\b(?:aaa|acs|server aaa|aaa server)\b", lowered)
|
|
1828
|
+
):
|
|
1829
|
+
capability_set.discard("server_aaa")
|
|
1830
|
+
if re.search(r"\bqos\b", lowered) and (
|
|
1831
|
+
re.search(r"\b(?:snmp|netflow|span|rspan|lldp|rep|dai|dot1x|802\.1x|dhcp snooping|port-security|port security)\b", lowered)
|
|
1832
|
+
or network_style == "l2_security_monitoring"
|
|
1833
|
+
):
|
|
1834
|
+
capability_set.add("qos")
|
|
1835
|
+
if network_style == "automation_controller" or re.search(r"\b(?:network controller|netcon|blockly|iox)\b", lowered):
|
|
1836
|
+
if re.search(r"\bpython\b", lowered):
|
|
1837
|
+
capability_set.add("python_programming")
|
|
1838
|
+
if re.search(r"\b(?:tcp|udp)\b", lowered) and re.search(r"\b(?:app|application|test app)\b", lowered):
|
|
1839
|
+
capability_set.add("tcp_udp_app")
|
|
1840
|
+
if "ospf" in capability_set and not re.search(r"\b(?:ipv6|ospfv3)\b", lowered):
|
|
1841
|
+
capability_set.add("ospfv2")
|
|
1842
|
+
if "eigrp" in capability_set and not re.search(r"\bipv6\b", lowered):
|
|
1843
|
+
capability_set.add("eigrp_ipv4")
|
|
1844
|
+
if "rip" in capability_set and not re.search(r"\b(?:ipv6|ripng)\b", lowered):
|
|
1845
|
+
capability_set.add("ripv2")
|
|
1846
|
+
if "nat" in capability_set:
|
|
1847
|
+
if re.search(r"\bpat\b|\boverload\b", lowered):
|
|
1848
|
+
capability_set.add("pat")
|
|
1849
|
+
if re.search(r"\bstatic\s+nat\b|\bnat\s+static\b", lowered):
|
|
1850
|
+
capability_set.add("nat_static")
|
|
1851
|
+
if re.search(r"\bdynamic\s+nat\b|\bnat\s+dynamic\b", lowered):
|
|
1852
|
+
capability_set.add("nat_dynamic")
|
|
1853
|
+
if network_style == "ipv4_routing_management" and not {"nat_static", "nat_dynamic"} & capability_set:
|
|
1854
|
+
capability_set.update({"nat_static", "nat_dynamic"})
|
|
1855
|
+
if "ssh" in capability_set:
|
|
1856
|
+
capability_set.add("ssh_ios")
|
|
1857
|
+
if "ntp" in capability_set:
|
|
1858
|
+
capability_set.add("ntp_ios")
|
|
1859
|
+
if (
|
|
1860
|
+
"server_syslog" in capability_set
|
|
1861
|
+
and re.search(r"\b(?:server|ftp|email|aaa|dns)\b", lowered)
|
|
1862
|
+
and not re.search(r"\b(?:logging\s+host|set\s+\S+\s+syslog\s+server)\b", lowered)
|
|
1863
|
+
):
|
|
1864
|
+
capability_set.discard("syslog_ios")
|
|
1865
|
+
if re.search(r"\blogging\s+host\b", lowered):
|
|
1866
|
+
capability_set.add("syslog_ios")
|
|
1867
|
+
if re.search(r"\bdefault\s+route\b|\b0\.0\.0\.0/0\b", lowered):
|
|
1868
|
+
capability_set.update({"static_route", "default_route"})
|
|
1869
|
+
elif re.search(r"\bstatic\s+route\b|\bip\s+route\b", lowered):
|
|
1870
|
+
capability_set.add("static_route")
|
|
1871
|
+
if re.search(r"\bdhcp\s+relay\b|\bdhcp-relay\b|\bhelper-address\b|\bip\s+helper\b", lowered):
|
|
1872
|
+
capability_set.add("dhcp_relay")
|
|
1873
|
+
if not re.search(r"\b(?:dhcp\s+pool|server\s+dhcp|dhcp\s+server|default-router)\b", lowered):
|
|
1874
|
+
capability_set.discard("dhcp_pool")
|
|
1875
|
+
capability_set.discard("router_dhcp")
|
|
1876
|
+
capability_set.discard("server_dhcp")
|
|
1877
|
+
if "dhcp_snooping" in capability_set and not re.search(r"\b(?:dhcp\s+pool|server\s+dhcp|dhcp\s+server|default-router)\b", lowered):
|
|
1878
|
+
capability_set.discard("dhcp_pool")
|
|
1879
|
+
capability_set.discard("router_dhcp")
|
|
1880
|
+
capability_set.discard("server_dhcp")
|
|
1881
|
+
department_count = _extract_department_count(normalized_prompt)
|
|
1882
|
+
per_department_devices = _extract_per_department_devices(normalized_prompt)
|
|
1883
|
+
|
|
1884
|
+
device_requirements = dict(device_counts)
|
|
1885
|
+
for device in devices:
|
|
1886
|
+
device_type = str(device["type"])
|
|
1887
|
+
device_requirements[device_type] = device_requirements.get(device_type, 0) + 1
|
|
1888
|
+
|
|
1889
|
+
# Hosts have to plug into something. "bir sebeke lazimdir 10 kompyuter
|
|
1890
|
+
# ucun" named ten PCs and no switch, so all ten became standalone targets
|
|
1891
|
+
# and generation refused when the donor ran out of spare PCs -- reported as
|
|
1892
|
+
# a donor limit, though the real problem was a topology with no switch in
|
|
1893
|
+
# it. One switch is the smallest thing that makes the request buildable.
|
|
1894
|
+
HOST_TYPES = {"PC", "Server", "Printer", "Laptop", "Tablet", "Smartphone"}
|
|
1895
|
+
host_total = sum(count for kind, count in device_requirements.items() if kind in HOST_TYPES)
|
|
1896
|
+
if host_total and not device_requirements.get("Switch") and not device_requirements.get("WirelessRouter"):
|
|
1897
|
+
device_requirements["Switch"] = 1
|
|
1898
|
+
inferred_switch = True
|
|
1899
|
+
else:
|
|
1900
|
+
inferred_switch = False
|
|
1901
|
+
|
|
1902
|
+
department_groups = _build_department_groups(normalized_prompt, department_count, per_department_devices, vlan_ids)
|
|
1903
|
+
if department_groups:
|
|
1904
|
+
device_requirements["Switch"] = max(device_requirements.get("Switch", 0), department_count)
|
|
1905
|
+
for device_type, per_group_count in per_department_devices.items():
|
|
1906
|
+
device_requirements[device_type] = max(device_requirements.get(device_type, 0), department_count * per_group_count)
|
|
1907
|
+
|
|
1908
|
+
topology_requirements: dict[str, object] = {}
|
|
1909
|
+
if vlan_ids:
|
|
1910
|
+
topology_requirements["vlan_ids"] = vlan_ids
|
|
1911
|
+
if uplink_intent:
|
|
1912
|
+
topology_requirements["uplink_intent"] = uplink_intent
|
|
1913
|
+
if host_link_intent:
|
|
1914
|
+
topology_requirements["host_link_intent"] = host_link_intent
|
|
1915
|
+
if host_vlan_assignment:
|
|
1916
|
+
topology_requirements["host_vlan_assignment"] = host_vlan_assignment
|
|
1917
|
+
if device_requirements.get("Switch", 0) > 1:
|
|
1918
|
+
topology_requirements["uplink_topology"] = "core_switch"
|
|
1919
|
+
if department_groups:
|
|
1920
|
+
topology_requirements["uplink_topology"] = "chain"
|
|
1921
|
+
topology_requirements["department_count"] = department_count
|
|
1922
|
+
if "router_dhcp" in capability_set or "dhcp_pool" in capability_set:
|
|
1923
|
+
topology_requirements["needs_dhcp_pool"] = True
|
|
1924
|
+
routing = next((cap for cap in ["ospf", "eigrp", "rip"] if cap in capability_set), None)
|
|
1925
|
+
if routing:
|
|
1926
|
+
topology_requirements["routing_protocol"] = routing
|
|
1927
|
+
service_requirements = _extract_service_requirements(sorted(capability_set), prompt)
|
|
1928
|
+
wireless_mode = _extract_wireless_mode(normalized_prompt, sorted(capability_set), device_requirements, network_style)
|
|
1929
|
+
|
|
1930
|
+
edit_operations: list[dict[str, object]] = []
|
|
1931
|
+
# `rename X to Y` is the command form. `X adini Y et` and `X in adi Y olsun`
|
|
1932
|
+
# are how the request is actually phrased; without them `--edit` understood
|
|
1933
|
+
# nothing, fell back to generating, and wrote an unchanged copy.
|
|
1934
|
+
rename_patterns = (
|
|
1935
|
+
r"rename\s+([A-Za-z0-9_-]+)\s+to\s+([A-Za-z0-9_-]+)",
|
|
1936
|
+
r"\b([A-Za-z0-9_-]+)\s*(?:in|nin|nun|un)?\s*adini\s+([A-Za-z0-9_-]+)\s*(?:et|ele|deyis)",
|
|
1937
|
+
r"\b([A-Za-z0-9_-]+)\s*(?:in|nin)?\s*adi\s+([A-Za-z0-9_-]+)\s+olsun",
|
|
1938
|
+
)
|
|
1939
|
+
rename_source = _transliterate(prompt)
|
|
1940
|
+
for pattern in rename_patterns:
|
|
1941
|
+
for old_name, new_name in re.findall(pattern, rename_source, flags=re.IGNORECASE):
|
|
1942
|
+
operation = {"op": "rename_device", "device": old_name, "new_name": new_name}
|
|
1943
|
+
if operation not in edit_operations:
|
|
1944
|
+
edit_operations.append(operation)
|
|
1945
|
+
edit_operations.extend(_extract_link_edit_operations(links))
|
|
1946
|
+
natural_edits, natural_switch_ops = _extract_natural_edit_operations(prompt)
|
|
1947
|
+
for operation in natural_edits:
|
|
1948
|
+
if operation not in edit_operations:
|
|
1949
|
+
edit_operations.append(operation)
|
|
1950
|
+
|
|
1951
|
+
switch_ops = _extract_switch_ops(prompt)
|
|
1952
|
+
for operation in natural_switch_ops:
|
|
1953
|
+
if operation not in switch_ops:
|
|
1954
|
+
switch_ops.append(operation)
|
|
1955
|
+
router_ops = _extract_router_ops(prompt)
|
|
1956
|
+
server_ops = _extract_server_ops(prompt)
|
|
1957
|
+
wireless_ops = _extract_wireless_ops(prompt)
|
|
1958
|
+
end_device_ops = _extract_end_device_ops(prompt)
|
|
1959
|
+
management_ops = _extract_management_ops(prompt)
|
|
1960
|
+
verification_ops = _extract_verification_ops(prompt)
|
|
1961
|
+
iot_ops = _extract_iot_ops(prompt)
|
|
1962
|
+
programming_ops = _extract_programming_ops(prompt)
|
|
1963
|
+
|
|
1964
|
+
if any(op["op"] == "associate_wireless_client" for op in wireless_ops):
|
|
1965
|
+
capability_set.update({"wireless_client", "wireless_client_association"})
|
|
1966
|
+
if any(op["op"] == "set_wireless_ssid" for op in wireless_ops):
|
|
1967
|
+
capability_set.update({"wireless_ap", "wireless_mutation"})
|
|
1968
|
+
for op in wireless_ops:
|
|
1969
|
+
if op.get("op") != "set_wireless_ssid":
|
|
1970
|
+
continue
|
|
1971
|
+
security = str(op.get("security") or "")
|
|
1972
|
+
if security == "wep":
|
|
1973
|
+
capability_set.add("wep")
|
|
1974
|
+
if security in {"wpa-enterprise", "wpa2-enterprise", "802.1x"} or op.get("radius_server"):
|
|
1975
|
+
capability_set.add("wpa_enterprise")
|
|
1976
|
+
elif network_style == "wireless_advanced":
|
|
1977
|
+
capability_set.discard("wireless_ap")
|
|
1978
|
+
capability_set.discard("wireless_mutation")
|
|
1979
|
+
if end_device_ops:
|
|
1980
|
+
capability_set.add("end_device_mutation")
|
|
1981
|
+
if iot_ops:
|
|
1982
|
+
capability_set.update({"iot", "iot_registration"})
|
|
1983
|
+
if any(op.get("op") == "set_iot_rule_state" for op in iot_ops):
|
|
1984
|
+
capability_set.add("iot_control")
|
|
1985
|
+
for operation in programming_ops:
|
|
1986
|
+
file_name = str(operation.get("file_name") or "").lower()
|
|
1987
|
+
app_name = str(operation.get("app_name") or "").lower()
|
|
1988
|
+
content = str(operation.get("content") or "").lower()
|
|
1989
|
+
if file_name.endswith(".py") or "python" in app_name:
|
|
1990
|
+
capability_set.add("python_programming")
|
|
1991
|
+
if file_name.endswith(".js") or "javascript" in app_name:
|
|
1992
|
+
capability_set.add("javascript_programming")
|
|
1993
|
+
if file_name.endswith(".visual") or "visual" in app_name or "<xml" in content:
|
|
1994
|
+
capability_set.add("blockly_programming")
|
|
1995
|
+
if "tcp" in " ".join([file_name, app_name, content]) or "udp" in " ".join([file_name, app_name, content]):
|
|
1996
|
+
capability_set.add("tcp_udp_app")
|
|
1997
|
+
if "realhttp" in content or "real http" in app_name:
|
|
1998
|
+
capability_set.add("real_http")
|
|
1999
|
+
if "realws" in content or "websocket" in content or "websocket" in app_name:
|
|
2000
|
+
capability_set.add("real_websocket")
|
|
2001
|
+
if "mqtt" in content or "mqtt" in app_name:
|
|
2002
|
+
capability_set.add("mqtt")
|
|
2003
|
+
if programming_ops and capability_set & {"python_programming", "javascript_programming", "blockly_programming"} and not capability_set & {"real_http", "real_websocket", "mqtt"}:
|
|
2004
|
+
network_style = network_style or "automation_controller"
|
|
2005
|
+
switch_op_names = {str(op.get("op")) for op in switch_ops}
|
|
2006
|
+
if "set_dhcp_snooping" in switch_op_names:
|
|
2007
|
+
capability_set.add("dhcp_snooping")
|
|
2008
|
+
if "set_dai" in switch_op_names:
|
|
2009
|
+
capability_set.add("dai")
|
|
2010
|
+
if "set_lldp" in switch_op_names:
|
|
2011
|
+
capability_set.add("lldp")
|
|
2012
|
+
if "set_rep" in switch_op_names:
|
|
2013
|
+
capability_set.add("rep")
|
|
2014
|
+
if "set_span" in switch_op_names:
|
|
2015
|
+
capability_set.add("span")
|
|
2016
|
+
if "set_port_security" in switch_op_names:
|
|
2017
|
+
capability_set.add("port_security")
|
|
2018
|
+
if "set_dot1x" in switch_op_names:
|
|
2019
|
+
capability_set.add("dot1x")
|
|
2020
|
+
if "set_qos_policy" in switch_op_names:
|
|
2021
|
+
capability_set.add("qos")
|
|
2022
|
+
if "set_stp" in switch_op_names:
|
|
2023
|
+
capability_set.add("stp")
|
|
2024
|
+
for op in switch_ops:
|
|
2025
|
+
if op.get("op") == "set_stp" and re.search(r"(?:rapid|rstp)", str(op.get("mode") or ""), flags=re.IGNORECASE):
|
|
2026
|
+
capability_set.add("rstp")
|
|
2027
|
+
if "set_etherchannel" in switch_op_names:
|
|
2028
|
+
capability_set.add("etherchannel")
|
|
2029
|
+
for op in switch_ops:
|
|
2030
|
+
if op.get("op") != "set_etherchannel":
|
|
2031
|
+
continue
|
|
2032
|
+
mode = str(op.get("mode") or "").lower()
|
|
2033
|
+
if mode in {"active", "passive"}:
|
|
2034
|
+
capability_set.add("lacp")
|
|
2035
|
+
if mode in {"desirable", "auto"}:
|
|
2036
|
+
capability_set.add("pagp")
|
|
2037
|
+
if "set_vtp" in switch_op_names:
|
|
2038
|
+
capability_set.add("vtp")
|
|
2039
|
+
if "set_dtp" in switch_op_names:
|
|
2040
|
+
capability_set.add("dtp")
|
|
2041
|
+
router_op_names = {str(op.get("op")) for op in router_ops}
|
|
2042
|
+
if "set_bgp_neighbor" in router_op_names:
|
|
2043
|
+
capability_set.add("bgp")
|
|
2044
|
+
if "set_snmp_community" in router_op_names:
|
|
2045
|
+
capability_set.add("snmp")
|
|
2046
|
+
if "set_netflow" in router_op_names:
|
|
2047
|
+
capability_set.add("netflow")
|
|
2048
|
+
if "set_gre_tunnel" in router_op_names:
|
|
2049
|
+
capability_set.add("gre")
|
|
2050
|
+
if "set_ppp_interface" in router_op_names:
|
|
2051
|
+
capability_set.add("ppp")
|
|
2052
|
+
if router_op_names & {"set_ipsec_transform_set", "set_crypto_map"}:
|
|
2053
|
+
capability_set.update({"ipsec", "vpn"})
|
|
2054
|
+
if "set_cbac_inspect" in router_op_names:
|
|
2055
|
+
capability_set.update({"cbac", "security_edge"})
|
|
2056
|
+
if router_op_names & {"set_zfw_zone_interface", "set_zfw_zone_pair", "set_zfw_policy"}:
|
|
2057
|
+
capability_set.update({"zfw", "security_edge"})
|
|
2058
|
+
if "set_qos_policy" not in switch_op_names:
|
|
2059
|
+
capability_set.discard("qos")
|
|
2060
|
+
capability_set.discard("server_http")
|
|
2061
|
+
capability_set.discard("server_https")
|
|
2062
|
+
if router_op_names & {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"}:
|
|
2063
|
+
capability_set.add("ipv6_slaac")
|
|
2064
|
+
if "set_dhcpv6_pool" in router_op_names:
|
|
2065
|
+
capability_set.add("dhcpv6_stateful")
|
|
2066
|
+
if "set_ospfv3_interface" in router_op_names:
|
|
2067
|
+
capability_set.add("ospfv3")
|
|
2068
|
+
if "set_eigrp_ipv6_interface" in router_op_names:
|
|
2069
|
+
capability_set.add("eigrp_ipv6")
|
|
2070
|
+
if "set_ripng_interface" in router_op_names:
|
|
2071
|
+
capability_set.add("ripng")
|
|
2072
|
+
if "set_hsrp_ipv6" in router_op_names:
|
|
2073
|
+
capability_set.add("hsrp")
|
|
2074
|
+
if "set_ospfv2_network" in router_op_names:
|
|
2075
|
+
capability_set.add("ospfv2")
|
|
2076
|
+
if "set_eigrp_ipv4_network" in router_op_names:
|
|
2077
|
+
capability_set.add("eigrp_ipv4")
|
|
2078
|
+
if "set_ripv2_network" in router_op_names:
|
|
2079
|
+
capability_set.add("ripv2")
|
|
2080
|
+
if "set_static_route" in router_op_names:
|
|
2081
|
+
capability_set.add("static_route")
|
|
2082
|
+
if any(op.get("op") == "set_static_route" and op.get("network") == "0.0.0.0" and op.get("prefix") == 0 for op in router_ops):
|
|
2083
|
+
capability_set.add("default_route")
|
|
2084
|
+
if "set_dhcp_relay" in router_op_names:
|
|
2085
|
+
capability_set.add("dhcp_relay")
|
|
2086
|
+
if "set_nat_interface" in router_op_names:
|
|
2087
|
+
capability_set.add("nat")
|
|
2088
|
+
if "set_nat_static" in router_op_names:
|
|
2089
|
+
capability_set.update({"nat", "nat_static"})
|
|
2090
|
+
if "set_pat_overload" in router_op_names:
|
|
2091
|
+
capability_set.update({"nat", "pat"})
|
|
2092
|
+
if "set_ssh_ios" in router_op_names:
|
|
2093
|
+
capability_set.update({"ssh", "ssh_ios"})
|
|
2094
|
+
if "set_ntp_server" in router_op_names:
|
|
2095
|
+
capability_set.update({"ntp", "ntp_ios"})
|
|
2096
|
+
if "set_syslog_server" in router_op_names:
|
|
2097
|
+
capability_set.update({"syslog_ios"})
|
|
2098
|
+
if router_op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone", "set_dial_peer_voice"}:
|
|
2099
|
+
capability_set.update({"voip", "call_manager"})
|
|
2100
|
+
if router_op_names & {"set_ephone_dn", "set_ephone"}:
|
|
2101
|
+
capability_set.add("ip_phone")
|
|
2102
|
+
if capability_set & {"vpn", "ipsec", "gre", "ppp", "security_edge"}:
|
|
2103
|
+
network_style = network_style or "wan_security"
|
|
2104
|
+
if capability_set & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
|
|
2105
|
+
network_style = network_style or "ipv6_routing"
|
|
2106
|
+
if capability_set & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
|
|
2107
|
+
network_style = network_style or "ipv4_routing_management"
|
|
2108
|
+
if capability_set & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}:
|
|
2109
|
+
network_style = network_style or "l2_security_monitoring"
|
|
2110
|
+
if capability_set & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}:
|
|
2111
|
+
network_style = "l2_resiliency_routing"
|
|
2112
|
+
if capability_set & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}:
|
|
2113
|
+
network_style = "wireless_advanced"
|
|
2114
|
+
if capability_set & {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"}:
|
|
2115
|
+
network_style = "physical_media_device"
|
|
2116
|
+
if capability_set & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}:
|
|
2117
|
+
network_style = "industrial_iot"
|
|
2118
|
+
if capability_set & {"real_http", "real_websocket"}:
|
|
2119
|
+
capability_set.discard("server_http")
|
|
2120
|
+
capability_set.discard("server_https")
|
|
2121
|
+
if not (
|
|
2122
|
+
iot_ops
|
|
2123
|
+
or re.search(r"\b(?:home\s+gateway|smart\s+home|iot\s+registration|iot\s+control|iot\s+rule)\b", lowered)
|
|
2124
|
+
or re.search(r"\bregister\s+.+\s+to\b", lowered)
|
|
2125
|
+
):
|
|
2126
|
+
capability_set.discard("iot")
|
|
2127
|
+
if capability_set & {"voip", "ip_phone", "call_manager", "linksys_voice"}:
|
|
2128
|
+
network_style = "voice_collaboration"
|
|
2129
|
+
|
|
2130
|
+
capabilities = sorted(capability_set)
|
|
2131
|
+
|
|
2132
|
+
parse_warnings: list[str] = []
|
|
2133
|
+
blocking_gaps: list[str] = []
|
|
2134
|
+
assumptions_used: list[str] = []
|
|
2135
|
+
if any(word in normalized_prompt for word in TOPOLOGY_HINT_WORDS) and not device_requirements and not devices:
|
|
2136
|
+
parse_warnings.append("Prompt includes topology words but no stable device counts were parsed.")
|
|
2137
|
+
if any(op["op"] == "set_management_vlan" for op in management_ops):
|
|
2138
|
+
topology_requirements["management_vlan"] = True
|
|
2139
|
+
|
|
2140
|
+
pc_count = device_requirements.get("PC", 0)
|
|
2141
|
+
if department_groups and vlan_ids and len(vlan_ids) >= len(department_groups) and not host_vlan_assignment:
|
|
2142
|
+
for index, group in enumerate(department_groups):
|
|
2143
|
+
vlan_id = vlan_ids[index]
|
|
2144
|
+
host_vlan_assignment[vlan_id] = host_vlan_assignment.get(vlan_id, 0) + int(group["devices"].get("PC", 0))
|
|
2145
|
+
assumptions_used.append("Assigned each department's PCs to the matching VLAN order.")
|
|
2146
|
+
if vlan_ids and pc_count and not host_vlan_assignment and not any(op["op"] == "set_access_port" for op in switch_ops):
|
|
2147
|
+
# "3 switch, 6 PC, VLAN 10/20/30" has an obvious reading: two hosts per
|
|
2148
|
+
# VLAN. Refusing was inconsistent with the branch directly above, which
|
|
2149
|
+
# already assigns department PCs to VLANs by order, and with the planner
|
|
2150
|
+
# defaulting port speeds, cable types, addressing and the VLAN IDs
|
|
2151
|
+
# themselves. Set PACKET_TRACER_STRICT_VLAN_ASSIGNMENT=1 to refuse instead.
|
|
2152
|
+
if strict_vlan_assignment():
|
|
2153
|
+
blocking_gaps.append("Host-to-VLAN assignment is missing. Specify how many PCs belong to each VLAN.")
|
|
2154
|
+
else:
|
|
2155
|
+
host_vlan_assignment.update(distribute_hosts_across_vlans(pc_count, vlan_ids))
|
|
2156
|
+
spread = ", ".join(f"VLAN {vlan_id}: {count}" for vlan_id, count in host_vlan_assignment.items())
|
|
2157
|
+
assumptions_used.append(
|
|
2158
|
+
f"Distributed {pc_count} PCs evenly across {len(vlan_ids)} VLANs ({spread}). "
|
|
2159
|
+
"Say how many PCs belong to each VLAN to override."
|
|
2160
|
+
)
|
|
2161
|
+
if vlan_ids and not device_requirements.get("Switch", 0) and not any(device.get("type") == "Switch" for device in devices):
|
|
2162
|
+
blocking_gaps.append("VLAN planning requires at least one switch.")
|
|
2163
|
+
if department_groups and vlan_ids and len(vlan_ids) < len(department_groups):
|
|
2164
|
+
blocking_gaps.append("Department count is larger than provided VLAN IDs.")
|
|
2165
|
+
|
|
2166
|
+
|
|
2167
|
+
if department_groups and "Switch" not in device_counts:
|
|
2168
|
+
assumptions_used.append("Added one switch per department group.")
|
|
2169
|
+
if inferred_switch:
|
|
2170
|
+
assumptions_used.append(
|
|
2171
|
+
"Added one switch: the prompt named hosts but nothing for them to connect to."
|
|
2172
|
+
)
|
|
2173
|
+
if department_groups and not network_style:
|
|
2174
|
+
network_style = "campus"
|
|
2175
|
+
assumptions_used.append("Interpreted department-based prompt as campus style.")
|
|
2176
|
+
if network_style == "wan_security":
|
|
2177
|
+
assumptions_used.append("Interpreted VPN/WAN/security wording as WAN-security edge style.")
|
|
2178
|
+
|
|
2179
|
+
goal = "edit" if pkt_path or any(word in normalized_prompt for word in ["deyis", "edit", "modify", "change", "rename", "update"]) else "generate"
|
|
2180
|
+
|
|
2181
|
+
# A prompt that carries no topology signal at all is not an underspecified
|
|
2182
|
+
# lab request, it is a different kind of message. "sebeke haqqinda melumat
|
|
2183
|
+
# ver" used to produce a two-device file, which is worse than a refusal:
|
|
2184
|
+
# the user gets a lab they never asked for and no indication anything
|
|
2185
|
+
# was misread. Every genuine request carries at least one signal.
|
|
2186
|
+
if goal == "generate" and not any(
|
|
2187
|
+
[
|
|
2188
|
+
device_counts,
|
|
2189
|
+
department_groups,
|
|
2190
|
+
vlan_ids,
|
|
2191
|
+
network_style,
|
|
2192
|
+
capabilities,
|
|
2193
|
+
switch_ops,
|
|
2194
|
+
router_ops,
|
|
2195
|
+
server_ops,
|
|
2196
|
+
wireless_ops,
|
|
2197
|
+
management_ops,
|
|
2198
|
+
end_device_ops,
|
|
2199
|
+
]
|
|
2200
|
+
):
|
|
2201
|
+
blocking_gaps.append(
|
|
2202
|
+
"This prompt does not describe a topology. Say which devices you want, "
|
|
2203
|
+
"for example '1 router 1 switch 3 komputer qur'."
|
|
2204
|
+
)
|
|
2205
|
+
confidence_score = _estimate_confidence(device_requirements, capabilities, parse_warnings, blocking_gaps, devices, links)
|
|
2206
|
+
return IntentPlan(
|
|
2207
|
+
goal=goal,
|
|
2208
|
+
prompt=prompt,
|
|
2209
|
+
pkt_path=pkt_path,
|
|
2210
|
+
capabilities=capabilities,
|
|
2211
|
+
network_style=network_style,
|
|
2212
|
+
wireless_mode=wireless_mode,
|
|
2213
|
+
requested_models=_extract_requested_models(normalized_prompt),
|
|
2214
|
+
wireless_settings=_restore_wireless_casing(
|
|
2215
|
+
_extract_wireless_settings(normalized_prompt), prompt
|
|
2216
|
+
),
|
|
2217
|
+
device_requirements=device_requirements,
|
|
2218
|
+
device_counts=device_counts,
|
|
2219
|
+
department_groups=department_groups,
|
|
2220
|
+
service_requirements=service_requirements,
|
|
2221
|
+
topology_requirements=topology_requirements,
|
|
2222
|
+
vlan_ids=vlan_ids,
|
|
2223
|
+
uplink_intent=uplink_intent,
|
|
2224
|
+
host_link_intent=host_link_intent,
|
|
2225
|
+
host_vlan_assignment=host_vlan_assignment,
|
|
2226
|
+
assumptions_used=assumptions_used,
|
|
2227
|
+
confidence_score=confidence_score,
|
|
2228
|
+
parse_warnings=parse_warnings,
|
|
2229
|
+
blocking_gaps=blocking_gaps,
|
|
2230
|
+
edit_operations=edit_operations,
|
|
2231
|
+
devices=devices,
|
|
2232
|
+
links=links,
|
|
2233
|
+
switch_ops=switch_ops,
|
|
2234
|
+
router_ops=router_ops,
|
|
2235
|
+
server_ops=server_ops,
|
|
2236
|
+
wireless_ops=wireless_ops,
|
|
2237
|
+
end_device_ops=end_device_ops,
|
|
2238
|
+
management_ops=management_ops,
|
|
2239
|
+
verification_ops=verification_ops,
|
|
2240
|
+
iot_ops=iot_ops,
|
|
2241
|
+
programming_ops=programming_ops,
|
|
2242
|
+
)
|