@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
@@ -3,16 +3,13 @@
3
3
  import { Address } from "../../core/address";
4
4
  import { Cabling } from "../../core/cabling";
5
5
  import { Secrets } from "../../core/secrets";
6
- import type { AclRule, Device, Ethertype, Family, Interface, Port, Ports, Rate, Severity, Vrrp } from "../../schema";
6
+ import type { AclRule, Device, Ethertype, Family, Interface, Lag, Port, Ports, Rate, Severity, Vrrp } from "../../model";
7
7
  import { Catalog } from "../catalog";
8
8
  import { Derive } from "../derive";
9
9
  import { JunosBgp } from "./bgp";
10
10
 
11
- type AnyPort = Port<string, string, string, string>;
12
- type AnyInterface = Interface<string, string>;
13
- type LagInterface = Extract<AnyInterface, { type: "lag" }>;
14
- type Switched = AnyPort | LagInterface;
15
- type AnyAclRule = AclRule<string>;
11
+ /** A port or a LAG: what switches VLANs. */
12
+ type Switched = Port | Lag;
16
13
 
17
14
  /** Collects `set` statements, and comments the plan reads. */
18
15
  export class JunosStatements {
@@ -51,49 +48,49 @@ export class JunosNames {
51
48
  this.lags = Derive.lagIds(device);
52
49
 
53
50
  // The default table's loopback is unit 0; one in a VRF takes the next unit.
54
- const loopbacks = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "loopback");
51
+ const loopbacks = Derive.interfaces(device).filter((entry) => entry.kind === "loopback");
55
52
  let unit = 0;
56
53
 
57
- for (const [name, iface] of loopbacks) if (!iface.vrf) this.loopbacks.set(name, 0);
54
+ for (const { name, settings } of loopbacks) if (!settings.vrf) this.loopbacks.set(name, 0);
58
55
 
59
- for (const [name, iface] of loopbacks) if (iface.vrf) this.loopbacks.set(name, ++unit);
56
+ for (const { name, settings } of loopbacks) if (settings.vrf) this.loopbacks.set(name, ++unit);
60
57
 
61
58
  // GRE tunnels are units of the one tunnel interface, in order.
62
- const tunnels = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "gre");
63
- tunnels.forEach(([name], index) => this.tunnels.set(name, index));
59
+ const tunnels = Derive.interfaces(device).filter((entry) => entry.kind === "gre");
60
+ tunnels.forEach(({ name }, index) => this.tunnels.set(name, index));
64
61
  }
65
62
 
66
63
  /** A physical port. A 10G cage holding a 1G optic is `ge-`, not `xe-`. */
67
64
  port(port: string): string {
68
65
  const name = Catalog.spell(this.device.platform, this.device.model, port);
69
- const settings = this.device.ports?.[port as keyof typeof this.device.ports];
66
+ const settings = this.device.interfaces?.ethernet?.[port];
70
67
  return settings?.speed === "1g" ? name.replace(/^xe-/, "ge-") : name;
71
68
  }
72
69
 
73
70
  /** The physical interface a port or a LAG is: `xe-0/0/1`, `ae1`. */
74
71
  physical(name: string): string {
75
- if (this.device.interfaces?.[name]?.type === "lag") return `ae${this.lags.get(name)}`;
72
+ if (this.device.interfaces?.lag?.[name]) return `ae${this.lags.get(name)}`;
76
73
 
77
74
  return this.port(name);
78
75
  }
79
76
 
80
77
  /** The logical interface that holds addresses: `xe-0/0/1.0`, `ae1.0`, `irb.10`, `lo0.0`. */
81
78
  unit(name: string): string {
82
- const iface = this.device.interfaces?.[name];
83
- if (iface?.type === "vlan") return `irb.${this.device.vlans![iface.vlan]!.id}`;
79
+ const vlanInterface = this.device.interfaces?.vlan_interface?.[name];
80
+ if (vlanInterface) return `irb.${this.device.switching!.vlans![vlanInterface.vlan]!.id}`;
84
81
 
85
- if (iface?.type === "loopback") return `lo0.${this.loopbacks.get(name)}`;
82
+ if (this.device.interfaces?.loopback?.[name]) return `lo0.${this.loopbacks.get(name)}`;
86
83
 
87
- if (iface?.type === "gre") return `gr-0/0/0.${this.tunnels.get(name)}`;
84
+ if (this.device.interfaces?.gre?.[name]) return `gr-0/0/0.${this.tunnels.get(name)}`;
88
85
 
89
86
  return `${this.physical(name)}.0`;
90
87
  }
91
88
 
92
89
  /** The physical ports behind a port or a LAG. */
93
90
  members(name: string): string[] {
94
- if (this.device.interfaces?.[name]?.type !== "lag") return [this.port(name)];
91
+ if (!this.device.interfaces?.lag?.[name]) return [this.port(name)];
95
92
 
96
- const ports = Object.entries(this.device.ports ?? {}).filter(([, settings]) => settings?.lag === name);
93
+ const ports = Derive.ports(this.device).filter(([, settings]) => settings.in_lag === name);
97
94
  return ports.map(([port]) => this.port(port));
98
95
  }
99
96
  }
@@ -147,7 +144,7 @@ export class JunosRender {
147
144
  "group-exchange-sha2",
148
145
  ];
149
146
 
150
- /** The transport and port each management service listens on. */
147
+ /** The transport and port each management service listens on, by the name of its term in the services filter. */
151
148
  private static readonly SERVICE_PORTS = {
152
149
  ssh: ["tcp", 22],
153
150
  telnet: ["tcp", 23],
@@ -267,14 +264,19 @@ export class JunosRender {
267
264
  }
268
265
 
269
266
  /** The family a hardware ACL rule is limited to, by its family or its addresses. */
270
- private static aclFamily(rule: AnyAclRule): Family | undefined {
271
- const match = rule.match;
267
+ private static aclFamily(rule: AclRule): Family | undefined {
268
+ const match = rule.if;
272
269
  if (match?.family) return match.family;
273
270
 
274
- const address = match?.src ?? match?.dst;
271
+ const address = JunosRender.each(match?.src ?? match?.dst)[0];
275
272
  return address ? Address.family(Address.host(address)) : undefined;
276
273
  }
277
274
 
275
+ /** One value or a list of them, as a list. `unsupported()` refuses a list in an ACL rule's match, so a rule rendered for a device has one. */
276
+ private static each<T extends string>(value: T | readonly T[] | undefined): T[] {
277
+ return value === undefined ? [] : ([value].flat() as T[]);
278
+ }
279
+
278
280
  /** The protocol as an IPv6 header names it. */
279
281
  private static nextHeader(protocol: string): string {
280
282
  return protocol === "icmp" ? "icmp6" : protocol;
@@ -297,7 +299,7 @@ export class JunosRender {
297
299
  for (const server of system.ntp?.servers ?? []) this.out.set("system ntp server", server);
298
300
 
299
301
  // Redirects are sent unless turned off, for each family.
300
- if (!system.ip?.icmp_redirects) {
302
+ if (!system.ip_stack?.icmp_redirects) {
301
303
  this.out.set("system no-redirects");
302
304
  this.out.set("system no-redirects-ipv6");
303
305
  }
@@ -308,7 +310,7 @@ export class JunosRender {
308
310
  if (local.files !== undefined) this.out.set("system syslog file messages archive files", local.files);
309
311
  }
310
312
 
311
- for (const remote of system.logging?.remote ?? []) {
313
+ for (const remote of this.device.monitoring?.syslog ?? []) {
312
314
  this.out.set("system syslog host", remote.address, "any", JunosRender.LEVEL[remote.level]);
313
315
  if (remote.port !== undefined && remote.port !== 514) this.out.set("system syslog host", remote.address, "port", remote.port);
314
316
  }
@@ -316,7 +318,9 @@ export class JunosRender {
316
318
 
317
319
  /** Every user but root logs in with a class. Root is the platform's own account and takes only its password and keys. */
318
320
  private users(): void {
319
- for (const [name, user] of Object.entries(this.device.users ?? {})) {
321
+ for (const [name, user] of Object.entries(this.device.system?.users ?? {})) {
322
+ if (!user) continue;
323
+
320
324
  const login = name === "root" ? "system root-authentication" : `system login user ${name} authentication`;
321
325
 
322
326
  if (name !== "root") {
@@ -357,7 +361,7 @@ export class JunosRender {
357
361
  ] as const;
358
362
 
359
363
  for (const [service, statement] of others) {
360
- const settings = management[service];
364
+ const settings = this.serviceSettings(service);
361
365
  if (!settings) continue;
362
366
 
363
367
  this.out.set(statement);
@@ -366,9 +370,17 @@ export class JunosRender {
366
370
  }
367
371
  }
368
372
 
373
+ /** A service's settings, by the name of its term in the services filter: SNMP is `monitoring.snmp`, NETCONF `management.native_protocol`. */
374
+ private serviceSettings(service: keyof typeof JunosRender.SERVICE_PORTS): { allow?: readonly string[]; port?: number } | undefined {
375
+ if (service === "snmp") return this.device.monitoring?.snmp;
376
+ if (service === "native") return this.device.management?.native_protocol;
377
+
378
+ return this.device.management?.[service];
379
+ }
380
+
369
381
  /** SNMP version 2c with one read community, version 3 with users in one read-only group, or both. */
370
382
  private snmp(): void {
371
- const snmp = this.device.management?.snmp;
383
+ const snmp = this.device.monitoring?.snmp;
372
384
  if (!snmp) return;
373
385
 
374
386
  if (snmp.contact) this.out.set("snmp contact", JunosRender.quote(snmp.contact));
@@ -391,13 +403,15 @@ export class JunosRender {
391
403
  }
392
404
 
393
405
  private vlans(): void {
394
- for (const [name, vlan] of Object.entries(this.device.vlans ?? {})) {
406
+ for (const [name, vlan] of Object.entries(this.device.switching?.vlans ?? {})) {
407
+ if (!vlan) continue;
408
+
395
409
  this.out.set(`vlans ${name} vlan-id`, vlan.id);
396
410
  if (vlan.description) this.out.set(`vlans ${name} description`, JunosRender.quote(vlan.description));
397
411
  }
398
412
 
399
- for (const [name, iface] of Object.entries(this.device.interfaces ?? {})) {
400
- if (iface.type === "vlan") this.out.set(`vlans ${iface.vlan} l3-interface`, this.names.unit(name));
413
+ for (const [name, iface] of Object.entries(this.device.interfaces?.vlan_interface ?? {})) {
414
+ if (iface) this.out.set(`vlans ${iface.vlan} l3-interface`, this.names.unit(name));
401
415
  }
402
416
  }
403
417
 
@@ -416,7 +430,8 @@ export class JunosRender {
416
430
  const members = new Set([...settings.trunk_vlans, ...(settings.native_vlan ? [settings.native_vlan] : [])]);
417
431
  for (const vlan of members) this.out.set(family, "vlan members", vlan);
418
432
 
419
- if (settings.native_vlan) this.out.set(`interfaces ${physical} native-vlan-id`, this.device.vlans![settings.native_vlan]!.id);
433
+ if (settings.native_vlan)
434
+ this.out.set(`interfaces ${physical} native-vlan-id`, this.device.switching!.vlans![settings.native_vlan]!.id);
420
435
  }
421
436
 
422
437
  const storm = JunosRender.stormProfile(settings);
@@ -439,10 +454,13 @@ export class JunosRender {
439
454
 
440
455
  /** One filter for each set of kinds a port or LAG lets in: those kinds, then nothing else. */
441
456
  private ethertypeFilters(): void {
442
- const holders = [...Object.values(this.device.ports ?? {}), ...Object.values(this.device.interfaces ?? {})];
457
+ const holders: Switched[] = [
458
+ ...Derive.ports(this.device).map(([, settings]) => settings),
459
+ ...this.lagSettings().map(([, lag]) => lag),
460
+ ];
443
461
 
444
462
  for (const settings of holders) {
445
- if (!settings || !("ethertypes" in settings) || !settings.ethertypes) continue;
463
+ if (!settings.ethertypes) continue;
446
464
 
447
465
  const filter = `firewall family ethernet-switching filter ${JunosRender.ethertypeFilter(settings.ethertypes)}`;
448
466
  for (const kind of JunosRender.ETHERTYPES.filter((kind) => settings.ethertypes!.includes(kind)))
@@ -453,7 +471,7 @@ export class JunosRender {
453
471
  }
454
472
 
455
473
  /** Addresses and VRRP groups of a logical interface such as `irb.10`. */
456
- private addressing(unit: string, settings: AnyPort | AnyInterface): void {
474
+ private addressing(unit: string, settings: Port | Interface): void {
457
475
  const [physical, number] = unit.split(".");
458
476
  const prefix = `interfaces ${physical} unit ${number}`;
459
477
  const addresses = (settings.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
@@ -477,7 +495,7 @@ export class JunosRender {
477
495
  /** Every port the model has. One the config does not declare is turned off, and so is a 10G cage's name for the optic it does not hold. */
478
496
  private ports(): void {
479
497
  for (const port of Catalog.ports(this.device.platform, this.device.model)) {
480
- const settings = this.device.ports?.[port as keyof typeof this.device.ports];
498
+ const settings = this.device.interfaces?.ethernet?.[port];
481
499
  const name = this.names.port(port);
482
500
 
483
501
  // A 10G cage is `xe-` with a 10G optic and `ge-` with a 1G one. The name not in use stays off, so a swapped optic does not bring up a port nobody configured.
@@ -491,8 +509,8 @@ export class JunosRender {
491
509
 
492
510
  if (settings.description) this.out.set(`interfaces ${name} description`, JunosRender.quote(settings.description));
493
511
 
494
- if (settings.lag) {
495
- this.out.set(`interfaces ${name} ether-options 802.3ad`, `ae${this.names.lags.get(settings.lag)}`);
512
+ if (settings.in_lag) {
513
+ this.out.set(`interfaces ${name} ether-options 802.3ad`, `ae${this.names.lags.get(settings.in_lag)}`);
496
514
  continue;
497
515
  }
498
516
 
@@ -504,12 +522,15 @@ export class JunosRender {
504
522
  }
505
523
  }
506
524
 
525
+ /** The device's LAGs, each with its settings, in the order written. */
526
+ private lagSettings(): [string, Lag][] {
527
+ return Object.entries(this.device.interfaces?.lag ?? {}).flatMap(([name, lag]) => (lag ? [[name, lag] as [string, Lag]] : []));
528
+ }
529
+
507
530
  private lags(): void {
508
531
  let highest = -1;
509
532
 
510
- for (const [name, lag] of Object.entries(this.device.interfaces ?? {})) {
511
- if (lag.type !== "lag") continue;
512
-
533
+ for (const [name, lag] of this.lagSettings()) {
513
534
  const physical = this.names.physical(name);
514
535
  highest = Math.max(highest, this.names.lags.get(name)!);
515
536
 
@@ -528,8 +549,8 @@ export class JunosRender {
528
549
 
529
550
  /** VLAN interfaces, loopbacks and GRE tunnels: a description, a tunnel's ends, addresses and VRRP, and an MTU per family. */
530
551
  private routedInterfaces(): void {
531
- for (const [name, iface] of Object.entries(this.device.interfaces ?? {})) {
532
- if (iface.type !== "vlan" && iface.type !== "loopback" && iface.type !== "gre") continue;
552
+ for (const { name, kind, settings: iface } of Derive.interfaces(this.device)) {
553
+ if (kind !== "vlan_interface" && kind !== "loopback" && kind !== "gre") continue;
533
554
 
534
555
  const unit = this.names.unit(name);
535
556
  const [physical, number] = unit.split(".");
@@ -537,9 +558,10 @@ export class JunosRender {
537
558
 
538
559
  if (iface.description) this.out.set(`${prefix} description`, JunosRender.quote(iface.description));
539
560
 
540
- if (iface.type === "gre") {
541
- this.out.set(`${prefix} tunnel source`, iface.local);
542
- this.out.set(`${prefix} tunnel destination`, iface.remote);
561
+ const tunnel = kind === "gre" ? this.device.interfaces?.gre?.[name] : undefined;
562
+ if (tunnel) {
563
+ this.out.set(`${prefix} tunnel source`, Address.host(tunnel.local_address));
564
+ this.out.set(`${prefix} tunnel destination`, tunnel.remote_address);
543
565
  }
544
566
 
545
567
  this.addressing(unit, iface);
@@ -555,8 +577,10 @@ export class JunosRender {
555
577
  /** One profile per threshold the ports and LAGs name. */
556
578
  private stormProfiles(): void {
557
579
  const switched: Switched[] = [
558
- ...Object.values(this.device.ports ?? {}).filter((settings): settings is AnyPort => !!settings && !settings.lag),
559
- ...Object.values(this.device.interfaces ?? {}).filter((iface): iface is LagInterface => iface.type === "lag"),
580
+ ...Derive.ports(this.device)
581
+ .filter(([, settings]) => !settings.in_lag)
582
+ .map(([, settings]) => settings),
583
+ ...this.lagSettings().map(([, lag]) => lag),
560
584
  ];
561
585
 
562
586
  for (const settings of switched) {
@@ -578,14 +602,17 @@ export class JunosRender {
578
602
  const management = this.device.management ?? {};
579
603
  const guarded: { service: keyof typeof JunosRender.SERVICE_PORTS; sources: readonly string[] }[] = [];
580
604
 
605
+ // `management.allow` holds addresses by now: an address set's name is turned into its addresses before a platform reads it.
606
+ const allowed = typeof management.allow === "string" ? undefined : management.allow;
607
+
581
608
  for (const service of Object.keys(JunosRender.SERVICE_PORTS) as (keyof typeof JunosRender.SERVICE_PORTS)[]) {
582
- const settings = management[service];
583
- const sources = settings?.allow ?? management.allow;
609
+ const settings = this.serviceSettings(service);
610
+ const sources = settings?.allow ?? allowed;
584
611
  if (settings && sources?.length) guarded.push({ service, sources });
585
612
  }
586
613
 
587
614
  const ntp = this.device.system?.ntp;
588
- const keepsTimeQuietly = !!ntp?.servers.length && !ntp.serve;
615
+ const keepsTimeQuietly = !!ntp?.servers.length && !this.device.services?.ntp_server;
589
616
  if (!guarded.length && !keepsTimeQuietly) return;
590
617
 
591
618
  const families = [
@@ -599,7 +626,7 @@ export class JunosRender {
599
626
 
600
627
  for (const { service, sources } of guarded) {
601
628
  const [protocol, standardPort] = JunosRender.SERVICE_PORTS[service];
602
- const port = management[service]?.port ?? standardPort;
629
+ const port = this.serviceSettings(service)?.port ?? standardPort;
603
630
  const term = `${filter} term ${service}`;
604
631
  const allowed = sources.filter(ofFamily);
605
632
 
@@ -638,9 +665,9 @@ export class JunosRender {
638
665
 
639
666
  const anchors = new Set(["lo0.0", ...[...this.names.loopbacks.values()].map((unit) => `lo0.${unit}`)]);
640
667
 
641
- for (const [port, settings] of Object.entries(this.device.ports ?? {})) {
668
+ for (const [port, settings] of Derive.ports(this.device)) {
642
669
  const name = this.names.port(port);
643
- if (settings?.addresses?.length && name.startsWith("em")) anchors.add(`${name}.0`);
670
+ if (settings.addresses?.length && name.startsWith("em")) anchors.add(`${name}.0`);
644
671
  }
645
672
 
646
673
  for (const anchor of anchors) {
@@ -652,15 +679,15 @@ export class JunosRender {
652
679
 
653
680
  /** A VRF is a virtual router: its own table, holding its interfaces. */
654
681
  private vrfs(): void {
655
- for (const [name, vrf] of Object.entries(this.device.vrfs ?? {})) {
682
+ for (const [name, vrf] of Object.entries(this.device.routing?.vrfs ?? {})) {
683
+ if (!vrf) continue;
684
+
656
685
  this.out.set(`routing-instances ${name} instance-type virtual-router`);
657
686
  if (vrf.description) this.out.set(`routing-instances ${name} description`, JunosRender.quote(vrf.description));
658
687
  }
659
688
 
660
- const routed = [...Object.entries(this.device.ports ?? {}), ...Object.entries(this.device.interfaces ?? {})];
661
-
662
- for (const [name, settings] of routed) {
663
- if (settings?.vrf) this.out.set(`routing-instances ${settings.vrf} interface`, this.names.unit(name));
689
+ for (const [name, settings] of this.routed()) {
690
+ if (settings.vrf) this.out.set(`routing-instances ${settings.vrf} interface`, this.names.unit(name));
664
691
  }
665
692
  }
666
693
 
@@ -668,7 +695,7 @@ export class JunosRender {
668
695
  private staticRoutes(): void {
669
696
  if (this.device.routing?.router_id) this.out.set("routing-options router-id", this.device.routing.router_id);
670
697
 
671
- for (const route of this.device.routing?.static ?? []) {
698
+ for (const route of this.device.routing?.static_routes ?? []) {
672
699
  const ipv6 = Address.family(Address.split(route.prefix).addr) === "ipv6";
673
700
  const table = ipv6 ? ` rib ${route.vrf ? `${route.vrf}.` : ""}inet6.0` : "";
674
701
  const options = route.vrf ? `routing-instances ${route.vrf} routing-options${table}` : `routing-options${table}`;
@@ -680,7 +707,7 @@ export class JunosRender {
680
707
  continue;
681
708
  }
682
709
 
683
- const nextHop = route.via ?? this.names.unit(route.interface!);
710
+ const nextHop = route.next_hop ? Address.host(route.next_hop) : this.names.unit(route.interface!);
684
711
  if (route.distance === undefined) this.out.set(statement, "next-hop", nextHop);
685
712
  else this.out.set(statement, "qualified-next-hop", nextHop, "preference", route.distance);
686
713
 
@@ -691,7 +718,7 @@ export class JunosRender {
691
718
 
692
719
  /** Spanning tree on every switched port and LAG, each with its own settings. RSTP when no mode is given. */
693
720
  private spanningTree(): void {
694
- const stp = this.device.stp;
721
+ const stp = this.device.switching?.stp;
695
722
  if (!stp) return;
696
723
 
697
724
  // The platform has no plain STP; RSTP is held to it.
@@ -701,13 +728,12 @@ export class JunosRender {
701
728
 
702
729
  const switched: [string, Switched][] = [];
703
730
 
704
- for (const [port, settings] of Object.entries(this.device.ports ?? {})) {
705
- if (settings && !settings.lag && (settings.access_vlan || settings.trunk_vlans))
706
- switched.push([this.names.port(port), settings]);
731
+ for (const [port, settings] of Derive.ports(this.device)) {
732
+ if (!settings.in_lag && (settings.access_vlan || settings.trunk_vlans)) switched.push([this.names.port(port), settings]);
707
733
  }
708
734
 
709
- for (const [name, lag] of Object.entries(this.device.interfaces ?? {})) {
710
- if (lag.type === "lag" && (lag.access_vlan || lag.trunk_vlans)) switched.push([this.names.physical(name), lag]);
735
+ for (const [name, lag] of this.lagSettings()) {
736
+ if (lag.access_vlan || lag.trunk_vlans) switched.push([this.names.physical(name), lag]);
711
737
  }
712
738
 
713
739
  for (const [name, settings] of switched) {
@@ -729,7 +755,7 @@ export class JunosRender {
729
755
  * The device sends a port's SNMP index as its port ID unless told to send its name, so a neighbor sees `et-0/0/52` rather than `520`.
730
756
  */
731
757
  private lldp(): void {
732
- const lldp = this.device.lldp;
758
+ const lldp = this.device.switching?.lldp;
733
759
 
734
760
  if (Cabling.runsAnywhere(this.device)) this.out.set("protocols lldp port-id-subtype interface-name");
735
761
 
@@ -737,9 +763,9 @@ export class JunosRender {
737
763
  else if (lldp)
738
764
  for (const name of lldp.interfaces) for (const port of this.names.members(name)) this.out.set("protocols lldp interface", port);
739
765
 
740
- for (const [port, settings] of Object.entries(this.device.ports ?? {})) {
741
- if (settings?.lldp === false) this.out.set("protocols lldp interface", this.names.port(port), "disable");
742
- else if (settings?.lldp === true) this.out.set("protocols lldp interface", this.names.port(port));
766
+ for (const [port, settings] of Derive.ports(this.device)) {
767
+ if (settings.lldp === false) this.out.set("protocols lldp interface", this.names.port(port), "disable");
768
+ else if (settings.lldp === true) this.out.set("protocols lldp interface", this.names.port(port));
743
769
  }
744
770
  }
745
771
 
@@ -748,18 +774,20 @@ export class JunosRender {
748
774
  const ospf = this.device.routing?.ospf;
749
775
  if (!ospf) return;
750
776
 
751
- const protocols = (ospf.families ?? ["ipv4"]).map((family) => (family === "ipv4" ? "ospf" : "ospf3"));
777
+ const protocols = (ospf.address_families ?? ["ipv4"]).map((family) => (family === "ipv4" ? "ospf" : "ospf3"));
752
778
 
753
779
  for (const protocol of protocols) {
754
780
  for (const [areaId, area] of Object.entries(ospf.areas)) {
755
781
  for (const [iface, settings] of Object.entries(area.interfaces)) {
782
+ if (!settings) continue;
783
+
756
784
  const statement = `protocols ${protocol} area ${areaId} interface ${this.names.unit(iface)}`;
757
785
  this.out.set(statement);
758
786
 
759
- if (settings?.cost !== undefined) this.out.set(statement, "metric", settings.cost);
760
- if (settings?.passive) this.out.set(statement, "passive");
761
- if (settings?.network === "point-to-point") this.out.set(statement, "interface-type p2p");
762
- if (settings?.bfd) JunosRender.bfd(this.out, statement, this.device);
787
+ if (settings.cost !== undefined) this.out.set(statement, "metric", settings.cost);
788
+ if (settings.passive) this.out.set(statement, "passive");
789
+ if (settings.network === "point-to-point") this.out.set(statement, "interface-type p2p");
790
+ if (settings.bfd) JunosRender.bfd(this.out, statement, this.device);
763
791
  }
764
792
  }
765
793
 
@@ -775,10 +803,8 @@ export class JunosRender {
775
803
 
776
804
  /** Each relay is a group of servers and the interface it listens on, in the table that interface routes in. */
777
805
  private dhcpRelay(): void {
778
- for (const [name, relay] of Object.entries(this.device.dhcp_relay ?? {})) {
779
- const vrf =
780
- this.device.interfaces?.[relay.interface]?.vrf ??
781
- this.device.ports?.[relay.interface as keyof typeof this.device.ports]?.vrf;
806
+ for (const [name, relay] of Object.entries(this.device.services?.dhcp_relay ?? {})) {
807
+ const vrf = Derive.settings(this.device, relay.interface)?.vrf;
782
808
  const relays = vrf ? `routing-instances ${vrf} forwarding-options dhcp-relay` : "forwarding-options dhcp-relay";
783
809
 
784
810
  for (const server of relay.servers) this.out.set(`${relays} server-group ${name}`, server);
@@ -789,7 +815,7 @@ export class JunosRender {
789
815
 
790
816
  /** sFlow from the management address, to each collector, on every declared port or those listed. A LAG is observed on its member ports. */
791
817
  private sflow(): void {
792
- const flow = this.device.flow_export;
818
+ const flow = this.device.monitoring?.flow_export;
793
819
  if (flow?.protocol !== "sflow") return;
794
820
 
795
821
  if (this.device.connection.host) this.out.set("protocols sflow agent-id", this.device.connection.host);
@@ -802,7 +828,7 @@ export class JunosRender {
802
828
  }
803
829
  }
804
830
 
805
- const declared = Object.entries(this.device.ports ?? {}).filter(([, settings]) => settings && !settings.lag);
831
+ const declared = Derive.ports(this.device).filter(([, settings]) => !settings.in_lag);
806
832
  const observed = flow.interfaces ?? declared.map(([port]) => port);
807
833
 
808
834
  for (const name of observed) {
@@ -818,18 +844,19 @@ export class JunosRender {
818
844
  const switched = new Set<string>();
819
845
  const routed = new Set<string>();
820
846
 
821
- for (const [port, settings] of Object.entries(this.device.ports ?? {})) {
822
- if (!settings?.acl) continue;
847
+ for (const [port, settings] of Derive.ports(this.device)) {
848
+ const acl = settings.ingress_acl;
849
+ if (!acl) continue;
823
850
 
824
851
  const physical = this.names.port(port);
825
852
 
826
853
  if (settings.access_vlan || settings.trunk_vlans) {
827
- switched.add(settings.acl);
828
- this.out.set(`interfaces ${physical} unit 0 family ethernet-switching filter input`, settings.acl);
854
+ switched.add(acl);
855
+ this.out.set(`interfaces ${physical} unit 0 family ethernet-switching filter input`, acl);
829
856
  } else {
830
- routed.add(settings.acl);
831
- this.out.set(`interfaces ${physical} unit 0 family inet filter input`, `${settings.acl}-v4`);
832
- this.out.set(`interfaces ${physical} unit 0 family inet6 filter input`, `${settings.acl}-v6`);
857
+ routed.add(acl);
858
+ this.out.set(`interfaces ${physical} unit 0 family inet filter input`, `${acl}-v4`);
859
+ this.out.set(`interfaces ${physical} unit 0 family inet6 filter input`, `${acl}-v6`);
833
860
  }
834
861
  }
835
862
 
@@ -842,8 +869,8 @@ export class JunosRender {
842
869
  }
843
870
 
844
871
  /** A rule's decision, through a policer when it has a rate. */
845
- private aclThen(term: string, policer: string, rule: AnyAclRule): void {
846
- if (rule.action === "drop") {
872
+ private aclThen(term: string, policer: string, rule: AclRule): void {
873
+ if (rule.then === "drop") {
847
874
  this.out.set(term, "then discard");
848
875
  return;
849
876
  }
@@ -861,8 +888,8 @@ export class JunosRender {
861
888
  private switchingFilter(acl: string): void {
862
889
  const filter = `firewall family ethernet-switching filter ${acl}`;
863
890
 
864
- (this.device.acls?.[acl] ?? []).forEach((rule, index) => {
865
- const match = rule.match ?? {};
891
+ (this.device.switching?.acls?.[acl] ?? []).forEach((rule, index) => {
892
+ const match = rule.if ?? {};
866
893
  const addressed = match.src || match.dst || match.protocol || match.src_port !== undefined || match.dst_port !== undefined;
867
894
  const family = JunosRender.aclFamily(rule);
868
895
  const families = addressed || family ? (family ? [family] : (["ipv4", "ipv6"] as const)) : [undefined];
@@ -875,8 +902,10 @@ export class JunosRender {
875
902
  const version = `from ip-version ${each}`;
876
903
  const v4 = each === "ipv4";
877
904
 
878
- if (match.src) this.out.set(term, version, v4 ? "ip-source-address" : "ip6-source-address", match.src);
879
- if (match.dst) this.out.set(term, version, v4 ? "ip-destination-address" : "ip6-destination-address", match.dst);
905
+ for (const src of JunosRender.each(match.src))
906
+ this.out.set(term, version, v4 ? "ip-source-address" : "ip6-source-address", src);
907
+ for (const dst of JunosRender.each(match.dst))
908
+ this.out.set(term, version, v4 ? "ip-destination-address" : "ip6-destination-address", dst);
880
909
  if (match.protocol)
881
910
  this.out.set(
882
911
  term,
@@ -902,17 +931,17 @@ export class JunosRender {
902
931
  const name = `${acl}-${family === "ipv4" ? "v4" : "v6"}`;
903
932
  const filter = `firewall family ${family === "ipv4" ? "inet" : "inet6"} filter ${name}`;
904
933
 
905
- (this.device.acls?.[acl] ?? []).forEach((rule, index) => {
934
+ (this.device.switching?.acls?.[acl] ?? []).forEach((rule, index) => {
906
935
  const ruleFamily = JunosRender.aclFamily(rule);
907
936
  if (ruleFamily && ruleFamily !== family) return;
908
937
 
909
- const match = rule.match ?? {};
938
+ const match = rule.if ?? {};
910
939
  const label = `${index + 1}`;
911
940
  const term = `${filter} term ${JunosRender.ruleName(label, rule.description)}`;
912
941
  const protocolWord = family === "ipv4" ? "protocol" : "next-header";
913
942
 
914
- if (match.src) this.out.set(term, "from source-address", match.src);
915
- if (match.dst) this.out.set(term, "from destination-address", match.dst);
943
+ for (const src of JunosRender.each(match.src)) this.out.set(term, "from source-address", src);
944
+ for (const dst of JunosRender.each(match.dst)) this.out.set(term, "from destination-address", dst);
916
945
  if (match.protocol)
917
946
  this.out.set(term, "from", protocolWord, family === "ipv4" ? match.protocol : JunosRender.nextHeader(match.protocol));
918
947
  for (const port of match.src_port !== undefined ? JunosRender.eachPort(match.src_port) : [])
@@ -928,7 +957,7 @@ export class JunosRender {
928
957
 
929
958
  /** A DHCP server is an address pool with its network, range, options and reservations, served on its interface. */
930
959
  private dhcpServers(): void {
931
- for (const [name, server] of Object.entries(this.device.dhcp ?? {})) {
960
+ for (const [name, server] of Object.entries(this.device.services?.dhcp_server ?? {})) {
932
961
  this.out.set(`system services dhcp-local-server group ${name} interface`, this.names.unit(server.interface));
933
962
 
934
963
  const pool = `access address-assignment pool ${name} family inet`;
@@ -952,10 +981,8 @@ export class JunosRender {
952
981
 
953
982
  /** Router advertisements announce each IPv6 network of the interface. */
954
983
  private routerAdvertisements(): void {
955
- const routed = [...Object.entries(this.device.ports ?? {}), ...Object.entries(this.device.interfaces ?? {})];
956
-
957
- for (const [name, settings] of routed) {
958
- if (!settings?.ipv6_ra) continue;
984
+ for (const [name, settings] of this.routed()) {
985
+ if (!settings.ipv6_ra) continue;
959
986
 
960
987
  const addresses = (settings.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
961
988
 
@@ -964,4 +991,12 @@ export class JunosRender {
964
991
  }
965
992
  }
966
993
  }
994
+
995
+ /** Every port, then every other interface kind by kind, each with its settings. */
996
+ private routed(): [string, Port | Interface][] {
997
+ return [
998
+ ...Derive.ports(this.device),
999
+ ...Derive.interfaces(this.device).map(({ name, settings }): [string, Interface] => [name, settings]),
1000
+ ];
1001
+ }
967
1002
  }