Merge pull request #341 from f3rs3n/fix/validate-pending-restore

fix(backup): reject incomplete pending restore staging
This commit is contained in:
MacRimi
2026-09-08 20:38:54 +02:00
committed by GitHub
2 changed files with 331 additions and 63 deletions
+94 -63
View File
@@ -2300,59 +2300,71 @@ _rs_prepare_pending_restore() {
local pending_base="/var/lib/proxmenux/restore-pending"
local restore_id pending_dir created_at
restore_id="$(date +%Y%m%d_%H%M%S)"
pending_dir="${pending_base}/${restore_id}"
mkdir -p "$pending_base/completed" || return 1
# Never reuse a published tree, even for two restores in the same second.
pending_dir=$(mktemp -d "${pending_base}/${restore_id}.XXXXXX") || return 1
restore_id="${pending_dir##*/}"
created_at="$(date -Iseconds)"
mkdir -p "$pending_dir/rootfs" "$pending_dir/metadata" "$pending_base/completed" || return 1
# Keep all writes private; callers use if, so do not rely on errexit.
if ! (
mkdir -p "$pending_dir/rootfs" "$pending_dir/metadata" || exit 1
local rel src dst
: > "$pending_dir/apply-on-boot.list"
for rel in "${pending_paths[@]}"; do
src="$staging_root/rootfs/$rel"
[[ -e "$src" ]] || continue
dst="$pending_dir/rootfs/$rel"
mkdir -p "$(dirname "$dst")"
if [[ -d "$src" ]]; then
mkdir -p "$dst"
rsync -aAXH --delete "$src/" "$dst/" 2>/dev/null || true
else
cp -a "$src" "$dst" 2>/dev/null || true
local rel src dst
: > "$pending_dir/apply-on-boot.list" || exit 1
for rel in "${pending_paths[@]}"; do
src="$staging_root/rootfs/$rel"
[[ -e "$src" ]] || {
msg_error "$(translate "Pending restore source is missing:") $rel"
exit 1
}
dst="$pending_dir/rootfs/$rel"
mkdir -p "$(dirname "$dst")" || exit 1
if [[ -d "$src" ]]; then
mkdir -p "$dst" || exit 1
if ! rsync -aAXH --delete "$src/" "$dst/" 2>/dev/null; then
msg_error "$(translate "Could not stage pending restore path:") $rel"
exit 1
fi
else
if ! cp -a "$src" "$dst" 2>/dev/null; then
msg_error "$(translate "Could not stage pending restore path:") $rel"
exit 1
fi
fi
echo "$rel" >> "$pending_dir/apply-on-boot.list" || exit 1
done
if [[ ! -s "$pending_dir/apply-on-boot.list" ]]; then
msg_warn "$(translate "Nothing to schedule for reboot from selected paths.")"
exit 1
fi
echo "$rel" >> "$pending_dir/apply-on-boot.list"
done
if [[ ! -s "$pending_dir/apply-on-boot.list" ]]; then
rm -rf "$pending_dir"
msg_warn "$(translate "Nothing to schedule for reboot from selected paths.")"
return 1
fi
if [[ -d "$staging_root/metadata" ]]; then
cp -a "$staging_root/metadata/." "$pending_dir/metadata/" 2>/dev/null || exit 1
fi
[[ -d "$staging_root/metadata" ]] && cp -a "$staging_root/metadata/." "$pending_dir/metadata/" 2>/dev/null || true
# Persist the rollback plan: VMs/LXCs/components that exist
# on the host but not in the backup. apply_cluster_postboot.sh
# surfaces a read-only "what differs" report from this file
# after the boot. When the operator opted into destructive
# rollback (HB_ROLLBACK_EXECUTE=1) we also destroy those
# guests RIGHT NOW — before the reboot — so the operator
# sees `qm destroy` live in the Monitor terminal AND the
# next boot comes up without those guests reserving hardware
# (critical for GPU passthrough: a stale VM with hostpci
# entries makes Proxmox auto-bind the GPU to vfio-pci before
# the nvidia driver can claim it).
local _rb_script="${SCRIPT_DIR}/restore/compute_rollback_plan.sh"
[[ ! -x "$_rb_script" ]] && _rb_script="${LOCAL_SCRIPTS:-/usr/local/share/proxmenux/scripts}/backup_restore/restore/compute_rollback_plan.sh"
if [[ -x "$_rb_script" ]]; then
bash "$_rb_script" "$staging_root" > "$pending_dir/rollback.json" 2>/dev/null || exit 1
if [[ ! -s "$pending_dir/rollback.json" ]]; then
rm -f "$pending_dir/rollback.json" || exit 1
fi
fi
# Persist the rollback plan: VMs/LXCs/components that exist
# on the host but not in the backup. apply_cluster_postboot.sh
# surfaces a read-only "what differs" report from this file
# after the boot. When the operator opted into destructive
# rollback (HB_ROLLBACK_EXECUTE=1) we also destroy those
# guests RIGHT NOW — before the reboot — so the operator
# sees `qm destroy` live in the Monitor terminal AND the
# next boot comes up without those guests reserving hardware
# (critical for GPU passthrough: a stale VM with hostpci
# entries makes Proxmox auto-bind the GPU to vfio-pci before
# the nvidia driver can claim it).
local _rb_script="${SCRIPT_DIR}/restore/compute_rollback_plan.sh"
[[ ! -x "$_rb_script" ]] && _rb_script="${LOCAL_SCRIPTS:-/usr/local/share/proxmenux/scripts}/backup_restore/restore/compute_rollback_plan.sh"
if [[ -x "$_rb_script" ]]; then
bash "$_rb_script" "$staging_root" > "$pending_dir/rollback.json" 2>/dev/null || true
[[ ! -s "$pending_dir/rollback.json" ]] && rm -f "$pending_dir/rollback.json"
fi
if [[ "${HB_ROLLBACK_EXECUTE:-0}" == "1" && -s "$pending_dir/rollback.json" ]] \
&& command -v jq >/dev/null 2>&1; then
_rs_execute_rollback "$pending_dir/rollback.json"
fi
cat > "$pending_dir/plan.env" <<EOF
cat > "$pending_dir/plan.env" <<EOF || exit 1
RESTORE_ID=${restore_id}
CREATED_AT=${created_at}
HB_RESTORE_INCLUDE_ZFS=${HB_RESTORE_INCLUDE_ZFS:-0}
@@ -2361,26 +2373,45 @@ HB_COMPAT_CROSS_VERSION=${HB_COMPAT_CROSS_VERSION:-0}
HB_COMPAT_KERNEL_DIRECTION=${HB_COMPAT_KERNEL_DIRECTION:-same}
HB_HYDRATION_APPLIED=${HB_HYDRATION_APPLIED:-0}
EOF
# Persist hardware-drift skips so apply_pending_restore.sh can filter
# them at boot. The RS_SKIP_PATHS env var only lives in the restore
# menu session; without writing it to disk, paths that would break
# the boot (stale EFI UUIDs, foreign zpool.cache, ...) leaked through
# to the post-boot apply and ended up corrupting the bootloader.
if [[ -n "${RS_SKIP_PATHS:-}" ]]; then
printf '%s\n' "$RS_SKIP_PATHS" > "$pending_dir/rs-skip-paths.txt"
chmod 600 "$pending_dir/rs-skip-paths.txt"
fi
echo "pending" > "$pending_dir/state"
ln -sfn "$pending_dir" "$pending_base/current"
_rs_install_pending_service_unit "$onboot_script"
systemctl daemon-reload >/dev/null 2>&1 || true
if ! systemctl enable proxmenux-restore-onboot.service >/dev/null 2>&1; then
msg_error "$(translate "Could not enable on-boot restore service.")"
# Persist hardware-drift skips so apply_pending_restore.sh can filter
# them at boot. The RS_SKIP_PATHS env var only lives in the restore
# menu session; without writing it to disk, paths that would break
# the boot (stale EFI UUIDs, foreign zpool.cache, ...) leaked through
# to the post-boot apply and ended up corrupting the bootloader.
if [[ -n "${RS_SKIP_PATHS:-}" ]]; then
printf '%s\n' "$RS_SKIP_PATHS" > "$pending_dir/rs-skip-paths.txt" || exit 1
chmod 600 "$pending_dir/rs-skip-paths.txt" || exit 1
fi
echo "pending" > "$pending_dir/state" || exit 1
); then
rm -rf "$pending_dir"
msg_error "$(translate "Could not stage pending restore. Nothing new was scheduled.")"
return 1
fi
# current is the boot consumer's readiness boundary (state alone is not).
# Prepare a sibling-filesystem link privately, then rename over current;
# never unlink the old job first or let mv follow a directory symlink.
if ! ln -s "$pending_dir" "$pending_dir/.current" \
|| ! _rs_install_pending_service_unit "$onboot_script" \
|| ! systemctl daemon-reload >/dev/null 2>&1 \
|| ! systemctl enable proxmenux-restore-onboot.service >/dev/null 2>&1; then
rm -rf "$pending_dir"
msg_error "$(translate "Could not prepare on-boot restore service. Nothing new was scheduled.")"
return 1
fi
if ! mv -Tf "$pending_dir/.current" "$pending_base/current"; then
rm -rf "$pending_dir"
msg_error "$(translate "Could not publish pending restore. Previous pending restore was kept.")"
return 1
fi
# Destructive rollback is allowed only after staging and publication succeed.
if [[ "${HB_ROLLBACK_EXECUTE:-0}" == "1" && -s "$pending_dir/rollback.json" ]] \
&& command -v jq >/dev/null 2>&1; then
_rs_execute_rollback "$pending_dir/rollback.json"
fi
echo -e ""
echo -e "${TAB}${BGN}$(translate "Pending restore ID:")${CL} ${BL}${restore_id}${CL}"
echo -e "${TAB}${BGN}$(translate "Pending restore dir:")${CL} ${BL}${pending_dir}${CL}"