mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-16 23:08:26 +00:00
update 1.2.2.2 beta
This commit is contained in:
@@ -549,12 +549,28 @@ _bk_manage_extra_paths() {
|
|||||||
--title "$(translate "Manage custom backup paths")" \
|
--title "$(translate "Manage custom backup paths")" \
|
||||||
--menu "\n${preview}\n" \
|
--menu "\n${preview}\n" \
|
||||||
"$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
|
"$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
|
||||||
|
"view" "$(translate "View current paths")" \
|
||||||
"add" "$(translate "+ Add a path")" \
|
"add" "$(translate "+ Add a path")" \
|
||||||
"del" "$(translate "− Remove a path")" \
|
"del" "$(translate "− Remove a path")" \
|
||||||
"back" "$(translate "← Return")" \
|
"back" "$(translate "← Return")" \
|
||||||
3>&1 1>&2 2>&3) || break
|
3>&1 1>&2 2>&3) || break
|
||||||
|
|
||||||
case "$choice" in
|
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)
|
add)
|
||||||
local new_path
|
local new_path
|
||||||
new_path=$(dialog --backtitle "ProxMenux" \
|
new_path=$(dialog --backtitle "ProxMenux" \
|
||||||
|
|||||||
@@ -75,13 +75,47 @@ _list_jobs() {
|
|||||||
done | sort
|
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() {
|
_show_job_status() {
|
||||||
local id="$1"
|
local id="$1"
|
||||||
local timer_state="disabled"
|
if _job_is_attached "$id"; then
|
||||||
local service_state="unknown"
|
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"
|
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")
|
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() {
|
_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 <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
|
||||||
|
}
|
||||||
|
|
||||||
_create_job() {
|
_create_job() {
|
||||||
local id backend on_calendar profile_mode
|
local id backend on_calendar profile_mode
|
||||||
id=$(dialog --backtitle "ProxMenux" --title "$(translate "New backup job")" \
|
id=$(dialog --backtitle "ProxMenux" --title "$(translate "New backup job")" \
|
||||||
@@ -175,6 +321,31 @@ _create_job() {
|
|||||||
"pbs" "Proxmox Backup Server" \
|
"pbs" "Proxmox Backup Server" \
|
||||||
3>&1 1>&2 2>&3) || return 1
|
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")" \
|
on_calendar=$(dialog --backtitle "ProxMenux" --title "$(translate "Schedule")" \
|
||||||
--inputbox "$(translate "systemd OnCalendar expression")"$'\n'"$(translate "Example: daily or Mon..Fri 03:00")" \
|
--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
|
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"
|
local runner="$LOCAL_SCRIPTS/backup_restore/run_scheduled_backup.sh"
|
||||||
[[ ! -f "$runner" ]] && runner="$SCRIPT_DIR/run_scheduled_backup.sh"
|
[[ ! -f "$runner" ]] && runner="$SCRIPT_DIR/run_scheduled_backup.sh"
|
||||||
|
|
||||||
# ── Visible execution ───────────────────────────────────
|
# Foreground execution — the runner detects TTY and prints a
|
||||||
# Clear the leftover dialog frame and announce what's about
|
# colored progress layout (mirrors _bk_local in backup_host.sh).
|
||||||
# to happen, so the operator stops looking at a frozen
|
# Plain-text log file is still written for audit / scheduler runs.
|
||||||
# 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.
|
|
||||||
_render_action_screen "$(translate "Running backup job:") $id"
|
_render_action_screen "$(translate "Running backup job:") $id"
|
||||||
echo
|
echo
|
||||||
|
"$runner" "$id"
|
||||||
# Snapshot existing log files so we can identify the new one the
|
local runner_exit
|
||||||
# runner is about to create (filename pattern is `${id}-${ts}.log`).
|
runner_exit=$?
|
||||||
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
|
|
||||||
|
|
||||||
echo
|
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...")"
|
msg_success "$(translate "Press Enter to continue...")"
|
||||||
read -r
|
read -r
|
||||||
|
return $runner_exit
|
||||||
}
|
}
|
||||||
|
|
||||||
_job_toggle() {
|
_job_toggle() {
|
||||||
@@ -415,9 +534,21 @@ _job_toggle() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Decide the action label up front so the title reflects what we
|
|
||||||
# actually just did (enable vs disable).
|
|
||||||
local action_label
|
local action_label
|
||||||
|
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
|
||||||
if systemctl is-enabled --quiet "proxmenux-backup-${id}.timer" >/dev/null 2>&1; then
|
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
|
systemctl disable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true
|
||||||
action_label="disabled"
|
action_label="disabled"
|
||||||
@@ -425,12 +556,13 @@ _job_toggle() {
|
|||||||
systemctl enable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true
|
systemctl enable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true
|
||||||
action_label="enabled"
|
action_label="enabled"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
_render_action_screen "$(translate "Enable/Disable job")"
|
_render_action_screen "$(translate "Enable/Disable job")"
|
||||||
if [[ "$action_label" == "disabled" ]]; then
|
if [[ "$action_label" == "disabled" ]]; then
|
||||||
msg_warn "$(translate "Job timer disabled:") $id"
|
msg_warn "$(translate "Job disabled:") $id"
|
||||||
else
|
else
|
||||||
msg_ok "$(translate "Job timer enabled:") $id"
|
msg_ok "$(translate "Job enabled:") $id"
|
||||||
fi
|
fi
|
||||||
msg_success "$(translate "Press Enter to continue...")"
|
msg_success "$(translate "Press Enter to continue...")"
|
||||||
read -r
|
read -r
|
||||||
@@ -450,8 +582,16 @@ _job_delete() {
|
|||||||
read -r
|
read -r
|
||||||
return 1
|
return 1
|
||||||
fi
|
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")" \
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
systemctl disable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true
|
systemctl disable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true
|
||||||
|
|||||||
@@ -2661,6 +2661,143 @@ hb_show_compat_report() {
|
|||||||
# Fail-soft: returns 0 even if jq is missing and we have to fall
|
# Fail-soft: returns 0 even if jq is missing and we have to fall
|
||||||
# back to printf-built JSON; never aborts the surrounding backup.
|
# 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 <TAB> storage <TAB> storage_type <TAB> schedule
|
||||||
|
# <TAB> prune-backups <TAB> 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() {
|
hb_write_archive_sidecar() {
|
||||||
local archive_path="$1"
|
local archive_path="$1"
|
||||||
local kind="${2:-}"
|
local kind="${2:-}"
|
||||||
|
|||||||
@@ -151,10 +151,14 @@ _sb_run_pbs() {
|
|||||||
cmd+=(--keyfile "$PBS_KEYFILE")
|
cmd+=(--keyfile "$PBS_KEYFILE")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
env PBS_PASSWORD="$PBS_PASSWORD" PBS_ENCRYPTION_PASSWORD="${PBS_ENCRYPTION_PASSWORD:-}" \
|
env PBS_PASSWORD="$PBS_PASSWORD" \
|
||||||
"${cmd[@]}" >/dev/null 2>&1 || return 1
|
PBS_ENCRYPTION_PASSWORD="${PBS_ENCRYPTION_PASSWORD:-}" \
|
||||||
|
PBS_FINGERPRINT="${PBS_FINGERPRINT:-}" \
|
||||||
|
"${cmd[@]}" 2>&1 || return 1
|
||||||
|
|
||||||
# Best effort prune for PBS group.
|
# Best effort prune for PBS group.
|
||||||
|
env PBS_PASSWORD="$PBS_PASSWORD" \
|
||||||
|
PBS_FINGERPRINT="${PBS_FINGERPRINT:-}" \
|
||||||
proxmox-backup-client prune "host/${backup_id}" --repository "$PBS_REPOSITORY" \
|
proxmox-backup-client prune "host/${backup_id}" --repository "$PBS_REPOSITORY" \
|
||||||
${KEEP_LAST:+--keep-last "$KEEP_LAST"} \
|
${KEEP_LAST:+--keep-last "$KEEP_LAST"} \
|
||||||
${KEEP_HOURLY:+--keep-hourly "$KEEP_HOURLY"} \
|
${KEEP_HOURLY:+--keep-hourly "$KEEP_HOURLY"} \
|
||||||
@@ -162,7 +166,7 @@ _sb_run_pbs() {
|
|||||||
${KEEP_WEEKLY:+--keep-weekly "$KEEP_WEEKLY"} \
|
${KEEP_WEEKLY:+--keep-weekly "$KEEP_WEEKLY"} \
|
||||||
${KEEP_MONTHLY:+--keep-monthly "$KEEP_MONTHLY"} \
|
${KEEP_MONTHLY:+--keep-monthly "$KEEP_MONTHLY"} \
|
||||||
${KEEP_YEARLY:+--keep-yearly "$KEEP_YEARLY"} \
|
${KEEP_YEARLY:+--keep-yearly "$KEEP_YEARLY"} \
|
||||||
>/dev/null 2>&1 || true
|
2>&1 || true
|
||||||
|
|
||||||
echo "PBS_SNAPSHOT=host/${backup_id}/${epoch}"
|
echo "PBS_SNAPSHOT=host/${backup_id}/${epoch}"
|
||||||
return 0
|
return 0
|
||||||
@@ -203,6 +207,7 @@ main() {
|
|||||||
{
|
{
|
||||||
echo "=== Scheduled backup job ${job_id} started at $(date -Iseconds) ==="
|
echo "=== Scheduled backup job ${job_id} started at $(date -Iseconds) ==="
|
||||||
echo "Backend: ${BACKEND:-}"
|
echo "Backend: ${BACKEND:-}"
|
||||||
|
echo "Profile: ${PROFILE_MODE:-default}"
|
||||||
} >"$log_file"
|
} >"$log_file"
|
||||||
|
|
||||||
local -a paths=()
|
local -a paths=()
|
||||||
@@ -219,21 +224,57 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
case "${BACKEND:-}" in
|
||||||
local)
|
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=$?
|
rc=$?
|
||||||
|
echo "$_output" >>"$log_file"
|
||||||
|
archive_path=$(grep "^LOCAL_ARCHIVE=" <<<"$_output" | cut -d'=' -f2-)
|
||||||
;;
|
;;
|
||||||
borg)
|
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
|
_sb_run_borg "$stage_root" "${job_id}-${ts}" >>"$log_file" 2>&1
|
||||||
rc=$?
|
rc=$?
|
||||||
|
archive_path="${BORG_REPO:-}::${job_id}-${ts}"
|
||||||
;;
|
;;
|
||||||
pbs)
|
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
|
_sb_run_pbs "$stage_root" "${PBS_BACKUP_ID:-hostcfg-$(hostname)}" "$(date +%s)" >>"$log_file" 2>&1
|
||||||
rc=$?
|
rc=$?
|
||||||
|
archive_path="${PBS_REPOSITORY:-}::host/${PBS_BACKUP_ID:-hostcfg-$(hostname)}"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown backend: ${BACKEND:-}" >>"$log_file"
|
echo "Unknown backend: ${BACKEND:-}" >>"$log_file"
|
||||||
@@ -241,17 +282,45 @@ main() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
elapsed=$((SECONDS - t_start))
|
||||||
|
|
||||||
|
echo "Cleaning up staging area ..." >>"$log_file"
|
||||||
rm -rf "$stage_root"
|
rm -rf "$stage_root"
|
||||||
|
|
||||||
if [[ $rc -eq 0 ]]; then
|
if [[ $rc -eq 0 ]]; then
|
||||||
echo "RESULT=ok" >>"$summary_file"
|
echo "RESULT=ok" >>"$summary_file"
|
||||||
echo "LOG_FILE=${log_file}" >>"$summary_file"
|
echo "LOG_FILE=${log_file}" >>"$summary_file"
|
||||||
echo "=== Job finished OK at $(date -Iseconds) ===" >>"$log_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
|
exit 0
|
||||||
else
|
else
|
||||||
echo "RESULT=failed" >>"$summary_file"
|
echo "RESULT=failed" >>"$summary_file"
|
||||||
echo "LOG_FILE=${log_file}" >>"$summary_file"
|
echo "LOG_FILE=${log_file}" >>"$summary_file"
|
||||||
echo "=== Job finished with errors at $(date -Iseconds) ===" >>"$log_file"
|
echo "=== Job finished with errors at $(date -Iseconds) ===" >>"$log_file"
|
||||||
|
(( TTY )) && msg_error "$(translate "Backup failed. See log:") $log_file"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
54
scripts/backup_restore/vzdump-hook.sh
Executable file
54
scripts/backup_restore/vzdump-hook.sh
Executable file
@@ -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
|
||||||
Reference in New Issue
Block a user