mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-27 02:48:24 +00:00
update 1.2.2.2 beta
This commit is contained in:
@@ -378,6 +378,40 @@ POSTBOOT_END_EPOCH=$(date +%s)
|
||||
POSTBOOT_DURATION=$((POSTBOOT_END_EPOCH - $(stat -c %Y "$LOG_FILE")))
|
||||
POSTBOOT_DURATION_FMT=$(printf '%dm%02ds' $((POSTBOOT_DURATION / 60)) $((POSTBOOT_DURATION % 60)))
|
||||
|
||||
# ── Rollback delta report (read-only) ──────────────────────────
|
||||
# If _rs_prepare_pending_restore left a rollback.json in the
|
||||
# pending dir, surface the deltas that a full rollback would
|
||||
# touch: VMs/LXCs created after the backup that are still here,
|
||||
# extra components not present in the backup. The operator
|
||||
# decides what to do with them — we don't destroy guests or
|
||||
# uninstall packages automatically (left to a future R2.D fase 2
|
||||
# once every installer ships an --auto-uninstall mode).
|
||||
ROLLBACK_PLAN_FILE=""
|
||||
[[ -n "$PENDING_DIR" && -f "$PENDING_DIR/rollback.json" ]] && \
|
||||
ROLLBACK_PLAN_FILE="$PENDING_DIR/rollback.json"
|
||||
if [[ -n "$ROLLBACK_PLAN_FILE" ]] && command -v jq >/dev/null 2>&1; then
|
||||
rb_vm_extras=$(jq -r '.vms_to_remove | join(", ") // ""' "$ROLLBACK_PLAN_FILE" 2>/dev/null)
|
||||
rb_lxc_extras=$(jq -r '.lxcs_to_remove | join(", ") // ""' "$ROLLBACK_PLAN_FILE" 2>/dev/null)
|
||||
rb_comp_extras=$(jq -r '.components_to_uninstall | join(", ") // ""' "$ROLLBACK_PLAN_FILE" 2>/dev/null)
|
||||
if [[ -n "$rb_vm_extras" || -n "$rb_lxc_extras" || -n "$rb_comp_extras" ]]; then
|
||||
echo ""
|
||||
echo "── Rollback delta report ──"
|
||||
echo "These entries exist on the host but were NOT in the restored backup."
|
||||
echo "Review them and remove manually if a clean rollback is desired:"
|
||||
[[ -n "$rb_vm_extras" ]] && echo " • VMs created after the backup: $rb_vm_extras"
|
||||
[[ -n "$rb_lxc_extras" ]] && echo " • LXCs created after the backup: $rb_lxc_extras"
|
||||
[[ -n "$rb_comp_extras" ]] && echo " • Components installed after the backup: $rb_comp_extras"
|
||||
echo ""
|
||||
echo "Manual cleanup commands (run only if you want to truly roll back):"
|
||||
for _id in $(echo "$rb_vm_extras" | tr ',' ' '); do
|
||||
[[ -n "$_id" ]] && echo " qm stop $_id 2>/dev/null; qm destroy $_id --purge"
|
||||
done
|
||||
for _id in $(echo "$rb_lxc_extras" | tr ',' ' '); do
|
||||
[[ -n "$_id" ]] && echo " pct stop $_id 2>/dev/null; pct destroy $_id --purge"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Notify ProxMenux Monitor that we're done ───────────────────
|
||||
# Routes through the user's configured channels (Telegram, Discord,
|
||||
# ntfy, etc.). Localhost-only endpoint, no auth needed. We try
|
||||
|
||||
0
scripts/backup_restore/apply_pending_restore.sh
Normal file → Executable file
0
scripts/backup_restore/apply_pending_restore.sh
Normal file → Executable file
367
scripts/backup_restore/backup_host.sh
Normal file → Executable file
367
scripts/backup_restore/backup_host.sh
Normal file → Executable file
@@ -1456,8 +1456,16 @@ _rs_apply() {
|
||||
done
|
||||
|
||||
elapsed=$((SECONDS - t_start))
|
||||
[[ "$group" == "hot" || "$group" == "all" ]] && \
|
||||
# Skip `systemctl daemon-reload` when invoked from the Monitor
|
||||
# (HB_MONITOR_FLOW=1). The reload itself doesn't restart the
|
||||
# Monitor's unit, but it marks units as "needs restart" and a
|
||||
# later systemctl call against the Monitor would cut the WS
|
||||
# session. The restored unit files are already on disk — they
|
||||
# take effect at the next reboot, which the Monitor flow asks
|
||||
# for explicitly at the end.
|
||||
if [[ "$group" == "hot" || "$group" == "all" ]] && [[ "${HB_MONITOR_FLOW:-0}" != "1" ]]; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo -e ""
|
||||
echo -e "${TAB}${BOLD}$(translate "Restore applied:")${CL}"
|
||||
@@ -1603,6 +1611,15 @@ _rs_prompt_zfs_opt_in() {
|
||||
|
||||
_rs_finish_flow() {
|
||||
echo -e ""
|
||||
if [[ "${HB_MONITOR_FLOW:-0}" == "1" ]]; then
|
||||
# Same UX as the TUI but the prompt explicitly says "close"
|
||||
# — the Monitor's ScriptTerminalModal sees isComplete=true on
|
||||
# WS close and dismisses (via the onComplete prop) so the
|
||||
# operator doesn't need to click the Close button.
|
||||
msg_success "$(translate "Press Enter to close...")"
|
||||
read -r
|
||||
return 0
|
||||
fi
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
}
|
||||
@@ -1661,6 +1678,10 @@ _rs_offer_reboot_after_pending() {
|
||||
|
||||
local prompt="$(translate "Pending restore prepared. A reboot is required to complete it.")"$'\n\n'"${bg_block}$(translate "Reboot now?")"
|
||||
|
||||
# Same dialog for TUI and Monitor — mirrors the post-install
|
||||
# flow (Yes → reboot, No → "You can reboot later manually").
|
||||
# The Monitor's PTY backs whiptail correctly, so the operator
|
||||
# sees the same Yes/No prompt as in the shell.
|
||||
if whiptail --title "$(translate "Reboot Required")" \
|
||||
--yesno "$prompt" 22 90; then
|
||||
msg_warn "$(translate "Rebooting the system...")"
|
||||
@@ -1800,6 +1821,19 @@ _rs_prepare_pending_restore() {
|
||||
|
||||
[[ -d "$staging_root/metadata" ]] && cp -a "$staging_root/metadata/." "$pending_dir/metadata/" 2>/dev/null || true
|
||||
|
||||
# Persist the rollback plan so apply_cluster_postboot.sh can
|
||||
# surface a clear "what differs vs backup" report after boot.
|
||||
# Read-only here — the script just informs the operator about
|
||||
# VMs/LXCs/components that exist now but not in the backup.
|
||||
# Actual destroy is left manual until --auto-uninstall lands
|
||||
# on every installer.
|
||||
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
|
||||
|
||||
cat > "$pending_dir/plan.env" <<EOF
|
||||
RESTORE_ID=${restore_id}
|
||||
CREATED_AT=${created_at}
|
||||
@@ -2061,18 +2095,22 @@ _rs_component_is_available() {
|
||||
}
|
||||
|
||||
_rs_unique_paths() {
|
||||
local __out_var="$1"
|
||||
# All locals prefixed with `_rup_` so the nameref doesn't bind
|
||||
# to one of OUR locals when a caller passes the same array name
|
||||
# (e.g. `_rs_unique_paths uniq "$@"` — bash would otherwise
|
||||
# resolve `__out_ref → uniq` to our local instead of the caller's).
|
||||
local _rup_out_var="$1"
|
||||
shift
|
||||
local -A seen=()
|
||||
local -a uniq=()
|
||||
local p
|
||||
for p in "$@"; do
|
||||
[[ -z "$p" || -n "${seen[$p]}" ]] && continue
|
||||
seen["$p"]=1
|
||||
uniq+=("$p")
|
||||
local -A _rup_seen=()
|
||||
local -a _rup_uniq=()
|
||||
local _rup_p
|
||||
for _rup_p in "$@"; do
|
||||
[[ -z "$_rup_p" || -n "${_rup_seen[$_rup_p]}" ]] && continue
|
||||
_rup_seen["$_rup_p"]=1
|
||||
_rup_uniq+=("$_rup_p")
|
||||
done
|
||||
local -n __out_ref="$__out_var"
|
||||
__out_ref=("${uniq[@]}")
|
||||
local -n _rup_out_ref="$_rup_out_var"
|
||||
_rup_out_ref=("${_rup_uniq[@]}")
|
||||
}
|
||||
|
||||
_rs_collect_stats_for_paths() {
|
||||
@@ -2125,52 +2163,95 @@ _rs_warn_dangerous_paths() {
|
||||
|
||||
_rs_select_component_paths() {
|
||||
local staging_root="$1"
|
||||
local __out_var="$2"
|
||||
local -n __out_ref="$__out_var"
|
||||
# IMPORTANT: every local inside this function must use a unique
|
||||
# prefix (`_rsp_`). Bash nameref resolution walks the dynamic
|
||||
# scope outwards and stops at the first matching name — if we
|
||||
# declare `local -a selected_paths=()` while the caller's array
|
||||
# is also called `selected_paths`, _rs_unique_paths's
|
||||
# `local -n __out_ref="selected_paths"` would resolve to OUR
|
||||
# local one and the caller's array silently stays empty (which
|
||||
# surfaced as the "Selected paths produced no entries to apply"
|
||||
# dialog despite a non-empty selection).
|
||||
local _rsp_out_var="$2"
|
||||
local -n _rsp_out_ref="$_rsp_out_var"
|
||||
|
||||
local -a component_ids=(
|
||||
network ssh_access host_identity cron_jobs apt_repos kernel_boot
|
||||
systemd_custom scripts root_config root_ssh zfs_cfg postfix_cfg cluster_cfg
|
||||
)
|
||||
local -a checklist=()
|
||||
local comp_id
|
||||
for comp_id in "${component_ids[@]}"; do
|
||||
_rs_component_is_available "$staging_root" "$comp_id" || continue
|
||||
checklist+=("$comp_id" "$(_rs_component_label "$comp_id")" "off")
|
||||
done
|
||||
|
||||
if [[ ${#checklist[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No components available")" \
|
||||
--msgbox "$(translate "No restorable components were detected in this backup.")" 8 68
|
||||
return 1
|
||||
# ── Build the list of paths actually present in this backup ──
|
||||
local -a _rsp_backup=()
|
||||
local _rsp_selected_file="$staging_root/metadata/selected_paths.txt"
|
||||
if [[ -f "$_rsp_selected_file" ]]; then
|
||||
local _rsp_rel
|
||||
while IFS= read -r _rsp_rel; do
|
||||
[[ -z "$_rsp_rel" ]] && continue
|
||||
_rsp_rel="${_rsp_rel#/}"
|
||||
[[ -e "$staging_root/rootfs/$_rsp_rel" ]] && _rsp_backup+=("$_rsp_rel")
|
||||
done < "$_rsp_selected_file"
|
||||
else
|
||||
# Pre-metadata backup: walk rootfs/ at depth 1-2.
|
||||
local _rsp_entry
|
||||
while IFS= read -r _rsp_entry; do
|
||||
[[ -z "$_rsp_entry" ]] && continue
|
||||
_rsp_entry="${_rsp_entry#./}"
|
||||
_rsp_backup+=("$_rsp_entry")
|
||||
done < <(cd "$staging_root/rootfs" 2>/dev/null \
|
||||
&& find . -mindepth 1 -maxdepth 2 -print 2>/dev/null)
|
||||
fi
|
||||
|
||||
local selected
|
||||
selected=$(dialog --backtitle "ProxMenux" --separate-output \
|
||||
--title "$(translate "Custom restore by components")" \
|
||||
--checklist "\n$(translate "Select components to restore:")" \
|
||||
24 94 14 "${checklist[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
if [[ -z "$selected" ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No components selected")" \
|
||||
--msgbox "$(translate "Select at least one component to continue.")" 8 66
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a selected_paths=()
|
||||
while IFS= read -r comp_id; do
|
||||
[[ -z "$comp_id" ]] && continue
|
||||
local rel
|
||||
while IFS= read -r rel; do
|
||||
[[ -n "$rel" && -e "$staging_root/rootfs/$rel" ]] && selected_paths+=("$rel")
|
||||
done < <(_rs_component_paths "$comp_id")
|
||||
done <<< "$selected"
|
||||
|
||||
_rs_unique_paths "$__out_var" "${selected_paths[@]}"
|
||||
|
||||
if [[ ${#__out_ref[@]} -eq 0 ]]; then
|
||||
if [[ ${#_rsp_backup[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No paths available")" \
|
||||
--msgbox "$(translate "Selected components have no matching paths in this backup.")" 8 72
|
||||
--msgbox "$(translate "This backup does not contain any restorable paths.")" 8 70
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── Non-interactive bypass for the Monitor ──
|
||||
local _rsp_selected=""
|
||||
if [[ -n "${HB_PRESELECTED_PATHS:-}" ]]; then
|
||||
local IFS=','
|
||||
local _rsp_pp
|
||||
for _rsp_pp in $HB_PRESELECTED_PATHS; do
|
||||
_rsp_pp="${_rsp_pp#"${_rsp_pp%%[![:space:]]*}"}"
|
||||
_rsp_pp="${_rsp_pp%"${_rsp_pp##*[![:space:]]}"}"
|
||||
[[ -z "$_rsp_pp" ]] && continue
|
||||
_rsp_pp="${_rsp_pp#/}"
|
||||
local _rsp_known
|
||||
for _rsp_known in "${_rsp_backup[@]}"; do
|
||||
[[ "$_rsp_pp" == "$_rsp_known" ]] && { _rsp_selected+="${_rsp_pp}"$'\n'; break; }
|
||||
done
|
||||
done
|
||||
unset IFS
|
||||
fi
|
||||
|
||||
# ── Dialog checklist (one entry per real backup path) ──
|
||||
if [[ -z "$_rsp_selected" ]]; then
|
||||
local -a _rsp_checklist=()
|
||||
local _rsp_p
|
||||
for _rsp_p in "${_rsp_backup[@]}"; do
|
||||
_rsp_checklist+=("$_rsp_p" "/$_rsp_p" "off")
|
||||
done
|
||||
|
||||
_rsp_selected=$(dialog --backtitle "ProxMenux" --separate-output \
|
||||
--title "$(translate "Custom restore by paths")" \
|
||||
--checklist "\n$(translate "Select the paths to restore (one per backup entry):")" \
|
||||
24 94 16 "${_rsp_checklist[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
if [[ -z "$_rsp_selected" ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Nothing selected")" \
|
||||
--msgbox "$(translate "Select at least one path to continue.")" 8 66
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local -a _rsp_picked=()
|
||||
local _rsp_line
|
||||
while IFS= read -r _rsp_line; do
|
||||
[[ -z "$_rsp_line" ]] && continue
|
||||
_rsp_picked+=("$_rsp_line")
|
||||
done <<< "$_rsp_selected"
|
||||
|
||||
_rs_unique_paths "$_rsp_out_var" "${_rsp_picked[@]}"
|
||||
|
||||
if [[ ${#_rsp_out_ref[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Nothing to restore")" \
|
||||
--msgbox "$(translate "Selected paths produced no entries to apply.")" 8 70
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
@@ -2183,127 +2264,74 @@ _rs_run_custom_restore() {
|
||||
_rs_select_component_paths "$staging_root" selected_paths || return 1
|
||||
_rs_collect_stats_for_paths "${selected_paths[@]}"
|
||||
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Custom restore")" \
|
||||
--menu "\n$(translate "Selected component paths:") ${RS_SEL_TOTAL}" \
|
||||
"$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
|
||||
1 "$(translate "Apply safe changes now") (${RS_SEL_HOT})" \
|
||||
2 "$(translate "Apply safe + reboot-required") ($((RS_SEL_HOT + RS_SEL_REBOOT)))" \
|
||||
3 "$(translate "Apply all selected now (advanced)") (${RS_SEL_TOTAL})" \
|
||||
4 "$(translate "Reselect components")" \
|
||||
5 "$(translate "Apply safe now + schedule remaining for next boot")" \
|
||||
6 "$(translate "Schedule selected components for next boot (no live apply)")" \
|
||||
0 "$(translate "Return")" \
|
||||
3>&1 1>&2 2>&3) || return 1
|
||||
# ── Smart auto-strategy (no 6-option menu any more) ──
|
||||
# The classifier already split every selected path into:
|
||||
# RS_SEL_HOT — applyable live, no risk
|
||||
# RS_SEL_REBOOT — needs reboot to take effect
|
||||
# RS_SEL_DANGEROUS — applyable live but risky (e.g. network
|
||||
# from a SSH session)
|
||||
# The operator doesn't need to know that taxonomy. We just:
|
||||
# 1. Show ONE confirmation summarizing what happens.
|
||||
# 2. Apply hot paths now.
|
||||
# 3. Schedule reboot + dangerous paths for next boot.
|
||||
# 4. Tell the operator a reboot is needed when there's pending.
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
if [[ "$RS_SEL_HOT" -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Nothing to apply")" \
|
||||
--msgbox "$(translate "No safe-now paths in selected components.")" 8 60
|
||||
continue
|
||||
fi
|
||||
if ! whiptail --title "$(translate "Confirm")" \
|
||||
--yesno "$(translate "Apply safe changes from selected components now?")" 9 72; then
|
||||
continue
|
||||
fi
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Applying selected safe changes")"
|
||||
_rs_apply "$staging_root" hot "${selected_paths[@]}"
|
||||
[[ "$RS_SEL_REBOOT" -gt 0 || "$RS_SEL_DANGEROUS" -gt 0 ]] && \
|
||||
msg_warn "$(translate "Some selected paths were not applied in safe mode.")"
|
||||
_rs_finish_flow
|
||||
return 0
|
||||
;;
|
||||
if [[ "$RS_SEL_TOTAL" -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Nothing to restore")" \
|
||||
--msgbox "$(translate "Selected paths produced no entries to apply.")" 8 70
|
||||
return 1
|
||||
fi
|
||||
|
||||
2)
|
||||
if [[ $((RS_SEL_HOT + RS_SEL_REBOOT)) -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Nothing to apply")" \
|
||||
--msgbox "$(translate "No safe/reboot paths in selected components.")" 8 64
|
||||
continue
|
||||
fi
|
||||
if ! whiptail --title "$(translate "Confirm")" \
|
||||
--yesno "$(translate "Apply safe + reboot-required paths from selected components now?")"$'\n\n'"$(translate "Risky live paths will be skipped.")" \
|
||||
11 78; then
|
||||
continue
|
||||
fi
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Applying selected safe + reboot changes")"
|
||||
[[ "$RS_SEL_HOT" -gt 0 ]] && _rs_apply "$staging_root" hot "${selected_paths[@]}"
|
||||
[[ "$RS_SEL_REBOOT" -gt 0 ]] && _rs_apply "$staging_root" reboot "${selected_paths[@]}"
|
||||
[[ "$RS_SEL_DANGEROUS" -gt 0 ]] && \
|
||||
msg_warn "$(translate "Risky selected paths were skipped in this mode.")"
|
||||
_rs_finish_flow
|
||||
return 0
|
||||
;;
|
||||
# SSH/network protection — if the operator is on a SSH session
|
||||
# and selected network paths, offer to move EVERYTHING to next
|
||||
# boot (avoids disconnection mid-restore). _rs_handle_ssh_network_risk
|
||||
# returns 2 when it has already scheduled for boot — in that case
|
||||
# we're done.
|
||||
local ssh_network_rc
|
||||
_rs_handle_ssh_network_risk "$staging_root" "${selected_paths[@]}"
|
||||
ssh_network_rc=$?
|
||||
[[ $ssh_network_rc -eq 2 ]] && return 0
|
||||
[[ $ssh_network_rc -ne 0 ]] && return 1
|
||||
|
||||
3)
|
||||
local ssh_network_rc
|
||||
_rs_handle_ssh_network_risk "$staging_root" "${selected_paths[@]}"
|
||||
ssh_network_rc=$?
|
||||
[[ $ssh_network_rc -eq 2 ]] && return 0
|
||||
[[ $ssh_network_rc -ne 0 ]] && continue
|
||||
local hot_count="$RS_SEL_HOT"
|
||||
local pending_count=$(( RS_SEL_REBOOT + RS_SEL_DANGEROUS ))
|
||||
|
||||
[[ "$RS_SEL_DANGEROUS" -gt 0 ]] && _rs_warn_dangerous_paths "${selected_paths[@]}"
|
||||
if ! whiptail --title "$(translate "Final confirmation")" \
|
||||
--yesno "$(translate "Apply ALL selected component paths now? This can include risky paths.")" \
|
||||
10 78; then
|
||||
continue
|
||||
fi
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Applying all selected component paths")"
|
||||
_rs_apply "$staging_root" all "${selected_paths[@]}"
|
||||
_rs_finish_flow
|
||||
return 0
|
||||
;;
|
||||
local body
|
||||
body="\Zb$(translate "About to restore") ${RS_SEL_TOTAL} $(translate "selected path(s):")\ZB"$'\n\n'
|
||||
if (( hot_count > 0 )); then
|
||||
body+=" • $(translate "Apply") \Zb\Z4${hot_count}\Zn $(translate "now")"$'\n'
|
||||
fi
|
||||
if (( pending_count > 0 )); then
|
||||
body+=" • $(translate "Schedule") \Zb\Z4${pending_count}\Zn $(translate "for next boot (needs reboot to take effect)")"$'\n'
|
||||
fi
|
||||
if (( pending_count > 0 )); then
|
||||
body+=$'\n'"\Zb\Z4$(translate "A reboot will be required to complete the restore.")\Zn"$'\n'
|
||||
fi
|
||||
body+=$'\n'"\Zb$(translate "Continue?")\ZB"
|
||||
|
||||
4)
|
||||
_rs_select_component_paths "$staging_root" selected_paths || continue
|
||||
_rs_collect_stats_for_paths "${selected_paths[@]}"
|
||||
;;
|
||||
if ! dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate "Confirm custom restore")" \
|
||||
--yesno "$body" 14 82; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
5)
|
||||
if ! whiptail --title "$(translate "Confirm")" \
|
||||
--yesno "$(translate "Apply safe selected paths now and schedule remaining selected paths for next boot?")" \
|
||||
10 82; then
|
||||
continue
|
||||
fi
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Applying safe selected paths and preparing pending restore")"
|
||||
[[ "$RS_SEL_HOT" -gt 0 ]] && _rs_apply "$staging_root" hot "${selected_paths[@]}"
|
||||
local -a pending_paths=()
|
||||
mapfile -t pending_paths < <(_rs_collect_pending_paths remaining_after_hot "${selected_paths[@]}")
|
||||
if _rs_prepare_pending_restore "$staging_root" "${pending_paths[@]}"; then
|
||||
msg_warn "$(translate "Reboot is required to complete the pending restore.")"
|
||||
fi
|
||||
_rs_finish_flow
|
||||
return 0
|
||||
;;
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Applying custom restore")"
|
||||
|
||||
6)
|
||||
if ! whiptail --title "$(translate "Confirm")" \
|
||||
--yesno "$(translate "Schedule selected component paths for next boot without applying live changes now?")" \
|
||||
10 82; then
|
||||
continue
|
||||
fi
|
||||
local -a pending_paths=()
|
||||
mapfile -t pending_paths < <(_rs_collect_pending_paths all_selected "${selected_paths[@]}")
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Preparing selected pending restore")"
|
||||
if _rs_prepare_pending_restore "$staging_root" "${pending_paths[@]}"; then
|
||||
msg_warn "$(translate "Reboot is required to apply the scheduled restore.")"
|
||||
fi
|
||||
_rs_finish_flow
|
||||
return 0
|
||||
;;
|
||||
if (( hot_count > 0 )); then
|
||||
_rs_apply "$staging_root" hot "${selected_paths[@]}"
|
||||
fi
|
||||
|
||||
0)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if (( pending_count > 0 )); then
|
||||
local -a pending_paths=()
|
||||
mapfile -t pending_paths < <(_rs_collect_pending_paths remaining_after_hot "${selected_paths[@]}")
|
||||
if _rs_prepare_pending_restore "$staging_root" "${pending_paths[@]}"; then
|
||||
msg_warn "$(translate "Reboot is required to complete the pending restore.")"
|
||||
fi
|
||||
fi
|
||||
|
||||
_rs_finish_flow
|
||||
return 0
|
||||
}
|
||||
|
||||
# Extras the Complete restore runs INLINE after applying file
|
||||
@@ -2562,4 +2590,9 @@ main_menu() {
|
||||
done
|
||||
}
|
||||
|
||||
main_menu
|
||||
# Only spawn the TUI when invoked directly. Sourcing the file
|
||||
# (e.g. from restore/monitor_apply.sh) imports all the functions
|
||||
# without triggering the main menu.
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main_menu
|
||||
fi
|
||||
|
||||
0
scripts/backup_restore/backup_scheduler.sh
Normal file → Executable file
0
scripts/backup_restore/backup_scheduler.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/build_manifest.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/build_manifest.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_guests.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_guests.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_hardware.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_hardware.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_kernel.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_kernel.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_proxmenux_state.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_proxmenux_state.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_source_host.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_source_host.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_storage.sh
Normal file → Executable file
0
scripts/backup_restore/collectors/collect_storage.sh
Normal file → Executable file
0
scripts/backup_restore/lib_host_backup_common.sh
Normal file → Executable file
0
scripts/backup_restore/lib_host_backup_common.sh
Normal file → Executable file
144
scripts/backup_restore/restore/compute_rollback_plan.sh
Executable file
144
scripts/backup_restore/restore/compute_rollback_plan.sh
Executable file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux — compute rollback plan (read-only)
|
||||
# ==========================================================
|
||||
# Compares a staged restore vs the current host state and emits
|
||||
# a JSON document describing what a "rollback to backup state"
|
||||
# would do:
|
||||
#
|
||||
# {
|
||||
# "backup_time": "...",
|
||||
# "vms_in_backup": [100, 101],
|
||||
# "vms_in_host": [100, 101, 105],
|
||||
# "vms_to_remove": [105], // exist now, not in backup
|
||||
# "vms_to_restore": [100, 101], // present in both → reapplied
|
||||
# "lxcs_in_backup": [201],
|
||||
# "lxcs_in_host": [201, 202],
|
||||
# "lxcs_to_remove": [202],
|
||||
# "lxcs_to_restore": [201],
|
||||
# "components_to_uninstall": ["coral_driver"], // installed now, missing in backup
|
||||
# "components_to_reinstall": ["nvidia_driver"] // installed in backup
|
||||
# }
|
||||
#
|
||||
# No side effects whatsoever — pure inspection. The actual
|
||||
# rollback is applied later by apply_cluster_postboot.sh after
|
||||
# the operator confirms in the UI.
|
||||
#
|
||||
# Usage:
|
||||
# compute_rollback_plan.sh <staging-directory>
|
||||
#
|
||||
# The staging directory must already be the post-extract layout
|
||||
# (rootfs/ + metadata/) — list_paths.sh's normalization should
|
||||
# have run first.
|
||||
# ==========================================================
|
||||
set -e
|
||||
|
||||
STAGING="${1:-}"
|
||||
if [[ -z "$STAGING" || ! -d "$STAGING" ]]; then
|
||||
printf 'compute_rollback_plan: usage: %s <staging-directory>\n' "$0" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
# ── Discover guest configs (VMs + LXCs) on both sides ──
|
||||
# /etc/pve/nodes/<node>/qemu-server/<id>.conf for VMs
|
||||
# /etc/pve/nodes/<node>/lxc/<id>.conf for LXCs
|
||||
# We collect just the numeric IDs; the rollback only needs to
|
||||
# know "exists in A, exists in B" — actual config rewrite is
|
||||
# done by the standard restore path for /etc/pve.
|
||||
|
||||
_collect_ids() {
|
||||
# $1 = root directory, $2 = subdir under /etc/pve/nodes/<n>/
|
||||
local root="$1" sub="$2"
|
||||
if [[ -d "$root/etc/pve/nodes" ]]; then
|
||||
find "$root/etc/pve/nodes" -mindepth 3 -maxdepth 3 -path "*/$sub/*.conf" -print 2>/dev/null \
|
||||
| sed -E "s|.*/([0-9]+)\\.conf$|\\1|" | sort -un
|
||||
fi
|
||||
}
|
||||
|
||||
mapfile -t vms_backup < <(_collect_ids "$STAGING/rootfs" qemu-server)
|
||||
mapfile -t vms_host < <(_collect_ids "/" qemu-server)
|
||||
mapfile -t lxcs_backup < <(_collect_ids "$STAGING/rootfs" lxc)
|
||||
mapfile -t lxcs_host < <(_collect_ids "/" lxc)
|
||||
|
||||
_diff() {
|
||||
# Echoes lines from A that are NOT in B.
|
||||
local -n a_ref=$1
|
||||
local -n b_ref=$2
|
||||
local x y found
|
||||
for x in "${a_ref[@]}"; do
|
||||
found=0
|
||||
for y in "${b_ref[@]}"; do
|
||||
[[ "$x" == "$y" ]] && { found=1; break; }
|
||||
done
|
||||
(( found == 0 )) && echo "$x"
|
||||
done
|
||||
}
|
||||
|
||||
mapfile -t vms_to_remove < <(_diff vms_host vms_backup)
|
||||
mapfile -t vms_to_restore < <(_diff vms_backup vms_host || true; for x in "${vms_backup[@]}"; do for y in "${vms_host[@]}"; do [[ "$x" == "$y" ]] && echo "$x"; done; done | sort -un)
|
||||
mapfile -t vms_to_restore < <(printf '%s\n' "${vms_backup[@]}")
|
||||
mapfile -t lxcs_to_remove < <(_diff lxcs_host lxcs_backup)
|
||||
mapfile -t lxcs_to_restore < <(printf '%s\n' "${lxcs_backup[@]}")
|
||||
|
||||
# ── Components: parse JSON on both sides, list deltas ──
|
||||
backup_components_file="$STAGING/rootfs/usr/local/share/proxmenux/components_status.json"
|
||||
host_components_file="/usr/local/share/proxmenux/components_status.json"
|
||||
|
||||
_installed_keys() {
|
||||
# Echoes one component key per line for `status == "installed"`.
|
||||
local f="$1"
|
||||
[[ ! -f "$f" ]] && return 0
|
||||
command -v jq >/dev/null 2>&1 || { return 0; }
|
||||
jq -r 'to_entries[] | select(.value.status == "installed") | .key' "$f" 2>/dev/null | sort -u
|
||||
}
|
||||
|
||||
mapfile -t comps_backup < <(_installed_keys "$backup_components_file")
|
||||
mapfile -t comps_host < <(_installed_keys "$host_components_file")
|
||||
mapfile -t comps_to_uninstall < <(_diff comps_host comps_backup)
|
||||
mapfile -t comps_to_reinstall < <(printf '%s\n' "${comps_backup[@]}")
|
||||
|
||||
# ── Backup timestamp from metadata for the UI ──
|
||||
backup_time=""
|
||||
if [[ -f "$STAGING/metadata/run_info.env" ]]; then
|
||||
backup_time=$(grep -m1 '^generated_at=' "$STAGING/metadata/run_info.env" 2>/dev/null \
|
||||
| cut -d= -f2-)
|
||||
fi
|
||||
|
||||
# ── Emit JSON. Pure printf — no jq required for the writer ──
|
||||
_json_arr_int() {
|
||||
printf '['
|
||||
local first=1 v
|
||||
for v in "$@"; do
|
||||
[[ -z "$v" ]] && continue
|
||||
if (( first )); then printf '%s' "$v"; first=0
|
||||
else printf ',%s' "$v"
|
||||
fi
|
||||
done
|
||||
printf ']'
|
||||
}
|
||||
_json_arr_str() {
|
||||
printf '['
|
||||
local first=1 v esc
|
||||
for v in "$@"; do
|
||||
[[ -z "$v" ]] && continue
|
||||
esc="${v//\\/\\\\}"; esc="${esc//\"/\\\"}"
|
||||
if (( first )); then printf '"%s"' "$esc"; first=0
|
||||
else printf ',"%s"' "$esc"
|
||||
fi
|
||||
done
|
||||
printf ']'
|
||||
}
|
||||
|
||||
printf '{'
|
||||
printf '"backup_time":"%s"' "${backup_time//\"/\\\"}"
|
||||
printf ',"vms_in_backup":'; _json_arr_int "${vms_backup[@]}"
|
||||
printf ',"vms_in_host":'; _json_arr_int "${vms_host[@]}"
|
||||
printf ',"vms_to_remove":'; _json_arr_int "${vms_to_remove[@]}"
|
||||
printf ',"vms_to_restore":'; _json_arr_int "${vms_to_restore[@]}"
|
||||
printf ',"lxcs_in_backup":'; _json_arr_int "${lxcs_backup[@]}"
|
||||
printf ',"lxcs_in_host":'; _json_arr_int "${lxcs_host[@]}"
|
||||
printf ',"lxcs_to_remove":'; _json_arr_int "${lxcs_to_remove[@]}"
|
||||
printf ',"lxcs_to_restore":'; _json_arr_int "${lxcs_to_restore[@]}"
|
||||
printf ',"components_to_uninstall":'; _json_arr_str "${comps_to_uninstall[@]}"
|
||||
printf ',"components_to_reinstall":'; _json_arr_str "${comps_to_reinstall[@]}"
|
||||
printf '}\n'
|
||||
68
scripts/backup_restore/restore/list_paths.sh
Executable file
68
scripts/backup_restore/restore/list_paths.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux — list backup paths actually present in staging
|
||||
# ==========================================================
|
||||
# Reads <staging>/metadata/selected_paths.txt (the canonical
|
||||
# record of WHICH paths went into this backup) and emits a JSON
|
||||
# array of those that still exist under <staging>/rootfs/.
|
||||
#
|
||||
# This replaces the 13-component grouping used previously: the
|
||||
# Custom restore checklist now shows one entry per real path
|
||||
# (default profile + any operator-added extras), so what you
|
||||
# backed up is exactly what you can restore.
|
||||
#
|
||||
# Usage:
|
||||
# list_paths.sh <staging-directory>
|
||||
#
|
||||
# Output: JSON array of paths with a leading slash, e.g.:
|
||||
# ["/etc/pve","/etc/network","/opt/mistuff"]
|
||||
# ==========================================================
|
||||
set -e
|
||||
|
||||
STAGING="${1:-}"
|
||||
if [[ -z "$STAGING" || ! -d "$STAGING" ]]; then
|
||||
printf 'list_paths: usage: %s <staging-directory>\n' "$0" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
SELECTED="$STAGING/metadata/selected_paths.txt"
|
||||
ROOTFS="$STAGING/rootfs"
|
||||
|
||||
paths=()
|
||||
if [[ -f "$SELECTED" ]]; then
|
||||
# The file stores paths without the leading slash (etc/pve,
|
||||
# var/lib/pve-cluster, ...). Filter to only those that are
|
||||
# actually present in the staged rootfs/ — anything in
|
||||
# missing_paths.txt at backup time would otherwise show up
|
||||
# here even though there's nothing to restore.
|
||||
while IFS= read -r rel; do
|
||||
[[ -z "$rel" ]] && continue
|
||||
rel="${rel#/}"
|
||||
if [[ -e "$ROOTFS/$rel" ]]; then
|
||||
paths+=("/$rel")
|
||||
fi
|
||||
done < "$SELECTED"
|
||||
else
|
||||
# No selected_paths.txt sidecar — backup might be from a
|
||||
# pre-metadata era. Walk rootfs/ at depth 1-2 and use those
|
||||
# as the path list. Best-effort fallback.
|
||||
while IFS= read -r entry; do
|
||||
[[ -z "$entry" ]] && continue
|
||||
paths+=("/${entry#./}")
|
||||
done < <(cd "$ROOTFS" 2>/dev/null && find . -mindepth 1 -maxdepth 2 -print 2>/dev/null | sed 's|^\./||')
|
||||
fi
|
||||
|
||||
# Emit JSON array — keep it dependency-free (no jq).
|
||||
printf '['
|
||||
first=1
|
||||
for p in "${paths[@]}"; do
|
||||
esc="${p//\\/\\\\}"
|
||||
esc="${esc//\"/\\\"}"
|
||||
if (( first )); then
|
||||
printf '"%s"' "$esc"
|
||||
first=0
|
||||
else
|
||||
printf ',"%s"' "$esc"
|
||||
fi
|
||||
done
|
||||
printf ']\n'
|
||||
107
scripts/backup_restore/restore/monitor_apply.sh
Executable file
107
scripts/backup_restore/restore/monitor_apply.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux Monitor restore wrapper
|
||||
# ==========================================================
|
||||
# Bridges the Monitor's "Restore" button to the TUI restore
|
||||
# functions in backup_host.sh. The Monitor takes care of the
|
||||
# extraction (snapshot → staging directory + manifest), then
|
||||
# launches this script in a ScriptTerminalModal so the operator
|
||||
# sees the same dialog flow used by the shell TUI.
|
||||
#
|
||||
# The Monitor's script_runner only forwards env vars (no argv).
|
||||
# Inputs (env):
|
||||
# STAGING — absolute path to the prepared staging dir
|
||||
# MODE — "full" or "custom"
|
||||
# PATHS — CSV of absolute backup paths, only when MODE=custom
|
||||
# (e.g. "/etc/pve,/etc/network,/opt/mistuff").
|
||||
# Each must be present in <staging>/metadata/selected_paths.txt
|
||||
# or the wrapper drops it. Omit to let the TUI checklist
|
||||
# pop up so the operator can pick interactively.
|
||||
# ==========================================================
|
||||
# Note: NO `set -e` here. backup_host.sh + its sourced utils/lib
|
||||
# emit a few non-zero exit codes during load (locale checks,
|
||||
# optional commands missing, etc.) — under set -e those abort the
|
||||
# wrapper silently and the operator only sees "Connection closed"
|
||||
# in the Monitor terminal with no clue why. We rely on explicit
|
||||
# error handling inside _rs_run_complete_guided / _rs_run_custom_restore.
|
||||
|
||||
STAGING="${STAGING:-}"
|
||||
MODE="${MODE:-full}"
|
||||
PATHS="${PATHS:-}"
|
||||
|
||||
# Also accept --staging / --mode / --paths positional args for
|
||||
# direct CLI use outside the Monitor (handy for debugging).
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--staging) shift; STAGING="${1:-}" ;;
|
||||
--mode) shift; MODE="${1:-full}" ;;
|
||||
--paths) shift; PATHS="${1:-}" ;;
|
||||
*) ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ -z "$STAGING" || ! -d "$STAGING" ]]; then
|
||||
printf 'monitor_apply: STAGING missing or not a directory (%s)\n' "$STAGING" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
HB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# Source the library + the TUI. backup_host.sh carries a guard so
|
||||
# sourcing it does NOT spawn the main menu — only the functions
|
||||
# (_rs_check_layout, _rs_run_complete_guided, _rs_run_custom_restore,
|
||||
# _rs_apply, ...) get loaded.
|
||||
# shellcheck source=/dev/null
|
||||
. "$HB_DIR/lib_host_backup_common.sh"
|
||||
# shellcheck source=/dev/null
|
||||
. "$HB_DIR/backup_host.sh"
|
||||
|
||||
# Normalize the staging layout to the canonical rootfs/+metadata/
|
||||
# shape. The Monitor's Python extract path already does this, but
|
||||
# rerunning here keeps the wrapper safe when invoked directly.
|
||||
if ! _rs_check_layout "$STAGING"; then
|
||||
printf 'monitor_apply: archive layout not recognized in %s\n' "$STAGING" >&2
|
||||
exit 65
|
||||
fi
|
||||
|
||||
# Tell the apply helpers that we're running under the Monitor.
|
||||
# Two effects:
|
||||
# • _rs_apply skips its final `systemctl daemon-reload` so the
|
||||
# Monitor service unit isn't marked "needs restart" and the
|
||||
# WebSocket survives the apply (the restored unit file IS
|
||||
# written to disk — it'll take effect at the next reboot).
|
||||
# • _rs_run_complete_guided / _rs_run_custom_restore skip the
|
||||
# second "Reboot now?" dialog and instead emit one final
|
||||
# "Restore prepared — reboot when ready" line. The operator
|
||||
# closes the modal in the web UI and reboots from the host
|
||||
# panel; no need for two consecutive yes/no prompts.
|
||||
export HB_MONITOR_FLOW=1
|
||||
|
||||
case "$MODE" in
|
||||
full)
|
||||
# The TUI path runs `_rs_collect_plan_stats` before calling
|
||||
# `_rs_run_complete_guided`. We were skipping it here and that's
|
||||
# why the confirm dialog showed "0 rutas seguras ahora /
|
||||
# 0 rutas para el próximo arranque" when launched from the Monitor.
|
||||
_rs_collect_plan_stats "$STAGING"
|
||||
_rs_run_complete_guided "$STAGING"
|
||||
;;
|
||||
custom)
|
||||
if [[ -n "$PATHS" ]]; then
|
||||
HB_PRESELECTED_PATHS="$PATHS" _rs_run_custom_restore "$STAGING"
|
||||
else
|
||||
_rs_run_custom_restore "$STAGING"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
printf 'monitor_apply: unknown MODE %s (expected: full | custom)\n' "$MODE" >&2
|
||||
exit 66
|
||||
;;
|
||||
esac
|
||||
|
||||
# Clean exit. _rs_finish_flow under HB_MONITOR_FLOW already returns
|
||||
# without waiting for Enter, so falling off the end here closes the
|
||||
# PTY → the WS closes → the modal sees `isComplete=true` (script_runner
|
||||
# sends "[Script exited with code 0]" on normal termination) so the
|
||||
# ScriptTerminalModal won't try to auto-reconnect.
|
||||
0
scripts/backup_restore/restore/parse_manifest.sh
Normal file → Executable file
0
scripts/backup_restore/restore/parse_manifest.sh
Normal file → Executable file
0
scripts/backup_restore/restore/preflight_checks.sh
Normal file → Executable file
0
scripts/backup_restore/restore/preflight_checks.sh
Normal file → Executable file
0
scripts/backup_restore/restore/reinstall_drivers.sh
Normal file → Executable file
0
scripts/backup_restore/restore/reinstall_drivers.sh
Normal file → Executable file
0
scripts/backup_restore/restore/remap_network.sh
Normal file → Executable file
0
scripts/backup_restore/restore/remap_network.sh
Normal file → Executable file
0
scripts/backup_restore/restore/restore_modes.sh
Normal file → Executable file
0
scripts/backup_restore/restore/restore_modes.sh
Normal file → Executable file
0
scripts/backup_restore/restore/run_restore.sh
Normal file → Executable file
0
scripts/backup_restore/restore/run_restore.sh
Normal file → Executable file
0
scripts/backup_restore/restore/validate_storage.sh
Normal file → Executable file
0
scripts/backup_restore/restore/validate_storage.sh
Normal file → Executable file
0
scripts/backup_restore/run_scheduled_backup.sh
Normal file → Executable file
0
scripts/backup_restore/run_scheduled_backup.sh
Normal file → Executable file
Reference in New Issue
Block a user