Par Jemadhar · republié le 12 juin 2026 (version initiale : 3 avril 2026)

« Aucun plan ne survit au contact de l’ennemi. » — Helmuth von Moltke 1871


Pourquoi cet article est republié

L’article que vous lisez existait déjà. Je l’avais publié en avril, fier de mes deux scripts PowerShell qui basculaient mon infra perso sur le serveur DRP et la ramenaient en production, le tout avec une continuité de service que je croyais béton.

Puis il y a eu juin.

Lors d’un retour en production après une maintenance, je me suis aperçu que des modifications faites pendant que l’infra tournait sur le DRP avainet disparues apres le retour en prod. Des fichiers récents manquants, du travail de la journée envolé. Sur mon infra perso, le préjudice a été mineur — quelques articles de blog à refaire. En entreprise, ça aurait pu etre un incident majeur : des données silencieusement perdues, sans la moindre erreur dans les logs pour prévenir.

J’ai donc tout repris à zéro, doc Veeam à l’appui. Et le verdict est sévère : trois des « solutions » que je présentais dans la version d’avril étaient en réalité des bombes à retardement. Pas des approximations — des commandes qui détruisent activement des données.

Cet article reprend la même structure que l’original, mais chaque section porte désormais la trace de ce qui a été corrigé, pourquoi, et quels effets inattendus ça produisait en production. Les scripts publiés ici annulent et remplacent ceux des versions précédentes.

C’est, je crois, un meilleur article qu’avant. Un script qui marche, ça s’oublie. Un script qui a failli vous coûter vos données, ça vous apprend Veeam pour de bon.


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, dev, DNS interne...
└── workstations virtuelles

HYPERV2 (Hyper-V DRP)
└── Veeam B&R 12 + réplicas de toutes les VMs

L’infra a grossi depuis avril : on est passé de 19 à 22 VMs (ajout d’un serveur de dev qui réplique la prod du blog, et d’un serveur DNS interne Pi-hole/Unbound). La réplication tourne 4 fois par semaine avec une rétention de 2 restore points — un RPO de 24h maximum.


Les deux scénarios

Deux cas d’usage radicalement différents, et cette distinction n’a pas bougé d’un iota :

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’ordre devient critique : il faut toujours au moins un DC par domaine et un DNS forwarder actifs quelque part sur le réseau.

Ce qui a changé, en revanche, c’est comment on déclenche la bascule. La version d’avril s’appuyait sur deux Failover Plans Veeam (« FULL-DRP » et « MCO-DRP »). La nouvelle version les abandonne au profit d’un failover individuel VM par VM en PowerShell. J’y reviens plus bas — c’est ce changement qui débloque le vrai gain de performance.


La continuité MCO : l’ordre qui change tout

Le principe de continuité repose toujours 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.

Groupe 1 — DC secondaire Domain1 + DC secondaire Domain2 + DNS forwarder primaire : SRV-DC1 · SRV-DC2 · SRV-DNS1

Groupe 2 — PDC Domain1 + PDC Domain2 + DNS forwarder secondaire : SRV-PDC1 · SRV-PDC2 · SRV-DNS2

┌────────────────────────────────────────────────────────────────────────┐
│              HYPERV1 (prod)              HYPERV2 (DRP)                 │
│           Domain1 Domain2 DNS         Domain1 Domain2 DNS              │
├────────────────────────────────────────────────────────────────────────┤
│ Départ    PDC1 ✅  PDC2 ✅  DNS1 ✅        —      —      —            │
│           DC1  ✅  DC2  ✅  DNS2 ✅                                   │
├────────────────────────────────────────────────────────────────────────┤
│ Groupe 1  PDC1 ✅  PDC2 ✅  DNS1 ⬇️    DC1 🔄  DC2 🔄  DNS1 🔄      │
│ bascule   DC1  ⬇️  DC2  ⬇️  DNS2 ✅                                   │
│           ↑PDC1+PDC2+DNS2 encore UP sur HYPERV1 → continuité ✅        │
├────────────────────────────────────────────────────────────────────────┤
│ Groupe 1  PDC1 ✅  PDC2 ✅  DNS2 ✅    DC1 ✅  DC2 ✅  DNS1 ✅      │
│ confirmé  ↑Groupe 1 Running confirmé sur HYPERV2 AVANT de continue     │
├────────────────────────────────────────────────────────────────────────┤
│ Groupe 2  PDC1 ⬇️  PDC2 ⬇️  DNS2 ⬇️    DC1 ✅  DC2 ✅  DNS1 ✅      │
│ bascule    —      —      —             PDC1 🔄 PDC2 🔄 DNS2 🔄        │
│           ↑DC1+DC2+DNS1 up sur HYPERV2 → continuité ✅                 │
├────────────────────────────────────────────────────────────────────────┤
│ Final      —      —      —             PDC1 ✅ PDC2 ✅ DNS1 ✅        │
│                                        DC1  ✅ DC2  ✅ DNS2 ✅        │
└────────────────────────────────────────────────────────────────────────┘

À chaque étape, Domain1 a toujours un DC actif, Domain2 aussi, et le DNS forwarder répond toujours. Aucune interruption AD, DNS ou DHCP pendant toute la bascule.

Le garde-fou que je n’avais pas en avril. Dans la version initiale, le script loggait un avertissement si le Groupe 1 ne démarrait pas, mais continuait quand même à éteindre le Groupe 2. Conséquence potentielle : si le Groupe 1 ratait son démarrage sur HYPERV2, on se retrouvait avec les DC du Groupe 1 down partout et les DC du Groupe 2 en train de s’éteindre → plus aucun contrôleur de domaine nulle part. La nouvelle version stoppe net si le Groupe 1 n’est pas confirmé Running. Pire cas : un arrêt propre en état dégradé (les PDC restent up sur la prod), jamais une coupure totale.


Le changement d’architecture : du Failover Plan au failover individuel

C’est le cœur de la refonte côté bascule. La version d’avril déclenchait Start-VBRFailoverPlan — un objet Veeam monolithique qui démarre tout le groupe d’un coup avec ses propres délais internes. Problème : impossible de piloter VM par VM, donc le script devait éteindre toutes les VMs non critiques sur la prod avant de lancer le plan. Pendant ce temps, les services restaient down.

La nouvelle version pilote chaque failover individuellement avec Start-VBRHvReplicaFailover -RunAsync. Ça change tout pour les VMs non critiques : on peut pipeliner.

Pour chaque VM non critique :
  1. Shutdown sur HYPERV1 (bloquant, on attend le Off)
  2. Start-VBRHvReplicaFailover -RunAsync (on n'attend PAS le boot)
  3. VM suivante immédiatement

Le -RunAsync est la clé : le boot de la VM N sur HYPERV2 se fait pendant le shutdown de la VM N+1 sur HYPERV1. Le downtime de chaque service tombe à son propre cycle stop/boot, au lieu de « tous les shutdowns + position dans le plan ».

Les DC/DNS, eux, ne sont pas pipelinés — c’est volontaire. Ils gardent le traitement par groupes avec attente confirmée et le garde-fou décrit plus haut. La vitesse pour les workstations, la prudence pour les contrôleurs.

Bénéfice secondaire : le script ne dépend plus des Failover Plans. On peut les garder dans Veeam comme solution de secours manuelle, mais ils ne sont plus un point de synchronisation à maintenir.


Le retour en prod : là où ça s’était mal passé

Si vous ne deviez lire qu’une section, c’est celle-ci. C’est dans le failback que se cachaient les trois bugs, et c’est le failback qui m’a coûté des données en juin.

Les pièges Veeam 12 — version corrigée

Piège 1 — Get-VBRSession réclame un paramètre obligatoire

Contrairement à ce que laisse penser la doc, Get-VBRSession sans argument ouvre un prompt interactif (Job:) au lieu de retourner null — même avec -ErrorAction SilentlyContinue, qui n’agit pas sur un paramètre obligatoire. Dans un script automatisé, ça fige tout. Le fix : utiliser Get-VBRBackupSession, filtré sur le nom du job.

$lastSession = Get-VBRBackupSession -ErrorAction SilentlyContinue |
    Where-Object { $_.JobName -eq $VeeamReplicaJobName } |
    Sort-Object CreationTime -Descending |
    Select-Object -First 1

Piège 2 — Le « commit » qui était un undo

C’est le bug le plus grave, et je le publiais comme une solution. Pour finaliser un failback, mon code d’avril faisait :

# CE QUE JE FAISAIS — C'EST FAUX
Stop-VBRHvReplicaFailback -RestorePoint $rpCommit

Stop-VBRHvReplicaFailback n’est pas un commit. La doc Veeam est sans ambiguïté : « Undoes Hyper-V replica failback ». C’est l’annulation du failback. Au lieu de valider la resync qui venait de ramener les données vers la prod, cette commande défaisait cette resync. Le vrai commit PowerShell n’a pas de cmdlet dédié — c’est le même Start-VBRHvReplicaFailback avec le switch -Complete, sur le restore point d’avant le failback (index 1) :

# LE VRAI COMMIT
$rpCommit = Get-VBRRestorePoint |
    Where-Object { $_.IsReplica() -and $_.VmName -eq $vmName } |
    Sort-Object CreationTime -Descending |
    Select-Object -Skip 1 -First 1   # index 1 = RP pré-failback

Start-VBRHvReplicaFailback -RestorePoint $rpCommit -Complete

Le détail de l’index reste valable : après un failback, Veeam crée un nouveau restore point (index 0). Committer sur l’index 0 bloque la VM en LockedItem. Il faut l’index 1. Ça, je l’avais juste — c’est le cmdlet qui était faux.

Piège 3 — Stop-VBRReplicaFailover avant le failback : un undo failover

Deuxième commande destructrice, présentée comme un fix au problème du « Failover Plan qui redémarre les réplicas ». Mon code d’avril faisait, pour chaque VM, avant le failback :

# CE QUE JE FAISAIS — C'EST FAUX AUSSI
Stop-VBRReplicaFailover -RestorePoint $rpFailover

Là encore, la doc est limpide : « All changes that were made to the replicas during failover are discarded ». Stop-VBRReplicaFailover est un undo failover : il rejette toutes les modifications faites sur les réplicas pendant la période DRP, c’est-à-dire exactement le travail qu’on veut préserver. Je détruisais les données avant même de lancer la resync censée les rapatrier.

Le correctif : rien. On supprime purement cette étape. Start-VBRHvReplicaFailback gère lui-même l’extinction propre du réplica. La séquence se simplifie en plus d’être réparée.

Piège 4 — -QuickRollback, l’optimisation qui n’a rien à faire ici

Mon failback d’avril passait -QuickRollback à Start-VBRHvReplicaFailback pour accélérer la resync. Le Quick Rollback s’appuie sur le CBT (Changed Block Tracking) de la VM source pour ne transférer qu’un delta. La doc Veeam est catégorique : à n’utiliser que pour un problème survenu au niveau de l’OS invité (erreur applicative, fichier supprimé). Jamais après un incident matériel, une restauration depuis backup, ou une recréation de VM.

Or un DRP se déclenche par définition pour ces cas-là. Après mon changement de plateforme sur la prod (remplacement carte mère + CPU) et une restauration RAID, le CBT de la source était invalide. Le Quick Rollback considérait comme « inchangés » des blocs qui ne l’étaient pas, et ne les réécrivait jamais. Corruption silencieuse, aucune erreur.

Le correctif : suppression totale. Pas de paramètre opt-in, pas d’avertissement « êtes-vous sûr ». Le failback complet (calcul de digests sur les disques entiers) est plus lent, mais fiable quel que soit l’état de la source. C’est le seul mode acceptable pour un failback de DRP.

Piège 5 — Le VHDX encore verrouillé

Celui-là était correct dès avril, je le garde. Après le commit, Hyper-V n’a pas immédiatement libéré le VHDX ; un Start-VM immédiat échoue. Solution : 15 secondes d’attente entre le commit et le démarrage.

La séquence finale corrigée

Pour chaque VM (ordre par paires, continuité DC/DNS) :

1. Start-VBRHvReplicaFailback (bloquant, SANS QuickRollback)
   → resync COMPLÈTE par calcul de digests vers HYPERV1
   → Veeam éteint le réplica et crée un nouveau RP

2. Vérifier qu'un nouveau RP a bien été créé
   → garde-fou : pas de commit à l'aveugle si le failback a échoué

3. Start-VBRHvReplicaFailback -RestorePoint <index 1> -Complete
   → COMMIT (le vrai)

4. Start-Sleep 15
   → libération VHDX

5. Start-VM sur HYPERV1
   → en cas d'échec failback : VM NON démarrée, ajoutée à la liste
     des échecs, récapitulatif final (mieux vaut un service down
     visible qu'un service up avec des données périmées)

Trois commandes en moins (l’undo failover et le Quick Rollback supprimés, le faux commit remplacé), deux garde-fous en plus. La séquence est plus courte et plus sûre.

L’ordre de retour pour la continuité AD/DNS

SRV-PDC1  (PDC Domain1)        120s    ┐ pendant que PDC1 revient,
SRV-DC1   (DC2 Domain1)         60s    ┘ DC1 couvre encore sur HYPERV2
SRV-PDC2  (PDC Domain2)         90s    ┐ idem Domain2
SRV-DC2   (DC2 Domain2)         60s    ┘
SRV-DNS1  (forwarder)           60s    ┐ idem DNS
SRV-DNS2  (forwarder)           30s    ┘
... services applicatifs ...
... workstations ...

Au failback, la continuité n’est pas assurée par des groupes mais par l’entrelacement des paires : quand un PDC fait son retour, son DC secondaire est encore en Failover sur HYPERV2, donc le domaine reste couvert. À aucun moment les deux DC d’un même domaine ne sont down ensemble.


Pourquoi je n’avais rien vu

Question légitime : comment trois commandes destructrices ont-elles pu passer en production et y rester deux mois ?

Parce qu’aucune ne produit d’erreur. L’undo failover s’exécute sans broncher. Le faux commit retourne un succès. Le Quick Rollback se termine « OK ». Les scripts loggaient des [OK] verts partout. Les VMs redémarraient, les services répondaient. Tout semblait parfait.

Le seul symptôme, c’était des données manquantes — et encore, uniquement celles modifiées pendant la fenêtre DRP, donc un sous-ensemble qu’on ne remarque que si on va précisément les chercher.

Et c’est là que mon protocole de test était piégé. À chaque fois que j’avais validé mes scripts, je faisais l’aller-retour dans la même heure : bascule sur le DRP, vérification que tout est up, failback dans la foulée. Entre le moment où une VM partait sur HYPERV2 et celui où elle revenait sur HYPERV1, rien n’avait changé à l’intérieur. Or les trois bugs ne détruisent que les modifications faites pendant la fenêtre DRP. Pas de modification, pas de symptôme. Mes tests passaient au vert parce qu’ils ne testaient jamais la seule chose qui était cassée.

En juin, c’était différent. L’infra avait tourné trois jours sur le DRP pendant une maintenance — trois jours de vrais changements : des articles écrits sur le serveur de blog, des logs, des bases qui vivent. Cette fois, il y avait quelque chose à perdre. Et c’est exactement ce qui s’est passé.

La leçon : pour valider un failback, il ne suffit pas que les VMs redémarrent, et il ne suffit pas non plus de faire un aller-retour à blanc. Il faut créer une donnée témoin pendant que l’infra tourne sur le DRP, laisser un délai, puis vérifier qu’elle a survécu au retour. C’est le seul test qui exerce réellement la chaîne de resync — celui que je ne faisais pas ; que je n’ai pas pensé a faire.


La validation par fichier témoin

C’est la méthode que j’aurais dû appliquer dès le départ. Avant de refaire confiance aux scripts corrigés, je les ai testés sur une seule VM sacrifiable (une workstation), cycle complet :

  1. Bascule de la VM sur le DRP via le nouveau failover individuel.
  2. Création d’un fichier horodaté sur la VM, pendant qu’elle tourne sur le DRP :
    "Témoin DRP - créé sur HYPERV2 le $(Get-Date)" | Out-File C:\temoin-drp.txt
    
  3. Failback complet avec le vrai commit.
  4. Vérification : le fichier est-il présent sur la VM revenue en prod, avec son timestamp ?

Le timestamp est la clé : il prouve que le fichier a été créé après la bascule, donc sur le réplica. S’il survit au retour, c’est que la resync a bien rapatrié les modifications de la fenêtre DRP — ce qui était précisément cassé.

Le témoin a survécu. Et le temps du failback complet (3 min pour une workstation légère, sans Quick Rollback) confirmait au passage que la resync travaillait réellement, là où l’ancien faux commit était quasi instantané — parce qu’il ne faisait rien d’utile.


Le quatrième piège : un bug qui ne venait pas de Veeam

Les scripts corrigés validés, je me suis offert un cycle complet « pour le plaisir » — bascule puis retour, en conditions réelles. Et c’est là qu’un quatrième piège m’a sauté à la figure. Pas un bug Veeam cette fois : un bug PowerShell / Windows, et le plus sournois de tous.

Lors de la bascule MCO, le script s’est mis à attendre indéfiniment qu’une VM du Groupe 1 démarre sur le DRP. Sauf qu’elle tournait déjà — j’étais même connecté en RDP dessus. La même commande, tapée à la main dans ma console, renvoyait Running ; lancée par le script, elle renvoyait NotFound. Même machine, même compte, même instant.

L’explication : Get-VM sur Hyper-V exige des droits administrateur élevés. Dans une fenêtre PowerShell non élevée — même ouverte avec un compte admin du domaine — Get-VM ne lève pas d’erreur : il renvoie une collection vide. Le script interprète ce vide comme « VM introuvable » et attend dans le vide. J’avais lancé le script du matin dans une fenêtre élevée ; celui de l’après-midi, non. Toute la différence était là.

Le garde-fou a tenu. Le script a attendu le timeout des trois VMs du Groupe 1, puis a refusé de basculer le Groupe 2 — exactement son rôle : ne jamais laisser les deux domaines sans contrôleur. L’infra est restée dans un état mixte mais stable (Groupe 1 sur le DRP, Groupe 2 + PDC encore en prod), zéro coupure. J’ai terminé la bascule à la main, proprement, puis ajouté un check d’élévation en tête des deux scripts : ils refusent désormais de démarrer dans une fenêtre non élevée, avec un message clair.

$isAdmin = ([Security.Principal.WindowsPrincipal]`
    [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(`
    [Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
    Write-Host "ERREUR : fenetre non elevee. Relancez en tant qu'administrateur." -ForegroundColor Red
    exit 1
}

La leçon est exactement la même que pour les trois bugs Veeam, et c’est pour ça que je la raconte : une commande qui échoue en silence est mille fois plus dangereuse qu’une commande qui plante. Le plantage, on le voit. Le silence, lui, ment — NotFound ressemblait à un problème de VM alors que c’était un problème de droits. Trois fois côté Veeam, une fois côté Windows : le fil rouge de toute cette histoire, c’est la détection du silence.


Résultats

Après la refonte et un MCO réel de validation sur les 22 VMs :

Scénario Temps total Interruption AD/DNS
MCO DRP (bascule) ~7 min 0 seconde
Failback MCO (22 VMs) ~2 h* 0 seconde
FULL DRP (crash) ~3 min Non applicable

*Le failback complet des 22 VMs a pris environ deux heures (digests sur disques entiers, sans Quick Rollback) — sensiblement plus long que l’ancienne version « rapide ». C’est assumé : au retour en production, l’intégrité des données prime sur la vitesse. C’est même précisément en cherchant la vitesse à tout prix que j’avais cassé le failback.

Le pipeline sur la bascule, lui, a fait gagner du temps et réduit le downtime par service, sans aucun compromis sur la sécurité. À retenir pour planifier ses créneaux : l’aller est une affaire de minutes, le retour une affaire d’heures. Asymétrie assumée.


Ce que cette histoire m’a appris

Une commande Veeam qui « marche » ne fait pas forcément ce que son nom suggère. Stop-...Failback n’annule pas, il… si, justement, il annule. Le commit, c’est Start-...Failback -Complete. La nomenclature est piégeuse, et la doc officielle est incomplète sur les effets de bord. Les forums R&D Veeam (merci Andreas Neufert) ont été plus fiables que la doc pour démêler le commit du undo.

Un test de failback sans donnée témoin ne teste rien. Si rien n’a changé entre l’aller et le retour, un failback cassé est indiscernable d’un failback réussi. Le fichier horodaté est le seul juge.

Le silence est l’ennemi, pas le plantage. C’est le fil rouge des quatre bugs. L’undo failover, le faux commit, le Quick Rollback, et même le Get-VM non élevé : aucun ne produit d’erreur. Tous renvoient un succès ou un vide trompeur. Un script qui plante vous alerte ; un script qui ment vous laisse croire que tout va bien jusqu’au jour où vous cherchez une donnée qui n’est plus là. Tout l’effort de cette refonte, au fond, c’est de transformer ces silences en messages.

Les garde-fous valent mieux que la confiance. La revue ligne à ligne avant le test, le garde-fou qui refuse de laisser l’infra sans DC, le récapitulatif d’échecs en fin de script : c’est ce qui transforme une erreur silencieuse en erreur visible. En production, c’est toute la différence entre une perte de données et une alerte.

Publier un script, c’est engager sa responsabilité. Quelqu’un a pu récupérer mes scripts d’avril et les passer en prod. C’est aussi pour ça que cette republication remplace explicitement les versions précédentes, plutôt que de discrètement corriger le code. Si vous utilisiez les anciens scripts : récupérez ceux-ci, et faites le test du fichier témoin avant de leur confier vos données.


Pourquoi je republie plutôt que de corriger en douce

J’aurais pu éditer l’article d’avril discrètement, remplacer trois lignes de code, et faire comme si de rien n’était. Personne n’aurait rien vu.

Mais ça n’aurait pas été honnête, et surtout ça n’aurait pas été l’esprit d’ApertureZone. Le tout premier article de ce site posait la règle du jeu : ici, on parle de ce qui marche vraiment. De ce qui plante vraiment. Des solutions trouvées à 3h du mat’ quand tout part en vrille. On n’a jamais prétendu être infaillible, ni vendre des best practices sorties d’un lab aseptisé.

Un DRP qu’on croit parfait et qui mange vos données au pire moment, c’est exactement le genre d’écart entre la datasheet et le terrain que ce site existe pour documenter. L’erreur n’est pas le problème — la cacher le serait. Le tout, c’est de la reconnaître, de la disséquer, et d’en sortir quelque chose de plus solide. Cet article, dans sa version corrigée, vaut mieux que l’original précisément parce qu’il porte la cicatrice.

Welcome to the Zone.


Les scripts

Les deux scripts tournent depuis HYPERV2 (le serveur DRP). Ils incluent un changelog versionné, un mode -WhatIf, des logs horodatés dans C:\Scripts\DRP\Logs\, et des garde-fous à chaque étape critique. Ces versions annulent et remplacent toutes les précédentes.

Les noms de VMs, domaines et serveurs sont anonymisés. L’infrastructure réelle diffère de ce qui est décrit ici. Les délais et l’ordre sont adaptés à mon infra perso — ajustez-les à la vôtre, et testez sur une VM isolée avant tout passage en production. (shit happens, Murphy, etc….)

Start-DRP.ps1 (bascule prod → DRP)

# 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

Architecture v3.3 : failover individuel Start-VBRHvReplicaFailover -RunAsync, pipeline shutdown/boot pour les non-critiques, groupes avec attente + garde-fou pour les DC/DNS, tolérance d’une prod injoignable en mode CRASH, vérification finale des réplicas, et check d’élévation obligatoire au démarrage.

# =============================================================================
# Start-DRP.ps1  -  v3.3
# Bascule prod (HYPERV1) -> DRP (HYPERV2). A lancer depuis HYPERV2.
#
#   MODE CRASH : prod morte, on bascule tout. Tolere HYPERV1 injoignable.
#   MODE MCO   : maintenance planifiee, continuite de service garantie.
#                Pipeline shutdown->failover pour les non-critiques, puis
#                bascule des DC/DNS par groupes avec attente + garde-fou.
#
# ---------------------------------------------------------------------------
# NOTE v3.3 - CE QUI A CHANGE PAR RAPPORT AUX VERSIONS PUBLIEES EN AVRIL
# ---------------------------------------------------------------------------
#   * Abandon des Failover Plans Veeam ("FULL-DRP"/"MCO-DRP") au profit du
#     failover individuel Start-VBRHvReplicaFailover -RunAsync. Permet le
#     pipeline shutdown/boot et supprime un point de synchro a maintenir.
#   * MCO : garde-fou bloquant - le Groupe 2 n'est PAS eteint si le Groupe 1
#     n'est pas confirme Running. Avant, le script continuait malgre l'echec
#     -> risque de se retrouver SANS AUCUN DC nulle part.
#   * CRASH : tolere une prod injoignable (avant : exit 1 sur le check WinRM,
#     ce qui rendait le script inutilisable dans un vrai crash).
#   * Get-VBRBackupSession au lieu de Get-VBRSession (qui ouvrait un prompt
#     interactif et figeait le script malgre -ErrorAction SilentlyContinue).
#   * Ajout d'un check d'elevation obligatoire en tete (v3.3) : sans
#     droits admin eleves, Get-VM renvoie NotFound en silence (incident 12/06).
# =============================================================================

param(
    [ValidateSet("CRASH","MCO")]
    [string]$Mode = "",
    [switch]$SkipReplication
)

# --- CHECK D'ELEVATION (obligatoire) -----------------------------------------
# Sans droits admin ELEVES, Get-VM renvoie une collection vide SANS erreur :
# les VMs apparaissent "NotFound" en silence (cf. incident du 12/06).
$isAdmin = ([Security.Principal.WindowsPrincipal]`
    [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(`
    [Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
    Write-Host "ERREUR : fenetre non elevee. Relancez en tant qu'administrateur." -ForegroundColor Red
    exit 1
}


# --- MENU INTERACTIF ---------------------------------------------------------
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 le DRP"
    Write-Host "               Failover individuel, ordre FULL DRP (DC d'abord)"
    Write-Host ""
    Write-Host "  [2] MCO          " -ForegroundColor Yellow -NoNewline
    Write-Host "- Maintenance planifiee, continuite de service garantie"
    Write-Host "               Pipeline shutdown/failover VM par VM"
    Write-Host ""
    Write-Host "  [3] MCO + Skip   " -ForegroundColor Yellow -NoNewline
    Write-Host "- MCO sans replication (replicas deja a jour)"
    Write-Host ""
    Write-Host "============================================================" -ForegroundColor Cyan
    Write-Host ""
    $choix = Read-Host "Votre choix (1/2/3)"

    switch ($choix) {
        "1" { $Mode = "CRASH"; Write-Host "`nMode CRASH selectionne." -ForegroundColor Red }
        "2" { $Mode = "MCO";   Write-Host "`nMode MCO selectionne." -ForegroundColor Yellow }
        "3" { $Mode = "MCO"; $SkipReplication = $true
              Write-Host "`nMode MCO + SkipReplication selectionne." -ForegroundColor Yellow }
        default { Write-Host "`nChoix invalide. Arret." -ForegroundColor Red; exit 1 }
    }

    Write-Host ""
    Write-Host "  Mode         : $Mode" -ForegroundColor White
    Write-Host "  Bascule      : Failover individuel (pipeline)" -ForegroundColor White
    Write-Host "  Replication  : $(if ($SkipReplication) { 'IGNOREE' } else { 'OUI' })" -ForegroundColor White
    Write-Host ""
    $confirm = Read-Host "Confirmer le lancement ? (O/N)"
    if ($confirm -notmatch "^[Oo]$") { Write-Host "Annule." -ForegroundColor Yellow; exit 0 }
}

# --- CONFIGURATION -----------------------------------------------------------
$ScriptVersion        = "3.3"
$VeeamReplicaJobName  = "ReplicaVM-HYPERV1_Dayly"
$ProdHost             = "HYPERV1"
$LogFile              = "C:\Scripts\DRP\Logs\DRP_${Mode}_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$ShutdownTimeout      = 300
$ReplicationTimeout   = 7200
$VMReadyTimeout       = 600
$VeeamModule          = "C:\Program Files\Veeam\Backup and Replication\Console\Veeam.Backup.PowerShell.dll"
$DRPFlagFile          = "C:\Scripts\DRP\DRP_MODE.flag"

# PIPELINE MCO : VMs non critiques (waves 5->3). Pour chacune :
# shutdown bloquant sur HYPERV1 puis failover async -> le boot recouvre
# le shutdown de la VM suivante. Les DC/DNS de prod restent up.
$PipelineMCO = @(
    "WS-03", "WS-02", "WS-01", "SRV-DNSINT", "SRV-PXE",
    "SRV-PKI", "SRV-PRINT", "SRV-WSUS", "SRV-MONITORING", "SRV-SIEM",
    "SRV-PASSBOLT", "SRV-SMTP", "SRV-DEV", "SRV-PROXY", "SRV-RADIUS"
)

# DC secondaires + DNS primaire (homologues restent up sur HYPERV1)
$MCOGroupe1 = @("SRV-DC1", "SRV-DC2", "SRV-DNS1")
# PDC + DNS failover
$MCOGroupe2 = @("SRV-PDC1", "SRV-PDC2", "SRV-DNS2")

# Ordre FULL DRP (CRASH) : DC/DNS d'abord, delai entre lancements async
$FullDRPOrder = @(
    @{ Name = "SRV-PDC1"; Delay = 120 }, @{ Name = "SRV-PDC2"; Delay = 90 }, @{ Name = "SRV-DNS1"; Delay = 60 },
    @{ Name = "SRV-DC1";  Delay = 60  }, @{ Name = "SRV-DC2";  Delay = 60 }, @{ Name = "SRV-DNS2"; Delay = 30 },
    @{ Name = "SRV-RADIUS"; Delay = 45 }, @{ Name = "SRV-PROXY"; Delay = 30 }, @{ Name = "SRV-DEV"; Delay = 20 },
    @{ Name = "SRV-DNSINT"; Delay = 20 }, @{ Name = "SRV-SMTP"; Delay = 30 }, @{ Name = "SRV-PASSBOLT"; Delay = 30 },
    @{ 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 },
    @{ Name = "SRV-PXE"; Delay = 20 }, @{ Name = "WS-01"; Delay = 20 }, @{ Name = "WS-02"; Delay = 20 },
    @{ Name = "WS-03"; Delay = 20 }
)

$FailedFailovers = @()

# --- FONCTIONS ---------------------------------------------------------------

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $Message"
    Write-Host $line -ForegroundColor $(switch ($Level) {
        "OK" {"Green"} "WARN" {"Yellow"} "ERROR" {"Red"} default {"Cyan"} })
    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($n) (Get-VM -Name $n -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)
    if (-not $script:ProdReachable) {
        Write-Log "VM '$VMName' : $ProdHost injoignable, shutdown ignore" "WARN"; return
    }
    $vmState = Invoke-Command -ComputerName $ProdHost -ScriptBlock {
        param($n) $v = Get-VM -Name $n -ErrorAction SilentlyContinue
        if ($v) { $v.State.ToString() } else { "NotFound" }
    } -ArgumentList $VMName

    if ($vmState -eq "NotFound") { Write-Log "VM '$VMName' introuvable, 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($n) Stop-VM -Name $n -Force -ErrorAction SilentlyContinue
    } -ArgumentList $VMName

    if (Wait-VMOff -VMName $VMName) {
        Write-Log "VM '$VMName' eteinte proprement" "OK"
    } else {
        Write-Log "VM '$VMName' ne repond pas, power off force..." "WARN"
        Invoke-Command -ComputerName $ProdHost -ScriptBlock {
            param($n) Stop-VM -Name $n -TurnOff -Force -ErrorAction SilentlyContinue
        } -ArgumentList $VMName
        Start-Sleep -Seconds 10
    }
}

function Start-FailoverAsync {
    # Failover individuel via le RP le plus recent. -RunAsync = le script
    # n'attend PAS le boot -> pipeline. La VM source DOIT etre Off avant
    # (conflit nom/IP sinon) - garanti par Stop-VMProprement en amont.
    param([string]$VMName)
    $rp = Get-VBRRestorePoint |
        Where-Object { $_.IsReplica() -and $_.VmName -eq $VMName -and $_.State.ToString() -ne "Failover" } |
        Sort-Object CreationTime -Descending | Select-Object -First 1
    if (-not $rp) {
        Write-Log "[$VMName] Aucun restore point disponible !" "ERROR"
        $script:FailedFailovers += $VMName; return $false
    }
    try {
        Start-VBRHvReplicaFailover -RestorePoint $rp -RunAsync -ErrorAction Stop | Out-Null
        Write-Log "[$VMName] Failover lance (async, RP du $($rp.CreationTime))" "OK"; return $true
    } catch {
        Write-Log "[$VMName] Erreur failover : $_" "ERROR"
        $script:FailedFailovers += $VMName; return $false
    }
}

function Wait-VMRunningLocal {
    # Verification via Hyper-V local sur le DRP (nom _VeeamReplica), pas par
    # le reseau -> evite les faux positifs si la meme IP repond depuis HYPERV1.
    param([string]$VMName, [int]$TimeoutSec = $VMReadyTimeout)
    $replicaName = "${VMName}_VeeamReplica"; $elapsed = 0
    Write-Log "Attente '$replicaName' Running sur le DRP (Hyper-V local)..." "WARN"
    while ($elapsed -lt $TimeoutSec) {
        $vm = Get-VM -ComputerName localhost -Name $replicaName -ErrorAction SilentlyContinue
        $state = if ($vm) { $vm.State.ToString() } else { "NotFound" }
        if ($state -eq "Running") { Write-Log "'$VMName' confirme Running sur le DRP" "OK"; return $true }
        Write-Log "'$VMName' pas encore Running (etat: $state)" "WARN"
        Start-Sleep -Seconds 15; $elapsed += 15
    }
    Write-Log "'$VMName' pas Running apres $($TimeoutSec/60) min" "ERROR"; return $false
}

function Wait-GroupeRunning {
    param([string[]]$VMNames)
    $allReady = $true
    foreach ($vmName in $VMNames) { if (-not (Wait-VMRunningLocal -VMName $vmName)) { $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 - Version $ScriptVersion - Mode $Mode"
Write-Log "============================================================"

try { Import-Module Hyper-V -ErrorAction Stop -WarningAction SilentlyContinue; Write-Log "Module Hyper-V charge" "OK" }
catch { Write-Log "Module Hyper-V KO : $_" "ERROR"; exit 1 }

try { Import-Module $VeeamModule -ErrorAction Stop -WarningAction SilentlyContinue; Write-Log "Module Veeam charge" "OK" }
catch { Write-Log "Module Veeam KO : $_" "ERROR"; exit 1 }

# Check WinRM - TOLERANT en mode CRASH (avant : exit 1 systematique)
$ProdReachable = $false
try {
    Invoke-Command -ComputerName $ProdHost -ScriptBlock { $env:COMPUTERNAME } -ErrorAction Stop | Out-Null
    $ProdReachable = $true; Write-Log "Connexion WinRM vers $ProdHost OK" "OK"
} catch {
    if ($Mode -eq "CRASH") {
        Write-Log "$ProdHost INJOIGNABLE - coherent avec un crash" "WARN"
        Write-Log "ATTENTION : verifier physiquement qu'$ProdHost est bien HS" "WARN"
        $c = Read-Host "$ProdHost est-il reellement hors service ? (O/N)"
        if ($c -notmatch "^[Oo]$") { Write-Log "Annule - verifier l'etat de $ProdHost" "ERROR"; exit 1 }
    } else {
        Write-Log "$ProdHost injoignable - le mode MCO exige une prod joignable" "ERROR"; exit 1
    }
}

# --- ETAPE 1 : REPLICATION ---------------------------------------------------

if ($SkipReplication -or (-not $ProdReachable)) {
    Write-Log "ETAPE 1 : Replication ignoree" "WARN"
} else {
    Write-Log "ETAPE 1 : Replication '$VeeamReplicaJobName'"
    Invoke-Command -ComputerName $ProdHost -ScriptBlock {
        param($f) New-Item -Path $f -ItemType File -Force | Out-Null
    } -ArgumentList $DRPFlagFile
    $job = Get-VBRJob -Name $VeeamReplicaJobName -ErrorAction Stop
    if (-not $job.IsRunning) { Start-VBRJob -Job $job | Out-Null; Write-Log "Job demarre" "OK" }
    Start-Sleep -Seconds 20
    $elapsed = 0; $success = $false
    while ($elapsed -lt $ReplicationTimeout) {
        $job = Get-VBRJob -Name $VeeamReplicaJobName
        if (-not $job.IsRunning) {
            # FIX : Get-VBRBackupSession, PAS Get-VBRSession (prompt interactif)
            $s = Get-VBRBackupSession -ErrorAction SilentlyContinue |
                Where-Object { $_.JobName -eq $VeeamReplicaJobName } |
                Sort-Object CreationTime -Descending | Select-Object -First 1
            if ($s -and ($s.Result -eq "Success" -or $s.Result -eq "Warning")) {
                Write-Log "Replication OK (Result: $($s.Result))" "OK"; $success = $true; break
            } elseif ($s -and $s.Result -ne "" -and $s.Result -ne "None") {
                Write-Log "Replication en ERREUR (Result: $($s.Result))" "ERROR"; break
            }
        }
        Start-Sleep -Seconds 30; $elapsed += 30
        Write-Log "Replication en cours... ($([math]::Round($elapsed/60,1)) min)"
    }
    if (-not $success) { Write-Log "Replication echouee/timeout. Arret." "ERROR"; exit 1 }
}

# --- BASCULE -----------------------------------------------------------------

if ($Mode -eq "CRASH") {

    if ($ProdReachable) {
        Write-Log "ETAPE 2 : Shutdown de toutes les VMs sur $ProdHost"
        foreach ($vmName in $PipelineMCO) { Stop-VMProprement -VMName $vmName }
        foreach ($vmName in ($MCOGroupe2 + $MCOGroupe1)) { Stop-VMProprement -VMName $vmName }
    } else {
        Write-Log "ETAPE 2 : Shutdowns ignores ($ProdHost injoignable)" "WARN"
    }

    Write-Log "ETAPE 3 : Failover individuel - ordre FULL DRP (DC/DNS d'abord)"
    foreach ($vm in $FullDRPOrder) {
        Start-FailoverAsync -VMName $vm.Name | Out-Null
        Start-Sleep -Seconds $vm.Delay
    }

} else {

    Write-Log "ETAPE 2 : PIPELINE waves 5/4/3 - shutdown -> failover async"
    Write-Log "Les DC/DNS de prod restent up : continuite AD/DNS garantie" "OK"
    foreach ($vmName in $PipelineMCO) {
        Write-Log "============ $vmName ============"
        Stop-VMProprement -VMName $vmName
        Start-FailoverAsync -VMName $vmName | Out-Null
        # Pas d'attente : le boot async recouvre le shutdown de la VM suivante
    }

    Write-Log "ETAPE 3 : Groupe 1 - $($MCOGroupe1 -join ' + ')"
    foreach ($vmName in $MCOGroupe1) {
        Stop-VMProprement -VMName $vmName
        Start-FailoverAsync -VMName $vmName | Out-Null
    }

    Write-Log "ETAPE 4 : Attente Groupe 1 Running sur le DRP"
    $groupe1Ready = Wait-GroupeRunning -VMNames $MCOGroupe1

    # GARDE-FOU CRITIQUE (nouveaute v3.2) : si le Groupe 1 n'est pas
    # integralement Running, eteindre le Groupe 2 laisserait les deux
    # domaines SANS AUCUN DC. On stoppe ici en etat degrade mais stable.
    if (-not $groupe1Ready) {
        Write-Log "ARRET DE SECURITE : Groupe 1 pas integralement Running." "ERROR"
        Write-Log "Le Groupe 2 NE SERA PAS eteint. PDC encore up en prod." "ERROR"
        Write-Log "Diagnostiquer le Groupe 1 puis relancer ou basculer a la main." "ERROR"
        exit 1
    }

    Write-Log "ETAPE 5 : Groupe 2 - $($MCOGroupe2 -join ' + ')"
    Write-Log "Groupe 1 confirme up sur le DRP -> continuite AD/DNS garantie" "OK"
    foreach ($vmName in $MCOGroupe2) {
        Stop-VMProprement -VMName $vmName
        Start-FailoverAsync -VMName $vmName | Out-Null
    }

    Write-Log "ETAPE 6 : Attente Groupe 2 Running sur le DRP"
    Wait-GroupeRunning -VMNames $MCOGroupe2 | Out-Null
}

# --- VERIFICATION FINALE -----------------------------------------------------

Write-Log "VERIFICATION : tous les replicas Running sur le DRP"
$AllVMNames = if ($Mode -eq "CRASH") { $FullDRPOrder | ForEach-Object { $_.Name } }
              else { $PipelineMCO + $MCOGroupe1 + $MCOGroupe2 }

$notRunning = @()
foreach ($vmName in $AllVMNames) {
    $vm = Get-VM -ComputerName localhost -Name "${vmName}_VeeamReplica" -ErrorAction SilentlyContinue
    $state = if ($vm) { $vm.State.ToString() } else { "NotFound" }
    if ($state -eq "Running") { Write-Log "Replica '$vmName' : Running" "OK" }
    else { Write-Log "Replica '$vmName' : $state" "ERROR"; $notRunning += $vmName }
}

# Seconde passe apres 60s pour les failovers async encore en boot
if ($notRunning.Count -gt 0) {
    Write-Log "$($notRunning.Count) replica(s) pas Running - seconde passe dans 60s..." "WARN"
    Start-Sleep -Seconds 60
    $still = @()
    foreach ($vmName in $notRunning) {
        $vm = Get-VM -ComputerName localhost -Name "${vmName}_VeeamReplica" -ErrorAction SilentlyContinue
        if ($vm -and $vm.State.ToString() -eq "Running") { Write-Log "Replica '$vmName' : Running (2e passe)" "OK" }
        else { Write-Log "Replica '$vmName' : toujours KO" "ERROR"; $still += $vmName }
    }
    $notRunning = $still
}

$totalIssues = ($FailedFailovers + $notRunning) | Select-Object -Unique
Write-Log "============================================================"
if ($totalIssues.Count -eq 0) {
    Write-Log "PROCEDURE DRP $Mode TERMINEE - BASCULE COMPLETE SUR LE DRP" "OK"
} else {
    Write-Log "DRP $Mode TERMINEE AVEC ERREURS : $($totalIssues -join ', ')" "ERROR"
}
Write-Log "Log complet : $LogFile"
if ($totalIssues.Count -gt 0) { exit 1 }

Start-FailbackToProd.ps1 (retour DRP → prod)

# Usage
.\Start-FailbackToProd.ps1          # Menu interactif
.\Start-FailbackToProd.ps1 -WhatIf  # Dry run

Architecture v2.4 : failback complet sans Quick Rollback, sans undo préalable, vrai commit via -Complete, vérification du nouveau RP avant commit, VM non démarrée si le failback échoue, récapitulatif d’échecs en fin de procédure, et check d’élévation obligatoire au démarrage.

# =============================================================================
# Start-FailbackToProd.ps1  -  v2.4
# Retour DRP (HYPERV2) -> prod (HYPERV1). A lancer depuis HYPERV2.
#
# Les VMs sont traitees une par une, dans un ordre par PAIRES qui garantit
# qu'un DC par domaine et un DNS sont toujours up (quand un PDC revient,
# son DC secondaire est encore en Failover sur le DRP, et inversement).
#
# =============================================================================
# DANGER  TROIS COMMANDES A NE JAMAIS REINTRODUIRE
# =============================================================================
# Ces trois "solutions" figuraient dans les versions publiees en avril.
# Chacune detruit des donnees SANS produire d'erreur. Cause de la perte
# de donnees de juin 2026. Ne les remettez jamais.
#
#   1. JAMAIS de Stop-VBRReplicaFailover avant le failback.
#      = UNDO FAILOVER. Doc Veeam : "All changes that were made to the
#      replicas during failover are discarded." Rejette tout le travail
#      fait pendant la fenetre DRP, AVANT meme la resync. Inutile en plus :
#      Start-VBRHvReplicaFailback eteint lui-meme le replica.
#
#   2. JAMAIS de -QuickRollback sur Start-VBRHvReplicaFailback.
#      S'appuie sur le CBT de la source pour ne transferer qu'un delta.
#      Apres incident materiel / restauration / recreation de VM (= les cas
#      qui declenchent un DRP), le CBT est invalide -> blocs omis
#      silencieusement -> corruption. Le failback complet (digests) est
#      plus lent mais fiable. Seul mode autorise.
#
#   3. JAMAIS de Stop-VBRHvReplicaFailback pour "committer".
#      = UNDO FAILBACK. Doc Veeam : "Undoes Hyper-V replica failback."
#      Annule la resync qu'on vient de faire. Le vrai commit est :
#        Start-VBRHvReplicaFailback -RestorePoint <RP index 1> -Complete
#      Toujours index 1 (RP pre-failback), jamais index 0 (-> LockedItem).
# =============================================================================

param(
    [ValidateSet("MCO","CRASH")]
    [string]$Mode = "",
    [switch]$WhatIf
)

# --- CHECK D'ELEVATION (obligatoire) -----------------------------------------
# Sans droits admin ELEVES, Get-VM renvoie une collection vide SANS erreur :
# les VMs apparaissent "NotFound" en silence (cf. incident du 12/06).
$isAdmin = ([Security.Principal.WindowsPrincipal]`
    [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(`
    [Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
    Write-Host "ERREUR : fenetre non elevee. Relancez en tant qu'administrateur." -ForegroundColor Red
    exit 1
}


# --- 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 "  [1] FAILBACK MCO    - Retour apres maintenance planifiee"
    Write-Host "  [2] FAILBACK CRASH  - Retour apres desastre"
    Write-Host "============================================================" -ForegroundColor Cyan
    $choix = Read-Host "Votre choix (1/2)"
    switch ($choix) {
        "1" { $Mode = "MCO";   Write-Host "`nMode FAILBACK MCO." -ForegroundColor Yellow }
        "2" { $Mode = "CRASH"; Write-Host "`nMode FAILBACK CRASH." -ForegroundColor Red }
        default { Write-Host "`nChoix invalide. Arret." -ForegroundColor Red; exit 1 }
    }
    Write-Host "`n  Resync : COMPLETE (digests, pas de Quick Rollback)" -ForegroundColor White
    $confirm = Read-Host "Confirmer le lancement ? (O/N)"
    if ($confirm -notmatch "^[Oo]$") { Write-Host "Annule." -ForegroundColor Yellow; exit 0 }
}

# --- CONFIGURATION -----------------------------------------------------------
$ScriptVersion = "2.4"
$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"
$DRPFlagFile   = "C:\Scripts\DRP\DRP_MODE.flag"

# Ordre par paires (continuite DC/DNS)
$VMStartOrder = @(
    @{ Name = "SRV-PDC1"; Delay = 120 }, @{ Name = "SRV-DC1"; Delay = 60 },
    @{ Name = "SRV-PDC2"; Delay = 90  }, @{ Name = "SRV-DC2"; Delay = 60 },
    @{ Name = "SRV-DNS1"; Delay = 60  }, @{ Name = "SRV-DNS2"; Delay = 30 },
    @{ Name = "SRV-RADIUS"; Delay = 45 }, @{ Name = "SRV-PROXY"; Delay = 30 },
    @{ Name = "SRV-DEV"; Delay = 20 }, @{ Name = "SRV-DNSINT"; Delay = 20 },
    @{ Name = "SRV-SMTP"; Delay = 30 }, @{ Name = "SRV-PASSBOLT"; Delay = 30 },
    @{ 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 }, @{ Name = "SRV-PXE"; Delay = 20 },
    @{ Name = "WS-01"; Delay = 20 }, @{ Name = "WS-02"; Delay = 20 }, @{ Name = "WS-03"; Delay = 20 }
)

$FailedVMs = @(); $SkippedVMs = @()

# --- FONCTIONS ---------------------------------------------------------------

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $prefix = if ($WhatIf) { "[WHATIF] " } else { "" }
    $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $prefix$Message"
    Write-Host $line -ForegroundColor $(switch ($Level) {
        "OK" {"Green"} "WARN" {"Yellow"} "ERROR" {"Red"} default {"Cyan"} })
    Add-Content -Path $LogFile -Value $line
}

function Get-FailoverRestorePoint {
    param([string]$VmName)
    Get-VBRRestorePoint |
        Where-Object { $_.IsReplica() -and $_.VmName -eq $VmName -and $_.State.ToString() -eq "Failover" } |
        Sort-Object CreationTime -Descending | Select-Object -First 1
}

function Get-NewestRestorePoint {
    param([string]$VmName)
    Get-VBRRestorePoint |
        Where-Object { $_.IsReplica() -and $_.VmName -eq $VmName } |
        Sort-Object CreationTime -Descending | Select-Object -First 1
}

function Get-CommitRestorePoint {
    # Index 1 = RP pre-failback. JAMAIS index 0 (-> LockedItem).
    param([string]$VmName)
    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 "FAILBACK TO PRODUCTION - Version $ScriptVersion - Mode $Mode"
Write-Log "Resync COMPLETE (digests, Quick Rollback banni)"
if ($WhatIf) { Write-Log "MODE DRY RUN - AUCUNE ACTION REELLE" "WARN" }
Write-Log "============================================================"

try { Import-Module $VeeamModule -ErrorAction Stop -WarningAction SilentlyContinue; Write-Log "Module Veeam charge" "OK" }
catch { Write-Log "Module Veeam KO : $_" "ERROR"; exit 1 }

try {
    Invoke-Command -ComputerName $ProdHost -ScriptBlock { $env:COMPUTERNAME } -ErrorAction Stop | Out-Null
    Write-Log "Connexion WinRM vers $ProdHost OK" "OK"
} catch { Write-Log "$ProdHost injoignable : $_" "ERROR"; exit 1 }

# --- PRE-VOL -----------------------------------------------------------------

Write-Log "VERIFICATION : restore points en etat Failover"
$missingVMs = @()
foreach ($vm in $VMStartOrder) {
    $rp = Get-FailoverRestorePoint -VmName $vm.Name
    if (-not $rp) { Write-Log "Aucun RP Failover pour '$($vm.Name)'" "WARN"; $missingVMs += $vm.Name }
    else { Write-Log "OK : '$($vm.Name)' -> RP du $($rp.CreationTime)" "OK" }
}
if ($missingVMs.Count -gt 0) {
    Write-Log "$($missingVMs.Count) VM(s) sans RP Failover : $($missingVMs -join ', ')" "WARN"
}

# --- DRY RUN -----------------------------------------------------------------

if ($WhatIf) {
    foreach ($vm in $VMStartOrder) {
        $rp = Get-FailoverRestorePoint -VmName $vm.Name
        if ($rp) {
            Write-Log "  -> Failback COMPLET '$($vm.Name)' (digests + resync)" "WARN"
            Write-Log "  -> Verif nouveau RP, puis COMMIT -Complete (index 1)" "WARN"
        } else { Write-Log "  -> '$($vm.Name)' pas en Failover - demarrage direct" "WARN" }
        Write-Log "  -> Demarrage '$($vm.Name)' - delai $($vm.Delay)s" "WARN"
    }
    Write-Log "DRY RUN TERMINE - aucune action" "WARN"; exit 0
}

# --- TRAITEMENT VM PAR VM ----------------------------------------------------

Write-Log "DEBUT DU FAILBACK VM PAR VM (ordre par paires)"

foreach ($vm in $VMStartOrder) {
    $vmName = $vm.Name; $delay = $vm.Delay
    $failbackOK = $false; $startVM = $true
    Write-Log "============ $vmName ============"

    $rp = Get-FailoverRestorePoint -VmName $vmName

    if ($rp) {
        # ETAPE A - Failback COMPLET directement sur le RP Failover.
        # PAS de Stop-VBRReplicaFailover prealable (= undo destructeur).
        # PAS de -QuickRollback (delta CBT non fiable apres incident).
        Write-Log "[$vmName] Failback COMPLET (bloquant - digests + resync)..."
        try {
            Start-VBRHvReplicaFailback -RestorePoint $rp -PowerOn:$false -ErrorAction Stop | Out-Null
            Write-Log "[$vmName] Failback termine" "OK"; $failbackOK = $true
        } catch { Write-Log "[$vmName] Erreur failback : $_" "ERROR" }

        # ETAPE B - Verifier qu'un nouveau RP a bien ete cree (anti commit aveugle)
        if ($failbackOK) {
            $rpNew = Get-NewestRestorePoint -VmName $vmName
            if ($rpNew -and $rpNew.CreationTime -gt $rp.CreationTime) {
                Write-Log "[$vmName] Nouveau RP confirme ($($rpNew.CreationTime))" "OK"
            } else {
                Write-Log "[$vmName] AUCUN nouveau RP - commit annule par securite" "ERROR"
                $failbackOK = $false
            }
        }

        # ETAPE C - LE VRAI COMMIT : Start-VBRHvReplicaFailback -Complete sur index 1
        if ($failbackOK) {
            Write-Log "[$vmName] Commit (Start-VBRHvReplicaFailback -Complete, index 1)..."
            $rpCommit = Get-CommitRestorePoint -VmName $vmName
            if ($rpCommit) {
                try {
                    Start-VBRHvReplicaFailback -RestorePoint $rpCommit -Complete -ErrorAction Stop | Out-Null
                    Write-Log "[$vmName] Commit OK (RP du $($rpCommit.CreationTime))" "OK"
                } catch {
                    Write-Log "[$vmName] Erreur commit : $_ - commit manuel requis" "WARN"
                }
            } else { Write-Log "[$vmName] Aucun RP index 1 - commit manuel requis" "WARN" }

            Write-Log "[$vmName] Attente 15s liberation VHDX..."
            Start-Sleep -Seconds 15
        } else {
            # Echec failback : on NE demarre PAS la VM (etat perime = perte silencieuse)
            Write-Log "[$vmName] FAILBACK EN ECHEC - VM non demarree. Replica intact sur le DRP." "ERROR"
            $FailedVMs += $vmName; $startVM = $false
        }
    } else {
        Write-Log "[$vmName] Pas de RP Failover - VM ignoree" "WARN"; $SkippedVMs += $vmName
    }

    # ETAPE E - Demarrage sur la prod (sauf echec failback)
    if ($startVM) {
        $vmState = Invoke-Command -ComputerName $ProdHost -ScriptBlock {
            param($n) $v = Get-VM -Name $n -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" "OK" }
        else {
            try {
                Invoke-Command -ComputerName $ProdHost -ScriptBlock {
                    param($n) Start-VM -Name $n -ErrorAction Stop
                } -ArgumentList $vmName
                Write-Log "[$vmName] VM demarree sur $ProdHost" "OK"
            } catch { Write-Log "[$vmName] Erreur demarrage : $_" "ERROR"; $FailedVMs += $vmName }
        }
        Write-Log "[$vmName] Attente $delay sec avant la VM suivante..."
        Start-Sleep -Seconds $delay
    }
}

# --- NETTOYAGE FLAG + RECAP --------------------------------------------------

try {
    Invoke-Command -ComputerName $ProdHost -ScriptBlock {
        param($f) if (Test-Path $f) { Remove-Item $f -Force }
    } -ArgumentList $DRPFlagFile -ErrorAction Stop
    Write-Log "Flag DRP supprime" "OK"
} catch { Write-Log "Suppression flag DRP impossible : $_" "WARN" }

Write-Log "============================================================"
if ($FailedVMs.Count -eq 0) {
    Write-Log "FAILBACK TERMINE - PRODUCTION RESTAUREE SUR $ProdHost" "OK"
} else {
    Write-Log "FAILBACK TERMINE AVEC ERREURS - INTERVENTION REQUISE" "ERROR"
    Write-Log "VM(s) en echec : $($FailedVMs -join ', ')" "ERROR"
    Write-Log "Leurs replicas/donnees restent sur le DRP. Traiter avant tout undo." "ERROR"
}
if ($SkippedVMs.Count -gt 0) { Write-Log "VM(s) ignorees : $($SkippedVMs -join ', ')" "WARN" }
Write-Log "Reactiver l'autostart : Get-VM | Set-VM -AutomaticStartAction StartIfRunning"
Write-Log "Log complet : $LogFile"
if ($FailedVMs.Count -gt 0) { exit 1 }

Chaque commande destructrice supprimée est documentée dans le bloc DANGER en tête de fichier, avec la citation de la doc Veeam, pour éviter toute réintroduction accidentelle.


Tags : #veeam #hyper-v #drp #powershell #active-directory