Auditer le stockage virtuel d’un cluster ne consiste pas seulement à additionner les capacités provisionnées des machines. Entre les VMDK thin, les snapshots, les disques détachés après une suppression incomplète ou les fichiers déposés manuellement dans un datastore, l’écart entre capacité logique, consommation physique et espace réellement exploitable peut devenir significatif. PowerCLI permet de produire un inventaire fiable, exportable et réutilisable dans un processus de reporting.

L’objectif de cet article est de fournir un script complet compatible avec VMware.PowerCLI 13 et versions ultérieures. Il audite les VMDK associés aux VM d’un cluster, calcule les métriques de capacité et d’usage, détecte les VMDK orphelins dans les datastores accessibles au cluster, puis exporte les résultats en CSV.

Le script distingue volontairement plusieurs notions :

Préparer l’environnement PowerCLI

Le poste d’administration doit disposer du module VMware.PowerCLI. Les versions récentes s’installent depuis PowerShell Gallery et fonctionnent avec Windows PowerShell 5.1 ou PowerShell 7 selon les composants utilisés.

Install-Module -Name VMware.PowerCLI -Scope CurrentUser

Dans un environnement disposant d’une autorité de certification interne ou d’un vCenter utilisant un certificat non approuvé par le poste d’administration, il peut être nécessaire de désactiver la validation stricte du certificat. Cette option est pratique pour un poste d’exploitation, mais elle ne doit pas masquer un problème de sécurité durable.

Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false

Le compte utilisé pour la connexion doit au minimum pouvoir lire :

Pour l’inventaire des fichiers orphelins, les droits de navigation datastore sont indispensables. Un compte limité aux objets VM peut produire un rapport partiel ou indiquer à tort que certains fichiers sont absents.

Comprendre les métriques de stockage vSphere

La propriété CapacityGB d’un objet disque PowerCLI correspond à la taille logique du VMDK. Elle est facilement exploitable, mais elle ne suffit pas à mesurer le coût stockage réel. Pour aller plus loin, consultez notre guide complet sur PowerCLI.

Pour les VMDK attachés, PowerCLI expose généralement les propriétés suivantes :

La consommation physique d’un VMDK peut être moins intuitive. Dans un datastore VMFS ou NFS traditionnel, un disque thin ne réserve initialement qu’une faible portion de son espace logique. Sa consommation augmente au fil des écritures de la VM. Sur vSAN, la consommation dépend aussi des objets, de la politique de stockage, de la tolérance aux pannes, de la compression, de la déduplication et des overheads internes.

Le script utilise donc deux sources :

  1. Les objets Get-HardDisk pour les VMDK attachés et leur capacité logique.
  2. La navigation datastore avec le provider VimDatastore: pour relever les fichiers .vmdk réellement présents.

Cette approche permet de comparer les VMDK configurés dans les VM avec les fichiers trouvés dans les datastores du cluster.

Périmètre et limites de la détection des VMDK orphelins

La notion de disque orphelin mérite quelques précautions. Un fichier .vmdk non attaché à une VM n’est pas automatiquement supprimable.

Un VMDK peut être légitime sans être directement attaché à une machine active :

Le rapport doit donc être utilisé comme un inventaire de contrôle, pas comme une liste de suppression automatique. Toute suppression doit être précédée d’une validation par le propriétaire applicatif, d’une vérification de la chaîne VMDK et idéalement d’une sauvegarde ou d’un snapshot de l’infrastructure de gestion lorsque cela est pertinent.

Le script classe les fichiers dans plusieurs états :

Script PowerCLI complet

Le script suivant accepte le nom d’un cluster, se connecte à vCenter, collecte les disques attachés, explore les datastores accessibles au cluster et génère trois CSV :

Les chemins de sortie sont créés automatiquement si nécessaire.

<#
.SYNOPSIS
    Audite les VMDK d'un cluster vSphere et exporte les résultats en CSV.

.DESCRIPTION
    Le script collecte :
    - Les disques VMDK attachés aux VM du cluster ;
    - Leur capacité logique ;
    - Leur format de provisioning ;
    - Les fichiers VMDK présents dans les datastores accessibles au cluster ;
    - Les candidats VMDK orphelins non référencés par les VM auditées.

    Attention :
    Un VMDK non référencé dans le périmètre du cluster n'est pas forcément
    supprimable. Il peut appartenir à une VM hors cluster, être une copie,
    ou participer à une chaîne de snapshots.

.PARAMETER VCenter
    Nom DNS ou adresse du serveur vCenter.

.PARAMETER ClusterName
    Nom exact du cluster vSphere à auditer.

.PARAMETER OutputPath
    Répertoire de sortie des fichiers CSV.

.EXAMPLE
    .\Audit-ClusterVmdk.ps1 `
        -VCenter "vcsa.lab.local" `
        -ClusterName "Cluster-Production" `
        -OutputPath "C:\Reports\vSphere"

<figure>
<img src="/images/body/body1-script-powercli-inventaire-vmdk-2026.webp" alt="Un inventaire automatisé révèle rapidement les disques orphelins." width="800" height="533" loading="lazy" />
<figcaption>Un inventaire automatisé révèle rapidement les disques orphelins.</figcaption>
</figure>


.NOTES
    Compatible VMware.PowerCLI 13+.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$VCenter,

    [Parameter(Mandatory = $true)]
    [string]$ClusterName,

    [Parameter(Mandatory = $true)]
    [string]$OutputPath
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

# Crée le répertoire d'export si nécessaire.
if (-not (Test-Path -Path $OutputPath)) {
    New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
}

# Horodatage unique afin de conserver l'historique des exécutions.
$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss"

$DetailedCsv = Join-Path $OutputPath "VMDK-Audit-$ClusterName-$Timestamp.csv"
$OrphanCsv   = Join-Path $OutputPath "VMDK-Orphans-$ClusterName-$Timestamp.csv"
$SummaryCsv  = Join-Path $OutputPath "VMDK-Summary-$ClusterName-$Timestamp.csv"

# Empêche PowerCLI d'afficher un avertissement CEIP interactif lors d'une première utilisation.
Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -Confirm:$false | Out-Null

try {
    Write-Host "Connexion a vCenter : $VCenter" -ForegroundColor Cyan
    $VIServer = Connect-VIServer -Server $VCenter

    Write-Host "Recherche du cluster : $ClusterName" -ForegroundColor Cyan
    $Cluster = Get-Cluster -Name $ClusterName

    # Récupère les VM du cluster, y compris les VM éteintes.
    # Les templates sont exclus : leurs VMDK peuvent être analysés séparément si nécessaire.
    $VMs = Get-VM -Location $Cluster | Where-Object {
        -not $_.ExtensionData.Config.Template
    }

    if (-not $VMs) {
        throw "Aucune VM non-template n'a ete trouvee dans le cluster '$ClusterName'."
    }

    # Datastores accessibles au cluster.
    # Cette liste représente le périmètre de recherche des fichiers VMDK.
    $Datastores = Get-Datastore -RelatedObject $Cluster | Where-Object {
        $_.State -eq "Available"
    }

    if (-not $Datastores) {
        throw "Aucun datastore disponible n'a ete trouve pour le cluster '$ClusterName'."
    }

    Write-Host "VM trouvees : $($VMs.Count)" -ForegroundColor Green
    Write-Host "Datastores disponibles : $($Datastores.Count)" -ForegroundColor Green

    # Hashtable des fichiers VMDK réellement référencés par les VM du cluster.
    # La clé est normalisée en minuscules pour éviter les écarts de casse.
    $AttachedVmdkPaths = @{}

    # Liste détaillée des résultats.
    $Results = New-Object System.Collections.Generic.List[object]

    Write-Host "Collecte des VMDK attaches aux VM..." -ForegroundColor Cyan

    foreach ($VM in $VMs) {
        $HardDisks = Get-HardDisk -VM $VM

        foreach ($HardDisk in $HardDisks) {
            # Filename ressemble généralement à :
            # [Datastore01] VM-APP01/VM-APP01_1.vmdk
            $VmdkPath = $HardDisk.Filename

            if ([string]::IsNullOrWhiteSpace($VmdkPath)) {
                continue
            }

            $NormalizedPath = $VmdkPath.ToLowerInvariant()
            $AttachedVmdkPaths[$NormalizedPath] = $true

            # Estimation de l'espace utilisé :
            # CapacityGB reste la taille logique.
            # La propriété ExtensionData.Backing est consultée pour enrichir
            # le rapport, mais le calcul physique précis dépend du type de datastore.
            $CapacityGB = [math]::Round([double]$HardDisk.CapacityGB, 2)

            $Results.Add([PSCustomObject]@{
                Cluster             = $Cluster.Name
                Datastore           = $HardDisk.Filename -replace '^\[([^\]]+)\].*$', '$1'
                VM                  = $VM.Name
                PowerState          = $VM.PowerState
                DiskName            = $HardDisk.Name
                VmdkPath            = $VmdkPath
                CapacityGB          = $CapacityGB
                UsedSpaceGB         = $null
                StorageFormat       = $HardDisk.StorageFormat
                DiskType            = $HardDisk.DiskType
                Persistence         = $HardDisk.Persistence
                Status              = "Attached"
                Notes               = "VMDK reference par une VM du cluster"
            })
        }
    }

    # Monte le provider PowerCLI pour parcourir les datastores comme un système de fichiers.
    # Le provider est créé automatiquement par PowerCLI après connexion a vCenter.
    Write-Host "Exploration des fichiers VMDK dans les datastores..." -ForegroundColor Cyan

    foreach ($Datastore in $Datastores) {
        Write-Host "  Analyse datastore : $($Datastore.Name)" -ForegroundColor DarkCyan

        $DatastoreRoot = "vmstore:\$($VIServer.Name)\$($Datastore.Name)"

        try {
            # Recherche récursive des descripteurs .vmdk.
            # Les fichiers -flat.vmdk, -delta.vmdk et -sesparse.vmdk sont
            # volontairement exclus ici : on inventorie d'abord les descripteurs.
            $VmdkFiles = Get-ChildItem -Path $DatastoreRoot -Recurse -ErrorAction Stop |
                Where-Object {
                    -not $_.PSIsContainer -and
                    $_.Name -match '\.vmdk$' -and
                    $_.Name -notmatch '(-flat|-delta|-sesparse)\.vmdk$'
                } Pour aller plus loin, consultez <a href="/stockage-virtualise/">les fondamentaux du stockage virtualisé</a>.

            foreach ($File in $VmdkFiles) {
                # Conversion du chemin VimDatastore: vers le format attendu
                # par HardDisk.Filename : [Datastore] dossier/fichier.vmdk
                #
                # Exemple File.DatastoreFullPath :
                # [Datastore01] VM-APP01/VM-APP01.vmdk
                $DatastoreFullPath = $File.DatastoreFullPath

                if ([string]::IsNullOrWhiteSpace($DatastoreFullPath)) {
                    continue
                }

                $NormalizedPath = $DatastoreFullPath.ToLowerInvariant()

                if ($AttachedVmdkPaths.ContainsKey($NormalizedPath)) {
                    # Le disque est déjà ajouté dans les résultats attachés.
                    # On ne crée pas de doublon.
                    continue
                }

                # Les suffixes suivants sont fréquents dans les chaînes de snapshots
                # ou fichiers auxiliaires. Ils exigent une analyse manuelle.
                $IsSnapshotLike = $File.Name -match '-\d{6}\.vmdk$' -or
                                  $File.Name -match 'delta|sesparse|redo'

                $Status = if ($IsSnapshotLike) {
                    "SnapshotOrDelta"
                }
                else {
                    "OrphanCandidate"
                }

                $Notes = if ($IsSnapshotLike) {
                    "Fichier potentiellement lie a un snapshot ou a une chaine delta : verifier avant toute action"
                }
                else {
                    "Descripteur VMDK non reference par les VM du cluster audite"
                }

                # File.Length correspond a la taille du descripteur, et non au disque virtuel.
                # La consommation réelle doit être obtenue via les fichiers associés
                # ou les statistiques du datastore. Elle est donc laissée a null ici.
                $Results.Add([PSCustomObject]@{
                    Cluster             = $Cluster.Name
                    Datastore           = $Datastore.Name
                    VM                  = $null
                    PowerState          = $null
                    DiskName            = $File.Name
                    VmdkPath            = $DatastoreFullPath
                    CapacityGB          = $null
                    UsedSpaceGB         = $null
                    StorageFormat       = $null
                    DiskType            = $null
                    Persistence         = $null
                    Status              = $Status
                    Notes               = $Notes
                })
            }
        }
        catch {
            Write-Warning "Impossible d'explorer le datastore '$($Datastore.Name)' : $($_.Exception.Message)"

            $Results.Add([PSCustomObject]@{
                Cluster             = $Cluster.Name
                Datastore           = $Datastore.Name
                VM                  = $null
                PowerState          = $null
                DiskName            = $null
                VmdkPath            = $null
                CapacityGB          = $null
                UsedSpaceGB         = $null
                StorageFormat       = $null
                DiskType            = $null
                Persistence         = $null
                Status              = "DatastoreScanFailed"
                Notes               = $_.Exception.Message
            })
        }
    }

    # Export détaillé : tous les VMDK et les erreurs éventuelles de scan.
    $Results |
        Sort-Object Datastore, VM, VmdkPath |
        Export-Csv -Path $DetailedCsv -NoTypeInformation -Encoding UTF8

    # Export dédié aux objets nécessitant une investigation.
    $Orphans = $Results | Where-Object {
        $_.Status -in @("OrphanCandidate", "SnapshotOrDelta")
    }

    $Orphans |
        Sort-Object Datastore, VmdkPath |
        Export-Csv -Path $OrphanCsv -NoTypeInformation -Encoding UTF8

    # Synthèse par datastore pour le reporting.
    $Summary = $Results |
        Group-Object Datastore |
        ForEach-Object {
            $Attached = $_.Group | Where-Object { $_.Status -eq "Attached" }
            $OrphanCandidates = $_.Group | Where-Object { $_.Status -eq "OrphanCandidate" }
            $SnapshotCandidates = $_.Group | Where-Object { $_.Status -eq "SnapshotOrDelta" }

            [PSCustomObject]@{
                Cluster                    = $Cluster.Name
                Datastore                  = $_.Name
                AttachedVmdkCount          = $Attached.Count
                AttachedProvisionedGB      = [math]::Round(
                    [double](($Attached | Measure-Object -Property CapacityGB -Sum).Sum), 2
                )
                OrphanCandidateCount       = $OrphanCandidates.Count
                SnapshotOrDeltaCount       = $SnapshotCandidates.Count
                DatastoreScanFailed        = (@($_.Group | Where-Object {
                    $_.Status -eq "DatastoreScanFailed"
                })).Count
            }
        }

    $Summary |
        Sort-Object Datastore |
        Export-Csv -Path $SummaryCsv -NoTypeInformation -Encoding UTF8

    Write-Host ""
    Write-Host "Audit termine." -ForegroundColor Green
    Write-Host "Rapport detaille : $DetailedCsv" -ForegroundColor Green
    Write-Host "Candidats orphelins : $OrphanCsv" -ForegroundColor Yellow
    Write-Host "Synthese : $SummaryCsv" -ForegroundColor Green

    Write-Host ""
    Write-Host "Resume :" -ForegroundColor Cyan
    Write-Host "  VMDK attaches : $(@($Results | Where-Object { $_.Status -eq 'Attached' }).Count)"
    Write-Host "  Candidats orphelins : $(@($Results | Where-Object { $_.Status -eq 'OrphanCandidate' }).Count)"
    Write-Host "  Snapshots/deltas a verifier : $(@($Results | Where-Object { $_.Status -eq 'SnapshotOrDelta' }).Count)"
}
finally {
    if ($VIServer) {
        Disconnect-VIServer -Server $VIServer -Confirm:$false
    }
}
PowerCLI transforme une tâche manuelle en script réutilisable.
PowerCLI transforme une tâche manuelle en script réutilisable.

Exécuter l’audit

Enregistrez le script sous le nom Audit-ClusterVmdk.ps1, puis exécutez-le depuis une session PowerShell disposant de PowerCLI.

.\Audit-ClusterVmdk.ps1 `
    -VCenter "vcsa.exemple.local" `
    -ClusterName "Cluster-Production" `
    -OutputPath "C:\Rapports\vSphere"

Si l’exécution de scripts est bloquée sur le poste d’administration, ne modifiez pas la politique globale sans raison. Une exécution ponctuelle dans le processus courant est souvent suffisante.

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

Le CSV détaillé peut ensuite être ouvert dans Excel, importé dans Power BI ou traité par un script de consolidation. Pour les environnements francophones où Excel attend souvent un séparateur ;, il est possible de remplacer Export-Csv par Export-Csv -Delimiter ';'. Pour aller plus loin, consultez l’administration d’un hôte ESXi.

Interpréter les résultats et compléter le calcul d’usage

Le rapport produit une valeur CapacityGB fiable pour les disques attachés, car elle est issue de la configuration VM. La colonne UsedSpaceGB est volontairement vide dans cette version générique du script. Cette précaution évite de présenter comme exacte une valeur qui varierait selon le type de datastore et le mode de provisioning. Pour aller plus loin, consultez l’automatisation avec Ansible et Terraform.

Sur VMFS et NFS, il est possible d’approcher l’occupation en additionnant les fichiers liés au VMDK : descripteur, fichier -flat.vmdk, fichiers -delta.vmdk et fichiers -sesparse.vmdk. Cette opération doit toutefois être faite avec prudence, car une même chaîne de snapshot peut contenir plusieurs fichiers et les conventions de nommage ne couvrent pas tous les cas.

Sur vSAN, la taille visible dans l’arborescence datastore ne représente pas toujours l’usage physique réel sur les disques du cluster. La politique de stockage, les composants objets, la résilience, l’efficacité des données et les opérations de resynchronisation modifient la consommation. Pour un reporting capacité vSAN, il est préférable de compléter ce script par les vues de capacité vSAN et les métriques d’objets exposées par vCenter.

Une approche pragmatique consiste à utiliser ce script pour répondre à trois questions distinctes :

  1. Quelle capacité logique est présentée aux VM ?
  2. Quels VMDK sont encore configurés et attachés ?
  3. Quels descripteurs VMDK apparaissent dans les datastores sans correspondance dans le périmètre VM audité ?

Cette séparation évite de confondre surallocation, usage physique et déchets de stockage.

Vérifier un candidat orphelin avant suppression

Avant de supprimer un candidat, contrôlez au minimum les éléments suivants :

Une vérification rapide peut être réalisée en recherchant le nom du disque dans toutes les VM visibles du vCenter, et non seulement dans le cluster initial.

$SearchName = "serveur-app_1.vmdk"

Get-VM | Get-HardDisk |
    Where-Object {
        $_.Filename -like "*$SearchName"
    } |
    Select-Object Parent, Name, Filename, CapacityGB

Pour un chemin précis, recherchez plutôt la valeur complète afin d’éviter les faux positifs sur des noms de fichiers identiques.

$SearchPath = "[Datastore01] VM-APP01/VM-APP01_1.vmdk"

Get-VM | Get-HardDisk |
    Where-Object {
        $_.Filename -eq $SearchPath
    } |
    Select-Object Parent, Name, Filename, CapacityGB

Si le disque ne retourne aucun résultat, cela augmente le niveau de confiance, mais ne remplace pas l’analyse de son dossier et de sa chaîne de dépendances. Une suppression directe depuis le datastore browser ou par Remove-Item doit rester une opération exceptionnelle et tracée.

Industrialiser le reporting

Pour transformer cet audit en contrôle récurrent, planifiez son exécution depuis un serveur d’administration ou une automatisation centralisée. Une fréquence hebdomadaire est généralement suffisante pour détecter les dérives de capacité et les VMDK résiduels. Dans les environnements avec de nombreuses opérations de restauration ou de migration, une fréquence quotidienne peut être justifiée.

Conservez les exports dans un répertoire horodaté et comparez les rapports successifs. Les indicateurs utiles sont notamment :

Le rapport est particulièrement efficace lorsqu’il est croisé avec les alertes de capacité datastore, les politiques de durée des snapshots et les comptes rendus des opérations de sauvegarde. L’objectif n’est pas seulement de récupérer de l’espace : il s’agit surtout d’établir une vue cohérente entre l’inventaire vCenter et la réalité des fichiers présents sur les stockages.