Initial commit.

This commit is contained in:
root
2025-10-12 13:23:48 +02:00
commit ca2b6731e0
5 changed files with 863 additions and 0 deletions

408
kvm-lvm-full-restore.sh Executable file
View File

@@ -0,0 +1,408 @@
#!/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