@tikoci/quickchr 0.4.2 → 0.4.4
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 +161 -37
- package/MANUAL.md +1271 -0
- package/README.md +54 -10
- package/package.json +7 -4
- package/src/cli/index.ts +211 -48
- package/src/cli/wizard.ts +42 -12
- package/src/index.ts +7 -3
- package/src/lib/cache.ts +19 -1
- package/src/lib/channels.ts +19 -4
- package/src/lib/completions.ts +6 -3
- package/src/lib/disk.ts +5 -2
- package/src/lib/forward-spec.ts +140 -3
- package/src/lib/paths.ts +13 -0
- package/src/lib/provision.ts +279 -59
- package/src/lib/qemu.ts +52 -22
- package/src/lib/quickchr.ts +237 -48
- package/src/lib/secrets.ts +3 -7
- package/src/lib/settings.ts +373 -0
- package/src/lib/state.ts +42 -1
- package/src/lib/types.ts +237 -30
package/MANUAL.md
ADDED
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
# quickchr Manual
|
|
2
|
+
|
|
3
|
+
> Audit-style reference for what quickchr actually does today. README is the
|
|
4
|
+
> elevator pitch and quickstart; DESIGN.md is the architecture rationale;
|
|
5
|
+
> this MANUAL is the complete, source-checked map of every CLI command,
|
|
6
|
+
> library API, and provisioning step.
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [What quickchr is](#1-what-quickchr-is)
|
|
11
|
+
2. [Install & doctor](#2-install--doctor)
|
|
12
|
+
3. [CLI command reference](#3-cli-command-reference)
|
|
13
|
+
4. [Library API reference](#4-library-api-reference)
|
|
14
|
+
5. [Provisioning pipeline](#5-provisioning-pipeline)
|
|
15
|
+
6. [Channels — REST, monitor, serial, QGA, console](#6-channels--rest-monitor-serial-qga-console)
|
|
16
|
+
7. [Networking](#7-networking)
|
|
17
|
+
8. [Storage layout](#8-storage-layout)
|
|
18
|
+
9. [Port layout](#9-port-layout)
|
|
19
|
+
10. [Acceleration & timeouts](#10-acceleration--timeouts)
|
|
20
|
+
11. [Auth, secrets, environment variables](#11-auth-secrets-environment-variables)
|
|
21
|
+
12. [Exec — running RouterOS commands](#12-exec--running-routeros-commands)
|
|
22
|
+
13. [Disks & snapshots](#13-disks--snapshots)
|
|
23
|
+
14. [Errors & recovery](#14-errors--recovery)
|
|
24
|
+
15. [Cross-references](#15-cross-references)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. What quickchr is
|
|
29
|
+
|
|
30
|
+
A **CLI plus importable library** for managing MikroTik CHR (Cloud Hosted
|
|
31
|
+
Router) virtual machines on macOS, Linux, and (experimentally) Windows.
|
|
32
|
+
quickchr wraps QEMU directly — it is **not** an orchestrator. Each
|
|
33
|
+
invocation manages one CHR instance; multi-router topologies are composed
|
|
34
|
+
by the user (or their AI agent) using `quickchr` as a building block.
|
|
35
|
+
|
|
36
|
+
**Scope boundary** (see DESIGN.md §Scope Boundary):
|
|
37
|
+
|
|
38
|
+
- ✅ Download CHR images, manage QEMU lifecycle, expose REST/SSH/WinBox
|
|
39
|
+
ports, persist machine state, run RouterOS CLI commands, install
|
|
40
|
+
packages, apply trial licenses, switch device-mode.
|
|
41
|
+
- ❌ Multi-router topology orchestration, test-matrix runners,
|
|
42
|
+
configuration-as-code DSLs. Use `examples/` as references and compose
|
|
43
|
+
yourself.
|
|
44
|
+
|
|
45
|
+
**Two ways to use it:**
|
|
46
|
+
|
|
47
|
+
- `quickchr <command>` — the CLI. Git-style subcommands plus an interactive
|
|
48
|
+
setup wizard (`quickchr setup`).
|
|
49
|
+
- `import { QuickCHR } from "@tikoci/quickchr"` — the library. Returns
|
|
50
|
+
`ChrInstance` handles for programmatic control.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. Install & doctor
|
|
55
|
+
|
|
56
|
+
### Prerequisites
|
|
57
|
+
|
|
58
|
+
| Component | Required for | Install |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| Bun ≥ 1.1 | runtime | macOS/Linux: `curl -fsSL https://bun.sh/install \| bash` · Windows: `powershell -c "irm bun.sh/install.ps1 \| iex"` (or `winget install Oven-sh.Bun`) |
|
|
61
|
+
| `qemu-system-x86_64` | x86 CHR | brew/apt/dnf/pacman · Windows: `winget install SoftwareFreedomConservancy.QEMU` |
|
|
62
|
+
| `qemu-system-aarch64` | arm64 CHR | brew/apt/dnf/pacman · Windows: bundled in the SFC QEMU package above |
|
|
63
|
+
| `qemu-img` | `--boot-size`, `--add-disk`, snapshots | included in all standard QEMU packages (brew, apt, dnf, SFC installer) |
|
|
64
|
+
| UEFI firmware (edk2) | arm64 CHR | `qemu-efi-aarch64` (apt), `edk2-aarch64` (dnf), bundled with `qemu` (brew) |
|
|
65
|
+
| `socket_vmnet` (optional) | rootless `shared`/`bridged` networking on macOS | `brew install socket_vmnet` |
|
|
66
|
+
| `socat` (optional) | piping serial console externally | brew/apt |
|
|
67
|
+
| `sshpass` (optional) | future SSH exec transport | brew/apt |
|
|
68
|
+
|
|
69
|
+
### Install quickchr
|
|
70
|
+
|
|
71
|
+
The first GitHub release is **0.1.1** — pre-release per the changelog's
|
|
72
|
+
odd/even policy. Not yet on npm.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Library
|
|
76
|
+
bun add github:tikoci/quickchr
|
|
77
|
+
|
|
78
|
+
# CLI from source (until npm publish)
|
|
79
|
+
git clone https://github.com/tikoci/quickchr
|
|
80
|
+
cd quickchr
|
|
81
|
+
bun install
|
|
82
|
+
bun run dev -- doctor
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Once `0.2.0` is published to npm:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bun install -g @tikoci/quickchr
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `quickchr doctor`
|
|
92
|
+
|
|
93
|
+
Reports prerequisite status, accelerator availability, image cache
|
|
94
|
+
size, machine count, free disk space, and shell-completion install
|
|
95
|
+
state. Exit code is `0` when no checks return `error` (warnings are OK).
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
quickchr doctor # human-readable
|
|
99
|
+
quickchr doctor --json # { ok, checks, staleImages } for CI
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `--json` form emits `{ ok, checks, staleImages }`, where `checks` is an
|
|
103
|
+
array of `{ label, status, detail }` and `staleImages` lists cached images older
|
|
104
|
+
than the current long-term release. The exit code still reflects `ok`.
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
108
|
+
Use this whenever a `start` fails — the diagnostic almost always points
|
|
109
|
+
at a missing dependency, missing firmware, or insufficient disk.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 3. CLI command reference
|
|
114
|
+
|
|
115
|
+
The dispatcher lives in `src/cli/index.ts` (lines 121–194). Help text is
|
|
116
|
+
available for every command via `quickchr help <command>`.
|
|
117
|
+
|
|
118
|
+
### Lifecycle
|
|
119
|
+
|
|
120
|
+
#### `add [name]`
|
|
121
|
+
|
|
122
|
+
Create a machine without starting it. Materializes the machine
|
|
123
|
+
directory, downloads the RouterOS image (cached), and writes
|
|
124
|
+
`machine.json` with all provisioning options stored for later.
|
|
125
|
+
|
|
126
|
+
| Flag | Default | Notes |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `--name <s>` | auto | `host-arch-N` if omitted |
|
|
129
|
+
| `--version <ver>` | latest in channel | e.g., `7.22.1` |
|
|
130
|
+
| `--channel <ch>` | `stable` | `stable`, `long-term`, `testing`, `development` |
|
|
131
|
+
| `--arch <a>` | host native | `arm64`, `x86`, or `auto` |
|
|
132
|
+
| `--cpu <n>` | `1` | vCPU count |
|
|
133
|
+
| `--mem <mb>` | `512` | RAM (cross-arch TCG bumps to `1024`) |
|
|
134
|
+
| `--boot-disk-format <f>` | `qcow2` | `qcow2` or `raw` |
|
|
135
|
+
| `--boot-size <size>` | — | Resize boot disk; needs `qemu-img`; auto-converts to qcow2 |
|
|
136
|
+
| `--add-disk <size>` | — | Extra blank qcow2 disk; repeatable |
|
|
137
|
+
| `--forward <spec>` | — | Extra hostfwd port or built-in service host-port pin; repeatable. See §Custom port forwards. |
|
|
138
|
+
| `--add-package <pkg>` | — | Provisioning, repeatable, ≥ 7.20.8 |
|
|
139
|
+
| `--install-all-packages` | false | Provisioning, ≥ 7.20.8 |
|
|
140
|
+
| `--add-user <user:pass>` | — | Provisioning, ≥ 7.20.8 |
|
|
141
|
+
| `--disable-admin` | false | Provisioning, ≥ 7.20.8 |
|
|
142
|
+
| `--secure-login` / `--no-secure-login` | unset | Provisioning, ≥ 7.20.8 |
|
|
143
|
+
| `--license-level <l>` | — | `p1`, `p10`, `unlimited`, ≥ 7.20.8 |
|
|
144
|
+
| `--license-account <a>` | env `MIKROTIK_WEB_ACCOUNT` | MikroTik.com email |
|
|
145
|
+
| `--license-password <p>` | env `MIKROTIK_WEB_PASSWORD` | |
|
|
146
|
+
| `--device-mode <m>` | — | `rose`, `advanced`, `basic`, `home`, `auto`, `skip`, ≥ 7.20.8 |
|
|
147
|
+
| `--device-mode-enable <f,...>` | — | Repeatable, comma-separated |
|
|
148
|
+
| `--device-mode-disable <f,...>` | — | Repeatable, comma-separated |
|
|
149
|
+
| `--add-network <spec>` | `user` | See §7. Repeatable. |
|
|
150
|
+
| `--no-network` | false | Headless, no NICs |
|
|
151
|
+
| `--no-winbox` | false | Exclude WinBox port |
|
|
152
|
+
| `--no-api-ssl` | false | Exclude API-SSL port |
|
|
153
|
+
| `--port-base <n>` | auto from 9100 | First port of the 10-port block |
|
|
154
|
+
|
|
155
|
+
`--vmnet-shared` and `--vmnet-bridge <iface>` are deprecated aliases for
|
|
156
|
+
`--add-network shared` / `--add-network bridged:<iface>`.
|
|
157
|
+
|
|
158
|
+
#### `start [name]`
|
|
159
|
+
|
|
160
|
+
Boot a stopped machine, or create-and-boot in one command. Blocks until
|
|
161
|
+
REST is ready (background mode, default), or attaches QEMU stdio to your
|
|
162
|
+
terminal (`--fg`). Accepts the same provisioning flags as `add`.
|
|
163
|
+
|
|
164
|
+
Additional flags:
|
|
165
|
+
|
|
166
|
+
| Flag | Notes |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `--all` | Start every stopped machine in background |
|
|
169
|
+
| `--bg` / `--background` | (default) detach QEMU; return after boot |
|
|
170
|
+
| `--fg` / `--foreground` (alias `--no-bg`, `--no-background`) | Serial console on stdio |
|
|
171
|
+
| `--install-deps` | Auto-install missing host deps (calls package manager) |
|
|
172
|
+
| `--timeout-extra <s>` / `-T <s>` | Add extra seconds to computed boot timeout |
|
|
173
|
+
| `--dry-run` | Print resolved options and QEMU command, exit |
|
|
174
|
+
|
|
175
|
+
Foreground mode shortcuts (QEMU mux):
|
|
176
|
+
|
|
177
|
+
| Key | Action |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `Ctrl-A X` | Quit QEMU |
|
|
180
|
+
| `Ctrl-A C` | Toggle monitor (`quit` to force-stop from monitor) |
|
|
181
|
+
| `Ctrl-A H` | Help |
|
|
182
|
+
|
|
183
|
+
When `--fg` is combined with provisioning options, quickchr boots
|
|
184
|
+
**background** first, runs provisioning, then **stops** and re-launches
|
|
185
|
+
foreground with stdio attached. This is observable as a brief stop in
|
|
186
|
+
machine state.
|
|
187
|
+
|
|
188
|
+
#### `stop [name | --all]`
|
|
189
|
+
|
|
190
|
+
Graceful shutdown via QEMU monitor. Updates `state.status = "stopped"`
|
|
191
|
+
and unregisters socket-named networks. Listing-mode if no name and not
|
|
192
|
+
`--all`.
|
|
193
|
+
|
|
194
|
+
#### `remove [name | --all]` (alias `rm`)
|
|
195
|
+
|
|
196
|
+
Stops if running, deletes the machine directory, and clears per-instance
|
|
197
|
+
credentials from the secret store.
|
|
198
|
+
|
|
199
|
+
#### `clean <name>`
|
|
200
|
+
|
|
201
|
+
Resets the machine to a fresh image: stops QEMU, re-copies the boot
|
|
202
|
+
image from cache, recreates extra disks per the saved config, deletes
|
|
203
|
+
EFI vars (UEFI re-initializes), and clears per-instance credentials.
|
|
204
|
+
Provisioning options remain in `machine.json` and re-run on next `start`.
|
|
205
|
+
|
|
206
|
+
### Inspection
|
|
207
|
+
|
|
208
|
+
#### `list [name]` (alias `ls`, `status`)
|
|
209
|
+
|
|
210
|
+
Without a name: tabular summary of every machine. With a name: detailed
|
|
211
|
+
view including credentials and connection tips. `--json` for
|
|
212
|
+
machine-readable output.
|
|
213
|
+
|
|
214
|
+
#### `get <name> [license | device-mode | admin] [--json]`
|
|
215
|
+
|
|
216
|
+
Live REST query. Hits the running CHR and returns the requested group
|
|
217
|
+
(or all if no group specified). Requires the machine to be running.
|
|
218
|
+
|
|
219
|
+
#### `inspect <name> [--json]`
|
|
220
|
+
|
|
221
|
+
Stable, versioned (`descriptorVersion: 1`) connection descriptor for a
|
|
222
|
+
**running** CHR — the quickchr ↔ centrs interface contract (issue #71,
|
|
223
|
+
[`docs/centrs-interface.md`](docs/centrs-interface.md)). Machine identity
|
|
224
|
+
(name, version, arch, cpu, mem, pid, machineDir, timestamps) plus a
|
|
225
|
+
per-service `services` map (`rest-api`, `native-api`, `ssh`), each entry
|
|
226
|
+
giving its host, port, transport, `tls` flag, availability, and auth; plus
|
|
227
|
+
optional `customForwards` (e.g. `winbox`) and topology-only `networks`.
|
|
228
|
+
`--json` is accepted for symmetry with other read commands; output is
|
|
229
|
+
always JSON. Does not include subprocess env vars — use `quickchr env` for
|
|
230
|
+
those.
|
|
231
|
+
|
|
232
|
+
Stopped machines fail with `MACHINE_STOPPED` instead of returning stale
|
|
233
|
+
connection details. Use `quickchr start <name>` first.
|
|
234
|
+
|
|
235
|
+
#### `env <name> [--json]`
|
|
236
|
+
|
|
237
|
+
Print the subprocess environment for a **running** CHR. By default it emits
|
|
238
|
+
shell-ready `KEY=value` lines; with `--json` it emits just the env map as
|
|
239
|
+
JSON. This is CLI parity with `ChrInstance.subprocessEnv()`.
|
|
240
|
+
|
|
241
|
+
The env map includes credentials (`QUICKCHR_AUTH`, `BASICAUTH`) so subprocesses
|
|
242
|
+
can connect without reading quickchr's secret store. Treat `quickchr env` and
|
|
243
|
+
`quickchr inspect` output like password material: avoid commits, public issue
|
|
244
|
+
comments, and unredacted CI logs.
|
|
245
|
+
|
|
246
|
+
#### `disk [name]`
|
|
247
|
+
|
|
248
|
+
Shows stored disk layout. With `qemu-img` installed, also reports
|
|
249
|
+
virtual + actual sizes for each disk.
|
|
250
|
+
|
|
251
|
+
#### `logs <name> [--follow] [-n N]`
|
|
252
|
+
|
|
253
|
+
Tail the per-machine `qemu.log`. `--follow` streams new output;
|
|
254
|
+
`-n` limits to last N lines.
|
|
255
|
+
|
|
256
|
+
#### `networks` (alias `net`)
|
|
257
|
+
|
|
258
|
+
`networks` enumerates host interfaces and their alias resolution
|
|
259
|
+
(`wifi`, `ethernet`, `auto`). `networks sockets` lists registered
|
|
260
|
+
named L2 sockets; `networks sockets create <name>` reserves a port for a
|
|
261
|
+
new tunnel.
|
|
262
|
+
|
|
263
|
+
#### `version [--json]`
|
|
264
|
+
|
|
265
|
+
Print quickchr version (matches `package.json`) plus the latest RouterOS
|
|
266
|
+
version per channel. `--json` emits just the channel map for CI consumers:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
quickchr version --json
|
|
270
|
+
# {"stable":"7.23.1","long-term":"7.21.4","testing":"7.23rc4","development":"7.24beta2"}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Offline, `--json` emits `{}`. For the "which channels are worth booting right
|
|
274
|
+
now" smarts (recency-aware active channels), import `resolveActiveChannels` /
|
|
275
|
+
`resolveChannelStatuses` from the library — see *Version & channel helpers* under
|
|
276
|
+
the Library API reference.
|
|
277
|
+
|
|
278
|
+
### Interaction
|
|
279
|
+
|
|
280
|
+
#### `exec <name> <command> [--via=auto|rest|qga|console|ssh] [--user u] [--password p] [--timeout=ms]`
|
|
281
|
+
|
|
282
|
+
Run a RouterOS CLI command. See §12 for transport details and JSON
|
|
283
|
+
output trick.
|
|
284
|
+
|
|
285
|
+
#### `console <name>`
|
|
286
|
+
|
|
287
|
+
Attach to the QEMU serial socket. Exit with `Ctrl-A X` (or `Ctrl-]` if
|
|
288
|
+
the underlying transport is socat). Requires the machine to be running.
|
|
289
|
+
|
|
290
|
+
#### `qga <name> <subcommand> [args...]`
|
|
291
|
+
|
|
292
|
+
Direct QEMU Guest Agent commands (`ping`, `info`, `osinfo`, `hostname`,
|
|
293
|
+
`time`, `timezone`, `network-interfaces`, `fsfreeze-status`, `exec`,
|
|
294
|
+
`file-read`, `file-write`, `shutdown`).
|
|
295
|
+
|
|
296
|
+
QGA is **x86 only and KVM only**. ARM64 CHR doesn't ship the agent;
|
|
297
|
+
macOS HVF and any TCG fallback don't initialize the virtio channel
|
|
298
|
+
(see `docs/qga-x86-macos-qemu10-investigation.md` for the QEMU bug).
|
|
299
|
+
|
|
300
|
+
### Configuration
|
|
301
|
+
|
|
302
|
+
#### `license <name> [--level p1|p10|unlimited]`
|
|
303
|
+
|
|
304
|
+
Apply or renew a CHR trial license on a running instance. Resolves
|
|
305
|
+
MikroTik.com credentials from `--license-account`/`--license-password`,
|
|
306
|
+
the env vars, or the secret store. Requires RouterOS ≥ 7.20.8.
|
|
307
|
+
|
|
308
|
+
#### `set <name> <subcommand>`
|
|
309
|
+
|
|
310
|
+
Currently exposes a small surface for in-place changes (e.g.,
|
|
311
|
+
`set <name> license …`). The `set/get` architecture is still being
|
|
312
|
+
designed; expect additions.
|
|
313
|
+
|
|
314
|
+
#### `snapshot <name> <list | save [snapname] | load <snapname> | delete <snapname>>` (alias `snap`)
|
|
315
|
+
|
|
316
|
+
Internal qcow2 snapshots. `save` requires the machine running (`savevm`
|
|
317
|
+
goes through QEMU monitor). `list` and `delete` work in either state
|
|
318
|
+
(uses `qemu-img info` when stopped). Requires `bootDiskFormat: qcow2`.
|
|
319
|
+
|
|
320
|
+
### Meta
|
|
321
|
+
|
|
322
|
+
#### `setup`
|
|
323
|
+
|
|
324
|
+
Interactive wizard (`@clack/prompts`) that walks through all machine creation
|
|
325
|
+
options and starts the CHR. Triggered by `quickchr setup` or automatically
|
|
326
|
+
when `quickchr` is run with no subcommand on a TTY.
|
|
327
|
+
|
|
328
|
+

|
|
329
|
+
|
|
330
|
+
**First-run detection**: if no machines exist yet, the wizard opens with a
|
|
331
|
+
welcome message before the first prompt.
|
|
332
|
+
|
|
333
|
+
**Setup menu loop**: when running via `quickchr setup` (not the bare `quickchr`
|
|
334
|
+
shortcut), a main menu appears after each machine starts, offering to create
|
|
335
|
+
another machine, view status, or quit.
|
|
336
|
+
|
|
337
|
+
**Wizard steps** (all traced from `src/cli/wizard.ts`):
|
|
338
|
+
|
|
339
|
+
| Step | Prompt | Default | Corresponding flag |
|
|
340
|
+
|---|---|---|---|
|
|
341
|
+
| 1 | Version source: channel or specific | channel | `--channel` / `--version` |
|
|
342
|
+
| 2 | Channel (if channel chosen) | `stable` (or the `default-channel` setting) | `--channel` |
|
|
343
|
+
| 3 | Architecture | host native (or the `default-arch` setting) | `--arch` |
|
|
344
|
+
| 4 | Instance name | auto-generated | `--name` |
|
|
345
|
+
| 5 | CPU cores | 1 | `--cpu` |
|
|
346
|
+
| 6 | Memory | 256 MB | `--mem` |
|
|
347
|
+
| 7 | Boot disk format (qcow2 / raw) | qcow2 | `--boot-disk-format` |
|
|
348
|
+
| 8 | Boot disk resize (optional, qcow2 only) | no resize | `--boot-size` |
|
|
349
|
+
| 9 | Extra blank disks (repeatable) | none | `--add-disk` |
|
|
350
|
+
| 10 | Additional networks (macOS/Linux) | user-mode only | `--add-network` |
|
|
351
|
+
| 11 | Provisioning (packages, device-mode, user, license) | optional | various |
|
|
352
|
+
| 12 | Run mode: background / foreground | background | `--bg` / `--fg` |
|
|
353
|
+
| 13 | Final confirmation | — | — |
|
|
354
|
+
|
|
355
|
+
Steps 7–9 are skipped when `qemu-img` is absent (a warning is shown). Step 10
|
|
356
|
+
only appears when the platform supports networks beyond user-mode (macOS or
|
|
357
|
+
Linux). The provisioning gate (step 11) only appears for RouterOS ≥ 7.20.8;
|
|
358
|
+
older versions show an upgrade prompt.
|
|
359
|
+
|
|
360
|
+
**Provisioning sub-steps** (within step 11):
|
|
361
|
+
|
|
362
|
+
| Sub-step | Options |
|
|
363
|
+
|---|---|
|
|
364
|
+
| Packages | No extra / All packages / Choose (multiselect per arch) |
|
|
365
|
+
| Device-mode | no / yes → profile (rose/advanced/basic/home) + extra feature flags |
|
|
366
|
+
| Login | quickchr managed (recommended) / custom user+pass / keep admin no-password |
|
|
367
|
+
| License | no / yes → level (p1/p10/unlimited) + credentials (stored or entered) |
|
|
368
|
+
|
|
369
|
+
**Post-start**: after a successful background boot, the wizard shows port
|
|
370
|
+
details (REST URL, SSH, WinBox), login credentials, and optionally installs
|
|
371
|
+
shell completions if not yet configured.
|
|
372
|
+
|
|
373
|
+
**Notes**:
|
|
374
|
+
|
|
375
|
+
- "Managed login" sets both `disableAdmin: true` and `secureLogin: true` —
|
|
376
|
+
the provisioning step only auto-creates the `quickchr` managed account when
|
|
377
|
+
`secureLogin` is explicitly `true`, so both fields must be set together or
|
|
378
|
+
admin would be disabled with no replacement login provisioned.
|
|
379
|
+
- Set `QUICKCHR_NO_PROMPT=1` to suppress all wizard flows (useful in scripts
|
|
380
|
+
and CI pipelines).
|
|
381
|
+
- `Ctrl-C` at any prompt cleanly cancels with no partial machine left behind.
|
|
382
|
+
|
|
383
|
+
#### `completions [bash|zsh|fish]`
|
|
384
|
+
|
|
385
|
+
Print or install shell completions. With no arg, detects current shell.
|
|
386
|
+
|
|
387
|
+
#### `settings [print|get|set|reset] [key] [value] [--json]`
|
|
388
|
+
|
|
389
|
+
Manage quickchr's own global preferences — repeated flags/defaults that
|
|
390
|
+
previously had no persisted home. Stored in `~/.config/quickchr/quickchr.env`
|
|
391
|
+
(dotenv-style: `QUICKCHR_KEY=value` lines). See §8 (Storage layout) and §11
|
|
392
|
+
(Environment variables) for the file format and full env var list.
|
|
393
|
+
|
|
394
|
+
Precedence per key (highest wins): CLI flag > `QUICKCHR_<KEY>` env var >
|
|
395
|
+
`quickchr.env` > built-in default.
|
|
396
|
+
|
|
397
|
+
| Key | Consumed by | Type | Built-in default |
|
|
398
|
+
|---|---|---|---|
|
|
399
|
+
| `default-channel` | `add`/`start`'s `--channel`, wizard's channel prompt | `stable`\|`long-term`\|`testing`\|`development` | `stable` |
|
|
400
|
+
| `default-arch` | `add`/`start`'s `--arch`, wizard's arch prompt | `arm64`\|`x86`\|`auto` | `auto` (host native) |
|
|
401
|
+
| `cache-max-size` | Auto-prune cap applied after each successful `start` | size string (e.g. `2G`, `512M`) | `2G` |
|
|
402
|
+
| `timeout-extra` | `start`'s `--timeout-extra`/`-T` when omitted | non-negative integer seconds | `0` |
|
|
403
|
+
| `secure-login` | `add`/`start`'s `--secure-login` when neither `--secure-login` nor `--no-secure-login` is passed | boolean | `unset` (reported as `(unset)` — see note) |
|
|
404
|
+
|
|
405
|
+
`secure-login` is the one key `settings print`/`get` report as `(unset)` rather than a
|
|
406
|
+
concrete `false`, unlike the other four: the setup wizard's login prompt needs to tell
|
|
407
|
+
"not configured" apart from "explicitly set to false" (it only pre-highlights "Keep
|
|
408
|
+
admin with no password" when the setting is concretely `false`; leaving it unset keeps
|
|
409
|
+
the wizard's own recommended "managed login" highlighted). Giving it a real
|
|
410
|
+
`builtinDefault` would collapse that distinction. Every *consumer* still treats
|
|
411
|
+
"unset" and "false" identically — `add`/`start` never force `secureLogin: true` unless
|
|
412
|
+
the setting resolves to exactly `true`.
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
quickchr settings print # all 5 keys, resolved value + source
|
|
416
|
+
quickchr settings set default-channel testing # write one key to quickchr.env
|
|
417
|
+
quickchr settings get default-arch # print one key's resolved value
|
|
418
|
+
quickchr settings reset default-channel # delete one managed line
|
|
419
|
+
quickchr settings reset # clear all managed lines
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Credentials (`--add-user`, `MIKROTIK_WEB_ACCOUNT`/`MIKROTIK_WEB_PASSWORD`) are
|
|
423
|
+
never stored here — `settings` only manages the 5 keys above, and does not
|
|
424
|
+
mutate any machine's `machine.json`.
|
|
425
|
+
|
|
426
|
+
### Global behavior
|
|
427
|
+
|
|
428
|
+
| Env var | Effect |
|
|
429
|
+
|---|---|
|
|
430
|
+
| `QUICKCHR_DATA_DIR` | Override the data root (defaults to `$XDG_DATA_HOME/quickchr` or platform equivalent — see §8) |
|
|
431
|
+
| `QUICKCHR_NO_PROMPT` | Force non-interactive mode (skips all wizards) |
|
|
432
|
+
| `QUICKCHR_DEBUG` | Emit `[debug]` lines from the progress logger |
|
|
433
|
+
| `QUICKCHR_INTEGRATION` | Required to run `test/integration/` |
|
|
434
|
+
| `MIKROTIK_WEB_ACCOUNT`, `MIKROTIK_WEB_PASSWORD` | License credentials fallback |
|
|
435
|
+
| `NO_COLOR` | Disable ANSI styling |
|
|
436
|
+
|
|
437
|
+
`--json` is honored on `list`, `status`, `get`, `env`, `snapshot`, and a
|
|
438
|
+
few others; `inspect` is always JSON and accepts `--json` for parity. Check
|
|
439
|
+
`quickchr help <command>`. Errors are caught at the top level and exit `1`
|
|
440
|
+
with a `[code] message` line plus the install hint when present.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## 4. Library API reference
|
|
445
|
+
|
|
446
|
+
Public exports are in `src/index.ts`. The two surfaces you use are the
|
|
447
|
+
`QuickCHR` class (machine lifecycle) and the `ChrInstance` interface
|
|
448
|
+
(per-instance operations).
|
|
449
|
+
|
|
450
|
+
### Static methods on `QuickCHR`
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
QuickCHR.start(opts?: StartOptions): Promise<ChrInstance>
|
|
454
|
+
QuickCHR.add(opts?: StartOptions): Promise<MachineState>
|
|
455
|
+
QuickCHR.list(): MachineState[]
|
|
456
|
+
QuickCHR.get(name: string): ChrInstance | null
|
|
457
|
+
QuickCHR.doctor(): Promise<DoctorResult>
|
|
458
|
+
QuickCHR.resolveVersion(channel: Channel): Promise<string>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
`start()` creates the machine if needed, downloads the image, allocates
|
|
462
|
+
a port block, spawns QEMU, waits for REST readiness, and runs all
|
|
463
|
+
provisioning steps before returning. It auto-cleans the machine if boot
|
|
464
|
+
times out (calls `stop()` then `remove()`). It acquires `start-lock` to
|
|
465
|
+
serialize concurrent starts of the same machine.
|
|
466
|
+
|
|
467
|
+
`add()` is the same minus the QEMU spawn — useful when you want to
|
|
468
|
+
materialize a machine and start it later (or hand the config to another
|
|
469
|
+
process).
|
|
470
|
+
|
|
471
|
+
`get()` returns `null` if the machine doesn't exist; otherwise it
|
|
472
|
+
loads `machine.json`, refreshes PID liveness, and returns a runtime
|
|
473
|
+
handle. `list()` is `get()` for everything in `machines/`.
|
|
474
|
+
|
|
475
|
+
### `ChrInstance`
|
|
476
|
+
|
|
477
|
+
**At a glance** — every method on a running instance:
|
|
478
|
+
|
|
479
|
+
| Group | Member | Purpose |
|
|
480
|
+
|---|---|---|
|
|
481
|
+
| Identity | `name`, `state`, `ports`, `restUrl`, `sshPort`, `portBase` | Static metadata persisted in `machine.json` |
|
|
482
|
+
| Capture | `captureInterface`, `tzspGatewayIp` | Platform-correct values for TZSP capture (`tshark -i …`) and RouterOS streaming target |
|
|
483
|
+
| Lifecycle | `waitForBoot()`, `waitFor(fn, ms?)`, `stop()`, `remove()`, `clean()`, `destroy()` | Boot readiness, custom polling, teardown |
|
|
484
|
+
| Comms | `rest()`, `exec()`, `monitor()`, `serial()`, `qga()` | REST, RouterOS CLI, QEMU monitor, serial console, QGA (x86 only) |
|
|
485
|
+
| Provisioning | `license()`, `setDeviceMode()`, `availablePackages()`, `installPackage()` | Post-boot configuration |
|
|
486
|
+
| Files | `upload()`, `download()` | SCP transfer to/from the CHR |
|
|
487
|
+
| Snapshots | `snapshot.{list,save,load,delete}()` | qcow2 savevm/loadvm |
|
|
488
|
+
| Diagnostics | `queryLoad()`, `subprocessEnv()`, `descriptor()` | Live CPU/mem sample, env vars/descriptor for child processes |
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
interface ChrInstance {
|
|
492
|
+
name: string;
|
|
493
|
+
state: MachineState; // full persisted config
|
|
494
|
+
ports: ChrPorts; // {http, https, ssh, api, apiSsl, winbox, ...}
|
|
495
|
+
restUrl: string; // http://127.0.0.1:{ports.http}
|
|
496
|
+
sshPort: number;
|
|
497
|
+
portBase: number; // start of this instance's reserved port block
|
|
498
|
+
captureInterface: string; // "lo0" on macOS, "any" on Linux — for TZSP capture
|
|
499
|
+
tzspGatewayIp: string; // "10.0.2.2" — QEMU slirp host gateway
|
|
500
|
+
|
|
501
|
+
waitForBoot(timeoutMs?: number): Promise<boolean>;
|
|
502
|
+
waitFor(condition: () => Promise<boolean>, timeoutMs?: number): Promise<boolean>;
|
|
503
|
+
stop(): Promise<void>;
|
|
504
|
+
remove(): Promise<void>;
|
|
505
|
+
clean(): Promise<void>;
|
|
506
|
+
destroy(): Promise<void>; // stop + remove
|
|
507
|
+
|
|
508
|
+
rest(path: string, opts?: RequestInit): Promise<unknown>;
|
|
509
|
+
exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
510
|
+
|
|
511
|
+
monitor(cmd: string): Promise<string>;
|
|
512
|
+
serial(): { readable: ReadableStream; writable: WritableStream };
|
|
513
|
+
qga(cmd: QgaCommand, args?: object): Promise<unknown>;
|
|
514
|
+
|
|
515
|
+
license(opts: LicenseOptions): Promise<void>;
|
|
516
|
+
setDeviceMode(opts: DeviceModeOptions, log?: ProgressLogger): Promise<void>;
|
|
517
|
+
availablePackages(): Promise<string[]>;
|
|
518
|
+
installPackage(pkgs: string | string[]): Promise<string[]>;
|
|
519
|
+
|
|
520
|
+
upload(localPath: string, remotePath?: string): Promise<void>;
|
|
521
|
+
download(remotePath: string, localPath: string): Promise<void>;
|
|
522
|
+
|
|
523
|
+
snapshot: {
|
|
524
|
+
list(): Promise<SnapshotInfo[]>;
|
|
525
|
+
save(name?: string): Promise<SnapshotInfo>;
|
|
526
|
+
load(name: string): Promise<void>;
|
|
527
|
+
delete(name: string): Promise<void>;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
queryLoad(): Promise<ChrLoadSample | null>;
|
|
531
|
+
subprocessEnv(): Promise<Record<string, string>>;
|
|
532
|
+
descriptor(): Promise<Descriptor>;
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`rest()` retries up to 3 times with 2s backoff on `ECONNRESET` (RouterOS
|
|
537
|
+
transiently resets connections post-boot/reboot). HTTP errors and
|
|
538
|
+
`QuickCHRError` from the REST layer are NOT retried.
|
|
539
|
+
|
|
540
|
+
`subprocessEnv()` returns env-var keys (`QUICKCHR_NAME`,
|
|
541
|
+
`QUICKCHR_REST_URL`, `QUICKCHR_REST_BASE`, `QUICKCHR_SSH_PORT`,
|
|
542
|
+
`QUICKCHR_AUTH`, plus legacy `URLBASE`/`BASICAUTH`) so child processes
|
|
543
|
+
can hit the CHR without re-resolving auth.
|
|
544
|
+
|
|
545
|
+
`descriptor()` returns the stable, versioned JSON shape used by `quickchr
|
|
546
|
+
inspect` — the quickchr ↔ centrs interface contract (issue #71,
|
|
547
|
+
[`docs/centrs-interface.md`](docs/centrs-interface.md)): machine identity,
|
|
548
|
+
status, a per-service `services` map (`rest-api`, `native-api`, `ssh` — each
|
|
549
|
+
with host/port/transport/`tls`/availability/auth), optional
|
|
550
|
+
`customForwards`/`networks`, and timestamps. It is intentionally
|
|
551
|
+
**running-only** and throws `MACHINE_STOPPED` for stopped machines. It has
|
|
552
|
+
no `env` field — use `subprocessEnv()` for env-var-shaped connection facts.
|
|
553
|
+
|
|
554
|
+
Both `subprocessEnv()` and `descriptor()` expose auth material by design —
|
|
555
|
+
`subprocessEnv()`'s `QUICKCHR_AUTH`/`BASICAUTH` env vars, and `descriptor()`'s
|
|
556
|
+
own per-service auth fields (`services["rest-api"].auth.password`/`.basic`/
|
|
557
|
+
`.header`; `services["native-api"].auth.password`, no basic/header;
|
|
558
|
+
`services.ssh.auth.privateKeyPath`). Treat their output as credentials and
|
|
559
|
+
redact before logging or attaching to bug reports.
|
|
560
|
+
|
|
561
|
+
### `StartOptions` (selected)
|
|
562
|
+
|
|
563
|
+
| Field | Type | Default | Notes |
|
|
564
|
+
|---|---|---|---|
|
|
565
|
+
| `name` | string | auto | Must not start with `-` |
|
|
566
|
+
| `version` | string | — | Mutually exclusive with `channel` |
|
|
567
|
+
| `channel` | Channel | `"stable"` | |
|
|
568
|
+
| `arch` | `"arm64" \| "x86"` | host | |
|
|
569
|
+
| `cpu` | number | `1` | |
|
|
570
|
+
| `mem` | number | `512` (1024 cross-arch TCG) | MiB |
|
|
571
|
+
| `background` | boolean | `true` | |
|
|
572
|
+
| `bootDiskFormat` | `"raw" \| "qcow2"` | `"raw"` (auto-`qcow2` if `bootSize`) | |
|
|
573
|
+
| `bootSize` | string | — | e.g., `"512M"`, `"2G"`. Needs `qemu-img` |
|
|
574
|
+
| `extraDisks` | string[] | `[]` | Each entry is a size string |
|
|
575
|
+
| `networks` | NetworkSpecifier[] | `["user"]` | See §7 |
|
|
576
|
+
| `portBase` | number | auto | First of 10 ports |
|
|
577
|
+
| `excludePorts` | ServiceName[] | `[]` | e.g., `["winbox"]` |
|
|
578
|
+
| `extraPorts` | PortMapping[] | `[]` | Custom forwards |
|
|
579
|
+
| `packages` | string[] | `[]` | Provisioning |
|
|
580
|
+
| `installAllPackages` | boolean | `false` | Provisioning |
|
|
581
|
+
| `user` | `{ name, password }` | — | Provisioning |
|
|
582
|
+
| `disableAdmin` | boolean | `false` | Provisioning |
|
|
583
|
+
| `secureLogin` | boolean | `false` | **`true` is the explicit trigger** for managed-account creation |
|
|
584
|
+
| `license` | `LicenseInput` | — | `"p1"` / `"p10"` / `"unlimited"` or `LicenseOptions` |
|
|
585
|
+
| `deviceMode` | `DeviceModeOptions` | — | `{mode?, enable?[], disable?[]}` |
|
|
586
|
+
| `installDeps` | boolean | — | Auto-install missing OS deps |
|
|
587
|
+
| `dryRun` | boolean | `false` | Print QEMU command and exit |
|
|
588
|
+
| `timeoutExtra` | number | `0` | Extra **milliseconds** added to boot timeout |
|
|
589
|
+
| `onProgress` | `(msg) => void` | `console.log` | `[debug]` lines only when `QUICKCHR_DEBUG=1` |
|
|
590
|
+
|
|
591
|
+
Provisioning fields trigger a version check at the top of `start()` /
|
|
592
|
+
`add()`: any provisioning option on RouterOS < 7.20.8 throws
|
|
593
|
+
`PROVISIONING_VERSION_UNSUPPORTED`.
|
|
594
|
+
|
|
595
|
+
See `src/lib/types.ts` for the full type surface (ChrPorts, MachineState,
|
|
596
|
+
NetworkConfig, NetworkSpecifier, PortMapping, ExecOptions, ExecResult,
|
|
597
|
+
DeviceModeOptions, LicenseInput/Level/Options, DoctorResult,
|
|
598
|
+
SnapshotInfo, ChrLoadSample, ErrorCode, plus QGA result types).
|
|
599
|
+
|
|
600
|
+
The barrel also re-exports utility functions from `auth.ts`, `disk.ts`,
|
|
601
|
+
`exec.ts`, `qga.ts`, `console.ts`, `license.ts`, `credentials.ts`,
|
|
602
|
+
`packages.ts`, `network.ts`, `log.ts`, `versions.ts`. Use them when the
|
|
603
|
+
high-level class doesn't fit (typically not needed).
|
|
604
|
+
|
|
605
|
+
### Programmatic networking
|
|
606
|
+
|
|
607
|
+
The CLI's `--add-network` and `--forward` flags are just `StartOptions.networks`
|
|
608
|
+
and `StartOptions.extraPorts`. Full decision guide:
|
|
609
|
+
[`docs/networking-recipes.md`](docs/networking-recipes.md).
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
import { QuickCHR, expandForwardSpec } from "@tikoci/quickchr";
|
|
613
|
+
|
|
614
|
+
const chr = await QuickCHR.start({
|
|
615
|
+
// ether1 = user (REST/mgmt + hostfwd); ether2 = L2 frames streamed to a host
|
|
616
|
+
// TCP server on `port` (e.g. to receive MNDP — see examples/mndp/).
|
|
617
|
+
networks: ["user", { type: "socket-connect", port }],
|
|
618
|
+
|
|
619
|
+
// host→guest forwards; UDP supported. Build PortMappings by hand…
|
|
620
|
+
extraPorts: [
|
|
621
|
+
{ name: "snmp", host: 9161, guest: 161, proto: "udp" },
|
|
622
|
+
// …or parse CLI-style spec strings, including a UDP range:
|
|
623
|
+
...expandForwardSpec("btest:9200-9210:2000-2010/udp"),
|
|
624
|
+
],
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
To **receive** UDP a guest sends, no forward is needed — bind an *unconnected*
|
|
629
|
+
host socket and have the guest send to `chr.tzspGatewayIp` (`10.0.2.2`); see
|
|
630
|
+
[`docs/networking-recipes.md`](docs/networking-recipes.md) §2 and
|
|
631
|
+
[`examples/udp-gateway/`](examples/udp-gateway/).
|
|
632
|
+
|
|
633
|
+
### Version & channel helpers
|
|
634
|
+
|
|
635
|
+
For tools that pick which CHRs to boot (e.g. CI matrices), the barrel exports
|
|
636
|
+
the RouterOS version facts and a recency classifier:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
import {
|
|
640
|
+
resolveAllVersions, compareRouterOsVersion,
|
|
641
|
+
resolveChannelStatuses, resolveActiveChannels,
|
|
642
|
+
classifyChannels, selectActiveChannels, channelMaturity,
|
|
643
|
+
CHANNELS, type Channel, type ChannelStatus,
|
|
644
|
+
} from "@tikoci/quickchr";
|
|
645
|
+
|
|
646
|
+
await resolveAllVersions();
|
|
647
|
+
// { stable: "7.23.1", "long-term": "7.21.4", testing: "7.23rc4", development: "7.24beta2" }
|
|
648
|
+
|
|
649
|
+
compareRouterOsVersion("7.24beta2", "7.24rc1"); // < 0 (beta < rc < release < patch)
|
|
650
|
+
|
|
651
|
+
await resolveActiveChannels();
|
|
652
|
+
// ["stable", "long-term", "development"] — released channels always, plus any
|
|
653
|
+
// pre-release at or ahead of stable (here testing 7.23rc4 is behind 7.23.1, excluded)
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
`resolveChannelStatuses()` returns `ChannelStatus[]`
|
|
657
|
+
(`{ channel, version, maturity, aheadOfStable }`); `resolveActiveChannels({ aheadOf })`
|
|
658
|
+
lets you measure pre-release recency against a channel other than `stable`. The pure
|
|
659
|
+
`classifyChannels(versions)` / `selectActiveChannels(versions, opts)` take a
|
|
660
|
+
`Record<Channel, string>` so you can classify without a network round-trip.
|
|
661
|
+
|
|
662
|
+
These answer **"what's worth booting,"** never "what must pass" — keep merge-gating
|
|
663
|
+
policy in the consumer.
|
|
664
|
+
|
|
665
|
+
The entry also re-exports the lower-level version helpers for pre-flight checks:
|
|
666
|
+
`resolveVersion`, `parseVersionParts`, `isValidVersion`, and
|
|
667
|
+
`isProvisioningSupportedVersion` (validate a version against the provisioning floor
|
|
668
|
+
before calling `start()`).
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## 5. Provisioning pipeline
|
|
673
|
+
|
|
674
|
+
Everything below the boot wait runs in **background** mode regardless of
|
|
675
|
+
the user's `--fg` choice; if foreground was requested, quickchr stops
|
|
676
|
+
QEMU after provisioning and re-launches with stdio attached.
|
|
677
|
+
|
|
678
|
+
**Version gate:** RouterOS ≥ 7.20.8 (first long-term baseline). Any
|
|
679
|
+
provisioning option on an older image throws
|
|
680
|
+
`PROVISIONING_VERSION_UNSUPPORTED` upfront. Boot, disk, networking, and
|
|
681
|
+
port mappings work on any 7.x.
|
|
682
|
+
|
|
683
|
+
**Order of operations** (`_provisionInstance` in `src/lib/quickchr.ts`):
|
|
684
|
+
|
|
685
|
+
1. **Packages** — `installAllPackages` or `packages[]` triggers SCP
|
|
686
|
+
upload of `.npk` files to the SSH port, then `/system/reboot`.
|
|
687
|
+
quickchr waits 2× the normal boot timeout (package init takes
|
|
688
|
+
longer). Persists the installed list to `state.packages`.
|
|
689
|
+
2. **Device-mode** — applies `deviceMode.mode` / `enable[]` / `disable[]`
|
|
690
|
+
via REST. RouterOS requires a hard power-cycle to confirm; quickchr
|
|
691
|
+
kills QEMU and restarts it. The blocking REST call returns
|
|
692
|
+
ECONNRESET when QEMU is killed — this is suppressed and treated as
|
|
693
|
+
success after the verification read-back. Persisted to
|
|
694
|
+
`state.deviceMode`.
|
|
695
|
+
3. **License** — `/system/license/renew` with `account`, `password`,
|
|
696
|
+
`level`, `duration=10s` so the server-side wait fits in a single
|
|
697
|
+
call. Read-back confirms the actual applied level and persists it
|
|
698
|
+
to `state.licenseLevel`. Errors come back inside HTTP 200 bodies as
|
|
699
|
+
`status: "ERROR: …"` strings — quickchr classifies these as
|
|
700
|
+
immediate failures (no retry).
|
|
701
|
+
4. **User & admin** — only runs when `user` is set OR `disableAdmin` is
|
|
702
|
+
true OR `secureLogin === true`. Creates the custom user and/or the
|
|
703
|
+
auto-generated `quickchr` managed account, optionally disables admin,
|
|
704
|
+
stores the password in the per-instance secret file, and writes the
|
|
705
|
+
user's name to `machine.json` (real password is NEVER written there).
|
|
706
|
+
Disabling admin **does not** lock the REST API — RouterOS's `expired`
|
|
707
|
+
flag only gates CLI/SSH/Winbox login, not REST.
|
|
708
|
+
|
|
709
|
+
Each step is independent. A failure in step N stops further steps but
|
|
710
|
+
does not roll back N–1. Re-running `start` re-applies pending
|
|
711
|
+
provisioning from `machine.json` only if the machine had not been fully
|
|
712
|
+
provisioned previously.
|
|
713
|
+
|
|
714
|
+
**Always read back what we wrote.** This catches version-specific drift
|
|
715
|
+
in REST responses and surfaces actionable errors instead of silent
|
|
716
|
+
state corruption.
|
|
717
|
+
|
|
718
|
+
The post-boot REST race (early endpoints can return wrong/empty bodies
|
|
719
|
+
even after `waitForBoot` returns) is handled per-endpoint: license,
|
|
720
|
+
device-mode, identity, and user reads each poll for field presence
|
|
721
|
+
with 15–30s deadlines. `waitForBoot` itself uses a two-consecutive-OK
|
|
722
|
+
guard on `/system/resource`.
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## 6. Channels — REST, monitor, serial, QGA, console
|
|
727
|
+
|
|
728
|
+
Every running instance exposes five host-facing channels. Names map to
|
|
729
|
+
absolute file paths under `<machineDir>/`; on Windows they are named
|
|
730
|
+
pipes (`\\.\pipe\…`).
|
|
731
|
+
|
|
732
|
+
| Channel | Purpose | Transport (POSIX) | Transport (Windows) |
|
|
733
|
+
|---|---|---|---|
|
|
734
|
+
| REST | RouterOS HTTP API | TCP `127.0.0.1:{ports.http}` | same |
|
|
735
|
+
| QEMU monitor | `info cpus`, `quit`, `savevm`, etc. | Unix socket `monitor.sock` | named pipe |
|
|
736
|
+
| Serial | RouterOS serial console | Unix socket `serial.sock` | named pipe |
|
|
737
|
+
| QGA (x86 only) | guest agent commands | Unix socket `qga.sock` | named pipe |
|
|
738
|
+
| QEMU log | stdout/stderr of QEMU itself | file `qemu.log` | same |
|
|
739
|
+
|
|
740
|
+
**REST** is what `chr.rest()` and `chr.exec({via: "rest"})` use. It
|
|
741
|
+
**must** go through `src/lib/rest.ts` (which uses `node:http` with
|
|
742
|
+
`agent: false`) — Bun's `fetch()` connection pool reuses sockets after
|
|
743
|
+
machine restarts and returns stale responses. See
|
|
744
|
+
`.github/instructions/bun-http.instructions.md`.
|
|
745
|
+
|
|
746
|
+
**Monitor** is for QEMU itself, not RouterOS. `instance.monitor("info
|
|
747
|
+
cpus")` returns the raw text. quickchr uses it internally for
|
|
748
|
+
`stop()`, `snapshot.save/load/delete`, and `queryLoad()`.
|
|
749
|
+
|
|
750
|
+
**Serial** is a bidirectional byte stream to the RouterOS console. Both
|
|
751
|
+
`chr.serial()` (returns streams) and `quickchr console <name>` (attaches
|
|
752
|
+
stdio) use it. Serial-based `exec` (`--via=console`) does prompt
|
|
753
|
+
detection plus buffer-offset tracking; works in restricted environments
|
|
754
|
+
where REST is unavailable, slower than REST.
|
|
755
|
+
|
|
756
|
+
**QGA** uses a JSON-line protocol (`guest-sync-delimited` first, then
|
|
757
|
+
`guest-exec`, etc.). Available **only** on x86 CHR running under KVM.
|
|
758
|
+
ARM64 CHR doesn't ship the agent; macOS HVF (and any TCG run) don't
|
|
759
|
+
initialize the virtio-serial channel correctly — that's a QEMU 10.x
|
|
760
|
+
behavior documented in `docs/qga-x86-macos-qemu10-investigation.md`.
|
|
761
|
+
|
|
762
|
+
**QEMU log** captures everything QEMU itself writes, including boot
|
|
763
|
+
panics, EFI errors, and signal traces. `quickchr logs <name>` tails it.
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
## 7. Networking
|
|
768
|
+
|
|
769
|
+
Each `--add-network <spec>` adds one NIC. Without any flag, you get a
|
|
770
|
+
single user-mode NIC (SLiRP) on `ether1`. Order matters: `user` should
|
|
771
|
+
always be `ether1` because RouterOS auto-creates a DHCP client only on
|
|
772
|
+
the first interface, and SLiRP's built-in DHCP needs the
|
|
773
|
+
`10.0.2.15` address to make `hostfwd` work (see DESIGN.md
|
|
774
|
+
§SLiRP-must-be-ether1 and `test/lab/slirp-hostfwd/`).
|
|
775
|
+
|
|
776
|
+
### Which mechanism? (by traffic shape)
|
|
777
|
+
|
|
778
|
+
Picking among the specifiers/forwards by *goal* — full guide with recipes in
|
|
779
|
+
[`docs/networking-recipes.md`](docs/networking-recipes.md):
|
|
780
|
+
|
|
781
|
+
| You want… | Reach for |
|
|
782
|
+
|---|---|
|
|
783
|
+
| Reach a guest TCP/UDP service (REST, SSH, SNMP, container port) | `user` NIC + `--forward`/`extraPorts` (`hostfwd`) |
|
|
784
|
+
| Reach a guest on many/dynamic ports | `--forward name:9200-9210:2000-2010/udp` (range) |
|
|
785
|
+
| Receive UDP the **guest sends** (syslog, NetFlow, TZSP, a reply) | gateway path — guest → `10.0.2.2:<port>`, host binds an **unconnected** socket; **no forward** |
|
|
786
|
+
| Receive guest L2 frames/broadcasts (MNDP, MAC-Telnet) | `socket-connect` NIC ([`docs/mndp.md`](docs/mndp.md)) |
|
|
787
|
+
| L2 link between two VMs | `socket::<name>` |
|
|
788
|
+
| Real LAN presence / DHCP from host | `shared` / `bridged:<iface>` |
|
|
789
|
+
|
|
790
|
+
### Specifiers
|
|
791
|
+
|
|
792
|
+
| Spec | What it does | Root needed? | Cross-VM? | Host-visible? |
|
|
793
|
+
|---|---|---|---|---|
|
|
794
|
+
| `user` | SLiRP user-mode NAT, hostfwd port mapping | no | no | via hostfwd ports only |
|
|
795
|
+
| `socket::<name>` | L2 socket pair between two VMs sharing the name | no | yes | no |
|
|
796
|
+
| `socket-listen:<port>` / `socket-connect:<port>` / `socket-mcast:<group>:<port>` | low-level QEMU socket netdevs | no | yes | no |
|
|
797
|
+
| `shared` | NAT'd L3 with DHCP from the host | yes (or `socket_vmnet` rootless) | yes | yes |
|
|
798
|
+
| `bridged:<iface>` | L2 bridged onto host iface | yes (or `socket_vmnet` rootless) | yes | yes |
|
|
799
|
+
| `tap:<iface>` | pre-created TAP, no quickchr setup | depends | yes | yes |
|
|
800
|
+
| `wifi`, `ethernet`, `auto` (alias) | resolves to detected host iface | as above | | |
|
|
801
|
+
|
|
802
|
+
Resolution chains:
|
|
803
|
+
|
|
804
|
+
- **`shared`** on macOS: socket_vmnet daemon → vmnet-shared (root-only QEMU). On Linux: pre-created TAP owned by the user, else root-created TAP+bridge.
|
|
805
|
+
- **`bridged:<iface>`** on macOS: socket_vmnet bridged → vmnet-bridged (root). On Linux: TAP attached to a bridge containing the named iface.
|
|
806
|
+
|
|
807
|
+
### Named L2 sockets
|
|
808
|
+
|
|
809
|
+
`quickchr networks sockets create lab-sw1` reserves a port and stores
|
|
810
|
+
metadata in `<dataDir>/networks/lab-sw1.json`. Two machines using
|
|
811
|
+
`--add-network socket::lab-sw1` form an L2 tunnel: the first to start
|
|
812
|
+
listens on the port; the second connects.
|
|
813
|
+
|
|
814
|
+
quickchr unregisters socket members on `stop`/`remove`/`clean` so port
|
|
815
|
+
allocation can recycle.
|
|
816
|
+
|
|
817
|
+
For platform internals (vmnet quirks, TAP creation, bridged-mode
|
|
818
|
+
restrictions, half-open SLiRP under TCG), see `docs/networking.md`.
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
|
|
822
|
+
## 8. Storage layout
|
|
823
|
+
|
|
824
|
+
### macOS / Linux (XDG)
|
|
825
|
+
|
|
826
|
+
```text
|
|
827
|
+
$XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
|
|
828
|
+
├── cache/ Downloaded RouterOS images
|
|
829
|
+
│ ├── chr-7.22.1.img.zip
|
|
830
|
+
│ ├── chr-7.22.1.img
|
|
831
|
+
│ └── chr-7.22.1-arm64.img
|
|
832
|
+
├── machines/
|
|
833
|
+
│ └── <name>/
|
|
834
|
+
│ ├── machine.json Persisted MachineState
|
|
835
|
+
│ ├── disk.img | disk.qcow2
|
|
836
|
+
│ ├── extra-disk-*.qcow2
|
|
837
|
+
│ ├── efi-vars.fd arm64 only (UEFI vars)
|
|
838
|
+
│ ├── monitor.sock QEMU monitor
|
|
839
|
+
│ ├── serial.sock serial console
|
|
840
|
+
│ ├── qga.sock QGA (x86 only)
|
|
841
|
+
│ ├── qemu.pid
|
|
842
|
+
│ ├── qemu.log
|
|
843
|
+
│ ├── start-lock start serialization
|
|
844
|
+
│ └── ssh/id_ed25519[.pub] SSH key (if managed user provisioned)
|
|
845
|
+
└── networks/ Named L2 socket reservations
|
|
846
|
+
└── <name>.json
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
`$XDG_DATA_HOME` defaults to `~/.local/share/quickchr/`. Override the
|
|
850
|
+
whole tree with `QUICKCHR_DATA_DIR=/some/path`.
|
|
851
|
+
|
|
852
|
+
**Global settings** live separately, under the XDG **config** tier, not this
|
|
853
|
+
data tree: `~/.config/quickchr/quickchr.env` (dotenv-style). See §3's
|
|
854
|
+
`settings` command (under Meta) for the command surface and §11 for the env
|
|
855
|
+
var list.
|
|
856
|
+
|
|
857
|
+
### Windows
|
|
858
|
+
|
|
859
|
+
```text
|
|
860
|
+
%LOCALAPPDATA%\quickchr\
|
|
861
|
+
├── cache\
|
|
862
|
+
├── machines\<name>\
|
|
863
|
+
│ ├── machine.json
|
|
864
|
+
│ ├── disk.img
|
|
865
|
+
│ ├── monitor (named pipe handle, no on-disk file)
|
|
866
|
+
│ └── …
|
|
867
|
+
└── …
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
Falls back to `%USERPROFILE%\AppData\Local\quickchr\` if `LOCALAPPDATA`
|
|
871
|
+
is unset.
|
|
872
|
+
|
|
873
|
+
### Credentials (separate from data dir)
|
|
874
|
+
|
|
875
|
+
- **Per-instance CHR credentials**: file in the user config dir
|
|
876
|
+
(`~/.config/quickchr/secrets.json` on POSIX; not the OS keychain — a
|
|
877
|
+
Keychain dialog hangs in non-TTY contexts like tests and CI).
|
|
878
|
+
- **MikroTik.com web account** (for license operations): tries
|
|
879
|
+
`Bun.secrets` (OS keychain) when interactive, falls back to the same
|
|
880
|
+
config-file store otherwise.
|
|
881
|
+
|
|
882
|
+
Both honor explicit CLI flags / env vars first.
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
886
|
+
## 9. Port layout
|
|
887
|
+
|
|
888
|
+
Each instance reserves a contiguous **block of 10 ports** starting at
|
|
889
|
+
`portBase`. Default base is `9100`; auto-allocator scans existing
|
|
890
|
+
machines and TCP-probes for a free block.
|
|
891
|
+
|
|
892
|
+
| Offset | Service | Guest port |
|
|
893
|
+
|---|---|---|
|
|
894
|
+
| +0 | HTTP / REST / Webfig | 80 |
|
|
895
|
+
| +1 | HTTPS | 443 |
|
|
896
|
+
| +2 | SSH | 22 |
|
|
897
|
+
| +3 | API | 8728 |
|
|
898
|
+
| +4 | API-SSL | 8729 |
|
|
899
|
+
| +5 | WinBox | 8291 |
|
|
900
|
+
| +6 | (reserved — QEMU monitor TCP, when used) | — |
|
|
901
|
+
| +7 | (reserved — serial TCP, when used) | — |
|
|
902
|
+
| +8 | (reserved — QGA TCP, when used) | — |
|
|
903
|
+
| +9 | (spare — `extraPorts`) | — |
|
|
904
|
+
|
|
905
|
+
Defaults map the first six offsets. `--no-winbox`, `--no-api-ssl`, and
|
|
906
|
+
`excludePorts: ["…"]` skip individual mappings. `extraPorts:
|
|
907
|
+
[{name,host,guest,proto}]` adds custom forwards in offsets 6–9.
|
|
908
|
+
If an extra mapping reuses a built-in service name such as `winbox`, it
|
|
909
|
+
pins/replaces that service's host port rather than creating a second
|
|
910
|
+
`ports.winbox` entry.
|
|
911
|
+
|
|
912
|
+
If port `9100` is in use, the next instance gets `9110`, etc. Manual
|
|
913
|
+
override via `--port-base`.
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
## 10. Acceleration & timeouts
|
|
918
|
+
|
|
919
|
+
`detectAccel()` selects the fastest available accelerator for each
|
|
920
|
+
arch:
|
|
921
|
+
|
|
922
|
+
| Host | Guest | Accel |
|
|
923
|
+
|---|---|---|
|
|
924
|
+
| macOS x86_64 | x86 | HVF |
|
|
925
|
+
| macOS x86_64 | arm64 | TCG |
|
|
926
|
+
| macOS arm64 (native bun) | arm64 | HVF |
|
|
927
|
+
| macOS arm64 (native bun) | x86 | TCG |
|
|
928
|
+
| macOS arm64 (Rosetta bun) | arm64 | TCG |
|
|
929
|
+
| Linux x86_64 + `/dev/kvm` writable | x86 | KVM |
|
|
930
|
+
| Linux aarch64 + `/dev/kvm` writable | arm64 | KVM |
|
|
931
|
+
| any other combo | any | TCG |
|
|
932
|
+
|
|
933
|
+
`Bun` running under Rosetta on Apple Silicon reports `process.arch ===
|
|
934
|
+
"x64"`, so HVF for arm64 is skipped (architecture mismatch). Run with
|
|
935
|
+
native arm64 Bun to get arm64 HVF.
|
|
936
|
+
|
|
937
|
+
### Boot timeout
|
|
938
|
+
|
|
939
|
+
```text
|
|
940
|
+
base = ceil(120s × accelTimeoutFactor(accel, isCrossArch))
|
|
941
|
+
withPackages ? base × 2 : base
|
|
942
|
+
final = base + (timeoutExtra ms)
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
Native (HVF/KVM): factor ~1 → 120s base.
|
|
946
|
+
Cross-arch TCG: factor ~15 → up to 1800s.
|
|
947
|
+
Package install adds a 2× multiplier (extra reboot cycle).
|
|
948
|
+
|
|
949
|
+
If boot times out, the machine is **automatically stopped + removed**.
|
|
950
|
+
Use `--timeout-extra` to add a buffer if you know your host is slow, or
|
|
951
|
+
persist a default via `quickchr settings set timeout-extra <seconds>` so
|
|
952
|
+
every `start` picks it up without repeating the flag.
|
|
953
|
+
|
|
954
|
+
---
|
|
955
|
+
|
|
956
|
+
## 11. Auth, secrets, environment variables
|
|
957
|
+
|
|
958
|
+
**Threat model:** quickchr is a test/dev harness. The privilege boundary
|
|
959
|
+
is "any process running as the user that owns the data dir." We protect
|
|
960
|
+
against:
|
|
961
|
+
|
|
962
|
+
- Off-box access via bridged/NAT'd interfaces (RouterOS ships with empty
|
|
963
|
+
admin password by default; managed login mitigates this).
|
|
964
|
+
- Accidental credential leakage in commits, CI logs, CLI output.
|
|
965
|
+
|
|
966
|
+
We **don't** protect against same-user code reading
|
|
967
|
+
`secrets.json`, `ps`-visible CLI args, or memory inspection.
|
|
968
|
+
|
|
969
|
+
### Auth resolution (`resolveAuth`)
|
|
970
|
+
|
|
971
|
+
Priority order, used by `chr.rest()`, `chr.exec()`, and
|
|
972
|
+
`subprocessEnv()`:
|
|
973
|
+
|
|
974
|
+
1. Explicit `--user` / `--password` (or `opts.user` / `opts.password`).
|
|
975
|
+
2. `state.user.name` from `machine.json` + password from secret store.
|
|
976
|
+
3. CHR default `admin` with empty password.
|
|
977
|
+
|
|
978
|
+
`disableAdmin: true` does NOT block REST API access — the `expired`
|
|
979
|
+
flag only gates CLI/SSH/Winbox login (and even there it's bypassable
|
|
980
|
+
with Ctrl-C). Don't add workarounds for the "expired admin" myth.
|
|
981
|
+
|
|
982
|
+
### Secret stores
|
|
983
|
+
|
|
984
|
+
- **Per-instance CHR creds**: config-file only (~/.config/quickchr/`).
|
|
985
|
+
Bun.secrets triggers a Keychain dialog in headless contexts that
|
|
986
|
+
never resolves.
|
|
987
|
+
- **MikroTik.com creds**: Bun.secrets when TTY is available; same
|
|
988
|
+
config file otherwise.
|
|
989
|
+
|
|
990
|
+
### Settings file (`quickchr.env`)
|
|
991
|
+
|
|
992
|
+
`~/.config/quickchr/quickchr.env` — user-scoped global preferences, separate
|
|
993
|
+
from the secret stores above. Dotenv-style (`QUICKCHR_KEY=value` lines),
|
|
994
|
+
managed via `quickchr settings get|set|print|reset` (§3, Meta). Atomic writes
|
|
995
|
+
(temp file + `fsync` + rename); `set`/`reset` touch only the matching line and
|
|
996
|
+
preserve every other line (comments, blank lines, foreign vars) byte-for-byte.
|
|
997
|
+
`reset <key>` deletes the line entirely rather than blanking it, since a blank
|
|
998
|
+
`KEY=` line is still "set" to a bash-sourced file. Never stores credentials —
|
|
999
|
+
the 5 managed keys are all structural preferences, not secrets.
|
|
1000
|
+
|
|
1001
|
+
### Environment variables consumed
|
|
1002
|
+
|
|
1003
|
+
| Var | Where |
|
|
1004
|
+
|---|---|
|
|
1005
|
+
| `QUICKCHR_DATA_DIR` | `state.ts` — overrides data root |
|
|
1006
|
+
| `QUICKCHR_NO_PROMPT` | CLI dispatcher — forces non-interactive mode |
|
|
1007
|
+
| `QUICKCHR_DEBUG` | progress logger — emits `[debug]` lines |
|
|
1008
|
+
| `QUICKCHR_INTEGRATION` | `bun:test` gate for `test/integration/` |
|
|
1009
|
+
| `QUICKCHR_DEFAULT_CHANNEL` | `settings.ts` — env tier for the `default-channel` setting |
|
|
1010
|
+
| `QUICKCHR_DEFAULT_ARCH` | `settings.ts` — env tier for the `default-arch` setting |
|
|
1011
|
+
| `QUICKCHR_CACHE_MAX_SIZE` | `settings.ts` — env tier for the `cache-max-size` setting |
|
|
1012
|
+
| `QUICKCHR_TIMEOUT_EXTRA` | `settings.ts` — env tier for the `timeout-extra` setting |
|
|
1013
|
+
| `QUICKCHR_SECURE_LOGIN` | `settings.ts` — env tier for the `secure-login` setting |
|
|
1014
|
+
| `MIKROTIK_WEB_ACCOUNT` | license-renewal account (fallback) |
|
|
1015
|
+
| `MIKROTIK_WEB_PASSWORD` | license-renewal password (fallback) |
|
|
1016
|
+
| `NO_COLOR` | disable ANSI styling |
|
|
1017
|
+
| `LOCALAPPDATA`, `USERPROFILE`, `HOME` | data-dir base resolution |
|
|
1018
|
+
|
|
1019
|
+
### Environment variables `subprocessEnv()` sets
|
|
1020
|
+
|
|
1021
|
+
For child processes you want to point at the CHR:
|
|
1022
|
+
|
|
1023
|
+
```text
|
|
1024
|
+
QUICKCHR_NAME machine name
|
|
1025
|
+
QUICKCHR_REST_URL http://127.0.0.1:{ports.http}
|
|
1026
|
+
QUICKCHR_REST_BASE same plus /rest
|
|
1027
|
+
QUICKCHR_SSH_PORT ports.ssh
|
|
1028
|
+
QUICKCHR_AUTH user:password
|
|
1029
|
+
URLBASE legacy alias for QUICKCHR_REST_BASE
|
|
1030
|
+
BASICAUTH legacy user:password value
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
These values are connection secrets. `QUICKCHR_AUTH` and `BASICAUTH` contain
|
|
1034
|
+
`user:password` credentials; `quickchr inspect` additionally exposes the
|
|
1035
|
+
derived Basic auth header. Do not commit or publicly log these values.
|
|
1036
|
+
|
|
1037
|
+
---
|
|
1038
|
+
|
|
1039
|
+
## 12. Exec — running RouterOS commands
|
|
1040
|
+
|
|
1041
|
+
`quickchr exec <name> <command>` and `chr.exec(command, opts)` both
|
|
1042
|
+
funnel through the same transport selector.
|
|
1043
|
+
|
|
1044
|
+
### Transports
|
|
1045
|
+
|
|
1046
|
+
| `--via` | Source | Notes |
|
|
1047
|
+
|---|---|---|
|
|
1048
|
+
| `auto` (default) | tries REST, falls back to **console** on network errors (not auth errors) | Best general choice |
|
|
1049
|
+
| `rest` | `POST /rest/execute` with `{"script": "…", "as-string": ""}` | RouterOS 7.1+. Synchronous (any value for `as-string` enables sync mode; presence is what counts). 60s server-side timeout. |
|
|
1050
|
+
| `console` | serial socket with prompt detection + buffer offset tracking | Fallback for restricted environments. Uses `\r` (not `\r\n`) on PTY. |
|
|
1051
|
+
| `qga` | QGA `guest-exec` | x86 + KVM only. Throws `QGA_UNSUPPORTED` on ARM64. |
|
|
1052
|
+
| `ssh` | reserved | Currently throws `EXEC_FAILED`; planned. |
|
|
1053
|
+
|
|
1054
|
+
### Output formatting
|
|
1055
|
+
|
|
1056
|
+
`exec` does NOT have a `--json` flag. Use the RouterOS-native trick:
|
|
1057
|
+
|
|
1058
|
+
```bash
|
|
1059
|
+
quickchr exec my-chr ":put [:serialize to=json [/ip/address/print detail as-value]]"
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
`:serialize` accepts `json` and `dsv` to output structure data from an array. The exec result body is the
|
|
1063
|
+
literal stdout — no quickchr processing.
|
|
1064
|
+
|
|
1065
|
+
| Command | Syntax | Description | Example |
|
|
1066
|
+
|:--|:--|:--|:--|
|
|
1067
|
+
| **serialize** | `:serialize [<value>] to=[arg]` | Serialize specified value/array to JSON or dsv (delimiter separated values) format. **`value`** specifies which values to process. **`to`** specifies the format - *json, dsv* **`delimiter`** sets the "separator". **`order`** specifies the order for variables. **`options`** specifies additional options*:* json.pretty - makes the JSON output more visually appealing;json.no-string-conversion - prevents implicit conversions from console string type to json number type;dsv.wrap-strings - wraps string values inside quotation marks;dsv.ignore-size - if array values have different sizes, e.g. `a=(1,2);b=(3,4);c=(5,6,7)`, this option will work around `array size mismatch` error and set "empty" values in those slots.dsv.remap - merges array of dictionaries into a single dictionary (useful when working with "`print as-value`") **file-name** enables the option to generate command's output into a file (available for download in the "/files" section). | `:put [:serialize value=a,b,c to=json]``["a","b","c"]``:local test {a=(1,2,3);b=(4,5,6);c=(7,"text",9)}; :put [ :serialize to=dsv delimiter=";" value=$test order=("c","a","b") ]``c;a;b``7;1;4``text;2;5``9;3;6``:global var ({ "string"="1234"; "number"=1234 });:put [ :serialize to=json value=$var ]``{"number":1234,"string":1234.000000}``:put [ :serialize to=json value=$var options=json.no-string-conversion ]``{"number":1234,"string":"1234"}``:put [:serialize to=dsv options=dsv.remap delimiter="#" [/ip/address/print as-value]]``.id#address#comment#interface#network``*1#192.168.88.1/24#defconf#bridge#192.168.88.0``*2#192.168.69.190/24##ether1#192.168.69.0`|
|
|
1068
|
+
|
|
1069
|
+
_Source: <https://manual.mikrotik.com/docs/developer-guides/scripting/index.md>
|
|
1070
|
+
> Note: `:serialize` expects an array, so a `/print` *without* `as-value` arg will return nothing when serialized using `to=json` or `to=dsv`.
|
|
1071
|
+
|
|
1072
|
+
### Authentication
|
|
1073
|
+
|
|
1074
|
+
Same `resolveAuth()` chain as REST. Override on the CLI with
|
|
1075
|
+
`--user u --password p`, or in the library with `{user, password}` in
|
|
1076
|
+
`ExecOptions`.
|
|
1077
|
+
|
|
1078
|
+
### Timeouts
|
|
1079
|
+
|
|
1080
|
+
`opts.timeout` defaults to 30s for QGA/console and 10s for REST when
|
|
1081
|
+
running under `auto` (so the fallback to console kicks in quickly).
|
|
1082
|
+
On explicit `--via=rest`, the 60s server-side `as-string` cap is the
|
|
1083
|
+
ceiling.
|
|
1084
|
+
|
|
1085
|
+
---
|
|
1086
|
+
|
|
1087
|
+
## 13. Disks & snapshots
|
|
1088
|
+
|
|
1089
|
+
### Boot disk
|
|
1090
|
+
|
|
1091
|
+
Default format is **raw** (the image MikroTik ships). `--boot-size`
|
|
1092
|
+
auto-converts to **qcow2** before the first boot and resizes to the
|
|
1093
|
+
requested size. Requires `qemu-img` on the host.
|
|
1094
|
+
|
|
1095
|
+
`--boot-disk-format=qcow2` (without `--boot-size`) just keeps the
|
|
1096
|
+
default size but enables snapshots.
|
|
1097
|
+
|
|
1098
|
+
### Extra disks
|
|
1099
|
+
|
|
1100
|
+
`--add-disk <size>` (repeatable) creates blank qcow2 images and attaches
|
|
1101
|
+
them as additional virtio-blk-pci devices. RouterOS sees them as
|
|
1102
|
+
`ether2`/`disk2`/etc. depending on whether you've also added NICs.
|
|
1103
|
+
`quickchr disk <name>` shows the layout; with `qemu-img` it also reports
|
|
1104
|
+
virtual + actual sizes.
|
|
1105
|
+
|
|
1106
|
+
### Custom port forwards and service port pinning
|
|
1107
|
+
|
|
1108
|
+
`--forward <spec>` (repeatable on `add` and `start`) adds a QEMU SLiRP
|
|
1109
|
+
`hostfwd` mapping. Spec grammar is `name[:host[:guest]][/proto]`.
|
|
1110
|
+
When `name` matches a key or alias in the built-in
|
|
1111
|
+
`WELL_KNOWN_GUEST_PORTS` registry (e.g. `smb`/`cifs`, `winbox`, `mqtt`,
|
|
1112
|
+
`http-alt`, `dns`, `snmp`, …), the `guest` port and `proto` are filled
|
|
1113
|
+
in for you; otherwise both must be supplied. Omitting `host` (or passing
|
|
1114
|
+
`0`) auto-allocates a free host port, mirroring the default service-port
|
|
1115
|
+
behaviour.
|
|
1116
|
+
|
|
1117
|
+
Reusing a built-in service name pins that service's host port. This is
|
|
1118
|
+
useful for legacy clients that assume a fixed local port. For example,
|
|
1119
|
+
the Windows Dude client's reliable Wine command-line login path connects
|
|
1120
|
+
to `127.0.0.1:8291`, so a fresh lab machine can be created with WinBox
|
|
1121
|
+
pinned directly to that host port:
|
|
1122
|
+
|
|
1123
|
+
```bash
|
|
1124
|
+
quickchr add my-chr --forward smb # host auto, guest 445/tcp
|
|
1125
|
+
quickchr add dude-lab --forward winbox:8291 # host 8291, guest 8291/tcp
|
|
1126
|
+
quickchr add my-chr --forward winbox:9300 # host pinned, guest 8291/tcp
|
|
1127
|
+
quickchr add my-chr --forward myapp:9200:7777/udp # fully explicit
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
**Port ranges.** For L3 peer protocols with dynamic data ports (e.g. RouterOS
|
|
1131
|
+
bandwidth-test allocates UDP ports at runtime), a range spec
|
|
1132
|
+
`name:hostStart-hostEnd[:guestStart-guestEnd][/proto]` expands to one `hostfwd`
|
|
1133
|
+
per port (QEMU has no native range):
|
|
1134
|
+
|
|
1135
|
+
```bash
|
|
1136
|
+
quickchr add btest-lab --forward btest:9200-9210:2000-2010/udp
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
Rules: the host range is **required** (ranges are not auto-allocated), the guest
|
|
1140
|
+
range defaults to the host numbers when omitted and must be the same length, and a
|
|
1141
|
+
range may span at most 64 ports. Programmatically, `expandForwardSpec(spec)`
|
|
1142
|
+
returns the `PortMapping[]` for an `extraPorts` array; `parseForwardSpec(spec)`
|
|
1143
|
+
remains the single-port form. To *receive* guest-originated UDP with no forward at
|
|
1144
|
+
all, see §7 (the gateway path) and `docs/networking-recipes.md`.
|
|
1145
|
+
|
|
1146
|
+
Port maps are part of `machine.json`. Once a machine exists, `quickchr
|
|
1147
|
+
start <name>` uses the stored mapping; create a new machine (or edit the
|
|
1148
|
+
state deliberately) if a built-in service needs a different host port.
|
|
1149
|
+
|
|
1150
|
+
### `clean`
|
|
1151
|
+
|
|
1152
|
+
`quickchr clean <name>` re-copies the boot image from cache and
|
|
1153
|
+
recreates extra disks per `machine.json`. Snapshots are gone (they live
|
|
1154
|
+
inside the qcow2 file). Per-instance credentials are wiped so the next
|
|
1155
|
+
boot re-provisions them.
|
|
1156
|
+
|
|
1157
|
+
### Snapshots
|
|
1158
|
+
|
|
1159
|
+
Use the QEMU monitor via `instance.snapshot`:
|
|
1160
|
+
|
|
1161
|
+
```ts
|
|
1162
|
+
await chr.snapshot.save("before-upgrade");
|
|
1163
|
+
await chr.snapshot.load("before-upgrade");
|
|
1164
|
+
await chr.snapshot.list();
|
|
1165
|
+
await chr.snapshot.delete("before-upgrade");
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
CLI:
|
|
1169
|
+
|
|
1170
|
+
```bash
|
|
1171
|
+
quickchr snapshot my-chr save before-upgrade
|
|
1172
|
+
quickchr snapshot my-chr list
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
Constraints:
|
|
1176
|
+
|
|
1177
|
+
- Boot disk must be qcow2.
|
|
1178
|
+
- `save` and `load` need the machine running (uses `savevm`/`loadvm`).
|
|
1179
|
+
- `list` and `delete` work either way (`qemu-img info` when stopped).
|
|
1180
|
+
- Snapshots live inside the qcow2 file, so `remove` deletes them all.
|
|
1181
|
+
- QEMU's practical cap is 16 snapshots per disk.
|
|
1182
|
+
|
|
1183
|
+
---
|
|
1184
|
+
|
|
1185
|
+
## 14. Errors & recovery
|
|
1186
|
+
|
|
1187
|
+
Every library throw is `QuickCHRError(code, message, installHint?)`.
|
|
1188
|
+
The CLI catches it and prints `[code] message` plus the install hint if
|
|
1189
|
+
present, then exits `1`.
|
|
1190
|
+
|
|
1191
|
+
### Codes
|
|
1192
|
+
|
|
1193
|
+
| Code | Likely cause | First check |
|
|
1194
|
+
|---|---|---|
|
|
1195
|
+
| `MISSING_QEMU` | host arch QEMU binary not on PATH | `quickchr doctor` |
|
|
1196
|
+
| `MISSING_FIRMWARE` | edk2 UEFI firmware not installed (arm64) | install `qemu-efi-aarch64` / `edk2-aarch64` |
|
|
1197
|
+
| `MISSING_UNZIP` | image extraction needs `unzip` | install OS package |
|
|
1198
|
+
| `PORT_CONFLICT` | no free 10-port block | stop a machine or set `--port-base` |
|
|
1199
|
+
| `BOOT_TIMEOUT` | CHR didn't respond within timeout | check `qemu.log`; under TCG, raise `--timeout-extra`; machine is **auto-removed** |
|
|
1200
|
+
| `DOWNLOAD_FAILED` | image fetch failed | retry; check connectivity to MikroTik download server |
|
|
1201
|
+
| `INVALID_VERSION` / `INVALID_ARCH` / `INVALID_NAME` / `INVALID_DISK_SIZE` | argument parse | fix the value |
|
|
1202
|
+
| `MACHINE_EXISTS` | name already in `machines/` | choose another name or `QuickCHR.get(name)` |
|
|
1203
|
+
| `MACHINE_NOT_FOUND` | name not in `machines/` | `quickchr list` |
|
|
1204
|
+
| `MACHINE_RUNNING` / `MACHINE_STOPPED` | wrong lifecycle state | `start` or `stop` first |
|
|
1205
|
+
| `MACHINE_LOCKED` | another `start` in progress | wait or pick another name |
|
|
1206
|
+
| `EXEC_FAILED` | RouterOS rejected the command (auth, syntax) | check creds, command syntax |
|
|
1207
|
+
| `PROCESS_FAILED` | QEMU spawn / reboot / verify failed | check `qemu.log`, host resources |
|
|
1208
|
+
| `SPAWN_FAILED` | exec/spawn of host binary failed | check binary exists + executable |
|
|
1209
|
+
| `QGA_UNSUPPORTED` | QGA on ARM64 | use `--via=rest` or `--via=console` |
|
|
1210
|
+
| `QGA_TIMEOUT` | QGA daemon unreachable | KVM-only; not on macOS HVF or TCG |
|
|
1211
|
+
| `NETWORK_UNAVAILABLE` | provisioning needs `user` NIC, none present | add `--add-network user` |
|
|
1212
|
+
| `INVALID_NETWORK` | unknown alias / unresolvable iface / dead socket daemon | `quickchr networks` |
|
|
1213
|
+
| `PROVISIONING_VERSION_UNSUPPORTED` | provisioning option on RouterOS < 7.20.8 | use `--channel long-term` or pin a `7.20.8+` version |
|
|
1214
|
+
| `INSUFFICIENT_DISK_SPACE` | host data dir full | free space or move via `QUICKCHR_DATA_DIR` |
|
|
1215
|
+
| `STATE_ERROR` | snapshot on raw boot disk | recreate with `--boot-disk-format=qcow2` |
|
|
1216
|
+
| `INVALID_SIZE_STRING` | invalid cache/settings size string | use size syntax like `512M`, `2G`, or `1.5G` |
|
|
1217
|
+
| `INVALID_SETTING_KEY` | `settings get`/`set`/`reset` received an unrecognized key | `quickchr settings print` to see the managed keys |
|
|
1218
|
+
| `INVALID_SETTING_VALUE` | `settings set` (or a `QUICKCHR_*` env var / `quickchr.env` line) failed validation for a recognized key | `quickchr help settings` for the expected type/enum per key |
|
|
1219
|
+
|
|
1220
|
+
### Auto-cleanup on boot timeout
|
|
1221
|
+
|
|
1222
|
+
If `start()` hits `BOOT_TIMEOUT`, quickchr automatically calls
|
|
1223
|
+
`stop()` then `remove()` on the failing instance. You'll see a
|
|
1224
|
+
diagnostic and the machine will be gone from `quickchr list`. This
|
|
1225
|
+
prevents orphan disks accumulating across attempts.
|
|
1226
|
+
|
|
1227
|
+
### Where to look
|
|
1228
|
+
|
|
1229
|
+
1. `quickchr doctor` — prerequisites, accelerator, disk space.
|
|
1230
|
+
2. `quickchr logs <name>` — QEMU's own stderr/stdout, including boot
|
|
1231
|
+
panics, EFI errors, signal traces.
|
|
1232
|
+
3. `<machineDir>/machine.json` — last-known state, ports, options.
|
|
1233
|
+
4. `bun run check` — type/lint regressions if you've been editing.
|
|
1234
|
+
5. `.github/instructions/` — agent-facing rules with deeper
|
|
1235
|
+
troubleshooting matrices for QEMU, RouterOS REST, provisioning,
|
|
1236
|
+
testing, CI, and Bun HTTP quirks.
|
|
1237
|
+
|
|
1238
|
+
---
|
|
1239
|
+
|
|
1240
|
+
## 15. Cross-references
|
|
1241
|
+
|
|
1242
|
+
- **README.md** — quickstart, install, simple examples.
|
|
1243
|
+
- **DESIGN.md** — architecture, layers, port/storage tables, design
|
|
1244
|
+
principles (scope boundary, networking philosophy, SLiRP-must-be-ether1,
|
|
1245
|
+
exec transport design, examples philosophy).
|
|
1246
|
+
- **CHANGELOG.md** — release history; `[Unreleased]` is the working
|
|
1247
|
+
set, `[0.1.1]` is the first GitHub release.
|
|
1248
|
+
- **CONTRIBUTING.md** — dev setup and `bun run check`.
|
|
1249
|
+
- **docs/networking-recipes.md** — "which mechanism for which traffic
|
|
1250
|
+
shape" decision guide (hostfwd, the guest→host gateway path, L2 capture,
|
|
1251
|
+
VM↔VM links, LAN). Start here when picking a networking approach.
|
|
1252
|
+
- **docs/networking.md** — platform-specific QEMU networking
|
|
1253
|
+
internals (vmnet, TAP, socket netdevs).
|
|
1254
|
+
- **docs/mndp.md** — worked recipe for receiving guest L2 broadcasts
|
|
1255
|
+
(MNDP) on the host via a `socket-connect` NIC.
|
|
1256
|
+
- **docs/qga-x86-macos-qemu10-investigation.md** — root cause for the
|
|
1257
|
+
QGA-on-macOS-arm64 limitation.
|
|
1258
|
+
- **`.github/instructions/`** — agent-facing rules (testing, qemu,
|
|
1259
|
+
routeros-rest, provisioning, ci, bun-http, general).
|
|
1260
|
+
- **`examples/`** — runnable scripts (`bun run`): `quickstart` (single-CHR smoke),
|
|
1261
|
+
`grounding` (apply config → read back — the one `bun:test`), `harness` (drive an
|
|
1262
|
+
external tool via `subprocessEnv()`/`descriptor()`), `dude` (install a package),
|
|
1263
|
+
`rollback`/`service-forward`/`file-transfer`/`device-mode`/`trial-license`,
|
|
1264
|
+
`version-matrix` (parallel matrix), `mndp`/`udp-gateway` (networking). Coverage
|
|
1265
|
+
map: `examples/COVERAGE.md`; convention: `.github/instructions/examples.instructions.md`.
|
|
1266
|
+
- **`routeros-quickchr` skill** (in
|
|
1267
|
+
[tikoci/routeros-skills](https://github.com/tikoci/routeros-skills)) —
|
|
1268
|
+
agent-facing guide to grounding RouterOS against a real router with quickchr
|
|
1269
|
+
(the apply→read-back loop, networking recipes, harness patterns).
|
|
1270
|
+
- **BACKLOG.md** — a map of how work is tracked. Open work lives in GitHub
|
|
1271
|
+
Issues; see CONTRIBUTING.md "Tracking work".
|