@takodotid/circuit 0.34.3 → 0.35.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 +13 -14
  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 +17 -11
  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,173 @@
1
+ // Keeps a network's exchange presence at PeeringDB in step with the config. Each interface with an `exchange` naming a `peeringdb_ixlan` is one record.
2
+ //
3
+ // Only globally reachable addresses are published: an exchange reached over a tunnel often lands on private or shared space, and publishing it would point others at a dead end. A record the config does not hold is reported, never deleted, and `operational` is never changed, since leaving an exchange is a decision made outside the config.
4
+
5
+ import { Address } from "../core/address";
6
+ import type { Network } from "../core/define";
7
+ import { Secrets } from "../core/secrets";
8
+ import type { Device, Interface } from "../schema";
9
+
10
+ /** A network's presence at one exchange LAN, as PeeringDB holds it. */
11
+ type NetIxLan = {
12
+ id: number;
13
+ net_id: number;
14
+ ixlan_id: number;
15
+ name: string;
16
+ speed: number;
17
+ ipaddr4: string | null;
18
+ ipaddr6: string | null;
19
+ is_rs_peer: boolean;
20
+ operational: boolean;
21
+ };
22
+
23
+ /** One record as the config describes it. */
24
+ type Wanted = {
25
+ name: string;
26
+ ixlan_id: number;
27
+ speed: number;
28
+ ipaddr4: string | null;
29
+ ipaddr6: string | null;
30
+ is_rs_peer: boolean;
31
+ };
32
+
33
+ /** A change to send. `id` is set when the record exists and is updated in place. */
34
+ export type PeeringDbChange = {
35
+ summary: string;
36
+ id?: number;
37
+ body: Record<string, unknown>;
38
+ };
39
+
40
+ export type PeeringDbPlan = {
41
+ changes: PeeringDbChange[];
42
+ /** Records PeeringDB holds that the config does not. */
43
+ unknown: string[];
44
+ };
45
+
46
+ type AnyInterface = Interface<string, string>;
47
+
48
+ export class PeeringDb {
49
+ private static readonly API = "https://www.peeringdb.com/api";
50
+
51
+ /** What would change at PeeringDB. Reads the public API; needs no key. */
52
+ static async plan(network: Network): Promise<PeeringDbPlan> {
53
+ if (!network.asn) throw new Error("the network declares no asn");
54
+
55
+ const response = await fetch(`${PeeringDb.API}/netixlan?asn=${network.asn}`, {
56
+ headers: { Accept: "application/json" },
57
+ signal: AbortSignal.timeout(30_000),
58
+ });
59
+ if (!response.ok) throw new Error(`PeeringDB answered ${response.status}`);
60
+
61
+ const existing = ((await response.json()) as { data: NetIxLan[] }).data;
62
+ const desired = PeeringDb.wanted(network);
63
+
64
+ // The record that owns every presence. Taken from an existing one, since the API has no other lookup by ASN here.
65
+ const netId = existing[0]?.net_id;
66
+ const changes: PeeringDbChange[] = [];
67
+
68
+ for (const record of desired) {
69
+ const current = existing.find((candidate) => candidate.ixlan_id === record.ixlan_id);
70
+
71
+ const body = {
72
+ net_id: netId,
73
+ ixlan_id: record.ixlan_id,
74
+ asn: network.asn,
75
+ speed: record.speed,
76
+ ipaddr4: record.ipaddr4,
77
+ ipaddr6: record.ipaddr6,
78
+ is_rs_peer: record.is_rs_peer,
79
+ operational: current?.operational ?? true,
80
+ };
81
+
82
+ if (!current) {
83
+ changes.push({ summary: `+ ${record.name}: ${record.ipaddr4}, ${record.ipaddr6}, ${record.speed} Mbit/s`, body });
84
+ continue;
85
+ }
86
+
87
+ const differs =
88
+ current.ipaddr4 !== record.ipaddr4 ||
89
+ current.ipaddr6 !== record.ipaddr6 ||
90
+ current.speed !== record.speed ||
91
+ current.is_rs_peer !== record.is_rs_peer;
92
+
93
+ if (differs) {
94
+ const before = `${current.ipaddr4}, ${current.ipaddr6}, ${current.speed} Mbit/s, route server ${current.is_rs_peer}`;
95
+ const after = `${record.ipaddr4}, ${record.ipaddr6}, ${record.speed} Mbit/s, route server ${record.is_rs_peer}`;
96
+ changes.push({ summary: `~ ${record.name}: ${before} to ${after}`, id: current.id, body });
97
+ }
98
+ }
99
+
100
+ const unknown = existing
101
+ .filter((record) => !desired.some((candidate) => candidate.ixlan_id === record.ixlan_id))
102
+ .map((record) => record.name);
103
+
104
+ if (changes.some((change) => !change.id) && !netId) {
105
+ throw new Error("PeeringDB holds no presence for this network yet, so its record id is unknown; add the first one by hand");
106
+ }
107
+
108
+ return { changes, unknown };
109
+ }
110
+
111
+ /** Send the changes, with the network's API key. */
112
+ static async send(network: Network, changes: PeeringDbChange[]): Promise<void> {
113
+ if (!network.peeringdb) throw new Error("the network declares no peeringdb api_key");
114
+ const apiKey = Secrets.reveal(network.peeringdb.api_key);
115
+
116
+ for (const change of changes) {
117
+ const response = await fetch(`${PeeringDb.API}/netixlan${change.id ? `/${change.id}` : ""}`, {
118
+ method: change.id ? "PUT" : "POST",
119
+ headers: { Authorization: `Api-Key ${apiKey}`, "Content-Type": "application/json" },
120
+ body: JSON.stringify(change.body),
121
+ signal: AbortSignal.timeout(30_000),
122
+ });
123
+
124
+ if (!response.ok) {
125
+ const reason = (await response.text()).slice(0, 300);
126
+ throw new Error(`${change.summary}: PeeringDB answered ${response.status} ${reason}`);
127
+ }
128
+
129
+ console.log(` sent ${change.summary}`);
130
+ }
131
+ }
132
+
133
+ private static addressesOf(iface: AnyInterface): string[] {
134
+ return (iface.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
135
+ }
136
+
137
+ /** Whether the device peers with a route server on this interface's subnets. */
138
+ private static peersWithRouteServer(device: Device, iface: AnyInterface): boolean {
139
+ const bgp = device.routing?.bgp;
140
+ const subnets = PeeringDb.addressesOf(iface);
141
+
142
+ return Object.values(bgp?.neighbors ?? {}).some((neighbor) => {
143
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
144
+ const role = neighbor.role ?? group?.role;
145
+ return role === "rs" && subnets.some((subnet) => Address.contains(subnet, neighbor.address));
146
+ });
147
+ }
148
+
149
+ /** Every exchange presence the config declares with a PeeringDB exchange LAN. */
150
+ private static wanted(network: Network): Wanted[] {
151
+ const records: Wanted[] = [];
152
+
153
+ for (const device of network.devices) {
154
+ for (const iface of Object.values(device.interfaces ?? {})) {
155
+ const exchange = iface.exchange;
156
+ if (!exchange?.peeringdb_ixlan) continue;
157
+
158
+ const published = PeeringDb.addressesOf(iface).map(Address.host).filter(Address.isGlobal);
159
+
160
+ records.push({
161
+ name: exchange.name,
162
+ ixlan_id: exchange.peeringdb_ixlan,
163
+ speed: exchange.speed,
164
+ ipaddr4: published.find((address) => Address.family(address) === "ipv4") ?? null,
165
+ ipaddr6: published.find((address) => Address.family(address) === "ipv6") ?? null,
166
+ is_rs_peer: PeeringDb.peersWithRouteServer(device, iface),
167
+ });
168
+ }
169
+ }
170
+
171
+ return records;
172
+ }
173
+ }
@@ -0,0 +1,90 @@
1
+ // Prometheus targets for snmp_exporter, in its file-based discovery format, and the `auths` section of its config: one auth per device, named after it, so each device is asked with its own credentials.
2
+ //
3
+ // The auths hold no secret. A password is written as `${NAME}`, the name of its secret, and snmp_exporter reads it from its environment when started with `--config.expand-environment-variables`.
4
+
5
+ import { Document } from "yaml";
6
+ import type { Network } from "../core/define";
7
+ import type { Device, Secret } from "../schema";
8
+
9
+ export type PrometheusTarget = { targets: string[]; labels: Record<string, string> };
10
+
11
+ /** One auth in snmp_exporter's config. */
12
+ type SnmpExporterAuth =
13
+ | {
14
+ version: 3;
15
+ username: string;
16
+ security_level: "authPriv";
17
+ password: string;
18
+ auth_protocol: string;
19
+ priv_protocol: string;
20
+ priv_password: string;
21
+ }
22
+ | { version: 2; community: string };
23
+
24
+ export class Prometheus {
25
+ /** snmp_exporter's names for the algorithms. */
26
+ private static readonly AUTH = { sha1: "SHA", sha256: "SHA256" } as const;
27
+ private static readonly PRIVACY = { aes128: "AES", des: "DES" } as const;
28
+
29
+ private static readonly AUTHS_HEADER =
30
+ " Written by `circuit prometheus --auths`. Load it beside snmp.yml, with --config.expand-environment-variables.";
31
+
32
+ /** The targets, and the devices left out because their config turns SNMP off. */
33
+ static targets(network: Network): { targets: PrometheusTarget[]; withoutSnmp: Device[] } {
34
+ return {
35
+ targets: network.devices
36
+ .filter((device) => device.management?.snmp && device.connection.host)
37
+ .map((device) => ({
38
+ targets: [device.connection.host!],
39
+ // `__param_auth` picks the device's own auth in snmp_exporter.
40
+ labels: { device: device.name, platform: device.platform, model: device.model, __param_auth: device.name },
41
+ })),
42
+ withoutSnmp: network.devices.filter((device) => !device.management?.snmp),
43
+ };
44
+ }
45
+
46
+ /** The `auths` section, as YAML, and what the operator must still fill in by hand. */
47
+ static snmpExporterAuths(network: Network): { yaml: string; notes: string[] } {
48
+ const notes: string[] = [];
49
+ const auths: Record<string, SnmpExporterAuth> = {};
50
+
51
+ for (const device of network.devices) {
52
+ const snmp = device.management?.snmp;
53
+ if (!snmp) continue;
54
+
55
+ const [name, user] = Object.entries(snmp.users ?? {})[0] ?? [];
56
+
57
+ if (name && user) {
58
+ auths[device.name] = {
59
+ version: 3,
60
+ username: name,
61
+ security_level: "authPriv",
62
+ password: Prometheus.fromEnvironment(user.auth_password, notes, `${device.name}'s SNMP password`),
63
+ auth_protocol: Prometheus.AUTH[user.auth],
64
+ priv_protocol: Prometheus.PRIVACY[user.privacy],
65
+ priv_password: Prometheus.fromEnvironment(user.privacy_password, notes, `${device.name}'s SNMP privacy password`),
66
+ };
67
+ } else {
68
+ // snmp_exporter reads no community from the environment, and Circuit never writes a secret's value.
69
+ auths[device.name] = { version: 2, community: "FILL_IN" };
70
+ notes.push(
71
+ `${device.name} has only an SNMP version 2c community, which snmp_exporter cannot read from its environment; write it in place of FILL_IN, or give the device a version 3 user`
72
+ );
73
+ }
74
+ }
75
+
76
+ // YAML 1.1 rules quote words such as `yes` and `on`, which older YAML readers take as true.
77
+ const document = new Document({ auths }, { version: "1.1" });
78
+ document.contents!.commentBefore = Prometheus.AUTHS_HEADER;
79
+
80
+ return { yaml: document.toString({ indent: 2 }), notes };
81
+ }
82
+
83
+ /** A password as snmp_exporter reads it from its environment, or a note when the secret is a file, which it cannot read. */
84
+ private static fromEnvironment(secret: Secret, notes: string[], where: string): string {
85
+ if ("secret" in secret) return `\${${secret.secret}}`;
86
+
87
+ notes.push(`${where} is read from a file; write its value in place of FILL_IN`);
88
+ return "FILL_IN";
89
+ }
90
+ }
@@ -0,0 +1,25 @@
1
+ // Registries the framework reads prefix sets from. PeeringDB, which it publishes to, is in ./peeringdb.ts.
2
+
3
+ import type { Family } from "../schema";
4
+ import type { Registry } from "../schema/policy";
5
+
6
+ export class Registries {
7
+ /** The prefixes of one family a registry gives for a query. */
8
+ static prefixes(name: Registry, query: string, family: Family): Promise<string[]> {
9
+ const sources: Record<Registry, (query: string, family: Family) => Promise<string[]>> = { "ripe-stat": Registries.ripeStat };
10
+ return sources[name](query, family);
11
+ }
12
+
13
+ /** Prefixes an origin AS announces, from RIPEstat. */
14
+ private static async ripeStat(query: string, family: Family): Promise<string[]> {
15
+ if (!/^AS\d+$/i.test(query)) throw new Error(`ripe-stat takes an AS number, got '${query}'`);
16
+ const res = await fetch(`https://stat.ripe.net/data/announced-prefixes/data.json?resource=${query.toUpperCase()}`, {
17
+ signal: AbortSignal.timeout(90_000),
18
+ });
19
+ if (!res.ok) throw new Error(`ripe-stat answered ${res.status}`);
20
+ const data = (await res.json()) as { data: { prefixes: { prefix: string }[] } };
21
+ return [
22
+ ...new Set(data.data.prefixes.map((entry) => entry.prefix).filter((entry) => entry.includes(":") === (family === "ipv6"))),
23
+ ].sort();
24
+ }
25
+ }
@@ -0,0 +1,41 @@
1
+ // Waiting for BGP sessions to be established: `circuit bgp --wait`, and the check after an apply.
2
+
3
+ import { setTimeout as sleep } from "node:timers/promises";
4
+ import type { BgpSession } from "./platform";
5
+
6
+ /** How long to wait, and how often to read again, both in seconds. */
7
+ export type WaitLimits = { seconds: number; every: number };
8
+
9
+ export class BgpWait {
10
+ /** `circuit bgp --wait` reads the sessions again every 15 seconds, for at most 600 seconds unless `--max` says otherwise. */
11
+ static readonly COMMAND: WaitLimits = { seconds: 600, every: 15 };
12
+
13
+ /**
14
+ * After an apply, the sessions established before have 180 seconds to be established again, read every 10 seconds. A session reset by a change opens again when one side retries; three minutes leaves room for a slow peer without holding the apply up for long. A session that never went down is seen at the first read, so a healthy apply does not wait.
15
+ */
16
+ static readonly AFTER_APPLY: WaitLimits = { seconds: 180, every: 10 };
17
+
18
+ /** Read the sessions until `enough` holds or time runs out, and give the last read. `waiting` is told of each read that fell short, before the pause. */
19
+ static async until(
20
+ read: () => Promise<BgpSession[]>,
21
+ enough: (sessions: BgpSession[]) => boolean,
22
+ limits: WaitLimits,
23
+ waiting: (sessions: BgpSession[]) => void = () => {}
24
+ ): Promise<BgpSession[]> {
25
+ const deadline = Date.now() + limits.seconds * 1000;
26
+ let sessions = await read();
27
+
28
+ while (!enough(sessions) && Date.now() < deadline) {
29
+ waiting(sessions);
30
+ await sleep(Math.min(limits.every * 1000, Math.max(0, deadline - Date.now())));
31
+ sessions = await read();
32
+ }
33
+
34
+ return sessions;
35
+ }
36
+
37
+ /** How many of these sessions are established. */
38
+ static upCount(sessions: readonly BgpSession[]): number {
39
+ return sessions.filter((session) => session.established).length;
40
+ }
41
+ }
@@ -0,0 +1,69 @@
1
+ // Every model each platform supports. Types read from here, so a model or port name that does not exist fails to compile.
2
+
3
+ import { models as fortios } from "./fortios/models";
4
+ import { models as junos } from "./junos/models";
5
+ import { models as raisecom } from "./raisecom-ros/models";
6
+ import { models as routeros } from "./routeros/models";
7
+ import { models as vrp } from "./vrp/models";
8
+
9
+ /** What a model's hardware holds, where a real device held less than its datasheet lists. `validate` checks a config against it. */
10
+ export type ModelLimits = {
11
+ /** The most member ports one LAG may have. More is an error. */
12
+ lag_members?: number;
13
+ /** About how many switch rules the switch chip held. More is a warning: a rule the chip cannot hold is invalid, and so is every rule after it, without a log line. */
14
+ switch_rules?: number;
15
+ /** How many distinct port ranges the switch rules held. More is a warning, for the same reason. */
16
+ switch_port_ranges?: number;
17
+ };
18
+
19
+ /** One hardware model: its ports per speed class, how the platform spells a port, and what its hardware holds. */
20
+ export type ModelSpec = {
21
+ ports: Readonly<Record<string, number>>;
22
+ spelling: Readonly<Record<string, string>>;
23
+ limits?: ModelLimits;
24
+ };
25
+
26
+ export class Catalog {
27
+ static readonly MODELS = { routeros, vrp, "raisecom-ros": raisecom, junos, fortios } as const satisfies Record<
28
+ string,
29
+ Record<string, ModelSpec>
30
+ >;
31
+
32
+ /** Every port a model has, in order. */
33
+ static ports(platform: string, model: string): string[] {
34
+ const spec = (Catalog.MODELS as Record<string, Record<string, ModelSpec>>)[platform]?.[model];
35
+ if (!spec) throw new Error(`${platform} has no model ${model}`);
36
+ return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
37
+ }
38
+
39
+ /** What the model's hardware holds. Empty for a model with no known limits, or a model the catalog does not have. */
40
+ static limits(platform: string, model: string): ModelLimits {
41
+ const spec = (Catalog.MODELS as Record<string, Record<string, ModelSpec>>)[platform]?.[model];
42
+ return spec?.limits ?? {};
43
+ }
44
+
45
+ /** The platform's own name for a port. */
46
+ static spell(platform: string, model: string, port: string): string {
47
+ const spec = (Catalog.MODELS as Record<string, Record<string, ModelSpec>>)[platform]?.[model];
48
+ const parts = /^(\d+[gm])-(\d+)$/.exec(port);
49
+ const template = parts && spec?.spelling[parts[1]!];
50
+ if (!template) throw new Error(`${model} has no port ${port}`);
51
+
52
+ // A platform that counts from 0, or numbers one class after another, shifts the position: `{n-1}`, `{n+47}`.
53
+ return template.replace(/\{n([+-]\d+)?\}/, (_, shift?: string) => String(Number(parts[2]) + Number(shift ?? 0)));
54
+ }
55
+ }
56
+
57
+ export type Models = typeof Catalog.MODELS;
58
+ export type PlatformName = keyof Models;
59
+ export type ModelName<Pl extends PlatformName> = keyof Models[Pl] & string;
60
+
61
+ type Upto<N extends number, A extends unknown[] = [], R = never> = A["length"] extends N
62
+ ? R
63
+ : Upto<N, [...A, 0], R | `${[...A, 0]["length"]}`>;
64
+ type PortsOf<Pl extends PlatformName, M extends ModelName<Pl>> = Models[Pl][M] extends { ports: infer P } ? P : never;
65
+
66
+ /** `10g-1` to `10g-48` on a model with 48 10G ports. */
67
+ export type PortName<Pl extends PlatformName, M extends ModelName<Pl>> = {
68
+ [S in keyof PortsOf<Pl, M>]: `${S & string}-${Upto<PortsOf<Pl, M>[S] & number>}`;
69
+ }[keyof PortsOf<Pl, M>];
@@ -0,0 +1,117 @@
1
+ // Values more than one adapter derives from a device in the same way.
2
+
3
+ import { Address } from "../core/address";
4
+ import type { Device } from "../schema";
5
+
6
+ /** A feature a platform renders once proved on one of its devices. */
7
+ export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter";
8
+
9
+ export class Derive {
10
+ /** Each LAG's number: its own `id`, or its position among the device's LAGs. */
11
+ static lagIds(device: Device): Map<string, number> {
12
+ const numbers = new Map<string, number>();
13
+ let position = 0;
14
+
15
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
16
+ if (iface.type !== "lag") continue;
17
+
18
+ position++;
19
+ numbers.set(name, iface.id ?? position);
20
+ }
21
+
22
+ return numbers;
23
+ }
24
+
25
+ /** The IPv4 networks of a port's or an interface's addresses. */
26
+ static ipv4Networks(device: Device, iface: string): string[] {
27
+ const settings = device.interfaces?.[iface] ?? device.ports?.[iface as keyof typeof device.ports];
28
+ const addresses = (settings?.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
29
+
30
+ return addresses.filter((address) => Address.family(address) === "ipv4").map((address) => Address.prefixOf(address));
31
+ }
32
+
33
+ /** Fields a platform renders only once they are proved on one of its devices. */
34
+ private static readonly UNPROVED_PORT_FIELDS = ["mac_limit", "ethertypes"] as const;
35
+
36
+ /** Fields a platform refuses until it has proved them on a device. A platform names the ones it has proved, and the rest are refused. */
37
+ static unproved(device: Device, proved: readonly ProvedFeature[] = []): string[] {
38
+ const found: string[] = [];
39
+ const routing = device.routing;
40
+ const unproved = "not proved on a device yet";
41
+ const has = (feature: ProvedFeature) => proved.includes(feature);
42
+
43
+ if (routing?.bfd && !has("bfd-timers")) found.push(`routing.bfd, BFD timers are ${unproved}`);
44
+
45
+ for (const route of routing?.static ?? []) {
46
+ if (route.bfd && !has("static-route-bfd"))
47
+ found.push(`routing.static ${route.prefix} bfd, BFD on a static route is ${unproved}`);
48
+ }
49
+
50
+ const bgp = routing?.bgp;
51
+
52
+ for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
53
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
54
+ const bfd = neighbor.bfd ?? group?.bfd;
55
+ const multihop = neighbor.multihop ?? group?.multihop;
56
+
57
+ if (bfd && multihop && !has("multihop-bfd"))
58
+ found.push(`routing.bgp.neighbors.${name}.bfd, BFD on a multihop session is ${unproved}`);
59
+ }
60
+
61
+ if (!has("alarm-severity")) {
62
+ for (const name of Object.keys(device.system?.alarms ?? {})) {
63
+ found.push(`system.alarms.${name}, an alarm's severity is ${unproved}`);
64
+ }
65
+ }
66
+
67
+ const holders = [
68
+ ...Object.entries(device.ports ?? {}).map(([name, settings]) => ({ where: `ports.${name}`, settings })),
69
+ ...Object.entries(device.interfaces ?? {}).map(([name, settings]) => ({
70
+ where: `interfaces.${name}`,
71
+ settings: settings.type === "lag" ? settings : undefined,
72
+ })),
73
+ ];
74
+
75
+ for (const { where, settings } of holders) {
76
+ for (const field of Derive.UNPROVED_PORT_FIELDS) {
77
+ const feature = field === "mac_limit" ? "mac-limit" : "ethertype-filter";
78
+ if (settings?.[field] !== undefined && !has(feature)) found.push(`${where}.${field}, ${unproved}`);
79
+ }
80
+ }
81
+
82
+ return found;
83
+ }
84
+
85
+ /** Services the switch platforms run. Each takes its sources from `management.allow`, on the protocol's own port. */
86
+ private static readonly SWITCH_SERVICES = ["ssh", "telnet", "snmp"] as const;
87
+
88
+ /** Fields neither switch platform, VRP nor Raisecom ROS, expresses: a service's own sources or port, a user's description or keys, the IP stack's host settings, a point-to-point address, and offload per interface. `ownSources` names the services a platform gives their own sources. */
89
+ static switchUnsupported(device: Device, ownSources: readonly ("ssh" | "telnet" | "snmp")[] = []): string[] {
90
+ const found: string[] = [];
91
+
92
+ for (const service of Derive.SWITCH_SERVICES) {
93
+ const settings = device.management?.[service];
94
+ if (settings?.allow && !ownSources.includes(service))
95
+ found.push(`management.${service}.allow, every service takes management.allow`);
96
+ if (settings?.port) found.push(`management.${service}.port`);
97
+ }
98
+
99
+ for (const [name, user] of Object.entries(device.users ?? {})) {
100
+ if (user.description) found.push(`users.${name}.description`);
101
+ if (user.ssh_keys?.length) found.push(`users.${name}.ssh_keys`);
102
+ }
103
+
104
+ if (device.system?.ip) found.push("system.ip");
105
+
106
+ for (const [name, port] of Object.entries(device.ports ?? {})) {
107
+ if (port?.hardware_offload !== undefined) found.push(`ports.${name}.hardware_offload`);
108
+ }
109
+
110
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
111
+ if (iface.addresses?.some((entry) => typeof entry !== "string")) found.push(`interfaces.${name}.addresses with a peer`);
112
+ if (iface.hardware_offload !== undefined) found.push(`interfaces.${name}.hardware_offload`);
113
+ }
114
+
115
+ return found;
116
+ }
117
+ }
@@ -27,6 +27,10 @@ Behaviour that looks like something else. Each one has been hit on a FortiGate 3
27
27
  - A tunnel's interface takes an IPv4 address only as a host, with the far end in `remote-ip`: `set ip 10.0.0.1 255.255.255.252` on one is dropped without a word.
28
28
  - A GRE or VXLAN tunnel creates an interface of its own, named after it, which goes when the tunnel goes. VXLAN has no local address: it leaves from the address of the interface it points to.
29
29
  - A static route has no VRF of its own. It lands in the VRF of the interface it leaves through, as `get router info routing-table all` shows, so a blackhole route cannot go in a VRF. OSPF and BGP have no VRF setting.
30
+ - BFD's timers, `bfd-desired-min-tx`, `bfd-required-min-rx` and `bfd-detect-mult` under `config system settings`, exist only while `bfd` is on there: `get` hides them otherwise. They default to 250, 250 and 3, which `show` does not print. So the render sets `bfd` before them, and a plan unsets them before `bfd`.
31
+ - A static route takes `set bfd enable`, IPv4 and IPv6 alike. The route stays in the table while its BFD session has never come up.
32
+ - BGP asks for a BFD session only once its session is up: `get router info bfd requests` lists nothing for a neighbor still `Active`, with `ebgp-enforce-multihop` or without.
33
+ - On a multihop BGP session BFD runs single-hop: it sends to UDP port 3784, and ignores the multihop BFD the peer sends to port 4784. Against a CCR2216 on RouterOS 7.23.5, with BGP up between the router's loopback and `update-source` on the FortiGate, `get router info bfd neighbor` stayed `DOWN` with remote discriminator `0`, the router's BFD session counted `packets-rx=0`, and the BGP session dropped 11 times in 3 minutes. `config router bfd` holds only single-hop neighbors. Circuit refuses BFD on a multihop session.
30
34
  - The stateless ACL, `firewall acl` and `acl6`, only drops: it has no action.
31
35
  - A route map stops at the first rule that matches, with no way to go on to the next, and its rules permit unless told to deny.
32
36
  - `show vpn certificate local` prints a certificate without its text or key. `show full-configuration` prints both, the key encrypted.
@@ -34,6 +38,8 @@ Behaviour that looks like something else. Each one has been hit on a FortiGate 3
34
38
  - A GRE or VXLAN interface takes no flow sampler, and says so without `Command fail`: `Netflow sampler isn't supported on Tunnel interface..`, and the same for sFlow and for VXLAN.
35
39
  - A port in an aggregate has no `icmp-send-redirect`, `command parse error`, and loses it when it joins one.
36
40
  - `purge` empties a table after asking `(y/n)`.
41
+ - A VLAN interface has no LLDP settings: `set lldp-transmission` on one is `command parse error`. An aggregate's LLDP sends and receives on each of its ports.
42
+ - `diagnose lldprx neighbor summary` names our port by an index of its own, `port 23` for `port5`. `diagnose lldprx neighbor details` names it in `port.txt`, and Circuit reads that. Both print nothing at all when no neighbor was received.
37
43
  - A loopback has no MTU of its own and runs no VRRP. An interface's name has at most 15 characters.
38
44
  - There is no WireGuard and no RPKI, `command parse error`. A route map has no large community and a BGP neighbor no role: neither is among their settings. SNMPv3 offers no SHA256.
39
45
  - `execute backup config flash <comment>` keeps a revision on the device, and `execute restore config flash <id>` restores one.