548 lines
25 KiB
Bash
548 lines
25 KiB
Bash
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# /opt/windmill-restore/restore.sh
|
||
# Windmill Backup Restore Worker
|
||
# Version: 1.0.10
|
||
#
|
||
# WAS DIESES SCRIPT MACHT:
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Läuft direkt auf dem Proxmox Restore-Server, gestartet von Windmill per SSH
|
||
# non-blocking (nohup ... &). Komplett eigenständig, keine offene Verbindung.
|
||
#
|
||
# Alle Pfade kommen aus der Datenbank via Windmill:
|
||
# --restore-mount Mountpoint des Restore-Volumes z.B. /mnt/HDD_5TB.1
|
||
# --restore-path PVE-Storage-Name für qmrestore z.B. 5TB.1
|
||
# --rsync-target Rsync-Ziel auf Backup-Server z.B. /backup/incoming/TNP
|
||
# --pbs-storage PVE-Storage-ID des PBS-Datastores z.B. pbs-tnp-invest-gmbh
|
||
#
|
||
# KEYS – beide werden per Rsync vom PBS-Server geholt (PBS_HOST aus pbs.conf):
|
||
# 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"
|
||
#
|
||
# 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_IMAGE_DIR – Dynamisch aus PVE-Storage-Pfad ermitteln
|
||
# [5] Images prüfen – Abbruch wenn leer/nicht vorhanden
|
||
# [6] VM vorbereiten – unlock, cdrom/ide0 entfernen, alle Netzwerkkarten
|
||
# löschen, Agent aktivieren
|
||
# [7] VM starten & – 120s auf qm-Agent warten (10s Schritte)
|
||
# Agent prüfen Kein Agent = qm_agent_ok=false, KEIN Abbruch
|
||
# [8] VM stoppen – Sauberes Shutdown, nach 30s force-stop
|
||
# [9] 7z-Archiv – VM-Images verschlüsselt zippen (7z-Passwort)
|
||
# [10] Rsync – ZIP zum Backup-Server (Firmen-Zielverzeichnis)
|
||
# 3 Versuche + Größenvergleich
|
||
# [11] Aufräumen – VM destroy, ZIP löschen
|
||
# [12] Webhook – JSON → Windmill schreibt DB & startet nächsten
|
||
# =============================================================================
|
||
set -euo pipefail
|
||
|
||
# ── Konfigdatei laden (PBS-Credentials) ──────────────────────────────────────
|
||
CONF_FILE="/opt/windmill-restore/pbs.conf"
|
||
[[ ! -f "$CONF_FILE" ]] && { echo "FEHLER: $CONF_FILE fehlt!" >&2; exit 1; }
|
||
# shellcheck source=/dev/null
|
||
source "$CONF_FILE"
|
||
# Enthält: PBS_HOST, PBS_PORT, PBS_USER, PBS_PASSWORD, PBS_FINGERPRINT
|
||
|
||
# ── Argument-Parser ───────────────────────────────────────────────────────────
|
||
JOB_UUID=""
|
||
BACKUP_PATH=""
|
||
CLIENT_NAME=""
|
||
RESTORE_MOUNT=""
|
||
RESTORE_PATH=""
|
||
RSYNC_TARGET=""
|
||
PBS_STORAGE=""
|
||
WEBHOOK_URL=""
|
||
WEBHOOK_TOKEN=""
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--job-uuid) JOB_UUID="$2"; shift 2 ;;
|
||
--backup-path) BACKUP_PATH="$2"; shift 2 ;;
|
||
--client) CLIENT_NAME="$2"; shift 2 ;;
|
||
--restore-mount) RESTORE_MOUNT="$2"; shift 2 ;;
|
||
--restore-path) RESTORE_PATH="$2"; shift 2 ;;
|
||
--rsync-target) RSYNC_TARGET="$2"; shift 2 ;;
|
||
--pbs-storage) PBS_STORAGE="$2"; shift 2 ;;
|
||
--webhook-url) WEBHOOK_URL="$2"; shift 2 ;;
|
||
--webhook-token) WEBHOOK_TOKEN="$2"; shift 2 ;;
|
||
*) echo "Unbekannter Parameter: $1" >&2; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
# Pflichtparameter prüfen
|
||
for var in JOB_UUID BACKUP_PATH CLIENT_NAME \
|
||
RESTORE_MOUNT RESTORE_PATH RSYNC_TARGET PBS_STORAGE WEBHOOK_URL; do
|
||
[[ -z "${!var}" ]] && { echo "FEHLER: --${var//_/-} fehlt" >&2; exit 1; }
|
||
done
|
||
|
||
[[ ! -d "$RESTORE_MOUNT" ]] && {
|
||
echo "FEHLER: Restore-Mount '$RESTORE_MOUNT' existiert nicht!" >&2
|
||
exit 1
|
||
}
|
||
|
||
# ── Logging ───────────────────────────────────────────────────────────────────
|
||
LOG_DIR="/opt/windmill-restore/logs"
|
||
mkdir -p "$LOG_DIR"
|
||
SAFE_CLIENT="${CLIENT_NAME//\//_}" # "vm/100" → "vm_100"
|
||
LOG_FILE="$LOG_DIR/${SAFE_CLIENT}_$(date +%Y%m%d_%H%M%S).log"
|
||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||
|
||
# ── Backup-Pfad zerlegen ──────────────────────────────────────────────────────
|
||
# BACKUP_PATH Format: "tnp-Invest-GmbH:vm/100/2024-01-15T02:00:00Z"
|
||
DATASTORE=$(echo "$BACKUP_PATH" | cut -d: -f1)
|
||
SNAPSHOT_PATH=$(echo "$BACKUP_PATH" | cut -d: -f2)
|
||
PVE_BACKUP_REF="${PBS_STORAGE}:backup/${SNAPSHOT_PATH}"
|
||
|
||
# ── Pfade & Messvariablen ─────────────────────────────────────────────────────
|
||
LAST_DATE=$(date +"%Y-%m-%d" -d "1 day ago")
|
||
ZIP_DIR="${RESTORE_MOUNT}/zips/${LAST_DATE}"
|
||
BACKUP_SERVER_HOST=$(cat /opt/windmill-restore/backup_server_host 2>/dev/null \
|
||
|| echo "backup-server")
|
||
|
||
RESTORE_START=$(date +%s)
|
||
STATUS="success"
|
||
ERROR_MSG=""
|
||
VM_ID_ORIGINAL=0
|
||
VM_ID_RESTORED=0
|
||
VM_NAME=""
|
||
VM_IMAGE_DIR=""
|
||
ACTUAL_DISK_BYTES=0
|
||
ZIP_SIZE_BYTES=0
|
||
ZIP_DURATION=0
|
||
RSYNC_SIZE_BYTES=0
|
||
RSYNC_OK="true"
|
||
RSYNC_RETRIES=0
|
||
QM_AGENT_OK="false"
|
||
RESTORE_DURATION=0
|
||
ZIP_PASSWORD=""
|
||
|
||
echo "============================================================"
|
||
echo " Windmill Restore Worker v1.0.9"
|
||
echo " Client: $CLIENT_NAME"
|
||
echo " Datastore: $DATASTORE"
|
||
echo " Backup: $BACKUP_PATH"
|
||
echo " PBS-Storage: $PBS_STORAGE"
|
||
echo " Restore-Mount: $RESTORE_MOUNT"
|
||
echo " Restore-Path: $RESTORE_PATH"
|
||
echo " Rsync-Target: $RSYNC_TARGET"
|
||
echo " Job-UUID: $JOB_UUID"
|
||
echo " Start: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo "============================================================"
|
||
|
||
# ── Webhook-Funktion ──────────────────────────────────────────────────────────
|
||
send_webhook() {
|
||
local wh_status="$1"
|
||
local wh_error="${2:-}"
|
||
local duration=$(( $(date +%s) - RESTORE_START ))
|
||
local payload
|
||
payload=$(printf '{
|
||
"job_uuid": "%s",
|
||
"client_name": "%s",
|
||
"status": "%s",
|
||
"error_message": "%s",
|
||
"vm_id_original": %d,
|
||
"vm_id_restored": %d,
|
||
"restore_duration_sec": %d,
|
||
"actual_disk_used_bytes": %d,
|
||
"zip_size_bytes": %d,
|
||
"zip_duration_sec": %d,
|
||
"rsync_size_bytes": %d,
|
||
"rsync_ok": %s,
|
||
"rsync_retries": %d,
|
||
"qm_agent_ok": %s,
|
||
"log_file": "%s"
|
||
}' \
|
||
"$JOB_UUID" "$CLIENT_NAME" "$wh_status" "$wh_error" \
|
||
"$VM_ID_ORIGINAL" "$VM_ID_RESTORED" \
|
||
"$duration" "$ACTUAL_DISK_BYTES" \
|
||
"$ZIP_SIZE_BYTES" "$ZIP_DURATION" \
|
||
"$RSYNC_SIZE_BYTES" "$RSYNC_OK" "$RSYNC_RETRIES" \
|
||
"$QM_AGENT_OK" "$LOG_FILE")
|
||
|
||
echo ""
|
||
echo "==> Sende Webhook..."
|
||
local http_code
|
||
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-X POST "$WEBHOOK_URL" \
|
||
-H "Content-Type: application/json" \
|
||
${WEBHOOK_TOKEN:+-H "Authorization: Bearer ${WEBHOOK_TOKEN}"} \
|
||
-d "$payload")
|
||
echo " HTTP: $http_code"
|
||
[[ "$http_code" =~ ^2 ]] && echo " Webhook OK." \
|
||
|| echo " WARNUNG: HTTP $http_code"
|
||
}
|
||
|
||
trap 'STATUS="failed"
|
||
send_webhook "failed" "Abgebrochen in Zeile $LINENO – $LOG_FILE"' ERR
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [0/12] 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 → qmrestore --keyfile
|
||
# 7z-Passwort: /root/Scripte/password_7z.txt → 7z -p
|
||
# Format: "tnp-Invest-GmbH: Passwort123"
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [0/12] Keys vom PBS-Server holen ($PBS_HOST)..."
|
||
KEY_DIR="/opt/windmill-restore/keys"
|
||
mkdir -p "$KEY_DIR"
|
||
chmod 700 "$KEY_DIR"
|
||
|
||
# ── PBS Encrypt-Keyfile ───────────────────────────────────────────────────────
|
||
KEYFILE_LOCAL="${KEY_DIR}/${DATASTORE}.keyfile"
|
||
if [[ -f "$KEYFILE_LOCAL" && -s "$KEYFILE_LOCAL" ]]; then
|
||
echo " Keyfile bereits vorhanden: $KEYFILE_LOCAL"
|
||
else
|
||
echo " Hole Keyfile: root@${PBS_HOST}:/root/Scripte/${DATASTORE}.keyfile"
|
||
rsync -az \
|
||
-e "ssh -o StrictHostKeyChecking=no" \
|
||
"root@${PBS_HOST}:/root/Scripte/${DATASTORE}.keyfile" \
|
||
"$KEYFILE_LOCAL" \
|
||
2>&1
|
||
chmod 600 "$KEYFILE_LOCAL"
|
||
echo " Keyfile gespeichert ✓"
|
||
fi
|
||
|
||
[[ ! -f "$KEYFILE_LOCAL" || ! -s "$KEYFILE_LOCAL" ]] && {
|
||
echo "FEHLER: Keyfile fehlt oder leer für Datastore '$DATASTORE'" >&2
|
||
exit 1
|
||
}
|
||
|
||
# ── 7z-Passwort aus password_7z.txt ──────────────────────────────────────────
|
||
PW_FILE_LOCAL="${KEY_DIR}/password_7z.txt"
|
||
if [[ ! -f "$PW_FILE_LOCAL" || ! -s "$PW_FILE_LOCAL" ]]; then
|
||
echo " Hole password_7z.txt: root@${PBS_HOST}:/root/Scripte/password_7z.txt"
|
||
rsync -az \
|
||
-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/12] SPACE-CHECK
|
||
# Nur Warnung – kein Abbruch.
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [1/12] Prüfe freien Speicherplatz auf $RESTORE_MOUNT..."
|
||
mkdir -p "$ZIP_DIR"
|
||
FREE_KB=$(df "$RESTORE_MOUNT" 2>/dev/null | awk 'NR==2{print $4}' || echo "0")
|
||
FREE_GB=$(( FREE_KB / 1024 / 1024 ))
|
||
echo " Frei: ${FREE_GB} GB"
|
||
[[ $FREE_GB -lt 50 ]] && echo " WARNUNG: Weniger als 50 GB frei!"
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [2/12] VM-ID ERMITTELN
|
||
# Original aus Snapshot-Pfad (vm/100/... → 100)
|
||
# Restore-ID: erste freie ab 1000, prüft QEMU + LXC
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [2/12] Ermittle VM-IDs..."
|
||
VM_ID_ORIGINAL=$(echo "$SNAPSHOT_PATH" | grep -oP '\d+' | head -1 || echo "0")
|
||
echo " Original VM-ID: $VM_ID_ORIGINAL"
|
||
|
||
VM_ID_RESTORED=$(
|
||
{
|
||
pvesh get /nodes/localhost/qemu --output-format json 2>/dev/null || echo "[]"
|
||
pvesh get /nodes/localhost/lxc --output-format json 2>/dev/null || echo "[]"
|
||
} | python3 -c "
|
||
import json, sys
|
||
data = []
|
||
for line in sys.stdin:
|
||
line = line.strip()
|
||
if line:
|
||
try: data.extend(json.loads(line))
|
||
except: pass
|
||
existing = {int(v.get('vmid', 0)) for v in data}
|
||
for i in range(1000, 2000):
|
||
if i not in existing:
|
||
print(i); break
|
||
" 2>/dev/null || echo "1000"
|
||
)
|
||
echo " Restore VM-ID: $VM_ID_RESTORED"
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [3/12] QMRESTORE VOM PBS-STORAGE
|
||
# --storage aus DB (restore_path), z.B. "5TB.1"
|
||
# --keyfile lokal gecachtes Keyfile vom PBS-Server
|
||
# --unique 1 verhindert Konflikte mit bestehenden VM-Configs
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [3/12] qmrestore vom PBS-Storage..."
|
||
echo " Backup-Ref: $PVE_BACKUP_REF"
|
||
echo " Storage: $RESTORE_PATH"
|
||
echo " Keyfile: $KEYFILE_LOCAL"
|
||
echo " VM-ID: $VM_ID_RESTORED"
|
||
|
||
RESTORE_START_INNER=$(date +%s)
|
||
|
||
qmrestore "$PVE_BACKUP_REF" "$VM_ID_RESTORED" \
|
||
--storage "$RESTORE_PATH" \
|
||
--keyfile "$KEYFILE_LOCAL" \
|
||
--unique 1 \
|
||
2>&1
|
||
|
||
RESTORE_DURATION=$(( $(date +%s) - RESTORE_START_INNER ))
|
||
echo " Restore abgeschlossen in ${RESTORE_DURATION}s"
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [4/12] VM_IMAGE_DIR DYNAMISCH ERMITTELN
|
||
# PVE kennt den Basispfad des Storages.
|
||
# pvesh get /storage/5TB.1 → "path": "/mnt/HDD_5TB.1"
|
||
# VM_IMAGE_DIR = /mnt/HDD_5TB.1/images/${VM_ID_RESTORED}
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [4/12] Ermittle VM-Image-Verzeichnis..."
|
||
STORAGE_BASE=$(pvesh get "/storage/${RESTORE_PATH}" --output-format json \
|
||
2>/dev/null | python3 -c "
|
||
import json, sys
|
||
cfg = json.load(sys.stdin)
|
||
print(cfg.get('path', ''))
|
||
" 2>/dev/null || echo "")
|
||
|
||
if [[ -n "$STORAGE_BASE" ]]; then
|
||
VM_IMAGE_DIR="${STORAGE_BASE}/images/${VM_ID_RESTORED}"
|
||
else
|
||
# Fallback: direkt aus pvesm path
|
||
VM_IMAGE_DIR=$(pvesm path "${RESTORE_PATH}:vm-${VM_ID_RESTORED}-disk-0" \
|
||
2>/dev/null | xargs dirname 2>/dev/null || echo "")
|
||
fi
|
||
|
||
if [[ -z "$VM_IMAGE_DIR" ]]; then
|
||
# Letzter Fallback: Standard-Pfad
|
||
VM_IMAGE_DIR="/var/lib/vz/images/${VM_ID_RESTORED}"
|
||
echo " WARNUNG: Storage-Pfad nicht ermittelt, nutze Fallback: $VM_IMAGE_DIR"
|
||
else
|
||
echo " VM-Image-Dir: $VM_IMAGE_DIR"
|
||
fi
|
||
|
||
ACTUAL_DISK_BYTES=$(du -sb "$VM_IMAGE_DIR" 2>/dev/null | awk '{print $1}' || echo "0")
|
||
echo " Image-Größe: $(( ACTUAL_DISK_BYTES / 1024 / 1024 / 1024 )) GB"
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [5/12] VM-IMAGES PRÜFEN
|
||
# Wenn leer oder nicht vorhanden → failed, Webhook senden, nächstes Backup.
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [5/12] Prüfe VM-Images..."
|
||
if [[ ! -d "$VM_IMAGE_DIR" ]] || [[ -z "$(ls -A "$VM_IMAGE_DIR" 2>/dev/null)" ]]; then
|
||
ERROR_MSG="VM_IMAGE_DIR leer oder nicht vorhanden: $VM_IMAGE_DIR"
|
||
echo " FEHLER: $ERROR_MSG"
|
||
qm destroy "$VM_ID_RESTORED" \
|
||
--destroy-unreferenced-disks 1 --purge 1 2>/dev/null || true
|
||
trap - ERR
|
||
send_webhook "failed" "$ERROR_MSG"
|
||
exit 0
|
||
fi
|
||
echo " Images vorhanden ✓"
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [6/12] VM VORBEREITEN
|
||
# unlock → stop → cdrom/ide0 entfernen → alle Netzwerkkarten (net0-net10)
|
||
# löschen → Agent aktivieren.
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [6/12] VM vorbereiten..."
|
||
qm unlock "$VM_ID_RESTORED" 2>/dev/null || true
|
||
qm stop "$VM_ID_RESTORED" 2>/dev/null || true
|
||
sleep 3
|
||
|
||
qm set "$VM_ID_RESTORED" -delete cdrom 2>/dev/null || true
|
||
qm set "$VM_ID_RESTORED" -delete ide0 2>/dev/null || true
|
||
|
||
for ((net=0; net<=10; net++)); do
|
||
qm set "$VM_ID_RESTORED" -delete "net${net}" 2>/dev/null || true
|
||
done
|
||
|
||
qm set "$VM_ID_RESTORED" --agent "enabled=1,type=virtio" 2>/dev/null || true
|
||
echo " VM vorbereitet (Netzwerkkarten entfernt, Agent aktiviert)."
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [7/12] VM STARTEN & QM-AGENT PRÜFEN
|
||
# 120s in 10s-Schritten. Agent über QEMU Guest Agent Channel, kein Netzwerk.
|
||
# Kein Agent = KEIN Abbruch, qm_agent_ok=false in DB.
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [7/12] Starte VM & warte auf qm-Agent (max. 120s)..."
|
||
qm start "$VM_ID_RESTORED" 2>/dev/null || true
|
||
|
||
AGENT_WAIT=0
|
||
AGENT_MAX=120
|
||
AGENT_INTERVAL=10
|
||
QM_AGENT_OK="false"
|
||
|
||
while [[ $AGENT_WAIT -lt $AGENT_MAX ]]; do
|
||
sleep $AGENT_INTERVAL
|
||
AGENT_WAIT=$(( AGENT_WAIT + AGENT_INTERVAL ))
|
||
echo -n " [${AGENT_WAIT}s/${AGENT_MAX}s] qm-Agent... "
|
||
if qm agent "$VM_ID_RESTORED" ping 2>/dev/null | grep -qi "pong\|ping"; then
|
||
QM_AGENT_OK="true"
|
||
echo "ONLINE ✓"
|
||
hostname_info=$(qm agent "$VM_ID_RESTORED" get-host-name 2>/dev/null \
|
||
| grep host-name | tr -d '"' || true)
|
||
fs_info=$(qm agent "$VM_ID_RESTORED" get-fsinfo 2>/dev/null \
|
||
| grep mountpoint | tr -d '"' || true)
|
||
echo " Hostname: ${hostname_info:-unbekannt}"
|
||
echo " Mountpoints: ${fs_info:-unbekannt}"
|
||
break
|
||
else
|
||
echo "nicht erreichbar."
|
||
fi
|
||
done
|
||
|
||
if [[ "$QM_AGENT_OK" == "false" ]]; then
|
||
echo " qm-Agent nach ${AGENT_MAX}s nicht erreichbar."
|
||
echo " → qm_agent_ok=false in DB, Restore läuft weiter."
|
||
fi
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [8/12] VM STOPPEN
|
||
# Sauberes Shutdown, nach 30s force-stop.
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [8/12] Stoppe VM..."
|
||
qm shutdown "$VM_ID_RESTORED" 2>/dev/null || true
|
||
sleep 30
|
||
qm stop "$VM_ID_RESTORED" --skiplock 1 2>/dev/null || true
|
||
echo " VM gestoppt."
|
||
sleep 5
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [9/12] 7Z-ARCHIV ERSTELLEN
|
||
# VM-Images aus VM_IMAGE_DIR/* zippen.
|
||
# VM-Name aus qemu-server.conf lesen → ZIP bekommt echten Namen.
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [9/12] Erstelle verschlüsseltes 7z-Archiv..."
|
||
|
||
VM_CONF="${VM_IMAGE_DIR}/qemu-server.conf"
|
||
VM_NAME=$(grep -m1 "^name:" "$VM_CONF" 2>/dev/null \
|
||
| awk -F': ' '{print $2}' | tr -d '[:space:]' \
|
||
|| echo "$SAFE_CLIENT")
|
||
echo " VM-Name: $VM_NAME"
|
||
|
||
ZIP_FILE="${ZIP_DIR}/${VM_NAME}-${VM_ID_ORIGINAL}.7z"
|
||
ZIP_START=$(date +%s)
|
||
|
||
7z a -t7z \
|
||
-mmt=4 \
|
||
-mx=1 \
|
||
-md=16M \
|
||
-p"${ZIP_PASSWORD}" \
|
||
-mhe=on \
|
||
"$ZIP_FILE" \
|
||
"${VM_IMAGE_DIR}/"* \
|
||
2>&1 | tail -5
|
||
|
||
ZIP_DURATION=$(( $(date +%s) - ZIP_START ))
|
||
ZIP_SIZE_BYTES=$(stat -c%s "$ZIP_FILE" 2>/dev/null || echo "0")
|
||
echo " ZIP: $(( ZIP_SIZE_BYTES / 1024 / 1024 )) MB in ${ZIP_DURATION}s"
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [10/12] RSYNC ZUM BACKUP-SERVER
|
||
# Zielverzeichnis aus DB (rsync_target) = Firmen-spezifischer Pfad.
|
||
# 3 Versuche mit 60s Pause + Größenvergleich lokal vs. remote.
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
# Rsync-Ziel mit Datumsordner
|
||
RSYNC_TARGET_DATE="${RSYNC_TARGET}/${LAST_DATE}"
|
||
echo "==> [10/12] Rsync → ${BACKUP_SERVER_HOST}:${RSYNC_TARGET_DATE}..."
|
||
MAX_RETRIES=3
|
||
|
||
rsync_transfer() {
|
||
rsync -avz --progress --timeout=300 \
|
||
"$ZIP_FILE" \
|
||
"${BACKUP_SERVER_HOST}:${RSYNC_TARGET_DATE}/" \
|
||
2>&1
|
||
}
|
||
|
||
ssh "$BACKUP_SERVER_HOST" "mkdir -p '${RSYNC_TARGET_DATE}'" 2>/dev/null || true
|
||
|
||
while [[ $RSYNC_RETRIES -lt $MAX_RETRIES ]]; do
|
||
if rsync_transfer; then
|
||
RSYNC_OK="true"
|
||
RSYNC_SIZE_BYTES=$ZIP_SIZE_BYTES
|
||
echo " Rsync OK: $(( RSYNC_SIZE_BYTES / 1024 / 1024 )) MB"
|
||
break
|
||
else
|
||
RSYNC_RETRIES=$(( RSYNC_RETRIES + 1 ))
|
||
if [[ $RSYNC_RETRIES -lt $MAX_RETRIES ]]; then
|
||
echo " Fehlgeschlagen ($RSYNC_RETRIES/$MAX_RETRIES). Warte 60s..."
|
||
sleep 60
|
||
else
|
||
RSYNC_OK="false"
|
||
STATUS="failed"
|
||
ERROR_MSG="Rsync fehlgeschlagen nach ${RSYNC_RETRIES} Versuchen"
|
||
echo " FEHLER: $ERROR_MSG"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
if [[ "$RSYNC_OK" == "true" ]]; then
|
||
REMOTE_SIZE=$(ssh "$BACKUP_SERVER_HOST" \
|
||
"stat -c%s '${RSYNC_TARGET_DATE}/$(basename "$ZIP_FILE")'" \
|
||
2>/dev/null || echo "0")
|
||
if [[ "$REMOTE_SIZE" != "$ZIP_SIZE_BYTES" ]]; then
|
||
echo " WARNUNG: Remote ${REMOTE_SIZE}B != lokal ${ZIP_SIZE_BYTES}B"
|
||
RSYNC_OK="false"
|
||
STATUS="failed"
|
||
ERROR_MSG="Größenabweichung: lokal=${ZIP_SIZE_BYTES} remote=${REMOTE_SIZE}"
|
||
else
|
||
echo " Größenprüfung OK: ${REMOTE_SIZE} Bytes."
|
||
fi
|
||
fi
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# [11/12] AUFRÄUMEN
|
||
# VM destroy inkl. Disks, lokale ZIP löschen.
|
||
# Keys bleiben gecacht für nächste VM desselben Datastores.
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo "==> [11/12] Aufräumen..."
|
||
qm destroy "$VM_ID_RESTORED" \
|
||
--destroy-unreferenced-disks 1 \
|
||
--purge 1 \
|
||
2>/dev/null || echo " VM $VM_ID_RESTORED nicht mehr vorhanden."
|
||
rm -f "$ZIP_FILE"
|
||
echo " VM ${VM_ID_RESTORED} entfernt, ZIP gelöscht."
|
||
echo " Keys gecacht in $KEY_DIR"
|
||
|
||
# ── Zusammenfassung & Webhook ─────────────────────────────────────────────────
|
||
TOTAL=$(( $(date +%s) - RESTORE_START ))
|
||
echo ""
|
||
echo "============================================================"
|
||
echo " Status: $STATUS"
|
||
echo " Gesamtdauer: ${TOTAL}s"
|
||
echo " VM-Name: ${VM_NAME:-$SAFE_CLIENT}"
|
||
echo " VM_IMAGE_DIR: $VM_IMAGE_DIR"
|
||
echo " Datastore: $DATASTORE"
|
||
echo " qm-Agent: $QM_AGENT_OK"
|
||
echo " Rsync: $RSYNC_OK (Versuche: $RSYNC_RETRIES)"
|
||
echo " ZIP: $(( ZIP_SIZE_BYTES / 1024 / 1024 )) MB"
|
||
[[ -n "$ERROR_MSG" ]] && echo " Fehler: $ERROR_MSG"
|
||
echo "============================================================"
|
||
|
||
trap - ERR
|
||
send_webhook "$STATUS" "$ERROR_MSG" |