compat: check the middlewared METHODS we call, not just the symbols we wrap
CI / shell (shellcheck + syntax) (push) Successful in 10s
CI / python 3.11 (push) Successful in 14s
CI / python 3.12 (push) Successful in 16s
CI / python 3.13 (push) Successful in 17s
TrueNAS compatibility / compat (push) Successful in 10s

This gap was hiding a catastrophe. TrueNAS 26 deletes plugins/zfs_/dataset.py and
plugins/zfs_/snapshot.py outright, taking zfs.dataset.query, zfs.snapshot.query and
zfs.snapshot.delete with them (26 uses filesystem.statfs and zfs.resource.*).

Nothing about the five cloud_backup files reveals that, so every other check went
green -- including the one I had just added. The patch would have applied cleanly
and then failed on the first backup, or worse: snapshotted fine and failed to
DELETE, orphaning one snapshot per descendant dataset (250 on a real pool) on every
run, forever.

So 26 is BROKEN and the nested module will not apply there. The async/sync wrapper
work and the vendored get_dataset_recursive stay -- they are correct and necessary
-- but 26 is not supported until the ZFS calls are ported, and that needs a real 26
box to verify. Shipping a port nobody has run is the failure this project exists to
avoid.

Also: do_delete is recognised as delete (24.10/25.04 use the CRUDService
convention), which was reporting both as BROKEN -- a false verdict that would have
disabled nested snapshots on boxes where they work.
This commit is contained in:
2026-07-13 18:25:14 +00:00
parent 498b2690e1
commit ecb64878ff
5 changed files with 306 additions and 43 deletions
+44 -19
View File
@@ -59,28 +59,53 @@ worse than no alert, because one day it carries a security fix.
### Added
- **TrueNAS 26 support.** 26 rewrites the entire `cloud_backup` path from async to
**synchronous**, and separately **deletes `get_dataset_recursive()`** — which one
of the injected blocks called out of the host module's namespace. Either one is a
broken backup found at restore time: an `async def` wrapper hands `sync.py` a
coroutine where it unpacks a tuple, and the vanished helper is a straight
`NameError`.
- **The compatibility check now covers the middlewared methods the patch _calls_,**
not only the symbols it wraps — and that gap was hiding a catastrophe.
The nested module is now **one synchronous implementation** (talking to middlewared
through `call_sync`) behind **two thin wrappers**. `apply.sh` reads which flavour
the installed middleware declares and injects the matching one: TrueNAS ≤ 25.10
reaches it via `await middleware.run_in_thread(...)`, and TrueNAS 26 — already in a
worker thread — calls it directly. The logic that owns the snapshots, the bind
mounts and the failure modes exists **once**; an async twin would mean every future
fix had to land twice, and the one that got missed would be the one that eats a
backup.
TrueNAS 26 **deletes `plugins/zfs_/dataset.py` and `plugins/zfs_/snapshot.py`
outright**, taking `zfs.dataset.query`, `zfs.snapshot.query` and
`zfs.snapshot.delete` with them (26 uses `filesystem.statfs` and `zfs.resource.*`).
Nothing about the five `cloud_backup` files reveals that, so every other check went
green. The patch would have applied perfectly and then **failed on the first
backup** — or, far worse, snapshotted successfully and failed to *delete*,
orphaning one snapshot per descendant dataset (**250 on a real pool**) on every
single run, forever.
A middleware whose three wrapped functions **disagree** about async-ness is refused
outright rather than guessed at. And `get_dataset_recursive` is now carried as our
own copy — removing the dependency on both versions instead of asserting it.
This is now an assumption class of its own, so a method disappearing is a BROKEN
verdict rather than a silent time bomb.
Both breaks were found by the daily compatibility check **while 26 was still in
beta**, which is the entire point of it.
- **Groundwork for TrueNAS 26** (async→sync and the deleted helper — see below).
**26 is still reported BROKEN and the nested module will not apply there**, because
the ZFS API rewrite above is not yet ported. Porting it needs a real 26 box to
verify against, and shipping a port nobody has run is exactly the failure this
project exists to avoid. On 26, TrueNAS is left stock: B2/S3 keeps working, nested
datasets are simply not covered.
### Fixed
- **The nested module is now one synchronous implementation behind two thin
wrappers.** TrueNAS 26 rewrites `cloud_backup` from async to **synchronous** and
separately **deletes `get_dataset_recursive()`**, which an injected block called out
of the host module's namespace. Either alone is a broken backup found at restore
time: an `async def` wrapper hands `sync.py` a coroutine where it unpacks a tuple,
and the vanished helper is a straight `NameError`.
The module now talks to middlewared through `call_sync`, and `apply.sh` reads which
flavour the installed middleware declares and injects the matching wrapper —
TrueNAS ≤ 25.10 reaches it via `await middleware.run_in_thread(...)`; a synchronous
TrueNAS, already in a worker thread, calls it directly. The logic that owns the
snapshots, the bind mounts and the failure modes exists **once**; an async twin
would mean every future fix had to land twice, and the one that got missed would be
the one that eats a backup. A middleware whose three wrapped functions **disagree**
about async-ness is refused outright rather than guessed at, and
`get_dataset_recursive` is carried as our own copy — removing the dependency on both
versions instead of asserting it.
- **`do_delete` is recognised as `delete`.** TrueNAS 24.10 and 25.04 declare
`do_delete` (the `CRUDService` convention); 25.10 renamed it to `delete`. Both
answer to `zfs.snapshot.delete`. Accepting only the literal name reported both older
releases as BROKEN — a false verdict that would have switched nested snapshots off
on boxes where they work perfectly.
- **An incompatible TrueNAS no longer sets the permanent kill switch.** `apply.sh`
reused a "nothing left to do" exit that touches `disabled`, which suppresses