BackupScript/restore.sh

700 lines
32 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.14
#
# 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] 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] 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 ─────────────────────────────────────────────────────────
CONF_FILE="/opt/windmill-restore/pbs.conf"
[[ ! -f "$CONF_FILE" ]] && { echo "FEHLER: $CONF_FILE fehlt!" >&2; exit 1; }
source "$CONF_FILE"
# ── Argument-Parser ───────────────────────────────────────────────────────────
JOB_UUID=""
BACKUP_PATH=""
CLIENT_NAME=""
RESTORE_MOUNT=""
RESTORE_PATH=""
RSYNC_TARGET=""
PBS_STORAGE=""
WEBHOOK_URL=""
WEBHOOK_TOKEN=""
BACKUP_SIZE_BYTES=0
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 ;;
--server-hostname) SERVER_HOSTNAME="$2"; shift 2 ;;
--backup-size) BACKUP_SIZE_BYTES="$2"; shift 2 ;;
*) echo "Unbekannter Parameter: $1" >&2; exit 1 ;;
esac
done
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//\//_}"
LOG_FILE="$LOG_DIR/${SAFE_CLIENT}_$(date +%Y%m%d_%H%M%S).log"
exec > >(tee -a "$LOG_FILE") 2>&1
# ── Backup-Pfad zerlegen ──────────────────────────────────────────────────────
# 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}"
# ── Messvariablen ─────────────────────────────────────────────────────────────
LAST_DATE=$(date +"%Y-%m-%d" -d "1 day ago")
# STI-BAC01: rsync_target ist lokal gemountet → ZIP direkt dorthin, kein Rsync
if [[ "$SERVER_HOSTNAME" == "STI-BAC01" ]]; then
ZIP_DIR="${RSYNC_TARGET}/${LAST_DATE}"
SKIP_RSYNC=1
else
ZIP_DIR="${RESTORE_MOUNT}/zips/${LAST_DATE}"
SKIP_RSYNC=0
fi
BACKUP_SERVER_HOST=$(cat /opt/windmill-restore/backup_server_host 2>/dev/null \
|| echo "backup-server")
# SERVER_HOSTNAME kommt als Parameter --server-hostname
# Fallback auf hostname -f falls nicht gesetzt
SERVER_HOSTNAME="${SERVER_HOSTNAME:-$(hostname -f 2>/dev/null || hostname)}"
RESTORE_START=$(date +%s)
STATUS="success"
ERROR_MSG=""
VM_ID_ORIGINAL=0
VM_ID_RESTORED=0
VM_NAME=""
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"
ZIP_FILE=""
ZIP_PASSWORD=""
echo "============================================================"
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"
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",
"server_hostname": "%s",
"free_space_gb": %d,
"vm_name": "%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" \
"$SERVER_HOSTNAME" "$FREE_GB" "${VM_NAME:-$SAFE_CLIENT}" \
"$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"
}
# ── ERR-Trap: aufräumen und Webhook senden ────────────────────────────────────
trap 'STATUS="failed"
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/13] 7Z-PASSWORT VOM PBS-SERVER HOLEN
# ═════════════════════════════════════════════════════════════════════════════
echo ""
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"
PW_FILE_LOCAL="${KEY_DIR}/password_7z.txt"
if [[ ! -f "$PW_FILE_LOCAL" || ! -s "$PW_FILE_LOCAL" ]]; then
echo " Hole password_7z.txt..."
rsync -az \
-e "ssh -o StrictHostKeyChecking=no" \
"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/13] SPACE-CHECK
# Prüft ob genug Platz für Restore + 50% für ZIP vorhanden ist.
# Benötigter Platz: backup_size_bytes * 1.5 aus dem Webhook-Parameter
# Falls nicht genug → Webhook mit failed senden und Exit
# ═════════════════════════════════════════════════════════════════════════════
echo ""
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 ))
FREE_BYTES=$(( FREE_KB * 1024 ))
echo " Frei: ${FREE_GB} GB"
# Benötigten Platz berechnen: Backup-Größe * 1.5
# BACKUP_SIZE_BYTES kommt als Parameter --backup-size
REQUIRED_BYTES=$(( BACKUP_SIZE_BYTES * 3 / 2 ))
REQUIRED_GB=$(( REQUIRED_BYTES / 1024 / 1024 / 1024 ))
echo " Benötigt: ~${REQUIRED_GB} GB (Restore + 50% für ZIP)"
if [[ $BACKUP_SIZE_BYTES -gt 0 ]] && [[ $FREE_BYTES -lt $REQUIRED_BYTES ]]; then
ERROR_MSG="Nicht genug Speicherplatz: ${FREE_GB}GB frei, ~${REQUIRED_GB}GB benötigt"
echo " FEHLER: $ERROR_MSG"
trap - ERR
send_webhook "failed" "$ERROR_MSG"
exit 0
fi
[[ $FREE_GB -lt 50 ]] && echo " WARNUNG: Weniger als 50 GB frei!"
# ═════════════════════════════════════════════════════════════════════════════
# [2/13] ID ERMITTELN
# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo "==> [2/13] Ermittle IDs..."
VM_ID_ORIGINAL=$(echo "$SNAPSHOT_PATH" | grep -oP '\d+' | head -1 || echo "0")
echo " Original-ID: $VM_ID_ORIGINAL (Typ: $BACKUP_TYPE)"
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-ID: $VM_ID_RESTORED"
# ═════════════════════════════════════════════════════════════════════════════
# [2.5/13] CONFIG-ONLY CHECK
# Config-only Restore um VM-Name zu ermitteln und zu prüfen ob ZIP bereits
# auf dem Backup-Server existiert → bei Fund: Restore überspringen
# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo "==> [2.5/13] Config-only Restore zum Prüfen..."
CONFIG_VM_NAME=""
if [[ "$BACKUP_TYPE" == "ct" ]]; then
pct restore "$VM_ID_RESTORED" "$PVE_BACKUP_REF" --storage "$RESTORE_PATH" --config-only 1 2>/dev/null || true
CONFIG_VM_NAME=$(grep -m1 "^hostname:" /etc/pve/lxc/${VM_ID_RESTORED}.conf 2>/dev/null | awk -F': ' '{print $2}' | tr -d '[:space:]' || echo "")
pct destroy "$VM_ID_RESTORED" --purge 1 2>/dev/null || true
else
qmrestore "$PVE_BACKUP_REF" "$VM_ID_RESTORED" --storage "$RESTORE_PATH" --config-only 1 2>/dev/null || true
CONFIG_VM_NAME=$(grep -m1 "^name:" /etc/pve/qemu-server/${VM_ID_RESTORED}.conf 2>/dev/null | awk -F': ' '{print $2}' | tr -d '[:space:]' || echo "")
qm destroy "$VM_ID_RESTORED" --purge 1 2>/dev/null || true
fi
echo " VM-Name aus Config: ${CONFIG_VM_NAME:-unbekannt}"
# Prüfen ob ZIP bereits auf Backup-Server vorhanden
if [[ -n "$CONFIG_VM_NAME" ]]; then
if [[ "$SKIP_RSYNC" == "1" ]]; then
# Lokaler Modus: direkt prüfen
ZIP_CHECK="${RSYNC_TARGET}/${LAST_DATE}/${CONFIG_VM_NAME}-${VM_ID_ORIGINAL}.7z"
if [[ -f "$ZIP_CHECK" ]]; then
echo " ZIP bereits vorhanden (lokal): $ZIP_CHECK"
echo " Überspringe Restore sende success Webhook."
VM_NAME="$CONFIG_VM_NAME"
ZIP_SIZE_BYTES=$(stat -c%s "$ZIP_CHECK" 2>/dev/null || echo "0")
RSYNC_OK="true"
RSYNC_SIZE_BYTES=$ZIP_SIZE_BYTES
QM_AGENT_OK="skipped"
trap - ERR
send_webhook "success" ""
exit 0
fi
else
ZIP_CHECK="${RSYNC_TARGET}/${LAST_DATE}/${CONFIG_VM_NAME}-${VM_ID_ORIGINAL}.7z"
if ssh "$BACKUP_SERVER_HOST" "test -f '$ZIP_CHECK'" 2>/dev/null; then
echo " ZIP bereits vorhanden (remote): $ZIP_CHECK"
echo " Überspringe Restore sende success Webhook."
VM_NAME="$CONFIG_VM_NAME"
ZIP_SIZE_BYTES=$(ssh "$BACKUP_SERVER_HOST" "stat -c%s '$ZIP_CHECK'" 2>/dev/null || echo "0")
RSYNC_OK="true"
RSYNC_SIZE_BYTES=$ZIP_SIZE_BYTES
QM_AGENT_OK="skipped"
trap - ERR
send_webhook "success" ""
exit 0
fi
fi
echo " Kein vorhandenes ZIP gefunden starte vollständigen Restore."
fi
# ═════════════════════════════════════════════════════════════════════════════
# [3/13] RESTORE
# VM → qmrestore
# CT → pct restore
# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo "==> [3/13] Restore vom PBS-Storage ($BACKUP_TYPE)..."
echo " Backup-Ref: $PVE_BACKUP_REF"
echo " Storage: $RESTORE_PATH"
echo " ID: $VM_ID_RESTORED"
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" \
--storage "$RESTORE_PATH" \
--unique 1 \
2>&1
fi
RESTORE_DURATION=$(( $(date +%s) - RESTORE_START_INNER ))
echo " Restore abgeschlossen in ${RESTORE_DURATION}s"
# ═════════════════════════════════════════════════════════════════════════════
# [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/13] Ermittle 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
if [[ "$BACKUP_TYPE" == "ct" ]]; then
# CT auf dir-Storage: alle möglichen Pfade durchsuchen
# Reihenfolge: images/ → private/ → rootdir/ (je nach Storage-Konfiguration)
IMAGE_DIR=""
for candidate in "${STORAGE_BASE}/images/${VM_ID_RESTORED}" "${STORAGE_BASE}/private/${VM_ID_RESTORED}" "${STORAGE_BASE}/rootdir/${VM_ID_RESTORED}"; do
if [[ -d "$candidate" ]] && [[ -n "$(ls -A "$candidate" 2>/dev/null)" ]]; then
IMAGE_DIR="$candidate"
echo " CT-Image gefunden unter: $IMAGE_DIR"
break
else
echo " Nicht gefunden: $candidate"
fi
done
# Falls kein Verzeichnis gefunden → find als letzter Versuch
if [[ -z "$IMAGE_DIR" ]]; then
IMAGE_DIR=$(find "$STORAGE_BASE" -maxdepth 2 -type d -name "$VM_ID_RESTORED" 2>/dev/null | head -1 || echo "")
[[ -n "$IMAGE_DIR" ]] && echo " CT-Image via find: $IMAGE_DIR"
fi
else
IMAGE_DIR="${STORAGE_BASE}/images/${VM_ID_RESTORED}"
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
# Letzter Fallback falls IMAGE_DIR noch leer
if [[ -z "$IMAGE_DIR" ]]; then
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: Kein Image-Verzeichnis gefunden, nutze Fallback: $IMAGE_DIR"
fi
echo " Image-Dir: $IMAGE_DIR"
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/13] IMAGES PRÜFEN
# ═════════════════════════════════════════════════════════════════════════════
echo ""
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"
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 ✓"
# ═════════════════════════════════════════════════════════════════════════════
# [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
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
# ═════════════════════════════════════════════════════════════════════════════
# [7/13] STARTEN & PRÜFEN
# VM: qm-Agent 120s warten
# CT: pct exec ping (vereinfacht)
# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo "==> [7/13] Starte & prüfe ($BACKUP_TYPE)..."
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_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
# ═════════════════════════════════════════════════════════════════════════════
# [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)
7z a -t7z \
-mmt=4 \
-mx=1 \
-md=16M \
-p"${ZIP_PASSWORD}" \
-mhe=on \
"$ZIP_FILE" \
"${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"
# ═════════════════════════════════════════════════════════════════════════════
# [11/13] RSYNC ZUM BACKUP-SERVER
# ═════════════════════════════════════════════════════════════════════════════
echo ""
RSYNC_TARGET_DATE="${RSYNC_TARGET}/${LAST_DATE}"
echo "==> [11/13] Rsync / Datei-Transfer..."
if [[ "$SKIP_RSYNC" == "1" ]]; then
# STI-BAC01: ZIP liegt bereits im Zielverzeichnis kein Rsync nötig
echo " Lokaler Modus: ZIP bereits in ${RSYNC_TARGET_DATE} kein Rsync."
RSYNC_OK="true"
RSYNC_SIZE_BYTES=$ZIP_SIZE_BYTES
echo " Groesse: $(( RSYNC_SIZE_BYTES / 1024 / 1024 )) MB"
else
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="Groessenabweichung: lokal=${ZIP_SIZE_BYTES} remote=${REMOTE_SIZE}"
else
echo " Groessenprüfung OK: ${REMOTE_SIZE} Bytes."
fi
fi
fi
# ═════════════════════════════════════════════════════════════════════════════
# [12/13] AUFRÄUMEN
# ═════════════════════════════════════════════════════════════════════════════
echo ""
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
if [[ "$SKIP_RSYNC" == "1" ]]; then
echo " ${BACKUP_TYPE^^} ${VM_ID_RESTORED} entfernt. ZIP bleibt am Zielort."
else
rm -f "$ZIP_FILE"
echo " ${BACKUP_TYPE^^} ${VM_ID_RESTORED} entfernt, ZIP gelöscht."
fi
# ── Zusammenfassung & Webhook ─────────────────────────────────────────────────
TOTAL=$(( $(date +%s) - RESTORE_START ))
echo ""
echo "============================================================"
echo " Status: $STATUS"
echo " Typ: $BACKUP_TYPE"
echo " Gesamtdauer: ${TOTAL}s"
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"
echo "============================================================"
trap - ERR
send_webhook "$STATUS" "$ERROR_MSG"