Opt-in ------ Nested-dataset snapshot support changes how backups read their source data, so it is now off by default and gated behind a marker file: install.sh --enable-nested-snapshots install.sh --disable-nested-snapshots With neither flag install.sh preserves the current setting, so a routine `git pull && bash install.sh` can never silently flip it. When disabled, apply.sh skips the patch entirely and the stock guard remains. uninstall.sh tears down staging mounts and removes the marker. Snapshot lifecycle ------------------ zfs.snapshot.delete defaults to recursive=False and stock restic_backup() calls it with no options. Stock is safe only because its validation means recursive is never True in the field. Enabling nested datasets makes recursive snapshots real: the parent then has one child snapshot per descendant dataset (160+ on an Apps pool), so stock's delete would orphan every child on EVERY successful run. The patch now owns the lifecycle end to end: - delete_snapshot_tree() sweeps the parent and all children, and is idempotent against stock's finally winning the race once our mounts are released - on a staging failure the tree is deleted here, because sync.py never completes `snapshot, local_path = await create_snapshot(...)` and so its finally deletes nothing at all - the snapshot is recorded in a sidecar file before anything is mounted, so a middlewared restart mid-backup cannot orphan it - a crashed run's snapshot tree is reclaimed on the next run instead of being overwritten and leaked Silent-omission fix ------------------- The dataset list is now enumerated AFTER the snapshot. Read beforehand it could miss a dataset created in the gap, which the recursive snapshot would capture but the staging plan would not -- silently omitting its data. Read afterwards, an unsnapshotted dataset trips the staging check and fails the run loudly. Also from the audit ------------------- - plan_staging scopes by dataset name, so skipped-dataset warnings no longer include every mountpoint-less dataset on the box, which buried the ones that matter - staging_root_for rejects "." / ".." components that would escape the staging base, and resolves STAGING_BASE at call time rather than freezing it into a default argument - uninstall.sh no longer `rm -rf`s a tree that may still contain live bind mounts, and unmounts by path depth rather than string length - apply_plan takes an injectable isdir; verify_staged drops an unused parameter - pin the shellcheck action instead of tracking @master 61 tests, ruff and shellcheck clean.
220 lines
9.0 KiB
Bash
Executable File
220 lines
9.0 KiB
Bash
Executable File
#!/bin/bash
|
|
# install.sh — run once on the TrueNAS box to set up truecloud-patch.
|
|
#
|
|
# Prerequisites: run as root on TrueNAS SCALE with middlewared running.
|
|
# Clone this repository to a persistent ZFS pool first:
|
|
#
|
|
# git clone https://github.com/sudolulo/truenas-truecloud-patch \
|
|
# /mnt/<pool>/truenas-truecloud-patch
|
|
# cd /mnt/<pool>/truenas-truecloud-patch && bash install.sh
|
|
#
|
|
# What this does:
|
|
# 1. Registers a PREINIT initshutdownscript so patch/apply.sh re-runs on
|
|
# every boot. At boot, apply.sh re-patches the overlay and schedules a
|
|
# one-time deferred middlewared restart to load the patched modules
|
|
# (PREINIT runs after middlewared starts, so a restart is required).
|
|
# 2. Applies the patches immediately (no reboot required).
|
|
# 3. Restarts middlewared so the backend change takes effect now.
|
|
|
|
set -euo pipefail
|
|
|
|
VERSION="0.3.0"
|
|
|
|
# The directory containing install.sh is the permanent install location.
|
|
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
_HOOK_COMMENT='TrueCloud provider patch (S3/B2)'
|
|
_NESTED_MARKER="$PATCH_DIR/nested_snapshots_enabled"
|
|
|
|
# ── Options ───────────────────────────────────────────────────────────────────
|
|
# Nested-dataset snapshot support is OPT-IN and off by default. It changes how
|
|
# backups read their source data, so an unattended re-run (e.g. after a
|
|
# `git pull`) must never flip it on or off by itself: with neither flag given,
|
|
# whatever was chosen previously is preserved.
|
|
_nested_choice=""
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: bash install.sh [options]
|
|
|
|
Options:
|
|
--enable-nested-snapshots Allow the "Take Snapshot" option on datasets that
|
|
have child datasets (every pool running Apps).
|
|
Stock TrueNAS refuses this; see README. Off by
|
|
default because it changes how backups read data.
|
|
--disable-nested-snapshots Turn it back off; the stock guard is restored.
|
|
-h, --help Show this help.
|
|
|
|
With neither flag, the current setting is left unchanged.
|
|
USAGE
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--enable-nested-snapshots) _nested_choice="on" ;;
|
|
--disable-nested-snapshots) _nested_choice="off" ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*)
|
|
echo "ERROR: unknown option: $1" >&2
|
|
echo "" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ ! -f "$PATCH_DIR/patch/apply.sh" ]; then
|
|
echo "ERROR: patch files not found at $PATCH_DIR/patch/" >&2
|
|
echo "Run install.sh from a clone of the repository on a persistent pool:" >&2
|
|
echo " git clone https://github.com/sudolulo/truenas-truecloud-patch \\" >&2
|
|
echo " /mnt/<pool>/truenas-truecloud-patch" >&2
|
|
echo " cd /mnt/<pool>/truenas-truecloud-patch && bash install.sh" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== TrueNAS TrueCloud Provider Patch v${VERSION} — Install ==="
|
|
echo ""
|
|
|
|
# ── Preflight ─────────────────────────────────────────────────────────────────
|
|
|
|
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
|
|
|
|
if ! midclt call core.ping &>/dev/null; then
|
|
echo "ERROR: middlewared is not responding. Is TrueNAS fully booted?" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ── Set permissions ───────────────────────────────────────────────────────────
|
|
|
|
echo "Setting permissions ..."
|
|
chmod +x "$PATCH_DIR/patch/apply.sh" "$PATCH_DIR/patch/create_task.py" \
|
|
"$PATCH_DIR/recover.sh" "$PATCH_DIR/uninstall.sh"
|
|
echo "Done."
|
|
echo ""
|
|
|
|
# ── Register PREINIT script ───────────────────────────────────────────────────
|
|
|
|
echo "Registering PREINIT boot hook ..."
|
|
|
|
EXISTING_ID=$(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'])
|
|
break
|
|
" 2>/dev/null || true)
|
|
|
|
if [ -n "$EXISTING_ID" ]; then
|
|
echo "Already registered (id=$EXISTING_ID). Updating path, timeout, and enabling ..."
|
|
if ! _midclt_out=$(midclt call initshutdownscript.update "$EXISTING_ID" \
|
|
"{\"enabled\": true, \"script\": \"$PATCH_DIR/patch/apply.sh\", \"timeout\": 120}" 2>&1); then
|
|
echo "ERROR: Failed to update PREINIT hook (id=$EXISTING_ID)." >&2
|
|
[ -n "$_midclt_out" ] && echo " midclt: $_midclt_out" >&2
|
|
echo " To remove the stale entry and retry:" >&2
|
|
echo " midclt call initshutdownscript.delete $EXISTING_ID" >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
if ! _midclt_out=$(midclt call initshutdownscript.create \
|
|
"{\"type\":\"SCRIPT\",\"script\":\"$PATCH_DIR/patch/apply.sh\",\"when\":\"PREINIT\",\"enabled\":true,\"timeout\":120,\"comment\":\"$_HOOK_COMMENT\"}" \
|
|
2>&1); then
|
|
echo "ERROR: Failed to register PREINIT hook." >&2
|
|
[ -n "$_midclt_out" ] && echo " midclt: $_midclt_out" >&2
|
|
exit 1
|
|
fi
|
|
echo "Registered."
|
|
fi
|
|
echo ""
|
|
|
|
# ── Clear kill switch if set ──────────────────────────────────────────────────
|
|
|
|
if [ -f "$PATCH_DIR/disabled" ]; then
|
|
rm "$PATCH_DIR/disabled"
|
|
echo "Removed kill switch ($PATCH_DIR/disabled) left from a previous recovery."
|
|
echo ""
|
|
fi
|
|
|
|
# ── Nested-dataset snapshot support (opt-in) ──────────────────────────────────
|
|
|
|
case "$_nested_choice" in
|
|
on)
|
|
touch "$_NESTED_MARKER"
|
|
echo "Nested-dataset snapshots: ENABLED"
|
|
echo " The \"Take Snapshot\" option will be allowed on datasets that have"
|
|
echo " child datasets. Backups then read from a frozen, complete staging"
|
|
echo " tree instead of live files."
|
|
echo ""
|
|
echo " This changes how your backups read their source data. Verify that a"
|
|
echo " backup completes AND that its restic snapshot actually contains"
|
|
echo " child-dataset data before you rely on it."
|
|
;;
|
|
off)
|
|
if [ -f "$_NESTED_MARKER" ]; then
|
|
rm -f "$_NESTED_MARKER"
|
|
echo "Nested-dataset snapshots: DISABLED (stock guard restored)."
|
|
echo " Any task that already has snapshot=true on a nested dataset will"
|
|
echo " fail validation on its next edit. Turn the option off on those"
|
|
echo " tasks, or re-run with --enable-nested-snapshots."
|
|
else
|
|
echo "Nested-dataset snapshots: already disabled."
|
|
fi
|
|
;;
|
|
*)
|
|
if [ -f "$_NESTED_MARKER" ]; then
|
|
echo "Nested-dataset snapshots: enabled (unchanged)."
|
|
else
|
|
echo "Nested-dataset snapshots: disabled (default)."
|
|
echo " Enable with: bash install.sh --enable-nested-snapshots"
|
|
fi
|
|
;;
|
|
esac
|
|
echo ""
|
|
|
|
# ── Apply now ─────────────────────────────────────────────────────────────────
|
|
|
|
echo "Applying patches ..."
|
|
_log_start=0
|
|
[ -f "$PATCH_DIR/apply.log" ] && _log_start=$(wc -c < "$PATCH_DIR/apply.log")
|
|
bash "$PATCH_DIR/patch/apply.sh"
|
|
echo ""
|
|
echo "Patch log (this run):"
|
|
tail -c "+$((_log_start + 1))" "$PATCH_DIR/apply.log" 2>/dev/null || true
|
|
echo ""
|
|
if tail -c "+$((_log_start + 1))" "$PATCH_DIR/apply.log" 2>/dev/null | grep -qE "WARNING:|ERROR:"; then
|
|
echo "WARNING: apply.sh reported one or more issues — see log above for details."
|
|
echo ""
|
|
fi
|
|
|
|
# ── Restart middlewared ───────────────────────────────────────────────────────
|
|
|
|
echo "Restarting middlewared so the backend patch takes effect ..."
|
|
if ! systemctl restart middlewared; then
|
|
echo "" >&2
|
|
echo "ERROR: middlewared failed to restart." >&2
|
|
echo " The patch IS installed and will activate automatically on the next boot." >&2
|
|
echo " To activate now, resolve the issue below and run: systemctl restart middlewared" >&2
|
|
echo " Check the system log for the root cause:" >&2
|
|
echo " journalctl -u middlewared -n 50" >&2
|
|
echo "If the problem is unrelated to this patch, recover with:" >&2
|
|
echo " bash $PATCH_DIR/recover.sh" >&2
|
|
exit 1
|
|
fi
|
|
echo "Done."
|
|
echo ""
|
|
echo "Verify the backend patch loaded correctly:"
|
|
echo " python3 $PATCH_DIR/patch/create_task.py verify"
|
|
echo ""
|
|
echo "Refresh your browser to pick up the UI change."
|
|
echo ""
|
|
echo "To create a TrueCloud Backup task with S3 or B2 credentials:"
|
|
echo " python3 $PATCH_DIR/patch/create_task.py --help"
|