236 lines
7.8 KiB
Bash
236 lines
7.8 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}"
|
|
|
|
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"
|