baudolo 7.1.x writes volume generations with `rsync -aHAX`, so the producing host keeps `user.*` extended attributes (OpenCloud keeps its storage metadata in `user.oc.*`). Every later copy dropped them again, because none of the other rsync calls passed `-X` (nor `-A`/`-H`): - NFS export snapshots (svc-bkp-nfs-2-local `script.sh`), - the backup host's pull (svc-bkp-remote-2-local `pull_specific_host.py`), - the device sync (svc-bkp-local-2-device `script.py`), - every recover into a live target (`utils/recovery/base.py`, used by the volume, NFS and device recover scripts), - the `recover full` and `recover remote` tree pulls. A disaster recovery through the documented chain therefore restored OpenCloud without `user.oc.*`, silently: the copies succeeded and the dirval stamp does not cover attributes. All of these now run `rsync -aHAX` (`-aHAXP` for the device sync). A target filesystem without xattr support makes rsync exit non-zero as soon as a source file carries attributes, so the hop fails loudly instead of dropping them; there is no per-device opt-out. The single-image pull in `recover remote` keeps `-a`. The provider's ssh-wrapper needs no change: its glob accepts any server flags and its character guard allows them. Two tests write a `user.oc.id` attribute and assert it survives the NFS snapshot and the recover; with the old `-a` both fail with `OSError: [Errno 61] No data available`. Closes #1021 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
recover
Uniform recovery for the svc-bkp-* backup roles. One command wraps each
role's files/python/recover.py behind a single grammar and drives a full
disaster recovery in the correct order.
Usage
infinito administration recover <type> <source> <target> [--preview] [--no-safety-backup]
# equivalently: python3 -m cli.administration.recover ...
<type>:device|nfs|volume|secrets|full<source>: see Source grammar<target>: host to restore onto
The complete model is machine-readable in schema.yml.
Flow
flowchart TD
S["source"]
S -->|"/dev or image file"| DEV["device: LUKS open + mount"]
S -->|"[user@]host (backup tree)"| PULL["rsync pull tree"]
S -->|"[user@]host:/dev (block)"| SSH["ssh: device recover on host"]
S -->|"[user@]host:/image"| PIMG["rsync pull image"]
S -->|"directory"| DIR["backup root"]
DEV --> ROOT["local backup root"]
PULL --> ROOT
SSH -->|"rsync pull tree"| ROOT
PIMG --> DEV
DIR --> ROOT
ROOT --> NFS["nfs -> export subtree"]
NFS --> VOL["volume -> docker volume(s)"]
VOL --> SEC["secrets -> system paths"]
SEC --> T["target host recovered"]
Types
| type | restores | source |
|---|---|---|
device |
LUKS device/image → local backup root | <device>[:subpath][:gen][:/restore-dir] |
nfs |
snapshot → NFS export subtree | files-dir or [user@]host:/path |
volume |
snapshot → docker volume | .../<volume>/files or host |
secrets |
snapshot → host secret paths | <generation>/files |
full |
every backup-stored type, in order | backup-root, host, or device |
Source grammar
A colon suffix on <source> is classified by shape:
:/absolute/path→ restore destination (overrides the default target):relative→ device on-device snapshot subpath:YYYYMMDDHHMMSS→ device generation (14 digits, default: newest)- a source not starting with
/→ remote[user@]host[:/path], rsync-pulled (nfs/volume/full) [user@]host:/dev/...→ a device on the host, recovered over ssh then pulled;[user@]host:/image→ the image is pulled and recovered locally (device/full)
The device LUKS passphrase is prompted interactively on a terminal, or read
from stdin when piped (printf '%s' "$pass" | recover device ...).
full
recover full <source> <target> recovers every type present in the backup,
in order, adapting to the source:
- device (
/dev/sdb1, image file) → device step first (restores the tree to its restore-root), thennfs → volume → secrets - host (
[user@]host) → rsync-pull the whole backup tree, thennfs → volume → secrets - backup-root (a directory) → discover + recover
nfs → volume → secretsdirectly
Under the root it recovers the newest generation of each repo
(backup-nfs-to-local, backup-docker-to-local, backup-secrets-to-local),
expanding volume per docker volume.
Safety
Each recover mirrors a snapshot with rsync -aHAX --delete and first runs the
role's backup unit as a rollback point. --no-safety-backup skips that
(only when the target holds nothing worth saving: fresh host / empty /
disposable). --preview prints the exact commands without running them.
Layout
| file | responsibility |
|---|---|
recoverers.py |
abstract Recoverer + one subclass per type, source parsing/validation |
full.py |
full orchestration: resolve source, discover repos, recover in order |
paths.py |
standard filesystem layout (backup root, export state, secrets, mount) |
__main__.py |
argparse entry + dispatch |
schema.yml |
the recovery schema |
Examples
recover nfs /backup/HASH/backup-nfs-to-local/GEN/files host01
recover volume /backup/HASH/backup-docker-to-local/GEN/matomo_data/files host01
recover secrets /backup/HASH/backup-secrets-to-local/GEN/files host01
recover device /dev/sdb1:usb-backup:20260710153000:/tmp/restored host01
recover nfs user@srchost:/backup/HASH/backup-nfs-to-local/GEN/files host01
recover full /var/lib/infinito/backup host01
recover full user@srchost host01
recover full /dev/sdb1:usb-backup host01
recover device user@srchost:/dev/sdb1:usb-backup host01
recover device user@srchost:/backup/usb.img:usb-backup host01