From 4abe0943587240e3dc06c9d961415307ae6c4adc Mon Sep 17 00:00:00 2001 From: "sebastian.serfling" Date: Tue, 17 Mar 2026 07:59:41 +0000 Subject: [PATCH] restore.sh aktualisiert --- restore.sh | 457 ++++++++++++++++++++++++++++------------------------- 1 file changed, 241 insertions(+), 216 deletions(-) diff --git a/restore.sh b/restore.sh index 6019b83..6139663 100644 --- a/restore.sh +++ b/restore.sh @@ -2,53 +2,34 @@ # ============================================================================= # /opt/windmill-restore/restore.sh # Windmill Backup Restore Worker -# Version: 1.0.10 +# Version: 1.0.11 # -# 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} +# Unterstützt sowohl VM (qm) als auch CT (pct) Backups. +# Backup-Typ wird automatisch aus dem Backup-Pfad erkannt (vm/ oder ct/). # # 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 +# [2] ID ermitteln – Original aus Backup-Pfad, Restore-ID ab 1000 +# [3] Restore – qmrestore (VM) oder pct restore (CT) +# [4] 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 +# [6] Vorbereiten – VM: unlock/cdrom/net entfernen/Agent +# CT: unlock/net entfernen +# [7] Starten & prüfen – VM: qm-Agent 120s | CT: pct exec ping +# [8] Stoppen – VM: qm shutdown | CT: pct stop +# [9] Config sichern – Originale Config ins ZIP-Verzeichnis +# [10] 7z-Archiv – Images verschlüsselt zippen +# [11] Rsync – ZIP zum Backup-Server +# [12] Aufräumen – destroy, ZIP löschen +# [13] Webhook – JSON → Windmill # ============================================================================= set -euo pipefail -# ── Konfigdatei laden (PBS-Credentials) ────────────────────────────────────── +# ── Konfigdatei laden ───────────────────────────────────────────────────────── 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="" @@ -76,31 +57,31 @@ while [[ $# -gt 0 ]]; do 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 + 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" +SAFE_CLIENT="${CLIENT_NAME//\//_}" 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" +# Format: "tnp-Invest-GmbH:vm/100/2024-01-15T02:00:00Z" +# oder "Jaehler-GmbH:ct/105/2024-01-15T06:00:00Z" DATASTORE=$(echo "$BACKUP_PATH" | cut -d: -f1) SNAPSHOT_PATH=$(echo "$BACKUP_PATH" | cut -d: -f2-) +BACKUP_TYPE=$(echo "$SNAPSHOT_PATH" | cut -d/ -f1) # "vm" oder "ct" PVE_BACKUP_REF="${PBS_STORAGE}:backup/${SNAPSHOT_PATH}" -# ── Pfade & Messvariablen ───────────────────────────────────────────────────── +# ── 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 \ @@ -112,7 +93,7 @@ ERROR_MSG="" VM_ID_ORIGINAL=0 VM_ID_RESTORED=0 VM_NAME="" -VM_IMAGE_DIR="" +IMAGE_DIR="" ACTUAL_DISK_BYTES=0 ZIP_SIZE_BYTES=0 ZIP_DURATION=0 @@ -120,12 +101,13 @@ RSYNC_SIZE_BYTES=0 RSYNC_OK="true" RSYNC_RETRIES=0 QM_AGENT_OK="false" -RESTORE_DURATION=0 +ZIP_FILE="" ZIP_PASSWORD="" echo "============================================================" -echo " Windmill Restore Worker v1.0.9" +echo " Windmill Restore Worker v1.0.11" echo " Client: $CLIENT_NAME" +echo " Typ: $BACKUP_TYPE" echo " Datastore: $DATASTORE" echo " Backup: $BACKUP_PATH" echo " PBS-Storage: $PBS_STORAGE" @@ -179,31 +161,40 @@ send_webhook() { || echo " WARNUNG: HTTP $http_code" } +# ── ERR-Trap: aufräumen und Webhook senden ──────────────────────────────────── trap 'STATUS="failed" - send_webhook "failed" "Abgebrochen in Zeile $LINENO – $LOG_FILE"' ERR + ERROR_LINE=$LINENO + echo "" + echo "FEHLER in Zeile ${ERROR_LINE} – räume auf..." + if [[ ${VM_ID_RESTORED:-0} -gt 0 ]]; then + if [[ "$BACKUP_TYPE" == "ct" ]]; then + pct stop "$VM_ID_RESTORED" 2>/dev/null || true + sleep 3 + pct destroy "$VM_ID_RESTORED" --purge 1 2>/dev/null || true + else + qm stop "$VM_ID_RESTORED" --skiplock 1 2>/dev/null || true + sleep 5 + qm destroy "$VM_ID_RESTORED" \ + --destroy-unreferenced-disks 1 --purge 1 2>/dev/null || true + fi + echo " ${BACKUP_TYPE^^} ${VM_ID_RESTORED} entfernt." + fi + [[ -n "${ZIP_FILE:-}" && -f "$ZIP_FILE" ]] && rm -f "$ZIP_FILE" + [[ -n "${IMAGE_DIR:-}" && -d "$IMAGE_DIR" ]] && rm -rf "$IMAGE_DIR" + send_webhook "failed" "Abgebrochen in Zeile ${ERROR_LINE} – $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). +# [0/13] 7Z-PASSWORT VOM PBS-SERVER HOLEN # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [0/12] 7z-Passwort vom PBS-Server holen ($PBS_HOST)..." +echo "==> [0/13] 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" + echo " Hole password_7z.txt..." rsync -az \ -e "ssh -o StrictHostKeyChecking=no" \ "root@${PBS_HOST}:/root/Scripte/password_7z.txt" \ @@ -219,17 +210,15 @@ 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 "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. +# [1/13] SPACE-CHECK # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [1/12] Prüfe freien Speicherplatz auf $RESTORE_MOUNT..." +echo "==> [1/13] 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 )) @@ -237,14 +226,12 @@ 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 +# [2/13] ID ERMITTELN # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [2/12] Ermittle VM-IDs..." +echo "==> [2/13] Ermittle IDs..." VM_ID_ORIGINAL=$(echo "$SNAPSHOT_PATH" | grep -oP '\d+' | head -1 || echo "0") -echo " Original VM-ID: $VM_ID_ORIGINAL" +echo " Original-ID: $VM_ID_ORIGINAL (Typ: $BACKUP_TYPE)" VM_ID_RESTORED=$( { @@ -264,38 +251,44 @@ for i in range(1000, 2000): print(i); break " 2>/dev/null || echo "1000" ) -echo " Restore VM-ID: $VM_ID_RESTORED" +echo " Restore-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 +# [3/13] RESTORE +# VM → qmrestore +# CT → pct restore # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [3/12] qmrestore vom PBS-Storage..." +echo "==> [3/13] Restore vom PBS-Storage ($BACKUP_TYPE)..." echo " Backup-Ref: $PVE_BACKUP_REF" echo " Storage: $RESTORE_PATH" -echo " VM-ID: $VM_ID_RESTORED" +echo " ID: $VM_ID_RESTORED" RESTORE_START_INNER=$(date +%s) -qmrestore "$PVE_BACKUP_REF" "$VM_ID_RESTORED" \ - --storage "$RESTORE_PATH" \ - --unique 1 \ - 2>&1 +if [[ "$BACKUP_TYPE" == "ct" ]]; then + pct restore "$VM_ID_RESTORED" "$PVE_BACKUP_REF" \ + --storage "$RESTORE_PATH" \ + --unique 1 \ + 2>&1 +else + qmrestore "$PVE_BACKUP_REF" "$VM_ID_RESTORED" \ + --storage "$RESTORE_PATH" \ + --unique 1 \ + 2>&1 +fi 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} +# [4/13] IMAGE_DIR DYNAMISCH ERMITTELN +# VM → /mnt/HDD_5TB.1/images/${VM_ID_RESTORED} +# CT → /mnt/HDD_5TB.1/private/${VM_ID_RESTORED} +# oder /mnt/HDD_5TB.1/rootdir/${VM_ID_RESTORED} # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [4/12] Ermittle VM-Image-Verzeichnis..." +echo "==> [4/13] Ermittle Image-Verzeichnis..." STORAGE_BASE=$(pvesh get "/storage/${RESTORE_PATH}" --output-format json \ 2>/dev/null | python3 -c " import json, sys @@ -304,146 +297,179 @@ print(cfg.get('path', '')) " 2>/dev/null || echo "") if [[ -n "$STORAGE_BASE" ]]; then - VM_IMAGE_DIR="${STORAGE_BASE}/images/${VM_ID_RESTORED}" + if [[ "$BACKUP_TYPE" == "ct" ]]; then + # CT-Images liegen unter private/ oder rootdir/ + if [[ -d "${STORAGE_BASE}/private/${VM_ID_RESTORED}" ]]; then + IMAGE_DIR="${STORAGE_BASE}/private/${VM_ID_RESTORED}" + else + IMAGE_DIR="${STORAGE_BASE}/rootdir/${VM_ID_RESTORED}" + fi + else + IMAGE_DIR="${STORAGE_BASE}/images/${VM_ID_RESTORED}" + fi 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 "") + # Fallback + if [[ "$BACKUP_TYPE" == "ct" ]]; then + IMAGE_DIR="/var/lib/vz/private/${VM_ID_RESTORED}" + else + IMAGE_DIR="/var/lib/vz/images/${VM_ID_RESTORED}" + fi + echo " WARNUNG: Storage-Pfad nicht ermittelt, nutze Fallback: $IMAGE_DIR" fi +echo " Image-Dir: $IMAGE_DIR" -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") +ACTUAL_DISK_BYTES=$(du -sb "$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. +# [5/13] IMAGES PRÜFEN # ═════════════════════════════════════════════════════════════════════════════ 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 "==> [5/13] Prüfe Images..." +if [[ ! -d "$IMAGE_DIR" ]] || [[ -z "$(ls -A "$IMAGE_DIR" 2>/dev/null)" ]]; then + ERROR_MSG="IMAGE_DIR leer oder nicht vorhanden: $IMAGE_DIR" echo " FEHLER: $ERROR_MSG" - qm destroy "$VM_ID_RESTORED" \ - --destroy-unreferenced-disks 1 --purge 1 2>/dev/null || true + if [[ "$BACKUP_TYPE" == "ct" ]]; then + pct destroy "$VM_ID_RESTORED" --purge 1 2>/dev/null || true + else + qm destroy "$VM_ID_RESTORED" \ + --destroy-unreferenced-disks 1 --purge 1 2>/dev/null || true + fi trap - ERR send_webhook "failed" "$ERROR_MSG" exit 0 fi echo " Images vorhanden ✓" -# ── Originale Config sichern BEVOR wir Netzwerkkarten etc. entfernen ───────── -# Die originale Config (mit Netzwerkkarten, original settings) wird gesichert -# und später ins ZIP gepackt damit die VM vollständig wiederhergestellt werden kann. -PVE_CONF="/etc/pve/qemu-server/${VM_ID_RESTORED}.conf" -ORIG_CONF_BACKUP="${VM_IMAGE_DIR}/qemu-server.original.conf" -if [[ -f "$PVE_CONF" ]]; then - cp "$PVE_CONF" "$ORIG_CONF_BACKUP" - echo " Originale Config gesichert: $ORIG_CONF_BACKUP" +# ═════════════════════════════════════════════════════════════════════════════ +# [6/13] VORBEREITEN +# VM: unlock → stop → cdrom/ide0/net entfernen → Agent aktivieren +# CT: unlock → stop → net entfernen +# ═════════════════════════════════════════════════════════════════════════════ +echo "" +echo "==> [6/13] Vorbereiten ($BACKUP_TYPE)..." + +if [[ "$BACKUP_TYPE" == "ct" ]]; then + pct unlock "$VM_ID_RESTORED" 2>/dev/null || true + pct stop "$VM_ID_RESTORED" 2>/dev/null || true + sleep 3 + # Alle Netzwerkinterfaces entfernen + for ((net=0; net<=10; net++)); do + pct set "$VM_ID_RESTORED" --delete "net${net}" 2>/dev/null || true + done + echo " CT vorbereitet (Netzwerkkarten entfernt)." else - echo " WARNUNG: PVE-Config nicht gefunden: $PVE_CONF" + 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)." fi # ═════════════════════════════════════════════════════════════════════════════ -# [6/12] VM VORBEREITEN -# unlock → stop → cdrom/ide0 entfernen → alle Netzwerkkarten (net0-net10) -# löschen → Agent aktivieren. +# [7/13] STARTEN & PRÜFEN +# VM: qm-Agent 120s warten +# CT: pct exec ping (vereinfacht) # ═════════════════════════════════════════════════════════════════════════════ 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 +echo "==> [7/13] Starte & prüfe ($BACKUP_TYPE)..." -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 +if [[ "$BACKUP_TYPE" == "ct" ]]; then + pct start "$VM_ID_RESTORED" 2>/dev/null || true + sleep 10 + # CT: prüfen ob gestartet + if pct status "$VM_ID_RESTORED" 2>/dev/null | grep -q "running"; 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 + echo " CT läuft ✓" + # Hostname aus CT holen + CT_HOSTNAME=$(pct exec "$VM_ID_RESTORED" -- hostname 2>/dev/null || echo "unbekannt") + echo " Hostname: $CT_HOSTNAME" else - echo "nicht erreichbar." + QM_AGENT_OK="false" + echo " CT nicht gestartet – läuft weiter." 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..." - -# Originale Config (mit Netzwerkkarten) als qemu-server.conf ins ZIP-Verzeichnis -# legen – nicht die angepasste Config (ohne Netzwerkkarten) vom Restore-Test. -if [[ -f "$ORIG_CONF_BACKUP" ]]; then - cp "$ORIG_CONF_BACKUP" "${VM_IMAGE_DIR}/qemu-server.conf" - echo " Originale Config für ZIP wiederhergestellt ✓" else - echo " WARNUNG: Keine originale Config vorhanden – angepasste Config wird gezippt." + qm start "$VM_ID_RESTORED" 2>/dev/null || true + AGENT_WAIT=0 + AGENT_MAX=120 + AGENT_INTERVAL=10 + 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) + echo " Hostname: ${hostname_info:-unbekannt}" + break + else + echo "nicht erreichbar." + fi + done + [[ "$QM_AGENT_OK" == "false" ]] && \ + echo " qm-Agent nicht erreichbar – qm_agent_ok=false in DB." fi -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" +# ═════════════════════════════════════════════════════════════════════════════ +# [8/13] STOPPEN +# ═════════════════════════════════════════════════════════════════════════════ +echo "" +echo "==> [8/13] Stoppe $BACKUP_TYPE..." +if [[ "$BACKUP_TYPE" == "ct" ]]; then + pct stop "$VM_ID_RESTORED" 2>/dev/null || true + sleep 10 +else + qm shutdown "$VM_ID_RESTORED" 2>/dev/null || true + sleep 30 + qm stop "$VM_ID_RESTORED" --skiplock 1 2>/dev/null || true + sleep 5 +fi +echo " Gestoppt." + +# ═════════════════════════════════════════════════════════════════════════════ +# [9/13] CONFIG SICHERN +# VM: qemu-server.conf aus /etc/pve/qemu-server/ +# CT: lxc.conf aus /etc/pve/lxc/ +# Originale Config (mit Netzwerk) ins ZIP-Verzeichnis legen. +# ═════════════════════════════════════════════════════════════════════════════ +echo "" +echo "==> [9/13] Config sichern..." + +if [[ "$BACKUP_TYPE" == "ct" ]]; then + PVE_CONF="/etc/pve/lxc/${VM_ID_RESTORED}.conf" + CONF_FILENAME="lxc.conf" + # CT-Name aus Config lesen + VM_NAME=$(grep -m1 "^hostname:" "$PVE_CONF" 2>/dev/null \ + | awk -F': ' '{print $2}' | tr -d '[:space:]' \ + || echo "$SAFE_CLIENT") +else + PVE_CONF="/etc/pve/qemu-server/${VM_ID_RESTORED}.conf" + CONF_FILENAME="qemu-server.conf" + VM_NAME=$(grep -m1 "^name:" "$PVE_CONF" 2>/dev/null \ + | awk -F': ' '{print $2}' | tr -d '[:space:]' \ + || echo "$SAFE_CLIENT") +fi + +if [[ -f "$PVE_CONF" ]]; then + cp "$PVE_CONF" "${IMAGE_DIR}/${CONF_FILENAME}" + echo " Config gesichert: ${IMAGE_DIR}/${CONF_FILENAME}" +else + echo " WARNUNG: Config nicht gefunden: $PVE_CONF" +fi +echo " Name: $VM_NAME" + +# ═════════════════════════════════════════════════════════════════════════════ +# [10/13] 7Z-ARCHIV +# ═════════════════════════════════════════════════════════════════════════════ +echo "" +echo "==> [10/13] Erstelle verschlüsseltes 7z-Archiv..." ZIP_FILE="${ZIP_DIR}/${VM_NAME}-${VM_ID_ORIGINAL}.7z" ZIP_START=$(date +%s) @@ -455,7 +481,7 @@ ZIP_START=$(date +%s) -p"${ZIP_PASSWORD}" \ -mhe=on \ "$ZIP_FILE" \ - "${VM_IMAGE_DIR}/"* \ + "${IMAGE_DIR}/"* \ 2>&1 | tail -5 ZIP_DURATION=$(( $(date +%s) - ZIP_START )) @@ -463,14 +489,11 @@ 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. +# [11/13] RSYNC ZUM BACKUP-SERVER # ═════════════════════════════════════════════════════════════════════════════ echo "" -# Rsync-Ziel mit Datumsordner RSYNC_TARGET_DATE="${RSYNC_TARGET}/${LAST_DATE}" -echo "==> [10/12] Rsync → ${BACKUP_SERVER_HOST}:${RSYNC_TARGET_DATE}..." +echo "==> [11/13] Rsync → ${BACKUP_SERVER_HOST}:${RSYNC_TARGET_DATE}..." MAX_RETRIES=3 rsync_transfer() { @@ -517,30 +540,32 @@ if [[ "$RSYNC_OK" == "true" ]]; then fi # ═════════════════════════════════════════════════════════════════════════════ -# [11/12] AUFRÄUMEN -# VM destroy inkl. Disks, lokale ZIP löschen. -# Keys bleiben gecacht für nächste VM desselben Datastores. +# [12/13] AUFRÄUMEN # ═════════════════════════════════════════════════════════════════════════════ 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." +echo "==> [12/13] Aufräumen..." +if [[ "$BACKUP_TYPE" == "ct" ]]; then + pct destroy "$VM_ID_RESTORED" --purge 1 \ + 2>/dev/null || echo " CT $VM_ID_RESTORED nicht mehr vorhanden." +else + qm destroy "$VM_ID_RESTORED" \ + --destroy-unreferenced-disks 1 \ + --purge 1 \ + 2>/dev/null || echo " VM $VM_ID_RESTORED nicht mehr vorhanden." +fi rm -f "$ZIP_FILE" -echo " VM ${VM_ID_RESTORED} entfernt, ZIP gelöscht." -echo " Keys gecacht in $KEY_DIR" +echo " ${BACKUP_TYPE^^} ${VM_ID_RESTORED} entfernt, ZIP gelöscht." # ── Zusammenfassung & Webhook ───────────────────────────────────────────────── TOTAL=$(( $(date +%s) - RESTORE_START )) echo "" echo "============================================================" echo " Status: $STATUS" +echo " Typ: $BACKUP_TYPE" 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 " Name: ${VM_NAME:-$SAFE_CLIENT}" +echo " Image-Dir: $IMAGE_DIR" +echo " qm-Agent/CT: $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"