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

This topic contains 5 replies, has 2 voices, and was last updated by Profile photo of Drew AZ Drew AZ 2 months, 2 weeks ago.

  • Author
    Posts
  • #51976
    Profile photo of Drew AZ
    Drew AZ
    Participant

    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

  • #52126
    Profile photo of Rob Simmers
    Rob Simmers
    Participant

    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.

  • #52190
    Profile photo of Drew AZ
    Drew AZ
    Participant

    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.

  • #53281
    Profile photo of Drew AZ
    Drew AZ
    Participant

    Anyone have any ideas on this?

  • #53380
    Profile photo of Rob Simmers
    Rob Simmers
    Participant

    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".
    
  • #54342
    Profile photo of Drew AZ
    Drew AZ
    Participant

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

You must be logged in to reply to this topic.