#!/usr/bin/env bash # ============================================================================= # /opt/windmill-restore/restore.sh # Windmill Backup Restore Worker # Version: 1.0.9 # # WAS DIESES SCRIPT MACHT: # ───────────────────────────────────────────────────────────────────────────── # Läuft direkt auf dem Proxmox Restore-Server, gestartet von Windmill per SSH # non-blocking (nohup ... &). Komplett eigenständig, keine offene Verbindung. # # Alle Pfade kommen aus der Datenbank via Windmill: # --restore-mount Mountpoint des Restore-Volumes z.B. /mnt/HDD_5TB.1 # --restore-path PVE-Storage-Name für qmrestore z.B. 5TB.1 # --rsync-target Rsync-Ziel auf Backup-Server z.B. /backup/incoming/TNP # --pbs-storage PVE-Storage-ID des PBS-Datastores z.B. pbs-tnp-invest-gmbh # # KEYS – beide werden per Rsync vom PBS-Server geholt (PBS_HOST aus pbs.conf): # PBS Encrypt-Key: /root/Scripte/${DATASTORE}.keyfile → für qmrestore # 7z-Passwort: /root/Scripte/password_7z.txt → für 7z-Archiv # Format: "tnp-Invest-GmbH: Passwort123" # # VM_IMAGE_DIR wird dynamisch aus dem PVE-Storage-Pfad ermittelt: # pvesh get /storage/${RESTORE_PATH} → path → /mnt/HDD_5TB.1/images/${VM_ID} # # ABLAUF: # [0] Keys holen – Keyfile + 7z-Passwort per Rsync vom PBS-Server # [1] Space-Check – Freier Platz auf restore-mount prüfen # [2] VM-ID ermitteln – Original aus Backup-Pfad, Restore-ID ab 1000 # [3] qmrestore – Direkt vom PBS-Storage mit --keyfile # [4] VM_IMAGE_DIR – Dynamisch aus PVE-Storage-Pfad ermitteln # [5] Images prüfen – Abbruch wenn leer/nicht vorhanden # [6] VM vorbereiten – unlock, cdrom/ide0 entfernen, alle Netzwerkkarten # löschen, Agent aktivieren # [7] VM starten & – 120s auf qm-Agent warten (10s Schritte) # Agent prüfen Kein Agent = qm_agent_ok=false, KEIN Abbruch # [8] VM stoppen – Sauberes Shutdown, nach 30s force-stop # [9] 7z-Archiv – VM-Images verschlüsselt zippen (7z-Passwort) # [10] Rsync – ZIP zum Backup-Server (Firmen-Zielverzeichnis) # 3 Versuche + Größenvergleich # [11] Aufräumen – VM destroy, ZIP löschen # [12] Webhook – JSON → Windmill schreibt DB & startet nächsten # ============================================================================= set -euo pipefail # ── Konfigdatei laden (PBS-Credentials) ────────────────────────────────────── CONF_FILE="/opt/windmill-restore/pbs.conf" [[ ! -f "$CONF_FILE" ]] && { echo "FEHLER: $CONF_FILE fehlt!" >&2; exit 1; } # shellcheck source=/dev/null source "$CONF_FILE" # Enthält: PBS_HOST, PBS_PORT, PBS_USER, PBS_PASSWORD, PBS_FINGERPRINT # ── Argument-Parser ─────────────────────────────────────────────────────────── JOB_UUID="" BACKUP_PATH="" CLIENT_NAME="" RESTORE_MOUNT="" RESTORE_PATH="" RSYNC_TARGET="" PBS_STORAGE="" WEBHOOK_URL="" WEBHOOK_TOKEN="" while [[ $# -gt 0 ]]; do case $1 in --job-uuid) JOB_UUID="$2"; shift 2 ;; --backup-path) BACKUP_PATH="$2"; shift 2 ;; --client) CLIENT_NAME="$2"; shift 2 ;; --restore-mount) RESTORE_MOUNT="$2"; shift 2 ;; --restore-path) RESTORE_PATH="$2"; shift 2 ;; --rsync-target) RSYNC_TARGET="$2"; shift 2 ;; --pbs-storage) PBS_STORAGE="$2"; shift 2 ;; --webhook-url) WEBHOOK_URL="$2"; shift 2 ;; --webhook-token) WEBHOOK_TOKEN="$2"; shift 2 ;; *) echo "Unbekannter Parameter: $1" >&2; exit 1 ;; esac done # Pflichtparameter prüfen for var in JOB_UUID BACKUP_PATH CLIENT_NAME \ RESTORE_MOUNT RESTORE_PATH RSYNC_TARGET PBS_STORAGE WEBHOOK_URL; do [[ -z "${!var}" ]] && { echo "FEHLER: --${var//_/-} fehlt" >&2; exit 1; } done [[ ! -d "$RESTORE_MOUNT" ]] && { echo "FEHLER: Restore-Mount '$RESTORE_MOUNT' existiert nicht!" >&2 exit 1 } # ── Logging ─────────────────────────────────────────────────────────────────── LOG_DIR="/opt/windmill-restore/logs" mkdir -p "$LOG_DIR" SAFE_CLIENT="${CLIENT_NAME//\//_}" # "vm/100" → "vm_100" LOG_FILE="$LOG_DIR/${SAFE_CLIENT}_$(date +%Y%m%d_%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 # ── Backup-Pfad zerlegen ────────────────────────────────────────────────────── # BACKUP_PATH Format: "tnp-Invest-GmbH:vm/100/2024-01-15T02:00:00Z" DATASTORE=$(echo "$BACKUP_PATH" | cut -d: -f1) SNAPSHOT_PATH=$(echo "$BACKUP_PATH" | cut -d: -f2) PVE_BACKUP_REF="${PBS_STORAGE}:backup/${SNAPSHOT_PATH}" # ── Pfade & Messvariablen ───────────────────────────────────────────────────── LAST_DATE=$(date +"%Y-%m-%d" -d "1 day ago") ZIP_DIR="${RESTORE_MOUNT}/zips/${LAST_DATE}" BACKUP_SERVER_HOST=$(cat /opt/windmill-restore/backup_server_host 2>/dev/null \ || echo "backup-server") RESTORE_START=$(date +%s) STATUS="success" ERROR_MSG="" VM_ID_ORIGINAL=0 VM_ID_RESTORED=0 VM_NAME="" VM_IMAGE_DIR="" ACTUAL_DISK_BYTES=0 ZIP_SIZE_BYTES=0 ZIP_DURATION=0 RSYNC_SIZE_BYTES=0 RSYNC_OK="true" RSYNC_RETRIES=0 QM_AGENT_OK="false" RESTORE_DURATION=0 ZIP_PASSWORD="" echo "============================================================" echo " Windmill Restore Worker v1.0.9" echo " Client: $CLIENT_NAME" echo " Datastore: $DATASTORE" echo " Backup: $BACKUP_PATH" echo " PBS-Storage: $PBS_STORAGE" echo " Restore-Mount: $RESTORE_MOUNT" echo " Restore-Path: $RESTORE_PATH" echo " Rsync-Target: $RSYNC_TARGET" echo " Job-UUID: $JOB_UUID" echo " Start: $(date '+%Y-%m-%d %H:%M:%S')" echo "============================================================" # ── Webhook-Funktion ────────────────────────────────────────────────────────── send_webhook() { local wh_status="$1" local wh_error="${2:-}" local duration=$(( $(date +%s) - RESTORE_START )) local payload payload=$(printf '{ "job_uuid": "%s", "client_name": "%s", "status": "%s", "error_message": "%s", "vm_id_original": %d, "vm_id_restored": %d, "restore_duration_sec": %d, "actual_disk_used_bytes": %d, "zip_size_bytes": %d, "zip_duration_sec": %d, "rsync_size_bytes": %d, "rsync_ok": %s, "rsync_retries": %d, "qm_agent_ok": %s, "log_file": "%s" }' \ "$JOB_UUID" "$CLIENT_NAME" "$wh_status" "$wh_error" \ "$VM_ID_ORIGINAL" "$VM_ID_RESTORED" \ "$duration" "$ACTUAL_DISK_BYTES" \ "$ZIP_SIZE_BYTES" "$ZIP_DURATION" \ "$RSYNC_SIZE_BYTES" "$RSYNC_OK" "$RSYNC_RETRIES" \ "$QM_AGENT_OK" "$LOG_FILE") echo "" echo "==> Sende Webhook..." local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" \ -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ ${WEBHOOK_TOKEN:+-H "Authorization: Bearer ${WEBHOOK_TOKEN}"} \ -d "$payload") echo " HTTP: $http_code" [[ "$http_code" =~ ^2 ]] && echo " Webhook OK." \ || echo " WARNUNG: HTTP $http_code" } trap 'STATUS="failed" send_webhook "failed" "Abgebrochen in Zeile $LINENO – $LOG_FILE"' ERR # ═════════════════════════════════════════════════════════════════════════════ # [0/12] KEYS VOM PBS-SERVER HOLEN # PBS_HOST kommt aus pbs.conf. Beide Dateien werden lokal gecacht. # Bei mehreren VMs desselben Datastores nur einmal geholt. # # PBS Encrypt-Keyfile: /root/Scripte/${DATASTORE}.keyfile → qmrestore --keyfile # 7z-Passwort: /root/Scripte/password_7z.txt → 7z -p # Format: "tnp-Invest-GmbH: Passwort123" # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [0/12] Keys vom PBS-Server holen ($PBS_HOST)..." KEY_DIR="/opt/windmill-restore/keys" mkdir -p "$KEY_DIR" chmod 700 "$KEY_DIR" # ── PBS Encrypt-Keyfile ─────────────────────────────────────────────────────── KEYFILE_LOCAL="${KEY_DIR}/${DATASTORE}.keyfile" if [[ -f "$KEYFILE_LOCAL" && -s "$KEYFILE_LOCAL" ]]; then echo " Keyfile bereits vorhanden: $KEYFILE_LOCAL" else echo " Hole Keyfile: root@${PBS_HOST}:/root/Scripte/${DATASTORE}.keyfile" rsync -az \ "root@${PBS_HOST}:/root/Scripte/${DATASTORE}.keyfile" \ "$KEYFILE_LOCAL" \ 2>&1 chmod 600 "$KEYFILE_LOCAL" echo " Keyfile gespeichert ✓" fi [[ ! -f "$KEYFILE_LOCAL" || ! -s "$KEYFILE_LOCAL" ]] && { echo "FEHLER: Keyfile fehlt oder leer für Datastore '$DATASTORE'" >&2 exit 1 } # ── 7z-Passwort aus password_7z.txt ────────────────────────────────────────── PW_FILE_LOCAL="${KEY_DIR}/password_7z.txt" if [[ ! -f "$PW_FILE_LOCAL" || ! -s "$PW_FILE_LOCAL" ]]; then echo " Hole password_7z.txt: root@${PBS_HOST}:/root/Scripte/password_7z.txt" rsync -az \ "root@${PBS_HOST}:/root/Scripte/password_7z.txt" \ "$PW_FILE_LOCAL" \ 2>&1 chmod 600 "$PW_FILE_LOCAL" echo " password_7z.txt gespeichert ✓" else echo " password_7z.txt bereits vorhanden." fi ZIP_PASSWORD=$(grep -m1 "^${DATASTORE}:" "$PW_FILE_LOCAL" \ | awk -F': ' '{print $2}' | tr -d '[:space:]') [[ -z "$ZIP_PASSWORD" ]] && { echo "FEHLER: Kein 7z-Passwort für '$DATASTORE' in password_7z.txt" >&2 exit 1 } echo " 7z-Passwort geladen ✓" # ═════════════════════════════════════════════════════════════════════════════ # [1/12] SPACE-CHECK # Nur Warnung – kein Abbruch. # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [1/12] Prüfe freien Speicherplatz auf $RESTORE_MOUNT..." mkdir -p "$ZIP_DIR" FREE_KB=$(df "$RESTORE_MOUNT" 2>/dev/null | awk 'NR==2{print $4}' || echo "0") FREE_GB=$(( FREE_KB / 1024 / 1024 )) echo " Frei: ${FREE_GB} GB" [[ $FREE_GB -lt 50 ]] && echo " WARNUNG: Weniger als 50 GB frei!" # ═════════════════════════════════════════════════════════════════════════════ # [2/12] VM-ID ERMITTELN # Original aus Snapshot-Pfad (vm/100/... → 100) # Restore-ID: erste freie ab 1000, prüft QEMU + LXC # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [2/12] Ermittle VM-IDs..." VM_ID_ORIGINAL=$(echo "$SNAPSHOT_PATH" | grep -oP '\d+' | head -1 || echo "0") echo " Original VM-ID: $VM_ID_ORIGINAL" VM_ID_RESTORED=$( { pvesh get /nodes/localhost/qemu --output-format json 2>/dev/null || echo "[]" pvesh get /nodes/localhost/lxc --output-format json 2>/dev/null || echo "[]" } | python3 -c " import json, sys data = [] for line in sys.stdin: line = line.strip() if line: try: data.extend(json.loads(line)) except: pass existing = {int(v.get('vmid', 0)) for v in data} for i in range(1000, 2000): if i not in existing: print(i); break " 2>/dev/null || echo "1000" ) echo " Restore VM-ID: $VM_ID_RESTORED" # ═════════════════════════════════════════════════════════════════════════════ # [3/12] QMRESTORE VOM PBS-STORAGE # --storage aus DB (restore_path), z.B. "5TB.1" # --keyfile lokal gecachtes Keyfile vom PBS-Server # --unique 1 verhindert Konflikte mit bestehenden VM-Configs # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [3/12] qmrestore vom PBS-Storage..." echo " Backup-Ref: $PVE_BACKUP_REF" echo " Storage: $RESTORE_PATH" echo " Keyfile: $KEYFILE_LOCAL" echo " VM-ID: $VM_ID_RESTORED" RESTORE_START_INNER=$(date +%s) qmrestore "$PVE_BACKUP_REF" "$VM_ID_RESTORED" \ --storage "$RESTORE_PATH" \ --keyfile "$KEYFILE_LOCAL" \ --unique 1 \ 2>&1 RESTORE_DURATION=$(( $(date +%s) - RESTORE_START_INNER )) echo " Restore abgeschlossen in ${RESTORE_DURATION}s" # ═════════════════════════════════════════════════════════════════════════════ # [4/12] VM_IMAGE_DIR DYNAMISCH ERMITTELN # PVE kennt den Basispfad des Storages. # pvesh get /storage/5TB.1 → "path": "/mnt/HDD_5TB.1" # VM_IMAGE_DIR = /mnt/HDD_5TB.1/images/${VM_ID_RESTORED} # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [4/12] Ermittle VM-Image-Verzeichnis..." STORAGE_BASE=$(pvesh get "/storage/${RESTORE_PATH}" --output-format json \ 2>/dev/null | python3 -c " import json, sys cfg = json.load(sys.stdin) print(cfg.get('path', '')) " 2>/dev/null || echo "") if [[ -n "$STORAGE_BASE" ]]; then VM_IMAGE_DIR="${STORAGE_BASE}/images/${VM_ID_RESTORED}" else # Fallback: direkt aus pvesm path VM_IMAGE_DIR=$(pvesm path "${RESTORE_PATH}:vm-${VM_ID_RESTORED}-disk-0" \ 2>/dev/null | xargs dirname 2>/dev/null || echo "") fi if [[ -z "$VM_IMAGE_DIR" ]]; then # Letzter Fallback: Standard-Pfad VM_IMAGE_DIR="/var/lib/vz/images/${VM_ID_RESTORED}" echo " WARNUNG: Storage-Pfad nicht ermittelt, nutze Fallback: $VM_IMAGE_DIR" else echo " VM-Image-Dir: $VM_IMAGE_DIR" fi ACTUAL_DISK_BYTES=$(du -sb "$VM_IMAGE_DIR" 2>/dev/null | awk '{print $1}' || echo "0") echo " Image-Größe: $(( ACTUAL_DISK_BYTES / 1024 / 1024 / 1024 )) GB" # ═════════════════════════════════════════════════════════════════════════════ # [5/12] VM-IMAGES PRÜFEN # Wenn leer oder nicht vorhanden → failed, Webhook senden, nächstes Backup. # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [5/12] Prüfe VM-Images..." if [[ ! -d "$VM_IMAGE_DIR" ]] || [[ -z "$(ls -A "$VM_IMAGE_DIR" 2>/dev/null)" ]]; then ERROR_MSG="VM_IMAGE_DIR leer oder nicht vorhanden: $VM_IMAGE_DIR" echo " FEHLER: $ERROR_MSG" qm destroy "$VM_ID_RESTORED" \ --destroy-unreferenced-disks 1 --purge 1 2>/dev/null || true trap - ERR send_webhook "failed" "$ERROR_MSG" exit 0 fi echo " Images vorhanden ✓" # ═════════════════════════════════════════════════════════════════════════════ # [6/12] VM VORBEREITEN # unlock → stop → cdrom/ide0 entfernen → alle Netzwerkkarten (net0-net10) # löschen → Agent aktivieren. # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [6/12] VM vorbereiten..." qm unlock "$VM_ID_RESTORED" 2>/dev/null || true qm stop "$VM_ID_RESTORED" 2>/dev/null || true sleep 3 qm set "$VM_ID_RESTORED" -delete cdrom 2>/dev/null || true qm set "$VM_ID_RESTORED" -delete ide0 2>/dev/null || true for ((net=0; net<=10; net++)); do qm set "$VM_ID_RESTORED" -delete "net${net}" 2>/dev/null || true done qm set "$VM_ID_RESTORED" --agent "enabled=1,type=virtio" 2>/dev/null || true echo " VM vorbereitet (Netzwerkkarten entfernt, Agent aktiviert)." # ═════════════════════════════════════════════════════════════════════════════ # [7/12] VM STARTEN & QM-AGENT PRÜFEN # 120s in 10s-Schritten. Agent über QEMU Guest Agent Channel, kein Netzwerk. # Kein Agent = KEIN Abbruch, qm_agent_ok=false in DB. # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [7/12] Starte VM & warte auf qm-Agent (max. 120s)..." qm start "$VM_ID_RESTORED" 2>/dev/null || true AGENT_WAIT=0 AGENT_MAX=120 AGENT_INTERVAL=10 QM_AGENT_OK="false" while [[ $AGENT_WAIT -lt $AGENT_MAX ]]; do sleep $AGENT_INTERVAL AGENT_WAIT=$(( AGENT_WAIT + AGENT_INTERVAL )) echo -n " [${AGENT_WAIT}s/${AGENT_MAX}s] qm-Agent... " if qm agent "$VM_ID_RESTORED" ping 2>/dev/null | grep -qi "pong\|ping"; then QM_AGENT_OK="true" echo "ONLINE ✓" hostname_info=$(qm agent "$VM_ID_RESTORED" get-host-name 2>/dev/null \ | grep host-name | tr -d '"' || true) fs_info=$(qm agent "$VM_ID_RESTORED" get-fsinfo 2>/dev/null \ | grep mountpoint | tr -d '"' || true) echo " Hostname: ${hostname_info:-unbekannt}" echo " Mountpoints: ${fs_info:-unbekannt}" break else echo "nicht erreichbar." fi done if [[ "$QM_AGENT_OK" == "false" ]]; then echo " qm-Agent nach ${AGENT_MAX}s nicht erreichbar." echo " → qm_agent_ok=false in DB, Restore läuft weiter." fi # ═════════════════════════════════════════════════════════════════════════════ # [8/12] VM STOPPEN # Sauberes Shutdown, nach 30s force-stop. # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [8/12] Stoppe VM..." qm shutdown "$VM_ID_RESTORED" 2>/dev/null || true sleep 30 qm stop "$VM_ID_RESTORED" --skiplock 1 2>/dev/null || true echo " VM gestoppt." sleep 5 # ═════════════════════════════════════════════════════════════════════════════ # [9/12] 7Z-ARCHIV ERSTELLEN # VM-Images aus VM_IMAGE_DIR/* zippen. # VM-Name aus qemu-server.conf lesen → ZIP bekommt echten Namen. # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [9/12] Erstelle verschlüsseltes 7z-Archiv..." VM_CONF="${VM_IMAGE_DIR}/qemu-server.conf" VM_NAME=$(grep -m1 "^name:" "$VM_CONF" 2>/dev/null \ | awk -F': ' '{print $2}' | tr -d '[:space:]' \ || echo "$SAFE_CLIENT") echo " VM-Name: $VM_NAME" ZIP_FILE="${ZIP_DIR}/${VM_NAME}-${VM_ID_ORIGINAL}.7z" ZIP_START=$(date +%s) 7z a -t7z \ -mmt=4 \ -mx=1 \ -md=16M \ -p"${ZIP_PASSWORD}" \ -mhe=on \ "$ZIP_FILE" \ "${VM_IMAGE_DIR}/"* \ 2>&1 | tail -5 ZIP_DURATION=$(( $(date +%s) - ZIP_START )) ZIP_SIZE_BYTES=$(stat -c%s "$ZIP_FILE" 2>/dev/null || echo "0") echo " ZIP: $(( ZIP_SIZE_BYTES / 1024 / 1024 )) MB in ${ZIP_DURATION}s" # ═════════════════════════════════════════════════════════════════════════════ # [10/12] RSYNC ZUM BACKUP-SERVER # Zielverzeichnis aus DB (rsync_target) = Firmen-spezifischer Pfad. # 3 Versuche mit 60s Pause + Größenvergleich lokal vs. remote. # ═════════════════════════════════════════════════════════════════════════════ echo "" # Rsync-Ziel mit Datumsordner RSYNC_TARGET_DATE="${RSYNC_TARGET}/${LAST_DATE}" echo "==> [10/12] Rsync → ${BACKUP_SERVER_HOST}:${RSYNC_TARGET_DATE}..." MAX_RETRIES=3 rsync_transfer() { rsync -avz --progress --timeout=300 \ "$ZIP_FILE" \ "${BACKUP_SERVER_HOST}:${RSYNC_TARGET_DATE}/" \ 2>&1 } ssh "$BACKUP_SERVER_HOST" "mkdir -p '${RSYNC_TARGET_DATE}'" 2>/dev/null || true while [[ $RSYNC_RETRIES -lt $MAX_RETRIES ]]; do if rsync_transfer; then RSYNC_OK="true" RSYNC_SIZE_BYTES=$ZIP_SIZE_BYTES echo " Rsync OK: $(( RSYNC_SIZE_BYTES / 1024 / 1024 )) MB" break else RSYNC_RETRIES=$(( RSYNC_RETRIES + 1 )) if [[ $RSYNC_RETRIES -lt $MAX_RETRIES ]]; then echo " Fehlgeschlagen ($RSYNC_RETRIES/$MAX_RETRIES). Warte 60s..." sleep 60 else RSYNC_OK="false" STATUS="failed" ERROR_MSG="Rsync fehlgeschlagen nach ${RSYNC_RETRIES} Versuchen" echo " FEHLER: $ERROR_MSG" fi fi done if [[ "$RSYNC_OK" == "true" ]]; then REMOTE_SIZE=$(ssh "$BACKUP_SERVER_HOST" \ "stat -c%s '${RSYNC_TARGET_DATE}/$(basename "$ZIP_FILE")'" \ 2>/dev/null || echo "0") if [[ "$REMOTE_SIZE" != "$ZIP_SIZE_BYTES" ]]; then echo " WARNUNG: Remote ${REMOTE_SIZE}B != lokal ${ZIP_SIZE_BYTES}B" RSYNC_OK="false" STATUS="failed" ERROR_MSG="Größenabweichung: lokal=${ZIP_SIZE_BYTES} remote=${REMOTE_SIZE}" else echo " Größenprüfung OK: ${REMOTE_SIZE} Bytes." fi fi # ═════════════════════════════════════════════════════════════════════════════ # [11/12] AUFRÄUMEN # VM destroy inkl. Disks, lokale ZIP löschen. # Keys bleiben gecacht für nächste VM desselben Datastores. # ═════════════════════════════════════════════════════════════════════════════ echo "" echo "==> [11/12] Aufräumen..." qm destroy "$VM_ID_RESTORED" \ --destroy-unreferenced-disks 1 \ --purge 1 \ 2>/dev/null || echo " VM $VM_ID_RESTORED nicht mehr vorhanden." rm -f "$ZIP_FILE" echo " VM ${VM_ID_RESTORED} entfernt, ZIP gelöscht." echo " Keys gecacht in $KEY_DIR" # ── Zusammenfassung & Webhook ───────────────────────────────────────────────── TOTAL=$(( $(date +%s) - RESTORE_START )) echo "" echo "============================================================" echo " Status: $STATUS" echo " Gesamtdauer: ${TOTAL}s" echo " VM-Name: ${VM_NAME:-$SAFE_CLIENT}" echo " VM_IMAGE_DIR: $VM_IMAGE_DIR" echo " Datastore: $DATASTORE" echo " qm-Agent: $QM_AGENT_OK" echo " Rsync: $RSYNC_OK (Versuche: $RSYNC_RETRIES)" echo " ZIP: $(( ZIP_SIZE_BYTES / 1024 / 1024 )) MB" [[ -n "$ERROR_MSG" ]] && echo " Fehler: $ERROR_MSG" echo "============================================================" trap - ERR send_webhook "$STATUS" "$ERROR_MSG"