restore.sh aktualisiert

main
sebastian.serfling 2026-03-17 07:59:41 +00:00
parent 5a7e603320
commit 4abe094358
1 changed files with 241 additions and 216 deletions

View File

@ -2,53 +2,34 @@
# ============================================================================= # =============================================================================
# /opt/windmill-restore/restore.sh # /opt/windmill-restore/restore.sh
# Windmill Backup Restore Worker # Windmill Backup Restore Worker
# Version: 1.0.10 # Version: 1.0.11
# #
# WAS DIESES SCRIPT MACHT: # Unterstützt sowohl VM (qm) als auch CT (pct) Backups.
# ───────────────────────────────────────────────────────────────────────────── # Backup-Typ wird automatisch aus dem Backup-Pfad erkannt (vm/ oder ct/).
# 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: # ABLAUF:
# [0] 7z-Passwort holen password_7z.txt per Rsync vom PBS-Server # [0] 7z-Passwort holen password_7z.txt per Rsync vom PBS-Server
# [1] Space-Check Freier Platz auf restore-mount prüfen # [1] Space-Check Freier Platz auf restore-mount prüfen
# [2] VM-ID ermitteln Original aus Backup-Pfad, Restore-ID ab 1000 # [2] ID ermitteln Original aus Backup-Pfad, Restore-ID ab 1000
# [3] qmrestore Direkt vom PBS-Storage (kein --keyfile nötig) # [3] Restore qmrestore (VM) oder pct restore (CT)
# [4] VM_IMAGE_DIR Dynamisch aus PVE-Storage-Pfad ermitteln # [4] IMAGE_DIR Dynamisch aus PVE-Storage-Pfad ermitteln
# [5] Images prüfen Abbruch wenn leer/nicht vorhanden # [5] Images prüfen Abbruch wenn leer/nicht vorhanden
# [6] VM vorbereiten unlock, cdrom/ide0 entfernen, alle Netzwerkkarten # [6] Vorbereiten VM: unlock/cdrom/net entfernen/Agent
# löschen, Agent aktivieren # CT: unlock/net entfernen
# [7] VM starten & 120s auf qm-Agent warten (10s Schritte) # [7] Starten & prüfen VM: qm-Agent 120s | CT: pct exec ping
# Agent prüfen Kein Agent = qm_agent_ok=false, KEIN Abbruch # [8] Stoppen VM: qm shutdown | CT: pct stop
# [8] VM stoppen Sauberes Shutdown, nach 30s force-stop # [9] Config sichern Originale Config ins ZIP-Verzeichnis
# [9] 7z-Archiv VM-Images verschlüsselt zippen (7z-Passwort) # [10] 7z-Archiv Images verschlüsselt zippen
# [10] Rsync ZIP zum Backup-Server (Firmen-Zielverzeichnis) # [11] Rsync ZIP zum Backup-Server
# 3 Versuche + Größenvergleich # [12] Aufräumen destroy, ZIP löschen
# [11] Aufräumen VM destroy, ZIP löschen # [13] Webhook JSON → Windmill
# [12] Webhook JSON → Windmill schreibt DB & startet nächsten
# ============================================================================= # =============================================================================
set -euo pipefail set -euo pipefail
# ── Konfigdatei laden (PBS-Credentials) ────────────────────────────────────── # ── Konfigdatei laden ─────────────────────────────────────────────────────────
CONF_FILE="/opt/windmill-restore/pbs.conf" CONF_FILE="/opt/windmill-restore/pbs.conf"
[[ ! -f "$CONF_FILE" ]] && { echo "FEHLER: $CONF_FILE fehlt!" >&2; exit 1; } [[ ! -f "$CONF_FILE" ]] && { echo "FEHLER: $CONF_FILE fehlt!" >&2; exit 1; }
# shellcheck source=/dev/null
source "$CONF_FILE" source "$CONF_FILE"
# Enthält: PBS_HOST, PBS_PORT, PBS_USER, PBS_PASSWORD, PBS_FINGERPRINT
# ── Argument-Parser ─────────────────────────────────────────────────────────── # ── Argument-Parser ───────────────────────────────────────────────────────────
JOB_UUID="" JOB_UUID=""
@ -76,31 +57,31 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
# Pflichtparameter prüfen
for var in JOB_UUID BACKUP_PATH CLIENT_NAME \ for var in JOB_UUID BACKUP_PATH CLIENT_NAME \
RESTORE_MOUNT RESTORE_PATH RSYNC_TARGET PBS_STORAGE WEBHOOK_URL; do RESTORE_MOUNT RESTORE_PATH RSYNC_TARGET PBS_STORAGE WEBHOOK_URL; do
[[ -z "${!var}" ]] && { echo "FEHLER: --${var//_/-} fehlt" >&2; exit 1; } [[ -z "${!var}" ]] && { echo "FEHLER: --${var//_/-} fehlt" >&2; exit 1; }
done done
[[ ! -d "$RESTORE_MOUNT" ]] && { [[ ! -d "$RESTORE_MOUNT" ]] && {
echo "FEHLER: Restore-Mount '$RESTORE_MOUNT' existiert nicht!" >&2 echo "FEHLER: Restore-Mount '$RESTORE_MOUNT' existiert nicht!" >&2; exit 1
exit 1
} }
# ── Logging ─────────────────────────────────────────────────────────────────── # ── Logging ───────────────────────────────────────────────────────────────────
LOG_DIR="/opt/windmill-restore/logs" LOG_DIR="/opt/windmill-restore/logs"
mkdir -p "$LOG_DIR" 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" LOG_FILE="$LOG_DIR/${SAFE_CLIENT}_$(date +%Y%m%d_%H%M%S).log"
exec > >(tee -a "$LOG_FILE") 2>&1 exec > >(tee -a "$LOG_FILE") 2>&1
# ── Backup-Pfad zerlegen ────────────────────────────────────────────────────── # ── 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) DATASTORE=$(echo "$BACKUP_PATH" | cut -d: -f1)
SNAPSHOT_PATH=$(echo "$BACKUP_PATH" | cut -d: -f2-) 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}" PVE_BACKUP_REF="${PBS_STORAGE}:backup/${SNAPSHOT_PATH}"
# ── Pfade & Messvariablen ───────────────────────────────────────────────────── # ── Messvariablen ─────────────────────────────────────────────────────────────
LAST_DATE=$(date +"%Y-%m-%d" -d "1 day ago") LAST_DATE=$(date +"%Y-%m-%d" -d "1 day ago")
ZIP_DIR="${RESTORE_MOUNT}/zips/${LAST_DATE}" ZIP_DIR="${RESTORE_MOUNT}/zips/${LAST_DATE}"
BACKUP_SERVER_HOST=$(cat /opt/windmill-restore/backup_server_host 2>/dev/null \ 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_ORIGINAL=0
VM_ID_RESTORED=0 VM_ID_RESTORED=0
VM_NAME="" VM_NAME=""
VM_IMAGE_DIR="" IMAGE_DIR=""
ACTUAL_DISK_BYTES=0 ACTUAL_DISK_BYTES=0
ZIP_SIZE_BYTES=0 ZIP_SIZE_BYTES=0
ZIP_DURATION=0 ZIP_DURATION=0
@ -120,12 +101,13 @@ RSYNC_SIZE_BYTES=0
RSYNC_OK="true" RSYNC_OK="true"
RSYNC_RETRIES=0 RSYNC_RETRIES=0
QM_AGENT_OK="false" QM_AGENT_OK="false"
RESTORE_DURATION=0 ZIP_FILE=""
ZIP_PASSWORD="" ZIP_PASSWORD=""
echo "============================================================" echo "============================================================"
echo " Windmill Restore Worker v1.0.9" echo " Windmill Restore Worker v1.0.11"
echo " Client: $CLIENT_NAME" echo " Client: $CLIENT_NAME"
echo " Typ: $BACKUP_TYPE"
echo " Datastore: $DATASTORE" echo " Datastore: $DATASTORE"
echo " Backup: $BACKUP_PATH" echo " Backup: $BACKUP_PATH"
echo " PBS-Storage: $PBS_STORAGE" echo " PBS-Storage: $PBS_STORAGE"
@ -179,31 +161,40 @@ send_webhook() {
|| echo " WARNUNG: HTTP $http_code" || echo " WARNUNG: HTTP $http_code"
} }
# ── ERR-Trap: aufräumen und Webhook senden ────────────────────────────────────
trap 'STATUS="failed" 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 # [0/13] 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 ""
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" KEY_DIR="/opt/windmill-restore/keys"
mkdir -p "$KEY_DIR" mkdir -p "$KEY_DIR"
chmod 700 "$KEY_DIR" chmod 700 "$KEY_DIR"
# ── 7z-Passwort aus password_7z.txt ──────────────────────────────────────────
PW_FILE_LOCAL="${KEY_DIR}/password_7z.txt" PW_FILE_LOCAL="${KEY_DIR}/password_7z.txt"
if [[ ! -f "$PW_FILE_LOCAL" || ! -s "$PW_FILE_LOCAL" ]]; then 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 \ rsync -az \
-e "ssh -o StrictHostKeyChecking=no" \ -e "ssh -o StrictHostKeyChecking=no" \
"root@${PBS_HOST}:/root/Scripte/password_7z.txt" \ "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:]') | awk -F': ' '{print $2}' | tr -d '[:space:]')
[[ -z "$ZIP_PASSWORD" ]] && { [[ -z "$ZIP_PASSWORD" ]] && {
echo "FEHLER: Kein 7z-Passwort für '$DATASTORE' in password_7z.txt" >&2 echo "FEHLER: Kein 7z-Passwort für '$DATASTORE' in password_7z.txt" >&2; exit 1
exit 1
} }
echo " 7z-Passwort geladen ✓" echo " 7z-Passwort geladen ✓"
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [1/12] SPACE-CHECK # [1/13] SPACE-CHECK
# Nur Warnung kein Abbruch.
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" 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" mkdir -p "$ZIP_DIR"
FREE_KB=$(df "$RESTORE_MOUNT" 2>/dev/null | awk 'NR==2{print $4}' || echo "0") FREE_KB=$(df "$RESTORE_MOUNT" 2>/dev/null | awk 'NR==2{print $4}' || echo "0")
FREE_GB=$(( FREE_KB / 1024 / 1024 )) 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!" [[ $FREE_GB -lt 50 ]] && echo " WARNUNG: Weniger als 50 GB frei!"
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [2/12] VM-ID ERMITTELN # [2/13] ID ERMITTELN
# Original aus Snapshot-Pfad (vm/100/... → 100)
# Restore-ID: erste freie ab 1000, prüft QEMU + LXC
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" 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") 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=$( VM_ID_RESTORED=$(
{ {
@ -264,38 +251,44 @@ for i in range(1000, 2000):
print(i); break print(i); break
" 2>/dev/null || echo "1000" " 2>/dev/null || echo "1000"
) )
echo " Restore VM-ID: $VM_ID_RESTORED" echo " Restore-ID: $VM_ID_RESTORED"
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [3/12] QMRESTORE VOM PBS-STORAGE # [3/13] RESTORE
# --storage aus DB (restore_path), z.B. "5TB.1" # VM → qmrestore
# --unique 1 verhindert Konflikte mit bestehenden VM-Configs # CT → pct restore
# Kein --keyfile nötig Entschlüsselung über registrierten pvesm PBS-Storage
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
echo "==> [3/12] qmrestore vom PBS-Storage..." echo "==> [3/13] Restore vom PBS-Storage ($BACKUP_TYPE)..."
echo " Backup-Ref: $PVE_BACKUP_REF" echo " Backup-Ref: $PVE_BACKUP_REF"
echo " Storage: $RESTORE_PATH" echo " Storage: $RESTORE_PATH"
echo " VM-ID: $VM_ID_RESTORED" echo " ID: $VM_ID_RESTORED"
RESTORE_START_INNER=$(date +%s) RESTORE_START_INNER=$(date +%s)
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" \ qmrestore "$PVE_BACKUP_REF" "$VM_ID_RESTORED" \
--storage "$RESTORE_PATH" \ --storage "$RESTORE_PATH" \
--unique 1 \ --unique 1 \
2>&1 2>&1
fi
RESTORE_DURATION=$(( $(date +%s) - RESTORE_START_INNER )) RESTORE_DURATION=$(( $(date +%s) - RESTORE_START_INNER ))
echo " Restore abgeschlossen in ${RESTORE_DURATION}s" echo " Restore abgeschlossen in ${RESTORE_DURATION}s"
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [4/12] VM_IMAGE_DIR DYNAMISCH ERMITTELN # [4/13] IMAGE_DIR DYNAMISCH ERMITTELN
# PVE kennt den Basispfad des Storages. # VM → /mnt/HDD_5TB.1/images/${VM_ID_RESTORED}
# pvesh get /storage/5TB.1 → "path": "/mnt/HDD_5TB.1" # CT → /mnt/HDD_5TB.1/private/${VM_ID_RESTORED}
# VM_IMAGE_DIR = /mnt/HDD_5TB.1/images/${VM_ID_RESTORED} # oder /mnt/HDD_5TB.1/rootdir/${VM_ID_RESTORED}
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
echo "==> [4/12] Ermittle VM-Image-Verzeichnis..." echo "==> [4/13] Ermittle Image-Verzeichnis..."
STORAGE_BASE=$(pvesh get "/storage/${RESTORE_PATH}" --output-format json \ STORAGE_BASE=$(pvesh get "/storage/${RESTORE_PATH}" --output-format json \
2>/dev/null | python3 -c " 2>/dev/null | python3 -c "
import json, sys import json, sys
@ -304,88 +297,107 @@ print(cfg.get('path', ''))
" 2>/dev/null || echo "") " 2>/dev/null || echo "")
if [[ -n "$STORAGE_BASE" ]]; then 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 else
# Fallback: direkt aus pvesm path IMAGE_DIR="${STORAGE_BASE}/rootdir/${VM_ID_RESTORED}"
VM_IMAGE_DIR=$(pvesm path "${RESTORE_PATH}:vm-${VM_ID_RESTORED}-disk-0" \
2>/dev/null | xargs dirname 2>/dev/null || echo "")
fi 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 else
echo " VM-Image-Dir: $VM_IMAGE_DIR" IMAGE_DIR="${STORAGE_BASE}/images/${VM_ID_RESTORED}"
fi fi
else
# 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"
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" echo " Image-Größe: $(( ACTUAL_DISK_BYTES / 1024 / 1024 / 1024 )) GB"
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [5/12] VM-IMAGES PRÜFEN # [5/13] IMAGES PRÜFEN
# Wenn leer oder nicht vorhanden → failed, Webhook senden, nächstes Backup.
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
echo "==> [5/12] Prüfe VM-Images..." echo "==> [5/13] Prüfe Images..."
if [[ ! -d "$VM_IMAGE_DIR" ]] || [[ -z "$(ls -A "$VM_IMAGE_DIR" 2>/dev/null)" ]]; then if [[ ! -d "$IMAGE_DIR" ]] || [[ -z "$(ls -A "$IMAGE_DIR" 2>/dev/null)" ]]; then
ERROR_MSG="VM_IMAGE_DIR leer oder nicht vorhanden: $VM_IMAGE_DIR" ERROR_MSG="IMAGE_DIR leer oder nicht vorhanden: $IMAGE_DIR"
echo " FEHLER: $ERROR_MSG" echo " FEHLER: $ERROR_MSG"
if [[ "$BACKUP_TYPE" == "ct" ]]; then
pct destroy "$VM_ID_RESTORED" --purge 1 2>/dev/null || true
else
qm destroy "$VM_ID_RESTORED" \ qm destroy "$VM_ID_RESTORED" \
--destroy-unreferenced-disks 1 --purge 1 2>/dev/null || true --destroy-unreferenced-disks 1 --purge 1 2>/dev/null || true
fi
trap - ERR trap - ERR
send_webhook "failed" "$ERROR_MSG" send_webhook "failed" "$ERROR_MSG"
exit 0 exit 0
fi fi
echo " Images vorhanden ✓" 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"
else
echo " WARNUNG: PVE-Config nicht gefunden: $PVE_CONF"
fi
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [6/12] VM VORBEREITEN # [6/13] VORBEREITEN
# unlock → stop → cdrom/ide0 entfernen → alle Netzwerkkarten (net0-net10) # VM: unlock → stop → cdrom/ide0/net entfernen → Agent aktivieren
# löschen → Agent aktivieren. # CT: unlock → stop → net entfernen
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
echo "==> [6/12] VM vorbereiten..." 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
qm unlock "$VM_ID_RESTORED" 2>/dev/null || true qm unlock "$VM_ID_RESTORED" 2>/dev/null || true
qm stop "$VM_ID_RESTORED" 2>/dev/null || true qm stop "$VM_ID_RESTORED" 2>/dev/null || true
sleep 3 sleep 3
qm set "$VM_ID_RESTORED" -delete cdrom 2>/dev/null || true qm set "$VM_ID_RESTORED" -delete cdrom 2>/dev/null || true
qm set "$VM_ID_RESTORED" -delete ide0 2>/dev/null || true qm set "$VM_ID_RESTORED" -delete ide0 2>/dev/null || true
for ((net=0; net<=10; net++)); do for ((net=0; net<=10; net++)); do
qm set "$VM_ID_RESTORED" -delete "net${net}" 2>/dev/null || true qm set "$VM_ID_RESTORED" -delete "net${net}" 2>/dev/null || true
done done
qm set "$VM_ID_RESTORED" --agent "enabled=1,type=virtio" 2>/dev/null || true qm set "$VM_ID_RESTORED" --agent "enabled=1,type=virtio" 2>/dev/null || true
echo " VM vorbereitet (Netzwerkkarten entfernt, Agent aktiviert)." echo " VM vorbereitet (Netzwerkkarten entfernt, Agent aktiviert)."
fi
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [7/12] VM STARTEN & QM-AGENT PRÜFEN # [7/13] STARTEN & PRÜFEN
# 120s in 10s-Schritten. Agent über QEMU Guest Agent Channel, kein Netzwerk. # VM: qm-Agent 120s warten
# Kein Agent = KEIN Abbruch, qm_agent_ok=false in DB. # CT: pct exec ping (vereinfacht)
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
echo "==> [7/12] Starte VM & warte auf qm-Agent (max. 120s)..." echo "==> [7/13] Starte & prüfe ($BACKUP_TYPE)..."
qm start "$VM_ID_RESTORED" 2>/dev/null || true
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 " 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
QM_AGENT_OK="false"
echo " CT nicht gestartet läuft weiter."
fi
else
qm start "$VM_ID_RESTORED" 2>/dev/null || true
AGENT_WAIT=0 AGENT_WAIT=0
AGENT_MAX=120 AGENT_MAX=120
AGENT_INTERVAL=10 AGENT_INTERVAL=10
QM_AGENT_OK="false"
while [[ $AGENT_WAIT -lt $AGENT_MAX ]]; do while [[ $AGENT_WAIT -lt $AGENT_MAX ]]; do
sleep $AGENT_INTERVAL sleep $AGENT_INTERVAL
AGENT_WAIT=$(( AGENT_WAIT + AGENT_INTERVAL )) AGENT_WAIT=$(( AGENT_WAIT + AGENT_INTERVAL ))
@ -395,55 +407,69 @@ while [[ $AGENT_WAIT -lt $AGENT_MAX ]]; do
echo "ONLINE ✓" echo "ONLINE ✓"
hostname_info=$(qm agent "$VM_ID_RESTORED" get-host-name 2>/dev/null \ hostname_info=$(qm agent "$VM_ID_RESTORED" get-host-name 2>/dev/null \
| grep host-name | tr -d '"' || true) | 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 " Hostname: ${hostname_info:-unbekannt}"
echo " Mountpoints: ${fs_info:-unbekannt}"
break break
else else
echo "nicht erreichbar." echo "nicht erreichbar."
fi fi
done done
[[ "$QM_AGENT_OK" == "false" ]] && \
if [[ "$QM_AGENT_OK" == "false" ]]; then echo " qm-Agent nicht erreichbar qm_agent_ok=false in DB."
echo " qm-Agent nach ${AGENT_MAX}s nicht erreichbar."
echo " → qm_agent_ok=false in DB, Restore läuft weiter."
fi fi
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [8/12] VM STOPPEN # [8/13] STOPPEN
# Sauberes Shutdown, nach 30s force-stop.
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
echo "==> [8/12] Stoppe VM..." 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 qm shutdown "$VM_ID_RESTORED" 2>/dev/null || true
sleep 30 sleep 30
qm stop "$VM_ID_RESTORED" --skiplock 1 2>/dev/null || true qm stop "$VM_ID_RESTORED" --skiplock 1 2>/dev/null || true
echo " VM gestoppt."
sleep 5 sleep 5
fi
echo " Gestoppt."
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [9/12] 7Z-ARCHIV ERSTELLEN # [9/13] CONFIG SICHERN
# VM-Images aus VM_IMAGE_DIR/* zippen. # VM: qemu-server.conf aus /etc/pve/qemu-server/
# VM-Name aus qemu-server.conf lesen → ZIP bekommt echten Namen. # CT: lxc.conf aus /etc/pve/lxc/
# Originale Config (mit Netzwerk) ins ZIP-Verzeichnis legen.
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
echo "==> [9/12] Erstelle verschlüsseltes 7z-Archiv..." echo "==> [9/13] Config sichern..."
# Originale Config (mit Netzwerkkarten) als qemu-server.conf ins ZIP-Verzeichnis if [[ "$BACKUP_TYPE" == "ct" ]]; then
# legen nicht die angepasste Config (ohne Netzwerkkarten) vom Restore-Test. PVE_CONF="/etc/pve/lxc/${VM_ID_RESTORED}.conf"
if [[ -f "$ORIG_CONF_BACKUP" ]]; then CONF_FILENAME="lxc.conf"
cp "$ORIG_CONF_BACKUP" "${VM_IMAGE_DIR}/qemu-server.conf" # CT-Name aus Config lesen
echo " Originale Config für ZIP wiederhergestellt ✓" VM_NAME=$(grep -m1 "^hostname:" "$PVE_CONF" 2>/dev/null \
else
echo " WARNUNG: Keine originale Config vorhanden angepasste Config wird gezippt."
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:]' \ | awk -F': ' '{print $2}' | tr -d '[:space:]' \
|| echo "$SAFE_CLIENT") || echo "$SAFE_CLIENT")
echo " VM-Name: $VM_NAME" 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_FILE="${ZIP_DIR}/${VM_NAME}-${VM_ID_ORIGINAL}.7z"
ZIP_START=$(date +%s) ZIP_START=$(date +%s)
@ -455,7 +481,7 @@ ZIP_START=$(date +%s)
-p"${ZIP_PASSWORD}" \ -p"${ZIP_PASSWORD}" \
-mhe=on \ -mhe=on \
"$ZIP_FILE" \ "$ZIP_FILE" \
"${VM_IMAGE_DIR}/"* \ "${IMAGE_DIR}/"* \
2>&1 | tail -5 2>&1 | tail -5
ZIP_DURATION=$(( $(date +%s) - ZIP_START )) 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" echo " ZIP: $(( ZIP_SIZE_BYTES / 1024 / 1024 )) MB in ${ZIP_DURATION}s"
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [10/12] RSYNC ZUM BACKUP-SERVER # [11/13] RSYNC ZUM BACKUP-SERVER
# Zielverzeichnis aus DB (rsync_target) = Firmen-spezifischer Pfad.
# 3 Versuche mit 60s Pause + Größenvergleich lokal vs. remote.
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
# Rsync-Ziel mit Datumsordner
RSYNC_TARGET_DATE="${RSYNC_TARGET}/${LAST_DATE}" 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 MAX_RETRIES=3
rsync_transfer() { rsync_transfer() {
@ -517,30 +540,32 @@ if [[ "$RSYNC_OK" == "true" ]]; then
fi fi
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# [11/12] AUFRÄUMEN # [12/13] AUFRÄUMEN
# VM destroy inkl. Disks, lokale ZIP löschen.
# Keys bleiben gecacht für nächste VM desselben Datastores.
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
echo "" echo ""
echo "==> [11/12] Aufräumen..." 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" \ qm destroy "$VM_ID_RESTORED" \
--destroy-unreferenced-disks 1 \ --destroy-unreferenced-disks 1 \
--purge 1 \ --purge 1 \
2>/dev/null || echo " VM $VM_ID_RESTORED nicht mehr vorhanden." 2>/dev/null || echo " VM $VM_ID_RESTORED nicht mehr vorhanden."
fi
rm -f "$ZIP_FILE" rm -f "$ZIP_FILE"
echo " VM ${VM_ID_RESTORED} entfernt, ZIP gelöscht." echo " ${BACKUP_TYPE^^} ${VM_ID_RESTORED} entfernt, ZIP gelöscht."
echo " Keys gecacht in $KEY_DIR"
# ── Zusammenfassung & Webhook ───────────────────────────────────────────────── # ── Zusammenfassung & Webhook ─────────────────────────────────────────────────
TOTAL=$(( $(date +%s) - RESTORE_START )) TOTAL=$(( $(date +%s) - RESTORE_START ))
echo "" echo ""
echo "============================================================" echo "============================================================"
echo " Status: $STATUS" echo " Status: $STATUS"
echo " Typ: $BACKUP_TYPE"
echo " Gesamtdauer: ${TOTAL}s" echo " Gesamtdauer: ${TOTAL}s"
echo " VM-Name: ${VM_NAME:-$SAFE_CLIENT}" echo " Name: ${VM_NAME:-$SAFE_CLIENT}"
echo " VM_IMAGE_DIR: $VM_IMAGE_DIR" echo " Image-Dir: $IMAGE_DIR"
echo " Datastore: $DATASTORE" echo " qm-Agent/CT: $QM_AGENT_OK"
echo " qm-Agent: $QM_AGENT_OK"
echo " Rsync: $RSYNC_OK (Versuche: $RSYNC_RETRIES)" echo " Rsync: $RSYNC_OK (Versuche: $RSYNC_RETRIES)"
echo " ZIP: $(( ZIP_SIZE_BYTES / 1024 / 1024 )) MB" echo " ZIP: $(( ZIP_SIZE_BYTES / 1024 / 1024 )) MB"
[[ -n "$ERROR_MSG" ]] && echo " Fehler: $ERROR_MSG" [[ -n "$ERROR_MSG" ]] && echo " Fehler: $ERROR_MSG"