#!/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 # - Logging zu Datei & Terminal (mit Timestamps) # - Locking, Exit-Codes, externe Config, Dry-Run, robuste Fehlerbehandlung # - Konfiguration ausgelagertin pbs-backup.v2.conf # - Backup-OK-Datei für externes Monitoring 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 </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" <