@takodotid/circuit 0.27.0 → 0.28.1

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 CHANGED
@@ -112,6 +112,7 @@ var valueOfReference = valueOf;
112
112
 
113
113
  // src/transport/console.ts
114
114
  import { spawn } from "node:child_process";
115
+ import { EventEmitter } from "node:events";
115
116
  import { Duplex } from "node:stream";
116
117
  import { Client as Client2 } from "ssh2";
117
118
 
@@ -252,6 +253,33 @@ while True:
252
253
  `;
253
254
  var RELAY_PATH = "/tmp/circuit-console.py";
254
255
  var PORT = /^\/dev\/[\w./:+-]+$/;
256
+ var WINDOWS_PORT = /^COM\d+$/i;
257
+ var WINDOWS_RELAY = `
258
+ $port = New-Object System.IO.Ports.SerialPort $env:CIRCUIT_PORT, ([int]$env:CIRCUIT_BAUD), 'None', 8, 'One'
259
+ try { $port.Open() } catch [System.UnauthorizedAccessException] { [Console]::Error.WriteLine("busy: another program holds " + $env:CIRCUIT_PORT); exit 3 } catch { [Console]::Error.WriteLine("cannot open " + $env:CIRCUIT_PORT + ": " + $_.Exception.Message); exit 2 }
260
+ [Console]::Error.WriteLine("ready")
261
+ $in = [Console]::OpenStandardInput()
262
+ $out = [Console]::OpenStandardOutput()
263
+ $buffer = New-Object byte[] 4096
264
+ $reading = $in.ReadAsync($buffer, 0, $buffer.Length)
265
+ while ($true) {
266
+ if ($reading.IsCompleted) {
267
+ if ($reading.Result -le 0) { break }
268
+ $port.Write($buffer, 0, $reading.Result)
269
+ $reading = $in.ReadAsync($buffer, 0, $buffer.Length)
270
+ }
271
+ $waiting = $port.BytesToRead
272
+ if ($waiting -gt 0) {
273
+ $data = New-Object byte[] $waiting
274
+ $got = $port.Read($data, 0, $waiting)
275
+ $out.Write($data, 0, $got)
276
+ $out.Flush()
277
+ } else {
278
+ Start-Sleep -Milliseconds 10
279
+ }
280
+ }
281
+ $port.Close()
282
+ `;
255
283
  function connectJump(jump) {
256
284
  return new Promise((done, fail) => {
257
285
  const client = new Client2;
@@ -268,38 +296,38 @@ function connectJump(jump) {
268
296
  });
269
297
  }
270
298
  async function openConsole(console2) {
271
- if (!PORT.test(console2.port))
272
- throw new Error(`console port ${console2.port} is not a path under /dev`);
299
+ const windows = !console2.jump && process.platform === "win32";
300
+ if (windows ? !WINDOWS_PORT.test(console2.port) : !PORT.test(console2.port)) {
301
+ throw new Error(`console port ${console2.port} is not ${windows ? "a COM port" : "a path under /dev"}`);
302
+ }
273
303
  if (!Number.isInteger(console2.baud) || console2.baud <= 0)
274
304
  throw new Error(`console baud ${console2.baud} is not a line speed`);
275
- const client = await connectJump(console2.jump);
305
+ for (let attempt = 1;; attempt++) {
306
+ try {
307
+ return await (console2.jump ? openOnJump(console2, console2.jump) : openHere(console2));
308
+ } catch (error) {
309
+ if (attempt >= 5 || !/busy:/.test(error.message))
310
+ throw error;
311
+ await new Promise((done) => setTimeout(done, 5000));
312
+ }
313
+ }
314
+ }
315
+ async function openOnJump(console2, jump) {
316
+ const client = await connectJump(jump);
276
317
  try {
277
318
  const encoded = Buffer.from(RELAY).toString("base64");
278
319
  await exec(client, `sh -c 'printf %s ${encoded} | base64 -d > ${RELAY_PATH}'`, 30000);
279
320
  const channel = await new Promise((done, fail) => {
280
321
  client.exec(`python3 ${RELAY_PATH} ${console2.port} ${console2.baud}`, (error, stream) => error ? fail(error) : done(stream));
281
322
  });
282
- await new Promise((done, fail) => {
283
- let said = "";
284
- const limit = setTimeout(() => fail(new Error(`the console relay did not start: ${said.trim()}`)), 30000);
285
- channel.stderr.on("data", (chunk) => {
286
- said += chunk.toString("utf8");
287
- if (said.includes(`ready
288
- `)) {
289
- clearTimeout(limit);
290
- done();
291
- }
292
- });
293
- channel.on("close", () => {
294
- clearTimeout(limit);
295
- fail(new Error(`the console relay stopped: ${said.trim() || "no reason given"}`));
296
- });
297
- });
323
+ await relayReady(channel);
298
324
  return {
299
- channel,
325
+ channel: asTerminal(channel),
300
326
  close: () => {
301
- channel.end();
302
- client.end();
327
+ quiet(channel, 1000).then(() => {
328
+ channel.end();
329
+ client.end();
330
+ });
303
331
  }
304
332
  };
305
333
  } catch (error) {
@@ -307,6 +335,199 @@ async function openConsole(console2) {
307
335
  throw error;
308
336
  }
309
337
  }
338
+ async function openHere(console2) {
339
+ const child = process.platform === "win32" ? spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_RELAY], {
340
+ env: { ...process.env, CIRCUIT_PORT: console2.port, CIRCUIT_BAUD: String(console2.baud) }
341
+ }) : spawn("python3", ["-c", RELAY, console2.port, String(console2.baud)]);
342
+ const channel = Object.assign(new EventEmitter, {
343
+ stderr: child.stderr,
344
+ write: (data) => child.stdin.write(data),
345
+ end: () => child.stdin.end()
346
+ });
347
+ child.stdout.on("data", (chunk) => channel.emit("data", chunk));
348
+ child.on("close", () => channel.emit("close"));
349
+ child.on("error", (error) => channel.emit("close", error));
350
+ const asChannel = channel;
351
+ try {
352
+ await relayReady(asChannel);
353
+ } catch (error) {
354
+ child.kill();
355
+ throw error;
356
+ }
357
+ return {
358
+ channel: asTerminal(asChannel),
359
+ close: () => {
360
+ quiet(asChannel, 1000).then(() => {
361
+ channel.end();
362
+ setTimeout(() => child.kill(), 2000).unref();
363
+ });
364
+ }
365
+ };
366
+ }
367
+ function relayReady(channel) {
368
+ return new Promise((done, fail) => {
369
+ let said = "";
370
+ const limit = setTimeout(() => fail(new Error(`the console relay did not start: ${said.trim()}`)), 30000);
371
+ channel.stderr.on("data", (chunk) => {
372
+ said += chunk.toString("utf8");
373
+ if (said.includes("ready")) {
374
+ clearTimeout(limit);
375
+ done();
376
+ }
377
+ });
378
+ channel.on("close", () => {
379
+ clearTimeout(limit);
380
+ fail(new Error(`the console relay stopped: ${said.trim() || "no reason given"}`));
381
+ });
382
+ });
383
+ }
384
+ function asTerminal(channel) {
385
+ const write = channel.write.bind(channel);
386
+ channel.write = (data, ...rest) => write(typeof data === "string" ? data.replace(/\r?\n/g, "\r") : data, ...rest);
387
+ return channel;
388
+ }
389
+ function consoleOf(device) {
390
+ const console2 = device.connection.console;
391
+ if (!console2)
392
+ throw new Error(`${device.name}: no connection.console`);
393
+ const user = device.users?.[device.connection.user];
394
+ if (!user)
395
+ throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
396
+ return { console: console2, user: device.connection.user, password: resolve(user.password) };
397
+ }
398
+ var ESCAPES = /\u001b(?:\[[0-9;?]*[A-Za-z]|[()][A-Z0-9]|[=>78c])/g;
399
+ function expect(channel, write, patterns, timeout = 30000) {
400
+ return new Promise((done, fail) => {
401
+ let text = "";
402
+ const stop = () => {
403
+ clearTimeout(limit);
404
+ channel.removeListener("data", onData);
405
+ };
406
+ const onData = (chunk) => {
407
+ text += chunk.toString("utf8").replace(/\r/g, "").replace(ESCAPES, "");
408
+ const index = patterns.findIndex((pattern) => pattern.test(text));
409
+ if (index < 0)
410
+ return;
411
+ stop();
412
+ done({ index, text });
413
+ };
414
+ const limit = setTimeout(() => {
415
+ stop();
416
+ fail(new Error(`the console did not answer: ${text.slice(-200)}`));
417
+ }, timeout);
418
+ channel.on("data", onData);
419
+ if (write)
420
+ channel.write(write);
421
+ });
422
+ }
423
+ async function consoleLogin(channel, dialect, user, password) {
424
+ const others = dialect.others ?? [];
425
+ const states = [dialect.login, dialect.password, ...others.map(([pattern]) => pattern), dialect.prompt];
426
+ const toward = (index) => {
427
+ if (index === 1)
428
+ return `
429
+ `;
430
+ if (index < 2 + others.length)
431
+ return others[index - 2][1];
432
+ return `${dialect.logout}
433
+ `;
434
+ };
435
+ await quiet(channel, 1000);
436
+ let state = (await expect(channel, `
437
+ `, states)).index;
438
+ for (let step = 0;state !== 0; step++) {
439
+ if (step >= 10)
440
+ throw new Error("the console never reached a login prompt");
441
+ state = (await expect(channel, toward(state), states)).index;
442
+ }
443
+ await quiet(channel, 1000);
444
+ const name = dialect.user ? dialect.user(user) : user;
445
+ if ((await expect(channel, `${name}
446
+ `, [dialect.password, dialect.login, dialect.prompt])).index !== 0) {
447
+ throw new Error("the console did not ask for a password");
448
+ }
449
+ let answer = await expect(channel, `${password}
450
+ `, [dialect.prompt, dialect.login, ...others.map(([pattern]) => pattern)], 60000);
451
+ for (let step = 0;answer.index >= 2; step++) {
452
+ if (step >= 5)
453
+ throw new Error("the console kept asking after the login");
454
+ answer = await expect(channel, others[answer.index - 2][1], [
455
+ dialect.prompt,
456
+ dialect.login,
457
+ ...others.map(([pattern]) => pattern)
458
+ ]);
459
+ }
460
+ if (answer.index === 1)
461
+ throw new Error(`the console refused the login of ${user}`);
462
+ await quiet(channel, 800);
463
+ }
464
+ function quiet(channel, silence) {
465
+ return new Promise((done) => {
466
+ let timer = setTimeout(finish, silence);
467
+ const limit = setTimeout(finish, 5000);
468
+ function finish() {
469
+ clearTimeout(timer);
470
+ clearTimeout(limit);
471
+ channel.removeListener("data", onData);
472
+ done();
473
+ }
474
+ function onData() {
475
+ clearTimeout(timer);
476
+ timer = setTimeout(finish, silence);
477
+ }
478
+ channel.on("data", onData);
479
+ });
480
+ }
481
+ function afterEcho(text, command) {
482
+ for (let start = text.indexOf(command[0] ?? "");start >= 0; start = text.indexOf(command[0], start + 1)) {
483
+ const end = echoEnd(text, command, start);
484
+ if (end !== undefined)
485
+ return text.slice(end).replace(/^\r?\n/, "");
486
+ }
487
+ return text;
488
+ }
489
+ function echoEnd(text, command, start) {
490
+ let at = start;
491
+ for (const character of command) {
492
+ while (at < text.length && text[at] !== character && /[\r\n]/.test(text[at]))
493
+ at++;
494
+ if (text[at] !== character)
495
+ return;
496
+ at++;
497
+ }
498
+ return at;
499
+ }
500
+
501
+ class ConsoleShell {
502
+ channel;
503
+ dialect;
504
+ constructor(channel, dialect) {
505
+ this.channel = channel;
506
+ this.dialect = dialect;
507
+ }
508
+ async run(command, timeout = 120000) {
509
+ const pagers = (this.dialect.others ?? []).filter(([, reply]) => !reply.endsWith(`
510
+ `));
511
+ const patterns = [this.dialect.prompt, ...pagers.map(([pattern]) => pattern)];
512
+ let { index, text } = await expect(this.channel, `${command}
513
+ `, patterns, timeout);
514
+ let output = "";
515
+ while (index > 0) {
516
+ const [pattern, reply] = pagers[index - 1];
517
+ output += text.replace(pattern, "");
518
+ ({ index, text } = await expect(this.channel, reply, patterns, timeout));
519
+ }
520
+ output += text;
521
+ const lines = afterEcho(output, command).split(`
522
+ `);
523
+ return lines.slice(0, -1).join(`
524
+ `);
525
+ }
526
+ logout() {
527
+ this.channel.write(`${this.dialect.logout}
528
+ `);
529
+ }
530
+ }
310
531
 
311
532
  // src/adapters/devices/fortios/names.ts
312
533
  function nameOf(device, name) {
@@ -2488,58 +2709,18 @@ async function login(device) {
2488
2709
  password: resolve(user.password)
2489
2710
  });
2490
2711
  }
2491
- var CONSOLE_STATES = [
2492
- ["login", /login: $/],
2493
- ["password", /Password: $/],
2494
- ["banner", BANNER2],
2495
- ["pager", /--More-- $/],
2496
- ["context", /\([^)\n]*\) [#$] $/],
2497
- ["prompt", /[#$] $/]
2498
- ];
2499
- function prompted(channel, write, timeout = 30000) {
2500
- return new Promise((done, fail) => {
2501
- let text = "";
2502
- const stop = () => {
2503
- clearTimeout(limit);
2504
- channel.removeListener("data", onData);
2505
- };
2506
- const onData = (chunk) => {
2507
- text += chunk.toString("utf8").replace(/\r/g, "");
2508
- const state = CONSOLE_STATES.find(([, pattern]) => pattern.test(text));
2509
- if (!state)
2510
- return;
2511
- stop();
2512
- done(state[0]);
2513
- };
2514
- const limit = setTimeout(() => {
2515
- stop();
2516
- fail(new Error(`the console did not answer: ${text.slice(-200)}`));
2517
- }, timeout);
2518
- channel.on("data", onData);
2519
- channel.write(write);
2520
- });
2521
- }
2522
- async function consoleLogin(channel, user, password) {
2523
- const toward = {
2524
- password: "\r",
2525
- banner: "a",
2526
- pager: "q",
2527
- context: "end\r",
2528
- prompt: "exit\r"
2529
- };
2530
- let state = await prompted(channel, "\r");
2531
- for (let step = 0;state !== "login"; step++) {
2532
- if (step >= 10)
2533
- throw new Error(`the console never reached a login prompt; it is at ${state}`);
2534
- state = await prompted(channel, toward[state]);
2535
- }
2536
- if (await prompted(channel, `${user}\r`) !== "password")
2537
- throw new Error("the console did not ask for a password");
2538
- const answer = await prompted(channel, `${password}\r`, 60000);
2539
- if (answer !== "prompt")
2540
- throw new Error(`the console refused the login of ${user}`);
2541
- return new Cli(channel, "\r");
2542
- }
2712
+ var FORTIOS_CONSOLE = {
2713
+ login: /login: $/,
2714
+ password: /Password: $/,
2715
+ prompt: /[#$] $/,
2716
+ logout: "exit",
2717
+ others: [
2718
+ [BANNER2, "a"],
2719
+ [/--More-- $/, "q"],
2720
+ [/\([^)\n]*\) [#$] $/, `end
2721
+ `]
2722
+ ]
2723
+ };
2543
2724
  async function consoleLogout(cli) {
2544
2725
  await leave(cli).catch(() => {
2545
2726
  return;
@@ -2548,23 +2729,13 @@ async function consoleLogout(cli) {
2548
2729
  return;
2549
2730
  });
2550
2731
  }
2551
- function consoleOf(device) {
2552
- const console2 = device.connection.console;
2553
- if (!console2)
2554
- throw new Error(`${device.name}: no connection.console`);
2555
- const user = device.users?.[device.connection.user];
2556
- if (!user)
2557
- throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
2558
- return { console: console2, user: device.connection.user, password: resolve(user.password) };
2559
- }
2560
2732
  async function consoleSession(device, work) {
2561
2733
  const { console: console2, user, password } = consoleOf(device);
2562
2734
  const opened = await openConsole(console2);
2563
2735
  try {
2564
- const cli = await consoleLogin(opened.channel, user, password);
2565
- const relogin = async () => {
2566
- await consoleLogin(opened.channel, user, password);
2567
- };
2736
+ await consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
2737
+ const cli = new Cli(opened.channel);
2738
+ const relogin = () => consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
2568
2739
  const result = await work(cli, relogin);
2569
2740
  await consoleLogout(cli);
2570
2741
  return result;
@@ -4356,8 +4527,8 @@ async function login2(device) {
4356
4527
  password: resolve(user.password)
4357
4528
  });
4358
4529
  }
4359
- async function checkVersion2(client, device) {
4360
- const version = await exec(client, "show version | match Junos:");
4530
+ async function checkVersion2(ask, device) {
4531
+ const version = await ask("show version | match Junos:");
4361
4532
  const release = /Junos: (\d+)\.(\S+)/.exec(version);
4362
4533
  if (!release)
4363
4534
  throw new Error(`${device.name} did not say which Junos it runs: ${version.slice(0, 200)}`);
@@ -4365,10 +4536,19 @@ async function checkVersion2(client, device) {
4365
4536
  throw new Error(`${device.name} runs Junos ${release[1]}.${release[2]}; this adapter renders for Junos ${SUPPORTED_MAJOR}`);
4366
4537
  }
4367
4538
  }
4368
- async function read2(device) {
4539
+ async function read2(device, _network = [], options = {}) {
4540
+ if (options.console) {
4541
+ return onConsole(device, async (channel) => {
4542
+ await checkVersion2((command) => run(channel, command), device);
4543
+ const output = await run(channel, "show configuration | display set | no-more", 180000);
4544
+ if (!/^set /m.test(output))
4545
+ throw new Error(`${device.name} returned no configuration`);
4546
+ return clean2(output);
4547
+ });
4548
+ }
4369
4549
  const client = await login2(device);
4370
4550
  try {
4371
- await checkVersion2(client, device);
4551
+ await checkVersion2((command) => exec(client, command), device);
4372
4552
  const output = await exec(client, "show configuration | display set | no-more", 180000);
4373
4553
  if (!/^set /m.test(output))
4374
4554
  throw new Error(`${device.name} returned no configuration`);
@@ -4391,6 +4571,34 @@ async function edit(channel, line, timeout = 60000) {
4391
4571
  const output = await readUntil(channel, (text) => EDIT_PROMPT.test(text.replace(/\r/g, "")), timeout, 300);
4392
4572
  return output.replace(/\r/g, "");
4393
4573
  }
4574
+ var JUNOS_CONSOLE = {
4575
+ login: /(?:^|\n)login: $/,
4576
+ password: /Password:\s*$/,
4577
+ prompt: /[>%] $/,
4578
+ logout: "exit",
4579
+ others: [[/# $/, `exit configuration-mode
4580
+ `]]
4581
+ };
4582
+ async function onConsole(device, work) {
4583
+ const { console: console2, user, password } = consoleOf(device);
4584
+ const opened = await openConsole(console2);
4585
+ const login = async () => {
4586
+ await consoleLogin(opened.channel, JUNOS_CONSOLE, user, password);
4587
+ const ready = openCli(opened.channel);
4588
+ opened.channel.write(`
4589
+ `);
4590
+ await ready;
4591
+ };
4592
+ try {
4593
+ await login();
4594
+ const result = await work(opened.channel, login);
4595
+ opened.channel.write(`${JUNOS_CONSOLE.logout}
4596
+ `);
4597
+ return result;
4598
+ } finally {
4599
+ opened.close();
4600
+ }
4601
+ }
4394
4602
  async function openCli(channel) {
4395
4603
  const banner = await readUntil(channel, (text) => PROMPT2.test(text.replace(/\r/g, "")), 20000, 500);
4396
4604
  if (/% $/.test(banner.replace(/\r/g, "")))
@@ -4411,7 +4619,7 @@ async function named(channel, line) {
4411
4619
  return [...names].map((name) => `delete ${[...words.slice(0, at), name, ...words.slice(at + 1)].join(" ")}`);
4412
4620
  }
4413
4621
  var REFUSED2 = /^terminal:\d+:\(\d+\) (?!warning)|^error: /m;
4414
- async function configure2(channel, plan, guard) {
4622
+ async function configure2(channel, plan, guard, paced = false) {
4415
4623
  const sent = plan.steps.flatMap((step) => step.send);
4416
4624
  const entered = await run(channel, "configure exclusive");
4417
4625
  if (!/Entering configuration mode/.test(entered) || /^error: /m.test(entered)) {
@@ -4437,9 +4645,13 @@ async function configure2(channel, plan, guard) {
4437
4645
  channel.write(`load set terminal
4438
4646
  `);
4439
4647
  await readUntil(channel, (text) => text.includes("[Type ^D at a new line to end input]"), 20000, 300);
4440
- for (const line of ordered)
4441
- channel.write(`${substitute(line)}
4648
+ for (const line of ordered) {
4649
+ const statement = substitute(line);
4650
+ channel.write(`${statement}
4442
4651
  `);
4652
+ if (paced)
4653
+ await readUntil(channel, (text) => text.replace(/\r/g, "").includes(statement.slice(-24)), 20000, 50);
4654
+ }
4443
4655
  const loaded = await edit(channel, "\x04", 300000);
4444
4656
  const refused = loaded.split(`
4445
4657
  `).filter((line) => REFUSED2.test(line));
@@ -4494,9 +4706,23 @@ async function configure2(channel, plan, guard) {
4494
4706
  console.log(" logged in again, commit confirmed");
4495
4707
  }
4496
4708
  async function apply2(device, _network, plan, options) {
4709
+ if (options.console) {
4710
+ await onConsole(device, async (channel, relogin) => {
4711
+ await checkVersion2((command) => run(channel, command), device);
4712
+ await configure2(channel, plan, {
4713
+ minutes: options.rollback,
4714
+ prove: async () => {
4715
+ await run(channel, "exit configuration-mode");
4716
+ await relogin();
4717
+ await run(channel, "configure exclusive");
4718
+ }
4719
+ }, true);
4720
+ });
4721
+ return;
4722
+ }
4497
4723
  const client = await login2(device);
4498
4724
  try {
4499
- await checkVersion2(client, device);
4725
+ await checkVersion2((command) => exec(client, command), device);
4500
4726
  const channel = await shell(client);
4501
4727
  await openCli(channel);
4502
4728
  await configure2(channel, plan, {
@@ -4754,7 +4980,7 @@ function fillPlan(set, prefixes) {
4754
4980
  async function fill2(device, _network, set, _family, prefixes) {
4755
4981
  const client = await login2(device);
4756
4982
  try {
4757
- await checkVersion2(client, device);
4983
+ await checkVersion2((command) => exec(client, command), device);
4758
4984
  const channel = await shell(client);
4759
4985
  await openCli(channel);
4760
4986
  await configure2(channel, fillPlan(set, prefixes));
@@ -4766,6 +4992,7 @@ async function fill2(device, _network, set, _family, prefixes) {
4766
4992
  }
4767
4993
  var junos = {
4768
4994
  extension: ".conf",
4995
+ console: true,
4769
4996
  unsupported: unsupported2,
4770
4997
  render: render2,
4771
4998
  read: read2,
@@ -5170,7 +5397,25 @@ function physicalPort(settings, vlanId, lagNumbers) {
5170
5397
  return lines;
5171
5398
  }
5172
5399
  var HOST_KEYS = ["ssh-rsa"];
5173
- async function open(device) {
5400
+ var RAISECOM_CONSOLE = {
5401
+ login: /login:\s*$/i,
5402
+ password: /password:\s*$/i,
5403
+ prompt: /[#>]\s*$/,
5404
+ logout: "exit",
5405
+ others: [[/\(config[^)\n]*\)#\s*$/, `end
5406
+ `]]
5407
+ };
5408
+ async function open(device, options = {}) {
5409
+ const session = options.console ? await consoleShell(device) : await sshShell(device);
5410
+ const version = await send(session.channel, "show version", 15000, 1500);
5411
+ if (!version.includes("ROS")) {
5412
+ session.close();
5413
+ throw new Error(`${device.name} did not answer as ROS`);
5414
+ }
5415
+ await send(session.channel, "terminal page-break disable", 15000, 1000);
5416
+ return session;
5417
+ }
5418
+ async function sshShell(device) {
5174
5419
  const user = device.users?.[device.connection.user];
5175
5420
  if (!user)
5176
5421
  throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
@@ -5187,16 +5432,45 @@ async function open(device) {
5187
5432
  client.end();
5188
5433
  };
5189
5434
  const banner = await readUntil(channel, (text) => /[#>]\s*$/.test(text.trimEnd()), 30000);
5190
- const version = await send(channel, "show version", 15000, 1500);
5191
- if (!/[#>]\s*$/.test(banner.trimEnd()) || !version.includes("ROS")) {
5435
+ if (!/[#>]\s*$/.test(banner.trimEnd())) {
5192
5436
  close();
5193
5437
  throw new Error(`${device.name} did not answer as ROS`);
5194
5438
  }
5195
- await send(channel, "terminal page-break disable", 15000, 1000);
5196
- return { channel, close };
5439
+ return {
5440
+ channel,
5441
+ close,
5442
+ fresh: async () => {
5443
+ close();
5444
+ return open(device);
5445
+ }
5446
+ };
5197
5447
  }
5198
- async function read3(device) {
5199
- const { channel, close } = await open(device);
5448
+ async function consoleShell(device) {
5449
+ const { console: console2, user, password } = consoleOf(device);
5450
+ const opened = await openConsole(console2);
5451
+ try {
5452
+ await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
5453
+ } catch (error) {
5454
+ opened.close();
5455
+ throw error;
5456
+ }
5457
+ const session = {
5458
+ channel: opened.channel,
5459
+ close: () => {
5460
+ opened.channel.write(`${RAISECOM_CONSOLE.logout}
5461
+ `);
5462
+ opened.close();
5463
+ },
5464
+ fresh: async () => {
5465
+ await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
5466
+ await send(opened.channel, "terminal page-break disable", 15000, 1000);
5467
+ return session;
5468
+ }
5469
+ };
5470
+ return session;
5471
+ }
5472
+ async function read3(device, _network = [], options = {}) {
5473
+ const { channel, close } = await open(device, options);
5200
5474
  try {
5201
5475
  const dump = await send(channel, "show running-config", 150000, 6000);
5202
5476
  const lines = dump.replace(/\r/g, "").split(`
@@ -5331,8 +5605,9 @@ function unsupported3(device) {
5331
5605
  var REFUSED3 = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
5332
5606
  var CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
5333
5607
  var RELEASE = /^no (\S+ access-list \d+)$/;
5334
- async function apply3(device, _network, plan) {
5335
- const { channel, close } = await open(device);
5608
+ async function apply3(device, _network, plan, options) {
5609
+ const session = await open(device, options);
5610
+ const { channel } = session;
5336
5611
  const released = [];
5337
5612
  try {
5338
5613
  await send(channel, "config", 1e4);
@@ -5366,11 +5641,10 @@ async function apply3(device, _network, plan) {
5366
5641
  });
5367
5642
  if (released.length)
5368
5643
  console.log(` bound again: ${released.join(", ")}`);
5644
+ session.close();
5369
5645
  throw error;
5370
- } finally {
5371
- close();
5372
5646
  }
5373
- const proof = await open(device).catch(() => {
5647
+ const proof = await session.fresh().catch(() => {
5374
5648
  return;
5375
5649
  });
5376
5650
  if (!proof)
@@ -5440,7 +5714,7 @@ function plan3(desired, current) {
5440
5714
  }
5441
5715
  return { steps: planned, problems: [] };
5442
5716
  }
5443
- var raisecom = { extension: ".txt", unsupported: unsupported3, render: render3, read: read3, plan: plan3, apply: apply3 };
5717
+ var raisecom = { extension: ".txt", console: true, unsupported: unsupported3, render: render3, read: read3, plan: plan3, apply: apply3 };
5444
5718
 
5445
5719
  // src/adapters/devices/routeros/export.ts
5446
5720
  var unwrap = (text) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
@@ -7078,8 +7352,56 @@ async function login3(device) {
7078
7352
  password: resolve(user.password)
7079
7353
  });
7080
7354
  }
7081
- async function checkVersion3(client, device) {
7082
- const version = (await exec(client, ":put [/system resource get version]")).trim();
7355
+ var ROUTEROS_CONSOLE = {
7356
+ login: /Login: $/,
7357
+ password: /Password: $/,
7358
+ prompt: /\] > $/,
7359
+ logout: "/quit",
7360
+ others: [
7361
+ [/-- \[Q quit\|D dump\|(?:C-z pause\|)?down\]$/, "D"],
7362
+ [/\[Y\/n\]:? ?$/, "n"]
7363
+ ],
7364
+ user: (user) => `${user}+cwt`
7365
+ };
7366
+ function sshSession(client, device) {
7367
+ return {
7368
+ run: (command, timeout) => exec(client, command, timeout),
7369
+ upload: (path, content) => upload(client, path, content),
7370
+ fresh: async () => {
7371
+ client.end();
7372
+ return sshSession(await login3(device), device);
7373
+ },
7374
+ end: () => client.end()
7375
+ };
7376
+ }
7377
+ async function consoleSession2(device) {
7378
+ const { console: console2, user, password } = consoleOf(device);
7379
+ const opened = await openConsole(console2);
7380
+ try {
7381
+ await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
7382
+ } catch (error) {
7383
+ opened.close();
7384
+ throw error;
7385
+ }
7386
+ const shell = new ConsoleShell(opened.channel, ROUTEROS_CONSOLE);
7387
+ const session = {
7388
+ run: (command, timeout) => shell.run(command, timeout),
7389
+ fresh: async () => {
7390
+ await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
7391
+ return session;
7392
+ },
7393
+ end: () => {
7394
+ shell.logout();
7395
+ opened.close();
7396
+ }
7397
+ };
7398
+ return session;
7399
+ }
7400
+ async function open2(device, options = {}) {
7401
+ return options.console ? consoleSession2(device) : sshSession(await login3(device), device);
7402
+ }
7403
+ async function checkVersion3(session, device) {
7404
+ const version = (await session.run(":put [/system resource get version]")).trim();
7083
7405
  const major = Number(version.split(".")[0]);
7084
7406
  if (major !== SUPPORTED_MAJOR2) {
7085
7407
  throw new Error(`${device.name} runs RouterOS ${version}; this adapter renders for version ${SUPPORTED_MAJOR2}`);
@@ -7094,13 +7416,13 @@ var LIST_CERTIFICATES = [
7094
7416
  ':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
7095
7417
  "}"
7096
7418
  ].join(" ");
7097
- async function read4(device) {
7098
- const client = await login3(device);
7419
+ async function read4(device, _network = [], options = {}) {
7420
+ const session = await open2(device, options);
7099
7421
  try {
7100
- await checkVersion3(client, device);
7101
- const config = await exec(client, "/export", 300000);
7102
- const users = await exec(client, "/user export");
7103
- const certificates = (await exec(client, LIST_CERTIFICATES)).trim();
7422
+ await checkVersion3(session, device);
7423
+ const config = await session.run("/export", 300000);
7424
+ const users = await session.run("/user export");
7425
+ const certificates = (await session.run(LIST_CERTIFICATES)).trim();
7104
7426
  const listed = certificates.split(`
7105
7427
  `).every((line) => line.trim().startsWith("add fingerprint="));
7106
7428
  if (certificates && !listed) {
@@ -7115,7 +7437,7 @@ ${certificates}` : "";
7115
7437
  return withoutTimestamp(text) + `
7116
7438
  `;
7117
7439
  } finally {
7118
- client.end();
7440
+ session.end();
7119
7441
  }
7120
7442
  }
7121
7443
  function unsupported4(device) {
@@ -7172,36 +7494,38 @@ function unsupported4(device) {
7172
7494
  }
7173
7495
  var ROLLBACK_SCHEDULER = "apply-rollback";
7174
7496
  var escapeForString = (text) => text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
7175
- async function refusal(client, command) {
7176
- const compiled = (await exec(client, `:put [:parse "${escapeForString(command)}"]`)).trim();
7497
+ async function refusal(session, command) {
7498
+ const compiled = (await session.run(`:put [:parse "${escapeForString(command)}"]`)).trim();
7177
7499
  const refused = !compiled.startsWith("(") || /syntax error|bad parameter|no such|expected/.test(compiled);
7178
7500
  return refused ? compiled.slice(0, 200) : undefined;
7179
7501
  }
7180
- async function installCertificate(client, device, name) {
7502
+ async function installCertificate(session, device, name) {
7181
7503
  const certificate = device.certificates?.[name];
7182
7504
  if (!certificate)
7183
7505
  throw new Error(`certificate ${name} is not declared`);
7184
- await upload(client, `${name}.crt`, certificate.certificate);
7185
- await exec(client, `/certificate import file-name=${name}.crt name=${name} passphrase=""`);
7506
+ if (!session.upload)
7507
+ throw new Error(`certificate ${name} is installed from a file, which only SSH can write; apply it without --console`);
7508
+ await session.upload(`${name}.crt`, certificate.certificate);
7509
+ await session.run(`/certificate import file-name=${name}.crt name=${name} passphrase=""`);
7186
7510
  if (certificate.private_key) {
7187
7511
  const value = resolve(certificate.private_key);
7188
7512
  const privateKey = value.includes("-----BEGIN") ? `${value}
7189
7513
  ` : Buffer.from(value, "base64").toString("utf8");
7190
- await upload(client, `${name}.key`, privateKey);
7191
- await exec(client, `/certificate import file-name=${name}.key name=${name} passphrase=""`);
7514
+ await session.upload(`${name}.key`, privateKey);
7515
+ await session.run(`/certificate import file-name=${name}.key name=${name} passphrase=""`);
7192
7516
  }
7193
- await exec(client, `/file remove [find name="${name}.crt" or name="${name}.key"]`);
7517
+ await session.run(`/file remove [find name="${name}.crt" or name="${name}.key"]`);
7194
7518
  }
7195
7519
  async function apply4(device, _network, changes, options) {
7196
- const client = await login3(device);
7197
- await checkVersion3(client, device);
7520
+ const session = await open2(device, options);
7521
+ await checkVersion3(session, device);
7198
7522
  const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
7199
7523
  for (const { title, command } of commands) {
7200
7524
  if (command.startsWith(INSTALL_CERTIFICATE))
7201
7525
  continue;
7202
- const reason = await refusal(client, substitute(command));
7526
+ const reason = await refusal(session, substitute(command));
7203
7527
  if (reason) {
7204
- client.end();
7528
+ session.end();
7205
7529
  throw new Error(`${title}: the device will not parse ${command.slice(0, 200)}
7206
7530
  ${reason}`);
7207
7531
  }
@@ -7216,16 +7540,16 @@ async function apply4(device, _network, changes, options) {
7216
7540
  ].join(" ");
7217
7541
  try {
7218
7542
  console.log(` saving ${backup}, arming a ${options.rollback} minute rollback`);
7219
- await exec(client, `/system backup save name=${backup} dont-encrypt=yes`, 120000);
7220
- await exec(client, `/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7221
- await exec(client, armRollback);
7543
+ await session.run(`/system backup save name=${backup} dont-encrypt=yes`, 120000);
7544
+ await session.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7545
+ await session.run(armRollback);
7222
7546
  for (const { title, command } of commands) {
7223
7547
  if (command.startsWith(INSTALL_CERTIFICATE)) {
7224
7548
  const certificateName = command.slice(INSTALL_CERTIFICATE.length).trim();
7225
- await installCertificate(client, device, certificateName);
7549
+ await installCertificate(session, device, certificateName);
7226
7550
  continue;
7227
7551
  }
7228
- const answer = (await exec(client, substitute(command), 120000)).trim();
7552
+ const answer = (await session.run(substitute(command), 120000)).trim();
7229
7553
  if (answer)
7230
7554
  throw new Error(`${title}: ${command.slice(0, 160)}
7231
7555
  device said: ${answer.slice(0, 300)}`);
@@ -7233,20 +7557,19 @@ async function apply4(device, _network, changes, options) {
7233
7557
  } catch (error) {
7234
7558
  console.log(` FAILED: ${error.message}`);
7235
7559
  console.log(` restoring ${backup} now; the rollback stays armed in case this does not get through`);
7236
- await exec(client, `/system backup load name=${backup} password=""`, 30000).catch(() => {
7560
+ await session.run(`/system backup load name=${backup} password=""`, 30000).catch(() => {
7237
7561
  return;
7238
7562
  });
7239
- client.end();
7563
+ session.end();
7240
7564
  throw new Error("apply failed and the backup is being restored");
7241
7565
  }
7242
- client.end();
7243
- const proof = await login3(device).catch(() => {
7566
+ const proof = await session.fresh().catch(() => {
7244
7567
  return;
7245
7568
  });
7246
7569
  if (!proof)
7247
7570
  throw new Error(`could not log in again; the device restores ${backup} within ${options.rollback} minutes`);
7248
- await exec(proof, `/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7249
- await exec(proof, `/file remove [find name="${backup}.backup"]`);
7571
+ await proof.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7572
+ await proof.run(`/file remove [find name="${backup}.backup"]`);
7250
7573
  proof.end();
7251
7574
  console.log(" logged in again, rollback disarmed");
7252
7575
  }
@@ -7270,6 +7593,7 @@ async function fill3(device, _network, set, family, prefixes) {
7270
7593
  }
7271
7594
  var routeros = {
7272
7595
  extension: ".rsc",
7596
+ console: true,
7273
7597
  unsupported: unsupported4,
7274
7598
  render: render4,
7275
7599
  read: read4,
@@ -7742,25 +8066,60 @@ async function openShell(client) {
7742
8066
  await send(channel, "screen-length 0 temporary", 1e4, 1500);
7743
8067
  return channel;
7744
8068
  }
7745
- async function read5(device) {
8069
+ var VRP_CONSOLE = {
8070
+ login: /Username:\s*$/i,
8071
+ password: /Password:\s*$/i,
8072
+ prompt: /<[^>\n]+>\s*$/,
8073
+ logout: "quit",
8074
+ others: [
8075
+ [/Change now\? ?\[Y\/N\]:?\s*$/i, `N
8076
+ `],
8077
+ [/\[~?\*?[^\]\n]+\]\s*$/, `return
8078
+ `]
8079
+ ]
8080
+ };
8081
+ async function onConsole2(device, work) {
8082
+ const { console: console2, user, password } = consoleOf(device);
8083
+ const opened = await openConsole(console2);
8084
+ const login = async () => {
8085
+ await consoleLogin(opened.channel, VRP_CONSOLE, user, password);
8086
+ await send(opened.channel, "screen-length 0 temporary", 1e4, 1500);
8087
+ };
8088
+ try {
8089
+ await login();
8090
+ const result = await work(opened.channel, login);
8091
+ opened.channel.write(`${VRP_CONSOLE.logout}
8092
+ `);
8093
+ return result;
8094
+ } finally {
8095
+ opened.close();
8096
+ }
8097
+ }
8098
+ async function read5(device, _network = [], options = {}) {
8099
+ if (options.console) {
8100
+ return onConsole2(device, async (channel) => configurationOf(device, await send(channel, "display current-configuration", 120000, 3000)));
8101
+ }
7746
8102
  const client = await login4(device);
7747
8103
  try {
7748
8104
  const channel = await openShell(client);
7749
8105
  const dump = await send(channel, "display current-configuration", 120000, 3000);
7750
8106
  channel.end();
7751
- const lines = dump.replace(/\r/g, "").split(`
7752
- `);
7753
- const start = lines.findIndex((line) => line.startsWith("!Software Version"));
7754
- if (start < 0)
7755
- throw new Error(`${device.name} returned no configuration`);
7756
- const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
7757
- return scrub3(body.join(`
7758
- `).trim() + `
7759
- `);
8107
+ return configurationOf(device, dump);
7760
8108
  } finally {
7761
8109
  client.end();
7762
8110
  }
7763
8111
  }
8112
+ function configurationOf(device, dump) {
8113
+ const lines = dump.replace(/\r/g, "").split(`
8114
+ `);
8115
+ const start = lines.findIndex((line) => line.startsWith("!Software Version"));
8116
+ if (start < 0)
8117
+ throw new Error(`${device.name} returned no configuration`);
8118
+ const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
8119
+ return scrub3(body.join(`
8120
+ `).trim() + `
8121
+ `);
8122
+ }
7764
8123
  function unsupported5(device) {
7765
8124
  const found = [];
7766
8125
  for (const [name, port] of Object.entries(device.ports ?? {})) {
@@ -7857,9 +8216,7 @@ async function sendLine(channel, line) {
7857
8216
  answer += await send(channel, "Y", 20000, 800);
7858
8217
  return answer;
7859
8218
  }
7860
- async function apply5(device, _network, plan) {
7861
- const client = await login4(device);
7862
- const channel = await openShell(client);
8219
+ async function commitPlan(channel, plan) {
7863
8220
  try {
7864
8221
  await send(channel, "system-view", 1e4, 1500);
7865
8222
  for (const step of plan.steps) {
@@ -7878,24 +8235,42 @@ async function apply5(device, _network, plan) {
7878
8235
  await send(channel, "clear configuration candidate", 1e4).catch(() => {
7879
8236
  return;
7880
8237
  });
7881
- channel.end();
7882
- client.end();
7883
8238
  throw error;
7884
8239
  }
7885
- const proof = await login4(device).catch(() => {
8240
+ }
8241
+ async function save(channel) {
8242
+ const saving = await send(channel, "save", 20000, 2000);
8243
+ if (/\[Y\/N\]/i.test(saving))
8244
+ await send(channel, "Y", 60000, 5000);
8245
+ }
8246
+ var NOT_SAVED = "Not saved: a reboot restores the previous configuration.";
8247
+ async function apply5(device, _network, plan, options) {
8248
+ if (options.console) {
8249
+ await onConsole2(device, async (channel, relogin) => {
8250
+ await commitPlan(channel, plan);
8251
+ await relogin().catch((error) => {
8252
+ throw new Error(`committed, but a fresh login on the console failed: ${error.message}. ${NOT_SAVED}`);
8253
+ });
8254
+ await save(channel);
8255
+ });
8256
+ console.log(" committed, login proved, saved");
7886
8257
  return;
7887
- });
7888
- if (!proof) {
8258
+ }
8259
+ const client = await login4(device);
8260
+ const channel = await openShell(client);
8261
+ try {
8262
+ await commitPlan(channel, plan);
8263
+ const proof = await login4(device).catch(() => {
8264
+ return;
8265
+ });
8266
+ if (!proof)
8267
+ throw new Error(`committed, but a fresh login failed. ${NOT_SAVED} Recover over the open session or the console.`);
8268
+ proof.end();
8269
+ await save(channel);
8270
+ } finally {
7889
8271
  channel.end();
7890
8272
  client.end();
7891
- throw new Error("committed, but a fresh login failed. Not saved: a reboot restores the previous configuration. Recover over the open session or the console.");
7892
8273
  }
7893
- proof.end();
7894
- const saving = await send(channel, "save", 20000, 2000);
7895
- if (/\[Y\/N\]/i.test(saving))
7896
- await send(channel, "Y", 60000, 5000);
7897
- channel.end();
7898
- client.end();
7899
8274
  console.log(" committed, login proved, saved");
7900
8275
  }
7901
8276
  function plan5(desired, current) {
@@ -7920,6 +8295,7 @@ function plan5(desired, current) {
7920
8295
  }
7921
8296
  var vrp = {
7922
8297
  extension: ".cfg",
8298
+ console: true,
7923
8299
  unsupported: unsupported5,
7924
8300
  render: render5,
7925
8301
  read: read5,