J’ai une infrastructure virtualisée sous Hyper-V avec un serveur de backup/DRP séparé. La stack complète tourne sur 19 VMs : deux domaines Active Directory avec approbation, des DNS forwarders Linux (BIND9), du RADIUS, du monitoring, des services applicatifs… bref, pas un truc qu’on peut se permettre de remettre en route n’importe comment.
L’objectif du projet était simple à énoncer, complexe à réaliser :
Basculer toute l’infra sur le serveur DRP en un minimum d’actions, avec continuité de service garantie (en tout cas ne pas couper l’aces a internet) puis revenir en prod proprement.
Spoiler : ça m’a pris plusieurs itérations, quelques sueurs froides et une bonne dose de PowerShell pour y arriver. Quelques litres de café aussi…
Architecture
HYPERV1 (Hyper-V prod)
├── 2 domaines AD avec approbation
│ ├── Domain1 : SRV-PDC1 (PDC + DNS intégré AD)
│ │ SRV-DC1 (DC secondaire + DNS intégré AD)
│ └── Domain2 : SRV-PDC2 (PDC + DNS intégré AD + DHCP)
│ SRV-DC2 (DC secondaire + DNS intégré AD + DHCP)
├── 2 DNS forwarders Linux BIND9 + NTP
│ ├── SRV-DNS1 (DNS forwarder primaire + NTP)
│ └── SRV-DNS2 (DNS forwarder secondaire + NTP)
├── RADIUS, Proxy, SMTP, SIEM, Monitoring...
└── 3 workstations virtuelles
HYPERV2 (Hyper-V DRP)
└── Veeam B&R 12 + réplicas NVMe de toutes les VMs
La réplication tourne 4 fois par semaine avec une rétention de 2 restore points — suffisant pour garantir un RPO de 24h maximum.
Les deux scénarios
Dès le départ, j’ai identifié deux cas d’usage radicalement différents :
FULL DRP — Crash réel La prod est morte. On bascule tout immédiatement. Les PDC démarrent en premier, le reste suit. Pas de continuité à gérer puisque tout est déjà down.
MCO DRP — Maintenance planifiée La prod tourne encore. On doit basculer sans interruption de service. Là, l’ordre de démarrage devient critique — il faut toujours avoir au moins un DC par domaine et un DNS forwarder actifs quelque part sur le réseau.
Cette distinction a imposé la création de deux Failover Plans distincts dans Veeam, avec des ordres de démarrage différents.
Le Failover Plan MCO : l’ordre qui change tout
Pour la maintenance planifiée, la séquence correcte est la suivante :
Le principe repose sur deux groupes qui basculent en alternance, garantissant qu’à chaque instant Domain1, Domain2 et le DNS ont toujours au moins un service actif quelque part sur le réseau.
Groupe 1 — PDC Domain1 + DC secondaire Domain2 + DNS forwarder primaire :
SRV-PDC1 · SRV-DC2 · SRV-DNS1
Groupe 2 — DC secondaire Domain1 + PDC Domain2 + DNS forwarder secondaire :
SRV-DC1 · SRV-PDC2 · SRV-DNS2
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ HYPERV1 (prod) HYPERV2 (DRP) │
│ Domain1 Domain2 DNS Domain1 Domain2 DNS │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ Départ SRV-PDC1 ✅ SRV-PDC2 ✅ DNS1 ✅ — — — │
│ SRV-DC1 ✅ SRV-DC2 ✅ DNS2 ✅ │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ Etape 1 SRV-PDC1 ⬇️ SRV-PDC2 ✅ DNS1 ⬇️ — — — │
│ Groupe 1 SRV-DC1 ✅ SRV-DC2 ⬇️ DNS2 ✅ │
│ down H1 ↑DC1+PDC2+DNS2 encore UP sur HYPERV1 → continuité garantie ✅ │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ Etape 2 SRV-DC1 ✅ SRV-PDC2 ✅ DNS2 ✅ SRV-PDC1 🔄 SRV-DC2 🔄 DNS1 🔄 │
│ Groupe 1 ↑Groupe 2 reste UP sur HYPERV1 ↑Groupe 1 démarre sur HYPERV2 │
│ up H2 ↑Failover Plan MCO-DRP Wave 1 lancé │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ Etape 3 SRV-DC1 ✅ SRV-PDC2 ✅ DNS2 ✅ SRV-PDC1 ✅ SRV-DC2 ✅ DNS1 ✅ │
│ Groupe 1 ↑Groupe 2 encore UP sur HYPERV1 ↑Groupe 1 confirmé Running H2 │
│ confirmé │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ Etape 4 SRV-DC1 ⬇️ SRV-PDC2 ⬇️ DNS2 ⬇️ SRV-PDC1 ✅ SRV-DC2 ✅ DNS1 ✅ │
│ Groupe 2 ↑Groupe 2 éteint sur HYPERV1 ↑PDC1+DC2+DNS1 up sur HYPERV2 │
│ down H1 → continuité garantie ✅ │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ Etape 5 — SRV-PDC1 ✅ SRV-DC2 ✅ DNS1 ✅ │
│ Groupe 2 SRV-DC1 ✅ SRV-PDC2 ✅ DNS2 ✅ │
│ up H2 ↑Tout sur HYPERV2 ✅ │
└────────────────────────────────────────────────────────────────────────────────────────┘
À chaque étape, Domain1 a toujours un DC actif, Domain2 aussi, et le DNS forwarder répond toujours. Aucune interruption de service AD, DNS ou DHCP pendant toute la durée de la bascule.
Le script Start-DRP.ps1
Tout est automatisé depuis HYPERV2 en PowerShell. Le script gère les deux modes via un menu interactif :
.\Start-DRP.ps1
============================================================
PROCEDURE DRP - Choisissez le mode de bascule
============================================================
[1] CRASH - Prod en panne, démarrage immédiat
Utilise le Failover Plan : FULL DRP
[2] MCO - Maintenance planifiée, continuité garantie
Utilise le Failover Plan : MCO DRP
[3] MCO + Skip - MCO sans réplication préalable
============================================================
Votre choix (1/2/3) :
Après confirmation, le script enchaîne automatiquement :
- Réplication manuelle — dernière synchro avant bascule
- Flag anti-shutdown — bloque le post-script qui éteint HYPERV1 après réplication
- Shutdown ordonné des VMs non critiques (waves 5→3)
- En mode MCO : extinction du Groupe 1, lancement du plan, attente ping Groupe 1, extinction Groupe 2
- En mode CRASH : extinction de tout, lancement du plan
La vérification du démarrage des réplicas se fait via Hyper-V local (Get-VM SRV-DC1_VeeamReplica) et non par le réseau — évite les faux positifs quand la même IP répond encore depuis HYPERV1.
Le retour en prod : Start-FailbackToProd.ps1
C’est là que les choses se sont compliquées. Plusieurs versions ont été nécessaires pour arriver à quelque chose de propre.
Les pièges Veeam 12
Piège 1 — Get-VBRJobSession n’existe plus
Remplacée par Get-VBRSession avec un filtre sur JobName.
Piège 2 — Le commit sur le mauvais restore point
Après un Start-VBRHvReplicaFailback, Veeam crée un nouveau restore point (index 0). Si on passe ce RP à Stop-VBRHvReplicaFailback pour committer, la VM se retrouve bloquée en LockedItem. Il faut committer sur l’index 1 — le RP d’avant le failback.
# Index 0 = nouveau RP créé par le failback → NE PAS committer ça
# Index 1 = ancien RP Failover → c'est lui qu'on committe
$rpCommit = Get-VBRRestorePoint |
Where-Object { $_.IsReplica() -and $_.VmName -eq $vmName } |
Sort-Object CreationTime -Descending |
Select-Object -Skip 1 -First 1
Stop-VBRHvReplicaFailback -RestorePoint $rpCommit
Piège 3 — Le Failover Plan qui redémarre les réplicas
Tant que le Failover Plan est actif, il redémarre automatiquement les réplicas après chaque commit. La solution : utiliser Stop-VBRReplicaFailover individuellement sur chaque VM avant de lancer son failback. Ça éteint proprement le replica sans toucher aux autres.
# Undo individuel — n'affecte pas les autres VMs du plan
Stop-VBRReplicaFailover -RestorePoint $rpFailover
Piège 4 — Le VHDX encore verrouillé
Après le commit, Hyper-V n’a pas encore libéré le fichier VHDX. Un Start-VM immédiat échoue avec “Le processus ne peut pas accéder au fichier”. Solution : attendre 15 secondes entre le commit et le démarrage de la VM.
La séquence finale pour chaque VM
1. Stop-VBRReplicaFailover (index 0)
→ Eteint proprement le replica sur HYPERV2
2. Start-VBRHvReplicaFailback (SANS RunAsync = bloquant)
→ Resync complète vers HYPERV1
→ Veeam crée un nouveau RP
3. Stop-VBRHvReplicaFailback (index 1)
→ Commit
4. Start-Sleep 15
→ Libération VHDX
5. Start-VM sur HYPERV1
→ Délai wave → VM suivante
L’ordre de retour pour la continuité AD/DNS
SRV-PDC1 (PDC Domain1 + DNS AD) 120s
SRV-PDC2 (PDC Domain2 + DNS AD + DHCP) 90s
SRV-DNS1 (DNS forwarder + NTP) 60s
SRV-DC1 (DC2 Domain1 + DNS AD) 60s
SRV-DC2 (DC2 Domain2 + DNS AD + DHCP) 60s
SRV-DNS2 (DNS forwarder + NTP) 30s
... services applicatifs ...
... workstations ...
La logique est identique au MCO DRP : à chaque instant, au moins un DC par domaine et un DNS sont actifs quelque part sur le réseau.
Les bugs qui m’ont coûté du temps
Le module Hyper-V pas chargé dans le contexte script
Get-VM fonctionnait parfaitement en session interactive mais retournait null dans le script. Cause : le module Hyper-V n’est pas chargé automatiquement dans un contexte PowerShell non-interactif. Fix :
Import-Module Hyper-V -ErrorAction Stop -WarningAction SilentlyContinue
L’état .State retourné comme PSObject via WinRM
Quand on interroge l’état d’une VM via Invoke-Command, .State retourne un objet PSObject et non une string. La comparaison $state -eq "Off" échoue silencieusement. Fix :
(Get-VM -Name $name).State.ToString()
Get-VBRSession demande un paramètre obligatoire
Contrairement à ce que laisse penser la documentation, Get-VBRSession sans paramètre ouvre un prompt interactif. Il faut passer -ErrorAction SilentlyContinue pour éviter le blocage dans un script automatisé.
Le deadlock MCO
Première implémentation : le script attendait que les réplicas soient Running sur HYPERV2 avant de les éteindre sur HYPERV1. Problème : Veeam ne démarre pas un replica tant que la VM source tourne. Résultat : deadlock infini. La solution était de les éteindre avant de lancer le Failover Plan, puis d’attendre leur démarrage.
Résultats
Après une journée de tests et d’itérations, les deux scripts fonctionnent en production :
| Scénario | Temps total | Interruption AD/DNS |
|---|---|---|
| MCO DRP (bascule) | ~8 min | 0 seconde |
| Failback MCO | ~25 min | 0 seconde |
| FULL DRP (crash) | ~3 min | Non applicable |
Le temps de failback est plus long car chaque VM est traitée séquentiellement avec sa resync individuelle — c’est le prix de la continuité de service.
Ce que j’aurais fait différemment
Tester sur une VM isolée avant de tout automatiser. Chaque piège Veeam (commit index 1, RunAsync vs bloquant, Stop-VBRReplicaFailover) aurait pu être découvert sur une seule VM avant de l’intégrer dans le script complet.
Documenter les commandes Veeam PowerShell au fur et à mesure. La documentation officielle est incomplète sur certains points (comportement de Start-VBRHvReplicaFailback avec -RunAsync, gestion du RP après failback). Les forums Veeam sont plus fiables.
Les scripts
Les deux scripts sont conçus pour tourner depuis HYPERV2 (le serveur DRP). Ils incluent un changelog versionné, un mode -WhatIf pour simuler sans action, et des logs horodatés complets dans C:\Scripts\DRP\Logs\.
Start-DRP.ps1
Bascule de HYPERV1 (prod) vers HYPERV2 (DRP). Menu interactif au lancement.
# Usage
.\Start-DRP.ps1 # Menu interactif
.\Start-DRP.ps1 -Mode MCO # MCO direct
.\Start-DRP.ps1 -Mode CRASH # Crash direct
.\Start-DRP.ps1 -Mode MCO -SkipReplication # MCO sans réplication
# =============================================================================
# Start-DRP.ps1
# Script de bascule DRP complet depuis GIEDI PRIME
#
# Deux modes de bascule :
#
# MODE CRASH (defaut) :
# Utilise quand la prod est en panne ou inaccessible.
# Demarre toutes les VMs sur HYPERV2 via le Failover Plan "FULL-DRP"
# sans se preoccuper de la continuite de service.
# Etapes :
# 1. Replication Veeam
# 2. Shutdown toutes les VMs sur HYPERV1
# 3. Lancement Failover Plan "FULL-DRP"
#
# MODE MCO (maintenance planifiee) :
# Utilise pour une maintenance programmee avec continuite de service.
# Garantit qu'un DC par domaine et un DNS sont toujours up pendant la bascule.
# Etapes :
# 1. Replication Veeam
# 2. Shutdown waves 5/4/3 sur HYPERV1
# 3. Lancement Failover Plan "MCO-DRP"
# 4. Attente Groupe 1 Running sur HYPERV2 (SRV-DC1+SRV-DC2+SRV-DNS1)
# puis extinction Groupe 1 sur HYPERV1
# 5. Attente Groupe 2 Running sur HYPERV2 (SRV-PDC1+SRV-PDC2+SRV-DNS2)
# puis extinction Groupe 2 sur HYPERV1
#
# Prerequis :
# - WinRM actif sur HYPERV1
# - Droits admin sur HYPERV1
# - Veeam Backup & Replication console installee sur GIEDI PRIME
# - Script a lancer en PowerShell Administrator sur GIEDI PRIME
# - Failover Plans "FULL-DRP" et "MCO-DRP" crees dans Veeam
#
# Usage :
# .\Start-DRP.ps1 -> Mode CRASH + replication
# .\Start-DRP.ps1 -SkipReplication -> Mode CRASH sans replication
# .\Start-DRP.ps1 -Mode MCO -> Mode MCO + replication
# .\Start-DRP.ps1 -Mode MCO -SkipReplication -> Mode MCO sans replication
#
# =============================================================================
# WARNING MAINTENANCE DU SCRIPT - LIRE AVANT TOUTE MODIFICATION
# =============================================================================
#
# A chaque ajout ou suppression d'une VM dans l'infrastructure :
#
# 1. Mettre a jour les Failover Plans "FULL-DRP" et "MCO-DRP" dans Veeam
# 2. Mettre a jour $VMShutdownNonCritical, $MCOGroupe1 et $MCOGroupe2
# 3. Mettre a jour le CHANGELOG
#
# Rappel ordre FULL-DRP :
# Wave 1 : SRV-PDC1 (120s) > SRV-PDC2 (90s) > SRV-DNS1 (60s)
# Wave 2 : SRV-DC1 (60s) > SRV-DC2 (60s) > SRV-DNS2 (30s)
# Wave 3 : SRV-RADIUS (45s) > SRV-PROXY (30s) > SRV-SMTP (30s) > SRV-PASSBOLT (30s)
# Wave 4 : SRV-SIEM (45s) > SRV-MONITORING (45s) > SRV-WSUS (30s)
# > SRV-PRINT (30s) > SRV-PKI (20s)
# Wave 5 : SRV-PXE (20s) > WS-01 (20s) > WS-02 (20s)
# > WS-03 (20s)
#
# Rappel ordre MCO-DRP :
# Wave 1 : SRV-DC1 (90s) > SRV-DC2 (90s) > SRV-DNS1 (60s)
# Wave 2 : SRV-PDC1 (120s) > SRV-PDC2 (90s) > SRV-DNS2 (30s)
# Wave 3 : SRV-RADIUS (45s) > SRV-PROXY (30s) > SRV-SMTP (30s) > SRV-PASSBOLT (30s)
# Wave 4 : SRV-SIEM (45s) > SRV-MONITORING (45s) > SRV-WSUS (30s)
# > SRV-PRINT (30s) > SRV-PKI (20s)
# Wave 5 : SRV-PXE (20s) > WS-01 (20s) > WS-02 (20s)
# > WS-03 (20s)
#
# =============================================================================
# CHANGELOG
# =============================================================================
# 2026-03-25 - v1.0 - Version initiale - 18 VMs
# 2026-03-25 - v1.1 - Correction nom du job : ReplicaVM-HYPERV1_Dayly
# 2026-03-25 - v1.2 - Ajout flag DRP
# 2026-03-28 - v1.3 - Import-Module au lieu de Add-PSSnapin
# 2026-03-28 - v1.4 - Correction detection fin de job + -SkipReplication
# 2026-03-28 - v1.5 - Correction Get-VBRSession
# 2026-03-28 - v1.6 - Correction .ToString() sur State via WinRM
# 2026-03-28 - v1.7 - Ajout verification NETLOGON DC critiques (mauvaise logique)
# 2026-03-28 - v1.8 - Correction Get-VBRSession -ErrorAction SilentlyContinue
# 2026-03-28 - v1.9 - Refactoring logique DC (mauvais ordre)
# 2026-03-28 - v2.0 - Refactoring complet logique DC/DNS par paires
# 2026-03-28 - v2.1 - Ajout mode MCO-DRP avec continuite de service garantie
# Groupe 1 (SRV-DC1+SRV-DC2+SRV-DNS1) demarre sur HYPERV2
# puis s'eteint sur HYPERV1 avant Groupe 2
# Verification via Hyper-V local (Get-VM _VeeamReplica)
# pour eviter tout conflit nom/IP pendant la bascule
# 2026-03-28 - v2.2 - Ajout menu interactif si -Mode non specifie
# Confirmation avant lancement
# 2026-03-28 - v2.3 - Correction null state sur Get-VM
# 2026-03-28 - v2.4 - Correction deadlock MCO :
# Groupe 1 s'eteint sur HYPERV1 AVANT le Failover Plan
# Le Failover Plan demarre Groupe 1 sur HYPERV2
# Puis Groupe 2 s'eteint sur HYPERV1
# =============================================================================
param(
[ValidateSet("CRASH","MCO")]
[string]$Mode = "",
[switch]$SkipReplication
)
# --- MENU INTERACTIF ---------------------------------------------------------
# Si Mode non specifie en parametre, afficher le menu de selection
if ($Mode -eq "") {
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " PROCEDURE DRP - Choisissez le mode de bascule" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host " [1] CRASH " -ForegroundColor Red -NoNewline
Write-Host "- Prod en panne, demarrage immediat sur GIEDI PRIME"
Write-Host " Utilise le Failover Plan : FULL-DRP"
Write-Host ""
Write-Host " [2] MCO " -ForegroundColor Yellow -NoNewline
Write-Host "- Maintenance planifiee, continuite de service garantie"
Write-Host " Utilise le Failover Plan : MCO-DRP"
Write-Host ""
Write-Host " [3] MCO + Skip " -ForegroundColor Yellow -NoNewline
Write-Host "- MCO sans replication (replicas deja a jour)"
Write-Host " Utilise le Failover Plan : MCO-DRP"
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
$choix = Read-Host "Votre choix (1/2/3)"
switch ($choix) {
"1" {
$Mode = "CRASH"
Write-Host ""
Write-Host "Mode CRASH selectionne." -ForegroundColor Red
}
"2" {
$Mode = "MCO"
Write-Host ""
Write-Host "Mode MCO selectionne." -ForegroundColor Yellow
}
"3" {
$Mode = "MCO"
$SkipReplication = $true
Write-Host ""
Write-Host "Mode MCO + SkipReplication selectionne." -ForegroundColor Yellow
}
default {
Write-Host ""
Write-Host "Choix invalide. Arret du script." -ForegroundColor Red
exit 1
}
}
# Confirmation avant de lancer
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " CONFIRMATION" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " Mode : $Mode" -ForegroundColor White
Write-Host " Failover Plan: $(if ($Mode -eq 'MCO') { 'MCO-DRP' } else { 'FULL-DRP' })" -ForegroundColor White
Write-Host " Replication : $(if ($SkipReplication) { 'IGNOREE' } else { 'OUI' })" -ForegroundColor White
Write-Host " Hote prod : HYPERV1" -ForegroundColor White
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
$confirm = Read-Host "Confirmer le lancement ? (O/N)"
if ($confirm -notmatch "^[Oo]$") {
Write-Host "Annule par l'utilisateur." -ForegroundColor Yellow
exit 0
}
}
# --- CONFIGURATION -----------------------------------------------------------
$ScriptVersion = "2.8"
$VeeamReplicaJobName = "ReplicaVM-HYPERV1_Dayly"
$VeeamFailoverPlan = if ($Mode -eq "MCO") { "MCO-DRP" } else { "FULL-DRP" }
$ProdHost = "HYPERV1"
$LogFile = "C:\Scripts\DRP\Logs\DRP_${Mode}_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$ShutdownTimeout = 300 # Secondes max pour attendre le shutdown d'une VM (5 min)
$ReplicationTimeout = 7200 # Secondes max pour attendre la fin de la replication (2h)
$VMReadyTimeout = 600 # Secondes max pour attendre qu'une VM soit Running sur HYPERV2 (10 min)
$VeeamModule = "C:\Program Files\Veeam\Backup and Replication\Console\Veeam.Backup.PowerShell.dll"
# VMs non critiques - shutdown dans les 2 modes (waves 5, 4, 3)
$VMShutdownNonCritical = @(
# Wave 5 - Workstations
"WS-03", "WS-02", "WS-01",
# Wave 4 - Services applicatifs
"SRV-PXE", "SRV-PKI", "SRV-PRINT", "SRV-WSUS", "SRV-MONITORING", "SRV-SIEM",
# Wave 3 - Services reseau
"SRV-PASSBOLT", "SRV-SMTP", "SRV-PROXY", "SRV-RADIUS"
)
# Mode CRASH - shutdown complet waves 2 et 1 (apres non critiques)
$VMShutdownCrash = @(
"SRV-DNS2", "SRV-DC2", "SRV-DC1",
"SRV-DNS1", "SRV-PDC2", "SRV-PDC1"
)
# Mode MCO - Groupe 1 : DC secondaires + DNS primaire
# Demarrent en Wave 1 du MCO-DRP → s'eteignent sur HYPERV1 une fois up sur HYPERV2
# (leurs homologues SRV-PDC1, SRV-PDC2, SRV-DNS2 restent up sur HYPERV1)
$MCOGroupe1 = @("SRV-DC1", "SRV-DC2", "SRV-DNS1")
# Mode MCO - Groupe 2 : DC primaires + DNS failover
# Demarrent en Wave 2 du MCO-DRP → s'eteignent sur HYPERV1 une fois up sur HYPERV2
$MCOGroupe2 = @("SRV-PDC1", "SRV-PDC2", "SRV-DNS2")
# --- FONCTIONS ---------------------------------------------------------------
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$timestamp] [$Level] $Message"
Write-Host $line -ForegroundColor $(switch ($Level) {
"INFO" { "Cyan" }
"OK" { "Green" }
"WARN" { "Yellow" }
"ERROR" { "Red" }
default { "White" }
})
Add-Content -Path $LogFile -Value $line
}
function Wait-VMOff {
param([string]$VMName, [int]$TimeoutSec = $ShutdownTimeout)
$elapsed = 0
while ($elapsed -lt $TimeoutSec) {
$state = Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($name)
(Get-VM -Name $name -ErrorAction SilentlyContinue).State.ToString()
} -ArgumentList $VMName
if ($state -eq "Off") { return $true }
Start-Sleep -Seconds 5
$elapsed += 5
}
return $false
}
function Stop-VMProprement {
param([string]$VMName)
$vmState = Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($name)
$v = Get-VM -Name $name -ErrorAction SilentlyContinue
if ($v) { $v.State.ToString() } else { "NotFound" }
} -ArgumentList $VMName
if ($vmState -eq "NotFound") {
Write-Log "VM '$VMName' introuvable sur $ProdHost, ignoree" "WARN"
return
}
if ($vmState -eq "Off") {
Write-Log "VM '$VMName' deja eteinte, ignoree" "OK"
return
}
Write-Log "Arret de '$VMName' (etat: $vmState)..."
Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($name)
Stop-VM -Name $name -Force -ErrorAction SilentlyContinue
} -ArgumentList $VMName
$isOff = Wait-VMOff -VMName $VMName
if ($isOff) {
Write-Log "VM '$VMName' eteinte proprement" "OK"
} else {
Write-Log "VM '$VMName' n'a pas repondu, power off force..." "WARN"
Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($name)
Stop-VM -Name $name -TurnOff -Force -ErrorAction SilentlyContinue
} -ArgumentList $VMName
Start-Sleep -Seconds 10
$finalState = Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($name)
(Get-VM -Name $name).State.ToString()
} -ArgumentList $VMName
if ($finalState -eq "Off") {
Write-Log "VM '$VMName' eteinte de force" "OK"
} else {
Write-Log "VM '$VMName' impossible a eteindre ! (etat: $finalState)" "ERROR"
}
}
}
function Wait-VMRunningLocal {
# Verifie que la VM replica est Running sur HYPERV2 via Hyper-V local
# On verifie le nom _VeeamReplica directement sur l hyperviseur local
# Pas de dependance reseau — evite tout conflit avec la VM source sur HYPERV1
param([string]$VMName, [int]$TimeoutSec = $VMReadyTimeout)
$replicaName = "${VMName}_VeeamReplica"
$elapsed = 0
Write-Log "Attente '$replicaName' Running sur GIEDI PRIME (Hyper-V local)..." "WARN"
while ($elapsed -lt $TimeoutSec) {
$vm = Get-VM -Name $replicaName -ErrorAction SilentlyContinue
$state = if ($vm) { $vm.State.ToString() } else { "NotFound" }
if ($state -eq "Running") {
Write-Log "'$VMName' confirme Running sur GIEDI PRIME" "OK"
return $true
}
Write-Log "'$VMName' pas encore Running (etat: $state) — $([math]::Round($elapsed/60,1)) min" "WARN"
Start-Sleep -Seconds 15
$elapsed += 15
}
Write-Log "'$VMName' pas Running apres $($TimeoutSec/60) min — on continue quand meme" "ERROR"
return $false
}
function Wait-GroupeRunning {
param([string[]]$VMNames)
Write-Log "Attente que toutes les VMs du groupe soient Running sur GIEDI PRIME..."
$allReady = $true
foreach ($vmName in $VMNames) {
$ready = Wait-VMRunningLocal -VMName $vmName
if (-not $ready) { $allReady = $false }
}
return $allReady
}
# --- INITIALISATION ----------------------------------------------------------
$logDir = Split-Path $LogFile
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
Write-Log "============================================================"
Write-Log "DEBUT DE LA PROCEDURE DRP"
Write-Log "Version : $ScriptVersion"
Write-Log "Mode : $Mode"
Write-Log "Failover Plan: $VeeamFailoverPlan"
Write-Log "Job Veeam : $VeeamReplicaJobName"
Write-Log "Hote prod : $ProdHost"
if ($SkipReplication) { Write-Log "Replication : ignoree (-SkipReplication)" "WARN" }
Write-Log "============================================================"
# Charger le module Hyper-V (necessaire pour Get-VM dans le contexte script)
Write-Log "Chargement du module Hyper-V..."
try {
Import-Module Hyper-V -ErrorAction Stop -WarningAction SilentlyContinue
Write-Log "Module Hyper-V charge" "OK"
} catch {
Write-Log "Impossible de charger le module Hyper-V : $_" "ERROR"
exit 1
}
# Charger le module Veeam
Write-Log "Chargement du module Veeam PowerShell..."
try {
Import-Module $VeeamModule -ErrorAction Stop -WarningAction SilentlyContinue
Write-Log "Module Veeam charge" "OK"
} catch {
Write-Log "Impossible de charger le module Veeam : $_" "ERROR"
exit 1
}
# Verifier WinRM sur HYPERV1
try {
Invoke-Command -ComputerName $ProdHost -ScriptBlock { $env:COMPUTERNAME } -ErrorAction Stop | Out-Null
Write-Log "Connexion WinRM vers $ProdHost OK" "OK"
} catch {
Write-Log "Impossible de se connecter a $ProdHost via WinRM : $_" "ERROR"
exit 1
}
# --- ETAPE 1 : REPLICATION ---------------------------------------------------
if ($SkipReplication) {
Write-Log "------------------------------------------------------------"
Write-Log "ETAPE 1 : Replication ignoree (-SkipReplication)" "WARN"
} else {
Write-Log "------------------------------------------------------------"
Write-Log "ETAPE 1 : Lancement du job de replication '$VeeamReplicaJobName'"
$flagFile = "C:\Scripts\DRP\DRP_MODE.flag"
try {
Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($f) New-Item -Path $f -ItemType File -Force | Out-Null
} -ArgumentList $flagFile
Write-Log "Flag DRP cree sur $ProdHost : $flagFile" "OK"
} catch {
Write-Log "Impossible de creer le flag DRP sur $ProdHost : $_" "ERROR"
exit 1
}
try {
$job = Get-VBRJob -Name $VeeamReplicaJobName -ErrorAction Stop
} catch {
Write-Log "Job de replication introuvable : $_" "ERROR"
Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($f) Remove-Item $f -Force -ErrorAction SilentlyContinue
} -ArgumentList $flagFile
exit 1
}
if ($job.IsRunning) {
Write-Log "Le job est deja en cours, attente de fin..." "WARN"
} else {
Start-VBRJob -Job $job | Out-Null
Write-Log "Job de replication demarre" "OK"
}
Write-Log "Attente du demarrage effectif du job..."
Start-Sleep -Seconds 20
Write-Log "Attente de la fin de la replication (timeout $($ReplicationTimeout/60) min)..."
$elapsed = 0
$success = $false
while ($elapsed -lt $ReplicationTimeout) {
$job = Get-VBRJob -Name $VeeamReplicaJobName
if (-not $job.IsRunning) {
$lastSession = Get-VBRSession -ErrorAction SilentlyContinue |
Where-Object { $_.JobName -eq $VeeamReplicaJobName } |
Sort-Object CreationTime -Descending |
Select-Object -First 1
if ($lastSession -and ($lastSession.Result -eq "Success" -or $lastSession.Result -eq "Warning")) {
Write-Log "Replication terminee avec succes (Result: $($lastSession.Result))" "OK"
$success = $true
break
} elseif ($lastSession -and $lastSession.Result -ne "" -and $lastSession.Result -ne "None") {
Write-Log "Replication terminee en ERREUR (Result: $($lastSession.Result))" "ERROR"
break
} else {
Write-Log "Job demarre, en attente du resultat..." "INFO"
Start-Sleep -Seconds 30
$elapsed += 30
continue
}
}
Start-Sleep -Seconds 30
$elapsed += 30
Write-Log "Replication en cours... ($([math]::Round($elapsed/60,1)) min ecoulees)"
}
if (-not $success) {
Write-Log "La replication a echoue ou depasse le timeout. Arret du script." "ERROR"
Write-Log "Relancer avec -SkipReplication si les replicas sont a jour." "ERROR"
exit 1
}
}
# --- ETAPE 2 : SHUTDOWN VMs NON CRITIQUES ------------------------------------
Write-Log "------------------------------------------------------------"
Write-Log "ETAPE 2 : Shutdown VMs non critiques sur $ProdHost (waves 5/4/3)"
foreach ($vmName in $VMShutdownNonCritical) {
Stop-VMProprement -VMName $vmName
}
# --- ETAPE 3 : SHUTDOWN DC/DNS + LANCEMENT FAILOVER PLAN --------------------
Write-Log "------------------------------------------------------------"
if ($Mode -eq "CRASH") {
# Mode CRASH : on eteint tout puis on lance le Failover Plan
Write-Log "ETAPE 3 : Mode CRASH — Shutdown DC/DNS sur $ProdHost"
foreach ($vmName in $VMShutdownCrash) {
Stop-VMProprement -VMName $vmName
}
Write-Log "------------------------------------------------------------"
Write-Log "ETAPE 4 : Lancement du Failover Plan '$VeeamFailoverPlan'"
try {
$fp = Get-VBRFailoverPlan -Name $VeeamFailoverPlan -ErrorAction Stop
Start-VBRFailoverPlan -FailoverPlan $fp | Out-Null
Write-Log "Failover Plan lance — VMs en cours de demarrage sur GIEDI PRIME" "OK"
} catch {
Write-Log "Erreur au lancement du Failover Plan : $_" "ERROR"
exit 1
}
} else {
# Mode MCO : sequence en 2 groupes avec continuite de service
#
# Sequence :
# 1. Eteindre Groupe 1 sur HYPERV1 (SRV-DC1+SRV-DC2+SRV-DNS1)
# SRV-PDC1+SRV-PDC2+SRV-DNS2 encore up sur HYPERV1 -> continuite AD/DNS
# 2. Lancer le Failover Plan MCO-DRP
# -> Wave 1 demarre SRV-DC1+SRV-DC2+SRV-DNS1 sur HYPERV2
# 3. Attendre Groupe 1 Running sur HYPERV2
# 4. Eteindre Groupe 2 sur HYPERV1 (SRV-PDC1+SRV-PDC2+SRV-DNS2)
# SRV-DC1+SRV-DC2+SRV-DNS1 up sur HYPERV2 -> continuite AD/DNS
Write-Log "ETAPE 3 : Mode MCO — Extinction Groupe 1 sur $ProdHost"
Write-Log "Groupe 1 : SRV-DC1 + SRV-DC2 + SRV-DNS1"
Write-Log "SRV-PDC1 + SRV-PDC2 + SRV-DNS2 restent up sur $ProdHost -> continuite AD/DNS" "WARN"
foreach ($vmName in $MCOGroupe1) {
Stop-VMProprement -VMName $vmName
}
Write-Log "Groupe 1 eteint sur $ProdHost" "OK"
Write-Log "------------------------------------------------------------"
Write-Log "ETAPE 4 : Lancement du Failover Plan '$VeeamFailoverPlan'"
try {
$fp = Get-VBRFailoverPlan -Name $VeeamFailoverPlan -ErrorAction Stop
Start-VBRFailoverPlan -FailoverPlan $fp | Out-Null
Write-Log "Failover Plan lance — Wave 1 demarre Groupe 1 sur GIEDI PRIME" "OK"
} catch {
Write-Log "Erreur au lancement du Failover Plan : $_" "ERROR"
exit 1
}
Write-Log "------------------------------------------------------------"
Write-Log "ETAPE 5 : Attente Groupe 1 Running sur GIEDI PRIME"
Write-Log "Groupe 1 : SRV-DC1 + SRV-DC2 + SRV-DNS1"
Wait-GroupeRunning -VMNames $MCOGroupe1 | Out-Null
Write-Log "------------------------------------------------------------"
Write-Log "ETAPE 6 : Extinction Groupe 2 sur $ProdHost"
Write-Log "Groupe 2 : SRV-PDC1 + SRV-PDC2 + SRV-DNS2"
Write-Log "Groupe 1 up sur GIEDI PRIME -> continuite AD/DNS garantie" "WARN"
foreach ($vmName in $MCOGroupe2) {
Stop-VMProprement -VMName $vmName
}
Write-Log "Groupe 2 eteint sur $ProdHost" "OK"
}
# --- ETAPE 5 : VERIFICATION FINALE -------------------------------------------
Write-Log "------------------------------------------------------------"
Write-Log "ETAPE 5 : Verification finale — toutes les VMs Off sur $ProdHost"
$allOff = $true
$vmStates = Invoke-Command -ComputerName $ProdHost -ScriptBlock {
Get-VM | Select-Object Name, @{N="State";E={$_.State.ToString()}}
}
foreach ($vm in $vmStates) {
if ($vm.State -ne "Off") {
Write-Log "VM '$($vm.Name)' encore en etat '$($vm.State)'" "ERROR"
$allOff = $false
} else {
Write-Log "VM '$($vm.Name)' : Off" "OK"
}
}
if (-not $allOff) {
Write-Log "Certaines VMs pas eteintes — verifiez manuellement" "WARN"
} else {
Write-Log "Toutes les VMs sont Off sur $ProdHost" "OK"
}
# --- FIN ---------------------------------------------------------------------
Write-Log "============================================================"
Write-Log "PROCEDURE DRP $Mode TERMINEE"
Write-Log "Surveillez le demarrage des VMs dans la console Veeam"
Write-Log "Log complet : $LogFile"
Write-Log "============================================================"
Start-FailbackToProd.ps1
Retour de HYPERV2 (DRP) vers HYPERV1 (prod). Menu interactif au lancement.
# Usage
.\Start-FailbackToProd.ps1 # Menu interactif
.\Start-FailbackToProd.ps1 -WhatIf # Dry run
# =============================================================================
# Start-FailbackToProd.ps1
# Script de retour en production depuis GIEDI PRIME
#
# Principe de continuite de service :
# Les VMs sont traitees par PAIRES pour garantir qu'un DC par domaine
# et un DNS sont toujours up pendant toute la duree du failback.
#
# Pour chaque VM dans l'ordre :
# 1. Stop-VBRReplicaFailover → Undo individuel, eteint le replica sur HYPERV2
# sans toucher aux autres VMs du Failover Plan
# 2. Start-VBRHvReplicaFailback (bloquant) → resync vers HYPERV1
# 3. Stop-VBRHvReplicaFailback (index 1) → commit
# 4. Attente 15s liberation VHDX
# 5. Start-VM sur HYPERV1
# 6. Delai wave → VM suivante
#
# Exemple Domain1 (sans interruption) :
# SRV-PDC1 undo → failback → commit → start HYPERV1 (120s)
# SRV-DC1 encore Failover HYPERV2 → D1 couvert ✅
# SRV-DC1 undo → failback → commit → start HYPERV1 (60s)
# SRV-PDC1 up HYPERV1 → D1 couvert ✅
#
# Prerequis :
# - WinRM actif sur HYPERV1
# - Droits admin sur HYPERV1
# - Veeam Backup & Replication console installee sur GIEDI PRIME
# - Script a lancer en PowerShell Administrator sur GIEDI PRIME
# - Les VMs DRP doivent etre en etat Failover dans Veeam
#
# Usage :
# .\Start-FailbackToProd.ps1 -> execution reelle (menu interactif)
# .\Start-FailbackToProd.ps1 -WhatIf -> dry run (simulation sans action)
#
# =============================================================================
# WARNING MAINTENANCE DU SCRIPT - LIRE AVANT TOUTE MODIFICATION
# =============================================================================
#
# A chaque ajout ou suppression d'une VM dans l'infrastructure :
#
# 1. Mettre a jour $VMStartOrder ci-dessous
# Respecter l'ordre par PAIRES pour la continuite DC/DNS :
# - PDC domaine X en premier, puis DC secondaire domaine X
# - DNS primaire en premier, puis DNS secondaire
#
# 2. Mettre a jour le CHANGELOG
#
# Rappel de l'ordre (paires pour continuite de service) :
# Wave 1 : SRV-PDC1 (120s) > SRV-PDC2 (90s) > SRV-DNS1 (60s)
# Wave 2 : SRV-DC1 (60s) > SRV-DC2 (60s) > SRV-DNS2 (30s)
# Wave 3 : SRV-RADIUS (45s) > SRV-PROXY (30s) > SRV-SMTP (30s) > SRV-PASSBOLT (30s)
# Wave 4 : SRV-SIEM (45s) > SRV-MONITORING (45s) > SRV-WSUS (30s)
# > SRV-PRINT (30s) > SRV-PKI (20s)
# Wave 5 : SRV-PXE (20s) > WS-01 (20s) > WS-02 (20s) > WS-03 (20s)
#
# =============================================================================
# CHANGELOG
# =============================================================================
# 2026-03-28 - v1.0 - Version initiale
# 2026-03-28 - v1.1 - Refactoring complet : logique VM par VM
# 2026-03-28 - v1.2 - Ajout verification DC avant commit
# 2026-04-03 - v1.3 - Ajout SRV-PASSBOLT (Passbolt) en Wave 3 apres SRV-SMTP (30s)
# 2026-04-03 - v1.4 - Suppression RunAsync, commit index 1, Undo global, delai 15s
# 2026-04-03 - v1.5 - Correction detection plan actif via restore points
# 2026-04-03 - v1.6 - Simplification logique Undo
# 2026-04-03 - v1.7 - Ajout menu interactif MCO/CRASH
# 2026-04-03 - v1.8 - Ordre par paires, suppression Undo global
# 2026-04-03 - v1.9 - Stop-VBRReplicaFailover individuel avant chaque failback
# 2026-04-03 - v2.0 - Ordre final : SRV-PDC1 > SRV-PDC2 > SRV-DNS1 > SRV-DC1
# > SRV-DC2 > SRV-DNS2 > services > workstations Remplacement Undo global par Stop-VBRReplicaFailover
# individuel avant chaque failback
# Garantit que le Failover Plan ne redemarre pas le replica
# apres le commit. Zero interruption de service.
# =============================================================================
param(
[ValidateSet("MCO","CRASH")]
[string]$Mode = "",
[switch]$WhatIf
)
# --- MENU INTERACTIF ---------------------------------------------------------
if ($Mode -eq "") {
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " PROCEDURE FAILBACK TO PRODUCTION" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host " [1] FAILBACK MCO " -ForegroundColor Yellow -NoNewline
Write-Host "- Retour apres maintenance planifiee"
Write-Host " Failover Plan : MCO-DRP"
Write-Host ""
Write-Host " [2] FAILBACK CRASH " -ForegroundColor Red -NoNewline
Write-Host "- Retour apres desastre"
Write-Host " Failover Plan : FULL-DRP"
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
$choix = Read-Host "Votre choix (1/2)"
switch ($choix) {
"1" { $Mode = "MCO"; Write-Host "`nMode FAILBACK MCO selectionne." -ForegroundColor Yellow }
"2" { $Mode = "CRASH"; Write-Host "`nMode FAILBACK CRASH selectionne." -ForegroundColor Red }
default {
Write-Host "`nChoix invalide. Arret du script." -ForegroundColor Red
exit 1
}
}
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " CONFIRMATION" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " Mode : FAILBACK $Mode" -ForegroundColor White
Write-Host " Hote prod : HYPERV1" -ForegroundColor White
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
$confirm = Read-Host "Confirmer le lancement ? (O/N)"
if ($confirm -notmatch "^[Oo]$") {
Write-Host "Annule par l'utilisateur." -ForegroundColor Yellow
exit 0
}
}
# --- CONFIGURATION -----------------------------------------------------------
$ScriptVersion = "2.0"
$ProdHost = "HYPERV1"
$LogFile = "C:\Scripts\DRP\Logs\FAILBACK_${Mode}_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$VeeamModule = "C:\Program Files\Veeam\Backup and Replication\Console\Veeam.Backup.PowerShell.dll"
# Ordre de demarrage prod
$VMStartOrder = @(
# Wave 1 - DC primaires + DNS primaire
@{ Name = "SRV-PDC1"; Delay = 120 },
@{ Name = "SRV-PDC2"; Delay = 90 },
@{ Name = "SRV-DNS1"; Delay = 60 },
# Wave 2 - DC secondaires + DNS secondaire
@{ Name = "SRV-DC1"; Delay = 60 },
@{ Name = "SRV-DC2"; Delay = 60 },
@{ Name = "SRV-DNS2"; Delay = 30 },
# Wave 3 - Services reseau
@{ Name = "SRV-RADIUS"; Delay = 45 },
@{ Name = "SRV-PROXY"; Delay = 30 },
@{ Name = "SRV-SMTP"; Delay = 30 },
@{ Name = "SRV-PASSBOLT"; Delay = 30 },
# Wave 4 - Services applicatifs
@{ Name = "SRV-SIEM"; Delay = 45 },
@{ Name = "SRV-MONITORING"; Delay = 45 },
@{ Name = "SRV-WSUS"; Delay = 30 },
@{ Name = "SRV-PRINT"; Delay = 30 },
@{ Name = "SRV-PKI"; Delay = 20 },
# Wave 5 - Workstations
@{ Name = "SRV-PXE"; Delay = 20 },
@{ Name = "WS-01"; Delay = 20 },
@{ Name = "WS-02"; Delay = 20 },
@{ Name = "WS-03"; Delay = 20 }
)
# --- FONCTIONS ---------------------------------------------------------------
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$prefix = if ($WhatIf) { "[WHATIF] " } else { "" }
$line = "[$timestamp] [$Level] $prefix$Message"
Write-Host $line -ForegroundColor $(switch ($Level) {
"INFO" { "Cyan" }
"OK" { "Green" }
"WARN" { "Yellow" }
"ERROR" { "Red" }
default { "White" }
})
Add-Content -Path $LogFile -Value $line
}
function Get-FailoverRestorePoint {
# Index 0 : restore point en etat Failover → passe a Stop-VBRReplicaFailover
# puis a Start-VBRHvReplicaFailback
param([string]$VmName)
return Get-VBRRestorePoint |
Where-Object { $_.IsReplica() -and $_.VmName -eq $VmName -and $_.State.ToString() -eq "Failover" } |
Sort-Object CreationTime -Descending |
Select-Object -First 1
}
function Get-CommitRestorePoint {
# Index 1 : second RP le plus recent apres le failback
# Start-VBRHvReplicaFailback cree un nouveau RP (index 0)
# On committe index 1 = l'ancien RP pour eviter VM bloquee en LockedItem
param([string]$VmName)
return Get-VBRRestorePoint |
Where-Object { $_.IsReplica() -and $_.VmName -eq $VmName } |
Sort-Object CreationTime -Descending |
Select-Object -Skip 1 -First 1
}
# --- INITIALISATION ----------------------------------------------------------
$logDir = Split-Path $LogFile
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
Write-Log "============================================================"
Write-Log "DEBUT DE LA PROCEDURE FAILBACK TO PRODUCTION"
Write-Log "Version : $ScriptVersion"
Write-Log "Mode : FAILBACK $Mode"
Write-Log "Hote prod : $ProdHost"
if ($WhatIf) { Write-Log "MODE DRY RUN - AUCUNE ACTION REELLE" "WARN" }
Write-Log "============================================================"
# Charger le module Veeam
Write-Log "Chargement du module Veeam PowerShell..."
try {
Import-Module $VeeamModule -ErrorAction Stop -WarningAction SilentlyContinue
Write-Log "Module Veeam charge" "OK"
} catch {
Write-Log "Impossible de charger le module Veeam : $_" "ERROR"
exit 1
}
# Verifier WinRM sur HYPERV1
try {
Invoke-Command -ComputerName $ProdHost -ScriptBlock { $env:COMPUTERNAME } -ErrorAction Stop | Out-Null
Write-Log "Connexion WinRM vers $ProdHost OK" "OK"
} catch {
Write-Log "Impossible de se connecter a $ProdHost via WinRM : $_" "ERROR"
exit 1
}
# --- VERIFICATION PRE-VOL ----------------------------------------------------
Write-Log "------------------------------------------------------------"
Write-Log "VERIFICATION : Restore points en etat Failover"
$missingVMs = @()
foreach ($vm in $VMStartOrder) {
$rp = Get-FailoverRestorePoint -VmName $vm.Name
if (-not $rp) {
Write-Log "ATTENTION : Aucun restore point Failover pour '$($vm.Name)'" "WARN"
$missingVMs += $vm.Name
} else {
Write-Log "OK : '$($vm.Name)' → restore point du $($rp.CreationTime)" "OK"
}
}
if ($missingVMs.Count -gt 0) {
Write-Log "$($missingVMs.Count) VM(s) sans restore point Failover : $($missingVMs -join ', ')" "WARN"
Write-Log "Ces VMs seront ignorees pour le failback" "WARN"
}
# --- DRY RUN -----------------------------------------------------------------
if ($WhatIf) {
Write-Log "------------------------------------------------------------"
Write-Log "DRY RUN - Simulation de la procedure par paires :" "WARN"
Write-Log "Continuite garantie : 1 DC par domaine + 1 DNS toujours up" "WARN"
Write-Log "------------------------------------------------------------" "WARN"
foreach ($vm in $VMStartOrder) {
$rp = Get-FailoverRestorePoint -VmName $vm.Name
if ($rp) {
Write-Log " → Stop-VBRReplicaFailover '$($vm.Name)' — eteint replica HYPERV2" "WARN"
Write-Log " → Failback '$($vm.Name)' (bloquant — resync vers HYPERV1)" "WARN"
Write-Log " → Commit failback '$($vm.Name)' (index 1)" "WARN"
Write-Log " → Attente 15s liberation VHDX" "WARN"
} else {
Write-Log " → '$($vm.Name)' pas en Failover — demarrage direct si present" "WARN"
}
Write-Log " → Demarrage '$($vm.Name)' sur $ProdHost — delai $($vm.Delay)s" "WARN"
Write-Log " ---" "WARN"
}
Write-Log "------------------------------------------------------------"
Write-Log "DRY RUN TERMINE - Aucune action effectuee" "WARN"
Write-Log "Relancer sans -WhatIf pour executer" "WARN"
exit 0
}
# --- TRAITEMENT VM PAR VM ----------------------------------------------------
Write-Log "------------------------------------------------------------"
Write-Log "DEBUT DU FAILBACK VM PAR VM (ordre par paires)"
Write-Log "Continuite garantie : 1 DC par domaine + 1 DNS toujours up" "OK"
foreach ($vm in $VMStartOrder) {
$vmName = $vm.Name
$delay = $vm.Delay
Write-Log "============ $vmName ============"
$rp = Get-FailoverRestorePoint -VmName $vmName
if ($rp) {
# ETAPE A - Undo individuel via Stop-VBRReplicaFailover
# Eteint proprement le replica sur HYPERV2 sans toucher aux autres
# Evite que le Failover Plan redemarre le replica apres le commit
Write-Log "[$vmName] Undo individuel (Stop-VBRReplicaFailover)..."
try {
Stop-VBRReplicaFailover -RestorePoint $rp -ErrorAction Stop | Out-Null
Write-Log "[$vmName] Replica eteint sur HYPERV2" "OK"
} catch {
Write-Log "[$vmName] Erreur Stop-VBRReplicaFailover : $_" "ERROR"
}
# ETAPE B - Failback bloquant vers HYPERV1
Write-Log "[$vmName] Lancement failback (bloquant — resync vers HYPERV1)..."
$rpFresh = Get-VBRRestorePoint |
Where-Object { $_.IsReplica() -and $_.VmName -eq $vmName } |
Sort-Object CreationTime -Descending |
Select-Object -First 1
if ($rpFresh) {
try {
Start-VBRHvReplicaFailback `
-RestorePoint $rpFresh `
-QuickRollback `
-PowerOn:$false `
-ErrorAction Stop | Out-Null
Write-Log "[$vmName] Failback termine" "OK"
} catch {
Write-Log "[$vmName] Erreur failback : $_" "ERROR"
}
} else {
Write-Log "[$vmName] Aucun restore point disponible pour le failback" "ERROR"
}
# ETAPE C - Commit sur index 1
Write-Log "[$vmName] Commit failback (index 1)..."
$rpCommit = Get-CommitRestorePoint -VmName $vmName
if ($rpCommit) {
try {
Stop-VBRHvReplicaFailback -RestorePoint $rpCommit -ErrorAction Stop | Out-Null
Write-Log "[$vmName] Commit OK (RP du $($rpCommit.CreationTime))" "OK"
} catch {
Write-Log "[$vmName] Erreur commit : $_" "WARN"
}
} else {
Write-Log "[$vmName] Aucun RP index 1 trouve" "WARN"
}
# ETAPE D - Attente liberation VHDX
Write-Log "[$vmName] Attente 15s liberation VHDX..."
Start-Sleep -Seconds 15
} else {
Write-Log "[$vmName] Pas de restore point Failover — VM ignoree pour le failback" "WARN"
}
# ETAPE E - Demarrer la VM sur HYPERV1
$vmState = Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($name)
$v = Get-VM -Name $name -ErrorAction SilentlyContinue
if ($v) { $v.State.ToString() } else { "NotFound" }
} -ArgumentList $vmName
if ($vmState -eq "NotFound") {
Write-Log "[$vmName] VM introuvable sur $ProdHost" "WARN"
} elseif ($vmState -eq "Running") {
Write-Log "[$vmName] VM deja Running sur $ProdHost" "OK"
} else {
try {
Invoke-Command -ComputerName $ProdHost -ScriptBlock {
param($name)
Start-VM -Name $name -ErrorAction Stop
} -ArgumentList $vmName
Write-Log "[$vmName] VM demarree sur $ProdHost" "OK"
} catch {
Write-Log "[$vmName] Erreur demarrage sur $ProdHost : $_" "ERROR"
}
}
# ETAPE F - Delai avant VM suivante
Write-Log "[$vmName] Attente $delay sec avant la VM suivante..."
Start-Sleep -Seconds $delay
}
# --- FIN ---------------------------------------------------------------------
Write-Log "============================================================"
Write-Log "FAILBACK TERMINE — PRODUCTION RESTAUREE SUR $ProdHost"
Write-Log "Verifiez les services (AD, DNS, DHCP, auth...)"
Write-Log "N'oubliez pas de reactiver l'autostart des VMs sur $ProdHost :"
Write-Log " Get-VM | Set-VM -AutomaticStartAction StartIfRunning"
Write-Log "Log complet : $LogFile"
Write-Log "============================================================"