@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,7 +6,7 @@ import type { Client, ClientChannel } from "ssh2";
6
6
  import { Address } from "../../core/address";
7
7
  import type { Neighbor } from "../../core/cabling";
8
8
  import { Secrets } from "../../core/secrets";
9
- import type { Device, Family, IP } from "../../schema";
9
+ import type { Device, Family, Lag, Port, Routed } from "../../model";
10
10
  import { ConsoleShell, SerialConsole, type ConsoleDialect } from "../../transport/console";
11
11
  import { Ssh } from "../../transport/ssh";
12
12
  import { Catalog } from "../catalog";
@@ -127,26 +127,29 @@ export class Junos extends Platform {
127
127
 
128
128
  unsupported(device: Device): string[] {
129
129
  const found: string[] = [];
130
+ const interfaces = Derive.interfaces(device);
130
131
 
131
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
132
- if (iface.type === "bridge") found.push(`interfaces.${name}, a bridge, which only FortiOS builds for now`);
133
- if (iface.type === "vlan" && iface.mac) found.push(`interfaces.${name}.mac, which only RouterOS sets for now`);
132
+ for (const { name, kind } of interfaces) {
133
+ if (kind === "bridge") found.push(`interfaces.bridge.${name}, a bridge, which only FortiOS builds for now`);
134
+ if (kind === "vlan_interface" && device.interfaces?.vlan_interface?.[name]?.mac)
135
+ found.push(`interfaces.vlan_interface.${name}.mac, which only RouterOS sets for now`);
134
136
  }
135
137
  const names = new JunosNames(device);
136
- const users = device.users ?? {};
138
+ const users = device.system?.users ?? {};
137
139
 
138
- const root = (users as Record<string, (typeof users)[keyof typeof users]>).root;
139
- if (!root) found.push("users without root, the platform needs a root password");
140
- if (root && root.role !== "admin") found.push("users.root.role, root is always admin");
141
- if (root?.description) found.push("users.root.description");
140
+ const root = users.root;
141
+ if (!root) found.push("system.users without root, the platform needs a root password");
142
+ if (root && root.role !== "admin") found.push("system.users.root.role, root is always admin");
143
+ if (root?.description) found.push("system.users.root.description");
142
144
  if (device.connection.user === "root") found.push("connection.user root, which logs in to a shell rather than the CLI");
143
145
 
144
146
  for (const [name, user] of Object.entries(users)) {
145
- if (Junos.SYSTEM_ACCOUNTS.has(name)) found.push(`users.${name}, a name the platform keeps for a system account`);
147
+ if (!user) continue;
148
+ if (Junos.SYSTEM_ACCOUNTS.has(name)) found.push(`system.users.${name}, a name the platform keeps for a system account`);
146
149
 
147
150
  for (const key of user.ssh_keys ?? []) {
148
151
  const kind = key.split(" ")[0]!;
149
- if (!JunosRender.KEY_STATEMENT[kind]) found.push(`users.${name}.ssh_keys, a key of type ${kind}`);
152
+ if (!JunosRender.KEY_STATEMENT[kind]) found.push(`system.users.${name}.ssh_keys, a key of type ${kind}`);
150
153
  }
151
154
  }
152
155
 
@@ -158,29 +161,28 @@ export class Junos extends Platform {
158
161
 
159
162
  if (management.console) found.push("management.console, the console uses the device's users");
160
163
  if (management.interfaces) found.push("management.interfaces, which only FortiOS builds for now");
161
- if (management.native?.interfaces?.length) found.push("management.native.interfaces, NETCONF runs over IP only");
164
+ if (management.native_protocol?.interfaces?.length) found.push("management.native_protocol.interfaces, NETCONF runs over IP only");
162
165
  if (management.ssh?.auth_timeout || management.ssh?.auth_retries) found.push("management.ssh.auth_timeout and auth_retries");
163
166
 
164
- for (const service of ["telnet", "ftp", "snmp"] as const) {
165
- if (management[service]?.port) found.push(`management.${service}.port`);
166
- }
167
+ if (management.telnet?.port) found.push("management.telnet.port");
168
+ if (management.ftp?.port) found.push("management.ftp.port");
169
+ if (device.monitoring?.snmp?.port) found.push("monitoring.snmp.port");
167
170
 
168
171
  const system = device.system ?? {};
169
172
 
170
173
  if (system.release_channel) found.push("system.release_channel");
171
- if (system.transceiver_monitoring !== undefined) found.push("system.transceiver_monitoring, it is always on");
172
- if (system.dns?.serve) found.push("system.dns.serve");
173
- if (system.ip?.syn_cookies) found.push("system.ip.syn_cookies");
174
+ if (device.monitoring?.transceivers !== undefined) found.push("monitoring.transceivers, it is always on");
175
+ if (device.services?.dns_server) found.push("services.dns_server");
176
+ if (system.ip_stack?.syn_cookies) found.push("system.ip_stack.syn_cookies");
174
177
  if (system.logging?.local?.lines_per_file !== undefined) found.push("system.logging.local.lines_per_file, files rotate by size");
175
178
 
176
179
  // Time is answered only to its servers, which the filter names by address.
177
- if (system.ntp && !system.ntp.serve && system.ntp.servers.some((server) => !Address.isValid(server))) {
178
- found.push("system.ntp.servers by name while not serving, the filter keeping others out needs addresses");
180
+ if (system.ntp && !device.services?.ntp_server && system.ntp.servers.some((server) => !Address.isValid(server))) {
181
+ found.push("system.ntp.servers by name without services.ntp_server, the filter keeping others out needs addresses");
179
182
  }
180
183
 
181
- for (const [name, port] of Object.entries(device.ports ?? {})) {
182
- if (!port) continue;
183
-
184
+ for (const [name, port] of Derive.ports(device)) {
185
+ const where = `interfaces.ethernet.${name}`;
184
186
  const physical = names.port(name);
185
187
 
186
188
  if (physical.startsWith("em")) {
@@ -188,119 +190,135 @@ export class Junos extends Platform {
188
190
  "access_vlan",
189
191
  "trunk_vlans",
190
192
  "native_vlan",
191
- "lag",
193
+ "in_lag",
192
194
  "stp",
193
195
  "storm_control",
194
196
  "speed",
195
197
  "vrrp",
196
198
  "vrf",
197
199
  "mtu",
198
- "acl",
200
+ "ingress_acl",
199
201
  "ipv6_ra",
200
202
  ] as const) {
201
- if (port[field] !== undefined) found.push(`ports.${name}.${field}, a management port only routes`);
203
+ if (port[field] !== undefined) found.push(`${where}.${field}, a management port only routes`);
202
204
  }
203
205
  }
204
206
 
205
207
  if (port.speed && !(port.speed === "1g" && physical.startsWith("ge-")))
206
- found.push(`ports.${name}.speed ${port.speed}, only a 1G optic in a 10G cage`);
208
+ found.push(`${where}.speed ${port.speed}, only a 1G optic in a 10G cage`);
207
209
 
208
- if (port.hardware_offload !== undefined) found.push(`ports.${name}.hardware_offload`);
210
+ if (port.hardware_offload !== undefined) found.push(`${where}.hardware_offload`);
209
211
 
210
212
  // A port filter cannot match the VLAN a frame is switched in: `learn-vlan-id` is refused on an ingress port.
211
- const acl = port.acl ? device.acls?.[port.acl] : undefined;
212
- if (acl?.some((rule) => rule.match?.vlan)) found.push(`acls.${port.acl} matching a VLAN, on port ${name}`);
213
+ const acl = port.ingress_acl ? device.switching?.acls?.[port.ingress_acl] : undefined;
214
+ if (acl?.some((rule) => rule.if?.vlan)) found.push(`switching.acls.${port.ingress_acl} matching a VLAN, on port ${name}`);
213
215
 
214
- found.push(...Junos.routedGaps(`ports.${name}`, port));
215
- found.push(...Junos.switchedGaps(`ports.${name}`, port, device));
216
+ found.push(...Junos.routedGaps(where, port));
217
+ found.push(...Junos.switchedGaps(where, port, device));
218
+ }
219
+
220
+ // A rule's match takes one address and one VLAN here; a list is one rule per value.
221
+ for (const [name, rules] of Object.entries(device.switching?.acls ?? {})) {
222
+ (rules ?? []).forEach((rule, index) => {
223
+ for (const field of ["src", "dst", "vlan"] as const) {
224
+ if (Array.isArray(rule.if?.[field]))
225
+ found.push(`switching.acls.${name}[${index}].if.${field}, a list: write one rule for each`);
226
+ }
227
+ });
216
228
  }
217
229
 
218
230
  const loopbackTables = new Set<string>();
219
231
 
220
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
221
- if (iface.type === "vxlan" || iface.type === "wireguard") {
222
- found.push(`interfaces.${name}, a ${iface.type}`);
232
+ for (const { name, kind, settings } of interfaces) {
233
+ const where = `interfaces.${kind}.${name}`;
234
+
235
+ if (kind === "vxlan" || kind === "wireguard") {
236
+ found.push(`${where}, a ${kind}`);
223
237
  continue;
224
238
  }
225
239
 
226
- if (iface.hardware_offload !== undefined) found.push(`interfaces.${name}.hardware_offload`);
227
- if (iface.type === "loopback" && iface.ipv6_ra) found.push(`interfaces.${name}.ipv6_ra, on a loopback`);
228
- if (iface.type === "loopback" && iface.mtu !== undefined) found.push(`interfaces.${name}.mtu, on a loopback`);
229
- if (iface.type === "gre" && iface.vrrp?.length) found.push(`interfaces.${name}.vrrp, on a tunnel`);
240
+ if (settings.hardware_offload !== undefined) found.push(`${where}.hardware_offload`);
241
+ if (kind === "loopback" && settings.ipv6_ra) found.push(`${where}.ipv6_ra, on a loopback`);
242
+ if (kind === "loopback" && settings.mtu !== undefined) found.push(`${where}.mtu, on a loopback`);
243
+ if (kind === "gre" && settings.vrrp?.length) found.push(`${where}.vrrp, on a tunnel`);
230
244
 
231
245
  // A VLAN interface sends no larger packets than the ports carrying its VLAN take; the device warns and carries on.
232
- if (iface.type === "vlan" && iface.mtu !== undefined) {
233
- const smallest = Math.min(...Junos.carriers(device, iface.vlan));
234
- if (iface.mtu > smallest) found.push(`interfaces.${name}.mtu ${iface.mtu}, above the ${smallest} its VLAN's ports carry`);
246
+ const vlanInterface = kind === "vlan_interface" ? device.interfaces?.vlan_interface?.[name] : undefined;
247
+ if (vlanInterface?.mtu !== undefined) {
248
+ const smallest = Math.min(...Junos.carriers(device, vlanInterface.vlan));
249
+ if (vlanInterface.mtu > smallest)
250
+ found.push(`${where}.mtu ${vlanInterface.mtu}, above the ${smallest} its VLAN's ports carry`);
235
251
  }
236
252
 
237
- found.push(...Junos.routedGaps(`interfaces.${name}`, iface));
253
+ found.push(...Junos.routedGaps(where, settings));
238
254
 
239
- if (iface.type === "lag") found.push(...Junos.switchedGaps(`interfaces.${name}`, iface, device));
255
+ const lag = kind === "lag" ? device.interfaces?.lag?.[name] : undefined;
256
+ if (lag) found.push(...Junos.switchedGaps(where, lag, device));
240
257
 
241
- if (iface.type === "loopback") {
242
- const table = iface.vrf ?? "";
243
- if (loopbackTables.has(table)) found.push(`interfaces.${name}, a second loopback in one routing table`);
258
+ if (kind === "loopback") {
259
+ const table = settings.vrf ?? "";
260
+ if (loopbackTables.has(table)) found.push(`${where}, a second loopback in one routing table`);
244
261
  loopbackTables.add(table);
245
- if (iface.vrrp?.length) found.push(`interfaces.${name}.vrrp, on a loopback`);
262
+ if (settings.vrrp?.length) found.push(`${where}.vrrp, on a loopback`);
246
263
  }
247
264
  }
248
265
 
249
266
  const ids = [...Derive.lagIds(device).values()];
250
- if (ids.length !== new Set(ids).size) found.push("interfaces, two LAGs with one number");
267
+ if (ids.length !== new Set(ids).size) found.push("interfaces.lag, two LAGs with one number");
251
268
 
252
- if (device.stp?.priority !== undefined && device.stp.priority % 4096 !== 0) found.push("stp.priority, in steps of 4096");
269
+ const stp = device.switching?.stp;
270
+ if (stp?.priority !== undefined && stp.priority % 4096 !== 0) found.push("switching.stp.priority, in steps of 4096");
253
271
 
254
272
  const routing = device.routing;
255
273
  found.push(...Junos.bgpGaps(device));
256
274
 
257
275
  const tables = new Map<string, number>();
258
276
 
259
- for (const route of routing?.static ?? []) {
260
- if (route.description) found.push(`routing.static ${route.prefix} description`);
277
+ for (const route of routing?.static_routes ?? []) {
278
+ if (route.description) found.push(`routing.static_routes ${route.prefix} description`);
261
279
 
262
280
  const table = `${route.vrf ?? ""} ${Address.split(route.prefix).addr}/${Address.split(route.prefix).len}`;
263
281
  tables.set(table, (tables.get(table) ?? 0) + 1);
264
282
  }
265
283
 
266
- for (const route of routing?.static ?? []) {
284
+ for (const route of routing?.static_routes ?? []) {
267
285
  const table = `${route.vrf ?? ""} ${Address.split(route.prefix).addr}/${Address.split(route.prefix).len}`;
268
286
  if (route.blackhole && tables.get(table)! > 1)
269
- found.push(`routing.static ${route.prefix}, a blackhole beside another route to the same prefix`);
287
+ found.push(`routing.static_routes ${route.prefix}, a blackhole beside another route to the same prefix`);
270
288
  }
271
289
 
272
290
  for (const area of Object.values(routing?.ospf?.areas ?? {})) {
273
- for (const iface of Object.keys(area.interfaces)) {
274
- const settings = device.interfaces?.[iface] ?? device.ports?.[iface as keyof typeof device.ports];
275
- if (settings?.vrf) found.push(`routing.ospf on ${iface}, which routes in a VRF`);
291
+ for (const [iface, settings] of Object.entries(area.interfaces)) {
292
+ if (settings && Derive.settings(device, iface)?.vrf) found.push(`routing.ospf on ${iface}, which routes in a VRF`);
276
293
  }
277
294
  }
278
295
 
279
- for (const field of ["firewall", "hardware_offload", "certificates"] as const) {
280
- if (device[field] !== undefined) found.push(field);
281
- }
296
+ if (device.firewall !== undefined) found.push("firewall");
297
+ if (system.hardware_offload !== undefined) found.push("system.hardware_offload");
298
+ if (system.certificates !== undefined) found.push("system.certificates");
282
299
 
283
- const relayed = new Set(Object.values(device.dhcp_relay ?? {}).map((relay) => relay.interface));
300
+ const services = device.services ?? {};
301
+ const relayed = new Set(Object.values(services.dhcp_relay ?? {}).map((relay) => relay.interface));
284
302
 
285
- for (const [name, server] of Object.entries(device.dhcp ?? {})) {
303
+ for (const [name, server] of Object.entries(services.dhcp_server ?? {})) {
286
304
  // The device refuses a relay and a server on one interface: `Interface irb.10 already configured`.
287
- if (relayed.has(server.interface)) found.push(`dhcp.${name}.interface, which a DHCP relay also uses`);
305
+ if (relayed.has(server.interface)) found.push(`services.dhcp_server.${name}.interface, which a DHCP relay also uses`);
288
306
 
289
- const settings = device.interfaces?.[server.interface] ?? device.ports?.[server.interface as keyof typeof device.ports];
290
- if (settings?.vrf) found.push(`dhcp.${name}.interface, which routes in a VRF`);
291
- if (Address.family(Address.host(server.network)) === "ipv6") found.push(`dhcp.${name}.network, IPv6`);
307
+ if (Derive.settings(device, server.interface)?.vrf) found.push(`services.dhcp_server.${name}.interface, which routes in a VRF`);
308
+ if (Address.family(Address.host(server.network)) === "ipv6") found.push(`services.dhcp_server.${name}.network, IPv6`);
292
309
  }
293
310
 
294
- for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
295
- if (relay.servers.some((server) => Address.family(server) === "ipv6")) found.push(`dhcp_relay.${name}.servers, IPv6`);
311
+ for (const [name, relay] of Object.entries(services.dhcp_relay ?? {})) {
312
+ if (relay.servers.some((server) => Address.family(server) === "ipv6")) found.push(`services.dhcp_relay.${name}.servers, IPv6`);
296
313
  }
297
314
 
298
- const flow = device.flow_export;
299
- if (flow && flow.protocol !== "sflow") found.push(`flow_export.protocol ${flow.protocol}, the platform exports sFlow`);
300
- if (flow?.protocol === "sflow" && flow.sampling === undefined) found.push("flow_export.sampling, the platform samples");
315
+ const flow = device.monitoring?.flow_export;
316
+ if (flow && flow.protocol !== "sflow") found.push(`monitoring.flow_export.protocol ${flow.protocol}, the platform exports sFlow`);
317
+ if (flow?.protocol === "sflow" && flow.sampling === undefined) found.push("monitoring.flow_export.sampling, the platform samples");
301
318
 
302
319
  for (const name of flow?.interfaces ?? []) {
303
- if (names.members(name).some((port) => port.startsWith("em"))) found.push(`flow_export.interfaces ${name}, a management port`);
320
+ if (names.members(name).some((port) => port.startsWith("em")))
321
+ found.push(`monitoring.flow_export.interfaces ${name}, a management port`);
304
322
  }
305
323
 
306
324
  found.push(...Derive.unproved(device, Junos.PROVED));
@@ -749,7 +767,7 @@ export class Junos extends Platform {
749
767
 
750
768
  // The CLI is opened on a prompt asked for after the login, since the login's own prompt is already read.
751
769
  const login = async () => {
752
- await SerialConsole.login(opened.channel, Junos.CONSOLE, user, password, console.passwordOnly);
770
+ await SerialConsole.login(opened.channel, Junos.CONSOLE, user, password, console.password_only);
753
771
  const ready = Junos.openCli(opened.channel);
754
772
  opened.channel.write("\n");
755
773
  await ready;
@@ -912,7 +930,10 @@ export class Junos extends Platform {
912
930
  /** The IP MTU of every port and LAG that carries a VLAN; 1500 where none is given. */
913
931
  private static carriers(device: Device, vlan: string): number[] {
914
932
  const sizes = [1500];
915
- const carrying = [...Object.values(device.ports ?? {}), ...Object.values(device.interfaces ?? {})];
933
+ const carrying: (Port | Lag | undefined)[] = [
934
+ ...Derive.ports(device).map(([, settings]) => settings),
935
+ ...Object.values(device.interfaces?.lag ?? {}),
936
+ ];
916
937
 
917
938
  for (const settings of carrying) {
918
939
  if (!settings || !("access_vlan" in settings || "trunk_vlans" in settings)) continue;
@@ -929,12 +950,16 @@ export class Junos extends Platform {
929
950
  const found: string[] = [];
930
951
  const bgp = device.routing?.bgp;
931
952
 
953
+ if (bgp && bgp.asn === undefined) found.push("routing.bgp.asn, absent: write it here or in circuit.ts");
954
+
932
955
  for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
956
+ if (!neighbor) continue;
957
+
933
958
  const settings = JunosBgp.settingsOf(device, neighbor);
934
959
  if (settings.role) found.push(`routing.bgp.neighbors.${name}.role, Junos 21 has no BGP roles`);
935
960
  // IPv4 routes over an IPv6 session need a local IPv4 address the config has no field for.
936
- if (Address.family(neighbor.address) === "ipv6" && settings.families?.includes("ipv4")) {
937
- found.push(`routing.bgp.neighbors.${name}.families ipv4 over an IPv6 session`);
961
+ if (Address.family(neighbor.address) === "ipv6" && settings.address_families?.includes("ipv4")) {
962
+ found.push(`routing.bgp.neighbors.${name}.address_families ipv4 over an IPv6 session`);
938
963
  }
939
964
 
940
965
  if (settings.keepalive !== undefined)
@@ -945,22 +970,30 @@ export class Junos extends Platform {
945
970
  if (server.description) found.push(`routing.rpki ${server.address} description`);
946
971
  }
947
972
 
948
- for (const [name, rules] of Object.entries(device.policies ?? {})) {
949
- rules.forEach((rule, index) => {
950
- const where = `policies.${name}[${index}]`;
973
+ for (const [name, rules] of Object.entries(device.routing?.policies ?? {})) {
974
+ (rules ?? []).forEach((rule, index) => {
975
+ const where = `routing.policies.${name}[${index}]`;
976
+ if ("communities" in rule && rule.communities !== undefined)
977
+ found.push(`${where}.communities, a rule the platform does not write`);
978
+ if (!JunosBgp.isRouteRule(rule)) return;
979
+
951
980
  if (rule.set?.preferred_source) found.push(`${where}.set.preferred_source`);
952
- if (rule.match?.prefix && rule.match.prefix_length) found.push(`${where}.match.prefix with prefix_length`);
981
+ if (rule.if?.prefix && rule.if.prefix_length) found.push(`${where}.if.prefix with prefix_length`);
953
982
  });
954
983
  }
955
984
 
985
+ // A rule takes its family from its prefix set, so a set holds one.
986
+ for (const [name, set] of Object.entries(device.routing?.prefix_sets ?? {})) {
987
+ const families = new Set(JunosBgp.prefixesOf(set ?? { prefixes: [] }).map((prefix) => Address.family(Address.host(prefix))));
988
+ if (families.size > 1)
989
+ found.push(`routing.prefix_sets.${name}.prefixes of both families, a policy rule takes its family from them`);
990
+ }
991
+
956
992
  return found;
957
993
  }
958
994
 
959
995
  /** Layer 3 settings the platform cannot express on a port or an interface. */
960
- private static routedGaps(
961
- where: string,
962
- settings: { addresses?: readonly (string | { address: string; peer: string })[]; vrrp?: readonly { id: number; address: IP }[] }
963
- ): string[] {
996
+ private static routedGaps(where: string, settings: Pick<Routed, "addresses" | "vrrp">): string[] {
964
997
  const found: string[] = [];
965
998
  const addresses = (settings.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
966
999
 
@@ -985,11 +1018,11 @@ export class Junos extends Platform {
985
1018
  found.push(`${where}.storm_control, one threshold for every kind`);
986
1019
  }
987
1020
 
988
- if (typeof settings.stp === "object" && !device.stp) found.push(`${where}.stp while spanning tree is off on the device`);
1021
+ if (typeof settings.stp === "object" && !device.switching?.stp) found.push(`${where}.stp while spanning tree is off on the device`);
989
1022
 
990
- // The limit and the filter sit on the switching family. The filter and an acl would both be its input filter.
1023
+ // The limit and the filter sit on the switching family. The filter and an ACL would both be its input filter.
991
1024
  const switched = settings.access_vlan || settings.trunk_vlans;
992
- const inLag = "lag" in settings && settings.lag;
1025
+ const inLag = "in_lag" in settings && settings.in_lag;
993
1026
 
994
1027
  for (const field of ["mac_limit", "ethertypes"] as const) {
995
1028
  if (settings[field] === undefined) continue;
@@ -998,8 +1031,8 @@ export class Junos extends Platform {
998
1031
  else if (!switched) found.push(`${where}.${field}, on a port or LAG that switches no VLAN`);
999
1032
  }
1000
1033
 
1001
- if (settings.ethertypes && "acl" in settings && settings.acl)
1002
- found.push(`${where}.ethertypes with an acl, the port takes one filter`);
1034
+ if (settings.ethertypes && "ingress_acl" in settings && settings.ingress_acl)
1035
+ found.push(`${where}.ethertypes with an ingress_acl, the port takes one filter`);
1003
1036
 
1004
1037
  return found;
1005
1038
  }