Patching at PREINIT and restarting minutes later is only sound while the patched files are still on the live path when middlewared re-imports them, and PREINIT cannot guarantee that. The overlay sits inside /usr, so anything that remounts that hierarchy detaches it — a systemd-sysext merge/refresh from another PREINIT hook, or middlewared's own docker.configure_nvidia at runtime. Init scripts run sequentially in id order, so a hook registered after this one always wins, and reordering them would not help because docker.configure_nvidia fires long after PREINIT is done. Observed on 25.10.6: the overlay was mounted at 16:41:56, a sysext refresh unmerged and remerged /usr four seconds later, and the deferred restart at 16:47:24 loaded stock modules. Every B2 cloud_backup task then failed with NotImplementedError for nineteen hours across four scheduled runs while apply.log and hook_status.json both reported the patch active. wait_restart.sh now re-applies immediately before restarting — after boot has settled, which is also after every sysext merge and docker nvidia configuration — verifies the marker is on the live path, restarts, and verifies again, retrying once. It is no longer exec'd, so something can run after the restart to find out what it loaded. apply.sh records the resolved middlewared directory in .mw_dir for that check, and honours TRUECLOUD_REAPPLY so the re-apply pass does not schedule a second restart. _ensure_writable treated "one of our overlays is listed here" as "already done", but it only reaches that check when the directory is not writable, and a live overlay of ours always is — a shadowed overlay was indistinguishable from a healthy one. It is now detached and re-mounted, reusing the upperdir so files patched earlier in the boot survive, with a fresh workdir and a retry on a private one, since overlayfs refuses a workdir a detached mount still holds. Add a CRITICAL hourly alert for the case none of this can prevent: the patch being on disk but not in the running process. apply.log can only report the first. The alert asks the second question from inside middlewared, where the patch's own stamps make it exact, and checks both halves since either can go missing alone. It stays quiet when the kill switch is set or the providers module has been retired as native, and is not muted by update_alerts_disabled. wait_restart.sh also logs to apply.log now: journald retention on a busy box is easily shorter than the interval between reboots, and the boot that caused this had already rotated away by the time it was investigated.
256 lines
12 KiB
Markdown
256 lines
12 KiB
Markdown
# truenas-truecloud-patch
|
|
|
|
[](https://git.arch.fyi/flan/truenas-truecloud-patch/actions)
|
|
|
|
Extends TrueNAS SCALE's **TrueCloud Backup** to:
|
|
|
|
- back up to **Backblaze B2 and any S3-compatible provider**, not just Storj;
|
|
- snapshot **datasets that have child datasets** — which is every box running Apps.
|
|
|
|
**Requires TrueNAS SCALE 24.10 or newer.** TrueCloud Backup does not exist before
|
|
that, and `install.sh` will refuse.
|
|
|
|
> Storj raised the price of their TrueNAS-integrated tier from **$5/month to
|
|
> $50/month** in 2026. TrueCloud Backup is the only native TrueNAS feature that gives
|
|
> you pre-backup ZFS snapshots, restic dedup, scheduled tasks with UI progress, and
|
|
> dataset-lock integration. Running restic by hand loses all of it. This gets the
|
|
> feature back with storage you already pay for.
|
|
|
|
---
|
|
|
|
## Install
|
|
|
|
Clone it onto a **pool** (not the boot device — that is wiped on TrueNAS upgrades),
|
|
then run `install.sh` as root:
|
|
|
|
```bash
|
|
git clone https://github.com/sudolulo/truenas-truecloud-patch.git \
|
|
/mnt/tank/truenas-truecloud-patch # replace `tank` with your pool
|
|
cd /mnt/tank/truenas-truecloud-patch
|
|
sudo bash install.sh
|
|
```
|
|
|
|
That registers a PREINIT boot hook, patches middleware in a volatile overlay, and
|
|
restarts middlewared. **It survives TrueNAS updates** — the patch is re-applied at
|
|
every boot, never written to the system dataset.
|
|
|
|
Nested-dataset snapshots are **opt-in**:
|
|
|
|
```bash
|
|
sudo bash install.sh --enable-nested-snapshots
|
|
```
|
|
|
|
Then create a TrueCloud Backup task in the UI with a B2 or S3 credential, or from
|
|
[the CLI](docs/cli.md).
|
|
|
|
**Check it worked:**
|
|
|
|
```bash
|
|
sudo python3 patch/create_task.py verify
|
|
```
|
|
|
|
If something is wrong, the reason is in `apply.log` — start at
|
|
[Recovery](docs/recovery.md).
|
|
|
|
---
|
|
|
|
## TrueNAS compatibility
|
|
|
|
<!-- BEGIN COMPAT MATRIX (generated by tools/compat.py --matrix --markdown) -->
|
|
| TrueNAS | B2/S3 providers | Nested snapshots | Hardware-verified |
|
|
| --- | --- | --- | --- |
|
|
| 24.10.2.4 | ok | ok | — |
|
|
| 25.04.2.6 | ok | ok | — |
|
|
| 25.10.5 | ok | ok | — |
|
|
| 24.10.2.5 _(unreleased)_ | ok | ok | — |
|
|
| 25.10.6 _(unreleased)_ | ok | ok | — |
|
|
| 26.0.0-BETA.3 _(unreleased)_ | ok | ok | — |
|
|
| master _(27-dev)_ | **BROKEN** | **BROKEN** | — |
|
|
|
|
| verdict | meaning |
|
|
| --- | --- |
|
|
| **ok** | Every assumption the patch makes about middleware still holds. |
|
|
| **BROKEN** | middleware changed underneath the patch. `apply.sh` **refuses to apply that module** on this version and leaves TrueNAS stock, so backups keep working — without the module's feature. |
|
|
| **native** | TrueNAS does this itself now. The module retires; it is not a failure. |
|
|
|
|
"ok" means *the patch's assumptions hold*, checked automatically against iX's
|
|
source. It does not mean a human ran a backup on it — that is the
|
|
**Hardware-verified** column, which is filled in by hand and only by doing it.
|
|
|
|
**`master` is not the next release.** iX branches each major off to its own
|
|
`release/` line and master rolls straight on to the one after — so master is
|
|
`27-dev` while 26 is still in beta. A **BROKEN** master means iX has changed
|
|
something that will reach users *a major release from now*, not in the version you
|
|
are about to install. Read the numbered rows for that.
|
|
|
|
A row like `25.10.5 _(unreleased)_` is the next maintenance release: branched by iX,
|
|
not tagged yet, and the very next thing a 25.10.4 box gets. It is checked precisely
|
|
because it is the one unshipped ref that reaches real users without warning.
|
|
<!-- END COMPAT MATRIX -->
|
|
|
|
The table is **regenerated daily by CI** against iXsystems' actual middleware source
|
|
— it is not a claim somebody typed once and forgot.
|
|
|
|
It is also **static analysis**: it proves the patch's assumptions still hold, which is
|
|
a weaker claim than "a backup ran and a restore came back". For what has actually been
|
|
run — which tasks, on which hardware, and the md5 of the file that came back — see
|
|
[docs/verification.md](docs/verification.md).
|
|
|
|
**TrueNAS 26 is supported** as of v0.7.0, and was verified on a real
|
|
**26.0.0-BETA.1** install: a 274-snapshot recursive backup of a 292-dataset pool, and a
|
|
byte-identical restore of a four-level-deep child dataset. The *Hardware-verified*
|
|
column tracks the newest beta iX has tagged (currently BETA.3), so it does not carry
|
|
that mark — a build nobody has actually run a backup on does not get credit for one.
|
|
|
|
26 rewrites `cloud_backup` from async to synchronous and deletes the private ZFS
|
|
methods this module used to call, so getting there took real work: the patch now
|
|
injects the wrapper flavour that matches the installed middleware, reads dataset and
|
|
snapshot lists **from ZFS rather than middleware** (whose queries hide TrueNAS's own
|
|
datasets — 84 of 270 on a real pool, including live app data), and owns the snapshot
|
|
sweep even when it stages nothing (26 decides `recursive` by a rule this patch does not
|
|
share, and would otherwise orphan one snapshot per zvol on every run).
|
|
|
|
**And if a future TrueNAS breaks it, you get a missing feature, not a broken backup.**
|
|
`apply.sh` re-checks the patch's assumptions at every boot and **refuses to apply a
|
|
module whose assumptions no longer hold** — TrueNAS is left stock, and the reason is
|
|
named in `apply.log`. Details: [How it works](docs/how-it-works.md#truenas-26).
|
|
|
|
---
|
|
|
|
## Updating
|
|
|
|
```bash
|
|
cd /mnt/tank/truenas-truecloud-patch
|
|
sudo bash update.sh # newest release
|
|
sudo bash update.sh --check # what would change?
|
|
sudo bash update.sh --rollback # back to the previous version
|
|
```
|
|
|
|
`update.sh` refuses to run on a dirty checkout, pins you to a release tag, and keeps
|
|
the previous revision so a rollback is one command. **There is no auto-update**: this
|
|
patches system internals as root, and a bad commit reaching your box unattended would
|
|
detonate on the next reboot — v0.0.4 shipped exactly such a bug and took 54 apps
|
|
down. The manual step *is* the safety gate.
|
|
|
|
---
|
|
|
|
## Update alerts
|
|
|
|
When a newer release exists, the patch raises a **TrueNAS alert** (the bell in the
|
|
UI) telling you so. It's on by default and checks once a day.
|
|
|
|
```bash
|
|
bash install.sh --no-update-alerts # turn it off
|
|
bash install.sh --update-alerts # turn it back on
|
|
```
|
|
|
|
**It will not nag you about a README.** A release whose CHANGELOG contains only a
|
|
`### Docs` section changed no code, and raises nothing. Anything that touched the
|
|
system raises an INFO alert; a release with a `### Security` section raises a
|
|
WARNING. The CHANGELOG's own section headings are the signal, and a security fix
|
|
anywhere in the range escalates the whole span — a docs-only release on top of a
|
|
security fix still reports as security.
|
|
|
|
**Release candidates never alert.** They are invisible to `update.sh` and to the
|
|
alert, which both take the newest plain `vX.Y.Z` tag. That is what lets debugging
|
|
happen in `-rc` tags instead of in your notification bell — see
|
|
[Releasing](docs/releasing.md).
|
|
|
|
**A second alert reports the patch not being loaded**, and this one you cannot
|
|
turn off with `--no-update-alerts` — it is CRITICAL, hourly, and it means B2/S3
|
|
backup tasks are about to fail. Being patched *on disk* and being patched *in the
|
|
running middlewared* are different facts, and only middlewared can answer the
|
|
second one: the patch stamps the objects it replaces, so a missing stamp means
|
|
the process imported stock code. It fires if something detaches the patch overlay
|
|
(a `systemd-sysext` merge over `/usr`, for instance) and the self-healing re-apply
|
|
in the deferred restart could not put it back. `bash install.sh` clears it. It
|
|
stays quiet when the kill switch is set, or when the providers module has been
|
|
retired because TrueNAS went native.
|
|
|
|
The changelog is read from whichever forge `origin` points at, derived from the
|
|
remote rather than hard-coded. That is not cosmetic: when the changelog cannot be
|
|
read, the alert deliberately fires **anyway** rather than risk hiding a security
|
|
fix — so a wrong URL would not silence the alert, it would make it fire on *every*
|
|
release, including the documentation-only ones this section promises to suppress.
|
|
|
|
### How it works, and why it's built this way
|
|
|
|
TrueNAS **cannot raise an alert from the CLI** — `midclt` exposes only
|
|
`alert.dismiss`, `alert.list`, `alert.list_categories`, `alert.list_policies` and
|
|
`alert.restore`. Alert *creation* is internal to middlewared, and none of its ~60
|
|
one-shot alert classes is generic enough to reuse. So the only way to get a real
|
|
alert is to register an `AlertSource`, which is what `patch/alert_source.py` does.
|
|
|
|
That is also the **least invasive** thing this patch does:
|
|
|
|
| | |
|
|
|---|---|
|
|
| providers module | **modifies** stock files (appends code to `b2.py`, `restic.py`) |
|
|
| nested module | **modifies** stock files (3 middleware modules) |
|
|
| **update alert** | **adds one file. Modifies nothing.** |
|
|
|
|
It's the native mechanism — the same one every built-in TrueNAS alert uses — and
|
|
TrueNAS polls it itself, so there is no cron job and no systemd timer.
|
|
|
|
- **Fail-safe.** Every error path returns `None`. It cannot take middlewared down.
|
|
- **Read-only.** `git ls-remote` plus an HTTPS fetch of the CHANGELOG. It never
|
|
writes to `.git`, so it cannot leave root-owned objects behind the way a
|
|
`git fetch` from middlewared (which runs as root) would.
|
|
- **Removed by `uninstall.sh`.**
|
|
|
|
It only *tells* you. It never updates anything — see
|
|
[Updating](#updating).
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
---
|
|
|
|
## Documentation
|
|
|
|
| | |
|
|
|---|---|
|
|
| [Nested-dataset snapshots](docs/nested-snapshots.md) | Why stock refuses, what this does instead, and **how to verify your backups actually contain the data** |
|
|
| [How it works](docs/how-it-works.md) | What is patched, how it survives updates, the boot sequence, and what happens when TrueNAS goes native |
|
|
| [Recovery](docs/recovery.md) | middlewared won't start, blank web UI, `verify` shows FAIL |
|
|
| [CLI](docs/cli.md) | Creating tasks with `create_task.py` |
|
|
| [Development](docs/releasing.md) | Tests, CI, and the release process |
|
|
|
|
## What's in the repo
|
|
|
|
| Path | What it is |
|
|
|---|---|
|
|
| `install.sh` | Register the boot hook, patch, restart middlewared. Also `--enable/--disable-nested-snapshots`. |
|
|
| `update.sh` | Fetch and apply a newer release. `--check`, `--rollback`, `--to`, `--main`. |
|
|
| `uninstall.sh` | Remove everything. |
|
|
| `recover.sh` | Emergency: kill switch + restart against stock files. |
|
|
| `patch/apply.sh` | The PREINIT script. Runs at **every boot**. |
|
|
| `patch/truecloud_nested.py` | Nested-dataset staging: plan, mount, verify, tear down, sweep snapshots. |
|
|
| `patch/create_task.py` | Create tasks with S3/B2 credentials; `verify` the patch state. |
|
|
| `tools/compat.py` | What the patch assumes about middlewared — and the checker. Run daily by CI *and* at every boot. |
|
|
| `release.sh` | Cut a release. Two stages, and the second is refused without the first. |
|
|
|
|
## Before you install
|
|
|
|
- This is **unofficial** and not affiliated with iXsystems.
|
|
- It patches **internal middleware APIs** with no stability contract. Every patch is
|
|
fail-safe: if it cannot apply, middlewared starts normally and the reason is logged.
|
|
- **Test your restores.** True of any backup; more so here. See [Verifying it
|
|
works](docs/nested-snapshots.md#verifying-it-works).
|
|
- Filing a TrueNAS bug? **Remove the patch first** and reproduce on a stock system.
|
|
- Provided as-is, no warranty. See LICENSE.
|
|
|
|
## Support
|
|
|
|
If truenas-truecloud-patch is useful to you, consider supporting development via
|
|
[GitHub Sponsors](https://github.com/sponsors/sudolulo) or [Ko-fi](https://ko-fi.com/sudolulo).
|