@takodotid/circuit 0.43.1 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -1,64 +1,11 @@
1
- // The network as a graph, read from the config alone: cables, the VLANs they carry, shared segments, BGP sessions, OSPF adjacencies and tunnels. `circuit graph` prints it and `circuit studio` draws it.
1
+ // The network as a graph, read from the project alone: cables, the VLANs they carry, shared segments, BGP sessions, OSPF adjacencies and tunnels. `circuit studio` draws it.
2
2
 
3
- import type { Device, Gre, Routed, Switched, Vxlan, WireGuardPeer } from "../schema";
4
- import type { Finding } from "./define";
3
+ import type { Device, Gre, Port, Routed, Switched, Vrrp, Vxlan, WireGuardPeer } from "../model";
4
+ import { Derive } from "../platforms/derive";
5
5
  import { Address } from "./address";
6
+ import type { CarriedVlan, Graph, GraphEdge, GraphNode, Layer } from "./graph";
6
7
 
7
- /** One view of the network. Every edge belongs to one. */
8
- export type Layer = "l1" | "l2" | "l3" | "bgp" | "ospf" | "tunnels";
9
-
10
- /** One of our devices, a segment several of them share, or something outside the network. */
11
- export type GraphNode = {
12
- /** Unique among the nodes, such as `device:edge-01` or `as:64501`. */
13
- id: string;
14
- kind: "device" | "segment" | "outside";
15
- /** A word or two, drawn on the node. */
16
- label: string;
17
- /** One fact per line, for a closer look. */
18
- detail: string[];
19
- };
20
-
21
- /** A VLAN a cable carries. */
22
- export type CarriedVlan = {
23
- name: string;
24
- id: number;
25
- /** Carried without a tag: the access VLAN, or a trunk's native VLAN. */
26
- untagged: boolean;
27
- };
28
-
29
- export type GraphEdge = {
30
- layer: Layer;
31
- /** Node ids. */
32
- from: string;
33
- to: string;
34
- /** A few words, drawn on the edge. */
35
- label: string;
36
- /** One fact per line, for a closer look. */
37
- detail: string[];
38
- /** Only one end declares it: the far device does not declare the cable, session or tunnel back. */
39
- oneSided?: true;
40
- /** On `l2`, the VLANs the cable carries. */
41
- vlans?: CarriedVlan[];
42
- };
43
-
44
- export type Graph = {
45
- nodes: GraphNode[];
46
- edges: GraphEdge[];
47
- };
48
-
49
- /** How a device compares with its last snapshot, found offline as `diff` finds it. */
50
- export type Drift = {
51
- status: "matches" | "differs" | "no snapshot" | "cannot plan";
52
- /** The plan `diff` would show, when it differs. */
53
- lines: string[];
54
- };
55
-
56
- /** The graph with what `validate` found and how each device drawn compares with its snapshot. */
57
- export type Annotated = Graph & {
58
- findings: Finding[];
59
- /** By device name. */
60
- drift: Record<string, Drift>;
61
- };
8
+ export type { CarriedVlan, Graph, GraphEdge, GraphNode, Layer } from "./graph";
62
9
 
63
10
  /** An address one of the devices holds on a port or interface. */
64
11
  export type Holder = {
@@ -69,8 +16,8 @@ export type Holder = {
69
16
  address: string;
70
17
  /** The far end of a point-to-point address. */
71
18
  peer?: string;
72
- vrrp: NonNullable<Routed<string>["vrrp"]>;
73
- /** The VRF the address is in. The default table when absent. */
19
+ vrrp: readonly Vrrp[];
20
+ /** The VRF the address is in. The main table when absent. */
74
21
  vrf?: string;
75
22
  };
76
23
 
@@ -112,14 +59,12 @@ export class Topology {
112
59
  return len === Address.bits(family);
113
60
  }
114
61
 
115
- /** Ports, then interfaces, that can hold an address. */
116
- static routedOf(device: Device): [string, Routed<string>][] {
117
- const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) =>
118
- port ? [[name, port] as [string, Routed<string>]] : []
119
- );
120
- const interfaces = Object.entries(device.interfaces ?? {}) as [string, Routed<string>][];
121
-
122
- return [...ports, ...interfaces];
62
+ /** Ports, then every other interface, that can hold an address. */
63
+ static routedOf(device: Device): [string, Routed][] {
64
+ return [
65
+ ...Derive.ports(device).map(([name, port]) => [name, port] as [string, Routed]),
66
+ ...Derive.interfaces(device).map(({ name, settings }) => [name, settings as Routed] as [string, Routed]),
67
+ ];
123
68
  }
124
69
 
125
70
  /** Every valid address the devices hold. An invalid one is left for `validate` to report. */
@@ -130,19 +75,38 @@ export class Topology {
130
75
  const address = typeof entry === "string" ? entry : entry.address;
131
76
  if (!Address.isCidr(address)) return [];
132
77
 
133
- const peer = typeof entry === "string" ? undefined : entry.peer;
134
- return [{ device, name, address, peer, vrrp: settings.vrrp ?? [], vrf: settings.vrf }];
78
+ const peer = typeof entry === "string" ? undefined : entry.peer_address;
79
+ return [
80
+ {
81
+ device,
82
+ name,
83
+ address,
84
+ vrrp: settings.vrrp ?? [],
85
+ ...(peer ? { peer } : {}),
86
+ ...(settings.vrf ? { vrf: settings.vrf } : {}),
87
+ },
88
+ ];
135
89
  })
136
90
  )
137
91
  );
138
92
  }
139
93
 
94
+ /** Who holds each address: the first holder of it. */
95
+ static owners(holders: readonly Holder[]): Map<string, Holder> {
96
+ const owners = new Map<string, Holder>();
97
+ for (const holder of holders) {
98
+ const key = Topology.keyOf(Address.host(holder.address))!;
99
+ if (!owners.has(key)) owners.set(key, holder);
100
+ }
101
+ return owners;
102
+ }
103
+
140
104
  private static describeDevice(device: Device): string[] {
141
105
  const addresses = Topology.routedOf(device)
142
106
  .filter(([, settings]) => settings.addresses?.length)
143
107
  .map(([name, settings]) => {
144
108
  const list = (settings.addresses ?? []).map((entry) =>
145
- typeof entry === "string" ? entry : `${entry.address} to ${entry.peer}`
109
+ typeof entry === "string" ? entry : `${entry.address} to ${entry.peer_address}`
146
110
  );
147
111
  const vrf = settings.vrf ? `, VRF ${settings.vrf}` : "";
148
112
  return `${name}: ${list.join(", ")}${vrf}`;
@@ -155,10 +119,10 @@ export class Topology {
155
119
  ];
156
120
  }
157
121
 
158
- /** The VLANs a port or LAG carries, by id. */
159
- private static carriedBy(device: Device, settings: Switched<string> | undefined): CarriedVlan[] {
122
+ /** The VLANs a port or LAG carries, by ID. */
123
+ static carriedBy(device: Device, settings: Switched | undefined): CarriedVlan[] {
160
124
  const vlan = (name: string | undefined, untagged: boolean): CarriedVlan[] => {
161
- const id = name ? device.vlans?.[name]?.id : undefined;
125
+ const id = name ? device.switching?.vlans?.[name]?.id : undefined;
162
126
  return name && id !== undefined ? [{ name, id, untagged }] : [];
163
127
  };
164
128
 
@@ -171,6 +135,11 @@ export class Topology {
171
135
  ].sort((a, b) => a.id - b.id);
172
136
  }
173
137
 
138
+ /** What carries a port's VLANs: the LAG it is in, or the port itself. */
139
+ static carrierOf(device: Device, port: Port): Switched | undefined {
140
+ return port.in_lag ? device.interfaces?.lag?.[port.in_lag] : port;
141
+ }
142
+
174
143
  private static vlanLabel(vlans: CarriedVlan[]): string {
175
144
  if (vlans.length === 1) return `${vlans[0]!.name}${vlans[0]!.untagged ? ", untagged" : ""}`;
176
145
  if (vlans.length <= 3) return vlans.map((vlan) => vlan.name).join(", ");
@@ -185,9 +154,9 @@ export class Topology {
185
154
  const bundles = new Map<string, { cable: GraphEdge; members: number }>();
186
155
 
187
156
  for (const device of devices) {
188
- for (const [portName, port] of Object.entries(device.ports ?? {})) {
189
- const link = port?.link;
190
- if (!port || !link) continue;
157
+ for (const [portName, port] of Derive.ports(device)) {
158
+ const link = port.link;
159
+ if (!link) continue;
191
160
 
192
161
  // The far end drew this cable already when both ends name each other.
193
162
  const here = `${device.name} ${portName}`;
@@ -196,7 +165,7 @@ export class Topology {
196
165
  drawn.add(`${here}|${there}`);
197
166
 
198
167
  const far = byName.get(link.device);
199
- const farPort = far?.ports?.[link.port];
168
+ const farPort = far?.interfaces?.ethernet?.[link.port];
200
169
  const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
201
170
 
202
171
  const to = far
@@ -207,8 +176,8 @@ export class Topology {
207
176
  const cableLine = `${here} to ${there}${through}`;
208
177
 
209
178
  // A LAG's member cables to one LAG on the far side are one edge.
210
- const lag = port.lag;
211
- const farLag = farPort?.lag;
179
+ const lag = port.in_lag;
180
+ const farLag = farPort?.in_lag;
212
181
  const bundleKey = `${device.name} ${lag}|${link.device} ${farLag}`;
213
182
  const bundle = lag && farLag && linksBack ? bundles.get(bundleKey) : undefined;
214
183
 
@@ -232,10 +201,7 @@ export class Topology {
232
201
 
233
202
  if (isBundle) bundles.set(bundleKey, { cable, members: 1 });
234
203
 
235
- // What carries the VLANs is the LAG when the port is in one.
236
- const lagSettings = lag ? device.interfaces?.[lag] : undefined;
237
- const carrier = lagSettings?.type === "lag" ? lagSettings : lag ? undefined : port;
238
- const vlans = Topology.carriedBy(device, carrier);
204
+ const vlans = Topology.carriedBy(device, Topology.carrierOf(device, port));
239
205
  if (!vlans.length) continue;
240
206
 
241
207
  graph.edge({
@@ -321,28 +287,37 @@ export class Topology {
321
287
  }
322
288
  }
323
289
 
324
- /** `bgp`: sessions between our devices, matched by the address each names, and to every AS outside. */
325
- private static sessions(devices: readonly Device[], owners: Map<string, Holder>, graph: Builder): void {
326
- const all = devices.flatMap((device) => {
290
+ /** Every BGP session of every device, with what it takes from its neighbor group. */
291
+ static sessionsOf(devices: readonly Device[]) {
292
+ return devices.flatMap((device) => {
327
293
  const bgp = device.routing?.bgp;
328
294
 
329
- return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
330
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
331
-
332
- return {
333
- device,
334
- name,
335
- address: neighbor.address,
336
- ownAs: bgp!.asn,
337
- remoteAs: neighbor.remote_as ?? group?.remote_as,
338
- role: neighbor.role ?? group?.role,
339
- description: neighbor.description ?? group?.description,
340
- import: neighbor.import ?? group?.import,
341
- export: neighbor.export ?? group?.export,
342
- };
295
+ return Object.entries(bgp?.neighbors ?? {}).flatMap(([name, neighbor]) => {
296
+ if (!neighbor) return [];
297
+ const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
298
+ const settings = { ...group, ...neighbor };
299
+
300
+ return [
301
+ {
302
+ device,
303
+ name,
304
+ address: neighbor.address,
305
+ ownAs: bgp?.asn,
306
+ group: neighbor.neighbor_group,
307
+ remoteAs: settings.remote_as,
308
+ role: settings.role,
309
+ description: settings.description,
310
+ import: settings.import_policy,
311
+ export: settings.export_policy,
312
+ },
313
+ ];
343
314
  });
344
315
  });
316
+ }
345
317
 
318
+ /** `bgp`: sessions between our devices, matched by the address each names, and to every AS outside. */
319
+ private static sessions(devices: readonly Device[], owners: Map<string, Holder>, graph: Builder): void {
320
+ const all = Topology.sessionsOf(devices);
346
321
  type Session = (typeof all)[number];
347
322
 
348
323
  const describe = (session: Session) => {
@@ -411,10 +386,10 @@ export class Topology {
411
386
 
412
387
  for (const device of devices) {
413
388
  for (const [area, { interfaces }] of Object.entries(device.routing?.ospf?.areas ?? {})) {
414
- for (const [name, settings] of Object.entries(interfaces)) {
389
+ for (const [name, settings] of Object.entries(interfaces ?? {})) {
415
390
  if (settings?.passive) continue;
416
391
 
417
- const routed = (device.ports?.[name] ?? device.interfaces?.[name]) as Routed<string> | undefined;
392
+ const routed = Derive.settings(device, name) as Routed | undefined;
418
393
  const cost = settings?.cost ? `, cost ${settings.cost}` : "";
419
394
 
420
395
  for (const entry of routed?.addresses ?? []) {
@@ -455,25 +430,29 @@ export class Topology {
455
430
 
456
431
  /** `tunnels`: GRE and VXLAN tunnels matched by their endpoints, and WireGuard peers matched to the device at the far end. */
457
432
  private static tunnels(devices: readonly Device[], owners: Map<string, Holder>, graph: Builder): void {
458
- const ends = devices.flatMap((device) =>
459
- Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
460
- iface.type === "gre" || iface.type === "vxlan" ? [{ device, name, iface: iface as Gre<string> | Vxlan<string> }] : []
461
- )
462
- );
433
+ type End = { device: Device; name: string; kind: "GRE" | "VXLAN"; iface: Gre | Vxlan; vni?: number };
463
434
 
464
- type End = (typeof ends)[number];
435
+ const ends: End[] = devices.flatMap((device) => [
436
+ ...Object.entries(device.interfaces?.gre ?? {}).flatMap(([name, iface]) =>
437
+ iface ? [{ device, name, kind: "GRE" as const, iface }] : []
438
+ ),
439
+ ...Object.entries(device.interfaces?.vxlan ?? {}).flatMap(([name, iface]) =>
440
+ iface ? [{ device, name, kind: "VXLAN" as const, iface, vni: iface.vni }] : []
441
+ ),
442
+ ]);
465
443
 
466
444
  const same = (a: string, b: string) => Topology.keyOf(a) !== undefined && Topology.keyOf(a) === Topology.keyOf(b);
445
+ const local = (end: End) => Address.host(end.iface.local_address);
467
446
  const mirrors = (a: End, b: End) =>
468
447
  a.device !== b.device &&
469
- a.iface.type === b.iface.type &&
470
- same(a.iface.local, b.iface.remote) &&
471
- same(a.iface.remote, b.iface.local) &&
472
- (a.iface.type !== "vxlan" || (b.iface.type === "vxlan" && a.iface.vni === b.iface.vni));
448
+ a.kind === b.kind &&
449
+ same(local(a), b.iface.remote_address) &&
450
+ same(a.iface.remote_address, local(b)) &&
451
+ a.vni === b.vni;
473
452
 
474
453
  const describe = (end: End) => {
475
- const vni = end.iface.type === "vxlan" ? `, VNI ${end.iface.vni}` : "";
476
- return `${end.device.name} ${end.name}: ${end.iface.local} to ${end.iface.remote}${vni}`;
454
+ const vni = end.vni !== undefined ? `, VNI ${end.vni}` : "";
455
+ return `${end.device.name} ${end.name}: ${local(end)} to ${end.iface.remote_address}${vni}`;
477
456
  };
478
457
 
479
458
  const paired = new Set<End>();
@@ -481,7 +460,6 @@ export class Topology {
481
460
  for (const end of ends) {
482
461
  if (paired.has(end)) continue;
483
462
 
484
- const kind = end.iface.type === "gre" ? "GRE" : "VXLAN";
485
463
  const mirror = ends.find((other) => !paired.has(other) && mirrors(end, other));
486
464
 
487
465
  if (mirror) {
@@ -491,21 +469,21 @@ export class Topology {
491
469
  layer: "tunnels",
492
470
  from: Topology.deviceId(end.device.name),
493
471
  to: Topology.deviceId(mirror.device.name),
494
- label: `${kind} ${end.name} to ${mirror.name}`,
472
+ label: `${end.kind} ${end.name} to ${mirror.name}`,
495
473
  detail: [describe(end), describe(mirror)],
496
474
  });
497
475
  continue;
498
476
  }
499
477
 
500
478
  // The far endpoint is one of our devices, which declares no tunnel back.
501
- const owner = owners.get(Topology.keyOf(end.iface.remote) ?? "")?.device;
479
+ const owner = owners.get(Topology.keyOf(end.iface.remote_address) ?? "")?.device;
502
480
 
503
481
  if (owner && owner !== end.device) {
504
482
  graph.edge({
505
483
  layer: "tunnels",
506
484
  from: Topology.deviceId(end.device.name),
507
485
  to: Topology.deviceId(owner.name),
508
- label: `${kind} ${end.name}`,
486
+ label: `${end.kind} ${end.name}`,
509
487
  detail: [describe(end), `${owner.name} declares no tunnel back`],
510
488
  oneSided: true,
511
489
  });
@@ -513,9 +491,9 @@ export class Topology {
513
491
  }
514
492
 
515
493
  const outside = graph.node(
516
- `address:${end.iface.remote}`,
494
+ `address:${end.iface.remote_address}`,
517
495
  "outside",
518
- end.iface.remote,
496
+ end.iface.remote_address,
519
497
  end.iface.description ? [end.iface.description] : []
520
498
  );
521
499
 
@@ -523,7 +501,7 @@ export class Topology {
523
501
  layer: "tunnels",
524
502
  from: Topology.deviceId(end.device.name),
525
503
  to: outside,
526
- label: `${kind} ${end.name}`,
504
+ label: `${end.kind} ${end.name}`,
527
505
  detail: [describe(end)],
528
506
  });
529
507
  }
@@ -533,9 +511,7 @@ export class Topology {
533
511
 
534
512
  private static wireguard(devices: readonly Device[], owners: Map<string, Holder>, graph: Builder): void {
535
513
  const interfaces = devices.flatMap((device) =>
536
- Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
537
- iface.type === "wireguard" ? [{ device, name, iface }] : []
538
- )
514
+ Object.entries(device.interfaces?.wireguard ?? {}).flatMap(([name, iface]) => (iface ? [{ device, name, iface }] : []))
539
515
  );
540
516
 
541
517
  // A peer is another of our devices when it may send from that device's tunnel address, or when its endpoint is that device's.
@@ -618,20 +594,14 @@ export class Topology {
618
594
  }
619
595
  }
620
596
 
621
- /** The network as a graph. It reads the config only, never a device, and works on a config that does not validate. */
597
+ /** The network as a graph. It reads the project only, never a device, and works on a project that does not validate. */
622
598
  static of(devices: readonly Device[]): Graph {
623
599
  const graph = new Builder();
624
600
 
625
601
  for (const device of devices) graph.node(Topology.deviceId(device.name), "device", device.name, Topology.describeDevice(device));
626
602
 
627
603
  const holders = Topology.holders(devices);
628
-
629
- // Who holds each address, for matching a session or tunnel to the device at its far end. The first holder wins.
630
- const owners = new Map<string, Holder>();
631
- for (const holder of holders) {
632
- const key = Topology.keyOf(Address.host(holder.address))!;
633
- if (!owners.has(key)) owners.set(key, holder);
634
- }
604
+ const owners = Topology.owners(holders);
635
605
 
636
606
  Topology.cables(devices, graph);
637
607
  Topology.segments(holders, owners, graph);
@@ -641,17 +611,4 @@ export class Topology {
641
611
 
642
612
  return { nodes: [...graph.nodes.values()], edges: graph.edges };
643
613
  }
644
-
645
- /** Only the edges on these layers, and among those only the ones that touch these devices when any are named. Every device named stays. */
646
- static only(graph: Graph, layers: readonly Layer[], deviceNames: readonly string[] = []): Graph {
647
- const named = new Set(deviceNames.map(Topology.deviceId));
648
- const touches = (edge: GraphEdge) => !named.size || named.has(edge.from) || named.has(edge.to);
649
-
650
- const edges = graph.edges.filter((edge) => layers.includes(edge.layer) && touches(edge));
651
- const ends = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
652
-
653
- const nodes = graph.nodes.filter((node) => (named.size ? named.has(node.id) : node.kind === "device") || ends.has(node.id));
654
-
655
- return { nodes, edges };
656
- }
657
614
  }
package/src/core/utils.ts CHANGED
@@ -1,10 +1,21 @@
1
- // Small helpers that stand alone: combining records from several sources, and reading a time zone's offset.
1
+ // Small helpers that stand alone: combining records from several sources, reading a time zone's offset, and reading a file of the project.
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
2
5
 
3
6
  type Merged<T extends readonly object[]> = T extends readonly [infer Head, ...infer Tail extends readonly object[]]
4
7
  ? Head & Merged<Tail>
5
8
  : unknown;
6
9
 
7
10
  export class Utils {
11
+ /** The project's folder. A path in the config, such as a certificate's `certificate_file`, is from here. Circuit sets it when it reads a project. */
12
+ static root = process.cwd();
13
+
14
+ /** The text of a file of the project, by its path from the project's folder. */
15
+ static projectFile(path: string): string {
16
+ return readFileSync(join(Utils.root, path), "utf8");
17
+ }
18
+
8
19
  /** Combine records into one, such as a device's own interfaces and those a module derives for it. Unlike spreading them, a name two of them hold is an error rather than one silently replacing the other. */
9
20
  static merge<const T extends readonly Record<string, unknown>[]>(...records: T): Merged<T> {
10
21
  const result: Record<string, unknown> = {};
package/src/model.ts ADDED
@@ -0,0 +1,83 @@
1
+ // The model as Circuit's own code reads it: a device as its project wrote it, after `src/project/project.ts` turned what the language adds into
2
+ // plain values (a list in a match is one rule per value, an interface set its interfaces, `management.allow` its addresses). Each name is plain text
3
+ // here: the project's types already checked it. The fields and their docs are those of `src/config/`, which a project writes.
4
+
5
+ import type * as C from "./config/index";
6
+
7
+ export type {
8
+ Certificate,
9
+ Ethertype,
10
+ Exchange,
11
+ Family,
12
+ Helper,
13
+ IP,
14
+ IPv4,
15
+ IPv6,
16
+ Link,
17
+ MacAddress,
18
+ PortRange,
19
+ Ports,
20
+ Prefix,
21
+ Protocol,
22
+ Rate,
23
+ } from "./config/index";
24
+ export type {
25
+ Secret,
26
+ Severity,
27
+ SnmpUser,
28
+ Stp,
29
+ Threshold,
30
+ Vrrp,
31
+ WireGuardPeer,
32
+ Bfd,
33
+ RpkiServer,
34
+ OspfInterface,
35
+ PrefixSet,
36
+ RouteSet,
37
+ User,
38
+ Vlan,
39
+ } from "./config/index";
40
+
41
+ /** A device: name, site, platform, model, connection and its groups. */
42
+ export type Device = C.AnyDevice;
43
+
44
+ /** A physical port, in `interfaces.ethernet`. */
45
+ export type Port = C.AnyPort;
46
+
47
+ export type Lag = C.Lag<string, string>;
48
+ export type VlanInterface = C.VlanInterface<string, string>;
49
+ export type Loopback = C.Loopback<string>;
50
+ export type Gre = C.Gre<string>;
51
+ export type Vxlan = C.Vxlan<string>;
52
+ export type WireGuard = C.WireGuard<string>;
53
+ export type Bridge = C.Bridge<string, string>;
54
+
55
+ /** Any interface that is not a port: what `interfaces` holds besides `ethernet`. */
56
+ export type Interface = Lag | VlanInterface | Loopback | Gre | Vxlan | WireGuard | Bridge;
57
+
58
+ /** The kinds of interface `interfaces` groups by, besides `ethernet`. */
59
+ export type InterfaceKind = "lag" | "vlan_interface" | "loopback" | "gre" | "vxlan" | "wireguard" | "bridge";
60
+
61
+ export type Switched = C.Switched<string>;
62
+ export type Routed = C.Routed<string>;
63
+ export type Address = C.Address;
64
+
65
+ export type FilterMatch = C.FilterMatch<string, string, string>;
66
+ export type FilterRule = C.FilterRule<string, string, string>;
67
+ export type NatRule = C.NatRule<string, string, string>;
68
+ export type AclRule = C.AclRule<string>;
69
+ export type PolicyRule = C.PolicyRule<string, string>;
70
+ export type RouteRule = C.RouteRule<string>;
71
+ export type CallRule = C.CallRule<string>;
72
+ export type RoutingRule = C.RoutingRule<string, string>;
73
+ export type StaticRoute = C.StaticRoute<string, string, string>;
74
+ export type NeighborSettings = C.NeighborSettings<string, string>;
75
+ export type Neighbor = C.Neighbor<string, string, string>;
76
+ export type Service = C.Service;
77
+ export type Console = C.Console;
78
+ export type Jump = C.Jump;
79
+ export type DhcpServer = C.DhcpServer<string>;
80
+ export type DhcpRelay = C.DhcpRelay<string>;
81
+ export type FlowExport = C.FlowExport<string>;
82
+ export type SyslogServer = C.SyslogServer;
83
+ export type Ospf = C.Ospf<string>;