diff --git a/CHANGELOG.md b/CHANGELOG.md index ecfe1a8..33d777c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,7 +76,14 @@ worse than no alert, because one day it carries a security fix. straight back into the same race. So the overlay is re-mounted for the benefit of the next restart, and the question of whether *this* middlewared actually holds the patch is left to the one thing that can answer it exactly — the - in-process alert below. + in-process alert below. That re-mount preserves `hook_status.json`'s + `patched_at`: `create_task.py verify` decides "loaded" by comparing + middlewared's start time against that stamp, so a re-apply running *after* the + restart would have made the stamp newer than the process which correctly + imported the patch, and `verify` would have reported FAIL forever on every + boot where the sysext merge detaches the overlay. Caught on hardware while + validating the candidate — a new lying status introduced by the fix for a + lying status. Two supporting fixes fell out of the same failure. `_ensure_writable` treated "one of our overlays is listed on this directory" as "already done" — but it diff --git a/patch/wait_restart.sh b/patch/wait_restart.sh index 8ffb729..eb25500 100755 --- a/patch/wait_restart.sh +++ b/patch/wait_restart.sh @@ -143,7 +143,26 @@ else _log "nvidia sysext merge follows it) -- re-mounting for the next restart." _log "Whether THIS middlewared loaded the patch is answered in-process by" _log "the 'installed but NOT loaded' alert, not by this check." + + # Preserve hook_status.json's patched_at across this re-mount. + # + # create_task.py verify decides "loaded" by comparing middlewared's start + # time against patched_at. This re-apply restores the SAME patch the boot + # pass already applied, but it runs *after* the restart -- so letting it + # re-stamp would make patched_at newer than the process that correctly + # imported the patch, and verify would report FAIL forever, on every boot + # where docker's sysext merge detaches the overlay. That is precisely the + # lying-status failure this release exists to remove, so do not introduce a + # new one. The snapshot lives in /run, never in the repo: a leftover file + # there would leave the tree dirty and update.sh refuses to run over that. + _saved_status=/run/truecloud-hook_status.pre + cp -p "$PATCH_DIR/hook_status.json" "$_saved_status" 2>/dev/null + TRUECLOUD_REAPPLY=1 /bin/bash "$PATCH_DIR/patch/apply.sh" + + if [ -f "$_saved_status" ]; then + mv -f "$_saved_status" "$PATCH_DIR/hook_status.json" 2>/dev/null + fi fi _log "=== deferred restart complete ===" diff --git a/tests/test_wait_restart.py b/tests/test_wait_restart.py index 7b0fd8b..ab352a8 100644 --- a/tests/test_wait_restart.py +++ b/tests/test_wait_restart.py @@ -172,3 +172,28 @@ def test_mount_retries_on_a_private_workdir(): body = src[start:end] assert body.count("mount -t overlay") == 2, "expected a retry mount" assert 'work="/run/truecloud-${tag}-work.$$"' in body + + +def test_post_restart_remount_preserves_the_patched_at_stamp(): + """create_task.py verify compares middlewared's start time to patched_at. + + The post-restart re-mount restores the same patch the boot pass applied, so + letting apply.sh re-stamp would make patched_at newer than the process that + correctly imported it -- verify would then report FAIL forever on every boot + where docker's sysext merge detaches the overlay. + """ + src = wait_restart_source() + tail = src[src.index("systemctl try-restart middlewared"):] + assert "hook_status.json" in tail + save = tail.index("/run/truecloud-hook_status.pre") + reapply = tail.index("TRUECLOUD_REAPPLY=1") + restore = tail.rindex("hook_status.json") + assert save < reapply < restore, "snapshot must bracket the re-apply" + + +def test_status_snapshot_is_not_written_into_the_repo(): + """A leftover file in the repo dir leaves the tree dirty, and update.sh + refuses to run over a dirty tree -- that once made the patch un-updatable.""" + src = wait_restart_source() + assert "/run/truecloud-hook_status.pre" in src + assert '"$PATCH_DIR/hook_status.json.pre' not in src