mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
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>
55 lines
1.8 KiB
Python
Executable File
55 lines
1.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# nocheck: mirrored-unit-test - subclasses utils.recovery.base and is exercised
|
|
# collectively by tests/unit/python/utils/recovery/test_role_recover_scripts.py; a
|
|
# per-role copy would assert the base class twice over
|
|
"""Restore an NFS export subtree from a backup snapshot.
|
|
|
|
Runs the role's deployed backup unit first (the usual differential
|
|
``backup-nfs-to-local`` generation of the live export), then mirrors the
|
|
snapshot into the target (``rsync -aHAX --delete``). Run on the host
|
|
serving the export with every consuming stack stopped; NFS clients
|
|
re-mount on redeploy.
|
|
|
|
Usage:
|
|
recover.py SOURCE_DIR TARGET_DIR [--no-safety-backup]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[4] # nocheck: project-root-import
|
|
sys.path.insert(0, str(_REPO_ROOT))
|
|
|
|
from utils.recovery.base import DirectoryRecovery # noqa: E402
|
|
from utils.storage.nfs import STATE_SUBDIR # noqa: E402
|
|
|
|
|
|
class NfsExportRecovery(DirectoryRecovery):
|
|
unit_pattern = "svc-bkp-nfs-2-local*.service"
|
|
rsync_extra_args = (
|
|
f"--filter=protect /{STATE_SUBDIR}/backup",
|
|
f"--exclude=/{STATE_SUBDIR}/backup",
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("source_dir", help="snapshot directory to restore from")
|
|
parser.add_argument("target_dir", help="live export subtree to restore into")
|
|
parser.add_argument(
|
|
"--no-safety-backup",
|
|
action="store_true",
|
|
help="skip the pre-recover safety backup of the current target (only when it holds nothing worth saving)",
|
|
)
|
|
args = parser.parse_args()
|
|
return NfsExportRecovery(
|
|
args.source_dir, args.target_dir, service_backup=not args.no_safety_backup
|
|
).run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|