Switch to overlay-only patching; remove sitecustomize.py

Patches to b2.py and restic.py are applied directly in the overlayfs at
PREINIT boot time. The sitecustomize.py import hook was belt-and-suspenders
that succeeded or failed alongside the file patch every time, providing no
genuine fallback.

- Delete patch/sitecustomize.py entirely
- apply.sh: remove sitecustomize install step; flatten if/elif/else structure;
  restore self-contained URL-fix logic in the restic.py BLOCK; rename overlay
  tag 'sc' -> 'mw'
- recover.sh: unmount overlays to restore original files immediately, no
  reboot required; kill-switch file prevents re-application on next boot
- uninstall.sh: remove sitecustomize.py removal section; update overlay tag
- install.sh: update preflight to check patch/apply.sh, not sitecustomize.py
- README: remove sitecustomize references throughout; update recovery docs
This commit is contained in:
2026-06-16 17:37:12 +00:00
parent f94e674561
commit 7498d48b2d
6 changed files with 76 additions and 443 deletions
+42 -71
View File
@@ -7,7 +7,6 @@
# TrueNAS updates replace /usr/ entirely; this script re-applies two patches:
#
# 1. Backend — b2.py and restic.py are patched directly in the overlay.
# sitecustomize.py is also installed as belt-and-suspenders.
#
# 2. Angular JS bundle — Widens the TrueCloud Backup credential dropdown
# from Storj-only to include S3 and B2.
@@ -39,10 +38,9 @@ if [ -f "$PATCH_DIR/disabled" ]; then
exit 0
fi
# On TrueNAS 25.x+, /usr is an immutable read-only filesystem.
# This function mounts a writable overlayfs on $1 using /run (tmpfs) for the
# upper/work dirs. The overlay is volatile per boot; this PREINIT script
# recreates it on every boot before middlewared starts.
# Mounts a writable overlayfs on $1 using /run (tmpfs) for the upper/work dirs
# when the directory is read-only. The overlay is volatile per boot; this
# PREINIT script recreates it on every boot before middlewared starts.
# Returns 0 if the directory is now writable, 1 if it could not be made so.
_ensure_writable() {
local dir="$1" tag="$2"
@@ -90,7 +88,7 @@ find_mw_python() {
echo "WARNING: '$py' cannot import middlewared; falling back to python3" >&2
py="python3"
if ! "$py" -c "import middlewared" 2>/dev/null; then
echo "WARNING: 'python3' also cannot import middlewared; sitecustomize.py may be installed in the wrong location" >&2
echo "WARNING: 'python3' also cannot import middlewared; backend patch will be skipped" >&2
fi
fi
@@ -105,10 +103,8 @@ PYTHON=$(find_mw_python)
echo "Using Python: $PYTHON"
# Derive site-packages from where middlewared actually lives.
# On TrueNAS 25.x, middlewared is in /usr/lib/python3/dist-packages/, while
# getsitepackages()[0] typically returns /usr/local/lib/python3.11/dist-packages/
# — the wrong directory. Using middlewared.__file__ ensures we install
# sitecustomize.py and patch files in the directory Python will actually read.
# getsitepackages()[0] may return the wrong directory; using middlewared.__file__
# ensures we patch files in the directory Python will actually read.
SITE_PKG=$("$PYTHON" -c "
import os
try:
@@ -137,54 +133,13 @@ _restic_ok=0
if [ -z "$SITE_PKG" ]; then
echo "WARNING: Cannot determine site-packages directory; skipping backend patch."
echo " Run: $PYTHON -c \"import site; print(site.getsitepackages())\""
elif ! _ensure_writable "$SITE_PKG" "mw"; then
echo "WARNING: Cannot make site-packages writable; skipping backend patch."
elif [ -z "$_MW_DIR" ]; then
echo "WARNING: Cannot determine middlewared directory; skipping backend patch."
else
_can_install=true
# On immutable OS, ensure site-packages is writable via overlay before
# attempting any writes.
if ! _ensure_writable "$SITE_PKG" "sc"; then
_can_install=false
fi
# Back up any pre-existing sitecustomize.py that isn't ours.
if [ "$_can_install" = true ] && \
[ -f "$SITE_PKG/sitecustomize.py" ] && \
! grep -q "truecloud-patch" "$SITE_PKG/sitecustomize.py" 2>/dev/null; then
if cp "$SITE_PKG/sitecustomize.py" \
"$SITE_PKG/sitecustomize.py.pre-truecloud-patch"; then
echo "OK: Backed up existing sitecustomize.py"
else
echo "WARNING: Could not back up existing sitecustomize.py; skipping install to avoid data loss."
_can_install=false
fi
fi
if [ "$_can_install" = true ]; then
# Substitute PATCH_DIR into the source so sitecustomize.py knows where
# to write hook_status.json and check the kill switch at runtime.
_sc_tmp="$SITE_PKG/sitecustomize.py.truecloud-tmp"
if TRUECLOUD_PATCH_DIR="$PATCH_DIR" \
"$PYTHON" -c "
import os, sys
d = os.environ['TRUECLOUD_PATCH_DIR']
with open(d + '/patch/sitecustomize.py', encoding='utf-8') as fh:
sys.stdout.write(fh.read().replace('/data/truecloud-patch', d))
" > "$_sc_tmp" && mv "$_sc_tmp" "$SITE_PKG/sitecustomize.py"; then
echo "OK: Installed sitecustomize.py → $SITE_PKG/sitecustomize.py"
else
rm -f "$_sc_tmp"
echo "WARNING: Failed to write $SITE_PKG/sitecustomize.py (permission error?)"
fi
fi
# ── Direct file patching ──────────────────────────────────────────────────
# Patch b2.py and restic.py directly in the overlay (primary approach).
# Each run strips any existing TRUECLOUD_PATCH block and rewrites it fresh,
# so a bugfix in the block takes effect immediately on the next apply.sh run
# without needing to manually clear the overlay.
if [ -n "$_MW_DIR" ] && [ "$_can_install" = true ]; then
_B2_PY="$_MW_DIR/rclone/remote/b2.py"
_RESTIC_PY="$_MW_DIR/plugins/cloud_backup/restic.py"
_B2_PY="$_MW_DIR/rclone/remote/b2.py"
_RESTIC_PY="$_MW_DIR/plugins/cloud_backup/restic.py"
# ── b2.py ─────────────────────────────────────────────────────────────
if [ -f "$_B2_PY" ]; then
@@ -231,7 +186,6 @@ import sys
BLOCK = """
# TRUECLOUD_PATCH — added by truenas-truecloud-patch/patch/apply.sh
# URL fix logic lives in sitecustomize._tc_fix_restic_cmd (single source of truth).
try:
_tc_orig_get_restic_config = get_restic_config
except NameError:
@@ -240,13 +194,35 @@ else:
def get_restic_config(cloud_backup):
import dataclasses as _dc
result = _tc_orig_get_restic_config(cloud_backup)
try:
import sitecustomize as _sc
return _sc._tc_fix_restic_cmd(result, _dc)
except Exception as _e:
import sys as _sys
_sys.stderr.write(f"[truecloud-patch] restic URL fix failed: {_e}\n")
return result
cmd = list(result.cmd)
for i, part in enumerate(cmd):
if part.startswith("--repo=") or part.startswith("--repository="):
pfx, _, url = part.partition("=")
pfx += "="
elif i and cmd[i - 1] in ("-r", "--repo", "--repository"):
pfx = None
url = part
else:
continue
scheme, sep, rest = url.partition(":")
if not sep:
break
changed = False
if rest.startswith("/") and not rest.startswith("//"):
rest = rest[1:]
changed = True
if scheme == "b2" and "/" in rest:
rest = rest.replace("/", ":", 1)
changed = True
if changed:
new_url = scheme + ":" + rest
cmd[i] = pfx + new_url if pfx is not None else new_url
try:
return _dc.replace(result, cmd=cmd)
except TypeError:
return result._replace(cmd=cmd)
break
return result
get_restic_config._truecloud_patched = True
"""
@@ -272,12 +248,7 @@ PYEOF
else
echo "WARNING: restic.py not found at $_RESTIC_PY"
fi
elif [ -z "$_MW_DIR" ]; then
echo "WARNING: Cannot determine middlewared directory; skipping direct file patch."
fi
# Write hook_status.json so 'verify' reflects the current patch state
# without requiring a backup run to trigger the import hook.
# Write hook_status.json so 'verify' reflects the current patch state.
"$PYTHON" -c "
import json, os, sys, time
b2_ok = sys.argv[1] == '1'