@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
@@ -1,20 +1,27 @@
1
1
  // How the platform names what the config names: ports and interfaces, prefixes, and the routing table an interface sits in.
2
2
 
3
3
  import { Address } from "../../core/address";
4
- import type { Device, Family } from "../../schema";
4
+ import type { Device, Family } from "../../model";
5
5
  import { Catalog } from "../catalog";
6
+ import { Derive, type NamedInterface } from "../derive";
6
7
 
7
- type Addressed = { addresses?: readonly (string | { address: string; peer?: string })[]; vrf?: string; lag?: string };
8
+ type Addressed = { addresses?: readonly (string | { address: string; peer_address?: string })[]; vrf?: string; in_lag?: string };
8
9
 
9
10
  export class FortiOsNames {
10
11
  /** The bridges a device has, in the config's order. A device with any runs in transparent mode, each bridge a forward domain numbered by its place. */
11
12
  static bridgesOf(device: Device): { name: string; members: readonly string[] }[] {
12
- return Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
13
- iface.type === "bridge" ? [{ name, members: iface.members }] : []
13
+ return Object.entries(device.interfaces?.bridge ?? {}).flatMap(([name, bridge]) =>
14
+ bridge ? [{ name, members: bridge.member_interfaces }] : []
14
15
  );
15
16
  }
16
17
 
18
+ /** Whether an interface is a GRE or VXLAN tunnel. */
19
+ static isTunnel(device: Device, name: string): boolean {
20
+ return Boolean(device.interfaces?.gre?.[name] ?? device.interfaces?.vxlan?.[name]);
21
+ }
22
+
17
23
  /** Whether the device runs in transparent mode: it has a bridge. */
24
+
18
25
  static isTransparent(device: Device): boolean {
19
26
  return FortiOsNames.bridgesOf(device).length > 0;
20
27
  }
@@ -26,12 +33,7 @@ export class FortiOsNames {
26
33
  if (!FortiOsNames.isTransparent(device)) return undefined;
27
34
 
28
35
  const members = new Set(FortiOsNames.bridgesOf(device).flatMap((bridge) => bridge.members));
29
- const holders = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})] as [
30
- string,
31
- Addressed | undefined,
32
- ][];
33
-
34
- for (const [name, settings] of holders) {
36
+ for (const [name, settings] of FortiOsNames.holders(device)) {
35
37
  if (members.has(name)) continue;
36
38
  const address = FortiOsNames.addressesOf(settings).find((each) => Address.family(Address.host(each)) === "ipv4");
37
39
  if (address) return { name, address };
@@ -48,13 +50,48 @@ export class FortiOsNames {
48
50
 
49
51
  /** The name the platform gives a port or an interface: a port by its spelling, any other interface by its key. */
50
52
  static nameOf(device: Device, name: string): string {
51
- if (device.interfaces && name in device.interfaces) return name;
53
+ if (FortiOsNames.interfacesOf(device).some((entry) => entry.name === name)) return name;
54
+
52
55
  return Catalog.spell(device.platform, device.model, name);
53
56
  }
54
57
 
55
58
  /** A port's or an interface's settings, by its key. */
56
59
  static settingsOf(device: Device, name: string): Addressed | undefined {
57
- return (device.interfaces?.[name] ?? device.ports?.[name as keyof typeof device.ports]) as Addressed | undefined;
60
+ return Derive.settings(device, name);
61
+ }
62
+
63
+ /**
64
+ * Every interface that is not a port, in the order the config writes them: its groups in the order they are written, and each group's interfaces in theirs. Where an order shows on the device, such as the interfaces NTP answers on, it is this one.
65
+ */
66
+ static interfacesOf(device: Device): NamedInterface[] {
67
+ return Object.entries(device.interfaces ?? {}).flatMap(([kind, group]) =>
68
+ kind === "ethernet"
69
+ ? []
70
+ : Object.entries((group ?? {}) as Record<string, NamedInterface["settings"] | undefined>).flatMap(([name, settings]) =>
71
+ settings ? [{ name, kind: kind as NamedInterface["kind"], settings }] : []
72
+ )
73
+ );
74
+ }
75
+
76
+ /** Every port, then every other interface, by key, with its settings. */
77
+ private static holders(device: Device): [string, Addressed][] {
78
+ return [
79
+ ...Derive.ports(device),
80
+ ...FortiOsNames.interfacesOf(device).map(({ name, settings }) => [name, settings] as [string, Addressed]),
81
+ ];
82
+ }
83
+
84
+ /** The sources `management.allow` names: its prefixes, or the members of the address set it names. */
85
+ static allowed(device: Device): readonly string[] {
86
+ const allow = device.management?.allow;
87
+ if (typeof allow !== "string") return allow ?? [];
88
+
89
+ return device.firewall?.address_sets?.[allow] ?? [];
90
+ }
91
+
92
+ /** The one value a match field holds, or undefined. A list is one value per rule, which Circuit writes out before the platform reads it; `unsupported()` refuses a list of several. */
93
+ static single<T>(value: T | readonly T[] | undefined): T | undefined {
94
+ return Array.isArray(value) ? (value as readonly T[])[0] : (value as T | undefined);
58
95
  }
59
96
 
60
97
  /** Every address of a port or an interface. */
@@ -84,16 +121,12 @@ export class FortiOsNames {
84
121
 
85
122
  /** The number of a VRF on the platform: its place among the device's VRFs, the default table being 0. */
86
123
  static vrfNumber(device: Device, vrf: string | undefined): number {
87
- return vrf ? Object.keys(device.vrfs ?? {}).indexOf(vrf) + 1 : 0;
124
+ return vrf ? Object.keys(device.routing?.vrfs ?? {}).indexOf(vrf) + 1 : 0;
88
125
  }
89
126
 
90
127
  /** Every port and interface that routes, by key, with its settings. A LAG member routes through its LAG. */
91
128
  private static routedHolders(device: Device): [string, Addressed][] {
92
- const holders = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})] as [
93
- string,
94
- Addressed | undefined,
95
- ][];
96
- return holders.filter((holder): holder is [string, Addressed] => holder[1] !== undefined && !holder[1].lag);
129
+ return FortiOsNames.holders(device).filter(([, settings]) => !settings.in_lag);
97
130
  }
98
131
 
99
132
  /** The interface whose own network holds an address, the most specific when several do, in one routing table. */
@@ -2,8 +2,10 @@
2
2
 
3
3
  import { Address } from "../../core/address";
4
4
  import { Secrets } from "../../core/secrets";
5
- import type { Device, Helper, Severity } from "../../schema";
5
+ import { Utils } from "../../core/utils";
6
+ import type { Device, Gre, Helper, Severity, StaticRoute, Vxlan } from "../../model";
6
7
  import { Catalog } from "../catalog";
8
+ import { Derive } from "../derive";
7
9
  import { FortiOsFirewall } from "./firewall";
8
10
  import { FortiOsNames } from "./names";
9
11
  import { FortiOsRouting } from "./routing";
@@ -13,7 +15,7 @@ import { FortiOsVdoms } from "./vdoms";
13
15
  export type Section = (typeof FortiOsRender.SECTIONS)[number];
14
16
 
15
17
  type RoutedSettings = {
16
- addresses?: readonly (string | { address: string; peer: string })[];
18
+ addresses?: readonly (string | { address: string; peer_address: string })[];
17
19
  vrrp?: readonly { id: number; address: string; priority?: number; preempt?: boolean; interval?: number }[];
18
20
  lldp?: boolean;
19
21
  vrf?: string;
@@ -251,10 +253,10 @@ export class FortiOsRender {
251
253
 
252
254
  if (management.https) services.push("https");
253
255
  if (management.ssh) services.push("ssh");
254
- if (management.snmp) services.push("snmp");
256
+ if (device.monitoring?.snmp) services.push("snmp");
255
257
  if (management.http) services.push("http");
256
258
  if (management.telnet) services.push("telnet");
257
- if (management.native) services.push("fgfm");
259
+ if (management.native_protocol) services.push("fgfm");
258
260
 
259
261
  return services.join(" ");
260
262
  }
@@ -289,7 +291,7 @@ export class FortiOsRender {
289
291
  FortiOsRender.logging(root, device);
290
292
  FortiOsRender.flowExport(root, device);
291
293
  FortiOsRender.dhcpServers(section("system dhcp server"), device);
292
- FortiOsRender.staticRoutes(section("router static"), section("router static6"), device);
294
+ FortiOsRender.staticRoutes(section("router static"), section("router static6"), device, device.routing?.static_routes ?? []);
293
295
  FortiOsRouting.render(root, device);
294
296
  FortiOsRender.helpers(section("system session-helper"), device);
295
297
  FortiOsFirewall.render(root, device);
@@ -300,16 +302,13 @@ export class FortiOsRender {
300
302
 
301
303
  /** A management VDOM's routes, and who reaches the device's own services through it, as sections of their own after everything else. */
302
304
  private static managementVdom(root: FortiOsBlock, device: Device): void {
303
- const management = device.vdoms?.management;
305
+ const management = device.system?.vdoms?.management;
304
306
  if (!management) return;
305
307
 
306
308
  const vdom = new FortiOsBlock();
307
309
  for (const section of FortiOsVdoms.MANAGEMENT_SECTIONS) vdom.block(section);
308
310
 
309
- FortiOsRender.staticRoutes(vdom.block("router static"), vdom.block("router static6"), {
310
- ...device,
311
- routing: { static: management.static ?? [] },
312
- });
311
+ FortiOsRender.staticRoutes(vdom.block("router static"), vdom.block("router static6"), device, management.static_routes ?? []);
313
312
  FortiOsFirewall.managementOnly(vdom, device);
314
313
 
315
314
  for (const [section, block] of vdom.blocks) root.blocks.set(FortiOsVdoms.key(management.name, section), block);
@@ -317,8 +316,10 @@ export class FortiOsRender {
317
316
 
318
317
  /** A certificate with its key is the device's own; one without is only trusted. */
319
318
  private static certificates(local: FortiOsBlock, trusted: FortiOsBlock, device: Device): void {
320
- for (const [name, certificate] of Object.entries(device.certificates ?? {})) {
321
- const text = FortiOsTree.quote(certificate.certificate.trim());
319
+ for (const [name, certificate] of Object.entries(device.system?.certificates ?? {})) {
320
+ if (!certificate) continue;
321
+
322
+ const text = FortiOsTree.quote(Utils.projectFile(certificate.certificate_file).trim());
322
323
 
323
324
  if (certificate.private_key)
324
325
  local
@@ -348,7 +349,7 @@ export class FortiOsRender {
348
349
  if (management.http) block.set("admin-https-redirect", "disable");
349
350
  if (device.system?.banner) block.set("pre-login-banner", "enable");
350
351
 
351
- if (device.lldp === true) block.set("lldp-transmission", "enable").set("lldp-reception", "enable");
352
+ if (device.switching?.lldp === true) block.set("lldp-transmission", "enable").set("lldp-reception", "enable");
352
353
 
353
354
  const vdom = FortiOsVdoms.managementOf(device);
354
355
  if (vdom) block.set("vdom-mode", "multi-vdom").set("management-vdom", FortiOsTree.quote(vdom));
@@ -356,7 +357,8 @@ export class FortiOsRender {
356
357
 
357
358
  /** Each user, with the profile its role takes. */
358
359
  private static users(profiles: FortiOsBlock, admins: FortiOsBlock, device: Device): void {
359
- const roles = new Set(Object.values(device.users ?? {}).map((user) => user.role));
360
+ const users = Object.entries(device.system?.users ?? {}).flatMap(([name, user]) => (user ? [[name, user] as const] : []));
361
+ const roles = new Set(users.map(([, user]) => user.role));
360
362
 
361
363
  // An operator changes everything but the system's own settings, which hold the users; read-only changes nothing.
362
364
  for (const role of ["operator", "read-only"] as const) {
@@ -367,7 +369,7 @@ export class FortiOsRender {
367
369
  profile.set(group, role === "operator" && group !== "sysgrp" ? "read-write" : "read");
368
370
  }
369
371
 
370
- for (const [name, user] of Object.entries(device.users ?? {})) {
372
+ for (const [name, user] of users) {
371
373
  const admin = admins.entry(name);
372
374
 
373
375
  admin.set("accprofile", FortiOsTree.quote(FortiOsRender.PROFILE[user.role]));
@@ -384,7 +386,7 @@ export class FortiOsRender {
384
386
  const access = FortiOsRender.allowAccess(device);
385
387
 
386
388
  for (const port of Catalog.ports(device.platform, device.model)) {
387
- const settings = device.ports?.[port as keyof typeof device.ports];
389
+ const settings = device.interfaces?.ethernet?.[port];
388
390
  const entry = block.entry(Catalog.spell(device.platform, device.model, port));
389
391
 
390
392
  entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, port)));
@@ -402,18 +404,18 @@ export class FortiOsRender {
402
404
  if (speed) entry.set("speed", speed);
403
405
 
404
406
  // A port in a LAG has no redirects of its own: `command parse error`.
405
- if (settings.lag) continue;
407
+ if (settings.in_lag) continue;
406
408
 
407
409
  FortiOsRender.redirects(entry, device);
408
410
  if (settings.mtu) entry.set("mtu-override", "enable").set("mtu", settings.mtu);
409
411
  FortiOsRender.routed(entry, device, port, settings, access);
410
412
  }
411
413
 
412
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
413
- if (iface.type !== "lag") continue;
414
+ for (const [name, iface] of Object.entries(device.interfaces?.lag ?? {})) {
415
+ if (!iface) continue;
414
416
 
415
- const members = Object.entries(device.ports ?? {})
416
- .filter(([, port]) => port?.lag === name)
417
+ const members = Derive.ports(device)
418
+ .filter(([, port]) => port.in_lag === name)
417
419
  .map(([port]) => Catalog.spell(device.platform, device.model, port));
418
420
 
419
421
  const entry = block.entry(name);
@@ -427,14 +429,14 @@ export class FortiOsRender {
427
429
  FortiOsRender.routed(entry, device, name, iface, access);
428
430
  }
429
431
 
430
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
431
- if (iface.type !== "vlan") continue;
432
+ for (const [name, iface] of Object.entries(device.interfaces?.vlan_interface ?? {})) {
433
+ if (!iface) continue;
432
434
 
433
435
  const parent = FortiOsRender.vlanParent(device, iface.vlan);
434
436
  const entry = block.entry(name);
435
437
  entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
436
438
  if (parent) entry.set("interface", FortiOsTree.quote(FortiOsNames.nameOf(device, parent)));
437
- entry.set("vlanid", device.vlans?.[iface.vlan]?.id ?? 0);
439
+ entry.set("vlanid", device.switching?.vlans?.[iface.vlan]?.id ?? 0);
438
440
  const domain = FortiOsNames.forwardDomainOf(device, name);
439
441
  if (domain) entry.set("forward-domain", domain);
440
442
  if (iface.description) entry.set("description", FortiOsTree.quote(iface.description));
@@ -443,8 +445,8 @@ export class FortiOsRender {
443
445
  FortiOsRender.routed(entry, device, name, iface, access);
444
446
  }
445
447
 
446
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
447
- if (iface.type !== "loopback") continue;
448
+ for (const [name, iface] of Object.entries(device.interfaces?.loopback ?? {})) {
449
+ if (!iface) continue;
448
450
 
449
451
  const entry = block.entry(name);
450
452
  entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
@@ -455,31 +457,34 @@ export class FortiOsRender {
455
457
  }
456
458
 
457
459
  // A tunnel's interface is created by its own table, which names the interface it leaves through.
458
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
459
- if (iface.type !== "gre" && iface.type !== "vxlan") continue;
460
+ for (const { name, kind, settings } of FortiOsNames.interfacesOf(device)) {
461
+ if (kind !== "gre" && kind !== "vxlan") continue;
462
+
463
+ const iface = settings as Gre | Vxlan;
464
+ const vxlan = kind === "vxlan" ? (settings as Vxlan) : undefined;
460
465
 
461
- const outgoing = FortiOsNames.holderOf(device, iface.local);
466
+ const outgoing = FortiOsNames.holderOf(device, iface.local_address);
462
467
  const via = outgoing ? FortiOsTree.quote(FortiOsNames.nameOf(device, outgoing)) : '""';
463
468
 
464
469
  // Between IPv6 endpoints the family is stated first: the addresses of the other family are not settings until it is.
465
- const ipv6 = Address.family(iface.remote) === "ipv6";
470
+ const ipv6 = Address.family(iface.remote_address) === "ipv6";
466
471
 
467
- if (iface.type === "gre") {
472
+ if (!vxlan) {
468
473
  const tunnel = root.block("system gre-tunnel").entry(name).set("interface", via);
469
- if (ipv6) tunnel.set("ip-version", 6).set("remote-gw6", iface.remote).set("local-gw6", iface.local);
470
- else tunnel.set("remote-gw", iface.remote).set("local-gw", iface.local);
474
+ if (ipv6) tunnel.set("ip-version", 6).set("remote-gw6", iface.remote_address).set("local-gw6", iface.local_address);
475
+ else tunnel.set("remote-gw", iface.remote_address).set("local-gw", iface.local_address);
471
476
  } else {
472
- const tunnel = root.block("system vxlan").entry(name).set("interface", via).set("vni", iface.vni);
473
- if (ipv6) tunnel.set("ip-version", "ipv6-unicast").set("remote-ip6", FortiOsTree.quote(iface.remote));
474
- else tunnel.set("remote-ip", FortiOsTree.quote(iface.remote));
475
- if (iface.port !== undefined && iface.port !== 4789) tunnel.set("dstport", iface.port);
477
+ const tunnel = root.block("system vxlan").entry(name).set("interface", via).set("vni", vxlan.vni);
478
+ if (ipv6) tunnel.set("ip-version", "ipv6-unicast").set("remote-ip6", FortiOsTree.quote(vxlan.remote_address));
479
+ else tunnel.set("remote-ip", FortiOsTree.quote(vxlan.remote_address));
480
+ if (vxlan.port !== undefined && vxlan.port !== 4789) tunnel.set("dstport", vxlan.port);
476
481
  }
477
482
 
478
483
  const entry = block.entry(name);
479
484
  entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
480
- entry.set("type", iface.type === "gre" ? "tunnel" : "vxlan");
485
+ entry.set("type", vxlan ? "vxlan" : "tunnel");
481
486
  entry.set("interface", via);
482
- if (iface.type === "vxlan" && iface.mac) entry.set("macaddr", iface.mac.toLowerCase());
487
+ if (vxlan?.mac) entry.set("macaddr", vxlan.mac.toLowerCase());
483
488
  if (iface.description) entry.set("description", FortiOsTree.quote(iface.description));
484
489
  if (iface.mtu) entry.set("mtu-override", "enable").set("mtu", iface.mtu);
485
490
  FortiOsRender.redirects(entry, device);
@@ -489,14 +494,14 @@ export class FortiOsRender {
489
494
 
490
495
  /** The platform sends ICMP redirects unless an interface says otherwise; the config sends them only when asked. */
491
496
  private static redirects(entry: FortiOsBlock, device: Device): void {
492
- if (!device.system?.ip?.icmp_redirects) entry.set("icmp-send-redirect", "disable");
497
+ if (!device.system?.ip_stack?.icmp_redirects) entry.set("icmp-send-redirect", "disable");
493
498
  }
494
499
 
495
500
  /** The one port or LAG that carries a VLAN tagged, which its VLAN interface sits on. `unsupported()` refuses any other case. */
496
501
  static vlanParent(device: Device, vlan: string): string | undefined {
497
502
  const carriers = [
498
- ...Object.entries(device.ports ?? {}).filter(([, port]) => !port?.lag && port?.trunk_vlans?.includes(vlan)),
499
- ...Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "lag" && iface.trunk_vlans?.includes(vlan)),
503
+ ...Derive.ports(device).filter(([, port]) => !port.in_lag && port.trunk_vlans?.includes(vlan)),
504
+ ...Object.entries(device.interfaces?.lag ?? {}).filter(([, lag]) => lag?.trunk_vlans?.includes(vlan)),
500
505
  ];
501
506
 
502
507
  return carriers.length === 1 ? carriers[0]![0] : undefined;
@@ -522,11 +527,11 @@ export class FortiOsRender {
522
527
  if (vrf) entry.set("vrf", vrf);
523
528
 
524
529
  // A tunnel holds an IPv4 address as a host and its peer with the link's mask.
525
- const peer = settings.addresses?.find((item): item is { address: string; peer: string } => typeof item !== "string");
530
+ const peer = settings.addresses?.find((item): item is { address: string; peer_address: string } => typeof item !== "string");
526
531
 
527
532
  if (peer) {
528
533
  entry.set("ip", `${Address.host(peer.address)} 255.255.255.255`);
529
- entry.set("remote-ip", `${peer.peer} ${Address.netmask(Address.split(peer.address).len)}`);
534
+ entry.set("remote-ip", `${peer.peer_address} ${Address.netmask(Address.split(peer.address).len)}`);
530
535
  entry.set("allowaccess", access);
531
536
  } else if (ipv4.length) {
532
537
  entry.set("ip", FortiOsNames.subnet(ipv4[0]!));
@@ -544,7 +549,7 @@ export class FortiOsRender {
544
549
  );
545
550
  }
546
551
 
547
- const relays = Object.values(device.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
552
+ const relays = Object.values(device.services?.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
548
553
  const relayed = relays.flatMap((relay) => relay.servers);
549
554
  const relayed4 = relayed.filter((server) => Address.family(server) === "ipv4");
550
555
  const relayed6 = relayed.filter((server) => Address.family(server) === "ipv6");
@@ -589,15 +594,16 @@ export class FortiOsRender {
589
594
  }
590
595
 
591
596
  // LLDP follows the device unless a port says otherwise, or the device lists where it runs.
592
- const listed = typeof device.lldp === "object" && device.lldp.interfaces.includes(name as never);
597
+ const lldp = device.switching?.lldp;
598
+ const listed = typeof lldp === "object" && lldp.interfaces.includes(name);
593
599
  if (settings.lldp === false) entry.set("lldp-transmission", "disable").set("lldp-reception", "disable");
594
- else if (listed || (settings.lldp === true && device.lldp !== true))
600
+ else if (listed || (settings.lldp === true && lldp !== true))
595
601
  entry.set("lldp-transmission", "enable").set("lldp-reception", "enable");
596
602
 
597
603
  // A tunnel is not sampled: its traffic is, on the interface it leaves through.
598
- const flow = device.flow_export;
599
- const tunnel = ["gre", "vxlan"].includes(device.interfaces?.[name]?.type ?? "");
600
- const sampled = flow && !tunnel && (!flow.interfaces || flow.interfaces.includes(name as never));
604
+ const flow = device.monitoring?.flow_export;
605
+ const tunnel = FortiOsNames.isTunnel(device, name);
606
+ const sampled = flow && !tunnel && (!flow.interfaces || flow.interfaces.includes(name));
601
607
 
602
608
  if (sampled && flow.protocol === "sflow") {
603
609
  entry.set("sflow-sampler", "enable");
@@ -619,7 +625,8 @@ export class FortiOsRender {
619
625
  if (ipv6[1]) block.set("ip6-secondary", ipv6[1]);
620
626
 
621
627
  // Recursive, the default mode, answers from what the device has resolved itself.
622
- if (device.system?.dns?.serve) for (const name of FortiOsNames.addressed(device)) servers.entry(FortiOsNames.nameOf(device, name));
628
+ if (device.services?.dns_server)
629
+ for (const name of FortiOsNames.addressed(device)) servers.entry(FortiOsNames.nameOf(device, name));
623
630
  }
624
631
 
625
632
  /** The servers the device keeps time from, and the interfaces it answers time queries on when it serves them. */
@@ -635,7 +642,7 @@ export class FortiOsRender {
635
642
  servers.forEach((server, index) => list.entry(index + 1).set("server", FortiOsTree.quote(server)));
636
643
  }
637
644
 
638
- if (ntp?.serve) {
645
+ if (device.services?.ntp_server) {
639
646
  block.set("server-mode", "enable");
640
647
  block.set("interface", FortiOsTree.quoteAll(FortiOsNames.addressed(device).map((name) => FortiOsNames.nameOf(device, name))));
641
648
  }
@@ -643,7 +650,7 @@ export class FortiOsRender {
643
650
 
644
651
  /** SNMP version 2c with one community, guarded by its hosts, and version 3 users. */
645
652
  private static snmp(root: FortiOsBlock, device: Device): void {
646
- const snmp = device.management?.snmp;
653
+ const snmp = device.monitoring?.snmp;
647
654
  if (!snmp) return;
648
655
 
649
656
  const sysinfo = root.block("system snmp sysinfo");
@@ -655,7 +662,7 @@ export class FortiOsRender {
655
662
  const community = root.block("system snmp community").entry(1);
656
663
  community.set("name", FortiOsTree.quote(Secrets.marker(snmp.community)));
657
664
 
658
- const allow = snmp.allow ?? device.management?.allow ?? [];
665
+ const allow = snmp.allow ?? FortiOsNames.allowed(device);
659
666
  const ipv4 = allow.filter((prefix) => Address.family(Address.host(prefix)) === "ipv4");
660
667
  const ipv6 = allow.filter((prefix) => Address.family(Address.host(prefix)) === "ipv6");
661
668
  ipv4.forEach((prefix, index) =>
@@ -701,7 +708,7 @@ export class FortiOsRender {
701
708
  // The disk keeps logs unless told not to; the config keeps them only in memory.
702
709
  root.block("log disk setting").set("status", "disable");
703
710
 
704
- (logging?.remote ?? []).slice(0, FortiOsRender.COLLECTORS).forEach((collector, index) => {
711
+ (device.monitoring?.syslog ?? []).slice(0, FortiOsRender.COLLECTORS).forEach((collector, index) => {
705
712
  const number = index === 0 ? "" : String(index + 1);
706
713
  const setting = root.block(`log syslogd${number} setting`);
707
714
  setting.set("status", "enable");
@@ -714,7 +721,7 @@ export class FortiOsRender {
714
721
 
715
722
  /** One collector, and sampling on each interface it observes. */
716
723
  private static flowExport(root: FortiOsBlock, device: Device): void {
717
- const flow = device.flow_export;
724
+ const flow = device.monitoring?.flow_export;
718
725
  const collector = flow?.collectors[0];
719
726
  if (!flow || !collector) return;
720
727
 
@@ -730,7 +737,7 @@ export class FortiOsRender {
730
737
  }
731
738
 
732
739
  private static dhcpServers(block: FortiOsBlock, device: Device): void {
733
- Object.values(device.dhcp ?? {}).forEach((server, index) => {
740
+ Object.values(device.services?.dhcp_server ?? {}).forEach((server, index) => {
734
741
  const entry = block.entry(index + 1);
735
742
  if (server.lease_time !== undefined && server.lease_time !== 604800) entry.set("lease-time", server.lease_time);
736
743
  entry.set("default-gateway", server.gateway);
@@ -750,10 +757,10 @@ export class FortiOsRender {
750
757
  }
751
758
 
752
759
  /** Each route by its position. A route goes in the routing table of the interface it leaves through. A default route is the platform's default destination and is not printed. */
753
- private static staticRoutes(ipv4: FortiOsBlock, ipv6: FortiOsBlock, device: Device): void {
760
+ private static staticRoutes(ipv4: FortiOsBlock, ipv6: FortiOsBlock, device: Device, routes: readonly StaticRoute[]): void {
754
761
  const counters = { ipv4: 0, ipv6: 0 };
755
762
 
756
- for (const route of device.routing?.static ?? []) {
763
+ for (const route of routes) {
757
764
  const family = Address.family(Address.split(route.prefix).addr);
758
765
  const entry = (family === "ipv4" ? ipv4 : ipv6).entry(++counters[family]);
759
766
  const prefix = FortiOsNames.network(route.prefix);
@@ -761,10 +768,10 @@ export class FortiOsRender {
761
768
  if (family === "ipv6" && prefix !== "::/0") entry.set("dst", prefix);
762
769
  if (family === "ipv4" && prefix !== "0.0.0.0/0") entry.set("dst", FortiOsNames.subnet(prefix));
763
770
 
764
- if (route.via) entry.set("gateway", route.via);
771
+ if (route.next_hop) entry.set("gateway", route.next_hop);
765
772
 
766
773
  // In transparent mode a route names no interface: it carries only the management address's own traffic.
767
- const outgoing = route.interface ?? (route.via ? FortiOsNames.interfaceFor(device, route.via, route.vrf) : undefined);
774
+ const outgoing = route.interface ?? (route.next_hop ? FortiOsNames.interfaceFor(device, route.next_hop, route.vrf) : undefined);
768
775
  if (outgoing && !route.blackhole && !FortiOsNames.isTransparent(device))
769
776
  entry.set("device", FortiOsTree.quote(FortiOsNames.nameOf(device, outgoing)));
770
777
 
@@ -793,7 +800,7 @@ export class FortiOsRender {
793
800
  private static helpers(block: FortiOsBlock, device: Device): void {
794
801
  let number = 0;
795
802
 
796
- for (const helper of device.firewall?.helpers ?? []) {
803
+ for (const helper of device.firewall?.conntrack_helpers ?? []) {
797
804
  for (const [name, protocol, port] of FortiOsRender.HELPERS[helper] ?? [])
798
805
  block.entry(++number).set("name", name).set("protocol", protocol).set("port", port);
799
806
  }