@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.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
package/src/core/topology.ts
CHANGED
|
@@ -1,64 +1,11 @@
|
|
|
1
|
-
// The network as a graph, read from the
|
|
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 "../
|
|
4
|
-
import
|
|
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
|
-
|
|
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:
|
|
73
|
-
/** The VRF the address is in. The
|
|
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
|
|
116
|
-
static routedOf(device: Device): [string, Routed
|
|
117
|
-
|
|
118
|
-
port
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
134
|
-
return [
|
|
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.
|
|
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
|
|
159
|
-
|
|
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
|
|
189
|
-
const link = port
|
|
190
|
-
if (!
|
|
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?.
|
|
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.
|
|
211
|
-
const farLag = farPort?.
|
|
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
|
-
|
|
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
|
-
/**
|
|
325
|
-
|
|
326
|
-
|
|
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 ?? {}).
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
470
|
-
same(a
|
|
471
|
-
same(a.iface.
|
|
472
|
-
|
|
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.
|
|
476
|
-
return `${end.device.name} ${end.name}: ${end
|
|
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.
|
|
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.
|
|
494
|
+
`address:${end.iface.remote_address}`,
|
|
517
495
|
"outside",
|
|
518
|
-
end.iface.
|
|
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
|
|
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,
|
|
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>;
|