Files
kvm/kvm-lvm-full-restore.sh
2025-10-12 13:23:48 +02:00

409 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -Eeuo pipefail
###############################################################################
# kvm-lvm-full-restore.sh
#
# Stellt ein Vollbackup (erstellt mit dem passenden Backup-Skript) auf ein
# Ziel-LVM zurück. Identische UI (Farben/Spinner/Quiet), Konfig-Lokation,
# NFS-/Mount-Checks, Logging. Interaktive Auswahl listet NUR echte Backups:
# (*.img | *.img.{zst,xz,gz}) KEINE *.sha256.
#
# Ablauf:
# 1) Config laden (conf/<skriptprefix>.conf relativ zum Skript)
# 2) CLI-Argumente parsen (haben Vorrang vor Config)
# 3) NFS-/Mount-Checks
# 4) Backup-Liste anzeigen → Auswahl per Index ODER Dateipfad einfügen
# 5) optional SHA256-Check
# 6) VM sauber stoppen (virsh), ggf. force mit --yes
# 7) Dekomprimieren → dd ins Ziel-LV
# 8) Dauer & Ergebnis loggen, TTY-Ausgabe farbig und übersichtlich
#
# Aufruf:
# kvm-lvm-full-restore.sh → interaktive Auswahl & Restore
# kvm-lvm-full-restore.sh --list → nur auflisten
# kvm-lvm-full-restore.sh --restore=<FILE> → genau diese Datei einspielen
# kvm-lvm-full-restore.sh --target-lv=<DEV> → Ziel-LV überschreiben
# kvm-lvm-full-restore.sh --yes → keine Rückfragen (force allowed)
# kvm-lvm-full-restore.sh --no-verify → SHA256-Check überspringen
# kvm-lvm-full-restore.sh --verify → SHA256-Check erzwingen (Default)
# kvm-lvm-full-restore.sh -h|--help|-help|help → Hilfe
###############################################################################
# ── Script meta ─────────────────────────────────────────────────────────────
SCRIPT_PATH="$(readlink -f "$0")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
SCRIPT_NAME="$(basename "$SCRIPT_PATH")"
SCRIPT_PREFIX="${SCRIPT_NAME%.*}"
CONF_DIR="${SCRIPT_DIR}/conf"
CONF_FILE="${CONF_DIR}/${SCRIPT_PREFIX}.conf"
# ── Help vorab definieren (wird beim Arg-Parsing gebraucht) ────────────────
show_help() {
cat <<EOF
Usage: $SCRIPT_NAME [options]
Stellt ein Vollbackup (*.img[.<ext>]) auf ein Ziel-LVM zurück.
Optionen:
-h, --help, -help, help Zeigt diese Hilfe.
--list Listet verfügbare Backups (neueste zuerst).
--restore=<FILE> Spielt genau diese Backup-Datei zurück.
--target-lv=<DEV> Ziel-LV (Blockdevice) überschreiben.
--yes Keine Rückfragen (Bestätigungen auto-OK, force-off erlaubt).
--no-verify Überspringt SHA256-Prüfung.
--verify Erzwingt SHA256-Prüfung (Standard).
Ablauf:
1) Auswahl Backup (Index oder Pfad), optional SHA-Check.
2) VM stoppen (virsh shutdown, Timeout), ggf. force wenn --yes.
3) <decompress> | dd of=<ZIEL-LV> bs=<DD_BS> (status=progress in TTY).
4) Dauer & Ergebnis ins Log; farbige UI im Terminal.
Wichtig:
- Das Ziel-LV wird überschrieben.
- Stelle sicher, dass die VM gestoppt ist und das Volume korrekt ist.
EOF
}
# ── Defaults (können durch .conf überschrieben werden) ─────────────────────
VM_NAME="${VM_NAME:-WinServer2025}"
LV_TARGET="${LV_TARGET:-/dev/VG-Windows-Server/WinSystem}"
BACKUP_DIR="${BACKUP_DIR:-/data/backup/WinServer2025}"
FILE_PREFIX="${FILE_PREFIX:-WinSystem_full}"
DD_BS="${DD_BS:-4M}"
QUIET="${QUIET:-0}"
VERIFY_SHA="${VERIFY_SHA:-1}"
REQUIRE_MOUNTED="${REQUIRE_MOUNTED:-1}"
REQUIRE_NFS_TYPE="${REQUIRE_NFS_TYPE:-0}"
# ── Config ZUERST laden (damit CLI-Argumente Vorrang haben) ────────────────
if [[ -f "$CONF_FILE" ]]; then
# shellcheck disable=SC1090
source "$CONF_FILE"
fi
# ── CLI-Argumente parsen (überschreiben Config) ────────────────────────────
RESTORE_FILE=""
YES=0
LIST_ONLY=0
for arg in "$@"; do
case "$arg" in
--list) LIST_ONLY=1 ;;
--restore=*) RESTORE_FILE="${arg#--restore=}" ;;
--target-lv=*) LV_TARGET="${arg#--target-lv=}" ;;
--yes) YES=1 ;;
--no-verify) VERIFY_SHA=0 ;;
--verify) VERIFY_SHA=1 ;;
-h|--help|-help|help) show_help; exit 0 ;;
*) ;;
esac
done
# ── Timestamps / Log ───────────────────────────────────────────────────────
DATE="$(date +%Y%m%d-%H%M%S)"
LOG_FILE="${BACKUP_DIR}/${FILE_PREFIX}_restore_${DATE}.log"
START_TS=$(date +%s)
# ── Farbe & UI ─────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
QUIET="${QUIET}"
else
QUIET=1
fi
if [[ -t 1 ]]; then
RST=$'\033[0m'; WHT=$'\033[37m'; GRN=$'\033[32m'; RED=$'\033[31m'; YEL=$'\033[33m'
else
RST=""; WHT=""; GRN=""; RED=""; YEL=""
fi
LBL_OK="${WHT}[ ${GRN}OK${WHT} ]${RST}"
LBL_DONE="${WHT}[ ${GRN}Done${WHT} ]${RST}"
LBL_ERR="${WHT}[ ${RED}Error${WHT} ]${RST}"
LBL_WARN="${WHT}[ ${YEL}warn${WHT} ]${RST}"
spinner() {
local pid="$1" msg="$2" i=0 marks='|/-\'
while kill -0 "$pid" 2>/dev/null; do
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
printf "\r${WHT}[ %s ${WHT}]%s%s" "${marks:i%4:1}" "${RST} " "$msg"
i=$(( (i+1) % 4 ))
fi
sleep 0.1
done
if [[ "$QUIET" -eq 0 && -t 1 ]]; then printf "\r\033[K"; fi
}
# ── Logging helpers ────────────────────────────────────────────────────────
have_cmd() { command -v "$1" >/dev/null 2>&1; }
log() { echo "[$(date +%F\ %T)] $*" >>"$LOG_FILE"; }
out() { [[ "$QUIET" -eq 0 ]] && echo -e "$*"; log "$*"; }
warn() { echo -e "${LBL_WARN} $*" >&2; log "WARN: $*"; }
die() { echo -e "${LBL_ERR} $*" >&2; log "ERROR: $*"; exit 1; }
run_step() {
local msg="$1"; shift
local cmd="$*"
log "STEP: $msg :: $cmd"
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
echo -n "$msg ... "
else
log "$msg ..."
fi
bash -c "$cmd" >>"$LOG_FILE" 2>&1 &
local pid=$!
spinner "$pid" "$msg"
if ! wait "$pid"; then
echo -e "${LBL_ERR} $msg"
die "$msg fehlgeschlagen. Details: $LOG_FILE"
fi
if [[ "$QUIET" -eq 0 && -t 1 ]]; then echo -e "${LBL_OK} $msg"; fi
log "OK: $msg"
}
cleanup() { :; }
trap cleanup EXIT
# ── Checks: BACKUP_DIR mount/NFS ───────────────────────────────────────────
ensure_backup_source() {
[[ -d "$BACKUP_DIR" ]] || die "BACKUP_DIR existiert nicht: ${BACKUP_DIR}"
if [[ "${REQUIRE_MOUNTED}" -eq 1 ]]; then
if ! mountpoint -q "$BACKUP_DIR"; then
if ! findmnt -n "$BACKUP_DIR" >/dev/null 2>&1; then
die "BACKUP_DIR ist nicht gemountet: ${BACKUP_DIR}"
fi
fi
fi
if [[ "${REQUIRE_NFS_TYPE}" -eq 1 ]]; then
local fstype
fstype="$(findmnt -no FSTYPE "$BACKUP_DIR" 2>/dev/null || true)"
[[ "$fstype" == "nfs" || "$fstype" == "nfs4" ]] || die "BACKUP_DIR ist kein NFS-Mount (FSTYPE=$fstype)."
fi
}
# ^^^ falls deine Shell 'end' nicht kennt, bitte durch 'fi' ersetzen
# ── Gültige Backup-Dateien finden (ohne .sha256) ───────────────────────────
get_backup_files() {
BACKUP_FILES=()
shopt -s nullglob
local candidates=(
"${BACKUP_DIR}/${FILE_PREFIX}_"*.img
"${BACKUP_DIR}/${FILE_PREFIX}_"*.img.zst
"${BACKUP_DIR}/${FILE_PREFIX}_"*.img.xz
"${BACKUP_DIR}/${FILE_PREFIX}_"*.img.gz
)
shopt -u nullglob
local existing=()
for f in "${candidates[@]}"; do
[[ -f "$f" ]] && existing+=( "$f" )
done
((${#existing[@]})) || return 1
IFS=$'\n' BACKUP_FILES=($(ls -1t "${existing[@]}")) ; unset IFS
return 0
}
# ── Liste anzeigen ─────────────────────────────────────────────────────────
list_backups() {
ensure_backup_source
if ! get_backup_files; then
echo "Keine Backups gefunden in: ${BACKUP_DIR}"
return 1
fi
printf "%s\n" "Verfügbare Backups (neueste zuerst):"
local idx=1
for f in "${BACKUP_FILES[@]}"; do
local size date
if stat --version >/dev/null 2>&1; then
size="$(stat -c %s "$f")"
date="$(stat -c %y "$f" | cut -d'.' -f1)"
else
size="$(stat -f %z "$f")"
date="$(stat -f %Sm -t '%Y-%m-%d %H:%M:%S' "$f")"
fi
local hsize; hsize="$(numfmt --to=iec --suffix=B "$size" 2>/dev/null || echo "${size}B")"
printf " %2d) %s %8s %s\n" "$idx" "$date" "$hsize" "$f"
idx=$((idx+1))
done
return 0
}
# ── Interaktive Auswahl ────────────────────────────────────────────────────
pick_backup_interactive() {
list_backups || return 1
echo
echo "Gib die Zahl (Index) ein ODER füge den vollständigen Dateipfad ein:"
read -r choice
[[ -n "$choice" ]] || return 1
# kompletter Pfad eingefügt?
if [[ -f "$choice" ]]; then
RESTORE_FILE="$choice"
return 0
fi
# Index gewählt?
if [[ "$choice" =~ ^[0-9]+$ ]]; then
local idx="$choice"
if ! get_backup_files; then return 1; fi
if (( idx>=1 && idx<=${#BACKUP_FILES[@]} )); then
RESTORE_FILE="${BACKUP_FILES[$((idx-1))]}"
return 0
fi
fi
return 1
}
# ── SHA256 check ───────────────────────────────────────────────────────────
sha_check() {
local file="$1"
local sha="${file}.sha256"
if [[ ! -f "$sha" ]]; then
warn "Keine SHA256-Datei gefunden: ${sha} überspringe Verifikation."
return 0
fi
run_step "SHA256 prüfen" "sha256sum -c \"$sha\""
}
# ── VM stoppen ─────────────────────────────────────────────────────────────
stop_vm_safely() {
local name="$1" timeout="${2:-180}"
if ! virsh dominfo "$name" >/dev/null 2>&1; then
warn "VM ${name} ist in libvirt nicht definiert überspringe Shutdown."
return 0
fi
local state; state="$(virsh domstate "$name" 2>/dev/null || true)"
case "$state" in
running|blocked|pmsuspended|paused)
out "VM ${name} wird sauber heruntergefahren ..."
if ! virsh shutdown "$name" >>"$LOG_FILE" 2>&1; then
warn "virsh shutdown fehlgeschlagen."
fi
local t=0
while [[ $t -lt $timeout ]]; do
state="$(virsh domstate "$name" 2>/dev/null || true)"
[[ "$state" == "shut off" || "$state" == "shutoff" ]] && break
sleep 2; t=$((t+2))
done
if [[ "$state" != "shut off" && "$state" != "shutoff" ]]; then
if [[ $YES -eq 1 ]]; then
warn "Timeout force off via virsh destroy."
virsh destroy "$name" >>"$LOG_FILE" 2>&1 || warn "virsh destroy fehlgeschlagen."
else
die "VM ist noch aktiv (Zustand: $state). Starte erneut mit --yes, um force-off zu erlauben."
fi
fi
;;
"shut off"|"shutoff"|crashed|nostate)
out "VM ${name} ist nicht aktiv (state: $state)."
;;
*)
warn "Unbekannter VM-State: '$state' fahre fort."
;;
esac
}
# ── Dekompression bestimmen ────────────────────────────────────────────────
decomp_cmd_for() {
local file="$1"
case "$file" in
*.zst) echo "zstd -d -c" ;;
*.xz) echo "unxz -c" ;;
*.gz) echo "gzip -d -c" ;;
*.img) echo "cat" ;;
*) echo "" ;;
esac
}
# ── MAIN ───────────────────────────────────────────────────────────────────
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
echo -e "${WHT}==>${RST} VM-Restore gestartet VM=${VM_NAME} Target=${LV_TARGET}"
echo -e "${WHT}==>${RST} Backups aus: ${BACKUP_DIR}"
echo
fi
log "Begin Restore: VM=${VM_NAME} Target=${LV_TARGET} BACKUP_DIR=${BACKUP_DIR} VERIFY_SHA=${VERIFY_SHA}"
ensure_backup_source
# Backup auswählen / listen
if [[ -z "$RESTORE_FILE" && $LIST_ONLY -eq 1 ]]; then
list_backups; exit $?
fi
if [[ -z "$RESTORE_FILE" ]]; then
if [[ "$QUIET" -eq 1 ]]; then
die "Keine Backup-Datei angegeben. Nutze --restore=<FILE> oder --list (Quiet-Modus)."
fi
if ! pick_backup_interactive; then
die "Keine gültige Auswahl getroffen."
fi
fi
[[ -r "$RESTORE_FILE" ]] || die "Backup-Datei nicht lesbar: ${RESTORE_FILE}"
[[ -b "$LV_TARGET" ]] || die "Ziel-LV nicht gefunden: ${LV_TARGET}"
# Bestätigung (interaktiv)
if [[ $YES -ne 1 && "$QUIET" -eq 0 && -t 1 ]]; then
echo
echo "Achtung: ${LV_TARGET} wird ÜBERSCHRIEBEN."
echo "Backup: ${RESTORE_FILE}"
read -r -p "Fortsetzen? (yes/NO): " ack
[[ "$ack" == "yes" ]] || die "Abgebrochen."
fi
# SHA prüfen / überspringen
if [[ $VERIFY_SHA -eq 1 ]]; then
sha_check "$RESTORE_FILE"
else
warn "SHA-Prüfung deaktiviert (--no-verify)."
fi
# VM stoppen
stop_vm_safely "$VM_NAME" 180
# Dekompressor bestimmen
DECOMP="$(decomp_cmd_for "$RESTORE_FILE")"
[[ -n "$DECOMP" ]] || die "Unbekannte Dateiendung, bitte gültige Backup-Datei angeben."
# Restore durchführen
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
out "Restore läuft -> dies kann je nach Größe/IO dauern ..."
fi
# Status=progress nur im TTY anzeigen, sonst leise
if [[ -t 1 ]]; then
run_step "Backup dekomprimieren & schreiben" \
"$DECOMP \"$RESTORE_FILE\" | dd of=\"$LV_TARGET\" bs=\"$DD_BS\" status=progress"
else
run_step "Backup dekomprimieren & schreiben" \
"$DECOMP \"$RESTORE_FILE\" | dd of=\"$LV_TARGET\" bs=\"$DD_BS\" status=none"
fi
# Dauer berechnen
END_TS=$(date +%s)
DURATION=$(( END_TS - START_TS ))
D_H=$(( DURATION / 3600 ))
D_M=$(( (DURATION % 3600) / 60 ))
D_S=$(( DURATION % 60 ))
log "Restore-Dauer: ${D_H}h ${D_M}m ${D_S}s (${DURATION}s)"
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
echo
echo -e "${LBL_OK} Restore abgeschlossen."
echo -e "${WHT} Quelle:${RST} ${RESTORE_FILE}"
echo -e "${WHT} Ziel :${RST} ${LV_TARGET}"
echo -e "${WHT} Dauer :${RST} ${D_H}h ${D_M}m ${D_S}s"
echo
echo "Hinweis:"
echo " - Falls die VM-Definition neu eingespielt werden soll:"
echo " virsh define \"${RESTORE_FILE%.img*}.xml\""
echo " - Danach starten:"
echo " virsh start ${VM_NAME}"
fi
exit 0