2026-04-13 14:49:48 +02:00
#!/bin/bash
# ==========================================================
# ProxMenux - Scheduled Backup Jobs
# ==========================================================
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
LOCAL_SCRIPTS_LOCAL = " $( cd " $SCRIPT_DIR /.. " && pwd ) "
LOCAL_SCRIPTS_DEFAULT = "/usr/local/share/proxmenux/scripts"
LOCAL_SCRIPTS = " $LOCAL_SCRIPTS_DEFAULT "
BASE_DIR = "/usr/local/share/proxmenux"
UTILS_FILE = " $LOCAL_SCRIPTS /utils.sh "
if [ [ -f " $LOCAL_SCRIPTS_LOCAL /utils.sh " ] ] ; then
LOCAL_SCRIPTS = " $LOCAL_SCRIPTS_LOCAL "
UTILS_FILE = " $LOCAL_SCRIPTS /utils.sh "
elif [ [ ! -f " $UTILS_FILE " ] ] ; then
UTILS_FILE = " $BASE_DIR /utils.sh "
fi
if [ [ -f " $UTILS_FILE " ] ] ; then
# shellcheck source=/dev/null
source " $UTILS_FILE "
else
echo "ERROR: utils.sh not found." >& 2
exit 1
fi
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
# shellcheck source=/dev/null
source " $LIB_FILE "
else
msg_error " $( translate "Cannot load backup library: lib_host_backup_common.sh" ) "
exit 1
fi
load_language
initialize_cache
JOBS_DIR = "/var/lib/proxmenux/backup-jobs"
LOG_DIR = "/var/log/proxmenux/backup-jobs"
mkdir -p " $JOBS_DIR " " $LOG_DIR " >/dev/null 2>& 1 || true
_job_file( ) { echo " ${ JOBS_DIR } / $1 .env " ; }
_job_paths_file( ) { echo " ${ JOBS_DIR } / $1 .paths " ; }
_service_file( ) { echo " /etc/systemd/system/proxmenux-backup- $1 .service " ; }
_timer_file( ) { echo " /etc/systemd/system/proxmenux-backup- $1 .timer " ; }
_normalize_uint( ) {
local v = " ${ 1 :- 0 } "
[ [ " $v " = ~ ^[ 0-9] +$ ] ] || v = 0
echo " $v "
}
_write_job_env( ) {
local file = " $1 "
shift
{
echo "# ProxMenux scheduled backup job"
local kv key val
for kv in " $@ " ; do
key = " ${ kv %%=* } "
val = " ${ kv #*= } "
printf '%s=%q\n' " $key " " $val "
done
} > " $file "
}
_list_jobs( ) {
local f
for f in " $JOBS_DIR " /*.env; do
[ [ -f " $f " ] ] || continue
basename " $f " .env
done | sort
}
2026-06-13 17:34:11 +02:00
# 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 "
}
2026-04-13 14:49:48 +02:00
_show_job_status( ) {
local id = " $1 "
2026-06-13 17:34:11 +02:00
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
2026-04-13 14:49:48 +02:00
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" )
2026-06-13 17:34:11 +02:00
if [ [ " $service_state " = = "active" ] ] ; then
echo "running"
elif [ [ " $timer_state " = = "enabled" ] ] ; then
echo "enabled"
else
echo "disabled"
fi
2026-04-13 14:49:48 +02:00
}
_write_job_units( ) {
local id = " $1 "
local on_calendar = " $2 "
local runner = " $LOCAL_SCRIPTS /backup_restore/run_scheduled_backup.sh "
[ [ ! -f " $runner " ] ] && runner = " $SCRIPT_DIR /run_scheduled_backup.sh "
cat > " $( _service_file " $id " ) " <<EOF
[ Unit]
Description = ProxMenux Scheduled Backup Job ( ${ id } )
After = network-online.target
Wants = network-online.target
[ Service]
Type = oneshot
ExecStart = ${ runner } ${ id }
Nice = 10
IOSchedulingClass = best-effort
IOSchedulingPriority = 7
EOF
cat > " $( _timer_file " $id " ) " <<EOF
[ Unit]
Description = ProxMenux Scheduled Backup Timer ( ${ id } )
[ Timer]
OnCalendar = ${ on_calendar }
Persistent = true
RandomizedDelaySec = 120
Unit = proxmenux-backup-${ id } .service
[ Install]
WantedBy = timers.target
EOF
systemctl daemon-reload >/dev/null 2>& 1 || true
}
_prompt_retention( ) {
local __out_var = " $1 "
local last hourly daily weekly monthly yearly
last = $( dialog --backtitle "ProxMenux" --title " $( translate "Retention" ) " \
--inputbox " $( translate "keep-last (0 disables)" ) " 9 60 "7" 3>& 1 1>& 2 2>& 3) || return 1
hourly = $( dialog --backtitle "ProxMenux" --title " $( translate "Retention" ) " \
--inputbox " $( translate "keep-hourly (0 disables)" ) " 9 60 "0" 3>& 1 1>& 2 2>& 3) || return 1
daily = $( dialog --backtitle "ProxMenux" --title " $( translate "Retention" ) " \
--inputbox " $( translate "keep-daily (0 disables)" ) " 9 60 "7" 3>& 1 1>& 2 2>& 3) || return 1
weekly = $( dialog --backtitle "ProxMenux" --title " $( translate "Retention" ) " \
--inputbox " $( translate "keep-weekly (0 disables)" ) " 9 60 "4" 3>& 1 1>& 2 2>& 3) || return 1
monthly = $( dialog --backtitle "ProxMenux" --title " $( translate "Retention" ) " \
--inputbox " $( translate "keep-monthly (0 disables)" ) " 9 60 "3" 3>& 1 1>& 2 2>& 3) || return 1
yearly = $( dialog --backtitle "ProxMenux" --title " $( translate "Retention" ) " \
--inputbox " $( translate "keep-yearly (0 disables)" ) " 9 60 "0" 3>& 1 1>& 2 2>& 3) || return 1
last = $( _normalize_uint " $last " )
hourly = $( _normalize_uint " $hourly " )
daily = $( _normalize_uint " $daily " )
weekly = $( _normalize_uint " $weekly " )
monthly = $( _normalize_uint " $monthly " )
yearly = $( _normalize_uint " $yearly " )
local -n out = " $__out_var "
out = (
" KEEP_LAST= $last "
" KEEP_HOURLY= $hourly "
" KEEP_DAILY= $daily "
" KEEP_WEEKLY= $weekly "
" KEEP_MONTHLY= $monthly "
" KEEP_YEARLY= $yearly "
)
}
2026-06-13 17:34:11 +02:00
# 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 <path>/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
}
2026-04-13 14:49:48 +02:00
_create_job( ) {
local id backend on_calendar profile_mode
id = $( dialog --backtitle "ProxMenux" --title " $( translate "New backup job" ) " \
--inputbox " $( translate "Job ID (letters, numbers, - _)" ) " 9 68 "hostcfg-daily" 3>& 1 1>& 2 2>& 3) || return 1
[ [ -z " $id " ] ] && return 1
id = $( echo " $id " | tr -cs '[:alnum:]_-' '-' | sed 's/^-*//; s/-*$//' )
[ [ -z " $id " ] ] && return 1
[ [ -f " $( _job_file " $id " ) " ] ] && {
dialog --backtitle "ProxMenux" --title " $( translate "Error" ) " \
--msgbox " $( translate "A job with this ID already exists." ) " 8 62
return 1
}
backend = $( dialog --backtitle "ProxMenux" --title " $( translate "Backend" ) " \
--menu " \n $( translate "Select backup backend:" ) " 14 70 6 \
"local" "Local archive" \
"borg" "Borg repository" \
"pbs" "Proxmox Backup Server" \
3>& 1 1>& 2 2>& 3) || return 1
2026-06-13 17:34:11 +02:00
# 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
2026-04-13 14:49:48 +02:00
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
[ [ -z " $on_calendar " ] ] && return 1
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 retention = ( )
_prompt_retention retention || return 1
local -a lines = (
" JOB_ID= $id "
" BACKEND= $backend "
" ON_CALENDAR= $on_calendar "
" PROFILE_MODE= $profile_mode "
"ENABLED=1"
)
lines += ( " ${ retention [@] } " )
case " $backend " in
local )
local dest_dir ext
2026-06-13 18:52:28 +02:00
dest_dir = $( hb_select_local_target) || return 1
2026-04-13 14:49:48 +02:00
ext = $( dialog --backtitle "ProxMenux" --title " $( translate "Archive format" ) " \
--menu " \n $( translate "Select local archive format:" ) " 12 62 4 \
"tar.zst" "tar + zstd (preferred)" \
"tar.gz" "tar + gzip" \
3>& 1 1>& 2 2>& 3) || return 1
lines += ( " LOCAL_DEST_DIR= $dest_dir " " LOCAL_ARCHIVE_EXT= $ext " )
; ;
borg)
local repo passphrase
hb_select_borg_repo repo || return 1
hb_prepare_borg_passphrase || return 1
passphrase = " ${ BORG_PASSPHRASE :- } "
lines += (
" BORG_REPO= $repo "
" BORG_PASSPHRASE= $passphrase "
" BORG_ENCRYPT_MODE= ${ BORG_ENCRYPT_MODE :- none } "
)
; ;
pbs)
hb_select_pbs_repository || return 1
hb_ask_pbs_encryption
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 } "
" PBS_KEYFILE= ${ HB_PBS_KEYFILE :- } "
" PBS_ENCRYPTION_PASSWORD= ${ HB_PBS_ENC_PASS :- } "
)
; ;
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
_write_job_units " $id " " $on_calendar "
systemctl enable --now " proxmenux-backup- ${ id } .timer " >/dev/null 2>& 1 || true
show_proxmenux_logo
msg_title " $( translate "Scheduled backup job created" ) "
echo -e ""
echo -e " ${ TAB } ${ BGN } $( translate "Job ID:" ) ${ CL } ${ BL } ${ id } ${ CL } "
echo -e " ${ TAB } ${ BGN } $( translate "Backend:" ) ${ CL } ${ BL } ${ backend } ${ CL } "
echo -e " ${ TAB } ${ BGN } $( translate "Schedule:" ) ${ CL } ${ BL } ${ on_calendar } ${ CL } "
echo -e " ${ TAB } ${ BGN } $( translate "Status:" ) ${ CL } ${ BL } $( _show_job_status " $id " ) ${ CL } "
echo -e ""
msg_success " $( translate "Press Enter to continue..." ) "
read -r
return 0
}
_pick_job( ) {
local title = " $1 "
local __out_var = " $2 "
local -a ids = ( )
mapfile -t ids < <( _list_jobs)
if [ [ ${# ids [@] } -eq 0 ] ] ; then
dialog --backtitle "ProxMenux" --title " $( translate "No jobs" ) " \
--msgbox " $( translate "No scheduled backup jobs found." ) " 8 62
return 1
fi
2026-06-09 00:13:24 +02:00
# Build the menu rows. The loop variable is INTENTIONALLY named
# `_iter_id` (not `id`) — every caller passes "id" as $__out_var so
# the nameref below should point at the caller's local. A loop
# variable named `id` here would shadow it, and the nameref would
# silently write into _pick_job's own scope instead, leaving the
# caller with an empty string. That manifested as:
# ✓ Job timer enabled: (empty)
# run_scheduled_backup.sh: Usage: ... <job_id>
# Both reported on 2026-06-07.
2026-04-13 14:49:48 +02:00
local -a menu = ( )
2026-06-09 00:13:24 +02:00
local i = 1 _iter_id
for _iter_id in " ${ ids [@] } " ; do
menu += ( " $i " " $_iter_id [ $( _show_job_status " $_iter_id " ) ] " )
2026-04-13 14:49:48 +02:00
( ( i++) )
done
local sel
sel = $( dialog --backtitle "ProxMenux" --title " $title " \
--menu " \n $( translate "Select a job:" ) " " $HB_UI_MENU_H " " $HB_UI_MENU_W " " $HB_UI_MENU_LIST " \
" ${ menu [@] } " 3>& 1 1>& 2 2>& 3) || return 1
local picked = " ${ ids [ $(( sel-1)) ] } "
local -n out = " $__out_var "
out = " $picked "
return 0
}
2026-06-09 00:13:24 +02:00
# Common screen reset for any post-dialog action result. The
# `dialog` calls in this script leave their box drawn on screen
# even after the user has confirmed; without this reset, the
# subsequent msg_ok / msg_warn / "Press Enter" output renders
# in the bottom-left corner UNDER the leftover dialog box.
# show_proxmenux_logo already runs `clear` internally, so we
# don't add another one — the convention used across proxmenux
# (create_vm_menu.sh, config_menu.sh, menu_post_install.sh) is:
# show_proxmenux_logo → msg_title → result message
# Reported 2026-06-07 when the operator hit "Run job now" and
# saw "Job executed successfully" floating over the picker.
_render_action_screen( ) {
show_proxmenux_logo
msg_title " $1 "
}
2026-04-13 14:49:48 +02:00
_job_run_now( ) {
local id = ""
_pick_job " $( translate "Run job now" ) " id || return 1
2026-06-09 00:13:24 +02:00
# 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>".
if [ [ -z " $id " ] ] ; then
_render_action_screen " $( translate "Run job now" ) "
msg_error " $( translate "Job selection returned empty id — aborting." ) "
msg_success " $( translate "Press Enter to continue..." ) "
read -r
return 1
fi
2026-04-13 14:49:48 +02:00
local runner = " $LOCAL_SCRIPTS /backup_restore/run_scheduled_backup.sh "
[ [ ! -f " $runner " ] ] && runner = " $SCRIPT_DIR /run_scheduled_backup.sh "
2026-06-09 00:13:24 +02:00
2026-06-13 17:34:11 +02:00
# 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.
2026-06-09 00:13:24 +02:00
_render_action_screen " $( translate "Running backup job:" ) $id "
echo
2026-06-13 17:34:11 +02:00
" $runner " " $id "
local runner_exit
runner_exit = $?
2026-06-09 00:13:24 +02:00
echo
2026-04-13 14:49:48 +02:00
msg_success " $( translate "Press Enter to continue..." ) "
read -r
2026-06-13 17:34:11 +02:00
return $runner_exit
2026-04-13 14:49:48 +02:00
}
_job_toggle( ) {
local id = ""
_pick_job " $( translate "Enable/Disable job" ) " id || return 1
2026-06-09 00:13:24 +02:00
if [ [ -z " $id " ] ] ; then
_render_action_screen " $( translate "Enable/Disable job" ) "
msg_error " $( translate "Job selection returned empty id — aborting." ) "
msg_success " $( translate "Press Enter to continue..." ) "
read -r
return 1
fi
local action_label
2026-06-13 17:34:11 +02:00
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
2026-04-13 14:49:48 +02:00
else
2026-06-13 17:34:11 +02:00
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
2026-06-09 00:13:24 +02:00
fi
_render_action_screen " $( translate "Enable/Disable job" ) "
if [ [ " $action_label " = = "disabled" ] ] ; then
2026-06-13 17:34:11 +02:00
msg_warn " $( translate "Job disabled:" ) $id "
2026-06-09 00:13:24 +02:00
else
2026-06-13 17:34:11 +02:00
msg_ok " $( translate "Job enabled:" ) $id "
2026-04-13 14:49:48 +02:00
fi
msg_success " $( translate "Press Enter to continue..." ) "
read -r
}
_job_delete( ) {
local id = ""
_pick_job " $( translate "Delete job" ) " id || return 1
2026-06-09 00:13:24 +02:00
# An empty id here would build malformed unit paths like
# /etc/systemd/system/proxmenux-backup-.timer, and the subsequent
# rm -f would silently no-op against bogus paths — making it LOOK
# like a successful delete while the real job stays untouched.
if [ [ -z " $id " ] ] ; then
_render_action_screen " $( translate "Delete job" ) "
msg_error " $( translate "Job selection returned empty id — aborting." ) "
msg_success " $( translate "Press Enter to continue..." ) "
read -r
return 1
fi
2026-06-13 17:34:11 +02:00
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
2026-04-13 14:49:48 +02:00
if ! whiptail --title " $( translate "Confirm delete" ) " \
2026-06-13 17:34:11 +02:00
--yesno " $confirm_body " 14 70; then
2026-04-13 14:49:48 +02:00
return 1
fi
systemctl disable --now " proxmenux-backup- ${ id } .timer " >/dev/null 2>& 1 || true
rm -f " $( _service_file " $id " ) " " $( _timer_file " $id " ) " " $( _job_file " $id " ) " " $( _job_paths_file " $id " ) "
systemctl daemon-reload >/dev/null 2>& 1 || true
2026-06-09 00:13:24 +02:00
_render_action_screen " $( translate "Delete job" ) "
2026-04-13 14:49:48 +02:00
msg_ok " $( translate "Job deleted:" ) $id "
msg_success " $( translate "Press Enter to continue..." ) "
read -r
}
_show_jobs( ) {
local tmp
tmp = $( mktemp) || return
{
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)
} > " $tmp "
dialog --backtitle "ProxMenux" --title " $( translate "Scheduled backup jobs" ) " \
--textbox " $tmp " 28 100 || true
rm -f " $tmp "
}
main_menu( ) {
while true; do
local choice
choice = $( dialog --backtitle "ProxMenux" \
--title " $( translate "Backup scheduler and retention" ) " \
--menu " \n $( translate "Choose action:" ) " " $HB_UI_MENU_H " " $HB_UI_MENU_W " " $HB_UI_MENU_LIST " \
1 " $( translate "Create scheduled backup job" ) " \
2 " $( translate "Show jobs and last run status" ) " \
3 " $( translate "Run a job now" ) " \
4 " $( translate "Enable / disable job timer" ) " \
5 " $( translate "Delete job" ) " \
0 " $( translate "Return" ) " \
3>& 1 1>& 2 2>& 3) || return 0
case " $choice " in
1) _create_job ; ;
2) _show_jobs ; ;
3) _job_run_now ; ;
4) _job_toggle ; ;
5) _job_delete ; ;
0) return 0 ; ;
esac
done
}
main_menu