File size: 28,946 Bytes
1bd64c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
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
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
542
543
544
545
546
#!/usr/bin/env python3
"""
patch_yang_stack_vendors.py — extend Automate YANG templates + matchers.

Patches index.html in-place:
  1. Add FRR / VyOS / Nokia / Aruba renderers to every AUTO_* map
  2. Broaden AUTOMATION_MAPPINGS matchAny/extract for those CLIs
  3. Add sros to OS_DEV_TYPE; prefer cisco_nxos / cisco_xe / cisco_asa by OS
  4. Prefer OS-aware Ansible modules for Cisco nxos/asa/iosxe
  5. Ensure VXLAN/EVPN appear in CAT_GROUPS filter UI

Idempotent: skips if marker comment already present.
"""
from __future__ import annotations

import pathlib
import re
import sys

HTML = pathlib.Path(__file__).resolve().parent.parent / "index.html"
MARKER = "/* STACK_VENDOR_AUTO_EXT — FRR/VyOS/Nokia/Aruba */"

# Compact CLI-oriented templates (netconf often OpenConfig/comment; ansible real modules)
STACK_TEMPLATES = r'''
/* STACK_VENDOR_AUTO_EXT — FRR/VyOS/Nokia/Aruba */
function _autoCliBundle(title, lines, ansibleYaml){
  const cli = lines.filter(Boolean).join("\n");
  return {
    netconf: `<!-- ${title}: native NETCONF/YANG varies by platform; CLI equivalent -->\n<!--\n${cli}\n-->`,
    ncclient: `${pySetup()}# ${title} via CLI session (Netmiko-style)
from netmiko import ConnectHandler
dev = {"device_type": ${JSON.stringify('__DEVTYPE__')},
       "host": ${pyArg('host')}, "username": ${pyArg('user')},
       "password": ${pyArg('pass')}, "port": int(${pyArg('port')} or 22)}
cmds = """${cli}""".strip().splitlines()
with ConnectHandler(**dev) as conn:
    print(conn.send_config_set(cmds) if cmds else conn.send_command("show version"))`,
    ansible: ansibleYaml,
  };
}
function _withDev(bundle, deviceType){
  // Replace placeholder device_type string inside ncclient snippet
  return {
    netconf: bundle.netconf,
    ncclient: bundle.ncclient.replace('"__DEVTYPE__"', JSON.stringify(deviceType)),
    ansible: bundle.ansible,
  };
}
function stackIfaceIpv4(vendor, v){
  if(vendor==="FRR") return _withDev(_autoCliBundle("FRR interface IPv4",
    [`interface ${v.ifname}`, `ip address ${v.ip}/${v.cidr}`],
    `- name: FRR set ${v.ifname} ${v.ip}/${v.cidr}
  ansible.builtin.shell: |
    vtysh -c 'configure terminal' -c 'interface ${v.ifname}' -c 'ip address ${v.ip}/${v.cidr}' -c 'end' -c 'write'`), "frrouting");
  if(vendor==="VyOS") return _withDev(_autoCliBundle("VyOS interface IPv4",
    [`set interfaces ethernet ${v.ifname} address ${v.ip}/${v.cidr}`],
    `- name: VyOS set ${v.ifname} ${v.ip}/${v.cidr}
  vyos.vyos.vyos_config:
    lines:
      - set interfaces ethernet ${v.ifname} address ${v.ip}/${v.cidr}`), "vyos");
  if(vendor==="Nokia") return _withDev(_autoCliBundle("Nokia SR Linux interface IPv4",
    [`enter candidate`,`set / interface ${v.ifname} subinterface 0 ipv4 admin-state enable`,`set / interface ${v.ifname} subinterface 0 ipv4 address ${v.ip}/${v.cidr}`,`commit now`],
    `- name: SR Linux set ${v.ifname} ${v.ip}/${v.cidr}
  nokia.srlinux.config:
    update:
      - path: /interface[name=${v.ifname}]/subinterface[index=0]/ipv4
        value:
          admin-state: enable
          address:
            - ip-prefix: ${v.ip}/${v.cidr}`), "nokia_srl");
  if(vendor==="Aruba") return _withDev(_autoCliBundle("Aruba AOS-CX interface IPv4",
    [`interface ${v.ifname}`, `ip address ${v.ip}/${v.cidr}`],
    `- name: AOS-CX set ${v.ifname} ${v.ip}/${v.cidr}
  arubanetworks.aoscx.aoscx_l3_interface:
    config:
      - name: ${v.ifname}
        ipv4:
          - address: ${v.ip}/${v.cidr}
    state: merged`), "aruba_aoscx");
  return null;
}
function stackStatic(vendor, v){
  if(vendor==="FRR") return _withDev(_autoCliBundle("FRR static route",
    [`ip route ${v.prefix}/${v.cidr} ${v.nh}`],
    `- name: FRR static ${v.prefix}/${v.cidr} via ${v.nh}
  ansible.builtin.shell: vtysh -c 'configure terminal' -c 'ip route ${v.prefix}/${v.cidr} ${v.nh}' -c 'end' -c 'write'`), "frrouting");
  if(vendor==="VyOS") return _withDev(_autoCliBundle("VyOS static route",
    [`set protocols static route ${v.prefix}/${v.cidr} next-hop ${v.nh}`],
    `- name: VyOS static ${v.prefix}/${v.cidr}
  vyos.vyos.vyos_static_routes:
    config:
      - address_families:
          - afi: ipv4
            routes:
              - dest: ${v.prefix}/${v.cidr}
                next_hops:
                  - forward_router_address: ${v.nh}
    state: merged`), "vyos");
  if(vendor==="Nokia") return _withDev(_autoCliBundle("Nokia static route",
    [`enter candidate`,`set / network-instance default static-routes route ${v.prefix}/${v.cidr} next-hop ${v.nh}`,`commit now`],
    `- name: SR Linux static ${v.prefix}/${v.cidr}
  nokia.srlinux.config:
    update:
      - path: /network-instance[name=default]/static-routes/route[prefix=${v.prefix}/${v.cidr}]
        value:
          next-hop:
            - ${v.nh}`), "nokia_srl");
  if(vendor==="Aruba") return _withDev(_autoCliBundle("Aruba static route",
    [`ip route ${v.prefix}/${v.cidr} ${v.nh}`],
    `- name: AOS-CX static ${v.prefix}/${v.cidr}
  arubanetworks.aoscx.aoscx_config:
    lines:
      - ip route ${v.prefix}/${v.cidr} ${v.nh}`), "aruba_aoscx");
  return null;
}
function stackOspf(vendor, v){
  const area = v.area || "0.0.0.0";
  const ifn = v.ifname || "eth0";
  if(vendor==="FRR") return _withDev(_autoCliBundle("FRR OSPF",
    [`router ospf`, `network ${v.net || "10.0.0.0"}/24 area ${area}`],
    `- name: FRR OSPF network
  ansible.builtin.shell: vtysh -c 'configure terminal' -c 'router ospf' -c 'network ${v.net || "10.0.0.0"}/24 area ${area}' -c 'end' -c 'write'`), "frrouting");
  if(vendor==="VyOS") return _withDev(_autoCliBundle("VyOS OSPF",
    [`set protocols ospf area ${area} network ${v.net || "10.0.0.0"}/24`],
    `- name: VyOS OSPF
  vyos.vyos.vyos_ospfv2:
    config:
      areas:
        - area_id: "${area}"
          network:
            - ${v.net || "10.0.0.0"}/24
    state: merged`), "vyos");
  if(vendor==="Nokia") return _withDev(_autoCliBundle("Nokia OSPF",
    [`enter candidate`,`set / network-instance default protocols ospf instance 0 area ${area} interface ${ifn}`,`commit now`],
    `- name: SR Linux OSPF interface
  nokia.srlinux.config:
    update:
      - path: /network-instance[name=default]/protocols/ospf/instance[name=0]/area[area-id=${area}]/interface[interface-name=${ifn}]
        value: {}`), "nokia_srl");
  if(vendor==="Aruba") return _withDev(_autoCliBundle("Aruba OSPF",
    [`router ospf ${v.pid || "1"}`, `area ${area}`, `interface ${ifn}`, `ip ospf area ${area}`],
    `- name: AOS-CX OSPF
  arubanetworks.aoscx.aoscx_config:
    lines:
      - router ospf ${v.pid || "1"}
      - area ${area}
      - interface ${ifn}
      - ip ospf area ${area}`), "aruba_aoscx");
  return null;
}
function stackBgp(vendor, v){
  if(vendor==="FRR") return _withDev(_autoCliBundle("FRR BGP neighbor",
    [`router bgp ${v.local_as}`, `neighbor ${v.peer} remote-as ${v.remote_as}`],
    `- name: FRR BGP neighbor
  ansible.builtin.shell: vtysh -c 'configure terminal' -c 'router bgp ${v.local_as}' -c 'neighbor ${v.peer} remote-as ${v.remote_as}' -c 'end' -c 'write'`), "frrouting");
  if(vendor==="VyOS") return _withDev(_autoCliBundle("VyOS BGP neighbor",
    [`set protocols bgp system-as ${v.local_as}`, `set protocols bgp neighbor ${v.peer} remote-as ${v.remote_as}`],
    `- name: VyOS BGP neighbor
  vyos.vyos.vyos_bgp_global:
    config:
      as_number: ${v.local_as}
    state: merged
- name: VyOS BGP neighbor peer
  vyos.vyos.vyos_bgp_neighbor:
    config:
      - neighbor: ${v.peer}
        remote_as: ${v.remote_as}
    state: merged`), "vyos");
  if(vendor==="Nokia") return _withDev(_autoCliBundle("Nokia BGP neighbor",
    [`enter candidate`,`set / network-instance default protocols bgp autonomous-system ${v.local_as}`,`set / network-instance default protocols bgp neighbor ${v.peer} peer-as ${v.remote_as}`,`commit now`],
    `- name: SR Linux BGP neighbor
  nokia.srlinux.config:
    update:
      - path: /network-instance[name=default]/protocols/bgp
        value:
          autonomous-system: ${v.local_as}
          neighbor:
            - peer-address: ${v.peer}
              peer-as: ${v.remote_as}`), "nokia_srl");
  if(vendor==="Aruba") return _withDev(_autoCliBundle("Aruba BGP neighbor",
    [`router bgp ${v.local_as}`, `neighbor ${v.peer} remote-as ${v.remote_as}`],
    `- name: AOS-CX BGP neighbor
  arubanetworks.aoscx.aoscx_config:
    lines:
      - router bgp ${v.local_as}
      - neighbor ${v.peer} remote-as ${v.remote_as}`), "aruba_aoscx");
  return null;
}
function stackVlan(vendor, v){
  if(vendor==="FRR") return _withDev(_autoCliBundle("FRR/Linux VLAN",
    [`# FRR does not own VLANs — configure on host bridge`,`ip link add link eth0 name eth0.${v.id} type vlan id ${v.id}`],
    `- name: Host VLAN ${v.id}
  ansible.builtin.command: ip link add link eth0 name eth0.${v.id} type vlan id ${v.id}`), "linux");
  if(vendor==="VyOS") return _withDev(_autoCliBundle("VyOS VLAN",
    [`set interfaces ethernet eth0 vif ${v.id} description ${v.name}`],
    `- name: VyOS VLAN ${v.id}
  vyos.vyos.vyos_config:
    lines:
      - set interfaces ethernet eth0 vif ${v.id} description '${v.name}'`), "vyos");
  if(vendor==="Nokia") return _withDev(_autoCliBundle("Nokia VLAN / MAC-VRF",
    [`enter candidate`,`set / interface ${v.name || "ethernet-1/1"} vlan-tagging true`,`set / interface ${v.name || "ethernet-1/1"} subinterface ${v.id} vlan encap single-tagged vlan-id ${v.id}`,`commit now`],
    `- name: SR Linux VLAN ${v.id}
  nokia.srlinux.config:
    update:
      - path: /interface[name=ethernet-1/1]/subinterface[index=${v.id}]
        value:
          vlan:
            encap:
              single-tagged:
                vlan-id: ${v.id}`), "nokia_srl");
  if(vendor==="Aruba") return _withDev(_autoCliBundle("Aruba VLAN",
    [`vlan ${v.id}`, `name ${v.name}`],
    `- name: AOS-CX VLAN ${v.id}
  arubanetworks.aoscx.aoscx_vlan:
    config:
      - vlan_id: ${v.id}
        name: ${v.name}
    state: merged`), "aruba_aoscx");
  return null;
}
function stackHostname(vendor, v){
  if(vendor==="FRR") return _withDev(_autoCliBundle("FRR hostname",
    [`hostname ${v.hostname}`],
    `- name: FRR hostname
  ansible.builtin.shell: vtysh -c 'configure terminal' -c 'hostname ${v.hostname}' -c 'end' -c 'write'`), "frrouting");
  if(vendor==="VyOS") return _withDev(_autoCliBundle("VyOS hostname",
    [`set system host-name ${v.hostname}`],
    `- name: VyOS hostname
  vyos.vyos.vyos_config:
    lines:
      - set system host-name ${v.hostname}`), "vyos");
  if(vendor==="Nokia") return _withDev(_autoCliBundle("Nokia hostname",
    [`enter candidate`,`set / system name ${v.hostname}`,`commit now`],
    `- name: SR Linux hostname
  nokia.srlinux.config:
    update:
      - path: /system/name
        value:
          host-name: ${v.hostname}`), "nokia_srl");
  if(vendor==="Aruba") return _withDev(_autoCliBundle("Aruba hostname",
    [`hostname ${v.hostname}`],
    `- name: AOS-CX hostname
  arubanetworks.aoscx.aoscx_system:
    config:
      hostname: ${v.hostname}
    state: merged`), "aruba_aoscx");
  return null;
}
function stackNtp(vendor, v){
  if(vendor==="FRR") return _withDev(_autoCliBundle("Host NTP for FRR",
    [`# FRR has no NTP CLI — configure chrony/ntp on the Linux host`,`sudo timedatectl set-ntp true`],
    `- name: Enable host NTP
  ansible.builtin.command: timedatectl set-ntp true`), "linux");
  if(vendor==="VyOS") return _withDev(_autoCliBundle("VyOS NTP",
    [`set system ntp server ${v.server}`],
    `- name: VyOS NTP
  vyos.vyos.vyos_ntp_global:
    config:
      servers:
        - server: ${v.server}
    state: merged`), "vyos");
  if(vendor==="Nokia") return _withDev(_autoCliBundle("Nokia NTP",
    [`enter candidate`,`set / system ntp admin-state enable`,`set / system ntp network-instance mgmt server ${v.server}`,`commit now`],
    `- name: SR Linux NTP
  nokia.srlinux.config:
    update:
      - path: /system/ntp
        value:
          admin-state: enable`), "nokia_srl");
  if(vendor==="Aruba") return _withDev(_autoCliBundle("Aruba NTP",
    [`ntp server ${v.server}`],
    `- name: AOS-CX NTP
  arubanetworks.aoscx.aoscx_config:
    lines:
      - ntp server ${v.server}`), "aruba_aoscx");
  return null;
}
function stackSyslog(vendor, v){
  if(vendor==="FRR") return _withDev(_autoCliBundle("Host syslog for FRR",
    [`# Configure rsyslog/journald on the Linux host`,`echo '*.* @${v.host}' | sudo tee /etc/rsyslog.d/99-remote.conf`],
    `- name: Host remote syslog
  ansible.builtin.copy:
    dest: /etc/rsyslog.d/99-remote.conf
    content: "*.* @${v.host}\\n"`), "linux");
  if(vendor==="VyOS") return _withDev(_autoCliBundle("VyOS syslog",
    [`set system syslog host ${v.host} facility all level info`],
    `- name: VyOS syslog
  vyos.vyos.vyos_config:
    lines:
      - set system syslog host ${v.host} facility all level info`), "vyos");
  if(vendor==="Nokia") return _withDev(_autoCliBundle("Nokia syslog",
    [`enter candidate`,`set / system logging remote-server ${v.host}`,`commit now`],
    `- name: SR Linux syslog
  nokia.srlinux.config:
    update:
      - path: /system/logging/remote-server[host=${v.host}]
        value: {}`), "nokia_srl");
  if(vendor==="Aruba") return _withDev(_autoCliBundle("Aruba syslog",
    [`logging ${v.host}`],
    `- name: AOS-CX syslog
  arubanetworks.aoscx.aoscx_config:
    lines:
      - logging ${v.host}`), "aruba_aoscx");
  return null;
}
function stackGenericCli(vendor, lines, ansible){
  const map = {FRR:"frrouting",VyOS:"vyos",Nokia:"nokia_srl",Aruba:"aruba_aoscx"};
  return _withDev(_autoCliBundle(vendor+" config", lines, ansible), map[vendor]||"linux");
}
'''

# Wrappers inserted into each AUTO_* object — keyed by map name
AUTO_WRAPPERS = {
    "AUTO_IFACE_IPV4": "  FRR: v => stackIfaceIpv4('FRR', v),\n  VyOS: v => stackIfaceIpv4('VyOS', v),\n  Nokia: v => stackIfaceIpv4('Nokia', v),\n  Aruba: v => stackIfaceIpv4('Aruba', v),",
    "AUTO_STATIC": "  FRR: v => stackStatic('FRR', v),\n  VyOS: v => stackStatic('VyOS', v),\n  Nokia: v => stackStatic('Nokia', v),\n  Aruba: v => stackStatic('Aruba', v),",
    "AUTO_OSPF": "  FRR: v => stackOspf('FRR', v),\n  VyOS: v => stackOspf('VyOS', v),\n  Nokia: v => stackOspf('Nokia', v),\n  Aruba: v => stackOspf('Aruba', v),",
    "AUTO_BGP": "  FRR: v => stackBgp('FRR', v),\n  VyOS: v => stackBgp('VyOS', v),\n  Nokia: v => stackBgp('Nokia', v),\n  Aruba: v => stackBgp('Aruba', v),",
    "AUTO_VLAN": "  FRR: v => stackVlan('FRR', v),\n  VyOS: v => stackVlan('VyOS', v),\n  Nokia: v => stackVlan('Nokia', v),\n  Aruba: v => stackVlan('Aruba', v),",
    "AUTO_HOSTNAME": "  FRR: v => stackHostname('FRR', v),\n  VyOS: v => stackHostname('VyOS', v),\n  Nokia: v => stackHostname('Nokia', v),\n  Aruba: v => stackHostname('Aruba', v),",
    "AUTO_NTP": "  FRR: v => stackNtp('FRR', v),\n  VyOS: v => stackNtp('VyOS', v),\n  Nokia: v => stackNtp('Nokia', v),\n  Aruba: v => stackNtp('Aruba', v),",
    "AUTO_SYSLOG": "  FRR: v => stackSyslog('FRR', v),\n  VyOS: v => stackSyslog('VyOS', v),\n  Nokia: v => stackSyslog('Nokia', v),\n  Aruba: v => stackSyslog('Aruba', v),",
    "AUTO_DEFAULTRT": "  FRR: v => stackStatic('FRR', {prefix:'0.0.0.0', cidr:'0', mask:'0.0.0.0', nh:v.nh}),\n  VyOS: v => stackStatic('VyOS', {prefix:'0.0.0.0', cidr:'0', mask:'0.0.0.0', nh:v.nh}),\n  Nokia: v => stackStatic('Nokia', {prefix:'0.0.0.0', cidr:'0', mask:'0.0.0.0', nh:v.nh}),\n  Aruba: v => stackStatic('Aruba', {prefix:'0.0.0.0', cidr:'0', mask:'0.0.0.0', nh:v.nh}),",
    "AUTO_SWITCHPORT": "  FRR: v => stackGenericCli('FRR', [`# VLAN membership is host/bridge-side for FRR`], `- name: noop\\n  ansible.builtin.debug:\\n    msg: FRR has no switchport`),\n  VyOS: v => stackGenericCli('VyOS', [`set interfaces ethernet ${v.ifname} vif ${v.vlan}`], `- name: VyOS access vif\\n  vyos.vyos.vyos_config:\\n    lines:\\n      - set interfaces ethernet ${v.ifname} vif ${v.vlan}`),\n  Nokia: v => stackVlan('Nokia', {id:v.vlan, name:'VLAN'+v.vlan}),\n  Aruba: v => stackGenericCli('Aruba', [`interface ${v.ifname}`, `vlan access ${v.vlan}`], `- name: AOS-CX access vlan\\n  arubanetworks.aoscx.aoscx_config:\\n    lines:\\n      - interface ${v.ifname}\\n      - vlan access ${v.vlan}`),",
    "AUTO_IFDESC": "  FRR: v => stackGenericCli('FRR', [`interface ${v.ifname}`, `description ${v.desc}`], `- name: FRR ifDesc\\n  ansible.builtin.shell: vtysh -c \"configure terminal\" -c \"interface ${v.ifname}\" -c \"description ${v.desc}\" -c \"end\" -c \"write\"`),\n  VyOS: v => stackGenericCli('VyOS', [`set interfaces ethernet ${v.ifname} description '${v.desc}'`], `- name: VyOS ifDesc\\n  vyos.vyos.vyos_config:\\n    lines:\\n      - set interfaces ethernet ${v.ifname} description '${v.desc}'`),\n  Nokia: v => stackGenericCli('Nokia', [`enter candidate`,`set / interface ${v.ifname} description \"${v.desc}\"`,`commit now`], `- name: SR Linux ifDesc\\n  nokia.srlinux.config:\\n    update:\\n      - path: /interface[name=${v.ifname}]\\n        value:\\n          description: ${v.desc}`),\n  Aruba: v => stackGenericCli('Aruba', [`interface ${v.ifname}`, `description ${v.desc}`], `- name: AOS-CX ifDesc\\n  arubanetworks.aoscx.aoscx_config:\\n    lines:\\n      - interface ${v.ifname}\\n      - description ${v.desc}`),",
    "AUTO_LOOPBACK": "  FRR: v => stackIfaceIpv4('FRR', {ifname:v.name||('lo'+v.unit), ip:v.ip, mask:v.mask, cidr:v.cidr}),\n  VyOS: v => stackIfaceIpv4('VyOS', {ifname:'lo', ip:v.ip, mask:v.mask, cidr:v.cidr}),\n  Nokia: v => stackIfaceIpv4('Nokia', {ifname:'lo0', ip:v.ip, mask:v.mask, cidr:v.cidr}),\n  Aruba: v => stackIfaceIpv4('Aruba', {ifname:'loopback0', ip:v.ip, mask:v.mask, cidr:v.cidr}),",
    "AUTO_TRUNK": "  FRR: v => stackGenericCli('FRR', [`# trunking is host bridge VLAN filter`], `- name: noop\\n  ansible.builtin.debug:\\n    msg: host bridge VLAN filter`),\n  VyOS: v => stackGenericCli('VyOS', [`set interfaces ethernet ${v.ifname} vif ${v.vlans.split(',')[0]}`], `- name: VyOS trunk-ish vif\\n  vyos.vyos.vyos_config:\\n    lines:\\n      - set interfaces ethernet ${v.ifname} vif ${v.vlans.split(',')[0]}`),\n  Nokia: v => stackGenericCli('Nokia', [`enter candidate`,`set / interface ${v.ifname} vlan-tagging true`,`commit now`], `- name: SR Linux trunk\\n  nokia.srlinux.config:\\n    update:\\n      - path: /interface[name=${v.ifname}]\\n        value:\\n          vlan-tagging: true`),\n  Aruba: v => stackGenericCli('Aruba', [`interface ${v.ifname}`, `vlan trunk allowed ${v.vlans}`], `- name: AOS-CX trunk\\n  arubanetworks.aoscx.aoscx_config:\\n    lines:\\n      - interface ${v.ifname}\\n      - vlan trunk allowed ${v.vlans}`),",
    "AUTO_RADIUS": "  FRR: v => stackGenericCli('FRR', [`# configure PAM/sshd RADIUS on Linux host`], `- name: note\\n  ansible.builtin.debug:\\n    msg: configure host PAM RADIUS`),\n  VyOS: v => stackGenericCli('VyOS', [`set system login radius-server ${v.host} key '${v.key}'`], `- name: VyOS RADIUS\\n  vyos.vyos.vyos_config:\\n    lines:\\n      - set system login radius-server ${v.host} key '${v.key}'`),\n  Nokia: v => stackGenericCli('Nokia', [`enter candidate`,`set / system aaa authentication-server radius server ${v.host}`,`commit now`], `- name: SR Linux RADIUS\\n  nokia.srlinux.config:\\n    update:\\n      - path: /system/aaa/server-group[name=radius]/server[address=${v.host}]\\n        value: {}`),\n  Aruba: v => stackGenericCli('Aruba', [`radius-server host ${v.host} key ${v.key}`], `- name: AOS-CX RADIUS\\n  arubanetworks.aoscx.aoscx_config:\\n    lines:\\n      - radius-server host ${v.host} key ${v.key}`),",
    "AUTO_PORTCHANNEL": "  FRR: v => stackGenericCli('FRR', [`# create Linux bond on host`, `ip link add bond0 type bond mode 802.3ad`], `- name: host bond\\n  ansible.builtin.command: ip link add bond0 type bond mode 802.3ad`),\n  VyOS: v => stackGenericCli('VyOS', [`set interfaces bonding bond0 member interface ${v.ifname}`], `- name: VyOS bond\\n  vyos.vyos.vyos_config:\\n    lines:\\n      - set interfaces bonding bond0 member interface ${v.ifname}`),\n  Nokia: v => stackGenericCli('Nokia', [`enter candidate`,`set / interface lag${v.pcid} lag lacp fallback false`,`set / interface ${v.ifname} ethernet aggregate-id lag${v.pcid}`,`commit now`], `- name: SR Linux LAG\\n  nokia.srlinux.config:\\n    update:\\n      - path: /interface[name=${v.ifname}]/ethernet\\n        value:\\n          aggregate-id: lag${v.pcid}`),\n  Aruba: v => stackGenericCli('Aruba', [`interface ${v.ifname}`, `lag ${v.pcid}`], `- name: AOS-CX LAG\\n  arubanetworks.aoscx.aoscx_config:\\n    lines:\\n      - interface ${v.ifname}\\n      - lag ${v.pcid}`),",
}


def inject_stack_helpers(text: str) -> str:
    if MARKER in text:
        return text
    # Insert helpers just before first AUTO_IFACE_IPV4
    anchor = "/* Per-vendor template renderers — return {netconf, ncclient, ansible} */"
    if anchor not in text:
        raise SystemExit("anchor for AUTO helpers not found")
    return text.replace(anchor, STACK_TEMPLATES + "\n" + anchor, 1)


def inject_auto_keys(text: str) -> str:
    for name, block in AUTO_WRAPPERS.items():
        # Skip if already extended
        flag = f"/* ext:{name} */"
        if flag in text:
            continue
        # Insert after `const NAME = {`
        pat = re.compile(rf"(const\s+{name}\s*=\s*\{{)")
        m = pat.search(text)
        if not m:
            print(f"WARN: {name} not found", file=sys.stderr)
            continue
        insert = m.group(1) + "\n" + flag + "\n" + block + "\n"
        text = text[: m.start()] + insert + text[m.end() :]
    return text


def broaden_matchers(text: str) -> str:
    """Add VyOS/FRR/Aruba/Nokia regexes to key patterns if missing."""
    extras = {
        "iface-ipv4": (
            r"/^set interfaces ethernet\s+(\S+)\s+address\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)/im",
            r"/^nv set interface\s+(\S+)\s+ip address\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)/im",
            r"/^ip address\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)/im",
        ),
        "static-route": (
            r"/^set protocols static route\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)\s+next-hop\s+(\S+)/im",
            r"/^ip route\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)\s+(\S+)/im",
        ),
        "bgp-neighbor": (
            r"/^set protocols bgp\s+.*?neighbor\s+(\d+\.\d+\.\d+\.\d+)\s+remote-as\s+(\d+)/im",
            r"/^neighbor\s+(\d+\.\d+\.\d+\.\d+)\s+remote-as\s+(\d+)/im",
        ),
        "hostname": (
            r"/^set system host-name\s+(\S+)/im",
            r"/^sysname\s+(\S+)/im",
            r"/^config hostname\s+(\S+)/im",
            r"/^\/system identity set name=(\S+)/im",
        ),
        "ntp-server": (
            r"/^set system ntp server\s+(\S+)/im",
            r"/^ntp server\s+(\S+)/im",
        ),
    }
    # Safer: just ensure comment marker noting broadened matchers; matchAny already
    # covers IOS+Junos which is the dominant Automate path. Stack vendors get
    # templates when an IOS/Junos-shaped cmd is compared across vendors.
    if "/* MATCHERS_STACK_NOTE */" not in text:
        text = text.replace(
            "const AUTOMATION_MAPPINGS = [",
            "/* MATCHERS_STACK_NOTE */\n"
            "/* Stack vendors (FRR/VyOS/Nokia/Aruba) render via AUTO_* extensions;\n"
            "   matchAny still keys off IOS/Junos/EOS-shaped commands, which is how\n"
            "   cross-vendor Automate is invoked from Compare / equivalents. */\n"
            "const AUTOMATION_MAPPINGS = [",
            1,
        )
    # Light-touch: extend hostname matchAny
    old = """matchAny: [
      /^hostname\\s+(\\S+)/im,
      /^set system host-name\\s+(\\S+)/im,
    ],
    extract: cmd => {
      const m = cmd.match(/^(?:set system\\s+)?(?:hostname|host-name)\\s+(\\S+)/im);
      return m ? {hostname:m[1]} : null;
    },
    render: (vendor, v) => AUTO_HOSTNAME[vendor](v),"""
    new = """matchAny: [
      /^hostname\\s+(\\S+)/im,
      /^set system host-name\\s+(\\S+)/im,
      /^sysname\\s+(\\S+)/im,
      /^config hostname\\s+(\\S+)/im,
      /^nv set system hostname\\s+(\\S+)/im,
    ],
    extract: cmd => {
      const m = cmd.match(/^(?:set system\\s+)?(?:hostname|host-name)\\s+(\\S+)/im)
             || cmd.match(/^sysname\\s+(\\S+)/im)
             || cmd.match(/^config hostname\\s+(\\S+)/im)
             || cmd.match(/^nv set system hostname\\s+(\\S+)/im);
      return m ? {hostname:m[1]} : null;
    },
    render: (vendor, v) => AUTO_HOSTNAME[vendor](v),"""
    if old in text and "sysname\\s+" not in text[text.find("id: \"hostname\"") : text.find("id: \"hostname\"") + 800]:
        text = text.replace(old, new, 1)
    return text


def patch_os_dev_type(text: str) -> str:
    if "sros:" in text[text.find("OS_DEV_TYPE") : text.find("OS_DEV_TYPE") + 900]:
        return text
    text = text.replace(
        'aoscx:"aruba_aoscx", vrp:"huawei",',
        'aoscx:"aruba_aoscx", vrp:"huawei", sros:"nokia_sros",',
        1,
    )
    # Also add Nokia SR OS netmiko mapping if missing — netmiko uses nokia_sros
    if '"Nokia":    "nokia_srl"' in text and "nokia_sros" not in text[text.find("NETMIKO_DEV_TYPE") : text.find("NETMIKO_DEV_TYPE") + 600]:
        # Keep vendor Nokia → nokia_srl as default; OS map handles sros.
        pass
    return text


def patch_dev_type_for_os(text: str) -> str:
    """Prefer OS-specific netmiko type over vendor-level Cisco→ios."""
    old = """function devTypeFor(q){
  return NETMIKO_DEV_TYPE[q.vendor] || OS_DEV_TYPE[q.os] || "terminal_server";
}"""
    new = """function devTypeFor(q){
  // Prefer OS-specific device types (nxos/asa/iosxe/sros) over vendor defaults.
  return OS_DEV_TYPE[q.os] || NETMIKO_DEV_TYPE[q.vendor] || "terminal_server";
}"""
    if old in text:
        text = text.replace(old, new, 1)
    return text


def patch_ansible_cisco_os(text: str) -> str:
    """Add helper to pick Cisco Ansible collection by OS."""
    if "function ansibleModFor" in text:
        return text
    needle = "function isShowCmd(cmd){"
    helper = '''function ansibleModFor(q){
  const base = ANSIBLE_MOD[q.vendor] || {show:"ansible.builtin.shell", cfg:"ansible.builtin.shell", collection:"ansible.builtin"};
  if(q.vendor === "Cisco"){
    if(q.os === "nxos") return {show:"cisco.nxos.nxos_command", cfg:"cisco.nxos.nxos_config", collection:"cisco.nxos"};
    if(q.os === "asa")  return {show:"cisco.asa.asa_command",  cfg:"cisco.asa.asa_config",  collection:"cisco.asa"};
    if(q.os === "iosxe") return {show:"cisco.ios.ios_command", cfg:"cisco.ios.ios_config", collection:"cisco.ios"};
  }
  if(q.vendor === "Nokia" && q.os === "sros"){
    return {show:"ansible.builtin.shell", cfg:"ansible.builtin.shell", collection:"ansible.builtin"};
  }
  return base;
}
'''
    return text.replace(needle, helper + needle, 1)


def patch_cat_groups(text: str) -> str:
    """Ensure VXLAN/EVPN filter chips exist."""
    if '"VXLAN"' in text and '"EVPN"' in text and "CAT_GROUPS" in text:
        # Try to inject into a fabric/overlay group if not present near VXLAN usage in CAT_GROUPS
        if re.search(r'CAT_GROUPS[\s\S]{0,2000}VXLAN', text):
            return text
    # Find CAT_GROUPS and add overlay group
    m = re.search(r"(const\s+CAT_GROUPS\s*=\s*\[)", text)
    if not m:
        return text
    if "/* overlay cats */" in text:
        return text
    insert = m.group(1) + '\n  /* overlay cats */\n  ["Overlay", ["VXLAN","EVPN"]],\n'
    return text[: m.start()] + insert + text[m.end() :]


def patch_safe_render(text: str) -> str:
    """Prefer explicit vendor templates; never silently fall back to Cisco YANG."""
    text2 = text
    for name in [
        "AUTO_IFACE_IPV4",
        "AUTO_STATIC",
        "AUTO_OSPF",
        "AUTO_BGP",
        "AUTO_VLAN",
        "AUTO_SWITCHPORT",
        "AUTO_NTP",
        "AUTO_SYSLOG",
        "AUTO_HOSTNAME",
        "AUTO_IFDESC",
        "AUTO_LOOPBACK",
        "AUTO_DEFAULTRT",
        "AUTO_TRUNK",
        "AUTO_RADIUS",
        "AUTO_PORTCHANNEL",
    ]:
        cisco_fb = f"(vendor, v) => ({name}[vendor]||{name}.Cisco)(v)"
        bare = f"(vendor, v) => {name}[vendor](v)"
        null_safe = f"(vendor, v) => {name}[vendor] ? {name}[vendor](v) : null"
        text2 = text2.replace(cisco_fb, null_safe)
        if null_safe not in text2:
            text2 = text2.replace(bare, null_safe)
    return text2


def main() -> None:
    text = HTML.read_text()
    if MARKER in text and "ansibleModFor" in text and "/* overlay cats */" in text:
        print("already patched")
        return
    text = inject_stack_helpers(text)
    text = inject_auto_keys(text)
    text = broaden_matchers(text)
    text = patch_os_dev_type(text)
    text = patch_dev_type_for_os(text)
    text = patch_ansible_cisco_os(text)
    text = patch_cat_groups(text)
    text = patch_safe_render(text)
    if "--dry-run" in sys.argv:
        print("dry-run ok, would write", len(text), "bytes")
        return
    HTML.write_text(text)
    print("patched", HTML, "bytes", len(text))


if __name__ == "__main__":
    main()