pbs-backup-client-script/pbs-backup.v2.sh
2025-08-23 01:26:06 +02:00

247 lines
7.8 KiB
Bash

#!/usr/bin/env bash
# Script Name: pbs-backup.v2.sh
# Beschreibung: Host-Backups zu Proxmox Backup Server (PBS) inkl. Notifications
# Aufruf: ./pbs-backup.v2.sh [--dry-run] [--no-mail] [--no-ntfy] [-q] [-h]
# Autor: Patrick Asmus
# Web: https://www.cleveradmin.de
# Git-Reposit.: https://git.techniverse.net/scriptos/pbs-backup-client-script.git
# Version: 2.0
# Datum: 22.08.2025
# Modifikation: - Ntfy-Support mit Auth implementiert
# - Logging und Ausgabe erweitert
# - Locking, Exit-Codes, externe Config, Dry-Run, robuste Fehlerbehandlung hinzugefügt
# - Konfiguration ausgelagertin eigene Konfigurationsdatei
# - Backup-OK-Datei für externes Monitoring implementiert
set -Eeuo pipefail
umask 077
# ---------- Variablen ---------- #
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/pbs-backup.v2.conf}"
DRY_RUN=false
FORCE_NO_MAIL=false
FORCE_NO_NTFY=false
VERBOSE=true
SCRIPT_NAME="$(basename "$0")"
HOSTNAME="$(hostname)"
# ---------- Helpers ---------- #
_timestamp() { date +"%Y-%m-%d %H:%M:%S"; }
log() {
local level="$1"; shift
local msg="$*"
local line="[$(_timestamp)] [$level] $msg"
echo "$line" | tee -a "$LOGFILE"
}
die() {
log "ERROR" "$*"
exit 1
}
check_dep() {
local bin="$1" hint="${2:-}"
command -v "$bin" >/dev/null 2>&1 || die "Benötigtes Programm '$bin' fehlt. $hint"
}
bool() { [[ "${1,,}" == "true" || "$1" == "1" || "${1,,}" == "yes" ]]; }
print_usage() {
cat <<EOF
Usage: $SCRIPT_NAME [Optionen]
Optionen:
--dry-run Nur anzeigen, was ausgeführt würde (kein Backup)
--no-mail Mail-Benachrichtigung für diesen Lauf deaktivieren
--no-ntfy Ntfy-Benachrichtigung für diesen Lauf deaktivieren
-q, --quiet Weniger Ausgabe (nur WARN/ERROR)
-h, --help Diese Hilfe
Hinweis:
Alle variablen Einstellungen kommen aus der Config:
$CONFIG_FILE
Standardmäßig wird eine Datei 'pbs-backup.v2.conf' im selben Verzeichnis wie das Script erwartet.
EOF
}
# ---------- CLI ---------- #
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--no-mail) FORCE_NO_MAIL=true; shift ;;
--no-ntfy) FORCE_NO_NTFY=true; shift ;;
-q|--quiet) VERBOSE=false; shift ;;
-h|--help) print_usage; exit 0 ;;
*) die "Unbekannte Option: $1 (siehe --help)" ;;
esac
done
# ---------- Config laden ---------- #
[[ -f "$CONFIG_FILE" ]] || die "Config nicht gefunden: $CONFIG_FILE"
source "$CONFIG_FILE"
[[ -n "${PBS_REPOSITORY:-}" ]] || die "PBS_REPOSITORY ist nicht gesetzt (in $CONFIG_FILE)."
if ! declare -p INCLUDE_DIRS >/dev/null 2>&1; then
die "INCLUDE_DIRS ist nicht gesetzt (Array) in $CONFIG_FILE."
fi
[[ "${#INCLUDE_DIRS[@]}" -gt 0 ]] || die "INCLUDE_DIRS ist leer in $CONFIG_FILE."
[[ -n "${LOGFILE:-}" ]] || die "LOGFILE ist nicht gesetzt (in $CONFIG_FILE)."
[[ -n "${LOCKFILE:-}" ]] || die "LOCKFILE ist nicht gesetzt (in $CONFIG_FILE)."
HEALTHCHECK_OK_FILE="${HEALTHCHECK_OK_FILE:-}"
$FORCE_NO_MAIL && MAIL_ENABLED=false
$FORCE_NO_NTFY && NTFY_ENABLED=false
$VERBOSE || exec >/dev/null 2>&1
RUNLOG="$(mktemp)"
_cleanup() { rm -f "$RUNLOG"; }
trap _cleanup EXIT
# ---------- Lock & Log ---------- #
if ! touch "$LOGFILE" 2>/dev/null; then
LOGFILE="/tmp/pbs-backup.v2.log"
touch "$LOGFILE" || die "Kann Logfile nicht anlegen: $LOGFILE"
fi
mkdir -p "$(dirname "$LOCKFILE")"
exec 9>"$LOCKFILE"
flock -n 9 || die "Es läuft bereits ein anderer Backup-Prozess (Lock: $LOCKFILE)"
log "INFO" "Starte $SCRIPT_NAME auf Host $HOSTNAME"
log "INFO" "Config: $CONFIG_FILE"
log "INFO" "Logfile: $LOGFILE"
check_dep "proxmox-backup-client" "Bitte PBS-Client installieren."
if bool "${MAIL_ENABLED:-}"; then
check_dep "${MAIL_CMD:-mail}" "Mail-/msmtp-Client nicht gefunden? Setze MAIL_CMD in $CONFIG_FILE."
fi
if bool "${NTFY_ENABLED:-}"; then
check_dep "curl" "curl wird für Ntfy benötigt."
fi
# ---------- Notifications ---------- #
send_mail() {
bool "${MAIL_ENABLED:-}" || return 0
local subject="$1"; shift
local body="$*"
[[ -n "${MAIL_RECIPIENTS:-}" ]] || { log "WARN" "MAIL_RECIPIENTS leer, Mail wird übersprungen."; return 0; }
local mail_cmd="${MAIL_CMD:-mail}"
printf "%s\n" "$body" | $mail_cmd -s "$subject" $MAIL_RECIPIENTS || log "ERROR" "Mail-Versand fehlgeschlagen."
}
send_ntfy() {
bool "${NTFY_ENABLED:-}" || return 0
local title="$1" priority="$2" body="$3"
[[ -n "${NTFY_TOPIC_URL:-}" ]] || { log "WARN" "NTFY_TOPIC_URL leer, Ntfy wird übersprungen."; return 0; }
local -a CURL_ARGS=( -sS -X POST "$NTFY_TOPIC_URL" -H "Title: $title" -H "Priority: $priority" --data-binary "$body" )
[[ -n "${NTFY_AUTH_TOKEN:-}" ]] && CURL_ARGS+=( -H "Authorization: Bearer $NTFY_AUTH_TOKEN" )
curl "${CURL_ARGS[@]}" >/dev/null || log "ERROR" "Ntfy-Versand fehlgeschlagen."
}
# ---------- Backup ---------- #
run_backup() {
local cmd=("proxmox-backup-client" "backup" "--repository" "$PBS_REPOSITORY")
for dir in "${INCLUDE_DIRS[@]}"; do
local base; base="$(basename "$dir")"
cmd+=("${base}.pxar:${dir}")
done
local cmd_pretty; cmd_pretty="$(printf "%q " "${cmd[@]}")"
log "INFO" "Backup-Command: ${cmd_pretty} # PBS_PASSWORD via env"
if $DRY_RUN; then
log "INFO" "DRY-RUN aktiv: Es wird nichts ausgeführt."
return 0
fi
{
echo "===== $(_timestamp) | BEGIN BACKUP RUN ====="
echo "Host: $HOSTNAME"
echo "Repository: $PBS_REPOSITORY"
echo "Quellen:"
printf " - %s\n" "${INCLUDE_DIRS[@]}"
echo "------------------------------------------------"
} | tee -a "$LOGFILE" | tee -a "$RUNLOG"
if (
[[ -n "${PBS_API:-}" ]] && export PBS_PASSWORD="$PBS_API"
[[ -n "${PBS_FINGERPRINT:-}" ]] && export PBS_FINGERPRINT
"${cmd[@]}"
) 2>&1 | tee -a "$LOGFILE" | tee -a "$RUNLOG"
then
echo "===== $(_timestamp) | END BACKUP RUN (OK) =====" | tee -a "$LOGFILE" | tee -a "$RUNLOG"
return 0
else
echo "===== $(_timestamp) | END BACKUP RUN (FAILED) =====" | tee -a "$LOGFILE" | tee -a "$RUNLOG"
return 1
fi
}
# ---------- Healthcheck ---------- #
write_health_ok() {
[[ -n "$HEALTHCHECK_OK_FILE" ]] || return 0
mkdir -p "$(dirname "$HEALTHCHECK_OK_FILE")" || true
cat > "$HEALTHCHECK_OK_FILE" <<EOF
status=OK
timestamp=$(date -Iseconds)
host=$HOSTNAME
repo=$PBS_REPOSITORY
duration_s=$1
EOF
chmod 0644 "$HEALTHCHECK_OK_FILE" || true
}
remove_health_ok() {
[[ -n "$HEALTHCHECK_OK_FILE" ]] || return 0
rm -f "$HEALTHCHECK_OK_FILE" || true
}
# ---------- Main ---------- #
START_EPOCH="$(date +%s)"
STATUS="unknown"
EXIT_CODE=1
NTFY_TAIL_LINES="${NTFY_TAIL_LINES:-200}"
if run_backup; then
STATUS="success"
log "INFO" "Backup erfolgreich abgeschlossen."
if bool "${MAIL_NOTIFY_ON_SUCCESS:-}"; then
send_mail "${MAIL_SUBJECT_SUCCESS:-" ${HOSTNAME} | Backup erfolgreich abgeschlossen"}" \
"$(printf "Das Backup wurde erfolgreich abgeschlossen.\n\n--- Letzte %s Zeilen ---\n%s" "$NTFY_TAIL_LINES" "$(tail -n "$NTFY_TAIL_LINES" "$RUNLOG")")"
fi
if bool "${NTFY_NOTIFY_ON_SUCCESS:-}"; then
send_ntfy "${NTFY_TITLE_SUCCESS:-"Backup erfolgreich: ${HOSTNAME}"}" \
"${NTFY_PRIORITY_SUCCESS:-3}" \
"$(tail -n "$NTFY_TAIL_LINES" "$RUNLOG")"
fi
EXIT_CODE=0
else
STATUS="failure"
log "ERROR" "Fehler beim Backup."
send_mail "${MAIL_SUBJECT_FAILURE:-" ${HOSTNAME} | Fehler beim Backup"}" \
"$(printf "Es ist ein Fehler beim Backup aufgetreten.\n\n--- Letzte %s Zeilen ---\n%s" "${NTFY_TAIL_LINES}" "$(tail -n "$NTFY_TAIL_LINES" "$RUNLOG")")"
send_ntfy "${NTFY_TITLE_FAILURE:-"Backup FEHLGESCHLAGEN: ${HOSTNAME}"}" \
"${NTFY_PRIORITY_FAILURE:-5}" \
"$(tail -n "$NTFY_TAIL_LINES" "$RUNLOG")"
EXIT_CODE=1
fi
DURATION="$(( $(date +%s) - START_EPOCH ))"
log "INFO" "Ende ($STATUS) | Dauer: ${DURATION}s"
if [[ "$EXIT_CODE" -eq 0 ]]; then
write_health_ok "$DURATION"
else
remove_health_ok
fi
exit "$EXIT_CODE"