@takodotid/circuit 0.34.3 → 0.35.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.
Files changed (240) hide show
  1. package/README.md +13 -14
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11345 -9024
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +17 -11
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -1,700 +0,0 @@
1
- // Raisecom ROS, as on the RAX700 series.
2
- //
3
- // An interface, an access-list or an OSPF process opens a block, `!` closes it, and `no` removes a line. Commands apply as they arrive and persist only on `write`. There is no rollback, so management is proved reachable in a fresh session before anything is written: a reboot then restores what was there before.
4
-
5
- import type { ClientChannel } from "ssh2";
6
- import { familyOf, host, netmask, split, wildcard } from "../../../core/addr";
7
- import { marker, resolve, substitute } from "../../../core/secrets";
8
- import { offset } from "../../../core/time";
9
- import type { Device, Port, SnmpUser, Switched, Vrrp } from "../../../schema";
10
- import { type ConsoleDialect, consoleLogin, consoleOf, openConsole } from "../../../transport/console";
11
- import { connect, managementHost, readUntil, send, shell } from "../../../transport/ssh";
12
- import { portsOf, spell } from "../catalog";
13
- import { ipv4Networks, lagIds, switchUnsupported } from "../derive";
14
- import { compare, type Dialect, entries, type Group, scrub, steps, vlanListChange } from "../lines";
15
- import type { ApplyOptions, DeviceAdapter, Plan, ReadOptions, Step } from "../types";
16
-
17
- /** Lines the device prints whatever is configured, or that are part of moving around the CLI. */
18
- const DEVICE_FACTS = [
19
- "Being processed",
20
- "System current configuration",
21
- "recv-mode",
22
- "mpls te periodic-flooding",
23
- "spanning-tree region-configuration",
24
- "interface NULL",
25
- "ip rarp",
26
- "quit",
27
- "config",
28
- "exit",
29
- // Implied by any layer 2 setting on a port; the plan sends it where needed.
30
- "portswitch",
31
- // A port's speed or duplex set back to auto prints as the default it is, and `no duplex` is refused.
32
- "speed auto",
33
- "duplex auto",
34
- ];
35
-
36
- /** Lines whose opposite is not `no` and the line. */
37
- const OPPOSITES: [RegExp, string][] = [
38
- [/^mode manual$/, "mode lacp"],
39
- [/^ip ospf passive-interface enable$/, "ip ospf passive-interface disable"],
40
- [/^lldp disable$/, "lldp enable"],
41
- [/^spanning-tree disable$/, "spanning-tree enable"],
42
- [/^switchport mode trunk$/, "switchport mode access"],
43
- [/^telnet-server disable$/, "telnet-server enable"],
44
- [/^shutdown$/, "no shutdown"],
45
- // The device refuses `no storm-control mode`; kbps is its default and prints nothing.
46
- [/^storm-control mode pps$/, "storm-control mode kbps"],
47
- ];
48
-
49
- /** Lines whose removal takes only their keyword, not their value. */
50
- const UNDO_BY_KEYWORD =
51
- /^(description|switchport access vlan|switchport trunk native vlan|switchport trunk allowed vlan|storm-control broadcast|ip address|port-channel|ip ospf cost|ip ospf network)\b/;
52
-
53
- function undo(line: string): string {
54
- for (const [pattern, opposite] of OPPOSITES) if (pattern.test(line)) return opposite;
55
- if (/^rule \d+/.test(line)) return `no ${line.split(" ").slice(0, 2).join(" ")}`;
56
-
57
- // A VRRP group goes with its address; its other settings return to their defaults by keyword. Preemption is on unless turned off.
58
- const vrrp = /^(no )?vrrp (\d+) (ip \S+|priority|timers advertise-interval|preempt)/.exec(line);
59
- if (vrrp) return vrrp[1] ? `vrrp ${vrrp[2]} preempt` : `no vrrp ${vrrp[2]} ${vrrp[3]}`;
60
-
61
- const ospfProcess = /^router ospf (\d+)/.exec(line);
62
- if (ospfProcess) return `no router ospf ${ospfProcess[1]}`;
63
-
64
- // An SNMPv3 user, its group and its access are each removed by their name alone.
65
- const snmpUser = /^snmp-server user (\S+)/.exec(line);
66
- if (snmpUser) return `no snmp-server user ${snmpUser[1]}`;
67
-
68
- const snmpGroup = /^snmp-server group \S+ user (\S+) usm$/.exec(line);
69
- if (snmpGroup) return `no snmp-server group user ${snmpGroup[1]} usm`;
70
-
71
- const snmpAccess = /^snmp-server access (\S+) .* usm (\S+)$/.exec(line);
72
- if (snmpAccess) return `no snmp-server access ${snmpAccess[1]} usm ${snmpAccess[2]}`;
73
-
74
- const keyword = UNDO_BY_KEYWORD.exec(line);
75
- if (keyword) return `no ${keyword[1]}`;
76
-
77
- return `no ${line}`;
78
- }
79
-
80
- export const dialect: Dialect = {
81
- block: /^(interface |access-list |router ospf )/,
82
- indented: false,
83
- terminator: "!",
84
- ignore: (line) => line.startsWith("!") || DEVICE_FACTS.some((fact) => line.startsWith(fact)),
85
- undo,
86
- leave: "exit",
87
- // A password prints as a cipher and renders as a marker; both are a secret.
88
- canon: (line) =>
89
- line
90
- .replace(/password (cipher )?(\S+)/, "password <secret>")
91
- .replace(/community <secret:\w+>/, "community <redacted>")
92
- // An SNMPv3 user prints its localized keys and renders its passphrases; both are secrets.
93
- .replace(
94
- /^snmp-server user (\S+) (?:authentication|authkey) (\S+) \S+ (?:privacy|privkey)\s+(\S+) \S+$/,
95
- "snmp-server user $1 $2 $3 <secret>"
96
- )
97
- // An access group prints a notify view the render leaves to its default.
98
- .replace(/^(snmp-server access \S+ read \S+) notify internet (usm \S+)$/, "$1 $2"),
99
- // A LAG exists before a port joins it, and an OSPF process comes after the interfaces it names.
100
- order: (path) => {
101
- const header = path[0] ?? "";
102
- if (header.startsWith("interface port-channel")) return 1;
103
- if (header.startsWith("interface ")) return 2;
104
- if (header.startsWith("router ospf")) return 3;
105
- return 0;
106
- },
107
- };
108
-
109
- /** The OSPF process number. */
110
- const OSPF_PROCESS = 1;
111
-
112
- /** The SNMPv3 access group every user belongs to, read-only on the whole tree. */
113
- const SNMP_GROUP = "circuit";
114
-
115
- const SNMP_AUTH: Partial<Record<SnmpUser["auth"], string>> = { sha1: "sha" };
116
-
117
- /** The access-list that guards every management service. */
118
- export const MANAGEMENT_ACL = 1000;
119
-
120
- /** `99,200-201,999`, the platform's compression of a VLAN list. */
121
- export function vlanList(ids: number[]): string {
122
- const sorted = [...new Set(ids)].sort((a, b) => a - b);
123
- const ranges: string[] = [];
124
-
125
- for (let i = 0; i < sorted.length; i++) {
126
- let end = i;
127
- while (sorted[end + 1] === sorted[end]! + 1) end++;
128
- ranges.push(i === end ? `${sorted[i]}` : `${sorted[i]}-${sorted[end]}`);
129
- i = end;
130
- }
131
-
132
- return ranges.join(",");
133
- }
134
-
135
- type AnyPort = Port<string, string, string, string>;
136
-
137
- export function render(device: Device): string {
138
- const lines: string[] = [];
139
- const vlans = device.vlans ?? {};
140
- const vlanId = (name: string) => vlans[name]!.id;
141
- const management = device.management ?? {};
142
- const allow = management.allow ?? [];
143
-
144
- const ids = Object.values(vlans).map((vlan) => vlan.id);
145
- if (ids.length) lines.push(`create vlan ${vlanList(ids)} active`, "!");
146
-
147
- const lagNumbers = lagIds(device);
148
-
149
- for (const [name, lag] of Object.entries(device.interfaces ?? {})) {
150
- if (lag.type !== "lag") continue;
151
-
152
- lines.push(`interface port-channel ${lagNumbers.get(name)}`);
153
- if (lag.description) lines.push(`description ${lag.description}`);
154
- if (lag.mode === "static") lines.push("mode manual");
155
- lines.push(...switching(lag, vlanId), "!");
156
- }
157
-
158
- if (allow.length) {
159
- // An access-list description takes one word.
160
- lines.push(`access-list ${MANAGEMENT_ACL}`, " description management");
161
- allow.forEach((cidr, index) => lines.push(` rule ${(index + 1) * 10} permit ${host(cidr)} ${netmask(split(cidr).len)}`));
162
- // Stated, so the list reads the same without knowing the firmware's implicit end.
163
- lines.push(" rule 100 deny any", "!");
164
- }
165
-
166
- for (const [name, user] of Object.entries(device.users ?? {})) lines.push(`user name ${name} password ${marker(user.password)}`);
167
- if (management.privilege_password) lines.push(`enable password ${marker(management.privilege_password)}`);
168
-
169
- const timezone = device.system?.timezone;
170
- if (timezone) {
171
- const minutes = offset(timezone);
172
- const absolute = Math.abs(minutes);
173
- lines.push(`clock timezone ${minutes < 0 ? "-" : "+"} ${Math.floor(absolute / 60)} ${absolute % 60} 0`);
174
- }
175
-
176
- lines.push("!");
177
-
178
- for (const port of portsOf(device.platform, device.model)) {
179
- const settings = device.ports?.[port as keyof typeof device.ports];
180
- lines.push(`interface ${spell(device.platform, device.model, port)}`, ...physicalPort(settings, vlanId, lagNumbers), "!");
181
- }
182
-
183
- const ospf = device.routing?.ospf;
184
-
185
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
186
- if (iface.type !== "vlan") continue;
187
-
188
- lines.push(`interface vlan ${vlanId(iface.vlan)}`);
189
-
190
- for (const entry of iface.addresses ?? []) {
191
- const address = typeof entry === "string" ? entry : entry.address;
192
- if (familyOf(address) === "ipv4") lines.push(`ip address ${host(address)} ${netmask(split(address).len)}`);
193
- }
194
-
195
- const ospfSettings = Object.values(ospf?.areas ?? {}).find((area) => area.interfaces[name])?.interfaces[name];
196
- if (ospfSettings?.network === "point-to-point") lines.push("ip ospf network ptp");
197
- if (ospfSettings?.cost !== undefined) lines.push(`ip ospf cost ${ospfSettings.cost}`);
198
- if (ospfSettings?.passive) lines.push("ip ospf passive-interface enable");
199
-
200
- for (const group of iface.vrrp ?? []) lines.push(...vrrpLines(group));
201
-
202
- lines.push("!");
203
- }
204
-
205
- if (ospf) {
206
- const routerId = device.routing?.router_id;
207
- lines.push(`router ospf ${OSPF_PROCESS}${routerId ? ` router-id ${routerId}` : ""}`);
208
-
209
- for (const [areaId, area] of Object.entries(ospf.areas)) {
210
- for (const iface of Object.keys(area.interfaces)) {
211
- for (const prefix of ipv4Networks(device, iface)) {
212
- lines.push(`network ${host(prefix)} ${wildcard(split(prefix).len)} area ${areaId}`);
213
- }
214
- }
215
- }
216
-
217
- for (const source of ospf.redistribute ?? []) lines.push(`redistribute ${source}`);
218
- lines.push("!");
219
- }
220
-
221
- // Storm control acts only once detection is on.
222
- if (Object.values(device.ports ?? {}).some((port) => port?.storm_control)) lines.push("storm-control detection enable");
223
- if (device.system?.transceiver_monitoring) lines.push("transceiver ddm enable");
224
- // LLDP and spanning tree are off by default and turned on by the config.
225
- if (device.lldp) lines.push("lldp enable");
226
- if (device.stp) {
227
- lines.push("spanning-tree enable");
228
- if (device.stp.priority !== undefined) lines.push(`spanning-tree priority ${device.stp.priority}`);
229
- }
230
-
231
- // The factory communities survive a reset, and one of them can write, so they are always removed.
232
- lines.push("no snmp-server community public", "no snmp-server community private");
233
- const snmp = management.snmp;
234
- if (snmp?.community) lines.push(`snmp-server community ${marker(snmp.community)} ro`);
235
-
236
- const snmpUsers = Object.entries(snmp?.users ?? {});
237
- if (snmpUsers.length) lines.push(`snmp-server access ${SNMP_GROUP} read internet usm authpriv`);
238
-
239
- for (const [name, user] of snmpUsers) {
240
- const authentication = `authentication ${SNMP_AUTH[user.auth]} ${marker(user.auth_password)}`;
241
- const privacy = `privacy ${user.privacy} ${marker(user.privacy_password)}`;
242
- lines.push(`snmp-server user ${name} ${authentication} ${privacy}`, `snmp-server group ${SNMP_GROUP} user ${name} usm`);
243
- }
244
-
245
- if ((snmp?.community || snmpUsers.length) && allow.length) lines.push(`snmp-server access-list ${MANAGEMENT_ACL}`);
246
-
247
- for (const route of device.routing?.static ?? []) {
248
- const { addr, len } = split(route.prefix);
249
- if (familyOf(addr) === "ipv4" && route.via) lines.push(`ip route ${addr} ${netmask(len)} ${route.via}`);
250
- }
251
-
252
- if (management.telnet) {
253
- lines.push("telnet-server enable");
254
- if (allow.length) lines.push(`telnet-server access-list ${MANAGEMENT_ACL}`);
255
- } else lines.push("telnet-server disable");
256
-
257
- const ssh = management.ssh;
258
- if (ssh?.auth_timeout) lines.push(`ssh2 server authentication-timeout ${ssh.auth_timeout}`);
259
- if (ssh?.auth_retries) lines.push(`ssh2 server authentication-retries ${ssh.auth_retries}`);
260
- if (ssh && allow.length) lines.push(`ssh2 access-list ${MANAGEMENT_ACL}`);
261
-
262
- const ntp = device.system?.ntp;
263
- if (ntp?.servers.length) lines.push("ntp enable", ...ntp.servers.map((server) => `ntp server ${server}`));
264
-
265
- return lines.join("\n") + "\n";
266
- }
267
-
268
- /** Access or trunk membership, for a port or a LAG. */
269
- function switching(settings: Switched<string>, vlanId: (name: string) => number): string[] {
270
- const lines: string[] = [];
271
- if (settings.access_vlan) lines.push(`switchport access vlan ${vlanId(settings.access_vlan)}`);
272
-
273
- if (settings.trunk_vlans) {
274
- if (settings.native_vlan) lines.push(`switchport trunk native vlan ${vlanId(settings.native_vlan)}`);
275
- lines.push(`switchport trunk allowed vlan ${vlanList(settings.trunk_vlans.map(vlanId))}`, "switchport mode trunk");
276
- }
277
-
278
- return lines;
279
- }
280
-
281
- /** A VRRP group. The platform enables a group once it has an address, and preempts unless told not to. */
282
- function vrrpLines(group: Vrrp): string[] {
283
- const lines = [`vrrp ${group.id} ip ${group.address}`];
284
- if (group.interval !== undefined && group.interval !== 1) lines.push(`vrrp ${group.id} timers advertise-interval ${group.interval}`);
285
- if (group.priority !== undefined && group.priority !== 100) lines.push(`vrrp ${group.id} priority ${group.priority}`);
286
- if (!group.preempt) lines.push(`no vrrp ${group.id} preempt`);
287
- return lines;
288
- }
289
-
290
- /** A port the config does not declare is shut down. A LAG member holds its membership and description; the LAG holds the rest. */
291
- function physicalPort(settings: AnyPort | undefined, vlanId: (name: string) => number, lagNumbers: Map<string, number>): string[] {
292
- if (!settings) return ["shutdown"];
293
-
294
- const lines: string[] = [];
295
- if (settings.description) lines.push(`description ${settings.description}`);
296
- if (settings.lag) return [...lines, `port-channel ${lagNumbers.get(settings.lag)}`];
297
-
298
- lines.push(...switching(settings, vlanId));
299
-
300
- if (settings.lldp === false) lines.push("lldp disable");
301
- if (settings.stp === false) lines.push("spanning-tree disable");
302
- const broadcast = settings.storm_control?.broadcast;
303
- if (broadcast && "pps" in broadcast) lines.push("storm-control mode pps", `storm-control broadcast pps ${broadcast.pps}`);
304
- return lines;
305
- }
306
-
307
- /** The firmware offers only an `ssh-rsa` host key, so that one is allowed for this platform alone. Key exchange, cipher and MAC still negotiate modern ones. */
308
- const HOST_KEYS = ["ssh-rsa"];
309
-
310
- /** A session, and how to prove access in a fresh login: a new SSH session, or logging out and in again on the console. */
311
- export type Session = { channel: ClientChannel; close: () => void; fresh: () => Promise<Session> };
312
-
313
- /** How the console asks for a login. Not yet proved on a device: the RAX721's console was never cabled to a jump host. */
314
- export const RAISECOM_CONSOLE: ConsoleDialect = {
315
- login: /login:\s*$/i,
316
- password: /password:\s*$/i,
317
- prompt: /[#>]\s*$/,
318
- logout: "exit",
319
- others: [[/\(config[^)\n]*\)#\s*$/, "end\n"]],
320
- };
321
-
322
- /** An interactive shell, paging off, over SSH or on the console. A session that does not answer as ROS is refused, so nothing meant for it can reach another shell. */
323
- export async function open(device: Device, options: ReadOptions = {}): Promise<Session> {
324
- const session = options.console ? await consoleShell(device) : await sshShell(device);
325
-
326
- const version = await send(session.channel, "show version", 15_000, 1500);
327
- if (!version.includes("ROS")) {
328
- session.close();
329
- throw new Error(`${device.name} did not answer as ROS`);
330
- }
331
-
332
- await send(session.channel, "terminal page-break disable", 15_000, 1000);
333
- return session;
334
- }
335
-
336
- async function sshShell(device: Device): Promise<Session> {
337
- const user = device.users?.[device.connection.user];
338
- if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
339
-
340
- const client = await connect({
341
- host: managementHost(device),
342
- port: device.connection.port,
343
- user: device.connection.user,
344
- password: resolve(user.password),
345
- hostKeys: HOST_KEYS,
346
- });
347
- const channel = await shell(client);
348
- const close = () => {
349
- channel.end();
350
- client.end();
351
- };
352
-
353
- const banner = await readUntil(channel, (text) => /[#>]\s*$/.test(text.trimEnd()), 30_000);
354
- if (!/[#>]\s*$/.test(banner.trimEnd())) {
355
- close();
356
- throw new Error(`${device.name} did not answer as ROS`);
357
- }
358
-
359
- return {
360
- channel,
361
- close,
362
- fresh: async () => {
363
- close();
364
- return open(device);
365
- },
366
- };
367
- }
368
-
369
- async function consoleShell(device: Device): Promise<Session> {
370
- const { console, user, password } = consoleOf(device);
371
- const opened = await openConsole(console);
372
-
373
- try {
374
- await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password, console.passwordOnly);
375
- } catch (error) {
376
- opened.close();
377
- throw error;
378
- }
379
-
380
- const session: Session = {
381
- channel: opened.channel,
382
- close: () => {
383
- opened.channel.write(`${RAISECOM_CONSOLE.logout}\n`);
384
- opened.close();
385
- },
386
- fresh: async () => {
387
- await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password, console.passwordOnly);
388
- await send(opened.channel, "terminal page-break disable", 15_000, 1000);
389
- return session;
390
- },
391
- };
392
-
393
- return session;
394
- }
395
-
396
- /** What the device runs, credentials scrubbed. */
397
- export async function read(device: Device, _network: readonly Device[] = [], options: ReadOptions = {}): Promise<string> {
398
- const { channel, close } = await open(device, options);
399
-
400
- try {
401
- // Without paging off and a long wait for silence, the dump stops halfway with no error. Even then a pause of the device's own can end it early: a complete dump ends at the prompt, and one that does not is read again rather than planned against.
402
- let dump = "";
403
- for (let attempt = 1; ; attempt++) {
404
- dump = await send(channel, "show running-config", 150_000, 6000);
405
- if (/[#>]\s*$/.test(dump.replace(/\r/g, "").trimEnd())) break;
406
- if (attempt === 3) throw new Error(`${device.name} returned an incomplete configuration three times`);
407
- // What the device still prints of the cut dump ends before the next one starts.
408
- await send(channel, "", 30_000, 6000);
409
- }
410
- const lines = dump.replace(/\r/g, "").split("\n");
411
- const start = lines.findIndex((line) => line.trim().startsWith("!ROS version"));
412
- if (start < 0) throw new Error(`${device.name} returned no configuration`);
413
-
414
- // The prompt is not configuration; uptime, clock and boot count change on an untouched device.
415
- const body = lines.slice(start).filter((line) => line.startsWith("!") || !/[#>]\s*$/.test(line.trim()));
416
- const text = body
417
- .join("\n")
418
- .replace(/^!(System (up )?time|Boot times):.*\n/gm, "")
419
- .trim();
420
- // An SNMPv3 user prints localized keys, which authenticate as well as the passphrase.
421
- return scrub(text + "\n").replace(/(authkey \S+|privkey\s+\S+) \S+/g, "$1 <redacted>");
422
- } finally {
423
- close();
424
- }
425
- }
426
-
427
- export function unsupported(device: Device): string[] {
428
- const found: string[] = [];
429
-
430
- if (device.management?.interfaces) found.push("management.interfaces, which only FortiOS builds for now");
431
-
432
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
433
- if (iface.type === "bridge") found.push(`interfaces.${name}, a bridge, which only FortiOS builds for now`);
434
- if (iface.type === "vlan" && iface.mac) found.push(`interfaces.${name}.mac, which only RouterOS sets for now`);
435
- }
436
-
437
- for (const [name, port] of Object.entries(device.ports ?? {})) {
438
- if (!port) continue;
439
-
440
- for (const field of ["speed", "acl", "mtu", "addresses", "vrrp", "vrf"] as const) {
441
- if (port[field] !== undefined) found.push(`ports.${name}.${field}`);
442
- }
443
-
444
- if (port.lldp === true) found.push(`ports.${name}.lldp true, LLDP is turned on per device`);
445
- if (typeof port.stp === "object" || port.stp === true) found.push(`ports.${name}.stp other than false`);
446
- const storm = port.storm_control;
447
- if (storm && (storm.multicast || storm.unknown_unicast || (storm.broadcast && "percent" in storm.broadcast))) {
448
- found.push(`ports.${name}.storm_control other than broadcast in pps`);
449
- }
450
- }
451
-
452
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
453
- if (iface.type === "lag") {
454
- for (const field of ["addresses", "vrrp", "vrf", "mtu", "storm_control", "stp"] as const) {
455
- if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}, a LAG here only switches`);
456
- }
457
- continue;
458
- }
459
-
460
- if (iface.type !== "vlan") {
461
- found.push(`interfaces.${name}, a ${iface.type}`);
462
- continue;
463
- }
464
-
465
- for (const field of ["vrf", "description", "mtu", "ipv6_ra"] as const) {
466
- if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}`);
467
- }
468
-
469
- for (const entry of iface.addresses ?? []) {
470
- const address = typeof entry === "string" ? entry : entry.address;
471
- if (familyOf(address) === "ipv6") found.push(`interfaces.${name}.addresses ${address}, only IPv4 here`);
472
- }
473
-
474
- for (const group of iface.vrrp ?? []) {
475
- if (familyOf(group.address) === "ipv6") found.push(`interfaces.${name}.vrrp for IPv6`);
476
- }
477
- }
478
-
479
- const routing = device.routing;
480
-
481
- for (const field of ["bgp", "rpki"] as const) {
482
- if (routing?.[field]) found.push(`routing.${field}`);
483
- }
484
-
485
- const ospf = routing?.ospf;
486
- if (ospf?.families?.includes("ipv6")) found.push("routing.ospf for IPv6");
487
-
488
- for (const [areaId, area] of Object.entries(ospf?.areas ?? {})) {
489
- for (const [name, settings] of Object.entries(area.interfaces)) {
490
- if (device.interfaces?.[name]?.type !== "vlan")
491
- found.push(`routing.ospf.areas.${areaId}.${name}, OSPF runs on VLAN interfaces here`);
492
- if (settings?.bfd) found.push(`routing.ospf.areas.${areaId}.${name}.bfd`);
493
- }
494
- }
495
-
496
- for (const route of routing?.static ?? []) {
497
- if (!route.via || route.vrf || familyOf(route.prefix) === "ipv6") {
498
- found.push(`routing.static ${route.prefix}, only an IPv4 next hop in the default table`);
499
- }
500
-
501
- if (route.description || route.distance) found.push(`routing.static ${route.prefix} description and distance`);
502
- }
503
-
504
- for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
505
- if (vlan.description) found.push(`vlans.${name}.description`);
506
- }
507
-
508
- const UNSUPPORTED_SECTIONS = [
509
- "firewall",
510
- "dhcp",
511
- "dhcp_relay",
512
- "acls",
513
- "policies",
514
- "prefix_sets",
515
- "hardware_offload",
516
- "certificates",
517
- "vrfs",
518
- "flow_export",
519
- ] as const;
520
-
521
- for (const field of UNSUPPORTED_SECTIONS) {
522
- if (device[field] !== undefined) found.push(field);
523
- }
524
-
525
- const management = device.management ?? {};
526
-
527
- for (const field of ["http", "https", "api", "api_tls", "ftp", "native", "console"] as const) {
528
- if (management[field]) found.push(`management.${field}`);
529
- }
530
-
531
- for (const [name, user] of Object.entries(management.snmp?.users ?? {})) {
532
- if (!SNMP_AUTH[user.auth])
533
- found.push(`management.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
534
- }
535
-
536
- if (management.snmp?.contact || management.snmp?.location) found.push("management.snmp.contact and location");
537
- if (management.ssh?.weak_crypto) found.push("management.ssh.weak_crypto");
538
-
539
- if (device.lldp && device.lldp !== true) found.push("lldp.interfaces, LLDP is per device");
540
- if (device.stp?.mode && device.stp.mode !== "mstp") found.push("stp.mode other than mstp");
541
-
542
- for (const [name, user] of Object.entries(device.users ?? {})) {
543
- if (user.role !== "admin") found.push(`users.${name}.role other than admin`);
544
- }
545
-
546
- const system = device.system ?? {};
547
-
548
- if (system.dns || system.logging || system.release_channel || system.banner || system.ntp?.serve) {
549
- found.push("system.dns, logging, release_channel, banner and ntp.serve");
550
- }
551
-
552
- found.push(...switchUnsupported(device));
553
-
554
- return found;
555
- }
556
-
557
- /** What the device prints when it refuses a line. */
558
- const REFUSED = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
559
-
560
- /** Lines that ask for `y` and do nothing without it. */
561
- const CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
562
-
563
- /** A service letting go of an access-list, so the list can change. */
564
- const RELEASE = /^no (\S+ access-list \d+)$/;
565
-
566
- export async function apply(device: Device, _network: readonly Device[], plan: Plan, options: ApplyOptions): Promise<void> {
567
- const session = await open(device, options);
568
- const { channel } = session;
569
- // Bindings released for an access-list edit. A failure binds them again before anything else, so the list keeps guarding management.
570
- const released: string[] = [];
571
-
572
- try {
573
- await send(channel, "config", 10_000);
574
-
575
- for (const step of plan.steps) {
576
- for (const line of step.send) {
577
- let answer = await send(channel, substitute(line), 20_000, 2000);
578
- // A confirming line asks for `y` unless the change needs no confirming, when the device sets it at once.
579
- const asks = /[[(]y\/n[\])]|input 'y'/i.test(answer) || (CONFIRM.test(line) && !/set successfully/i.test(answer));
580
- if (asks) answer += await send(channel, "y", 10_000, 1000);
581
- if (REFUSED.test(answer)) throw new Error(`${step.title}: ${line}\n device said: ${answer.trim().slice(0, 300)}`);
582
-
583
- const release = RELEASE.exec(line);
584
- if (release) released.push(release[1]!);
585
- else if (released.includes(line)) released.splice(released.indexOf(line), 1);
586
- }
587
- }
588
-
589
- await send(channel, "exit", 10_000);
590
- } catch (error) {
591
- await send(channel, "end", 10_000).catch(() => undefined);
592
- await send(channel, "config", 10_000).catch(() => undefined);
593
- for (const binding of released) await send(channel, binding, 10_000).catch(() => undefined);
594
- if (released.length) console.log(` bound again: ${released.join(", ")}`);
595
- session.close();
596
- throw error;
597
- }
598
-
599
- const proof = await session.fresh().catch(() => undefined);
600
- if (!proof)
601
- throw new Error(
602
- "applied, but a fresh login failed. Not written: a reboot restores the previous configuration. Recover from the console."
603
- );
604
- await send(proof.channel, "write", 60_000, 3000);
605
- proof.close();
606
- console.log(" applied, login proved, written");
607
- }
608
-
609
- /** Services holding the management access-list. It cannot change while any of them does. */
610
- const holders = (config: string) =>
611
- [...config.matchAll(new RegExp(`^([a-z0-9-]+) access-list ${MANAGEMENT_ACL}$`, "gm"))].map((match) => match[1]!);
612
-
613
- /** The VLAN IDs of a `create vlan 10,20-30 active` line. */
614
- export function createdIds(line: string): number[] {
615
- const list = line.replace(/^create vlan /, "").replace(/ active$/, "");
616
- const ids: number[] = [];
617
-
618
- for (const part of list.split(",")) {
619
- const [first, last] = part.split("-").map(Number);
620
- for (let id = first!; id <= (last ?? first!); id++) ids.push(id);
621
- }
622
-
623
- return ids;
624
- }
625
-
626
- function plan(desired: string, current: string): ReturnType<DeviceAdapter["plan"]> {
627
- const groups = compare(desired, current, dialect);
628
-
629
- // An access-list rule is not overwritten by one with the same number; the old one is removed first.
630
- const existing = entries(current, dialect);
631
-
632
- for (const group of groups) {
633
- if (!group.path[0]?.startsWith("access-list ")) continue;
634
-
635
- for (const line of group.add) {
636
- const number = /^rule (\d+) /.exec(line)?.[1];
637
- if (!number) continue;
638
-
639
- const old = existing.find(
640
- (entry) =>
641
- entry.path.join("\n") === group.path.join("\n") && entry.line.startsWith(`rule ${number} `) && entry.line !== line
642
- );
643
- if (old && !group.remove.includes(old.line)) group.remove.push(old.line);
644
- }
645
- }
646
-
647
- // A VLAN is created in a list and removed one at a time, with `no vlan <id>`.
648
- vlanListChange(
649
- groups,
650
- "create vlan ",
651
- createdIds,
652
- (ids) => [`create vlan ${vlanList(ids)} active`],
653
- (ids) => ids.map((id) => `vlan ${id}`)
654
- );
655
-
656
- // A port takes layer 2 settings only once it is switched.
657
- for (const group of groups) {
658
- const isPort = group.path[0]?.startsWith("interface ") ?? false;
659
- const switches = group.add.some((line) => line.startsWith("switchport") || line.startsWith("port-channel"));
660
- if (isPort && switches) group.add.unshift("portswitch");
661
- }
662
-
663
- // A LAG is removed only once no port is in it, so its removal goes after every other step.
664
- const lagRemovals: string[] = [];
665
-
666
- for (const group of groups) {
667
- if (group.path.length) continue;
668
- lagRemovals.push(...group.remove.filter((line) => line.startsWith("interface port-channel")));
669
- group.remove = group.remove.filter((line) => !line.startsWith("interface port-channel"));
670
- }
671
-
672
- // Editing the access-list means releasing every service holding it first, and binding the wanted ones after.
673
- const releaseAndBind = (group: Group) => {
674
- if (group.path[0] !== `access-list ${MANAGEMENT_ACL}`) return { before: [], after: [] };
675
-
676
- return {
677
- before: holders(current).map((service) => `no ${service} access-list ${MANAGEMENT_ACL}`),
678
- after: holders(desired).map((service) => `${service} access-list ${MANAGEMENT_ACL}`),
679
- };
680
- };
681
-
682
- const planned = steps(
683
- groups.filter((group) => group.add.length || group.remove.length),
684
- dialect,
685
- releaseAndBind
686
- );
687
-
688
- if (lagRemovals.length) {
689
- const removeLags: Step = {
690
- title: "LAGs no longer used",
691
- show: lagRemovals.map((line) => `- ${line}`),
692
- send: lagRemovals.map(undo),
693
- };
694
- planned.push(removeLags);
695
- }
696
-
697
- return { steps: planned, problems: [] };
698
- }
699
-
700
- export const raisecom: DeviceAdapter = { extension: ".txt", console: true, unsupported, render, read, plan, apply };