diff --git a/README.md b/README.md index a9726e7..c3ff284 100644 --- a/README.md +++ b/README.md @@ -86,3 +86,48 @@ NetworkManager oder systemd-networkd neu aufzubauen. prüfen. - Nach dem Restore zuerst nur eine VM starten und insbesondere UEFI, virtuelles TPM, Secure Boot, BitLocker und Netzwerk testen. + +## PAW-Netzwerk aus `50-paw-install.yml` (Version 1.2) + +Das Paket sichert jetzt zusätzlich die vom Playbook verwendete Konfiguration: + +- `/etc/NetworkManager/system-connections/` +- `/etc/NetworkManager/dispatcher.d/` +- `/etc/libvirt/qemu/networks/` +- relevante sysctl-, UFW-, Routing- und `/hilden`-Dateien +- aktuelle MAC von `br-lan` +- permanente MACs und Hardwarepfade aller physischen Interfaces +- Zuordnung der Bridge-Ports zu `br-lan` + +Das alte PAW-Profil bindet die Ports fest an `eth0` und `enp2s0f0`. Beim Restore +werden diese Namen anhand der gesicherten permanenten MAC-Adressen auf die unter +Debian tatsächlich vorhandenen Gerätenamen umgesetzt. Zusätzlich wird die +bisher dynamische MAC von `br-lan` als `bridge.mac-address` im +NetworkManager-Keyfile festgeschrieben. + +Empfohlene Reihenfolge auf Debian: + +```bash +# 1. 50-paw-install.yml ausführen +# 2. VM- und Netzwerk-Restore vorbereiten, NetworkManager noch nicht neu starten +sudo RESTORE_PAW_NETWORK=1 ./libvirt-restore.sh /mnt/backup/libvirt-host + +# 3. Profile kontrollieren +sudo grep -R -E '^(interface-name|mac-address|master)=' \ + /etc/NetworkManager/system-connections/ + +# 4. Nur lokal an der Konsole aktivieren +sudo RESTORE_PAW_NETWORK=1 APPLY_PAW_NETWORK=1 \ + ./libvirt-restore.sh /mnt/backup/libvirt-host +``` + +Alternativ kann nur der Netzwerkteil ausgeführt werden: + +```bash +sudo ./paw-network-backup.sh /mnt/backup/paw-network +sudo ./paw-network-restore.sh /mnt/backup/paw-network +sudo APPLY_NETWORK=1 ./paw-network-restore.sh /mnt/backup/paw-network +``` + +Der Netzwerk-Restore startet NetworkManager standardmäßig **nicht** neu. Das +verhindert einen unbeabsichtigten Verbindungsabbruch bei einem Restore per SSH. diff --git a/libvirt-backup.sh b/libvirt-backup.sh index 9b8cb85..38f677f 100644 --- a/libvirt-backup.sh +++ b/libvirt-backup.sh @@ -5,11 +5,12 @@ set -Eeuo pipefail IFS=$'\n\t' umask 077 -VERSION="1.0" +VERSION="1.2" URI="${LIBVIRT_URI:-qemu:///system}" BACKUP_ROOT="${1:-}" RSYNC_OPTS=(-aHAX --numeric-ids --sparse --info=progress2) ALLOW_RUNNING=0 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { cat <<'EOF' @@ -28,6 +29,7 @@ Das Skript sichert: - UEFI-NVRAM-Dateien - dateibasierte TPM-Backend-Zustände - qemu-img-Informationen und Prüfsummen + - PAW-Netzwerk aus 50-paw-install.yml inklusive br-lan- und Port-MACs Blockgeräte wie LVM-LVs werden nur dokumentiert und NICHT kopiert. EOF @@ -41,7 +43,7 @@ log() { printf '\n==> %s\n' "$*"; } [[ $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 +for cmd in virsh rsync sha256sum python3 qemu-img find sort awk sed findmnt; do command -v "$cmd" >/dev/null || die "Benötigtes Programm fehlt: $cmd" done @@ -51,6 +53,20 @@ mkdir -p "$BACKUP_ROOT" [[ -z "$(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -print -quit)" ]] \ || die "Backup-Ziel ist nicht leer: $BACKUP_ROOT" +# Früh erkennen, ob das Ziel beschreibbar ist und der Mount read-only ist. +mount_target="$(findmnt -no TARGET -T "$BACKUP_ROOT" 2>/dev/null || true)" +mount_opts="$(findmnt -no OPTIONS -T "$BACKUP_ROOT" 2>/dev/null || true)" +[[ ",$mount_opts," != *,ro,* ]] \ + || die "Backup-Dateisystem ist nur lesbar eingehängt: ${mount_target:-unbekannt}" + +probe="$BACKUP_ROOT/.libvirt-backup-write-test.$$" +printf 'libvirt backup write test\n' > "$probe" \ + || die "Backup-Ziel ist nicht beschreibbar: $BACKUP_ROOT" +sync "$probe" 2>/dev/null || sync +grep -qx 'libvirt backup write test' "$probe" \ + || die "Schreib-/Lesetest auf dem Backup-Ziel fehlgeschlagen." +rm -f "$probe" + mkdir -p "$BACKUP_ROOT"/{domains,networks,pools,interfaces,files,metadata,logs} exec > >(tee -a "$BACKUP_ROOT/logs/backup.log") 2>&1 @@ -118,7 +134,10 @@ export_objects() { 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') + done < <( + virsh -c "$URI" "$list_cmd" --all --name 2>/dev/null | + sed 's/^[[:space:]]*//; s/[[:space:]]*$//; /^[[:space:]]*$/d' + ) } log "Exportiere Netzwerke, Storage-Pools und Host-Interfaces" @@ -243,6 +262,26 @@ if [[ -s "$BACKUP_ROOT/metadata/block-sources.txt" ]]; then warn "Diese LVM-/Block-Datenträger separat sichern, z. B. per LVM-Snapshot, dd oder Storage-Replikation." fi +log "Sichere PAW-Netzwerk und br-lan-MAC" +if [[ -x "$SCRIPT_DIR/paw-network-backup.sh" ]]; then + "$SCRIPT_DIR/paw-network-backup.sh" "$BACKUP_ROOT/network" +else + warn "paw-network-backup.sh fehlt neben dem Backup-Skript; Netzwerk wurde nicht gesichert." +fi + +log "Prüfe Backup-Datenträger vor der Prüfsummenphase" +mount_opts="$(findmnt -no OPTIONS -T "$BACKUP_ROOT" 2>/dev/null || true)" +[[ ",$mount_opts," != *,ro,* ]] \ + || die "Backup-Dateisystem wurde während des Backups nur-lesbar. Backup ist unvollständig." + +probe="$BACKUP_ROOT/.libvirt-backup-final-test.$$" +printf 'final write test\n' > "$probe" \ + || die "Abschließender Schreibtest fehlgeschlagen. Backup ist unvollständig." +sync "$probe" 2>/dev/null || sync +grep -qx 'final write test' "$probe" \ + || die "Abschließender Lesetest fehlgeschlagen. Backup ist unvollständig." +rm -f "$probe" + log "Erzeuge Dateiliste und SHA-256-Prüfsummen" ( cd "$BACKUP_ROOT" diff --git a/libvirt-restore.sh b/libvirt-restore.sh index 7c364b2..8e0bcf5 100644 --- a/libvirt-restore.sh +++ b/libvirt-restore.sh @@ -13,6 +13,9 @@ 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' @@ -27,6 +30,8 @@ Umgebungsvariablen: 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. @@ -90,6 +95,17 @@ while IFS=$'\t' read -r kind original relative; do 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' diff --git a/paw-network-backup.sh b/paw-network-backup.sh new file mode 100644 index 0000000..ddf9005 --- /dev/null +++ b/paw-network-backup.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Sichert die durch 50-paw-install.yml relevante PAW-Netzwerkkonfiguration. +set -Eeuo pipefail +IFS=$'\n\t' +umask 077 + +DEST="${1:?Aufruf: sudo $0 /pfad/zum/backup/network}" +[[ $EUID -eq 0 ]] || { echo "FEHLER: Bitte als root ausführen." >&2; exit 1; } + +mkdir -p "$DEST"/{rootfs,metadata,state} + +copy_path() { + local src="$1" rel + [[ -e "$src" ]] || return 0 + rel="${src#/}" + mkdir -p "$DEST/rootfs/$(dirname "$rel")" + rsync -aHAX --numeric-ids "$src" "$DEST/rootfs/$rel" + printf '%s\t%s\n' "$src" "rootfs/$rel" >> "$DEST/metadata/files.tsv" +} + +: > "$DEST/metadata/files.tsv" + +# Genau die Pfade, die 50-paw-install.yml installiert bzw. verwendet. +copy_path /etc/NetworkManager/system-connections +copy_path /etc/NetworkManager/dispatcher.d +copy_path /etc/libvirt/qemu/networks +copy_path /etc/sysctl.d +copy_path /usr/local/bin/ufw-paw.sh +copy_path /etc/default/ufw +copy_path /etc/ufw +copy_path /etc/iproute2/rt_tables +copy_path /hilden + +# Laufzeit-Zustand dokumentieren. +ip -details link show > "$DEST/state/ip-link.txt" +ip -details address show > "$DEST/state/ip-address.txt" +ip route show table all > "$DEST/state/ip-routes-all.txt" +ip rule show > "$DEST/state/ip-rules.txt" +bridge link show > "$DEST/state/bridge-links.txt" 2>/dev/null || true +bridge vlan show > "$DEST/state/bridge-vlans.txt" 2>/dev/null || true +nmcli -f all connection show > "$DEST/state/nm-connections.txt" 2>/dev/null || true +nmcli -f all device status > "$DEST/state/nm-devices.txt" 2>/dev/null || true +nmcli --show-secrets connection show > "$DEST/state/nm-connections-with-secrets.txt" 2>/dev/null || true +nft list ruleset > "$DEST/state/nftables.txt" 2>/dev/null || true +ufw status verbose > "$DEST/state/ufw-status.txt" 2>/dev/null || true + +# Bridge-MAC sichern. Diese ist für DHCP-Leases/Netzidentität relevant. +if [[ -e /sys/class/net/br-lan/address ]]; then + cat /sys/class/net/br-lan/address > "$DEST/metadata/br-lan.mac" +fi + +# Permanente MAC, aktuelle MAC und Hardwarepfad aller Interfaces erfassen. +printf 'interface\tcurrent_mac\tpermanent_mac\tifindex\tdriver\tdevice_path\tid_path\tid_net_name_path\tid_net_name_mac\n' \ + > "$DEST/metadata/interfaces.tsv" + +for sysdev in /sys/class/net/*; do + iface="${sysdev##*/}" + current="$(cat "$sysdev/address" 2>/dev/null || true)" + permanent="$(ethtool -P "$iface" 2>/dev/null | awk '{print $3}' || true)" + [[ "$permanent" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]] || permanent="$current" + ifindex="$(cat "$sysdev/ifindex" 2>/dev/null || true)" + driver="$(basename "$(readlink -f "$sysdev/device/driver" 2>/dev/null)" 2>/dev/null || true)" + device_path="$(readlink -f "$sysdev/device" 2>/dev/null || true)" + props="$(udevadm info -q property -p "$sysdev" 2>/dev/null || true)" + id_path="$(awk -F= '$1=="ID_PATH"{print substr($0,index($0,"=")+1)}' <<<"$props")" + id_name_path="$(awk -F= '$1=="ID_NET_NAME_PATH"{print substr($0,index($0,"=")+1)}' <<<"$props")" + id_name_mac="$(awk -F= '$1=="ID_NET_NAME_MAC"{print substr($0,index($0,"=")+1)}' <<<"$props")" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$iface" "$current" "$permanent" "$ifindex" "$driver" "$device_path" \ + "$id_path" "$id_name_path" "$id_name_mac" >> "$DEST/metadata/interfaces.tsv" +done + +# Die in den PAW-Keyfiles verwendeten Ports inklusive ihrer permanenten MAC sichern. +printf 'profile\tconnection_id\told_interface\tpermanent_mac\tmaster\n' \ + > "$DEST/metadata/paw-ports.tsv" +shopt -s nullglob +for profile in /etc/NetworkManager/system-connections/*.nmconnection; do + type="$(awk -F= '$1=="type"{print $2; exit}' "$profile")" + [[ "$type" == "ethernet" ]] || continue + master="$(awk -F= '$1=="master"{print $2; exit}' "$profile")" + [[ "$master" == "br-lan" ]] || continue + con_id="$(awk -F= '$1=="id"{print $2; exit}' "$profile")" + old_if="$(awk -F= '$1=="interface-name"{print $2; exit}' "$profile")" + pmac="" + if [[ -n "$old_if" && -e "/sys/class/net/$old_if" ]]; then + pmac="$(ethtool -P "$old_if" 2>/dev/null | awk '{print $3}' || true)" + [[ "$pmac" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]] || pmac="$(cat "/sys/class/net/$old_if/address")" + fi + printf '%s\t%s\t%s\t%s\t%s\n' "$(basename "$profile")" "$con_id" "$old_if" "$pmac" "$master" \ + >> "$DEST/metadata/paw-ports.tsv" +done + +chmod -R go-rwx "$DEST" +echo "PAW-Netzwerk gesichert: $DEST" +[[ -f "$DEST/metadata/br-lan.mac" ]] && echo "br-lan MAC: $(cat "$DEST/metadata/br-lan.mac")" diff --git a/paw-network-restore.sh b/paw-network-restore.sh new file mode 100644 index 0000000..cf73495 --- /dev/null +++ b/paw-network-restore.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Stellt die für 50-paw-install.yml relevante NetworkManager-Konfiguration wieder her. +set -Eeuo pipefail +IFS=$'\n\t' +umask 077 + +SRC="${1:?Aufruf: sudo $0 /pfad/zum/backup/network}" +APPLY_NETWORK="${APPLY_NETWORK:-0}" +OVERWRITE_NETWORK="${OVERWRITE_NETWORK:-1}" +[[ $EUID -eq 0 ]] || { echo "FEHLER: Bitte als root ausführen." >&2; exit 1; } +[[ -d "$SRC/rootfs" ]] || { echo "FEHLER: Ungültiges Netzwerk-Backup: $SRC" >&2; exit 1; } + +log(){ printf '\n==> %s\n' "$*"; } +warn(){ echo "WARNUNG: $*" >&2; } + +# Aktuelle Geräte anhand permanenter MAC inventarisieren. +declare -A MAC_TO_IF +for sysdev in /sys/class/net/*; do + iface="${sysdev##*/}" + [[ "$iface" == "lo" ]] && continue + pmac="$(ethtool -P "$iface" 2>/dev/null | awk '{print tolower($3)}' || true)" + [[ "$pmac" =~ ^([0-9a-f]{2}:){5}[0-9a-f]{2}$ ]] || pmac="$(tr '[:upper:]' '[:lower:]' < "$sysdev/address")" + MAC_TO_IF["$pmac"]="$iface" +done + +log "Kopiere gesicherte PAW-Konfiguration" +while IFS=$'\t' read -r original relative; do + [[ -n "${original:-}" && -n "${relative:-}" ]] || continue + source_path="$SRC/$relative" + [[ -e "$source_path" ]] || { warn "Quelle fehlt: $source_path"; continue; } + if [[ -e "$original" && "$OVERWRITE_NETWORK" != "1" ]]; then + warn "Vorhanden, übersprungen: $original" + continue + fi + mkdir -p "$(dirname "$original")" + if [[ -d "$source_path" ]]; then + mkdir -p "$original" + rsync -aHAX --numeric-ids "$source_path/" "$original/" + else + rsync -aHAX --numeric-ids "$source_path" "$original" + fi +done < "$SRC/metadata/files.tsv" + +log "Ordne br-lan-Ports anhand permanenter MAC neu zu" +if [[ -f "$SRC/metadata/paw-ports.tsv" ]]; then + tail -n +2 "$SRC/metadata/paw-ports.tsv" | while IFS=$'\t' read -r profile con_id old_if pmac master; do + [[ -n "$profile" ]] || continue + target="/etc/NetworkManager/system-connections/$profile" + [[ -f "$target" ]] || { warn "Profil fehlt nach Restore: $target"; continue; } + pmac="$(tr '[:upper:]' '[:lower:]' <<<"$pmac")" + new_if="${MAC_TO_IF[$pmac]:-}" + if [[ -z "$pmac" || -z "$new_if" ]]; then + warn "Keine aktuelle Schnittstelle für $con_id (alte Schnittstelle $old_if, MAC ${pmac:-unbekannt}) gefunden." + continue + fi + sed -i -E "s/^interface-name=.*/interface-name=$new_if/" "$target" + # Hardwarebindung ergänzen/aktualisieren, damit wechselnde Gerätenamen unschädlich sind. + python3 - "$target" "$pmac" <<'PY' +import configparser, sys +p, mac = sys.argv[1:] +cfg = configparser.RawConfigParser(strict=False, interpolation=None) +cfg.optionxform = str +cfg.read(p) +if not cfg.has_section('802-3-ethernet'): + cfg.add_section('802-3-ethernet') +cfg.set('802-3-ethernet', 'mac-address', mac) +with open(p, 'w') as f: + cfg.write(f, space_around_delimiters=False) +PY + echo "$con_id: $old_if -> $new_if ($pmac)" + done +fi + +# Bridge-MAC festschreiben. 50-paw-install.yml tut dies bislang nicht. +if [[ -s "$SRC/metadata/br-lan.mac" ]]; then + bridge_mac="$(tr '[:upper:]' '[:lower:]' < "$SRC/metadata/br-lan.mac")" + bridge_profile="/etc/NetworkManager/system-connections/br-lan.nmconnection" + if [[ -f "$bridge_profile" ]]; then + python3 - "$bridge_profile" "$bridge_mac" <<'PY' +import configparser, sys +p, mac = sys.argv[1:] +cfg = configparser.RawConfigParser(strict=False, interpolation=None) +cfg.optionxform = str +cfg.read(p) +if not cfg.has_section('bridge'): + cfg.add_section('bridge') +cfg.set('bridge', 'mac-address', mac) +with open(p, 'w') as f: + cfg.write(f, space_around_delimiters=False) +PY + echo "br-lan MAC festgelegt: $bridge_mac" + else + warn "br-lan.nmconnection fehlt; Bridge-MAC konnte nicht eingetragen werden." + fi +fi + +chown root:root /etc/NetworkManager/system-connections/*.nmconnection 2>/dev/null || true +chmod 600 /etc/NetworkManager/system-connections/*.nmconnection 2>/dev/null || true +chown root:root /etc/NetworkManager/dispatcher.d/* 2>/dev/null || true +chmod 0755 /etc/NetworkManager/dispatcher.d/* 2>/dev/null || true + +log "Validiere NetworkManager-Keyfiles" +for f in /etc/NetworkManager/system-connections/*.nmconnection; do + [[ -f "$f" ]] || continue + grep -q '^\[connection\]' "$f" || warn "Ungültiges Profil: $f" +done + +if [[ "$APPLY_NETWORK" == "1" ]]; then + log "Aktiviere NetworkManager-Konfiguration" + nmcli connection reload + systemctl restart NetworkManager + sleep 3 + nmcli connection up br-lan || warn "br-lan konnte nicht automatisch aktiviert werden." + ip -br link show br-lan || true +else + log "Konfiguration vorbereitet, aber nicht aktiviert" + echo "Aktivierung lokal an der Konsole:" + echo " sudo APPLY_NETWORK=1 $0 '$SRC'" +fi