From 4c65d5a07a0624d198a7af6b88d1f1ee11f64218 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sat, 13 Jun 2026 17:34:11 +0200 Subject: [PATCH] update 1.2.2.2 beta --- scripts/backup_restore/backup_host.sh | 16 + scripts/backup_restore/backup_scheduler.sh | 284 +++++++++++++----- .../backup_restore/lib_host_backup_common.sh | 137 +++++++++ .../backup_restore/run_scheduled_backup.sh | 95 +++++- scripts/backup_restore/vzdump-hook.sh | 54 ++++ 5 files changed, 501 insertions(+), 85 deletions(-) create mode 100755 scripts/backup_restore/vzdump-hook.sh diff --git a/scripts/backup_restore/backup_host.sh b/scripts/backup_restore/backup_host.sh index 090a91d0..81685ef5 100644 --- a/scripts/backup_restore/backup_host.sh +++ b/scripts/backup_restore/backup_host.sh @@ -549,12 +549,28 @@ _bk_manage_extra_paths() { --title "$(translate "Manage custom backup paths")" \ --menu "\n${preview}\n" \ "$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \ + "view" "$(translate "View current paths")" \ "add" "$(translate "+ Add a path")" \ "del" "$(translate "− Remove a path")" \ "back" "$(translate "← Return")" \ 3>&1 1>&2 2>&3) || break case "$choice" in + view) + if (( count == 0 )); then + dialog --backtitle "ProxMenux" --msgbox \ + "$(translate "You haven't added any custom paths yet.")" 8 60 + continue + fi + local list_body="" pv + for pv in "${paths[@]}"; do + list_body+="• ${pv}"$'\n' + done + dialog --backtitle "ProxMenux" \ + --title "$(translate "Custom backup paths") (${count})" \ + --msgbox "\n${list_body}" \ + "$HB_UI_MENU_H" "$HB_UI_MENU_W" + ;; add) local new_path new_path=$(dialog --backtitle "ProxMenux" \ diff --git a/scripts/backup_restore/backup_scheduler.sh b/scripts/backup_restore/backup_scheduler.sh index 333bacf3..f19b9cf2 100644 --- a/scripts/backup_restore/backup_scheduler.sh +++ b/scripts/backup_restore/backup_scheduler.sh @@ -75,13 +75,47 @@ _list_jobs() { 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). +_job_is_attached() { + local id="$1" f + f=$(_job_file "$id") + [[ -f "$f" ]] || return 1 + grep -q "^PVE_STORAGE=" "$f" +} + +# Reads a key=val pair from the job .env file (handles `printf %q` +# quoting that _write_job_env produces). +_job_env_get() { + local id="$1" key="$2" f raw + f=$(_job_file "$id") + [[ -f "$f" ]] || return 1 + raw=$(grep -E "^${key}=" "$f" | head -1 | cut -d'=' -f2-) + eval "echo $raw" 2>/dev/null || echo "$raw" +} + _show_job_status() { local id="$1" - local timer_state="disabled" - local service_state="unknown" + if _job_is_attached "$id"; then + local storage + storage=$(_job_env_get "$id" "PVE_STORAGE") + local enabled + enabled=$(_job_env_get "$id" "ENABLED") + [[ "$enabled" == "0" ]] && { echo "attached(disabled) → storage:$storage"; return; } + echo "attached → storage:$storage" + return + fi + local timer_state="disabled" service_state systemctl is-enabled --quiet "proxmenux-backup-${id}.timer" >/dev/null 2>&1 && timer_state="enabled" service_state=$(systemctl is-active "proxmenux-backup-${id}.service" 2>/dev/null || echo "inactive") - echo "${timer_state}/${service_state}" + if [[ "$service_state" == "active" ]]; then + echo "running" + elif [[ "$timer_state" == "enabled" ]]; then + echo "enabled" + else + echo "disabled" + fi } _write_job_units() { @@ -155,6 +189,118 @@ _prompt_retention() { ) } +# Builds a "host backup attached to a PVE vzdump job" — no systemd +# timer is created; the trigger is the vzdump hook that fires when +# the parent job runs. Schedule and retention come from the parent. +_create_job_attached() { + local id="$1" + local backend="$2" + + local -a jobs=() + mapfile -t jobs < <(hb_pve_list_vzdump_jobs_for_backend "$backend") + if (( ${#jobs[@]} == 0 )); then + dialog --backtitle "ProxMenux" --title "$(translate "No compatible PVE jobs")" \ + --msgbox "$(translate "No PVE vzdump job uses a") $backend $(translate "storage.")" 8 70 + return 1 + fi + + local -a menu=() + local i=1 row pve_id pve_storage _ pve_schedule _pve_prune pve_enabled + for row in "${jobs[@]}"; do + IFS=$'\t' read -r pve_id pve_storage _ pve_schedule _pve_prune pve_enabled <<<"$row" + local label="${pve_id} · ${pve_storage} · ${pve_schedule}" + [[ "$pve_enabled" == "0" ]] && label+=" $(translate "(disabled)")" + menu+=("$i" "$label") + ((i++)) + done + local sel + sel=$(dialog --backtitle "ProxMenux" --title "$(translate "Pick PVE vzdump job")" \ + --menu "\n$(translate "Select the parent job to attach to:")" \ + "$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" "${menu[@]}" \ + 3>&1 1>&2 2>&3) || return 1 + + local pve_prune + IFS=$'\t' read -r pve_id pve_storage _ pve_schedule pve_prune pve_enabled <<<"${jobs[$((sel-1))]}" + + local profile_mode + profile_mode=$(dialog --backtitle "ProxMenux" --title "$(translate "Profile")" \ + --menu "\n$(translate "Select backup profile:")" 12 68 4 \ + "default" "Default critical paths" \ + "custom" "Custom selected paths" \ + 3>&1 1>&2 2>&3) || return 1 + + local -a paths=() + hb_select_profile_paths "$profile_mode" paths || return 1 + + local -a lines=( + "JOB_ID=$id" + "BACKEND=$backend" + "PVE_PARENT_JOB=$pve_id" + "PVE_STORAGE=$pve_storage" + "PROFILE_MODE=$profile_mode" + "ENABLED=1" + ) + + # Inherit retention from the parent job (one KEEP_* per prune-backups key). + local kv + while IFS= read -r kv; do + [[ -n "$kv" ]] && lines+=("$kv") + done < <(hb_pve_prune_to_keep_env "$pve_prune") + + case "$backend" in + pbs) + hb_select_pbs_repository || return 1 + local bid + bid="hostcfg-$(hostname)" + bid=$(dialog --backtitle "ProxMenux" --title "PBS" \ + --inputbox "$(translate "Backup ID for this job:")" \ + "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "$bid" 3>&1 1>&2 2>&3) || return 1 + bid=$(echo "$bid" | tr -cs '[:alnum:]_-' '-' | sed 's/-*$//') + lines+=( + "PBS_REPOSITORY=${HB_PBS_REPOSITORY}" + "PBS_PASSWORD=${HB_PBS_SECRET}" + "PBS_BACKUP_ID=${bid}" + ) + ;; + local) + # Derive the dump directory from the storage entry. PVE stores + # vzdump archives under /dump/ when the storage is dir/nfs. + local dest_dir="/var/lib/vz/dump" + local sp + sp=$(awk -v sid="$pve_storage" ' + /^[a-z]+:[[:space:]]/ { in_block=($2==sid) } + in_block && /^[[:space:]]+path[[:space:]]/ { sub(/^[[:space:]]+path[[:space:]]+/,""); print; exit } + ' /etc/pve/storage.cfg) || true + [[ -n "$sp" ]] && dest_dir="${sp%/}/dump" + lines+=("LOCAL_DEST_DIR=$dest_dir" "LOCAL_ARCHIVE_EXT=tar.zst") + ;; + esac + + _write_job_env "$(_job_file "$id")" "${lines[@]}" + : > "$(_job_paths_file "$id")" + local p + for p in "${paths[@]}"; do + echo "$p" >> "$(_job_paths_file "$id")" + done + + # No unit / timer — the trigger is the vzdump hook fired by the parent PVE job. + hb_install_vzdump_hook >/dev/null 2>&1 || \ + msg_warn "$(translate "Could not install vzdump hook in /etc/vzdump.conf")" + + show_proxmenux_logo + msg_title "$(translate "Host backup attached to PVE job")" + echo + echo -e "${TAB}${BGN}$(translate "Job ID:")${CL} ${BL}${id}${CL}" + echo -e "${TAB}${BGN}$(translate "Attached to PVE job:")${CL} ${BL}${pve_id}${CL}" + echo -e "${TAB}${BGN}$(translate "Inherited schedule:")${CL} ${BL}${pve_schedule}${CL}" + echo -e "${TAB}${BGN}$(translate "Inherited retention:")${CL} ${BL}${pve_prune}${CL}" + echo -e "${TAB}${BGN}$(translate "Backend:")${CL} ${BL}${backend} → ${pve_storage}${CL}" + echo + msg_success "$(translate "Press Enter to continue...")" + read -r + return 0 +} + _create_job() { local id backend on_calendar profile_mode id=$(dialog --backtitle "ProxMenux" --title "$(translate "New backup job")" \ @@ -175,6 +321,31 @@ _create_job() { "pbs" "Proxmox Backup Server" \ 3>&1 1>&2 2>&3) || return 1 + # Offer attach-mode for backends that map to a PVE storage. The + # vzdump scheduler in PVE already handles trigger + retention for + # VM/CT backups; the host backup can ride alongside it via a hook. + # Borg has no PVE-side scheduler, so attach makes no sense there. + if [[ "$backend" == "pbs" || "$backend" == "local" ]]; then + local creation_mode + creation_mode=$(dialog --backtitle "ProxMenux" --title "$(translate "How to schedule")" \ + --menu "\n$(translate "Choose how this host backup will be triggered:")" 14 78 4 \ + "new" "$(translate "New scheduled job (own timer + retention)")" \ + "attach" "$(translate "Attach to an existing PVE vzdump job (inherit schedule + retention)")" \ + 3>&1 1>&2 2>&3) || return 1 + if [[ "$creation_mode" == "attach" ]]; then + # If no compatible PVE job exists yet, show a helpful pointer + # instead of silently dropping back to "new" mode. + if [[ -z "$(hb_pve_list_vzdump_jobs_for_backend "$backend" 2>/dev/null | head -1)" ]]; then + dialog --backtitle "ProxMenux" --title "$(translate "No compatible PVE jobs")" \ + --msgbox "$(translate "No PVE vzdump job uses a") $backend $(translate "storage yet.")"$'\n\n'"$(translate "Create one first in Datacenter → Backup, then return here to attach.")" \ + 12 78 + return 1 + fi + _create_job_attached "$id" "$backend" + return $? + fi + fi + on_calendar=$(dialog --backtitle "ProxMenux" --title "$(translate "Schedule")" \ --inputbox "$(translate "systemd OnCalendar expression")"$'\n'"$(translate "Example: daily or Mon..Fri 03:00")" \ 11 72 "daily" 3>&1 1>&2 2>&3) || return 1 @@ -337,71 +508,19 @@ _job_run_now() { local runner="$LOCAL_SCRIPTS/backup_restore/run_scheduled_backup.sh" [[ ! -f "$runner" ]] && runner="$SCRIPT_DIR/run_scheduled_backup.sh" - # ── Visible execution ─────────────────────────────────── - # Clear the leftover dialog frame and announce what's about - # to happen, so the operator stops looking at a frozen - # picker. We then tail the runner's log file in the - # background so progress (or errors) are visible as they - # happen, instead of the user staring at a black screen. - # No msg_info banner between the title and the streaming - # log — the title already says we're running, the streamed - # `=== Scheduled backup job X started ===` is the better - # progress cue. + # Foreground execution — the runner detects TTY and prints a + # colored progress layout (mirrors _bk_local in backup_host.sh). + # Plain-text log file is still written for audit / scheduler runs. _render_action_screen "$(translate "Running backup job:") $id" echo - - # Snapshot existing log files so we can identify the new one the - # runner is about to create (filename pattern is `${id}-${ts}.log`). - local existing_logs new_log="" - existing_logs="$(ls -1 "${LOG_DIR}/${id}-"*.log 2>/dev/null || true)" - - # Launch the runner in the background so we can tail its log - # while it's still writing. - "$runner" "$id" & - local runner_pid=$! - - # Wait up to ~10s for the new log file to appear, then start tail. - # On a small config-only backup the job may finish before we even - # find the log; that's fine, we just skip tailing. - local tail_pid="" - local _i - for _i in $(seq 1 20); do - local f - for f in "${LOG_DIR}/${id}-"*.log; do - [[ -f "$f" ]] || continue - if ! grep -qFx "$f" <<<"$existing_logs" 2>/dev/null; then - new_log="$f" - break 2 - fi - done - # Stop probing if the runner already exited. - kill -0 "$runner_pid" 2>/dev/null || break - sleep 0.5 - done - - if [[ -n "$new_log" ]]; then - tail -f "$new_log" & - tail_pid=$! - fi - - wait "$runner_pid" - local runner_exit=$? - - if [[ -n "$tail_pid" ]]; then - # Give tail a beat to flush the last buffered lines, then close it. - sleep 0.5 - kill "$tail_pid" 2>/dev/null || true - wait "$tail_pid" 2>/dev/null || true - fi + "$runner" "$id" + local runner_exit + runner_exit=$? echo - if [[ "$runner_exit" == "0" ]]; then - msg_ok "$(translate "Job executed successfully.")" - else - msg_warn "$(translate "Job execution finished with errors. Check logs.")" - fi msg_success "$(translate "Press Enter to continue...")" read -r + return $runner_exit } _job_toggle() { @@ -415,22 +534,35 @@ _job_toggle() { return 1 fi - # Decide the action label up front so the title reflects what we - # actually just did (enable vs disable). local action_label - if systemctl is-enabled --quiet "proxmenux-backup-${id}.timer" >/dev/null 2>&1; then - systemctl disable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true - action_label="disabled" + if _job_is_attached "$id"; then + # Attached jobs have no systemd timer — flip the ENABLED flag in + # the .env so the vzdump hook respects it on the next parent run. + local f current + f=$(_job_file "$id") + current=$(_job_env_get "$id" "ENABLED") + if [[ "$current" == "0" ]]; then + sed -i 's/^ENABLED=.*/ENABLED=1/' "$f" + action_label="enabled" + else + sed -i 's/^ENABLED=.*/ENABLED=0/' "$f" + action_label="disabled" + fi else - systemctl enable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true - action_label="enabled" + if systemctl is-enabled --quiet "proxmenux-backup-${id}.timer" >/dev/null 2>&1; then + systemctl disable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true + action_label="disabled" + else + systemctl enable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true + action_label="enabled" + fi fi _render_action_screen "$(translate "Enable/Disable job")" if [[ "$action_label" == "disabled" ]]; then - msg_warn "$(translate "Job timer disabled:") $id" + msg_warn "$(translate "Job disabled:") $id" else - msg_ok "$(translate "Job timer enabled:") $id" + msg_ok "$(translate "Job enabled:") $id" fi msg_success "$(translate "Press Enter to continue...")" read -r @@ -450,8 +582,16 @@ _job_delete() { read -r return 1 fi + local confirm_body + confirm_body="$(translate "Delete scheduled backup job?")"$'\n\n'"ID: ${id}" + if _job_is_attached "$id"; then + local storage + storage=$(_job_env_get "$id" "PVE_STORAGE") + confirm_body+=$'\n'"$(translate "Type: attached to PVE storage") ${storage}" + confirm_body+=$'\n\n'"$(translate "Only the host backup hook is removed — PVE vzdump jobs targeting this storage stay intact.")" + fi if ! whiptail --title "$(translate "Confirm delete")" \ - --yesno "$(translate "Delete scheduled backup job?")"$'\n\n'"ID: ${id}" 10 66; then + --yesno "$confirm_body" 14 70; then return 1 fi systemctl disable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true diff --git a/scripts/backup_restore/lib_host_backup_common.sh b/scripts/backup_restore/lib_host_backup_common.sh index d8400a11..d5b9010e 100644 --- a/scripts/backup_restore/lib_host_backup_common.sh +++ b/scripts/backup_restore/lib_host_backup_common.sh @@ -2661,6 +2661,143 @@ hb_show_compat_report() { # Fail-soft: returns 0 even if jq is missing and we have to fall # back to printf-built JSON; never aborts the surrounding backup. # ========================================================== +# ========================================================== +# PVE vzdump jobs — parsing and attach helpers +# +# When the operator already has a vzdump job scheduling backups of +# their VMs/CTs to PBS or a local datastore, they can "attach" a +# host-config backup to that same job. The host inherits the job's +# schedule, target storage, and retention; the trigger is a vzdump +# hook script that fires on `job-end`. +# ========================================================== + +# Returns the type of a PVE storage as declared in /etc/pve/storage.cfg. +# Empty if the storage doesn't exist. Common values: pbs, dir, nfs, +# zfspool, lvmthin, cifs, btrfs. +hb_pve_storage_type() { + local storage_id="$1" + [[ -z "$storage_id" || ! -f /etc/pve/storage.cfg ]] && return 1 + awk -v sid="$storage_id" ' + /^[a-z]+:[[:space:]]/ { + t=$1; sub(":","",t) + if ($2 == sid) { print t; exit } + } + ' /etc/pve/storage.cfg +} + +# Lists every vzdump job in /etc/pve/jobs.cfg, one TSV row per job. +# Columns: id storage storage_type schedule +# prune-backups enabled +# Empty cells are kept as "-" so the row keeps 6 columns even when +# the job omits the field. +hb_pve_list_vzdump_jobs() { + [[ -f /etc/pve/jobs.cfg ]] || return 0 + awk ' + function flush() { + if (id != "") { + printf "%s\t%s\t%s\t%s\t%s\t%s\n", \ + id, \ + (storage=="" ? "-" : storage), \ + "PLACEHOLDER_TYPE", \ + (schedule=="" ? "-" : schedule), \ + (prune=="" ? "-" : prune), \ + (enabled=="" ? "1" : enabled) + } + id=""; storage=""; schedule=""; prune=""; enabled="" + } + /^vzdump:/ { flush(); id=$2; next } + /^[a-z]+:/ { flush(); next } # next block of a different type + /^[[:space:]]+schedule[[:space:]]/ { sub(/^[[:space:]]+schedule[[:space:]]+/, ""); schedule=$0; next } + /^[[:space:]]+storage[[:space:]]/ { sub(/^[[:space:]]+storage[[:space:]]+/, ""); storage=$0; next } + /^[[:space:]]+prune-backups[[:space:]]/ { sub(/^[[:space:]]+prune-backups[[:space:]]+/, ""); prune=$0; next } + /^[[:space:]]+enabled[[:space:]]/ { sub(/^[[:space:]]+enabled[[:space:]]+/, ""); enabled=$0; next } + END { flush() } + ' /etc/pve/jobs.cfg | while IFS=$'\t' read -r id storage type schedule prune enabled; do + type=$(hb_pve_storage_type "$storage") + [[ -z "$type" ]] && type="-" + printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$id" "$storage" "$type" "$schedule" "$prune" "$enabled" + done +} + +# Filters hb_pve_list_vzdump_jobs by a backend family. +# Args: $1 = "pbs" or "local" +# pbs → only storage_type == "pbs" +# local → only file/block storage we can write to as a local archive +hb_pve_list_vzdump_jobs_for_backend() { + local backend="$1" + hb_pve_list_vzdump_jobs | while IFS=$'\t' read -r id storage type schedule prune enabled; do + case "$backend" in + pbs) + [[ "$type" == "pbs" ]] && printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$id" "$storage" "$type" "$schedule" "$prune" "$enabled" + ;; + local) + case "$type" in + dir|nfs|cifs|zfspool|lvmthin|btrfs) + printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$id" "$storage" "$type" "$schedule" "$prune" "$enabled" + ;; + esac + ;; + esac + done +} + +# Parses a `prune-backups` value (e.g. "keep-last=7,keep-daily=14") +# and emits KEY=VAL pairs for sourcing into a scheduler job env file. +# Maps proxmox' keep-last/hourly/daily/weekly/monthly/yearly to our +# KEEP_LAST/HOURLY/DAILY/WEEKLY/MONTHLY/YEARLY. +hb_pve_prune_to_keep_env() { + local prune="$1" + [[ -z "$prune" || "$prune" == "-" ]] && return 0 + local kv k v upper + while IFS=',' read -ra kv; do + for k in "${kv[@]}"; do + v="${k#*=}" + k="${k%=*}" + case "$k" in + keep-last) echo "KEEP_LAST=$v" ;; + keep-hourly) echo "KEEP_HOURLY=$v" ;; + keep-daily) echo "KEEP_DAILY=$v" ;; + keep-weekly) echo "KEEP_WEEKLY=$v" ;; + keep-monthly) echo "KEEP_MONTHLY=$v" ;; + keep-yearly) echo "KEEP_YEARLY=$v" ;; + esac + done + done <<<"$prune" +} + +hb_install_vzdump_hook() { + local src="/usr/local/share/proxmenux/scripts/backup_restore/vzdump-hook.sh" + local dst="/etc/proxmenux/vzdump-hook.sh" + local chain="/etc/proxmenux/vzdump-hook-chain.sh" + local conf="/etc/vzdump.conf" + + [[ -f "$src" ]] || return 1 + mkdir -p /etc/proxmenux + install -m 0755 "$src" "$dst" || return 1 + + [[ -f "$conf" ]] || : >"$conf" + + local current + current=$(awk -F'[[:space:]]*:[[:space:]]*' \ + '/^[[:space:]]*script[[:space:]]*:/ { sub(/[#].*/,"",$2); gsub(/[[:space:]]/,"",$2); print $2; exit }' \ + "$conf") + + if [[ -z "$current" ]]; then + printf 'script: %s\n' "$dst" >>"$conf" + return 0 + fi + + [[ "$current" == "$dst" ]] && return 0 + + # Existing third-party hook — preserve it as a chain target. + if [[ -x "$current" && ! -e "$chain" ]]; then + cp -p "$current" "$chain" + fi + sed -i "s|^[[:space:]]*script[[:space:]]*:.*|script: ${dst}|" "$conf" +} + hb_write_archive_sidecar() { local archive_path="$1" local kind="${2:-}" diff --git a/scripts/backup_restore/run_scheduled_backup.sh b/scripts/backup_restore/run_scheduled_backup.sh index e63f7b9e..f36e58a0 100644 --- a/scripts/backup_restore/run_scheduled_backup.sh +++ b/scripts/backup_restore/run_scheduled_backup.sh @@ -151,18 +151,22 @@ _sb_run_pbs() { cmd+=(--keyfile "$PBS_KEYFILE") fi - env PBS_PASSWORD="$PBS_PASSWORD" PBS_ENCRYPTION_PASSWORD="${PBS_ENCRYPTION_PASSWORD:-}" \ - "${cmd[@]}" >/dev/null 2>&1 || return 1 + env PBS_PASSWORD="$PBS_PASSWORD" \ + PBS_ENCRYPTION_PASSWORD="${PBS_ENCRYPTION_PASSWORD:-}" \ + PBS_FINGERPRINT="${PBS_FINGERPRINT:-}" \ + "${cmd[@]}" 2>&1 || return 1 # Best effort prune for PBS group. - proxmox-backup-client prune "host/${backup_id}" --repository "$PBS_REPOSITORY" \ - ${KEEP_LAST:+--keep-last "$KEEP_LAST"} \ - ${KEEP_HOURLY:+--keep-hourly "$KEEP_HOURLY"} \ - ${KEEP_DAILY:+--keep-daily "$KEEP_DAILY"} \ - ${KEEP_WEEKLY:+--keep-weekly "$KEEP_WEEKLY"} \ - ${KEEP_MONTHLY:+--keep-monthly "$KEEP_MONTHLY"} \ - ${KEEP_YEARLY:+--keep-yearly "$KEEP_YEARLY"} \ - >/dev/null 2>&1 || true + env PBS_PASSWORD="$PBS_PASSWORD" \ + PBS_FINGERPRINT="${PBS_FINGERPRINT:-}" \ + proxmox-backup-client prune "host/${backup_id}" --repository "$PBS_REPOSITORY" \ + ${KEEP_LAST:+--keep-last "$KEEP_LAST"} \ + ${KEEP_HOURLY:+--keep-hourly "$KEEP_HOURLY"} \ + ${KEEP_DAILY:+--keep-daily "$KEEP_DAILY"} \ + ${KEEP_WEEKLY:+--keep-weekly "$KEEP_WEEKLY"} \ + ${KEEP_MONTHLY:+--keep-monthly "$KEEP_MONTHLY"} \ + ${KEEP_YEARLY:+--keep-yearly "$KEEP_YEARLY"} \ + 2>&1 || true echo "PBS_SNAPSHOT=host/${backup_id}/${epoch}" return 0 @@ -203,6 +207,7 @@ main() { { echo "=== Scheduled backup job ${job_id} started at $(date -Iseconds) ===" echo "Backend: ${BACKEND:-}" + echo "Profile: ${PROFILE_MODE:-default}" } >"$log_file" local -a paths=() @@ -219,21 +224,57 @@ main() { exit 1 fi - hb_prepare_staging "$stage_root" "${paths[@]}" >>"$log_file" 2>&1 + # Interactive output mirrors the colored layout of _bk_local in + # backup_host.sh when stdout is a TTY (operator launched "Run job + # now"). Otherwise — timer / vzdump hook — only the plain log + # file is written. + local TTY=0 + [[ -t 1 ]] && TTY=1 + + if (( TTY )); then + echo -e "${TAB}${BGN}$(translate "Backend:")${CL} ${BL}${BACKEND}${CL}" + echo -e "${TAB}${BGN}$(translate "Profile:")${CL} ${BL}${PROFILE_MODE:-default}${CL}" + echo -e "${TAB}${BGN}$(translate "Paths to back up:")${CL} ${BL}${#paths[@]}${CL}" + echo + msg_info "$(translate "Preparing staging area...")" + fi + { + echo "Paths to back up: ${#paths[@]}" + echo "Preparing staging area at $stage_root ..." + } >>"$log_file" + hb_prepare_staging "$stage_root" "${paths[@]}" >>"$log_file" 2>&1 + local staged_files staged_size + staged_files=$(find "$stage_root/rootfs" -type f 2>/dev/null | wc -l) + staged_size=$(hb_file_size "$stage_root/rootfs" 2>/dev/null || echo "?") + echo "Staging ready: $staged_files files copied (size $staged_size)." >>"$log_file" + (( TTY )) && msg_ok "$(translate "Staging ready.") $(translate "Data size:") $staged_size — $staged_files $(translate "files")" + + local rc=1 t_start elapsed archive_path="" + t_start=$SECONDS - local rc=1 case "${BACKEND:-}" in local) - _sb_run_local "$stage_root" "$job_id" "$ts" "${LOCAL_DEST_DIR:-/var/lib/vz/dump}" >>"$log_file" 2>&1 + (( TTY )) && { echo; msg_info "$(translate "Creating local archive...")"; stop_spinner; } + echo "Writing local archive to ${LOCAL_DEST_DIR:-/var/lib/vz/dump} ..." >>"$log_file" + local _output + _output=$(_sb_run_local "$stage_root" "$job_id" "$ts" "${LOCAL_DEST_DIR:-/var/lib/vz/dump}" 2>>"$log_file") rc=$? + echo "$_output" >>"$log_file" + 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" _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" _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)}" ;; *) echo "Unknown backend: ${BACKEND:-}" >>"$log_file" @@ -241,17 +282,45 @@ main() { ;; esac + elapsed=$((SECONDS - t_start)) + + echo "Cleaning up staging area ..." >>"$log_file" rm -rf "$stage_root" if [[ $rc -eq 0 ]]; then echo "RESULT=ok" >>"$summary_file" echo "LOG_FILE=${log_file}" >>"$summary_file" echo "=== Job finished OK at $(date -Iseconds) ===" >>"$log_file" + if (( TTY )); then + local archive_size="-" + case "${BACKEND:-}" in + local) [[ -f "$archive_path" ]] && archive_size=$(hb_file_size "$archive_path") ;; + esac + local method_label + case "${BACKEND:-}" in + local) method_label="Local archive (tar)" ;; + borg) method_label="Borg repository" ;; + pbs) method_label="Proxmox Backup Server" ;; + esac + echo + echo -e "${TAB}${BOLD}$(translate "Backup completed:")${CL}" + echo -e "${TAB}${BGN}$(translate "Method:")${CL} ${BL}${method_label}${CL}" + [[ -n "$archive_path" ]] && \ + echo -e "${TAB}${BGN}$(translate "Archive:")${CL} ${BL}${archive_path}${CL}" + echo -e "${TAB}${BGN}$(translate "Data size:")${CL} ${BL}${staged_size}${CL}" + [[ "$archive_size" != "-" ]] && \ + echo -e "${TAB}${BGN}$(translate "Archive size:")${CL} ${BL}${archive_size}${CL}" + echo -e "${TAB}${BGN}$(translate "Duration:")${CL} ${BL}$(hb_human_elapsed "$elapsed")${CL}" + echo -e "${TAB}${BGN}$(translate "Log:")${CL} ${BL}${log_file}${CL}" + echo + msg_ok "$(translate "Backup completed successfully.")" + fi exit 0 else echo "RESULT=failed" >>"$summary_file" echo "LOG_FILE=${log_file}" >>"$summary_file" echo "=== Job finished with errors at $(date -Iseconds) ===" >>"$log_file" + (( TTY )) && msg_error "$(translate "Backup failed. See log:") $log_file" exit 1 fi } diff --git a/scripts/backup_restore/vzdump-hook.sh b/scripts/backup_restore/vzdump-hook.sh new file mode 100755 index 00000000..b98b25b2 --- /dev/null +++ b/scripts/backup_restore/vzdump-hook.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# ProxMenux vzdump hook: bridges PVE vzdump jobs to attached host-config +# backups. Installed system-wide via /etc/vzdump.conf ("script:" line). +# PVE 9 invokes the hook for every phase and only exports a fixed set of env +# vars (STOREID, DUMPDIR, VMTYPE, HOSTNAME, TARGET, LOGFILE) — JOB_ID is NOT +# exported. We therefore match each proxmenux .env by its PVE_STORAGE field +# against STOREID and only act once per PVE job, in the job-end phase. + +set -u + +PHASE="${1:-}" +PROXMENUX_JOBS_DIR="${PROXMENUX_JOBS_DIR:-/var/lib/proxmenux/backup-jobs}" +PROXMENUX_LOG_DIR="/var/log/proxmenux" +PROXMENUX_RUNNER="/usr/local/share/proxmenux/scripts/backup_restore/run_scheduled_backup.sh" +CHAIN_HOOK="/etc/proxmenux/vzdump-hook-chain.sh" + +# Chain to any pre-existing hook that we displaced when we registered ours. +if [[ -x "$CHAIN_HOOK" ]]; then + "$CHAIN_HOOK" "$@" || true +fi + +[[ "$PHASE" != "job-end" ]] && exit 0 +[[ -z "${STOREID:-}" ]] && exit 0 + +mkdir -p "$PROXMENUX_LOG_DIR" +HOOK_LOG="$PROXMENUX_LOG_DIR/vzdump-hook.log" +echo "[$(date '+%F %T')] phase=$PHASE STOREID=$STOREID" >>"$HOOK_LOG" + +if [[ ! -x "$PROXMENUX_RUNNER" ]]; then + echo " runner missing: $PROXMENUX_RUNNER" >>"$HOOK_LOG" + exit 0 +fi + +shopt -s nullglob +for env_file in "$PROXMENUX_JOBS_DIR"/*.env; do + storage="" enabled="" pmx_id="" + while IFS='=' read -r k v; do + case "$k" in + PVE_STORAGE) storage="$v" ;; + ENABLED) enabled="$v" ;; + JOB_ID) pmx_id="$v" ;; + esac + done <"$env_file" + + [[ "$storage" != "$STOREID" ]] && continue + [[ "${enabled:-1}" != "1" ]] && { echo " skip $pmx_id (disabled)" >>"$HOOK_LOG"; continue; } + [[ -z "$pmx_id" ]] && continue + + echo " -> run $pmx_id" >>"$HOOK_LOG" + bash "$PROXMENUX_RUNNER" "$pmx_id" >>"$HOOK_LOG" 2>&1 || \ + echo " ! $pmx_id exited non-zero" >>"$HOOK_LOG" +done + +exit 0