@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
@@ -22,7 +22,7 @@ Behaviour that looks like something else. Each one has been hit on a CE6855 runn
22
22
  - `vlan batch` has every VLAN on one line, and undoing a list that includes a VLAN with an L3 interface is refused. A change sends only the VLANs added and `undo vlan batch` for the ones removed.
23
23
  - A VLAN name is unique across VLANs: a new VLAN cannot take a name another still has. VLANs no longer used, and their VLAN interfaces, are removed first, in the same candidate.
24
24
  - `undo ssh server key-exchange <algorithms>` is refused with "Too many parameters"; `undo ssh server key-exchange` takes none. `hmac` was proved the same way. `cipher` is undone the same way but was not proved on its own.
25
- - A console on a password only (`passwordOnly`) shows `Please Press ENTER.` after a logout or a refused password, and `Error: The password is invalid.` for a wrong one.
25
+ - A console on a password only (`password_only`) shows `Please Press ENTER.` after a logout or a refused password, and `Error: The password is invalid.` for a wrong one.
26
26
  - Once SNMP is on, the switch prints an engine ID of its own, `snmp-agent local-engineid`, derived from its MAC, and prints each SNMPv3 user as a bare `snmp-agent usm-user v3 <name>` line before its settings. Planning against either as unrendered would undo the user; seen on a CE6855 on V200R005C00SPC800.
27
27
  - `apply --secrets` sends every line holding a secret again, the local users' passwords included, and a password among the user's last ten is refused, which discards the whole candidate. Send it only when a password changed.
28
28
  - An SNMPv3 key prints as `cipher %^%#...%^%#`, which can be turned back into the key. The read redacts every value after `cipher`, as it does a password.
@@ -6,7 +6,7 @@ import type { Client, ClientChannel } from "ssh2";
6
6
  import { Address } from "../../core/address";
7
7
  import type { Neighbor } from "../../core/cabling";
8
8
  import { Secrets } from "../../core/secrets";
9
- import type { Device } from "../../schema";
9
+ import type { Device, Lag, VlanInterface } from "../../model";
10
10
  import { ConsoleShell, SerialConsole, type ConsoleDialect } from "../../transport/console";
11
11
  import { Ssh } from "../../transport/ssh";
12
12
  import { Catalog } from "../catalog";
@@ -106,46 +106,53 @@ export class Vrp extends Platform {
106
106
 
107
107
  if (device.management?.interfaces) found.push("management.interfaces, which only FortiOS builds for now");
108
108
 
109
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
110
- if (iface.type === "bridge") found.push(`interfaces.${name}, a bridge, which only FortiOS builds for now`);
111
- if (iface.type === "vlan" && iface.mac) found.push(`interfaces.${name}.mac, which only RouterOS sets for now`);
112
- }
109
+ const interfaces = Derive.interfaces(device);
113
110
 
114
- for (const [name, port] of Object.entries(device.ports ?? {})) {
115
- if (!port) continue;
111
+ for (const { name, kind, settings } of interfaces) {
112
+ if (kind === "bridge") found.push(`interfaces.bridge.${name}, a bridge, which only FortiOS builds for now`);
113
+ if (kind === "vlan_interface" && (settings as VlanInterface).mac)
114
+ found.push(`interfaces.vlan_interface.${name}.mac, which only RouterOS sets for now`);
115
+ }
116
116
 
117
- for (const field of ["speed", "acl", "mtu", "addresses", "vrrp", "vrf"] as const) {
118
- if (port[field] !== undefined) found.push(`ports.${name}.${field}, ports are switched; use a VLAN interface`);
117
+ for (const [name, port] of Derive.ports(device)) {
118
+ for (const field of ["speed", "ingress_acl", "mtu", "addresses", "vrrp", "vrf", "ipv6_ra", "hardware_offload"] as const) {
119
+ if (port[field] !== undefined) found.push(`interfaces.ethernet.${name}.${field}, ports are switched; use a VLAN interface`);
119
120
  }
120
121
 
121
122
  for (const field of ["mac_limit", "ethertypes"] as const) {
122
- if (port[field] !== undefined && port.lag) found.push(`ports.${name}.${field} on a LAG member, set it on the LAG`);
123
+ if (port[field] !== undefined && port.in_lag)
124
+ found.push(`interfaces.ethernet.${name}.${field} on a LAG member, set it on the LAG`);
123
125
  }
124
126
 
125
- if (port.lldp === true && !device.lldp) found.push(`ports.${name}.lldp true while LLDP is off on the device`);
127
+ if (port.lldp === true && !device.switching?.lldp)
128
+ found.push(`interfaces.ethernet.${name}.lldp true while LLDP is off on the device`);
126
129
  const storm = Object.values(port.storm_control ?? {});
127
- if (storm.some((threshold) => "pps" in threshold)) found.push(`ports.${name}.storm_control in pps, the platform takes percent`);
130
+ if (storm.some((threshold) => "pps" in threshold))
131
+ found.push(`interfaces.ethernet.${name}.storm_control in pps, the platform takes percent`);
128
132
  }
129
133
 
130
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
131
- if (iface.type !== "vlan" && iface.type !== "lag") found.push(`interfaces.${name}, a ${iface.type}`);
132
- if (iface.type === "lag" && iface.addresses?.length) found.push(`interfaces.${name}.addresses, LAGs are switched`);
133
- if (iface.type === "lag" && iface.mac_limit !== undefined) found.push(`interfaces.${name}.mac_limit, on a LAG is not proved`);
134
- if (iface.type === "lag" && iface.ethertypes) found.push(`interfaces.${name}.ethertypes, on a LAG is not proved`);
135
- if (iface.mtu !== undefined) found.push(`interfaces.${name}.mtu`);
136
- if (iface.ipv6_ra) found.push(`interfaces.${name}.ipv6_ra`);
137
- for (const group of iface.vrrp ?? [])
138
- if (Address.family(group.address) === "ipv6") found.push(`interfaces.${name}.vrrp for IPv6`);
134
+ for (const { name, kind, settings } of interfaces) {
135
+ const where = `interfaces.${kind}.${name}`;
136
+ if (kind !== "vlan_interface" && kind !== "lag") found.push(`${where}, a ${kind}`);
137
+ if (kind === "lag" && settings.addresses?.length) found.push(`${where}.addresses, LAGs are switched`);
138
+ if (kind === "lag" && (settings as Lag).mac_limit !== undefined) found.push(`${where}.mac_limit, on a LAG is not proved`);
139
+ if (kind === "lag" && (settings as Lag).ethertypes) found.push(`${where}.ethertypes, on a LAG is not proved`);
140
+ if (settings.mtu !== undefined) found.push(`${where}.mtu`);
141
+ if (settings.ipv6_ra) found.push(`${where}.ipv6_ra`);
142
+ if (settings.hardware_offload !== undefined) found.push(`${where}.hardware_offload, the switch chip forwards everything`);
143
+ for (const group of settings.vrrp ?? []) if (Address.family(group.address) === "ipv6") found.push(`${where}.vrrp for IPv6`);
139
144
  }
140
145
 
141
- for (const [name, vrf] of Object.entries(device.vrfs ?? {})) {
142
- if (vrf.description) found.push(`vrfs.${name}.description`);
146
+ const routing = device.routing;
147
+
148
+ for (const [name, vrf] of Object.entries(routing?.vrfs ?? {})) {
149
+ if (vrf?.description) found.push(`routing.vrfs.${name}.description`);
143
150
  }
144
151
 
145
- const routing = device.routing;
146
152
  if (routing?.bgp) found.push("routing.bgp");
147
153
  if (routing?.rpki) found.push("routing.rpki");
148
- if (routing?.ospf?.families?.includes("ipv6")) found.push("routing.ospf for IPv6");
154
+ if (routing?.ospf?.address_families?.includes("ipv6")) found.push("routing.ospf for IPv6");
155
+ if (routing?.router_id && !routing.ospf) found.push("routing.router_id without routing.ospf, only OSPF uses it here");
149
156
 
150
157
  const bfd = routing?.bfd;
151
158
  if (bfd && !(Number.isInteger(bfd.interval) && bfd.interval >= 50 && bfd.interval <= 1000))
@@ -153,46 +160,59 @@ export class Vrp extends Platform {
153
160
  if (bfd && !(Number.isInteger(bfd.multiplier) && bfd.multiplier >= 3 && bfd.multiplier <= 50))
154
161
  found.push("routing.bfd.multiplier, between 3 and 50 on the platform");
155
162
 
156
- for (const route of routing?.static ?? []) {
163
+ for (const route of routing?.static_routes ?? []) {
157
164
  if (!route.bfd) continue;
158
165
 
159
- const where = `routing.static ${route.prefix} bfd`;
166
+ const where = `routing.static_routes ${route.prefix} bfd`;
167
+ const nextHop = route.next_hop && Address.host(route.next_hop);
160
168
  if (route.vrf) found.push(`${where} in a VRF, not proved`);
161
- else if (!route.via || Address.family(route.via) !== "ipv4") found.push(`${where}, needs an IPv4 via`);
162
- else if (!VrpRender.localAddressTo(device, route.via))
163
- found.push(`${where}, the device has no address on the network of ${route.via}`);
169
+ else if (!nextHop || Address.family(nextHop) !== "ipv4") found.push(`${where}, needs an IPv4 next_hop`);
170
+ else if (!VrpRender.localAddressTo(device, nextHop))
171
+ found.push(`${where}, the device has no address on the network of ${nextHop}`);
164
172
  }
165
173
 
166
- for (const [index, route] of (routing?.static ?? []).entries()) {
174
+ for (const [index, route] of (routing?.static_routes ?? []).entries()) {
167
175
  if (route.description && Address.family(Address.split(route.prefix).addr) === "ipv6")
168
- found.push(`routing.static[${index}].description for IPv6`);
176
+ found.push(`routing.static_routes[${index}].description for IPv6`);
169
177
  }
170
178
 
171
- for (const field of ["firewall", "dhcp", "acls", "policies", "prefix_sets", "hardware_offload", "certificates"] as const) {
172
- if (device[field] !== undefined) found.push(field);
179
+ const refusedGroups = [
180
+ ["firewall", device.firewall],
181
+ ["services.dhcp_server", device.services?.dhcp_server],
182
+ ["switching.acls", device.switching?.acls],
183
+ ["routing.policies", routing?.policies],
184
+ ["routing.prefix_sets", routing?.prefix_sets],
185
+ ["system.hardware_offload", device.system?.hardware_offload],
186
+ ["system.certificates", device.system?.certificates],
187
+ ] as const;
188
+
189
+ for (const [path, value] of refusedGroups) {
190
+ if (value !== undefined) found.push(path);
173
191
  }
174
192
 
175
- for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
176
- if (device.interfaces?.[relay.interface]?.type !== "vlan")
177
- found.push(`dhcp_relay.${name}.interface, relays run on VLAN interfaces`);
193
+ for (const [name, relay] of Object.entries(device.services?.dhcp_relay ?? {})) {
194
+ if (!device.interfaces?.vlan_interface?.[relay.interface])
195
+ found.push(`services.dhcp_relay.${name}.interface, relays run on VLAN interfaces`);
178
196
  }
179
197
 
180
198
  // The platform reserves this name.
181
- if (device.users && "admin" in device.users) found.push("users.admin, the platform reserves the name");
199
+ if (device.system?.users?.admin) found.push("system.users.admin, the platform reserves the name");
182
200
 
183
201
  const management = device.management ?? {};
184
202
 
185
- for (const field of ["http", "https", "api", "api_tls", "ftp", "native", "privilege_password"] as const) {
203
+ for (const field of ["http", "https", "api", "api_tls", "ftp", "native_protocol", "privilege_password"] as const) {
186
204
  if (management[field]) found.push(`management.${field}`);
187
205
  }
188
206
 
189
207
  if (management.ssh?.auth_timeout || management.ssh?.auth_retries) found.push("management.ssh.auth_timeout and auth_retries");
190
208
 
191
- if (device.lldp && device.lldp !== true) found.push("lldp.interfaces, use lldp on the ports instead");
209
+ const lldp = device.switching?.lldp;
210
+ if (lldp && lldp !== true) found.push("switching.lldp.interfaces, use lldp on the ports instead");
192
211
 
193
212
  const system = device.system ?? {};
194
213
 
195
214
  if (system.dns || system.release_channel || system.banner) found.push("system.dns, release_channel and banner");
215
+ if (device.services?.dns_server) found.push("services.dns_server");
196
216
 
197
217
  const local = system.logging?.local;
198
218
  if (local?.lines_per_file !== undefined)
@@ -200,27 +220,31 @@ export class Vrp extends Platform {
200
220
  if (local?.files !== undefined && !(Number.isInteger(local.files) && local.files >= 3 && local.files <= 500))
201
221
  found.push("system.logging.local.files, between 3 and 500 on the platform");
202
222
 
203
- for (const collector of system.logging?.remote ?? []) {
204
- if (Address.family(collector.address) === "ipv6") found.push(`system.logging.remote ${collector.address}, IPv6 is not proved`);
223
+ const monitoring = device.monitoring ?? {};
224
+
225
+ for (const collector of monitoring.syslog ?? []) {
226
+ if (Address.family(collector.address) === "ipv6") found.push(`monitoring.syslog ${collector.address}, IPv6 is not proved`);
205
227
  }
206
228
 
207
- for (const source of management.snmp?.allow ?? []) {
208
- if (Address.family(Address.split(source).addr) === "ipv6") found.push(`management.snmp.allow ${source}, IPv6 is not proved`);
229
+ for (const source of monitoring.snmp?.allow ?? []) {
230
+ if (Address.family(Address.split(source).addr) === "ipv6") found.push(`monitoring.snmp.allow ${source}, IPv6 is not proved`);
209
231
  }
210
232
 
211
- if (system.transceiver_monitoring !== undefined) found.push("system.transceiver_monitoring");
233
+ if (monitoring.transceivers !== undefined) found.push("monitoring.transceivers");
212
234
 
213
235
  // SNMP takes sources of its own here, in an ACL of its own.
214
- found.push(...Derive.switchUnsupported(device).filter((field) => !field.startsWith("management.snmp.allow")));
236
+ found.push(...Derive.switchUnsupported(device, ["snmp"]));
215
237
  found.push(...Derive.unproved(device, ["alarm-severity", "bfd-timers", "static-route-bfd", "mac-limit", "ethertype-filter"]));
216
238
 
217
- const flow = device.flow_export;
218
- if (flow && flow.protocol !== "sflow") found.push(`flow_export.protocol ${flow.protocol}, the platform exports sFlow`);
239
+ const flow = monitoring.flow_export;
240
+ if (flow && flow.protocol !== "sflow") found.push(`monitoring.flow_export.protocol ${flow.protocol}, the platform exports sFlow`);
219
241
  const sampling = flow?.protocol === "sflow" ? flow.sampling : undefined;
220
242
  const samplingInRange = sampling !== undefined && Number.isInteger(sampling) && sampling >= 4096 && sampling <= 4_294_967_295;
221
243
 
222
244
  if (flow?.protocol === "sflow" && !samplingInRange) {
223
- found.push("flow_export.sampling, required and between 4096 and 4294967295: the platform samples, one packet in 4096 at most");
245
+ found.push(
246
+ "monitoring.flow_export.sampling, required and between 4096 and 4294967295: the platform samples, one packet in 4096 at most"
247
+ );
224
248
  }
225
249
 
226
250
  return found;
@@ -565,7 +589,7 @@ export class Vrp extends Platform {
565
589
  const opened = await SerialConsole.open(console);
566
590
 
567
591
  const login = async () => {
568
- await SerialConsole.login(opened.channel, Vrp.CONSOLE, user, password, console.passwordOnly);
592
+ await SerialConsole.login(opened.channel, Vrp.CONSOLE, user, password, console.password_only);
569
593
  await Ssh.send(opened.channel, "screen-length 0 temporary", 10_000, 1500);
570
594
  };
571
595
 
@@ -3,14 +3,10 @@
3
3
  import { Address } from "../../core/address";
4
4
  import { Secrets } from "../../core/secrets";
5
5
  import { Utils } from "../../core/utils";
6
- import type { Device, Interface, Port, Vrrp } from "../../schema";
6
+ import type { Device, Lag, Port, VlanInterface, Vrrp } from "../../model";
7
7
  import { Catalog } from "../catalog";
8
8
  import { Derive } from "../derive";
9
9
 
10
- type AnyPort = Port<string, string, string, string>;
11
- type AnyInterface = Interface<string, string>;
12
- type LagInterface = Extract<AnyInterface, { type: "lag" }>;
13
-
14
10
  /** Builds the configuration as blocks: a header line, its indented lines, then `#`. */
15
11
  export class VrpRender {
16
12
  /** The ACL that guards every management service. */
@@ -66,19 +62,21 @@ export class VrpRender {
66
62
  private static readonly ETHERTYPE = { ipv4: "ip", ipv6: "ipv6", arp: "arp" } as const;
67
63
 
68
64
  private readonly lines: string[] = [];
69
- private readonly vlans: NonNullable<Device["vlans"]>;
70
- private readonly interfaces: NonNullable<Device["interfaces"]>;
65
+ private readonly vlans: NonNullable<NonNullable<Device["switching"]>["vlans"]>;
66
+ private readonly lags: NonNullable<NonNullable<Device["interfaces"]>["lag"]>;
67
+ private readonly vlanInterfaces: NonNullable<NonNullable<Device["interfaces"]>["vlan_interface"]>;
71
68
  private readonly lagNumbers: Map<string, number>;
72
69
  /** The layer 2 ACL of each set of ethertypes the ports take, by the set written as `ipv4 ipv6 arp`. */
73
70
  private readonly ethertypeAcls = new Map<string, number>();
74
71
 
75
72
  private constructor(private readonly device: Device) {
76
- this.vlans = device.vlans ?? {};
77
- this.interfaces = device.interfaces ?? {};
73
+ this.vlans = device.switching?.vlans ?? {};
74
+ this.lags = device.interfaces?.lag ?? {};
75
+ this.vlanInterfaces = device.interfaces?.vlan_interface ?? {};
78
76
  this.lagNumbers = Derive.lagIds(device);
79
77
 
80
- for (const port of Object.values(device.ports ?? {})) {
81
- const key = port?.ethertypes?.join(" ");
78
+ for (const [, port] of Derive.ports(device)) {
79
+ const key = port.ethertypes?.join(" ");
82
80
  if (key && !this.ethertypeAcls.has(key)) this.ethertypeAcls.set(key, VrpRender.FIRST_L2_ACL + this.ethertypeAcls.size);
83
81
  }
84
82
  }
@@ -89,6 +87,13 @@ export class VrpRender {
89
87
  }
90
88
 
91
89
  /** `99 999 to 1000 2000`, the platform's compression of a VLAN list. */
90
+ /** `management.allow` as a list. The project turns an address set named there into its addresses before rendering. */
91
+ static managementAllow(device: Device): readonly string[] {
92
+ const allow = device.management?.allow ?? [];
93
+ if (typeof allow === "string") throw new Error(`management.allow names the address set ${allow}, not its addresses`);
94
+ return allow;
95
+ }
96
+
92
97
  static vlanBatch(ids: number[]): string {
93
98
  const sorted = [...new Set(ids)].sort((a, b) => a - b);
94
99
  const ranges: string[] = [];
@@ -114,23 +119,23 @@ export class VrpRender {
114
119
  this.users();
115
120
  this.vrfs();
116
121
 
117
- for (const [name, lag] of Object.entries(this.interfaces)) {
118
- if (lag.type === "lag") this.block(`interface ${this.nameOf(name)}`, [...VrpRender.lagMode(lag), ...this.switching(lag)]);
122
+ for (const [name, lag] of Object.entries(this.lags)) {
123
+ if (lag) this.block(`interface ${this.nameOf(name)}`, [...VrpRender.lagMode(lag), ...this.switching(lag)]);
119
124
  }
120
125
 
121
- for (const [name, iface] of Object.entries(this.interfaces)) {
122
- if (iface.type === "vlan") this.block(`interface ${this.nameOf(name)}`, this.vlanInterface(name, iface));
126
+ for (const [name, iface] of Object.entries(this.vlanInterfaces)) {
127
+ if (iface) this.block(`interface ${this.nameOf(name)}`, this.vlanInterface(name, iface));
123
128
  }
124
129
 
125
130
  for (const port of Catalog.ports(device.platform, device.model)) {
126
- const settings = device.ports?.[port as keyof typeof device.ports];
127
- this.block(`interface ${this.nameOf(port)}`, this.physicalPort(port, settings));
131
+ this.block(`interface ${this.nameOf(port)}`, this.physicalPort(port, device.interfaces?.ethernet?.[port]));
128
132
  }
129
133
 
130
134
  this.staticRoutes();
131
135
  this.ospf();
132
136
  if (this.wantsBfd()) this.block("bfd", []);
133
- if (device.dhcp_relay && Object.keys(device.dhcp_relay).length) {
137
+ const relays = device.services?.dhcp_relay;
138
+ if (relays && Object.keys(relays).length) {
134
139
  this.line("dhcp enable");
135
140
  this.separator();
136
141
  }
@@ -167,10 +172,10 @@ export class VrpRender {
167
172
 
168
173
  /** What the platform calls a port, a LAG or a VLAN interface. */
169
174
  private nameOf(portOrInterface: string): string {
170
- const iface = this.interfaces[portOrInterface];
171
- if (iface?.type === "lag") return `Eth-Trunk${this.lagNumbers.get(portOrInterface)}`;
175
+ if (this.lags[portOrInterface]) return `Eth-Trunk${this.lagNumbers.get(portOrInterface)}`;
172
176
 
173
- if (iface?.type === "vlan") return `Vlanif${this.vlanId(iface.vlan)}`;
177
+ const vlanInterface = this.vlanInterfaces[portOrInterface];
178
+ if (vlanInterface) return `Vlanif${this.vlanId(vlanInterface.vlan)}`;
174
179
 
175
180
  return Catalog.spell(this.device.platform, this.device.model, portOrInterface);
176
181
  }
@@ -192,7 +197,7 @@ export class VrpRender {
192
197
  this.separator();
193
198
  }
194
199
 
195
- const stp = device.stp;
200
+ const stp = device.switching?.stp;
196
201
  if (!stp) this.line("stp disable");
197
202
  else {
198
203
  // MSTP is the platform's default and is not written.
@@ -200,19 +205,19 @@ export class VrpRender {
200
205
  if (stp.priority !== undefined) this.line(`stp priority ${stp.priority}`);
201
206
  }
202
207
  // LLDP is off by default and turned on by the config.
203
- if (device.lldp) this.line("lldp enable");
208
+ if (device.switching?.lldp) this.line("lldp enable");
204
209
  this.separator();
205
210
 
206
- const ntp = device.system?.ntp;
207
- if (!ntp?.serve) {
211
+ if (!device.services?.ntp_server) {
208
212
  this.line("ntp server disable");
209
213
  this.line("ntp ipv6 server disable");
210
214
  }
211
215
 
212
- for (const server of ntp?.servers ?? []) this.line(`ntp unicast-server ${server}`);
216
+ // A server written with its length is written as the address alone.
217
+ for (const server of device.system?.ntp?.servers ?? []) this.line(`ntp unicast-server ${Address.host(server)}`);
213
218
  this.separator();
214
219
 
215
- const ids = Object.values(device.vlans ?? {}).map((vlan) => vlan.id);
220
+ const ids = Object.values(this.vlans).flatMap((vlan) => (vlan ? [vlan.id] : []));
216
221
  if (ids.length) {
217
222
  this.line(`vlan batch ${VrpRender.vlanBatch(ids)}`);
218
223
  this.separator();
@@ -226,14 +231,15 @@ export class VrpRender {
226
231
  }
227
232
 
228
233
  private vlanBlocks(): void {
229
- for (const [name, vlan] of Object.entries(this.device.vlans ?? {})) {
234
+ for (const [name, vlan] of Object.entries(this.vlans)) {
235
+ if (!vlan) continue;
230
236
  this.block(`vlan ${vlan.id}`, [`name ${name}`, ...(vlan.description ? [`description ${vlan.description}`] : [])]);
231
237
  }
232
238
  }
233
239
 
234
240
  /** One ACL guards every management service: a permit per source, then an explicit deny. */
235
241
  private managementAcl(): void {
236
- const allow = this.device.management?.allow ?? [];
242
+ const allow = VrpRender.managementAllow(this.device);
237
243
  if (!allow.length) return;
238
244
 
239
245
  this.block(`acl number ${VrpRender.MANAGEMENT_ACL}`, ["description Management access", ...VrpRender.aclRules(allow)]);
@@ -249,7 +255,7 @@ export class VrpRender {
249
255
  }
250
256
 
251
257
  private snmpAcl(): void {
252
- const allow = this.device.management?.snmp?.allow ?? [];
258
+ const allow = this.device.monitoring?.snmp?.allow ?? [];
253
259
  if (!allow.length) return;
254
260
 
255
261
  this.block(`acl number ${VrpRender.SNMP_ACL}`, ["description SNMP access", ...VrpRender.aclRules(allow)]);
@@ -269,7 +275,8 @@ export class VrpRender {
269
275
  private users(): void {
270
276
  const lines: string[] = [];
271
277
 
272
- for (const [name, user] of Object.entries(this.device.users ?? {})) {
278
+ for (const [name, user] of Object.entries(this.device.system?.users ?? {})) {
279
+ if (!user) continue;
273
280
  lines.push(`local-user ${name} password irreversible-cipher ${Secrets.marker(user.password)}`);
274
281
  lines.push(`local-user ${name} service-type ssh`);
275
282
  lines.push(`local-user ${name} level ${VrpRender.USER_LEVEL[user.role]}`);
@@ -279,17 +286,19 @@ export class VrpRender {
279
286
  }
280
287
 
281
288
  private vrfs(): void {
282
- for (const name of Object.keys(this.device.vrfs ?? {})) this.block(`ip vpn-instance ${name}`, ["ipv4-family"]);
289
+ for (const [name, vrf] of Object.entries(this.device.routing?.vrfs ?? {})) {
290
+ if (vrf) this.block(`ip vpn-instance ${name}`, ["ipv4-family"]);
291
+ }
283
292
  }
284
293
 
285
- private static lagMode(lag: LagInterface): string[] {
294
+ private static lagMode(lag: Lag): string[] {
286
295
  const lines = [lag.mode === "static" ? "mode manual load-balance" : "mode lacp-static"];
287
296
  if (lag.description) lines.unshift(`description ${lag.description}`);
288
297
  return lines;
289
298
  }
290
299
 
291
300
  /** Layer 2 membership of a port or LAG. */
292
- private switching(settings: AnyPort | LagInterface): string[] {
301
+ private switching(settings: Port | Lag): string[] {
293
302
  const lines: string[] = [];
294
303
  if (settings.access_vlan) lines.push(`port default vlan ${this.vlanId(settings.access_vlan)}`);
295
304
  if (settings.trunk_vlans) {
@@ -340,7 +349,7 @@ export class VrpRender {
340
349
  return lines;
341
350
  }
342
351
 
343
- private vlanInterface(name: string, iface: Extract<AnyInterface, { type: "vlan" }>): string[] {
352
+ private vlanInterface(name: string, iface: VlanInterface): string[] {
344
353
  const device = this.device;
345
354
  const lines: string[] = [];
346
355
  if (iface.description) lines.push(`description ${iface.description}`);
@@ -356,14 +365,14 @@ export class VrpRender {
356
365
 
357
366
  for (const group of iface.vrrp ?? []) lines.push(...VrpRender.vrrpLines(group));
358
367
 
359
- const relays = Object.values(device.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
368
+ const relays = Object.values(device.services?.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
360
369
  if (relays.length) {
361
370
  lines.push("dhcp select relay");
362
371
  for (const server of relays.flatMap((relay) => relay.servers)) lines.push(`dhcp relay binding server ip ${server}`);
363
372
  }
364
373
 
365
374
  for (const area of Object.values(device.routing?.ospf?.areas ?? {})) {
366
- const settings = area.interfaces[name as keyof typeof area.interfaces];
375
+ const settings = area.interfaces[name];
367
376
  if (!settings) continue;
368
377
 
369
378
  if (settings.cost !== undefined) lines.push(`ospf cost ${settings.cost}`);
@@ -375,12 +384,12 @@ export class VrpRender {
375
384
  }
376
385
 
377
386
  /** A port the config does not declare is shut down. A LAG member holds only its membership. */
378
- private physicalPort(port: string, settings: AnyPort | undefined): string[] {
387
+ private physicalPort(port: string, settings: Port | undefined): string[] {
379
388
  if (!settings) return ["shutdown"];
380
389
 
381
390
  // A port joins a LAG only while it holds no other setting, so membership comes before its description.
382
- if (settings.lag) {
383
- const membership = `eth-trunk ${this.lagNumbers.get(settings.lag)}`;
391
+ if (settings.in_lag) {
392
+ const membership = `eth-trunk ${this.lagNumbers.get(settings.in_lag)}`;
384
393
  return settings.description ? [membership, `description ${settings.description}`] : [membership];
385
394
  }
386
395
 
@@ -391,7 +400,7 @@ export class VrpRender {
391
400
  if (settings.mac_limit !== undefined) lines.push(`mac-address limit maximum ${settings.mac_limit}`);
392
401
  if (settings.ethertypes) lines.push(`traffic-filter acl ${this.ethertypeAcls.get(settings.ethertypes.join(" "))} inbound`);
393
402
 
394
- const flow = this.device.flow_export;
403
+ const flow = this.device.monitoring?.flow_export;
395
404
  const observed = flow?.protocol === "sflow" && (!flow.interfaces || flow.interfaces.includes(port));
396
405
  if (observed) {
397
406
  for (let index = 0; index < flow!.collectors.length; index++)
@@ -407,8 +416,9 @@ export class VrpRender {
407
416
  * Static routes. A route watched by BFD needs a BFD template for its next hop first, from the device's own address on that network, with the device's timers.
408
417
  */
409
418
  private staticRoutes(): void {
410
- const routes = this.device.routing?.static ?? [];
411
- const watched = [...new Set(routes.filter((route) => route.bfd && route.via).map((route) => route.via!))];
419
+ const routes = this.device.routing?.static_routes ?? [];
420
+ // A next hop written with its length is written as the address alone.
421
+ const watched = [...new Set(routes.filter((route) => route.bfd && route.next_hop).map((route) => Address.host(route.next_hop!)))];
412
422
 
413
423
  for (const nextHop of watched) {
414
424
  const local = VrpRender.localAddressTo(this.device, nextHop);
@@ -418,7 +428,7 @@ export class VrpRender {
418
428
 
419
429
  for (const route of routes) {
420
430
  const { addr, len } = Address.split(route.prefix);
421
- const nextHop = route.blackhole ? "NULL0" : (route.via ?? this.nameOf(route.interface!));
431
+ const nextHop = route.blackhole ? "NULL0" : route.next_hop ? Address.host(route.next_hop) : this.nameOf(route.interface!);
422
432
  const vrf = route.vrf ? ` vpn-instance ${route.vrf}` : "";
423
433
  const preference = route.distance ? ` preference ${route.distance}` : "";
424
434
  const description = route.description ? ` description ${route.description}` : "";
@@ -433,8 +443,8 @@ export class VrpRender {
433
443
 
434
444
  /** The device's own IPv4 address on the network that holds `nextHop`. `unsupported()` refuses a watched route without one. */
435
445
  static localAddressTo(device: Device, nextHop: string): string | undefined {
436
- for (const iface of Object.values(device.interfaces ?? {})) {
437
- for (const entry of iface.addresses ?? []) {
446
+ for (const { settings } of Derive.interfaces(device)) {
447
+ for (const entry of settings.addresses ?? []) {
438
448
  const address = typeof entry === "string" ? entry : entry.address;
439
449
  if (Address.contains(Address.prefixOf(address), nextHop)) return Address.host(address);
440
450
  }
@@ -482,7 +492,7 @@ export class VrpRender {
482
492
  private wantsBfd(): boolean {
483
493
  const areas = Object.values(this.device.routing?.ospf?.areas ?? {});
484
494
  const onOspf = areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
485
- const onStatic = (this.device.routing?.static ?? []).some((route) => route.bfd);
495
+ const onStatic = (this.device.routing?.static_routes ?? []).some((route) => route.bfd);
486
496
 
487
497
  return onOspf || onStatic;
488
498
  }
@@ -490,7 +500,7 @@ export class VrpRender {
490
500
  /** sFlow from the management address, to each collector in order. */
491
501
  private flowExport(): void {
492
502
  const device = this.device;
493
- const flow = device.flow_export;
503
+ const flow = device.monitoring?.flow_export;
494
504
  if (flow?.protocol !== "sflow") return;
495
505
 
496
506
  if (device.connection.host) this.line(`sflow agent ip ${device.connection.host}`);
@@ -505,10 +515,10 @@ export class VrpRender {
505
515
 
506
516
  /** The log files' level and count, then each syslog collector. A default the device does not print is not written: level `debugging`, 200 files, port 514. */
507
517
  private logging(): void {
508
- const logging = this.device.system?.logging;
509
- if (!logging) return;
518
+ const local = this.device.system?.logging?.local;
519
+ const collectors = this.device.monitoring?.syslog;
520
+ if (!this.device.system?.logging && !collectors) return;
510
521
 
511
- const local = logging.local;
512
522
  if (local && local.level !== "debug") {
513
523
  this.line(`info-center source default channel ${VrpRender.LOGFILE_CHANNEL} log level ${VrpRender.LOG_LEVEL[local.level]}`);
514
524
  }
@@ -516,7 +526,7 @@ export class VrpRender {
516
526
  if (local?.files !== undefined && local.files !== VrpRender.LOGFILES_DEFAULT)
517
527
  this.line(`info-center max-logfile-number ${local.files}`);
518
528
 
519
- for (const collector of logging.remote ?? []) {
529
+ for (const collector of collectors ?? []) {
520
530
  const level = collector.level === "debug" ? "" : ` level ${VrpRender.LOG_LEVEL[collector.level]}`;
521
531
  const port = collector.port !== undefined && collector.port !== VrpRender.SYSLOG_PORT ? ` port ${collector.port}` : "";
522
532
  this.line(`info-center loghost ${collector.address}${level}${port}`);
@@ -539,12 +549,12 @@ export class VrpRender {
539
549
  /** SNMP version 2c with one read community, version 3 with users in one read-only group, or both. */
540
550
  private snmp(): void {
541
551
  const device = this.device;
542
- const snmp = device.management?.snmp;
552
+ const snmp = device.monitoring?.snmp;
543
553
  if (!snmp) return;
544
554
 
545
555
  const versions = [snmp.community ? "v2c" : "", snmp.users ? "v3" : ""].filter(Boolean).join(" ");
546
556
  const own = (snmp.allow?.length ?? 0) > 0;
547
- const guarded = (device.management?.allow?.length ?? 0) > 0;
557
+ const guarded = VrpRender.managementAllow(device).length > 0;
548
558
  const acl = own ? ` acl ${VrpRender.SNMP_ACL}` : guarded ? ` acl ${VrpRender.MANAGEMENT_ACL}` : "";
549
559
 
550
560
  this.line("snmp-agent");
@@ -577,11 +587,12 @@ export class VrpRender {
577
587
  const ssh = device.management?.ssh;
578
588
  if (!ssh) return;
579
589
 
580
- const guarded = (device.management?.allow?.length ?? 0) > 0;
590
+ const guarded = VrpRender.managementAllow(device).length > 0;
581
591
 
582
592
  this.line("stelnet server enable");
583
593
 
584
- for (const user of Object.keys(device.users ?? {})) {
594
+ for (const [user, settings] of Object.entries(device.system?.users ?? {})) {
595
+ if (!settings) continue;
585
596
  this.line(`ssh user ${user}`);
586
597
  this.line(`ssh user ${user} authentication-type password`);
587
598
  this.line(`ssh user ${user} service-type stelnet`);