@takodotid/circuit 0.26.2 → 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 +694 -90
- package/dist/types/adapters/devices/fortios/index.d.ts +5 -2
- package/dist/types/adapters/devices/fortios/plan.d.ts +2 -0
- 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/types.d.ts +7 -1
- package/dist/types/adapters/devices/vrp/index.d.ts +6 -3
- package/dist/types/schema/device.d.ts +25 -1
- package/dist/types/transport/console.d.ts +57 -0
- package/package.json +1 -1
- package/src/adapters/devices/fortios/TRAPS.md +2 -1
- package/src/adapters/devices/fortios/index.ts +69 -13
- package/src/adapters/devices/fortios/plan.ts +40 -1
- 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/types.ts +8 -1
- package/src/adapters/devices/vrp/index.ts +91 -26
- package/src/cli/index.ts +14 -4
- package/src/schema/device.ts +27 -1
- package/src/transport/console.ts +409 -0
package/dist/cli/index.js
CHANGED
|
@@ -110,6 +110,12 @@ var MARKER = /<secret:([A-Za-z0-9_./:-]+)>/g;
|
|
|
110
110
|
var substitute = (text) => text.replace(MARKER, (_, reference) => valueOf(reference));
|
|
111
111
|
var valueOfReference = valueOf;
|
|
112
112
|
|
|
113
|
+
// src/transport/console.ts
|
|
114
|
+
import { spawn } from "node:child_process";
|
|
115
|
+
import { EventEmitter } from "node:events";
|
|
116
|
+
import { Duplex } from "node:stream";
|
|
117
|
+
import { Client as Client2 } from "ssh2";
|
|
118
|
+
|
|
113
119
|
// src/transport/ssh.ts
|
|
114
120
|
import { Client } from "ssh2";
|
|
115
121
|
function connect(login) {
|
|
@@ -202,6 +208,321 @@ function upload(client, path, content) {
|
|
|
202
208
|
});
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
// src/transport/console.ts
|
|
212
|
+
var RELAY = `import fcntl, os, select, sys, termios
|
|
213
|
+
|
|
214
|
+
port, baud = sys.argv[1], int(sys.argv[2])
|
|
215
|
+
fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
219
|
+
except OSError:
|
|
220
|
+
sys.stderr.write("busy: another session holds " + port + "\\n")
|
|
221
|
+
sys.exit(3)
|
|
222
|
+
|
|
223
|
+
attrs = termios.tcgetattr(fd)
|
|
224
|
+
attrs[0] = attrs[1] = attrs[3] = 0
|
|
225
|
+
attrs[2] = termios.CS8 | termios.CREAD | termios.CLOCAL
|
|
226
|
+
attrs[4] = attrs[5] = getattr(termios, "B%d" % baud)
|
|
227
|
+
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
|
228
|
+
termios.tcflush(fd, termios.TCIOFLUSH)
|
|
229
|
+
|
|
230
|
+
sys.stderr.write("ready\\n")
|
|
231
|
+
sys.stderr.flush()
|
|
232
|
+
|
|
233
|
+
while True:
|
|
234
|
+
ready, _, _ = select.select([0, fd], [], [])
|
|
235
|
+
|
|
236
|
+
if 0 in ready:
|
|
237
|
+
data = os.read(0, 4096)
|
|
238
|
+
if not data:
|
|
239
|
+
break
|
|
240
|
+
while data:
|
|
241
|
+
try:
|
|
242
|
+
data = data[os.write(fd, data):]
|
|
243
|
+
except BlockingIOError:
|
|
244
|
+
select.select([], [fd], [])
|
|
245
|
+
|
|
246
|
+
if fd in ready:
|
|
247
|
+
try:
|
|
248
|
+
data = os.read(fd, 4096)
|
|
249
|
+
except BlockingIOError:
|
|
250
|
+
continue
|
|
251
|
+
if data:
|
|
252
|
+
os.write(1, data)
|
|
253
|
+
`;
|
|
254
|
+
var RELAY_PATH = "/tmp/circuit-console.py";
|
|
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
|
+
`;
|
|
283
|
+
function connectJump(jump) {
|
|
284
|
+
return new Promise((done, fail) => {
|
|
285
|
+
const client = new Client2;
|
|
286
|
+
const login = { username: jump.user, password: resolve(jump.password), readyTimeout: 60000 };
|
|
287
|
+
client.on("ready", () => done(client)).on("error", fail);
|
|
288
|
+
if (!jump.cloudflared) {
|
|
289
|
+
client.connect({ ...login, host: jump.host, port: jump.port ?? 22 });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const tunnel = spawn("cloudflared", ["access", "ssh", "--hostname", jump.host], { stdio: ["pipe", "pipe", "ignore"] });
|
|
293
|
+
tunnel.on("error", (error) => fail(new Error(`cloudflared did not start: ${error.message}`)));
|
|
294
|
+
client.on("close", () => tunnel.kill());
|
|
295
|
+
client.connect({ ...login, sock: Duplex.from({ readable: tunnel.stdout, writable: tunnel.stdin }) });
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async function openConsole(console2) {
|
|
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
|
+
}
|
|
303
|
+
if (!Number.isInteger(console2.baud) || console2.baud <= 0)
|
|
304
|
+
throw new Error(`console baud ${console2.baud} is not a line speed`);
|
|
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);
|
|
317
|
+
try {
|
|
318
|
+
const encoded = Buffer.from(RELAY).toString("base64");
|
|
319
|
+
await exec(client, `sh -c 'printf %s ${encoded} | base64 -d > ${RELAY_PATH}'`, 30000);
|
|
320
|
+
const channel = await new Promise((done, fail) => {
|
|
321
|
+
client.exec(`python3 ${RELAY_PATH} ${console2.port} ${console2.baud}`, (error, stream) => error ? fail(error) : done(stream));
|
|
322
|
+
});
|
|
323
|
+
await relayReady(channel);
|
|
324
|
+
return {
|
|
325
|
+
channel: asTerminal(channel),
|
|
326
|
+
close: () => {
|
|
327
|
+
channel.end();
|
|
328
|
+
client.end();
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
client.end();
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
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
|
+
}
|
|
525
|
+
|
|
205
526
|
// src/adapters/devices/fortios/names.ts
|
|
206
527
|
function nameOf(device, name) {
|
|
207
528
|
if (device.interfaces && name in device.interfaces)
|
|
@@ -1134,7 +1455,9 @@ var TABLES = {
|
|
|
1134
1455
|
"firewall policy6": ORDERED,
|
|
1135
1456
|
"firewall local-in-policy": ORDERED,
|
|
1136
1457
|
"firewall local-in-policy6": ORDERED,
|
|
1137
|
-
"firewall central-snat-map": ORDERED
|
|
1458
|
+
"firewall central-snat-map": ORDERED,
|
|
1459
|
+
"router static": { distinct: true },
|
|
1460
|
+
"router static6": { distinct: true }
|
|
1138
1461
|
};
|
|
1139
1462
|
var IGNORED = new Set(["uuid", "snmp-index"]);
|
|
1140
1463
|
var IGNORED_IN = { "system global": ["cfg-save", "cfg-revert-timeout", "alias"] };
|
|
@@ -1271,7 +1594,7 @@ function compare(path, want, have, table, top, context) {
|
|
|
1271
1594
|
}
|
|
1272
1595
|
for (const [header, child] of want.blocks)
|
|
1273
1596
|
nested(header, child, have.blocks.get(header) ?? new Block);
|
|
1274
|
-
const entries = table?.byContent ? byContent(path, want, have) : byId(path, want, have, table, context);
|
|
1597
|
+
const entries = table?.byContent ? byContent(path, want, have) : table?.distinct ? distinctById(path, want, have, table, context) : byId(path, want, have, table, context);
|
|
1275
1598
|
send.push(...entries.send);
|
|
1276
1599
|
show.push(...entries.show);
|
|
1277
1600
|
if (top) {
|
|
@@ -1320,6 +1643,30 @@ function byId(path, want, have, table, context) {
|
|
|
1320
1643
|
}
|
|
1321
1644
|
return { send, show, removals, removed };
|
|
1322
1645
|
}
|
|
1646
|
+
function distinctById(path, want, have, table, context) {
|
|
1647
|
+
const deletes = [];
|
|
1648
|
+
const creates = [];
|
|
1649
|
+
const show = [];
|
|
1650
|
+
for (const [key, entry] of want.entries) {
|
|
1651
|
+
const old = have.entries.get(key);
|
|
1652
|
+
const where = [...path, bare(entry.id)];
|
|
1653
|
+
if (old) {
|
|
1654
|
+
if (!compare(where, entry.block, old.block, undefined, false, context).send.length)
|
|
1655
|
+
continue;
|
|
1656
|
+
deletes.push(`delete ${old.id}`);
|
|
1657
|
+
show.push(...shown("-", where, old.block));
|
|
1658
|
+
}
|
|
1659
|
+
creates.push(...creation(entry, table));
|
|
1660
|
+
show.push(...shown("+", where, entry.block));
|
|
1661
|
+
}
|
|
1662
|
+
for (const [key, entry] of have.entries) {
|
|
1663
|
+
if (want.entries.has(key) || isFactory(table, entry))
|
|
1664
|
+
continue;
|
|
1665
|
+
deletes.push(`delete ${entry.id}`);
|
|
1666
|
+
show.push(...shown("-", [...path, bare(entry.id)], entry.block));
|
|
1667
|
+
}
|
|
1668
|
+
return { send: [...deletes, ...creates], show, removals: [], removed: [] };
|
|
1669
|
+
}
|
|
1323
1670
|
function moves(want, have) {
|
|
1324
1671
|
const wanted = [...want.entries.keys()];
|
|
1325
1672
|
const order = [...have.entries.keys()].filter((key) => want.entries.has(key));
|
|
@@ -2356,7 +2703,43 @@ async function login(device) {
|
|
|
2356
2703
|
password: resolve(user.password)
|
|
2357
2704
|
});
|
|
2358
2705
|
}
|
|
2359
|
-
|
|
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
|
+
};
|
|
2718
|
+
async function consoleLogout(cli) {
|
|
2719
|
+
await leave(cli).catch(() => {
|
|
2720
|
+
return;
|
|
2721
|
+
});
|
|
2722
|
+
await cli.run("exit", 1e4).catch(() => {
|
|
2723
|
+
return;
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
async function consoleSession(device, work) {
|
|
2727
|
+
const { console: console2, user, password } = consoleOf(device);
|
|
2728
|
+
const opened = await openConsole(console2);
|
|
2729
|
+
try {
|
|
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);
|
|
2733
|
+
const result = await work(cli, relogin);
|
|
2734
|
+
await consoleLogout(cli);
|
|
2735
|
+
return result;
|
|
2736
|
+
} finally {
|
|
2737
|
+
opened.close();
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
async function session(device, work, options = {}) {
|
|
2741
|
+
if (options.console)
|
|
2742
|
+
return consoleSession(device, (cli) => work(cli));
|
|
2360
2743
|
const client = await login(device);
|
|
2361
2744
|
try {
|
|
2362
2745
|
const channel = await shell(client, 4000);
|
|
@@ -2412,8 +2795,8 @@ async function readConfig(cli, device) {
|
|
|
2412
2795
|
throw new Error(`${device.name} returned no configuration`);
|
|
2413
2796
|
return scrub(clean(text));
|
|
2414
2797
|
}
|
|
2415
|
-
function read(device) {
|
|
2416
|
-
return session(device, (cli) => readConfig(cli, device));
|
|
2798
|
+
function read(device, _network = [], options = {}) {
|
|
2799
|
+
return session(device, (cli) => readConfig(cli, device), options);
|
|
2417
2800
|
}
|
|
2418
2801
|
function withSecrets(line) {
|
|
2419
2802
|
const key = line.startsWith("set private-key ");
|
|
@@ -2490,7 +2873,7 @@ async function configure(cli, plan, guard) {
|
|
|
2490
2873
|
class Reloaded extends Error {
|
|
2491
2874
|
}
|
|
2492
2875
|
var RELOAD = { attempts: 20, interval: 15000 };
|
|
2493
|
-
async function settle(device) {
|
|
2876
|
+
async function settle(device, options) {
|
|
2494
2877
|
for (let attempt = 0;attempt < RELOAD.attempts; attempt++) {
|
|
2495
2878
|
await new Promise((done) => setTimeout(done, RELOAD.interval));
|
|
2496
2879
|
const settled = await session(device, async (cli) => {
|
|
@@ -2498,18 +2881,26 @@ async function settle(device) {
|
|
|
2498
2881
|
return false;
|
|
2499
2882
|
await saving(cli, "automatic");
|
|
2500
2883
|
return true;
|
|
2501
|
-
}).catch(() => false);
|
|
2884
|
+
}, options).catch(() => false);
|
|
2502
2885
|
if (settled)
|
|
2503
2886
|
return "the device is back and saves changes on its own again";
|
|
2504
2887
|
}
|
|
2505
2888
|
return "the device did not come back in time and still reverts every change; set `cfg-save automatic` under `config system global`";
|
|
2506
2889
|
}
|
|
2507
2890
|
async function apply(device, _network, plan, options) {
|
|
2891
|
+
const seconds = options.rollback * 60;
|
|
2508
2892
|
try {
|
|
2893
|
+
if (options.console) {
|
|
2894
|
+
await consoleSession(device, async (cli, relogin) => {
|
|
2895
|
+
await checkVersion(cli, device);
|
|
2896
|
+
await configure(cli, plan, { seconds, prove: relogin });
|
|
2897
|
+
});
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2509
2900
|
await session(device, async (cli) => {
|
|
2510
2901
|
await checkVersion(cli, device);
|
|
2511
2902
|
await configure(cli, plan, {
|
|
2512
|
-
seconds
|
|
2903
|
+
seconds,
|
|
2513
2904
|
prove: async () => {
|
|
2514
2905
|
const proof = await login(device);
|
|
2515
2906
|
proof.end();
|
|
@@ -2520,7 +2911,7 @@ async function apply(device, _network, plan, options) {
|
|
|
2520
2911
|
if (!(error instanceof Reloaded))
|
|
2521
2912
|
throw error;
|
|
2522
2913
|
throw new Error(`${error.message}
|
|
2523
|
-
${await settle(device)}`);
|
|
2914
|
+
${await settle(device, options)}`);
|
|
2524
2915
|
}
|
|
2525
2916
|
}
|
|
2526
2917
|
function fillLines(set, family, prefixes) {
|
|
@@ -2920,6 +3311,7 @@ function serviceGaps(device) {
|
|
|
2920
3311
|
}
|
|
2921
3312
|
var fortios = {
|
|
2922
3313
|
extension: ".conf",
|
|
3314
|
+
console: true,
|
|
2923
3315
|
unsupported,
|
|
2924
3316
|
render,
|
|
2925
3317
|
read,
|
|
@@ -4129,8 +4521,8 @@ async function login2(device) {
|
|
|
4129
4521
|
password: resolve(user.password)
|
|
4130
4522
|
});
|
|
4131
4523
|
}
|
|
4132
|
-
async function checkVersion2(
|
|
4133
|
-
const version = await
|
|
4524
|
+
async function checkVersion2(ask, device) {
|
|
4525
|
+
const version = await ask("show version | match Junos:");
|
|
4134
4526
|
const release = /Junos: (\d+)\.(\S+)/.exec(version);
|
|
4135
4527
|
if (!release)
|
|
4136
4528
|
throw new Error(`${device.name} did not say which Junos it runs: ${version.slice(0, 200)}`);
|
|
@@ -4138,10 +4530,19 @@ async function checkVersion2(client, device) {
|
|
|
4138
4530
|
throw new Error(`${device.name} runs Junos ${release[1]}.${release[2]}; this adapter renders for Junos ${SUPPORTED_MAJOR}`);
|
|
4139
4531
|
}
|
|
4140
4532
|
}
|
|
4141
|
-
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
|
+
}
|
|
4142
4543
|
const client = await login2(device);
|
|
4143
4544
|
try {
|
|
4144
|
-
await checkVersion2(client, device);
|
|
4545
|
+
await checkVersion2((command) => exec(client, command), device);
|
|
4145
4546
|
const output = await exec(client, "show configuration | display set | no-more", 180000);
|
|
4146
4547
|
if (!/^set /m.test(output))
|
|
4147
4548
|
throw new Error(`${device.name} returned no configuration`);
|
|
@@ -4164,6 +4565,34 @@ async function edit(channel, line, timeout = 60000) {
|
|
|
4164
4565
|
const output = await readUntil(channel, (text) => EDIT_PROMPT.test(text.replace(/\r/g, "")), timeout, 300);
|
|
4165
4566
|
return output.replace(/\r/g, "");
|
|
4166
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
|
+
}
|
|
4167
4596
|
async function openCli(channel) {
|
|
4168
4597
|
const banner = await readUntil(channel, (text) => PROMPT2.test(text.replace(/\r/g, "")), 20000, 500);
|
|
4169
4598
|
if (/% $/.test(banner.replace(/\r/g, "")))
|
|
@@ -4267,9 +4696,23 @@ async function configure2(channel, plan, guard) {
|
|
|
4267
4696
|
console.log(" logged in again, commit confirmed");
|
|
4268
4697
|
}
|
|
4269
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
|
+
}
|
|
4270
4713
|
const client = await login2(device);
|
|
4271
4714
|
try {
|
|
4272
|
-
await checkVersion2(client, device);
|
|
4715
|
+
await checkVersion2((command) => exec(client, command), device);
|
|
4273
4716
|
const channel = await shell(client);
|
|
4274
4717
|
await openCli(channel);
|
|
4275
4718
|
await configure2(channel, plan, {
|
|
@@ -4527,7 +4970,7 @@ function fillPlan(set, prefixes) {
|
|
|
4527
4970
|
async function fill2(device, _network, set, _family, prefixes) {
|
|
4528
4971
|
const client = await login2(device);
|
|
4529
4972
|
try {
|
|
4530
|
-
await checkVersion2(client, device);
|
|
4973
|
+
await checkVersion2((command) => exec(client, command), device);
|
|
4531
4974
|
const channel = await shell(client);
|
|
4532
4975
|
await openCli(channel);
|
|
4533
4976
|
await configure2(channel, fillPlan(set, prefixes));
|
|
@@ -4539,6 +4982,7 @@ async function fill2(device, _network, set, _family, prefixes) {
|
|
|
4539
4982
|
}
|
|
4540
4983
|
var junos = {
|
|
4541
4984
|
extension: ".conf",
|
|
4985
|
+
console: true,
|
|
4542
4986
|
unsupported: unsupported2,
|
|
4543
4987
|
render: render2,
|
|
4544
4988
|
read: read2,
|
|
@@ -4943,7 +5387,25 @@ function physicalPort(settings, vlanId, lagNumbers) {
|
|
|
4943
5387
|
return lines;
|
|
4944
5388
|
}
|
|
4945
5389
|
var HOST_KEYS = ["ssh-rsa"];
|
|
4946
|
-
|
|
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) {
|
|
4947
5409
|
const user = device.users?.[device.connection.user];
|
|
4948
5410
|
if (!user)
|
|
4949
5411
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
@@ -4960,16 +5422,45 @@ async function open(device) {
|
|
|
4960
5422
|
client.end();
|
|
4961
5423
|
};
|
|
4962
5424
|
const banner = await readUntil(channel, (text) => /[#>]\s*$/.test(text.trimEnd()), 30000);
|
|
4963
|
-
|
|
4964
|
-
if (!/[#>]\s*$/.test(banner.trimEnd()) || !version.includes("ROS")) {
|
|
5425
|
+
if (!/[#>]\s*$/.test(banner.trimEnd())) {
|
|
4965
5426
|
close();
|
|
4966
5427
|
throw new Error(`${device.name} did not answer as ROS`);
|
|
4967
5428
|
}
|
|
4968
|
-
|
|
4969
|
-
|
|
5429
|
+
return {
|
|
5430
|
+
channel,
|
|
5431
|
+
close,
|
|
5432
|
+
fresh: async () => {
|
|
5433
|
+
close();
|
|
5434
|
+
return open(device);
|
|
5435
|
+
}
|
|
5436
|
+
};
|
|
5437
|
+
}
|
|
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;
|
|
4970
5461
|
}
|
|
4971
|
-
async function read3(device) {
|
|
4972
|
-
const { channel, close } = await open(device);
|
|
5462
|
+
async function read3(device, _network = [], options = {}) {
|
|
5463
|
+
const { channel, close } = await open(device, options);
|
|
4973
5464
|
try {
|
|
4974
5465
|
const dump = await send(channel, "show running-config", 150000, 6000);
|
|
4975
5466
|
const lines = dump.replace(/\r/g, "").split(`
|
|
@@ -5104,8 +5595,9 @@ function unsupported3(device) {
|
|
|
5104
5595
|
var REFUSED3 = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
|
|
5105
5596
|
var CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
|
|
5106
5597
|
var RELEASE = /^no (\S+ access-list \d+)$/;
|
|
5107
|
-
async function apply3(device, _network, plan) {
|
|
5108
|
-
const
|
|
5598
|
+
async function apply3(device, _network, plan, options) {
|
|
5599
|
+
const session = await open(device, options);
|
|
5600
|
+
const { channel } = session;
|
|
5109
5601
|
const released = [];
|
|
5110
5602
|
try {
|
|
5111
5603
|
await send(channel, "config", 1e4);
|
|
@@ -5139,11 +5631,10 @@ async function apply3(device, _network, plan) {
|
|
|
5139
5631
|
});
|
|
5140
5632
|
if (released.length)
|
|
5141
5633
|
console.log(` bound again: ${released.join(", ")}`);
|
|
5634
|
+
session.close();
|
|
5142
5635
|
throw error;
|
|
5143
|
-
} finally {
|
|
5144
|
-
close();
|
|
5145
5636
|
}
|
|
5146
|
-
const proof = await
|
|
5637
|
+
const proof = await session.fresh().catch(() => {
|
|
5147
5638
|
return;
|
|
5148
5639
|
});
|
|
5149
5640
|
if (!proof)
|
|
@@ -5213,7 +5704,7 @@ function plan3(desired, current) {
|
|
|
5213
5704
|
}
|
|
5214
5705
|
return { steps: planned, problems: [] };
|
|
5215
5706
|
}
|
|
5216
|
-
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 };
|
|
5217
5708
|
|
|
5218
5709
|
// src/adapters/devices/routeros/export.ts
|
|
5219
5710
|
var unwrap = (text) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
|
|
@@ -6851,8 +7342,56 @@ async function login3(device) {
|
|
|
6851
7342
|
password: resolve(user.password)
|
|
6852
7343
|
});
|
|
6853
7344
|
}
|
|
6854
|
-
|
|
6855
|
-
|
|
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();
|
|
6856
7395
|
const major = Number(version.split(".")[0]);
|
|
6857
7396
|
if (major !== SUPPORTED_MAJOR2) {
|
|
6858
7397
|
throw new Error(`${device.name} runs RouterOS ${version}; this adapter renders for version ${SUPPORTED_MAJOR2}`);
|
|
@@ -6867,13 +7406,13 @@ var LIST_CERTIFICATES = [
|
|
|
6867
7406
|
':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
|
|
6868
7407
|
"}"
|
|
6869
7408
|
].join(" ");
|
|
6870
|
-
async function read4(device) {
|
|
6871
|
-
const
|
|
7409
|
+
async function read4(device, _network = [], options = {}) {
|
|
7410
|
+
const session = await open2(device, options);
|
|
6872
7411
|
try {
|
|
6873
|
-
await checkVersion3(
|
|
6874
|
-
const config = await
|
|
6875
|
-
const users = await
|
|
6876
|
-
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();
|
|
6877
7416
|
const listed = certificates.split(`
|
|
6878
7417
|
`).every((line) => line.trim().startsWith("add fingerprint="));
|
|
6879
7418
|
if (certificates && !listed) {
|
|
@@ -6888,7 +7427,7 @@ ${certificates}` : "";
|
|
|
6888
7427
|
return withoutTimestamp(text) + `
|
|
6889
7428
|
`;
|
|
6890
7429
|
} finally {
|
|
6891
|
-
|
|
7430
|
+
session.end();
|
|
6892
7431
|
}
|
|
6893
7432
|
}
|
|
6894
7433
|
function unsupported4(device) {
|
|
@@ -6945,36 +7484,38 @@ function unsupported4(device) {
|
|
|
6945
7484
|
}
|
|
6946
7485
|
var ROLLBACK_SCHEDULER = "apply-rollback";
|
|
6947
7486
|
var escapeForString = (text) => text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
|
|
6948
|
-
async function refusal(
|
|
6949
|
-
const compiled = (await
|
|
7487
|
+
async function refusal(session, command) {
|
|
7488
|
+
const compiled = (await session.run(`:put [:parse "${escapeForString(command)}"]`)).trim();
|
|
6950
7489
|
const refused = !compiled.startsWith("(") || /syntax error|bad parameter|no such|expected/.test(compiled);
|
|
6951
7490
|
return refused ? compiled.slice(0, 200) : undefined;
|
|
6952
7491
|
}
|
|
6953
|
-
async function installCertificate(
|
|
7492
|
+
async function installCertificate(session, device, name) {
|
|
6954
7493
|
const certificate = device.certificates?.[name];
|
|
6955
7494
|
if (!certificate)
|
|
6956
7495
|
throw new Error(`certificate ${name} is not declared`);
|
|
6957
|
-
|
|
6958
|
-
|
|
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=""`);
|
|
6959
7500
|
if (certificate.private_key) {
|
|
6960
7501
|
const value = resolve(certificate.private_key);
|
|
6961
7502
|
const privateKey = value.includes("-----BEGIN") ? `${value}
|
|
6962
7503
|
` : Buffer.from(value, "base64").toString("utf8");
|
|
6963
|
-
await upload(
|
|
6964
|
-
await
|
|
7504
|
+
await session.upload(`${name}.key`, privateKey);
|
|
7505
|
+
await session.run(`/certificate import file-name=${name}.key name=${name} passphrase=""`);
|
|
6965
7506
|
}
|
|
6966
|
-
await
|
|
7507
|
+
await session.run(`/file remove [find name="${name}.crt" or name="${name}.key"]`);
|
|
6967
7508
|
}
|
|
6968
7509
|
async function apply4(device, _network, changes, options) {
|
|
6969
|
-
const
|
|
6970
|
-
await checkVersion3(
|
|
7510
|
+
const session = await open2(device, options);
|
|
7511
|
+
await checkVersion3(session, device);
|
|
6971
7512
|
const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
|
|
6972
7513
|
for (const { title, command } of commands) {
|
|
6973
7514
|
if (command.startsWith(INSTALL_CERTIFICATE))
|
|
6974
7515
|
continue;
|
|
6975
|
-
const reason = await refusal(
|
|
7516
|
+
const reason = await refusal(session, substitute(command));
|
|
6976
7517
|
if (reason) {
|
|
6977
|
-
|
|
7518
|
+
session.end();
|
|
6978
7519
|
throw new Error(`${title}: the device will not parse ${command.slice(0, 200)}
|
|
6979
7520
|
${reason}`);
|
|
6980
7521
|
}
|
|
@@ -6989,16 +7530,16 @@ async function apply4(device, _network, changes, options) {
|
|
|
6989
7530
|
].join(" ");
|
|
6990
7531
|
try {
|
|
6991
7532
|
console.log(` saving ${backup}, arming a ${options.rollback} minute rollback`);
|
|
6992
|
-
await
|
|
6993
|
-
await
|
|
6994
|
-
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);
|
|
6995
7536
|
for (const { title, command } of commands) {
|
|
6996
7537
|
if (command.startsWith(INSTALL_CERTIFICATE)) {
|
|
6997
7538
|
const certificateName = command.slice(INSTALL_CERTIFICATE.length).trim();
|
|
6998
|
-
await installCertificate(
|
|
7539
|
+
await installCertificate(session, device, certificateName);
|
|
6999
7540
|
continue;
|
|
7000
7541
|
}
|
|
7001
|
-
const answer = (await
|
|
7542
|
+
const answer = (await session.run(substitute(command), 120000)).trim();
|
|
7002
7543
|
if (answer)
|
|
7003
7544
|
throw new Error(`${title}: ${command.slice(0, 160)}
|
|
7004
7545
|
device said: ${answer.slice(0, 300)}`);
|
|
@@ -7006,20 +7547,19 @@ async function apply4(device, _network, changes, options) {
|
|
|
7006
7547
|
} catch (error) {
|
|
7007
7548
|
console.log(` FAILED: ${error.message}`);
|
|
7008
7549
|
console.log(` restoring ${backup} now; the rollback stays armed in case this does not get through`);
|
|
7009
|
-
await
|
|
7550
|
+
await session.run(`/system backup load name=${backup} password=""`, 30000).catch(() => {
|
|
7010
7551
|
return;
|
|
7011
7552
|
});
|
|
7012
|
-
|
|
7553
|
+
session.end();
|
|
7013
7554
|
throw new Error("apply failed and the backup is being restored");
|
|
7014
7555
|
}
|
|
7015
|
-
|
|
7016
|
-
const proof = await login3(device).catch(() => {
|
|
7556
|
+
const proof = await session.fresh().catch(() => {
|
|
7017
7557
|
return;
|
|
7018
7558
|
});
|
|
7019
7559
|
if (!proof)
|
|
7020
7560
|
throw new Error(`could not log in again; the device restores ${backup} within ${options.rollback} minutes`);
|
|
7021
|
-
await
|
|
7022
|
-
await
|
|
7561
|
+
await proof.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
|
|
7562
|
+
await proof.run(`/file remove [find name="${backup}.backup"]`);
|
|
7023
7563
|
proof.end();
|
|
7024
7564
|
console.log(" logged in again, rollback disarmed");
|
|
7025
7565
|
}
|
|
@@ -7043,6 +7583,7 @@ async function fill3(device, _network, set, family, prefixes) {
|
|
|
7043
7583
|
}
|
|
7044
7584
|
var routeros = {
|
|
7045
7585
|
extension: ".rsc",
|
|
7586
|
+
console: true,
|
|
7046
7587
|
unsupported: unsupported4,
|
|
7047
7588
|
render: render4,
|
|
7048
7589
|
read: read4,
|
|
@@ -7515,25 +8056,60 @@ async function openShell(client) {
|
|
|
7515
8056
|
await send(channel, "screen-length 0 temporary", 1e4, 1500);
|
|
7516
8057
|
return channel;
|
|
7517
8058
|
}
|
|
7518
|
-
|
|
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
|
+
}
|
|
7519
8092
|
const client = await login4(device);
|
|
7520
8093
|
try {
|
|
7521
8094
|
const channel = await openShell(client);
|
|
7522
8095
|
const dump = await send(channel, "display current-configuration", 120000, 3000);
|
|
7523
8096
|
channel.end();
|
|
7524
|
-
|
|
7525
|
-
`);
|
|
7526
|
-
const start = lines.findIndex((line) => line.startsWith("!Software Version"));
|
|
7527
|
-
if (start < 0)
|
|
7528
|
-
throw new Error(`${device.name} returned no configuration`);
|
|
7529
|
-
const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
|
|
7530
|
-
return scrub3(body.join(`
|
|
7531
|
-
`).trim() + `
|
|
7532
|
-
`);
|
|
8097
|
+
return configurationOf(device, dump);
|
|
7533
8098
|
} finally {
|
|
7534
8099
|
client.end();
|
|
7535
8100
|
}
|
|
7536
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
|
+
}
|
|
7537
8113
|
function unsupported5(device) {
|
|
7538
8114
|
const found = [];
|
|
7539
8115
|
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
@@ -7630,9 +8206,7 @@ async function sendLine(channel, line) {
|
|
|
7630
8206
|
answer += await send(channel, "Y", 20000, 800);
|
|
7631
8207
|
return answer;
|
|
7632
8208
|
}
|
|
7633
|
-
async function
|
|
7634
|
-
const client = await login4(device);
|
|
7635
|
-
const channel = await openShell(client);
|
|
8209
|
+
async function commitPlan(channel, plan) {
|
|
7636
8210
|
try {
|
|
7637
8211
|
await send(channel, "system-view", 1e4, 1500);
|
|
7638
8212
|
for (const step of plan.steps) {
|
|
@@ -7651,24 +8225,42 @@ async function apply5(device, _network, plan) {
|
|
|
7651
8225
|
await send(channel, "clear configuration candidate", 1e4).catch(() => {
|
|
7652
8226
|
return;
|
|
7653
8227
|
});
|
|
7654
|
-
channel.end();
|
|
7655
|
-
client.end();
|
|
7656
8228
|
throw error;
|
|
7657
8229
|
}
|
|
7658
|
-
|
|
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");
|
|
7659
8247
|
return;
|
|
7660
|
-
}
|
|
7661
|
-
|
|
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 {
|
|
7662
8261
|
channel.end();
|
|
7663
8262
|
client.end();
|
|
7664
|
-
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.");
|
|
7665
8263
|
}
|
|
7666
|
-
proof.end();
|
|
7667
|
-
const saving = await send(channel, "save", 20000, 2000);
|
|
7668
|
-
if (/\[Y\/N\]/i.test(saving))
|
|
7669
|
-
await send(channel, "Y", 60000, 5000);
|
|
7670
|
-
channel.end();
|
|
7671
|
-
client.end();
|
|
7672
8264
|
console.log(" committed, login proved, saved");
|
|
7673
8265
|
}
|
|
7674
8266
|
function plan5(desired, current) {
|
|
@@ -7693,6 +8285,7 @@ function plan5(desired, current) {
|
|
|
7693
8285
|
}
|
|
7694
8286
|
var vrp = {
|
|
7695
8287
|
extension: ".cfg",
|
|
8288
|
+
console: true,
|
|
7696
8289
|
unsupported: unsupported5,
|
|
7697
8290
|
render: render5,
|
|
7698
8291
|
read: read5,
|
|
@@ -8661,6 +9254,7 @@ var HELP = `usage: circuit <command> [device...] [options]
|
|
|
8661
9254
|
apply <device> read the device, plan against what it runs, and send with --confirm
|
|
8662
9255
|
--secrets also send every secret, to rotate one
|
|
8663
9256
|
--rollback=N minutes before a device that can restore itself does, default 10
|
|
9257
|
+
--console through the device's serial console on its jump host, instead of SSH; also for snapshot
|
|
8664
9258
|
refresh <device> fetch prefix sets that come from a registry onto the device, with --confirm
|
|
8665
9259
|
wireguard <device> <interface> <peer>
|
|
8666
9260
|
a client config for one peer, its private key left for the peer to fill
|
|
@@ -8721,7 +9315,8 @@ var names = rest.filter((arg) => !arg.startsWith("--"));
|
|
|
8721
9315
|
var confirmed = flags.includes("--confirm");
|
|
8722
9316
|
var options = {
|
|
8723
9317
|
secrets: flags.includes("--secrets"),
|
|
8724
|
-
rollback: Number(flags.find((flag) => flag.startsWith("--rollback="))?.split("=")[1] ?? 10)
|
|
9318
|
+
rollback: Number(flags.find((flag) => flag.startsWith("--rollback="))?.split("=")[1] ?? 10),
|
|
9319
|
+
...flags.includes("--console") ? { console: true } : {}
|
|
8725
9320
|
};
|
|
8726
9321
|
function fail(message) {
|
|
8727
9322
|
console.error(message);
|
|
@@ -8738,7 +9333,16 @@ function one() {
|
|
|
8738
9333
|
const [device] = selected(false);
|
|
8739
9334
|
return device ?? fail("name a device");
|
|
8740
9335
|
}
|
|
8741
|
-
|
|
9336
|
+
function adapterOf(device) {
|
|
9337
|
+
const adapter = adapters[device.platform] ?? fail(`no adapter for ${device.platform}`);
|
|
9338
|
+
if (!options.console)
|
|
9339
|
+
return adapter;
|
|
9340
|
+
if (!adapter.console)
|
|
9341
|
+
fail(`${device.name}: ${device.platform} cannot be reached through a console yet`);
|
|
9342
|
+
if (!device.connection.console)
|
|
9343
|
+
fail(`${device.name}: --console needs connection.console`);
|
|
9344
|
+
return adapter;
|
|
9345
|
+
}
|
|
8742
9346
|
var generated = join4(root2, ".circuit");
|
|
8743
9347
|
var stateDirectory = join4(generated, "state");
|
|
8744
9348
|
var statePath = (device) => join4(stateDirectory, device.name + adapterOf(device).extension);
|
|
@@ -8805,7 +9409,7 @@ function diffCommand() {
|
|
|
8805
9409
|
}
|
|
8806
9410
|
async function snapshotCommand() {
|
|
8807
9411
|
for (const device of selected(true)) {
|
|
8808
|
-
const text = await adapterOf(device).read(device, network3.devices);
|
|
9412
|
+
const text = await adapterOf(device).read(device, network3.devices, options);
|
|
8809
9413
|
writeState(device, text);
|
|
8810
9414
|
console.log(`${device.name}: ${text.split(`
|
|
8811
9415
|
`).length} lines`);
|
|
@@ -8824,7 +9428,7 @@ async function applyCommand() {
|
|
|
8824
9428
|
const device = one();
|
|
8825
9429
|
guard([device]);
|
|
8826
9430
|
const adapter = adapterOf(device);
|
|
8827
|
-
const live = await adapter.read(device, network3.devices);
|
|
9431
|
+
const live = await adapter.read(device, network3.devices, options);
|
|
8828
9432
|
writeState(device, live);
|
|
8829
9433
|
const plan = adapter.plan(adapter.render(device), live, options);
|
|
8830
9434
|
if (!show(device, plan))
|
|
@@ -8838,7 +9442,7 @@ Nothing sent. Re-run with --confirm.`);
|
|
|
8838
9442
|
return;
|
|
8839
9443
|
}
|
|
8840
9444
|
await adapter.apply(device, network3.devices, plan, options);
|
|
8841
|
-
writeState(device, await adapter.read(device, network3.devices));
|
|
9445
|
+
writeState(device, await adapter.read(device, network3.devices, options));
|
|
8842
9446
|
console.log("snapshot updated");
|
|
8843
9447
|
}
|
|
8844
9448
|
async function refreshCommand() {
|