Dateien nach "/" hochladen

This commit is contained in:
2026-07-14 08:07:49 +00:00
parent e73405ea51
commit aad3cc29f1
5 changed files with 317 additions and 3 deletions

View File

@@ -86,3 +86,48 @@ NetworkManager oder systemd-networkd neu aufzubauen.
prüfen. prüfen.
- Nach dem Restore zuerst nur eine VM starten und insbesondere UEFI, virtuelles - Nach dem Restore zuerst nur eine VM starten und insbesondere UEFI, virtuelles
TPM, Secure Boot, BitLocker und Netzwerk testen. 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.

View File

@@ -5,11 +5,12 @@ set -Eeuo pipefail
IFS=$'\n\t' IFS=$'\n\t'
umask 077 umask 077
VERSION="1.0" VERSION="1.2"
URI="${LIBVIRT_URI:-qemu:///system}" URI="${LIBVIRT_URI:-qemu:///system}"
BACKUP_ROOT="${1:-}" BACKUP_ROOT="${1:-}"
RSYNC_OPTS=(-aHAX --numeric-ids --sparse --info=progress2) RSYNC_OPTS=(-aHAX --numeric-ids --sparse --info=progress2)
ALLOW_RUNNING=0 ALLOW_RUNNING=0
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@@ -28,6 +29,7 @@ Das Skript sichert:
- UEFI-NVRAM-Dateien - UEFI-NVRAM-Dateien
- dateibasierte TPM-Backend-Zustände - dateibasierte TPM-Backend-Zustände
- qemu-img-Informationen und Prüfsummen - 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. Blockgeräte wie LVM-LVs werden nur dokumentiert und NICHT kopiert.
EOF EOF
@@ -41,7 +43,7 @@ log() { printf '\n==> %s\n' "$*"; }
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen." [[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
[[ -n "$BACKUP_ROOT" ]] || { usage; exit 2; } [[ -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" command -v "$cmd" >/dev/null || die "Benötigtes Programm fehlt: $cmd"
done done
@@ -51,6 +53,20 @@ mkdir -p "$BACKUP_ROOT"
[[ -z "$(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -print -quit)" ]] \ [[ -z "$(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -print -quit)" ]] \
|| die "Backup-Ziel ist nicht leer: $BACKUP_ROOT" || 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} mkdir -p "$BACKUP_ROOT"/{domains,networks,pools,interfaces,files,metadata,logs}
exec > >(tee -a "$BACKUP_ROOT/logs/backup.log") 2>&1 exec > >(tee -a "$BACKUP_ROOT/logs/backup.log") 2>&1
@@ -118,7 +134,10 @@ export_objects() {
warn "$kind konnte nicht exportiert werden: $name" warn "$kind konnte nicht exportiert werden: $name"
rm -f "$BACKUP_ROOT/$outdir/$safe.xml" rm -f "$BACKUP_ROOT/$outdir/$safe.xml"
fi 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" 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." warn "Diese LVM-/Block-Datenträger separat sichern, z. B. per LVM-Snapshot, dd oder Storage-Replikation."
fi 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" log "Erzeuge Dateiliste und SHA-256-Prüfsummen"
( (
cd "$BACKUP_ROOT" cd "$BACKUP_ROOT"

View File

@@ -13,6 +13,9 @@ RESTORE_INTERFACES="${RESTORE_INTERFACES:-0}"
START_NETWORKS="${START_NETWORKS:-1}" START_NETWORKS="${START_NETWORKS:-1}"
START_POOLS="${START_POOLS:-1}" START_POOLS="${START_POOLS:-1}"
AUTOSTART_OBJECTS="${AUTOSTART_OBJECTS:-0}" 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() { usage() {
cat <<'EOF' cat <<'EOF'
@@ -27,6 +30,8 @@ Umgebungsvariablen:
START_NETWORKS=0 importierte Netzwerke nicht starten START_NETWORKS=0 importierte Netzwerke nicht starten
START_POOLS=0 importierte Pools nicht starten START_POOLS=0 importierte Pools nicht starten
AUTOSTART_OBJECTS=1 importierte Domains/Netze/Pools auf Autostart setzen 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. Standardmäßig werden vorhandene Dateien und Definitionen NICHT überschrieben.
Die Dateien werden an ihre ursprünglichen absoluten Pfade zurückkopiert. 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 fi
done < "$BACKUP_ROOT/metadata/file-map.tsv" 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() { define_network() {
local xml="$1" name local xml="$1" name
name="$(python3 - "$xml" <<'PY' name="$(python3 - "$xml" <<'PY'

95
paw-network-backup.sh Normal file
View File

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

119
paw-network-restore.sh Normal file
View File

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