@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 +542 -166
- package/dist/types/adapters/devices/fortios/index.d.ts +3 -4
- package/dist/types/adapters/devices/junos/index.d.ts +6 -3
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +10 -5
- package/dist/types/adapters/devices/routeros/index.d.ts +16 -3
- package/dist/types/adapters/devices/vrp/index.d.ts +6 -3
- package/dist/types/schema/device.d.ts +4 -4
- package/dist/types/transport/console.d.ts +50 -2
- package/package.json +1 -1
- package/src/adapters/devices/fortios/TRAPS.md +1 -0
- package/src/adapters/devices/fortios/index.ts +16 -84
- package/src/adapters/devices/junos/TRAPS.md +3 -0
- package/src/adapters/devices/junos/index.ts +81 -9
- package/src/adapters/devices/raisecom-ros/index.ts +72 -16
- package/src/adapters/devices/routeros/TRAPS.md +1 -0
- package/src/adapters/devices/routeros/index.ts +105 -35
- package/src/adapters/devices/vrp/index.ts +91 -26
- package/src/schema/device.ts +4 -4
- package/src/transport/console.ts +319 -26
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
302
|
-
|
|
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
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
[
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
]
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
-
|
|
2565
|
-
const
|
|
2566
|
-
|
|
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(
|
|
4360
|
-
const version = await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5196
|
-
|
|
5439
|
+
return {
|
|
5440
|
+
channel,
|
|
5441
|
+
close,
|
|
5442
|
+
fresh: async () => {
|
|
5443
|
+
close();
|
|
5444
|
+
return open(device);
|
|
5445
|
+
}
|
|
5446
|
+
};
|
|
5197
5447
|
}
|
|
5198
|
-
async function
|
|
5199
|
-
const {
|
|
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
|
|
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
|
|
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
|
-
|
|
7082
|
-
|
|
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
|
|
7419
|
+
async function read4(device, _network = [], options = {}) {
|
|
7420
|
+
const session = await open2(device, options);
|
|
7099
7421
|
try {
|
|
7100
|
-
await checkVersion3(
|
|
7101
|
-
const config = await
|
|
7102
|
-
const users = await
|
|
7103
|
-
const certificates = (await
|
|
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
|
-
|
|
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(
|
|
7176
|
-
const compiled = (await
|
|
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(
|
|
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
|
-
|
|
7185
|
-
|
|
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(
|
|
7191
|
-
await
|
|
7514
|
+
await session.upload(`${name}.key`, privateKey);
|
|
7515
|
+
await session.run(`/certificate import file-name=${name}.key name=${name} passphrase=""`);
|
|
7192
7516
|
}
|
|
7193
|
-
await
|
|
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
|
|
7197
|
-
await checkVersion3(
|
|
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(
|
|
7526
|
+
const reason = await refusal(session, substitute(command));
|
|
7203
7527
|
if (reason) {
|
|
7204
|
-
|
|
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
|
|
7220
|
-
await
|
|
7221
|
-
await
|
|
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(
|
|
7549
|
+
await installCertificate(session, device, certificateName);
|
|
7226
7550
|
continue;
|
|
7227
7551
|
}
|
|
7228
|
-
const answer = (await
|
|
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
|
|
7560
|
+
await session.run(`/system backup load name=${backup} password=""`, 30000).catch(() => {
|
|
7237
7561
|
return;
|
|
7238
7562
|
});
|
|
7239
|
-
|
|
7563
|
+
session.end();
|
|
7240
7564
|
throw new Error("apply failed and the backup is being restored");
|
|
7241
7565
|
}
|
|
7242
|
-
|
|
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
|
|
7249
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|