Dateien nach "/" hochladen
This commit is contained in:
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."
|
||||
Reference in New Issue
Block a user