@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/cross-checks.ts
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
// Rules that span devices: what one device declares about another has to agree with what the other declares.
|
|
2
|
-
|
|
3
|
-
import type { Device, Gre, Port, Prefix, Routed, Switched, Vxlan } from "../schema";
|
|
4
|
-
import { Address } from "./address";
|
|
5
|
-
import type { Finding } from "./define";
|
|
6
|
-
import { type Holder, Topology } from "./topology";
|
|
7
|
-
|
|
8
|
-
export class CrossChecks {
|
|
9
|
-
/** Every rule across devices. */
|
|
10
|
-
static run(devices: readonly Device[]): Finding[] {
|
|
11
|
-
const holders = Topology.holders(devices);
|
|
12
|
-
|
|
13
|
-
// Who holds each address. The first holder wins; a second is reported by checkAddresses.
|
|
14
|
-
const owners = new Map<string, Holder>();
|
|
15
|
-
for (const holder of holders) {
|
|
16
|
-
const key = Topology.keyOf(Address.host(holder.address))!;
|
|
17
|
-
if (!owners.has(key)) owners.set(key, holder);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return [
|
|
21
|
-
...CrossChecks.checkLinks(devices),
|
|
22
|
-
...CrossChecks.checkAddresses(holders),
|
|
23
|
-
...CrossChecks.checkSessions(devices, owners),
|
|
24
|
-
...CrossChecks.checkVrrp(holders),
|
|
25
|
-
...CrossChecks.checkTunnels(devices, owners),
|
|
26
|
-
];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The VLAN ids a port or LAG carries, by how it carries them, so two ends can be compared across devices. */
|
|
30
|
-
private static carried(device: Device, settings: Switched<string>) {
|
|
31
|
-
const id = (vlan: string | undefined) => (vlan ? device.vlans?.[vlan]?.id : undefined);
|
|
32
|
-
return {
|
|
33
|
-
access: id(settings.access_vlan),
|
|
34
|
-
native: id(settings.native_vlan),
|
|
35
|
-
trunk: (settings.trunk_vlans ?? []).map(id).sort((a, b) => (a ?? 0) - (b ?? 0)),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** How a port carries its VLANs, in words: "VLANs 10, 20 tagged and VLAN 30 untagged as native". */
|
|
40
|
-
private static describeCarried({ access, native, trunk }: ReturnType<typeof CrossChecks.carried>): string {
|
|
41
|
-
const parts: string[] = [];
|
|
42
|
-
const tagged = trunk.filter((id) => id !== undefined);
|
|
43
|
-
|
|
44
|
-
if (access !== undefined) parts.push(`VLAN ${access} untagged`);
|
|
45
|
-
if (tagged.length) parts.push(`${tagged.length === 1 ? "VLAN" : "VLANs"} ${tagged.join(", ")} tagged`);
|
|
46
|
-
if (native !== undefined) parts.push(`VLAN ${native} untagged as native`);
|
|
47
|
-
|
|
48
|
-
return parts.length ? parts.join(" and ") : "no VLANs";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** VLAN names by id, for the VLANs a port or LAG carries. */
|
|
52
|
-
private static vlanNames(device: Device, settings: Switched<string>): Map<number, string> {
|
|
53
|
-
const names = [settings.access_vlan, settings.native_vlan, ...(settings.trunk_vlans ?? [])].filter((name) => name !== undefined);
|
|
54
|
-
|
|
55
|
-
return new Map(names.flatMap((name) => (device.vlans?.[name] ? [[device.vlans[name].id, name] as const] : [])));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** What a port's settings are kept on: the LAG when the port is a member of one, otherwise the port. */
|
|
59
|
-
private static carrierOf(device: Device, portName: string, port: Port<string, string, string, string>) {
|
|
60
|
-
const lag = port.lag ? device.interfaces?.[port.lag] : undefined;
|
|
61
|
-
if (port.lag && lag?.type === "lag") return { name: port.lag, settings: lag };
|
|
62
|
-
|
|
63
|
-
return { name: portName, settings: port };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The plain addresses of one family on an interface, leaving out point-to-point ones. */
|
|
67
|
-
private static plainAddresses(settings: Routed<string>, family: string): string[] {
|
|
68
|
-
return (settings.addresses ?? []).filter(
|
|
69
|
-
(entry): entry is Prefix => typeof entry === "string" && Address.isCidr(entry) && Address.family(entry) === family
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Why the two ends of a cable cannot reach each other by address, or undefined when they can. */
|
|
74
|
-
private static addressDisagreement(here: Routed<string>, there: Routed<string>, far: Device, farName: string): string | undefined {
|
|
75
|
-
for (const family of ["ipv4", "ipv6"]) {
|
|
76
|
-
const ours = CrossChecks.plainAddresses(here, family);
|
|
77
|
-
const theirs = CrossChecks.plainAddresses(there, family);
|
|
78
|
-
if (!ours.length || !theirs.length) continue;
|
|
79
|
-
|
|
80
|
-
// A prefix includes its length, so 10.0.0.0/24 and 10.0.0.0/25 are not one.
|
|
81
|
-
const theirPrefixes = new Set(theirs.map(Address.prefixOf));
|
|
82
|
-
if (!ours.some((address) => theirPrefixes.has(Address.prefixOf(address)))) {
|
|
83
|
-
return `has ${ours.join(", ")}, and ${far.name} ${farName} has ${theirs.join(", ")}: not one prefix of one length`;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// A point-to-point address names the far end's own address as its peer.
|
|
88
|
-
const farHosts = new Set(
|
|
89
|
-
(there.addresses ?? []).map((entry) => Topology.keyOf(Address.host(typeof entry === "string" ? entry : entry.address)))
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
for (const entry of here.addresses ?? []) {
|
|
93
|
-
if (typeof entry === "string" || !Topology.keyOf(entry.peer) || farHosts.has(Topology.keyOf(entry.peer))) continue;
|
|
94
|
-
return `${entry.address} names peer ${entry.peer}, which ${far.name} ${farName} does not hold`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** A link names a device and port that exist, the far end links back through the same carrier, and both ends carry the same VLANs the same way, with the same MTU, speed and addresses. */
|
|
101
|
-
private static checkLinks(devices: readonly Device[]): Finding[] {
|
|
102
|
-
const findings: Finding[] = [];
|
|
103
|
-
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
104
|
-
|
|
105
|
-
for (const device of devices) {
|
|
106
|
-
for (const [portName, port] of Object.entries(device.ports ?? {})) {
|
|
107
|
-
const link = port?.link;
|
|
108
|
-
if (!port || !link) continue;
|
|
109
|
-
|
|
110
|
-
const report = (message: string, level: Finding["level"] = "error") =>
|
|
111
|
-
findings.push({
|
|
112
|
-
level,
|
|
113
|
-
device: device.name,
|
|
114
|
-
message: `port ${portName} link: ${message}`,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const far = byName.get(link.device);
|
|
118
|
-
if (!far) {
|
|
119
|
-
report(`device ${link.device} is not in the network`);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const farPort = far.ports?.[link.port];
|
|
124
|
-
if (!farPort) {
|
|
125
|
-
report(`${link.device} does not declare port ${link.port}`);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const linksBack = farPort.link?.device === device.name && farPort.link.port === portName;
|
|
130
|
-
if (!linksBack) {
|
|
131
|
-
report(`${link.device} ${link.port} does not link back to it`);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (link.through !== farPort.link?.through) {
|
|
136
|
-
const say = (through: string | undefined) => (through ? `through ${through}` : "direct");
|
|
137
|
-
report(`${say(link.through)} here, and ${say(farPort.link?.through)} on ${link.device} ${link.port}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const near = CrossChecks.carrierOf(device, portName, port);
|
|
141
|
-
const away = CrossChecks.carrierOf(far, link.port, farPort);
|
|
142
|
-
|
|
143
|
-
// A LAG's VLANs are on the LAG, so its member ports are compared by what their LAGs carry.
|
|
144
|
-
const inLag = near.name !== portName;
|
|
145
|
-
const farInLag = away.name !== link.port;
|
|
146
|
-
if (inLag !== farInLag) {
|
|
147
|
-
report(
|
|
148
|
-
inLag
|
|
149
|
-
? `${portName} is in ${near.name}, and ${link.device} ${link.port} is in no LAG`
|
|
150
|
-
: `${portName} is in no LAG, and ${link.device} ${link.port} is in ${away.name}`
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const here = CrossChecks.carried(device, near.settings);
|
|
155
|
-
const there = CrossChecks.carried(far, away.settings);
|
|
156
|
-
if (JSON.stringify(here) !== JSON.stringify(there)) {
|
|
157
|
-
report(
|
|
158
|
-
`${near.name} carries ${CrossChecks.describeCarried(here)}, and ${link.device} ${away.name} carries ${CrossChecks.describeCarried(there)}`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (port.speed && farPort.speed && port.speed !== farPort.speed) {
|
|
163
|
-
report(`speed ${port.speed}, and ${link.device} ${link.port} is ${farPort.speed}`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const mtu = near.settings.mtu ?? 1500;
|
|
167
|
-
const farMtu = away.settings.mtu ?? 1500;
|
|
168
|
-
if (mtu !== farMtu) report(`MTU ${mtu} on ${near.name}, and ${link.device} ${away.name} has ${farMtu}`);
|
|
169
|
-
|
|
170
|
-
const disagreement = CrossChecks.addressDisagreement(near.settings, away.settings, far, away.name);
|
|
171
|
-
if (disagreement) report(`${near.name} ${disagreement}`);
|
|
172
|
-
|
|
173
|
-
// One VLAN under two names works, but reads as two VLANs.
|
|
174
|
-
const farNames = CrossChecks.vlanNames(far, away.settings);
|
|
175
|
-
for (const [id, name] of CrossChecks.vlanNames(device, near.settings)) {
|
|
176
|
-
const farName = farNames.get(id);
|
|
177
|
-
if (farName && farName !== name) report(`VLAN ${id} is ${name} here and ${farName} on ${link.device}`, "warning");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return findings;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** No address is held by two devices in one VRF. Two loopbacks may hold one on purpose, for anycast, so that is a warning. */
|
|
186
|
-
private static checkAddresses(holders: Holder[]): Finding[] {
|
|
187
|
-
const findings: Finding[] = [];
|
|
188
|
-
const first = new Map<string, Holder>();
|
|
189
|
-
|
|
190
|
-
const isLoopback = (holder: Holder) => holder.device.interfaces?.[holder.name]?.type === "loopback";
|
|
191
|
-
|
|
192
|
-
for (const holder of holders) {
|
|
193
|
-
const key = `${holder.vrf ?? ""} ${Topology.keyOf(Address.host(holder.address))!}`;
|
|
194
|
-
const earlier = first.get(key);
|
|
195
|
-
|
|
196
|
-
if (!earlier) {
|
|
197
|
-
first.set(key, holder);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Two on one device are reported by the device's own checks.
|
|
202
|
-
if (earlier.device === holder.device) continue;
|
|
203
|
-
|
|
204
|
-
const anycast = isLoopback(earlier) && isLoopback(holder);
|
|
205
|
-
|
|
206
|
-
findings.push({
|
|
207
|
-
level: anycast ? "warning" : "error",
|
|
208
|
-
device: holder.device.name,
|
|
209
|
-
message: `${Address.host(holder.address)} on ${holder.name} is also on ${earlier.device.name} ${earlier.name}${anycast ? ", which is right only for anycast" : ""}`,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return findings;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** A BGP session to an address another of our devices holds needs that device's session back, in the AS it expects, from the address it names. */
|
|
217
|
-
private static checkSessions(devices: readonly Device[], owners: Map<string, Holder>): Finding[] {
|
|
218
|
-
const findings: Finding[] = [];
|
|
219
|
-
|
|
220
|
-
const sessionsOf = (device: Device) => {
|
|
221
|
-
const bgp = device.routing?.bgp;
|
|
222
|
-
|
|
223
|
-
return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
|
|
224
|
-
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
name,
|
|
228
|
-
address: neighbor.address,
|
|
229
|
-
remoteAs: neighbor.remote_as ?? group?.remote_as,
|
|
230
|
-
localAddress: neighbor.local_address ?? group?.local_address,
|
|
231
|
-
};
|
|
232
|
-
});
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
for (const device of devices) {
|
|
236
|
-
for (const session of sessionsOf(device)) {
|
|
237
|
-
const far = owners.get(Topology.keyOf(session.address) ?? "")?.device;
|
|
238
|
-
if (!far || far === device) continue;
|
|
239
|
-
|
|
240
|
-
const report = (message: string) =>
|
|
241
|
-
findings.push({ level: "error", device: device.name, message: `neighbor ${session.name}: ${message}` });
|
|
242
|
-
|
|
243
|
-
const farBgp = far.routing?.bgp;
|
|
244
|
-
if (!farBgp) {
|
|
245
|
-
report(`${session.address} is ${far.name}'s, which runs no BGP`);
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (session.remoteAs !== undefined && session.remoteAs !== farBgp.asn) {
|
|
250
|
-
report(`expects AS${session.remoteAs}, and ${far.name} is AS${farBgp.asn}`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Two routers often hold one session per family, and a session of the other family does not answer this one.
|
|
254
|
-
const back = sessionsOf(far).find(
|
|
255
|
-
(other) =>
|
|
256
|
-
owners.get(Topology.keyOf(other.address) ?? "")?.device === device &&
|
|
257
|
-
Address.family(other.address) === Address.family(session.address)
|
|
258
|
-
);
|
|
259
|
-
if (!back) {
|
|
260
|
-
report(`${far.name} has no session back to ${device.name}`);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// The far end only accepts the session from the address it names.
|
|
265
|
-
if (session.localAddress && Topology.keyOf(session.localAddress) !== Topology.keyOf(back.address)) {
|
|
266
|
-
report(`starts from ${session.localAddress}, and ${far.name}'s session ${back.name} expects ${back.address}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return findings;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/** On one segment, a VRRP id has one address and an address has one id, on every device. */
|
|
275
|
-
private static checkVrrp(holders: Holder[]): Finding[] {
|
|
276
|
-
const findings: Finding[] = [];
|
|
277
|
-
const bySegment = new Map<string, { holder: Holder; id: number; address: string }[]>();
|
|
278
|
-
|
|
279
|
-
for (const holder of holders) {
|
|
280
|
-
const segment = Address.prefixOf(holder.address);
|
|
281
|
-
|
|
282
|
-
for (const group of holder.vrrp) {
|
|
283
|
-
if (!Topology.keyOf(group.address) || Address.family(group.address) !== Address.family(holder.address)) continue;
|
|
284
|
-
if (!Address.contains(segment, group.address)) continue;
|
|
285
|
-
|
|
286
|
-
bySegment.set(segment, [...(bySegment.get(segment) ?? []), { holder, id: group.id, address: group.address }]);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
for (const members of bySegment.values()) {
|
|
291
|
-
for (const member of members) {
|
|
292
|
-
const where = `${member.holder.device.name} ${member.holder.name}`;
|
|
293
|
-
const others = members.filter((other) => other.holder.device !== member.holder.device);
|
|
294
|
-
|
|
295
|
-
const sameId = others.find(
|
|
296
|
-
(other) => other.id === member.id && Topology.keyOf(other.address) !== Topology.keyOf(member.address)
|
|
297
|
-
);
|
|
298
|
-
if (sameId) {
|
|
299
|
-
findings.push({
|
|
300
|
-
level: "error",
|
|
301
|
-
device: member.holder.device.name,
|
|
302
|
-
message: `VRRP ${member.id} on ${member.holder.name} is ${member.address}, and on ${sameId.holder.device.name} ${sameId.holder.name} it is ${sameId.address}`,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const sameAddress = others.find(
|
|
307
|
-
(other) => Topology.keyOf(other.address) === Topology.keyOf(member.address) && other.id !== member.id
|
|
308
|
-
);
|
|
309
|
-
if (sameAddress) {
|
|
310
|
-
findings.push({
|
|
311
|
-
level: "error",
|
|
312
|
-
device: member.holder.device.name,
|
|
313
|
-
message: `VRRP address ${member.address} is id ${member.id} on ${where}, and id ${sameAddress.id} on ${sameAddress.holder.device.name} ${sameAddress.holder.name}`,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return findings;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** A GRE or VXLAN tunnel to another of our devices has a tunnel back there, with the endpoints mirrored and, for VXLAN, the same VNI and port. */
|
|
323
|
-
private static checkTunnels(devices: readonly Device[], owners: Map<string, Holder>): Finding[] {
|
|
324
|
-
const findings: Finding[] = [];
|
|
325
|
-
|
|
326
|
-
const ends = devices.flatMap((device) =>
|
|
327
|
-
Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
|
|
328
|
-
iface.type === "gre" || iface.type === "vxlan" ? [{ device, name, iface: iface as Gre<string> | Vxlan<string> }] : []
|
|
329
|
-
)
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
const same = (a: string, b: string) => Topology.keyOf(a) !== undefined && Topology.keyOf(a) === Topology.keyOf(b);
|
|
333
|
-
|
|
334
|
-
for (const end of ends) {
|
|
335
|
-
const far = owners.get(Topology.keyOf(end.iface.remote) ?? "")?.device;
|
|
336
|
-
if (!far || far === end.device) continue;
|
|
337
|
-
|
|
338
|
-
const report = (message: string) =>
|
|
339
|
-
findings.push({ level: "error", device: end.device.name, message: `interface ${end.name}: ${message}` });
|
|
340
|
-
const kind = end.iface.type === "gre" ? "GRE" : "VXLAN";
|
|
341
|
-
|
|
342
|
-
// Two routers may run several VXLAN tunnels between them, one for each VNI, so a tunnel pairs with the one of its own VNI first.
|
|
343
|
-
const back = ends.filter(
|
|
344
|
-
(other) =>
|
|
345
|
-
other.device === far &&
|
|
346
|
-
other.iface.type === end.iface.type &&
|
|
347
|
-
same(other.iface.local, end.iface.remote) &&
|
|
348
|
-
same(other.iface.remote, end.iface.local)
|
|
349
|
-
);
|
|
350
|
-
const vniOf = (iface: Gre<string> | Vxlan<string>) => (iface.type === "vxlan" ? iface.vni : undefined);
|
|
351
|
-
const mirror = back.find((other) => vniOf(other.iface) === vniOf(end.iface)) ?? back[0];
|
|
352
|
-
|
|
353
|
-
if (!mirror) {
|
|
354
|
-
report(`${end.iface.remote} is ${far.name}'s, which has no ${kind} tunnel from there back to ${end.iface.local}`);
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (end.iface.type !== "vxlan" || mirror.iface.type !== "vxlan") continue;
|
|
359
|
-
|
|
360
|
-
if (end.iface.vni !== mirror.iface.vni) report(`VNI ${end.iface.vni}, and ${far.name} ${mirror.name} has ${mirror.iface.vni}`);
|
|
361
|
-
|
|
362
|
-
const port = end.iface.port ?? 4789;
|
|
363
|
-
const farPort = mirror.iface.port ?? 4789;
|
|
364
|
-
if (port !== farPort) report(`UDP port ${port}, and ${far.name} ${mirror.name} has ${farPort}`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return findings;
|
|
368
|
-
}
|
|
369
|
-
}
|
package/src/core/define.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import type { ModelName, PlatformName, PortName } from "../platforms/catalog";
|
|
2
|
-
import type { Device, Secret } from "../schema";
|
|
3
|
-
|
|
4
|
-
/** A value resolved by name when a device is applied, from the environment or `.env` beside the config, and through 1Password when that value is an `op://` reference. Never the value itself. */
|
|
5
|
-
export const secret = (name: string): Secret => ({ secret: name });
|
|
6
|
-
|
|
7
|
-
/** A value read from a file when a device is applied, such as a private key in PEM. The path is relative to the config file; keep the file out of git. */
|
|
8
|
-
export const secretFile = (path: string): Secret => {
|
|
9
|
-
if (!/^[A-Za-z0-9_./-]+$/.test(path)) throw new Error(`secret file path ${path} may hold only letters, digits, _ . / and -`);
|
|
10
|
-
return { secret_file: path };
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Declare one device.
|
|
15
|
-
*
|
|
16
|
-
* The names it declares (VLANs, interfaces, policies and the rest) are the only names its other fields accept, and the editor suggests them. The port names come from the model.
|
|
17
|
-
*/
|
|
18
|
-
export function defineDevice<
|
|
19
|
-
const Platform extends PlatformName,
|
|
20
|
-
const Model extends ModelName<Platform>,
|
|
21
|
-
const Vlans extends string = never,
|
|
22
|
-
const Interfaces extends string = never,
|
|
23
|
-
const Policies extends string = never,
|
|
24
|
-
const PrefixSets extends string = never,
|
|
25
|
-
const BgpGroups extends string = never,
|
|
26
|
-
const Acls extends string = never,
|
|
27
|
-
const AddressSets extends string = never,
|
|
28
|
-
const Users extends string = never,
|
|
29
|
-
const Vrfs extends string = never,
|
|
30
|
-
const Certificates extends string = never,
|
|
31
|
-
const Tables extends string = never,
|
|
32
|
-
>(
|
|
33
|
-
device: Device<
|
|
34
|
-
Platform,
|
|
35
|
-
Model,
|
|
36
|
-
PortName<Platform, Model>,
|
|
37
|
-
Vlans,
|
|
38
|
-
Interfaces,
|
|
39
|
-
Policies,
|
|
40
|
-
PrefixSets,
|
|
41
|
-
BgpGroups,
|
|
42
|
-
Acls,
|
|
43
|
-
AddressSets,
|
|
44
|
-
Users,
|
|
45
|
-
Vrfs,
|
|
46
|
-
Certificates,
|
|
47
|
-
Tables
|
|
48
|
-
>
|
|
49
|
-
): Device {
|
|
50
|
-
return device as unknown as Device;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Something a check found. */
|
|
54
|
-
export type Finding = {
|
|
55
|
-
/** An error stops `diff` and `apply`; a warning is reported only. */
|
|
56
|
-
level: "error" | "warning";
|
|
57
|
-
/** The device it is about, when it is about one. */
|
|
58
|
-
device?: string;
|
|
59
|
-
/** What is wrong, in a sentence. */
|
|
60
|
-
message: string;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/** A rule of your own design, run by `validate` beside the framework's. */
|
|
64
|
-
export type Check = (devices: readonly Device[]) => readonly Finding[];
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* One BGP community the network defines, in the community description format of the NLNOG Ring looking glass, which bgp.tools also reads.
|
|
68
|
-
*
|
|
69
|
-
* `community` takes `nnn` for any number, `x` for one digit and `a-b` for a range, such as `65535:0:nnn`. `description` refers to what each wildcard matched as `$0`, `$1` and so on, such as `do not announce to AS$0`.
|
|
70
|
-
*/
|
|
71
|
-
export type Community = {
|
|
72
|
-
community: string;
|
|
73
|
-
description: string;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/** The whole network. */
|
|
77
|
-
export type Network = {
|
|
78
|
-
/** Every device the framework manages. */
|
|
79
|
-
devices: readonly Device[];
|
|
80
|
-
/** Rules of your own design. */
|
|
81
|
-
checks?: readonly Check[];
|
|
82
|
-
/** The AS the network operates, for what is published about it. */
|
|
83
|
-
asn?: number;
|
|
84
|
-
/** The BGP communities the network defines, printed by `circuit communities` for a looking glass or bgp.tools. */
|
|
85
|
-
communities?: readonly Community[];
|
|
86
|
-
/**
|
|
87
|
-
* The site each device is at, such as a data centre or an office, by name. Devices ignore it.
|
|
88
|
-
*
|
|
89
|
-
* `circuit netbox` puts the device on the NetBox site of that name, and `circuit librenms` puts it in a LibreNMS device group of that name.
|
|
90
|
-
*/
|
|
91
|
-
site?: (device: Device) => string;
|
|
92
|
-
/** Keeps the network's PeeringDB record in step with the config: `circuit peeringdb`. Needs `asn`. */
|
|
93
|
-
peeringdb?: {
|
|
94
|
-
/** An API key with write access to the network's record. */
|
|
95
|
-
api_key: Secret;
|
|
96
|
-
};
|
|
97
|
-
/** Keeps LibreNMS monitoring every device, under its name, with its SNMP credentials and location, and in a group for its `site`: `circuit librenms`. */
|
|
98
|
-
librenms?: {
|
|
99
|
-
/** Where LibreNMS is, such as `https://librenms.example.com`. */
|
|
100
|
-
url: string;
|
|
101
|
-
/** An API token that can add and change devices and device groups. */
|
|
102
|
-
api_token: Secret;
|
|
103
|
-
};
|
|
104
|
-
/** Keeps NetBox in step with the config: `circuit netbox`. Needs `site`. */
|
|
105
|
-
netbox?: {
|
|
106
|
-
/** Where NetBox is, such as `https://netbox.example.com`. */
|
|
107
|
-
url: string;
|
|
108
|
-
/** An API token that can write. */
|
|
109
|
-
api_token: Secret;
|
|
110
|
-
/** What NetBox needs to know about a device that its config does not say. */
|
|
111
|
-
device: (device: Device) => NetBoxDevice;
|
|
112
|
-
};
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/** What NetBox needs to know about a device that its config does not say. */
|
|
116
|
-
export type NetBoxDevice = {
|
|
117
|
-
/** The device role's name in NetBox, such as `Core switch`. Circuit makes the role when NetBox has none of that name. */
|
|
118
|
-
role: string;
|
|
119
|
-
/** The device type's model name in NetBox, made by the platform's vendor. The device's `model` when absent. */
|
|
120
|
-
device_type?: string;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/** Declare the network, in `circuit.config.ts`. */
|
|
124
|
-
export const defineNetwork = (network: Network): Network => network;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// A project's own TypeScript files, and which of them declares a device: for renaming a device everywhere, and for a device's history.
|
|
2
|
-
|
|
3
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
-
import { join, relative } from "node:path";
|
|
5
|
-
|
|
6
|
-
export class ProjectFiles {
|
|
7
|
-
/** Directories that hold no config of the project's own. */
|
|
8
|
-
private static readonly SKIPPED = new Set(["node_modules", ".git", ".circuit", "dist"]);
|
|
9
|
-
|
|
10
|
-
/** The first name written out after a call to defineDevice: the device's own, not that of another device a link, a peer or a tunnel names further on. */
|
|
11
|
-
private static readonly FIRST_NAME = /^[\s\S]*?\bname\s*:\s*(["'`])(.*?)\1/;
|
|
12
|
-
|
|
13
|
-
/** Every TypeScript file of the project, relative to its root, in name order. */
|
|
14
|
-
static sources(root: string, directory = root): string[] {
|
|
15
|
-
const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
16
|
-
|
|
17
|
-
return entries.flatMap((entry) => {
|
|
18
|
-
if (entry.isDirectory())
|
|
19
|
-
return ProjectFiles.SKIPPED.has(entry.name) ? [] : ProjectFiles.sources(root, join(directory, entry.name));
|
|
20
|
-
return /\.(ts|mts|cts)$/.test(entry.name) ? [relative(root, join(directory, entry.name))] : [];
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Whether a file's text declares the device: it calls defineDevice and gives it the name, written out whole. */
|
|
25
|
-
static declaresDevice(text: string, name: string): boolean {
|
|
26
|
-
return [...text.matchAll(/\bdefineDevice\s*\(/g)].some(
|
|
27
|
-
(call) => ProjectFiles.FIRST_NAME.exec(text.slice(call.index + call[0].length))?.[2] === name
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** The project files that declare the device, relative to the root. */
|
|
32
|
-
static deviceFiles(root: string, name: string, sources = ProjectFiles.sources(root)): string[] {
|
|
33
|
-
return sources.filter((file) => ProjectFiles.declaresDevice(readFileSync(join(root, file), "utf8"), name));
|
|
34
|
-
}
|
|
35
|
-
}
|