@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
@@ -2,13 +2,10 @@
2
2
 
3
3
  import { Address } from "../../core/address";
4
4
  import { Secrets } from "../../core/secrets";
5
- import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../schema";
5
+ import type { Device, Family, Neighbor, NeighborSettings, PolicyRule, PrefixSet, RouteRule } from "../../model";
6
6
  import { FortiOsNames } from "./names";
7
7
  import { FortiOsBlock, FortiOsTree } from "./tree";
8
8
 
9
- type AnyRule = PolicyRule<string, string>;
10
- type AnyNeighbor = Neighbor<string, string, string>;
11
-
12
9
  /** The lengths a prefix list's rule takes within its prefix. */
13
10
  type Lengths = { ge?: number; le?: number };
14
11
 
@@ -59,8 +56,8 @@ class RouteMaps {
59
56
  return name;
60
57
  }
61
58
 
62
- private rule(entry: FortiOsBlock, rule: AnyRule, family: Family, label: string): void {
63
- const match = rule.match ?? {};
59
+ private rule(entry: FortiOsBlock, rule: RouteRule, family: Family, label: string): void {
60
+ const match = rule.if ?? {};
64
61
  const set = rule.set ?? {};
65
62
  const address = family === "ipv4" ? "match-ip-address" : "match-ip6-address";
66
63
  const bits = FortiOsRouting.BITS[family];
@@ -90,27 +87,29 @@ class RouteMaps {
90
87
  entry.set(address, FortiOsTree.quote(name));
91
88
  }
92
89
 
93
- if (match.community)
90
+ if (match.standard_community)
94
91
  entry.set(
95
92
  "match-community",
96
- FortiOsTree.quote(this.communities(`circuit-match-${RouteMaps.slug(match.community)}`, [match.community], false))
93
+ FortiOsTree.quote(
94
+ this.communities(`circuit-match-${RouteMaps.slug(match.standard_community)}`, [match.standard_community], false)
95
+ )
97
96
  );
98
97
 
99
98
  if (set.local_pref !== undefined) entry.set("set-local-preference", set.local_pref);
100
99
  if (set.med !== undefined) entry.set("set-metric", set.med);
101
100
  if (set.prepend !== undefined)
102
- entry.set("set-aspath", FortiOsTree.quote(Array(set.prepend).fill(this.device.routing!.bgp!.asn).join(" ")));
101
+ entry.set("set-aspath", FortiOsTree.quote(Array(set.prepend).fill(this.device.routing?.bgp?.asn).join(" ")));
103
102
 
104
103
  // Replacing the communities makes removing some needless; added ones join the new ones.
105
- if (set.communities) {
106
- entry.set("set-community", FortiOsTree.quoteAll([...set.communities, ...(set.add_communities ?? [])]));
104
+ if (set.standard_communities) {
105
+ entry.set("set-community", FortiOsTree.quoteAll([...set.standard_communities, ...(set.add_standard_communities ?? [])]));
107
106
  } else {
108
- if (set.add_communities)
109
- entry.set("set-community", FortiOsTree.quoteAll(set.add_communities)).set("set-community-additive", "enable");
107
+ if (set.add_standard_communities)
108
+ entry.set("set-community", FortiOsTree.quoteAll(set.add_standard_communities)).set("set-community-additive", "enable");
110
109
 
111
- if (set.remove_communities) {
112
- const name = `circuit-remove-${RouteMaps.slug(set.remove_communities.join(" "))}`;
113
- entry.set("set-community-delete", FortiOsTree.quote(this.communities(name, set.remove_communities, true)));
110
+ if (set.remove_standard_communities) {
111
+ const name = `circuit-remove-${RouteMaps.slug(set.remove_standard_communities.join(" "))}`;
112
+ entry.set("set-community-delete", FortiOsTree.quote(this.communities(name, set.remove_standard_communities, true)));
114
113
  }
115
114
  }
116
115
 
@@ -118,13 +117,12 @@ class RouteMaps {
118
117
  entry.set(family === "ipv4" ? "set-ip-nexthop" : "set-ip6-nexthop", set.next_hop);
119
118
 
120
119
  // Permit is the default and is not printed.
121
- if (rule.action === "reject") entry.set("action", "deny");
120
+ if (rule.then === "reject") entry.set("action", "deny");
122
121
  }
123
122
 
124
123
  /** The prefixes of a set the config lists. */
125
124
  private prefixesOf(set: string): string[] {
126
- const prefixes = this.device.prefix_sets?.[set];
127
- return prefixes && "prefixes" in prefixes ? [...prefixes.prefixes] : [];
125
+ return FortiOsRouting.listed(this.device.routing?.prefix_sets?.[set]) ?? [];
128
126
  }
129
127
 
130
128
  /** A prefix list of its own, rule by rule; a prefix no length fits is left out. */
@@ -167,19 +165,25 @@ export class FortiOsRouting {
167
165
  static readonly BITS = { ipv4: 32, ipv6: 128 } as const;
168
166
 
169
167
  /** A neighbor's settings with its group's beneath them. */
170
- static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string> {
171
- const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
168
+ static neighborSettings(device: Device, neighbor: Neighbor): NeighborSettings {
169
+ const group = neighbor.neighbor_group ? device.routing?.bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
172
170
  return { ...group, ...neighbor };
173
171
  }
174
172
 
175
173
  /** The families a session carries: its own, or the family of its address. */
176
- static familiesOfNeighbor(device: Device, neighbor: AnyNeighbor): Family[] {
177
- return [...(FortiOsRouting.neighborSettings(device, neighbor).families ?? [Address.family(neighbor.address)])];
174
+ static familiesOfNeighbor(device: Device, neighbor: Neighbor): Family[] {
175
+ return [...(FortiOsRouting.neighborSettings(device, neighbor).address_families ?? [Address.family(neighbor.address)])];
176
+ }
177
+
178
+ /** The prefixes a set lists, from a list or a map of named prefixes; undefined for a set a registry fills. */
179
+ static listed(set: PrefixSet | undefined): string[] | undefined {
180
+ if (!set || !("prefixes" in set)) return undefined;
181
+ return Array.isArray(set.prefixes) ? [...set.prefixes] : Object.values(set.prefixes);
178
182
  }
179
183
 
180
184
  /** Whether a static route, OSPF or BGP detects a dead neighbor with BFD, which the device then runs. */
181
185
  static usesBfd(device: Device): boolean {
182
- const routes = (device.routing?.static ?? []).some((route) => route.bfd);
186
+ const routes = (device.routing?.static_routes ?? []).some((route) => route.bfd);
183
187
  const areas = Object.values(device.routing?.ospf?.areas ?? {});
184
188
  const ospf = areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
185
189
  const bgp = Object.values(device.routing?.bgp?.neighbors ?? {}).some(
@@ -219,7 +223,9 @@ export class FortiOsRouting {
219
223
 
220
224
  /** Each prefix set as a prefix list matching its prefixes and any more specific. One a registry fills starts with a rule that denies every prefix, so it matches nothing until `refresh` fills it. */
221
225
  private static prefixLists(root: FortiOsBlock, device: Device): void {
222
- for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
226
+ for (const [name, set] of Object.entries(device.routing?.prefix_sets ?? {})) {
227
+ if (!set) continue;
228
+
223
229
  if ("source" in set) {
224
230
  const { registry, query, family } = set.source;
225
231
  const entry = root
@@ -233,9 +239,10 @@ export class FortiOsRouting {
233
239
  continue;
234
240
  }
235
241
 
236
- const family = set.prefixes[0] ? FortiOsNames.familyOfPrefix(set.prefixes[0]) : "ipv4";
242
+ const prefixes = FortiOsRouting.listed(set) ?? [];
243
+ const family = prefixes[0] ? FortiOsNames.familyOfPrefix(prefixes[0]) : "ipv4";
237
244
  const entry = root.block(FortiOsRouting.PREFIX_LIST[family]).entry(name);
238
- set.prefixes.forEach((prefix, index) =>
245
+ prefixes.forEach((prefix, index) =>
239
246
  FortiOsRouting.listRule(entry.block("rule").entry(index + 1), family, prefix, FortiOsRouting.bounds(prefix, family))
240
247
  );
241
248
  }
@@ -251,7 +258,7 @@ export class FortiOsRouting {
251
258
  /** OSPF for IPv4. An area runs on the networks of its interfaces; an interface named with a cost, a link type or BFD has settings of its own; a passive one forms no adjacency. */
252
259
  private static ospf(root: FortiOsBlock, device: Device): void {
253
260
  const ospf = device.routing?.ospf;
254
- if (!ospf || !(ospf.families ?? ["ipv4"]).includes("ipv4")) return;
261
+ if (!ospf || !(ospf.address_families ?? ["ipv4"]).includes("ipv4")) return;
255
262
 
256
263
  const block = root.block("router ospf");
257
264
  if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
@@ -290,7 +297,7 @@ export class FortiOsRouting {
290
297
  /** OSPFv3 for IPv6. Every interface of an area runs it in that area. */
291
298
  private static ospf6(root: FortiOsBlock, device: Device): void {
292
299
  const ospf = device.routing?.ospf;
293
- if (!ospf?.families?.includes("ipv6")) return;
300
+ if (!ospf?.address_families?.includes("ipv6")) return;
294
301
 
295
302
  const block = root.block("router ospf6");
296
303
  if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
@@ -323,12 +330,14 @@ export class FortiOsRouting {
323
330
  if (!bgp) return;
324
331
 
325
332
  const block = root.block("router bgp");
326
- block.set("as", bgp.asn);
333
+ if (bgp.asn !== undefined) block.set("as", bgp.asn);
327
334
  if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
328
335
 
329
336
  const maps = new RouteMaps(root, device);
330
337
 
331
338
  for (const neighbor of Object.values(bgp.neighbors ?? {})) {
339
+ if (!neighbor) continue;
340
+
332
341
  const settings = FortiOsRouting.neighborSettings(device, neighbor);
333
342
  const families = FortiOsRouting.familiesOfNeighbor(device, neighbor);
334
343
  const entry = block.block("neighbor").entry(neighbor.address);
@@ -345,8 +354,8 @@ export class FortiOsRouting {
345
354
  for (const family of families) {
346
355
  const suffix = family === "ipv4" ? "" : "6";
347
356
  if (settings.max_prefixes !== undefined) entry.set(`maximum-prefix${suffix}`, settings.max_prefixes);
348
- entry.set(`route-map-in${suffix}`, FortiOsTree.quote(maps.of(settings.import, family)));
349
- entry.set(`route-map-out${suffix}`, FortiOsTree.quote(maps.of(settings.export, family)));
357
+ entry.set(`route-map-in${suffix}`, FortiOsTree.quote(maps.of(settings.import_policy, family)));
358
+ entry.set(`route-map-out${suffix}`, FortiOsTree.quote(maps.of(settings.export_policy, family)));
350
359
  }
351
360
 
352
361
  if (settings.keepalive !== undefined) entry.set("keep-alive-timer", settings.keepalive);
@@ -360,7 +369,7 @@ export class FortiOsRouting {
360
369
 
361
370
  const networks = { ipv4: 0, ipv6: 0 };
362
371
 
363
- for (const prefix of bgp.networks ?? []) {
372
+ for (const prefix of bgp.originated_prefixes ?? []) {
364
373
  const family = FortiOsNames.familyOfPrefix(prefix);
365
374
  const entry = block.block(family === "ipv4" ? "network" : "network6").entry(++networks[family]);
366
375
  entry.set(family === "ipv4" ? "prefix" : "prefix6", FortiOsRouting.listPrefix(prefix, family));
@@ -368,30 +377,41 @@ export class FortiOsRouting {
368
377
  }
369
378
 
370
379
  /** The family a rule is limited to, by its match: a family, a prefix, or a prefix set of one family. */
371
- static familyOfRule(device: Device, rule: AnyRule): Family | undefined {
372
- const match = rule.match;
380
+ static familyOfRule(device: Device, rule: RouteRule): Family | undefined {
381
+ const match = rule.if;
373
382
  if (match?.family) return match.family;
374
383
  if (match?.prefix) return FortiOsNames.familyOfPrefix(match.prefix);
375
384
 
376
- const set = match?.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
385
+ const set = match?.prefix_set ? device.routing?.prefix_sets?.[match.prefix_set] : undefined;
377
386
  if (set && "source" in set) return set.source.family;
378
- if (set?.prefixes[0]) return FortiOsNames.familyOfPrefix(set.prefixes[0]);
387
+
388
+ const first = FortiOsRouting.listed(set)?.[0];
389
+ if (first) return FortiOsNames.familyOfPrefix(first);
379
390
 
380
391
  return undefined;
381
392
  }
382
393
 
383
- /** A policy's rules with each call replaced by the rules it calls, in order. A called policy that decides nothing hands back to the caller. */
384
- static flatten(device: Device, policy: string, seen: string[] = [policy]): AnyRule[] {
385
- const flat: AnyRule[] = [];
394
+ /** A policy's rules with each call replaced by the rules it calls, in order. A called policy that decides nothing hands back to the caller. A `{ communities }` rule, which Circuit writes out before the platform reads it, is left out; `unsupported()` refuses one. */
395
+ static flatten(device: Device, policy: string, seen: string[] = [policy]): RouteRule[] {
396
+ const flat: RouteRule[] = [];
386
397
 
387
- for (const rule of device.policies?.[policy] ?? []) {
388
- if (rule.call && !seen.includes(rule.call)) flat.push(...FortiOsRouting.flatten(device, rule.call, [...seen, rule.call]));
389
- if (rule.match || rule.set || rule.action) flat.push(rule);
398
+ for (const rule of device.routing?.policies?.[policy] ?? []) {
399
+ if ("call" in rule && rule.call !== undefined) {
400
+ if (!seen.includes(rule.call)) flat.push(...FortiOsRouting.flatten(device, rule.call, [...seen, rule.call]));
401
+ continue;
402
+ }
403
+
404
+ if (FortiOsRouting.isRouteRule(rule) && (rule.if || rule.set || rule.then)) flat.push(rule);
390
405
  }
391
406
 
392
407
  return flat;
393
408
  }
394
409
 
410
+ /** Whether a policy rule is a rule on routes, not a call or a `{ communities }` rule. */
411
+ static isRouteRule(rule: PolicyRule): rule is RouteRule {
412
+ return !("call" in rule && rule.call !== undefined) && !("communities" in rule && rule.communities !== undefined);
413
+ }
414
+
395
415
  /** Routing the platform cannot express, or that this adapter does not yet write. */
396
416
  static gaps(device: Device): string[] {
397
417
  const found: string[] = [];
@@ -399,9 +419,10 @@ export class FortiOsRouting {
399
419
 
400
420
  if (routing?.rpki) found.push("routing.rpki, the platform has no route origin validation");
401
421
  if (routing?.router_id && !routing.ospf && !routing.bgp) found.push("routing.router_id without OSPF or BGP, which each hold it");
422
+ if (routing?.bgp && routing.bgp.asn === undefined) found.push("routing.bgp without asn, which the platform's BGP needs");
402
423
 
403
424
  const ospf = routing?.ospf;
404
- if (ospf?.families?.includes("ipv6") && !routing?.router_id) found.push("routing.ospf for IPv6 without routing.router_id");
425
+ if (ospf?.address_families?.includes("ipv6") && !routing?.router_id) found.push("routing.ospf for IPv6 without routing.router_id");
405
426
 
406
427
  for (const area of Object.values(ospf?.areas ?? {})) {
407
428
  for (const name of Object.keys(area.interfaces)) {
@@ -411,12 +432,14 @@ export class FortiOsRouting {
411
432
  const ipv4 = FortiOsNames.addressesOf(FortiOsNames.settingsOf(device, name)).some(
412
433
  (address) => Address.family(Address.host(address)) === "ipv4"
413
434
  );
414
- if ((ospf?.families ?? ["ipv4"]).includes("ipv4") && !ipv4)
435
+ if ((ospf?.address_families ?? ["ipv4"]).includes("ipv4") && !ipv4)
415
436
  found.push(`routing.ospf on ${name}, which has no IPv4 address for OSPF to run on`);
416
437
  }
417
438
  }
418
439
 
419
440
  for (const [name, neighbor] of Object.entries(routing?.bgp?.neighbors ?? {})) {
441
+ if (!neighbor) continue;
442
+
420
443
  const where = `routing.bgp.neighbors.${name}`;
421
444
  const settings = FortiOsRouting.neighborSettings(device, neighbor);
422
445
 
@@ -430,27 +453,35 @@ export class FortiOsRouting {
430
453
  }
431
454
 
432
455
  if (Address.family(neighbor.address) === "ipv6" && FortiOsRouting.familiesOfNeighbor(device, neighbor).includes("ipv4")) {
433
- found.push(`${where}.families ipv4 over an IPv6 session`);
456
+ found.push(`${where}.address_families ipv4 over an IPv6 session`);
434
457
  }
435
458
  }
436
459
 
437
- for (const [name, rules] of Object.entries(device.policies ?? {})) {
438
- rules.forEach((rule, index) => found.push(...FortiOsRouting.ruleGaps(device, `policies.${name}[${index}]`, rule)));
460
+ for (const [name, rules] of Object.entries(routing?.policies ?? {})) {
461
+ (rules ?? []).forEach((rule, index) =>
462
+ found.push(...FortiOsRouting.ruleGaps(device, `routing.policies.${name}[${index}]`, rule))
463
+ );
439
464
  }
440
465
 
441
466
  return found;
442
467
  }
443
468
 
444
- private static ruleGaps(device: Device, where: string, rule: AnyRule): string[] {
469
+ private static ruleGaps(device: Device, where: string, rule: PolicyRule): string[] {
470
+ if (!FortiOsRouting.isRouteRule(rule)) {
471
+ return "communities" in rule && rule.communities !== undefined
472
+ ? [`${where}.communities, a rule Circuit writes out as route rules before the platform reads it`]
473
+ : [];
474
+ }
475
+
445
476
  const found: string[] = [];
446
- const match = rule.match ?? {};
477
+ const match = rule.if ?? {};
447
478
  const set = rule.set ?? {};
448
479
 
449
- if ((rule.match || rule.set) && !rule.action)
450
- found.push(`${where}, a rule without action: the platform stops at the first rule that matches`);
480
+ if ((rule.if || rule.set) && !rule.then)
481
+ found.push(`${where}, a rule without then: the platform stops at the first rule that matches`);
451
482
 
452
- for (const field of ["as_path_length", "through_as", "rpki", "large_community"] as const) {
453
- if (match[field] !== undefined) found.push(`${where}.match.${field}`);
483
+ for (const field of ["as_path_length", "as_path_contains", "rpki", "large_community"] as const) {
484
+ if (match[field] !== undefined) found.push(`${where}.if.${field}`);
454
485
  }
455
486
 
456
487
  for (const field of [
@@ -463,12 +494,12 @@ export class FortiOsRouting {
463
494
  if (set[field] !== undefined) found.push(`${where}.set.${field}`);
464
495
  }
465
496
 
466
- if (match.prefix && match.prefix_length) found.push(`${where}.match.prefix with prefix_length`);
467
- if (match.prefix && match.prefix_set) found.push(`${where}.match.prefix with prefix_set`);
497
+ if (match.prefix && match.prefix_length) found.push(`${where}.if.prefix with prefix_length`);
498
+ if (match.prefix && match.prefix_set) found.push(`${where}.if.prefix with prefix_set`);
468
499
 
469
- const prefixSet = match.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
500
+ const prefixSet = match.prefix_set ? device.routing?.prefix_sets?.[match.prefix_set] : undefined;
470
501
  if (prefixSet && "source" in prefixSet && match.prefix_length) {
471
- found.push(`${where}.match.prefix_length with a prefix set a registry fills`);
502
+ found.push(`${where}.if.prefix_length with a prefix set a registry fills`);
472
503
  }
473
504
 
474
505
  return found;
@@ -1,7 +1,9 @@
1
1
  // Virtual domains: a management VDOM beside `root`. In multi-VDOM mode some sections are the device's own, under `config global`, and the rest belong to one VDOM, under `config vdom` and `edit <name>`. The render and the read write a management VDOM's sections as `@<name> <section>`, so both sides compare like any other section; a plan is made for each scope on its own, and each of its steps sent in that scope.
2
2
 
3
- import type { Device } from "../../schema";
3
+ import type { Device } from "../../model";
4
+ import { Derive } from "../derive";
4
5
  import type { Plan, Step } from "../platform";
6
+ import { FortiOsNames } from "./names";
5
7
  import { FortiOsBlock, FortiOsTree } from "./tree";
6
8
 
7
9
  /** Where a section lives in multi-VDOM mode: the device's own, or one VDOM's. */
@@ -49,15 +51,15 @@ export class FortiOsVdoms {
49
51
 
50
52
  /** The management VDOM's name, when the device has one. */
51
53
  static managementOf(device: Device): string | undefined {
52
- return device.vdoms?.management.name;
54
+ return device.system?.vdoms?.management.name;
53
55
  }
54
56
 
55
57
  /** The VDOM a port or interface is in, by Circuit's name. A port in a LAG is where its LAG is. */
56
58
  static vdomOf(device: Device, name: string): string {
57
- const management = device.vdoms?.management;
59
+ const management = device.system?.vdoms?.management;
58
60
  if (!management) return FortiOsVdoms.ROOT;
59
61
 
60
- const lag = device.ports?.[name as keyof typeof device.ports]?.lag;
62
+ const lag = device.interfaces?.ethernet?.[name]?.in_lag;
61
63
  const holder = lag ?? name;
62
64
  return (management.interfaces as readonly string[]).includes(holder) ? management.name : FortiOsVdoms.ROOT;
63
65
  }
@@ -119,7 +121,7 @@ export class FortiOsVdoms {
119
121
  }
120
122
  if (!wantMulti && haveMulti) {
121
123
  return refused(
122
- "the device runs several VDOMs and the config has no vdoms. Write vdoms.management, or turn multi-VDOM mode off by hand"
124
+ "the device runs several VDOMs and the config has no vdoms. Write system.vdoms.management, or turn multi-VDOM mode off by hand"
123
125
  );
124
126
  }
125
127
  if (!wantMulti) return planOne(desired, current);
@@ -187,29 +189,58 @@ export class FortiOsVdoms {
187
189
  });
188
190
  }
189
191
 
192
+ /** What makes a management VDOM wrong, whatever the platform can express: it is not `root`, it lists a LAG rather than its ports, and each of its routes has a next hop and leaves through one of its interfaces. */
193
+ static problems(device: Device): string[] {
194
+ const management = device.system?.vdoms?.management;
195
+ if (!management) return [];
196
+
197
+ const found: string[] = [];
198
+ const where = "system.vdoms.management";
199
+
200
+ if (management.name === FortiOsVdoms.ROOT)
201
+ found.push(`${where}.name is root, the VDOM everything else is in; give it a name of its own`);
202
+
203
+ for (const name of management.interfaces) {
204
+ const lag = device.interfaces?.ethernet?.[name]?.in_lag;
205
+ if (lag) found.push(`${where}.interfaces ${name} is in LAG ${lag}; list the LAG`);
206
+ }
207
+
208
+ for (const route of management.static_routes ?? []) {
209
+ const at = `${where}.static_routes ${route.prefix}`;
210
+ if (!route.next_hop && !route.interface && !route.blackhole) found.push(`${at} has no next hop`);
211
+ if (route.interface && !(management.interfaces as readonly string[]).includes(route.interface))
212
+ found.push(`${at} leaves through ${route.interface}, which is not in the VDOM`);
213
+ }
214
+
215
+ return found;
216
+ }
217
+
190
218
  /** What a management VDOM can't hold yet, or a config with one can't have. */
219
+
191
220
  static gaps(device: Device): string[] {
192
- if (!device.vdoms) return [];
221
+ const management = device.system?.vdoms?.management;
222
+ if (!management) return [];
193
223
 
194
224
  const found: string[] = [];
195
- const tunnels = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "gre" || iface.type === "vxlan");
225
+ const tunnels = FortiOsNames.interfacesOf(device).filter(({ kind }) => kind === "gre" || kind === "vxlan");
196
226
  if (tunnels.length)
197
- found.push(`vdoms with GRE or VXLAN tunnels (${tunnels.map(([name]) => name).join(", ")}), whose tables belong to a VDOM`);
198
- if (Object.values(device.interfaces ?? {}).some((iface) => iface.type === "bridge"))
199
- found.push("vdoms with a bridge, which needs transparent mode");
227
+ found.push(
228
+ `system.vdoms with GRE or VXLAN tunnels (${tunnels.map(({ name }) => name).join(", ")}), whose tables belong to a VDOM`
229
+ );
230
+ if (FortiOsNames.isTransparent(device)) found.push("system.vdoms with a bridge, which needs transparent mode");
200
231
 
201
- const management = device.vdoms.management;
202
232
  const inManagement = (name: string | undefined) =>
203
233
  name !== undefined && (management.interfaces as readonly string[]).includes(name);
204
234
 
205
- for (const route of device.routing?.static ?? []) {
235
+ for (const route of device.routing?.static_routes ?? []) {
206
236
  if (inManagement(route.interface))
207
- found.push(`routing.static ${route.prefix} leaves through ${route.interface}, in vdoms.management; write it there`);
237
+ found.push(
238
+ `routing.static_routes ${route.prefix} leaves through ${route.interface}, in system.vdoms.management; write it in its static_routes`
239
+ );
208
240
  }
209
241
  for (const name of management.interfaces) {
210
- const settings =
211
- device.interfaces?.[name as keyof typeof device.interfaces] ?? device.ports?.[name as keyof typeof device.ports];
212
- if (settings && "vrf" in settings && settings.vrf) found.push(`vdoms.management interface ${name} in VRF ${settings.vrf}`);
242
+ const vrf = Derive.settings(device, name)?.vrf;
243
+ if (vrf) found.push(`system.vdoms.management interface ${name} in VRF ${vrf}`);
213
244
  }
214
245
 
215
246
  return found;
@@ -22,7 +22,7 @@ Behaviour that looks like something else. Each one has been hit on a QFX5100-48S
22
22
  - The factory VLAN `default` can be deleted.
23
23
  - BGP without a licence commits with only a warning, `requires 'bgp' license`, and sessions establish. The same is true for the other licensed features.
24
24
  - Junos accepts and announces every route over eBGP unless a policy says otherwise: `Default eBGP mode: advertise - accept, receive - accept`. A session without a policy is given `circuit-reject-all`.
25
- - An export policy sees every active route, not only BGP's. Every export starts with `circuit-bgp-origin`, which passes on BGP's routes and the prefixes in `routing.bgp.networks` and rejects the rest.
25
+ - An export policy sees every active route, not only BGP's. Every export starts with `circuit-bgp-origin`, which passes on BGP's routes and the prefixes in `routing.bgp.originated_prefixes` and rejects the rest.
26
26
  - A policy's or a filter's terms are kept in order, and `set` adds a new term at the end. A change to one rewrites it whole.
27
27
  - One policy term cannot filter IPv4 and IPv6 prefixes together: `invalid prefix ::/0 for family inet`. A rule for both families becomes a term for each.
28
28
  - A port filter cannot match the VLAN a frame is switched in: `learn-vlan-id not supported on ingress interface`.
@@ -38,5 +38,5 @@ Behaviour that looks like something else. Each one has been hit on a QFX5100-48S
38
38
  - A static route with BFD is withdrawn while its next hop does not answer BFD, so `show route` shows nothing for it. A session that is down sends every 2 seconds, `Transmit Interval 2.000`, whatever the interval.
39
39
  - `bfd-liveness-detection` on a static route also watches a `qualified-next-hop`: a session to that address starts.
40
40
  - `set switch-options interface xe-0/0/1 interface-mac-limit 1 packet-action drop` is stored on the logical interface and in two statements: `... interface xe-0/0/1.0 interface-mac-limit 1` and `... interface-mac-limit packet-action drop`. The render writes those two.
41
- - An `ethernet-switching` filter takes `from ether-type ipv4`, `ipv6` and `arp` by name. Ethertypes and an `acl` would both be the port's `filter input`, so Circuit refuses the two on one port rather than merge them.
41
+ - An `ethernet-switching` filter takes `from ether-type ipv4`, `ipv6` and `arp` by name. Ethertypes and an `ingress_acl` would both be the port's `filter input`, so Circuit refuses the two on one port rather than merge them.
42
42
  - LLDP sends a port's SNMP index as its port ID, `PortIdSubtype: local`, `PortId: 520`, so a neighbor cannot tell which port it is. `set protocols lldp port-id-subtype interface-name` makes it send `et-0/0/52`, and the render sets it wherever LLDP runs.