@takodotid/circuit 0.34.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11347 -9018
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
- package/src/adapters/devices/raisecom-ros/index.ts +0 -693
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Registries the framework reads prefix sets from. PeeringDB, which it publishes to, is in ./peeringdb.ts.
|
|
2
|
-
|
|
3
|
-
import type { Family } from "../../schema";
|
|
4
|
-
import type { Registry } from "../../schema/policy";
|
|
5
|
-
|
|
6
|
-
/** Prefixes an origin AS announces, from RIPEstat. */
|
|
7
|
-
async function ripeStat(query: string, family: Family): Promise<string[]> {
|
|
8
|
-
if (!/^AS\d+$/i.test(query)) throw new Error(`ripe-stat takes an AS number, got '${query}'`);
|
|
9
|
-
const res = await fetch(`https://stat.ripe.net/data/announced-prefixes/data.json?resource=${query.toUpperCase()}`, {
|
|
10
|
-
signal: AbortSignal.timeout(90_000),
|
|
11
|
-
});
|
|
12
|
-
if (!res.ok) throw new Error(`ripe-stat answered ${res.status}`);
|
|
13
|
-
const data = (await res.json()) as { data: { prefixes: { prefix: string }[] } };
|
|
14
|
-
return [
|
|
15
|
-
...new Set(data.data.prefixes.map((entry) => entry.prefix).filter((entry) => entry.includes(":") === (family === "ipv6"))),
|
|
16
|
-
].sort();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const registries: Record<Registry, (query: string, family: Family) => Promise<string[]>> = { "ripe-stat": ripeStat };
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// Keeps LibreNMS in step with the config: every device Circuit manages is monitored there, polled with the SNMP credentials its config gives it. A device LibreNMS holds that the config does not is reported, never deleted.
|
|
2
|
-
|
|
3
|
-
import type { Network } from "../../core/define";
|
|
4
|
-
import { resolve } from "../../core/secrets";
|
|
5
|
-
import type { Device } from "../../schema";
|
|
6
|
-
|
|
7
|
-
/** What `librenms` would do. */
|
|
8
|
-
export type LibreNmsPlan = {
|
|
9
|
-
/** Devices to add. */
|
|
10
|
-
additions: Device[];
|
|
11
|
-
/** Devices that cannot be added: their config turns SNMP off. */
|
|
12
|
-
withoutSnmp: Device[];
|
|
13
|
-
/** Hostnames LibreNMS holds that no device in the config has. */
|
|
14
|
-
unknown: string[];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/** LibreNMS's names for the algorithms. */
|
|
18
|
-
const AUTH = { sha1: "SHA", sha256: "SHA-256" } as const;
|
|
19
|
-
const PRIVACY = { aes128: "AES", des: "DES" } as const;
|
|
20
|
-
|
|
21
|
-
function settingsOf(network: Network) {
|
|
22
|
-
if (!network.librenms) throw new Error("the network has no librenms settings");
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
base: `${network.librenms.url.replace(/\/+$/, "")}/api/v0`,
|
|
26
|
-
// The header every version of API v0 accepts.
|
|
27
|
-
headers: { "X-Auth-Token": resolve(network.librenms.api_token), "Content-Type": "application/json" },
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** How LibreNMS polls a device: its first SNMP version 3 user, or else its version 2c community. */
|
|
32
|
-
function credentials(device: Device): Record<string, string> | undefined {
|
|
33
|
-
const snmp = device.management?.snmp;
|
|
34
|
-
const [name, user] = Object.entries(snmp?.users ?? {})[0] ?? [];
|
|
35
|
-
|
|
36
|
-
if (name && user) {
|
|
37
|
-
return {
|
|
38
|
-
snmpver: "v3",
|
|
39
|
-
authlevel: "authPriv",
|
|
40
|
-
authname: name,
|
|
41
|
-
authpass: resolve(user.auth_password),
|
|
42
|
-
authalgo: AUTH[user.auth],
|
|
43
|
-
cryptopass: resolve(user.privacy_password),
|
|
44
|
-
cryptoalgo: PRIVACY[user.privacy],
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (snmp?.community) return { snmpver: "v2c", community: resolve(snmp.community) };
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Compare the config with what LibreNMS monitors. A device is known to LibreNMS by its management address. */
|
|
53
|
-
export async function planLibreNms(network: Network): Promise<LibreNmsPlan> {
|
|
54
|
-
const { base, headers } = settingsOf(network);
|
|
55
|
-
|
|
56
|
-
const response = await fetch(`${base}/devices?type=all`, { headers, signal: AbortSignal.timeout(30_000) });
|
|
57
|
-
if (!response.ok) throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
58
|
-
|
|
59
|
-
const monitored = new Set(((await response.json()) as { devices: { hostname: string }[] }).devices.map((device) => device.hostname));
|
|
60
|
-
// A device reached only through its console has no address to poll.
|
|
61
|
-
const polled = network.devices.filter((device) => device.connection.host);
|
|
62
|
-
const ours = new Set(polled.map((device) => device.connection.host));
|
|
63
|
-
const missing = polled.filter((device) => !monitored.has(device.connection.host!));
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
additions: missing.filter((device) => device.management?.snmp),
|
|
67
|
-
withoutSnmp: missing.filter((device) => !device.management?.snmp),
|
|
68
|
-
unknown: [...monitored].filter((hostname) => !ours.has(hostname)).sort(),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Add each device, named after its `name` and polled with the credentials from its config. */
|
|
73
|
-
export async function sendLibreNms(network: Network, additions: readonly Device[]): Promise<void> {
|
|
74
|
-
const { base, headers } = settingsOf(network);
|
|
75
|
-
|
|
76
|
-
for (const device of additions) {
|
|
77
|
-
const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
|
|
78
|
-
const response = await fetch(`${base}/devices`, {
|
|
79
|
-
method: "POST",
|
|
80
|
-
headers,
|
|
81
|
-
body: JSON.stringify(body),
|
|
82
|
-
signal: AbortSignal.timeout(60_000),
|
|
83
|
-
});
|
|
84
|
-
const answer = (await response.json().catch(() => ({}))) as { status?: string; message?: string };
|
|
85
|
-
|
|
86
|
-
if (!response.ok || answer.status !== "ok") {
|
|
87
|
-
throw new Error(`${device.name}: LibreNMS refused it: ${answer.message ?? response.status}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
console.log(` ${device.name}: ${answer.message}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
// Keeps a network's exchange presence at PeeringDB in step with the config. Each interface with an `exchange` naming a `peeringdb_ixlan` is one record.
|
|
2
|
-
//
|
|
3
|
-
// Only globally reachable addresses are published: an exchange reached over a tunnel often lands on private or shared space, and publishing it would point others at a dead end. A record the config does not hold is reported, never deleted, and `operational` is never changed, since leaving an exchange is a decision made outside the config.
|
|
4
|
-
|
|
5
|
-
import { contains, familyOf, host, isGlobal } from "../../core/addr";
|
|
6
|
-
import type { Network } from "../../core/define";
|
|
7
|
-
import { resolve } from "../../core/secrets";
|
|
8
|
-
import type { Device, Interface } from "../../schema";
|
|
9
|
-
|
|
10
|
-
const API = "https://www.peeringdb.com/api";
|
|
11
|
-
|
|
12
|
-
/** A network's presence at one exchange LAN, as PeeringDB holds it. */
|
|
13
|
-
type NetIxLan = {
|
|
14
|
-
id: number;
|
|
15
|
-
net_id: number;
|
|
16
|
-
ixlan_id: number;
|
|
17
|
-
name: string;
|
|
18
|
-
speed: number;
|
|
19
|
-
ipaddr4: string | null;
|
|
20
|
-
ipaddr6: string | null;
|
|
21
|
-
is_rs_peer: boolean;
|
|
22
|
-
operational: boolean;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/** One record as the config describes it. */
|
|
26
|
-
type Wanted = {
|
|
27
|
-
name: string;
|
|
28
|
-
ixlan_id: number;
|
|
29
|
-
speed: number;
|
|
30
|
-
ipaddr4: string | null;
|
|
31
|
-
ipaddr6: string | null;
|
|
32
|
-
is_rs_peer: boolean;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** A change to send. `id` is set when the record exists and is updated in place. */
|
|
36
|
-
export type PeeringDbChange = {
|
|
37
|
-
summary: string;
|
|
38
|
-
id?: number;
|
|
39
|
-
body: Record<string, unknown>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type PeeringDbPlan = {
|
|
43
|
-
changes: PeeringDbChange[];
|
|
44
|
-
/** Records PeeringDB holds that the config does not. */
|
|
45
|
-
unknown: string[];
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
type AnyInterface = Interface<string, string>;
|
|
49
|
-
|
|
50
|
-
const addressesOf = (iface: AnyInterface) => (iface.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
|
|
51
|
-
|
|
52
|
-
/** Whether the device peers with a route server on this interface's subnets. */
|
|
53
|
-
function peersWithRouteServer(device: Device, iface: AnyInterface): boolean {
|
|
54
|
-
const bgp = device.routing?.bgp;
|
|
55
|
-
const subnets = addressesOf(iface);
|
|
56
|
-
|
|
57
|
-
return Object.values(bgp?.neighbors ?? {}).some((neighbor) => {
|
|
58
|
-
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
59
|
-
const role = neighbor.role ?? group?.role;
|
|
60
|
-
return role === "rs" && subnets.some((subnet) => contains(subnet, neighbor.address));
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Every exchange presence the config declares with a PeeringDB exchange LAN. */
|
|
65
|
-
function wanted(network: Network): Wanted[] {
|
|
66
|
-
const records: Wanted[] = [];
|
|
67
|
-
|
|
68
|
-
for (const device of network.devices) {
|
|
69
|
-
for (const iface of Object.values(device.interfaces ?? {})) {
|
|
70
|
-
const exchange = iface.exchange;
|
|
71
|
-
if (!exchange?.peeringdb_ixlan) continue;
|
|
72
|
-
|
|
73
|
-
const published = addressesOf(iface).map(host).filter(isGlobal);
|
|
74
|
-
|
|
75
|
-
records.push({
|
|
76
|
-
name: exchange.name,
|
|
77
|
-
ixlan_id: exchange.peeringdb_ixlan,
|
|
78
|
-
speed: exchange.speed,
|
|
79
|
-
ipaddr4: published.find((address) => familyOf(address) === "ipv4") ?? null,
|
|
80
|
-
ipaddr6: published.find((address) => familyOf(address) === "ipv6") ?? null,
|
|
81
|
-
is_rs_peer: peersWithRouteServer(device, iface),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return records;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** What would change at PeeringDB. Reads the public API; needs no key. */
|
|
90
|
-
export async function planPeeringDb(network: Network): Promise<PeeringDbPlan> {
|
|
91
|
-
if (!network.asn) throw new Error("the network declares no asn");
|
|
92
|
-
|
|
93
|
-
const response = await fetch(`${API}/netixlan?asn=${network.asn}`, {
|
|
94
|
-
headers: { Accept: "application/json" },
|
|
95
|
-
signal: AbortSignal.timeout(30_000),
|
|
96
|
-
});
|
|
97
|
-
if (!response.ok) throw new Error(`PeeringDB answered ${response.status}`);
|
|
98
|
-
|
|
99
|
-
const existing = ((await response.json()) as { data: NetIxLan[] }).data;
|
|
100
|
-
const desired = wanted(network);
|
|
101
|
-
|
|
102
|
-
// The record that owns every presence. Taken from an existing one, since the API has no other lookup by ASN here.
|
|
103
|
-
const netId = existing[0]?.net_id;
|
|
104
|
-
const changes: PeeringDbChange[] = [];
|
|
105
|
-
|
|
106
|
-
for (const record of desired) {
|
|
107
|
-
const current = existing.find((candidate) => candidate.ixlan_id === record.ixlan_id);
|
|
108
|
-
|
|
109
|
-
const body = {
|
|
110
|
-
net_id: netId,
|
|
111
|
-
ixlan_id: record.ixlan_id,
|
|
112
|
-
asn: network.asn,
|
|
113
|
-
speed: record.speed,
|
|
114
|
-
ipaddr4: record.ipaddr4,
|
|
115
|
-
ipaddr6: record.ipaddr6,
|
|
116
|
-
is_rs_peer: record.is_rs_peer,
|
|
117
|
-
operational: current?.operational ?? true,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
if (!current) {
|
|
121
|
-
changes.push({ summary: `+ ${record.name}: ${record.ipaddr4}, ${record.ipaddr6}, ${record.speed} Mbit/s`, body });
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const differs =
|
|
126
|
-
current.ipaddr4 !== record.ipaddr4 ||
|
|
127
|
-
current.ipaddr6 !== record.ipaddr6 ||
|
|
128
|
-
current.speed !== record.speed ||
|
|
129
|
-
current.is_rs_peer !== record.is_rs_peer;
|
|
130
|
-
|
|
131
|
-
if (differs) {
|
|
132
|
-
const before = `${current.ipaddr4}, ${current.ipaddr6}, ${current.speed} Mbit/s, route server ${current.is_rs_peer}`;
|
|
133
|
-
const after = `${record.ipaddr4}, ${record.ipaddr6}, ${record.speed} Mbit/s, route server ${record.is_rs_peer}`;
|
|
134
|
-
changes.push({ summary: `~ ${record.name}: ${before} to ${after}`, id: current.id, body });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const unknown = existing
|
|
139
|
-
.filter((record) => !desired.some((candidate) => candidate.ixlan_id === record.ixlan_id))
|
|
140
|
-
.map((record) => record.name);
|
|
141
|
-
|
|
142
|
-
if (changes.some((change) => !change.id) && !netId) {
|
|
143
|
-
throw new Error("PeeringDB holds no presence for this network yet, so its record id is unknown; add the first one by hand");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return { changes, unknown };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Send the changes, with the network's API key. */
|
|
150
|
-
export async function sendPeeringDb(network: Network, changes: PeeringDbChange[]): Promise<void> {
|
|
151
|
-
if (!network.peeringdb) throw new Error("the network declares no peeringdb api_key");
|
|
152
|
-
const apiKey = resolve(network.peeringdb.api_key);
|
|
153
|
-
|
|
154
|
-
for (const change of changes) {
|
|
155
|
-
const response = await fetch(`${API}/netixlan${change.id ? `/${change.id}` : ""}`, {
|
|
156
|
-
method: change.id ? "PUT" : "POST",
|
|
157
|
-
headers: { Authorization: `Api-Key ${apiKey}`, "Content-Type": "application/json" },
|
|
158
|
-
body: JSON.stringify(change.body),
|
|
159
|
-
signal: AbortSignal.timeout(30_000),
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (!response.ok) {
|
|
163
|
-
const reason = (await response.text()).slice(0, 300);
|
|
164
|
-
throw new Error(`${change.summary}: PeeringDB answered ${response.status} ${reason}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
console.log(` sent ${change.summary}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Prometheus targets for snmp_exporter, in its file-based discovery format, and the `auths` section of its config: one auth per device, named after it, so each device is asked with its own credentials.
|
|
2
|
-
//
|
|
3
|
-
// The auths hold no secret. A password is written as `${NAME}`, the name of its secret, and snmp_exporter reads it from its environment when started with `--config.expand-environment-variables`.
|
|
4
|
-
|
|
5
|
-
import type { Network } from "../../core/define";
|
|
6
|
-
import type { Device, Secret } from "../../schema";
|
|
7
|
-
|
|
8
|
-
export type PrometheusTarget = { targets: string[]; labels: Record<string, string> };
|
|
9
|
-
|
|
10
|
-
/** snmp_exporter's names for the algorithms. */
|
|
11
|
-
const AUTH = { sha1: "SHA", sha256: "SHA256" } as const;
|
|
12
|
-
const PRIVACY = { aes128: "AES", des: "DES" } as const;
|
|
13
|
-
|
|
14
|
-
/** The targets, and the devices left out because their config turns SNMP off. */
|
|
15
|
-
export function prometheusTargets(network: Network): { targets: PrometheusTarget[]; withoutSnmp: Device[] } {
|
|
16
|
-
return {
|
|
17
|
-
targets: network.devices
|
|
18
|
-
.filter((device) => device.management?.snmp && device.connection.host)
|
|
19
|
-
.map((device) => ({
|
|
20
|
-
targets: [device.connection.host!],
|
|
21
|
-
// `__param_auth` picks the device's own auth in snmp_exporter.
|
|
22
|
-
labels: { device: device.name, platform: device.platform, model: device.model, __param_auth: device.name },
|
|
23
|
-
})),
|
|
24
|
-
withoutSnmp: network.devices.filter((device) => !device.management?.snmp),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** A password as snmp_exporter reads it from its environment, or a note when the secret is a file, which it cannot read. */
|
|
29
|
-
function fromEnvironment(secret: Secret, notes: string[], where: string): string {
|
|
30
|
-
if ("secret" in secret) return `\${${secret.secret}}`;
|
|
31
|
-
|
|
32
|
-
notes.push(`${where} is read from a file; write its value in place of FILL_IN`);
|
|
33
|
-
return "FILL_IN";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The `auths` section, as YAML, and what the operator must still fill in by hand. */
|
|
37
|
-
export function snmpExporterAuths(network: Network): { yaml: string; notes: string[] } {
|
|
38
|
-
const notes: string[] = [];
|
|
39
|
-
const lines = [
|
|
40
|
-
"# Written by `circuit prometheus --auths`. Load it beside snmp.yml, with --config.expand-environment-variables.",
|
|
41
|
-
"auths:",
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
for (const device of network.devices) {
|
|
45
|
-
const snmp = device.management?.snmp;
|
|
46
|
-
if (!snmp) continue;
|
|
47
|
-
|
|
48
|
-
const [name, user] = Object.entries(snmp.users ?? {})[0] ?? [];
|
|
49
|
-
lines.push(` ${device.name}:`);
|
|
50
|
-
|
|
51
|
-
if (name && user) {
|
|
52
|
-
lines.push(
|
|
53
|
-
" version: 3",
|
|
54
|
-
` username: ${name}`,
|
|
55
|
-
" security_level: authPriv",
|
|
56
|
-
` password: ${fromEnvironment(user.auth_password, notes, `${device.name}'s SNMP password`)}`,
|
|
57
|
-
` auth_protocol: ${AUTH[user.auth]}`,
|
|
58
|
-
` priv_protocol: ${PRIVACY[user.privacy]}`,
|
|
59
|
-
` priv_password: ${fromEnvironment(user.privacy_password, notes, `${device.name}'s SNMP privacy password`)}`
|
|
60
|
-
);
|
|
61
|
-
} else {
|
|
62
|
-
// snmp_exporter reads no community from the environment, and Circuit never writes a secret's value.
|
|
63
|
-
lines.push(" version: 2", " community: FILL_IN");
|
|
64
|
-
notes.push(
|
|
65
|
-
`${device.name} has only an SNMP version 2c community, which snmp_exporter cannot read from its environment; write it in place of FILL_IN, or give the device a version 3 user`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return { yaml: lines.join("\n") + "\n", notes };
|
|
71
|
-
}
|
package/src/cli/graph.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// What `circuit graph` prints: the graph of the network with what `validate` found and how each device compares with its last snapshot, as Mermaid, DOT or JSON.
|
|
2
|
-
|
|
3
|
-
import { type Annotated, deviceId, type GraphNode } from "../core/graph";
|
|
4
|
-
|
|
5
|
-
/** `error` when validate found an error on the device, `warning` for a warning or a difference from its snapshot. */
|
|
6
|
-
export function markOf(graph: Annotated, node: GraphNode): "error" | "warning" | undefined {
|
|
7
|
-
if (node.kind !== "device") return undefined;
|
|
8
|
-
|
|
9
|
-
const findings = graph.findings.filter((finding) => finding.device && deviceId(finding.device) === node.id);
|
|
10
|
-
if (findings.some((finding) => finding.level === "error")) return "error";
|
|
11
|
-
|
|
12
|
-
const drift = graph.drift[node.label]?.status;
|
|
13
|
-
if (findings.length || drift === "differs" || drift === "cannot plan") return "warning";
|
|
14
|
-
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Mermaid, which GitHub draws wherever Markdown is shown. */
|
|
19
|
-
export function mermaid(graph: Annotated): string {
|
|
20
|
-
const ids = new Map(graph.nodes.map((node, index) => [node.id, `n${index}`]));
|
|
21
|
-
// Lines of text, each escaped, as one quoted label.
|
|
22
|
-
const text = (...lines: string[]) =>
|
|
23
|
-
`"${lines.map((line) => line.replace(/"/g, "#quot;").replace(/</g, "#lt;").replace(/>/g, "#gt;")).join("<br/>")}"`;
|
|
24
|
-
|
|
25
|
-
const shapes: Record<GraphNode["kind"], (label: string) => string> = {
|
|
26
|
-
device: (label) => `[${label}]`,
|
|
27
|
-
segment: (label) => `([${label}])`,
|
|
28
|
-
outside: (label) => `{{${label}}}`,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const lines = ["graph LR"];
|
|
32
|
-
|
|
33
|
-
for (const node of graph.nodes) {
|
|
34
|
-
const label = node.kind === "device" ? text(node.label, node.detail[0] ?? "") : text(node.label);
|
|
35
|
-
lines.push(` ${ids.get(node.id)}${shapes[node.kind](label)}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
for (const edge of graph.edges) {
|
|
39
|
-
const arrow = edge.oneSided ? "-.-" : "---";
|
|
40
|
-
lines.push(` ${ids.get(edge.from)} ${arrow}|${text(edge.label)}| ${ids.get(edge.to)}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
lines.push(
|
|
44
|
-
" classDef error stroke:#d1242f,stroke-width:3px",
|
|
45
|
-
" classDef warning stroke:#bf8700,stroke-width:3px",
|
|
46
|
-
" classDef outside stroke-dasharray:4 3"
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
for (const node of graph.nodes) {
|
|
50
|
-
const mark = markOf(graph, node) ?? (node.kind === "outside" ? "outside" : undefined);
|
|
51
|
-
if (mark) lines.push(` class ${ids.get(node.id)} ${mark}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return lines.join("\n") + "\n";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** DOT, which Graphviz turns into an image: `circuit graph --format=dot | dot -Tsvg > network.svg`. */
|
|
58
|
-
export function dot(graph: Annotated): string {
|
|
59
|
-
const text = (value: string) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
60
|
-
const colors = { error: "#d1242f", warning: "#bf8700" };
|
|
61
|
-
|
|
62
|
-
const shapes: Record<GraphNode["kind"], string> = {
|
|
63
|
-
device: "shape=box, style=rounded",
|
|
64
|
-
segment: "shape=ellipse",
|
|
65
|
-
outside: "shape=box, style=dashed",
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const lines = [
|
|
69
|
-
"graph network {",
|
|
70
|
-
" rankdir=LR;",
|
|
71
|
-
' node [fontname="Helvetica"];',
|
|
72
|
-
' edge [fontname="Helvetica", fontsize=10];',
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
for (const node of graph.nodes) {
|
|
76
|
-
const label = node.kind === "device" ? `${node.label}\n${node.detail[0] ?? ""}` : node.label;
|
|
77
|
-
const mark = markOf(graph, node);
|
|
78
|
-
const color = mark ? `, color=${text(colors[mark])}, penwidth=2` : "";
|
|
79
|
-
lines.push(` ${text(node.id)} [label=${text(label)}, ${shapes[node.kind]}${color}];`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const edge of graph.edges) {
|
|
83
|
-
const style = edge.oneSided ? ", style=dashed" : "";
|
|
84
|
-
lines.push(` ${text(edge.from)} -- ${text(edge.to)} [label=${text(edge.label)}${style}];`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
lines.push("}");
|
|
88
|
-
return lines.join("\n") + "\n";
|
|
89
|
-
}
|