@takodotid/circuit 0.28.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +19 -9
- package/dist/types/adapters/devices/junos/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/adapters/devices/fortios/TRAPS.md +1 -0
- package/src/adapters/devices/junos/TRAPS.md +3 -0
- package/src/adapters/devices/junos/index.ts +21 -10
- package/src/transport/console.ts +15 -4
package/dist/cli/index.js
CHANGED
|
@@ -324,8 +324,10 @@ async function openOnJump(console2, jump) {
|
|
|
324
324
|
return {
|
|
325
325
|
channel: asTerminal(channel),
|
|
326
326
|
close: () => {
|
|
327
|
-
channel.
|
|
328
|
-
|
|
327
|
+
quiet(channel, 1000).then(() => {
|
|
328
|
+
channel.end();
|
|
329
|
+
client.end();
|
|
330
|
+
});
|
|
329
331
|
}
|
|
330
332
|
};
|
|
331
333
|
} catch (error) {
|
|
@@ -355,8 +357,10 @@ async function openHere(console2) {
|
|
|
355
357
|
return {
|
|
356
358
|
channel: asTerminal(asChannel),
|
|
357
359
|
close: () => {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
+
quiet(asChannel, 1000).then(() => {
|
|
361
|
+
channel.end();
|
|
362
|
+
setTimeout(() => child.kill(), 2000).unref();
|
|
363
|
+
});
|
|
360
364
|
}
|
|
361
365
|
};
|
|
362
366
|
}
|
|
@@ -428,6 +432,7 @@ async function consoleLogin(channel, dialect, user, password) {
|
|
|
428
432
|
return `${dialect.logout}
|
|
429
433
|
`;
|
|
430
434
|
};
|
|
435
|
+
await quiet(channel, 1000);
|
|
431
436
|
let state = (await expect(channel, `
|
|
432
437
|
`, states)).index;
|
|
433
438
|
for (let step = 0;state !== 0; step++) {
|
|
@@ -435,6 +440,7 @@ async function consoleLogin(channel, dialect, user, password) {
|
|
|
435
440
|
throw new Error("the console never reached a login prompt");
|
|
436
441
|
state = (await expect(channel, toward(state), states)).index;
|
|
437
442
|
}
|
|
443
|
+
await quiet(channel, 1000);
|
|
438
444
|
const name = dialect.user ? dialect.user(user) : user;
|
|
439
445
|
if ((await expect(channel, `${name}
|
|
440
446
|
`, [dialect.password, dialect.login, dialect.prompt])).index !== 0) {
|
|
@@ -4566,7 +4572,7 @@ async function edit(channel, line, timeout = 60000) {
|
|
|
4566
4572
|
return output.replace(/\r/g, "");
|
|
4567
4573
|
}
|
|
4568
4574
|
var JUNOS_CONSOLE = {
|
|
4569
|
-
login: /login: $/,
|
|
4575
|
+
login: /(?:^|\n)login: $/,
|
|
4570
4576
|
password: /Password:\s*$/,
|
|
4571
4577
|
prompt: /[>%] $/,
|
|
4572
4578
|
logout: "exit",
|
|
@@ -4613,7 +4619,7 @@ async function named(channel, line) {
|
|
|
4613
4619
|
return [...names].map((name) => `delete ${[...words.slice(0, at), name, ...words.slice(at + 1)].join(" ")}`);
|
|
4614
4620
|
}
|
|
4615
4621
|
var REFUSED2 = /^terminal:\d+:\(\d+\) (?!warning)|^error: /m;
|
|
4616
|
-
async function configure2(channel, plan, guard) {
|
|
4622
|
+
async function configure2(channel, plan, guard, paced = false) {
|
|
4617
4623
|
const sent = plan.steps.flatMap((step) => step.send);
|
|
4618
4624
|
const entered = await run(channel, "configure exclusive");
|
|
4619
4625
|
if (!/Entering configuration mode/.test(entered) || /^error: /m.test(entered)) {
|
|
@@ -4639,9 +4645,13 @@ async function configure2(channel, plan, guard) {
|
|
|
4639
4645
|
channel.write(`load set terminal
|
|
4640
4646
|
`);
|
|
4641
4647
|
await readUntil(channel, (text) => text.includes("[Type ^D at a new line to end input]"), 20000, 300);
|
|
4642
|
-
for (const line of ordered)
|
|
4643
|
-
|
|
4648
|
+
for (const line of ordered) {
|
|
4649
|
+
const statement = substitute(line);
|
|
4650
|
+
channel.write(`${statement}
|
|
4644
4651
|
`);
|
|
4652
|
+
if (paced)
|
|
4653
|
+
await readUntil(channel, (text) => text.replace(/\r/g, "").includes(statement.slice(-24)), 20000, 50);
|
|
4654
|
+
}
|
|
4645
4655
|
const loaded = await edit(channel, "\x04", 300000);
|
|
4646
4656
|
const refused = loaded.split(`
|
|
4647
4657
|
`).filter((line) => REFUSED2.test(line));
|
|
@@ -4706,7 +4716,7 @@ async function apply2(device, _network, plan, options) {
|
|
|
4706
4716
|
await relogin();
|
|
4707
4717
|
await run(channel, "configure exclusive");
|
|
4708
4718
|
}
|
|
4709
|
-
});
|
|
4719
|
+
}, true);
|
|
4710
4720
|
});
|
|
4711
4721
|
return;
|
|
4712
4722
|
}
|
|
@@ -32,7 +32,7 @@ export type Guard = {
|
|
|
32
32
|
*
|
|
33
33
|
* Deletions go before the statements that are set, so a path deleted and set again in one plan ends up set.
|
|
34
34
|
*/
|
|
35
|
-
export declare function configure(channel: ClientChannel, plan: Plan, guard?: Guard): Promise<void>;
|
|
35
|
+
export declare function configure(channel: ClientChannel, plan: Plan, guard?: Guard, paced?: boolean): Promise<void>;
|
|
36
36
|
export declare function apply(device: Device, _network: readonly Device[], plan: Plan, options: ApplyOptions): Promise<void>;
|
|
37
37
|
export declare function unsupported(device: Device): string[];
|
|
38
38
|
/** The plan that replaces a prefix list's entries with what a registry returned. */
|
package/package.json
CHANGED
|
@@ -39,3 +39,4 @@ Behaviour that looks like something else. Each one has been hit on a FortiGate 3
|
|
|
39
39
|
- `execute backup config flash <comment>` keeps a revision on the device, and `execute restore config flash <id>` restores one.
|
|
40
40
|
- The ports are `port1` to `port4` at 40G and `port5` to `port32` at 10G, then `mgmt1` and `mgmt2`. The factory dedicates both management ports to management and gives them addresses `192.168.1.99` and `192.168.2.99`.
|
|
41
41
|
- On the serial console a line ends at a carriage return. A session left open stays logged in, and one left inside `config` stays there, so the next login must first leave and log out. The console takes 9600 bits per second: reading the sections Circuit manages takes about two minutes.
|
|
42
|
+
- A data port can stay `netdev status: down`, with `rx_carrier: 0` in `diagnose hardware deviceinfo nic`, while the far end reads link-ok and frames reach the port's switch chip. Fixing the speed does not change it, in a LAG or out of one; a reboot does. `link_status: Up` there says nothing: a port with no cable shows it too.
|
|
@@ -29,3 +29,6 @@ Behaviour that looks like something else. Each one has been hit on a QFX5100-48S
|
|
|
29
29
|
- IPv4 routes over an IPv6 session need a local IPv4 address under the family: `Local IPv4 address must be configured`.
|
|
30
30
|
- A DHCP server and a DHCP relay cannot share an interface: `Interface irb.10 already configured`.
|
|
31
31
|
- A port's MTU counts the Ethernet header. `mtu 9018` carries 9000-byte packets with a VLAN tag, and a VLAN interface's 9000 then commits without a warning.
|
|
32
|
+
- The serial console has no flow control. Statements pasted into `load set terminal` at once overrun it: some arrive garbled, the load reports syntax errors, and the `^D` that ends it can be lost, leaving the console in the load. Circuit sends each statement after the echo of the one before.
|
|
33
|
+
- After a login the console prints `Last login: ...`, which ends in `login: ` like the login prompt; only a `login: ` on a line of its own is the prompt.
|
|
34
|
+
- A factory-default switch runs Auto Image Upgrade, which prints on the console every few seconds until `delete chassis auto-image-upgrade` is committed.
|
|
@@ -360,7 +360,8 @@ async function edit(channel: ClientChannel, line: string, timeout = 60_000): Pro
|
|
|
360
360
|
|
|
361
361
|
/** How the console asks for a login. Root lands in a shell, `%`, every other user in the CLI, `>`; a session left in configuration mode, `#`, leaves it first. Not yet proved on a device. */
|
|
362
362
|
export const JUNOS_CONSOLE: ConsoleDialect = {
|
|
363
|
-
|
|
363
|
+
// On a line of its own: the banner after a login says `Last login: ...`, and a chunk can end right after it.
|
|
364
|
+
login: /(?:^|\n)login: $/,
|
|
364
365
|
password: /Password:\s*$/,
|
|
365
366
|
prompt: /[>%] $/,
|
|
366
367
|
logout: "exit",
|
|
@@ -434,7 +435,7 @@ export type Guard = { minutes: number; prove: () => Promise<void> };
|
|
|
434
435
|
*
|
|
435
436
|
* Deletions go before the statements that are set, so a path deleted and set again in one plan ends up set.
|
|
436
437
|
*/
|
|
437
|
-
export async function configure(channel: ClientChannel, plan: Plan, guard?: Guard): Promise<void> {
|
|
438
|
+
export async function configure(channel: ClientChannel, plan: Plan, guard?: Guard, paced = false): Promise<void> {
|
|
438
439
|
const sent = plan.steps.flatMap((step) => step.send);
|
|
439
440
|
|
|
440
441
|
const entered = await run(channel, "configure exclusive");
|
|
@@ -460,7 +461,12 @@ export async function configure(channel: ClientChannel, plan: Plan, guard?: Guar
|
|
|
460
461
|
channel.write("load set terminal\n");
|
|
461
462
|
await readUntil(channel, (text) => text.includes("[Type ^D at a new line to end input]"), 20_000, 300);
|
|
462
463
|
|
|
463
|
-
for
|
|
464
|
+
// A serial console has no flow control: statements sent at once overrun it and arrive garbled. There, each waits for the echo of the one before.
|
|
465
|
+
for (const line of ordered) {
|
|
466
|
+
const statement = substitute(line);
|
|
467
|
+
channel.write(`${statement}\n`);
|
|
468
|
+
if (paced) await readUntil(channel, (text) => text.replace(/\r/g, "").includes(statement.slice(-24)), 20_000, 50);
|
|
469
|
+
}
|
|
464
470
|
|
|
465
471
|
const loaded = await edit(channel, "\u0004", 300_000);
|
|
466
472
|
const refused = loaded.split("\n").filter((line) => REFUSED.test(line));
|
|
@@ -518,14 +524,19 @@ export async function apply(device: Device, _network: readonly Device[], plan: P
|
|
|
518
524
|
await checkVersion((command) => run(channel, command), device);
|
|
519
525
|
|
|
520
526
|
// A fresh login on the console ends the session that holds the candidate, so the proof logs in again and takes the candidate back to confirm the commit.
|
|
521
|
-
await configure(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
+
await configure(
|
|
528
|
+
channel,
|
|
529
|
+
plan,
|
|
530
|
+
{
|
|
531
|
+
minutes: options.rollback,
|
|
532
|
+
prove: async () => {
|
|
533
|
+
await run(channel, "exit configuration-mode");
|
|
534
|
+
await relogin();
|
|
535
|
+
await run(channel, "configure exclusive");
|
|
536
|
+
},
|
|
527
537
|
},
|
|
528
|
-
|
|
538
|
+
true
|
|
539
|
+
);
|
|
529
540
|
});
|
|
530
541
|
return;
|
|
531
542
|
}
|
package/src/transport/console.ts
CHANGED
|
@@ -144,9 +144,12 @@ async function openOnJump(console: Console, jump: Jump): Promise<ConsoleSession>
|
|
|
144
144
|
|
|
145
145
|
return {
|
|
146
146
|
channel: asTerminal(channel),
|
|
147
|
+
// What was written last, such as a logout, is still on its way to the device; the relay ends once the console goes quiet.
|
|
147
148
|
close: () => {
|
|
148
|
-
channel.
|
|
149
|
-
|
|
149
|
+
void quiet(channel, 1_000).then(() => {
|
|
150
|
+
channel.end();
|
|
151
|
+
client.end();
|
|
152
|
+
});
|
|
150
153
|
},
|
|
151
154
|
};
|
|
152
155
|
} catch (error) {
|
|
@@ -186,8 +189,10 @@ async function openHere(console: Console): Promise<ConsoleSession> {
|
|
|
186
189
|
return {
|
|
187
190
|
channel: asTerminal(asChannel),
|
|
188
191
|
close: () => {
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
void quiet(asChannel, 1_000).then(() => {
|
|
193
|
+
channel.end();
|
|
194
|
+
setTimeout(() => child.kill(), 2_000).unref();
|
|
195
|
+
});
|
|
191
196
|
},
|
|
192
197
|
};
|
|
193
198
|
}
|
|
@@ -301,12 +306,18 @@ export async function consoleLogin(channel: Channel, dialect: ConsoleDialect, us
|
|
|
301
306
|
return `${dialect.logout}\n`;
|
|
302
307
|
};
|
|
303
308
|
|
|
309
|
+
// What the last session printed as it left, such as a login prompt after its logout, arrives late; it must not be taken for an answer.
|
|
310
|
+
await quiet(channel, 1_000);
|
|
311
|
+
|
|
304
312
|
let state = (await expect(channel, "\n", states)).index;
|
|
305
313
|
for (let step = 0; state !== 0; step++) {
|
|
306
314
|
if (step >= 10) throw new Error("the console never reached a login prompt");
|
|
307
315
|
state = (await expect(channel, toward(state), states)).index;
|
|
308
316
|
}
|
|
309
317
|
|
|
318
|
+
// A console can print the login prompt more than once; a name typed while it does is lost.
|
|
319
|
+
await quiet(channel, 1_000);
|
|
320
|
+
|
|
310
321
|
const name = dialect.user ? dialect.user(user) : user;
|
|
311
322
|
if ((await expect(channel, `${name}\n`, [dialect.password, dialect.login, dialect.prompt])).index !== 0) {
|
|
312
323
|
throw new Error("the console did not ask for a password");
|