Sometimes I get frustrated with how something is done and I put my head down and get to work. In this case it's working with VMWare disks.

On occasion we have to expand our virtual disks due to normal data growth. To do this we have to make a request to our System Operations team (they are a great team BTW and super responsive), in that request we have to include the exact size of the disk we need to expand because there is no easy way to tell which disk is which from inside the guest VM. This is where my frustration comes in. So I got to work and a few iterations of code later I found a solution.

The Common Thread

I must have poured over objects running Get-Member for hours trying to find the common thread that tied the Windows disk to the VMWare disk. In the end it was pretty simple: "physical" order

When configuring disks in VCenter you can configure one or more SCSI controllers:

List of SCSI controllers in VCenter

These are numbered starting with 0. If you look at the disks themselves, you will see they are identified by the controller id + the disk id for that controller:

List of disks in VCenter

So how do the disks look in Windows? Well, you might think that the disk number in Windows would honor the order the disks are attached tot he virtual controllers, but you would be wrong. Just like I was. To see the disks in order of disk number, run the following code:

Invoke-Command -ComputerName server1.markw.dev -ScriptBlock {
    Get-PhysicalDisk | Sort-Object -Property DeviceID
} `
| Select-Object -Property `
    DeviceID,
    @{Name="Size";Expression={"$($_.Size/1GB)GB"}}

In my example case I see the following:

DeviceID Size
-------- ----
0        50GB
1        10GB
2        50GB
3        70GB
4        32GB
5        25GB
6        75GB

If you refer to the screenshots from VCenter you'll see the order of the disks does NOT match. So what next? Well, we'll fast forward, but next I did a lot of work poking around in Windows and trying various commands in PowerShell piped into Get-Member. What I eventually found was pretty simple, the PhysicalLocation and DeviceID properties:

Invoke-Command -ComputerName server1.markw.dev -ScriptBlock {
    Get-PhysicalDisk | Sort-Object -Property DeviceID
} `
    | Select-Object -Property `
        PhysicalLocation,
        DeviceID,
        @{Name="Size";Expression={"$($_.Size/1GB)GB"}} -Unique `
    | Sort-Object -Property `
        PhysicalLocation,
        DeviceID

This gave me the following output:

PhysicalLocation DeviceID Size
---------------- -------- ----
SCSI0            0        50GB
SCSI0            1        10GB
SCSI0            2        50GB
SCSI1            5        25GB
SCSI1            6        75GB
SCSI2            3        70GB
SCSI2            4        32GB

If we again refer back to our screenshot, this disk order matches!

Let's put it all together now (NOTE: This function requires the VMWare PowerCLI PowerShell modules):

function Get-VMDiskInformation {
    <#
    .SYNOPSIS
    Returns information about the VMWare volumes and associated
    Windows drives on a given guest.

    .DESCRIPTION
    Function connects to vSphere and a given Windows guest and
    returns the Windows phyical disk information mapped to the
    disks in VCenter.

    .PARAMETER VSphereServer
    FQDN of vSphereServer that hosts the VM

    .PARAMETER ComputerFQDN
    FQDN of the target server

    .PARAMETER Credentials
    An option parameter to take a credential object in. This
    is then used to authenticate with VSphere
    #>
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$True)][ValidateNotNullOrEmpty()]
        [string]$VSphereServer,
        [Parameter(Mandatory=$True)][ValidateNotNullOrEmpty()]
        [string]$ComputerFQDN,
        $Credentials
    )

    Import-Module VMware.VimAutomation.Core

    # get user credentials to connect to vSphere
    if ( -not $Credentials ) {
        $Credentials = $(Get-Credential)
    }

    # initialise arrays
    $g_mounts = @()
    $results = @()

    # set error action
    $ErrorActionPreference = "Stop"

    # connecting to vSphere
    $Start = Get-Date
    Write-Host "Connection to VSphere Server..."
    $vSphere = Get-VIServer -Server $VSphereServer -Credential $Credentials -Force

    # retrieving VM object
    Write-Host "Getting VM Object..."
    $VMName =   Get-View -Viewtype VirtualMachine -Property guest.hostname, name `
                | Where-Object { $_.guest.hostname -eq $ComputerFQDN } `
                | Select-Object -ExpandProperty Name
    $VM = Get-VM -Server $vSphere.Name -Name $VMName

    Write-Host "Getting Disks..."
    # getting disk information
    $v_disks = $VM | Get-HardDisk | Sort-Object -Property Id
    $Session = New-CimSession -ComputerName $ComputerFQDN

    # Some servers return duplicate entries when grabbing the physical disks,
    # so we first grab the in-use serial numbers to filter the physical disks
    $g_disk_serials = Get-Disk -CimSession $Session | Select-Object -ExpandProperty SerialNumber

    # get physical disk info, but only for disks with serial numbers that appear
    # in `Get-Disk`. We NEED the physical location to properly map the drives.
    $g_disks = Get-PhysicalDisk -CimSession $Session `
                | Where-Object { $_.SerialNumber -in $g_disk_serials } `
                | Select-Object -Property PhysicalLocation,DeviceID,SerialNumber -Unique `
                | Sort-Object -Property PhysicalLocation,DeviceID

    Write-Verbose "Disk Info:"
    Write-Verbose " - VMWare Disks: $($v_disks.Count)"
    $v_disks | ForEach-Object {
        Write-Verbose "  - $($_)"
    }
    Write-Verbose " - Guest Disks: $($g_disks.Count)"
    $g_disks | ForEach-Object {
        Write-Verbose "  - $($_)"
    }

    Write-Host "Finding mount points..."
    # adding disk information to g_mounts array, ignore empty mount paths
    $CimPartInfo =  Get-Partition -CimSession $Session `
                    | Where-Object { $_.AccessPaths } `
                    | Select-Object -Property DiskNumber,AccessPaths,PartitionNumber
    foreach ($CimPart in ($CimPartInfo | Where-Object { $_.AccessPaths } )) {
        $g_mounts += [PSCustomObject]@{
            Path = $CimPart.AccessPaths[0]
            DiskNumber = $CimPart.DiskNumber
            PartitionNumber = $CimPart.PartitionNumber
        }
    }
    Write-Verbose "Mount Point Info:"
    $g_mounts | ForEach-Object {
        Write-Verbose " - ID: $($_.DiskNumber) - $($_.Path)"
    }

    # map the vm disks to the guest disks (include mount points if used)
    for($i=0;$i -lt $v_disks.Count;$i++){
        $v_disk = $v_disks[$i]
        $g_disk = $g_disks[$i]
        $g_mount = $g_mounts | Where-Object { $_.DiskNumber -eq $g_disk.DeviceId -and $_.Path }

        # adding disk information to results array
        if ( $g_drives -notcontains 900 ) {
            $results += [PSCustomObject]@{
                # In cases where a disk has multiple partitions and multiple mount points
                # we only care about reporting the disk number once.
                DiskNumber = $g_mount.DiskNumber | Select -First 1
                PartitionNumber = $g_mount.PartitionNumber
                VMWareDisk = $v_disk.Name
                DiskSerial = $g_disk.SerialNumber
                Path = $g_mount.Path
                CapacityGB = $v_disk.CapacityGB
            }
        }
    }

    # return results array
    $results
}

[download this script]

This function does a little more than we covered above, but to keep it simple it does the following:

This function goes a step farther as well and tells you which mountpoint the disk is mounted on (if mountpoints are used).

Conclusion

This project was a LOT harder than it should have been. I did a lot of searching and never really found a simple way to do this. Most of the posts I came across recommended using the disk size to do the matching but this just felt really hacky to me. In the final version of this function I also access data from a Pure Storage array to identify the specific VVol associated with the guest disk, but that seemed a little to specific for a blog post. If you are interested in that part, send me an email.