Files
truenas-truecloud-patch/uninstall.sh
T
flan 24f1f2c648 Fix post-merge audit findings; unify staging teardown
Audit
-----
- create_task.py verify failed on a DEFAULT install. hook_status.json emitted a
  per-file entry for the nested module with ok:false whenever the feature was
  switched off -- the default -- so verify printed [FAIL] and exited 1, right
  after the README tells users to run it. Status is now per MODULE with an
  `active` flag, and verify renders an inactive module as [SKIP].

- A partial apply suppressed the middlewared restart. The exit code conflated
  "nothing applied" with "one module applied, one failed", so a failing providers
  patch would prevent the restart that a freshly-applied nested patch needs,
  leaving it on disk and never loaded. Exit 2 now means partial and the restart
  still fires.

- The native-nested probe could never fire. It scanned crud.py for the guard
  message, but our own injected block quotes that message, so once applied the
  probe would always conclude the guard was still present. It now reads only the
  stock portion of the file.

- recover.sh did not unmount staging trees, so an emergency recovery left bind
  mounts pinning ZFS snapshots that could then never be destroyed.

- uninstall.sh deleted sidecar files without reading them. A sidecar is the only
  record that an interrupted run's snapshot tree is still on disk; both scripts
  now name the snapshot before clearing it.

- Removed a dead branch in the restart gate (unreachable: the kill switch exits).

Refactor
--------
- Staging teardown had been copy-pasted into uninstall.sh and recover.sh -- two
  untested shell copies of the fiddly depth-ordering and lazy-umount logic. Both
  now call `python3 patch/truecloud_nested.py cleanup`, so there is exactly one
  implementation and it is the one under test.

- Dropped the in-memory ACTIVE dict. The sidecar file was already the source of
  truth; a second in-process record could only desync -- and it is precisely the
  middlewared-restart case (which empties it) that must not orphan a snapshot
  tree. One record, on disk, or none.

Not done: the overlay-unmount loop is duplicated across apply.sh/uninstall.sh/
recover.sh. It is pre-existing, and apply.sh runs at PREINIT under a tight
timeout -- giving it a source dependency would trade 10 lines of duplication for
a boot-time failure mode.

74 tests, ruff and shellcheck clean.
2026-07-12 22:29:31 +00:00

138 lines
4.8 KiB
Bash
Executable File

#!/bin/bash
# uninstall.sh — remove all traces of truecloud-patch from a TrueNAS box.
set -euo pipefail
VERSION="0.3.0"
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
_HOOK_COMMENT='TrueCloud provider patch (S3/B2)'
echo "=== TrueNAS TrueCloud Provider Patch v${VERSION} — Uninstall ==="
echo ""
if [ "$(id -u)" -ne 0 ]; then
echo "ERROR: must be run as root." >&2
exit 1
fi
if ! command -v midclt &>/dev/null; then
echo "ERROR: midclt not found. Run this script on TrueNAS SCALE." >&2
exit 1
fi
# ── Remove PREINIT hook ───────────────────────────────────────────────────────
echo "Removing PREINIT boot hook ..."
IDS=$(midclt call initshutdownscript.query '[]' | \
python3 -c "
import sys, json
for s in json.load(sys.stdin):
if s.get('comment') == '$_HOOK_COMMENT':
print(s['id'])
" 2>/dev/null || true)
if [ -n "$IDS" ]; then
for id in $IDS; do
if midclt call initshutdownscript.delete "$id" > /dev/null; then
echo " Removed initshutdownscript id=$id"
else
echo " WARNING: could not delete id=$id (already gone?)"
fi
done
else
echo " No entry found (already removed or never installed)."
fi
echo ""
# ── Restore UI bundle ─────────────────────────────────────────────────────────
echo "Restoring UI bundle backup ..."
RESTORED=0
_restore_failed=0
while IFS= read -r backup; do
original="${backup%.pre-truecloud-patch}"
if mv "$backup" "$original"; then
echo " Restored: $original"
RESTORED=1
else
echo " WARNING: Could not restore $original — backup left at $backup"
_restore_failed=1
fi
# Keep these paths in sync with WEBUI_CANDIDATES in patch/patch_ui.py
done < <(find /usr/share/truenas /usr/share/truenas-ui /var/www/truenas \
-name "*.js.pre-truecloud-patch" 2>/dev/null)
if [ "$RESTORED" -eq 0 ]; then
echo " No backup files found."
echo " On an immutable OS the UI patch is volatile and already gone after reboot."
fi
echo ""
# ── Unmount overlays ──────────────────────────────────────────────────────────
echo "Unmounting truecloud overlays (if any) ..."
_ov_found=0
for _tag in mw ui; do
if mount | grep -qF "truecloud-${_tag} on "; then
_ov_mnt=$(mount | grep "truecloud-${_tag} on " | awk '{print $3}' | head -1)
if umount "$_ov_mnt" 2>/dev/null; then
echo " Unmounted: $_ov_mnt"
else
echo " WARNING: Could not unmount overlay on $_ov_mnt"
fi
_ov_found=1
fi
done
if [ "$_ov_found" -eq 0 ]; then
echo " None active."
fi
echo ""
# ── Unmount nested-snapshot staging trees ─────────────────────────────────────
# These bind mounts pin their ZFS snapshots, so they must go before anything
# tries to destroy those snapshots. Deepest first.
# Delegated to the patch module rather than reimplemented here: the depth
# ordering and lazy-umount fallback are fiddly, and a shell copy would be the
# untested one.
echo "Unmounting nested-snapshot staging trees (if any) ..."
if ! python3 "$PATCH_DIR/patch/truecloud_nested.py" cleanup; then
echo " WARNING: staging mounts remain. Unmount them manually; until you do,"
echo " the ZFS snapshots they pin cannot be destroyed."
fi
# The opt-in marker lives in the repo dir; remove it so a later re-install
# starts from the safe default (feature off).
if [ -f "$PATCH_DIR/nested_snapshots_enabled" ]; then
rm -f "$PATCH_DIR/nested_snapshots_enabled"
echo " Removed nested-snapshot opt-in marker."
fi
echo ""
if [ "$_restore_failed" -eq 1 ]; then
echo ""
echo "ERROR: One or more UI bundle backups could not be restored." >&2
echo " $PATCH_DIR has been left intact (recover.sh and patch files are safe)." >&2
echo " Restore the backup(s) manually, then re-run uninstall.sh." >&2
exit 1
fi
# Cancel a deferred boot restart if one is still queued — we restart ourselves.
systemctl stop truecloud-mw-restart.service 2>/dev/null || true
systemctl reset-failed truecloud-mw-restart.service 2>/dev/null || true
echo "Restarting middlewared ..."
if systemctl restart middlewared; then
echo ""
echo "Uninstall complete. Refresh your browser to see the restored UI."
else
echo ""
echo "WARNING: middlewared did not start cleanly after uninstall."
echo "Check the system log for details:"
echo " journalctl -u middlewared -n 50"
exit 1
fi