@takodotid/circuit 0.43.0 → 1.0.0-rc.1

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 (257) 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 +67 -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 +348 -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/plan.ts +132 -44
  63. package/src/platforms/fortios/render.ts +69 -62
  64. package/src/platforms/fortios/routing.ts +89 -58
  65. package/src/platforms/fortios/vdoms.ts +47 -16
  66. package/src/platforms/junos/TRAPS.md +2 -2
  67. package/src/platforms/junos/bgp.ts +63 -42
  68. package/src/platforms/junos/index.ts +122 -89
  69. package/src/platforms/junos/render.ts +139 -104
  70. package/src/platforms/platform.ts +10 -8
  71. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  72. package/src/platforms/raisecom-ros/index.ts +89 -70
  73. package/src/platforms/raisecom-ros/render.ts +49 -45
  74. package/src/platforms/routeros/TRAPS.md +3 -3
  75. package/src/platforms/routeros/index.ts +121 -60
  76. package/src/platforms/routeros/render.ts +214 -195
  77. package/src/platforms/vrp/TRAPS.md +1 -1
  78. package/src/platforms/vrp/index.ts +74 -50
  79. package/src/platforms/vrp/render.ts +67 -56
  80. package/src/project/project.ts +725 -0
  81. package/src/studio/app/env.d.ts +9 -0
  82. package/src/studio/app/index.html +18 -0
  83. package/src/studio/app/src/App.vue +95 -0
  84. package/src/studio/app/src/components/AddressesView.vue +38 -0
  85. package/src/studio/app/src/components/CablesView.vue +29 -0
  86. package/src/studio/app/src/components/CompareView.vue +160 -0
  87. package/src/studio/app/src/components/DataTable.vue +49 -0
  88. package/src/studio/app/src/components/DeviceView.vue +237 -0
  89. package/src/studio/app/src/components/DevicesView.vue +32 -0
  90. package/src/studio/app/src/components/Faceplate.vue +75 -0
  91. package/src/studio/app/src/components/GraphView.vue +282 -0
  92. package/src/studio/app/src/components/LineDiff.vue +24 -0
  93. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  94. package/src/studio/app/src/components/SearchView.vue +72 -0
  95. package/src/studio/app/src/components/SecretsView.vue +112 -0
  96. package/src/studio/app/src/components/SessionsView.vue +35 -0
  97. package/src/studio/app/src/components/Settings.vue +95 -0
  98. package/src/studio/app/src/components/ToolsView.vue +118 -0
  99. package/src/studio/app/src/components/VlansView.vue +50 -0
  100. package/src/studio/app/src/lib/csv.ts +19 -0
  101. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  102. package/src/studio/app/src/lib/format.ts +38 -0
  103. package/src/studio/app/src/lib/net.ts +68 -0
  104. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  105. package/src/studio/app/src/lib/store.ts +71 -0
  106. package/src/studio/app/src/lib/tools.ts +217 -0
  107. package/src/studio/app/src/main.ts +9 -0
  108. package/src/studio/{public → app/src}/style.css +14 -4
  109. package/src/studio/app/tsconfig.json +17 -0
  110. package/src/studio/app/vite.config.ts +16 -0
  111. package/src/studio/assets.generated.ts +3 -0
  112. package/src/studio/data.ts +96 -188
  113. package/src/studio/git.ts +27 -43
  114. package/src/studio/server.ts +196 -107
  115. package/src/studio/types.ts +141 -0
  116. package/src/transport/console.ts +3 -3
  117. package/src/transport/ssh.ts +1 -1
  118. package/dist/chunks/index-3b343xva.js +0 -177
  119. package/dist/chunks/index-6wb41q67.js +0 -62
  120. package/dist/chunks/index-n7w3tg5y.js +0 -149
  121. package/dist/chunks/index-yzw86rc8.js +0 -921
  122. package/dist/chunks/new-w3zys5y4.js +0 -172
  123. package/dist/chunks/server-cxr9ynxc.js +0 -259
  124. package/dist/cli/index.js +0 -12872
  125. package/dist/helpers/index.js +0 -881
  126. package/dist/index.js +0 -28
  127. package/dist/types/core/address-space.d.ts +0 -62
  128. package/dist/types/core/address.d.ts +0 -44
  129. package/dist/types/core/cabling.d.ts +0 -41
  130. package/dist/types/core/cross-checks.d.ts +0 -28
  131. package/dist/types/core/define.d.ts +0 -79
  132. package/dist/types/core/project-files.d.ts +0 -12
  133. package/dist/types/core/schema-docs.d.ts +0 -25
  134. package/dist/types/core/secrets.d.ts +0 -62
  135. package/dist/types/core/topology.d.ts +0 -96
  136. package/dist/types/core/utils.d.ts +0 -10
  137. package/dist/types/core/validator.d.ts +0 -23
  138. package/dist/types/helpers/bgp.d.ts +0 -195
  139. package/dist/types/helpers/cables.d.ts +0 -69
  140. package/dist/types/helpers/checks.d.ts +0 -36
  141. package/dist/types/helpers/exchange.d.ts +0 -55
  142. package/dist/types/helpers/filters.d.ts +0 -51
  143. package/dist/types/helpers/index.d.ts +0 -14
  144. package/dist/types/helpers/route-server.d.ts +0 -52
  145. package/dist/types/helpers/upstreams.d.ts +0 -192
  146. package/dist/types/index.d.ts +0 -10
  147. package/dist/types/integrations/librenms.d.ts +0 -60
  148. package/dist/types/integrations/netbox.d.ts +0 -89
  149. package/dist/types/integrations/peeringdb.d.ts +0 -24
  150. package/dist/types/integrations/prometheus.d.ts +0 -24
  151. package/dist/types/integrations/registries.d.ts +0 -8
  152. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  153. package/dist/types/platforms/catalog.d.ts +0 -109
  154. package/dist/types/platforms/derive.d.ts +0 -17
  155. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  156. package/dist/types/platforms/fortios/index.d.ts +0 -140
  157. package/dist/types/platforms/fortios/models.d.ts +0 -14
  158. package/dist/types/platforms/fortios/names.d.ts +0 -52
  159. package/dist/types/platforms/fortios/plan.d.ts +0 -104
  160. package/dist/types/platforms/fortios/render.d.ts +0 -77
  161. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  162. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  163. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  164. package/dist/types/platforms/index.d.ts +0 -13
  165. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  166. package/dist/types/platforms/junos/index.d.ts +0 -114
  167. package/dist/types/platforms/junos/models.d.ts +0 -14
  168. package/dist/types/platforms/junos/render.d.ts +0 -154
  169. package/dist/types/platforms/line-diff.d.ts +0 -67
  170. package/dist/types/platforms/platform.d.ts +0 -135
  171. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  172. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  173. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  174. package/dist/types/platforms/routeros/export.d.ts +0 -59
  175. package/dist/types/platforms/routeros/index.d.ts +0 -116
  176. package/dist/types/platforms/routeros/models.d.ts +0 -18
  177. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  178. package/dist/types/platforms/routeros/render.d.ts +0 -250
  179. package/dist/types/platforms/vrp/index.d.ts +0 -83
  180. package/dist/types/platforms/vrp/models.d.ts +0 -12
  181. package/dist/types/platforms/vrp/render.d.ts +0 -96
  182. package/dist/types/schema/common.d.ts +0 -30
  183. package/dist/types/schema/device.d.ts +0 -126
  184. package/dist/types/schema/firewall.d.ts +0 -131
  185. package/dist/types/schema/index.d.ts +0 -8
  186. package/dist/types/schema/interfaces.d.ts +0 -205
  187. package/dist/types/schema/policy.d.ts +0 -107
  188. package/dist/types/schema/routing.d.ts +0 -161
  189. package/dist/types/schema/system.d.ts +0 -198
  190. package/dist/types/schema/time-zones.d.ts +0 -2
  191. package/dist/types/transport/console.d.ts +0 -115
  192. package/dist/types/transport/path-check.d.ts +0 -32
  193. package/dist/types/transport/ssh.d.ts +0 -27
  194. package/src/cli/graph-format.ts +0 -91
  195. package/src/cli/new.ts +0 -234
  196. package/src/core/address-space.ts +0 -320
  197. package/src/core/cross-checks.ts +0 -369
  198. package/src/core/define.ts +0 -124
  199. package/src/core/project-files.ts +0 -35
  200. package/src/core/schema-docs.ts +0 -174
  201. package/src/core/validator.ts +0 -508
  202. package/src/helpers/bgp.ts +0 -473
  203. package/src/helpers/cables.ts +0 -74
  204. package/src/helpers/checks.ts +0 -201
  205. package/src/helpers/exchange.ts +0 -123
  206. package/src/helpers/filters.ts +0 -107
  207. package/src/helpers/index.ts +0 -25
  208. package/src/helpers/route-server.ts +0 -212
  209. package/src/helpers/upstreams.ts +0 -369
  210. package/src/index.ts +0 -13
  211. package/src/integrations/librenms.ts +0 -291
  212. package/src/integrations/netbox.ts +0 -620
  213. package/src/integrations/peeringdb.ts +0 -173
  214. package/src/integrations/prometheus.ts +0 -90
  215. package/src/schema/common.ts +0 -41
  216. package/src/schema/device.ts +0 -145
  217. package/src/schema/firewall.ts +0 -140
  218. package/src/schema/index.ts +0 -8
  219. package/src/schema/policy.ts +0 -115
  220. package/src/schema/routing.ts +0 -173
  221. package/src/schema/system.ts +0 -203
  222. package/src/studio/public/app.js +0 -186
  223. package/src/studio/public/compare.js +0 -250
  224. package/src/studio/public/dom.js +0 -138
  225. package/src/studio/public/evaluate.js +0 -687
  226. package/src/studio/public/export.js +0 -130
  227. package/src/studio/public/graph.js +0 -1541
  228. package/src/studio/public/index.html +0 -83
  229. package/src/studio/public/net.js +0 -134
  230. package/src/studio/public/tables.js +0 -1289
  231. package/src/studio/public/tools.js +0 -610
  232. package/templates/colocation/checks.ts +0 -53
  233. package/templates/colocation/circuit.config.ts +0 -9
  234. package/templates/colocation/edge-01.ts +0 -133
  235. package/templates/colocation/routing.ts +0 -13
  236. package/templates/colocation/tenancy.ts +0 -76
  237. package/templates/colocation/tenants/acme.ts +0 -19
  238. package/templates/colocation/tenants/index.ts +0 -6
  239. package/templates/colocation/tor-01.ts +0 -33
  240. package/templates/edge-router/checks.ts +0 -20
  241. package/templates/edge-router/circuit.config.ts +0 -8
  242. package/templates/edge-router/edge-01.ts +0 -110
  243. package/templates/edge-router/routing.ts +0 -13
  244. package/templates/project/AGENTS.md +0 -51
  245. package/templates/project/CLAUDE.md +0 -1
  246. package/templates/project/README.md +0 -16
  247. package/templates/project/gitignore +0 -5
  248. package/templates/project/tsconfig.json +0 -13
  249. package/templates/single-site/checks.ts +0 -26
  250. package/templates/single-site/circuit.config.ts +0 -8
  251. package/templates/single-site/router.ts +0 -86
  252. package/templates/single-site/site.ts +0 -18
  253. package/templates/single-site/switch.ts +0 -34
  254. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  257. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -1,921 +0,0 @@
1
- import {
2
- Catalog
3
- } from "./index-6wb41q67.js";
4
- import {
5
- Address2
6
- } from "./index-n7w3tg5y.js";
7
-
8
- // src/core/topology.ts
9
- class Builder {
10
- nodes = new Map;
11
- edges = [];
12
- node(id, kind, label, detail = []) {
13
- const existing = this.nodes.get(id);
14
- if (!existing)
15
- this.nodes.set(id, { id, kind, label, detail: [...detail] });
16
- else
17
- for (const line of detail)
18
- if (!existing.detail.includes(line))
19
- existing.detail.push(line);
20
- return id;
21
- }
22
- edge(edge) {
23
- this.edges.push(edge);
24
- return edge;
25
- }
26
- }
27
-
28
- class Topology {
29
- static LAYERS = ["l1", "l2", "l3", "bgp", "ospf", "tunnels"];
30
- static deviceId(name) {
31
- return `device:${name}`;
32
- }
33
- static keyOf(address) {
34
- if (!address || !Address2.isValid(address))
35
- return;
36
- return `${Address2.family(address)}:${Address2.toNumber(address)}`;
37
- }
38
- static isHostRoute(cidr) {
39
- const { len, family } = Address2.split(cidr);
40
- return len === Address2.bits(family);
41
- }
42
- static routedOf(device) {
43
- const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) => port ? [[name, port]] : []);
44
- const interfaces = Object.entries(device.interfaces ?? {});
45
- return [...ports, ...interfaces];
46
- }
47
- static holders(devices) {
48
- return devices.flatMap((device) => Topology.routedOf(device).flatMap(([name, settings]) => (settings.addresses ?? []).flatMap((entry) => {
49
- const address = typeof entry === "string" ? entry : entry.address;
50
- if (!Address2.isCidr(address))
51
- return [];
52
- const peer = typeof entry === "string" ? undefined : entry.peer;
53
- return [{ device, name, address, peer, vrrp: settings.vrrp ?? [], vrf: settings.vrf }];
54
- })));
55
- }
56
- static describeDevice(device) {
57
- const addresses = Topology.routedOf(device).filter(([, settings]) => settings.addresses?.length).map(([name, settings]) => {
58
- const list = (settings.addresses ?? []).map((entry) => typeof entry === "string" ? entry : `${entry.address} to ${entry.peer}`);
59
- const vrf = settings.vrf ? `, VRF ${settings.vrf}` : "";
60
- return `${name}: ${list.join(", ")}${vrf}`;
61
- });
62
- return [
63
- `${device.platform} ${device.model}`,
64
- device.connection.host ? `reached at ${device.connection.host}` : "reached through its console",
65
- ...addresses
66
- ];
67
- }
68
- static carriedBy(device, settings) {
69
- const vlan = (name, untagged) => {
70
- const id = name ? device.vlans?.[name]?.id : undefined;
71
- return name && id !== undefined ? [{ name, id, untagged }] : [];
72
- };
73
- const trunk = (settings?.trunk_vlans ?? []).filter((name) => name !== settings?.native_vlan);
74
- return [
75
- ...vlan(settings?.access_vlan, true),
76
- ...vlan(settings?.native_vlan, true),
77
- ...trunk.flatMap((name) => vlan(name, false))
78
- ].sort((a, b) => a.id - b.id);
79
- }
80
- static vlanLabel(vlans) {
81
- if (vlans.length === 1)
82
- return `${vlans[0].name}${vlans[0].untagged ? ", untagged" : ""}`;
83
- if (vlans.length <= 3)
84
- return vlans.map((vlan) => vlan.name).join(", ");
85
- return `${vlans.length} VLANs`;
86
- }
87
- static cables(devices, graph) {
88
- const byName = new Map(devices.map((device) => [device.name, device]));
89
- const drawn = new Set;
90
- const bundles = new Map;
91
- for (const device of devices) {
92
- for (const [portName, port] of Object.entries(device.ports ?? {})) {
93
- const link = port?.link;
94
- if (!port || !link)
95
- continue;
96
- const here = `${device.name} ${portName}`;
97
- const there = `${link.device} ${link.port}`;
98
- if (drawn.has(`${there}|${here}`))
99
- continue;
100
- drawn.add(`${here}|${there}`);
101
- const far = byName.get(link.device);
102
- const farPort = far?.ports?.[link.port];
103
- const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
104
- const to = far ? Topology.deviceId(far.name) : graph.node(`missing:${link.device}`, "outside", link.device, ["named by a link, and not in the network"]);
105
- const through = link.through ? `, through ${link.through}` : "";
106
- const cableLine = `${here} to ${there}${through}`;
107
- const lag = port.lag;
108
- const farLag = farPort?.lag;
109
- const bundleKey = `${device.name} ${lag}|${link.device} ${farLag}`;
110
- const bundle = lag && farLag && linksBack ? bundles.get(bundleKey) : undefined;
111
- if (bundle) {
112
- bundle.members++;
113
- bundle.cable.detail.push(cableLine);
114
- bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables${through}`;
115
- continue;
116
- }
117
- const isBundle = Boolean(lag && farLag && linksBack);
118
- const cable = graph.edge({
119
- layer: "l1",
120
- from: Topology.deviceId(device.name),
121
- to,
122
- label: `${isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`}${through}`,
123
- detail: [cableLine],
124
- ...linksBack ? {} : { oneSided: true }
125
- });
126
- if (isBundle)
127
- bundles.set(bundleKey, { cable, members: 1 });
128
- const lagSettings = lag ? device.interfaces?.[lag] : undefined;
129
- const carrier = lagSettings?.type === "lag" ? lagSettings : lag ? undefined : port;
130
- const vlans = Topology.carriedBy(device, carrier);
131
- if (!vlans.length)
132
- continue;
133
- graph.edge({
134
- layer: "l2",
135
- from: Topology.deviceId(device.name),
136
- to,
137
- label: Topology.vlanLabel(vlans),
138
- detail: [
139
- `${device.name} ${lag ?? portName}`,
140
- ...vlans.map((vlan) => `${vlan.name}, VLAN ${vlan.id}${vlan.untagged ? ", untagged" : ""}`)
141
- ],
142
- vlans,
143
- ...linksBack ? {} : { oneSided: true }
144
- });
145
- }
146
- }
147
- }
148
- static segments(holders, owners, graph) {
149
- const bySegment = new Map;
150
- const pairs = new Set;
151
- for (const holder of holders) {
152
- if (holder.peer) {
153
- const far = owners.get(Topology.keyOf(holder.peer) ?? "");
154
- if (!far || far.device === holder.device)
155
- continue;
156
- const ends = [`${holder.device.name} ${holder.name}`, `${far.device.name} ${far.name}`].sort().join("|");
157
- if (pairs.has(ends))
158
- continue;
159
- pairs.add(ends);
160
- graph.edge({
161
- layer: "l3",
162
- from: Topology.deviceId(holder.device.name),
163
- to: Topology.deviceId(far.device.name),
164
- label: `${Address2.host(holder.address)} to ${Address2.host(far.address)}`,
165
- detail: [`${holder.device.name} ${holder.name} ${holder.address}`, `${far.device.name} ${far.name} ${far.address}`]
166
- });
167
- continue;
168
- }
169
- if (Topology.isHostRoute(holder.address))
170
- continue;
171
- const segment = Address2.prefixOf(holder.address);
172
- bySegment.set(segment, [...bySegment.get(segment) ?? [], holder]);
173
- }
174
- for (const [segment, members] of bySegment) {
175
- if (new Set(members.map((member) => member.device.name)).size < 2)
176
- continue;
177
- const memberLines = members.map((member) => `${member.device.name} ${member.name} ${member.address}`);
178
- const vrrpLines = members.flatMap((member) => member.vrrp.map((group) => `VRRP ${group.id} ${group.address} on ${member.device.name}, priority ${group.priority ?? 100}`));
179
- if (members.length === 2) {
180
- const [a, b] = members;
181
- graph.edge({
182
- layer: "l3",
183
- from: Topology.deviceId(a.device.name),
184
- to: Topology.deviceId(b.device.name),
185
- label: segment,
186
- detail: [...memberLines, ...vrrpLines]
187
- });
188
- continue;
189
- }
190
- const id = graph.node(`segment:${segment}`, "segment", segment, [...memberLines, ...vrrpLines]);
191
- for (const member of members) {
192
- graph.edge({
193
- layer: "l3",
194
- from: Topology.deviceId(member.device.name),
195
- to: id,
196
- label: `${member.name} ${Address2.host(member.address)}`,
197
- detail: [`${member.device.name} ${member.name} ${member.address}`]
198
- });
199
- }
200
- }
201
- }
202
- static sessions(devices, owners, graph) {
203
- const all = devices.flatMap((device) => {
204
- const bgp = device.routing?.bgp;
205
- return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
206
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
207
- return {
208
- device,
209
- name,
210
- address: neighbor.address,
211
- ownAs: bgp.asn,
212
- remoteAs: neighbor.remote_as ?? group?.remote_as,
213
- role: neighbor.role ?? group?.role,
214
- description: neighbor.description ?? group?.description,
215
- import: neighbor.import ?? group?.import,
216
- export: neighbor.export ?? group?.export
217
- };
218
- });
219
- });
220
- const describe = (session) => {
221
- const parts = [
222
- session.address,
223
- session.remoteAs ? `AS${session.remoteAs}` : "no remote AS",
224
- session.role,
225
- session.import ? `import ${session.import}` : "imports nothing",
226
- session.export ? `export ${session.export}` : "exports nothing"
227
- ];
228
- return `${session.device.name} ${session.name}: ${parts.filter(Boolean).join(", ")}`;
229
- };
230
- const ownerOf = (session) => owners.get(Topology.keyOf(session.address) ?? "")?.device;
231
- const paired = new Set;
232
- for (const session of all) {
233
- if (paired.has(session))
234
- continue;
235
- const far = ownerOf(session);
236
- if (far && far !== session.device) {
237
- const back = all.find((other) => other.device === far && !paired.has(other) && ownerOf(other) === session.device && Address2.family(other.address) === Address2.family(session.address));
238
- if (back)
239
- paired.add(back);
240
- graph.edge({
241
- layer: "bgp",
242
- from: Topology.deviceId(session.device.name),
243
- to: Topology.deviceId(far.name),
244
- label: session.remoteAs === session.ownAs ? "iBGP" : "eBGP",
245
- detail: [describe(session), ...back ? [describe(back)] : [`${far.name} has no session back`]],
246
- ...back ? {} : { oneSided: true }
247
- });
248
- continue;
249
- }
250
- const outside = session.remoteAs ? graph.node(`as:${session.remoteAs}`, "outside", `AS${session.remoteAs}`, session.description ? [session.description] : []) : graph.node(`address:${session.address}`, "outside", session.address, ["a BGP neighbor without a remote AS"]);
251
- const internal = session.remoteAs === session.ownAs;
252
- graph.edge({
253
- layer: "bgp",
254
- from: Topology.deviceId(session.device.name),
255
- to: outside,
256
- label: [session.name, session.role].filter(Boolean).join(", "),
257
- detail: [describe(session), ...internal ? ["internal, and no device in the network holds the address"] : []],
258
- ...internal ? { oneSided: true } : {}
259
- });
260
- }
261
- }
262
- static adjacencies(devices, graph) {
263
- const byKey = new Map;
264
- for (const device of devices) {
265
- for (const [area, { interfaces }] of Object.entries(device.routing?.ospf?.areas ?? {})) {
266
- for (const [name, settings] of Object.entries(interfaces)) {
267
- if (settings?.passive)
268
- continue;
269
- const routed = device.ports?.[name] ?? device.interfaces?.[name];
270
- const cost = settings?.cost ? `, cost ${settings.cost}` : "";
271
- for (const entry of routed?.addresses ?? []) {
272
- const address = typeof entry === "string" ? entry : entry.address;
273
- if (!Address2.isCidr(address) || Topology.isHostRoute(address))
274
- continue;
275
- const key = `${area} ${Address2.prefixOf(address)}`;
276
- const member = { device, name, line: `${device.name} ${name} ${address}${cost}` };
277
- byKey.set(key, [...byKey.get(key) ?? [], member]);
278
- }
279
- }
280
- }
281
- }
282
- for (const [key, members] of byKey) {
283
- const [area] = key.split(" ");
284
- for (const [index, a] of members.entries()) {
285
- for (const b of members.slice(index + 1)) {
286
- if (a.device === b.device)
287
- continue;
288
- graph.edge({
289
- layer: "ospf",
290
- from: Topology.deviceId(a.device.name),
291
- to: Topology.deviceId(b.device.name),
292
- label: `area ${area}`,
293
- detail: [a.line, b.line]
294
- });
295
- }
296
- }
297
- }
298
- }
299
- static endpointHost(endpoint) {
300
- return endpoint.replace(/:\d+$/, "").replace(/^\[(.*)\]$/, "$1");
301
- }
302
- static tunnels(devices, owners, graph) {
303
- const ends = devices.flatMap((device) => Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "gre" || iface.type === "vxlan" ? [{ device, name, iface }] : []));
304
- const same = (a, b) => Topology.keyOf(a) !== undefined && Topology.keyOf(a) === Topology.keyOf(b);
305
- const mirrors = (a, b) => a.device !== b.device && a.iface.type === b.iface.type && same(a.iface.local, b.iface.remote) && same(a.iface.remote, b.iface.local) && (a.iface.type !== "vxlan" || b.iface.type === "vxlan" && a.iface.vni === b.iface.vni);
306
- const describe = (end) => {
307
- const vni = end.iface.type === "vxlan" ? `, VNI ${end.iface.vni}` : "";
308
- return `${end.device.name} ${end.name}: ${end.iface.local} to ${end.iface.remote}${vni}`;
309
- };
310
- const paired = new Set;
311
- for (const end of ends) {
312
- if (paired.has(end))
313
- continue;
314
- const kind = end.iface.type === "gre" ? "GRE" : "VXLAN";
315
- const mirror = ends.find((other) => !paired.has(other) && mirrors(end, other));
316
- if (mirror) {
317
- paired.add(mirror);
318
- graph.edge({
319
- layer: "tunnels",
320
- from: Topology.deviceId(end.device.name),
321
- to: Topology.deviceId(mirror.device.name),
322
- label: `${kind} ${end.name} to ${mirror.name}`,
323
- detail: [describe(end), describe(mirror)]
324
- });
325
- continue;
326
- }
327
- const owner = owners.get(Topology.keyOf(end.iface.remote) ?? "")?.device;
328
- if (owner && owner !== end.device) {
329
- graph.edge({
330
- layer: "tunnels",
331
- from: Topology.deviceId(end.device.name),
332
- to: Topology.deviceId(owner.name),
333
- label: `${kind} ${end.name}`,
334
- detail: [describe(end), `${owner.name} declares no tunnel back`],
335
- oneSided: true
336
- });
337
- continue;
338
- }
339
- const outside = graph.node(`address:${end.iface.remote}`, "outside", end.iface.remote, end.iface.description ? [end.iface.description] : []);
340
- graph.edge({
341
- layer: "tunnels",
342
- from: Topology.deviceId(end.device.name),
343
- to: outside,
344
- label: `${kind} ${end.name}`,
345
- detail: [describe(end)]
346
- });
347
- }
348
- Topology.wireguard(devices, owners, graph);
349
- }
350
- static wireguard(devices, owners, graph) {
351
- const interfaces = devices.flatMap((device) => Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "wireguard" ? [{ device, name, iface }] : []));
352
- const farEndOf = (device, peer) => interfaces.find(({ device: other, iface }) => {
353
- if (other === device)
354
- return false;
355
- const tunnelAddresses = (iface.addresses ?? []).map((entry) => Address2.host(typeof entry === "string" ? entry : entry.address));
356
- const sendsFrom = tunnelAddresses.some((address) => Address2.isValid(address) && peer.allowed_addresses.some((allowed) => Address2.isCidr(allowed) && Address2.contains(allowed, address)));
357
- const endpoint = peer.endpoint ? Topology.endpointHost(peer.endpoint) : undefined;
358
- const isEndpoint = endpoint !== undefined && owners.get(Topology.keyOf(endpoint) ?? "")?.device === other;
359
- return sendsFrom || isEndpoint;
360
- });
361
- const drawn = new Set;
362
- for (const { device, name, iface } of interfaces) {
363
- const clients = [];
364
- for (const peer of iface.peers ?? []) {
365
- const far = farEndOf(device, peer);
366
- if (far) {
367
- const ends = [`${device.name} ${name}`, `${far.device.name} ${far.name}`].sort().join("|");
368
- if (drawn.has(ends))
369
- continue;
370
- drawn.add(ends);
371
- graph.edge({
372
- layer: "tunnels",
373
- from: Topology.deviceId(device.name),
374
- to: Topology.deviceId(far.device.name),
375
- label: `WireGuard ${name} to ${far.name}`,
376
- detail: [`${device.name} ${name} peer ${peer.name}`]
377
- });
378
- continue;
379
- }
380
- if (!peer.endpoint) {
381
- clients.push(peer);
382
- continue;
383
- }
384
- const endpoint = Topology.endpointHost(peer.endpoint);
385
- const outside = graph.node(`address:${endpoint}`, "outside", peer.name, [`WireGuard endpoint ${peer.endpoint}`]);
386
- graph.edge({
387
- layer: "tunnels",
388
- from: Topology.deviceId(device.name),
389
- to: outside,
390
- label: `WireGuard ${name}`,
391
- detail: [`${device.name} ${name} peer ${peer.name}, allowed ${peer.allowed_addresses.join(", ")}`]
392
- });
393
- }
394
- if (!clients.length)
395
- continue;
396
- const outside = graph.node(`clients:${device.name}:${name}`, "outside", `${clients.length} ${clients.length === 1 ? "client" : "clients"}`, clients.map((peer) => `${peer.name}: ${peer.allowed_addresses.join(", ")}`));
397
- graph.edge({
398
- layer: "tunnels",
399
- from: Topology.deviceId(device.name),
400
- to: outside,
401
- label: `WireGuard ${name}`,
402
- detail: [`peers that dial in to ${device.name} ${name}`]
403
- });
404
- }
405
- }
406
- static of(devices) {
407
- const graph = new Builder;
408
- for (const device of devices)
409
- graph.node(Topology.deviceId(device.name), "device", device.name, Topology.describeDevice(device));
410
- const holders = Topology.holders(devices);
411
- const owners = new Map;
412
- for (const holder of holders) {
413
- const key = Topology.keyOf(Address2.host(holder.address));
414
- if (!owners.has(key))
415
- owners.set(key, holder);
416
- }
417
- Topology.cables(devices, graph);
418
- Topology.segments(holders, owners, graph);
419
- Topology.sessions(devices, owners, graph);
420
- Topology.adjacencies(devices, graph);
421
- Topology.tunnels(devices, owners, graph);
422
- return { nodes: [...graph.nodes.values()], edges: graph.edges };
423
- }
424
- static only(graph, layers, deviceNames = []) {
425
- const named = new Set(deviceNames.map(Topology.deviceId));
426
- const touches = (edge) => !named.size || named.has(edge.from) || named.has(edge.to);
427
- const edges = graph.edges.filter((edge) => layers.includes(edge.layer) && touches(edge));
428
- const ends = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
429
- const nodes = graph.nodes.filter((node) => (named.size ? named.has(node.id) : node.kind === "device") || ends.has(node.id));
430
- return { nodes, edges };
431
- }
432
- }
433
-
434
- // src/core/address-space.ts
435
- class AddressSpace {
436
- static isHostRoute(cidr) {
437
- return Address2.split(cidr).len === Address2.bits(Address2.split(cidr).family);
438
- }
439
- static numberOf(address) {
440
- return Address2.toNumber(address);
441
- }
442
- static usable(prefix) {
443
- const { first, last, family } = Address2.range(prefix);
444
- const { len } = Address2.split(prefix);
445
- if (family === "ipv4" && len <= 30)
446
- return { first: first + 1n, last: last - 1n };
447
- if (family === "ipv6" && len < 127)
448
- return { first: first + 1n, last };
449
- return { first, last };
450
- }
451
- static merged(ranges) {
452
- const sorted = [...ranges].sort((a, b) => a.first < b.first ? -1 : a.first > b.first ? 1 : 0);
453
- const result = [];
454
- for (const range of sorted) {
455
- const previous = result.at(-1);
456
- if (previous && range.first <= previous.last + 1n)
457
- previous.last = range.last > previous.last ? range.last : previous.last;
458
- else
459
- result.push({ ...range });
460
- }
461
- return result;
462
- }
463
- static gaps(within, taken) {
464
- const result = [];
465
- let cursor = within.first;
466
- for (const range of AddressSpace.merged(taken)) {
467
- if (range.last < within.first || range.first > within.last)
468
- continue;
469
- if (range.first > cursor)
470
- result.push({ first: cursor, last: range.first - 1n });
471
- if (range.last + 1n > cursor)
472
- cursor = range.last + 1n;
473
- }
474
- if (cursor <= within.last)
475
- result.push({ first: cursor, last: within.last });
476
- return result;
477
- }
478
- static count(value) {
479
- if (value <= 1n << 20n)
480
- return String(value);
481
- const power = value.toString(2).length - 1;
482
- const nearer = value - (1n << BigInt(power)) >= 1n << BigInt(power - 1) ? power + 1 : power;
483
- return `about 2^${nearer}`;
484
- }
485
- static byAddress(a, b) {
486
- const first = (use) => AddressSpace.numberOf(use.address.split("-")[0]);
487
- return first(a) < first(b) ? -1 : first(a) > first(b) ? 1 : 0;
488
- }
489
- static comparePrefixes(a, b) {
490
- const [ra, rb] = [Address2.range(a), Address2.range(b)];
491
- if (ra.family !== rb.family)
492
- return ra.family === "ipv4" ? -1 : 1;
493
- return ra.first < rb.first ? -1 : ra.first > rb.first ? 1 : 0;
494
- }
495
- static byPrefix(a, b) {
496
- return AddressSpace.comparePrefixes(a.prefix, b.prefix);
497
- }
498
- static report(devices, within) {
499
- const segments = new Map;
500
- const blocks = [];
501
- const segmentOf = (address) => [...segments.keys()].find((prefix) => Address2.contains(prefix, address));
502
- for (const holder of Topology.holders(devices)) {
503
- const vrf = holder.device.ports?.[holder.name]?.vrf ?? holder.device.interfaces?.[holder.name]?.vrf;
504
- const what = `${holder.device.name} ${holder.name}${vrf ? `, VRF ${vrf}` : ""}`;
505
- if (AddressSpace.isHostRoute(holder.address) || holder.peer) {
506
- blocks.push({ prefix: `${Address2.host(holder.address)}/${Address2.bits(Address2.family(holder.address))}`, what });
507
- continue;
508
- }
509
- const prefix = Address2.prefixOf(holder.address);
510
- const segment = segments.get(prefix) ?? { uses: [], taken: [] };
511
- const value = AddressSpace.numberOf(Address2.host(holder.address));
512
- segment.uses.push({ address: Address2.host(holder.address), what });
513
- segment.taken.push({ first: value, last: value });
514
- segments.set(prefix, segment);
515
- for (const group of holder.vrrp) {
516
- if (!Address2.isValid(group.address) || !Address2.contains(prefix, group.address))
517
- continue;
518
- if (segment.uses.some((use) => use.address === group.address))
519
- continue;
520
- const holders = devices.filter((device) => Object.values({ ...device.ports, ...device.interfaces }).some((settings) => settings?.vrrp?.some((other) => other.id === group.id && other.address === group.address)));
521
- segment.uses.push({
522
- address: group.address,
523
- what: `VRRP ${group.id} on ${holders.map((device) => device.name).join(", ")}`
524
- });
525
- segment.taken.push({ first: AddressSpace.numberOf(group.address), last: AddressSpace.numberOf(group.address) });
526
- }
527
- }
528
- const take = (address, what, first = address, last = address) => {
529
- const prefix = segmentOf(first);
530
- if (!prefix)
531
- return false;
532
- const segment = segments.get(prefix);
533
- if (segment.uses.some((use) => use.address === address))
534
- return true;
535
- segment.uses.push({ address, what });
536
- segment.taken.push({ first: AddressSpace.numberOf(first), last: AddressSpace.numberOf(last) });
537
- return true;
538
- };
539
- for (const device of devices) {
540
- for (const [name, server] of Object.entries(device.dhcp ?? {})) {
541
- const [start, end] = server.pool;
542
- if (Address2.isValid(start) && Address2.isValid(end))
543
- take(`${start}-${end}`, `DHCP pool ${name} on ${device.name}`, start, end);
544
- for (const reservation of server.reservations ?? []) {
545
- if (Address2.isValid(reservation.address))
546
- take(reservation.address, `reserved for ${reservation.description ?? reservation.mac} by ${device.name}`);
547
- }
548
- }
549
- for (const route of device.routing?.static ?? []) {
550
- if (route.via && Address2.isValid(route.via))
551
- take(route.via, `next hop for ${route.prefix} on ${device.name}`);
552
- }
553
- for (const [name, neighbor] of Object.entries(device.routing?.bgp?.neighbors ?? {})) {
554
- if (Address2.isValid(neighbor.address))
555
- take(neighbor.address, `BGP neighbor ${name} of ${device.name}`);
556
- }
557
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
558
- if (iface.type !== "wireguard")
559
- continue;
560
- for (const peer of iface.peers ?? []) {
561
- for (const allowed of peer.allowed_addresses) {
562
- if (!Address2.isCidr(allowed))
563
- continue;
564
- const what = `WireGuard peer ${peer.name} on ${device.name} ${name}`;
565
- if (AddressSpace.isHostRoute(allowed) && take(Address2.host(allowed), what))
566
- continue;
567
- if (!segmentOf(Address2.host(allowed)))
568
- blocks.push({ prefix: Address2.prefixOf(allowed), what });
569
- }
570
- }
571
- }
572
- }
573
- const owners = new Map;
574
- for (const device of devices) {
575
- for (const prefix of device.routing?.bgp?.networks ?? []) {
576
- if (!Address2.isCidr(prefix))
577
- continue;
578
- owners.set(Address2.prefixOf(prefix), [...owners.get(Address2.prefixOf(prefix)) ?? [], device.name]);
579
- }
580
- }
581
- for (const device of devices) {
582
- for (const route of device.routing?.static ?? []) {
583
- if (!Address2.isCidr(route.prefix) || Address2.split(route.prefix).len === 0)
584
- continue;
585
- const overlapsSegment = [...segments.keys()].some((prefix) => Address2.contains(route.prefix, prefix) || Address2.contains(prefix, route.prefix));
586
- const coversOwned = [...owners.keys()].some((owned) => Address2.contains(route.prefix, owned));
587
- if (overlapsSegment || coversOwned)
588
- continue;
589
- const how = route.blackhole ? "discarded" : route.via ? `routed via ${route.via}` : `routed out ${route.interface}`;
590
- blocks.push({ prefix: Address2.prefixOf(route.prefix), what: `${how} by ${device.name}` });
591
- }
592
- }
593
- const asked = within ? Address2.prefixOf(within) : undefined;
594
- const spacePrefixes = asked ? [asked] : [...owners.keys()];
595
- const spaceOf = (prefix) => spacePrefixes.filter((space) => Address2.contains(space, prefix)).sort((a, b) => Address2.split(b).len - Address2.split(a).len)[0];
596
- const toSegment = (prefix) => {
597
- const { uses, taken } = segments.get(prefix);
598
- const free = AddressSpace.gaps(AddressSpace.usable(prefix), taken);
599
- const total = free.reduce((sum, range) => sum + range.last - range.first + 1n, 0n);
600
- const family = Address2.family(prefix);
601
- return {
602
- prefix,
603
- uses: uses.sort(AddressSpace.byAddress),
604
- free: AddressSpace.count(total),
605
- ...free[0] ? { next: Address2.fromNumber(free[0].first, family) } : {}
606
- };
607
- };
608
- const uniqueBlocks = blocks.filter((block, index) => blocks.findIndex((other) => other.prefix === block.prefix && other.what === block.what) === index);
609
- const spaces = spacePrefixes.sort(AddressSpace.comparePrefixes).map((space) => {
610
- const inside = (prefix) => spaceOf(prefix) === space;
611
- const spaceSegments = [...segments.keys()].filter(inside).sort(AddressSpace.comparePrefixes);
612
- const spaceBlocks = uniqueBlocks.filter((block) => inside(block.prefix)).sort(AddressSpace.byPrefix);
613
- const nested = spacePrefixes.filter((other) => other !== space && Address2.contains(space, other));
614
- const taken = [...spaceSegments, ...spaceBlocks.map((block) => block.prefix), ...nested].map((prefix) => Address2.range(prefix));
615
- const family = Address2.family(space);
616
- return {
617
- prefix: space,
618
- owner: asked && !owners.has(space) ? "asked for" : `originated by ${(owners.get(space) ?? []).join(", ")}`,
619
- segments: spaceSegments.map(toSegment),
620
- blocks: spaceBlocks,
621
- free: AddressSpace.gaps(Address2.range(space), taken).flatMap((range) => Address2.prefixesBetween(range.first, range.last, family))
622
- };
623
- });
624
- const outside = (prefix) => !spaceOf(prefix);
625
- return {
626
- spaces,
627
- elsewhere: asked ? { segments: [], blocks: [] } : {
628
- segments: [...segments.keys()].filter(outside).sort(AddressSpace.comparePrefixes).map(toSegment),
629
- blocks: uniqueBlocks.filter((block) => outside(block.prefix)).sort(AddressSpace.byPrefix)
630
- }
631
- };
632
- }
633
- static freeOfLength(free, length, limit) {
634
- const prefixes = [];
635
- let total = 0n;
636
- for (const prefix of free) {
637
- const { len, family } = Address2.split(prefix);
638
- if (len > length || length > Address2.bits(family))
639
- continue;
640
- const { first } = Address2.range(prefix);
641
- const step = 1n << BigInt(Address2.bits(family) - length);
642
- const fits = 1n << BigInt(length - len);
643
- total += fits;
644
- for (let index = 0n;index < fits && prefixes.length < limit; index++) {
645
- prefixes.push(`${Address2.fromNumber(first + index * step, family)}/${length}`);
646
- }
647
- }
648
- return { prefixes, total };
649
- }
650
- }
651
-
652
- // src/studio/data.ts
653
- import { existsSync, readFileSync } from "node:fs";
654
- import { dirname, join } from "node:path";
655
- import { fileURLToPath } from "node:url";
656
-
657
- // src/core/schema-docs.ts
658
- class SchemaDocs {
659
- static FILES = [
660
- ["device.ts", "Device"],
661
- ["interfaces.ts", "Ports and interfaces"],
662
- ["routing.ts", "Routing"],
663
- ["policy.ts", "Route policies"],
664
- ["firewall.ts", "Firewall and ACLs"],
665
- ["system.ts", "System and management"],
666
- ["common.ts", "Common values"]
667
- ];
668
- static commentText(lines) {
669
- return lines.map((line) => line.replace(/^\s*\/\*\*\s?|\s*\*\/\s*$|^\s*\*\s?/g, "").trim()).filter(Boolean).join(" ");
670
- }
671
- static PARAMETER_MEANING = { Pl: "platform", M: "model" };
672
- static withoutGenerics(type, generics) {
673
- let result = type;
674
- while (/NoInfer<([^<>]*)>/.test(result)) {
675
- result = result.replace(/NoInfer<([^<>]*)>/g, (_, inner) => inner.includes("|") ? `(${inner})` : inner);
676
- }
677
- for (const name of generics) {
678
- result = result.replace(new RegExp(`\\b${name}<[^<>]*(?:<[^<>]*>[^<>]*)*>`, "g"), name);
679
- }
680
- return result.replace(/"[^"]*"|\b(Pl|M|N|V|I|P|S|G|A|AS|U|F|C|L|K)\b/g, (found, parameter) => parameter ? SchemaDocs.PARAMETER_MEANING[parameter] ?? "name" : found).replace(/\bname(?: \| name)+\b/g, "name").replace(/\(name\)/g, "name");
681
- }
682
- static parse(source, generics) {
683
- const lines = source.split(`
684
- `);
685
- const types = [];
686
- let comment = [];
687
- let inComment = false;
688
- let current;
689
- let depth = 0;
690
- const parents = [];
691
- let header;
692
- function finishHeader() {
693
- const text = header;
694
- header = undefined;
695
- const body = text.slice(text.indexOf("> =") >= 0 ? text.indexOf("> =") + 3 : text.indexOf("=") + 1).trim();
696
- if (!body.includes("{") || body.startsWith("`")) {
697
- current.definition = body.replace(/;$/, "").replace(/^\|\s*/, "");
698
- } else {
699
- const before = body.slice(0, body.indexOf("{"));
700
- const included = before.split("&").map((part) => part.trim()).filter((part) => /^\w/.test(part));
701
- if (included.length)
702
- current.includes = included;
703
- }
704
- depth = (text.match(/{/g) ?? []).length - (text.match(/}/g) ?? []).length;
705
- }
706
- for (const line of lines) {
707
- const trimmed = line.trim();
708
- if (trimmed.startsWith("/**")) {
709
- comment = [trimmed];
710
- inComment = !trimmed.endsWith("*/");
711
- continue;
712
- }
713
- if (inComment) {
714
- comment.push(trimmed);
715
- if (trimmed.endsWith("*/"))
716
- inComment = false;
717
- continue;
718
- }
719
- if (header !== undefined) {
720
- header += ` ${trimmed}`;
721
- if (trimmed.endsWith("{") || trimmed.endsWith(";"))
722
- finishHeader();
723
- continue;
724
- }
725
- const declaration = /^(?:export )?type (\w+)(<)?/.exec(trimmed);
726
- if (declaration && depth === 0) {
727
- if (declaration[2])
728
- generics.add(declaration[1]);
729
- current = { name: declaration[1], description: SchemaDocs.commentText(comment), fields: [] };
730
- types.push(current);
731
- comment = [];
732
- header = trimmed;
733
- if (trimmed.endsWith("{") || trimmed.endsWith(";"))
734
- finishHeader();
735
- continue;
736
- }
737
- if (!current)
738
- continue;
739
- const field = /^(?:readonly\s+)?("?[\w-]+"?)(\?)?:\s*(.*)$/.exec(trimmed);
740
- if (field && depth >= 1) {
741
- const [, name, optional, rest] = field;
742
- const path = [...parents, name.replace(/"/g, "")].join(".");
743
- const opensObject = rest.trim().endsWith("{");
744
- const type = opensObject ? "object" : rest.replace(/;$/, "").trim();
745
- current.fields.push({ path: `${path}${optional ? "?" : ""}`, type, description: SchemaDocs.commentText(comment) });
746
- comment = [];
747
- if (opensObject) {
748
- parents.push(name.replace(/"/g, ""));
749
- depth++;
750
- }
751
- continue;
752
- }
753
- const opens = (trimmed.match(/{/g) ?? []).length;
754
- const closes = (trimmed.match(/}/g) ?? []).length;
755
- for (let index = 0;index < closes - opens; index++) {
756
- if (parents.length >= depth - 1 && parents.length > 0)
757
- parents.pop();
758
- }
759
- depth += opens - closes;
760
- if (depth <= 0) {
761
- depth = 0;
762
- parents.length = 0;
763
- }
764
- comment = [];
765
- }
766
- return types;
767
- }
768
- static read(sources) {
769
- const generics = new Set;
770
- const parsed = sources.map((source) => SchemaDocs.parse(source, generics));
771
- for (const types of parsed) {
772
- for (const type of types) {
773
- if (type.definition)
774
- type.definition = SchemaDocs.withoutGenerics(type.definition, generics);
775
- if (type.includes)
776
- type.includes = type.includes.map((name) => SchemaDocs.withoutGenerics(name, generics));
777
- for (const field of type.fields)
778
- field.type = SchemaDocs.withoutGenerics(field.type, generics);
779
- }
780
- }
781
- return parsed;
782
- }
783
- }
784
-
785
- // src/studio/data.ts
786
- class StudioModel {
787
- static build(devices, graph, texts) {
788
- const owners = new Map;
789
- for (const holder of Topology.holders(devices)) {
790
- const key = Topology.keyOf(Address2.host(holder.address));
791
- if (!owners.has(key))
792
- owners.set(key, holder);
793
- }
794
- const attempt = (work, otherwise) => {
795
- try {
796
- return work();
797
- } catch (error) {
798
- return otherwise(error);
799
- }
800
- };
801
- return {
802
- graph,
803
- devices,
804
- ports: Object.fromEntries(devices.map((device) => [
805
- device.name,
806
- attempt(() => Catalog.ports(device.platform, device.model), () => [])
807
- ])),
808
- rendered: Object.fromEntries(devices.map((device) => [
809
- device.name,
810
- attempt(() => texts.render(device), (error) => `Cannot be rendered: ${error.message}`)
811
- ])),
812
- snapshots: Object.fromEntries(devices.flatMap((device) => {
813
- const text = texts.snapshot(device);
814
- return text === undefined ? [] : [[device.name, text]];
815
- })),
816
- files: Object.fromEntries(devices.map((device) => [device.name, texts.files(device)])),
817
- addresses: AddressSpace.report(devices),
818
- sessions: StudioModel.sessionsOf(devices, owners),
819
- cables: StudioModel.cablesOf(devices),
820
- docs: StudioModel.schemaDocs()
821
- };
822
- }
823
- static packageRoot() {
824
- let directory = dirname(fileURLToPath(import.meta.url));
825
- while (true) {
826
- const manifest = join(directory, "package.json");
827
- if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@takodotid/circuit")
828
- return directory;
829
- const parent = dirname(directory);
830
- if (parent === directory)
831
- return;
832
- directory = parent;
833
- }
834
- }
835
- static sessionsOf(devices, owners) {
836
- const all = devices.flatMap((device) => {
837
- const bgp = device.routing?.bgp;
838
- return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
839
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
840
- const settings = { ...group, ...neighbor };
841
- return {
842
- deviceObject: device,
843
- device: device.name,
844
- name,
845
- address: neighbor.address,
846
- group: neighbor.group,
847
- remoteAs: settings.remote_as,
848
- role: settings.role,
849
- import: settings.import,
850
- export: settings.export,
851
- description: settings.description
852
- };
853
- });
854
- });
855
- const ownerOf = (address) => owners.get(Topology.keyOf(address) ?? "")?.device;
856
- return all.map(({ deviceObject, ...session }) => {
857
- const far = ownerOf(session.address);
858
- if (far && far !== deviceObject) {
859
- const back = all.some((other) => other.deviceObject === far && ownerOf(other.address) === deviceObject && Address2.family(other.address) === Address2.family(session.address));
860
- return { ...session, far: far.name, ...back ? {} : { oneSided: true } };
861
- }
862
- return { ...session, ...session.remoteAs ? { far: `AS${session.remoteAs}` } : {} };
863
- });
864
- }
865
- static cablesOf(devices) {
866
- const byName = new Map(devices.map((device) => [device.name, device]));
867
- const drawn = new Set;
868
- const cables = [];
869
- const vlansOf = (device, portName) => {
870
- const port = device.ports?.[portName];
871
- const lag = port?.lag ? device.interfaces?.[port.lag] : undefined;
872
- const carrier = lag?.type === "lag" ? lag : port;
873
- const names = [carrier?.access_vlan, carrier?.native_vlan, ...carrier?.trunk_vlans ?? []].filter((name) => name !== undefined);
874
- return [...new Set(names)].map((name) => `${name} (${device.vlans?.[name]?.id ?? "?"})`);
875
- };
876
- for (const device of devices) {
877
- for (const [portName, port] of Object.entries(device.ports ?? {})) {
878
- const link = port?.link;
879
- if (!port || !link)
880
- continue;
881
- const here = `${device.name} ${portName}`;
882
- const there = `${link.device} ${link.port}`;
883
- if (drawn.has(`${there}|${here}`))
884
- continue;
885
- drawn.add(`${here}|${there}`);
886
- const farPort = byName.get(link.device)?.ports?.[link.port];
887
- const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
888
- cables.push({
889
- device: device.name,
890
- port: portName,
891
- farDevice: link.device,
892
- farPort: link.port,
893
- ...port.lag ? { lag: port.lag } : {},
894
- ...farPort?.lag ? { farLag: farPort.lag } : {},
895
- ...port.description ? { description: port.description } : {},
896
- ...link.through ? { through: link.through } : {},
897
- vlans: vlansOf(device, portName),
898
- ...linksBack ? {} : { oneSided: true }
899
- });
900
- }
901
- }
902
- return cables;
903
- }
904
- static schemaDocs() {
905
- const root = StudioModel.packageRoot();
906
- if (!root)
907
- return {};
908
- const sources = SchemaDocs.FILES.map(([file]) => readFileSync(join(root, "src", "schema", file), "utf8"));
909
- const docs = {};
910
- for (const type of SchemaDocs.read(sources).flat()) {
911
- docs[type.name] = {
912
- description: type.description,
913
- includes: (type.includes ?? []).map((name) => name.replace(/<.*$/, "")),
914
- fields: Object.fromEntries(type.fields.map((field) => [field.path.replace(/\?$/, ""), { type: field.type, description: field.description }]))
915
- };
916
- }
917
- return docs;
918
- }
919
- }
920
-
921
- export { Topology, AddressSpace, StudioModel };