omp-conductor 0.19.5 → 0.19.6
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/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/command-help.ts +18 -8
- package/src/command-manifest.ts +22 -5
- package/src/commands/context.ts +1 -0
- package/src/commands/report.ts +63 -0
- package/src/commands/restore-db.ts +1 -1
- package/src/commands/snapshot-db.ts +67 -0
- package/src/commands/upgrade-install.ts +1 -1
- package/src/commands/upgrade.ts +2 -2
- package/src/commands/verb.ts +1 -0
- package/src/commands/watch.ts +14 -2
- package/src/config.ts +17 -0
- package/src/daemon.ts +23 -3
- package/src/doctor.ts +36 -3
- package/src/fleet.ts +51 -3
- package/src/orchestrator-tick.ts +14 -3
- package/src/setup-host.ts +10 -10
- package/src/setup-wizard.ts +1 -1
- package/src/status-render.ts +10 -0
- package/src/store.ts +296 -22
- package/src/types.ts +32 -3
- package/src/upgrade-journal.ts +35 -1
- package/src/upgrade-verify.ts +27 -6
- package/src/upgrade.ts +301 -22
- package/src/verbs/actions.ts +6 -1
- package/src/verbs/server.ts +54 -20
- package/systemd/omp-conductor-recover.sh +55 -7
- package/systemd/recover-unit-test.sh +99 -6
|
@@ -61,6 +61,7 @@ PROG=omp-conductor-recover
|
|
|
61
61
|
SYSTEMCTL=${SYSTEMCTL_BIN:-systemctl}
|
|
62
62
|
JOURNALCTL=${JOURNALCTL_BIN:-journalctl}
|
|
63
63
|
CONDUCTOR=${OMP_CONDUCTOR_BIN:-omp-conductor}
|
|
64
|
+
BUN=${BUN_BIN:-bun}
|
|
64
65
|
|
|
65
66
|
# The fleet's conductor home: where config.json, the sqlite store and the
|
|
66
67
|
# upgrade's durable snapshots live. The unit renders OMP_CONDUCTOR_HOME and
|
|
@@ -84,6 +85,12 @@ JOURNAL_LINES=${RECOVER_JOURNAL_LINES:-200}
|
|
|
84
85
|
VERIFY_POLL_S=${RECOVER_VERIFY_POLL_S:-2}
|
|
85
86
|
VERIFY_TRIES=${RECOVER_VERIFY_TRIES:-10}
|
|
86
87
|
PROJECT=${RECOVER_PROJECT:-}
|
|
88
|
+
# Recovery action scope and durable-report ownership are deliberately separate.
|
|
89
|
+
# `RECOVER_PROJECT` is only safe on a single-project host; the report still
|
|
90
|
+
# needs one configured project to own its outbox row on a shared host.
|
|
91
|
+
REPORT_PROJECT=${RECOVER_REPORT_PROJECT:-$PROJECT}
|
|
92
|
+
AFFECTED_PROJECTS=${RECOVER_PROJECTS:-$PROJECT}
|
|
93
|
+
REPORT_ROUTING_NOTE=
|
|
87
94
|
# The bound on the post-restore re-arm. `arm` with a claim-only proof performs
|
|
88
95
|
# no Telegram send or wait, so it completes in milliseconds; the timeout exists
|
|
89
96
|
# so a misbehaving conductor can never hang this bounded playbook (#613).
|
|
@@ -190,6 +197,40 @@ append_evidence() { # <evidence path> <text>
|
|
|
190
197
|
printf '%s\n' "$2" >>"$1" 2>/dev/null || true
|
|
191
198
|
}
|
|
192
199
|
|
|
200
|
+
|
|
201
|
+
# Resolve the host-global report owner from the live machine-written config.
|
|
202
|
+
# Bun is not an extra prerequisite: omp-conductor itself runs under Bun, and
|
|
203
|
+
# the recovery unit's canonical PATH already contains that executable. When a
|
|
204
|
+
# manual/degraded invocation cannot read the config, the script still attempts
|
|
205
|
+
# the legacy unscoped report and records that degradation in the evidence.
|
|
206
|
+
resolve_report_scope() {
|
|
207
|
+
if [[ -n $REPORT_PROJECT && -n $AFFECTED_PROJECTS ]]; then
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
local lines first all
|
|
211
|
+
if lines=$(
|
|
212
|
+
"$BUN" -e '
|
|
213
|
+
const path = Bun.argv.at(-1);
|
|
214
|
+
const config = await Bun.file(path).json();
|
|
215
|
+
const names = Array.isArray(config.projects)
|
|
216
|
+
? config.projects.map((project) => project?.name).filter((name) => typeof name === "string" && name.length > 0)
|
|
217
|
+
: [];
|
|
218
|
+
if (names.length === 0) process.exit(2);
|
|
219
|
+
console.log(names[0]);
|
|
220
|
+
console.log(names.join(","));
|
|
221
|
+
' "$CONFIG_PATH" 2>/dev/null
|
|
222
|
+
); then
|
|
223
|
+
first=${lines%%$'\n'*}
|
|
224
|
+
all=${lines#*$'\n'}
|
|
225
|
+
REPORT_PROJECT=${REPORT_PROJECT:-$first}
|
|
226
|
+
AFFECTED_PROJECTS=${AFFECTED_PROJECTS:-$all}
|
|
227
|
+
fi
|
|
228
|
+
if [[ -z $REPORT_PROJECT || -z $AFFECTED_PROJECTS ]]; then
|
|
229
|
+
AFFECTED_PROJECTS=${AFFECTED_PROJECTS:-unknown}
|
|
230
|
+
REPORT_ROUTING_NOTE="configured project resolution failed for $CONFIG_PATH; attempting the report without a project selector"
|
|
231
|
+
fi
|
|
232
|
+
return 0
|
|
233
|
+
}
|
|
193
234
|
# --------------------------------------------------------------------------
|
|
194
235
|
# escalation — always, through the durable outbox (#123, #288)
|
|
195
236
|
# --------------------------------------------------------------------------
|
|
@@ -199,23 +240,27 @@ append_evidence() { # <evidence path> <text>
|
|
|
199
240
|
# (the durable outbox) before anything is sent, and the daemon owns delivery
|
|
200
241
|
# with bounded retries once it is back. This script only ever enqueues.
|
|
201
242
|
escalation_text() { # <failed-unit or empty> <action> <outcome> <evidence>
|
|
243
|
+
local projects=${AFFECTED_PROJECTS:-unknown}
|
|
202
244
|
if [[ ${1:-none} == none ]]; then
|
|
203
245
|
printf '%s\n' \
|
|
204
|
-
"fleet recovery (#485): a recovery trigger fired but no fleet unit was failing when it ran — the fleet got itself up or was started externally; evidence=$4"
|
|
246
|
+
"fleet recovery (#485): a recovery trigger fired but no fleet unit was failing when it ran — the fleet got itself up or was started externally; affected-projects=$projects; evidence=$4"
|
|
205
247
|
return
|
|
206
248
|
fi
|
|
207
249
|
printf '%s\n' \
|
|
208
|
-
"fleet recovery (#485): $1 entered failed state; recovery=${2:-none}; outcome=${3}; evidence=${4}; next: read the evidence file (and its doctor output) before touching the fleet"
|
|
250
|
+
"fleet recovery (#485): $1 entered failed state; affected-projects=$projects; recovery=${2:-none}; outcome=${3}; evidence=${4}; next: read the evidence file (and its doctor output) before touching the fleet"
|
|
209
251
|
}
|
|
210
252
|
|
|
211
253
|
escalate() { # <failed-unit or empty> <action-name> <outcome> <evidence-path>
|
|
212
254
|
local unit=${1:-none} text args
|
|
213
|
-
|
|
214
|
-
if [[ -n $
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
args=("$CONDUCTOR" report --kind tier2 --text "$text")
|
|
255
|
+
resolve_report_scope
|
|
256
|
+
if [[ -n $REPORT_ROUTING_NOTE ]]; then
|
|
257
|
+
log "$REPORT_ROUTING_NOTE"
|
|
258
|
+
append_evidence "$4" "report routing: $REPORT_ROUTING_NOTE"
|
|
218
259
|
fi
|
|
260
|
+
text=$(escalation_text "$unit" "$2" "$3" "$4")
|
|
261
|
+
args=("$CONDUCTOR" report --kind tier2)
|
|
262
|
+
[[ -n $REPORT_PROJECT ]] && args+=(--project "$REPORT_PROJECT")
|
|
263
|
+
args+=(--text "$text")
|
|
219
264
|
say "escalation: ${args[*]}"
|
|
220
265
|
if dry_run; then
|
|
221
266
|
return 0
|
|
@@ -365,6 +410,9 @@ run_recovery() {
|
|
|
365
410
|
done
|
|
366
411
|
|
|
367
412
|
if dry_run; then
|
|
413
|
+
resolve_report_scope
|
|
414
|
+
say "plan: report owner — ${REPORT_PROJECT:-unscoped}; affected projects — $AFFECTED_PROJECTS"
|
|
415
|
+
[[ -n $REPORT_ROUTING_NOTE ]] && say "plan: report routing degraded — $REPORT_ROUTING_NOTE"
|
|
368
416
|
say "dry run: nothing will be written or executed"
|
|
369
417
|
[[ -z $failed ]] && say "plan: no unit failed — escalate only"
|
|
370
418
|
if [[ -n $failed ]]; then
|
|
@@ -50,6 +50,7 @@ fi
|
|
|
50
50
|
|
|
51
51
|
tmp=$(mktemp -d "${TMPDIR:-/tmp}/omp-recover-test.XXXXXX")
|
|
52
52
|
trap 'rm -rf "$tmp"' EXIT
|
|
53
|
+
bun_bin=$(command -v bun)
|
|
53
54
|
mkdir -p "$tmp/bin"
|
|
54
55
|
|
|
55
56
|
# --------------------------------------------------------------------------
|
|
@@ -156,6 +157,18 @@ write_project_config() { # <case-dir> <proof>
|
|
|
156
157
|
printf '{\n "version": 2,\n "projects": [\n { "name": "demo", "arm": { "proof": "%s" } }\n ]\n}\n' "$2" >"$1/state/config.json"
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
write_multi_project_config() { # <case-dir>
|
|
161
|
+
cat >"$1/state/config.json" <<'JSON'
|
|
162
|
+
{
|
|
163
|
+
"version": 2,
|
|
164
|
+
"projects": [
|
|
165
|
+
{ "name": "alpha" },
|
|
166
|
+
{ "name": "beta" }
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
JSON
|
|
170
|
+
}
|
|
171
|
+
|
|
159
172
|
run_recover() { # <case-dir> [env assignments…]
|
|
160
173
|
local d="$1"
|
|
161
174
|
shift
|
|
@@ -182,7 +195,9 @@ run_recover() { # <case-dir> [env assignments…]
|
|
|
182
195
|
# marks the named unit active, which is what a real peer restart does to a
|
|
183
196
|
# healthy peer), so tests wanting daemon-side healing pass HEAL_ON_START=<unit>
|
|
184
197
|
# and the stub's `start` flips that unit back to active.
|
|
185
|
-
run_with_heal() { # <case-dir> <heal-on-start>
|
|
198
|
+
run_with_heal() { # <case-dir> <heal-on-start> [env assignments…]
|
|
199
|
+
local d="$1" heal="$2"
|
|
200
|
+
shift 2
|
|
186
201
|
(
|
|
187
202
|
cd "$here" || exit 1
|
|
188
203
|
env \
|
|
@@ -190,14 +205,15 @@ run_with_heal() { # <case-dir> <heal-on-start>
|
|
|
190
205
|
SYSTEMCTL_BIN="$tmp/bin/systemctl" \
|
|
191
206
|
JOURNALCTL_BIN="$tmp/bin/journalctl" \
|
|
192
207
|
OMP_CONDUCTOR_BIN="$tmp/bin/omp-conductor" \
|
|
193
|
-
RECOVER_STATE_DIR="$
|
|
208
|
+
RECOVER_STATE_DIR="$d/state" \
|
|
194
209
|
RECOVER_PROJECT=demo \
|
|
195
210
|
RECOVER_VERIFY_POLL_S=0 \
|
|
196
211
|
RECOVER_VERIFY_TRIES=3 \
|
|
197
|
-
STUB_CALLS="$
|
|
198
|
-
STUB_UNITS="$
|
|
199
|
-
STUB_HEAL_ON_START="$
|
|
200
|
-
|
|
212
|
+
STUB_CALLS="$d/calls" \
|
|
213
|
+
STUB_UNITS="$d/units" \
|
|
214
|
+
STUB_HEAL_ON_START="$heal" \
|
|
215
|
+
"$@" \
|
|
216
|
+
bash "$recover" >"$d/out" 2>"$d/stderr"
|
|
201
217
|
)
|
|
202
218
|
echo $?
|
|
203
219
|
}
|
|
@@ -412,6 +428,83 @@ check 'a trigger with nothing failed escalates (fleet came back)' \
|
|
|
412
428
|
check 'a trigger with nothing failed exits 0' '0' "$code"
|
|
413
429
|
check 'a trigger with nothing failed clears the old counter' '0' "$(marker "$d")"
|
|
414
430
|
|
|
431
|
+
# ---------------------------------------------------------------------------
|
|
432
|
+
# case — a host-global recovery on a multi-project config still has one
|
|
433
|
+
# deterministic outbox owner, while its body names every affected project.
|
|
434
|
+
# ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
d=$(newcase multi-project-report)
|
|
437
|
+
write_multi_project_config "$d"
|
|
438
|
+
unit_state "$d" omp-conductor.service active
|
|
439
|
+
unit_state "$d" herdr-fleet.service active
|
|
440
|
+
code=$(run_recover "$d" \
|
|
441
|
+
RECOVER_PROJECT= \
|
|
442
|
+
RECOVER_REPORT_PROJECT= \
|
|
443
|
+
RECOVER_PROJECTS= \
|
|
444
|
+
BUN_BIN="$bun_bin")
|
|
445
|
+
report_call=$(grep '^omp-conductor report' "$d/calls" | head -n 1)
|
|
446
|
+
check 'multi-project recovery exits after queuing its report' '0' "$code"
|
|
447
|
+
check 'multi-project recovery uses the first configured outbox owner' \
|
|
448
|
+
'--project alpha' "$report_call"
|
|
449
|
+
check 'multi-project recovery names every affected project' \
|
|
450
|
+
'affected-projects=alpha,beta' "$report_call"
|
|
451
|
+
check 'multi-project recovery queues exactly one report' \
|
|
452
|
+
'1' "$(grep -c '^omp-conductor report' "$d/calls")"
|
|
453
|
+
|
|
454
|
+
d=$(newcase multi-project-recovery-action)
|
|
455
|
+
write_multi_project_config "$d"
|
|
456
|
+
unit_state "$d" omp-conductor.service active
|
|
457
|
+
unit_state "$d" herdr-fleet.service failed
|
|
458
|
+
code=$(run_with_heal "$d" herdr-fleet.service \
|
|
459
|
+
RECOVER_PROJECT= \
|
|
460
|
+
RECOVER_REPORT_PROJECT= \
|
|
461
|
+
RECOVER_PROJECTS= \
|
|
462
|
+
BUN_BIN="$bun_bin")
|
|
463
|
+
report_call=$(grep '^omp-conductor report' "$d/calls" | head -n 1)
|
|
464
|
+
check 'verified multi-project recovery action exits cleanly' '0' "$code"
|
|
465
|
+
check 'verified multi-project recovery action queues one scoped report' \
|
|
466
|
+
'1' "$(grep -c '^omp-conductor report' "$d/calls")"
|
|
467
|
+
check 'verified multi-project recovery action uses configured owner' \
|
|
468
|
+
'--project alpha' "$report_call"
|
|
469
|
+
check 'verified multi-project recovery action names every project' \
|
|
470
|
+
'affected-projects=alpha,beta' "$report_call"
|
|
471
|
+
|
|
472
|
+
d=$(newcase multi-project-dry-run)
|
|
473
|
+
write_multi_project_config "$d"
|
|
474
|
+
unit_state "$d" omp-conductor.service active
|
|
475
|
+
unit_state "$d" herdr-fleet.service failed
|
|
476
|
+
code=$(run_recover "$d" \
|
|
477
|
+
RECOVER_PROJECT= \
|
|
478
|
+
RECOVER_REPORT_PROJECT= \
|
|
479
|
+
RECOVER_PROJECTS= \
|
|
480
|
+
RECOVER_DRY_RUN=1 \
|
|
481
|
+
BUN_BIN="$bun_bin")
|
|
482
|
+
check 'multi-project dry run exits cleanly' '0' "$code"
|
|
483
|
+
check 'multi-project dry run names its report owner' \
|
|
484
|
+
'plan: report owner — alpha' "$(cat "$d/out")"
|
|
485
|
+
check 'multi-project dry run names every affected project' \
|
|
486
|
+
'affected projects — alpha,beta' "$(cat "$d/out")"
|
|
487
|
+
|
|
488
|
+
d=$(newcase report-routing-fallback)
|
|
489
|
+
unit_state "$d" omp-conductor.service active
|
|
490
|
+
unit_state "$d" herdr-fleet.service active
|
|
491
|
+
code=$(run_recover "$d" \
|
|
492
|
+
RECOVER_PROJECT= \
|
|
493
|
+
RECOVER_REPORT_PROJECT= \
|
|
494
|
+
RECOVER_PROJECTS= \
|
|
495
|
+
BUN_BIN="$bun_bin")
|
|
496
|
+
report_call=$(grep '^omp-conductor report' "$d/calls" | head -n 1)
|
|
497
|
+
check 'unreadable config routing still attempts a durable report' \
|
|
498
|
+
'omp-conductor report --kind tier2 --text' "$report_call"
|
|
499
|
+
if [[ $report_call == *"--project "* ]]; then
|
|
500
|
+
fail "routing fallback does not guess an unconfigured project — $report_call"
|
|
501
|
+
else
|
|
502
|
+
pass 'routing fallback does not guess an unconfigured project'
|
|
503
|
+
fi
|
|
504
|
+
check 'routing fallback remains a successful recovered run' '0' "$code"
|
|
505
|
+
check 'routing fallback is preserved in durable evidence' \
|
|
506
|
+
'attempting the report without a project selector' "$(cat "$d/state/recovery"/evidence-*)"
|
|
507
|
+
|
|
415
508
|
# ---------------------------------------------------------------------------
|
|
416
509
|
# case DRY-RUN — the operator's rehearsal mode prints decisions and mutates
|
|
417
510
|
# nothing, not even the recovery dir or the fixture call log.
|