diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9588eec --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Runtime files written to the repo root by apply.sh and sitecustomize.py +/apply.log +/apply.log.1 +/apply.log.2 +/hook_status.json +/disabled diff --git a/README.md b/README.md index 0dcacff..63f21e4 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ on every update) and are therefore re-applied automatically on every boot. Both changes are **fail-safe**: if a patch cannot be applied (e.g. TrueNAS restructured the relevant code), middlewared starts normally with Storj-only -support and the reason is logged to `/data/truecloud-patch/apply.log`. +support and the reason is logged to `apply.log` in your repo root. ## Supported providers after patching @@ -76,12 +76,12 @@ support and the reason is logged to `/data/truecloud-patch/apply.log`. ## How persistence works -TrueNAS SCALE updates replace `/usr/` entirely. The patch survives by storing -all scripts in `/data/truecloud-patch/` (a persistent ZFS dataset) and -registering a **PREINIT initshutdownscript** in the TrueNAS database. This -causes `apply.sh` to run on every boot before `middlewared` starts, placing -`sitecustomize.py` in the correct site-packages directory and re-patching the -UI bundle. +TrueNAS SCALE updates replace `/usr/` entirely. The patch survives by keeping +this repository on a **persistent ZFS pool** (your data pool, not `/tmp` or a +system path) and registering a **PREINIT initshutdownscript** in the TrueNAS +database. On every boot, `patch/apply.sh` runs from the repo before +`middlewared` starts, placing `sitecustomize.py` in the correct site-packages +directory and re-patching the UI bundle. ## Python version compatibility @@ -95,14 +95,20 @@ UI bundle. ## Install -Run on your TrueNAS box as root, with the system fully booted: +Clone the repository to a **persistent ZFS pool** so it survives OS updates, +then run `install.sh` from there: ```bash -git clone https://github.com/sudolulo/truenas-truecloud-patch.git -cd truenas-truecloud-patch +# Replace /mnt/tank with your pool name +git clone https://github.com/sudolulo/truenas-truecloud-patch.git \ + /mnt/tank/truenas-truecloud-patch +cd /mnt/tank/truenas-truecloud-patch bash install.sh ``` +The directory you clone into becomes the permanent install location. The PREINIT +boot hook points to it — **do not delete or move the repo after install.** + Refresh your browser. S3 and B2 credentials now appear in the **Data Protection → TrueCloud Backup → Add** credential dropdown. @@ -124,12 +130,14 @@ If the UI still shows only Storj after refreshing (e.g. the JS bundle pattern changed in a new TrueNAS version), create tasks directly via the REST API: ```bash +# Replace /mnt/tank/truenas-truecloud-patch with your clone path + # List your cloud credentials to find the right ID -python3 /data/truecloud-patch/create_task.py \ +python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py \ --host 192.168.1.1 --api-key list-credentials # Create a task with a B2 credential (id=3) -python3 /data/truecloud-patch/create_task.py \ +python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py \ --host 192.168.1.1 --api-key create \ --name "tank-to-b2" \ --path /mnt/tank/data \ @@ -147,12 +155,12 @@ Get an API key from **System → API Keys → Add**. ## Uninstall ```bash -bash /data/truecloud-patch/uninstall.sh +bash /mnt/tank/truenas-truecloud-patch/uninstall.sh ``` -Removes the PREINIT hook, `sitecustomize.py`, and restores the original UI -bundle from backup. The backend changes vanish on the next `middlewared` -restart. +Replace the path with your clone location. Removes the PREINIT hook, +`sitecustomize.py`, and restores the original UI bundle from backup. The +backend changes vanish on the next `middlewared` restart. --- @@ -252,7 +260,7 @@ restic -r "$REPO" ls latest ## After a TrueNAS update -1. Check the log: `cat /data/truecloud-patch/apply.log | tail -30` +1. Check the log: `cat /mnt/tank/truenas-truecloud-patch/apply.log | tail -30` 2. If you see "WARNING: … pattern not found", the UI patch needs updating. [Open an issue](https://github.com/sudolulo/truenas-truecloud-patch/issues) with your TrueNAS version number. @@ -269,18 +277,18 @@ Run this from the TrueNAS shell (local console, SSH, or the debug shell in the UI): ```bash -bash /data/truecloud-patch/recover.sh +bash /mnt/tank/truenas-truecloud-patch/recover.sh ``` -This creates a kill-switch file (`/data/truecloud-patch/disabled`). -`sitecustomize.py` checks for it at Python startup; if present, the import -hook is skipped entirely and middlewared starts clean with Storj-only support. -Nothing else on your system is affected. +Replace the path with your clone location. This creates a kill-switch file +(`disabled`) in the repo root. `sitecustomize.py` checks for it at Python +startup; if present, the import hook is skipped entirely and middlewared starts +clean with Storj-only support. Nothing else on your system is affected. If you cannot run a script and only have a bare shell prompt: ```bash -touch /data/truecloud-patch/disabled +touch /mnt/tank/truenas-truecloud-patch/disabled systemctl restart middlewared ``` @@ -294,8 +302,8 @@ journalctl -u middlewared -n 50 To re-enable the patch once you have investigated: ```bash -rm /data/truecloud-patch/disabled -bash /data/truecloud-patch/apply.sh +rm /mnt/tank/truenas-truecloud-patch/disabled +bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh ``` --- @@ -317,22 +325,22 @@ mv /usr/share/truenas/webui/main.XXXXXXXX.js.pre-truecloud-patch \ ``` Refresh your browser. The UI will return to normal (Storj-only until the -patch re-runs at next reboot, or you run `bash /data/truecloud-patch/apply.sh` -manually). +patch re-runs at next reboot, or you run +`bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh` manually). --- ### Backend verify shows FAIL ```bash -python3 /data/truecloud-patch/create_task.py verify +python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py verify ``` If one or more entries show `[FAIL]`: 1. **Check the apply log** for errors during the last boot: ```bash - cat /data/truecloud-patch/apply.log | tail -40 + cat /mnt/tank/truenas-truecloud-patch/apply.log | tail -40 ``` 2. **Check middlewared's own log** for Python tracebacks: ```bash @@ -351,14 +359,14 @@ If one or more entries show `[FAIL]`: **Apply log** (check after each reboot or install): ```bash -cat /data/truecloud-patch/apply.log +cat /mnt/tank/truenas-truecloud-patch/apply.log ``` **Verify backend patch is loaded** (while middlewared is running): ```bash -python3 /data/truecloud-patch/create_task.py verify +python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py verify ``` -This reads `/data/truecloud-patch/hook_status.json`, written by the import +This reads `hook_status.json` in your repo root, written by the import hook once both target modules have been loaded by middlewared. If it reports "No hook status file found" immediately after install, restart middlewared and try again — the file is written when middlewared imports the relevant diff --git a/install.sh b/install.sh index ef181a5..a6e14df 100755 --- a/install.sh +++ b/install.sh @@ -2,24 +2,29 @@ # 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//truenas-truecloud-patch +# cd /mnt//truenas-truecloud-patch && bash install.sh # # What this does: -# 1. Copies patch files to /data/truecloud-patch/ (survives OS updates). -# 2. Registers a PREINIT initshutdownscript in the TrueNAS database so -# apply.sh re-applies the patches on every boot before middlewared starts. -# 3. Applies the patches immediately (no reboot required). -# 4. Restarts middlewared so the backend change takes effect now. +# 1. Registers a PREINIT initshutdownscript so patch/apply.sh re-runs on +# every boot before middlewared starts. +# 2. Applies the patches immediately (no reboot required). +# 3. Restarts middlewared so the backend change takes effect now. set -euo pipefail -PATCH_DIR="/data/truecloud-patch" -REPO_DIR="$(cd "$(dirname "$0")" && pwd)" +# The directory containing install.sh is the permanent install location. +PATCH_DIR="$(cd "$(dirname "$0")" && pwd)" -if [ ! -f "$REPO_DIR/patch/sitecustomize.py" ]; then - echo "ERROR: patch files not found at $REPO_DIR/patch/" >&2 - echo "Run install.sh from the cloned repository, not via pipe:" >&2 - echo " git clone https://github.com/sudolulo/truenas-truecloud-patch" >&2 - echo " cd truenas-truecloud-patch && bash install.sh" >&2 +if [ ! -f "$PATCH_DIR/patch/sitecustomize.py" ]; 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//truenas-truecloud-patch" >&2 + echo " cd /mnt//truenas-truecloud-patch && bash install.sh" >&2 exit 1 fi @@ -43,17 +48,11 @@ if ! midclt call core.ping &>/dev/null; then exit 1 fi -# ── Copy files ──────────────────────────────────────────────────────────────── +# ── Set permissions ─────────────────────────────────────────────────────────── -echo "Copying patch files to $PATCH_DIR ..." -mkdir -p "$PATCH_DIR" -cp "$REPO_DIR/patch/sitecustomize.py" "$PATCH_DIR/" -cp "$REPO_DIR/patch/patch_ui.py" "$PATCH_DIR/" -cp "$REPO_DIR/patch/apply.sh" "$PATCH_DIR/" -cp "$REPO_DIR/patch/create_task.py" "$PATCH_DIR/" -cp "$REPO_DIR/recover.sh" "$PATCH_DIR/" -cp "$REPO_DIR/uninstall.sh" "$PATCH_DIR/" -chmod +x "$PATCH_DIR/apply.sh" "$PATCH_DIR/create_task.py" "$PATCH_DIR/recover.sh" "$PATCH_DIR/uninstall.sh" +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 "" @@ -65,7 +64,7 @@ EXISTING_ID=$(midclt call initshutdownscript.query '[]' | \ python3 -c " import sys, json for s in json.load(sys.stdin): - if s.get('script') == '/data/truecloud-patch/apply.sh': + if s.get('script') == '$PATCH_DIR/patch/apply.sh': print(s['id']) break " 2>/dev/null || true) @@ -76,7 +75,7 @@ if [ -n "$EXISTING_ID" ]; then '{"enabled": true}' > /dev/null else midclt call initshutdownscript.create \ - '{"type":"SCRIPT","script":"/data/truecloud-patch/apply.sh","when":"PREINIT","enabled":true,"comment":"TrueCloud provider patch (S3/B2)"}' \ + "{\"type\":\"SCRIPT\",\"script\":\"$PATCH_DIR/patch/apply.sh\",\"when\":\"PREINIT\",\"enabled\":true,\"comment\":\"TrueCloud provider patch (S3/B2)\"}" \ > /dev/null echo "Registered." fi @@ -94,7 +93,7 @@ fi echo "Applying patches ..." _log_start=$(wc -c < "$PATCH_DIR/apply.log" 2>/dev/null || echo 0) -bash "$PATCH_DIR/apply.sh" +bash "$PATCH_DIR/patch/apply.sh" echo "" echo "Patch log ($PATCH_DIR/apply.log):" tail -30 "$PATCH_DIR/apply.log" @@ -121,9 +120,9 @@ fi echo "Done." echo "" echo "Verify the backend patch loaded correctly:" -echo " python3 $PATCH_DIR/create_task.py verify" +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/create_task.py --help" +echo " python3 $PATCH_DIR/patch/create_task.py --help" diff --git a/patch/apply.sh b/patch/apply.sh index 75a3368..16fb216 100755 --- a/patch/apply.sh +++ b/patch/apply.sh @@ -1,7 +1,6 @@ #!/bin/bash -# /data/truecloud-patch/apply.sh +# patch/apply.sh — registered as a TrueNAS PREINIT initshutdownscript. # -# Registered as a TrueNAS PREINIT initshutdownscript. # Runs on every boot BEFORE middlewared starts, so patches land before # the first Python process for middlewared is created. # @@ -18,7 +17,8 @@ # A failed patch logs a warning and continues; middlewared always starts. # Never use `set -e` in a PREINIT script. -PATCH_DIR="/data/truecloud-patch" +# Derive PATCH_DIR from this script's location (parent of the patch/ directory). +PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)" LOG="$PATCH_DIR/apply.log" # Rotate log at 512 KB to avoid unbounded growth on a system volume. @@ -32,7 +32,7 @@ exec >> "$LOG" 2>&1 echo "=== $(date -Iseconds) ===" # Kill switch: if this file exists, skip all patching and exit cleanly. -# Recovery: touch /data/truecloud-patch/disabled (then reboot or restart middlewared). +# Recovery: touch "$PATCH_DIR/disabled" (then reboot or restart middlewared). if [ -f "$PATCH_DIR/disabled" ]; then echo "Kill switch active ($PATCH_DIR/disabled exists) — patch not applied." echo "To re-enable: rm $PATCH_DIR/disabled" @@ -98,7 +98,11 @@ else fi if [ "$_can_install" = true ]; then - if cp "$PATCH_DIR/sitecustomize.py" "$SITE_PKG/sitecustomize.py"; then + # Substitute PATCH_DIR into the source so sitecustomize.py knows where + # to write hook_status.json and check the kill switch at runtime. + if sed "s|/data/truecloud-patch|$PATCH_DIR|g" \ + "$PATCH_DIR/patch/sitecustomize.py" \ + > "$SITE_PKG/sitecustomize.py"; then echo "OK: Installed sitecustomize.py → $SITE_PKG/sitecustomize.py" else echo "WARNING: Failed to write $SITE_PKG/sitecustomize.py (permission error?)" @@ -110,7 +114,7 @@ fi echo "--- UI patch ---" -"$PYTHON" "$PATCH_DIR/patch_ui.py" || echo "WARNING: patch_ui.py exited non-zero; UI dropdown may still show Storj only." +"$PYTHON" "$PATCH_DIR/patch/patch_ui.py" || echo "WARNING: patch_ui.py exited non-zero; UI dropdown may still show Storj only." # ── Done ────────────────────────────────────────────────────────────────────── diff --git a/patch/create_task.py b/patch/create_task.py index cd3dfcd..6630f72 100755 --- a/patch/create_task.py +++ b/patch/create_task.py @@ -48,7 +48,8 @@ import sys import urllib.error import urllib.request -_STATUS_FILE = "/data/truecloud-patch/hook_status.json" +_PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json") def make_client(host, api_key, insecure=False): @@ -117,7 +118,7 @@ def cmd_verify(): print("All patches installed. Run a test backup to confirm end-to-end.") else: print("One or more patches failed to apply.") - print("Check /data/truecloud-patch/apply.log and journalctl -u middlewared") + print(f"Check {os.path.join(_PATCH_DIR, 'apply.log')} and journalctl -u middlewared") sys.exit(1) def cmd_list_credentials(client, _args): diff --git a/recover.sh b/recover.sh index b810a8a..a83caad 100755 --- a/recover.sh +++ b/recover.sh @@ -3,19 +3,19 @@ # # Run this from the TrueNAS shell (local console, SSH, or debug shell): # -# bash /data/truecloud-patch/recover.sh +# bash /mnt/tank/truenas-truecloud-patch/recover.sh # # What it does: -# 1. Creates /data/truecloud-patch/disabled — sitecustomize.py checks for this -# file at startup and skips the import hook entirely, so middlewared starts -# clean without any of our code running. +# 1. Creates a "disabled" file in the repo root — sitecustomize.py checks for +# this file at startup and skips the import hook entirely, so middlewared +# starts clean without any of our code running. # 2. Restarts middlewared. # # To re-enable the patch after investigating: -# rm /data/truecloud-patch/disabled -# bash /data/truecloud-patch/apply.sh +# rm /mnt/tank/truenas-truecloud-patch/disabled +# bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh -PATCH_DIR="/data/truecloud-patch" +PATCH_DIR="$(cd "$(dirname "$0")" && pwd)" if [ "$(id -u)" -ne 0 ]; then echo "ERROR: must be run as root." >&2 diff --git a/uninstall.sh b/uninstall.sh index cf9e3b6..a181e04 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -3,7 +3,7 @@ set -euo pipefail -PATCH_DIR="/data/truecloud-patch" +PATCH_DIR="$(cd "$(dirname "$0")" && pwd)" echo "=== TrueNAS TrueCloud Provider Patch — Uninstall ===" echo "" @@ -26,7 +26,7 @@ IDS=$(midclt call initshutdownscript.query '[]' | \ python3 -c " import sys, json for s in json.load(sys.stdin): - if s.get('script') == '/data/truecloud-patch/apply.sh': + if s.get('script') == '$PATCH_DIR/patch/apply.sh': print(s['id']) " 2>/dev/null || true)