@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 +527 -161
- package/dist/types/adapters/devices/fortios/index.d.ts +3 -4
- package/dist/types/adapters/devices/junos/index.d.ts +5 -2
- 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/index.ts +16 -84
- package/src/adapters/devices/junos/index.ts +68 -7
- 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 +306 -24
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
|
-
|
|
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
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
|
|
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
|
-
}
|
|
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
|
-
|
|
2565
|
-
const
|
|
2566
|
-
|
|
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(
|
|
4360
|
-
const version = await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5196
|
-
|
|
5429
|
+
return {
|
|
5430
|
+
channel,
|
|
5431
|
+
close,
|
|
5432
|
+
fresh: async () => {
|
|
5433
|
+
close();
|
|
5434
|
+
return open(device);
|
|
5435
|
+
}
|
|
5436
|
+
};
|
|
5197
5437
|
}
|
|
5198
|
-
async function
|
|
5199
|
-
const {
|
|
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
|
|
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
|
|
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
|
-
|
|
7082
|
-
|
|
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
|
|
7409
|
+
async function read4(device, _network = [], options = {}) {
|
|
7410
|
+
const session = await open2(device, options);
|
|
7099
7411
|
try {
|
|
7100
|
-
await checkVersion3(
|
|
7101
|
-
const config = await
|
|
7102
|
-
const users = await
|
|
7103
|
-
const certificates = (await
|
|
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
|
-
|
|
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(
|
|
7176
|
-
const compiled = (await
|
|
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(
|
|
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
|
-
|
|
7185
|
-
|
|
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(
|
|
7191
|
-
await
|
|
7504
|
+
await session.upload(`${name}.key`, privateKey);
|
|
7505
|
+
await session.run(`/certificate import file-name=${name}.key name=${name} passphrase=""`);
|
|
7192
7506
|
}
|
|
7193
|
-
await
|
|
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
|
|
7197
|
-
await checkVersion3(
|
|
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(
|
|
7516
|
+
const reason = await refusal(session, substitute(command));
|
|
7203
7517
|
if (reason) {
|
|
7204
|
-
|
|
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
|
|
7220
|
-
await
|
|
7221
|
-
await
|
|
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(
|
|
7539
|
+
await installCertificate(session, device, certificateName);
|
|
7226
7540
|
continue;
|
|
7227
7541
|
}
|
|
7228
|
-
const answer = (await
|
|
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
|
|
7550
|
+
await session.run(`/system backup load name=${backup} password=""`, 30000).catch(() => {
|
|
7237
7551
|
return;
|
|
7238
7552
|
});
|
|
7239
|
-
|
|
7553
|
+
session.end();
|
|
7240
7554
|
throw new Error("apply failed and the backup is being restored");
|
|
7241
7555
|
}
|
|
7242
|
-
|
|
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
|
|
7249
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|