Files
libvirtbackup/libvirt-restore.sh
2026-07-14 08:07:49 +00:00

252 lines
8.5 KiB
Bash

#!/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"