omp-conductor 0.15.9 → 0.15.11
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/README.md +273 -2543
- package/REFERENCE.md +2638 -0
- package/package.json +3 -2
- package/schema/config.schema.json +8 -23
- package/src/arm-challenge.ts +112 -0
- package/src/ask.ts +434 -0
- package/src/board.ts +81 -15
- package/src/brief-upgrade.ts +114 -8
- package/src/briefs/orchestrator.md +55 -29
- package/src/briefs/policy.md +14 -5
- package/src/briefs/worker.md +7 -1
- package/src/chain-check.ts +1 -1
- package/src/check-trailing-newlines.ts +82 -0
- package/src/cli.ts +190 -1391
- package/src/commands/arm.ts +21 -0
- package/src/commands/board.ts +23 -0
- package/src/commands/brief-upgrade.ts +186 -0
- package/src/commands/context.ts +49 -0
- package/src/commands/daemon.ts +71 -0
- package/src/commands/dashboard.ts +74 -0
- package/src/commands/decision.ts +103 -0
- package/src/commands/disarm.ts +21 -0
- package/src/commands/doctor.ts +98 -0
- package/src/commands/event.ts +62 -0
- package/src/commands/extend.ts +64 -0
- package/src/commands/friction.ts +56 -0
- package/src/commands/help.ts +9 -0
- package/src/commands/hold.ts +26 -0
- package/src/commands/intake.ts +134 -0
- package/src/commands/ledger.ts +69 -0
- package/src/commands/message.ts +48 -0
- package/src/commands/report.ts +170 -0
- package/src/commands/restart.ts +76 -0
- package/src/commands/resume.ts +58 -0
- package/src/commands/setup.ts +93 -0
- package/src/commands/start.ts +23 -0
- package/src/commands/stats.ts +131 -0
- package/src/commands/status.ts +48 -0
- package/src/commands/stop.ts +51 -0
- package/src/commands/tail.ts +109 -0
- package/src/commands/unblock.ts +39 -0
- package/src/commands/upgrade-install.ts +31 -0
- package/src/commands/upgrade-rollback.ts +23 -0
- package/src/commands/upgrade.ts +25 -0
- package/src/commands/verb.ts +83 -0
- package/src/commands/version.ts +30 -0
- package/src/commands/worker.ts +100 -0
- package/src/config-schema.ts +38 -1
- package/src/config.ts +10 -3
- package/src/daemon.ts +613 -94
- package/src/dashboard/app.js +120 -0
- package/src/dashboard/index.html +34 -0
- package/src/dashboard/server.ts +267 -0
- package/src/dashboard/style.css +180 -0
- package/src/decisions.ts +39 -14
- package/src/diff-flags.ts +131 -241
- package/src/doctor.ts +795 -0
- package/src/escalate.ts +60 -19
- package/src/failure-class.ts +29 -3
- package/src/fleet.ts +58 -1
- package/src/graph-health.ts +1 -1
- package/src/label-projection.ts +1 -1
- package/src/lifecycle.ts +198 -2
- package/src/notices.ts +9 -0
- package/src/omp.ts +2 -0
- package/src/orchestrator-tick.ts +315 -17
- package/src/release-policy.ts +135 -23
- package/src/reports.ts +19 -5
- package/src/setup-host.ts +420 -8
- package/src/setup-install.ts +69 -14
- package/src/setup-wizard.ts +199 -61
- package/src/setup.ts +131 -35
- package/src/stats.ts +331 -0
- package/src/store.ts +206 -21
- package/src/tracker/github.ts +27 -4
- package/src/types.ts +144 -31
- package/src/unblock.ts +55 -11
- package/src/upgrade-journal.ts +220 -0
- package/src/upgrade-verify.ts +506 -0
- package/src/upgrade.ts +295 -26
- package/src/verbs/actions.ts +73 -1
- package/src/verbs/protocol.ts +29 -4
- package/src/verbs/server.ts +183 -20
- package/systemd/omp-conductor-recover.sh +433 -0
- package/systemd/omp-conductor.service.example +7 -0
- package/systemd/recover-unit-test.sh +428 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# omp-conductor fleet recovery — offline tests for omp/systemd/omp-conductor-recover.sh
|
|
4
|
+
#
|
|
5
|
+
# No network, no real systemctl/journalctl/omp-conductor, and never a touch on
|
|
6
|
+
# this host's units. Every external call goes through stub binaries on a
|
|
7
|
+
# fixture PATH; a fixture state dir drives the unit state machine, so what is
|
|
8
|
+
# under test is the playbook's decision: evidence written before anything,
|
|
9
|
+
# exactly one bounded action from the fixed set, always an escalation through
|
|
10
|
+
# the outbox, and no loop.
|
|
11
|
+
#
|
|
12
|
+
# Prints one line per case and exits non-zero if any of them fails.
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
17
|
+
recover="$here/omp-conductor-recover.sh"
|
|
18
|
+
|
|
19
|
+
failures=0
|
|
20
|
+
pass() { printf 'ok %s\n' "$1"; }
|
|
21
|
+
fail() {
|
|
22
|
+
printf 'FAIL %s\n' "$1"
|
|
23
|
+
failures=$((failures + 1))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
check() { # <name> <expected substring> <actual>
|
|
27
|
+
if [[ $3 == *"$2"* ]]; then
|
|
28
|
+
pass "$1"
|
|
29
|
+
else
|
|
30
|
+
fail "$1"
|
|
31
|
+
printf ' expected substring: %s\n' "$2"
|
|
32
|
+
printf ' actual: %s\n' "$3"
|
|
33
|
+
fi
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------
|
|
37
|
+
# syntax
|
|
38
|
+
# --------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
syntax_errors=$(
|
|
41
|
+
bash -n "$recover" 2>&1
|
|
42
|
+
bash -n "$here/recover-unit-test.sh" 2>&1
|
|
43
|
+
)
|
|
44
|
+
if [[ -z $syntax_errors ]]; then
|
|
45
|
+
pass 'syntax: recovery playbook and test suite'
|
|
46
|
+
else
|
|
47
|
+
fail 'syntax: recovery playbook and test suite'
|
|
48
|
+
printf '%s\n' "$syntax_errors"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
tmp=$(mktemp -d "${TMPDIR:-/tmp}/omp-recover-test.XXXXXX")
|
|
52
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
53
|
+
mkdir -p "$tmp/bin"
|
|
54
|
+
|
|
55
|
+
# --------------------------------------------------------------------------
|
|
56
|
+
# fixture stubs. The systemctl stub reads per-unit state files so tests can
|
|
57
|
+
# decide which unit is failed and whether `start` heals it; every call is
|
|
58
|
+
# logged so tests pin call ORDER (evidence before action) and the single
|
|
59
|
+
# action rule.
|
|
60
|
+
# --------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
cat >"$tmp/bin/systemctl" <<'STUB'
|
|
63
|
+
#!/usr/bin/env bash
|
|
64
|
+
echo "systemctl $*" >>"$STUB_CALLS"
|
|
65
|
+
unit="$2"
|
|
66
|
+
state_path="$STUB_UNITS/$unit"
|
|
67
|
+
state=$(cat "$state_path" 2>/dev/null || echo absent)
|
|
68
|
+
case "$1" in
|
|
69
|
+
is-failed)
|
|
70
|
+
[[ $state == failed ]] && echo failed || echo inactive ;;
|
|
71
|
+
is-active)
|
|
72
|
+
if [[ $state == failed ]]; then echo failed; elif [[ $state == absent ]]; then echo inactive; else echo active; fi ;;
|
|
73
|
+
reset-failed) : ;;
|
|
74
|
+
start)
|
|
75
|
+
[[ $unit == "$STUB_HEAL_ON_START" ]] && echo active > "$state_path" ;;
|
|
76
|
+
restart)
|
|
77
|
+
echo active > "$state_path" ;;
|
|
78
|
+
*)
|
|
79
|
+
echo "unexpected systemctl $*" >&2 ;;
|
|
80
|
+
esac
|
|
81
|
+
STUB
|
|
82
|
+
|
|
83
|
+
cat >"$tmp/bin/journalctl" <<'STUB'
|
|
84
|
+
#!/usr/bin/env bash
|
|
85
|
+
echo "journalctl $*" >>"$STUB_CALLS"
|
|
86
|
+
echo "journal: fleet crashed here"
|
|
87
|
+
echo "journal: last line before death"
|
|
88
|
+
STUB
|
|
89
|
+
|
|
90
|
+
cat >"$tmp/bin/omp-conductor" <<'STUB'
|
|
91
|
+
#!/usr/bin/env bash
|
|
92
|
+
echo "omp-conductor $*" >>"$STUB_CALLS"
|
|
93
|
+
case "$1" in
|
|
94
|
+
doctor)
|
|
95
|
+
printf '%s\n' '{"project":"demo","status":"fail","findings":[{"id":"systemd-unit","status":"fail","summary":"installed differs"}]}'
|
|
96
|
+
;;
|
|
97
|
+
--version)
|
|
98
|
+
echo "0.15.11"
|
|
99
|
+
;;
|
|
100
|
+
report)
|
|
101
|
+
echo "report 485-abc queued for demo (tier2)"
|
|
102
|
+
;;
|
|
103
|
+
*)
|
|
104
|
+
exit 2 ;;
|
|
105
|
+
esac
|
|
106
|
+
STUB
|
|
107
|
+
|
|
108
|
+
chmod +x "$tmp/bin/"*
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------------
|
|
111
|
+
# fixture helpers
|
|
112
|
+
# --------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
newcase() { # <name> -> case dir
|
|
115
|
+
local d="$tmp/case-$1"
|
|
116
|
+
mkdir -p "$d/state/backups/config" "$d/units"
|
|
117
|
+
printf '%s\n' "$d"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
unit_state() { # <case-dir> <unit> <state|absent>
|
|
121
|
+
if [[ $3 == absent ]]; then
|
|
122
|
+
rm -f "$1/units/$2"
|
|
123
|
+
else
|
|
124
|
+
printf '%s\n' "$3" >"$1/units/$2"
|
|
125
|
+
fi
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# A pre-upgrade snapshot `upgrade` durably keeps (configBackupDir()):
|
|
129
|
+
# config.json.pre-upgrade-<ts>. The test pass the age in seconds.
|
|
130
|
+
write_snapshot() { # <case-dir> <age-seconds>
|
|
131
|
+
local d="$1" age="${2:-0}" now
|
|
132
|
+
now=$(date +%s)
|
|
133
|
+
local snap="$d/state/backups/config/config.json.pre-upgrade-$now"
|
|
134
|
+
printf '%s\n' '{"preUpgrade":true}' >"$snap"
|
|
135
|
+
if (( age > 0 )); then
|
|
136
|
+
touch -d "@$(( now - age ))" "$snap"
|
|
137
|
+
fi
|
|
138
|
+
printf '%s\n' "$snap"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
marker() { # <case-dir> -> attempts count or 0
|
|
142
|
+
local f="$1/state/recovery/attempts"
|
|
143
|
+
if [[ -f $f ]]; then
|
|
144
|
+
sed -n '1p' "$f"
|
|
145
|
+
else
|
|
146
|
+
echo 0
|
|
147
|
+
fi
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
run_recover() { # <case-dir> [env assignments…]
|
|
151
|
+
local d="$1"
|
|
152
|
+
shift
|
|
153
|
+
(
|
|
154
|
+
cd "$here" || exit 1
|
|
155
|
+
env \
|
|
156
|
+
PATH="$tmp/bin:/usr/bin:/bin" \
|
|
157
|
+
SYSTEMCTL_BIN="$tmp/bin/systemctl" \
|
|
158
|
+
JOURNALCTL_BIN="$tmp/bin/journalctl" \
|
|
159
|
+
OMP_CONDUCTOR_BIN="$tmp/bin/omp-conductor" \
|
|
160
|
+
RECOVER_STATE_DIR="$d/state" \
|
|
161
|
+
RECOVER_PROJECT=demo \
|
|
162
|
+
RECOVER_VERIFY_POLL_S=0 \
|
|
163
|
+
RECOVER_VERIFY_TRIES=3 \
|
|
164
|
+
STUB_CALLS="$d/calls" \
|
|
165
|
+
STUB_UNITS="$d/units" \
|
|
166
|
+
"$@" \
|
|
167
|
+
bash "$recover" >"$d/out" 2>"$d/stderr"
|
|
168
|
+
)
|
|
169
|
+
echo $?
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# `systemctl restart` never heals the FAILED unit in this fixture (it only
|
|
173
|
+
# marks the named unit active, which is what a real peer restart does to a
|
|
174
|
+
# healthy peer), so tests wanting daemon-side healing pass HEAL_ON_START=<unit>
|
|
175
|
+
# and the stub's `start` flips that unit back to active.
|
|
176
|
+
run_with_heal() { # <case-dir> <heal-on-start>
|
|
177
|
+
(
|
|
178
|
+
cd "$here" || exit 1
|
|
179
|
+
env \
|
|
180
|
+
PATH="$tmp/bin:/usr/bin:/bin" \
|
|
181
|
+
SYSTEMCTL_BIN="$tmp/bin/systemctl" \
|
|
182
|
+
JOURNALCTL_BIN="$tmp/bin/journalctl" \
|
|
183
|
+
OMP_CONDUCTOR_BIN="$tmp/bin/omp-conductor" \
|
|
184
|
+
RECOVER_STATE_DIR="$1/state" \
|
|
185
|
+
RECOVER_PROJECT=demo \
|
|
186
|
+
RECOVER_VERIFY_POLL_S=0 \
|
|
187
|
+
RECOVER_VERIFY_TRIES=3 \
|
|
188
|
+
STUB_CALLS="$1/calls" \
|
|
189
|
+
STUB_UNITS="$1/units" \
|
|
190
|
+
STUB_HEAL_ON_START="$2" \
|
|
191
|
+
bash "$recover" >"$1/out" 2>"$1/stderr"
|
|
192
|
+
)
|
|
193
|
+
echo $?
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# --------------------------------------------------------------------------
|
|
197
|
+
# the decision functions, driven directly. The unit text holds the other half
|
|
198
|
+
# of the bounds (StartLimitBurst=2, no OnFailure=) — setup-host.test.ts pins it.
|
|
199
|
+
# --------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
SCAFFOLD=$(newcase decisions)
|
|
202
|
+
FLEET_UNITS=herdr-fleet.service
|
|
203
|
+
RECOVER_STATE_DIR="$SCAFFOLD/state"
|
|
204
|
+
. "$recover"
|
|
205
|
+
|
|
206
|
+
# RECOVER_PANE is the only action for a failed herdr-fleet unit: herdr's own
|
|
207
|
+
# startup hook recovers the fleet pane, so the pane leg is `start herdr`.
|
|
208
|
+
check 'herdr failure chooses RECOVER_PANE' \
|
|
209
|
+
'RECOVER_PANE|herdr-fleet.service' "$(choose_action herdr-fleet.service)"
|
|
210
|
+
|
|
211
|
+
D2=$(newcase decisions-daemon)
|
|
212
|
+
RECOVER_STATE_DIR="$D2/state"
|
|
213
|
+
BACKUP_ROOT="$D2/state/backups/config"
|
|
214
|
+
|
|
215
|
+
# The daemon's action hinges on a fresh pre-upgrade snapshot: with one, the
|
|
216
|
+
# mechanical conclusion is "the upgrade did it" and the action is ROLLBACK.
|
|
217
|
+
write_snapshot "$D2" 0 >/dev/null
|
|
218
|
+
check 'a fresh pre-upgrade snapshot chooses ROLLBACK' \
|
|
219
|
+
"ROLLBACK|$BACKUP_ROOT/config.json.pre-upgrade-" "$(choose_action omp-conductor.service)"
|
|
220
|
+
|
|
221
|
+
# Without one, the only safe motion is restarting the peer unit.
|
|
222
|
+
rm -f "$BACKUP_ROOT"/config.json.pre-upgrade-*
|
|
223
|
+
check 'no snapshot chooses RESTART_PEER' \
|
|
224
|
+
'RESTART_PEER|herdr-fleet.service' "$(choose_action omp-conductor.service)"
|
|
225
|
+
|
|
226
|
+
# A snapshot too old to implicate THIS failure is not evidence.
|
|
227
|
+
write_snapshot "$D2" 7200 >/dev/null
|
|
228
|
+
check 'a stale snapshot does not choose ROLLBACK' \
|
|
229
|
+
'RESTART_PEER|herdr-fleet.service' "$(choose_action omp-conductor.service)"
|
|
230
|
+
unset FLEET_UNITS RECOVER_STATE_DIR BACKUP_ROOT
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# case A — RECOVER_PANE: herdr-fleet failed and the start heals it in time.
|
|
234
|
+
# Evidence must be written before a single systemd mutation is attempted.
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
d=$(newcase recover-pane)
|
|
238
|
+
unit_state "$d" herdr-fleet.service failed
|
|
239
|
+
unit_state "$d" omp-conductor.service active
|
|
240
|
+
code=$(run_with_heal "$d" herdr-fleet.service)
|
|
241
|
+
check 'RECOVER_PANE exits 0 when the unit heals' '0' "$code"
|
|
242
|
+
check 'RECOVER_PANE reports success and names the pane hook' \
|
|
243
|
+
'startup hook recovers the fleet pane' "$(grep 'outcome:' "$d/out")"
|
|
244
|
+
|
|
245
|
+
# Order the acceptance pins: journal + doctor + version (evidence) strictly
|
|
246
|
+
# before the first systemd mutation (reset-failed/start).
|
|
247
|
+
journal_line=$(grep -n '^journalctl' "$d/calls" | head -n 1 | cut -d: -f1 | tr -d ' ')
|
|
248
|
+
reset_line=$(grep -n '^systemctl reset-failed' "$d/calls" | head -n 1 | cut -d: -f1 | tr -d ' ')
|
|
249
|
+
if [[ -n $journal_line && -n $reset_line && $journal_line -lt $reset_line ]]; then
|
|
250
|
+
pass 'evidence collection precedes the single action'
|
|
251
|
+
else
|
|
252
|
+
fail "evidence collection precedes the single action (journal=$journal_line reset=$reset_line)"
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
# Exactly one restorative action: one reset-failed + one start, no restart.
|
|
256
|
+
starts=$(grep -c '^systemctl start ' "$d/calls" || true)
|
|
257
|
+
resets=$(grep -c '^systemctl reset-failed ' "$d/calls" || true)
|
|
258
|
+
restarts=$(grep -c '^systemctl restart ' "$d/calls" || true)
|
|
259
|
+
if [[ $starts == 1 && $resets == 1 && $restarts == 0 ]]; then
|
|
260
|
+
pass 'exactly one restorative action (start herdr-fleet)'
|
|
261
|
+
else
|
|
262
|
+
fail "exactly one restorative action — starts=$starts resets=$resets restarts=$restarts"
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
check 'successful recovery still escalates tier-2 through the outbox' \
|
|
266
|
+
'omp-conductor report --kind tier2' "$(grep '^omp-conductor report' "$d/calls" | head -n 1)"
|
|
267
|
+
check 'the escalation names the durable evidence file' \
|
|
268
|
+
"$d/state/recovery/evidence-" "$(grep '^omp-conductor report' "$d/calls" | head -n 1)"
|
|
269
|
+
# The evidence bundle itself carries the collected facts.
|
|
270
|
+
ev=$(ls "$d/state/recovery"/evidence-* | head -n 1)
|
|
271
|
+
check 'the evidence bundle holds unit state, doctor JSON, versions and journal' \
|
|
272
|
+
'=== omp-conductor fleet recovery evidence' "$(head -n 1 "$ev")"
|
|
273
|
+
check 'the evidence bundle names the failed unit' \
|
|
274
|
+
'herdr-fleet.service' "$(grep '^failed unit:' "$ev")"
|
|
275
|
+
check 'the evidence bundle carries the doctor output' \
|
|
276
|
+
'"systemd-unit"' "$(grep 'installed differs' "$ev")"
|
|
277
|
+
check 'the evidence bundle carries the journal tail' \
|
|
278
|
+
'journal: last line before death' "$(grep 'journal:' "$ev")"
|
|
279
|
+
|
|
280
|
+
[[ $(marker "$d") == 0 ]]
|
|
281
|
+
pass 'the attempts counter is cleared after a successful recovery'
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# case ROLLBACK: daemon failed AND a fresh pre-upgrade snapshot exists.
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
d=$(newcase rollback)
|
|
288
|
+
write_snapshot "$d" 5 >/dev/null
|
|
289
|
+
printf '%s\n' '{"live":true}' >"$d/state/config.json"
|
|
290
|
+
unit_state "$d" omp-conductor.service failed
|
|
291
|
+
unit_state "$d" herdr-fleet.service active
|
|
292
|
+
code=$(run_with_heal "$d" omp-conductor.service)
|
|
293
|
+
check 'ROLLBACK exits 0 when the daemon heals' '0' "$code"
|
|
294
|
+
check 'ROLLBACK restores the pre-upgrade bytes over config.json' \
|
|
295
|
+
'{"preUpgrade":true}' "$(cat "$d/state/config.json")"
|
|
296
|
+
check 'ROLLBACK starts the daemon unit' \
|
|
297
|
+
'systemctl start omp-conductor.service' "$(grep '^systemctl start ' "$d/calls" | head -n 1)"
|
|
298
|
+
check 'ROLLBACK escalates as well' \
|
|
299
|
+
'omp-conductor report --kind tier2' "$(grep '^omp-conductor report' "$d/calls" | tail -n 1)"
|
|
300
|
+
check 'the pre-upgrade snapshot survives the rollback for the operator' \
|
|
301
|
+
'{"preUpgrade":true}' "$(cat "$d/state/backups/config"/config.json.pre-upgrade-*)"
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# case RESTART_PEER and the bound: attempts cap at two, then escalate-only.
|
|
305
|
+
# A daemon crash with no upgrade evidence must never get a blind daemon start.
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
d=$(newcase restart-peer)
|
|
309
|
+
unit_state "$d" omp-conductor.service failed
|
|
310
|
+
unit_state "$d" herdr-fleet.service active
|
|
311
|
+
|
|
312
|
+
code=$(run_recover "$d")
|
|
313
|
+
check 'RESTART_PEER restarts exactly the peer unit and nothing else' \
|
|
314
|
+
'systemctl restart herdr-fleet.service' "$(cat "$d/calls")"
|
|
315
|
+
check 'RESTART_PEER never starts the failed daemon' \
|
|
316
|
+
'' "$(grep '^systemctl start omp-conductor' "$d/calls" || true)"
|
|
317
|
+
check 'a peer restart that leaves the daemon failed exits 1' '1' "$code"
|
|
318
|
+
check 'the failed attempt is recorded' '1' "$(marker "$d")"
|
|
319
|
+
|
|
320
|
+
# A second trigger in the same episode is one more attempt, not a loop…
|
|
321
|
+
code=$(run_recover "$d")
|
|
322
|
+
check 'a second attempt is allowed below the cap and also exits 1' '1' "$code"
|
|
323
|
+
check 'two failed attempts are recorded' '2' "$(marker "$d")"
|
|
324
|
+
|
|
325
|
+
# …and the third trigger only escalates: no mutating systemd call at all
|
|
326
|
+
# (is-failed probes for evidence are reads, not actions).
|
|
327
|
+
mutating() { grep -c '^systemctl \(start\|restart\|reset-failed\) ' "$1" || true; }
|
|
328
|
+
before=$(mutating "$d/calls")
|
|
329
|
+
code=$(run_recover "$d")
|
|
330
|
+
after=$(mutating "$d/calls")
|
|
331
|
+
check 'attempts exhausted — the run says so' \
|
|
332
|
+
'attempts exhausted' "$(cat "$d/out")"
|
|
333
|
+
check 'an exhausted run performs no systemd action' \
|
|
334
|
+
"$before" "$after"
|
|
335
|
+
check 'the escalation is still queued after exhaustion' \
|
|
336
|
+
'omp-conductor report --kind tier2' "$(grep '^omp-conductor report' "$d/calls" | tail -n 1)"
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# case — nothing failed when recovery ran (the "got itself up" case): always
|
|
340
|
+
# escalate, and a previously exhausted episode counter gets a fresh start.
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
d=$(newcase nothing-failed)
|
|
344
|
+
mkdir -p "$d/state/recovery"
|
|
345
|
+
printf '3\n%s\n' "$(date +%s)" >"$d/state/recovery/attempts"
|
|
346
|
+
unit_state "$d" omp-conductor.service active
|
|
347
|
+
unit_state "$d" herdr-fleet.service active
|
|
348
|
+
code=$(run_recover "$d")
|
|
349
|
+
check 'a trigger with nothing failed escalates (fleet came back)' \
|
|
350
|
+
'no unit failed' "$(cat "$d/out")"
|
|
351
|
+
check 'a trigger with nothing failed exits 0' '0' "$code"
|
|
352
|
+
check 'a trigger with nothing failed clears the old counter' '0' "$(marker "$d")"
|
|
353
|
+
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
# case DRY-RUN — the operator's rehearsal mode prints decisions and mutates
|
|
356
|
+
# nothing, not even the recovery dir or the fixture call log.
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
d=$(newcase dry-run)
|
|
360
|
+
unit_state "$d" herdr-fleet.service failed
|
|
361
|
+
unit_state "$d" omp-conductor.service active
|
|
362
|
+
(
|
|
363
|
+
cd "$here" || exit 1
|
|
364
|
+
env \
|
|
365
|
+
PATH="$tmp/bin:/usr/bin:/bin" \
|
|
366
|
+
SYSTEMCTL_BIN="$tmp/bin/systemctl" \
|
|
367
|
+
JOURNALCTL_BIN="$tmp/bin/journalctl" \
|
|
368
|
+
OMP_CONDUCTOR_BIN="$tmp/bin/omp-conductor" \
|
|
369
|
+
RECOVER_STATE_DIR="$d/state" \
|
|
370
|
+
RECOVER_PROJECT=demo \
|
|
371
|
+
RECOVER_DRY_RUN=1 \
|
|
372
|
+
STUB_CALLS="$d/calls" \
|
|
373
|
+
STUB_UNITS="$d/units" \
|
|
374
|
+
bash "$recover" >"$d/out" 2>"$d/stderr"
|
|
375
|
+
)
|
|
376
|
+
check 'a dry run names the action and the escalation' \
|
|
377
|
+
'action: RECOVER_PANE' "$(cat "$d/out")"
|
|
378
|
+
check 'a dry run performs no systemctl call' \
|
|
379
|
+
'' "$(cat "$d/calls" 2>/dev/null || true)"
|
|
380
|
+
check 'a dry run writes no evidence or counter' \
|
|
381
|
+
'' "$(ls "$d/state/recovery" 2>/dev/null || true)"
|
|
382
|
+
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
# case — escalation failure fails the recovery (bounded), evidence still there.
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
d=$(newcase escalation-fail)
|
|
388
|
+
mkdir -p "$d/bin"
|
|
389
|
+
cat >"$d/bin/omp-conductor" <<'STUB'
|
|
390
|
+
#!/usr/bin/env bash
|
|
391
|
+
echo "omp-conductor $*" >>"$STUB_CALLS"
|
|
392
|
+
case "$1" in
|
|
393
|
+
doctor) echo '{"status":"pass"}'; exit 0 ;;
|
|
394
|
+
--version) echo "0.15.11" ;;
|
|
395
|
+
report) exit 3 ;;
|
|
396
|
+
esac
|
|
397
|
+
STUB
|
|
398
|
+
chmod +x "$d/bin/omp-conductor"
|
|
399
|
+
unit_state "$d" herdr-fleet.service failed
|
|
400
|
+
unit_state "$d" omp-conductor.service absent
|
|
401
|
+
(
|
|
402
|
+
cd "$here" || exit 1
|
|
403
|
+
env \
|
|
404
|
+
PATH="$tmp/bin:/usr/bin:/bin" \
|
|
405
|
+
SYSTEMCTL_BIN="$tmp/bin/systemctl" \
|
|
406
|
+
JOURNALCTL_BIN="$tmp/bin/journalctl" \
|
|
407
|
+
OMP_CONDUCTOR_BIN="$d/bin/omp-conductor" \
|
|
408
|
+
RECOVER_STATE_DIR="$d/state" \
|
|
409
|
+
RECOVER_PROJECT=demo \
|
|
410
|
+
RECOVER_VERIFY_POLL_S=0 \
|
|
411
|
+
RECOVER_VERIFY_TRIES=2 \
|
|
412
|
+
STUB_CALLS="$d/calls" \
|
|
413
|
+
STUB_UNITS="$d/units" \
|
|
414
|
+
bash "$recover" >"$d/out" 2>"$d/stderr"
|
|
415
|
+
)
|
|
416
|
+
code=$?
|
|
417
|
+
check 'a failed escalation fails the recovery run' '1' "$code"
|
|
418
|
+
check 'the evidence file exists even when the escalation failed' \
|
|
419
|
+
'=== omp-conductor fleet recovery evidence (#485) ===' "$(head -n 1 "$d/state/recovery"/evidence-*)"
|
|
420
|
+
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
if (( failures > 0 )); then
|
|
424
|
+
printf 'FAIL: %s test(s) failed\n' "$failures"
|
|
425
|
+
exit 1
|
|
426
|
+
fi
|
|
427
|
+
echo 'all tests passed'
|
|
428
|
+
exit 0
|