Make nested snapshots opt-in; fix snapshot leaks found in audit
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.
This commit is contained in:
+25
-1
@@ -4,7 +4,13 @@
|
||||
|
||||
### Added
|
||||
|
||||
- **`snapshot = true` now works on datasets that have child datasets.**
|
||||
- **`snapshot = true` now works on datasets that have child datasets** —
|
||||
**opt-in, off by default** (`install.sh --enable-nested-snapshots` /
|
||||
`--disable-nested-snapshots`). It changes how backups read their source data,
|
||||
so it is never enabled implicitly; with neither flag `install.sh` preserves
|
||||
the existing setting, so a `git pull && bash install.sh` cannot silently flip
|
||||
it. When disabled, `apply.sh` skips the patch entirely and the stock guard
|
||||
remains. `uninstall.sh` tears down any staging mounts and removes the marker.
|
||||
Stock TrueNAS refuses this with *"This option is only available for datasets
|
||||
that have no further nesting"*, which makes the snapshot option unusable for
|
||||
the single most common case on any box running Apps — every app is its own
|
||||
@@ -46,6 +52,24 @@
|
||||
`snapshot.py`, then `sync.py`, and only then `crud.py`. A partial failure
|
||||
leaves the guard intact and the option merely unavailable — never
|
||||
"guard removed, traversal missing".
|
||||
- **The patch owns the whole snapshot lifecycle.** `zfs.snapshot.delete`
|
||||
defaults to `recursive=False` and stock `restic_backup()` calls it with no
|
||||
options. Stock gets away with that only because its validation means
|
||||
`recursive` is never True in the field — but enabling nested datasets makes
|
||||
recursive snapshots real, so the parent now has one child snapshot per
|
||||
descendant dataset (160+ on a typical Apps pool). Relying on stock's delete
|
||||
would therefore orphan every child snapshot **on every successful run**.
|
||||
This patch sweeps the parent *and* all children, is idempotent against
|
||||
stock's `finally` winning the race, records the snapshot in a sidecar file
|
||||
(so a middlewared restart mid-backup cannot orphan it), reclaims the tree
|
||||
left by a crashed run, and deletes the tree when staging fails — where
|
||||
sync.py's own `finally` would otherwise delete nothing at all, because its
|
||||
`snapshot` local never gets assigned.
|
||||
- **The dataset list is enumerated *after* the snapshot, never before.** A
|
||||
list read beforehand can miss a dataset created in the gap: the recursive
|
||||
snapshot would capture it but the staging plan would not, silently omitting
|
||||
its data. Read afterwards, an unsnapshotted dataset trips the staging check
|
||||
and fails the run loudly instead.
|
||||
- **Every injected block no-ops** if `_truecloud_nested` is absent.
|
||||
- Datasets that cannot contribute to a file tree (`mountpoint=none|legacy`,
|
||||
unmounted/locked, encrypted-and-locked) are skipped and **reported** —
|
||||
|
||||
Reference in New Issue
Block a user