Hello everyone,
This morning I had to set up a backup job for a VCHA at a client’s site.
I found that Veeam cannot natively and correctly back up a VCHA cluster due to several technical constraints imposed by VMware:
- Taking a snapshot on a Passive or Witness node can trigger a failover or an inconsistent state, yet Veeam uses snapshots for image-based backups
- VMware recommends backing up only the Active node and excluding the Passive and Witness nodes, yet Veeam cannot automatically distinguish the active node
To back up a VCHA, you must be able to exclude the passive node when the job starts.
To do this, I run two scripts: one that runs before the job and one that runs after the job.
I’m sorry, the script comments are in French.
I hope this technique will be helpful to some of you.
Morgan.
________________________________________________________________________
Script before the 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 after the 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
#########################################
