Change Windows Drive Letter Based on CSV Input & Custom Object Match

I’ve created a script based on input from various VMware powercli forums that builds a listing of VMDK files/datastores for a given Windows VM, along with pieces of corresponding WMI information from within the Windows server guest. This part of the script seems to be working reliably, here is the script so far:

# Add Snap-in for VMware PowerCLI
Add-PSSnapin VMware.VimAutomation.Core | Out-Null

# Prompt for vCenter and CSV input file details
$vCenter = Read-Host "vCenter Name or IP"
$CSVLocation = Read-Host "Full path to .csv file"
Connect-VIServer -Server $vCenter | Out-Null

# Import list of VMs from input file
$CSV = Import-Csv $CSVLocation | sort VM -Unique

# Loop through VMs from CSV and match VMDKs to Windows drives
$results = foreach($computer in $CSV)
{
$computer = $computer.VM
$VMView = Get-VM -Name $computer | Get-View
$ServerDiskToVolume = @(
Get-WmiObject -Class Win32_DiskDrive -ComputerName $computer | foreach {

$Dsk = $_
$query = "ASSOCIATORS OF {Win32_DiskDrive.DeviceID='$($_.DeviceID)'} WHERE ResultClass=Win32_DiskPartition" 

Get-WmiObject -Query $query -ComputerName $computer | foreach { 

$query = "ASSOCIATORS OF {Win32_DiskPartition.DeviceID='$($_.DeviceID)'} WHERE ResultClass=Win32_LogicalDisk" 

Get-WmiObject -Query $query -ComputerName $computer | Select DeviceID,
VolumeName,
@{ label = "SCSITarget"; expression = {$dsk.SCSITargetId} },
@{ label = "SCSIBus"; expression = {$dsk.SCSIBus} }
}
}
)

# Loop through all the SCSI controllers on the VM and find those that match the Controller and Target
$VMDisks = ForEach ($VirtualSCSIController in ($VMView.Config.Hardware.Device | Where {$_.DeviceInfo.Label -match "SCSI Controller"}))
{
ForEach ($VirtualDiskDevice in ($VMView.Config.Hardware.Device | Where {$_.ControllerKey -eq $VirtualSCSIController.Key}))
{
#Match up the VM to a logical disk
$MatchingDisk = @( $ServerDiskToVolume | Where {$_.SCSITarget -eq $VirtualDiskDevice.UnitNumber -and $_.SCSIBus -eq $VirtualSCSIController.BusNumber} )

#Build a custom object to hold results
[pscustomobject]@{
VM = $VMView.Name
HostName = $VMView.Guest.HostName
DiskFile = $VirtualDiskDevice.Backing.FileName
DiskName = $VirtualDiskDevice.DeviceInfo.Label
DiskSizeGB = [math]::Round($VirtualDiskDevice.CapacityInKB/1024KB,0)
SCSIController = $VirtualSCSIController.BusNumber
SCSITarget = $VirtualDiskDevice.UnitNumber
CurrentDriveLetter = $MatchingDisk.DeviceID
}
}
}
$VMDisks
}

# Output results to console
$results | Format-Table -AutoSize

# Disconnect from vCenter
Disconnect-VIServer -Server $vCenter -Confirm:$false

Here is a sample of the input CSV file:

VM,Datastore,DiskPath,DriveLetter,SizeGB,FullPath
SERVER01,DTS001,SERVER01/SERVER01.vmdk,T,5,[DTS001] SERVER01/SERVER01.vmdk
SERVER02,DTS002,SERVER02/SERVER02_1.vmdk,T,5,[DTS002] SERVER02/SERVER02_1.vmdk

Here is a sample of the output that the script returns:

VM       HostName                DiskFile                         DiskName    DiskSizeGB SCSIController SCSITarget CurrentDriveLetter
--       --------                --------                         --------    ---------- -------------- ---------- ------------------
SERVER01 SERVER01.DOMAIN.COM 	[DTS001] SERVER01/SERVER01_3.vmdk Hard disk 1         40              0          0 C:
SERVER01 SERVER01.DOMAIN.COM 	[DTS001] SERVER01/SERVER01.vmdk   Hard disk 2          5              0          1 Q:
SERVER02 SERVER02.DOMAIN.COM 	[DTS002] SERVER02/SERVER02.vmdk   Hard disk 1         60              0          0 C:
SERVER02 SERVER02.DOMAIN.COM 	[DTS002] SERVER02/SERVER02_1.vmdk Hard disk 2          5              0          1 Q:

I need help to modify the script to, based on the CSV information, change the drive letter for the matching VMDK disk (assuming it is different). Just to reiterate, the CSV has the “correct” drive letter in the “DriveLetter” column.

I know something like this can take a current drive letter in the Windows guest and assign a new one, but I’m not sure how to fit this into my script at this point:

Get-WmiObject -Class Win32_Volume -ComputerName $VM1 -Filter "DriveLetter='$oldletter'" | Set-WmiInstance -Arguments @{DriveLetter=$newletter}

I’m just not sure how to feed the matched information from the script above into the $oldletter variable and the CSV information into the $newletter variable.

Any assistance in finishing this off would be greatly appreciated.

Drew

Drew,

So, I updated the format and noted that you are missing a lot of basic error handling. I added some and noted some other places you should definently have try\catch blocks:

# Add Snap-in for VMware PowerCLI
Add-PSSnapin VMware.VimAutomation.Core | Out-Null

try {
    # Prompt for vCenter and CSV input file details
    $vCenter = Read-Host "vCenter Name or IP"
    Connect-VIServer -Server $vCenter | Out-Null
}
catch {
    "Failed to connect to server {0}. {1}" -f $_
}

# Import list of VMs from input file
$CSVLocation = Read-Host "Full path to .csv file"
if (Test-Path -Path $CSVLocation) {
    $CSV = Import-Csv $CSVLocation | sort VM -Unique
}
else {
    "{0} was not found.  Check the path and file name and try again" -f $CSVLocation
}

# Loop through VMs from CSV and match VMDKs to Windows drives
$results = foreach($computer in $CSV) {
    $computer = $computer.VM
    #Should be wrapped with a try\catch, especially connecting to a remote system
    $VMView = Get-VM -Name $computer | Get-View
    #Should be wrapped with a try\catch, especially connecting to a remote system
    $ServerDiskToVolume = foreach ($Disk in (Get-WmiObject -Class Win32_DiskDrive -ComputerName $computer)) {
        $query = "ASSOCIATORS OF {Win32_DiskDrive.DeviceID='$($Disk.DeviceID)'} WHERE ResultClass=Win32_DiskPartition" 
        #Should be wrapped with a try\catch, especially connecting to a remote system

        foreach ($Partition in (Get-WmiObject -Query $query -ComputerName $computer)) { 

            $query = "ASSOCIATORS OF {Win32_DiskPartition.DeviceID='$($Partition.DeviceID)'} WHERE ResultClass=Win32_LogicalDisk" 
            #Should be wrapped with a try\catch, especially connecting to a remote system
            Get-WmiObject -Query $query -ComputerName $computer | 
            Select DeviceID,
                    VolumeName,
                    @{ label = "SCSITarget"; expression = {$Disk.SCSITargetId} },
                    @{ label = "SCSIBus"; expression = {$Disk.SCSIBus} }
        } #foreach $partition
    } #foreach $Disk


    # Loop through all the SCSI controllers on the VM and find those that match the Controller and Target
    foreach ($VirtualSCSIController in ($VMView.Config.Hardware.Device | Where {$_.DeviceInfo.Label -match "SCSI Controller"})){
        foreach ($VirtualDiskDevice in ($VMView.Config.Hardware.Device | Where {$_.ControllerKey -eq $VirtualSCSIController.Key})){
            #Match up the VM to a logical disk
            $MatchingDisk = @( $ServerDiskToVolume | Where {$_.SCSITarget -eq $VirtualDiskDevice.UnitNumber -and $_.SCSIBus -eq $VirtualSCSIController.BusNumber} )

            #Build a custom object to hold results
            [pscustomobject]@{
                VM = $VMView.Name
                HostName = $VMView.Guest.HostName
                DiskFile = $VirtualDiskDevice.Backing.FileName
                DiskName = $VirtualDiskDevice.DeviceInfo.Label
                DiskSizeGB = [math]::Round($VirtualDiskDevice.CapacityInKB/1024KB,0)
                SCSIController = $VirtualSCSIController.BusNumber
                SCSITarget = $VirtualDiskDevice.UnitNumber
                CurrentDriveLetter = $MatchingDisk.DeviceID
            }
        } #foreach $VirtualDiskDevice
    } #foreach $VirtualSCSIController
}

# Output results to console
$results | Format-Table -AutoSize

# Disconnect from vCenter
Disconnect-VIServer -Server $vCenter -Confirm:$false

My first recommendation is you should really have your collection (e.g. Get) of information in a function. It sounds like you want to gather data and then based on that information, if the drive letters don’t match then Set something. Your logic would be something like (sudo code):

$csv = Import-CSV C:\MyCSV

$results = foreach ($computer in $csv) {
    Get-MyVMInformation -Computer $computer
}

foreach{ $result in $results } {
    $csvRecord = $CSV | Where{$result.DiskFile -like ("*{0}" -f $_.DiskPath)}
    if ($results.CurrentDriveLetter -ne $csvRecord.DriveLetter) {
        $result | Set-MyVMDriveLetter
    }
         
}

Granted there are a lot ways to do things, but my thoughts are you want to GET or query and then check against your CSV for distension and have another function to SET the drive letter. For the SET function, you would pass relevant information (computer, olddriveletter, newdriveletter, etc.) to the function to perform the SET operation.

Rob, thanks for the reply and the work you did on the error handling. I think you’ve summarized well what I’m trying to do on the next step. The script as-is gets the results of the current drive letter (so I don’t think any additional gets of information should be necessary), and the CSV file contains the correct drive letter.

The part I need help with is how to take that information I already have, and assign the new drive letter (if necessary). I think the logic flow should be something like this:

  1. Loop through entries from the CSV.
  2. Check if the “FullPath” field from the CSV equals the “DiskFile” field from the custom object results.
  3. If it does find a match, take the drive letter from the CSV entry and assign the new drive letter in the remote Windows guest using WMI, maybe with a scriptblock?

Any help with the code for that would be most appreciated. The pseudo-code you posted does give me some ideas but I think I’ll need some help with the function.

Anyone have any ideas on this?

This is a pretty complete shell for what you need to do. You should be able to take the code to get disk information and put it into the Get-VMDisk function. Make sure you remove the Format-Table when returning your results, it ends the pipeline. Also, consider that Get-VMDisk could very easily be the name of another cmdlet, so rename to something ambiguous.

function Get-VMDisk {
    [CmdletBinding()]
    param(
		[Parameter(Mandatory=$False,
        ValueFromPipeline=$True,
		ValueFromPipelineByPropertyName=$True,
        HelpMessage='Name of the computer to perform disk query')]
        [Alias('Name', 'CN', 'VM')]		
        [string]$ComputerName = $env:COMPUTERNAME,
		[Parameter(Mandatory=$true,
        HelpMessage='Name of the Virtual Server?')]
		[Alias('VIServer')]
        [string]$VirtualCenterName
	)
    begin {
        Write-Verbose ("Connecting to vCenter Server {0}" -f $VirtualCenterName)
    }
    process {
        $results = foreach ($computer in $ComputerName) {
            Write-Verbose ("Do something to computer {0}" -f $computer)
        }

    }
    end {
        #Mock results for testing
        $results = @()
        $results += [pscustomobject]@{
            VM                =  "SERVER01"
            HostName          =  "SERVER01.DOMAIN.COM"
            DiskFile          =  "[DTS001] SERVER01/SERVER01_3.vmdk"
            DiskName          =  "Hard disk 1"
            DiskSizeGB        =  40
            SCSIController    =  0
            SCSITarget        =  0
            CurrentDriveLetter=  "C:"
        }
        $results += [pscustomobject]@{
            VM                =  "SERVER01"
            HostName          =  "SERVER01.DOMAIN.COM"
            DiskFile          =  "[DTS001] SERVER01/SERVER01.vmdk"
            DiskName          =  "Hard disk 2"
            DiskSizeGB        =  5
            SCSIController    =  0
            SCSITarget        =  1
            CurrentDriveLetter=  "Q:"
        }
        $results += [pscustomobject]@{
            VM                =  "SERVER02"
            HostName          =  "SERVER02.DOMAIN.COM"
            DiskFile          =  "[DTS002] SERVER02/SERVER02.vmdk"
            DiskName          =  "Hard disk 1"
            DiskSizeGB        =  60
            SCSIController    =  0
            SCSITarget        =  0
            CurrentDriveLetter=  "C:"
        }
        $results += [pscustomobject]@{
            VM                =  "SERVER02"
            HostName          =  "SERVER02.DOMAIN.COM"
            DiskFile          =  "[DTS002] SERVER02/SERVER02_1.vmdk"
            DiskName          =  "Hard disk 2"
            DiskSizeGB        =  5
            SCSIController    =  0
            SCSITarget        =  1
            CurrentDriveLetter=  "Q:"
        }

 
 
        Write-Verbose ("Disconnecting from vCenter Server {0}" -f $VirtualCenterName)
        $results
        
    }

}

function Set-VMDiskLetter {
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]
    param(
		[Parameter(Mandatory=$true,
		ValueFromPipelineByPropertyName=$True,
        HelpMessage='Name of the computer to perform disk query')]
        [Alias('Name', 'CN', 'VM')]		
        [string]$ComputerName,
		[Parameter(Mandatory=$true,
        ValueFromPipelineByPropertyName=$True,
        HelpMessage='Name of the Virtual Server?')]
		[Alias('DriveLetter','DL')]
        [string]$CurrentDriveLetter,
		[Parameter(Mandatory=$true,
        ValueFromPipelineByPropertyName=$True,
        HelpMessage='Name of the Virtual Server?')]
		[Alias('TDL')]
        [string]$TargetDriveLetter
	)
    begin{}
    process {
        #Add a colon if it's missing
        if ($CurrentDriveLetter -notlike "*:"){$CurrentDriveLetter = "{0}:" -f $CurrentDriveLetter}
        if ($TargetDriveLetter -notlike "*:"){$TargetDriveLetter = "{0}:" -f $TargetDriveLetter}

        if ($Pscmdlet.ShouldProcess($ComputerName,"Updating drive letter $CurrentDriveLetter to $TargetDriveLetter")) {
            try {
                Get-WmiObject -Class Win32_Volume -ComputerName $ComputerName -Filter "DriveLetter='$CurrentDriveLetter'" -ErrorAction Stop| 
                Set-WmiInstance -Arguments @{DriveLetter=$TargetDriveLetter} -ErrorAction Stop
            }
            catch {
                $msg = "{0} - Unable to update drive {1} to {2} on {3}. {4}" -f $MyInvocation.MyCommand.Name, $CurrentDriveLetter, $TargetDriveLetter, $ComputerName, $_
                Throw $msg
            }
        }
    }
    end {} 
       
}


#Import CSV
$csv = Import-CSV C:\Users\Rob\Desktop\Archive\test.csv
#Collect disk information from server list
$disks = $csv | Get-VMDisk -VirtualCenterName "10.0.0.1" -Verbose
#Take the final results and use a calculated expression to do a lookup on the CSV for matching disk paths
$finalDisks = $disks | Select *, @{Name="TargetDriveLetter";Expression={$diskFile = $_.DiskFile; $CSV | Where{$_.FullPath -eq $diskFile} | Select -ExpandProperty DriveLetter}}
#Pass the required information to the Set-VMDiskLetter function if TargetDriveLetter is not null
$finalDisks | Where{$_.TargetDriveLetter} | Set-VMDiskLetter -WhatIf

Output:

VERBOSE: Connecting to vCenter Server 10.0.0.1
VERBOSE: Do something to computer SERVER01
VERBOSE: Do something to computer SERVER02
VERBOSE: Disconnecting from vCenter Server 10.0.0.1
What if: Performing the operation "Updating drive letter Q: to T:" on target "SERVER01".
What if: Performing the operation "Updating drive letter Q: to T:" on target "SERVER02".

I’ve been reviewing the last post and I’m not seeing how to fit that code in with what I have.