Auto-disable when TrueNAS ships native B2 support; assume immutable /usr

apply.sh now inspects B2RcloneRemote.__dict__ before patching. If TrueNAS
has shipped get_restic_config natively, it sets the kill switch, unmounts
overlays, and logs a clear instruction to run uninstall.sh.

Also: drop all conditional 'if read-only' language — overlay is always
mounted unconditionally since /usr is always immutable on TrueNAS SCALE.

README updated with auto-disable behaviour and revised native-support table.
This commit is contained in:
2026-06-16 17:45:34 +00:00
parent 7498d48b2d
commit 96434d9994
2 changed files with 65 additions and 29 deletions
+31 -23
View File
@@ -47,7 +47,8 @@ By installing this patch you accept the following:
- **No warranty.** This software is provided as-is. See the LICENSE file. - **No warranty.** This software is provided as-is. See the LICENSE file.
If TrueNAS adds native B2 or S3 support to TrueCloud Backup, the patch If TrueNAS adds native B2 or S3 support to TrueCloud Backup, the patch
detects it and degrades gracefully — see [Native support](#if-truenas-adds-native-support) below. detects it at boot, disables itself, and tells you to run `uninstall.sh` —
see [Native support](#if-truenas-adds-native-support) below.
--- ---
@@ -79,14 +80,11 @@ support and the reason is logged to `apply.log` in your repo root.
TrueNAS SCALE updates replace `/usr/` entirely. The patch survives by keeping 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 this repository on a **persistent ZFS pool** (your data pool, not `/tmp` or a
system path) and registering a **PREINIT initshutdownscript** in the TrueNAS system path) and registering a **PREINIT initshutdownscript** in the TrueNAS
database. On every boot, `patch/apply.sh` runs from the repo before database. On every boot, `patch/apply.sh` runs before `middlewared` starts. It
`middlewared` starts, patching `b2.py` and `restic.py` directly in the overlay mounts a writable [overlayfs](https://docs.kernel.org/filesystems/overlayfs.html)
and re-patching the UI bundle. over the relevant directories (upper layer in `/run`, recreated each boot), then
patches `b2.py` and `restic.py` directly in that overlay and re-patches the UI
If `/usr` is a read-only filesystem, `apply.sh` handles this automatically by bundle. No extra configuration is needed.
mounting a writable [overlayfs](https://docs.kernel.org/filesystems/overlayfs.html)
on top of the relevant directories. The overlay lives in `/run` (tmpfs) and is
recreated on every boot. No extra configuration is needed.
--- ---
@@ -257,22 +255,32 @@ restic -r "$REPO" ls latest
## If TrueNAS adds native support ## If TrueNAS adds native support
When a TrueNAS update ships native B2 or S3 support in TrueCloud Backup, the `apply.sh` checks at every boot whether TrueNAS has shipped native B2 restic
patch handles each component as follows: support (by inspecting `B2RcloneRemote.__dict__`). If it has:
| Component | What happens | Action needed | 1. The kill switch (`disabled` file) is set — no patching on any future boot.
2. Any active overlays are unmounted immediately.
3. The following message is written to `apply.log`:
```
NOTICE: TrueNAS now provides native B2 restic support — truecloud-patch is no longer needed.
NOTICE: Setting kill switch; patching will be skipped on all future boots.
NOTICE: Run the following to fully remove the patch:
NOTICE: bash /mnt/tank/truenas-truecloud-patch/uninstall.sh
```
Check the log after any TrueNAS update:
```bash
cat /mnt/tank/truenas-truecloud-patch/apply.log | tail -20
```
**Scenarios where the auto-detect may not fire** (manual check needed):
| Scenario | What happens | Action |
|---|---|---| |---|---|---|
| **B2 `get_restic_config`** added directly to `B2RcloneRemote` | `__dict__` guard detects it; our method is **not attached** | None — native version used automatically | | B2 support added to a **base class** (not `B2RcloneRemote` directly) | `__dict__` check misses it; our method shadows native | Uninstall manually |
| **restic.py URL builder** fixed to emit `b2:bucket:path` directly | Our wrapper sees no `/` to fix; it becomes a **no-op** | None — correct URL passes through unchanged | | B2 **credential schema changed** (e.g. `provider["account"]` renamed) | `KeyError` on first backup | Uninstall or update the patch |
| **`get_restic_config` moved** out of `restic.py` entirely | `NameError` guard in the patched file catches it; wrapper silently does nothing | None — but run `verify` to confirm state | | **URL builder** fixed but B2 class unchanged | URL wrapper becomes a no-op; no harm, but patch is dead weight | Uninstall at your convenience |
| **B2 credential schema changed** (e.g. `provider["account"]` renamed) | Our B2 config function raises `KeyError`; backup task fails | Uninstall or update the patch |
| **B2 `get_restic_config`** added to a **base class** (not `B2RcloneRemote`) | `__dict__` check misses it; our method is attached and **shadows** the native one | Uninstall the patch |
**Recommended check after any TrueNAS update that adds TrueCloud provider
support**: run `python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py verify`
and attempt a B2 backup. If both pass, the patch is coexisting correctly. If
the backup fails with a credential or URL error that worked before the update,
uninstall the patch — TrueNAS has shipped a conflicting implementation.
--- ---
+34 -6
View File
@@ -57,10 +57,10 @@ _ensure_writable() {
mkdir -p "$upper" "$work" mkdir -p "$upper" "$work"
if mount -t overlay "truecloud-${tag}" \ if mount -t overlay "truecloud-${tag}" \
-o "lowerdir=$dir,upperdir=$upper,workdir=$work" "$dir" 2>/dev/null; then -o "lowerdir=$dir,upperdir=$upper,workdir=$work" "$dir" 2>/dev/null; then
echo "OK: Mounted writable overlay on $dir (immutable filesystem)" echo "OK: Mounted writable overlay on $dir"
return 0 return 0
fi fi
echo "WARNING: $dir is read-only and overlay mount failed." echo "WARNING: overlay mount failed on $dir — backend patch will be skipped."
return 1 return 1
} }
@@ -97,11 +97,41 @@ find_mw_python() {
# ── Step 1: backend patch ───────────────────────────────────────────────────── # ── Step 1: backend patch ─────────────────────────────────────────────────────
echo "--- backend patch ---"
PYTHON=$(find_mw_python) PYTHON=$(find_mw_python)
echo "Using Python: $PYTHON" echo "Using Python: $PYTHON"
# ── Native support check ──────────────────────────────────────────────────────
# If TrueNAS has shipped native B2 restic support, this patch is no longer
# needed. Set the kill switch and instruct the user to uninstall cleanly.
_tc_native=$("$PYTHON" -c "
try:
from middlewared.rclone.remote.b2 import B2RcloneRemote
print('yes' if 'get_restic_config' in B2RcloneRemote.__dict__ else 'no')
except Exception:
print('no')
" 2>/dev/null || echo "no")
if [ "$_tc_native" = "yes" ]; then
echo "NOTICE: TrueNAS now provides native B2 restic support — truecloud-patch is no longer needed."
echo "NOTICE: Setting kill switch; patching will be skipped on all future boots."
echo "NOTICE: Run the following to fully remove the patch:"
echo "NOTICE: bash $PATCH_DIR/uninstall.sh"
touch "$PATCH_DIR/disabled"
for _tag in mw ui; do
if mount | grep -qF "truecloud-${_tag} on "; then
_mnt=$(mount | grep "truecloud-${_tag} on " | awk '{print $3}' | head -1)
umount "$_mnt" 2>/dev/null && echo "NOTICE: Unmounted overlay on $_mnt" || true
fi
done
echo "=== done ==="
exit 0
fi
# ── Step 1: backend patch ─────────────────────────────────────────────────────
echo "--- backend patch ---"
# Derive site-packages from where middlewared actually lives. # Derive site-packages from where middlewared actually lives.
# getsitepackages()[0] may return the wrong directory; using middlewared.__file__ # getsitepackages()[0] may return the wrong directory; using middlewared.__file__
# ensures we patch files in the directory Python will actually read. # ensures we patch files in the directory Python will actually read.
@@ -109,8 +139,6 @@ SITE_PKG=$("$PYTHON" -c "
import os import os
try: try:
import middlewared import middlewared
# e.g. /usr/lib/python3/dist-packages/middlewared/__init__.py
# -> /usr/lib/python3/dist-packages/
print(os.path.dirname(os.path.dirname(os.path.abspath(middlewared.__file__)))) print(os.path.dirname(os.path.dirname(os.path.abspath(middlewared.__file__))))
except ImportError: except ImportError:
import site import site