Proxmox Cluster Sync

Syncronisation von virtuellen Maschinen zwischen 2 Proxmox Clustern via pve-zsync

Installation pve-zsync

apt-get install pve-zsync

Syncronisation

source = IP des PVE Node + die ID der VM

pve-zsync create --source 192.168.1.1:100 --dest zfsstoragepoolname --verbose --maxsnap 2 --name test1

VM Config Datei kopieren und anpassen

cp /var/lib/pve-zsync/<VMID>.conf.rep_<JOB_NAME><VMID>_<TIMESTAMP> /etc/pve/qemu-server/<VMID>.conf

in der Config muss noch der Name des Storage korrigiert werden.

Sync Job löschen

pve-zsync destroy --source 192.168.1.1:100 --name test1

Gesamt-Script

#!/bin/bash
# Script zur Synchronisation von VMs über ZFS Snapshots und Entfernen alter Snapshots.
# ACHTUNG: Der Ziel-Pool wird komplett geleert! Dieser darf nur für die Synchronisation genutzt werden.

set -euo pipefail  # Strict error handling


# --- Einstellungen ---
# Primary Proxmox Node zur Stammdatenabfrage 
primary_pve_node="pve01"
# IP zu Hostname Zuordnung des Source Proxmox Clusters
declare -A IP_TO_HOST=(
  ["pve01"]="10.2.1.1"
  ["pve02"]="10.2.1.2"
  ["pve03"]="10.2.1.3"
)
# Einstellungen für den Sync
# Der Pool, dessen VMs synchronisiert werden sollen
pool_name_to_sync="Test-Vms"
# Name des Sync Jobs (wird auch beim ZFS Snapshot verwendet)
sync_job_name="RemoteZFSSync"

source_storage_name="Storage01"
# Achtung dieser Storage wird immer komplett geleert!
destination_storage_name="Storage"
# Achtung alle VMs in diesem Pool werden gelöscht!
destination_pool_name="RemoteSyncedVMs"


lookup_ip_for_hostname() {
  local ip="$1"

  # Prüfen, ob IP angegeben ist
  if [[ -z "$ip" ]]; then
    echo "Fehler: keine IP-Adresse angegeben!" >&2
    return 1
  fi

  # Lookup durchführen
  local hostname="${IP_TO_HOST[$ip]}"

  # Ausgabe oder Fehlermeldung
  if [[ -n "$hostname" ]]; then
    echo "$hostname"
  else
    echo "Unbekannte IP-Adresse: $ip" >&2
    return 2
  fi
}

remove_all_disks_from_storage() {
  local search_term="$1"

  # --- Prüfung der Eingabe ---
  if [[ -z "$search_term" ]]; then
    echo "Verwendung: $0 <teil-des-snapshot-namens>"
    echo "Beispiel: $0 autobackup"
    exit 1
  fi

  # --- Snapshots finden ---
  local snapshots
  snapshots=$(zfs list -t snapshot -o name -H | grep -i "$search_term")
  if [[ -z "$snapshots" ]]; then
    echo "Keine Snapshots gefunden, die '${search_term}' enthalten."
    exit 0
  fi

  # --- Snapshots löschen ---
  while IFS= read -r snap; do
    zfs destroy -r "$snap"
  done <<< "$snapshots"
}

remove_synced_vms() {
  local pool="$1"

  if [[ -z "$pool" ]]; then
    echo "Fehler: Kein Poolname angegeben."
    echo "Verwendung: delete_vms_in_pool <poolname>"
    return 1
  fi

  echo "Hole VM-IDs aus Pool: $pool"
  local vms
  vms=$(pvesh get /pools/$pool --output-format json | jq -r '.members[].vmid' 2>/dev/null)

  if [[ -z "$vms" ]]; then
    echo "Keine VMs im Pool '$pool' gefunden oder Pool existiert nicht."
    return 0
  fi

  echo "Stoppe alle VMs/LXCs im Pool '$pool'..."
  for vmid in $vms; do
    if qm status "$vmid" &>/dev/null; then
      echo "Stoppe VM $vmid..."
      qm stop "$vmid" --skiplock --timeout 30 2>/dev/null
    fi
  done

  echo "Warte 5 Sekunden, um sicherzustellen, dass alle Maschinen gestoppt sind..."
  sleep 5

  echo "Lösche folgende VMs/LXCs aus Pool '$pool': $vms"
  for vmid in $vms; do
    if qm config "$vmid" &>/dev/null; then
      echo "Lösche VM $vmid..."
      qm destroy "$vmid" --purge --skiplock 2>/dev/null
    fi
  done

  echo "Alle VMs aus Pool '$pool' wurden gestoppt und gelöscht."
  echo "Lösche alle Disks im Ziel Storage: $destination_storage_name"
  remove_all_disks_from_storage "$destination_storage_name"
}

rename_storage_names_in_config() {
  if [[ $# -ne 3 ]]; then
    echo "Verwendung: $0 <datei> <alter_storage_name> <neuer_storage_name>"
    echo "Beispiel:  $0 vm-101.conf Storage01 Storage"
    exit 1
  fi

  local datei="$1"
  local alt="$2"
  local neu="$3"

  if [[ ! -f "$datei" ]]; then
    echo "Fehler: Datei '$datei' nicht gefunden."
    exit 1
  fi
  cp "$datei" "${datei}.bak"
  sed -i "s/\b${alt}\b:/${neu}:/g" "$datei"
}

get_latest_replica_file() {
  local dir="$1"
  local vmid="$2"

  if [[ -z "$dir" || -z "$vmid" ]]; then
    echo "Verwendung: get_latest_replica_file <verzeichnis> <vmid>"
    return 1
  fi

  # Dateien suchen, die mit "<vmid>.conf.qemu.rep_RemoteZFSSync_" beginnen
  local pattern="${vmid}.conf.qemu.rep_RemoteZFSSync_"
  local latest_file

  latest_file=$(find "$dir" -type f -name "${pattern}*" -printf '%T@ %p\n' 2>/dev/null \
    | sort -nr | head -n 1 | awk '{print $2}')

  if [[ -n "$latest_file" ]]; then
    echo "$latest_file"
    cp "$latest_file" "/etc/pve/qemu-server/9${vmid}.conf"
    rename_storage_names_in_config "/etc/pve/qemu-server/9${vmid}.conf" "Storage01" "Storage"
    # VM zu einem Pool Hinzufügen
    pvesh set /pools/$destination_pool_name -vms "9${vmid}"

  else
    echo "Keine Datei gefunden, die mit '${pattern}' beginnt." >&2
    return 1
  fi

}

remove_synced_vms "$destination_pool_name"

mapfile -t vms < <(
  ssh "$(lookup_ip_for_hostname $primary_pve_node)" pvesh get /cluster/resources --type vm --output json |
    jq -r '.[] | "\(.vmid) \(.name) \(.node) \(.pool)"'
)

for vm in "${vms[@]}"; do
  # Remove Remote Snapshots:
  
  read -r vmid name node pool <<< "$vm"
  ssh "$(lookup_ip_for_hostname "$node")" 'bash -s' -- < ./removeSourceSnapshots.sh "$sync_job_name"

  echo "VMID: $vmid | Name: $name | Node: $node | Pool: $pool"
  if [[ "$pool" == "$pool_name_to_sync" ]]; then
    pve-zsync create --source "$(lookup_ip_for_hostname "$node"):$vmid" --dest "$destination_storage_name" --verbose  --name "$sync_job_name"
    pve-zsync destroy --source "$(lookup_ip_for_hostname "$node"):$vmid" --name "$sync_job_name"
    get_latest_replica_file "/var/lib/pve-zsync" "$vmid"

  else
    echo "Überspringe VMID $vmid (Pool: $pool passt nicht zu $pool_name_to_sync)"
  fi
done