Initial commit.
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.swp
|
||||||
|
*.log
|
||||||
|
conf/*.conf
|
||||||
31
conf/kvm-lvm-full-backup.conf.sample
Normal file
31
conf/kvm-lvm-full-backup.conf.sample
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# ===== vm-full-backup.conf =====
|
||||||
|
VM_NAME="WinServer2025"
|
||||||
|
LV_ORIG="/dev/VG-Windows-Server/WinSystem"
|
||||||
|
|
||||||
|
# 1 TiB LV: Snapshot-Startwert 150 GiB (bei hoher Last ggf. erhöhen)
|
||||||
|
SNAP_SIZE="150G"
|
||||||
|
|
||||||
|
# Zielordner (kann NFS sein)
|
||||||
|
BACKUP_DIR="/data/backup/${VM_NAME}"
|
||||||
|
|
||||||
|
# Optional: sicherstellen, dass BACKUP_DIR gemountet ist (z.B. NFS)
|
||||||
|
REQUIRE_MOUNTED=0 # 1 = BACKUP_DIR muss gemountet sein
|
||||||
|
REQUIRE_NFS_TYPE=0 # 1 = BACKUP_DIR muss nfs/nfs4 Typ sein
|
||||||
|
|
||||||
|
# Dateiprefix für Backup/Logs/XML
|
||||||
|
FILE_PREFIX="WinSystem_full"
|
||||||
|
|
||||||
|
# Aufbewahrung
|
||||||
|
KEEP=2
|
||||||
|
|
||||||
|
# Speed-orientiert:
|
||||||
|
#
|
||||||
|
# DD_BS="4M" # Größe der von DD auf einmal geschriebenen Blöcke
|
||||||
|
# COMPRESS='cat' # keine Kompression (sehr schnell, aber riesig)
|
||||||
|
#
|
||||||
|
DD_BS="4M"
|
||||||
|
COMPRESS='cat'
|
||||||
|
|
||||||
|
# QUIET wird bei Cron/kein TTY automatisch 1
|
||||||
|
# QUIET=0
|
||||||
|
|
||||||
26
conf/kvm-lvm-full-restore.conf.sample
Normal file
26
conf/kvm-lvm-full-restore.conf.sample
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ===== vm-full-restore.conf =====
|
||||||
|
# libvirt VM-Name (zum Stoppen vor Restore)
|
||||||
|
VM_NAME="WinServer2025"
|
||||||
|
|
||||||
|
# Ziel-Blockdevice (wird überschrieben!)
|
||||||
|
LV_TARGET="/dev/VG-Windows-Server/WinSystem"
|
||||||
|
|
||||||
|
# Speicherort der Backups (muss die .img.<ext> + .sha256 + .xml enthalten)
|
||||||
|
BACKUP_DIR="/data/backup/${VM_NAME}"
|
||||||
|
|
||||||
|
# Dateiprefix der Backups (muss zum Backup-Skript passen)
|
||||||
|
FILE_PREFIX="WinSystem_full"
|
||||||
|
|
||||||
|
# dd Blocksize (Performance)
|
||||||
|
DD_BS="4M"
|
||||||
|
|
||||||
|
# SHA-Verifikation standardmäßig an (1) oder aus (0)
|
||||||
|
VERIFY_SHA=0
|
||||||
|
|
||||||
|
# Quiet wird automatisch 1 ohne TTY
|
||||||
|
# QUIET=0
|
||||||
|
|
||||||
|
# NFS/Mount-Schutz
|
||||||
|
REQUIRE_MOUNTED=0 # 1: BACKUP_DIR muss gemountet sein
|
||||||
|
REQUIRE_NFS_TYPE=0 # 1: FSTYPE muss nfs/nfs4 sein
|
||||||
|
|
||||||
395
kvm-lvm-full-backup.sh
Executable file
395
kvm-lvm-full-backup.sh
Executable file
@@ -0,0 +1,395 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# kvm-lvm-full-backup.sh
|
||||||
|
#
|
||||||
|
# Vollbackup einer KVM/libvirt-VM, deren Disk auf einem LVM-Volume liegt.
|
||||||
|
#
|
||||||
|
# Funktionsweise / Ablauf:
|
||||||
|
# 1. Liest Konfigurationsdatei <prefix>.conf aus conf/-Unterordner des Skripts.
|
||||||
|
# 2. Optional: Guest-Freeze (qemu-guest-agent/VSS) zur Dateikonsistenz.
|
||||||
|
# → Nur wenn Guest-Agent via 'guest-ping' erreichbar ist.
|
||||||
|
# 3. Erzeugt LVM-Snapshot des Original-LV (Copy-on-Write).
|
||||||
|
# 4. Exportiert die VM-Definition (XML) in den Backup-Ordner.
|
||||||
|
# 5. Liest den Snapshot blockweise mit dd und komprimiert (zstd/xz/gzip/...).
|
||||||
|
# 6. Schreibt Ergebnis als Vollbackup (*.img[.<ext>]) in BACKUP_DIR.
|
||||||
|
# 7. Testet Archiv (Integrität) und erzeugt SHA256-Hashdatei.
|
||||||
|
# 8. Entfernt Snapshot wieder.
|
||||||
|
# 9. Rotiert ältere Backups (löscht Archiv + SHA + XML + Log gleicher Generation).
|
||||||
|
# 10. Schreibt Laufzeit & Ergebnis ins Log; im Terminal farbige Statusausgabe & Spinner.
|
||||||
|
#
|
||||||
|
# Backup-Typ: Vollsicherung (kompletter Inhalt des LV wird gelesen/gesichert).
|
||||||
|
#
|
||||||
|
# Aufruf:
|
||||||
|
# kvm-lvm-full-backup.sh -> Startet Backup gemäß Konfiguration
|
||||||
|
# kvm-lvm-full-backup.sh -h|--help|-help|help -> Hilfe anzeigen
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
### ── 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"
|
||||||
|
|
||||||
|
# Defaults (werden durch .conf überschrieben)
|
||||||
|
VM_NAME="${VM_NAME:-WinServer2025}"
|
||||||
|
LV_ORIG="${LV_ORIG:-/dev/VG-Windows-Server/WinSystem}"
|
||||||
|
SNAP_SIZE="${SNAP_SIZE:-150G}" # 1 TiB LV → Startwert 150G
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/data/backup/WinServer2025}"
|
||||||
|
FILE_PREFIX="${FILE_PREFIX:-WinSystem_full}"
|
||||||
|
KEEP="${KEEP:-7}"
|
||||||
|
DD_BS="${DD_BS:-4M}" # größer für Speed
|
||||||
|
COMPRESS="${COMPRESS:-zstd -T0 -3}" # Speed-orientiert
|
||||||
|
QUIET="${QUIET:-0}" # bei Cron/kein TTY wird auto=1 gesetzt
|
||||||
|
|
||||||
|
# NFS-Checks (optional)
|
||||||
|
REQUIRE_MOUNTED="${REQUIRE_MOUNTED:-1}" # 1 = BACKUP_DIR muss gemountet sein
|
||||||
|
REQUIRE_NFS_TYPE="${REQUIRE_NFS_TYPE:-0}" # 1 = BACKUP_DIR muss nfs/nfs4 sein
|
||||||
|
|
||||||
|
# Rotation Dry-Run (nur anzeigen, was gelöscht würde)
|
||||||
|
ROTATION_DRY_RUN="${ROTATION_DRY_RUN:-0}"
|
||||||
|
|
||||||
|
# Timestamps / Log
|
||||||
|
DATE="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
LOG_FILE="${BACKUP_DIR}/${FILE_PREFIX}_${DATE}.log"
|
||||||
|
START_TS=$(date +%s)
|
||||||
|
|
||||||
|
### ── Help ──────────────────────────────────────────────────────────────────
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename $0) [options]
|
||||||
|
|
||||||
|
Vollbackup einer libvirt-VM, deren Disk auf einem LVM-Volume liegt.
|
||||||
|
|
||||||
|
Ohne Parameter wird ein vollständiges Backup gemäß Konfigurationsdatei erstellt.
|
||||||
|
|
||||||
|
Optionen:
|
||||||
|
-h, --help, -help, help Zeigt diese Hilfe und beendet das Programm.
|
||||||
|
|
||||||
|
Ablauf:
|
||||||
|
1. Liest Konfigurationsdatei (conf/<skriptprefix>.conf).
|
||||||
|
2. Guest-Agent Probe ('guest-ping'); optional Freeze/Thaw.
|
||||||
|
3. Erzeugt LVM-Snapshot (Copy-on-Write).
|
||||||
|
4. Exportiert VM-XML in BACKUP_DIR.
|
||||||
|
5. Liest Snapshot blockweise (dd) und komprimiert (zstd/xz/gzip/...).
|
||||||
|
6. Schreibt Vollbackup (*.img[.<ext>]) nach BACKUP_DIR.
|
||||||
|
7. Testet Archiv und erzeugt SHA256-Hash.
|
||||||
|
8. Entfernt Snapshot.
|
||||||
|
9. Rotiert alte Backups inkl. XML & Log gleicher Generation.
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
- Backup-Image (*.img bzw. *.img.<ext>) im BACKUP_DIR
|
||||||
|
- SHA256-Hash (*.sha256)
|
||||||
|
- VM-XML (*.xml)
|
||||||
|
- Logdatei im Skriptordner
|
||||||
|
|
||||||
|
Restore-Beispiele (Produktiv):
|
||||||
|
lvcreate -L <size> -n WinSystem /dev/VG-Windows-Server
|
||||||
|
sha256sum -c /data/backup/WinServer2025/WinSystem_full_YYYYmmdd-HHMMSS.img.<ext>.sha256
|
||||||
|
<decompress> /data/backup/WinServer2025/WinSystem_full_YYYYmmdd-HHMMSS.img.<ext> \\
|
||||||
|
| dd of=/dev/VG-Windows-Server/WinSystem bs=4M status=progress
|
||||||
|
virsh define /data/backup/WinServer2025/WinSystem_full_YYYYmmdd-HHMMSS.xml
|
||||||
|
virsh start WinServer2025
|
||||||
|
|
||||||
|
Probe-Restore (Test ohne Produktiv-Volume zu berühren):
|
||||||
|
lvcreate -L <size> -n WinSystem_TEST /dev/VG-Windows-Server
|
||||||
|
<decompress> /data/backup/WinServer2025/WinSystem_full_YYYYmmdd-HHMMSS.img.<ext> \\
|
||||||
|
| dd of=/dev/VG-Windows-Server/WinSystem_TEST bs=4M status=progress
|
||||||
|
kpartx -av /dev/VG-Windows-Server/WinSystem_TEST
|
||||||
|
mount -o ro -t ntfs3 /dev/mapper/VG--Windows--Server-WinSystem_TESTp2 /mnt/wintest
|
||||||
|
umount /mnt/wintest && kpartx -dv /dev/VG-Windows-Server/WinSystem_TEST && lvremove -f /dev/VG-Windows-Server/WinSystem_TEST
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|-help|help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
### ── Farbe & UI ────────────────────────────────────────────────────────────
|
||||||
|
# Quiet automatisch bei kein TTY
|
||||||
|
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 (Snapshot/Thaw) ───────────────────────────────────────────────
|
||||||
|
cleanup() {
|
||||||
|
# Thaw sicherheitshalber (falls Freeze zuvor OK)
|
||||||
|
if virsh qemu-agent-command "$VM_NAME" '{"execute":"guest-ping"}' >/dev/null 2>&1; then
|
||||||
|
virsh domfsthaw "$VM_NAME" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if [[ -n "${LV_SNAP:-}" ]] && lvdisplay "$LV_SNAP" >/dev/null 2>&1; then
|
||||||
|
lvremove -f "$LV_SNAP" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
### ── Start & Config laden ──────────────────────────────────────────────────
|
||||||
|
if [[ -f "$CONF_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONF_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DATE="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
LOG_FILE="${BACKUP_DIR}/${FILE_PREFIX}_${DATE}.log"
|
||||||
|
|
||||||
|
# NFS/mount-Prüfungen
|
||||||
|
ensure_backup_target() {
|
||||||
|
mkdir -p "$BACKUP_DIR" || die "Kann BACKUP_DIR nicht anlegen/schreiben: ${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} (NFS/Remote offline?)"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kompressions-Endung & Test
|
||||||
|
case "$COMPRESS" in
|
||||||
|
*xz*) EXT="xz"; TEST_CMD=(xz -t) ;;
|
||||||
|
*zstd*) EXT="zst"; TEST_CMD=(zstd -t) ;;
|
||||||
|
*gzip*|*pigz*) EXT="gz"; TEST_CMD=(gzip -t) ;;
|
||||||
|
*cat*) EXT="img"; TEST_CMD=() ;; # keine Kompression → nur .img
|
||||||
|
*) EXT="bin"; TEST_CMD=() ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
OUT_BASENAME="${FILE_PREFIX}_${DATE}"
|
||||||
|
# Dateinamen korrekt bilden: bei COMPRESS='cat' nur .img, sonst .img.<ext>
|
||||||
|
if [[ "$EXT" == "img" ]]; then
|
||||||
|
ARCHIVE="${BACKUP_DIR}/${OUT_BASENAME}.img"
|
||||||
|
else
|
||||||
|
ARCHIVE="${BACKUP_DIR}/${OUT_BASENAME}.img.${EXT}"
|
||||||
|
fi
|
||||||
|
SHA="${ARCHIVE}.sha256"
|
||||||
|
XML="${BACKUP_DIR}/${OUT_BASENAME}.xml"
|
||||||
|
|
||||||
|
### ── Header ────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
|
||||||
|
echo -e "${WHT}==>${RST} Starte Vollbackup: VM=${VM_NAME}, LV=${LV_ORIG}"
|
||||||
|
echo -e "${WHT}==>${RST} Ziel: ${ARCHIVE}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
log "Begin Backup: VM=${VM_NAME} LV=${LV_ORIG} -> ${ARCHIVE}"
|
||||||
|
|
||||||
|
[[ -b "$LV_ORIG" ]] || die "LV nicht gefunden: ${LV_ORIG}"
|
||||||
|
ensure_backup_target
|
||||||
|
|
||||||
|
### ── Guest-Agent robust prüfen & ggf. Freeze ───────────────────────────────
|
||||||
|
agent_ok=0
|
||||||
|
if virsh qemu-agent-command "$VM_NAME" '{"execute":"guest-ping"}' >/dev/null 2>&1; then
|
||||||
|
agent_ok=1
|
||||||
|
fi
|
||||||
|
if (( agent_ok )); then
|
||||||
|
if ! virsh domfsfreeze "$VM_NAME" >>"$LOG_FILE" 2>&1; then
|
||||||
|
warn "domfsfreeze fehlgeschlagen – fahre ohne Freeze fort."
|
||||||
|
else
|
||||||
|
if [[ "$QUIET" -eq 0 && -t 1 ]]; then echo -e "${LBL_DONE} Gast-Dateisystem(e) gefreezt"; fi
|
||||||
|
log "Guest-Freeze OK"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$QUIET" -eq 0 && -t 1 ]]; then echo -e "${LBL_WARN} Guest-Agent nicht erreichbar – ohne Freeze"; fi
|
||||||
|
log "Guest agent ping failed; skipping freeze."
|
||||||
|
fi
|
||||||
|
|
||||||
|
### ── Snapshot erstellen ────────────────────────────────────────────────────
|
||||||
|
SNAP_NAME="$(basename "$LV_ORIG")_snap"
|
||||||
|
LV_SNAP="$(dirname "$LV_ORIG")/${SNAP_NAME}"
|
||||||
|
run_step "LVM-Snapshot erstellen (${SNAP_SIZE})" "lvcreate -L \"$SNAP_SIZE\" -s -n \"$SNAP_NAME\" \"$LV_ORIG\""
|
||||||
|
|
||||||
|
# Sofort Thaw (falls Freeze OK)
|
||||||
|
if (( agent_ok )); then
|
||||||
|
if ! virsh domfsthaw "$VM_NAME" >>"$LOG_FILE" 2>&1; then
|
||||||
|
warn "domfsthaw fehlgeschlagen – fahre fort."
|
||||||
|
else
|
||||||
|
if [[ "$QUIET" -eq 0 && -t 1 ]]; then echo -e "${LBL_DONE} Gast-Dateisystem(e) wieder aufgetaut"; fi
|
||||||
|
log "Guest-Thaw OK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
### ── VM-XML exportieren ────────────────────────────────────────────────────
|
||||||
|
run_step "VM-XML exportieren" "virsh dumpxml \"$VM_NAME\" > \"$XML\""
|
||||||
|
|
||||||
|
### ── Snapshot sichern & komprimieren ───────────────────────────────────────
|
||||||
|
SIZE_BYTES="$(blockdev --getsize64 "$LV_ORIG")"
|
||||||
|
if have_cmd pv; then
|
||||||
|
run_step "Snapshot sichern & komprimieren" \
|
||||||
|
"dd if=\"$LV_SNAP\" bs=\"$DD_BS\" status=none | pv -ptrab -s \"$SIZE_BYTES\" >>\"$LOG_FILE\" 2>&1 | $COMPRESS > \"$ARCHIVE\""
|
||||||
|
else
|
||||||
|
run_step "Snapshot sichern & komprimieren" \
|
||||||
|
"dd if=\"$LV_SNAP\" bs=\"$DD_BS\" status=none | $COMPRESS > \"$ARCHIVE\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
### ── LVM Snapshot entfernen ────────────────────────────────────────────────────
|
||||||
|
run_step "LVM-Snapshot entfernen" "lvremove -f \"$LV_SNAP\""
|
||||||
|
|
||||||
|
### ── Archiv testen & Hash erzeugen ────────────────────────────────────────
|
||||||
|
if ((${#TEST_CMD[@]})); then
|
||||||
|
run_step "Archiv-Integrität prüfen" "${TEST_CMD[*]} \"$ARCHIVE\""
|
||||||
|
else
|
||||||
|
warn "Kein integrierter Test für COMPRESS='$COMPRESS' konfiguriert."
|
||||||
|
fi
|
||||||
|
run_step "SHA256 erzeugen" "sha256sum \"$ARCHIVE\" | tee \"$SHA\" >/dev/null"
|
||||||
|
|
||||||
|
### ── Rotation (Report + Dry-Run) ───────────────────────────────────────────
|
||||||
|
# Bewahre nur die letzten $KEEP Generationen (Basis: FILE_PREFIX_YYYYmmdd-HHMMSS)
|
||||||
|
shopt -s nullglob
|
||||||
|
backups=( "${BACKUP_DIR}/${FILE_PREFIX}_"*.img* )
|
||||||
|
removed_bases=()
|
||||||
|
removed_files=()
|
||||||
|
|
||||||
|
if ((${#backups[@]}>0)); then
|
||||||
|
IFS=$'\n' backups_sorted=($(ls -1t "${backups[@]}")) ; unset IFS
|
||||||
|
|
||||||
|
declare -A seen=()
|
||||||
|
basen=()
|
||||||
|
for f in "${backups_sorted[@]}"; do
|
||||||
|
base="$f"
|
||||||
|
base="${base%.sha256}" # evtl. .sha256 abwerfen
|
||||||
|
base="${base%.*}" # letzte Endung (.zst/.xz/.gz/.img) abwerfen
|
||||||
|
base="${base%.img}" # .img abwerfen → reine Generation
|
||||||
|
base="$(basename "$base")"
|
||||||
|
if [[ -z "${seen[$base]:-}" ]]; then
|
||||||
|
seen[$base]=1
|
||||||
|
basen+=( "$base" )
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ((${#basen[@]}>KEEP)); then
|
||||||
|
for base in "${basen[@]:$KEEP}"; do
|
||||||
|
cand=( \
|
||||||
|
"${BACKUP_DIR}/${base}.img" \
|
||||||
|
"${BACKUP_DIR}/${base}.img."* \
|
||||||
|
"${BACKUP_DIR}/${base}.img."*".sha256" \
|
||||||
|
"${BACKUP_DIR}/${base}.sha256" \
|
||||||
|
"${BACKUP_DIR}/${base}.xml" \
|
||||||
|
"${SCRIPT_DIR}/${base}.log" \
|
||||||
|
)
|
||||||
|
for f in "${cand[@]}"; do
|
||||||
|
[[ -e "$f" ]] || continue
|
||||||
|
if [[ "${ROTATION_DRY_RUN:-0}" -eq 1 ]]; then
|
||||||
|
removed_files+=( "(würde löschen) $f" )
|
||||||
|
log "Rotation (dry-run): würde entfernen: $f"
|
||||||
|
else
|
||||||
|
if rm -f -- "$f"; then
|
||||||
|
removed_files+=( "$f" )
|
||||||
|
log "Rotation: entfernt: $f"
|
||||||
|
else
|
||||||
|
warn "Rotation: konnte nicht löschen: $f"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
removed_bases+=( "$base" )
|
||||||
|
done
|
||||||
|
else
|
||||||
|
log "Rotation: nichts zu entfernen (Generationen=${#basen[@]}, KEEP=$KEEP)."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Rotation: keine Backups gefunden, übersprungen."
|
||||||
|
fi
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
# Zusammenfassung (TTY kurz, Log detailliert)
|
||||||
|
if ((${#removed_bases[@]}>0)); then
|
||||||
|
if [[ "${ROTATION_DRY_RUN:-0}" -eq 1 ]]; then
|
||||||
|
log "Rotation (dry-run): ${#removed_bases[@]} Generation(en) betroffen: ${removed_bases[*]}"
|
||||||
|
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
|
||||||
|
echo -e "${LBL_DONE} Rotation (dry-run): ${#removed_bases[@]} Generation(en) betroffen:"
|
||||||
|
for b in "${removed_bases[@]}"; do echo " - ${b}"; done
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Rotation: ${#removed_bases[@]} Generation(en) entfernt: ${removed_bases[*]}"
|
||||||
|
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
|
||||||
|
echo -e "${LBL_DONE} Rotation: ${#removed_bases[@]} Generation(en) entfernt:"
|
||||||
|
for b in "${removed_bases[@]}"; do echo " - ${b}"; done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Rotation: keine Generation entfernt."
|
||||||
|
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
|
||||||
|
echo -e "${LBL_OK} Rotation: nichts zu entfernen"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
### ── Dauer berechnen & Abschluss ───────────────────────────────────────────
|
||||||
|
END_TS=$(date +%s)
|
||||||
|
DURATION=$(( END_TS - START_TS ))
|
||||||
|
D_H=$(( DURATION / 3600 ))
|
||||||
|
D_M=$(( (DURATION % 3600) / 60 ))
|
||||||
|
D_S=$(( DURATION % 60 ))
|
||||||
|
log "Backup-Dauer: ${D_H}h ${D_M}m ${D_S}s (${DURATION}s)"
|
||||||
|
|
||||||
|
if [[ "$QUIET" -eq 0 && -t 1 ]]; then
|
||||||
|
echo
|
||||||
|
echo -e "${LBL_OK} Backup abgeschlossen: ${ARCHIVE}"
|
||||||
|
echo -e "${WHT} XML:${RST} ${XML}"
|
||||||
|
echo -e "${WHT} SHA:${RST} ${SHA}"
|
||||||
|
echo -e "${WHT} Dauer:${RST} ${D_H}h ${D_M}m ${D_S}s"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
||||||
408
kvm-lvm-full-restore.sh
Executable file
408
kvm-lvm-full-restore.sh
Executable 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
|
||||||
|
|
||||||
Reference in New Issue
Block a user