@takodotid/circuit 0.43.1 → 1.0.0-rc.2

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 (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -6,9 +6,9 @@ import type { Client } from "ssh2";
6
6
  import { Utils } from "../../core/utils";
7
7
  import { Address } from "../../core/address";
8
8
  import type { Neighbor } from "../../core/cabling";
9
- import type { Finding } from "../../core/define";
9
+ import type { Finding } from "../finding";
10
10
  import { Secrets } from "../../core/secrets";
11
- import type { Device, Family } from "../../schema";
11
+ import type { Device, Family } from "../../model";
12
12
  import { SerialConsole, type ConsoleDialect, ConsoleShell } from "../../transport/console";
13
13
  import { Ssh } from "../../transport/ssh";
14
14
  import {
@@ -144,41 +144,41 @@ export class RouterOs extends Platform {
144
144
  override readonly console = RouterOs.CONSOLE;
145
145
 
146
146
  /**
147
- * A BGP session with a `table` announces from that table, not from main: each prefix of `networks` it announces needs a route in the table too, or it is not announced (TRAPS.md). A session left with nothing to announce is an error; a prefix missing from its table is a warning, since its export may not announce it anyway.
147
+ * A BGP session with a `routing_table` announces from that table, not from main: each prefix of `originated_prefixes` it announces needs a route in the table too, or it is not announced (TRAPS.md). A session left with nothing to announce is an error; a prefix missing from its table is a warning, since its export may not announce it anyway.
148
148
  */
149
149
  checks(device: Device): Finding[] {
150
150
  const bgp = device.routing?.bgp;
151
- const statics = device.routing?.static ?? [];
151
+ const statics = device.routing?.static_routes ?? [];
152
152
  const findings: Finding[] = [];
153
153
 
154
154
  for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
155
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
156
- const table = Utils.otherTable(neighbor.table ?? group?.table);
155
+ const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
156
+ const table = Utils.otherTable(neighbor.routing_table ?? group?.routing_table);
157
157
  if (!table) continue;
158
158
 
159
- const families = neighbor.families ?? group?.families ?? [Address.family(neighbor.address)];
160
- const networks = (bgp?.networks ?? []).filter((prefix) => families.includes(Address.family(Address.host(prefix))));
159
+ const families = neighbor.address_families ?? group?.address_families ?? [Address.family(neighbor.address)];
160
+ const originated = (bgp?.originated_prefixes ?? []).filter((prefix) => families.includes(Address.family(Address.host(prefix))));
161
161
  const inTable = (prefix: string) =>
162
- statics.some((route) => route.table === table && Address.canonical(route.prefix) === Address.canonical(prefix));
163
- // What its export accepts by prefix: a network it would reject anyway needs no route in the table. Every network when the export names none.
164
- const exportPolicy = neighbor.export ?? group?.export;
165
- const accepted = (exportPolicy ? (device.policies?.[exportPolicy] ?? []) : [])
166
- .filter((rule) => rule.action === "accept" && rule.match?.prefix)
167
- .map((rule) => Address.canonical(rule.match!.prefix!));
168
- const announced = accepted.length ? networks.filter((prefix) => accepted.includes(Address.canonical(prefix))) : networks;
162
+ statics.some((route) => route.routing_table === table && Address.canonical(route.prefix) === Address.canonical(prefix));
163
+ // What its export accepts by prefix: a prefix it would reject anyway needs no route in the table. Every prefix when the export names none.
164
+ const exportPolicy = neighbor.export_policy ?? group?.export_policy;
165
+ const accepted = (exportPolicy ? (device.routing?.policies?.[exportPolicy] ?? []) : [])
166
+ .flatMap((rule) => (RouterOsRender.onRoutes(rule) && rule.then === "accept" && rule.if?.prefix ? [rule.if.prefix] : []))
167
+ .map((prefix) => Address.canonical(prefix));
168
+ const announced = accepted.length ? originated.filter((prefix) => accepted.includes(Address.canonical(prefix))) : originated;
169
169
 
170
170
  const missing = announced.filter((prefix) => !inTable(prefix));
171
171
  if (!missing.length) continue;
172
172
 
173
173
  const where = `routing.bgp.neighbors.${name}`;
174
- const route = (prefix: string) => `{ prefix: "${prefix}", blackhole: true, table: "${table}" }`;
174
+ const route = (prefix: string) => `{ prefix: "${prefix}", blackhole: true, routing_table: "${table}" }`;
175
175
  const fix = `On RouterOS a session announces from its own table, ${table}`;
176
176
 
177
177
  if (missing.length === announced.length) {
178
178
  findings.push({
179
179
  level: "error",
180
180
  device: device.name,
181
- message: `${where} would announce nothing: ${fix}, which has no route for any of networks. Add the routes it announces to ${table} too, such as ${route(missing[0]!)} in routing.static`,
181
+ message: `${where} would announce nothing: ${fix}, which has no route for any of originated_prefixes. Add the routes it announces to ${table} too, such as ${route(missing[0]!)} in routing.static_routes`,
182
182
  });
183
183
  continue;
184
184
  }
@@ -187,7 +187,7 @@ export class RouterOs extends Platform {
187
187
  findings.push({
188
188
  level: "warning",
189
189
  device: device.name,
190
- message: `${where} does not announce ${prefix}: ${fix}, which has no route for it. If its export should announce it, add ${route(prefix)} to routing.static`,
190
+ message: `${where} does not announce ${prefix}: ${fix}, which has no route for it. If its export should announce it, add ${route(prefix)} to routing.static_routes`,
191
191
  });
192
192
  }
193
193
  }
@@ -198,74 +198,89 @@ export class RouterOs extends Platform {
198
198
  unsupported(device: Device): string[] {
199
199
  const found: string[] = [];
200
200
 
201
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
202
- if (iface.type === "bridge") found.push(`interfaces.${name}, a bridge, which only FortiOS builds for now`);
201
+ for (const [name] of RouterOsRender.entries(device.interfaces?.bridge)) {
202
+ found.push(`interfaces.bridge.${name}, a bridge, which only FortiOS builds for now`);
203
203
  }
204
204
 
205
- for (const [name, port] of Object.entries(device.ports ?? {})) {
206
- if (!port) continue;
207
-
205
+ for (const [name, port] of Derive.ports(device)) {
208
206
  if (port.speed && !RouterOsRender.medium(Catalog.spell(device.platform, device.model, name), port.speed)) {
209
- found.push(`ports.${name}.speed ${port.speed}, the port has no fixed speed by that name`);
207
+ found.push(`interfaces.ethernet.${name}.speed ${port.speed}, the port has no fixed speed by that name`);
210
208
  }
211
209
 
212
- const thresholds = Object.values(port.storm_control ?? {});
210
+ const thresholds = Object.values(port.storm_control ?? {}).filter((threshold) => threshold !== undefined);
213
211
  const percents = new Set(thresholds.map((threshold) => ("percent" in threshold ? threshold.percent : undefined)));
214
212
 
215
- if (port.mac_limit !== undefined) found.push(`ports.${name}.mac_limit, ${RouterOs.NO_MAC_LIMIT}`);
213
+ if (port.mac_limit !== undefined) found.push(`interfaces.ethernet.${name}.mac_limit, ${RouterOs.NO_MAC_LIMIT}`);
216
214
 
217
215
  if (thresholds.some((threshold) => "pps" in threshold)) {
218
- found.push(`ports.${name}.storm_control in pps, the platform takes percent`);
216
+ found.push(`interfaces.ethernet.${name}.storm_control in pps, the platform takes percent`);
219
217
  } else if (percents.size > 1) {
220
- found.push(`ports.${name}.storm_control with a different percent per kind, the platform takes one`);
218
+ found.push(`interfaces.ethernet.${name}.storm_control with a different percent per kind, the platform takes one`);
221
219
  }
222
220
  }
223
221
 
224
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
225
- if (iface.type !== "lag") continue;
222
+ // Routing in software is set per VLAN interface, on the switch chip that offloads.
223
+ for (const [name, port] of Derive.ports(device))
224
+ if (port.hardware_offload !== undefined) found.push(`interfaces.ethernet.${name}.hardware_offload, set on a VLAN interface`);
225
+ for (const { name, kind, settings } of Derive.interfaces(device)) {
226
+ if (settings.hardware_offload === undefined) continue;
227
+ if (kind !== "vlan_interface") found.push(`interfaces.${kind}.${name}.hardware_offload, set on a VLAN interface`);
228
+ else if (settings.hardware_offload && !device.system?.hardware_offload)
229
+ found.push(`interfaces.vlan_interface.${name}.hardware_offload true while system.hardware_offload is off`);
230
+ }
226
231
 
227
- if (iface.mac_limit !== undefined) found.push(`interfaces.${name}.mac_limit, ${RouterOs.NO_MAC_LIMIT}`);
228
- if (iface.ethertypes !== undefined) found.push(`interfaces.${name}.ethertypes, on a LAG, which is not proved on a device yet`);
232
+ for (const [name, lag] of RouterOsRender.entries(device.interfaces?.lag)) {
233
+ if (lag.mac_limit !== undefined) found.push(`interfaces.lag.${name}.mac_limit, ${RouterOs.NO_MAC_LIMIT}`);
234
+ if (lag.ethertypes !== undefined)
235
+ found.push(`interfaces.lag.${name}.ethertypes, on a LAG, which is not proved on a device yet`);
229
236
 
230
237
  for (const field of ["storm_control", "stp"] as const) {
231
- if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}, on a LAG`);
238
+ if (lag[field] !== undefined) found.push(`interfaces.lag.${name}.${field}, on a LAG`);
232
239
  }
233
240
  }
234
241
 
235
242
  const bgp = device.routing?.bgp;
236
243
 
237
244
  for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
238
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
239
- const families = neighbor.families ?? group?.families ?? [Address.family(neighbor.address)];
245
+ const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
246
+ const families = neighbor.address_families ?? group?.address_families ?? [Address.family(neighbor.address)];
240
247
 
241
- if (families.length > 1) found.push(`routing.bgp.neighbors.${name}.families, one family per session`);
248
+ if (families.length > 1) found.push(`routing.bgp.neighbors.${name}.address_families, one family per session`);
242
249
  }
243
250
 
244
251
  const management = device.management;
252
+ const snmp = device.monitoring?.snmp;
245
253
 
246
254
  if (management?.console) found.push("management.console, the console uses the device's users");
247
255
  if (management?.interfaces) found.push("management.interfaces, which only FortiOS builds for now");
248
256
  if (management?.privilege_password) found.push("management.privilege_password, the platform has no privileged mode");
249
257
 
250
- if (management?.snmp?.port) found.push("management.snmp.port");
258
+ if (snmp?.port) found.push("monitoring.snmp.port");
251
259
 
252
260
  if (management?.ssh?.auth_timeout || management?.ssh?.auth_retries) {
253
261
  found.push("management.ssh.auth_timeout and auth_retries");
254
262
  }
255
263
 
256
- for (const [name, user] of Object.entries(management?.snmp?.users ?? {})) {
264
+ for (const [name, user] of Object.entries(snmp?.users ?? {})) {
257
265
  if (user.auth !== "sha1") {
258
- found.push(`management.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
266
+ found.push(
267
+ `monitoring.snmp.users.${name}.auth ${user.auth}: Circuit writes SHA1 only on RouterOS (SHA-2 comes with RouterOS 7.26, not proved yet)`
268
+ );
259
269
  }
260
270
  }
261
271
 
262
- if (device.system?.transceiver_monitoring === false) found.push("system.transceiver_monitoring false, it is always on");
263
- if (device.flow_export?.protocol === "sflow") found.push("flow_export.protocol sflow, the platform exports NetFlow and IPFIX");
272
+ if (device.monitoring?.transceivers === false) found.push("monitoring.transceivers false, it is always on");
273
+ if (device.monitoring?.flow_export?.protocol === "sflow") {
274
+ found.push("monitoring.flow_export.protocol sflow, the platform exports NetFlow and IPFIX");
275
+ }
264
276
 
265
- for (const route of device.routing?.static ?? []) {
266
- if (route.bfd) found.push(`routing.static ${route.prefix} bfd, a static route's check-gateway takes arp, none or ping on 7.23`);
277
+ for (const route of device.routing?.static_routes ?? []) {
278
+ if (route.bfd)
279
+ found.push(`routing.static_routes ${route.prefix} bfd, a static route's check-gateway takes arp, none or ping on 7.23`);
267
280
  }
268
281
 
282
+ found.push(...RouterOs.unsupportedShapes(device));
283
+
269
284
  // BFD on a static route and a MAC limit are refused above, for the reason the device gave, and an ethertype filter on a LAG as unproved.
270
285
  found.push(
271
286
  ...Derive.unproved(device, [
@@ -281,6 +296,54 @@ export class RouterOs extends Platform {
281
296
  return found;
282
297
  }
283
298
 
299
+ /** What the v1 model can write that the platform does not render: a list of values in one rule's match, an interface set, an address set named in `management.allow`, and a `{ communities }` rule. */
300
+ private static unsupportedShapes(device: Device): string[] {
301
+ const found: string[] = [];
302
+ const firewall = device.firewall;
303
+ const list = (value: unknown) => Array.isArray(value) && value.length > 1;
304
+
305
+ if (firewall?.interface_sets) found.push("firewall.interface_sets, which the platform does not build yet");
306
+
307
+ const chains = (["input", "forward", "output"] as const).map(
308
+ (chain) => [`firewall.filter.${chain}.rules`, firewall?.filter?.[chain]?.rules] as const
309
+ );
310
+ const nat = [
311
+ ["firewall.nat.source", firewall?.nat?.source],
312
+ ["firewall.nat.destination", firewall?.nat?.destination],
313
+ ] as const;
314
+
315
+ for (const [where, rules] of [...chains, ...nat]) {
316
+ (rules ?? []).forEach((rule, index) => {
317
+ for (const field of ["src", "dst", "in_interface", "out_interface"] as const) {
318
+ if (list(rule.if?.[field])) found.push(`${where}[${index}].if.${field}, a list: write one rule for each`);
319
+ }
320
+
321
+ for (const field of ["in_interface_set", "out_interface_set"] as const) {
322
+ if (rule.if?.[field] !== undefined) found.push(`${where}[${index}].if.${field}, which the platform does not build yet`);
323
+ }
324
+ });
325
+ }
326
+
327
+ for (const [name, rules] of RouterOsRender.entries(device.switching?.acls)) {
328
+ rules.forEach((rule, index) => {
329
+ for (const field of ["src", "dst", "vlan"] as const) {
330
+ if (list(rule.if?.[field])) found.push(`switching.acls.${name}[${index}].if.${field}, a list: write one rule for each`);
331
+ }
332
+ });
333
+ }
334
+
335
+ const allow = device.management?.allow;
336
+ if (typeof allow === "string") found.push(`management.allow ${allow}, an address set by name: write its prefixes`);
337
+
338
+ for (const [name, rules] of RouterOsRender.entries(device.routing?.policies)) {
339
+ rules.forEach((rule, index) => {
340
+ if (rule.communities !== undefined) found.push(`routing.policies.${name}[${index}].communities ${rule.communities}`);
341
+ });
342
+ }
343
+
344
+ return found;
345
+ }
346
+
284
347
  render(device: Device): string {
285
348
  return RouterOsRender.of(device);
286
349
  }
@@ -487,7 +550,7 @@ export class RouterOs extends Platform {
487
550
  }
488
551
 
489
552
  /**
490
- * Three checks after an apply. When the plan touched switch rules, none may be invalid. BGP sessions established before the apply must be established again, and announce again each of the device's `networks` they announced before, within `BgpWait.AFTER_APPLY`. A session can stay up and stop announcing (TRAPS.md), which the session check alone would not see.
553
+ * Three checks after an apply. When the plan touched switch rules, none may be invalid. BGP sessions established before the apply must be established again, and announce again each of the device's `originated_prefixes` they announced before, within `BgpWait.AFTER_APPLY`. A session can stay up and stop announcing (TRAPS.md), which the session check alone would not see.
491
554
  */
492
555
  async health(device: Device, plan: Plan, options: HealthOptions = {}): Promise<HealthCheck> {
493
556
  const switchRules = plan.steps.some((step) => step.send.some((command) => command.startsWith(RouterOs.SWITCH_RULES)));
@@ -551,9 +614,7 @@ export class RouterOs extends Platform {
551
614
  */
552
615
  private static farPort(network: readonly Device[], system: string, reported: string): string {
553
616
  const far = network.find((candidate) => system === candidate.name || system.startsWith(`${candidate.name}.`));
554
- const described = Object.entries(far?.ports ?? {})
555
- .filter(([, port]) => port?.description === reported)
556
- .map(([name]) => name);
617
+ const described = (far ? Derive.ports(far) : []).filter(([, port]) => port.description === reported).map(([name]) => name);
557
618
 
558
619
  return described.length === 1 ? described[0]! : reported;
559
620
  }
@@ -625,9 +686,9 @@ export class RouterOs extends Platform {
625
686
  );
626
687
  }
627
688
 
628
- /** What each session announces of the device's own `networks`, as `neighbor prefix`. None, without logging in, on a device without networks. */
689
+ /** What each session announces of the device's own `originated_prefixes`, as `neighbor prefix`. None, without logging in, on a device that originates none. */
629
690
  private async announcedNow(device: Device, options: ReadOptions): Promise<string[]> {
630
- if (!device.routing?.bgp?.networks?.length) return [];
691
+ if (!device.routing?.bgp?.originated_prefixes?.length) return [];
631
692
 
632
693
  const session = await this.session(device, options);
633
694
 
@@ -638,11 +699,11 @@ export class RouterOs extends Platform {
638
699
  }
639
700
  }
640
701
 
641
- /** The `networks` each session announces, as `neighbor prefix`, sorted. An advertisement names its session, which is matched to its neighbor by address first, as in `bgpSessions`. */
702
+ /** The `originated_prefixes` each session announces, as `neighbor prefix`, sorted. An advertisement names its session, which is matched to its neighbor by address first, as in `bgpSessions`. */
642
703
  private async announced(device: Device, session: ReadSession): Promise<string[]> {
643
704
  const bgp = device.routing?.bgp;
644
- const networks = bgp?.networks ?? [];
645
- if (!networks.length) return [];
705
+ const originated = bgp?.originated_prefixes ?? [];
706
+ if (!originated.length) return [];
646
707
 
647
708
  const sessions = (await session.run(RouterOs.LIST_BGP_SESSIONS, 60_000))
648
709
  .split(/\r?\n/)
@@ -656,7 +717,7 @@ export class RouterOs extends Platform {
656
717
  return (byAddress ?? byName)?.[0];
657
718
  };
658
719
 
659
- const output = await session.run(RouterOs.listAdvertisements(networks), 60_000);
720
+ const output = await session.run(RouterOs.listAdvertisements(originated), 60_000);
660
721
  return output
661
722
  .split(/\r?\n/)
662
723
  .map((line) => line.trim().split("|"))
@@ -687,7 +748,7 @@ export class RouterOs extends Platform {
687
748
 
688
749
  return gone.map((pair) => {
689
750
  const [neighbor, prefix] = pair.split(" ");
690
- return `BGP session ${neighbor} announced ${prefix} before the apply and does not ${limits.seconds} s after it, though it may still be established. A RouterOS session announces from its routing table: that table needs a route for ${prefix}, and a session whose table changed must restart. Look with: circuit show ${device.name} '/routing bgp advertisements print'`;
751
+ return `BGP session ${neighbor} announced ${prefix} before the apply and does not ${limits.seconds} s after it, though it may still be established. A RouterOS session announces from its routing table: that table needs a route for ${prefix}, and a session whose table changed must restart. Look with: circuit exec ${device.name} '/routing bgp advertisements print'`;
691
752
  });
692
753
  }
693
754
 
@@ -703,7 +764,7 @@ export class RouterOs extends Platform {
703
764
  }
704
765
 
705
766
  return [
706
- `${invalid} switch rule(s) are invalid: the switch chip cannot hold them, and every rule after the first invalid one is invalid too, so those rules do nothing. Use fewer distinct port ranges in this device's ACLs, then apply again. See them with: circuit show ${device.name} '${RouterOs.SWITCH_RULES} print where invalid'`,
767
+ `${invalid} switch rule(s) are invalid: the switch chip cannot hold them, and every rule after the first invalid one is invalid too, so those rules do nothing. Use fewer distinct port ranges in this device's ACLs, then apply again. See them with: circuit exec ${device.name} '${RouterOs.SWITCH_RULES} print where invalid'`,
707
768
  ];
708
769
  }
709
770
 
@@ -737,7 +798,7 @@ export class RouterOs extends Platform {
737
798
  const opened = await SerialConsole.open(console);
738
799
 
739
800
  try {
740
- await SerialConsole.login(opened.channel, RouterOs.CONSOLE, user, password, console.passwordOnly);
801
+ await SerialConsole.login(opened.channel, RouterOs.CONSOLE, user, password, console.password_only);
741
802
  } catch (error) {
742
803
  opened.close();
743
804
  throw error;
@@ -748,7 +809,7 @@ export class RouterOs extends Platform {
748
809
  const session: Session = {
749
810
  run: (command, timeout) => shell.run(command, timeout),
750
811
  fresh: async () => {
751
- await SerialConsole.login(opened.channel, RouterOs.CONSOLE, user, password, console.passwordOnly);
812
+ await SerialConsole.login(opened.channel, RouterOs.CONSOLE, user, password, console.password_only);
752
813
  return session;
753
814
  },
754
815
  end: () => {
@@ -798,12 +859,12 @@ export class RouterOs extends Platform {
798
859
 
799
860
  /** Upload a certificate and its key, import both under one name, and delete the files. */
800
861
  private static async installCertificate(session: Session, device: Device, name: string): Promise<void> {
801
- const certificate = device.certificates?.[name];
802
- if (!certificate) throw new Error(`certificate ${name} is not declared`);
862
+ const certificate = device.system?.certificates?.[name];
863
+ if (!certificate) throw new Error(`certificate ${name} is not declared in system.certificates`);
803
864
  if (!session.upload)
804
865
  throw new Error(`certificate ${name} is installed from a file, which only SSH can write; apply it without --console`);
805
866
 
806
- await session.upload(`${name}.crt`, certificate.certificate);
867
+ await session.upload(`${name}.crt`, Utils.projectFile(certificate.certificate_file));
807
868
  await session.run(`/certificate import file-name=${name}.crt name=${name} passphrase=""`);
808
869
 
809
870
  if (certificate.private_key) {