Dateien nach "/" hochladen
This commit is contained in:
88
README.md
88
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.
|
||||
|
||||
260
libvirt-backup.sh
Normal file
260
libvirt-backup.sh
Normal file
@@ -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."
|
||||
235
libvirt-restore.sh
Normal file
235
libvirt-restore.sh
Normal file
@@ -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"
|
||||
Reference in New Issue
Block a user