BackupScript/restore.sh

528 lines
24 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env bash
# =============================================================================
# /opt/windmill-restore/restore.sh
# Windmill Backup Restore Worker
# Version: 1.0.10
#
# 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
#
# 7z-Passwort wird per Rsync vom PBS-Server geholt (PBS_HOST aus pbs.conf):
# 7z-Passwort: /root/Scripte/password_7z.txt → für 7z-Archiv
# Format: "tnp-Invest-GmbH: Passwort123"
# PBS Encrypt-Key entfällt Entschlüsselung läuft über pvesm (Fingerprint in DB)
#
# 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] 7z-Passwort holen password_7z.txt 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 (kein --keyfile nötig)
# [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] 7Z-PASSWORT VOM PBS-SERVER HOLEN
# PBS_HOST kommt aus pbs.conf. Datei wird lokal gecacht.
# Bei mehreren VMs desselben Datastores nur einmal geholt.
#
# 7z-Passwort: /root/Scripte/password_7z.txt → 7z -p
# Format: "tnp-Invest-GmbH: Passwort123"
#
# PBS Encrypt-Key entfällt Entschlüsselung läuft über den registrierten
# pvesm PBS-Storage (Fingerprint ist in bronze.backup.datastore.config hinterlegt
# und wurde von Windmill Step C beim pvesm add pbs eingetragen).
# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo "==> [0/12] 7z-Passwort vom PBS-Server holen ($PBS_HOST)..."
KEY_DIR="/opt/windmill-restore/keys"
mkdir -p "$KEY_DIR"
chmod 700 "$KEY_DIR"
# ── 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"
# --unique 1 verhindert Konflikte mit bestehenden VM-Configs
# Kein --keyfile nötig Entschlüsselung über registrierten pvesm PBS-Storage
# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo "==> [3/12] qmrestore vom PBS-Storage..."
echo " Backup-Ref: $PVE_BACKUP_REF"
echo " Storage: $RESTORE_PATH"
echo " VM-ID: $VM_ID_RESTORED"
RESTORE_START_INNER=$(date +%s)
qmrestore "$PVE_BACKUP_REF" "$VM_ID_RESTORED" \
--storage "$RESTORE_PATH" \
--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"