Windows Failover Cluster 2022 : VHD Set → CSV — comment retrouver le lien et renommer proprement

Dans des guest clusters Hyper‑V, les VHD Set convertis en CSV deviennent vite « Cluster Disk 1/2/3 ». Comment retrouver, depuis l’invité, quel CSV correspond à quel fichier .vhds côté hôte et les renommer proprement avec PowerShell ? Voici une méthode fiable, automatisable et sans hacks.

Sommaire

Vue d’ensemble de la question

Dans des guest clusters (WSFC dans des VM Hyper‑V), des disques partagés VHD Set (.vhds) sont présentés puis convertis en CSV. Par défaut, les CSV s’appellent Cluster Disk 1, Cluster Disk 2, etc., et montent sous C:\ClusterStorage\…. L’objectif est de renommer les CSV selon une convention dérivée du nom du fichier .vhds (ex. mycluster1-data1.vhds → CSV Data1), mais :

  • PowerShell ne fournit pas de lien direct visible entre un CSV (côté invité) et le fichier .vhds (côté hôte).
  • Get-VMHardDiskDrive peut ne pas retourner les attachements VHD Set.
  • Si l’on renomme manuellement les sous‑dossiers sous C:\ClusterStorage, on perd l’association évidente montage ↔ CSV.

Solution éprouvée : corrélation par l’ID de disque (GUID)

Le même identifiant de disque virtuel est exposé côté hôte (dans le VHD/AVHDX du VHD Set) et côté invité (dans le disque/volume du CSV). Nous allons l’exploiter pour faire la jonction, calculer un nouveau nom selon votre convention, puis renommer les CSV proprement via Rename-ClusterSharedVolume.

Principe

  1. Côté invité : extraire le Disk GUID de chaque volume CSV, quelle que soit l’apparence de son point de montage (ex. C:\ClusterStorage\Volume1 ou C:\ClusterStorage\Data1).
  2. Côté hôte Hyper‑V : pour chaque VHD Set, lire l’Identifier via Get-VHD sur les fichiers .avhdx associés.
  3. Jointure sur le GUID, déduction d’un FriendlyName à partir du nom .vhds, et renommage des CSV avec Rename-ClusterSharedVolume (ce qui met à jour à la fois le nom du disque cluster et le dossier sous C:\ClusterStorage).

Côté invité : lister les CSV et récupérer le GUID du disque

Ce snippet retourne pour chaque CSV : son nom actuel, son point de montage et le GUID du disque support.

# Exécuter depuis un nœud du guest cluster (Admin)
$csvMap = Get-ClusterSharedVolume | ForEach-Object {
    $mp = $_.SharedVolumeInfo.FriendlyVolumeName  # C:\ClusterStorage\Volume1 ou \Data1
    # Normalise le chemin d'accès (ajoute le \ final si nécessaire)
    $accessPath = ($mp.TrimEnd('\') + '\')
    # Récupère le disque portant ce point de montage
    $disk = Get-Partition -AccessPath $accessPath | Get-Disk
    [pscustomobject]@{
        CsvName   = $_.Name
        MountPath = $mp
        DiskGuid  = $disk.Guid
        DiskNum   = $disk.Number
        FSLabel   = $_.SharedVolumeInfo.FileSystemLabel
    }
}
$csvMap | Sort-Object CsvName | Format-Table -AutoSize

Remarque : n’utilisez pas l’Explorateur pour renommer les sous‑dossiers de C:\ClusterStorage. Utilisez Rename-ClusterSharedVolume, qui maintient la cohérence entre le nom du CSV, le label du volume et le point de montage.

Variante robuste si le point de montage est un chemin de périphérique

Dans certains environnements, FriendlyVolumeName peut ressembler à \\?\Volume{GUID}\. Voici une alternative à partir du UniqueId du volume :

$csvMap = Get-ClusterSharedVolume | ForEach-Object {
    $fv = $_.SharedVolumeInfo.FriendlyVolumeName
    $vol = Get-Volume -Path ($fv.TrimEnd('\') + '\')
    $disk = Get-Disk -Number (Get-Partition -AccessPath $vol.Path).DiskNumber
    [pscustomobject]@{
        CsvName   = $_.Name
        MountPath = $fv
        DiskGuid  = $disk.Guid
    }
}

Côté hôte Hyper‑V : récupérer l’ID de chaque VHD Set

Un VHD Set s’appuie sur un fichier .vhds (métadonnées) et un ou plusieurs .avhdx (données). Get-VHD sur un .avhdx retourne l’identifiant du disque virtuel. Le code ci‑dessous scanne un ou plusieurs répertoires racine, déduplique par GUID, et renvoie la correspondance GUID → nom logique du VHD Set.

# Exécuter sur l'hôte Hyper‑V (ou via Invoke-Command)
$roots = @('D:\VMStorage\VHDSet','E:\Clustershare\Disks')

\$vhdIdMap = foreach (\$root in \$roots) {
Get-ChildItem -Path \$root -Filter \*.avhdx -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
try {
\$v = Get-VHD -Path \$*.FullName
\$setName = (Get-ChildItem \$*.Directory -Filter \*.vhds -ErrorAction SilentlyContinue |
Select-Object -First 1).BaseName
if (\$setName -and \$v.DiskIdentifier) {
\[pscustomobject]@{
VhdSetBaseName = \$setName        # ex. mycluster1-data1
AvhdxPath      = \$*.FullName
DiskGuid       = \$v.DiskIdentifier
SizeGB         = \[math]::Round(\$v.Size/1GB,1)
}
}
} catch { }
}
} | Sort-Object DiskGuid, AvhdxPath | Group-Object DiskGuid | ForEach-Object {
\# déduplique : 1 GUID -> 1 nom de VHD set
\$*.Group | Select-Object -First 1
}

\$vhdIdMap | Format-Table VhdSetBaseName, DiskGuid, SizeGB 

Jointure GUID et renommage des CSV

La jointure associe chaque CSV au VHD Set correspondant. Ci‑dessous, on extrait le purpose (tout ce qui suit le premier tiret dans <cluster>-<purpose>) pour faire un nom court (Data1, Logs, etc.) puis on renomme avec l’outillage cluster.

# Côté invité : $csvMap provient du bloc précédent
# Côté hôte : importer $vhdIdMap (via fichier, REST, ou Invoke-Command qui renvoie l'objet)

\$join = foreach (\$c in \$csvMap) {
\$m = \$vhdIdMap | Where-Object { $\_.DiskGuid -eq \$c.DiskGuid }
if (\$m) {
\$purpose = (\$m.VhdSetBaseName -split '-',2)\[-1]         # ex. "data1"
\$friendly = (Get-Culture).TextInfo.ToTitleCase(\$purpose) # "Data1"
\[pscustomobject]@{
CsvName    = \$c.CsvName
MountPath  = \$c.MountPath
DiskGuid   = \$c.DiskGuid
VhdSetName = \$m.VhdSetBaseName
NewCsvName = \$friendly
}
}
}

# Aperçu de la bascule

\$join | Sort-Object CsvName | Format-Table -AutoSize

# Renommer proprement (met à jour le dossier sous C:\ClusterStorage)

\$join | ForEach-Object {
Rename-ClusterSharedVolume -Name \$*.CsvName -NewName \$*.NewCsvName
} 

Effet attendu : le nom du CSV et le dossier monté sous C:\ClusterStorage\ deviennent Data1, Logs, etc., sans manipuler l’Explorateur ni le Registre.

Script complet, prêt à l’emploi

Le script suivant encapsule les étapes ci‑dessus : collecte côté invité, collecte côté hôtes Hyper‑V (un ou plusieurs), jointure, preview et application optionnelle. Il inclut une logique de transformation du nom (<prefix>-<purpose>Purpose) personnalisable.

#requires -Version 5.1
#requires -Modules FailoverClusters,Hyper-V,Storage

\[CmdletBinding(SupportsShouldProcess)]
param(
\[Parameter(Mandatory)]
\[string\[]]\$HyperVHosts,                    # hôtes Hyper-V qui stockent les VHD Set
\[Parameter(Mandatory)]
\[string\[]]\$VhdRoots,                       # dossiers racine à scanner sur chaque hôte
\[ValidateSet('SplitDash','Regex','Passthrough')]
\[string]\$NameRule = 'SplitDash',
\[string]\$Regex = '^(?.+?)-(?\.+)\$',
\[switch]\$Apply
)

function Get-CsvDiskGuidMap {
Get-ClusterSharedVolume | ForEach-Object {
\$mp = \$*.SharedVolumeInfo.FriendlyVolumeName
\$disk = Get-Partition -AccessPath (\$mp.TrimEnd('') + '') | Get-Disk
\[pscustomobject]@{
CsvName   = \$*.Name
MountPath = \$mp
DiskGuid  = \$disk.Guid
}
}
}

function Get-VhdSetIdMapRemote {
param(\[string\[]]\$Roots)
@'
param(\[string\[]]\$Roots)
\$vhdIdMap = foreach (\$root in \$Roots) {
Get-ChildItem -Path \$root -Filter \*.avhdx -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
try {
\$v = Get-VHD -Path \$*.FullName
\$setName = (Get-ChildItem \$*.Directory -Filter \*.vhds -ErrorAction SilentlyContinue |
Select-Object -First 1).BaseName
if (\$setName -and \$v.DiskIdentifier) {
\[pscustomobject]@{
VhdSetBaseName = \$setName
AvhdxPath      = \$*.FullName
DiskGuid       = \$v.DiskIdentifier
SizeGB         = \[math]::Round(\$v.Size/1GB,1)
}
}
} catch { }
}
} | Sort-Object DiskGuid, AvhdxPath | Group-Object DiskGuid | ForEach-Object {
\$*.Group | Select-Object -First 1
}
return \$vhdIdMap
'@
}

function Resolve-NewCsvName {
param(\[Parameter(Mandatory)]\[string]\$VhdSetBaseName)
switch (\$NameRule) {
'SplitDash' {
\$purpose = (\$VhdSetBaseName -split '-',2)\[-1]
return (Get-Culture).TextInfo.ToTitleCase(\$purpose)
}
'Regex' {
\$m = \[regex]::Match(\$VhdSetBaseName, \$Regex)
if (\$m.Success -and \$m.Groups\['purpose'].Value) {
return (Get-Culture).TextInfo.ToTitleCase(\$m.Groups\['purpose'].Value)
}
return \$VhdSetBaseName
}
'Passthrough' { return \$VhdSetBaseName }
}
}

Write-Verbose "Collecte CSV (invité)..."
\$csvMap = Get-CsvDiskGuidMap

Write-Verbose "Collecte VHD Set (hôtes Hyper‑V)..."
\$script = Get-VhdSetIdMapRemote
\$vhdIdMap = foreach (\$h in \$HyperVHosts) {
Invoke-Command -ComputerName \$h -ScriptBlock (\[scriptblock]::Create(\$script)) -ArgumentList (\$VhdRoots) -ErrorAction Stop
}

# jointure

\$plan = foreach (\$c in \$csvMap) {
\$m = \$vhdIdMap | Where-Object { $\_.DiskGuid -eq \$c.DiskGuid }
if (\$m) {
\[pscustomobject]@{
CsvName    = \$c.CsvName
MountPath  = \$c.MountPath
DiskGuid   = \$c.DiskGuid
VhdSetName = \$m.VhdSetBaseName
NewCsvName = (Resolve-NewCsvName -VhdSetBaseName \$m.VhdSetBaseName)
}
} else {
Write-Warning "Aucune correspondance VHD Set pour CSV '\$(\$c.CsvName)' (\$(\$c.MountPath)) GUID=\$(\$c.DiskGuid)"
}
}

"--- Aperçu du plan ---"
\$plan | Sort-Object CsvName | Format-Table -AutoSize

if (\$Apply) {
foreach (\$p in \$plan) {
if (\$PSCmdlet.ShouldProcess("\$(\$p.CsvName) -> \$(\$p.NewCsvName)", "Rename-ClusterSharedVolume")) {
Rename-ClusterSharedVolume -Name \$p.CsvName -NewName \$p.NewCsvName -ErrorAction Stop
}
}
"Renommage terminé."
} else {
"Exécutez à nouveau avec -Apply pour appliquer."
} 

Conseil d’exploitation : lancez d’abord le script en « lecture » (sans -Apply) pour afficher le plan. Si tout est OK, rejouez avec -Apply. Exemples :

# Prévisualisation
.\Rename-CsvFromVhdSet.ps1 -HyperVHosts @('HV01','HV02') -VhdRoots @('D:\VMStorage','E:\VhdSet') -Verbose

# Application

.\Rename-CsvFromVhdSet.ps1 -HyperVHosts @('HV01','HV02') -VhdRoots @('D:\VMStorage','E:\VhdSet') -Apply 

Inventorier les attachements VHD Set quand Get‑VMHardDiskDrive est silencieux

Selon les versions, Get-VMHardDiskDrive n’affiche pas les shared VHD Set. Passez par WMI/CIM Hyper‑V (Msvm_ResourceAllocationSettingData) pour lister les ressources disque et récupérer HostResource (chemin des .vhds/.avhdx), le parent et l’InstanceID (slot).

# Exécuter sur l'hôte Hyper‑V
$vm = 'NomDeLaVM'
$rasd = Get-CimInstance -Namespace root\virtualization\v2 `
        -Query "ASSOCIATORS OF {Msvm_ComputerSystem.ElementName='$vm'}
                WHERE ResultClass = Msvm_ResourceAllocationSettingData
                AND AssocClass = Msvm_SystemDevice"

\$disks = \$rasd | Where-Object {
\$*.ResourceSubType -eq 'Microsoft\:Hyper-V\:Virtual Hard Disk' -and
(\$*.HostResource -match '.vhds\$' -or $\_.HostResource -match '.avhdx\$')
} | Select-Object ElementName, HostResource, Parent, InstanceID

\$disks | Format-Table -AutoSize 

Cette approche donne une liste fiable des fichiers attachés (y compris VHD Set), leur emplacement et leur slot, utile pour l’outillage et les audits.

Lier un montage arbitraire à son CSV

Même si le dossier sous C:\ClusterStorage\ a été renommé, l’association reste exposée par l’API Cluster :

Get-ClusterSharedVolume | Select-Object `
    Name,
    @{n='MountPath';e={$_.SharedVolumeInfo.FriendlyVolumeName}},
    @{n='FS';e={$_.SharedVolumeInfo.FileSystem}},
    @{n='Label';e={$_.SharedVolumeInfo.FileSystemLabel}}

Pour retrouver le disque physique correspondant à un chemin de montage arbitraire :

$path = 'C:\ClusterStorage\QuelqueChose\'
(Get-Partition -AccessPath $path | Get-Disk) | Select-Object Number, Guid, UniqueId

Exemples concrets : de .vhds à CSV

Supposons trois VHD Set :

  • sqlcluster-data1.vhds
  • sqlcluster-logs.vhds
  • sqlcluster-backup.vhds

Après jointure par GUID et transformation SplitDash, le plan de renommage ressemblera à ceci :

CSV actuelPoint de montageGUIDVHD SetNouveau nom CSV
Cluster Disk 1C:\ClusterStorage\Volume1{3c7b8a0f-3d6c-4a53-a7a3-2f8c5e8e0c1a}sqlcluster-data1Data1
Cluster Disk 2C:\ClusterStorage\Volume2{e2d1f412-1d1d-4ca7-9b25-3a0e1c0ba9b2}sqlcluster-logsLogs
Cluster Disk 3C:\ClusterStorage\Volume3{a8c2f9de-7e01-4f3c-9ca3-9b8f606f5a23}sqlcluster-backupBackup

Bonnes pratiques et pièges à éviter

  • Ne renommez pas les sous‑dossiers de C:\ClusterStorage à la main : utilisez Rename-ClusterSharedVolume, qui maintient la cohérence des métadonnées du cluster et du point de montage.
  • Pas de Registre : tout ce qu’il faut est exposé par les cmdlets Cluster/Storage/Hyper‑V.
  • Label de volume : alignez le FileSystemLabel avec le nom CSV pour simplifier les audits (Set-Volume -DriveLetter ... -NewFileSystemLabel ... si nécessaire).
  • Fenêtre de changement : le renommage ne coupe pas l’I/O, mais prévoyez une brève fenêtre (surveillance, sauvegarde moderne) pour éviter tout bruit opérationnel.
  • Permissions : exécutez en tant qu’Administrateur local + droits d’admin cluster; pour la collecte sur hôtes, Activez/validez WinRM si vous utilisez Invoke-Command.
  • Convention de nommage : standardisez une règle (ex. <service>-<purpose>) et codez‑la dans la fonction de transformation.

Cas particuliers & dépannage

SymptômeCause probableCorrectif
Le CSV n’apparaît pas dans le planAucun VHD Set avec un GUID correspondantExécuter la collecte côté hôte sur tous les hôtes susceptibles de détenir les fichiers; vérifier les chemins $VhdRoots.
Get-VMHardDiskDrive ne liste pas le VHD SetLimitations d’exposition des disques partagésUtiliser le bloc WMI/CIM Msvm_ResourceAllocationSettingData ci‑dessus.
Exception Access denied sur Get-VHDAutorisations ou fichier verrouilléExécuter en Admin sur l’hôte; vérifier l’accès aux partages/volumes où résident les .avhdx.
Nom CSV déjà utiliséCollision de nommageAdapter la règle (ex. ajouter un suffixe -01) ou renommer en deux temps.
Chemins CSV non standard (étiquettes fantaisistes)Renommage manuel passéFiez‑vous à SharedVolumeInfo et au GUID disque; ne supposez rien d’un label/dossier.

Pourquoi cette méthode est robuste ?

  • Le GUID ne ment pas : c’est l’identifiant du disque virtuel exposé à la VM. Il traverse les couches Hyper‑V/Cluster sans être affecté par les labels/dossiers.
  • Sans dépendre d’un mapping volatile : pas besoin de parser des chemins à la main ou d’assumer une topologie de stockage particulière.
  • Idempotent : relancer le script résout le même mapping; si un CSV est déjà bien nommé, aucune action n’est entreprise.

Aller plus loin : rapport d’audit

Vous pouvez produire un CSV d’audit des associations actuelles (utile pour CMDB/PRISME) sans modifier quoi que ce soit :

$report = $join | Select-Object CsvName, MountPath, DiskGuid, VhdSetName, NewCsvName
$path = Join-Path $env:TEMP "GuestCSV-$(Get-Date -Format yyyyMMdd-HHmmss).csv"
$report | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $path
"Rapport : $path"

Alternative : Storage Spaces Direct dans le guest cluster

Si vous cherchez moins de gestion fichier‑par‑fichier et plus de fonctionnalités (résilience, pool, tiering), S2D côté invité peut simplifier l’exploitation en fournissant un espace de noms commun et des CSV gérés par le pool. Cela dit, c’est plus lourd (matrice de compatibilité, réseau est/ouest, supervision) et ne remplace pas toujours la simplicité d’un VHD Set pour de petits clusters.

Résumé opérationnel

  • Il n’existe pas d’API unique « CSV → .vhds ».
  • La corrélation par GUID de disque (hôte : Get‑VHD sur .avhdx  / invité : Get‑Partition … | Get‑Disk) est fiable et permet d’automatiser le renommage CSV (Rename‑ClusterSharedVolume).
  • Pour inventorier les attachements VHD Set quand les cmdlets simplifiées ne suffisent pas, utilisez WMI/CIM Hyper‑V.
  • Évitez le renommage manuel des dossiers sous C:\ClusterStorage ; utilisez toujours la cmdlet cluster.

FAQ rapide

Le renommage d’un CSV interrompt‑il l’I/O ?
Non, le disque reste en ligne ; seul le nom convivial et le point de montage sont mis à jour par le cluster.

Et si plusieurs .avhdx pointent sur le même .vhds ?
Le DiskIdentifier sera identique ; la déduplication par GUID du script garantit une correspondance 1:1 CSV → VHD Set.

Puis‑je imposer un autre schéma de nommage ?
Oui : utilisez l’option Regex dans le script, ou remplacez la fonction Resolve-NewCsvName par votre logique maison (ex. réglages par environnement/équipe).

Dois‑je renommer aussi le label NTFS ?
Ce n’est pas obligatoire mais fortement recommandé pour qu’un Get-Volume rende des noms parlants.


Annexe : snippets essentiels

Lister rapidement CSV → GUID

Get-ClusterSharedVolume | ForEach-Object {
  '{0}  {1}  {2}' -f $_.Name, $_.SharedVolumeInfo.FriendlyVolumeName,
  ((Get-Partition -AccessPath ($_.SharedVolumeInfo.FriendlyVolumeName.TrimEnd('\') + '\') | Get-Disk).Guid)
}

Renommer un seul CSV en toute sécurité

Rename-ClusterSharedVolume -Name 'Cluster Disk 1' -NewName 'Data1'

Récupérer les fichiers VHD Set d’une VM donnée (hôte)

$vm = 'SQLNODE01'
Get-CimInstance -Namespace root\virtualization\v2 -Query @"
ASSOCIATORS OF {Msvm_ComputerSystem.ElementName='$vm'}
WHERE ResultClass = Msvm_ResourceAllocationSettingData
AND AssocClass = Msvm_SystemDevice
"@ | Where-Object {
  $_.ResourceSubType -eq 'Microsoft:Hyper-V:Virtual Hard Disk'
} | Select-Object ElementName, HostResource

En appliquant cette méthodologie, vous obtenez un inventaire traçable et des CSV nommés proprement, ce qui simplifie l’exploitation quotidienne et les audits, sans bricolage ni manipulation manuelle risquée.

Sommaire