@takodotid/circuit 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js 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,35 +296,33 @@ 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
327
  channel.end();
302
328
  client.end();
@@ -307,6 +333,195 @@ async function openConsole(console2) {
307
333
  throw error;
308
334
  }
309
335
  }
336
+ async function openHere(console2) {
337
+ const child = process.platform === "win32" ? spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_RELAY], {
338
+ env: { ...process.env, CIRCUIT_PORT: console2.port, CIRCUIT_BAUD: String(console2.baud) }
339
+ }) : spawn("python3", ["-c", RELAY, console2.port, String(console2.baud)]);
340
+ const channel = Object.assign(new EventEmitter, {
341
+ stderr: child.stderr,
342
+ write: (data) => child.stdin.write(data),
343
+ end: () => child.stdin.end()
344
+ });
345
+ child.stdout.on("data", (chunk) => channel.emit("data", chunk));
346
+ child.on("close", () => channel.emit("close"));
347
+ child.on("error", (error) => channel.emit("close", error));
348
+ const asChannel = channel;
349
+ try {
350
+ await relayReady(asChannel);
351
+ } catch (error) {
352
+ child.kill();
353
+ throw error;
354
+ }
355
+ return {
356
+ channel: asTerminal(asChannel),
357
+ close: () => {
358
+ channel.end();
359
+ setTimeout(() => child.kill(), 2000).unref();
360
+ }
361
+ };
362
+ }
363
+ function relayReady(channel) {
364
+ return new Promise((done, fail) => {
365
+ let said = "";
366
+ const limit = setTimeout(() => fail(new Error(`the console relay did not start: ${said.trim()}`)), 30000);
367
+ channel.stderr.on("data", (chunk) => {
368
+ said += chunk.toString("utf8");
369
+ if (said.includes("ready")) {
370
+ clearTimeout(limit);
371
+ done();
372
+ }
373
+ });
374
+ channel.on("close", () => {
375
+ clearTimeout(limit);
376
+ fail(new Error(`the console relay stopped: ${said.trim() || "no reason given"}`));
377
+ });
378
+ });
379
+ }
380
+ function asTerminal(channel) {
381
+ const write = channel.write.bind(channel);
382
+ channel.write = (data, ...rest) => write(typeof data === "string" ? data.replace(/\r?\n/g, "\r") : data, ...rest);
383
+ return channel;
384
+ }
385
+ function consoleOf(device) {
386
+ const console2 = device.connection.console;
387
+ if (!console2)
388
+ throw new Error(`${device.name}: no connection.console`);
389
+ const user = device.users?.[device.connection.user];
390
+ if (!user)
391
+ throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
392
+ return { console: console2, user: device.connection.user, password: resolve(user.password) };
393
+ }
394
+ var ESCAPES = /\u001b(?:\[[0-9;?]*[A-Za-z]|[()][A-Z0-9]|[=>78c])/g;
395
+ function expect(channel, write, patterns, timeout = 30000) {
396
+ return new Promise((done, fail) => {
397
+ let text = "";
398
+ const stop = () => {
399
+ clearTimeout(limit);
400
+ channel.removeListener("data", onData);
401
+ };
402
+ const onData = (chunk) => {
403
+ text += chunk.toString("utf8").replace(/\r/g, "").replace(ESCAPES, "");
404
+ const index = patterns.findIndex((pattern) => pattern.test(text));
405
+ if (index < 0)
406
+ return;
407
+ stop();
408
+ done({ index, text });
409
+ };
410
+ const limit = setTimeout(() => {
411
+ stop();
412
+ fail(new Error(`the console did not answer: ${text.slice(-200)}`));
413
+ }, timeout);
414
+ channel.on("data", onData);
415
+ if (write)
416
+ channel.write(write);
417
+ });
418
+ }
419
+ async function consoleLogin(channel, dialect, user, password) {
420
+ const others = dialect.others ?? [];
421
+ const states = [dialect.login, dialect.password, ...others.map(([pattern]) => pattern), dialect.prompt];
422
+ const toward = (index) => {
423
+ if (index === 1)
424
+ return `
425
+ `;
426
+ if (index < 2 + others.length)
427
+ return others[index - 2][1];
428
+ return `${dialect.logout}
429
+ `;
430
+ };
431
+ let state = (await expect(channel, `
432
+ `, states)).index;
433
+ for (let step = 0;state !== 0; step++) {
434
+ if (step >= 10)
435
+ throw new Error("the console never reached a login prompt");
436
+ state = (await expect(channel, toward(state), states)).index;
437
+ }
438
+ const name = dialect.user ? dialect.user(user) : user;
439
+ if ((await expect(channel, `${name}
440
+ `, [dialect.password, dialect.login, dialect.prompt])).index !== 0) {
441
+ throw new Error("the console did not ask for a password");
442
+ }
443
+ let answer = await expect(channel, `${password}
444
+ `, [dialect.prompt, dialect.login, ...others.map(([pattern]) => pattern)], 60000);
445
+ for (let step = 0;answer.index >= 2; step++) {
446
+ if (step >= 5)
447
+ throw new Error("the console kept asking after the login");
448
+ answer = await expect(channel, others[answer.index - 2][1], [
449
+ dialect.prompt,
450
+ dialect.login,
451
+ ...others.map(([pattern]) => pattern)
452
+ ]);
453
+ }
454
+ if (answer.index === 1)
455
+ throw new Error(`the console refused the login of ${user}`);
456
+ await quiet(channel, 800);
457
+ }
458
+ function quiet(channel, silence) {
459
+ return new Promise((done) => {
460
+ let timer = setTimeout(finish, silence);
461
+ const limit = setTimeout(finish, 5000);
462
+ function finish() {
463
+ clearTimeout(timer);
464
+ clearTimeout(limit);
465
+ channel.removeListener("data", onData);
466
+ done();
467
+ }
468
+ function onData() {
469
+ clearTimeout(timer);
470
+ timer = setTimeout(finish, silence);
471
+ }
472
+ channel.on("data", onData);
473
+ });
474
+ }
475
+ function afterEcho(text, command) {
476
+ for (let start = text.indexOf(command[0] ?? "");start >= 0; start = text.indexOf(command[0], start + 1)) {
477
+ const end = echoEnd(text, command, start);
478
+ if (end !== undefined)
479
+ return text.slice(end).replace(/^\r?\n/, "");
480
+ }
481
+ return text;
482
+ }
483
+ function echoEnd(text, command, start) {
484
+ let at = start;
485
+ for (const character of command) {
486
+ while (at < text.length && text[at] !== character && /[\r\n]/.test(text[at]))
487
+ at++;
488
+ if (text[at] !== character)
489
+ return;
490
+ at++;
491
+ }
492
+ return at;
493
+ }
494
+
495
+ class ConsoleShell {
496
+ channel;
497
+ dialect;
498
+ constructor(channel, dialect) {
499
+ this.channel = channel;
500
+ this.dialect = dialect;
501
+ }
502
+ async run(command, timeout = 120000) {
503
+ const pagers = (this.dialect.others ?? []).filter(([, reply]) => !reply.endsWith(`
504
+ `));
505
+ const patterns = [this.dialect.prompt, ...pagers.map(([pattern]) => pattern)];
506
+ let { index, text } = await expect(this.channel, `${command}
507
+ `, patterns, timeout);
508
+ let output = "";
509
+ while (index > 0) {
510
+ const [pattern, reply] = pagers[index - 1];
511
+ output += text.replace(pattern, "");
512
+ ({ index, text } = await expect(this.channel, reply, patterns, timeout));
513
+ }
514
+ output += text;
515
+ const lines = afterEcho(output, command).split(`
516
+ `);
517
+ return lines.slice(0, -1).join(`
518
+ `);
519
+ }
520
+ logout() {
521
+ this.channel.write(`${this.dialect.logout}
522
+ `);
523
+ }
524
+ }
310
525
 
311
526
  // src/adapters/devices/fortios/names.ts
312
527
  function nameOf(device, name) {
@@ -2488,58 +2703,18 @@ async function login(device) {
2488
2703
  password: resolve(user.password)
2489
2704
  });
2490
2705
  }
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
- }
2706
+ var FORTIOS_CONSOLE = {
2707
+ login: /login: $/,
2708
+ password: /Password: $/,
2709
+ prompt: /[#$] $/,
2710
+ logout: "exit",
2711
+ others: [
2712
+ [BANNER2, "a"],
2713
+ [/--More-- $/, "q"],
2714
+ [/\([^)\n]*\) [#$] $/, `end
2715
+ `]
2716
+ ]
2717
+ };
2543
2718
  async function consoleLogout(cli) {
2544
2719
  await leave(cli).catch(() => {
2545
2720
  return;
@@ -2548,23 +2723,13 @@ async function consoleLogout(cli) {
2548
2723
  return;
2549
2724
  });
2550
2725
  }
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
2726
  async function consoleSession(device, work) {
2561
2727
  const { console: console2, user, password } = consoleOf(device);
2562
2728
  const opened = await openConsole(console2);
2563
2729
  try {
2564
- const cli = await consoleLogin(opened.channel, user, password);
2565
- const relogin = async () => {
2566
- await consoleLogin(opened.channel, user, password);
2567
- };
2730
+ await consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
2731
+ const cli = new Cli(opened.channel);
2732
+ const relogin = () => consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
2568
2733
  const result = await work(cli, relogin);
2569
2734
  await consoleLogout(cli);
2570
2735
  return result;
@@ -4356,8 +4521,8 @@ async function login2(device) {
4356
4521
  password: resolve(user.password)
4357
4522
  });
4358
4523
  }
4359
- async function checkVersion2(client, device) {
4360
- const version = await exec(client, "show version | match Junos:");
4524
+ async function checkVersion2(ask, device) {
4525
+ const version = await ask("show version | match Junos:");
4361
4526
  const release = /Junos: (\d+)\.(\S+)/.exec(version);
4362
4527
  if (!release)
4363
4528
  throw new Error(`${device.name} did not say which Junos it runs: ${version.slice(0, 200)}`);
@@ -4365,10 +4530,19 @@ async function checkVersion2(client, device) {
4365
4530
  throw new Error(`${device.name} runs Junos ${release[1]}.${release[2]}; this adapter renders for Junos ${SUPPORTED_MAJOR}`);
4366
4531
  }
4367
4532
  }
4368
- async function read2(device) {
4533
+ async function read2(device, _network = [], options = {}) {
4534
+ if (options.console) {
4535
+ return onConsole(device, async (channel) => {
4536
+ await checkVersion2((command) => run(channel, command), device);
4537
+ const output = await run(channel, "show configuration | display set | no-more", 180000);
4538
+ if (!/^set /m.test(output))
4539
+ throw new Error(`${device.name} returned no configuration`);
4540
+ return clean2(output);
4541
+ });
4542
+ }
4369
4543
  const client = await login2(device);
4370
4544
  try {
4371
- await checkVersion2(client, device);
4545
+ await checkVersion2((command) => exec(client, command), device);
4372
4546
  const output = await exec(client, "show configuration | display set | no-more", 180000);
4373
4547
  if (!/^set /m.test(output))
4374
4548
  throw new Error(`${device.name} returned no configuration`);
@@ -4391,6 +4565,34 @@ async function edit(channel, line, timeout = 60000) {
4391
4565
  const output = await readUntil(channel, (text) => EDIT_PROMPT.test(text.replace(/\r/g, "")), timeout, 300);
4392
4566
  return output.replace(/\r/g, "");
4393
4567
  }
4568
+ var JUNOS_CONSOLE = {
4569
+ login: /login: $/,
4570
+ password: /Password:\s*$/,
4571
+ prompt: /[>%] $/,
4572
+ logout: "exit",
4573
+ others: [[/# $/, `exit configuration-mode
4574
+ `]]
4575
+ };
4576
+ async function onConsole(device, work) {
4577
+ const { console: console2, user, password } = consoleOf(device);
4578
+ const opened = await openConsole(console2);
4579
+ const login = async () => {
4580
+ await consoleLogin(opened.channel, JUNOS_CONSOLE, user, password);
4581
+ const ready = openCli(opened.channel);
4582
+ opened.channel.write(`
4583
+ `);
4584
+ await ready;
4585
+ };
4586
+ try {
4587
+ await login();
4588
+ const result = await work(opened.channel, login);
4589
+ opened.channel.write(`${JUNOS_CONSOLE.logout}
4590
+ `);
4591
+ return result;
4592
+ } finally {
4593
+ opened.close();
4594
+ }
4595
+ }
4394
4596
  async function openCli(channel) {
4395
4597
  const banner = await readUntil(channel, (text) => PROMPT2.test(text.replace(/\r/g, "")), 20000, 500);
4396
4598
  if (/% $/.test(banner.replace(/\r/g, "")))
@@ -4494,9 +4696,23 @@ async function configure2(channel, plan, guard) {
4494
4696
  console.log(" logged in again, commit confirmed");
4495
4697
  }
4496
4698
  async function apply2(device, _network, plan, options) {
4699
+ if (options.console) {
4700
+ await onConsole(device, async (channel, relogin) => {
4701
+ await checkVersion2((command) => run(channel, command), device);
4702
+ await configure2(channel, plan, {
4703
+ minutes: options.rollback,
4704
+ prove: async () => {
4705
+ await run(channel, "exit configuration-mode");
4706
+ await relogin();
4707
+ await run(channel, "configure exclusive");
4708
+ }
4709
+ });
4710
+ });
4711
+ return;
4712
+ }
4497
4713
  const client = await login2(device);
4498
4714
  try {
4499
- await checkVersion2(client, device);
4715
+ await checkVersion2((command) => exec(client, command), device);
4500
4716
  const channel = await shell(client);
4501
4717
  await openCli(channel);
4502
4718
  await configure2(channel, plan, {
@@ -4754,7 +4970,7 @@ function fillPlan(set, prefixes) {
4754
4970
  async function fill2(device, _network, set, _family, prefixes) {
4755
4971
  const client = await login2(device);
4756
4972
  try {
4757
- await checkVersion2(client, device);
4973
+ await checkVersion2((command) => exec(client, command), device);
4758
4974
  const channel = await shell(client);
4759
4975
  await openCli(channel);
4760
4976
  await configure2(channel, fillPlan(set, prefixes));
@@ -4766,6 +4982,7 @@ async function fill2(device, _network, set, _family, prefixes) {
4766
4982
  }
4767
4983
  var junos = {
4768
4984
  extension: ".conf",
4985
+ console: true,
4769
4986
  unsupported: unsupported2,
4770
4987
  render: render2,
4771
4988
  read: read2,
@@ -5170,7 +5387,25 @@ function physicalPort(settings, vlanId, lagNumbers) {
5170
5387
  return lines;
5171
5388
  }
5172
5389
  var HOST_KEYS = ["ssh-rsa"];
5173
- async function open(device) {
5390
+ var RAISECOM_CONSOLE = {
5391
+ login: /login:\s*$/i,
5392
+ password: /password:\s*$/i,
5393
+ prompt: /[#>]\s*$/,
5394
+ logout: "exit",
5395
+ others: [[/\(config[^)\n]*\)#\s*$/, `end
5396
+ `]]
5397
+ };
5398
+ async function open(device, options = {}) {
5399
+ const session = options.console ? await consoleShell(device) : await sshShell(device);
5400
+ const version = await send(session.channel, "show version", 15000, 1500);
5401
+ if (!version.includes("ROS")) {
5402
+ session.close();
5403
+ throw new Error(`${device.name} did not answer as ROS`);
5404
+ }
5405
+ await send(session.channel, "terminal page-break disable", 15000, 1000);
5406
+ return session;
5407
+ }
5408
+ async function sshShell(device) {
5174
5409
  const user = device.users?.[device.connection.user];
5175
5410
  if (!user)
5176
5411
  throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
@@ -5187,16 +5422,45 @@ async function open(device) {
5187
5422
  client.end();
5188
5423
  };
5189
5424
  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")) {
5425
+ if (!/[#>]\s*$/.test(banner.trimEnd())) {
5192
5426
  close();
5193
5427
  throw new Error(`${device.name} did not answer as ROS`);
5194
5428
  }
5195
- await send(channel, "terminal page-break disable", 15000, 1000);
5196
- return { channel, close };
5429
+ return {
5430
+ channel,
5431
+ close,
5432
+ fresh: async () => {
5433
+ close();
5434
+ return open(device);
5435
+ }
5436
+ };
5197
5437
  }
5198
- async function read3(device) {
5199
- const { channel, close } = await open(device);
5438
+ async function consoleShell(device) {
5439
+ const { console: console2, user, password } = consoleOf(device);
5440
+ const opened = await openConsole(console2);
5441
+ try {
5442
+ await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
5443
+ } catch (error) {
5444
+ opened.close();
5445
+ throw error;
5446
+ }
5447
+ const session = {
5448
+ channel: opened.channel,
5449
+ close: () => {
5450
+ opened.channel.write(`${RAISECOM_CONSOLE.logout}
5451
+ `);
5452
+ opened.close();
5453
+ },
5454
+ fresh: async () => {
5455
+ await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
5456
+ await send(opened.channel, "terminal page-break disable", 15000, 1000);
5457
+ return session;
5458
+ }
5459
+ };
5460
+ return session;
5461
+ }
5462
+ async function read3(device, _network = [], options = {}) {
5463
+ const { channel, close } = await open(device, options);
5200
5464
  try {
5201
5465
  const dump = await send(channel, "show running-config", 150000, 6000);
5202
5466
  const lines = dump.replace(/\r/g, "").split(`
@@ -5331,8 +5595,9 @@ function unsupported3(device) {
5331
5595
  var REFUSED3 = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
5332
5596
  var CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
5333
5597
  var RELEASE = /^no (\S+ access-list \d+)$/;
5334
- async function apply3(device, _network, plan) {
5335
- const { channel, close } = await open(device);
5598
+ async function apply3(device, _network, plan, options) {
5599
+ const session = await open(device, options);
5600
+ const { channel } = session;
5336
5601
  const released = [];
5337
5602
  try {
5338
5603
  await send(channel, "config", 1e4);
@@ -5366,11 +5631,10 @@ async function apply3(device, _network, plan) {
5366
5631
  });
5367
5632
  if (released.length)
5368
5633
  console.log(` bound again: ${released.join(", ")}`);
5634
+ session.close();
5369
5635
  throw error;
5370
- } finally {
5371
- close();
5372
5636
  }
5373
- const proof = await open(device).catch(() => {
5637
+ const proof = await session.fresh().catch(() => {
5374
5638
  return;
5375
5639
  });
5376
5640
  if (!proof)
@@ -5440,7 +5704,7 @@ function plan3(desired, current) {
5440
5704
  }
5441
5705
  return { steps: planned, problems: [] };
5442
5706
  }
5443
- var raisecom = { extension: ".txt", unsupported: unsupported3, render: render3, read: read3, plan: plan3, apply: apply3 };
5707
+ var raisecom = { extension: ".txt", console: true, unsupported: unsupported3, render: render3, read: read3, plan: plan3, apply: apply3 };
5444
5708
 
5445
5709
  // src/adapters/devices/routeros/export.ts
5446
5710
  var unwrap = (text) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
@@ -7078,8 +7342,56 @@ async function login3(device) {
7078
7342
  password: resolve(user.password)
7079
7343
  });
7080
7344
  }
7081
- async function checkVersion3(client, device) {
7082
- const version = (await exec(client, ":put [/system resource get version]")).trim();
7345
+ var ROUTEROS_CONSOLE = {
7346
+ login: /Login: $/,
7347
+ password: /Password: $/,
7348
+ prompt: /\] > $/,
7349
+ logout: "/quit",
7350
+ others: [
7351
+ [/-- \[Q quit\|D dump\|(?:C-z pause\|)?down\]$/, "D"],
7352
+ [/\[Y\/n\]:? ?$/, "n"]
7353
+ ],
7354
+ user: (user) => `${user}+cwt`
7355
+ };
7356
+ function sshSession(client, device) {
7357
+ return {
7358
+ run: (command, timeout) => exec(client, command, timeout),
7359
+ upload: (path, content) => upload(client, path, content),
7360
+ fresh: async () => {
7361
+ client.end();
7362
+ return sshSession(await login3(device), device);
7363
+ },
7364
+ end: () => client.end()
7365
+ };
7366
+ }
7367
+ async function consoleSession2(device) {
7368
+ const { console: console2, user, password } = consoleOf(device);
7369
+ const opened = await openConsole(console2);
7370
+ try {
7371
+ await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
7372
+ } catch (error) {
7373
+ opened.close();
7374
+ throw error;
7375
+ }
7376
+ const shell = new ConsoleShell(opened.channel, ROUTEROS_CONSOLE);
7377
+ const session = {
7378
+ run: (command, timeout) => shell.run(command, timeout),
7379
+ fresh: async () => {
7380
+ await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
7381
+ return session;
7382
+ },
7383
+ end: () => {
7384
+ shell.logout();
7385
+ opened.close();
7386
+ }
7387
+ };
7388
+ return session;
7389
+ }
7390
+ async function open2(device, options = {}) {
7391
+ return options.console ? consoleSession2(device) : sshSession(await login3(device), device);
7392
+ }
7393
+ async function checkVersion3(session, device) {
7394
+ const version = (await session.run(":put [/system resource get version]")).trim();
7083
7395
  const major = Number(version.split(".")[0]);
7084
7396
  if (major !== SUPPORTED_MAJOR2) {
7085
7397
  throw new Error(`${device.name} runs RouterOS ${version}; this adapter renders for version ${SUPPORTED_MAJOR2}`);
@@ -7094,13 +7406,13 @@ var LIST_CERTIFICATES = [
7094
7406
  ':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
7095
7407
  "}"
7096
7408
  ].join(" ");
7097
- async function read4(device) {
7098
- const client = await login3(device);
7409
+ async function read4(device, _network = [], options = {}) {
7410
+ const session = await open2(device, options);
7099
7411
  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();
7412
+ await checkVersion3(session, device);
7413
+ const config = await session.run("/export", 300000);
7414
+ const users = await session.run("/user export");
7415
+ const certificates = (await session.run(LIST_CERTIFICATES)).trim();
7104
7416
  const listed = certificates.split(`
7105
7417
  `).every((line) => line.trim().startsWith("add fingerprint="));
7106
7418
  if (certificates && !listed) {
@@ -7115,7 +7427,7 @@ ${certificates}` : "";
7115
7427
  return withoutTimestamp(text) + `
7116
7428
  `;
7117
7429
  } finally {
7118
- client.end();
7430
+ session.end();
7119
7431
  }
7120
7432
  }
7121
7433
  function unsupported4(device) {
@@ -7172,36 +7484,38 @@ function unsupported4(device) {
7172
7484
  }
7173
7485
  var ROLLBACK_SCHEDULER = "apply-rollback";
7174
7486
  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();
7487
+ async function refusal(session, command) {
7488
+ const compiled = (await session.run(`:put [:parse "${escapeForString(command)}"]`)).trim();
7177
7489
  const refused = !compiled.startsWith("(") || /syntax error|bad parameter|no such|expected/.test(compiled);
7178
7490
  return refused ? compiled.slice(0, 200) : undefined;
7179
7491
  }
7180
- async function installCertificate(client, device, name) {
7492
+ async function installCertificate(session, device, name) {
7181
7493
  const certificate = device.certificates?.[name];
7182
7494
  if (!certificate)
7183
7495
  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=""`);
7496
+ if (!session.upload)
7497
+ throw new Error(`certificate ${name} is installed from a file, which only SSH can write; apply it without --console`);
7498
+ await session.upload(`${name}.crt`, certificate.certificate);
7499
+ await session.run(`/certificate import file-name=${name}.crt name=${name} passphrase=""`);
7186
7500
  if (certificate.private_key) {
7187
7501
  const value = resolve(certificate.private_key);
7188
7502
  const privateKey = value.includes("-----BEGIN") ? `${value}
7189
7503
  ` : 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=""`);
7504
+ await session.upload(`${name}.key`, privateKey);
7505
+ await session.run(`/certificate import file-name=${name}.key name=${name} passphrase=""`);
7192
7506
  }
7193
- await exec(client, `/file remove [find name="${name}.crt" or name="${name}.key"]`);
7507
+ await session.run(`/file remove [find name="${name}.crt" or name="${name}.key"]`);
7194
7508
  }
7195
7509
  async function apply4(device, _network, changes, options) {
7196
- const client = await login3(device);
7197
- await checkVersion3(client, device);
7510
+ const session = await open2(device, options);
7511
+ await checkVersion3(session, device);
7198
7512
  const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
7199
7513
  for (const { title, command } of commands) {
7200
7514
  if (command.startsWith(INSTALL_CERTIFICATE))
7201
7515
  continue;
7202
- const reason = await refusal(client, substitute(command));
7516
+ const reason = await refusal(session, substitute(command));
7203
7517
  if (reason) {
7204
- client.end();
7518
+ session.end();
7205
7519
  throw new Error(`${title}: the device will not parse ${command.slice(0, 200)}
7206
7520
  ${reason}`);
7207
7521
  }
@@ -7216,16 +7530,16 @@ async function apply4(device, _network, changes, options) {
7216
7530
  ].join(" ");
7217
7531
  try {
7218
7532
  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);
7533
+ await session.run(`/system backup save name=${backup} dont-encrypt=yes`, 120000);
7534
+ await session.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7535
+ await session.run(armRollback);
7222
7536
  for (const { title, command } of commands) {
7223
7537
  if (command.startsWith(INSTALL_CERTIFICATE)) {
7224
7538
  const certificateName = command.slice(INSTALL_CERTIFICATE.length).trim();
7225
- await installCertificate(client, device, certificateName);
7539
+ await installCertificate(session, device, certificateName);
7226
7540
  continue;
7227
7541
  }
7228
- const answer = (await exec(client, substitute(command), 120000)).trim();
7542
+ const answer = (await session.run(substitute(command), 120000)).trim();
7229
7543
  if (answer)
7230
7544
  throw new Error(`${title}: ${command.slice(0, 160)}
7231
7545
  device said: ${answer.slice(0, 300)}`);
@@ -7233,20 +7547,19 @@ async function apply4(device, _network, changes, options) {
7233
7547
  } catch (error) {
7234
7548
  console.log(` FAILED: ${error.message}`);
7235
7549
  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(() => {
7550
+ await session.run(`/system backup load name=${backup} password=""`, 30000).catch(() => {
7237
7551
  return;
7238
7552
  });
7239
- client.end();
7553
+ session.end();
7240
7554
  throw new Error("apply failed and the backup is being restored");
7241
7555
  }
7242
- client.end();
7243
- const proof = await login3(device).catch(() => {
7556
+ const proof = await session.fresh().catch(() => {
7244
7557
  return;
7245
7558
  });
7246
7559
  if (!proof)
7247
7560
  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"]`);
7561
+ await proof.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7562
+ await proof.run(`/file remove [find name="${backup}.backup"]`);
7250
7563
  proof.end();
7251
7564
  console.log(" logged in again, rollback disarmed");
7252
7565
  }
@@ -7270,6 +7583,7 @@ async function fill3(device, _network, set, family, prefixes) {
7270
7583
  }
7271
7584
  var routeros = {
7272
7585
  extension: ".rsc",
7586
+ console: true,
7273
7587
  unsupported: unsupported4,
7274
7588
  render: render4,
7275
7589
  read: read4,
@@ -7742,25 +8056,60 @@ async function openShell(client) {
7742
8056
  await send(channel, "screen-length 0 temporary", 1e4, 1500);
7743
8057
  return channel;
7744
8058
  }
7745
- async function read5(device) {
8059
+ var VRP_CONSOLE = {
8060
+ login: /Username:\s*$/i,
8061
+ password: /Password:\s*$/i,
8062
+ prompt: /<[^>\n]+>\s*$/,
8063
+ logout: "quit",
8064
+ others: [
8065
+ [/Change now\? ?\[Y\/N\]:?\s*$/i, `N
8066
+ `],
8067
+ [/\[~?\*?[^\]\n]+\]\s*$/, `return
8068
+ `]
8069
+ ]
8070
+ };
8071
+ async function onConsole2(device, work) {
8072
+ const { console: console2, user, password } = consoleOf(device);
8073
+ const opened = await openConsole(console2);
8074
+ const login = async () => {
8075
+ await consoleLogin(opened.channel, VRP_CONSOLE, user, password);
8076
+ await send(opened.channel, "screen-length 0 temporary", 1e4, 1500);
8077
+ };
8078
+ try {
8079
+ await login();
8080
+ const result = await work(opened.channel, login);
8081
+ opened.channel.write(`${VRP_CONSOLE.logout}
8082
+ `);
8083
+ return result;
8084
+ } finally {
8085
+ opened.close();
8086
+ }
8087
+ }
8088
+ async function read5(device, _network = [], options = {}) {
8089
+ if (options.console) {
8090
+ return onConsole2(device, async (channel) => configurationOf(device, await send(channel, "display current-configuration", 120000, 3000)));
8091
+ }
7746
8092
  const client = await login4(device);
7747
8093
  try {
7748
8094
  const channel = await openShell(client);
7749
8095
  const dump = await send(channel, "display current-configuration", 120000, 3000);
7750
8096
  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
- `);
8097
+ return configurationOf(device, dump);
7760
8098
  } finally {
7761
8099
  client.end();
7762
8100
  }
7763
8101
  }
8102
+ function configurationOf(device, dump) {
8103
+ const lines = dump.replace(/\r/g, "").split(`
8104
+ `);
8105
+ const start = lines.findIndex((line) => line.startsWith("!Software Version"));
8106
+ if (start < 0)
8107
+ throw new Error(`${device.name} returned no configuration`);
8108
+ const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
8109
+ return scrub3(body.join(`
8110
+ `).trim() + `
8111
+ `);
8112
+ }
7764
8113
  function unsupported5(device) {
7765
8114
  const found = [];
7766
8115
  for (const [name, port] of Object.entries(device.ports ?? {})) {
@@ -7857,9 +8206,7 @@ async function sendLine(channel, line) {
7857
8206
  answer += await send(channel, "Y", 20000, 800);
7858
8207
  return answer;
7859
8208
  }
7860
- async function apply5(device, _network, plan) {
7861
- const client = await login4(device);
7862
- const channel = await openShell(client);
8209
+ async function commitPlan(channel, plan) {
7863
8210
  try {
7864
8211
  await send(channel, "system-view", 1e4, 1500);
7865
8212
  for (const step of plan.steps) {
@@ -7878,24 +8225,42 @@ async function apply5(device, _network, plan) {
7878
8225
  await send(channel, "clear configuration candidate", 1e4).catch(() => {
7879
8226
  return;
7880
8227
  });
7881
- channel.end();
7882
- client.end();
7883
8228
  throw error;
7884
8229
  }
7885
- const proof = await login4(device).catch(() => {
8230
+ }
8231
+ async function save(channel) {
8232
+ const saving = await send(channel, "save", 20000, 2000);
8233
+ if (/\[Y\/N\]/i.test(saving))
8234
+ await send(channel, "Y", 60000, 5000);
8235
+ }
8236
+ var NOT_SAVED = "Not saved: a reboot restores the previous configuration.";
8237
+ async function apply5(device, _network, plan, options) {
8238
+ if (options.console) {
8239
+ await onConsole2(device, async (channel, relogin) => {
8240
+ await commitPlan(channel, plan);
8241
+ await relogin().catch((error) => {
8242
+ throw new Error(`committed, but a fresh login on the console failed: ${error.message}. ${NOT_SAVED}`);
8243
+ });
8244
+ await save(channel);
8245
+ });
8246
+ console.log(" committed, login proved, saved");
7886
8247
  return;
7887
- });
7888
- if (!proof) {
8248
+ }
8249
+ const client = await login4(device);
8250
+ const channel = await openShell(client);
8251
+ try {
8252
+ await commitPlan(channel, plan);
8253
+ const proof = await login4(device).catch(() => {
8254
+ return;
8255
+ });
8256
+ if (!proof)
8257
+ throw new Error(`committed, but a fresh login failed. ${NOT_SAVED} Recover over the open session or the console.`);
8258
+ proof.end();
8259
+ await save(channel);
8260
+ } finally {
7889
8261
  channel.end();
7890
8262
  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
8263
  }
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
8264
  console.log(" committed, login proved, saved");
7900
8265
  }
7901
8266
  function plan5(desired, current) {
@@ -7920,6 +8285,7 @@ function plan5(desired, current) {
7920
8285
  }
7921
8286
  var vrp = {
7922
8287
  extension: ".cfg",
8288
+ console: true,
7923
8289
  unsupported: unsupported5,
7924
8290
  render: render5,
7925
8291
  read: read5,