While managing large Broadcom VMware environments, understanding how multiple Virtual Machines virtual disks are mapped across datastores, SCSI controllers, and guest partitions is critical for performance tuning, troubleshooting, and capacity planning. Doing this manually is time-consuming and error-prone—especially when you're dealing with multiple VMs and complex storage configurations.
This article walks you through a PowerShell-based tool that automates the collection of VMware VM disk information using PowerCLI and Windows CIM classes, and renders a dynamic HTML visualization for clear insight. Below is the screenshot of how this visualization looks.
After writing and using by my colleagues I have observed It is been very very useful to detect VM disk mappings.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
#requires -version 5 <# .SYNOPSIS Show Vmware Virtual Machine Disks mapping view in GUI HTML. .DESCRIPTION Show Vmware Virtual Machine list all Disks mapping view in GUI HTML. .PARAMETER GroupName Prompts you provide VM name, vCenter/Esxi credentials and Windows VM credentials to connect VM. .INPUTS System.Windows.Forms.Form .OUTPUTS HTML Diagram .NOTES Version: 1.0 Author: Janvi Creation Date: 15 May 2024 Purpose/Change: Get Vmware Virtual Machine Disks mapping view in HTML Useful URLs: http://vcloud-lab.com Tested on: Windows 2019 DataCenter Edition, PowerShell 7.4.2, vCenter 8.0, Chrome and Edge Browser .EXAMPLE PS C:\>. /Show-VMDiskRelationshipMapping.ps1 List all VMware Virtual Machine disk mapping #> $details = . $PSScriptRoot\_extras\VMdetailsGUI.ps1 | ConvertFrom-Json $vmName = $details.vm.vmName try { Import-Module VMware.VimAutomation.core -ErrorAction Stop | Out-Null $vCenterLogin = Connect-VIServer -Server $details.vc.vCenter -User $details.vc.vCenterUser -Password $details.vc.vCenterPassword -ErrorAction Stop $hdds = Get-VM $vmName -ErrorAction Stop | Get-HardDisk } catch { <#Do this if a terminating exception happens#> Write-Host $error[0].exception.Message break } $remoteDisksScript = { if (-not(Test-Path c:\Temp)) { New-Item -Path C:\ -Name Temp -ItemType Directory -Force } #if (-not(Test-Path c:\Temp)) $allDrives = Get-CimInstance -ClassName Win32_DiskDrive $usedDisks = $allDisks= $allDrives | ForEach-Object { $disk = $_ $partitions = "ASSOCIATORS OF " + "{Win32_DiskDrive.DeviceID='$($disk.DeviceID)'} " + "WHERE AssocClass = Win32_DiskDriveToDiskPartition" Get-CimInstance -Query $partitions | ForEach-Object { $partition = $_ $drives = "ASSOCIATORS OF " + "{Win32_DiskPartition.DeviceID='$($partition.DeviceID)'} " + "WHERE AssocClass = Win32_LogicalDiskToPartition" Get-CimInstance -Query $drives | ForEach-Object { [PSCustomObject]@{ DriveLetter = $_.DeviceID.Replace(':','') Disk = $disk.DeviceID.Substring(4) PhysicalDiskNo = $disk.DeviceID.Substring(4).TrimStart('PHYSICALDRIVE') DiskSize = [Math]::Ceiling($disk.Size/1GB) DiskModel = $disk.Model Partition = $partition.Name RawSize = [Math]::Ceiling($partition.Size/1GB) VolumeName = $_.VolumeName SizeGB = [Math]::Ceiling($_.Size/1GB) FreeSpaceGB = [Math]::Ceiling($_.FreeSpace/1GB) VolumeSerialNo = $_.VolumeSerialNumber DriveType = $_.DriveType FileSystem = $_.FileSystem SerialNumber = $disk.SerialNumber ScsiBus = $Disk.SCSIBus SCSILogicalUnit = $disk.SCSILogicalUnit SCSIPort = $disk.SCSIPort SCSITargetId = $disk.SCSITargetId Index = $disk.Index DiskIndex = $partition.DiskIndex PartitionIndex = $Partition.Index } #[PSCustomObject]@{ } #Get-CimInstance -Query $drives | ForEach-Object { } #Get-CimInstance -Query $partitions | ForEach-Object { } #$usedDisks = $allDisks= $allDrives | ForEach-Object { $rawDisks = $allDrives | Where-Object {$_.SerialNumber -notin $usedDisks.SerialNumber} $i = 100 foreach ($rawDisk in $rawDisks) { ++$i $allDisks += [PSCustomObject]@{ DriveLetter = "raw$i" Disk = $rawDisk.DeviceID.Substring(4) PhysicalDiskNo = $i DiskSize = $rawDisk.Size DiskModel = $rawDisk.Caption Partition = 'N/a' RawSize = [Math]::Ceiling($rawDisk.Size/1GB) VolumeName = 'N/a' SizeGB = [Math]::Ceiling($rawDisk.Size/1GB) FreeSpaceGB = 'N/a' VolumeSerialNo = 'N/a' DriveType = 'N/a' FileSystem = 'N/a' SerialNumber = $rawDisk.SerialNumber ScsiBus = $rawDisk.SCSIBus SCSILogicalUnit = $rawDisk.SCSILogicalUnit SCSIPort = $rawDisk.SCSIPort SCSITargetId = $rawDisk.SCSITargetId Index = $rawDisk.Index DiskIndex = $rawDisk.Index PartitionIndex = 'N/a' } #$allDisks += [PSCustomObject]@{ } #foreach ($rawDisk in $rawDisks) $allDisks #| Export-Csv -NoTypeInformation $PSScriptRoot\DiskInfo.csv #Import-Csv C:\Temp\DiskInfo.csv } # Provid clear text string for username and password [string]$userName = $details.windows.winUser [string]$userPassword = $details.windows.winPassword # Convert to SecureString [securestring]$secStringPassword = ConvertTo-SecureString $userPassword -AsPlainText -Force # convert to password object [pscredential]$credObject = New-Object System.Management.Automation.PSCredential ($userName, $secStringPassword) try { $session = New-PSSession -ComputerName $vmName -Credential $credObject #(Get-Credential -UserName 'vcloud-lab\vjanvi' -Message 'Remote Server Credentials') $allDisks = Invoke-Command -Session $session -ScriptBlock $remoteDisksScript } catch { <#Do this if a terminating exception happens#> Write-Host $error[0].exception.Message break } #Invoke-VMScript -VM $vmName -ScriptText 'Get-CimInstance -ClassName Win32_BIOS' -ScriptType Powershell -GuestUser 'vcloud-lab.com\vjanvi' -GuestPassword 'Password' #$allDisks | Import-Csv $PSScriptRoot\DiskInfo.csv $completeInfo = @() foreach ($hdd in $hdds) { $rawHddUuid = $hdd.extensiondata.backing.uuid $rawHddUuid = $rawHddUuid.Replace('-','') $matchedHdd = $allDisks | Where-Object {$_.SerialNumber -eq $rawHddUuid} $matchedHdd | Add-Member -Name StorageFormat -MemberType NoteProperty -Value $hdd.StorageFormat $matchedHdd | Add-Member -Name Filename -MemberType NoteProperty -Value $hdd.Filename $matchedHdd | Add-Member -Name CapacityGB -MemberType NoteProperty -Value $hdd.CapacityGB $matchedHdd | Add-Member -Name VMName -MemberType NoteProperty -Value $vmName $matchedHdd | Add-Member -Name HDDName -MemberType NoteProperty -Value $hdd.Name $matchedHdd | Add-Member -Name ControllerKey -MemberType NoteProperty -Value $hdd.ExtensionData.ControllerKey $matchedHdd | Add-Member -Name UnitNumber -MemberType NoteProperty -Value $hdd.ExtensionData.UnitNumber $matchedHdd | Add-Member -Name DataStoreName -MemberType NoteProperty -Value (Get-Datastore -Id $hdd.ExtensionData.Backing.Datastore[0]).Name $completeInfo += $matchedHdd } $completeInfo | Export-Csv -NoTypeInformation -Path $PSScriptRoot\DiskInfo.csv $drives = Import-Csv $PSScriptRoot\DiskInfo.csv $physicalDisks = $drives | Group-Object -Property PhysicalDiskNo $groupNumbering = $physicalDisks.Name | ForEach-Object {[int]$_} | Sort-Object $links = $null foreach ($number in $groupNumbering) { #foreach ($physicaldisk in $physicalDisks) { $physicaldisk = $physicaldisks | Where-Object {$_.Name -eq $number} $physicalDriveNo = $physicaldisk.Group[0].Disk $previousGroupDisk = 1 foreach ($group in $physicaldisk.Group) { #https://mermaid.js.org/config/schema-docs/config.html#theme $links += " {0}({0}: fa:fa-computer) ===> {1};" -f $group.DriveLetter, $physicalDriveNo if ($group.DriveLetter -notmatch 'raw') { $links += " style {0} stroke:#FFFFFF,fill:#ff595e,color:white,fontSize:30px;`n" -f $group.DriveLetter } else { $links += " style {0} stroke:#FFFFFF,fill:#0a2344,color:white,fontSize:30px;`n" -f $group.DriveLetter } if (($physicalDisk.Count -eq 1) -or ($previousGroupDisk -eq 1)) { $links += " {0} ===> SCSI{1}{2}(SCSI{1}:{2} fa:fa-server);`n" -f $physicalDriveNo, $group.SCSIPort, $group.SCSITargetId $links += " style {0} stroke:#FFFFFF,fill:#ff924c;`n" -f $physicalDriveNo $links += " SCSI{0}{1}(SCSI{0}:{1}) -.-> SCSI{2};`n" -f $group.SCSIPort, $group.SCSITargetId, $group.SCSIPort $links += " style SCSI{0}{1} stroke:#FFFFFF,fill:#ffca3a;`n" -f $group.SCSIPort, $group.SCSITargetId $links += " SCSI{0} -.-> {1}({2});`n" -f $group.SCSIPort, ($group.HDDName -replace '\s',''), $group.HDDName $links += " style SCSI{0} stroke:#FFFFFF,fill:#8ac926;`n" -f $group.SCSIPort $links += " style {0} stroke:#FFFFFF,fill:#1982c4,color:white;" -f ($group.HDDName -replace '\s','') $links += " {0}({1}) ---> {2}({3} fa:fa-database);" -f ($group.HDDName -replace '\s',''), $group.HDDName, ($group.DataStoreName -replace '\s',''), $group.DataStoreName $links += " style {0} stroke:#FFFFFF,fill:#6a4c93,color:white;" -f ($group.DataStoreName -replace '\s','') } $previousGroupDisk++ } } Import-Module "$PSScriptRoot\_extras\EPS\1.0.0\EPS.psm1" -Force | Out-Null $html = Invoke-EpsTemplate -Path "$PSScriptRoot\_extras\Diagram.eps" $html | Out-File $PSScriptRoot\DiskMapping.html "--------------------------------------------------`n" Write-Host "Open $PSScriptRoot\DiskMapping.html in your favorate browser" -BackgroundColor DarkGreen "`n--------------------------------------------------" Disconnect-VIServer * -Force -Confirm:$false |
Download this script bundle here or it is also available on github.com.
💡 What This Script Does
- Connects securely to vCenter using PowerCLI
- Collects all virtual hard disks (Get-HardDisk) for a specific VM
- Remotely queries the Windows VM for physical disk, partition, and volume info using Get-CimInstance
- Maps each disk’s:
- SCSI Port and Target ID
- Guest Drive Letters
- Volume and FileSystem
- Underlying Datastore in vSphere
- Uses Mermaid.js diagrams to generate an intuitive HTML GUI showing the disk-to-SCSI-to-storage relationships
🎯 Why This Is Useful
- 🔧 Troubleshooting: Quickly identify which virtual disk corresponds to which Windows drive (e.g., if C:\ is actually on a slower datastore).
- 🧱 Infrastructure Audits: See which SCSI controller each disk is attached to.
- 📊 Reporting: Save the HTML output and use it as documentation or attach it to change requests.
- ⚡ Automation-Ready: Easily extend the script to loop through multiple VMs or export JSON/XML reports.
HTML-based diagram that visually connects each guest drive to its virtual disk, SCSI bus, and VMware datastore. Raw disks, formatted drives, and even orphaned volumes are all included for full visibility.
Whether you're a virtualization engineer or a PowerShell enthusiast, this script saves hours of manual mapping and lets you visualize your VM’s disk layout with clarity and precision. It’s an excellent addition to your VMware automation toolkit.
Useful Articles
VMWARE VSPHERE UPDATE MANAGER (VUM) - IMPORTING ESXI ISO AND CREATE UPGRADE BASELINE
VMWARE VSPHERE UPDATE MANAGER (VUM) - UPGRADE ESXI OS
ESXi 6.0 update offline bundle via esxcli commandline: DependencyError VIB bootbank requires VSAN ImageProfile
ESXi 6.5 upgrade bundle via command line: No Space Left On Device Error
Registering HPE ILO amplifier pack (Hardware support manager) with vCenter 7 Lifecycle manager
VMware LifeCycle Manager import updates bundle and patch ESXi server
How to update a patch on an ESXi host via command line
Add a Trusted Root Certificate to the Certificate Store VMware Photon OS
Patching update VMware vCenter Server Appliance from a zipped update bundle Web server
Powershell GUI VMware ESXi custom patch bundle builder
Create a custom TCPIP stack on ESXi server - VMware PowerCLI GUI