@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.
@@ -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 [`${device.platform} ${device.model}`, `reached at ${device.connection.host}`, ...addresses];
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) => {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  packageRoot
3
- } from "./index-22sbskdx.js";
3
+ } from "./index-47debt16.js";
4
4
 
5
5
  // src/studio/server.ts
6
6
  import { execFile as execFile2, spawn } from "node:child_process";
package/dist/cli/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  addressesOf,
12
12
  freeOfLength,
13
13
  studioData
14
- } from "../chunks/index-22sbskdx.js";
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.connection.host,
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
- out.set("protocols sflow agent-id", device.connection.host);
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.connection.host,
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.connection.host,
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.connection.host,
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
- config.line(`sflow agent ip ${device.connection.host}`);
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.connection.host,
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 ours = new Set(network.devices.map((device) => device.connection.host));
8361
- const missing = network.devices.filter((device) => !monitored.has(device.connection.host));
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 (!options.console)
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, options);
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, options);
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, options);
9455
- writeState(device, await adapter.read(device, network3.devices, options));
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-1ftkqkx5.js");
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: IP;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
5
5
  "keywords": [
6
6
  "network",
@@ -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.connection.host,
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.connection.host,
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.connection.host,
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.connection.host,
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.connection.host,
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
- const ours = new Set(network.devices.map((device) => device.connection.host));
61
- const missing = network.devices.filter((device) => !monitored.has(device.connection.host));
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
- /** The adapter of a device, which must be able to use its console when `--console` asks for it. */
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 (!options.console) return adapter;
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, options);
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, options);
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, options);
269
+ await adapter.apply(device, network.devices, plan, optionsFor(device));
265
270
 
266
- writeState(device, await adapter.read(device, network.devices, options));
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 [`${device.platform} ${device.model}`, `reached at ${device.connection.host}`, ...addresses];
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. */
@@ -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);
@@ -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: IP;
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. */
@@ -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) => {