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