Dateien nach "/" hochladen

This commit is contained in:
2026-07-13 18:40:45 +00:00
parent 1fdf3cfd44
commit e73405ea51
3 changed files with 582 additions and 1 deletions

View File

@@ -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
View 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
View 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"