@takodotid/circuit 0.34.3 → 0.36.0

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 (240) hide show
  1. package/README.md +14 -15
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11345 -9024
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +18 -12
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -0,0 +1,91 @@
1
+ // How good the path to a device is, measured before an apply sends anything. A path that drops packets is how a change loses the device halfway.
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { isIPv6 } from "node:net";
5
+
6
+ /** What the pings found: how many were sent, and the round trip of each that came back, in milliseconds. */
7
+ export type PathMeasure = { sent: number; times: number[] };
8
+
9
+ /** Whether an apply may go ahead over the path, and a line that says why. */
10
+ export type PathVerdict = { refuse: boolean; summary: string };
11
+
12
+ export class PathCheck {
13
+ /** Pings sent in all. With 20, one lost is 5%. */
14
+ static readonly PINGS = 20;
15
+
16
+ /** Ping programs run side by side, so the check takes about five seconds on every system: each sends one ping a second. */
17
+ private static readonly RUNS = 4;
18
+
19
+ /** The most loss allowed, in percent: one of 20 pings may be lost, two may not. */
20
+ static readonly MAX_LOSS_PERCENT = 5;
21
+
22
+ /** The highest average round trip allowed, in milliseconds. */
23
+ static readonly MAX_AVERAGE_MS = 300;
24
+
25
+ /** How long one ping program may run before it is stopped and what it printed is counted. */
26
+ private static readonly LIMIT_MS = 15_000;
27
+
28
+ /**
29
+ * The round trip of every reply in what `ping` printed, in milliseconds. A reply is the one line with a time after `=` or `<` and before `ms`, as in `time=12 ms` or `time<1ms`: the same in every language `ping` prints in. A summary line has several times, or none right before `ms`, and is not counted.
30
+ */
31
+ static parse(output: string): number[] {
32
+ const times: number[] = [];
33
+
34
+ for (const line of output.split(/\r?\n/)) {
35
+ const found = [...line.matchAll(/[=<]\s*(\d+(?:[.,]\d+)?)\s*ms\b/gi)];
36
+ if (found.length === 1) times.push(Number(found[0]![1]!.replace(",", ".")));
37
+ }
38
+
39
+ return times;
40
+ }
41
+
42
+ /** Whether the path is good enough to apply over. A device that answers no ping at all may only block it, so that alone is not refused. */
43
+ static verdict(host: string, measure: PathMeasure): PathVerdict {
44
+ const received = Math.min(measure.times.length, measure.sent);
45
+
46
+ if (!received) return { refuse: false, summary: `${host} answers no ping, so the path to it was not measured` };
47
+
48
+ const lost = measure.sent - received;
49
+ const lossPercent = Math.round((lost / measure.sent) * 100);
50
+ const average = Math.round(measure.times.reduce((sum, time) => sum + time, 0) / measure.times.length);
51
+ const summary = `${host}: ${lost} of ${measure.sent} pings lost (${lossPercent}%), ${average} ms on average`;
52
+
53
+ const refuse = lossPercent > PathCheck.MAX_LOSS_PERCENT || average > PathCheck.MAX_AVERAGE_MS;
54
+ return { refuse, summary };
55
+ }
56
+
57
+ /** Ping a host with the system's `ping`. Undefined when there is no `ping` to run. */
58
+ static async measure(host: string): Promise<PathMeasure | undefined> {
59
+ const count = PathCheck.PINGS / PathCheck.RUNS;
60
+ const outputs = await Promise.all(Array.from({ length: PathCheck.RUNS }, () => PathCheck.ping(host, count)));
61
+ if (outputs.some((output) => output === undefined)) return undefined;
62
+
63
+ // A duplicate reply is printed as a reply too; no run counts more replies than it sent.
64
+ const times = outputs.flatMap((output) => PathCheck.parse(output!).slice(0, count));
65
+ return { sent: PathCheck.PINGS, times };
66
+ }
67
+
68
+ /** What one run of `ping` printed. Windows takes `-n` and a wait per ping in milliseconds; Linux and macOS take `-c`, and macOS pings IPv6 with `ping6`. */
69
+ private static ping(host: string, count: number): Promise<string | undefined> {
70
+ const windows = process.platform === "win32";
71
+ const program = !windows && process.platform === "darwin" && isIPv6(host) ? "ping6" : "ping";
72
+ const args = windows ? ["-n", String(count), "-w", "1000", host] : ["-c", String(count), host];
73
+
74
+ return new Promise((done) => {
75
+ const child = spawn(program, args, { windowsHide: true });
76
+ let output = "";
77
+
78
+ const limit = setTimeout(() => child.kill(), PathCheck.LIMIT_MS);
79
+
80
+ child.stdout.on("data", (chunk: Buffer) => (output += chunk.toString("utf8")));
81
+ child.on("error", () => {
82
+ clearTimeout(limit);
83
+ done(undefined);
84
+ });
85
+ child.on("close", () => {
86
+ clearTimeout(limit);
87
+ done(output);
88
+ });
89
+ });
90
+ }
91
+ }
@@ -12,121 +12,125 @@ export type Login = {
12
12
  hostKeys?: string[];
13
13
  };
14
14
 
15
- /** The address SSH reaches a device at. A device without one is reached only through its console. */
16
- export function managementHost(device: Device): string {
17
- if (!device.connection.host) throw new Error(`${device.name} has no management address; it is reached through its console`);
18
- return device.connection.host;
19
- }
15
+ export class Ssh {
16
+ /** The address SSH reaches a device at. A device without one is reached only through its console. */
17
+ static hostOf(device: Device): string {
18
+ if (!device.connection.host) throw new Error(`${device.name} has no management address; it is reached through its console`);
19
+ return device.connection.host;
20
+ }
21
+
22
+ /** Open a session. */
23
+ static connect(login: Login): Promise<Client> {
24
+ return new Promise((resolve, reject) => {
25
+ const client = new Client();
26
+
27
+ client
28
+ .on("ready", () => resolve(client))
29
+ .on("error", reject)
30
+ .connect({
31
+ host: login.host,
32
+ port: login.port ?? 22,
33
+ username: login.user,
34
+ password: login.password,
35
+ readyTimeout: 30_000,
36
+ ...(login.hostKeys ? { algorithms: { serverHostKey: login.hostKeys as never } } : {}),
37
+ });
38
+ });
39
+ }
20
40
 
21
- /** Open a session. */
22
- export function connect(login: Login): Promise<Client> {
23
- return new Promise((resolve, reject) => {
24
- const client = new Client();
25
-
26
- client
27
- .on("ready", () => resolve(client))
28
- .on("error", reject)
29
- .connect({
30
- host: login.host,
31
- port: login.port ?? 22,
32
- username: login.user,
33
- password: login.password,
34
- readyTimeout: 30_000,
35
- ...(login.hostKeys ? { algorithms: { serverHostKey: login.hostKeys as never } } : {}),
36
- });
37
- });
38
- }
41
+ /** Run one command and return what it printed, stderr included. */
42
+ static exec(client: Client, command: string, timeout = 120_000): Promise<string> {
43
+ return new Promise((resolve, reject) => {
44
+ client.exec(command, (error, stream) => {
45
+ if (error) return reject(error);
39
46
 
40
- /** Run one command and return what it printed, stderr included. */
41
- export function exec(client: Client, command: string, timeout = 120_000): Promise<string> {
42
- return new Promise((resolve, reject) => {
43
- client.exec(command, (error, stream) => {
44
- if (error) return reject(error);
47
+ let output = "";
48
+ const append = (chunk: Buffer) => (output += chunk.toString("utf8"));
49
+ const timer = setTimeout(() => resolve(output.trim()), timeout);
45
50
 
46
- let output = "";
47
- const append = (chunk: Buffer) => (output += chunk.toString("utf8"));
48
- const timer = setTimeout(() => resolve(output.trim()), timeout);
51
+ stream.on("data", append);
52
+ stream.stderr.on("data", append);
49
53
 
50
- stream.on("data", append);
51
- stream.stderr.on("data", append);
52
-
53
- stream.on("close", () => {
54
- clearTimeout(timer);
55
- resolve(output.trim());
54
+ stream.on("close", () => {
55
+ clearTimeout(timer);
56
+ resolve(output.trim());
57
+ });
56
58
  });
57
59
  });
58
- });
59
- }
60
+ }
60
61
 
61
- /** An interactive shell, for platforms that take configuration only there. */
62
- export function shell(client: Client, columns = 200, rows = 1000): Promise<ClientChannel> {
63
- return new Promise((resolve, reject) => {
64
- client.shell({ cols: columns, rows }, (error, channel) => (error ? reject(error) : resolve(channel)));
65
- });
66
- }
62
+ /** An interactive shell, for platforms that take configuration only there. */
63
+ static shell(client: Client, columns = 200, rows = 1000): Promise<ClientChannel> {
64
+ return new Promise((resolve, reject) => {
65
+ client.shell({ cols: columns, rows }, (error, channel) => (error ? reject(error) : resolve(channel)));
66
+ });
67
+ }
67
68
 
68
- /** Collect output until `done` holds, then wait `settle` for stragglers, or give up at `timeout`. */
69
- export function readUntil(channel: ClientChannel, done: (output: string) => boolean, timeout: number, settle = 400): Promise<string> {
70
- return new Promise((resolve) => {
71
- let output = "";
72
- let finished = false;
69
+ /** Collect output until `done` holds, then wait `settle` for stragglers, or give up at `timeout`. */
70
+ static readUntil(channel: ClientChannel, done: (output: string) => boolean, timeout: number, settle = 400): Promise<string> {
71
+ return new Promise((resolve) => {
72
+ let output = "";
73
+ let finished = false;
73
74
 
74
- const finish = () => {
75
- if (finished) return;
75
+ const finish = () => {
76
+ if (finished) return;
76
77
 
77
- finished = true;
78
- channel.removeListener("data", onData);
79
- resolve(output);
80
- };
78
+ finished = true;
79
+ channel.removeListener("data", onData);
80
+ resolve(output);
81
+ };
81
82
 
82
- const onData = (chunk: Buffer) => {
83
- output += chunk.toString("utf8");
84
- if (done(output)) setTimeout(finish, settle);
85
- };
83
+ const onData = (chunk: Buffer) => {
84
+ output += chunk.toString("utf8");
85
+ if (done(output)) setTimeout(finish, settle);
86
+ };
86
87
 
87
- channel.on("data", onData);
88
- setTimeout(finish, timeout);
89
- });
90
- }
88
+ channel.on("data", onData);
89
+ setTimeout(finish, timeout);
90
+ });
91
+ }
91
92
 
92
- /** Send one line into an interactive shell and collect output until it goes quiet for `settle`. */
93
- export function send(channel: ClientChannel, line: string, timeout = 20_000, settle = 1500): Promise<string> {
94
- channel.write(line + "\n");
93
+ /** Send one line into an interactive shell and collect output until it goes quiet for `settle`. */
94
+ static send(channel: ClientChannel, line: string, timeout = 20_000, settle = 1500): Promise<string> {
95
+ channel.write(line + "\n");
95
96
 
96
- return new Promise((resolve) => {
97
- let output = "";
98
- let timer: ReturnType<typeof setTimeout>;
97
+ return new Promise((resolve) => {
98
+ let output = "";
99
+ let timer: ReturnType<typeof setTimeout>;
99
100
 
100
- const finish = () => {
101
- channel.removeListener("data", onData);
102
- resolve(output);
103
- };
101
+ const finish = () => {
102
+ channel.removeListener("data", onData);
103
+ resolve(output);
104
+ };
104
105
 
105
- const onData = (chunk: Buffer) => {
106
- output += chunk.toString("utf8");
107
- clearTimeout(timer);
108
- timer = setTimeout(finish, settle);
109
- };
106
+ const onData = (chunk: Buffer) => {
107
+ output += chunk.toString("utf8");
108
+ clearTimeout(timer);
109
+ timer = setTimeout(finish, settle);
110
+ };
110
111
 
111
- channel.on("data", onData);
112
- timer = setTimeout(finish, timeout);
113
- });
114
- }
112
+ channel.on("data", onData);
113
+ timer = setTimeout(finish, timeout);
114
+ });
115
+ }
115
116
 
116
- export const sleep = (milliseconds: number) => new Promise((done) => setTimeout(done, milliseconds));
117
+ static sleep(milliseconds: number): Promise<unknown> {
118
+ return new Promise((done) => setTimeout(done, milliseconds));
119
+ }
117
120
 
118
- /** Write a file to the device over SFTP. */
119
- export function upload(client: Client, path: string, content: string | Buffer): Promise<void> {
120
- return new Promise((resolve, reject) => {
121
- client.sftp((error, sftp) => {
122
- if (error) return reject(error);
121
+ /** Write a file to the device over SFTP. */
122
+ static upload(client: Client, path: string, content: string | Buffer): Promise<void> {
123
+ return new Promise((resolve, reject) => {
124
+ client.sftp((error, sftp) => {
125
+ if (error) return reject(error);
123
126
 
124
- sftp.writeFile(path, content, (writeError) => {
125
- sftp.end();
127
+ sftp.writeFile(path, content, (writeError) => {
128
+ sftp.end();
126
129
 
127
- if (writeError) reject(writeError);
128
- else resolve();
130
+ if (writeError) reject(writeError);
131
+ else resolve();
132
+ });
129
133
  });
130
134
  });
131
- });
135
+ }
132
136
  }
@@ -1,16 +1,19 @@
1
1
  // Rules of your own. `circuit validate` runs them beside Circuit's own, and an error stops `diff` and `apply`.
2
2
 
3
3
  import type { Check, Finding } from "@takodotid/circuit";
4
- import { contains } from "@takodotid/circuit";
5
- import { exportsEndInReject, trustBoundary } from "@takodotid/circuit/presets";
4
+ import { Address } from "@takodotid/circuit";
5
+ import { Checks } from "@takodotid/circuit/helpers";
6
6
  import { TENANTS } from "./tenants";
7
7
 
8
8
  export default [
9
9
  // Nothing leaves by accident: every export ends by rejecting what it did not accept.
10
- exportsEndInReject,
10
+ Checks.exportsEndInReject,
11
11
 
12
12
  // Traffic from the transit and the exchange reaches the tenants only through the router, which filters it.
13
- trustBoundary({ untrusted: ["transit", "ix"], routers: ["edge-01"] }),
13
+ Checks.trustBoundary({ untrusted: ["transit", "ix"], routers: ["edge-01"] }),
14
+
15
+ // A native VLAN stays on one trunk and nowhere else, or it joins every trunk it is native on.
16
+ Checks.nativeVlansAreDeadEnds,
14
17
 
15
18
  // Tenants never share a name, a number, an address or a port. A port given twice would go to the last tenant alone.
16
19
  () => {
@@ -23,7 +26,9 @@ export default [
23
26
  if (other.number === t.number) error(`tenants ${t.name} and ${other.name} share number ${t.number}`);
24
27
 
25
28
  for (const block of t.blocks) {
26
- const clash = other.blocks.find(({ prefix }) => contains(prefix, block.prefix) || contains(block.prefix, prefix));
29
+ const clash = other.blocks.find(
30
+ ({ prefix }) => Address.contains(prefix, block.prefix) || Address.contains(block.prefix, prefix)
31
+ );
27
32
  if (clash) error(`${block.prefix} of ${t.name} overlaps ${clash.prefix} of ${other.name}`);
28
33
  }
29
34
  }
@@ -36,7 +41,7 @@ export default [
36
41
  }
37
42
 
38
43
  for (const { prefix, gateway } of t.blocks) {
39
- if (!contains(prefix, gateway)) error(`the gateway ${gateway} of ${t.name} is outside ${prefix}`);
44
+ if (!Address.contains(prefix, gateway)) error(`the gateway ${gateway} of ${t.name} is outside ${prefix}`);
40
45
  }
41
46
  });
42
47
 
@@ -3,8 +3,8 @@
3
3
  // Hurricane Electric and SGIX stand for your own transit and exchange. Addresses are from the documentation ranges; use the ones your providers give you.
4
4
 
5
5
  import type { Prefix } from "@takodotid/circuit";
6
- import { defineDevice, merge, secret } from "@takodotid/circuit";
7
- import { antiSpoofing, badTcpFlags, bgpSanity, MARTIANS, PRIVATE_RANGES } from "@takodotid/circuit/presets";
6
+ import { defineDevice, secret, Utils } from "@takodotid/circuit";
7
+ import { Bgp, Filters } from "@takodotid/circuit/helpers";
8
8
  import { ASN, communities } from "./routing";
9
9
  import { edgeRules, gateways, internetVlanNames, internetVlans } from "./tenancy";
10
10
  import { TENANTS } from "./tenants";
@@ -16,7 +16,7 @@ const OURS = ["198.51.100.0/24", "2001:db8:100::/48"] as const satisfies readonl
16
16
  const MANAGEMENT = "10.0.0.0/24" satisfies Prefix;
17
17
 
18
18
  /** Sources that never arrive from the internet: our own space, and addresses that are not on the internet at all. */
19
- const SPOOFED = [OURS[0], ...MARTIANS, ...PRIVATE_RANGES];
19
+ const SPOOFED = [OURS[0], ...Filters.MARTIANS, ...Filters.PRIVATE_RANGES];
20
20
 
21
21
  export default defineDevice({
22
22
  name: "edge-01",
@@ -28,7 +28,7 @@ export default defineDevice({
28
28
  system: { timezone: "Asia/Jakarta", ntp: { servers: ["pool.ntp.org"] } },
29
29
  management: { allow: [MANAGEMENT], ssh: {} },
30
30
 
31
- vlans: merge(
31
+ vlans: Utils.merge(
32
32
  {
33
33
  mgmt: { id: 99, description: "Management of the switches" },
34
34
  transit: { id: 300, description: "IP transit from Hurricane Electric, AS6939" },
@@ -49,7 +49,7 @@ export default defineDevice({
49
49
  },
50
50
  },
51
51
 
52
- interfaces: merge(
52
+ interfaces: Utils.merge(
53
53
  {
54
54
  mgmt: { type: "vlan", vlan: "mgmt", addresses: ["10.0.99.1/24"] },
55
55
  transit: { type: "vlan", vlan: "transit", addresses: ["203.0.113.2/30"] },
@@ -95,7 +95,7 @@ export default defineDevice({
95
95
  },
96
96
 
97
97
  policies: {
98
- SANITY: bgpSanity(),
98
+ SANITY: Bgp.sanity(),
99
99
  // A route from the exchange is preferred over the same route from the transit: higher local preference wins.
100
100
  "TRANSIT-IN": [{ call: "SANITY" }, communities.tag("transit", 6939), { set: { local_pref: 100 } }, { action: "accept" }],
101
101
  "IX-IN": [{ call: "SANITY" }, communities.tag("exchange", 55518), { set: { local_pref: 200 } }, { action: "accept" }],
@@ -117,15 +117,15 @@ export default defineDevice({
117
117
  { description: "BGP", match: { protocol: "tcp", dst_port: 179, src_set: "bgp-neighbors" }, action: "accept" },
118
118
  ],
119
119
  },
120
- forward: { rules: [...badTcpFlags()] },
120
+ forward: { rules: [...Filters.badTcpFlags()] },
121
121
  },
122
122
  },
123
123
 
124
124
  // In the switch chip, before a packet reaches the router's CPU: spoofed sources first, then each tenant's own rules.
125
125
  acls: {
126
126
  edge: [
127
- ...antiSpoofing("transit", SPOOFED),
128
- ...antiSpoofing("ix", SPOOFED),
127
+ ...Filters.antiSpoofing("transit", SPOOFED),
128
+ ...Filters.antiSpoofing("ix", SPOOFED),
129
129
  ...edgeRules(TENANTS, "transit"),
130
130
  ...edgeRules(TENANTS, "ix"),
131
131
  ],
@@ -1,12 +1,12 @@
1
1
  // What your network is on the internet: its AS number and its BGP communities.
2
2
 
3
- import { communityScheme } from "@takodotid/circuit/presets";
3
+ import { Bgp } from "@takodotid/circuit/helpers";
4
4
 
5
5
  /** Your AS number. */
6
6
  export const ASN = 64500;
7
7
 
8
8
  /** What your BGP communities mean. The router tags each route it learns with them, and `circuit communities` publishes them. */
9
- export const communities = communityScheme({
9
+ export const communities = Bgp.communityScheme({
10
10
  asn: ASN,
11
11
  learned_from: { function: 1, classes: { transit: 1, exchange: 2 } },
12
12
  learned_from_as: 3,
@@ -1,7 +1,7 @@
1
1
  // What a tenant is, and what each device carries for it. A tenant's file says what is the tenant's own; its VLANs and names follow from its name and number.
2
2
 
3
3
  import type { AclRule, Interface, IP, Port, Prefix, Vlan } from "@takodotid/circuit";
4
- import { familyOf } from "@takodotid/circuit";
4
+ import { Address } from "@takodotid/circuit";
5
5
 
6
6
  /** A tenant's networks. `internet` carries its servers' public addresses and reaches the router. `private` connects its own servers to each other and stays on the switches. */
7
7
  type TenantNetwork = "internet" | "private";
@@ -75,7 +75,7 @@ export function gateways<N extends string>(tenants: readonly Tenant<N>[]) {
75
75
  type: "vlan",
76
76
  vlan: internetVlan(t),
77
77
  description: `Tenant ${t.name}`,
78
- addresses: t.blocks.map(({ prefix, gateway }) => `${gateway}/${prefix.split("/")[1]}`),
78
+ addresses: t.blocks.map(({ prefix, gateway }): Prefix => `${gateway}/${Address.split(prefix).len}`),
79
79
  };
80
80
  }
81
81
 
@@ -87,7 +87,7 @@ export const edgeRules = <V extends string>(tenants: readonly Tenant[], vlan: V)
87
87
  tenants.flatMap((t) =>
88
88
  (t.acl ?? []).flatMap((rule) =>
89
89
  t.blocks
90
- .filter(({ prefix }) => familyOf(prefix) === "ipv4")
90
+ .filter(({ prefix }) => Address.family(prefix) === "ipv4")
91
91
  .map(({ prefix }) => ({
92
92
  ...rule,
93
93
  ...(rule.description ? { description: `${vlan}: ${rule.description}` } : {}),
@@ -1,6 +1,6 @@
1
1
  // The top-of-rack switch: the tenants' servers, and one uplink to the router.
2
2
 
3
- import { defineDevice, merge, secret } from "@takodotid/circuit";
3
+ import { defineDevice, secret, Utils } from "@takodotid/circuit";
4
4
  import { internetVlanNames, internetVlans, portsOn, privateVlans } from "./tenancy";
5
5
  import { TENANTS } from "./tenants";
6
6
 
@@ -17,10 +17,10 @@ export default defineDevice({
17
17
  lldp: true,
18
18
  stp: {},
19
19
 
20
- vlans: merge({ mgmt: { id: 99, description: "Management" } }, internetVlans(TENANTS), privateVlans(TENANTS, "tor-01")),
20
+ vlans: Utils.merge({ mgmt: { id: 99, description: "Management" } }, internetVlans(TENANTS), privateVlans(TENANTS, "tor-01")),
21
21
 
22
22
  // Every tenant's ports, from its file in tenants/, beside the switch's own.
23
- ports: merge(portsOn(TENANTS, "tor-01"), {
23
+ ports: Utils.merge(portsOn(TENANTS, "tor-01"), {
24
24
  "40g-1": {
25
25
  description: "edge-01",
26
26
  trunk_vlans: ["mgmt", ...internetVlanNames(TENANTS)],
@@ -1,19 +1,19 @@
1
1
  // Rules of your own. `circuit validate` runs them beside Circuit's own, and an error stops `diff` and `apply`.
2
2
 
3
3
  import type { Check } from "@takodotid/circuit";
4
- import { exportsEndInReject, neighborPolicies, trustBoundary } from "@takodotid/circuit/presets";
4
+ import { Bgp, Checks } from "@takodotid/circuit/helpers";
5
5
 
6
6
  export default [
7
7
  // Nothing leaves by accident: every export ends by rejecting what it did not accept.
8
- exportsEndInReject,
8
+ Checks.exportsEndInReject,
9
9
 
10
10
  // Traffic from the transit and the exchange reaches the rest of the network only through the router.
11
- trustBoundary({ untrusted: ["transit", "ix"], routers: ["edge-01"] }),
11
+ Checks.trustBoundary({ untrusted: ["transit", "ix"], routers: ["edge-01"] }),
12
12
 
13
13
  // Every BGP session filters what it takes in.
14
14
  (devices) =>
15
15
  devices.flatMap((device) =>
16
- neighborPolicies(device)
16
+ Bgp.neighborPolicies(device)
17
17
  .filter((neighbor) => !neighbor.import)
18
18
  .map((neighbor) => ({ level: "error" as const, device: device.name, message: `${neighbor.name} has no import policy` }))
19
19
  ),
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { Prefix } from "@takodotid/circuit";
6
6
  import { defineDevice, secret } from "@takodotid/circuit";
7
- import { antiSpoofing, badTcpFlags, bgpSanity, MARTIANS, PRIVATE_RANGES } from "@takodotid/circuit/presets";
7
+ import { Bgp, Filters } from "@takodotid/circuit/helpers";
8
8
  import { ASN, communities } from "./routing";
9
9
 
10
10
  /** The address space this router announces. */
@@ -14,7 +14,7 @@ const OURS = ["198.51.100.0/24", "2001:db8:100::/48"] as const satisfies readonl
14
14
  const MANAGEMENT = "10.0.0.0/24" satisfies Prefix;
15
15
 
16
16
  /** Sources that never arrive from the internet: our own space, and addresses that are not on the internet at all. */
17
- const SPOOFED = [OURS[0], ...MARTIANS, ...PRIVATE_RANGES];
17
+ const SPOOFED = [OURS[0], ...Filters.MARTIANS, ...Filters.PRIVATE_RANGES];
18
18
 
19
19
  export default defineDevice({
20
20
  name: "edge-01",
@@ -79,7 +79,7 @@ export default defineDevice({
79
79
  },
80
80
 
81
81
  policies: {
82
- SANITY: bgpSanity(),
82
+ SANITY: Bgp.sanity(),
83
83
  // A route from the exchange is preferred over the same route from the transit: higher local preference wins.
84
84
  "TRANSIT-IN": [{ call: "SANITY" }, communities.tag("transit", 6939), { set: { local_pref: 100 } }, { action: "accept" }],
85
85
  "IX-IN": [{ call: "SANITY" }, communities.tag("exchange", 55518), { set: { local_pref: 200 } }, { action: "accept" }],
@@ -101,10 +101,10 @@ export default defineDevice({
101
101
  { description: "BGP", match: { protocol: "tcp", dst_port: 179, src_set: "bgp-neighbors" }, action: "accept" },
102
102
  ],
103
103
  },
104
- forward: { rules: [...badTcpFlags()] },
104
+ forward: { rules: [...Filters.badTcpFlags()] },
105
105
  },
106
106
  },
107
107
 
108
108
  // In the switch chip, before a packet reaches the router's CPU.
109
- acls: { edge: [...antiSpoofing("transit", SPOOFED), ...antiSpoofing("ix", SPOOFED)] },
109
+ acls: { edge: [...Filters.antiSpoofing("transit", SPOOFED), ...Filters.antiSpoofing("ix", SPOOFED)] },
110
110
  });
@@ -1,12 +1,12 @@
1
1
  // What your network is on the internet: its AS number and its BGP communities.
2
2
 
3
- import { communityScheme } from "@takodotid/circuit/presets";
3
+ import { Bgp } from "@takodotid/circuit/helpers";
4
4
 
5
5
  /** Your AS number. */
6
6
  export const ASN = 64500;
7
7
 
8
8
  /** What your BGP communities mean. The router tags each route it learns with them, and `circuit communities` publishes them. */
9
- export const communities = communityScheme({
9
+ export const communities = Bgp.communityScheme({
10
10
  asn: ASN,
11
11
  learned_from: { function: 1, classes: { transit: 1, exchange: 2 } },
12
12
  learned_from_as: 3,
@@ -22,6 +22,9 @@ What each field means is written as a comment on the field, in `node_modules/@ta
22
22
  | `npx circuit diff [device]` | Shows what would change, compared with the last snapshot | no |
23
23
  | `npx circuit build <device>` | Prints the full configuration the device should run | no |
24
24
  | `npx circuit snapshot [device]` | Reads what each device runs now into `.circuit/state/` | no, only reads |
25
+ | `npx circuit show <device> '<command>'` | Runs commands that only read on the device, secrets hidden | no, only reads |
26
+ | `npx circuit bgp [device] [name-regex]` | Each BGP neighbor in the config next to its live session: up or down | no, only reads |
27
+ | `npx circuit console <device> '<line>'` | Sends lines to the device's serial console, unchecked | **can** |
25
28
  | `npx circuit apply <device>` | Reads the device, then shows what would change on it | no |
26
29
  | `npx circuit apply <device> --confirm` | Sends the change to the device | **yes** |
27
30
  | `npx circuit secrets` | Lists every secret the config uses, and whether each one can be read | no |
@@ -41,7 +44,8 @@ What each field means is written as a comment on the field, in `node_modules/@ta
41
44
 
42
45
  ## Rules
43
46
 
44
- - Never run `apply --confirm`, `refresh --confirm` or `peeringdb --confirm` without the owner's explicit approval for that run.
47
+ - Never run `apply --confirm`, `refresh --confirm` or `peeringdb --confirm`, or `console` with a line that changes something, without the owner's explicit approval for that run.
45
48
  - If a plan removes something you did not expect, stop and ask. It usually means the file is missing something the device needs.
46
49
  - Be careful with anything that carries your own access to the device: the management address, the management VLAN, the uplink, `management.allow`, and the login user. A mistake there locks everyone out.
50
+ - A platform supports only the OS versions listed at https://circuit.tako.id/platforms/. Before the first apply to a device, check that it runs one of them.
47
51
  - When you are not sure what a field does, or what the owner wants, ask instead of guessing.
@@ -1,7 +1,7 @@
1
1
  // Rules of your own. `circuit validate` runs them beside Circuit's own, and an error stops `diff` and `apply`.
2
2
 
3
3
  import type { Check } from "@takodotid/circuit";
4
- import { vlansOf } from "@takodotid/circuit/presets";
4
+ import { Checks } from "@takodotid/circuit/helpers";
5
5
 
6
6
  export default [
7
7
  // A port without a description is hard to trace later. A warning, so it does not block anything.
@@ -16,7 +16,7 @@ export default [
16
16
  (devices) =>
17
17
  devices.flatMap((device) =>
18
18
  Object.entries(device.ports ?? {})
19
- .filter(([, port]) => port?.access_vlan && vlansOf(port).includes("mgmt"))
19
+ .filter(([, port]) => port?.access_vlan && Checks.vlansOf(port).includes("mgmt"))
20
20
  .map(([name]) => ({
21
21
  level: "error" as const,
22
22
  device: device.name,