mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-26 18:38:30 +00:00
create 1.2.2.3 beta
This commit is contained in:
@@ -19,11 +19,114 @@ set +u
|
||||
|
||||
MARKER="${PMX_CLUSTER_APPLY_MARKER:-/var/lib/proxmenux/cluster-apply-pending}"
|
||||
LOG_DIR="${PMX_LOG_DIR:-/var/log/proxmenux}"
|
||||
# State file the Monitor Web polls to show a live progress card on
|
||||
# the Backups tab. A dismiss action (POST /api/host-backups/restore/dismiss)
|
||||
# just flips `acknowledged` to true — the file itself lives until the
|
||||
# next restore overwrites it, and a copy is archived under history/
|
||||
# when the run finishes so the operator can browse past restores.
|
||||
STATE_DIR="/var/lib/proxmenux"
|
||||
STATE_FILE="$STATE_DIR/restore-state.json"
|
||||
HISTORY_DIR="$STATE_DIR/restore-history"
|
||||
mkdir -p "$STATE_DIR" "$HISTORY_DIR" >/dev/null 2>&1 || true
|
||||
|
||||
mkdir -p "$LOG_DIR" >/dev/null 2>&1 || true
|
||||
LOG_FILE="${LOG_DIR}/proxmenux-cluster-postboot-$(date +%Y%m%d_%H%M%S).log"
|
||||
exec >>"$LOG_FILE" 2>&1
|
||||
|
||||
# ── State-file helpers ─────────────────────────────────────────
|
||||
# Every milestone advances `steps_done` and optionally updates a
|
||||
# handful of other fields. Writes go through a temp file + rename
|
||||
# so the Monitor never reads a half-written JSON. All calls are
|
||||
# `|| true` at the callsite — if jq or write fails, the restore
|
||||
# still proceeds; only the UI progress reporting suffers.
|
||||
_state_started_at="$(date -Iseconds)"
|
||||
_state_steps_total=0
|
||||
_state_steps_done=0
|
||||
_state_write() {
|
||||
# Merges a JSON snippet ($1) into the existing state file.
|
||||
# Missing state file → seeded from an empty JSON object first.
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
[[ -f "$STATE_FILE" ]] || echo '{}' > "$STATE_FILE"
|
||||
local tmp
|
||||
tmp=$(mktemp "${STATE_FILE}.XXXXXX") || return 0
|
||||
if jq -c ". * $1" "$STATE_FILE" > "$tmp" 2>/dev/null; then
|
||||
mv -f "$tmp" "$STATE_FILE"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
}
|
||||
_state_step() {
|
||||
# Called as a *step transition*: the previous step just finished,
|
||||
# start the next one. Increments steps_done and sets the label to
|
||||
# $1. Init seeds current_step with the first step's label; every
|
||||
# _state_step call after that advances to the NEXT step.
|
||||
#
|
||||
# Example flow with 3 total steps:
|
||||
# init → steps_done=0, current_step="Applying cluster config"
|
||||
# step "Foo" → steps_done=1, current_step="Foo"
|
||||
# step "Bar" → steps_done=2, current_step="Bar"
|
||||
# _state_finish→ steps_done=3, status="complete"
|
||||
local label="$1"
|
||||
_state_steps_done=$((_state_steps_done + 1))
|
||||
# jq variable names sdone/stotal — plain `done` collides with the
|
||||
# bash reserved word when the arg is on its own continuation line.
|
||||
_state_write "$(jq -n \
|
||||
--arg step "$label" \
|
||||
--argjson sdone "$_state_steps_done" \
|
||||
--argjson stotal "$_state_steps_total" \
|
||||
'{current_step:$step, steps_done:$sdone, steps_total:$stotal}')"
|
||||
}
|
||||
_state_component() {
|
||||
# Add or update an entry in state.components. Arrays are replaced
|
||||
# by jq's `*` merge, so plain _state_write can't be used for the
|
||||
# per-component list — this helper explicitly appends new entries
|
||||
# and updates existing ones by name.
|
||||
# $1 = component name (nvidia_driver, coral_driver, …)
|
||||
# $2 = status: installing | ok | failed
|
||||
# $3 = per-component log path (empty allowed)
|
||||
# $4 = installer exit code (only meaningful for failed; empty otherwise)
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
[[ -f "$STATE_FILE" ]] || echo '{}' > "$STATE_FILE"
|
||||
local tmp
|
||||
tmp=$(mktemp "${STATE_FILE}.XXXXXX") || return 0
|
||||
if jq -c \
|
||||
--arg name "$1" \
|
||||
--arg status "$2" \
|
||||
--arg log "$3" \
|
||||
--arg rc "${4:-}" \
|
||||
'($.components // []) as $comps
|
||||
| ($comps | map(.name) | index($name)) as $idx
|
||||
| ({name:$name, status:$status, log:$log}
|
||||
+ (if $rc == "" then {} else {exit_code:$rc} end)) as $entry
|
||||
| .components =
|
||||
(if $idx == null then $comps + [$entry]
|
||||
else ($comps | map(if .name == $name then $entry else . end)) end)' \
|
||||
"$STATE_FILE" > "$tmp" 2>/dev/null; then
|
||||
mv -f "$tmp" "$STATE_FILE"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
}
|
||||
_state_finish() {
|
||||
# $1 = "complete" | "failed"
|
||||
local final_status="$1"
|
||||
_state_write "$(jq -n \
|
||||
--arg s "$final_status" \
|
||||
--arg t "$(date -Iseconds)" \
|
||||
--arg dur "${POSTBOOT_DURATION_FMT:-}" \
|
||||
'{status:$s, finished_at:$t, duration:$dur}')"
|
||||
# Archive a copy to history/ so past restores stay browsable
|
||||
# from the Monitor even after the operator dismisses the card.
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
cp -f "$STATE_FILE" "$HISTORY_DIR/$(date +%Y%m%d_%H%M%S)-${final_status}.json" 2>/dev/null || true
|
||||
# Keep the last 20 entries in the history dir. Everything
|
||||
# older is expected to have been reviewed already. Using
|
||||
# find+sort-by-mtime keeps this safe against odd filenames.
|
||||
find "$HISTORY_DIR" -maxdepth 1 -type f -name '*.json' -printf '%T@ %p\n' 2>/dev/null \
|
||||
| sort -rn | tail -n +21 | cut -d' ' -f2- | xargs -r rm -f 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== ProxMenux cluster post-boot apply at $(date -Iseconds) ==="
|
||||
|
||||
if [[ ! -f "$MARKER" ]]; then
|
||||
@@ -44,9 +147,51 @@ echo "Pending dir: $PENDING_DIR"
|
||||
echo "Needs initramfs: $NEEDS_INITRAMFS"
|
||||
echo "Needs grub: $NEEDS_GRUB"
|
||||
|
||||
# Compute how many milestones the Monitor will see for this run.
|
||||
# Base 3 = apply /etc/pve + boot sanity check + finalize. Add one
|
||||
# for each optional phase that will actually run.
|
||||
_state_steps_total=3
|
||||
[[ "$NEEDS_INITRAMFS" == "1" ]] && _state_steps_total=$((_state_steps_total + 1))
|
||||
[[ "$NEEDS_GRUB" == "1" ]] && _state_steps_total=$((_state_steps_total + 1))
|
||||
# Count components that will be re-installed via --auto-reinstall.
|
||||
# Read the same components_status.json the reinstall loop uses so
|
||||
# our step count matches exactly what the loop will process.
|
||||
_COMP_STATUS_PATH="/usr/local/share/proxmenux/components_status.json"
|
||||
_COMPONENT_KEYS=(nvidia_driver amdgpu_top intel_gpu_tools coral_driver)
|
||||
_comps_to_reinstall=0
|
||||
if command -v jq >/dev/null 2>&1 && [[ -f "$_COMP_STATUS_PATH" ]]; then
|
||||
for _k in "${_COMPONENT_KEYS[@]}"; do
|
||||
[[ "$(jq -r ".$_k.status // \"\"" "$_COMP_STATUS_PATH" 2>/dev/null)" == "installed" ]] \
|
||||
&& _comps_to_reinstall=$((_comps_to_reinstall + 1))
|
||||
done
|
||||
fi
|
||||
(( _comps_to_reinstall > 0 )) && _state_steps_total=$((_state_steps_total + _comps_to_reinstall))
|
||||
|
||||
# Seed the initial state file so the Monitor's poll sees the run
|
||||
# almost immediately after the postboot unit starts. `acknowledged`
|
||||
# false means the Backups tab card will show; the operator flips
|
||||
# it to true (via POST /dismiss) after they've read the summary.
|
||||
_state_write "$(jq -n \
|
||||
--arg started "$_state_started_at" \
|
||||
--arg log "$LOG_FILE" \
|
||||
--argjson steps "$_state_steps_total" \
|
||||
'{status:"running",
|
||||
started_at:$started,
|
||||
finished_at:null,
|
||||
current_step:"Applying cluster config",
|
||||
steps_done:0,
|
||||
steps_total:$steps,
|
||||
log_path:$log,
|
||||
components:[],
|
||||
rollback_delta:{},
|
||||
sanity_warnings:[],
|
||||
summary:null,
|
||||
acknowledged:false}')"
|
||||
|
||||
if [[ -z "$RECOVERY_ROOT" || ! -d "$RECOVERY_ROOT" ]]; then
|
||||
echo "Recovery root invalid — aborting cleanly."
|
||||
rm -f "$MARKER"
|
||||
_state_finish "failed" || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -282,6 +427,7 @@ if [[ "$NEEDS_INITRAMFS" == "1" ]] || [[ "$NEEDS_GRUB" == "1" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "$NEEDS_INITRAMFS" == "1" ]] && command -v update-initramfs >/dev/null 2>&1; then
|
||||
_state_step "Rebuilding initramfs" || true
|
||||
echo "Running: update-initramfs -u -k all (5-10 min — restore touched initramfs inputs)"
|
||||
if update-initramfs -u -k all 2>&1 | tail -10; then
|
||||
echo " ✓ update-initramfs done"
|
||||
@@ -296,6 +442,7 @@ else
|
||||
fi
|
||||
|
||||
if [[ "$NEEDS_GRUB" == "1" ]] && command -v update-grub >/dev/null 2>&1; then
|
||||
_state_step "Updating bootloader" || true
|
||||
echo "Running: update-grub"
|
||||
if update-grub 2>&1 | tail -3; then
|
||||
echo " ✓ update-grub done"
|
||||
@@ -358,6 +505,8 @@ if command -v jq >/dev/null 2>&1 && [[ -f "$COMPONENTS_STATUS" ]]; then
|
||||
|
||||
echo ""
|
||||
echo " → $comp (running $installer --auto-reinstall)"
|
||||
_state_step "Reinstalling $comp" || true
|
||||
_state_component "$comp" "installing" "" ""
|
||||
# Redirect to a per-component log instead of piping. NVIDIA's
|
||||
# runfile installer forks helpers that inherit stdout, so a
|
||||
# `bash $installer | sed | tail` pipeline never sees EOF after
|
||||
@@ -368,8 +517,10 @@ if command -v jq >/dev/null 2>&1 && [[ -f "$COMPONENTS_STATUS" ]]; then
|
||||
sed -e 's/^/ /' "$comp_log" | tail -15
|
||||
if (( rc == 0 )); then
|
||||
echo " ✓ $comp ok (full log: $comp_log)"
|
||||
_state_component "$comp" "ok" "$comp_log" ""
|
||||
else
|
||||
echo " ✗ $comp installer exited $rc — see $comp_log"
|
||||
_state_component "$comp" "failed" "$comp_log" "$rc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
@@ -393,6 +544,14 @@ 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)
|
||||
# Copy the structured plan into the state file so the Monitor's
|
||||
# detail modal can render each list as its own table without
|
||||
# re-parsing the CSV strings above.
|
||||
_state_write "$(jq -c '{rollback_delta:{
|
||||
vms_to_remove: (.vms_to_remove // []),
|
||||
lxcs_to_remove: (.lxcs_to_remove // []),
|
||||
components_to_uninstall: (.components_to_uninstall // [])
|
||||
}}' "$ROLLBACK_PLAN_FILE" 2>/dev/null)" || true
|
||||
if [[ -n "$rb_vm_extras" || -n "$rb_lxc_extras" || -n "$rb_comp_extras" ]]; then
|
||||
echo ""
|
||||
echo "── Rollback delta report ──"
|
||||
@@ -419,6 +578,7 @@ fi
|
||||
# configured, or a stale reference survived. Runs on every restore
|
||||
# (cheap); warnings feed the completion notification so it tells
|
||||
# the truth instead of a blanket "all fine".
|
||||
_state_step "Boot sanity check" || true
|
||||
SANITY_WARNINGS=""
|
||||
_sanity_warn() {
|
||||
if [[ -z "$SANITY_WARNINGS" ]]; then
|
||||
@@ -455,6 +615,14 @@ if [[ -n "$SANITY_WARNINGS" ]]; then
|
||||
echo "Cross-version: ${HB_COMPAT_CROSS_VERSION:-0}"
|
||||
fi
|
||||
|
||||
# Persist sanity warnings as a JSON array so the Monitor's detail
|
||||
# modal can render each warning as its own line. Empty warnings →
|
||||
# empty array, which the UI treats as "sanity OK".
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
_state_write "$(jq -cn --arg s "$SANITY_WARNINGS" \
|
||||
'{sanity_warnings: (if $s == "" then [] else ($s | split("; ")) end)}')" || true
|
||||
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
|
||||
@@ -509,6 +677,21 @@ if command -v curl >/dev/null 2>&1; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Persist a compact summary the Monitor's card can render inline.
|
||||
# Mirrors what the notification block sends; keeps a single source
|
||||
# of truth for both the alert channels and the Web UI.
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
_state_write "$(jq -cn \
|
||||
--arg hostname "$(hostname)" \
|
||||
--arg guests "${copied_guests:-0}" \
|
||||
--arg stubs "${stub_created:-0}" \
|
||||
--arg stale_nodes "${removed_nodes:-0}" \
|
||||
--arg components "${COMPONENTS_REINSTALLED_CSV:-none}" \
|
||||
--arg duration "$POSTBOOT_DURATION_FMT" \
|
||||
'{summary:{hostname:$hostname, guests:$guests, stubs:$stubs, stale_nodes:$stale_nodes, components:$components, duration:$duration}}')" || true
|
||||
fi
|
||||
_state_finish "complete" || true
|
||||
|
||||
echo ""
|
||||
echo "=== Apply finished at $(date -Iseconds) — total ${POSTBOOT_DURATION_FMT} ==="
|
||||
echo "Log: $LOG_FILE"
|
||||
|
||||
@@ -163,7 +163,7 @@ _bk_pbs() {
|
||||
# is still passphrase-protected by openssl.
|
||||
if [[ -n "$HB_PBS_KEYFILE_OPT" && -f "$HB_STATE_DIR/pbs-key.recovery.enc" ]]; then
|
||||
hb_pbs_upload_recovery_blob "$epoch" \
|
||||
|| msg_warn "$(translate "Recovery blob upload failed — main backup is OK, but keyfile recovery from PBS will not be available for this snapshot.")"
|
||||
|| msg_warn "$(translate "Recovery blob upload failed — main backup is OK, but keyfile recovery from PBS will not be available for this backup.")"
|
||||
fi
|
||||
|
||||
elapsed=$((SECONDS - t_start))
|
||||
@@ -174,7 +174,7 @@ _bk_pbs() {
|
||||
echo -e "${TAB}${BGN}$(translate "Method:")${CL} ${BL}Proxmox Backup Server (PBS)${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Repository:")${CL} ${BL}${HB_PBS_REPOSITORY}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Backup ID:")${CL} ${BL}${backup_id}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Snapshot:")${CL} ${BL}host/${backup_id}/${snap_time}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Backup path:")${CL} ${BL}host/${backup_id}/${snap_time}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Data size:")${CL} ${BL}${staged_size}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Duration:")${CL} ${BL}$(hb_human_elapsed "$elapsed")${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Encryption:")${CL} ${BL}${_pbs_enc_label}${CL}"
|
||||
@@ -843,10 +843,11 @@ _rs_extract_pbs() {
|
||||
--output-format json 2>/dev/null \
|
||||
| jq -r '.[]
|
||||
| select(."backup-type" == "host"
|
||||
and not (
|
||||
(."backup-id" | startswith("proxmenux-keyrecovery-"))
|
||||
or ((."backup-id" | startswith("hostcfg-")) and (."backup-id" | endswith("-keyrecovery")))
|
||||
))
|
||||
and (
|
||||
((."backup-id" | startswith("proxmenux-keyrecovery-"))
|
||||
or ((."backup-id" | startswith("hostcfg-")) and (."backup-id" | endswith("-keyrecovery"))))
|
||||
| not
|
||||
))
|
||||
| "\(."backup-type")|\(."backup-id")|\(."backup-time")"' 2>/dev/null \
|
||||
| while IFS='|' read -r _type _id _epoch; do
|
||||
local _iso
|
||||
@@ -863,8 +864,8 @@ _rs_extract_pbs() {
|
||||
# it. msg_error alone gets erased the moment we `return 1`
|
||||
# because the restore_menu loop redraws the source picker
|
||||
# immediately afterward.
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No snapshots")" \
|
||||
--msgbox "$(translate "No host snapshots were found in this PBS repository:")"$'\n\n'"$HB_PBS_REPOSITORY" \
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No backups")" \
|
||||
--msgbox "$(translate "No host backups were found in this PBS repository:")"$'\n\n'"$HB_PBS_REPOSITORY" \
|
||||
10 78
|
||||
return 1
|
||||
fi
|
||||
@@ -873,8 +874,8 @@ _rs_extract_pbs() {
|
||||
for snapshot in "${snapshots[@]}"; do menu+=("$i" "$snapshot"); ((i++)); done
|
||||
local sel
|
||||
sel=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Select snapshot to restore")" \
|
||||
--menu "\n$(translate "Available host snapshots:")" \
|
||||
--title "$(translate "Select backup to restore")" \
|
||||
--menu "\n$(translate "Available host backups:")" \
|
||||
"$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" "${menu[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
snapshot="${snapshots[$((sel-1))]}"
|
||||
|
||||
@@ -893,7 +894,7 @@ _rs_extract_pbs() {
|
||||
)
|
||||
if [[ ${#archives[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No archives")" \
|
||||
--msgbox "$(translate "No .pxar archives were found in this snapshot:")"$'\n\n'"$snapshot" \
|
||||
--msgbox "$(translate "No .pxar archives were found in this backup:")"$'\n\n'"$snapshot" \
|
||||
10 78
|
||||
return 1
|
||||
fi
|
||||
@@ -915,7 +916,7 @@ _rs_extract_pbs() {
|
||||
msg_title "$(translate "Restore from PBS → staging")"
|
||||
echo -e ""
|
||||
echo -e "${TAB}${BGN}$(translate "Repository:")${CL} ${BL}${HB_PBS_REPOSITORY}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Snapshot:")${CL} ${BL}${snapshot}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Backup:")${CL} ${BL}${snapshot}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Archive:")${CL} ${BL}${archive}${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Staging directory:")${CL} ${BL}${staging_root}${CL}"
|
||||
echo -e ""
|
||||
@@ -960,16 +961,16 @@ _rs_extract_pbs() {
|
||||
# error rather than the raw log.
|
||||
local extra_hint=""
|
||||
if grep -qiE 'encryption key|unable to (load|read) key|no key (file|found)|decrypt|failed to decrypt' "$log_file" 2>/dev/null; then
|
||||
extra_hint=$'\n\n'"$(translate "This snapshot is encrypted but no keyfile is available on this host.")"
|
||||
extra_hint=$'\n\n'"$(translate "This backup is encrypted but no keyfile is available on this host.")"
|
||||
if [[ -f "$HB_STATE_DIR/pbs-key.conf" ]]; then
|
||||
extra_hint+=$'\n\n'"$(translate "A keyfile is present but doesn't match the one used to create the snapshot. Make sure you have the correct keyfile from the source host.")"
|
||||
extra_hint+=$'\n\n'"$(translate "A keyfile is present but doesn't match the one used to create the backup. Make sure you have the correct keyfile from the source host.")"
|
||||
else
|
||||
extra_hint+=$'\n\n'"$(translate "No keyfile recovery copy was found in PBS for this snapshot — it was created before the recovery feature existed. The encrypted content cannot be recovered.")"
|
||||
extra_hint+=$'\n\n'"$(translate "No keyfile recovery copy was found in PBS for this backup — it was created before the recovery feature existed. The encrypted content cannot be recovered.")"
|
||||
fi
|
||||
fi
|
||||
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "PBS extraction failed")" \
|
||||
--msgbox "$(translate "Could not extract from PBS.")"$'\n\n'"$(translate "Snapshot:") $snapshot"$'\n'"$(translate "Archive:") $archive$extra_hint" \
|
||||
--msgbox "$(translate "Could not extract from PBS.")"$'\n\n'"$(translate "Backup:") $snapshot"$'\n'"$(translate "Archive:") $archive$extra_hint" \
|
||||
16 78
|
||||
hb_show_log "$log_file" "$(translate "PBS restore error log")"
|
||||
return 1
|
||||
@@ -1751,8 +1752,9 @@ _rs_offer_reboot_after_pending() {
|
||||
bg_block+=" • ${label} (${eta})"$'\n'
|
||||
done
|
||||
bg_block+=$'\n'"$(translate "Monitor progress:")"$'\n'
|
||||
bg_block+=" tail -f /var/log/proxmenux/proxmenux-cluster-postboot-*.log"$'\n'
|
||||
bg_block+=" systemctl status proxmenux-apply-cluster-postboot.service"$'\n\n'
|
||||
bg_block+=" • $(translate "ProxMenux Monitor → Backups tab (live progress card with ETA, logs, rollback delta)")"$'\n'
|
||||
bg_block+=" • tail -f /var/log/proxmenux/proxmenux-cluster-postboot-*.log"$'\n'
|
||||
bg_block+=" • systemctl status proxmenux-apply-cluster-postboot.service"$'\n\n'
|
||||
bg_block+="$(translate "If notifications are enabled (Telegram/Discord/ntfy/...), you will receive a \"Host restore finished\" message when all background tasks complete.")"$'\n\n'
|
||||
fi
|
||||
|
||||
@@ -2681,6 +2683,7 @@ _rs_run_complete_guided() {
|
||||
body+=$'\n'"${RS_HYDRATION_SUMMARY}"
|
||||
fi
|
||||
body+=$'\n'"\Zb\Z4$(translate "A reboot is required to finish the restore.")\Zn"$'\n\n'
|
||||
body+="$(translate "After the reboot you can follow the post-restore work live from ProxMenux Monitor → Backups tab (ETA, per-component status, log tail, rollback delta).")"$'\n\n'
|
||||
body+="$(translate "If notifications are enabled (Telegram/Discord/ntfy/...), you will receive a \"Host restore finished\" message when all background tasks complete.")"$'\n\n'
|
||||
body+="\Zb$(translate "Continue?")\ZB"
|
||||
|
||||
@@ -3089,6 +3092,7 @@ _rs_run_custom_restore() {
|
||||
fi
|
||||
if (( pending_count > 0 )); then
|
||||
body+=$'\n'"\Zb\Z4$(translate "A reboot will be required to complete the restore.")\Zn"$'\n'
|
||||
body+="$(translate "Follow post-restore progress live from ProxMenux Monitor → Backups tab after the reboot.")"$'\n'
|
||||
fi
|
||||
body+=$'\n'"\Zb$(translate "Continue?")\ZB"
|
||||
|
||||
|
||||
@@ -75,6 +75,22 @@ _list_jobs() {
|
||||
done | sort
|
||||
}
|
||||
|
||||
# Same as _list_jobs but skips one-shot manual runs (Sprint B).
|
||||
# Manual entries are `.env` files with `MANUAL_RUN=1` — they're
|
||||
# closed executions, not configured tasks. Used by "Show jobs"
|
||||
# and "Run a job now" so those views only surface real backup
|
||||
# tasks; Delete / Toggle keep using _list_jobs so the operator
|
||||
# can still clean up leftover manual entries from those menus.
|
||||
_list_scheduled_jobs() {
|
||||
local f id
|
||||
for f in "$JOBS_DIR"/*.env; do
|
||||
[[ -f "$f" ]] || continue
|
||||
grep -q '^MANUAL_RUN=1$' "$f" 2>/dev/null && continue
|
||||
id=$(basename "$f" .env)
|
||||
printf '%s\n' "$id"
|
||||
done | sort
|
||||
}
|
||||
|
||||
# Returns 0 if the job is attached to a PVE vzdump storage (no systemd
|
||||
# timer — the trigger comes from the vzdump hook, matched by PVE_STORAGE
|
||||
# against $STOREID set by PVE for every backup phase).
|
||||
@@ -443,9 +459,18 @@ _create_job() {
|
||||
_pick_job() {
|
||||
local title="$1"
|
||||
local __out_var="$2"
|
||||
# Optional scope: "all" (default) surfaces every .env in JOBS_DIR
|
||||
# including one-shot manual runs; "scheduled" filters those out so
|
||||
# Run-now only shows real configured tasks. Delete / Toggle keep
|
||||
# the default so the operator can still clean up manual leftovers.
|
||||
local scope="${3:-all}"
|
||||
|
||||
local -a ids=()
|
||||
mapfile -t ids < <(_list_jobs)
|
||||
if [[ "$scope" == "scheduled" ]]; then
|
||||
mapfile -t ids < <(_list_scheduled_jobs)
|
||||
else
|
||||
mapfile -t ids < <(_list_jobs)
|
||||
fi
|
||||
if [[ ${#ids[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No jobs")" \
|
||||
--msgbox "$(translate "No scheduled backup jobs found.")" 8 62
|
||||
@@ -495,8 +520,11 @@ _render_action_screen() {
|
||||
}
|
||||
|
||||
_job_run_now() {
|
||||
# "scheduled" scope — one-shot manual runs are closed executions,
|
||||
# not tasks to re-fire. Filtering them out of this picker prevents
|
||||
# accidental re-runs of an operator's past manual backups.
|
||||
local id=""
|
||||
_pick_job "$(translate "Run job now")" id || return 1
|
||||
_pick_job "$(translate "Run job now")" id scheduled || return 1
|
||||
# Defensive guard against a future regression of the nameref-shadowing
|
||||
# bug that left $id empty here on 2026-06-07. Without this, the runner
|
||||
# gets called with no argument and emits "Usage: ... <job_id>".
|
||||
@@ -527,8 +555,12 @@ _job_run_now() {
|
||||
}
|
||||
|
||||
_job_toggle() {
|
||||
# "scheduled" scope — one-shot manual runs carry ENABLED=0 by
|
||||
# definition and can't be re-fired from a timer, so offering them
|
||||
# in this picker was noise. Delete keeps the "all" scope so the
|
||||
# operator can still clean up manual entries from that menu.
|
||||
local id=""
|
||||
_pick_job "$(translate "Enable/Disable job")" id || return 1
|
||||
_pick_job "$(translate "Enable/Disable job")" id scheduled || return 1
|
||||
if [[ -z "$id" ]]; then
|
||||
_render_action_screen "$(translate "Enable/Disable job")"
|
||||
msg_error "$(translate "Job selection returned empty id — aborting.")"
|
||||
@@ -610,21 +642,79 @@ _job_delete() {
|
||||
_show_jobs() {
|
||||
local tmp
|
||||
tmp=$(mktemp) || return
|
||||
|
||||
# Per-job block: header line with status badge + 3 detail lines
|
||||
# summarising the backend destination and the last run — same
|
||||
# information the Monitor UI shows on the Backups tab, condensed
|
||||
# for the shell dialog.
|
||||
local -a job_ids=()
|
||||
local id
|
||||
while IFS= read -r id; do
|
||||
[[ -z "$id" ]] && continue
|
||||
job_ids+=("$id")
|
||||
done < <(_list_scheduled_jobs)
|
||||
|
||||
{
|
||||
echo "=== $(translate "Scheduled backup jobs") ==="
|
||||
echo ""
|
||||
local id
|
||||
while IFS= read -r id; do
|
||||
[[ -z "$id" ]] && continue
|
||||
echo "• $id [$(_show_job_status "$id")]"
|
||||
if [[ -f "${LOG_DIR}/${id}-last.status" ]]; then
|
||||
sed 's/^/ /' "${LOG_DIR}/${id}-last.status"
|
||||
fi
|
||||
echo ""
|
||||
done < <(_list_jobs)
|
||||
if [[ ${#job_ids[@]} -eq 0 ]]; then
|
||||
translate "No scheduled backup jobs configured."
|
||||
else
|
||||
local status backend dest label profile last_run last_result
|
||||
for id in "${job_ids[@]}"; do
|
||||
status=$(_show_job_status "$id")
|
||||
backend=$(_job_env_get "$id" BACKEND || echo "")
|
||||
profile=$(_job_env_get "$id" PROFILE_MODE || echo default)
|
||||
case "$backend" in
|
||||
pbs)
|
||||
local pbs_repo pbs_bid
|
||||
pbs_repo=$(_job_env_get "$id" PBS_REPOSITORY || echo "?")
|
||||
pbs_bid=$(_job_env_get "$id" PBS_BACKUP_ID || echo "?")
|
||||
dest="$pbs_repo (id=$pbs_bid)"
|
||||
label="PBS"
|
||||
;;
|
||||
local)
|
||||
dest=$(_job_env_get "$id" LOCAL_DEST_DIR || echo "?")
|
||||
label="Local archive"
|
||||
;;
|
||||
borg)
|
||||
dest=$(_job_env_get "$id" BORG_REPO || echo "?")
|
||||
label="Borg"
|
||||
;;
|
||||
*)
|
||||
dest="?"
|
||||
label="${backend:-?}"
|
||||
;;
|
||||
esac
|
||||
last_run=""; last_result=""
|
||||
if [[ -f "${LOG_DIR}/${id}-last.status" ]]; then
|
||||
local last_at
|
||||
last_at=$(grep -m1 '^RUN_AT=' "${LOG_DIR}/${id}-last.status" | cut -d= -f2-)
|
||||
last_result=$(grep -m1 '^RESULT=' "${LOG_DIR}/${id}-last.status" | cut -d= -f2-)
|
||||
# Trim the timezone suffix for readability (14:13:54 vs 14:13:54+02:00)
|
||||
last_run="${last_at%%+*}"
|
||||
last_run="${last_run%%-[0-9][0-9]:[0-9][0-9]}"
|
||||
last_run="${last_run/T/ }"
|
||||
fi
|
||||
|
||||
printf "• %s [%s]\n" "$id" "$status"
|
||||
printf " %-9s %s · %s\n" "$(translate "Backend:")" "$label" "$dest"
|
||||
printf " %-9s %s\n" "$(translate "Profile:")" "$profile"
|
||||
if [[ -n "$last_run" ]]; then
|
||||
printf " %-9s %s → %s\n" "$(translate "Last run:")" "$last_run" "${last_result:-?}"
|
||||
else
|
||||
printf " %-9s %s\n" "$(translate "Last run:")" "$(translate "never")"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
fi
|
||||
} > "$tmp"
|
||||
|
||||
# Same window size as the parent scheduler menu — keeps the
|
||||
# dimensions consistent across the flow instead of one dialog
|
||||
# shrinking or growing per view.
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Scheduled backup jobs")" \
|
||||
--textbox "$tmp" 28 100 || true
|
||||
--textbox "$tmp" "$HB_UI_MENU_H" "$HB_UI_MENU_W" || true
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
|
||||
@@ -953,20 +953,18 @@ hb_pbs_import_keyfile() {
|
||||
return 0
|
||||
}
|
||||
|
||||
hb_pbs_setup_recovery() {
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
|
||||
# Choosing "encrypt" already implies wanting to protect the keyfile.
|
||||
# No second yes/no — go straight to the passphrase input. Cancel at
|
||||
# any passphrase prompt propagates up as return 1 so the caller can
|
||||
# drop the operator back to the previous menu.
|
||||
# Prompt for a recovery passphrase pair; on OK returns the passphrase on
|
||||
# stdout. Cancel or empty input returns non-zero WITHOUT creating any
|
||||
# file. The caller decides when — and whether — to persist a keyfile
|
||||
# and its recovery blob after this. Splitting the prompt out of the
|
||||
# side-effectful phase means "user cancels passphrase" leaves the disk
|
||||
# in exactly the state it was before the encryption flow started.
|
||||
_hb_pbs_prompt_recovery_pass() {
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Recovery setup failed")" \
|
||||
--msgbox "$(hb_translate "openssl is not installed — cannot create recovery copy. Install openssl and retry.")" 9 70
|
||||
return 1
|
||||
fi
|
||||
|
||||
local pass1 pass2
|
||||
while true; do
|
||||
pass1=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Recovery passphrase")" \
|
||||
@@ -980,8 +978,20 @@ hb_pbs_setup_recovery() {
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Passphrases do not match. Try again.")" 8 50
|
||||
done
|
||||
printf '%s' "$pass1"
|
||||
}
|
||||
|
||||
if ! printf '%s' "$pass1" | hb_pbs_encrypt_recovery "$key_file" "$recovery_enc"; then
|
||||
# Encrypt the on-disk keyfile with the supplied passphrase, drop an
|
||||
# offsite copy under /root, and show the operator the success dialog.
|
||||
# Precondition: the keyfile exists at $HB_STATE_DIR/pbs-key.conf.
|
||||
# Returns non-zero only on openssl failure (rare). The caller is
|
||||
# responsible for undoing whatever it just did if this fails.
|
||||
_hb_pbs_finalize_recovery() {
|
||||
local pass="$1"
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
|
||||
if ! printf '%s' "$pass" | hb_pbs_encrypt_recovery "$key_file" "$recovery_enc"; then
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Recovery setup failed")" \
|
||||
--msgbox "$(hb_translate "openssl encryption failed.")" 9 70
|
||||
return 1
|
||||
@@ -1009,6 +1019,15 @@ hb_pbs_setup_recovery() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Public wrapper preserved for external callers that already have a
|
||||
# keyfile on disk and just want the recovery blob generated. Combines
|
||||
# the prompt + finalize phases.
|
||||
hb_pbs_setup_recovery() {
|
||||
local pass
|
||||
pass=$(_hb_pbs_prompt_recovery_pass) || return 1
|
||||
_hb_pbs_finalize_recovery "$pass"
|
||||
}
|
||||
|
||||
# Upload the local recovery .enc to PBS as a separate snapshot
|
||||
# group. Called from _bk_pbs after the main backup succeeds.
|
||||
# Skips silently if no recovery copy is present. Returns 0 on
|
||||
@@ -1111,7 +1130,7 @@ hb_pbs_try_keyfile_recovery() {
|
||||
local recovery_snapshot="host/${picked_id}/${iso}"
|
||||
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Keyfile recovery available")" \
|
||||
--yesno "$(hb_translate "Local keyfile is missing but a recovery copy was found in PBS.")"$'\n\n'"$(hb_translate "Snapshot:") $recovery_snapshot"$'\n\n'"$(hb_translate "Recover the keyfile using your recovery passphrase?")" \
|
||||
--yesno "$(hb_translate "Local keyfile is missing but a recovery copy was found in PBS.")"$'\n\n'"$(hb_translate "Backup:") $recovery_snapshot"$'\n\n'"$(hb_translate "Recover the keyfile using your recovery passphrase?")" \
|
||||
13 78 || return 1
|
||||
|
||||
# Download the blob once; we may retry passphrase entry without
|
||||
@@ -1160,166 +1179,176 @@ hb_pbs_try_keyfile_recovery() {
|
||||
}
|
||||
|
||||
|
||||
# Internal helper: create a fresh PBS keyfile at the canonical path,
|
||||
# offer the recovery passphrase, and export HB_PBS_KEYFILE_OPT.
|
||||
# Returns 0 on success, 1 on any failure or cancel (leaves state
|
||||
# clean — canonical path is wiped on cancel so the next attempt
|
||||
# starts fresh).
|
||||
# Internal helper: create a fresh PBS keyfile at the canonical path
|
||||
# and its paired recovery blob. The passphrase is prompted BEFORE the
|
||||
# keyfile hits disk — so if the operator cancels at the passphrase
|
||||
# prompt, nothing is created and nothing needs cleaning up. The only
|
||||
# time we ever have to remove a keyfile here is if openssl fails to
|
||||
# encrypt the recovery blob (rare); that's a real error, not a cancel.
|
||||
_hb_pbs_create_new_keyfile() {
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
|
||||
# 1. Collect the recovery passphrase. Cancel here → zero side effect.
|
||||
local pass
|
||||
pass=$(_hb_pbs_prompt_recovery_pass) || return 1
|
||||
|
||||
# 2. Create the keyfile. Point of no return: any failure past here
|
||||
# leaves us with state to clean up.
|
||||
msg_info "$(hb_translate "Creating PBS encryption key...")"
|
||||
mkdir -p "$HB_STATE_DIR"
|
||||
local create_stderr
|
||||
create_stderr=$(proxmox-backup-client key create --kdf none "$key_file" </dev/null 2>&1 >/dev/null)
|
||||
local create_rc=$?
|
||||
if [[ $create_rc -eq 0 && -s "$key_file" ]]; then
|
||||
chmod 600 "$key_file"
|
||||
msg_ok "$(hb_translate "Encryption key created:") $key_file"
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
if ! hb_pbs_setup_recovery; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
if [[ $create_rc -ne 0 || ! -s "$key_file" ]]; then
|
||||
# Sweep any zero-byte residue and surface the real error.
|
||||
[[ -f "$key_file" && ! -s "$key_file" ]] && rm -f "$key_file" 2>/dev/null
|
||||
local err_msg
|
||||
err_msg="$(hb_translate "Failed to create encryption key. Backup cancelled — fix the underlying issue and retry.")"$'\n\n'
|
||||
err_msg+="$(hb_translate "Tool exit code:") $create_rc"$'\n'
|
||||
err_msg+="$(hb_translate "Tool output:")"$'\n'
|
||||
err_msg+="${create_stderr:-(empty)}"
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption key creation failed")" \
|
||||
--msgbox "$err_msg" 14 78
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
chmod 600 "$key_file"
|
||||
msg_ok "$(hb_translate "Encryption key created:") $key_file"
|
||||
|
||||
# 3. Encrypt the recovery blob with the already-collected passphrase.
|
||||
# Only an openssl-level failure lands us in the wipe branch.
|
||||
if ! _hb_pbs_finalize_recovery "$pass"; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create failed — sweep zero-byte residue and surface the real error.
|
||||
[[ -f "$key_file" && ! -s "$key_file" ]] && rm -f "$key_file" 2>/dev/null
|
||||
local err_msg
|
||||
err_msg="$(hb_translate "Failed to create encryption key. Backup cancelled — fix the underlying issue and retry.")"$'\n\n'
|
||||
err_msg+="$(hb_translate "Tool exit code:") $create_rc"$'\n'
|
||||
err_msg+="$(hb_translate "Tool output:")"$'\n'
|
||||
err_msg+="${create_stderr:-(empty)}"
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption key creation failed")" \
|
||||
--msgbox "$err_msg" 14 78
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Internal helper: prompt the operator for a source path, validate
|
||||
# with hb_pbs_import_keyfile, install at the canonical path, offer
|
||||
# the recovery passphrase, and export HB_PBS_KEYFILE_OPT. Returns
|
||||
# 0 on success, 1 on cancel or failure (state is left clean).
|
||||
# Internal helper: prompt for the source path, validate WITHOUT
|
||||
# copying, ask the recovery passphrase, and only then install the
|
||||
# keyfile at the canonical path. Cancel at any dialog before the
|
||||
# install step leaves the disk untouched — no "phantom" keyfile.
|
||||
_hb_pbs_import_dialog() {
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
local src
|
||||
|
||||
# 1. Ask the path.
|
||||
src=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Import PBS keyfile")" \
|
||||
--inputbox "$(hb_translate "Absolute path to your existing PBS keyfile:")"$'\n\n'"$(hb_translate "The file is validated with 'proxmox-backup-client key info' and copied to")"$'\n'"$key_file $(hb_translate "with chmod 600.")" \
|
||||
14 78 "" 3>&1 1>&2 2>&3) || return 1
|
||||
src="$(echo "$src" | xargs)"
|
||||
[[ -z "$src" ]] && return 1
|
||||
|
||||
hb_pbs_import_keyfile "$src"
|
||||
local rc=$?
|
||||
case $rc in
|
||||
0)
|
||||
msg_ok "$(hb_translate "Imported existing encryption key:") $key_file"
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
# Recovery blob is still offered. When the same key lives
|
||||
# on multiple hosts, a single blob works for all — but
|
||||
# re-uploading from each host is idempotent and lets the
|
||||
# operator recover the key from PBS even if only ONE host
|
||||
# is left standing.
|
||||
if ! hb_pbs_setup_recovery; then
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
1)
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "The file does not exist, is empty or is not readable.")"$'\n\n'"$(hb_translate "Path:") $src" \
|
||||
10 78
|
||||
return 1
|
||||
;;
|
||||
2)
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "proxmox-backup-client did not recognise this file as a valid PBS keyfile.")"$'\n\n'"$(hb_translate "Path:") $src" \
|
||||
10 78
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "Could not copy the keyfile into place. Check permissions on:") $HB_STATE_DIR" \
|
||||
10 78
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
# 2. Validate the source without touching the canonical path yet.
|
||||
# Same shape as the check inside hb_pbs_import_keyfile, so a
|
||||
# later install call can't disagree with what we saw here.
|
||||
if [[ ! -s "$src" || ! -r "$src" ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "The file does not exist, is empty or is not readable.")"$'\n\n'"$(hb_translate "Path:") $src" \
|
||||
10 78
|
||||
return 1
|
||||
fi
|
||||
if ! proxmox-backup-client key info --output-format json "$src" >/dev/null 2>&1; then
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "proxmox-backup-client did not recognise this file as a valid PBS keyfile.")"$'\n\n'"$(hb_translate "Path:") $src" \
|
||||
10 78
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 3. Collect the recovery passphrase. Cancel here → zero side effect
|
||||
# (the source file is still where it was; we haven't copied yet).
|
||||
local pass
|
||||
pass=$(_hb_pbs_prompt_recovery_pass) || return 1
|
||||
|
||||
# 4. Now install the keyfile. Point of no return.
|
||||
if ! hb_pbs_import_keyfile "$src"; then
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "Could not copy the keyfile into place. Check permissions on:") $HB_STATE_DIR" \
|
||||
10 78
|
||||
return 1
|
||||
fi
|
||||
msg_ok "$(hb_translate "Imported existing encryption key:") $key_file"
|
||||
|
||||
# 5. Encrypt the recovery blob with the passphrase collected in step 3.
|
||||
# openssl failure is the only path that has to undo the install.
|
||||
if ! _hb_pbs_finalize_recovery "$pass"; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
hb_ask_pbs_encryption() {
|
||||
# Two-step flow:
|
||||
# 1. Yes/No: "Do you want to encrypt this backup?"
|
||||
# No → plain backup, no keyfile touched.
|
||||
# Yes → step 2.
|
||||
# 2a. Keyfile already exists on this host → use it silently.
|
||||
# 2b. No keyfile yet → menu with 2 options:
|
||||
# new → generate + confirm passphrase (legacy flow).
|
||||
# import → prompt path, validate, copy into place.
|
||||
# cancel → ABORT the whole backup (operator asked for
|
||||
# encryption but didn't pick a method; we don't
|
||||
# silently downgrade to plain).
|
||||
# A single-run helper leftover from a cancelled/failed create can
|
||||
# leave a zero-byte keyfile behind; we wipe it before deciding so
|
||||
# the "keyfile exists" branch never trips on an empty file.
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
export HB_PBS_KEYFILE_OPT=""
|
||||
export HB_PBS_ENC_PASS=""
|
||||
|
||||
# Wipe any scrollback that might leak above our dialogs — most
|
||||
# often the terminal title or a stray line from a prior manual
|
||||
# `proxmox-backup-client` invocation in the same SSH session.
|
||||
clear
|
||||
printf '\033]0;ProxMenux\007'
|
||||
|
||||
# Wipe any zero-byte keyfile left behind by a previous cancelled
|
||||
# or failed key-create run — otherwise the "existing" branch would
|
||||
# hand `--keyfile <empty>` to proxmox-backup-client and die mid
|
||||
# backup with an opaque "unable to load encryption key" error.
|
||||
if [[ -f "$key_file" && ! -s "$key_file" ]]; then
|
||||
rm -f "$key_file" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Build the menu. When a keyfile is already present the operator
|
||||
# can Use it as-is; otherwise the choice is Generate / Import /
|
||||
# Skip. Same shape both times so muscle memory works.
|
||||
local -a menu_items=()
|
||||
if [[ -s "$key_file" ]]; then
|
||||
menu_items+=("existing" "$(hb_translate "Use existing keyfile at") $key_file")
|
||||
# ── Step 1: yes/no ─────────────────────────────────────────
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption")" \
|
||||
--yesno "$(hb_translate "Do you want to encrypt this backup?")" 8 60; then
|
||||
# No → continue without encryption. `HB_PBS_KEYFILE_OPT` stays
|
||||
# empty; the runner will call proxmox-backup-client without
|
||||
# --keyfile.
|
||||
return 0
|
||||
fi
|
||||
menu_items+=(
|
||||
"new" "$(hb_translate "Generate a new keyfile (one per host — safest isolation)")"
|
||||
"import" "$(hb_translate "Import an existing keyfile from a path (shared across hosts)")"
|
||||
"none" "$(hb_translate "Skip — no encryption for this backup")"
|
||||
)
|
||||
local menu_h=14
|
||||
(( ${#menu_items[@]} > 8 )) && menu_h=16
|
||||
|
||||
# ── Step 2a: keyfile already present → reuse silently ─────
|
||||
if [[ -s "$key_file" ]]; then
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
msg_ok "$(hb_translate "Using existing encryption key:") $key_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Step 2b: no keyfile → let the operator generate or import ─
|
||||
local choice
|
||||
choice=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption")" \
|
||||
--menu "$(hb_translate "Choose how to protect this backup:")" \
|
||||
"$menu_h" 78 4 "${menu_items[@]}" \
|
||||
3>&1 1>&2 2>&3) || return 0
|
||||
choice=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption key")" \
|
||||
--menu "$(hb_translate "No encryption key is stored on this host. Choose how to set one up:")" \
|
||||
12 78 2 \
|
||||
"new" "$(hb_translate "Generate a new keyfile (one per host — safest isolation)")" \
|
||||
"import" "$(hb_translate "Import an existing keyfile from a path (shared across hosts)")" \
|
||||
3>&1 1>&2 2>&3) || return 1 # Cancel → abort backup.
|
||||
|
||||
case "$choice" in
|
||||
existing)
|
||||
# An existing keyfile is trusted — it only lands here after
|
||||
# a previous encrypted backup completed successfully (the
|
||||
# create / import paths wipe the file on cancel).
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
msg_ok "$(hb_translate "Using existing encryption key:") $key_file"
|
||||
return 0
|
||||
;;
|
||||
new)
|
||||
# Operator asked to Replace or first-time Generate — wipe
|
||||
# any prior keyfile + recovery blob so the create path
|
||||
# starts from a clean slate.
|
||||
if [[ -s "$key_file" ]]; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
fi
|
||||
_hb_pbs_create_new_keyfile
|
||||
return $?
|
||||
;;
|
||||
import)
|
||||
if [[ -s "$key_file" ]]; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
fi
|
||||
_hb_pbs_import_dialog
|
||||
return $?
|
||||
;;
|
||||
*)
|
||||
# "none" or unknown → no encryption for this backup.
|
||||
return 0
|
||||
# Empty selection from dialog — treat as cancel.
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -2446,6 +2475,15 @@ hb_prompt_dest_dir() {
|
||||
}
|
||||
|
||||
hb_prompt_restore_source_dir() {
|
||||
# Same fd-9 trick used by hb_prompt_mounted_path: this function
|
||||
# is called as `dir=$(hb_prompt_restore_source_dir)` which
|
||||
# captures stdout, so any --msgbox we open inside would render
|
||||
# into the subshell (invisible to the operator) and the caller
|
||||
# would think the flow "hung". Stash real stdout in fd 9,
|
||||
# redirect stdout to the tty for every dialog, and emit the
|
||||
# actual return value through fd 9 at the end.
|
||||
exec 9>&1 >/dev/tty
|
||||
|
||||
local choice out
|
||||
|
||||
choice=$(dialog --backtitle "ProxMenux" \
|
||||
@@ -2470,11 +2508,16 @@ hb_prompt_restore_source_dir() {
|
||||
esac
|
||||
|
||||
out=$(hb_trim_dialog_value "$out")
|
||||
[[ -n "$out" && -d "$out" ]] || {
|
||||
msg_error "$(hb_translate "Directory does not exist.")"
|
||||
if [[ -z "$out" || ! -d "$out" ]]; then
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(hb_translate "Directory not found")" \
|
||||
--msgbox "$(hb_translate "The selected path does not exist on this host:")"$'\n\n'"${out:-<empty>}" \
|
||||
10 78 || true
|
||||
return 1
|
||||
}
|
||||
echo "$out"
|
||||
fi
|
||||
# Emit the real return value through the saved fd 9 — the caller's
|
||||
# `$(...)` capture reads from fd 9 (see the exec at the top).
|
||||
echo "$out" >&9
|
||||
}
|
||||
|
||||
# Return the set of scheduler job_ids that currently have a .env on
|
||||
@@ -2524,6 +2567,13 @@ hb_is_host_backup_archive() {
|
||||
}
|
||||
|
||||
hb_prompt_local_archive() {
|
||||
# See hb_prompt_restore_source_dir above for why we do the fd-9
|
||||
# trick — same story: caller uses `archive=$(hb_prompt_local_archive)`
|
||||
# so any --msgbox inside would render into the subshell and
|
||||
# never reach the operator. Force dialog output to the tty and
|
||||
# emit the selected archive path via fd 9 at the end.
|
||||
exec 9>&1 >/dev/tty
|
||||
|
||||
local base_dir="$1"
|
||||
local title="${2:-$(hb_translate "Select backup archive")}"
|
||||
local -a rows=() files=() menu=()
|
||||
@@ -2592,7 +2642,9 @@ hb_prompt_local_archive() {
|
||||
--menu "$menu_prompt" \
|
||||
"$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" "${menu[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
echo "${files[$((choice-1))]}"
|
||||
# Emit the selected archive path through fd 9 so the caller's
|
||||
# `$(...)` capture actually receives it (see the exec at the top).
|
||||
echo "${files[$((choice-1))]}" >&9
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
|
||||
@@ -27,6 +27,9 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
LIB_FILE="$SCRIPT_DIR/lib_host_backup_common.sh"
|
||||
[[ ! -f "$LIB_FILE" ]] && LIB_FILE="$LOCAL_SCRIPTS_DEFAULT/backup_restore/lib_host_backup_common.sh"
|
||||
if [[ -f "$LIB_FILE" ]]; then
|
||||
@@ -516,15 +519,15 @@ main() {
|
||||
archive_path=$(grep "^LOCAL_ARCHIVE=" <<<"$_output" | cut -d'=' -f2-)
|
||||
;;
|
||||
borg)
|
||||
(( TTY )) && { echo; msg_info "$(translate "Sending snapshot to Borg repository...")"; stop_spinner; }
|
||||
echo "Sending snapshot to Borg repository ${BORG_REPO:-} ..." >>"$log_file"
|
||||
(( TTY )) && { echo; msg_info "$(translate "Sending backup to Borg repository...")"; stop_spinner; }
|
||||
echo "Sending backup to Borg repository ${BORG_REPO:-} ..." >>"$log_file"
|
||||
_sb_run_borg "$stage_root" "${job_id}-${ts}" >>"$log_file" 2>&1
|
||||
rc=$?
|
||||
archive_path="${BORG_REPO:-}::${job_id}-${ts}"
|
||||
;;
|
||||
pbs)
|
||||
(( TTY )) && { echo; msg_info "$(translate "Sending snapshot to PBS...")"; stop_spinner; }
|
||||
echo "Sending snapshot to PBS ${PBS_REPOSITORY:-} (id=${PBS_BACKUP_ID:-hostcfg-$(hostname)}) ..." >>"$log_file"
|
||||
(( TTY )) && { echo; msg_info "$(translate "Sending backup to PBS...")"; stop_spinner; }
|
||||
echo "Sending backup to PBS ${PBS_REPOSITORY:-} (id=${PBS_BACKUP_ID:-hostcfg-$(hostname)}) ..." >>"$log_file"
|
||||
_sb_run_pbs "$stage_root" "${PBS_BACKUP_ID:-hostcfg-$(hostname)}" "$(date +%s)" >>"$log_file" 2>&1
|
||||
rc=$?
|
||||
archive_path="${PBS_REPOSITORY:-}::host/${PBS_BACKUP_ID:-hostcfg-$(hostname)}"
|
||||
|
||||
Reference in New Issue
Block a user