@takodotid/circuit 0.28.1 → 0.29.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/chunks/{index-22sbskdx.js → index-47debt16.js} +5 -1
- package/dist/chunks/{server-1ftkqkx5.js → server-tjbb3rqc.js} +1 -1
- package/dist/cli/index.js +30 -17
- package/dist/types/schema/device.d.ts +2 -2
- package/dist/types/transport/ssh.d.ts +3 -0
- package/package.json +1 -1
- package/src/adapters/devices/fortios/index.ts +2 -2
- package/src/adapters/devices/junos/index.ts +2 -2
- package/src/adapters/devices/junos/render.ts +1 -1
- package/src/adapters/devices/raisecom-ros/index.ts +2 -2
- package/src/adapters/devices/routeros/index.ts +2 -2
- package/src/adapters/devices/vrp/index.ts +3 -3
- package/src/adapters/registries/librenms.ts +4 -2
- package/src/adapters/registries/prometheus.ts +2 -2
- package/src/cli/index.ts +11 -6
- package/src/core/graph.ts +5 -1
- package/src/core/validate.ts +2 -0
- package/src/schema/device.ts +2 -2
- package/src/transport/ssh.ts +7 -0
|
@@ -117,7 +117,11 @@ function describeDevice(device) {
|
|
|
117
117
|
const vrf = settings.vrf ? `, VRF ${settings.vrf}` : "";
|
|
118
118
|
return `${name}: ${list.join(", ")}${vrf}`;
|
|
119
119
|
});
|
|
120
|
-
return [
|
|
120
|
+
return [
|
|
121
|
+
`${device.platform} ${device.model}`,
|
|
122
|
+
device.connection.host ? `reached at ${device.connection.host}` : "reached through its console",
|
|
123
|
+
...addresses
|
|
124
|
+
];
|
|
121
125
|
}
|
|
122
126
|
function carriedBy(device, settings) {
|
|
123
127
|
const vlan = (name, untagged) => {
|
package/dist/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
addressesOf,
|
|
12
12
|
freeOfLength,
|
|
13
13
|
studioData
|
|
14
|
-
} from "../chunks/index-
|
|
14
|
+
} from "../chunks/index-47debt16.js";
|
|
15
15
|
import {
|
|
16
16
|
familyOf2,
|
|
17
17
|
split,
|
|
@@ -118,6 +118,11 @@ import { Client as Client2 } from "ssh2";
|
|
|
118
118
|
|
|
119
119
|
// src/transport/ssh.ts
|
|
120
120
|
import { Client } from "ssh2";
|
|
121
|
+
function managementHost(device) {
|
|
122
|
+
if (!device.connection.host)
|
|
123
|
+
throw new Error(`${device.name} has no management address; it is reached through its console`);
|
|
124
|
+
return device.connection.host;
|
|
125
|
+
}
|
|
121
126
|
function connect(login) {
|
|
122
127
|
return new Promise((resolve, reject) => {
|
|
123
128
|
const client = new Client;
|
|
@@ -2703,7 +2708,7 @@ async function login(device) {
|
|
|
2703
2708
|
if (!user)
|
|
2704
2709
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
2705
2710
|
return connect({
|
|
2706
|
-
host: device
|
|
2711
|
+
host: managementHost(device),
|
|
2707
2712
|
port: device.connection.port,
|
|
2708
2713
|
user: device.connection.user,
|
|
2709
2714
|
password: resolve(user.password)
|
|
@@ -3947,7 +3952,8 @@ function sflow(out, device, names) {
|
|
|
3947
3952
|
const flow = device.flow_export;
|
|
3948
3953
|
if (flow?.protocol !== "sflow")
|
|
3949
3954
|
return;
|
|
3950
|
-
|
|
3955
|
+
if (device.connection.host)
|
|
3956
|
+
out.set("protocols sflow agent-id", device.connection.host);
|
|
3951
3957
|
if (flow.sampling !== undefined)
|
|
3952
3958
|
out.set("protocols sflow sample-rate ingress", flow.sampling);
|
|
3953
3959
|
for (const collector of flow.collectors) {
|
|
@@ -4521,7 +4527,7 @@ async function login2(device) {
|
|
|
4521
4527
|
if (!user)
|
|
4522
4528
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
4523
4529
|
return connect({
|
|
4524
|
-
host: device
|
|
4530
|
+
host: managementHost(device),
|
|
4525
4531
|
port: device.connection.port,
|
|
4526
4532
|
user: device.connection.user,
|
|
4527
4533
|
password: resolve(user.password)
|
|
@@ -5420,7 +5426,7 @@ async function sshShell(device) {
|
|
|
5420
5426
|
if (!user)
|
|
5421
5427
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
5422
5428
|
const client = await connect({
|
|
5423
|
-
host: device
|
|
5429
|
+
host: managementHost(device),
|
|
5424
5430
|
port: device.connection.port,
|
|
5425
5431
|
user: device.connection.user,
|
|
5426
5432
|
password: resolve(user.password),
|
|
@@ -7346,7 +7352,7 @@ async function login3(device) {
|
|
|
7346
7352
|
if (!user)
|
|
7347
7353
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
7348
7354
|
return connect({
|
|
7349
|
-
host: device
|
|
7355
|
+
host: managementHost(device),
|
|
7350
7356
|
port: device.connection.port,
|
|
7351
7357
|
user: device.connection.user,
|
|
7352
7358
|
password: resolve(user.password)
|
|
@@ -7985,7 +7991,8 @@ function flowExport3(config, device) {
|
|
|
7985
7991
|
const flow = device.flow_export;
|
|
7986
7992
|
if (flow?.protocol !== "sflow")
|
|
7987
7993
|
return;
|
|
7988
|
-
|
|
7994
|
+
if (device.connection.host)
|
|
7995
|
+
config.line(`sflow agent ip ${device.connection.host}`);
|
|
7989
7996
|
flow.collectors.forEach((collector, index) => {
|
|
7990
7997
|
const number = FIRST_COLLECTOR + index;
|
|
7991
7998
|
config.line(`sflow collector ${number} ip ${collector.address} udp-port ${collector.port ?? 6343}`);
|
|
@@ -8051,7 +8058,7 @@ async function login4(device) {
|
|
|
8051
8058
|
if (!user)
|
|
8052
8059
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
8053
8060
|
return connect({
|
|
8054
|
-
host: device
|
|
8061
|
+
host: managementHost(device),
|
|
8055
8062
|
port: device.connection.port,
|
|
8056
8063
|
user: device.connection.user,
|
|
8057
8064
|
password: resolve(user.password),
|
|
@@ -8357,8 +8364,9 @@ async function planLibreNms(network) {
|
|
|
8357
8364
|
if (!response.ok)
|
|
8358
8365
|
throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
8359
8366
|
const monitored = new Set((await response.json()).devices.map((device) => device.hostname));
|
|
8360
|
-
const
|
|
8361
|
-
const
|
|
8367
|
+
const polled = network.devices.filter((device) => device.connection.host);
|
|
8368
|
+
const ours = new Set(polled.map((device) => device.connection.host));
|
|
8369
|
+
const missing = polled.filter((device) => !monitored.has(device.connection.host));
|
|
8362
8370
|
return {
|
|
8363
8371
|
additions: missing.filter((device) => device.management?.snmp),
|
|
8364
8372
|
withoutSnmp: missing.filter((device) => !device.management?.snmp),
|
|
@@ -8481,7 +8489,7 @@ var AUTH2 = { sha1: "SHA", sha256: "SHA256" };
|
|
|
8481
8489
|
var PRIVACY2 = { aes128: "AES", des: "DES" };
|
|
8482
8490
|
function prometheusTargets(network) {
|
|
8483
8491
|
return {
|
|
8484
|
-
targets: network.devices.filter((device) => device.management?.snmp).map((device) => ({
|
|
8492
|
+
targets: network.devices.filter((device) => device.management?.snmp && device.connection.host).map((device) => ({
|
|
8485
8493
|
targets: [device.connection.host],
|
|
8486
8494
|
labels: { device: device.name, platform: device.platform, model: device.model, __param_auth: device.name }
|
|
8487
8495
|
})),
|
|
@@ -8838,6 +8846,8 @@ function checkDevice(device, report) {
|
|
|
8838
8846
|
};
|
|
8839
8847
|
if (!device.users?.[device.connection.user])
|
|
8840
8848
|
report(`connection user '${device.connection.user}' is not one of its users`);
|
|
8849
|
+
if (!device.connection.host && !device.connection.console)
|
|
8850
|
+
report("connection has neither a host nor a console to reach the device through");
|
|
8841
8851
|
checkVlans(device, report);
|
|
8842
8852
|
checkAddresses2(device, report, cidr, ip);
|
|
8843
8853
|
checkPorts(device, names, report);
|
|
@@ -9343,9 +9353,12 @@ function one() {
|
|
|
9343
9353
|
const [device] = selected(false);
|
|
9344
9354
|
return device ?? fail("name a device");
|
|
9345
9355
|
}
|
|
9356
|
+
function optionsFor(device) {
|
|
9357
|
+
return options.console || !device.connection.host ? { ...options, console: true } : options;
|
|
9358
|
+
}
|
|
9346
9359
|
function adapterOf(device) {
|
|
9347
9360
|
const adapter = adapters[device.platform] ?? fail(`no adapter for ${device.platform}`);
|
|
9348
|
-
if (!
|
|
9361
|
+
if (!optionsFor(device).console)
|
|
9349
9362
|
return adapter;
|
|
9350
9363
|
if (!adapter.console)
|
|
9351
9364
|
fail(`${device.name}: ${device.platform} cannot be reached through a console yet`);
|
|
@@ -9419,7 +9432,7 @@ function diffCommand() {
|
|
|
9419
9432
|
}
|
|
9420
9433
|
async function snapshotCommand() {
|
|
9421
9434
|
for (const device of selected(true)) {
|
|
9422
|
-
const text = await adapterOf(device).read(device, network3.devices,
|
|
9435
|
+
const text = await adapterOf(device).read(device, network3.devices, optionsFor(device));
|
|
9423
9436
|
writeState(device, text);
|
|
9424
9437
|
console.log(`${device.name}: ${text.split(`
|
|
9425
9438
|
`).length} lines`);
|
|
@@ -9438,7 +9451,7 @@ async function applyCommand() {
|
|
|
9438
9451
|
const device = one();
|
|
9439
9452
|
guard([device]);
|
|
9440
9453
|
const adapter = adapterOf(device);
|
|
9441
|
-
const live = await adapter.read(device, network3.devices,
|
|
9454
|
+
const live = await adapter.read(device, network3.devices, optionsFor(device));
|
|
9442
9455
|
writeState(device, live);
|
|
9443
9456
|
const plan = adapter.plan(adapter.render(device), live, options);
|
|
9444
9457
|
if (!show(device, plan))
|
|
@@ -9451,8 +9464,8 @@ Not sent: resolve the problems first.`);
|
|
|
9451
9464
|
Nothing sent. Re-run with --confirm.`);
|
|
9452
9465
|
return;
|
|
9453
9466
|
}
|
|
9454
|
-
await adapter.apply(device, network3.devices, plan,
|
|
9455
|
-
writeState(device, await adapter.read(device, network3.devices,
|
|
9467
|
+
await adapter.apply(device, network3.devices, plan, optionsFor(device));
|
|
9468
|
+
writeState(device, await adapter.read(device, network3.devices, optionsFor(device)));
|
|
9456
9469
|
console.log("snapshot updated");
|
|
9457
9470
|
}
|
|
9458
9471
|
async function refreshCommand() {
|
|
@@ -9741,7 +9754,7 @@ async function studioCommand() {
|
|
|
9741
9754
|
const port = Number(flagValue("port") ?? 4600);
|
|
9742
9755
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
9743
9756
|
fail("--port takes a number from 1 to 65535");
|
|
9744
|
-
const { isLoopback, studio } = await import("../chunks/server-
|
|
9757
|
+
const { isLoopback, studio } = await import("../chunks/server-tjbb3rqc.js");
|
|
9745
9758
|
const host = flagValue("host") ?? "127.0.0.1";
|
|
9746
9759
|
const allowHosts = (flagValue("allow-host") ?? "").split(",").filter(Boolean);
|
|
9747
9760
|
if (!isLoopback(host) && !allowHosts.length) {
|
|
@@ -6,8 +6,8 @@ import type { Routing } from "./routing";
|
|
|
6
6
|
import type { Certificate, DhcpRelay, DhcpServer, FlowExport, Management, System, User } from "./system";
|
|
7
7
|
/** How the framework reaches the device. */
|
|
8
8
|
export type Connection<U extends string> = {
|
|
9
|
-
/** Management address. */
|
|
10
|
-
host
|
|
9
|
+
/** Management address. Absent for a device reached only through its console, `console`. */
|
|
10
|
+
host?: IP;
|
|
11
11
|
/** One of the device's `users`. Its password is the login. */
|
|
12
12
|
user: U;
|
|
13
13
|
/** SSH port. 22 when absent. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Client, type ClientChannel } from "ssh2";
|
|
2
|
+
import type { Device } from "../schema";
|
|
2
3
|
export type Login = {
|
|
3
4
|
host: string;
|
|
4
5
|
port?: number;
|
|
@@ -7,6 +8,8 @@ export type Login = {
|
|
|
7
8
|
/** Host key algorithms to accept, for a device that negotiates one it then signs wrongly. */
|
|
8
9
|
hostKeys?: string[];
|
|
9
10
|
};
|
|
11
|
+
/** The address SSH reaches a device at. A device without one is reached only through its console. */
|
|
12
|
+
export declare function managementHost(device: Device): string;
|
|
10
13
|
/** Open a session. */
|
|
11
14
|
export declare function connect(login: Login): Promise<Client>;
|
|
12
15
|
/** Run one command and return what it printed, stderr included. */
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import { familyOf, host, split } from "../../../core/addr";
|
|
|
7
7
|
import { MARKER, resolve, valueOfReference } from "../../../core/secrets";
|
|
8
8
|
import type { AclRule, Device, Family, FilterRule } from "../../../schema";
|
|
9
9
|
import { type ConsoleDialect, consoleLogin, consoleOf, openConsole } from "../../../transport/console";
|
|
10
|
-
import { connect, shell } from "../../../transport/ssh";
|
|
10
|
+
import { connect, managementHost, shell } from "../../../transport/ssh";
|
|
11
11
|
import { portsOf, spell } from "../catalog";
|
|
12
12
|
import type { ApplyOptions, DeviceAdapter, Plan, ReadOptions } from "../types";
|
|
13
13
|
import { acceptsBeforeDrop, familiesOf, isImplied, serviceOf } from "./firewall";
|
|
@@ -141,7 +141,7 @@ export async function login(device: Device): Promise<Client> {
|
|
|
141
141
|
if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
142
142
|
|
|
143
143
|
return connect({
|
|
144
|
-
host: device
|
|
144
|
+
host: managementHost(device),
|
|
145
145
|
port: device.connection.port,
|
|
146
146
|
user: device.connection.user,
|
|
147
147
|
password: resolve(user.password),
|
|
@@ -7,7 +7,7 @@ import { familyOf, host, isAddress, split } from "../../../core/addr";
|
|
|
7
7
|
import { resolve, substitute } from "../../../core/secrets";
|
|
8
8
|
import type { Device, Family } from "../../../schema";
|
|
9
9
|
import { type ConsoleDialect, consoleLogin, consoleOf, openConsole } from "../../../transport/console";
|
|
10
|
-
import { connect, exec, readUntil, shell } from "../../../transport/ssh";
|
|
10
|
+
import { connect, exec, managementHost, readUntil, shell } from "../../../transport/ssh";
|
|
11
11
|
import { lagIds } from "../derive";
|
|
12
12
|
import type { ApplyOptions, DeviceAdapter, Plan, ReadOptions, Step } from "../types";
|
|
13
13
|
import { GENERATED, settingsOf } from "./bgp";
|
|
@@ -296,7 +296,7 @@ export async function login(device: Device): Promise<Client> {
|
|
|
296
296
|
if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
297
297
|
|
|
298
298
|
return connect({
|
|
299
|
-
host: device
|
|
299
|
+
host: managementHost(device),
|
|
300
300
|
port: device.connection.port,
|
|
301
301
|
user: device.connection.user,
|
|
302
302
|
password: resolve(user.password),
|
|
@@ -675,7 +675,7 @@ function sflow(out: Statements, device: Device, names: Names): void {
|
|
|
675
675
|
const flow = device.flow_export;
|
|
676
676
|
if (flow?.protocol !== "sflow") return;
|
|
677
677
|
|
|
678
|
-
out.set("protocols sflow agent-id", device.connection.host);
|
|
678
|
+
if (device.connection.host) out.set("protocols sflow agent-id", device.connection.host);
|
|
679
679
|
if (flow.sampling !== undefined) out.set("protocols sflow sample-rate ingress", flow.sampling);
|
|
680
680
|
|
|
681
681
|
for (const collector of flow.collectors) {
|
|
@@ -8,7 +8,7 @@ import { marker, resolve, substitute } from "../../../core/secrets";
|
|
|
8
8
|
import { offset } from "../../../core/time";
|
|
9
9
|
import type { Device, Port, SnmpUser, Switched, Vrrp } from "../../../schema";
|
|
10
10
|
import { type ConsoleDialect, consoleLogin, consoleOf, openConsole } from "../../../transport/console";
|
|
11
|
-
import { connect, readUntil, send, shell } from "../../../transport/ssh";
|
|
11
|
+
import { connect, managementHost, readUntil, send, shell } from "../../../transport/ssh";
|
|
12
12
|
import { portsOf, spell } from "../catalog";
|
|
13
13
|
import { ipv4Networks, lagIds, switchUnsupported } from "../derive";
|
|
14
14
|
import { compare, type Dialect, entries, type Group, scrub, steps, vlanListChange } from "../lines";
|
|
@@ -338,7 +338,7 @@ async function sshShell(device: Device): Promise<Session> {
|
|
|
338
338
|
if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
339
339
|
|
|
340
340
|
const client = await connect({
|
|
341
|
-
host: device
|
|
341
|
+
host: managementHost(device),
|
|
342
342
|
port: device.connection.port,
|
|
343
343
|
user: device.connection.user,
|
|
344
344
|
password: resolve(user.password),
|
|
@@ -7,7 +7,7 @@ import { familyOf } from "../../../core/addr";
|
|
|
7
7
|
import { resolve, substitute } from "../../../core/secrets";
|
|
8
8
|
import type { Device, Family } from "../../../schema";
|
|
9
9
|
import { type ConsoleDialect, consoleLogin, consoleOf, ConsoleShell, openConsole } from "../../../transport/console";
|
|
10
|
-
import { connect, exec, upload } from "../../../transport/ssh";
|
|
10
|
+
import { connect, exec, managementHost, upload } from "../../../transport/ssh";
|
|
11
11
|
import type { ApplyOptions, DeviceAdapter, Plan, ReadOptions } from "../types";
|
|
12
12
|
import { INSTALL_CERTIFICATE, plan } from "./plan";
|
|
13
13
|
import { spell } from "../catalog";
|
|
@@ -21,7 +21,7 @@ export async function login(device: Device): Promise<Client> {
|
|
|
21
21
|
if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
22
22
|
|
|
23
23
|
return connect({
|
|
24
|
-
host: device
|
|
24
|
+
host: managementHost(device),
|
|
25
25
|
port: device.connection.port,
|
|
26
26
|
user: device.connection.user,
|
|
27
27
|
password: resolve(user.password),
|
|
@@ -8,7 +8,7 @@ import { marker, resolve, substitute, valueOfReference } from "../../../core/sec
|
|
|
8
8
|
import { offset } from "../../../core/time";
|
|
9
9
|
import type { Device, Interface, Port, Vrrp } from "../../../schema";
|
|
10
10
|
import { type ConsoleDialect, consoleLogin, consoleOf, openConsole } from "../../../transport/console";
|
|
11
|
-
import { connect, readUntil, send, shell } from "../../../transport/ssh";
|
|
11
|
+
import { connect, managementHost, readUntil, send, shell } from "../../../transport/ssh";
|
|
12
12
|
import { portsOf, spell } from "../catalog";
|
|
13
13
|
import { ipv4Networks, lagIds, switchUnsupported } from "../derive";
|
|
14
14
|
import { compare, type Dialect, scrub, steps, vlanListChange } from "../lines";
|
|
@@ -465,7 +465,7 @@ function flowExport(config: Config, device: Device): void {
|
|
|
465
465
|
const flow = device.flow_export;
|
|
466
466
|
if (flow?.protocol !== "sflow") return;
|
|
467
467
|
|
|
468
|
-
config.line(`sflow agent ip ${device.connection.host}`);
|
|
468
|
+
if (device.connection.host) config.line(`sflow agent ip ${device.connection.host}`);
|
|
469
469
|
|
|
470
470
|
flow.collectors.forEach((collector, index) => {
|
|
471
471
|
const number = FIRST_COLLECTOR + index;
|
|
@@ -550,7 +550,7 @@ export async function login(device: Device): Promise<Client> {
|
|
|
550
550
|
const user = device.users?.[device.connection.user];
|
|
551
551
|
if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
552
552
|
return connect({
|
|
553
|
-
host: device
|
|
553
|
+
host: managementHost(device),
|
|
554
554
|
port: device.connection.port,
|
|
555
555
|
user: device.connection.user,
|
|
556
556
|
password: resolve(user.password),
|
|
@@ -57,8 +57,10 @@ export async function planLibreNms(network: Network): Promise<LibreNmsPlan> {
|
|
|
57
57
|
if (!response.ok) throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
58
58
|
|
|
59
59
|
const monitored = new Set(((await response.json()) as { devices: { hostname: string }[] }).devices.map((device) => device.hostname));
|
|
60
|
-
|
|
61
|
-
const
|
|
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!));
|
|
62
64
|
|
|
63
65
|
return {
|
|
64
66
|
additions: missing.filter((device) => device.management?.snmp),
|
|
@@ -15,9 +15,9 @@ const PRIVACY = { aes128: "AES", des: "DES" } as const;
|
|
|
15
15
|
export function prometheusTargets(network: Network): { targets: PrometheusTarget[]; withoutSnmp: Device[] } {
|
|
16
16
|
return {
|
|
17
17
|
targets: network.devices
|
|
18
|
-
.filter((device) => device.management?.snmp)
|
|
18
|
+
.filter((device) => device.management?.snmp && device.connection.host)
|
|
19
19
|
.map((device) => ({
|
|
20
|
-
targets: [device.connection.host],
|
|
20
|
+
targets: [device.connection.host!],
|
|
21
21
|
// `__param_auth` picks the device's own auth in snmp_exporter.
|
|
22
22
|
labels: { device: device.name, platform: device.platform, model: device.model, __param_auth: device.name },
|
|
23
23
|
})),
|
package/src/cli/index.ts
CHANGED
|
@@ -133,10 +133,15 @@ function one(): Device {
|
|
|
133
133
|
return device ?? fail("name a device");
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/**
|
|
136
|
+
/** How a device is reached: through its console when `--console` asks, or when it has no management address. */
|
|
137
|
+
function optionsFor(device: Device): ApplyOptions {
|
|
138
|
+
return options.console || !device.connection.host ? { ...options, console: true } : options;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The adapter of a device, which must be able to use its console when the device is reached through it. */
|
|
137
142
|
function adapterOf(device: Device): DeviceAdapter {
|
|
138
143
|
const adapter = adapters[device.platform] ?? fail(`no adapter for ${device.platform}`);
|
|
139
|
-
if (!
|
|
144
|
+
if (!optionsFor(device).console) return adapter;
|
|
140
145
|
|
|
141
146
|
if (!adapter.console) fail(`${device.name}: ${device.platform} cannot be reached through a console yet`);
|
|
142
147
|
if (!device.connection.console) fail(`${device.name}: --console needs connection.console`);
|
|
@@ -225,7 +230,7 @@ function diffCommand(): void {
|
|
|
225
230
|
|
|
226
231
|
async function snapshotCommand(): Promise<void> {
|
|
227
232
|
for (const device of selected(true)) {
|
|
228
|
-
const text = await adapterOf(device).read(device, network.devices,
|
|
233
|
+
const text = await adapterOf(device).read(device, network.devices, optionsFor(device));
|
|
229
234
|
writeState(device, text);
|
|
230
235
|
console.log(`${device.name}: ${text.split("\n").length} lines`);
|
|
231
236
|
}
|
|
@@ -248,7 +253,7 @@ async function applyCommand(): Promise<void> {
|
|
|
248
253
|
const adapter = adapterOf(device);
|
|
249
254
|
|
|
250
255
|
// The plan is made against what the device runs now, never against an old snapshot.
|
|
251
|
-
const live = await adapter.read(device, network.devices,
|
|
256
|
+
const live = await adapter.read(device, network.devices, optionsFor(device));
|
|
252
257
|
writeState(device, live);
|
|
253
258
|
|
|
254
259
|
const plan = adapter.plan(adapter.render(device), live, options);
|
|
@@ -261,9 +266,9 @@ async function applyCommand(): Promise<void> {
|
|
|
261
266
|
return;
|
|
262
267
|
}
|
|
263
268
|
|
|
264
|
-
await adapter.apply(device, network.devices, plan,
|
|
269
|
+
await adapter.apply(device, network.devices, plan, optionsFor(device));
|
|
265
270
|
|
|
266
|
-
writeState(device, await adapter.read(device, network.devices,
|
|
271
|
+
writeState(device, await adapter.read(device, network.devices, optionsFor(device)));
|
|
267
272
|
console.log("snapshot updated");
|
|
268
273
|
}
|
|
269
274
|
|
package/src/core/graph.ts
CHANGED
|
@@ -143,7 +143,11 @@ function describeDevice(device: Device): string[] {
|
|
|
143
143
|
return `${name}: ${list.join(", ")}${vrf}`;
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
return [
|
|
146
|
+
return [
|
|
147
|
+
`${device.platform} ${device.model}`,
|
|
148
|
+
device.connection.host ? `reached at ${device.connection.host}` : "reached through its console",
|
|
149
|
+
...addresses,
|
|
150
|
+
];
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
/** The VLANs a port or LAG carries, by id. */
|
package/src/core/validate.ts
CHANGED
|
@@ -79,6 +79,8 @@ function checkDevice(device: Device, report: Report): void {
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
if (!device.users?.[device.connection.user]) report(`connection user '${device.connection.user}' is not one of its users`);
|
|
82
|
+
if (!device.connection.host && !device.connection.console)
|
|
83
|
+
report("connection has neither a host nor a console to reach the device through");
|
|
82
84
|
|
|
83
85
|
checkVlans(device, report);
|
|
84
86
|
checkAddresses(device, report, cidr, ip);
|
package/src/schema/device.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type { Certificate, DhcpRelay, DhcpServer, FlowExport, Management, System
|
|
|
7
7
|
|
|
8
8
|
/** How the framework reaches the device. */
|
|
9
9
|
export type Connection<U extends string> = {
|
|
10
|
-
/** Management address. */
|
|
11
|
-
host
|
|
10
|
+
/** Management address. Absent for a device reached only through its console, `console`. */
|
|
11
|
+
host?: IP;
|
|
12
12
|
/** One of the device's `users`. Its password is the login. */
|
|
13
13
|
user: U;
|
|
14
14
|
/** SSH port. 22 when absent. */
|
package/src/transport/ssh.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SSH sessions to devices. Adapters choose how to talk to their platform; this holds what they share.
|
|
2
2
|
|
|
3
3
|
import { Client, type ClientChannel } from "ssh2";
|
|
4
|
+
import type { Device } from "../schema";
|
|
4
5
|
|
|
5
6
|
export type Login = {
|
|
6
7
|
host: string;
|
|
@@ -11,6 +12,12 @@ export type Login = {
|
|
|
11
12
|
hostKeys?: string[];
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
/** The address SSH reaches a device at. A device without one is reached only through its console. */
|
|
16
|
+
export function managementHost(device: Device): string {
|
|
17
|
+
if (!device.connection.host) throw new Error(`${device.name} has no management address; it is reached through its console`);
|
|
18
|
+
return device.connection.host;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
/** Open a session. */
|
|
15
22
|
export function connect(login: Login): Promise<Client> {
|
|
16
23
|
return new Promise((resolve, reject) => {
|