From e73405ea51d2be8ea14e0a65206e38d18f556904 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 13 Jul 2026 18:40:45 +0000 Subject: [PATCH] Dateien nach "/" hochladen --- README.md | 88 ++++++++++++++- libvirt-backup.sh | 260 +++++++++++++++++++++++++++++++++++++++++++++ libvirt-restore.sh | 235 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 582 insertions(+), 1 deletion(-) create mode 100644 libvirt-backup.sh create mode 100644 libvirt-restore.sh diff --git a/README.md b/README.md index 384060a..a9726e7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,88 @@ -# libvirtbackup +# Libvirt-VM-Backup und Restore +## Voraussetzungen + +Auf dem Ubuntu-Host: + +```bash +sudo apt install rsync libvirt-clients qemu-utils python3 +``` + +Auf dem Debian-Ziel: + +```bash +sudo apt install qemu-system-x86 qemu-utils libvirt-daemon-system \ + libvirt-clients ovmf swtpm swtpm-tools rsync python3 +``` + +## Backup + +1. Windows vollständig herunterfahren. +2. BitLocker-Recovery-Keys separat sichern. +3. Backup-Datenträger einhängen. +4. Skript starten: + +```bash +chmod +x libvirt-backup.sh libvirt-restore.sh +sudo ./libvirt-backup.sh /mnt/backup/libvirt-host +``` + +Prüfen: + +```bash +cd /mnt/backup/libvirt-host +sha256sum -c metadata/SHA256SUMS +cat metadata/block-sources.txt +cat metadata/missing-files.txt +``` + +`block-sources.txt` muss leer sein, sofern keine LVM-/Block-Devices verwendet +werden. Enthaltene Blockgeräte sind zusätzlich separat zu sichern. + +## Restore + +Auf Debian zunächst libvirt/QEMU installieren und den Backup-Datenträger +einbinden: + +```bash +sudo ./libvirt-restore.sh /mnt/backup/libvirt-host +``` + +Standardmäßig überschreibt das Skript keine vorhandenen Dateien oder +libvirt-Definitionen. Für eine bewusst erzwungene Wiederherstellung: + +```bash +sudo OVERWRITE_FILES=1 REPLACE_DEFINITIONS=1 \ + ./libvirt-restore.sh /mnt/backup/libvirt-host +``` + +Autostart für importierte Objekte setzen: + +```bash +sudo AUTOSTART_OBJECTS=1 ./libvirt-restore.sh /mnt/backup/libvirt-host +``` + +Exportierte Host-Interfaces/Bridges werden aus Sicherheitsgründen nicht +automatisch definiert. Das ist bewusst separat: + +```bash +sudo RESTORE_INTERFACES=1 ./libvirt-restore.sh /mnt/backup/libvirt-host +``` + +Bei Bridges ist es häufig besser, die Netzwerkkonfiguration auf Debian mit +NetworkManager oder systemd-networkd neu aufzubauen. + +## Wichtige Grenzen + +- Offline-Backup: Die VMs müssen ausgeschaltet sein. +- File-basierte Images, UEFI-NVRAM und dateibasierte TPM-Zustände werden kopiert. +- LVM-LVs, ZFS-ZVOLs, Ceph/RBD, iSCSI und sonstige Block-/Netzwerk-Storage- + Quellen werden nur dokumentiert. +- Snapshot-Backing-Chains werden nicht automatisch aufgelöst. Referenzierte + Backing-Dateien müssen selbst als Disk-Quelle in der Konfiguration vorkommen + oder zusätzlich gesichert werden. +- Firmwarepfade können sich zwischen Ubuntu und Debian unterscheiden. Bei + OVMF-Fehlern das Domain-XML und die auf Debian installierten Firmwaredateien + prüfen. +- Nach dem Restore zuerst nur eine VM starten und insbesondere UEFI, virtuelles + TPM, Secure Boot, BitLocker und Netzwerk testen. diff --git a/libvirt-backup.sh b/libvirt-backup.sh new file mode 100644 index 0000000..9b8cb85 --- /dev/null +++ b/libvirt-backup.sh @@ -0,0 +1,260 @@ +#!/usr/bin/env bash +# libvirt-backup.sh +# Offline-Backup persistenter libvirt/QEMU-VMs für eine Host-Neuinstallation. +set -Eeuo pipefail +IFS=$'\n\t' +umask 077 + +VERSION="1.0" +URI="${LIBVIRT_URI:-qemu:///system}" +BACKUP_ROOT="${1:-}" +RSYNC_OPTS=(-aHAX --numeric-ids --sparse --info=progress2) +ALLOW_RUNNING=0 + +usage() { + cat <<'EOF' +Aufruf: + sudo ./libvirt-backup.sh /pfad/zum/backup + sudo LIBVIRT_URI=qemu:///system ./libvirt-backup.sh /pfad/zum/backup + +Umgebungsvariablen: + LIBVIRT_URI libvirt-Verbindung, Standard: qemu:///system + ALLOW_RUNNING=1 Unsicher: Backup trotz laufender VMs erlauben + +Das Skript sichert: + - Domain-XMLs + - libvirt-Netzwerk-, Storage-Pool- und Interface-XMLs + - alle file-basierten Disk-/CDROM-/Floppy-Quellen aus den Domain-XMLs + - UEFI-NVRAM-Dateien + - dateibasierte TPM-Backend-Zustände + - qemu-img-Informationen und Prüfsummen + +Blockgeräte wie LVM-LVs werden nur dokumentiert und NICHT kopiert. +EOF +} + +die() { echo "FEHLER: $*" >&2; exit 1; } +warn() { echo "WARNUNG: $*" >&2; } +log() { printf '\n==> %s\n' "$*"; } + +[[ "${ALLOW_RUNNING:-0}" == "1" ]] && ALLOW_RUNNING=1 +[[ $EUID -eq 0 ]] || die "Bitte als root ausführen." +[[ -n "$BACKUP_ROOT" ]] || { usage; exit 2; } + +for cmd in virsh rsync sha256sum python3 qemu-img find sort awk sed; do + command -v "$cmd" >/dev/null || die "Benötigtes Programm fehlt: $cmd" +done + +BACKUP_ROOT="$(readlink -m "$BACKUP_ROOT")" +[[ "$BACKUP_ROOT" != "/" ]] || die "Backup-Ziel darf nicht / sein." +mkdir -p "$BACKUP_ROOT" +[[ -z "$(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -print -quit)" ]] \ + || die "Backup-Ziel ist nicht leer: $BACKUP_ROOT" + +mkdir -p "$BACKUP_ROOT"/{domains,networks,pools,interfaces,files,metadata,logs} +exec > >(tee -a "$BACKUP_ROOT/logs/backup.log") 2>&1 + +log "Prüfe libvirt-Verbindung $URI" +virsh -c "$URI" uri >/dev/null + +mapfile -t VMS < <(virsh -c "$URI" list --all --name | sed '/^[[:space:]]*$/d') +((${#VMS[@]} > 0)) || warn "Keine persistenten Domains gefunden." + +if (( ! ALLOW_RUNNING )); then + running=() + for vm in "${VMS[@]}"; do + state="$(virsh -c "$URI" domstate "$vm" 2>/dev/null | head -n1 | tr '[:upper:]' '[:lower:]')" + case "$state" in + "shut off"|"abgeschaltet"|"ausgeschaltet") ;; + *) running+=("$vm ($state)") ;; + esac + done + if ((${#running[@]})); then + printf 'Nicht ausgeschaltete VMs:\n' >&2 + printf ' - %s\n' "${running[@]}" >&2 + die "Alle VMs sauber herunterfahren oder ausdrücklich ALLOW_RUNNING=1 setzen." + fi +fi + +log "Schreibe Host- und Paketinformationen" +{ + echo "backup_format_version=$VERSION" + echo "created_utc=$(date -u +%FT%TZ)" + echo "hostname=$(hostname -f 2>/dev/null || hostname)" + echo "libvirt_uri=$URI" + echo "kernel=$(uname -srmo)" + echo "virsh_version=$(virsh --version)" + echo "qemu_img_version=$(qemu-img --version | head -n1)" +} > "$BACKUP_ROOT/metadata/backup-info.txt" + +cp -a /etc/os-release "$BACKUP_ROOT/metadata/os-release" 2>/dev/null || true +virsh -c "$URI" capabilities > "$BACKUP_ROOT/metadata/capabilities.xml" 2>/dev/null || true +virsh -c "$URI" nodeinfo > "$BACKUP_ROOT/metadata/nodeinfo.txt" 2>/dev/null || true + +log "Exportiere Domain-Definitionen" +for vm in "${VMS[@]}"; do + safe="$(printf '%s' "$vm" | sed 's#[/[:cntrl:]]#_#g')" + virsh -c "$URI" dumpxml --inactive --security-info "$vm" \ + > "$BACKUP_ROOT/domains/$safe.xml" \ + || virsh -c "$URI" dumpxml --inactive "$vm" > "$BACKUP_ROOT/domains/$safe.xml" + printf '%s\t%s\n' "$safe" "$vm" >> "$BACKUP_ROOT/metadata/domains.tsv" + virsh -c "$URI" domblklist "$vm" --details \ + > "$BACKUP_ROOT/domains/$safe.domblklist.txt" || true + virsh -c "$URI" domiflist "$vm" \ + > "$BACKUP_ROOT/domains/$safe.domiflist.txt" || true + virsh -c "$URI" snapshot-list "$vm" --tree \ + > "$BACKUP_ROOT/domains/$safe.snapshots.txt" || true +done + +export_objects() { + local kind="$1" list_cmd="$2" dump_cmd="$3" outdir="$4" manifest="$5" + local name safe + while IFS= read -r name; do + [[ -n "$name" ]] || continue + safe="$(printf '%s' "$name" | sed 's#[/[:cntrl:]]#_#g')" + if virsh -c "$URI" "$dump_cmd" "$name" > "$BACKUP_ROOT/$outdir/$safe.xml"; then + printf '%s\t%s\n' "$safe" "$name" >> "$BACKUP_ROOT/metadata/$manifest" + else + warn "$kind konnte nicht exportiert werden: $name" + rm -f "$BACKUP_ROOT/$outdir/$safe.xml" + fi + done < <(virsh -c "$URI" "$list_cmd" --all --name 2>/dev/null | sed '/^[[:space:]]*$/d') +} + +log "Exportiere Netzwerke, Storage-Pools und Host-Interfaces" +export_objects "Netzwerk" net-list net-dumpxml networks networks.tsv +export_objects "Storage-Pool" pool-list pool-dumpxml pools pools.tsv +export_objects "Interface" iface-list iface-dumpxml interfaces interfaces.tsv + +log "Ermittle referenzierte Dateien und Blockgeräte" +python3 - "$BACKUP_ROOT" <<'PY' +import os, sys, glob, csv +import xml.etree.ElementTree as ET + +root = sys.argv[1] +file_paths = set() +block_paths = set() +rows = [] + +for xml_path in glob.glob(os.path.join(root, "domains", "*.xml")): + vm = os.path.basename(xml_path)[:-4] + tree = ET.parse(xml_path) + dom = tree.getroot() + + for disk in dom.findall("./devices/disk"): + dtype = disk.get("type", "") + device = disk.get("device", "") + src = disk.find("source") + if src is None: + continue + if dtype == "file" and src.get("file"): + p = src.get("file") + file_paths.add(p) + rows.append((vm, "disk-file", device, p)) + elif dtype == "block" and src.get("dev"): + p = src.get("dev") + block_paths.add(p) + rows.append((vm, "disk-block", device, p)) + elif dtype == "dir" and src.get("dir"): + p = src.get("dir") + rows.append((vm, "disk-dir", device, p)) + elif dtype == "volume": + rows.append((vm, "disk-volume", device, + f"{src.get('pool','')}/{src.get('volume','')}")) + elif dtype == "network": + rows.append((vm, "disk-network", device, + src.get("name") or src.get("protocol") or "")) + + nvram = dom.find("./os/nvram") + if nvram is not None and nvram.text and nvram.text.strip(): + p = nvram.text.strip() + file_paths.add(p) + rows.append((vm, "nvram", "", p)) + + for backend in dom.findall("./devices/tpm/backend"): + for source in backend.findall("source"): + p = source.get("path") or source.get("file") + if p: + file_paths.add(p) + rows.append((vm, "tpm-file", "", p)) + +with open(os.path.join(root, "metadata", "references.tsv"), "w", + newline="", encoding="utf-8") as f: + w = csv.writer(f, delimiter="\t", lineterminator="\n") + w.writerow(("domain_xml", "kind", "device", "source")) + w.writerows(sorted(rows)) + +with open(os.path.join(root, "metadata", "file-sources.txt"), "w", + encoding="utf-8") as f: + for p in sorted(file_paths): + f.write(p + "\n") + +with open(os.path.join(root, "metadata", "block-sources.txt"), "w", + encoding="utf-8") as f: + for p in sorted(block_paths): + f.write(p + "\n") +PY + +: > "$BACKUP_ROOT/metadata/file-map.tsv" +: > "$BACKUP_ROOT/metadata/missing-files.txt" + +copy_source() { + local src="$1" rel dst + [[ -n "$src" ]] || return 0 + + if [[ ! -e "$src" ]]; then + warn "Referenzierte Datei fehlt: $src" + printf '%s\n' "$src" >> "$BACKUP_ROOT/metadata/missing-files.txt" + return 0 + fi + + # Originalpfad ohne führenden Slash innerhalb files/rootfs/ ablegen. + rel="${src#/}" + dst="$BACKUP_ROOT/files/rootfs/$rel" + mkdir -p "$(dirname "$dst")" + + if [[ -d "$src" ]]; then + mkdir -p "$dst" + rsync "${RSYNC_OPTS[@]}" "$src/" "$dst/" + printf '%s\t%s\t%s\n' "directory" "$src" "files/rootfs/$rel" \ + >> "$BACKUP_ROOT/metadata/file-map.tsv" + else + rsync "${RSYNC_OPTS[@]}" "$src" "$dst" + printf '%s\t%s\t%s\n' "file" "$src" "files/rootfs/$rel" \ + >> "$BACKUP_ROOT/metadata/file-map.tsv" + + case "$src" in + *.qcow|*.qcow2|*.qed|*.vmdk|*.vdi|*.raw|*.img) + qemu-img info --backing-chain --output=json "$src" \ + > "$dst.qemu-img-info.json" 2>/dev/null || true + ;; + esac + fi +} + +log "Kopiere referenzierte Dateien" +while IFS= read -r src; do + copy_source "$src" +done < "$BACKUP_ROOT/metadata/file-sources.txt" + +if [[ -s "$BACKUP_ROOT/metadata/block-sources.txt" ]]; then + warn "Blockgeräte wurden gefunden und nicht kopiert:" + sed 's/^/ - /' "$BACKUP_ROOT/metadata/block-sources.txt" + warn "Diese LVM-/Block-Datenträger separat sichern, z. B. per LVM-Snapshot, dd oder Storage-Replikation." +fi + +log "Erzeuge Dateiliste und SHA-256-Prüfsummen" +( + cd "$BACKUP_ROOT" + find . -type f \ + ! -path './metadata/SHA256SUMS' \ + ! -path './logs/backup.log' \ + -print0 | sort -z | xargs -0 sha256sum > metadata/SHA256SUMS +) + +sync +log "Backup abgeschlossen: $BACKUP_ROOT" +echo "Vor der Neuinstallation prüfen:" +echo " cd '$BACKUP_ROOT' && sha256sum -c metadata/SHA256SUMS" +[[ ! -s "$BACKUP_ROOT/metadata/block-sources.txt" ]] \ + || echo "ACHTUNG: Blockgeräte laut metadata/block-sources.txt separat sichern." diff --git a/libvirt-restore.sh b/libvirt-restore.sh new file mode 100644 index 0000000..7c364b2 --- /dev/null +++ b/libvirt-restore.sh @@ -0,0 +1,235 @@ +#!/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}" + +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 + +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" + +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"