Files
flan 30b9f18166
CI / shell (shellcheck + syntax) (push) Successful in 10s
CI / python 3.13 (push) Failing after 16s
CI / python 3.12 (push) Failing after 17s
Release / release (push) Failing after 15s
CI / python 3.11 (push) Failing after 12s
TrueNAS compatibility / compat (push) Successful in 14s
release v0.7.0
2026-07-14 01:45:23 +00:00

305 lines
13 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.7.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=""
_alert_choice=""
_ALERT_MARKER="$PATCH_DIR/update_alerts_disabled"
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.
--no-update-alerts Do not raise a TrueNAS alert when an update exists.
--update-alerts Re-enable those alerts (they are on by default).
-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" ;;
--no-update-alerts) _alert_choice="off" ;;
--update-alerts) _alert_choice="on" ;;
-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
# ── Minimum TrueNAS version ───────────────────────────────────────────────────
#
# TrueCloud Backup -- the feature this whole project extends -- was introduced in
# 24.10. On anything older, `plugins/cloud_backup/` does not exist at all: there is
# no restic, no cloud_backup task type, and nothing for the patch to attach to. It
# would not break the box, it would simply do nothing, silently, while the user
# believed their backups were configured. Say no clearly instead.
#
# A version we cannot PARSE is not a version we may refuse on: warn and continue.
# Refusing to install over a string we failed to read would be a worse failure than
# the one being prevented.
MIN_TRUENAS="24.10"
_tc_version_raw=""
if command -v midclt &>/dev/null; then
_tc_version_raw=$(midclt call system.version 2>/dev/null || true) # TrueNAS-25.10.4
fi
# Both spellings are real: modern releases report `TrueNAS-25.10.4`, older ones
# `TrueNAS-SCALE-24.04.2` -- and the SCALE- form is used by exactly the versions
# this gate exists to turn away, so failing to parse it would let them through.
_tc_version=$(printf '%s' "$_tc_version_raw" \
| sed -n 's/^TrueNAS-\(SCALE-\)\{0,1\}\([0-9]\{1,\}\.[0-9]\{1,\}\).*/\2/p')
if [ -z "$_tc_version" ]; then
echo "WARNING: could not determine the TrueNAS version" \
"${_tc_version_raw:+(got '${_tc_version_raw}')}."
echo "WARNING: this patch requires TrueNAS SCALE ${MIN_TRUENAS} or newer. Continuing anyway."
echo ""
elif [ "$(printf '%s\n%s\n' "$MIN_TRUENAS" "$_tc_version" | sort -V | head -1)" != "$MIN_TRUENAS" ]; then
echo "ERROR: TrueNAS ${_tc_version} is too old — this patch requires ${MIN_TRUENAS} or newer." >&2
echo "" >&2
echo " TrueCloud Backup does not exist before ${MIN_TRUENAS}, so there is nothing" >&2
echo " here for the patch to extend. Upgrade TrueNAS first." >&2
exit 1
else
echo "TrueNAS ${_tc_version} (minimum ${MIN_TRUENAS}) — ok"
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 ..."
# Guard each path: under `set -e` a chmod on a missing file aborts the install.
# The file set changes between versions, so `update.sh --rollback` to an older
# revision must not be killed by a name this version happens to know about.
for _exe in patch/apply.sh patch/create_task.py recover.sh uninstall.sh update.sh; do
if [ -f "$PATCH_DIR/$_exe" ]; then
chmod +x "$PATCH_DIR/$_exe"
fi
done
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"
# Tear down any staging tree first: those bind mounts PIN their ZFS
# snapshots, so leaving them would block those snapshots from ever
# being destroyed. apply.sh (below) then reverts the patched files.
python3 "$PATCH_DIR/patch/truecloud_nested.py" cleanup || \
echo " WARNING: staging mounts remain; unmount them manually."
echo "Nested-dataset snapshots: DISABLED."
echo " apply.sh will revert the patched middleware files and the stock"
echo " guard is restored when middlewared restarts (this script does that)."
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 first, 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 ""
# ── Update alerts (on by default) ─────────────────────────────────────────────
# TrueNAS cannot raise an alert from the CLI (midclt exposes only dismiss/list/
# restore), so this installs an AlertSource into middlewared/alert/source/ — the
# same mechanism every built-in TrueNAS alert uses. It ADDS a file and modifies
# none, which makes it the least invasive thing this patch does.
#
# It only alerts for releases that changed something: a documentation-only release
# raises nothing.
case "$_alert_choice" in
off)
touch "$_ALERT_MARKER"
echo "Update alerts: DISABLED (apply.sh will remove the alert source)."
;;
on)
rm -f "$_ALERT_MARKER"
echo "Update alerts: enabled."
;;
*)
if [ -f "$_ALERT_MARKER" ]; then
echo "Update alerts: disabled (unchanged)."
else
echo "Update alerts: enabled (checks daily; docs-only releases are ignored)."
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"