#!/usr/bin/env bash # libvirt-restore.sh # Restore eines mit libvirt-backup.sh erzeugten Offline-Backups. set -Eeuo pipefail IFS=$'\n\t' umask 077 URI="${LIBVIRT_URI:-qemu:///system}" BACKUP_ROOT="${1:-}" OVERWRITE_FILES="${OVERWRITE_FILES:-0}" REPLACE_DEFINITIONS="${REPLACE_DEFINITIONS:-0}" RESTORE_INTERFACES="${RESTORE_INTERFACES:-0}" START_NETWORKS="${START_NETWORKS:-1}" START_POOLS="${START_POOLS:-1}" AUTOSTART_OBJECTS="${AUTOSTART_OBJECTS:-0}" RESTORE_PAW_NETWORK="${RESTORE_PAW_NETWORK:-0}" APPLY_PAW_NETWORK="${APPLY_PAW_NETWORK:-0}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { cat <<'EOF' Aufruf: sudo ./libvirt-restore.sh /pfad/zum/backup Umgebungsvariablen: LIBVIRT_URI=qemu:///system libvirt-Verbindung OVERWRITE_FILES=1 vorhandene Image-/NVRAM-/TPM-Dateien überschreiben REPLACE_DEFINITIONS=1 vorhandene gleichnamige Domains/Netze/Pools ersetzen RESTORE_INTERFACES=1 exportierte libvirt-Host-Interfaces definieren START_NETWORKS=0 importierte Netzwerke nicht starten START_POOLS=0 importierte Pools nicht starten AUTOSTART_OBJECTS=1 importierte Domains/Netze/Pools auf Autostart setzen RESTORE_PAW_NETWORK=1 NetworkManager/PAW-Konfiguration und MAC-Zuordnung restaurieren APPLY_PAW_NETWORK=1 NetworkManager danach neu starten (nur lokal/mit Konsole) Standardmäßig werden vorhandene Dateien und Definitionen NICHT überschrieben. Die Dateien werden an ihre ursprünglichen absoluten Pfade zurückkopiert. EOF } die() { echo "FEHLER: $*" >&2; exit 1; } warn() { echo "WARNUNG: $*" >&2; } log() { printf '\n==> %s\n' "$*"; } [[ $EUID -eq 0 ]] || die "Bitte als root ausführen." [[ -n "$BACKUP_ROOT" ]] || { usage; exit 2; } BACKUP_ROOT="$(readlink -m "$BACKUP_ROOT")" for cmd in virsh rsync sha256sum python3 find awk sed; do command -v "$cmd" >/dev/null || die "Benötigtes Programm fehlt: $cmd" done [[ -d "$BACKUP_ROOT/domains" ]] || die "Kein gültiges Backup: domains/ fehlt." [[ -f "$BACKUP_ROOT/metadata/file-map.tsv" ]] || die "Kein gültiges Backup: file-map.tsv fehlt." mkdir -p "$BACKUP_ROOT/logs" exec > >(tee -a "$BACKUP_ROOT/logs/restore.log") 2>&1 log "Prüfe Backup-Prüfsummen" ( cd "$BACKUP_ROOT" sha256sum -c metadata/SHA256SUMS ) log "Prüfe libvirt-Verbindung $URI" virsh -c "$URI" uri >/dev/null if [[ -s "$BACKUP_ROOT/metadata/block-sources.txt" ]]; then warn "Das Backup referenziert nicht enthaltene Blockgeräte:" sed 's/^/ - /' "$BACKUP_ROOT/metadata/block-sources.txt" warn "Diese müssen vor dem Definieren/Starten der VMs separat wiederhergestellt werden." fi log "Stelle referenzierte Dateien an ihren Originalpfaden wieder her" while IFS=$'\t' read -r kind original relative; do [[ -n "${original:-}" && -n "${relative:-}" ]] || continue src="$BACKUP_ROOT/$relative" dst="$original" [[ -e "$src" ]] || { warn "Backup-Quelle fehlt: $src"; continue; } if [[ -e "$dst" && "$OVERWRITE_FILES" != "1" ]]; then warn "Überspringe vorhandenes Ziel: $dst" continue fi mkdir -p "$(dirname "$dst")" if [[ "$kind" == "directory" ]]; then mkdir -p "$dst" rsync -aHAX --numeric-ids --sparse \ $([[ "$OVERWRITE_FILES" == "1" ]] && printf '%s' '--delete') \ "$src/" "$dst/" else rsync -aHAX --numeric-ids --sparse "$src" "$dst" fi done < "$BACKUP_ROOT/metadata/file-map.tsv" if [[ "$RESTORE_PAW_NETWORK" == "1" ]]; then log "Stelle PAW-Netzwerk und br-lan-MAC wieder her" if [[ -x "$SCRIPT_DIR/paw-network-restore.sh" && -d "$BACKUP_ROOT/network" ]]; then APPLY_NETWORK="$APPLY_PAW_NETWORK" "$SCRIPT_DIR/paw-network-restore.sh" "$BACKUP_ROOT/network" else warn "Netzwerk-Backup oder paw-network-restore.sh fehlt." fi else log "PAW-Netzwerk-Restore übersprungen; RESTORE_PAW_NETWORK=1 würde ihn ausführen" fi define_network() { local xml="$1" name name="$(python3 - "$xml" <<'PY' import sys, xml.etree.ElementTree as ET print((ET.parse(sys.argv[1]).getroot().findtext("name") or "").strip()) PY )" [[ -n "$name" ]] || { warn "Netzwerk ohne Namen: $xml"; return; } if virsh -c "$URI" net-info "$name" >/dev/null 2>&1; then if [[ "$REPLACE_DEFINITIONS" != "1" ]]; then warn "Netzwerk existiert bereits, übersprungen: $name" return fi virsh -c "$URI" net-destroy "$name" >/dev/null 2>&1 || true virsh -c "$URI" net-undefine "$name" fi virsh -c "$URI" net-define "$xml" [[ "$START_NETWORKS" == "1" ]] && virsh -c "$URI" net-start "$name" || true [[ "$AUTOSTART_OBJECTS" == "1" ]] && virsh -c "$URI" net-autostart "$name" || true } define_pool() { local xml="$1" name name="$(python3 - "$xml" <<'PY' import sys, xml.etree.ElementTree as ET print((ET.parse(sys.argv[1]).getroot().findtext("name") or "").strip()) PY )" [[ -n "$name" ]] || { warn "Pool ohne Namen: $xml"; return; } if virsh -c "$URI" pool-info "$name" >/dev/null 2>&1; then if [[ "$REPLACE_DEFINITIONS" != "1" ]]; then warn "Storage-Pool existiert bereits, übersprungen: $name" return fi virsh -c "$URI" pool-destroy "$name" >/dev/null 2>&1 || true virsh -c "$URI" pool-undefine "$name" fi virsh -c "$URI" pool-define "$xml" [[ "$START_POOLS" == "1" ]] && virsh -c "$URI" pool-start "$name" || true [[ "$AUTOSTART_OBJECTS" == "1" ]] && virsh -c "$URI" pool-autostart "$name" || true } define_interface() { local xml="$1" name name="$(python3 - "$xml" <<'PY' import sys, xml.etree.ElementTree as ET r=ET.parse(sys.argv[1]).getroot() print((r.get("name") or r.findtext("name") or "").strip()) PY )" [[ -n "$name" ]] || { warn "Interface ohne Namen: $xml"; return; } if virsh -c "$URI" iface-info "$name" >/dev/null 2>&1; then if [[ "$REPLACE_DEFINITIONS" != "1" ]]; then warn "Interface existiert bereits, übersprungen: $name" return fi virsh -c "$URI" iface-undefine "$name" fi virsh -c "$URI" iface-define "$xml" } define_domain() { local xml="$1" name name="$(python3 - "$xml" <<'PY' import sys, xml.etree.ElementTree as ET print((ET.parse(sys.argv[1]).getroot().findtext("name") or "").strip()) PY )" [[ -n "$name" ]] || { warn "Domain ohne Namen: $xml"; return; } if virsh -c "$URI" dominfo "$name" >/dev/null 2>&1; then if [[ "$REPLACE_DEFINITIONS" != "1" ]]; then warn "Domain existiert bereits, übersprungen: $name" return fi state="$(virsh -c "$URI" domstate "$name" | head -n1 | tr '[:upper:]' '[:lower:]')" case "$state" in "shut off"|"abgeschaltet"|"ausgeschaltet") ;; *) die "Domain $name läuft noch; vor dem Ersetzen herunterfahren." ;; esac virsh -c "$URI" undefine "$name" --nvram --managed-save --snapshots-metadata \ 2>/dev/null || virsh -c "$URI" undefine "$name" fi virsh -c "$URI" define "$xml" [[ "$AUTOSTART_OBJECTS" == "1" ]] && virsh -c "$URI" autostart "$name" || true } log "Definiere virtuelle Netzwerke" shopt -s nullglob for xml in "$BACKUP_ROOT"/networks/*.xml; do define_network "$xml"; done log "Definiere Storage-Pools" for xml in "$BACKUP_ROOT"/pools/*.xml; do define_pool "$xml"; done if [[ "$RESTORE_INTERFACES" == "1" ]]; then log "Definiere libvirt-Host-Interfaces" warn "Netzwerkabbrüche sind möglich, wenn Bridge-/Bond-Konfigurationen aktiviert werden." for xml in "$BACKUP_ROOT"/interfaces/*.xml; do define_interface "$xml"; done else log "Überspringe Host-Interfaces; RESTORE_INTERFACES=1 würde sie definieren" fi log "Prüfe Domain-XMLs auf fehlende lokale Quellen" python3 - "$BACKUP_ROOT" <<'PY' import os, sys, glob import xml.etree.ElementTree as ET root=sys.argv[1] missing=[] for xp in glob.glob(os.path.join(root, "domains", "*.xml")): dom=ET.parse(xp).getroot() name=dom.findtext("name") or os.path.basename(xp) for disk in dom.findall("./devices/disk"): src=disk.find("source") if src is None: continue p=src.get("file") or src.get("dev") if p and not os.path.exists(p): missing.append((name, p)) n=dom.find("./os/nvram") if n is not None and n.text and n.text.strip() and not os.path.exists(n.text.strip()): missing.append((name, n.text.strip())) if missing: print("WARNUNG: Fehlende Quellen:", file=sys.stderr) for name,p in missing: print(f" - {name}: {p}", file=sys.stderr) PY log "Definiere Domains" for xml in "$BACKUP_ROOT"/domains/*.xml; do define_domain "$xml"; done log "Restore abgeschlossen" echo "VMs wurden absichtlich nicht gestartet." echo "Kontrolle:" echo " sudo virsh -c '$URI' list --all" echo " sudo virsh -c '$URI' domblklist VMNAME --details" echo "Danach zunächst eine VM testweise starten:" echo " sudo virsh -c '$URI' start VMNAME"