Files
computer-playbook/cli/administration/recover/README.md
kevinveenbirkenbachandClaude Opus 5.5 21eba1e88f fix(backup): carry hard links, ACLs and xattrs on every backup and recover hop
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>
2026-10-20 18:20:46 +02:00

4.3 KiB

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), then nfs → volume → secrets
  • host ([user@]host) → rsync-pull the whole backup tree, then nfs → volume → secrets
  • backup-root (a directory) → discover + recover nfs → volume → secrets directly

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