diff --git a/restore.sh b/restore.sh index 41fff9f..8c276fb 100644 --- a/restore.sh +++ b/restore.sh @@ -2,7 +2,7 @@ # ============================================================================= # /opt/windmill-restore/restore.sh # Windmill Backup Restore Worker -# Version: 1.0.8 +# Version: 1.0.9 # # WAS DIESES SCRIPT MACHT: # ───────────────────────────────────────────────────────────────────────────── @@ -10,8 +10,8 @@ # 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/4TB -# --restore-path PVE-Storage-Name für qmrestore z.B. local-lvm +# --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 # @@ -19,37 +19,38 @@ # 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" -# Beide werden lokal gecacht – bei mehreren VMs desselben Kunden -# nur einmal vom PBS-Server geholt. +# +# 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 vorbereiten – unlock, cdrom/ide0 entfernen, alle Netzwerkkarten +# [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 -# [5] VM starten & – 120s auf qm-Agent warten (10s Schritte) +# [7] VM starten & – 120s auf qm-Agent warten (10s Schritte) # Agent prüfen Kein Agent = qm_agent_ok=false, KEIN Abbruch -# [6] VM stoppen – Sauberes Shutdown, nach 30s force-stop -# [7] 7z-Archiv – VM-Images verschlüsselt zippen (7z-Passwort) -# [8] Rsync – ZIP zum Backup-Server (Firmen-Zielverzeichnis) +# [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 -# [9] Aufräumen – VM destroy, ZIP löschen -# [10] Webhook – JSON → Windmill schreibt DB & startet nächsten +# [11] Aufräumen – VM destroy, ZIP löschen +# [12] Webhook – JSON → Windmill schreibt DB & startet nächsten # ============================================================================= set -euo pipefail # ── Konfigdatei laden (PBS-Credentials) ────────────────────────────────────── -# pbs.conf wird von Windmill Step C per SFTP deployt (chmod 600) -# Enthält: PBS_HOST, PBS_PORT, PBS_USER, PBS_PASSWORD, PBS_FINGERPRINT 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 ─────────────────────────────────────────────────────────── -# Kein --encrypt-key – Keys werden per Rsync vom PBS-Server geholt JOB_UUID="" BACKUP_PATH="" CLIENT_NAME="" @@ -75,7 +76,7 @@ while [[ $# -gt 0 ]]; do esac done -# Pflichtparameter prüfen – kein Fallback +# 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; } @@ -95,9 +96,6 @@ exec > >(tee -a "$LOG_FILE") 2>&1 # ── Backup-Pfad zerlegen ────────────────────────────────────────────────────── # BACKUP_PATH Format: "tnp-Invest-GmbH:vm/100/2024-01-15T02:00:00Z" -# DATASTORE = "tnp-Invest-GmbH" -# SNAPSHOT_PATH = "vm/100/2024-01-15T02:00:00Z" -# PVE_BACKUP_REF = "pbs-tnp-invest-gmbh:backup/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}" @@ -113,6 +111,7 @@ 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 @@ -122,10 +121,9 @@ RSYNC_RETRIES=0 QM_AGENT_OK="false" RESTORE_DURATION=0 ZIP_PASSWORD="" -KEYFILE_LOCAL="" echo "============================================================" -echo " Windmill Restore Worker v1.0.8" +echo " Windmill Restore Worker v1.0.9" echo " Client: $CLIENT_NAME" echo " Datastore: $DATASTORE" echo " Backup: $BACKUP_PATH" @@ -184,21 +182,16 @@ trap 'STATUS="failed" send_webhook "failed" "Abgebrochen in Zeile $LINENO – $LOG_FILE"' ERR # ═════════════════════════════════════════════════════════════════════════════ -# [0/10] KEYS VOM PBS-SERVER HOLEN -# PBS_HOST kommt aus pbs.conf (bereits gesourct). -# Beide Dateien werden lokal gecacht – bei mehreren VMs desselben Datastores -# werden sie nur einmal geholt, nicht bei jeder VM erneut. +# [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 -# → wird an qmrestore --keyfile übergeben -# → entschlüsselt die PBS-VM-Images beim Restore -# -# 7z-Passwort: /root/Scripte/password_7z.txt -# → Format: "tnp-Invest-GmbH: Passwort123" -# → wird an 7z -p übergeben zum Verschlüsseln des ZIP-Archivs +# 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/10] Keys vom PBS-Server holen ($PBS_HOST)..." +echo "==> [0/12] Keys vom PBS-Server holen ($PBS_HOST)..." KEY_DIR="/opt/windmill-restore/keys" mkdir -p "$KEY_DIR" chmod 700 "$KEY_DIR" @@ -236,8 +229,6 @@ else echo " password_7z.txt bereits vorhanden." fi -# Passwort für diesen Datastore extrahieren -# Format der Datei: "tnp-Invest-GmbH: Passwort123" ZIP_PASSWORD=$(grep -m1 "^${DATASTORE}:" "$PW_FILE_LOCAL" \ | awk -F': ' '{print $2}' | tr -d '[:space:]') @@ -248,24 +239,24 @@ ZIP_PASSWORD=$(grep -m1 "^${DATASTORE}:" "$PW_FILE_LOCAL" \ echo " 7z-Passwort geladen ✓" # ═════════════════════════════════════════════════════════════════════════════ -# [1/10] SPACE-CHECK -# Nur Warnung – kein Abbruch. PBS-Restore bricht selbst ab bei vollem Disk. +# [1/12] SPACE-CHECK +# Nur Warnung – kein Abbruch. # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [1/10] Prüfe freien Speicherplatz auf $RESTORE_MOUNT..." +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 30 ]] && echo " WARNUNG: Weniger als 30 GB frei!" +[[ $FREE_GB -lt 50 ]] && echo " WARNUNG: Weniger als 50 GB frei!" # ═════════════════════════════════════════════════════════════════════════════ -# [2/10] VM-ID ERMITTELN +# [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/10] Ermittle VM-IDs..." +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" @@ -290,14 +281,13 @@ for i in range(1000, 2000): echo " Restore VM-ID: $VM_ID_RESTORED" # ═════════════════════════════════════════════════════════════════════════════ -# [3/10] QMRESTORE VOM PBS-STORAGE -# Restore direkt vom registrierten PBS-PVE-Storage. -# --storage aus DB (restore_path), z.B. "local-lvm", "local" +# [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/10] qmrestore vom PBS-Storage..." +echo "==> [3/12] qmrestore vom PBS-Storage..." echo " Backup-Ref: $PVE_BACKUP_REF" echo " Storage: $RESTORE_PATH" echo " Keyfile: $KEYFILE_LOCAL" @@ -314,18 +304,64 @@ qmrestore "$PVE_BACKUP_REF" "$VM_ID_RESTORED" \ RESTORE_DURATION=$(( $(date +%s) - RESTORE_START_INNER )) echo " Restore abgeschlossen in ${RESTORE_DURATION}s" -VM_IMAGE_DIR="/var/lib/vz/images/${VM_ID_RESTORED}" +# ═════════════════════════════════════════════════════════════════════════════ +# [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" # ═════════════════════════════════════════════════════════════════════════════ -# [4/10] VM VORBEREITEN -# unlock → stop → cdrom/ide0 entfernen → alle Netzwerkkarten (net0-net10) -# löschen → Agent aktivieren. -# Kein Netzwerk = kein versehentlicher Zugang zum Produktivnetz beim Test. +# [5/12] VM-IMAGES PRÜFEN +# Wenn leer oder nicht vorhanden → failed, Webhook senden, nächstes Backup. # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [4/10] VM vorbereiten..." +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 @@ -341,13 +377,12 @@ qm set "$VM_ID_RESTORED" --agent "enabled=1,type=virtio" 2>/dev/null || true echo " VM vorbereitet (Netzwerkkarten entfernt, Agent aktiviert)." # ═════════════════════════════════════════════════════════════════════════════ -# [5/10] VM STARTEN & QM-AGENT PRÜFEN -# 120s in 10s-Schritten. Agent läuft über QEMU Guest Agent Channel – -# kein Netzwerk nötig. Kein Agent nach 120s = KEIN Abbruch, -# qm_agent_ok=false wird im Webhook an Windmill gemeldet → DB-Eintrag. +# [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 "==> [5/10] Starte VM & warte auf qm-Agent (max. 120s)..." +echo "==> [7/12] Starte VM & warte auf qm-Agent (max. 120s)..." qm start "$VM_ID_RESTORED" 2>/dev/null || true AGENT_WAIT=0 @@ -380,11 +415,11 @@ if [[ "$QM_AGENT_OK" == "false" ]]; then fi # ═════════════════════════════════════════════════════════════════════════════ -# [6/10] VM STOPPEN -# Sauberes Shutdown, nach 30s force-stop falls VM noch läuft. +# [8/12] VM STOPPEN +# Sauberes Shutdown, nach 30s force-stop. # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [6/10] Stoppe VM..." +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 @@ -392,15 +427,12 @@ echo " VM gestoppt." sleep 5 # ═════════════════════════════════════════════════════════════════════════════ -# [7/10] 7Z-ARCHIV ERSTELLEN -# VM-Images aus /var/lib/vz/images/$VM_ID_RESTORED/* zippen. +# [9/12] 7Z-ARCHIV ERSTELLEN +# VM-Images aus VM_IMAGE_DIR/* zippen. # VM-Name aus qemu-server.conf lesen → ZIP bekommt echten Namen. -# Passwort kommt aus password_7z.txt (Schritt [0], pro Datastore). -# -mx=1 → schnellste Kompression (Images meist schon komprimiert) -# -mhe=on → Header-Verschlüsselung (Dateinamen ohne Key unsichtbar) # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [7/10] Erstelle verschlüsseltes 7z-Archiv..." +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 \ @@ -408,7 +440,7 @@ VM_NAME=$(grep -m1 "^name:" "$VM_CONF" 2>/dev/null \ || echo "$SAFE_CLIENT") echo " VM-Name: $VM_NAME" -ZIP_FILE="${ZIP_DIR}/${VM_NAME}_$(date +%Y%m%d).7z" +ZIP_FILE="${ZIP_DIR}/${VM_ID_ORIGINAL}_${VM_NAME}_$(date +%Y%m%d).7z" ZIP_START=$(date +%s) 7z a -t7z \ @@ -426,12 +458,12 @@ 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" # ═════════════════════════════════════════════════════════════════════════════ -# [8/10] RSYNC ZUM BACKUP-SERVER +# [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 "" -echo "==> [8/10] Rsync → ${BACKUP_SERVER_HOST}:${RSYNC_TARGET}..." +echo "==> [10/12] Rsync → ${BACKUP_SERVER_HOST}:${RSYNC_TARGET}..." MAX_RETRIES=3 rsync_transfer() { @@ -478,20 +510,19 @@ if [[ "$RSYNC_OK" == "true" ]]; then fi # ═════════════════════════════════════════════════════════════════════════════ -# [9/10] AUFRÄUMEN -# VM destroy inkl. Disks (/var/lib/vz/images/$VM_ID wird durch destroy entfernt). -# Lokale ZIP löschen. -# Keys bleiben gecacht für nächste VM desselben Datastores/Kunden. +# [11/12] AUFRÄUMEN +# VM destroy inkl. Disks, lokale ZIP löschen. +# Keys bleiben gecacht für nächste VM desselben Datastores. # ═════════════════════════════════════════════════════════════════════════════ echo "" -echo "==> [9/10] Aufräumen..." +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 (für nächste VM desselben Kunden)." +echo " Keys gecacht in $KEY_DIR" # ── Zusammenfassung & Webhook ───────────────────────────────────────────────── TOTAL=$(( $(date +%s) - RESTORE_START )) @@ -500,6 +531,7 @@ 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)"