Skip to main content

Sauvegarder un vCenter HA (VCHA) avec Veeam

  • June 19, 2026
  • 3 comments
  • 27 views

morgan.munck
Forum|alt.badge.img

Bonjour à tous,

Ce matin je devais préparer un job de sauvegarde d’un VCHA chez un client.

J’ai constaté que Veeam ne peut pas sauvegarder nativement et correctement un cluster VCHA en raison de plusieurs contraintes techniques imposées par VMware :

  • Prendre un snapshot sur un nœud Passive ou Witness peut déclencher un failover ou un état incohérent, or Veeam utilise des snapshots pour les sauvegardes image-based
  • VMware recommande de ne sauvegarder que le nœud Active et d’exclure les nœuds Passive et Witness, or Veeam ne peut pas distinguer automatiquement le nœud actif

Pour pouvoir sauvegarder un VCHA il faut pouvoir exclure le noeud passif lors du lancement du job.

Pour ce faire je lance deux script, un script qui s’exécute avant le job et un script qui s’exécute après le job.


En espérant que cette technique pourra aider certains d’entre vous.

Morgan.

 

________________________________________________________________________


Script avant le job:
 

#########################################
# PRE-JOB - Exclude vCHA Passive Node
#########################################

$vcenter  = "vcenter@domain.com"
$username = "user"
$password = "mdp"
$jobname  = "jobname"
$logFile  = "C:\Backup\Scripts\Logs\PreJob_vCHA_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$tmpFile  = "C:\Backup\Scripts\Logs\passive_vm_name.tmp"

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $entry = "[$timestamp][$Level] $Message"
    Write-Output $entry
    Add-Content -Path $logFile -Value $entry
}

New-Item -ItemType Directory -Path (Split-Path $logFile) -Force | Out-Null
Write-Log "=== Début Pre-Job vCHA ==="

#########################################
# Chargement modules
try {
    Import-Module Veeam.Backup.PowerShell -ErrorAction Stop
    Write-Log "Module Veeam.Backup.PowerShell chargé."

    if (-not (Get-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue)) {
        Import-Module VMware.VimAutomation.Core -ErrorAction Stop
        Write-Log "Module VMware.VimAutomation.Core chargé."
    } else {
        Write-Log "Module VMware.VimAutomation.Core déjà chargé."
    }
} catch {
    Write-Log "ERREUR chargement modules : $_" "ERROR"
    exit 1
}

#########################################
# Connexion vCenter
try {
    Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
    $viConnection = Connect-VIServer -Server $vcenter -User $username -Password $password -ErrorAction Stop
    Write-Log "Connecté au vCenter : $vcenter"
} catch {
    Write-Log "ERREUR connexion vCenter : $_" "ERROR"
    exit 1
}

#########################################
# Récupération du noeud passif vCHA
try {
    $si          = Get-View ServiceInstance
    $failoverMgr = Get-View -Id $si.Content.FailoverClusterManager -ErrorAction Stop
    Write-Log "FailoverClusterManager récupéré."

    $healthInfo     = $failoverMgr.GetVchaClusterHealth()
    $vcClusterState = $healthInfo.RuntimeInfo.ClusterState
    $nodeState      = $healthInfo.RuntimeInfo.NodeInfo
    Write-Log "État du cluster vCHA : $vcClusterState"

    foreach ($node in $nodeState) {
        Write-Log "Nœud détecté - Role: $($node.NodeRole) | IP: $($node.NodeIp)"
    }

    $passiveNode = $nodeState | Where-Object { $_.NodeRole -eq "passive" }

    if (-not $passiveNode) {
        Write-Log "Aucun nœud passif trouvé - sauvegarde normale." "WARN"
        Disconnect-VIServer -Server $vcenter -Confirm:$false
        exit 0
    }
    Write-Log "Nœud passif identifié - IP : $($passiveNode.NodeIp)"

} catch {
    Write-Log "ERREUR récupération info vCHA : $_" "ERROR"
    Disconnect-VIServer -Server $vcenter -Confirm:$false
    exit 1
}

#########################################
# Identification de la VM passive via IP
try {
    $passiveVM  = $null
    $retryCount = 0
    $maxRetry   = 3

    while (-not $passiveVM -and $retryCount -lt $maxRetry) {
        $passiveVM = Get-VM | Where-Object {
            $_.Guest.IPAddress -contains $passiveNode.NodeIp
        }
        if (-not $passiveVM) {
            $retryCount++
            Write-Log "VM passive non trouvée via IP (tentative $retryCount/$maxRetry), attente 10s..." "WARN"
            Start-Sleep -Seconds 10
        }
    }

    if (-not $passiveVM) {
        Write-Log "Impossible de trouver la VM avec l'IP $($passiveNode.NodeIp)" "ERROR"
        Disconnect-VIServer -Server $vcenter -Confirm:$false
        exit 1
    }
    Write-Log "VM passive identifiée : $($passiveVM.Name)"

} catch {
    Write-Log "ERREUR identification VM : $_" "ERROR"
    Disconnect-VIServer -Server $vcenter -Confirm:$false
    exit 1
}

#########################################
# Déconnexion vCenter
Disconnect-VIServer -Server $vcenter -Confirm:$false | Out-Null
Write-Log "Déconnecté du vCenter."

#########################################
# Suppression temporaire du job Veeam
try {
    $vbrJob     = Get-VBRJob -Name $jobname -ErrorAction Stop
    $jobObjects = Get-VBRJobObject -Job $vbrJob

    Write-Log "Objets présents dans le job :"
    foreach ($obj in $jobObjects) {
        Write-Log "  -> Name: $($obj.Name) | Type: $($obj.Type) | IsExcluded: $($obj.IsExcluded)"
    }

    $jobObject = $jobObjects | Where-Object { $_.Name -eq $passiveVM.Name }

    if (-not $jobObject) {
        Write-Log "VM '$($passiveVM.Name)' absente du job, rien à faire." "INFO"
        exit 0
    }

    if ($jobObject.IsExcluded -eq $true) {
        Write-Log "VM '$($passiveVM.Name)' déjà exclue, rien à faire." "INFO"
        exit 0
    }

    # Sauvegarde du nom pour le Post-Job
    $passiveVM.Name | Out-File -FilePath $tmpFile -Force -Encoding UTF8
    Write-Log "Nom VM sauvegardé dans : $tmpFile"

    # Suppression sans -Confirm
    Remove-VBRJobObject -Objects $jobObject -ErrorAction Stop | Out-Null
    Write-Log "VM '$($passiveVM.Name)' retirée du job '$jobname'." "INFO"

    # Vérification post-suppression
    $jobObjectsAfter = Get-VBRJobObject -Job $vbrJob
    Write-Log "État du job après suppression :"
    foreach ($obj in $jobObjectsAfter) {
        Write-Log "  [POST] -> Name: $($obj.Name) | Type: $($obj.Type) | IsExcluded: $($obj.IsExcluded)"
    }

} catch {
    Write-Log "ERREUR suppression VM du job : $_" "ERROR"
    exit 1
}

Write-Log "=== Fin Pre-Job vCHA - Succès ==="
exit 0
#########################################
# PRE-JOB - End
#########################################

 

 

 

Script après le job:

 

#########################################
# POST-JOB - Réintégration vCHA Passive Node
#########################################

$vcenter  = "vcenter@domain.com"
$username = "user"
$password = "mdp"
$jobname  = "jobname"
$logFile  = "C:\Backup\Scripts\Logs\PostJob_vCHA_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$tmpFile  = "C:\Backup\Scripts\Logs\passive_vm_name.tmp"

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $entry = "[$timestamp][$Level] $Message"
    Write-Output $entry
    Add-Content -Path $logFile -Value $entry
}

New-Item -ItemType Directory -Path (Split-Path $logFile) -Force | Out-Null
Write-Log "=== Début Post-Job vCHA ==="

#########################################
# Vérification fichier tmp
if (-not (Test-Path $tmpFile)) {
    Write-Log "Fichier tmp introuvable ($tmpFile) - aucune VM à réintégrer." "WARN"
    Write-Log "=== Fin Post-Job vCHA ==="
    exit 0
}

$vmName = (Get-Content $tmpFile -Raw -Encoding UTF8).Trim()
if ([string]::IsNullOrEmpty($vmName)) {
    Write-Log "Fichier tmp vide - aucune VM à réintégrer." "WARN"
    exit 0
}
Write-Log "VM à réintégrer : $vmName"

#########################################
# Chargement modules
try {
    Import-Module Veeam.Backup.PowerShell -ErrorAction Stop
    Write-Log "Module Veeam.Backup.PowerShell chargé."
} catch {
    Write-Log "ERREUR chargement module Veeam : $_" "ERROR"
    exit 1
}

#########################################
# Réintégration dans le job
try {
    $vbrJob = Get-VBRJob -Name $jobname -ErrorAction Stop
    if (-not $vbrJob) {
        Write-Log "Job '$jobname' introuvable." "ERROR"
        exit 1
    }

    $existing = Get-VBRJobObject -Job $vbrJob | Where-Object { $_.Name -eq $vmName }

    Write-Log "État actuel des objets du job :"
    foreach ($obj in Get-VBRJobObject -Job $vbrJob) {
        Write-Log "  -> Name: $($obj.Name) | Type: $($obj.Type) | IsExcluded: $($obj.IsExcluded)"
    }

    if ($existing -and $existing.IsExcluded -eq $false) {
        Write-Log "VM '$vmName' déjà présente en Include. Rien à faire." "INFO"
        Remove-Item $tmpFile -Force
        exit 0
    }


    if ($existing -and $existing.IsExcluded -eq $true) {
        Write-Log "VM '$vmName' présente mais exclue - suppression de l'entrée exclue..." "INFO"
        Remove-VBRJobObject -Objects $existing -ErrorAction Stop | Out-Null
        Write-Log "Entrée exclue supprimée."
    }

    # Recherche entité Veeam
    $vbrServer = Get-VBRServer | Where-Object { $_.Name -like "*$($vcenter.Split('.')[0])*" }
    if (-not $vbrServer) {
        Write-Log "Serveur vCenter introuvable dans Veeam." "ERROR"
        exit 1
    }
    Write-Log "Serveur Veeam : $($vbrServer.Name)"

    $vbrEntity = Find-VBRViEntity -Server $vbrServer -Name $vmName -ErrorAction Stop
    if (-not $vbrEntity) {
        Write-Log "Entité Veeam introuvable pour '$vmName'." "ERROR"
        exit 1
    }
    Write-Log "Entité Veeam trouvée : $($vbrEntity.Name)"

    # Réintégration
    Add-VBRViJobObject -Job $vbrJob -Entities $vbrEntity -ErrorAction Stop | Out-Null
    Write-Log "VM '$vmName' réintégrée en Include dans le job '$jobname'." "INFO"

    # Vérification finale
    Write-Log "État du job après réintégration :"
    foreach ($obj in Get-VBRJobObject -Job $vbrJob) {
        Write-Log "  [POST] -> Name: $($obj.Name) | Type: $($obj.Type) | IsExcluded: $($obj.IsExcluded)"
    }

    Remove-Item $tmpFile -Force
    Write-Log "Fichier tmp supprimé."

} catch {
    Write-Log "ERREUR réintégration VM : $_" "ERROR"
    exit 1
}

Write-Log "=== Fin Post-Job vCHA - Succès ==="
exit 0

#########################################
# POST-JOB - End
#########################################

3 comments

Eric Machabert
Forum|alt.badge.img+4

Merci Morgan, n’y a t-il pas une contrainte sur la restauration en mode image ? j’avais mémoire qu’il fallait plutôt faire le backup “mode fichier” et qu’en cas de restauration il fallait rebuild un vcenter puis restaurer le backup. Mais je dois surement être obsolète sur le sujet :-)

 


morgan.munck
Forum|alt.badge.img
  • Author
  • VUG Leader
  • June 23, 2026

En principe il n’y a pas de contrainte à condition de restaurer la VM + le Witness. J’ai prévu de faire les tests de restauration d’ici quelques semaines. Je reviendrais ici faire un petit récapitulatif.


Eric Machabert
Forum|alt.badge.img+4

Tiens nous au courant c’est toujours intéressant !