@takodotid/circuit 0.13.0 → 0.14.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/dist/cli/index.js +102 -1
- package/dist/types/adapters/registries/librenms.d.ts +15 -0
- package/dist/types/adapters/registries/prometheus.d.ts +11 -0
- package/dist/types/core/define.d.ts +7 -0
- package/package.json +1 -1
- package/src/adapters/registries/librenms.ts +90 -0
- package/src/adapters/registries/prometheus.ts +19 -0
- package/src/cli/index.ts +35 -1
- package/src/core/define.ts +7 -0
package/dist/cli/index.js
CHANGED
|
@@ -3347,6 +3347,67 @@ async function ripeStat(query, family) {
|
|
|
3347
3347
|
}
|
|
3348
3348
|
var registries = { "ripe-stat": ripeStat };
|
|
3349
3349
|
|
|
3350
|
+
// src/adapters/registries/librenms.ts
|
|
3351
|
+
var AUTH = { sha1: "SHA", sha256: "SHA-256" };
|
|
3352
|
+
var PRIVACY = { aes128: "AES", des: "DES" };
|
|
3353
|
+
function settingsOf2(network2) {
|
|
3354
|
+
if (!network2.librenms)
|
|
3355
|
+
throw new Error("the network has no librenms settings");
|
|
3356
|
+
return {
|
|
3357
|
+
base: `${network2.librenms.url.replace(/\/+$/, "")}/api/v0`,
|
|
3358
|
+
headers: { "X-Auth-Token": resolve(network2.librenms.api_token), "Content-Type": "application/json" }
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
function credentials(device) {
|
|
3362
|
+
const snmp3 = device.management?.snmp;
|
|
3363
|
+
const [name, user] = Object.entries(snmp3?.users ?? {})[0] ?? [];
|
|
3364
|
+
if (name && user) {
|
|
3365
|
+
return {
|
|
3366
|
+
snmpver: "v3",
|
|
3367
|
+
authlevel: "authPriv",
|
|
3368
|
+
authname: name,
|
|
3369
|
+
authpass: resolve(user.auth_password),
|
|
3370
|
+
authalgo: AUTH[user.auth],
|
|
3371
|
+
cryptopass: resolve(user.privacy_password),
|
|
3372
|
+
cryptoalgo: PRIVACY[user.privacy]
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
if (snmp3?.community)
|
|
3376
|
+
return { snmpver: "v2c", community: resolve(snmp3.community) };
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
async function planLibreNms(network2) {
|
|
3380
|
+
const { base, headers } = settingsOf2(network2);
|
|
3381
|
+
const response = await fetch(`${base}/devices?type=all`, { headers, signal: AbortSignal.timeout(30000) });
|
|
3382
|
+
if (!response.ok)
|
|
3383
|
+
throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
3384
|
+
const monitored = new Set((await response.json()).devices.map((device) => device.hostname));
|
|
3385
|
+
const ours = new Set(network2.devices.map((device) => device.connection.host));
|
|
3386
|
+
const missing = network2.devices.filter((device) => !monitored.has(device.connection.host));
|
|
3387
|
+
return {
|
|
3388
|
+
additions: missing.filter((device) => device.management?.snmp),
|
|
3389
|
+
withoutSnmp: missing.filter((device) => !device.management?.snmp),
|
|
3390
|
+
unknown: [...monitored].filter((hostname) => !ours.has(hostname)).sort()
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
async function sendLibreNms(network2, additions) {
|
|
3394
|
+
const { base, headers } = settingsOf2(network2);
|
|
3395
|
+
for (const device of additions) {
|
|
3396
|
+
const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
|
|
3397
|
+
const response = await fetch(`${base}/devices`, {
|
|
3398
|
+
method: "POST",
|
|
3399
|
+
headers,
|
|
3400
|
+
body: JSON.stringify(body),
|
|
3401
|
+
signal: AbortSignal.timeout(60000)
|
|
3402
|
+
});
|
|
3403
|
+
const answer = await response.json().catch(() => ({}));
|
|
3404
|
+
if (!response.ok || answer.status !== "ok") {
|
|
3405
|
+
throw new Error(`${device.name}: LibreNMS refused it: ${answer.message ?? response.status}`);
|
|
3406
|
+
}
|
|
3407
|
+
console.log(` ${device.name}: ${answer.message}`);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3350
3411
|
// src/adapters/registries/peeringdb.ts
|
|
3351
3412
|
var API = "https://www.peeringdb.com/api";
|
|
3352
3413
|
var addressesOf = (iface) => (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
@@ -3440,6 +3501,17 @@ async function sendPeeringDb(network2, changes2) {
|
|
|
3440
3501
|
}
|
|
3441
3502
|
}
|
|
3442
3503
|
|
|
3504
|
+
// src/adapters/registries/prometheus.ts
|
|
3505
|
+
function prometheusTargets(network2) {
|
|
3506
|
+
return {
|
|
3507
|
+
targets: network2.devices.filter((device) => device.management?.snmp).map((device) => ({
|
|
3508
|
+
targets: [device.connection.host],
|
|
3509
|
+
labels: { device: device.name, platform: device.platform, model: device.model }
|
|
3510
|
+
})),
|
|
3511
|
+
withoutSnmp: network2.devices.filter((device) => !device.management?.snmp)
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3443
3515
|
// src/core/validate.ts
|
|
3444
3516
|
import { X509Certificate as X509Certificate2 } from "node:crypto";
|
|
3445
3517
|
function validate(network2) {
|
|
@@ -3796,6 +3868,8 @@ var HELP = `usage: circuit <command> [device...] [options]
|
|
|
3796
3868
|
a client config for one peer, its private key left for the peer to fill
|
|
3797
3869
|
communities the network's BGP communities, one per line, for a looking glass or bgp.tools
|
|
3798
3870
|
peeringdb bring PeeringDB's exchange records in line with the config, with --confirm
|
|
3871
|
+
librenms add every device LibreNMS does not monitor yet, with --confirm
|
|
3872
|
+
prometheus every device with SNMP, as Prometheus targets for snmp_exporter
|
|
3799
3873
|
secrets every secret the config uses, and whether each can be read
|
|
3800
3874
|
|
|
3801
3875
|
--config <path> the network's config file, circuit.config.ts in the working directory by default`;
|
|
@@ -4030,6 +4104,31 @@ Nothing sent. Re-run with --confirm.`);
|
|
|
4030
4104
|
}
|
|
4031
4105
|
await sendPeeringDb(network2, plan4.changes);
|
|
4032
4106
|
}
|
|
4107
|
+
async function librenmsCommand() {
|
|
4108
|
+
const plan4 = await planLibreNms(network2);
|
|
4109
|
+
for (const hostname of plan4.unknown)
|
|
4110
|
+
console.log(` LibreNMS monitors ${hostname}, which the config does not. Remove it by hand if it is gone.`);
|
|
4111
|
+
for (const device of plan4.withoutSnmp)
|
|
4112
|
+
console.log(` ${device.name} has no SNMP in its config, so LibreNMS cannot poll it. Add management.snmp first.`);
|
|
4113
|
+
for (const device of plan4.additions)
|
|
4114
|
+
console.log(` add ${device.name}, ${device.connection.host}`);
|
|
4115
|
+
if (!plan4.additions.length) {
|
|
4116
|
+
console.log("LibreNMS monitors every device it can.");
|
|
4117
|
+
return;
|
|
4118
|
+
}
|
|
4119
|
+
if (!confirmed) {
|
|
4120
|
+
console.log(`
|
|
4121
|
+
Nothing sent. Re-run with --confirm.`);
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
await sendLibreNms(network2, plan4.additions);
|
|
4125
|
+
}
|
|
4126
|
+
function prometheusCommand() {
|
|
4127
|
+
const { targets, withoutSnmp } = prometheusTargets(network2);
|
|
4128
|
+
for (const device of withoutSnmp)
|
|
4129
|
+
console.error(`${device.name} has no SNMP in its config; left out`);
|
|
4130
|
+
console.log(JSON.stringify(targets, null, 4));
|
|
4131
|
+
}
|
|
4033
4132
|
function secretsIn(value2, found = new Set) {
|
|
4034
4133
|
if (isSecret(value2))
|
|
4035
4134
|
found.add(referenceOf(value2));
|
|
@@ -4042,7 +4141,7 @@ function secretsIn(value2, found = new Set) {
|
|
|
4042
4141
|
return found;
|
|
4043
4142
|
}
|
|
4044
4143
|
function secretsCommand() {
|
|
4045
|
-
const references = [...secretsIn([network2.devices, network2.peeringdb])].sort();
|
|
4144
|
+
const references = [...secretsIn([network2.devices, network2.peeringdb, network2.librenms])].sort();
|
|
4046
4145
|
let missing = 0;
|
|
4047
4146
|
for (const reference of references) {
|
|
4048
4147
|
const name = reference.replace(/^file:/, "");
|
|
@@ -4068,6 +4167,8 @@ var commands = {
|
|
|
4068
4167
|
wireguard: wireguardCommand,
|
|
4069
4168
|
communities: communitiesCommand,
|
|
4070
4169
|
peeringdb: peeringdbCommand,
|
|
4170
|
+
librenms: librenmsCommand,
|
|
4171
|
+
prometheus: prometheusCommand,
|
|
4071
4172
|
secrets: secretsCommand
|
|
4072
4173
|
};
|
|
4073
4174
|
var run = commands[command2];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Network } from "../../core/define";
|
|
2
|
+
import type { Device } from "../../schema";
|
|
3
|
+
/** What `librenms` would do. */
|
|
4
|
+
export type LibreNmsPlan = {
|
|
5
|
+
/** Devices to add. */
|
|
6
|
+
additions: Device[];
|
|
7
|
+
/** Devices that cannot be added: their config turns SNMP off. */
|
|
8
|
+
withoutSnmp: Device[];
|
|
9
|
+
/** Hostnames LibreNMS holds that no device in the config has. */
|
|
10
|
+
unknown: string[];
|
|
11
|
+
};
|
|
12
|
+
/** Compare the config with what LibreNMS monitors. A device is known to LibreNMS by its management address. */
|
|
13
|
+
export declare function planLibreNms(network: Network): Promise<LibreNmsPlan>;
|
|
14
|
+
/** Add each device, named after its `name` and polled with the credentials from its config. */
|
|
15
|
+
export declare function sendLibreNms(network: Network, additions: readonly Device[]): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Network } from "../../core/define";
|
|
2
|
+
import type { Device } from "../../schema";
|
|
3
|
+
export type PrometheusTarget = {
|
|
4
|
+
targets: string[];
|
|
5
|
+
labels: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
/** The targets, and the devices left out because their config turns SNMP off. */
|
|
8
|
+
export declare function prometheusTargets(network: Network): {
|
|
9
|
+
targets: PrometheusTarget[];
|
|
10
|
+
withoutSnmp: Device[];
|
|
11
|
+
};
|
|
@@ -45,6 +45,13 @@ export type Network = {
|
|
|
45
45
|
/** An API key with write access to the network's record. */
|
|
46
46
|
api_key: Secret;
|
|
47
47
|
};
|
|
48
|
+
/** Keeps LibreNMS monitoring every device: `circuit librenms`. */
|
|
49
|
+
librenms?: {
|
|
50
|
+
/** Where LibreNMS is, such as `https://librenms.example.com`. */
|
|
51
|
+
url: string;
|
|
52
|
+
/** An API token that can add devices. */
|
|
53
|
+
api_token: Secret;
|
|
54
|
+
};
|
|
48
55
|
};
|
|
49
56
|
/** Declare the network, in `circuit.config.ts`. */
|
|
50
57
|
export declare const defineNetwork: (network: Network) => Network;
|
package/package.json
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
const ours = new Set(network.devices.map((device) => device.connection.host));
|
|
61
|
+
const missing = network.devices.filter((device) => !monitored.has(device.connection.host));
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
additions: missing.filter((device) => device.management?.snmp),
|
|
65
|
+
withoutSnmp: missing.filter((device) => !device.management?.snmp),
|
|
66
|
+
unknown: [...monitored].filter((hostname) => !ours.has(hostname)).sort(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Add each device, named after its `name` and polled with the credentials from its config. */
|
|
71
|
+
export async function sendLibreNms(network: Network, additions: readonly Device[]): Promise<void> {
|
|
72
|
+
const { base, headers } = settingsOf(network);
|
|
73
|
+
|
|
74
|
+
for (const device of additions) {
|
|
75
|
+
const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
|
|
76
|
+
const response = await fetch(`${base}/devices`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers,
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
signal: AbortSignal.timeout(60_000),
|
|
81
|
+
});
|
|
82
|
+
const answer = (await response.json().catch(() => ({}))) as { status?: string; message?: string };
|
|
83
|
+
|
|
84
|
+
if (!response.ok || answer.status !== "ok") {
|
|
85
|
+
throw new Error(`${device.name}: LibreNMS refused it: ${answer.message ?? response.status}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(` ${device.name}: ${answer.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Prometheus targets for snmp_exporter, in its file-based discovery format: one entry per device that answers SNMP, labelled with what the config knows about it.
|
|
2
|
+
|
|
3
|
+
import type { Network } from "../../core/define";
|
|
4
|
+
import type { Device } from "../../schema";
|
|
5
|
+
|
|
6
|
+
export type PrometheusTarget = { targets: string[]; labels: Record<string, string> };
|
|
7
|
+
|
|
8
|
+
/** The targets, and the devices left out because their config turns SNMP off. */
|
|
9
|
+
export function prometheusTargets(network: Network): { targets: PrometheusTarget[]; withoutSnmp: Device[] } {
|
|
10
|
+
return {
|
|
11
|
+
targets: network.devices
|
|
12
|
+
.filter((device) => device.management?.snmp)
|
|
13
|
+
.map((device) => ({
|
|
14
|
+
targets: [device.connection.host],
|
|
15
|
+
labels: { device: device.name, platform: device.platform, model: device.model },
|
|
16
|
+
})),
|
|
17
|
+
withoutSnmp: network.devices.filter((device) => !device.management?.snmp),
|
|
18
|
+
};
|
|
19
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
7
7
|
import { adapters } from "../adapters/devices";
|
|
8
8
|
import type { ApplyOptions, DeviceAdapter, Plan } from "../adapters/devices/types";
|
|
9
9
|
import { registries } from "../adapters/registries";
|
|
10
|
+
import { planLibreNms, sendLibreNms } from "../adapters/registries/librenms";
|
|
10
11
|
import { planPeeringDb, sendPeeringDb } from "../adapters/registries/peeringdb";
|
|
12
|
+
import { prometheusTargets } from "../adapters/registries/prometheus";
|
|
11
13
|
import { network as prefixOf } from "../core/addr";
|
|
12
14
|
import type { Network } from "../core/define";
|
|
13
15
|
import { isSecret, referenceOf, resolve as reveal, sourceOf, useRoot } from "../core/secrets";
|
|
@@ -29,6 +31,8 @@ const HELP = `usage: circuit <command> [device...] [options]
|
|
|
29
31
|
a client config for one peer, its private key left for the peer to fill
|
|
30
32
|
communities the network's BGP communities, one per line, for a looking glass or bgp.tools
|
|
31
33
|
peeringdb bring PeeringDB's exchange records in line with the config, with --confirm
|
|
34
|
+
librenms add every device LibreNMS does not monitor yet, with --confirm
|
|
35
|
+
prometheus every device with SNMP, as Prometheus targets for snmp_exporter
|
|
32
36
|
secrets every secret the config uses, and whether each can be read
|
|
33
37
|
|
|
34
38
|
--config <path> the network's config file, circuit.config.ts in the working directory by default`;
|
|
@@ -325,6 +329,34 @@ async function peeringdbCommand(): Promise<void> {
|
|
|
325
329
|
await sendPeeringDb(network, plan.changes);
|
|
326
330
|
}
|
|
327
331
|
|
|
332
|
+
async function librenmsCommand(): Promise<void> {
|
|
333
|
+
const plan = await planLibreNms(network);
|
|
334
|
+
|
|
335
|
+
for (const hostname of plan.unknown)
|
|
336
|
+
console.log(` LibreNMS monitors ${hostname}, which the config does not. Remove it by hand if it is gone.`);
|
|
337
|
+
for (const device of plan.withoutSnmp)
|
|
338
|
+
console.log(` ${device.name} has no SNMP in its config, so LibreNMS cannot poll it. Add management.snmp first.`);
|
|
339
|
+
for (const device of plan.additions) console.log(` add ${device.name}, ${device.connection.host}`);
|
|
340
|
+
|
|
341
|
+
if (!plan.additions.length) {
|
|
342
|
+
console.log("LibreNMS monitors every device it can.");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!confirmed) {
|
|
347
|
+
console.log("\nNothing sent. Re-run with --confirm.");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await sendLibreNms(network, plan.additions);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function prometheusCommand(): void {
|
|
355
|
+
const { targets, withoutSnmp } = prometheusTargets(network);
|
|
356
|
+
for (const device of withoutSnmp) console.error(`${device.name} has no SNMP in its config; left out`);
|
|
357
|
+
console.log(JSON.stringify(targets, null, 4));
|
|
358
|
+
}
|
|
359
|
+
|
|
328
360
|
/** Every secret reference in a value, however deep. */
|
|
329
361
|
function secretsIn(value: unknown, found = new Set<string>()): Set<string> {
|
|
330
362
|
if (isSecret(value)) found.add(referenceOf(value));
|
|
@@ -335,7 +367,7 @@ function secretsIn(value: unknown, found = new Set<string>()): Set<string> {
|
|
|
335
367
|
}
|
|
336
368
|
|
|
337
369
|
function secretsCommand(): void {
|
|
338
|
-
const references = [...secretsIn([network.devices, network.peeringdb])].sort();
|
|
370
|
+
const references = [...secretsIn([network.devices, network.peeringdb, network.librenms])].sort();
|
|
339
371
|
let missing = 0;
|
|
340
372
|
|
|
341
373
|
for (const reference of references) {
|
|
@@ -365,6 +397,8 @@ const commands: Record<string, () => void | Promise<void>> = {
|
|
|
365
397
|
wireguard: wireguardCommand,
|
|
366
398
|
communities: communitiesCommand,
|
|
367
399
|
peeringdb: peeringdbCommand,
|
|
400
|
+
librenms: librenmsCommand,
|
|
401
|
+
prometheus: prometheusCommand,
|
|
368
402
|
secrets: secretsCommand,
|
|
369
403
|
};
|
|
370
404
|
|
package/src/core/define.ts
CHANGED
|
@@ -86,6 +86,13 @@ export type Network = {
|
|
|
86
86
|
/** An API key with write access to the network's record. */
|
|
87
87
|
api_key: Secret;
|
|
88
88
|
};
|
|
89
|
+
/** Keeps LibreNMS monitoring every device: `circuit librenms`. */
|
|
90
|
+
librenms?: {
|
|
91
|
+
/** Where LibreNMS is, such as `https://librenms.example.com`. */
|
|
92
|
+
url: string;
|
|
93
|
+
/** An API token that can add devices. */
|
|
94
|
+
api_token: Secret;
|
|
95
|
+
};
|
|
89
96
|
};
|
|
90
97
|
|
|
91
98
|
/** Declare the network, in `circuit.config.ts`. */
|