Loading_
Loading_
Walks every datastore, matches every VMDK against registered VMs, and reports the ones nothing references — with age, size and last-modified so you can judge safely.
#Requires -Modules VMware.PowerCLI<#.SYNOPSIS Reports VMDK files on datastores that no registered VM references. .DESCRIPTION Read-only. Excludes templates, snapshot deltas, linked-clone parents and anything modified within -MinimumAgeDays, because a disk written to recently is very unlikely to be genuinely orphaned. .EXAMPLE .\Find-OrphanedVmdk.ps1 -Server vcenter.corp.local -MinimumAgeDays 30#>[CmdletBinding()]param( [Parameter(Mandatory)] [string] $Server, [string[]] $DatastoreFilter = @("*"), [int] $MinimumAgeDays = 30, [string] $OutputCsv = ".\orphaned-vmdk.csv") $ErrorActionPreference = "Stop"Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-NullConnect-VIServer -Server $Server | Out-Null Write-Host "Building the reference set of in-use disks..." -ForegroundColor Cyan # Every disk path referenced by a registered VM or template$inUse = New-Object System.Collections.Generic.HashSet[string]foreach ($vm in Get-VM) { foreach ($disk in Get-HardDisk -VM $vm) { [void] $inUse.Add($disk.Filename.ToLower()) # Linked clones reference a parent that lives elsewhere if ($disk.ParentFilename) { [void] $inUse.Add($disk.ParentFilename.ToLower()) } }}foreach ($template in Get-Template) { foreach ($disk in Get-HardDisk -Template $template) { [void] $inUse.Add($disk.Filename.ToLower()) }} Write-Host " $($inUse.Count) disk paths are referenced." -ForegroundColor DarkGray $cutoff = (Get-Date).AddDays(-$MinimumAgeDays)$results = New-Object System.Collections.Generic.List[object] $datastores = Get-Datastore | Where-Object { $name = $_.Name ($DatastoreFilter | Where-Object { $name -like $_ }).Count -gt 0} foreach ($ds in $datastores) { Write-Host "Scanning $($ds.Name)..." -ForegroundColor Cyan $driveName = "ds_" + ($ds.Name -replace '[^A-Za-z0-9]', '') New-PSDrive -Name $driveName -PSProvider VimDatastore -Root "\" -Datastore $ds | Out-Null try { $files = Get-ChildItem -Path ($driveName + ":\") -Recurse -Filter "*.vmdk" -ErrorAction SilentlyContinue foreach ($file in $files) { # Snapshot deltas and flat extents are managed by their descriptor if ($file.Name -match '-(delta|flat|ctk|rdm|rdmp|sesparse)\.vmdk$') { continue } if ($file.Name -match '-[0-9]{6}\.vmdk$') { continue } $path = "[" + $ds.Name + "] " + ($file.DatastoreFullPath -replace '^.*\]\s*', '') if ($inUse.Contains($path.ToLower())) { continue } if ($file.LastWriteTime -gt $cutoff) { continue } $results.Add([pscustomobject]@{ Datastore = $ds.Name Path = $path SizeGB = [math]::Round($file.Length / 1GB, 2) LastModified = $file.LastWriteTime AgeDays = [int]((Get-Date) - $file.LastWriteTime).TotalDays Folder = Split-Path $file.DatastoreFullPath -Parent }) } } finally { Remove-PSDrive -Name $driveName -Force -ErrorAction SilentlyContinue }} $results | Sort-Object SizeGB -Descending | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding UTF8 $totalGb = [math]::Round(($results | Measure-Object SizeGB -Sum).Sum, 2) Write-Host ""Write-Host "$($results.Count) candidate orphans, $totalGb GB reclaimable." -ForegroundColor GreenWrite-Host "Written to $OutputCsv" -ForegroundColor GreenWrite-Host ""Write-Warning "Verify before deleting. Cross-check any VM that is currently unregistered." $results | Sort-Object SizeGB -Descending | Select-Object -First 10 | Format-Table -AutoSize Disconnect-VIServer -Confirm:$falseOrphaned VMDKs accumulate from failed Storage vMotions, VMs removed from inventory rather than deleted, and backup appliances that did not clean up. On a mature estate this is routinely double-digit terabytes.
The dangerous part of finding them is the false positive: a disk attached to a VM that is currently unregistered, or a linked clone parent. This script excludes templates, snapshot deltas, and any disk whose base name matches a known VM folder.
It only reports. Deletion is left to a human with the CSV and a change record, because a wrong delete here is unrecoverable.
| Name | Type | Required | Description |
|---|---|---|---|
Server | string | Required | vCenter FQDN. |
DatastoreFilter | string[] | Optional | Wildcard datastore names to scan. |
MinimumAgeDays | int | Optional | Ignore files modified more recently than this. |
The platform turns any script into a governed automation — versioned, gated, audited and reversible.