Back up /var/lib/proxmenux whole in the default host profile

This commit is contained in:
MacRimi
2026-09-25 22:53:35 +02:00
parent 557c34e1f6
commit 14583ad9f6
6 changed files with 145 additions and 10 deletions
@@ -10,6 +10,18 @@ LOG_DIR="${PMX_RESTORE_LOG_DIR:-/var/log/proxmenux}"
DEST_PREFIX="${PMX_RESTORE_DEST_PREFIX:-/}" DEST_PREFIX="${PMX_RESTORE_DEST_PREFIX:-/}"
PRE_BACKUP_BASE="${PMX_RESTORE_PRE_BACKUP_BASE:-/var/lib/proxmenux/pre-restore}" PRE_BACKUP_BASE="${PMX_RESTORE_PRE_BACKUP_BASE:-/var/lib/proxmenux/pre-restore}"
RECOVERY_BASE="${PMX_RESTORE_RECOVERY_BASE:-/var/lib/proxmenux/recovery}" RECOVERY_BASE="${PMX_RESTORE_RECOVERY_BASE:-/var/lib/proxmenux/recovery}"
# Same list as hb_state_dir_excludes: restore machinery stays with this host.
STATE_DIR_EXCLUDES=(
--exclude=/restore-pending/
--exclude=/pre-restore/
--exclude=/recovery/
--exclude=/restore-history/
--exclude=/restore-state.json
--exclude=/cluster-apply-pending
--exclude=/post-restore-maintenance-pending
--exclude=/exports/
--exclude=/helpers_cache.json
)
mkdir -p "$LOG_DIR" "$PENDING_BASE/completed" >/dev/null 2>&1 || true mkdir -p "$LOG_DIR" "$PENDING_BASE/completed" >/dev/null 2>&1 || true
LOG_FILE="${LOG_DIR}/proxmenux-restore-onboot-$(date +%Y%m%d_%H%M%S).log" LOG_FILE="${LOG_DIR}/proxmenux-restore-onboot-$(date +%Y%m%d_%H%M%S).log"
@@ -157,27 +169,39 @@ while IFS= read -r rel; do
fi fi
fi fi
state_excludes=()
[[ "$rel" == "var/lib/proxmenux" ]] && state_excludes=("${STATE_DIR_EXCLUDES[@]}")
if [[ -e "$dst" ]]; then if [[ -e "$dst" ]]; then
mkdir -p "$backup_root/$(dirname "$rel")" >/dev/null 2>&1 || true mkdir -p "$backup_root/$(dirname "$rel")" >/dev/null 2>&1 || true
cp -a "$dst" "$backup_root/$rel" >/dev/null 2>&1 || true if (( ${#state_excludes[@]} )); then
# backup_root lives inside /var/lib/proxmenux/pre-restore.
mkdir -p "$backup_root/$rel" >/dev/null 2>&1 || true
rsync -aAXH "${state_excludes[@]}" "$dst/" "$backup_root/$rel/" >/dev/null 2>&1 || true
else
cp -a "$dst" "$backup_root/$rel" >/dev/null 2>&1 || true
fi
fi fi
if [[ -d "$src" ]]; then if [[ -d "$src" ]]; then
mkdir -p "$dst" >/dev/null 2>&1 || true mkdir -p "$dst" >/dev/null 2>&1 || true
[[ "$rel" == "var/lib/proxmenux" && -d "$src/backup-jobs" ]] && state_jobs=1 || state_jobs=0
# When an exclude list is present, drop --delete so the host's # When an exclude list is present, drop --delete so the host's
# copy of the excluded file isn't removed by rsync after being # copy of the excluded file isn't removed by rsync after being
# skipped from the source side. # skipped from the source side.
if [[ ${#RSYNC_EXCLUDES[@]} -gt 0 ]]; then if [[ ${#RSYNC_EXCLUDES[@]} -gt 0 ]]; then
if rsync -aAXH "${RSYNC_EXCLUDES[@]}" "$src/" "$dst/" >/dev/null 2>&1; then if rsync -aAXH "${RSYNC_EXCLUDES[@]}" "${state_excludes[@]}" "$src/" "$dst/" >/dev/null 2>&1; then
((applied++)) ((applied++))
[[ "$rel" == "var/lib/proxmenux/backup-jobs" || "$rel" == "var/lib/proxmenux/backup-jobs/"* ]] && jobs_restored=1 [[ "$rel" == "var/lib/proxmenux/backup-jobs" || "$rel" == "var/lib/proxmenux/backup-jobs/"* ]] && jobs_restored=1
(( state_jobs )) && jobs_restored=1
else else
((failed++)) ((failed++))
fi fi
else else
if rsync -aAXH --delete "$src/" "$dst/" >/dev/null 2>&1; then if rsync -aAXH --delete "${state_excludes[@]}" "$src/" "$dst/" >/dev/null 2>&1; then
((applied++)) ((applied++))
[[ "$rel" == "var/lib/proxmenux/backup-jobs" || "$rel" == "var/lib/proxmenux/backup-jobs/"* ]] && jobs_restored=1 [[ "$rel" == "var/lib/proxmenux/backup-jobs" || "$rel" == "var/lib/proxmenux/backup-jobs/"* ]] && jobs_restored=1
(( state_jobs )) && jobs_restored=1
else else
((failed++)) ((failed++))
fi fi
+12 -1
View File
@@ -1754,10 +1754,19 @@ _rs_apply() {
[[ "${HB_RESTORE_INCLUDE_ZFS:-0}" != "1" ]] && { ((skipped++)); continue; } [[ "${HB_RESTORE_INCLUDE_ZFS:-0}" != "1" ]] && { ((skipped++)); continue; }
fi fi
local -a state_excludes=()
[[ "$rel" == "var/lib/proxmenux" ]] && mapfile -t state_excludes < <(hb_state_dir_excludes)
# Save current before overwriting # Save current before overwriting
if [[ -e "$dst" ]]; then if [[ -e "$dst" ]]; then
mkdir -p "$backup_root/$(dirname "$rel")" mkdir -p "$backup_root/$(dirname "$rel")"
cp -a "$dst" "$backup_root/$rel" 2>/dev/null || true if (( ${#state_excludes[@]} )); then
# backup_root lives inside /var/lib/proxmenux/pre-restore.
mkdir -p "$backup_root/$rel"
rsync -aAXH "${state_excludes[@]}" "$dst/" "$backup_root/$rel/" 2>/dev/null || true
else
cp -a "$dst" "$backup_root/$rel" 2>/dev/null || true
fi
fi fi
# Apply # Apply
@@ -1787,9 +1796,11 @@ _rs_apply() {
--exclude "restore-pending/" --exclude "restore-pending/"
) )
fi fi
rsync_extra+=("${state_excludes[@]}")
if rsync -aAXH --delete "${rsync_extra[@]}" "$src/" "$dst/" 2>/dev/null; then if rsync -aAXH --delete "${rsync_extra[@]}" "$src/" "$dst/" 2>/dev/null; then
((applied++)) ((applied++))
[[ "$rel" == "var/lib/proxmenux/backup-jobs" || "$rel" == "var/lib/proxmenux/backup-jobs/"* ]] && jobs_restored=1 [[ "$rel" == "var/lib/proxmenux/backup-jobs" || "$rel" == "var/lib/proxmenux/backup-jobs/"* ]] && jobs_restored=1
[[ "$rel" == "var/lib/proxmenux" && -d "$src/backup-jobs" ]] && jobs_restored=1
else else
((skipped++)) ((skipped++))
fi fi
@@ -131,7 +131,7 @@ hb_default_profile_paths() {
"/usr/local/bin" "/usr/local/bin"
"/usr/local/sbin" "/usr/local/sbin"
"/usr/local/share/proxmenux" "/usr/local/share/proxmenux"
"$HB_BACKUP_JOBS_DIR" "/var/lib/proxmenux" # backup jobs and host state; restore machinery excluded
# ── Root home (rsync excludes volatile dirs) ───────── # ── Root home (rsync excludes volatile dirs) ─────────
"/root" "/root"
@@ -144,6 +144,22 @@ hb_default_profile_paths() {
printf '%s\n' "${paths[@]}" printf '%s\n' "${paths[@]}"
} }
# rsync excludes for /var/lib/proxmenux, relative to that directory.
# Restore staging, rollback copies and post-restore markers belong to the
# host that runs the restore: they are neither backed up nor overwritten.
hb_state_dir_excludes() {
printf '%s\n' \
--exclude=/restore-pending/ \
--exclude=/pre-restore/ \
--exclude=/recovery/ \
--exclude=/restore-history/ \
--exclude=/restore-state.json \
--exclude=/cluster-apply-pending \
--exclude=/post-restore-maintenance-pending \
--exclude=/exports/ \
--exclude=/helpers_cache.json
}
# ========================================================== # ==========================================================
# PATH CLASSIFICATION (restore safety) # PATH CLASSIFICATION (restore safety)
# Returns: dangerous | reboot | hot # Returns: dangerous | reboot | hot
@@ -715,6 +731,12 @@ hb_prepare_staging() {
) )
fi fi
if (( ! operator_added )) && [[ "$rel" == "var/lib/proxmenux" ]]; then
local -a state_excludes=()
mapfile -t state_excludes < <(hb_state_dir_excludes)
rsync_opts+=("${state_excludes[@]}")
fi
if rsync "${rsync_opts[@]}" "$p/" "$target/"; then if rsync "${rsync_opts[@]}" "$p/" "$target/"; then
echo "$rel" >> "$selected_file" echo "$rel" >> "$selected_file"
else else
+79 -1
View File
@@ -230,7 +230,7 @@ staging_state_tests() {
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$LIB_SCRIPT" source "$LIB_SCRIPT"
if hb_default_profile_paths | grep -Fxq '/var/lib/proxmenux/backup-jobs'; then if hb_default_profile_paths | grep -Fxq '/var/lib/proxmenux'; then
pass "Default profile includes scheduled backup job definitions" pass "Default profile includes scheduled backup job definitions"
else else
fail "Default profile does not include scheduled backup job definitions" fail "Default profile does not include scheduled backup job definitions"
@@ -579,6 +579,83 @@ EOJ
fi fi
} }
pending_state_dir_restore_tests() {
log "\n=== Pending restore of the ProxMenux state directory (sandbox) ==="
if ! help mapfile >/dev/null 2>&1; then
skip "Pending state-directory restore test requires the Linux runtime."
return
fi
local pending_base="$TMP_ROOT/state-restore-pending"
local logs_dir="$TMP_ROOT/state-restore-logs"
local target_root="$TMP_ROOT/state-restore-target"
local target_state="$target_root/var/lib/proxmenux"
local pre_backup_base="$target_state/pre-restore"
local recovery_base="$target_state/recovery"
local source_state="$pending_base/r3/rootfs/var/lib/proxmenux"
mkdir -p "$source_state/backup-jobs" "$source_state/oci-installations" \
"$source_state/restore-pending/old" "$target_state/restore-history" \
"$target_state/exports" "$pre_backup_base/earlier"
cat > "$source_state/backup-jobs/stateful.env" <<'EOJ'
JOB_ID=stateful
BACKEND=local
ON_CALENDAR=daily
PROFILE_MODE=custom
ENABLED=1
LOCAL_DEST_DIR=/var/lib/vz/dump
LOCAL_ARCHIVE_EXT=tar.gz
EOJ
echo "/etc/hosts" > "$source_state/backup-jobs/stateful.paths"
echo '{}' > "$source_state/oci-installations/app.json"
echo "source" > "$source_state/restore-pending/old/marker"
echo "source" > "$source_state/cluster-apply-pending"
echo "target" > "$target_state/restore-history/entry.json"
echo "target" > "$target_state/exports/report.pdf"
echo "target" > "$target_state/stale.json"
echo "target" > "$pre_backup_base/earlier/keep"
echo "var/lib/proxmenux" > "$pending_base/r3/apply-on-boot.list"
echo "HB_RESTORE_INCLUDE_ZFS=0" > "$pending_base/r3/plan.env"
ln -sfn "$pending_base/r3" "$pending_base/current"
if PMX_RESTORE_PENDING_BASE="$pending_base" PMX_RESTORE_LOG_DIR="$logs_dir" \
PMX_RESTORE_DEST_PREFIX="$target_root" PMX_RESTORE_PRE_BACKUP_BASE="$pre_backup_base" \
PMX_RESTORE_RECOVERY_BASE="$recovery_base" \
bash "$APPLY_ONBOOT" >>"$REPORT_FILE" 2>&1; then
pass "Pending restore applies the state directory"
else
fail "Pending restore failed while applying the state directory"
return
fi
if [[ -f "$target_state/backup-jobs/stateful.env" && -f "$target_state/oci-installations/app.json" && \
-f "$target_root/etc/systemd/system/proxmenux-backup-stateful.timer" ]]; then
pass "State directory restore brings jobs and state, and rebuilds the timer"
else
fail "State directory restore did not bring jobs, state or the timer"
fi
if [[ ! -e "$target_state/restore-pending/old" && ! -e "$target_state/cluster-apply-pending" ]]; then
pass "Restore staging and post-boot markers from the backup are not applied"
else
fail "Restore staging or post-boot markers from the backup were applied"
fi
if [[ -f "$target_state/restore-history/entry.json" && -f "$target_state/exports/report.pdf" && \
-f "$pre_backup_base/earlier/keep" && ! -e "$target_state/stale.json" ]]; then
pass "Excluded host entries survive the restore; other entries follow the backup"
else
fail "The restore removed excluded host entries or kept entries absent from the backup"
fi
local saved
saved=$(find "$pre_backup_base" -mindepth 1 -maxdepth 1 -name '*-onboot' | head -1)
if [[ -n "$saved" && -f "$saved/var/lib/proxmenux/stale.json" && ! -e "$saved/var/lib/proxmenux/pre-restore" ]]; then
pass "Previous state is saved without copying the rollback directory into itself"
else
fail "Previous state was not saved, or the rollback directory was copied into itself"
fi
}
main() { main() {
log "ProxMenux backup/restore test matrix" log "ProxMenux backup/restore test matrix"
log "Report: $REPORT_FILE" log "Report: $REPORT_FILE"
@@ -591,6 +668,7 @@ main() {
scheduler_e2e_tests scheduler_e2e_tests
pending_restore_tests pending_restore_tests
pending_jobs_restore_tests pending_jobs_restore_tests
pending_state_dir_restore_tests
log "\n=== Summary ===" log "\n=== Summary ==="
log "PASS=$PASS" log "PASS=$PASS"
@@ -66,8 +66,8 @@
}, },
{ {
"category": "ProxMenux binaries & root", "category": "ProxMenux binaries & root",
"paths": "/usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux, /root (volatile subdirs excluded)", "paths": "/usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux, /var/lib/proxmenux, /root (volatile subdirs excluded)",
"why": "ProxMenux-installed binaries and per-user configuration under <code>/root</code>. Volatile paths (<code>.bash_history</code>, <code>.cache/</code>, <code>tmp/</code>, <code>.local/share/Trash/</code>) are excluded from the copy." "why": "ProxMenux-installed binaries and per-user configuration under <code>/root</code>. Volatile paths (<code>.bash_history</code>, <code>.cache/</code>, <code>tmp/</code>, <code>.local/share/Trash/</code>) are excluded from the copy. <code>/var/lib/proxmenux</code> holds the scheduled backup jobs and the host state kept by ProxMenux; the restore staging, the rollback copies and the post-restore markers are excluded, because they belong to the host that runs a restore."
}, },
{ {
"category": "ZFS state (conditional)", "category": "ZFS state (conditional)",
@@ -66,8 +66,8 @@
}, },
{ {
"category": "Binarios ProxMenux y root", "category": "Binarios ProxMenux y root",
"paths": "/usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux, /root (subdirs volátiles excluidos)", "paths": "/usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux, /var/lib/proxmenux, /root (subdirs volátiles excluidos)",
"why": "Binarios instalados por ProxMenux y configuración por usuario bajo <code>/root</code>. Las rutas volátiles (<code>.bash_history</code>, <code>.cache/</code>, <code>tmp/</code>, <code>.local/share/Trash/</code>) quedan fuera de la copia." "why": "Binarios instalados por ProxMenux y configuración por usuario bajo <code>/root</code>. Las rutas volátiles (<code>.bash_history</code>, <code>.cache/</code>, <code>tmp/</code>, <code>.local/share/Trash/</code>) quedan fuera de la copia. <code>/var/lib/proxmenux</code> guarda los trabajos de backup programados y el estado del host que mantiene ProxMenux; quedan fuera la preparación de una restauración, las copias de rollback y las marcas posteriores a la restauración, porque pertenecen al host que la ejecuta."
}, },
{ {
"category": "Estado ZFS (condicional)", "category": "Estado ZFS (condicional)",