@tikoci/centrs 0.1.1 → 0.1.3
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/CHANGELOG.md +107 -2
- package/GLOSSARY.txt +87 -0
- package/README.md +25 -9
- package/SECURITY.md +3 -3
- package/commands/AGENTS.md +3 -2
- package/commands/api/AGENTS.md +212 -0
- package/commands/api/README.md +177 -0
- package/commands/api/examples.md +428 -0
- package/commands/btest/README.md +56 -31
- package/commands/btest/examples.md +35 -10
- package/commands/check/README.md +371 -53
- package/commands/check/examples.md +390 -0
- package/commands/devices/README.md +177 -37
- package/commands/devices/examples.md +148 -0
- package/commands/execute/README.md +48 -7
- package/commands/execute/examples.md +90 -6
- package/commands/retrieve/README.md +37 -51
- package/commands/retrieve/examples.md +40 -9
- package/commands/settings/README.md +286 -22
- package/commands/settings/examples.md +408 -0
- package/commands/terminal/README.md +6 -0
- package/commands/transfer/README.md +32 -2
- package/commands/transfer/examples.md +66 -0
- package/docs/CONSTITUTION.md +40 -15
- package/docs/MATRIX.md +69 -142
- package/docs/errors/README.md +2 -1
- package/docs/errors/cdb/empty-selection.md +19 -0
- package/docs/errors/input/incomplete-gps.md +9 -0
- package/docs/errors/input/invalid-altitude.md +9 -0
- package/docs/errors/input/invalid-bbox.md +9 -0
- package/docs/errors/input/invalid-coordinate.md +9 -0
- package/docs/errors/input/invalid-radius.md +9 -0
- package/docs/errors/internal/settings-failed.md +9 -0
- package/docs/errors/routeros/btest-connection-count-single-stream.md +22 -0
- package/docs/errors/routeros/btest-udp-tx-size-ignored.md +15 -0
- package/docs/errors/settings/consequential-value.md +26 -0
- package/docs/errors/settings/invalid-boolean.md +21 -3
- package/docs/errors/settings/invalid-format.md +18 -3
- package/docs/errors/settings/invalid-integer.md +18 -3
- package/docs/errors/settings/invalid-timeout.md +20 -3
- package/docs/errors/settings/invalid-via.md +22 -3
- package/docs/errors/settings/reserved-key.md +29 -0
- package/docs/errors/settings/skip-env-file-active.md +25 -0
- package/docs/errors/settings/unknown-key.md +25 -0
- package/docs/errors/usage/fanout-not-supported.md +16 -0
- package/docs/errors/usage/invalid-method.md +23 -0
- package/package.json +9 -9
- package/src/api-fanout.ts +454 -0
- package/src/api.ts +1627 -0
- package/src/btest.ts +61 -1
- package/src/cli/api.ts +660 -0
- package/src/cli/btest.ts +20 -2
- package/src/cli/common.ts +75 -0
- package/src/cli/devices.ts +235 -6
- package/src/cli/discover.ts +3 -1
- package/src/cli/execute.ts +107 -6
- package/src/cli/mcp.ts +8 -1
- package/src/cli/retrieve.ts +135 -90
- package/src/cli/selection.ts +200 -0
- package/src/cli/settings.ts +286 -0
- package/src/cli/terminal.ts +22 -7
- package/src/cli/transfer.ts +171 -47
- package/src/cli.ts +29 -0
- package/src/core/error-catalog.ts +69 -0
- package/src/core/fanout.ts +301 -0
- package/src/core/inspect.ts +170 -0
- package/src/devices.ts +212 -20
- package/src/discover.ts +7 -2
- package/src/execute-fanout.ts +438 -0
- package/src/execute.ts +134 -70
- package/src/index.ts +166 -9
- package/src/mcp/config.ts +12 -2
- package/src/mcp/tools.ts +137 -2
- package/src/protocols/adapter.ts +357 -9
- package/src/protocols/btest-session.ts +279 -46
- package/src/protocols/btest.ts +6 -2
- package/src/protocols/index.ts +11 -5
- package/src/protocols/native-api.ts +223 -30
- package/src/resolver/cdb.ts +10 -2
- package/src/resolver/comment-kv.ts +58 -4
- package/src/resolver/config-file.ts +136 -0
- package/src/resolver/facts.ts +78 -0
- package/src/resolver/geo.ts +453 -0
- package/src/resolver/index.ts +46 -0
- package/src/resolver/mac.ts +12 -3
- package/src/resolver/selection.ts +304 -0
- package/src/resolver/settings.ts +54 -5
- package/src/resolver/target.ts +20 -2
- package/src/retrieve-fanout.ts +232 -268
- package/src/retrieve.ts +69 -94
- package/src/settings.ts +1078 -0
- package/src/terminal.ts +11 -1
- package/src/transfer-fanout.ts +527 -0
- package/src/transfer.ts +61 -11
- package/commands/stream/README.md +0 -70
|
@@ -486,3 +486,151 @@ Each alias produces the same envelope as its canonical verb — `meta.operation`
|
|
|
486
486
|
reports the canonical command (`list`/`show`/`remove`), never the alias. An
|
|
487
487
|
unknown verb still errors with `input/invalid-command`, and the error lists the
|
|
488
488
|
canonical verbs plus the accepted aliases.
|
|
489
|
+
|
|
490
|
+
## Location / GPS
|
|
491
|
+
|
|
492
|
+
Slice 1 of issue #146: `lat`/`lon`/`altitude`/`altitude-type` are recognized
|
|
493
|
+
comment-kv facts (see `README.md`, Location / GPS) — stored, queryable via
|
|
494
|
+
`--where`, and returned as a `location` block in `devices show`/`list` output.
|
|
495
|
+
Coordinate order is **lat-first everywhere**: `--gps <lat>,<lon>[,…]`.
|
|
496
|
+
|
|
497
|
+
### 42. `add --gps` sets lat/lon/altitude; `show` returns a `location` block
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
centrs devices add 198.51.100.40 --user admin --password p \
|
|
501
|
+
--gps 37.7749,-122.4194,16 --cdb-file $CDB
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
`ok: true`. Re-reading via `show` returns
|
|
505
|
+
`data.entry.location = { lat: 37.7749, lon: -122.4194, altitude: 16, altitudeType: "MSL" }`
|
|
506
|
+
(the 3-part `--gps` form defaults the missing altitude-type to `MSL`).
|
|
507
|
+
|
|
508
|
+
### 43. Discrete `--lat`/`--lon` round-trip the same way as `--gps`
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
centrs devices set 192.0.2.5 --lat 51.5072 --lon -0.1276 --cdb-file $CDB
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
`ok: true`. `devices show 192.0.2.5` returns
|
|
515
|
+
`data.entry.location = { lat: 51.5072, lon: -0.1276 }` (no `altitude` key —
|
|
516
|
+
altitude was never set on this record).
|
|
517
|
+
|
|
518
|
+
### 44. `--altitude` alone defaults `altitudeType` to `MSL` in the envelope
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
centrs devices set 192.0.2.6 --lat 48.8566 --lon 2.3522 --altitude 35 \
|
|
522
|
+
--cdb-file $CDB
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
`ok: true`. `altitude-type` was never written to the comment (only
|
|
526
|
+
`--altitude` was passed), but `devices show 192.0.2.6` still returns
|
|
527
|
+
`data.entry.location.altitudeType = "MSL"` — the metadata-only default applied
|
|
528
|
+
at read time (`deviceLocation` in `src/resolver/geo.ts`).
|
|
529
|
+
|
|
530
|
+
### 45. Setting `lat` without `lon` errors with `input/incomplete-gps`
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
centrs devices set 198.51.100.1 --lat 10 --cdb-file $CDB
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Errors with `input/incomplete-gps` (target `198.51.100.1` carries no prior
|
|
537
|
+
`lon`). No write; no backup written.
|
|
538
|
+
|
|
539
|
+
### 46. An out-of-range `--lat` errors with `input/invalid-coordinate`
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
centrs devices set 192.0.2.5 --lat 200 --lon 0 --cdb-file $CDB
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Errors with `input/invalid-coordinate`. No write; no backup written.
|
|
546
|
+
|
|
547
|
+
### 47. An invalid `--altitude-type` errors with `input/invalid-altitude`
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
centrs devices set 192.0.2.5 --lat 10 --lon 10 --altitude-type wgs84 \
|
|
551
|
+
--cdb-file $CDB
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Errors with `input/invalid-altitude`. No write; no backup written.
|
|
555
|
+
|
|
556
|
+
### 48. Alias flags canonicalize to the stored key; no more `cdb/unknown-option`
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
centrs devices set 192.0.2.5 --latitude 10 --lng 20 --cdb-file $CDB
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
`ok: true`, **no** `cdb/unknown-option` warning — `--latitude`/`--lng` are
|
|
563
|
+
canonicalized to `lat`/`lon` in one place (`canonicalizeGeoKey`,
|
|
564
|
+
`src/resolver/geo.ts`) before storage. `devices show 192.0.2.5` returns
|
|
565
|
+
`data.entry.comment` containing `lat=10` and `lon=20` (the canonical keys), and
|
|
566
|
+
`data.entry.location = { lat: 10, lon: 20 }`.
|
|
567
|
+
|
|
568
|
+
### 49. `list --where lat=<verbatim>` matches the exact stored string
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
centrs devices list --where lat=37.774900 --cdb-file $CDB
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
`ok: true`; matches only the record whose stored `lat` fact is the exact
|
|
575
|
+
string `37.774900` (values are stored verbatim as typed, never reformatted —
|
|
576
|
+
see `README.md`, Location / GPS). `--where lat=37.7749` (numerically equal,
|
|
577
|
+
reformatted) matches nothing.
|
|
578
|
+
|
|
579
|
+
## Geo query (`list --near` / `list --bbox`)
|
|
580
|
+
|
|
581
|
+
`--near`/`--bbox` filter `devices list` by device GPS (great-circle radius or a
|
|
582
|
+
lat-first bounding box). Coordinates are lat-first everywhere (see `README.md`,
|
|
583
|
+
Location / GPS); a geo-less device carries no location and is silently excluded.
|
|
584
|
+
The same predicates ride the shared fan-out grammar on every non-terminal
|
|
585
|
+
command (`execute`/`retrieve`/`api`/`transfer`) — see `docs/CONSTITUTION.md`
|
|
586
|
+
(Target selection).
|
|
587
|
+
|
|
588
|
+
### 50. `list --near <lat>,<lon>,<radius>` selects in-radius devices
|
|
589
|
+
|
|
590
|
+
`192.0.2.5` is in San Francisco and `192.0.2.6` in New York (set via `--gps`);
|
|
591
|
+
the other records carry no GPS.
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
centrs devices list --near 37.7749,-122.4194,50km --cdb-file $CDB
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
`ok: true`; `data` holds only `192.0.2.5`. New York is ~4100 km away (outside the
|
|
598
|
+
50 km radius) and the geo-less records never match. The radius unit is one of
|
|
599
|
+
`m`/`km`/`mi`/`ft`; a bare number is kilometers.
|
|
600
|
+
|
|
601
|
+
### 51. `list --bbox <south>,<west>,<north>,<east>` selects devices in the box
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
centrs devices list --bbox 37.70,-122.52,37.83,-122.35 --cdb-file $CDB
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
`ok: true`; `data` holds only the device whose GPS falls inside the lat-first
|
|
608
|
+
box (edges inclusive). The box must satisfy `south <= north` and `west <= east`
|
|
609
|
+
(no antimeridian wrap), else `input/invalid-bbox`.
|
|
610
|
+
|
|
611
|
+
### 52. `--near`/`--bbox` are `list`-only
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
centrs devices set 192.0.2.5 --near 37.7749,-122.4194,50km --cdb-file $CDB
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Errors with `input/invalid-command`: `--where`/`--near`/`--bbox` *select*
|
|
618
|
+
records for `list`; `add`/`set` name the `<target>` directly. Store GPS with
|
|
619
|
+
`--gps`/`--lat`/`--lon` instead.
|
|
620
|
+
|
|
621
|
+
### 53. An unrelated `set` on a record with a lone `lat` does not fail
|
|
622
|
+
|
|
623
|
+
The lat/lon pairing check fires only when the write actually touches a
|
|
624
|
+
coordinate. A record that already carries a lone `lat` (e.g. a hand-edited or
|
|
625
|
+
imported comment) can still take unrelated updates.
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
centrs devices add 203.0.113.7 --user u --password p \
|
|
629
|
+
--comment "lat=37.7749" --cdb-file $CDB
|
|
630
|
+
centrs devices set 203.0.113.7 timeout=5000 --cdb-file $CDB
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Both succeed: the `add` stores the freeform comment verbatim, and the later
|
|
634
|
+
`set timeout=5000` does **not** raise `input/incomplete-gps` even though the
|
|
635
|
+
record's GPS pair is incomplete. Touching `lat`/`lon` (e.g. `set lat=…`) would
|
|
636
|
+
re-enable the pairing check.
|
|
@@ -6,8 +6,9 @@ CLI-shaped commands — there is no separate `update` command.
|
|
|
6
6
|
|
|
7
7
|
Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
|
|
8
8
|
`docs/MATRIX.md` and `commands/execute/examples.md`, examples 1–11 over REST,
|
|
9
|
-
12–
|
|
10
|
-
`bun run test:integration`)
|
|
9
|
+
12–19 over the native API, 20–22 over mac-telnet, and S1–S4 over ssh, green via
|
|
10
|
+
`bun run test:integration`), including multi-target fan-out (see **Target
|
|
11
|
+
selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
11
12
|
`not-started`. SNMP is retrieve-only and rejects `execute`.
|
|
12
13
|
|
|
13
14
|
## Intent
|
|
@@ -52,6 +53,12 @@ Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
|
|
|
52
53
|
else passes its `detail`/body plus the HTTP status through
|
|
53
54
|
`mapRouterOsError({ transport: "rest-api", httpStatus })`. There is no longer
|
|
54
55
|
a hand-rolled `routeros/*` table on the REST side.
|
|
56
|
+
- **Run-time fault mapping — later (JG-28).** `mapRouterOsError`'s table covers
|
|
57
|
+
the faults seen so far; a known gap is errors that *pass* `:parse`/validation
|
|
58
|
+
but fail at **run time** (a syntactically valid command the router rejects when
|
|
59
|
+
executed). `tikoci/vscode-tikbook` has an error-string mapping for these worth
|
|
60
|
+
porting into `src/core/routeros-errors.ts`. Background research, not yet
|
|
61
|
+
scheduled — captured here so it isn't lost (no GitHub issue by design).
|
|
55
62
|
- Output is *string*-shaped for script-mode; richer parsing is a future
|
|
56
63
|
concern. The envelope must still distinguish RouterOS errors from successful
|
|
57
64
|
runs (a 200 with a RouterOS error string is still an error — see
|
|
@@ -111,8 +118,14 @@ Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
|
|
|
111
118
|
| `--yes` | Confirm write-shaped add/set/remove commands in non-interactive runs. |
|
|
112
119
|
| `--max-bytes <n>` | Byte budget for the rendered envelope; excess output is truncated with a warning + `meta.truncated` (not an error), matching `retrieve`. (Renamed from `--max-results`.) |
|
|
113
120
|
| `--format <text\|json\|yaml>` | Output format (alias `--json`). Defaults to `text`; `CENTRS_FORMAT` sets the default. |
|
|
121
|
+
| `--group <name>` | Run across every CDB record in WinBox group `<name>`. Repeatable, combinable with `<target>` positionals plus `--where`/`--all`/`--default`; de-duped by record index. See **Target selection**. |
|
|
114
122
|
| `--where <attr>=<value>` | Device-class selector — run across every CDB record whose stored fact/comment-kv matches (e.g. `--where board=RB5009 /system/package/check-for-updates`). Repeatable (AND). See constitution: target selection. |
|
|
115
|
-
|
|
|
123
|
+
| `--all` | Run across every CDB record (excludes the reserved `__default__`). See **Target selection**. |
|
|
124
|
+
| `--default` | Select the reserved `__default__` record. See **Target selection**. |
|
|
125
|
+
| `--near <lat>,<lon>,<radius>` | Geo selector — run across CDB records whose GPS is within the great-circle radius (units `m`/`km`/`mi`/`ft`; bare = km). Lat-first. Union predicate; geo-less devices never match. See **Target selection**. |
|
|
126
|
+
| `--bbox <south>,<west>,<north>,<east>` | Geo selector — run across CDB records whose GPS is inside the lat-first bounding box. Union predicate. See **Target selection**. |
|
|
127
|
+
| `--concurrency <n>` | Max in-flight targets during fan-out (integer ≥ 1). Transport-aware default: `rest-api` 8, others 4. |
|
|
128
|
+
| `--` | End-of-options marker. Every token after `--` is taken as the literal RouterOS command, even flag-shaped ones — e.g. `centrs execute $R -- /interface print where disabled=yes`. In fan-out, positional targets come BEFORE `--` and the command AFTER it (`centrs execute r1 r2 -- /system/reboot`). Use it when the command contains tokens that would otherwise be claimed as centrs flags; otherwise the command must be quoted as a single argument. |
|
|
116
129
|
|
|
117
130
|
## SSH key selection
|
|
118
131
|
|
|
@@ -141,6 +154,34 @@ contents, and any inline key material are always sensitive and must be listed in
|
|
|
141
154
|
redacted only if the caller marks them sensitive or the error couples them with
|
|
142
155
|
key material.
|
|
143
156
|
|
|
157
|
+
## Target selection
|
|
158
|
+
|
|
159
|
+
`execute` runs the **same** RouterOS command across a multi-target selection over
|
|
160
|
+
the shared grammar (multiple `<target>` positionals, repeatable `--group`/
|
|
161
|
+
`--where`, the geo selectors `--near`/`--bbox`, `--all`, `--default`). The
|
|
162
|
+
grammar, the locked `FanoutData` envelope,
|
|
163
|
+
the record-order reassembly, and the granular **0/2/1 exit code** are normative in
|
|
164
|
+
[`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar).
|
|
165
|
+
|
|
166
|
+
execute's positional **boundary is `--`** (it differs from `api`/`retrieve`,
|
|
167
|
+
whose last positional is the operation arg, because an execute command is itself
|
|
168
|
+
many tokens):
|
|
169
|
+
|
|
170
|
+
- `execute r1 r2 -- /system/reboot` — targets `[r1, r2]` before `--`, command
|
|
171
|
+
after. **More than one positional target requires `--`.**
|
|
172
|
+
- `execute --group prod /system/resource/print` — selector-only: targets come
|
|
173
|
+
from the selector and every positional is the command (no `--` needed).
|
|
174
|
+
- `execute r1 /system/resource/print` — a single positional target with no
|
|
175
|
+
selector stays **single-target** (legacy split: first positional is the target,
|
|
176
|
+
the rest is the command), never `FanoutData`.
|
|
177
|
+
|
|
178
|
+
Because `execute` is the primary **write** path, a write-shaped fan-out
|
|
179
|
+
(add/set/remove) is gated by **`--yes`**, confirmed **once** up front; without it
|
|
180
|
+
the error names the blast radius (how many routers). Write-ness is determined from
|
|
181
|
+
the command alone (`canonicalizeExecuteCommand` + `isWriteShaped`), so the gate
|
|
182
|
+
fires before any target is dialed. Per-target validation still runs because
|
|
183
|
+
RouterOS schemas can differ by version.
|
|
184
|
+
|
|
144
185
|
## mac-telnet L2 validation
|
|
145
186
|
|
|
146
187
|
Decided (2026-06-06): the real-L2 integration path is `@tikoci/quickchr`'s
|
|
@@ -156,7 +197,7 @@ macOS needs `SO_REUSEPORT`; mcast works on Linux/CI). Grounding: quickchr
|
|
|
156
197
|
|
|
157
198
|
`execute / mac-telnet` is now **`CHR-passed`** end to end over real L2 against
|
|
158
199
|
stock CHR 7.23.1 (`test/integration/mac-telnet-console.test.ts` +
|
|
159
|
-
`mactelnet-l2-bridge.ts`, examples
|
|
200
|
+
`mactelnet-l2-bridge.ts`, examples 20–22): `executeEnvelope` resolves a MAC
|
|
160
201
|
target, the `MacTelnetAdapter` opens a `MacTelnetConsole` over a UDP datagram
|
|
161
202
|
transport, and reads, writes (REST-verified), and validation-rejects all work.
|
|
162
203
|
|
|
@@ -195,9 +236,9 @@ Protocol selection: an unresolved MAC target auto-selects mac-telnet (covered by
|
|
|
195
236
|
## Open shape questions
|
|
196
237
|
|
|
197
238
|
- ~~How to expose multi-line / async / progress output~~ — **resolved:**
|
|
198
|
-
open-ended follow/streaming reads are
|
|
199
|
-
|
|
200
|
-
|
|
239
|
+
open-ended follow/streaming reads are `api … --stream` (NDJSON stream of
|
|
240
|
+
envelopes; see `commands/api/README.md`). `execute` stays single-shot
|
|
241
|
+
read/write; it does not grow a follow mode.
|
|
201
242
|
- ~~Whether `--strict` should reject "succeeded with stderr-like content."~~ —
|
|
202
243
|
**decided:** `--strict` promotes any warning on an otherwise-successful run to
|
|
203
244
|
`ok: false` (nonzero exit), like `-Werror`; the default stays lenient
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Each numbered example is an executable spec, run against a CHR 7.23 router booted
|
|
4
4
|
by `@tikoci/quickchr`: `test/integration/execute.test.ts` covers examples 1–11
|
|
5
|
-
over REST and 12–
|
|
6
|
-
covers examples
|
|
5
|
+
over REST and 12–19 over the native API; `test/integration/mac-telnet-console.test.ts`
|
|
6
|
+
covers examples 20–22 over mac-telnet (real L2 via the `socket-connect` bridge).
|
|
7
7
|
If a line here is not exercised by a test, the test file is wrong; if a line
|
|
8
8
|
passes only with `validate=false`, the **implementation** is wrong (see
|
|
9
9
|
`docs/CONSTITUTION.md`).
|
|
@@ -242,6 +242,23 @@ centrs execute $A '/ip/address/add address=198.51.100.23/32 interface=ether1 com
|
|
|
242
242
|
Envelope: `ok: false`, `error.code=usage/confirmation-required`,
|
|
243
243
|
`meta.via=native-api`, no validation command is sent, and no address is added.
|
|
244
244
|
|
|
245
|
+
### 19. Script-mode command runs through `/execute` with `as-string` (parity with REST)
|
|
246
|
+
|
|
247
|
+
A non-path-shaped console expression over native-api falls through to
|
|
248
|
+
`/execute` the same way REST does (example 4), instead of being refused.
|
|
249
|
+
`as-string` makes native `/execute` block and return the captured output
|
|
250
|
+
rather than scheduling a background job and returning only its id.
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
centrs execute $A ':put [/system/identity/get name]' --via native-api --port $API_PORT --username $U --password $P
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Envelope: `ok: true`, `data.ret` is a string containing the CHR identity (same
|
|
257
|
+
shape and cross-transport value as example 4 over REST), `meta.via=native-api`,
|
|
258
|
+
and `meta.validation.syntax=true`. Regression coverage for the bug where the
|
|
259
|
+
native adapter sent `/execute` without `as-string`, silently discarding
|
|
260
|
+
captured output.
|
|
261
|
+
|
|
245
262
|
## mac-telnet (`--via mac-telnet`)
|
|
246
263
|
|
|
247
264
|
Layer-2 execute over the RouterOS interactive console (UDP/20561). `$MAC` is the
|
|
@@ -252,11 +269,11 @@ is a **single console `:put [:parse …]`** — it reports both `syntax error` a
|
|
|
252
269
|
at once (no `/console/inspect` table parsing). The console opens through a
|
|
253
270
|
first-login license screen (auto-answered) and a ~10s terminal-negotiation stall;
|
|
254
271
|
**a successful write prints nothing** over the console (no `.id`/`ret`, unlike
|
|
255
|
-
REST/native). Examples
|
|
272
|
+
REST/native). Examples 20–22 are green via `bun run test:integration`
|
|
256
273
|
(`test/integration/mac-telnet-console.test.ts`, CHR 7.23.1) over the real L2
|
|
257
274
|
bridge; the console reader is also exercised directly in the same test.
|
|
258
275
|
|
|
259
|
-
###
|
|
276
|
+
### 20. Read a command over mac-telnet
|
|
260
277
|
|
|
261
278
|
```bash
|
|
262
279
|
centrs execute $MAC '/system/identity/print' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P
|
|
@@ -266,7 +283,7 @@ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is the console output an
|
|
|
266
283
|
contains the device identity (cross-checked against REST). `meta.validation.source`
|
|
267
284
|
is `:put [:parse ...] over mac-telnet`.
|
|
268
285
|
|
|
269
|
-
###
|
|
286
|
+
### 21. Write (add) over mac-telnet
|
|
270
287
|
|
|
271
288
|
```bash
|
|
272
289
|
centrs execute $MAC '/ip/address/add address=198.51.100.40/32 interface=ether1' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P --yes
|
|
@@ -276,7 +293,7 @@ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is empty (a successful
|
|
|
276
293
|
console write prints nothing — there is no `.id` to return). A subsequent REST
|
|
277
294
|
read of `/ip/address` shows `198.51.100.40`.
|
|
278
295
|
|
|
279
|
-
###
|
|
296
|
+
### 22. Validation rejects an unknown attribute over mac-telnet
|
|
280
297
|
|
|
281
298
|
```bash
|
|
282
299
|
centrs execute $MAC '/ip/address/add address=198.51.100.41/32 interface=ether1 no-such-arg=x' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P --yes
|
|
@@ -337,6 +354,73 @@ centrs execute 127.0.0.1 '/ip/address/add address=198.51.100.51/32 interface=eth
|
|
|
337
354
|
Envelope: `ok: false`, `error.code=validation/unknown-attribute`, `meta.via=ssh`,
|
|
338
355
|
and no address is added — the console `:parse` gate catches `bogus` pre-mutation.
|
|
339
356
|
|
|
357
|
+
## Target selection (fan-out)
|
|
358
|
+
|
|
359
|
+
These exercise the shared target-selection grammar
|
|
360
|
+
([`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar)),
|
|
361
|
+
run by `test/integration/execute-fanout.test.ts` when CHR integration is enabled.
|
|
362
|
+
`$CDB` has two records in group `fanout-chr`: record 0 is the live CHR (comment
|
|
363
|
+
fact `role=edge`) and record 1 is an unreachable REST URL (`role=core`). Note the
|
|
364
|
+
execute `--` boundary: targets/selectors come before it, the command after.
|
|
365
|
+
|
|
366
|
+
### F1. Group fan-out of a read command
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
centrs execute --group fanout-chr --via rest-api --cdb-file $CDB --json -- /system/resource/print
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`, targets ordered by
|
|
373
|
+
`recordIndex`, `meta.operation.kind = fanout`; the unreachable target is an inner
|
|
374
|
+
`ok: false` (`transport/connection-refused`). Process exit `2`.
|
|
375
|
+
|
|
376
|
+
### F2. Empty / unknown group
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
centrs execute --group no-such-group --via rest-api --cdb-file $CDB --json -- /system/resource/print
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`,
|
|
383
|
+
`warnings` include `cdb/empty-group`. Exit `0`.
|
|
384
|
+
|
|
385
|
+
### F3. `--where` device-class selector (subset)
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
centrs execute --where role=edge --via rest-api --cdb-file $CDB --json -- /system/resource/print
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Selects only record 0: `data.summary = { total: 1, ok: 1, failed: 0 }`,
|
|
392
|
+
`meta.operation.selection.where = ["role=edge"]`, exit `0`.
|
|
393
|
+
|
|
394
|
+
### F4. `--all` (every CDB record)
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
centrs execute --all --via rest-api --cdb-file $CDB --json -- /system/resource/print
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
`data.summary = { total: 2, ok: 1, failed: 1 }`,
|
|
401
|
+
`meta.operation.selection.all = true`, exit `2`.
|
|
402
|
+
|
|
403
|
+
### F5. Write-shaped fan-out without `--yes` is refused (blast radius)
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
centrs execute --group fanout-chr --via rest-api --cdb-file $CDB --json -- /system/identity/set name=fanout-blast
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
A write-shaped fan-out is gated once up front: `ok: false`,
|
|
410
|
+
`error.code = usage/confirmation-required`, the summary names the blast radius
|
|
411
|
+
(`2 router(s)`), exit `1`. Nothing is mutated. Add `--yes` to fan the write out.
|
|
412
|
+
|
|
413
|
+
### F6. Multiple positional targets (ad-hoc literals) before `--`
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
centrs execute $REACHABLE_URL $UNREACHABLE_URL --via rest-api --username $U --password $P --json -- /system/resource/print
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
More than one positional target is fan-out mode (the `--` separates them from the
|
|
420
|
+
command). With no `--cdb-file`, both are ad-hoc literals, labeled by
|
|
421
|
+
`meta.target.input` with no `recordIndex`: `data.summary = { total: 2, ok: 1,
|
|
422
|
+
failed: 1 }`, exit `2`.
|
|
423
|
+
|
|
340
424
|
## Protocol selection notes
|
|
341
425
|
|
|
342
426
|
For an unresolved MAC target, execute auto-selection defaults to mac-telnet
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# retrieve
|
|
2
2
|
|
|
3
|
+
Status: `CHR-passed` over `rest-api` and `native-api`, including multi-target
|
|
4
|
+
fan-out (see **Target selection**). `snmp` is `not-started`. Matches
|
|
5
|
+
`docs/MATRIX.md`.
|
|
6
|
+
|
|
3
7
|
Read RouterOS state. RouterOS menu reads model `<path>/<verb>` where the verb
|
|
4
8
|
is `print`-style (`print`, `get`, and async POST-shaped reads as they're
|
|
5
9
|
added). SNMP reads use `retrieve <router> snmp <oid|MIB name>` and resolve
|
|
@@ -45,7 +49,7 @@ centrs retrieve <router> snmp <oid|MIB name> [flags]
|
|
|
45
49
|
| `--attributes <a,b,c>` | Alias for `--attribute` with comma-separated input. |
|
|
46
50
|
| `--all-attributes` | Equivalent to RouterOS `details=true`. Mutually exclusive with `--attribute(s)`. |
|
|
47
51
|
| `--list-attributes` (alias `--list`) | Return the available attribute names for the path. No `print`/`get` is run. |
|
|
48
|
-
| `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is
|
|
52
|
+
| `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is `api … --stream`. See constitution: protocol selection. |
|
|
49
53
|
| `--query <expr>` | RouterOS-side **row** filter (maps to `.query`), repeatable; returns matching rows as an **array**. This is how you read one row by name (`--query name=ether1`). **Not Implemented** yet — returns `validation/not-implemented`. |
|
|
50
54
|
| `--filter <expr>` | RouterOS row filter; same `.query` mapping and not-implemented handling as `--query`. |
|
|
51
55
|
| `--where <attr>=<value>` | Device-class selector: fan out across CDB records whose stored fact/comment-kv matches (e.g. `--where board=RB5009`). Repeatable (AND). Filters *which devices*, not RouterOS rows. See constitution: target selection. |
|
|
@@ -64,7 +68,11 @@ centrs retrieve <router> snmp <oid|MIB name> [flags]
|
|
|
64
68
|
| `--cdb-file` / `--cdb-password` | Override CDB file location / decrypt password. |
|
|
65
69
|
| `--ros-version <version>` | SNMP MIB lookup only: pin the MikroTik MIB version to cache/download. |
|
|
66
70
|
| `--ros-channel <channel>` | SNMP MIB lookup only: `stable`, `long-term`, `testing`, or `development`; default `stable`. |
|
|
67
|
-
| `--group <name>` | Fan out across every CDB target in WinBox group `<name>`. Repeatable, and combinable with `<router>` positionals plus `--all`/`--default`; the union is de-duped by CDB record index. See **
|
|
71
|
+
| `--group <name>` | Fan out across every CDB target in WinBox group `<name>`. Repeatable, and combinable with `<router>` positionals plus `--all`/`--default`; the union is de-duped by CDB record index. See **Target selection**. |
|
|
72
|
+
| `--all` | Fan out across every CDB record (excludes the reserved `__default__`). See **Target selection**. |
|
|
73
|
+
| `--default` | Select the reserved `__default__` record. See **Target selection**. |
|
|
74
|
+
| `--near <lat>,<lon>,<radius>` | Geo selector: fan out across CDB records whose GPS is within the great-circle radius (units `m`/`km`/`mi`/`ft`; bare = km). Lat-first. Union predicate; geo-less devices never match. See **Target selection**. |
|
|
75
|
+
| `--bbox <south>,<west>,<north>,<east>` | Geo selector: fan out across CDB records whose GPS is inside the lat-first bounding box. Union predicate. See **Target selection**. |
|
|
68
76
|
| `--concurrency <n>` | Max in-flight targets during fanout (integer ≥ 1). Defaults are transport-aware: `rest-api` 8, `native-api` 4. Rejected with `usage/invalid-concurrency` otherwise. |
|
|
69
77
|
|
|
70
78
|
## Validation
|
|
@@ -137,55 +145,33 @@ warning entry — it is not an error. (Earlier text said a byte overflow returns
|
|
|
137
145
|
*error* with the needed size; superseded — truncation is an `ok: true` footnote so
|
|
138
146
|
partial data stays usable.)
|
|
139
147
|
|
|
140
|
-
##
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
target: {},
|
|
168
|
-
via: "rest-api", // null if inner targets disagree on protocol
|
|
169
|
-
settings: { ... },
|
|
170
|
-
operation: {
|
|
171
|
-
kind: "fanout",
|
|
172
|
-
group: "prod-edge",
|
|
173
|
-
concurrency: 8,
|
|
174
|
-
summary: { total: 2, ok: 1, failed: 1 },
|
|
175
|
-
request: { path: "/system/resource", validate: true, format: "json", ... }
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
An empty or unknown group is `ok: true` with
|
|
182
|
-
`data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`, and a
|
|
183
|
-
`cdb/empty-group` warning. Validation runs per target because RouterOS schemas
|
|
184
|
-
can differ by version. Fanout retries each target up to two times only for
|
|
185
|
-
`transport/network` and `transport/connection-closed`; REST 5xx failures are
|
|
186
|
-
mapped to `transport/connection-closed`. It does not retry `routeros/*`,
|
|
187
|
-
`validation/*`, `auth/*`, `cdb/*`, `target/*`, timeouts, DNS, TLS, or refused
|
|
188
|
-
connections.
|
|
148
|
+
## Target selection
|
|
149
|
+
|
|
150
|
+
retrieve fans out over the **shared target-selection grammar** — multiple
|
|
151
|
+
`<router>` positionals, repeatable `--group`/`--where`, the geo selectors
|
|
152
|
+
`--near`/`--bbox`, `--all`, and `--default`, combined as a set and de-duped by
|
|
153
|
+
CDB record index. The grammar, the locked `FanoutData`
|
|
154
|
+
envelope (`data = { summary, targets[] }`, outer `ok` = orchestration success,
|
|
155
|
+
per-target failure is an inner `ok:false`), the record-order reassembly, and the
|
|
156
|
+
granular **0/2/1 exit code** are all normative in
|
|
157
|
+
[`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar).
|
|
158
|
+
The boundary is intent-keyed: a plain single-positional `retrieve <router> <path>`
|
|
159
|
+
stays the single-target envelope; a selector flag or a second positional target
|
|
160
|
+
switches to fan-out.
|
|
161
|
+
|
|
162
|
+
retrieve-specific behavior on top of the shared engine:
|
|
163
|
+
|
|
164
|
+
- The CDB is loaded + decrypted **once** for the whole fan-out; each target is
|
|
165
|
+
then resolved and **validated independently**, because RouterOS schemas can
|
|
166
|
+
differ by version (`/console/inspect` runs per target).
|
|
167
|
+
- An empty or unknown selection is `ok: true` with
|
|
168
|
+
`data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`, a
|
|
169
|
+
`cdb/empty-group` / `cdb/empty-selection` warning, and exit `0`.
|
|
170
|
+
- Each target retries up to two times **only** for `transport/network` and
|
|
171
|
+
`transport/connection-closed` (REST 5xx is mapped to
|
|
172
|
+
`transport/connection-closed`). It does **not** retry `routeros/*`,
|
|
173
|
+
`validation/*`, `auth/*`, `cdb/*`, `target/*`, timeouts, DNS, TLS, or refused
|
|
174
|
+
connections.
|
|
189
175
|
|
|
190
176
|
## Definition of done
|
|
191
177
|
|
|
@@ -162,15 +162,16 @@ Call succeeds with a `cdb/password-not-needed` warning in `meta.warnings`.
|
|
|
162
162
|
centrs retrieve $R /system/resource --cdb-file $CDB --cdb-password ignored --json
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
##
|
|
165
|
+
## Target selection (fan-out)
|
|
166
166
|
|
|
167
|
-
These
|
|
168
|
-
|
|
167
|
+
These exercise the shared target-selection grammar
|
|
168
|
+
([`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar)).
|
|
169
|
+
`test/integration/fanout-retrieve.test.ts` runs them when CHR integration is
|
|
170
|
+
enabled. `$CDB` contains two records in group `fanout-chr`: record 0 is the live
|
|
171
|
+
CHR (comment fact `role=edge`) and record 1 is an unreachable REST URL (comment
|
|
172
|
+
fact `role=core`).
|
|
169
173
|
|
|
170
|
-
### F1. Group
|
|
171
|
-
|
|
172
|
-
`$CDB` contains two records in group `fanout-chr`: record 0 is the live CHR and
|
|
173
|
-
record 1 is an unreachable REST URL.
|
|
174
|
+
### F1. Group fan-out with one inner success and one inner failure
|
|
174
175
|
|
|
175
176
|
```bash
|
|
176
177
|
centrs retrieve --group fanout-chr /system/resource --cdb-file $CDB --json
|
|
@@ -179,7 +180,7 @@ centrs retrieve --group fanout-chr /system/resource --cdb-file $CDB --json
|
|
|
179
180
|
Outer envelope: `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`,
|
|
180
181
|
`data.targets` is ordered by CDB `recordIndex`, and `meta.operation.kind` is
|
|
181
182
|
`fanout`. The unreachable target is an inner `ok: false` envelope with
|
|
182
|
-
`transport/connection-refused`.
|
|
183
|
+
`transport/connection-refused`. Process exit `2` (partial).
|
|
183
184
|
|
|
184
185
|
### F2. Empty / unknown group
|
|
185
186
|
|
|
@@ -188,7 +189,37 @@ centrs retrieve --group no-such-group /system/resource --cdb-file $CDB --json
|
|
|
188
189
|
```
|
|
189
190
|
|
|
190
191
|
Envelope: `ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`,
|
|
191
|
-
`data.targets = []`, and warnings include `cdb/empty-group`.
|
|
192
|
+
`data.targets = []`, and warnings include `cdb/empty-group`. Exit `0`.
|
|
193
|
+
|
|
194
|
+
### F3. `--where` device-class selector (subset)
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
centrs retrieve --where role=edge /system/resource --cdb-file $CDB --json
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`--where` matches the raw comment fact, selecting only record 0:
|
|
201
|
+
`data.summary = { total: 1, ok: 1, failed: 0 }`,
|
|
202
|
+
`meta.operation.selection.where = ["role=edge"]`, exit `0`.
|
|
203
|
+
|
|
204
|
+
### F4. `--all` (every CDB record)
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
centrs retrieve --all /system/resource --cdb-file $CDB --json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`--all` fans across every record (excluding `__default__`) — here both, so
|
|
211
|
+
`data.summary = { total: 2, ok: 1, failed: 1 }`,
|
|
212
|
+
`meta.operation.selection.all = true`, exit `2`.
|
|
213
|
+
|
|
214
|
+
### F5. Multiple positional targets (ad-hoc literals)
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
centrs retrieve $REACHABLE_URL $UNREACHABLE_URL /system/resource --username $U --password $P --json
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
More than one positional target is fan-out mode without any selector flag. With
|
|
221
|
+
no `--cdb-file`, both are ad-hoc literals, labeled by `meta.target.input` with no
|
|
222
|
+
`recordIndex`: `data.summary = { total: 2, ok: 1, failed: 1 }`, exit `2`.
|
|
192
223
|
|
|
193
224
|
## Format
|
|
194
225
|
|