@tikoci/quickchr 0.4.8 → 0.5.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/DESIGN.md +219 -14
- package/MANUAL.md +396 -71
- package/README.md +33 -8
- package/package.json +2 -2
- package/src/cli/flags.ts +22 -3
- package/src/cli/index.ts +461 -104
- package/src/cli/wizard.ts +10 -2
- package/src/index.ts +2 -0
- package/src/lib/api-login.ts +138 -0
- package/src/lib/cache.ts +220 -38
- package/src/lib/completions.ts +20 -11
- package/src/lib/device-mode.ts +149 -10
- package/src/lib/diagnostics.ts +13 -6
- package/src/lib/disk.ts +90 -33
- package/src/lib/download.ts +3 -2
- package/src/lib/images.ts +34 -15
- package/src/lib/lock.ts +498 -0
- package/src/lib/names.ts +24 -0
- package/src/lib/network.ts +16 -16
- package/src/lib/packages.ts +66 -40
- package/src/lib/provision.ts +3 -2
- package/src/lib/provisioning-window.ts +356 -0
- package/src/lib/qemu.ts +54 -2
- package/src/lib/quickchr.ts +950 -359
- package/src/lib/rename.ts +41 -0
- package/src/lib/settings.ts +20 -9
- package/src/lib/shutdown.ts +49 -0
- package/src/lib/socket-registry.ts +2 -2
- package/src/lib/state.ts +89 -2
- package/src/lib/storage-lease.ts +169 -0
- package/src/lib/storage.ts +223 -66
- package/src/lib/types.ts +109 -23
- package/src/lib/versions.ts +54 -0
package/MANUAL.md
CHANGED
|
@@ -41,6 +41,10 @@ by the user (or their AI agent) using `quickchr` as a building block.
|
|
|
41
41
|
- ❌ Multi-router topology orchestration, test-matrix runners,
|
|
42
42
|
configuration-as-code DSLs. Use `examples/` as references and compose
|
|
43
43
|
yourself.
|
|
44
|
+
- ⚠️ **RouterOS 6** is a best-effort, boot-only target (x86, CLI only): quickchr
|
|
45
|
+
downloads, boots and tracks a 6.x CHR, and does nothing else to it. It exists so
|
|
46
|
+
v6→v7 migration tooling has a disposable 6.x router to test against — it is not
|
|
47
|
+
RouterOS 6 support. See [RouterOS 6](#routeros-6-boot-only) below.
|
|
44
48
|
|
|
45
49
|
**Two ways to use it:**
|
|
46
50
|
|
|
@@ -68,31 +72,40 @@ by the user (or their AI agent) using `quickchr` as a building block.
|
|
|
68
72
|
|
|
69
73
|
### Install quickchr
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
odd/even policy. Not yet on npm.
|
|
75
|
+
Install the stable npm channel:
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
|
-
|
|
76
|
-
bun add
|
|
78
|
+
bun install -g @tikoci/quickchr
|
|
79
|
+
bun add @tikoci/quickchr
|
|
80
|
+
```
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
For prerelease QA, use `@next` for the current candidate or, after publication,
|
|
83
|
+
`@0.5.1` to pin this candidate. A release is published to `next` first and promoted
|
|
84
|
+
to `latest` once QA passes; `next` is never older than `latest`. The quickchr package
|
|
85
|
+
channel is independent of `--channel`, which selects a RouterOS firmware channel.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bun install -g @tikoci/quickchr@0.5.1
|
|
89
|
+
bun add @tikoci/quickchr@0.5.1
|
|
90
|
+
quickchr --version
|
|
83
91
|
```
|
|
84
92
|
|
|
85
|
-
|
|
93
|
+
To run an unpublished checkout:
|
|
86
94
|
|
|
87
95
|
```bash
|
|
88
|
-
|
|
96
|
+
git clone https://github.com/tikoci/quickchr
|
|
97
|
+
cd quickchr
|
|
98
|
+
bun install
|
|
99
|
+
bun run dev -- doctor
|
|
89
100
|
```
|
|
90
101
|
|
|
91
102
|
### `quickchr doctor`
|
|
92
103
|
|
|
93
104
|
Reports prerequisite status, accelerator availability, image cache
|
|
94
105
|
size, machine count, free disk space, and shell-completion install
|
|
95
|
-
state.
|
|
106
|
+
state. The disk check measures the data dir's filesystem and warns when free
|
|
107
|
+
space is under `storage-reserve` + 2 GiB. Exit code is `0` when no checks return
|
|
108
|
+
`error` (warnings are OK).
|
|
96
109
|
|
|
97
110
|
```bash
|
|
98
111
|
quickchr doctor # human-readable
|
|
@@ -146,6 +159,7 @@ directory, downloads the RouterOS image (cached), and writes
|
|
|
146
159
|
| `--device-mode <m>` | — | `rose`, `advanced`, `basic`, `home`, `auto`, `skip`, ≥ 7.20.8 |
|
|
147
160
|
| `--device-mode-enable <f,...>` | — | Repeatable, comma-separated |
|
|
148
161
|
| `--device-mode-disable <f,...>` | — | Repeatable, comma-separated |
|
|
162
|
+
| `--no-device-mode` | — | Skip device-mode provisioning entirely |
|
|
149
163
|
| `--add-network <spec>` | `user` | See §7. Repeatable. |
|
|
150
164
|
| `--no-network` | false | Headless, no NICs |
|
|
151
165
|
| `--no-winbox` | false | Exclude WinBox port |
|
|
@@ -159,20 +173,36 @@ an error naming its replacement, not a silently ignored flag.
|
|
|
159
173
|
#### `start [name]`
|
|
160
174
|
|
|
161
175
|
Boot a stopped machine, or create-and-boot in one command. Blocks until
|
|
162
|
-
|
|
163
|
-
terminal (`--fg`). Accepts the same provisioning flags as `add`.
|
|
176
|
+
RouterOS is ready (REST answers; on 6.x, a native-API login), or attaches QEMU
|
|
177
|
+
stdio to your terminal (`--fg`). Accepts the same provisioning flags as `add`.
|
|
178
|
+
QEMU runs detached in its own session either way, so it survives a signal to the
|
|
179
|
+
shell that started it.
|
|
180
|
+
|
|
181
|
+
When `<name>` matches no machine (a typo, or one removed earlier), `start` creates a
|
|
182
|
+
factory CHR under that name and says so: `<name> created and started`, or
|
|
183
|
+
`created — not waiting for RouterOS` with `--no-wait`. A machine that already
|
|
184
|
+
existed, including one made by `add`, prints `started`.
|
|
164
185
|
|
|
165
186
|
Additional flags:
|
|
166
187
|
|
|
167
188
|
| Flag | Notes |
|
|
168
189
|
|---|---|
|
|
169
|
-
| `--all` | Start every stopped machine in background |
|
|
170
|
-
| `--
|
|
190
|
+
| `--all` | Start every stopped machine in background; accepts readiness, acceleration, timeout and secure-login options only. Other options are refused rather than ignored. `--secure-login` is resolved per machine: one that has never booted creates the managed login; one that already booted accepts it as a no-op only if it already has one, and is refused otherwise. With `--no-wait`, a never-booted machine refuses it (provisioning needs the wait). Machines start one at a time, so a refusal stops the batch after the earlier machines have started |
|
|
191
|
+
| `--no-wait` | Return as soon as QEMU is running (past the 1.5 s early-exit check), with state and ports persisted; do not wait for RouterOS. Refused with any provisioning flag, including first-boot options `add` recorded, and with `--fg` |
|
|
171
192
|
| `--fg` / `--foreground` (alias `--no-bg`, `--no-background`) | Serial console on stdio |
|
|
172
|
-
| `--
|
|
193
|
+
| `--bg` / `--background` | **Deprecated, does nothing.** Background is the default, and the flag never made `start` return early — it only ever chose where the serial console went. Prints a warning; use `--no-wait` |
|
|
173
194
|
| `--timeout-extra <s>` / `-T <s>` | Add extra seconds to computed boot timeout |
|
|
174
195
|
| `--dry-run` | Print resolved options and QEMU command, exit |
|
|
175
196
|
|
|
197
|
+
After `--no-wait`, readiness is yours. centrs (0.1.8+) waits for it in one command:
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
quickchr start lab --no-wait
|
|
201
|
+
centrs retrieve --quickchr lab /system/resource --wait 5m
|
|
202
|
+
# RouterOS 6: centrs validates against the 7.x command tree, so skip validation
|
|
203
|
+
centrs retrieve --quickchr lab6 /system/resource --via native-api --no-validate --wait 5m
|
|
204
|
+
```
|
|
205
|
+
|
|
176
206
|
Foreground mode shortcuts (QEMU mux):
|
|
177
207
|
|
|
178
208
|
| Key | Action |
|
|
@@ -188,9 +218,74 @@ machine state.
|
|
|
188
218
|
|
|
189
219
|
#### `stop [name | --all]`
|
|
190
220
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
221
|
+
Power cut: sends SIGTERM to QEMU, waits up to 5 seconds, then SIGKILL if
|
|
222
|
+
needed. This does not request a guest shutdown; pending RouterOS changes
|
|
223
|
+
may be lost. Updates `state.status = "stopped"` and unregisters socket-named
|
|
224
|
+
networks. Listing-mode if no name and not `--all`.
|
|
225
|
+
|
|
226
|
+
Use `stop` for normal teardown and device-mode power-cycle confirmation.
|
|
227
|
+
Use `shutdown` when the caller wants to request a guest shutdown first.
|
|
228
|
+
|
|
229
|
+
#### `shutdown <name> [--timeout <seconds>]`
|
|
230
|
+
|
|
231
|
+
Sends `/system/shutdown` once through REST, independently waits for QEMU to
|
|
232
|
+
exit, then falls back to a power cut if it remains running. Default grace
|
|
233
|
+
budget: **10 seconds**. `--timeout` takes positive integer seconds and
|
|
234
|
+
overrides `QUICKCHR_SHUTDOWN_TIMEOUT`. Neither changes the boot timeout.
|
|
235
|
+
The maximum is 2,147,483 seconds (the HTTP timer's limit).
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
quickchr shutdown lab
|
|
239
|
+
quickchr shutdown lab --timeout 20
|
|
240
|
+
QUICKCHR_SHUTDOWN_TIMEOUT=20 quickchr shutdown lab
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The CLI reports `guest exited`, `already stopped`, or **forced power cut —
|
|
244
|
+
guest shutdown not confirmed**. Forced fallback exits successfully if QEMU
|
|
245
|
+
is confirmed gone; invalid input, lock failure, or inability to stop QEMU
|
|
246
|
+
is an error. Disks, credentials and restart configuration survive.
|
|
247
|
+
|
|
248
|
+
The guest request and exit observation share one grace budget. Lifecycle
|
|
249
|
+
lock acquisition, the existing SIGTERM/SIGKILL escalation, and final exit
|
|
250
|
+
verification have separate bounded waits. A 10-second grace budget does
|
|
251
|
+
not bound the entire CLI invocation to 10 seconds.
|
|
252
|
+
|
|
253
|
+
An HTTP error does not establish failure: on CHR 7.24.5, both x86/HVF and
|
|
254
|
+
arm64/TCG returned HTTP 400 `interrupted`, then exited. Shutdown uses
|
|
255
|
+
process exit as its completion signal. Observed exit does not guarantee
|
|
256
|
+
application or storage durability. Unavailable REST (including RouterOS 6),
|
|
257
|
+
failed authentication and a guest that does not halt all use the same
|
|
258
|
+
explicit hard-stop fallback; no console/QGA/ACPI replay is attempted.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const result = await chr.shutdown({ timeoutMs: 20_000 });
|
|
262
|
+
// outcome: "guest-exited" | "forced" | "already-stopped"
|
|
263
|
+
// requestError can accompany guest-exited (e.g. HTTP 400 interrupted).
|
|
264
|
+
console.log(result.outcome);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Library `timeoutMs` values are positive integer milliseconds, maximum
|
|
268
|
+
2,147,483,647, with a 10,000 ms default. Library callers supply options
|
|
269
|
+
explicitly; the CLI environment override is not read by `shutdown()`.
|
|
270
|
+
|
|
271
|
+
For consumers of an older quickchr version, the manual workaround needs
|
|
272
|
+
both request-error handling and independent exit observation:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
const pid = chr.state.pid;
|
|
276
|
+
if (pid) {
|
|
277
|
+
try { await chr.exec("/system/shutdown", { via: "rest", timeout: 10_000 }); }
|
|
278
|
+
catch { /* HTTP 400 interrupted may accompany successful guest shutdown. */ }
|
|
279
|
+
const exited = await chr.waitFor(async () => {
|
|
280
|
+
try { process.kill(pid, 0); return false; } catch { return true; }
|
|
281
|
+
}, 10_000);
|
|
282
|
+
if (!exited) console.warn("Guest shutdown not confirmed; forcing a power cut");
|
|
283
|
+
}
|
|
284
|
+
await chr.stop(); // reconcile state/network membership, or hard-stop if still alive
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
This older-version workaround has separate request and wait budgets;
|
|
288
|
+
`shutdown()` provides one shared budget and structured fallback reporting.
|
|
194
289
|
|
|
195
290
|
#### `remove [name | --all]` (alias `rm`)
|
|
196
291
|
|
|
@@ -207,10 +302,17 @@ flag. The next `start` therefore logs in as factory `admin` with an empty
|
|
|
207
302
|
password. EFI vars are deliberately **kept** (they store boot order, not
|
|
208
303
|
guest state; wiping them costs an arm64 device rescan).
|
|
209
304
|
|
|
210
|
-
`clean`
|
|
211
|
-
(
|
|
212
|
-
|
|
213
|
-
|
|
305
|
+
`clean` itself never provisions — it only resets the disk. What it does do is
|
|
306
|
+
**reopen the provisioning window** (#176), so the *next* `start` re-applies the
|
|
307
|
+
provisioning intent `machine.json` still holds: `packages`, `deviceMode`,
|
|
308
|
+
`secureLogin`. Budget for that start accordingly; it costs what a first boot costs,
|
|
309
|
+
not what a restart costs.
|
|
310
|
+
|
|
311
|
+
What does **not** come back on its own is anything `clean` had to clear because it
|
|
312
|
+
described the erased guest rather than an intent — `user`, `disableAdmin`,
|
|
313
|
+
`licenseLevel` — and any option from a request that was *refused*, since a refused
|
|
314
|
+
request is never persisted. Pass those flags again, which is why the refusal says to
|
|
315
|
+
repeat the original command rather than to run a bare `start`.
|
|
214
316
|
|
|
215
317
|
### Inspection
|
|
216
318
|
|
|
@@ -238,6 +340,9 @@ machine as a directory to delete rather than state to load.
|
|
|
238
340
|
Live REST query. Hits the running CHR and returns the requested group
|
|
239
341
|
(or all if no group specified). Requires the machine to be running.
|
|
240
342
|
|
|
343
|
+
`device-mode` lists the mode plus every feature RouterOS reports, split into
|
|
344
|
+
enabled and disabled — the read-back for `quickchr set <name> --device-mode…`.
|
|
345
|
+
|
|
241
346
|
#### `inspect <name> [--json]`
|
|
242
347
|
|
|
243
348
|
Stable, versioned (`descriptorVersion: 1`) connection descriptor for a
|
|
@@ -303,7 +408,8 @@ the Library API reference.
|
|
|
303
408
|
#### `exec <name> <command> [--via=auto|rest|qga|console|ssh] [--user u] [--password p] [--timeout=ms]`
|
|
304
409
|
|
|
305
410
|
Run a RouterOS CLI command. See §12 for transport details and JSON
|
|
306
|
-
output trick.
|
|
411
|
+
output trick. Exit 0 means the command was delivered, not that RouterOS
|
|
412
|
+
accepted it — see §12 "Exit status" for the pass/fail route.
|
|
307
413
|
|
|
308
414
|
#### `console <name>`
|
|
309
415
|
|
|
@@ -328,11 +434,43 @@ Apply or renew a CHR trial license on a running instance. Resolves
|
|
|
328
434
|
MikroTik.com credentials from `--license-account`/`--license-password`,
|
|
329
435
|
the env vars, or the secret store. Requires RouterOS ≥ 7.20.8.
|
|
330
436
|
|
|
331
|
-
#### `set <name> <
|
|
437
|
+
#### `set <name> [--license | --device-mode <m>] [--device-mode-enable <f>] [--device-mode-disable <f>]`
|
|
332
438
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
439
|
+
Change a property on a machine that has **already booted** — the post-boot half
|
|
440
|
+
of the provisioning flags. `add`/`start` apply them at first boot; `set` applies
|
|
441
|
+
them now. It is the route `PROVISIONING_WINDOW_CLOSED` names, printed with the
|
|
442
|
+
flags that were asked for so it can be run as given.
|
|
443
|
+
|
|
444
|
+
| flag | what it does |
|
|
445
|
+
|---|---|
|
|
446
|
+
| `--license` | Apply or renew a CHR trial license (§ `license` above for credential resolution). |
|
|
447
|
+
| `--device-mode <m>` | `rose`, `advanced`, `basic`, `home`, `auto`. |
|
|
448
|
+
| `--device-mode-enable <f,…>` | Set one or more device-mode flags to yes. Repeatable, comma-separated. |
|
|
449
|
+
| `--device-mode-disable <f,…>` | Set one or more to no. |
|
|
450
|
+
|
|
451
|
+
**Device-mode restarts the machine.** RouterOS only applies a device-mode change
|
|
452
|
+
across a power cycle, and quickchr owns the power button — which is why this lives
|
|
453
|
+
here rather than in a tool that reaches a router over the network: the same request
|
|
454
|
+
issued from inside the guest never returns, because it is waiting for a power cycle
|
|
455
|
+
it cannot perform on itself. quickchr prints every setting that will move before it
|
|
456
|
+
restarts, and skips the restart entirely when the guest already matches.
|
|
457
|
+
|
|
458
|
+
Naming a feature without a mode resolves the mode to `rose` — so
|
|
459
|
+
`set lab --device-mode-enable container` moves `mode` as well. That move is printed
|
|
460
|
+
before it happens, not discovered afterwards.
|
|
461
|
+
|
|
462
|
+
Preconditions: the machine must be **running** and have a **user-mode NIC**
|
|
463
|
+
(device-mode is applied over localhost REST), on RouterOS ≥ 7.20.8. Each is a
|
|
464
|
+
named refusal rather than a timeout.
|
|
465
|
+
|
|
466
|
+
The change is folded into `machine.json` rather than overwriting it — device-mode
|
|
467
|
+
updates move only the settings they name, so the record stays cumulative — and
|
|
468
|
+
recorded in `provisioning.steps`, which makes a later `start` passing the same
|
|
469
|
+
device-mode a recognised no-op instead of a refusal.
|
|
470
|
+
|
|
471
|
+
**Not available here:** `--add-user`, `--disable-admin`, `--secure-login`. Those
|
|
472
|
+
rewrite working config rather than unlock a capability, and they are the steps that
|
|
473
|
+
drift; their route stays `quickchr clean <name>` followed by the original command.
|
|
336
474
|
|
|
337
475
|
#### `snapshot <name> <list | save [snapname] | load <snapname> | delete <snapname>>` (alias `snap`)
|
|
338
476
|
|
|
@@ -372,7 +510,7 @@ another machine, view status, or quit.
|
|
|
372
510
|
| 9 | Extra blank disks (repeatable) | none | `--add-disk` |
|
|
373
511
|
| 10 | Additional networks (macOS/Linux) | user-mode only | `--add-network` |
|
|
374
512
|
| 11 | Provisioning (packages, device-mode, user, license) | optional | various |
|
|
375
|
-
| 12 | Run mode: background / foreground | background |
|
|
513
|
+
| 12 | Run mode: background / foreground | background | (default) / `--fg` |
|
|
376
514
|
| 13 | Final confirmation | — | — |
|
|
377
515
|
|
|
378
516
|
Steps 7–9 are skipped when `qemu-img` is absent (a warning is shown). Step 10
|
|
@@ -444,6 +582,64 @@ appended directly to `$GITHUB_OUTPUT`.
|
|
|
444
582
|
`--json` intentionally emits one JSON object for programmatic parsing; use the
|
|
445
583
|
default output form when writing step outputs directly.
|
|
446
584
|
|
|
585
|
+
`cache list [--json]` shows each cache entry with its version, arch, size, age
|
|
586
|
+
(time since it was cached or last used) and whether it is in use. Entries are:
|
|
587
|
+
|
|
588
|
+
- **images** (`chr-<ver>[-arm64].img`, and a `.img.zip` not yet extracted). An
|
|
589
|
+
image is **in use** while any machine, running or stopped, was created from
|
|
590
|
+
that version and arch: `clean` and a failed first boot rebuild from it.
|
|
591
|
+
- **package archives** (`packages-<arch>-<ver>`): the `all_packages` ZIP and its
|
|
592
|
+
extraction, sized and deleted together, since the extraction is only a cache
|
|
593
|
+
hit while its ZIP is there. In use while a machine that asks for packages has
|
|
594
|
+
not finished its first boot.
|
|
595
|
+
- **partial** entries: an interrupted download (`*.<pid>-<id>.part`) or
|
|
596
|
+
extraction (`.quickchr-image-*`, `.quickchr-packages-*`). In use while the
|
|
597
|
+
process that wrote it is alive (an extraction: for 30 minutes after its last
|
|
598
|
+
write). `prune` removes stale ones before anything else.
|
|
599
|
+
|
|
600
|
+
`cache list --json` prints one object per entry, including `kind` (`image`, `image-zip`,
|
|
601
|
+
`packages` or `partial`) and `paths` (every file deleting the entry removes).
|
|
602
|
+
Files in the cache that quickchr did not write are reported as "Other files"
|
|
603
|
+
and never deleted. `prune` and `clear` never delete an in-use entry.
|
|
604
|
+
|
|
605
|
+
```bash
|
|
606
|
+
quickchr cache list
|
|
607
|
+
quickchr cache prune --older-than 7.22 --dry-run # versions below 7.22
|
|
608
|
+
quickchr cache prune --max-age 30d # not used for more than 30 days
|
|
609
|
+
quickchr cache prune --max-size 2G # oldest versions first, until <= 2 GiB
|
|
610
|
+
quickchr cache clear --all # every entry not in use
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
`prune` needs at least one of `--older-than <ver>`, `--max-age <dur>` or
|
|
614
|
+
`--max-size <size>`, and applies them in that order when combined. `--max-size`
|
|
615
|
+
evicts the oldest RouterOS version first (oldest cache time breaks ties) until
|
|
616
|
+
the total, in-use entries included, fits. Durations are `<N>(s|m|h|d|w)`; sizes
|
|
617
|
+
are `<N>(K|M|G|T)` with an optional `i`/`B`, read as IEC binary. `--dry-run`
|
|
618
|
+
lists what `prune` or `clear` would delete without deleting it. `clear` refuses
|
|
619
|
+
without `--all`. A delete that fails is listed on stderr, is not counted as
|
|
620
|
+
freed, and makes the command exit 1. A cache hit counts as a use, so `--max-age`
|
|
621
|
+
measures time since an entry was last used.
|
|
622
|
+
|
|
623
|
+
Separately, a successful `start` prunes to the `cache-max-size` setting (default
|
|
624
|
+
`2G`; see `settings` below). That automatic prune, and the one below, skip any
|
|
625
|
+
entry used in the last 30 minutes, which another create may be reading.
|
|
626
|
+
|
|
627
|
+
**Disk space.** Creating a machine (`add`, or `start` with a new name) and
|
|
628
|
+
filling the cache (an image or package download) are admitted when each
|
|
629
|
+
filesystem the operation writes to has room for what it writes there plus the
|
|
630
|
+
`storage-reserve` setting (default `2G`), less what other quickchr operations
|
|
631
|
+
in progress have already claimed. The estimate is the boot disk
|
|
632
|
+
(192 MiB), plus an image download (256 MiB) and a package download (192 MiB)
|
|
633
|
+
when those are not cached yet, so a cold create needs about 2.6 GiB free and a
|
|
634
|
+
warm one about 2.2 GiB. When there is not enough, quickchr first evicts what the
|
|
635
|
+
cache can spare on that filesystem (oldest version first; never entries a
|
|
636
|
+
machine or an operation in progress needs, nor recently used ones), measures
|
|
637
|
+
again until there is room or nothing more can go, and only then refuses with `INSUFFICIENT_DISK_SPACE`,
|
|
638
|
+
naming the free space, what it needed, what it evicted and what it kept.
|
|
639
|
+
Restarting an existing machine, `stop`, `remove` and read-only commands are
|
|
640
|
+
never gated. The estimate covers creation, not a guest that later fills its
|
|
641
|
+
disk: extra disks and `--boot-size` are sparse and grow as the guest writes.
|
|
642
|
+
|
|
447
643
|
#### `settings [print|get|set|reset] [key] [value] [--json]`
|
|
448
644
|
|
|
449
645
|
Manage quickchr's own global preferences — repeated flags/defaults that
|
|
@@ -460,6 +656,7 @@ Precedence per key (highest wins): CLI flag > `QUICKCHR_<KEY>` env var >
|
|
|
460
656
|
| `default-arch` | `add`/`start`'s `--arch`, wizard's arch prompt | `arm64`\|`x86`\|`auto` | `auto` (host native) |
|
|
461
657
|
| `accel` | `add`/`start`'s `--accel`; every `detectAccel()` call in the run | `auto`\|`tcg`\|`hvf`\|`kvm` | `auto` (detect) |
|
|
462
658
|
| `cache-max-size` | Auto-prune cap applied after each successful `start` | size string (e.g. `2G`, `512M`) | `2G` |
|
|
659
|
+
| `storage-reserve` | Free space creating a machine or filling the cache must leave, beyond what it writes | size string | `2G` |
|
|
463
660
|
| `timeout-extra` | `start`'s `--timeout-extra`/`-T` when omitted | non-negative integer seconds | `0` |
|
|
464
661
|
| `secure-login` | `add`/`start`'s `--secure-login` when neither `--secure-login` nor `--no-secure-login` is passed | boolean | `unset` (reported as `(unset)` — see note) |
|
|
465
662
|
|
|
@@ -478,7 +675,7 @@ the wizard's own recommended "managed login" highlighted). Giving it a real
|
|
|
478
675
|
the setting resolves to exactly `true`.
|
|
479
676
|
|
|
480
677
|
```bash
|
|
481
|
-
quickchr settings print # all
|
|
678
|
+
quickchr settings print # all 7 keys, resolved value + source
|
|
482
679
|
quickchr settings set default-channel testing # write one key to quickchr.env
|
|
483
680
|
quickchr settings get default-arch # print one key's resolved value
|
|
484
681
|
quickchr settings reset default-channel # delete one managed line
|
|
@@ -486,7 +683,7 @@ quickchr settings reset # clear all managed lines
|
|
|
486
683
|
```
|
|
487
684
|
|
|
488
685
|
Credentials (`--add-user`, `MIKROTIK_WEB_ACCOUNT`/`MIKROTIK_WEB_PASSWORD`) are
|
|
489
|
-
never stored here — `settings` only manages the
|
|
686
|
+
never stored here — `settings` only manages the 7 keys above, and does not
|
|
490
687
|
mutate any machine's `machine.json`.
|
|
491
688
|
|
|
492
689
|
### Global behavior
|
|
@@ -497,7 +694,7 @@ mutate any machine's `machine.json`.
|
|
|
497
694
|
| `QUICKCHR_NO_PROMPT` | Force non-interactive mode (skips all wizards) |
|
|
498
695
|
| `QUICKCHR_DEBUG` | Emit `[debug]` lines from the progress logger |
|
|
499
696
|
| `QUICKCHR_INTEGRATION` | Required to run `test/integration/` |
|
|
500
|
-
| `QUICKCHR_PRESERVE_ON_FAILURE` | `=1`
|
|
697
|
+
| `QUICKCHR_PRESERVE_ON_FAILURE` | `=1` skips removal/reset after a first-boot timeout or failed first-boot provisioning run (QEMU is still stopped). Previously booted machines always survive a restart timeout |
|
|
501
698
|
| `QUICKCHR_SERIAL_LOG` | `=1` tees the guest serial console to `<machineDir>/serial.log`. **Secret-bearing** — serial provisioning types the generated password in cleartext |
|
|
502
699
|
| `QUICKCHR_DEEP_BOOT_DIAGNOSTICS` | `=1` lets a boot-timeout capture write to the guest (a mangle counting rule + host probes) to localize a silent packet drop. Skipped when `QUICKCHR_PRESERVE_ON_FAILURE=1` |
|
|
503
700
|
| `MIKROTIK_WEB_ACCOUNT`, `MIKROTIK_WEB_PASSWORD` | License credentials fallback |
|
|
@@ -552,10 +749,29 @@ QuickCHR.resolveVersion(channel: Channel): Promise<string>
|
|
|
552
749
|
```
|
|
553
750
|
|
|
554
751
|
`start()` creates the machine if needed, downloads the image, allocates
|
|
555
|
-
a port block, spawns QEMU, waits for REST
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
752
|
+
a port block, spawns QEMU, waits for readiness (REST on 7.x, a native-API
|
|
753
|
+
`/login` on RouterOS 6), and runs all provisioning steps before returning.
|
|
754
|
+
With `wait: false` (`--no-wait`) it returns once QEMU is running and leaves
|
|
755
|
+
readiness to the caller; provisioning options are refused in that mode.
|
|
756
|
+
On a boot timeout it stops QEMU, then removes a machine this call created,
|
|
757
|
+
resets an existing never-booted machine to its factory disk, and preserves a
|
|
758
|
+
previously booted one — see [Auto-cleanup on boot timeout](#auto-cleanup-on-boot-timeout).
|
|
759
|
+
|
|
760
|
+
**Concurrency.** Machines may be created and started in parallel, from one
|
|
761
|
+
`Promise.all` or from separate processes. Each creation chooses its port
|
|
762
|
+
block and NIC MACs under a data-dir-wide allocation lock and reserves them
|
|
763
|
+
until `machine.json` is written, so concurrent auto-allocated machines never
|
|
764
|
+
share a block. An explicit `portBase` is never moved: one that overlaps
|
|
765
|
+
another machine's block fails with `PORT_CONFLICT`. Per machine, every
|
|
766
|
+
lifecycle operation takes the machine lock (`machines/<name>/.lock/`):
|
|
767
|
+
|
|
768
|
+
- `stop()`, `clean()` and `remove()` **wait** for an operation already in
|
|
769
|
+
progress (up to the machine's boot timeout), then act on the state it left.
|
|
770
|
+
- A second `start()` of the same machine, `setDeviceMode()` and removing a
|
|
771
|
+
leftover directory **fail at once** with `MACHINE_LOCKED`, naming the
|
|
772
|
+
operation, pid and age of the holder.
|
|
773
|
+
- A machine running in the **foreground** holds the lock until its VM exits.
|
|
774
|
+
Nothing waits for it. Quit it from its console.
|
|
559
775
|
|
|
560
776
|
`add()` is the same minus the QEMU spawn — useful when you want to
|
|
561
777
|
materialize a machine and start it later (or hand the config to another
|
|
@@ -598,7 +814,7 @@ throws `INVALID_NAME` for a name that is not a usable path segment.
|
|
|
598
814
|
|---|---|---|
|
|
599
815
|
| Identity | `name`, `state`, `ports`, `restUrl`, `sshPort`, `portBase` | Static metadata persisted in `machine.json` |
|
|
600
816
|
| Capture | `captureInterface`, `hostGatewayIp` | Platform-correct values for TZSP capture (`tshark -i …`) and the guest→host address any UDP emitter targets |
|
|
601
|
-
| Lifecycle | `waitForBoot()`, `waitFor(fn, ms?)`, `stop()`, `remove()`, `clean()`, `destroy()` | Boot readiness, custom polling, teardown |
|
|
817
|
+
| Lifecycle | `waitForBoot()`, `waitFor(fn, ms?)`, `stop()`, `shutdown(opts?)`, `remove()`, `clean()`, `destroy()` | Boot readiness, custom polling, power-cut or guest-shutdown teardown |
|
|
602
818
|
| Comms | `rest()`, `exec()`, `monitor()`, `serial()`, `qga()` | REST, RouterOS CLI, QEMU monitor, serial console, QGA (x86 only) |
|
|
603
819
|
| Provisioning | `license()`, `setDeviceMode()`, `availablePackages()`, `installPackage()` | Post-boot configuration |
|
|
604
820
|
| Files | `upload()`, `download()` | SCP transfer to/from the CHR |
|
|
@@ -615,11 +831,11 @@ interface ChrInstance {
|
|
|
615
831
|
portBase: number; // start of this instance's reserved port block
|
|
616
832
|
captureInterface: string; // "lo0" on macOS, "any" on Linux — for TZSP capture
|
|
617
833
|
hostGatewayIp: string; // "10.0.2.2" — the host, as seen from inside the guest
|
|
618
|
-
tzspGatewayIp: string; // deprecated alias for hostGatewayIp (#26)
|
|
619
834
|
|
|
620
835
|
waitForBoot(timeoutMs?: number): Promise<boolean>;
|
|
621
836
|
waitFor(condition: () => Promise<boolean>, timeoutMs?: number): Promise<boolean>;
|
|
622
|
-
stop(): Promise<void>;
|
|
837
|
+
stop(): Promise<void>; // power cut: SIGTERM, then SIGKILL
|
|
838
|
+
shutdown(opts?: ShutdownOptions): Promise<ShutdownResult>; // /system/shutdown, bounded fallback
|
|
623
839
|
remove(): Promise<void>;
|
|
624
840
|
clean(): Promise<void>;
|
|
625
841
|
destroy(): Promise<void>; // stop + remove
|
|
@@ -682,13 +898,14 @@ redact before logging or attaching to bug reports.
|
|
|
682
898
|
| Field | Type | Default | Notes |
|
|
683
899
|
|---|---|---|---|
|
|
684
900
|
| `name` | string | auto | Must not start with `-` |
|
|
685
|
-
| `version` | string | — |
|
|
901
|
+
| `version` | string | — | Wins over `channel`: passing both warns and uses `version` |
|
|
686
902
|
| `channel` | Channel | `"stable"` | |
|
|
687
903
|
| `arch` | `"arm64" \| "x86"` | host | |
|
|
688
904
|
| `cpu` | number | `1` | |
|
|
689
905
|
| `mem` | number | `512` (1024 cross-arch TCG) | MiB |
|
|
690
|
-
| `background` | boolean | `true` | |
|
|
691
|
-
| `
|
|
906
|
+
| `background` | boolean | `true` | Where the serial console goes — not whether `start()` waits |
|
|
907
|
+
| `wait` | boolean | `true` | `false` resolves once QEMU is running and leaves readiness to the caller (`instance.waitForBoot()`); refused with provisioning or `background: false` (`INVALID_ARGUMENT`) |
|
|
908
|
+
| `bootDiskFormat` | `"raw" \| "qcow2"` | `"raw"` (auto-`qcow2` if `bootSize`) | The CLI always passes a format, defaulting to `qcow2`; the `"raw"` default applies to library callers only |
|
|
692
909
|
| `bootSize` | string | — | e.g., `"512M"`, `"2G"`. Needs `qemu-img` |
|
|
693
910
|
| `extraDisks` | string[] | `[]` | Each entry is a size string |
|
|
694
911
|
| `networks` | NetworkSpecifier[] | `["user"]` | See §7 |
|
|
@@ -702,7 +919,7 @@ redact before logging or attaching to bug reports.
|
|
|
702
919
|
| `secureLogin` | boolean | `false` | **`true` is the explicit trigger** for managed-account creation |
|
|
703
920
|
| `license` | `LicenseInput` | — | `"p1"` / `"p10"` / `"unlimited"` or `LicenseOptions` |
|
|
704
921
|
| `deviceMode` | `DeviceModeOptions` | — | `{mode?, enable?[], disable?[]}` |
|
|
705
|
-
| `installDeps` | boolean | — |
|
|
922
|
+
| `installDeps` | boolean | — | **Deprecated, ignored.** Never installed anything; run `quickchr doctor` for install hints |
|
|
706
923
|
| `dryRun` | boolean | `false` | Print QEMU command and exit |
|
|
707
924
|
| `timeoutExtra` | number | `0` | Extra **milliseconds** added to boot timeout |
|
|
708
925
|
| `onProgress` | `(msg) => void` | `console.log` | `[debug]` lines only when `QUICKCHR_DEBUG=1` |
|
|
@@ -843,10 +1060,43 @@ port mappings work on any 7.x.
|
|
|
843
1060
|
Disabling admin **does not** lock the REST API — RouterOS's `expired`
|
|
844
1061
|
flag only gates CLI/SSH/Winbox login, not REST.
|
|
845
1062
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1063
|
+
A failure in any step fails the whole first boot, and quickchr rolls it back
|
|
1064
|
+
rather than leave a half-provisioned guest: a machine `start` created in that
|
|
1065
|
+
call is removed (as a boot timeout removes it), and a machine created by `add`
|
|
1066
|
+
has its disk reset to the factory image and `machine.json` restored to what
|
|
1067
|
+
`add` recorded. Either way the retry is **the same command again**. With
|
|
1068
|
+
`QUICKCHR_PRESERVE_ON_FAILURE=1` the half-provisioned machine is kept, stopped,
|
|
1069
|
+
for inspection — `quickchr clean <name>` then resets it.
|
|
1070
|
+
|
|
1071
|
+
On the first `start` of a machine created by `add`, a provisioning option you
|
|
1072
|
+
leave out keeps what `add` recorded, and an explicit negation
|
|
1073
|
+
(`--no-install-all-packages`, `--no-disable-admin`, `--no-secure-login`,
|
|
1074
|
+
`--no-device-mode`) overrides it.
|
|
1075
|
+
|
|
1076
|
+
**The provisioning window closes at the machine's first boot** — not at
|
|
1077
|
+
`add`. A machine created with no provisioning options can still take them
|
|
1078
|
+
on its first `start`, which is where they are applied from `machine.json`.
|
|
1079
|
+
After that boot the guest may hold configuration quickchr did not put
|
|
1080
|
+
there, so provisioning cannot run against it: an option passed anyway is
|
|
1081
|
+
**refused**, never ignored, with `PROVISIONING_WINDOW_CLOSED` naming where
|
|
1082
|
+
the change can still be made. An option `machine.json` already records is
|
|
1083
|
+
reported as already applied rather than failing, so passing the same flags
|
|
1084
|
+
on every start is fine.
|
|
1085
|
+
|
|
1086
|
+
Routes that still work after the window has closed:
|
|
1087
|
+
|
|
1088
|
+
| change | route |
|
|
1089
|
+
|---|---|
|
|
1090
|
+
| license | `quickchr set <name> --license` |
|
|
1091
|
+
| device-mode | `quickchr set <name> --device-mode…` / `instance.setDeviceMode()` (power-cycles the VM) |
|
|
1092
|
+
| packages | `instance.installPackage()` (library; reboots the guest) |
|
|
1093
|
+
| anything else | `quickchr clean <name>` resets the disk to the factory image and reopens the window, at first-boot cost |
|
|
1094
|
+
|
|
1095
|
+
`clean()` retains provisioning *intent* — packages, device-mode, managed login — so the
|
|
1096
|
+
next `start` re-applies those on its own. It clears `user`, `disableAdmin` and
|
|
1097
|
+
`licenseLevel`, which are facts about the erased guest rather than intent, and a refused
|
|
1098
|
+
request is never persisted. **Repeat the original command after the reset** rather than
|
|
1099
|
+
a bare `quickchr start <name>`, or those options are silently left out.
|
|
850
1100
|
|
|
851
1101
|
**Always read back what we wrote.** This catches version-specific drift
|
|
852
1102
|
in REST responses and surfaces actionable errors instead of silent
|
|
@@ -1011,6 +1261,7 @@ restrictions, half-open SLiRP under TCG), see `docs/networking.md`.
|
|
|
1011
1261
|
|
|
1012
1262
|
```text
|
|
1013
1263
|
$XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
|
|
1264
|
+
├── .allocate.lock/ held while a new machine picks its ports/MACs
|
|
1014
1265
|
├── cache/ Downloaded RouterOS images
|
|
1015
1266
|
│ ├── chr-7.22.1.img.zip
|
|
1016
1267
|
│ ├── chr-7.22.1.img
|
|
@@ -1018,15 +1269,15 @@ $XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
|
|
|
1018
1269
|
├── machines/
|
|
1019
1270
|
│ └── <name>/
|
|
1020
1271
|
│ ├── machine.json Persisted MachineState
|
|
1021
|
-
│ ├──
|
|
1022
|
-
│ ├──
|
|
1272
|
+
│ ├── boot.qcow2 | disk.img boot disk: qcow2 (CLI default) or raw — one, not both
|
|
1273
|
+
│ ├── disk<N>.qcow2 extra disks
|
|
1023
1274
|
│ ├── efi-vars.fd arm64 only (UEFI vars)
|
|
1024
1275
|
│ ├── monitor.sock QEMU monitor
|
|
1025
1276
|
│ ├── serial.sock serial console
|
|
1026
1277
|
│ ├── qga.sock QGA (x86 only)
|
|
1027
1278
|
│ ├── qemu.pid
|
|
1028
1279
|
│ ├── qemu.log
|
|
1029
|
-
│ ├──
|
|
1280
|
+
│ ├── .lock/ lifecycle lock + in-flight port/MAC reservation
|
|
1030
1281
|
│ └── ssh/id_ed25519[.pub] SSH key (if managed user provisioned)
|
|
1031
1282
|
└── networks/ Named L2 socket reservations
|
|
1032
1283
|
└── <name>.json
|
|
@@ -1047,7 +1298,7 @@ var list.
|
|
|
1047
1298
|
├── cache\
|
|
1048
1299
|
├── machines\<name>\
|
|
1049
1300
|
│ ├── machine.json
|
|
1050
|
-
│ ├── disk.img
|
|
1301
|
+
│ ├── boot.qcow2 | disk.img
|
|
1051
1302
|
│ ├── monitor (named pipe handle, no on-disk file)
|
|
1052
1303
|
│ └── …
|
|
1053
1304
|
└── …
|
|
@@ -1059,11 +1310,11 @@ is unset.
|
|
|
1059
1310
|
### Credentials (separate from data dir)
|
|
1060
1311
|
|
|
1061
1312
|
- **Per-instance CHR credentials**: file in the user config dir
|
|
1062
|
-
(`~/.config/quickchr/
|
|
1313
|
+
(`~/.config/quickchr/com.quickchr.instance.json`; not the OS keychain — a
|
|
1063
1314
|
Keychain dialog hangs in non-TTY contexts like tests and CI).
|
|
1064
1315
|
- **MikroTik.com web account** (for license operations): tries
|
|
1065
1316
|
`Bun.secrets` (OS keychain) when interactive, falls back to the same
|
|
1066
|
-
config-file store otherwise.
|
|
1317
|
+
config-file store otherwise (`~/.config/quickchr/com.quickchr.mikrotik-web.json`).
|
|
1067
1318
|
|
|
1068
1319
|
Both honor explicit CLI flags / env vars first.
|
|
1069
1320
|
|
|
@@ -1095,8 +1346,10 @@ If an extra mapping reuses a built-in service name such as `winbox`, it
|
|
|
1095
1346
|
pins/replaces that service's host port rather than creating a second
|
|
1096
1347
|
`ports.winbox` entry.
|
|
1097
1348
|
|
|
1098
|
-
If port `9100` is in use, the next instance gets `9110`, etc.
|
|
1099
|
-
|
|
1349
|
+
If port `9100` is in use, the next instance gets `9110`, etc. Concurrent
|
|
1350
|
+
creations get distinct blocks (see **Concurrency** under [Static methods on `QuickCHR`](#static-methods-on-quickchr)).
|
|
1351
|
+
Manual override via `--port-base`, which is never remapped: a base that
|
|
1352
|
+
overlaps another machine's block is a `PORT_CONFLICT`.
|
|
1100
1353
|
|
|
1101
1354
|
---
|
|
1102
1355
|
|
|
@@ -1136,8 +1389,12 @@ Native (HVF/KVM): factor ~1 → 120s base.
|
|
|
1136
1389
|
Cross-arch TCG: factor ~15 → up to 1800s.
|
|
1137
1390
|
Package install adds a 2× multiplier (extra reboot cycle).
|
|
1138
1391
|
|
|
1139
|
-
If boot times out,
|
|
1140
|
-
|
|
1392
|
+
If boot times out, QEMU is stopped. A previously booted machine is **preserved**
|
|
1393
|
+
with its disks, credentials and configuration; retry with `quickchr start <name>`
|
|
1394
|
+
or explicitly remove it. A machine created by this `start()` is removed, and an
|
|
1395
|
+
existing never-booted machine is reset to its factory disk and pre-launch record.
|
|
1396
|
+
`QUICKCHR_PRESERVE_ON_FAILURE=1` skips removal/reset for those first-boot cases.
|
|
1397
|
+
See the [cleanup table](#auto-cleanup-on-boot-timeout) for all three cases. Use `--timeout-extra` to add a buffer if you know your host is slow, or
|
|
1141
1398
|
persist a default via `quickchr settings set timeout-extra <seconds>` so
|
|
1142
1399
|
every `start` picks it up without repeating the flag.
|
|
1143
1400
|
|
|
@@ -1154,7 +1411,8 @@ against:
|
|
|
1154
1411
|
- Accidental credential leakage in commits, CI logs, CLI output.
|
|
1155
1412
|
|
|
1156
1413
|
We **don't** protect against same-user code reading
|
|
1157
|
-
`
|
|
1414
|
+
the `~/.config/quickchr/*.json` credential files, `ps`-visible CLI args, or memory
|
|
1415
|
+
inspection.
|
|
1158
1416
|
|
|
1159
1417
|
### Auth resolution (`resolveAuth`)
|
|
1160
1418
|
|
|
@@ -1186,7 +1444,7 @@ managed via `quickchr settings get|set|print|reset` (§3, Meta). Atomic writes
|
|
|
1186
1444
|
preserve every other line (comments, blank lines, foreign vars) byte-for-byte.
|
|
1187
1445
|
`reset <key>` deletes the line entirely rather than blanking it, since a blank
|
|
1188
1446
|
`KEY=` line is still "set" to a bash-sourced file. Never stores credentials —
|
|
1189
|
-
the
|
|
1447
|
+
the 7 managed keys are all structural preferences, not secrets.
|
|
1190
1448
|
|
|
1191
1449
|
### Environment variables consumed
|
|
1192
1450
|
|
|
@@ -1196,13 +1454,15 @@ the 6 managed keys are all structural preferences, not secrets.
|
|
|
1196
1454
|
| `QUICKCHR_NO_PROMPT` | CLI dispatcher — forces non-interactive mode |
|
|
1197
1455
|
| `QUICKCHR_DEBUG` | progress logger — emits `[debug]` lines |
|
|
1198
1456
|
| `QUICKCHR_INTEGRATION` | `bun:test` gate for `test/integration/` |
|
|
1199
|
-
| `QUICKCHR_PRESERVE_ON_FAILURE` | `quickchr.ts` — skip `remove()` on `BOOT_TIMEOUT
|
|
1457
|
+
| `QUICKCHR_PRESERVE_ON_FAILURE` | `quickchr.ts` — skip `remove()` on `BOOT_TIMEOUT`, and the rollback of a failed provisioning run, so the machine dir survives for diagnosis |
|
|
1200
1458
|
| `QUICKCHR_SERIAL_LOG` | `qemu.ts` — adds `logfile=` to the serial chardev (writes cleartext provisioning passwords) |
|
|
1201
1459
|
| `QUICKCHR_DEEP_BOOT_DIAGNOSTICS` | `guest-snapshot.ts` — allows the guest-mutating counting-rule probe during boot-failure capture |
|
|
1202
1460
|
| `QUICKCHR_DEFAULT_CHANNEL` | `settings.ts` — env tier for the `default-channel` setting |
|
|
1203
1461
|
| `QUICKCHR_DEFAULT_ARCH` | `settings.ts` — env tier for the `default-arch` setting |
|
|
1204
1462
|
| `QUICKCHR_ACCEL` | `settings.ts` / `platform.ts` — env tier for the `accel` setting; forces `-accel` and bypasses detection |
|
|
1205
1463
|
| `QUICKCHR_CACHE_MAX_SIZE` | `settings.ts` — env tier for the `cache-max-size` setting |
|
|
1464
|
+
| `QUICKCHR_STORAGE_RESERVE` | `settings.ts` — env tier for the `storage-reserve` setting |
|
|
1465
|
+
| `QUICKCHR_SHUTDOWN_TIMEOUT` | CLI `shutdown` grace budget in positive integer seconds; default 10, overridden by `--timeout` |
|
|
1206
1466
|
| `QUICKCHR_TIMEOUT_EXTRA` | `settings.ts` — env tier for the `timeout-extra` setting |
|
|
1207
1467
|
| `QUICKCHR_SECURE_LOGIN` | `settings.ts` — env tier for the `secure-login` setting |
|
|
1208
1468
|
| `MIKROTIK_WEB_ACCOUNT` | license-renewal account (fallback) |
|
|
@@ -1251,6 +1511,29 @@ funnel through the same transport selector.
|
|
|
1251
1511
|
> (see [`docs/centrs-interface.md`](./docs/centrs-interface.md)) — it never reads
|
|
1252
1512
|
> `machine.json`, and neither should you.
|
|
1253
1513
|
|
|
1514
|
+
### Exit status
|
|
1515
|
+
|
|
1516
|
+
`quickchr exec` exits 0 when the command was **delivered**, not when RouterOS
|
|
1517
|
+
accepted it. `/rest/execute` returns HTTP 200 with the rejection text in the same
|
|
1518
|
+
envelope as a success, so a refused command looks like this:
|
|
1519
|
+
|
|
1520
|
+
```console
|
|
1521
|
+
$ quickchr exec my-chr "/user/add name=-2 group=read password=x"; echo "rc=$?"
|
|
1522
|
+
failure: invalid user name (/user/add; line 1)
|
|
1523
|
+
rc=0
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
The library is the same: `chr.exec()` resolves, and `ExecResult` has no failure
|
|
1527
|
+
field. quickchr does not pattern-match RouterOS error text, whose wording changes
|
|
1528
|
+
between versions (#142). When a script needs a pass/fail exit code, run the
|
|
1529
|
+
command through centrs, which validates it first and exits non-zero on a rejection.
|
|
1530
|
+
A write command needs `--yes` outside a TTY; without it centrs refuses with
|
|
1531
|
+
`usage/confirmation-required` (also exit 1) and sends nothing:
|
|
1532
|
+
|
|
1533
|
+
```bash
|
|
1534
|
+
centrs execute --quickchr my-chr --yes "/user/add name=-2 group=read password=x"
|
|
1535
|
+
```
|
|
1536
|
+
|
|
1254
1537
|
### Transports
|
|
1255
1538
|
|
|
1256
1539
|
| `--via` | Source | Notes |
|
|
@@ -1366,7 +1649,13 @@ stored instance credentials, `state.user`, the managed SSH keypair under
|
|
|
1366
1649
|
`<machineDir>/ssh/`, and `disableAdmin` — because the accounts they named
|
|
1367
1650
|
went with the disk. The machine comes back as a factory CHR: `admin` with
|
|
1368
1651
|
an empty password, which is what `rest()`, `exec()`, and `inspect` resolve
|
|
1369
|
-
to afterwards.
|
|
1652
|
+
to afterwards.
|
|
1653
|
+
|
|
1654
|
+
`clean()` performs no provisioning of its own, but it does reopen the provisioning
|
|
1655
|
+
window: the next `start` re-applies the retained intent (`packages`, `deviceMode`,
|
|
1656
|
+
`secureLogin`) as a first boot would. The credential facts above are cleared rather
|
|
1657
|
+
than retained, so an explicit `--add-user` / `--disable-admin` — and anything from a
|
|
1658
|
+
request that was refused, which is never persisted — has to be passed again.
|
|
1370
1659
|
|
|
1371
1660
|
### Snapshots
|
|
1372
1661
|
|
|
@@ -1410,13 +1699,13 @@ present, then exits `1`.
|
|
|
1410
1699
|
| `MISSING_FIRMWARE` | edk2 UEFI firmware not installed (arm64) | install `qemu-efi-aarch64` / `edk2-aarch64` |
|
|
1411
1700
|
| `MISSING_UNZIP` | image extraction needs `unzip` | install OS package |
|
|
1412
1701
|
| `PORT_CONFLICT` | no free 10-port block | stop a machine or set `--port-base` |
|
|
1413
|
-
| `BOOT_TIMEOUT` | CHR didn't respond within timeout | check `qemu.log`; under TCG, raise `--timeout-extra`;
|
|
1702
|
+
| `BOOT_TIMEOUT` | CHR didn't respond within timeout | check `qemu.log`; under TCG, raise `--timeout-extra`; previously booted machines are **preserved**; new machines are removed, existing first boots reset, unless `QUICKCHR_PRESERVE_ON_FAILURE=1` |
|
|
1414
1703
|
| `DOWNLOAD_FAILED` | image fetch failed | retry; check connectivity to MikroTik download server |
|
|
1415
1704
|
| `INVALID_VERSION` / `INVALID_ARCH` / `INVALID_NAME` / `INVALID_DISK_SIZE` | argument parse | fix the value |
|
|
1416
1705
|
| `MACHINE_EXISTS` | name already in `machines/` | choose another name or `QuickCHR.get(name)` |
|
|
1417
|
-
| `MACHINE_NOT_FOUND` | name not in `machines
|
|
1706
|
+
| `MACHINE_NOT_FOUND` | name not in `machines/`, or a booted machine's disk file is missing | `quickchr list`; for a missing disk, restore it or `quickchr clean <name>` |
|
|
1418
1707
|
| `MACHINE_RUNNING` / `MACHINE_STOPPED` | wrong lifecycle state | `start` or `stop` first |
|
|
1419
|
-
| `MACHINE_LOCKED` | another
|
|
1708
|
+
| `MACHINE_LOCKED` | another operation holds the machine (the message names it, its pid and age) | wait for it; a `foreground` holder must be quit from its console |
|
|
1420
1709
|
| `EXEC_FAILED` | RouterOS rejected the command (auth, syntax) | check creds, command syntax |
|
|
1421
1710
|
| `PROCESS_FAILED` | QEMU spawn / reboot / verify failed | check `qemu.log`, host resources |
|
|
1422
1711
|
| `SPAWN_FAILED` | exec/spawn of host binary failed | check binary exists + executable |
|
|
@@ -1424,8 +1713,10 @@ present, then exits `1`.
|
|
|
1424
1713
|
| `QGA_TIMEOUT` | QGA daemon unreachable | KVM-only; not on macOS HVF or TCG |
|
|
1425
1714
|
| `NETWORK_UNAVAILABLE` | provisioning needs `user` NIC, none present | add `--add-network user` |
|
|
1426
1715
|
| `INVALID_NETWORK` | unknown alias / unresolvable iface / dead socket daemon | `quickchr networks` |
|
|
1427
|
-
| `PROVISIONING_VERSION_UNSUPPORTED` | provisioning option on RouterOS < 7.20.8 | use `--channel long-term` or pin a `7.20.8+` version |
|
|
1428
|
-
| `
|
|
1716
|
+
| `PROVISIONING_VERSION_UNSUPPORTED` | provisioning option on RouterOS < 7.20.8 (including any 6.x) | use `--channel long-term` or pin a `7.20.8+` version |
|
|
1717
|
+
| `REST_UNAVAILABLE` | REST operation (`exec`, `get`, `rest()`) on a RouterOS 6 machine | use the native API — `centrs … --via native-api --no-validate` |
|
|
1718
|
+
| `INVALID_ARGUMENT` | a flag combination quickchr will not honour — e.g. `--no-wait` with provisioning | drop one of the two |
|
|
1719
|
+
| `INSUFFICIENT_DISK_SPACE` | data dir's filesystem has less than the operation's estimate plus `storage-reserve`, even after evicting what the cache could spare | free space, remove machines, lower `storage-reserve`, or move via `QUICKCHR_DATA_DIR` |
|
|
1429
1720
|
| `STATE_ERROR` | snapshot on raw boot disk | recreate with `--boot-disk-format=qcow2` |
|
|
1430
1721
|
| `INVALID_SIZE_STRING` | invalid cache/settings size string | use size syntax like `512M`, `2G`, or `1.5G` |
|
|
1431
1722
|
| `INVALID_SETTING_KEY` | `settings get`/`set`/`reset` received an unrecognized key | `quickchr settings print` to see the managed keys |
|
|
@@ -1433,10 +1724,44 @@ present, then exits `1`.
|
|
|
1433
1724
|
|
|
1434
1725
|
### Auto-cleanup on boot timeout
|
|
1435
1726
|
|
|
1436
|
-
If `start()` hits `BOOT_TIMEOUT`, quickchr
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1727
|
+
If `start()` hits `BOOT_TIMEOUT`, quickchr stops the failing instance and then:
|
|
1728
|
+
|
|
1729
|
+
| Machine at launch entry | Timeout cleanup | Recovery |
|
|
1730
|
+
| --- | --- | --- |
|
|
1731
|
+
| Created by this `start()` call | Remove automatically | Repeat the original command |
|
|
1732
|
+
| Existing, never booted, provisioning window open | Reset factory disk and restore the pre-launch record, window open | `quickchr start <name>` |
|
|
1733
|
+
| Existing, provisioning window already closed | Preserve disks, credentials and persistent configuration; clear PID and release socket memberships | `quickchr start <name>`, or explicit `quickchr remove <name>` |
|
|
1734
|
+
|
|
1735
|
+
`QUICKCHR_PRESERVE_ON_FAILURE=1` skips removal/reset for the first two cases and
|
|
1736
|
+
keeps the machine as it failed. Previously booted machines survive without the
|
|
1737
|
+
flag, including older records whose only boot-history marker is `lastStartedAt`.
|
|
1738
|
+
The diagnostic report is written outside the machine directory before cleanup;
|
|
1739
|
+
QEMU logs in a preserved machine also remain available. A configured guest does
|
|
1740
|
+
not need `clean` to retry: that command deliberately erases its configuration.
|
|
1741
|
+
|
|
1742
|
+
### RouterOS 6 (boot-only)
|
|
1743
|
+
|
|
1744
|
+
A best-effort target for v6→v7 migration testing (tikoci/centrs#386), CLI only —
|
|
1745
|
+
the setup wizard does not offer it.
|
|
1746
|
+
|
|
1747
|
+
```sh
|
|
1748
|
+
quickchr add --name lab6 --version 6.49.23 --arch x86
|
|
1749
|
+
quickchr start lab6
|
|
1750
|
+
centrs retrieve --quickchr lab6 /system/resource --via native-api --no-validate
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
- **x86 only.** MikroTik publishes no arm64 CHR for 6.x; `--arch arm64` is refused
|
|
1754
|
+
before download (`INVALID_ARCH`).
|
|
1755
|
+
- **Readiness is a native-API `/login`** on guest port 8728 (two consecutive
|
|
1756
|
+
answers), since 6.x has no REST. A 6.49.23 guest is ready in ~25 s under HVF.
|
|
1757
|
+
- **No provisioning.** Every provisioning flag, and the post-boot `set` routes, are
|
|
1758
|
+
refused before download where possible (`PROVISIONING_VERSION_UNSUPPORTED`). The
|
|
1759
|
+
`secure-login` setting is a default, not a request, so it does not apply to 6.x.
|
|
1760
|
+
- **No REST.** `exec` (REST/auto transport), `get`'s live groups and
|
|
1761
|
+
`ChrInstance.rest()` fail with `REST_UNAVAILABLE` rather than time out.
|
|
1762
|
+
`inspect` reports `rest-api` unavailable and `native-api` available.
|
|
1763
|
+
- `list`, `inspect`, `stop`, `remove` and `start --no-wait` work as for 7.x
|
|
1764
|
+
(checked live on 6.49.23).
|
|
1440
1765
|
|
|
1441
1766
|
### Where to look
|
|
1442
1767
|
|