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.
541 lines
23 KiB
Markdown
541 lines
23 KiB
Markdown
# truenas-truecloud-patch
|
||
|
||
Extends TrueNAS SCALE's **TrueCloud Backup** feature to:
|
||
|
||
- work with S3-compatible providers and native Backblaze B2, instead of Storj only;
|
||
- take **consistent snapshots of datasets that have child datasets** — which is
|
||
every box running Apps (see [Nested-dataset snapshots](#nested-dataset-snapshots)).
|
||
|
||
---
|
||
|
||
## Why this exists
|
||
|
||
In 2026, Storj raised the price of their TrueNAS-integrated storage tier from
|
||
**$5/month to $50/month** — a 10× increase. For many home lab and small-office
|
||
users, the TrueCloud Backup feature became unaffordable overnight.
|
||
|
||
TrueCloud Backup is the only native TrueNAS mechanism that provides:
|
||
- Integrated ZFS snapshot support before each backup
|
||
- Restic-based incremental deduplication
|
||
- Scheduled tasks with progress and log tracking in the UI
|
||
- Dataset lock integration
|
||
|
||
Running restic manually is possible but loses all of the above.
|
||
This patch restores access to the TrueCloud Backup feature for users who
|
||
need a provider other than Storj, with storage they already pay for or that
|
||
costs a fraction of the new Storj price.
|
||
|
||
---
|
||
|
||
## ⚠ Disclaimer — please read before installing
|
||
|
||
**This project is unofficial, unsupported, and not affiliated with iXsystems
|
||
or the TrueNAS project in any way.**
|
||
|
||
By installing this patch you accept the following:
|
||
|
||
- **Unsupported configuration.** TrueNAS support staff are not obligated to
|
||
help with any issue on a system running this patch. If you file a bug report,
|
||
remove the patch first and reproduce the issue on an unmodified system.
|
||
|
||
- **May break on TrueNAS updates.** The patch targets internal middleware APIs
|
||
that are not part of any public contract. They can change at any time. When
|
||
they do, the patch silently degrades to Storj-only behaviour rather than
|
||
breaking TrueNAS — but you should check the log after each update.
|
||
|
||
- **Your backups are your responsibility.** Verify that your backup jobs
|
||
complete successfully and that restores work before relying on them for
|
||
disaster recovery.
|
||
|
||
- **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
|
||
detects it at boot, disables itself, and tells you to run `uninstall.sh` —
|
||
see [Native support](#if-truenas-adds-native-support) below.
|
||
|
||
---
|
||
|
||
## What is actually patched
|
||
|
||
**Nothing in TrueNAS's persistent database or configuration is modified**
|
||
(other than the boot-hook entry itself). On every boot, `patch/apply.sh` runs
|
||
as a PREINIT script. It mounts a writable
|
||
[overlayfs](https://docs.kernel.org/filesystems/overlayfs.html) over the
|
||
relevant directories in `/usr/` (upper layer in `/run` tmpfs), then patches
|
||
`b2.py` and `restic.py` inside that overlay. The overlay is volatile — it
|
||
exists only for the current boot — but the PREINIT script recreates it
|
||
automatically on every subsequent boot. Nothing in `/usr/` is written to
|
||
directly.
|
||
|
||
PREINIT scripts are executed *by* middlewared, which by then has already
|
||
imported the stock modules — so after patching, `apply.sh` schedules a single
|
||
detached middlewared restart (transient systemd unit `truecloud-mw-restart`
|
||
running `patch/wait_restart.sh`) that loads the patched modules once boot has
|
||
*actually* settled: the script waits for the systemd boot job queue to drain
|
||
and for the docker/apps state machine to reach a terminal state before
|
||
restarting. Expect one middlewared restart shortly after every boot; the UI
|
||
and API are briefly unavailable while it happens, and running services are
|
||
not affected.
|
||
|
||
| Layer | What changes | Technique |
|
||
|---|---|---|
|
||
| **Backend** | `B2RcloneRemote` gains `get_restic_config()` — skipped automatically if TrueNAS already provides one on the class. `restic.py` URL builder is fixed: strips the stray leading slash and converts the slash separator to a colon (`b2:bucket:path`), which is the format restic 0.16.x expects. URL wrapper is a no-op if the URL is already correctly formed. | File patch applied inside the overlayfs upper layer |
|
||
| **Nested snapshots** | `_truecloud_nested.py` is installed into `plugins/cloud/`, and `plugins/cloud/{snapshot,crud}.py` + `plugins/cloud_backup/sync.py` are patched so `snapshot = true` works on a dataset that has child datasets. See [below](#nested-dataset-snapshots). | New module + file patches inside the overlayfs upper layer |
|
||
| **UI** | The Angular bundle's `filterByProviders` binding is widened from `["STORJ_IX"]` to `["STORJ_IX","S3","B2"]` | In-place text replacement in the compiled JS chunk; original is backed up before patching |
|
||
|
||
All 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 `apply.log` in your repo root.
|
||
|
||
## Nested-dataset snapshots
|
||
|
||
> **Opt-in, and off by default.** This feature changes how backups read their
|
||
> source data, so it is never enabled implicitly:
|
||
>
|
||
> ```bash
|
||
> bash install.sh --enable-nested-snapshots # turn it on
|
||
> bash install.sh --disable-nested-snapshots # turn it back off
|
||
> ```
|
||
>
|
||
> With neither flag, `install.sh` leaves the current setting alone — so a
|
||
> `git pull && bash install.sh` can never silently flip it. The B2/S3 provider
|
||
> patch is unaffected either way.
|
||
|
||
TrueCloud Backup's **Take Snapshot** option makes restic read from a frozen ZFS
|
||
snapshot instead of live files. Without it the backup reads data *while apps are
|
||
writing to it* — databases get captured mid-write, and an app that rewrites its
|
||
files continuously can stall a backup indefinitely as restic chases a moving
|
||
target.
|
||
|
||
Stock TrueNAS refuses to enable it on most real-world paths:
|
||
|
||
```
|
||
[EINVAL] cloud_backup_update.snapshot:
|
||
This option is only available for datasets that have no further nesting
|
||
```
|
||
|
||
That rules out **any pool running Apps** — every app is its own dataset, usually
|
||
with `config`/`pgdata` children of its own. On a typical box that is 100+ nested
|
||
datasets, so the feature is effectively unusable exactly where it matters most.
|
||
|
||
### Why stock refuses — and why you must not simply delete the check
|
||
|
||
The guard is **correct**. `plugins/cloud/snapshot.py` already takes a *recursive*
|
||
ZFS snapshot — but it then points restic at the **parent** dataset's
|
||
`.zfs/snapshot/<snap>/` directory, and ZFS does not expose child datasets
|
||
through a parent's snapshot directory:
|
||
|
||
```
|
||
/mnt/Tap/.zfs/snapshot/<snap>/apps/ -> 0 entries (children invisible)
|
||
/mnt/Tap/apps/lidarr/config/.zfs/snapshot/<snap>/ -> the real data
|
||
```
|
||
|
||
So if you just remove the validation, restic walks a **near-empty tree, reports
|
||
SUCCESS, and uploads almost nothing.** You get a green backup job protecting no
|
||
data. iX gate the config rather than ship a backup that lies about succeeding.
|
||
|
||
> **⚠ If you are tempted to patch this yourself: deleting those four lines in
|
||
> `plugins/cloud/crud.py` is the obvious move and it is catastrophic.** The
|
||
> guard is load-bearing. It must be *replaced* with a working traversal, never
|
||
> merely removed.
|
||
|
||
### What this patch does instead
|
||
|
||
After the (already recursive) snapshot is taken, every descendant dataset's own
|
||
`.zfs/snapshot/<snap>` is bind-mounted into a **staging tree** that mirrors the
|
||
original layout, and restic is pointed at the staging root — a complete,
|
||
consistent, point-in-time view of the whole subtree. Only then is the guard
|
||
relaxed.
|
||
|
||
Safety properties, in order of importance:
|
||
|
||
- **Staging failure is loud.** If any descendant cannot be staged, the backup
|
||
*fails*. A silently-incomplete backup is precisely what the stock guard exists
|
||
to prevent, and it would be worse than not having the feature at all.
|
||
- **Post-mount verification** asserts every planned target really is a mountpoint
|
||
and the staging root is non-empty — so this cannot regress into the empty
|
||
backup it exists to fix.
|
||
- **The guard is relaxed last.** `apply.sh` installs the traversal, patches
|
||
`snapshot.py`, then `sync.py`, and only then `crud.py`. Any partial failure
|
||
leaves the guard intact and the option merely unavailable — never
|
||
"guard removed, traversal missing".
|
||
- Datasets that cannot contribute to a file tree (`mountpoint=none|legacy`,
|
||
unmounted, locked/encrypted) are skipped and **reported to the log** — never
|
||
dropped silently.
|
||
- Scoped to **cloud_backup only**. Cloud Sync (rclone) shares the same
|
||
validation mixin but has no staging teardown wired in, so its guard is
|
||
deliberately left in place.
|
||
|
||
Side benefit: the staging root is a **stable path per task**, so restic can find
|
||
its parent snapshot between runs. Stock's `.zfs/snapshot/<name>-<timestamp>/`
|
||
path changes every run, defeating restic's parent detection and forcing a full
|
||
re-scan each time.
|
||
|
||
**Expected log noise:** stock `restic_backup()` deletes the ZFS snapshot in its
|
||
own `finally`, which fails with `EBUSY` while the staging mounts pin it. You will
|
||
see one benign `Error deleting snapshot ...` warning per run; the patch then
|
||
unmounts and deletes the snapshot for real.
|
||
|
||
## Supported providers after patching
|
||
|
||
| Provider | Credential type in TrueNAS |
|
||
|---|---|
|
||
| Backblaze B2 (native B2 API) | `B2` |
|
||
| AWS S3, Wasabi, Cloudflare R2, MinIO, and any S3-compatible endpoint | `S3` |
|
||
| Storj (unchanged) | `STORJ_IX` |
|
||
|
||
## How persistence works
|
||
|
||
Two different things must survive two different events:
|
||
|
||
| Event | What would be lost | What makes it survive |
|
||
|---|---|---|
|
||
| **Reboot** | The overlay holding the patched files lives in `/run` (tmpfs) and vanishes | The PREINIT hook re-runs `apply.sh` on every boot and schedules one middlewared restart to load the result |
|
||
| **TrueNAS update** | `/usr/` is replaced entirely; custom files in `/etc/` are wiped with the new boot environment | This repo lives on your **data pool**, and the hook registration lives in the **TrueNAS config database** — both survive updates. The first boot after an update is just a normal boot |
|
||
|
||
### What happens on every boot
|
||
|
||
1. **middlewared starts** with the stock (unpatched) modules. This is
|
||
unavoidable: PREINIT scripts are executed *by* middlewared
|
||
(`ix-preinit.service` → `midclt call initshutdownscript.execute_init_tasks`),
|
||
so nothing registered there can run before it.
|
||
2. **Pools import** (`ix-zfs.service`), making `/mnt/<pool>` — and this
|
||
repository — available.
|
||
3. **`apply.sh` runs** (`ix-preinit.service`): mounts the writable overlay
|
||
(upper layer in `/run`), patches `b2.py` and `restic.py` on disk inside it,
|
||
patches the UI bundle, and writes `apply.log` and `hook_status.json`.
|
||
4. **A deferred restart is scheduled.** The middlewared that is running
|
||
imported the stock modules in step 1 and never re-imports, so the on-disk
|
||
patch alone is not enough. `apply.sh` detects it was invoked by middlewared
|
||
and creates a transient systemd unit (`truecloud-mw-restart`, via
|
||
`systemd-run --no-block`) running `patch/wait_restart.sh` — detached so it
|
||
cannot disrupt the remainder of the boot sequence.
|
||
5. **Once boot has settled, middlewared restarts once** and imports the
|
||
patched modules from the overlay. `wait_restart.sh` holds the restart until
|
||
the systemd boot job queue has drained (so in-flight `ix-*` units like
|
||
`ix-reporting` finish first) *and* middlewared's docker/apps startup has
|
||
reached a terminal state — plain unit ordering cannot see either, and
|
||
restarting middlewared while they run kills apps and dashboard reporting
|
||
for the whole boot. S3/B2 backup support is then active until the next
|
||
reboot, when the cycle repeats.
|
||
|
||
What you will observe: one middlewared restart shortly after every boot (a
|
||
brief web UI/API blip; running services are unaffected). Between steps 3
|
||
and 5 there is a short window — typically well under a minute — where the UI
|
||
already shows S3/B2 (the JS bundle is read from disk per request) but the
|
||
backend is still stock. A backup job that fires inside that window fails once
|
||
with `NotImplementedError` and succeeds on its next run; see
|
||
[Troubleshooting](#troubleshooting) if it persists beyond boot.
|
||
|
||
Manual runs of `bash patch/apply.sh` never trigger the restart — that only
|
||
happens in boot context. `install.sh` and `recover.sh` perform their own
|
||
explicit restarts instead, which is why a manual re-apply must be followed by
|
||
`systemctl restart middlewared`.
|
||
|
||
---
|
||
|
||
## Install
|
||
|
||
Clone the repository to a **persistent ZFS pool** so it survives OS updates,
|
||
then run `install.sh` from there:
|
||
|
||
```bash
|
||
# 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 is registered with the exact path you chose, and TrueNAS will
|
||
call that path on every boot.
|
||
|
||
> **Do not delete or move the repository after install.**
|
||
> If you need to relocate it, run `bash uninstall.sh` first, move the directory,
|
||
> then run `bash install.sh` again from the new location. Deleting the repo
|
||
> without uninstalling leaves a dangling PREINIT hook in the TrueNAS database —
|
||
> if that happens, see [Emergency recovery](#emergency-recovery) below.
|
||
|
||
Refresh your browser. S3 and B2 credentials now appear in the
|
||
**Data Protection → TrueCloud Backup → Add** credential dropdown.
|
||
|
||
## Updating
|
||
|
||
To update to a new version of the patch:
|
||
|
||
```bash
|
||
cd /mnt/tank/truenas-truecloud-patch
|
||
|
||
# If install.sh was previously run as root, the .git directory may be owned
|
||
# by root. Fix it first, or just pull as root:
|
||
sudo git pull # easiest option
|
||
# — or —
|
||
sudo chown -R $(whoami) .git && git pull
|
||
|
||
bash install.sh
|
||
```
|
||
|
||
`install.sh` clears any stale kill switch, re-applies the updated patches,
|
||
and restarts middlewared. Run `python3 patch/create_task.py verify` afterwards
|
||
to confirm the patches loaded successfully.
|
||
|
||
Check [CHANGELOG.md](CHANGELOG.md) to see what changed between versions.
|
||
|
||
---
|
||
|
||
## Creating a task via CLI
|
||
|
||
If the UI still shows only Storj after refreshing (e.g. the JS bundle pattern
|
||
changed in a new TrueNAS version), create tasks directly. Run this **on the
|
||
TrueNAS host** — it talks to the local middleware via `midclt`, so it needs no
|
||
host address or API key:
|
||
|
||
```bash
|
||
# Replace /mnt/tank/truenas-truecloud-patch with your clone path
|
||
|
||
# List your cloud credentials to find the right ID
|
||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py list-credentials
|
||
|
||
# Create a task with a B2 credential (id=3)
|
||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py create \
|
||
--name "tank-to-b2" \
|
||
--path /mnt/tank/data \
|
||
--credential 3 \
|
||
--bucket my-bucket \
|
||
--folder backups/tank \
|
||
--password "restic-repo-password" \
|
||
--cache-path /mnt/tank/.restic-cache \
|
||
--keep-last 14
|
||
```
|
||
|
||
> **Always pass `--cache-path`.** Without it TrueNAS runs restic with `--no-cache`,
|
||
> which re-fetches all repo metadata from the provider every run — glacially slow
|
||
> on large repos. Point it at a writable dir on a pool with free space.
|
||
|
||
> Versions ≤ 0.1.0 used the `/api/v2.0` REST API with `--host`/`--api-key`; those
|
||
> flags are now accepted-but-ignored (REST is removed in TrueNAS 26.04).
|
||
|
||
---
|
||
|
||
## Uninstall
|
||
|
||
```bash
|
||
bash /mnt/tank/truenas-truecloud-patch/uninstall.sh
|
||
```
|
||
|
||
Replace the path with your clone location. Removes the PREINIT hook,
|
||
unmounts the overlay (restoring the original backend files immediately),
|
||
and restores the original UI bundle from backup.
|
||
|
||
---
|
||
|
||
## If TrueNAS adds native support
|
||
|
||
`apply.sh` checks at every boot whether TrueNAS has shipped native B2 restic
|
||
support (by inspecting `B2RcloneRemote.__dict__`). If it has:
|
||
|
||
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 support added to a **base class** (not `B2RcloneRemote` directly) | `__dict__` check misses it; our method shadows native | Uninstall manually |
|
||
| B2 **credential schema changed** (e.g. `provider["account"]` renamed) | `KeyError` on first backup | Uninstall or update the patch |
|
||
| **URL builder** fixed but B2 class unchanged | URL wrapper becomes a no-op; no harm, but patch is dead weight | Uninstall at your convenience |
|
||
|
||
---
|
||
|
||
## After a TrueNAS update
|
||
|
||
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.
|
||
3. The backend patch (B2 support + URL fix) is more stable — check that a
|
||
B2 backup job still completes successfully after any update.
|
||
|
||
---
|
||
|
||
## Emergency recovery
|
||
|
||
### middlewared won't start
|
||
|
||
Run this from the TrueNAS shell (local console, SSH, or the debug shell in
|
||
the UI):
|
||
|
||
```bash
|
||
bash /mnt/tank/truenas-truecloud-patch/recover.sh
|
||
```
|
||
|
||
Replace the path with your clone location. This creates a kill-switch file
|
||
(`disabled`) in the repo root, unmounts the overlay so the original files are
|
||
visible immediately, then restarts middlewared. No reboot required.
|
||
|
||
If you cannot run a script and only have a bare shell prompt:
|
||
|
||
```bash
|
||
touch /mnt/tank/truenas-truecloud-patch/disabled
|
||
systemctl restart middlewared
|
||
```
|
||
|
||
If you don't remember where you cloned the repo (midclt won't work while middlewared is
|
||
down), find the path two ways:
|
||
|
||
```bash
|
||
# Option 1 — search the filesystem:
|
||
find /mnt -name "recover.sh" -path "*/truenas-truecloud-patch/*" 2>/dev/null
|
||
|
||
# Option 2 — query the TrueNAS database directly:
|
||
sqlite3 /data/freenas-v1.db \
|
||
"SELECT script FROM initshutdownscript WHERE comment = 'TrueCloud provider patch (S3/B2)';"
|
||
```
|
||
|
||
The `script` column shows the full path to `patch/apply.sh`; your clone root is one
|
||
level up (strip `/patch/apply.sh` from the end). Then run the `touch` command above
|
||
with that path.
|
||
|
||
If middlewared **still** won't start after the kill switch is set, the problem
|
||
is unrelated to this patch. Check:
|
||
|
||
```bash
|
||
journalctl -u middlewared -n 50
|
||
```
|
||
|
||
To re-enable the patch once you have investigated:
|
||
|
||
```bash
|
||
rm /mnt/tank/truenas-truecloud-patch/disabled
|
||
bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh
|
||
systemctl restart middlewared # manual apply.sh runs never restart for you
|
||
```
|
||
|
||
---
|
||
|
||
### Web UI is blank or broken
|
||
|
||
If the TrueNAS web interface loads blank or shows JavaScript errors, the
|
||
Angular bundle may have been interrupted mid-write (e.g. power cut during
|
||
boot). The original bundle is always backed up before patching, so recovery
|
||
is straightforward:
|
||
|
||
```bash
|
||
# Find the backup (the path varies by TrueNAS version):
|
||
find /usr/share/truenas /usr/share/truenas-ui /var/www/truenas -name "*.js.pre-truecloud-patch" 2>/dev/null
|
||
|
||
# Restore it — substitute the actual path from the find output:
|
||
mv /usr/share/truenas/webui/main.XXXXXXXX.js.pre-truecloud-patch \
|
||
/usr/share/truenas/webui/main.XXXXXXXX.js
|
||
```
|
||
|
||
Refresh your browser. The UI will return to normal (Storj-only until the
|
||
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 /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 /mnt/tank/truenas-truecloud-patch/apply.log | tail -40
|
||
```
|
||
2. **Check middlewared's own log** for Python tracebacks:
|
||
```bash
|
||
grep -i "truecloud\|traceback\|error" /var/log/middlewared.log 2>/dev/null | tail -30
|
||
journalctl -u middlewared -n 50
|
||
```
|
||
3. **A FAIL is non-fatal.** middlewared runs normally; the affected provider
|
||
falls back to Storj-only. Your existing backups are not at risk.
|
||
4. **If the detail says the module doesn't exist**, a TrueNAS update renamed
|
||
or restructured the internal API.
|
||
[Open an issue](https://github.com/sudolulo/truenas-truecloud-patch/issues)
|
||
with your TrueNAS version number and the full verify output.
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
**Backups fail with `NotImplementedError` after a reboot**
|
||
|
||
The traceback ends in `rclone/base.py` → `raise NotImplementedError` and
|
||
contains no `_tc_` frames: the running middlewared is executing stock code.
|
||
Either the deferred restart never fired, or the patch never landed on disk
|
||
this boot. Diagnose in this order:
|
||
|
||
```bash
|
||
# Did apply.sh run this boot, at which version, and did it schedule the restart?
|
||
tail -40 /mnt/tank/truenas-truecloud-patch/apply.log
|
||
|
||
# Full check — compares the running process against the patch timestamp
|
||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py verify
|
||
|
||
# Did the deferred restart unit run, fail, or never get created?
|
||
systemctl status truecloud-mw-restart.service
|
||
journalctl -u truecloud-mw-restart.service --no-pager | tail -20
|
||
```
|
||
|
||
- `verify` reports the process started **before** the patch → the restart
|
||
didn't happen. `systemctl restart middlewared` fixes it immediately; the
|
||
journal output above tells you why it was missed.
|
||
- `apply.log` shows the kill switch is active → `rm .../disabled`, then
|
||
`bash install.sh`.
|
||
- `apply.log` has no entry for this boot → the hook didn't run; re-run
|
||
`bash install.sh` to re-register it.
|
||
- `apply.log` header shows `[v0.0.3]` or older → update:
|
||
`git pull && bash install.sh` (v0.0.4 fixed patches not loading after
|
||
reboot).
|
||
|
||
**Apply log** (check after each reboot or install):
|
||
```bash
|
||
cat /mnt/tank/truenas-truecloud-patch/apply.log
|
||
```
|
||
|
||
**Verify backend patch is loaded** (while middlewared is running):
|
||
```bash
|
||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py verify
|
||
```
|
||
Reads `hook_status.json` written by `apply.sh` at boot **and** checks that the
|
||
running middlewared process started *after* the patches were applied — an
|
||
on-disk patch that middlewared has not loaded yet is reported as FAIL with
|
||
instructions. Does not require `--host` or `--api-key`.
|
||
|
||
**Middlewared log:**
|
||
```bash
|
||
grep truecloud-patch /var/log/middlewared.log 2>/dev/null | tail -20
|
||
journalctl -u middlewared -n 100 2>/dev/null | grep truecloud-patch
|
||
```
|
||
|
||
**Verify the UI patch** (should print your TrueNAS version):
|
||
```bash
|
||
grep -c 'STORJ_IX.*S3.*B2' \
|
||
$(find /usr/share/truenas -name '*.js' 2>/dev/null) 2>/dev/null \
|
||
| grep -v ':0'
|
||
```
|
||
|
||
**`create_task.py` — "midclt not found" or permission errors**
|
||
`create_task.py` now talks to the local middleware via `midclt`, so run it **on
|
||
the TrueNAS host** (not remotely) as a user with middleware access (root). There
|
||
is no HTTPS/API-key call anymore, so there is no TLS certificate to configure.
|