@tikoci/centrs 0.1.2 → 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 +59 -0
- package/GLOSSARY.txt +71 -0
- package/README.md +24 -8
- package/SECURITY.md +3 -3
- package/commands/api/README.md +27 -9
- package/commands/api/examples.md +109 -6
- 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 +39 -4
- package/commands/execute/examples.md +90 -6
- package/commands/retrieve/README.md +36 -50
- 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 +36 -11
- package/docs/MATRIX.md +68 -172
- 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/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/package.json +6 -6
- package/src/api-fanout.ts +454 -0
- package/src/api.ts +91 -19
- package/src/cli/api.ts +135 -11
- package/src/cli/btest.ts +20 -2
- package/src/cli/common.ts +11 -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 +7 -0
- package/src/core/error-catalog.ts +55 -0
- package/src/core/fanout.ts +143 -2
- package/src/devices.ts +212 -20
- package/src/discover.ts +7 -2
- package/src/execute-fanout.ts +438 -0
- package/src/execute.ts +120 -23
- package/src/index.ts +105 -1
- package/src/mcp/config.ts +12 -2
- package/src/mcp/tools.ts +137 -2
- package/src/protocols/adapter.ts +38 -18
- package/src/protocols/index.ts +11 -5
- 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 +225 -133
- package/src/retrieve.ts +39 -7
- 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/src/retrieve-fanout.ts
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Multi-target fan-out for `retrieve`.
|
|
3
3
|
*
|
|
4
|
-
* A `--group
|
|
5
|
-
* through the single-target
|
|
4
|
+
* A target selection (`--group` / `--where` / `--all` / `--default` / multiple
|
|
5
|
+
* positionals) expands to N members and runs each through the single-target
|
|
6
|
+
* retrieve tail (`buildResolvedRetrieve` → `runResolvedRetrieve`), collecting a
|
|
6
7
|
* per-target inner `CentrsEnvelope` into the LOCKED `FanoutData` shape from
|
|
7
8
|
* `src/core/envelope.ts`. The outer envelope's `ok` means the orchestration
|
|
8
9
|
* produced a complete per-target result set; a per-target failure is an INNER
|
|
9
10
|
* `ok: false` envelope, never an outer failure and never a thrown error out of
|
|
10
11
|
* the batch.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
13
|
+
* Boundaries (see `commands/retrieve/README.md`, `docs/CONSTITUTION.md`), shared
|
|
14
|
+
* with `api-fanout.ts`:
|
|
15
|
+
* - The CDB is loaded + decrypted ONCE (`expandCdbSelection`); a cdb member is
|
|
16
|
+
* resolved with its pre-resolved record, so `resolveCdb`'s `__default__`
|
|
17
|
+
* synthetic fallback never collides ad-hoc literals.
|
|
18
|
+
* - A `--default` / `__default__` member is guarded from being dialed as the
|
|
19
|
+
* literal hostname `"__default__"` — it fails that one target deterministically
|
|
20
|
+
* with `target/unresolved`.
|
|
21
|
+
* - Each target is resolved + validated independently (validation is per-target
|
|
22
|
+
* via live `/console/inspect`; schemas differ by version).
|
|
16
23
|
* - Concurrency is a bounded worker pool; defaults are transport-aware
|
|
17
24
|
* (REST 8, native-api 4) and overridable with `--concurrency`.
|
|
18
|
-
* -
|
|
19
|
-
*
|
|
20
|
-
* - `data.targets[]` is ordered by CDB `recordIndex` regardless of completion
|
|
21
|
-
* order; `summary` counts are derived from inner `ok`.
|
|
25
|
+
* - `data.targets[]` is ordered by CDB `recordIndex` (literals appended in
|
|
26
|
+
* positional order); `summary` counts are derived from inner `ok`.
|
|
22
27
|
*
|
|
23
|
-
* Outer `ok: false` is reserved for
|
|
28
|
+
* Outer `ok: false` is reserved for fan-out failing BEFORE reliable per-target
|
|
24
29
|
* results exist (bad request shape, CDB decrypt failure); those throw out of
|
|
25
|
-
* {@link
|
|
30
|
+
* {@link retrieveFanout} and the CLI renders them with
|
|
26
31
|
* {@link buildRetrieveFanoutErrorEnvelope}.
|
|
27
32
|
*/
|
|
28
33
|
|
|
@@ -33,19 +38,24 @@ import type {
|
|
|
33
38
|
FanoutSummary,
|
|
34
39
|
} from "./core/envelope.ts";
|
|
35
40
|
import {
|
|
41
|
+
buildFanoutEnvelope,
|
|
42
|
+
buildFanoutResolveFailure,
|
|
43
|
+
commonVia,
|
|
36
44
|
defaultFanoutSleep,
|
|
37
45
|
resolveFanoutConcurrency,
|
|
38
|
-
|
|
39
|
-
runWithRetry,
|
|
46
|
+
runFanout,
|
|
40
47
|
summarizeFanout,
|
|
41
48
|
} from "./core/fanout.ts";
|
|
42
49
|
import { CentrsError, serializeCentrsError } from "./errors.ts";
|
|
43
50
|
import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
|
|
44
51
|
import {
|
|
45
|
-
type
|
|
46
|
-
type
|
|
47
|
-
type
|
|
48
|
-
|
|
52
|
+
type CdbSelectionExpansion,
|
|
53
|
+
type CdbSelectionMember,
|
|
54
|
+
type CdbSelectionResolveInput,
|
|
55
|
+
expandCdbSelection,
|
|
56
|
+
isDefaultRecordTarget,
|
|
57
|
+
loadEnvFileDefaults,
|
|
58
|
+
type TargetSelection,
|
|
49
59
|
} from "./resolver/index.ts";
|
|
50
60
|
import {
|
|
51
61
|
buildResolvedRetrieve,
|
|
@@ -59,17 +69,27 @@ import {
|
|
|
59
69
|
type RetrieveSuccessEnvelope,
|
|
60
70
|
resolveMacForRetrieve,
|
|
61
71
|
resolveRetrieveGlobalContext,
|
|
72
|
+
resolveRetrieveRequest,
|
|
62
73
|
runResolvedRetrieve,
|
|
63
74
|
toYaml,
|
|
64
75
|
validateRetrieveRequestShape,
|
|
65
76
|
} from "./retrieve.ts";
|
|
66
77
|
|
|
78
|
+
/** Compact description of the selector that produced the fan-out. */
|
|
79
|
+
export interface RetrieveSelectionSummary {
|
|
80
|
+
groups: readonly string[];
|
|
81
|
+
where: readonly string[];
|
|
82
|
+
all: boolean;
|
|
83
|
+
default: boolean;
|
|
84
|
+
positionals: readonly string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
export interface RetrieveFanoutOperationMeta {
|
|
68
88
|
kind: "fanout";
|
|
69
|
-
group: string;
|
|
70
89
|
concurrency: number;
|
|
71
90
|
summary: FanoutSummary;
|
|
72
91
|
request: RetrieveRequestSummary;
|
|
92
|
+
selection: RetrieveSelectionSummary;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
export type RetrieveFanoutData = FanoutData<unknown, RetrieveOperationMeta>;
|
|
@@ -81,60 +101,109 @@ export type RetrieveFanoutErrorEnvelope =
|
|
|
81
101
|
CentrsErrorEnvelope<RetrieveFanoutOperationMeta>;
|
|
82
102
|
|
|
83
103
|
/** Test/override seams; production callers pass nothing. */
|
|
84
|
-
export interface
|
|
104
|
+
export interface RetrieveFanoutInternals {
|
|
85
105
|
/** Per-target executor (attempts; throws on failure). Defaults to the live tail. */
|
|
86
106
|
execute?: (
|
|
87
107
|
resolved: ResolvedRetrieveRequest,
|
|
88
|
-
recordIndex: number,
|
|
89
108
|
) => Promise<RetrieveSuccessEnvelope>;
|
|
90
|
-
/**
|
|
109
|
+
/** Selection expansion. Defaults to the real CDB-backed expansion. */
|
|
91
110
|
expand?: (
|
|
92
|
-
|
|
111
|
+
selection: TargetSelection,
|
|
112
|
+
input: CdbSelectionResolveInput,
|
|
93
113
|
env: Record<string, string | undefined>,
|
|
94
|
-
|
|
114
|
+
config?: Record<string, string | undefined>,
|
|
115
|
+
) => Promise<CdbSelectionExpansion>;
|
|
95
116
|
/** Backoff sleeper. Defaults to a real timer; tests pass a no-op. */
|
|
96
117
|
sleep?: (ms: number) => Promise<void>;
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
export interface RetrieveFanoutOptions {
|
|
121
|
+
concurrency?: number;
|
|
122
|
+
/** CLI: true (literal positionals allowed). MCP: false (CDB is the allowlist). */
|
|
123
|
+
allowAdhoc?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function summarizeSelection(
|
|
127
|
+
selection: TargetSelection,
|
|
128
|
+
): RetrieveSelectionSummary {
|
|
129
|
+
return {
|
|
130
|
+
groups: selection.groups,
|
|
131
|
+
where: selection.where.map((clause) => `${clause.key}=${clause.value}`),
|
|
132
|
+
all: selection.all,
|
|
133
|
+
default: selection.default,
|
|
134
|
+
positionals: selection.positionals,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function memberTargetMeta(member: CdbSelectionMember): {
|
|
139
|
+
input?: string;
|
|
140
|
+
identity?: string;
|
|
141
|
+
recordIndex?: number;
|
|
142
|
+
} {
|
|
143
|
+
if (member.kind === "cdb") {
|
|
144
|
+
return {
|
|
145
|
+
input: member.resolution.identity,
|
|
146
|
+
identity: member.resolution.identity,
|
|
147
|
+
recordIndex: member.recordIndex,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { input: member.input };
|
|
151
|
+
}
|
|
152
|
+
|
|
99
153
|
/**
|
|
100
|
-
*
|
|
154
|
+
* Ad-hoc literal targets have no CDB record. `resolveRetrieveRequest` may borrow
|
|
155
|
+
* the `__default__` record's index when filling fallback creds, so the inner
|
|
156
|
+
* envelope would carry that borrowed `recordIndex` — making distinct literals
|
|
157
|
+
* collide on one index. Drop it for literal members and stamp the caller's `input`
|
|
158
|
+
* so the target stays identifiable in renderers (mirrors `api-fanout.ts`).
|
|
159
|
+
*/
|
|
160
|
+
function relabelAdhocMember(
|
|
161
|
+
envelope: RetrieveEnvelope,
|
|
162
|
+
member: CdbSelectionMember,
|
|
163
|
+
): RetrieveEnvelope {
|
|
164
|
+
if (member.kind !== "literal") {
|
|
165
|
+
return envelope;
|
|
166
|
+
}
|
|
167
|
+
const { recordIndex: _borrowed, ...target } = envelope.meta.target;
|
|
168
|
+
return {
|
|
169
|
+
...envelope,
|
|
170
|
+
meta: { ...envelope.meta, target: { ...target, input: member.input } },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Run a `retrieve` fan-out. Always returns a success envelope when the
|
|
101
176
|
* orchestration completes (even if every target failed); throws only for
|
|
102
177
|
* pre-flight failures (bad request shape, CDB decrypt) the caller renders as an
|
|
103
178
|
* outer error.
|
|
104
179
|
*/
|
|
105
|
-
export async function
|
|
180
|
+
export async function retrieveFanout(
|
|
106
181
|
request: RetrieveRequest,
|
|
182
|
+
selection: TargetSelection,
|
|
107
183
|
env: Record<string, string | undefined> = Bun.env,
|
|
108
|
-
internals:
|
|
184
|
+
internals: RetrieveFanoutInternals = {},
|
|
185
|
+
options: RetrieveFanoutOptions = {},
|
|
109
186
|
): Promise<RetrieveFanoutEnvelope> {
|
|
110
|
-
const group = request.group;
|
|
111
|
-
if (group === undefined || group.length === 0) {
|
|
112
|
-
throw new CentrsError({
|
|
113
|
-
code: "usage/missing-group",
|
|
114
|
-
summary: "retrieve group fanout requires a non-empty `--group` value.",
|
|
115
|
-
remediation:
|
|
116
|
-
"Pass `--group <name>` matching a CDB group, e.g. `centrs retrieve --group prod /system/resource`.",
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
187
|
const attributeSelections = validateRetrieveRequestShape(request);
|
|
188
|
+
const config = await loadEnvFileDefaults(env);
|
|
121
189
|
const global = resolveRetrieveGlobalContext(
|
|
122
190
|
request,
|
|
123
191
|
env,
|
|
124
192
|
attributeSelections,
|
|
193
|
+
config,
|
|
125
194
|
);
|
|
126
|
-
|
|
127
|
-
resolveFanoutConcurrency(request.concurrency, global.via);
|
|
128
|
-
}
|
|
195
|
+
const selectionSummary = summarizeSelection(selection);
|
|
129
196
|
|
|
130
|
-
const expand = internals.expand ??
|
|
197
|
+
const expand = internals.expand ?? expandCdbSelection;
|
|
131
198
|
const expansion = await expand(
|
|
199
|
+
selection,
|
|
132
200
|
{
|
|
133
|
-
group,
|
|
134
201
|
cdbFile: request.cdbFile,
|
|
135
202
|
cdbPassword: request.cdbPassword,
|
|
203
|
+
allowAdhoc: options.allowAdhoc ?? true,
|
|
136
204
|
},
|
|
137
205
|
env,
|
|
206
|
+
config,
|
|
138
207
|
);
|
|
139
208
|
|
|
140
209
|
const concurrencyVia = resolveFanoutConcurrencyProtocol(
|
|
@@ -144,13 +213,13 @@ export async function retrieveGroup(
|
|
|
144
213
|
expansion,
|
|
145
214
|
);
|
|
146
215
|
const concurrency = resolveFanoutConcurrency(
|
|
147
|
-
request.concurrency,
|
|
216
|
+
options.concurrency ?? request.concurrency,
|
|
148
217
|
concurrencyVia,
|
|
149
218
|
);
|
|
150
219
|
|
|
151
220
|
if (expansion.empty) {
|
|
152
|
-
return
|
|
153
|
-
|
|
221
|
+
return retrieveFanoutEnvelope({
|
|
222
|
+
selection: selectionSummary,
|
|
154
223
|
concurrency,
|
|
155
224
|
summary: { total: 0, ok: 0, failed: 0 },
|
|
156
225
|
requestSummary: global.summary,
|
|
@@ -164,40 +233,68 @@ export async function retrieveGroup(
|
|
|
164
233
|
const execute = internals.execute ?? runResolvedRetrieve;
|
|
165
234
|
const sleep = internals.sleep ?? defaultFanoutSleep;
|
|
166
235
|
|
|
167
|
-
const targets = await
|
|
168
|
-
|
|
236
|
+
const targets = await runFanout<
|
|
237
|
+
CdbSelectionMember,
|
|
238
|
+
ResolvedRetrieveRequest,
|
|
239
|
+
RetrieveEnvelope
|
|
240
|
+
>({
|
|
241
|
+
members: expansion.targets,
|
|
169
242
|
concurrency,
|
|
170
|
-
async (member)
|
|
171
|
-
|
|
172
|
-
|
|
243
|
+
resolve: async (member) => {
|
|
244
|
+
if (member.kind === "cdb") {
|
|
245
|
+
if (isDefaultRecordTarget(member.resolution.target)) {
|
|
246
|
+
throw new CentrsError({
|
|
247
|
+
code: "target/unresolved",
|
|
248
|
+
summary:
|
|
249
|
+
"The `__default__` record is a credential fallback, not a connectable router.",
|
|
250
|
+
remediation:
|
|
251
|
+
"Select real devices with `--group` / `--where` / `--all` or a positional; `--default` cannot be a RouterOS target.",
|
|
252
|
+
context: { target: member.resolution.target },
|
|
253
|
+
});
|
|
254
|
+
}
|
|
173
255
|
const macResolution = await resolveMacForRetrieve(
|
|
174
256
|
request,
|
|
175
257
|
env,
|
|
176
258
|
member.resolution,
|
|
259
|
+
config,
|
|
177
260
|
);
|
|
178
|
-
|
|
261
|
+
return buildResolvedRetrieve(
|
|
179
262
|
request,
|
|
180
263
|
env,
|
|
181
264
|
member.resolution,
|
|
182
265
|
attributeSelections,
|
|
183
266
|
macResolution,
|
|
267
|
+
config,
|
|
184
268
|
);
|
|
185
|
-
} catch (error) {
|
|
186
|
-
return buildResolveFailureEnvelope(member.resolution, error);
|
|
187
269
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{
|
|
270
|
+
// Ad-hoc literal: full single-target resolution (CDB load → `__default__`
|
|
271
|
+
// fallback creds), mirroring single-target `retrieve`.
|
|
272
|
+
return resolveRetrieveRequest(
|
|
273
|
+
{ ...request, targetInput: member.input },
|
|
274
|
+
env,
|
|
192
275
|
);
|
|
193
276
|
},
|
|
194
|
-
|
|
277
|
+
onResolveError: (member, error) =>
|
|
278
|
+
buildFanoutResolveFailure<RetrieveOperationMeta>({
|
|
279
|
+
error,
|
|
280
|
+
target: memberTargetMeta(member),
|
|
281
|
+
warnings: member.kind === "cdb" ? [...member.resolution.warnings] : [],
|
|
282
|
+
summary: "Failed to resolve a fanout target.",
|
|
283
|
+
}),
|
|
284
|
+
execute: async (resolved, member) =>
|
|
285
|
+
relabelAdhocMember(await execute(resolved), member),
|
|
286
|
+
onExecuteError: (resolved, member, error) =>
|
|
287
|
+
relabelAdhocMember(
|
|
288
|
+
buildRetrieveErrorEnvelopeFromResolved(resolved, error),
|
|
289
|
+
member,
|
|
290
|
+
),
|
|
291
|
+
sleep,
|
|
292
|
+
});
|
|
195
293
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
group,
|
|
294
|
+
return retrieveFanoutEnvelope({
|
|
295
|
+
selection: selectionSummary,
|
|
199
296
|
concurrency,
|
|
200
|
-
summary,
|
|
297
|
+
summary: summarizeFanout(targets),
|
|
201
298
|
requestSummary: global.summary,
|
|
202
299
|
settings: global.settings,
|
|
203
300
|
via: commonVia(targets),
|
|
@@ -206,6 +303,37 @@ export async function retrieveGroup(
|
|
|
206
303
|
});
|
|
207
304
|
}
|
|
208
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Back-compat shim for the `--group`-only fan-out: builds a single-group
|
|
308
|
+
* {@link TargetSelection} and delegates to {@link retrieveFanout}. Kept for the
|
|
309
|
+
* MCP `centrs_retrieve` tool, whose surface exposes only `group`.
|
|
310
|
+
*/
|
|
311
|
+
export async function retrieveGroup(
|
|
312
|
+
request: RetrieveRequest,
|
|
313
|
+
env: Record<string, string | undefined> = Bun.env,
|
|
314
|
+
internals: RetrieveFanoutInternals = {},
|
|
315
|
+
): Promise<RetrieveFanoutEnvelope> {
|
|
316
|
+
const group = request.group;
|
|
317
|
+
if (group === undefined || group.length === 0) {
|
|
318
|
+
throw new CentrsError({
|
|
319
|
+
code: "usage/missing-group",
|
|
320
|
+
summary: "retrieve group fanout requires a non-empty `--group` value.",
|
|
321
|
+
remediation:
|
|
322
|
+
"Pass `--group <name>` matching a CDB group, e.g. `centrs retrieve --group prod /system/resource`.",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const selection: TargetSelection = {
|
|
326
|
+
positionals: [],
|
|
327
|
+
groups: [group],
|
|
328
|
+
all: false,
|
|
329
|
+
default: false,
|
|
330
|
+
where: [],
|
|
331
|
+
};
|
|
332
|
+
return retrieveFanout(request, selection, env, internals, {
|
|
333
|
+
concurrency: request.concurrency,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
209
337
|
/** Outer error envelope for a fanout that failed before per-target results. */
|
|
210
338
|
export function buildRetrieveFanoutErrorEnvelope(
|
|
211
339
|
request: RetrieveRequest,
|
|
@@ -216,7 +344,7 @@ export function buildRetrieveFanoutErrorEnvelope(
|
|
|
216
344
|
? error
|
|
217
345
|
: new CentrsError({
|
|
218
346
|
code: "internal/unhandled",
|
|
219
|
-
summary: "retrieve
|
|
347
|
+
summary: "retrieve fan-out failed with an unexpected error.",
|
|
220
348
|
remediation:
|
|
221
349
|
"Re-run with `--format json` to capture the structured error details for debugging.",
|
|
222
350
|
cause: error,
|
|
@@ -245,14 +373,20 @@ function resolveFanoutConcurrencyProtocol(
|
|
|
245
373
|
request: RetrieveRequest,
|
|
246
374
|
env: Record<string, string | undefined>,
|
|
247
375
|
globalVia: RouterOsProtocol,
|
|
248
|
-
expansion:
|
|
376
|
+
expansion: CdbSelectionExpansion,
|
|
249
377
|
): RouterOsProtocol {
|
|
378
|
+
// Deliberately CLI/env only, not `config` — a `centrs.env` default is the
|
|
379
|
+
// weakest precedence tier and must not be treated as a hard pin the way an
|
|
380
|
+
// explicit `--via`/`CENTRS_VIA` is; doing so would defeat per-target CDB
|
|
381
|
+
// `via=` overrides for members that disagree with the global default.
|
|
250
382
|
if (request.via !== undefined || env["CENTRS_VIA"] !== undefined) {
|
|
251
383
|
return globalVia;
|
|
252
384
|
}
|
|
253
385
|
if (
|
|
254
386
|
expansion.targets.some(
|
|
255
|
-
(
|
|
387
|
+
(member) =>
|
|
388
|
+
member.kind === "cdb" &&
|
|
389
|
+
member.resolution.overrides.via?.value === "native-api",
|
|
256
390
|
)
|
|
257
391
|
) {
|
|
258
392
|
return "native-api";
|
|
@@ -260,37 +394,8 @@ function resolveFanoutConcurrencyProtocol(
|
|
|
260
394
|
return globalVia;
|
|
261
395
|
}
|
|
262
396
|
|
|
263
|
-
function buildResolveFailureEnvelope(
|
|
264
|
-
resolution: CdbResolution,
|
|
265
|
-
error: unknown,
|
|
266
|
-
): RetrieveEnvelope {
|
|
267
|
-
const centrsError =
|
|
268
|
-
error instanceof CentrsError
|
|
269
|
-
? error
|
|
270
|
-
: new CentrsError({
|
|
271
|
-
code: "internal/unhandled",
|
|
272
|
-
summary: "Failed to resolve a fanout target.",
|
|
273
|
-
cause: error,
|
|
274
|
-
});
|
|
275
|
-
return {
|
|
276
|
-
ok: false,
|
|
277
|
-
error: serializeCentrsError(centrsError),
|
|
278
|
-
warnings: [...resolution.warnings],
|
|
279
|
-
tips: [],
|
|
280
|
-
meta: {
|
|
281
|
-
target: {
|
|
282
|
-
input: resolution.identity,
|
|
283
|
-
identity: resolution.identity,
|
|
284
|
-
recordIndex: resolution.recordIndex,
|
|
285
|
-
},
|
|
286
|
-
via: null,
|
|
287
|
-
settings: {},
|
|
288
|
-
},
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
397
|
interface FanoutEnvelopeInput {
|
|
293
|
-
|
|
398
|
+
selection: RetrieveSelectionSummary;
|
|
294
399
|
concurrency: number;
|
|
295
400
|
summary: FanoutSummary;
|
|
296
401
|
requestSummary: RetrieveRequestSummary;
|
|
@@ -300,45 +405,28 @@ interface FanoutEnvelopeInput {
|
|
|
300
405
|
targets: readonly RetrieveEnvelope[];
|
|
301
406
|
}
|
|
302
407
|
|
|
303
|
-
|
|
408
|
+
/** retrieve's outer fan-out success envelope (delegates to the shared core). */
|
|
409
|
+
function retrieveFanoutEnvelope(
|
|
304
410
|
input: FanoutEnvelopeInput,
|
|
305
411
|
): RetrieveFanoutEnvelope {
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
412
|
+
return buildFanoutEnvelope<
|
|
413
|
+
unknown,
|
|
414
|
+
RetrieveOperationMeta,
|
|
415
|
+
RetrieveFanoutOperationMeta
|
|
416
|
+
>({
|
|
417
|
+
summary: input.summary,
|
|
418
|
+
targets: input.targets,
|
|
312
419
|
warnings: input.warnings,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
concurrency: input.concurrency,
|
|
322
|
-
summary: input.summary,
|
|
323
|
-
request: input.requestSummary,
|
|
324
|
-
},
|
|
420
|
+
settings: input.settings,
|
|
421
|
+
via: input.via,
|
|
422
|
+
operation: {
|
|
423
|
+
kind: "fanout",
|
|
424
|
+
concurrency: input.concurrency,
|
|
425
|
+
summary: input.summary,
|
|
426
|
+
request: input.requestSummary,
|
|
427
|
+
selection: input.selection,
|
|
325
428
|
},
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function commonVia(
|
|
330
|
-
targets: readonly RetrieveEnvelope[],
|
|
331
|
-
): RouterOsProtocol | null {
|
|
332
|
-
let via: RouterOsProtocol | null | undefined;
|
|
333
|
-
for (const target of targets) {
|
|
334
|
-
const candidate = target.meta.via;
|
|
335
|
-
if (via === undefined) {
|
|
336
|
-
via = candidate;
|
|
337
|
-
} else if (via !== candidate) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return via ?? null;
|
|
429
|
+
});
|
|
342
430
|
}
|
|
343
431
|
|
|
344
432
|
/**
|
|
@@ -376,12 +464,16 @@ function renderFanoutText(
|
|
|
376
464
|
const operation = envelope.meta.operation;
|
|
377
465
|
const { total, ok, failed } = envelope.data.summary;
|
|
378
466
|
const lines: string[] = [
|
|
379
|
-
`
|
|
467
|
+
`retrieve ${operation?.request.path ?? "?"}: ${ok}/${total} ok, ${failed} failed (concurrency ${operation?.concurrency ?? "?"})`,
|
|
380
468
|
];
|
|
381
469
|
|
|
382
470
|
for (const target of envelope.data.targets) {
|
|
383
471
|
const meta = target.meta;
|
|
384
|
-
const label =
|
|
472
|
+
const label =
|
|
473
|
+
meta.target.identity ??
|
|
474
|
+
meta.target.host ??
|
|
475
|
+
meta.target.input ??
|
|
476
|
+
"(unknown)";
|
|
385
477
|
const index = meta.target.recordIndex ?? "-";
|
|
386
478
|
if (target.ok) {
|
|
387
479
|
lines.push(
|