I recently came across a customer who had many applications running in clusters which required RDM’s and wanted to automate the process of attaching and sharing the RDM’s between multiple Virtual Machines. PowerCLI being the preferred method for the customer to automate anything, I started out by mapping the steps which had to be performed to successfully attach and share an RDM.

  • Find the available free ports on the SCSI Controller and add a new SCSI Controller if required.
  • Create a custom object to hold all the information about the virtual machine, RDM and SCSI Controller Bus and Port Numbers being used.
  • Use the information captured to attach the RDM on the first Virtual Machine.
  • Capture the new Disk information and share the same device to other Virtual Machines.

(Note : All the functions mentioned below have been written with the assumption that all Virtual Machines are identical in terms of existing storage mapped.)

 

  • First thing first – Setup the parameters for the script call.
    1. PrimaryVirtualMachineName – VM on which the RDM will be added initially.
    2. SecondaryVirtualMachinesName – Comma separated virtual machine names of VM’s with which RDM is to be shared.
    3. PathtoRDMfile – Path to the file containing list of RDM WWN’s.

 

param(

        $PrimaryVirtualMachineName,

        $SecondaryVirtualMachinesName = @(),

        $PathtoRDMfile

)

 

  • Now we will create a function which will create a custom RDM object to hold all the information which is required to successfully attach and share an RDM. This is not actually required, but makes if easier to hold all the required information in a single place and makes it easier to retrieve it when required.

 

function GetVMCustomObject {

param (

        $VirtualMachine,

        $RDMS

)  

$ESXCLI = $VirtualMachine | get-vmhost | Get-EsxCli -V2

$devobject = @()

foreach($RDM in $RDMS)

{

       

        $RDM = 'naa.'+$RDM

        $Parameters = $ESXCLI.storage.core.device.list.CreateArgs()

        $Parameters.device = $RDM.ToLower()

        try{

        $naa=$ESXCLI.storage.core.device.list.Invoke($Parameters)

        write-host found device $naa.device

        $device = New-Object psobject

        $device | add-member -MemberType NoteProperty -name "NAAID" -Value $naa.Device

        $device | add-member -MemberType NoteProperty -name "SizeMB" -Value $naa.Size

        $device | add-member -MemberType NoteProperty -name "DeviceName" -Value $naa.devfspath

        $device | Add-Member -MemberType NoteProperty -name "BusNumber" -Value $null

        $device | add-member -MemberType NoteProperty -name "UnitNumber" -value $null

        #$device | Add-Member -MemberType NoteProperty -Name "Device Key" -Value $null

        $device | add-member -MemberType NoteProperty -name "FileName" -Value $null

        $devobject += $device

 

}

catch

    {

        Write-host $RDM does not exist on host (get-vmhost -vm $VirtualMachine)

        Read-Host "Press any key to exit the Script."

        Exit

}

}

return $devobject

}

 

  • Next up is the function to create a new SCSI controller if required.

 

function CreateScSiController {

param (

        [int]$BusNumber,

        $VirtualMachine

)

$spec = New-Object VMware.Vim.VirtualMachineConfigSpec

$spec.DeviceChange = @()

$spec.DeviceChange += New-Object VMware.Vim.VirtualDeviceConfigSpec

$spec.DeviceChange[0].Device = New-Object VMware.Vim.ParaVirtualSCSIController

$spec.DeviceChange[0].Device.SharedBus = 'physicalSharing'

$spec.DeviceChange[0].Device.ScsiCtlrUnitNumber = 7

$spec.DeviceChange[0].Device.DeviceInfo = New-Object VMware.Vim.Description

$spec.DeviceChange[0].Device.DeviceInfo.Summary = 'New SCSI controller'

$spec.DeviceChange[0].Device.DeviceInfo.Label = 'New SCSI controller'

$spec.DeviceChange[0].Device.Key = -106

$spec.DeviceChange[0].Device.BusNumber = $BusNumber

$spec.DeviceChange[0].Operation = 'add'

$VirtualMachine.ExtensionData.ReconfigVM($spec)

}

 

 

  • Next we will query the existing SCSI controller attached and find the available free ports on the existing SCSI controller and use the function last created to add a new SCSI controller if required.

Note : This function has been written to always start with SCSI controller with highest Bus Number but could be easily modified to use any of the existing controllers.

 

function SCSiFreePorts {

param (

        #Required ports is RDMS.count

$RequiredPorts,

        $PrimaryVirtualMachine,

        $SecondaryVirtualMachines

)

 

$ControllertoUse = @()

$FreePorts = 0;

$AvailablePorts = @()

while ($FreePorts -lt $RequiredPorts) {

        $ControllerNumber = @()

        $Controllers = Get-ScsiController -vm $PrimaryVirtualMachine? {$_.BusSharingMode -eq 'Physical' -and $_.Type -eq 'paravirtual'}

        $LatestControllerNumber = $null

if ($Controllers) {

            foreach ($Controller in $Controllers) {

$ControllerNumber += $Controller.ExtensionData.BusNumber

            }

            $LatestControllerNumber = ($ControllerNumber | measure -Maximum).Maximum

$RecentController = $Controllers | ? {$_.ExtensionData.BusNumber -eq $LatestControllerNumber}

            $FreePorts += 15 - $RecentController.ExtensionData.Device.count

            $ControllertoUse += $RecentController

        }

        if (($FreePorts -lt $RequiredPorts) -and ($LatestControllerNumber -eq 3)) {

            Write-Host "SCSI controller Limit has been exhausted and can not accomodate all RDM's. Exiting the Script."

            Exit

        }

        if (($FreePorts -lt $RequiredPorts) -or !$Controllers) {

            CreateScSiController -BusNumber ($LatestControllerNumber+1) -VirtualMachine $PrimaryVirtualMachine

            foreach($Virtualmachine in $SecondaryVirtualMachines)

            {

                CreateScSiController -BusNumber ($LatestControllerNumber+1) -VirtualMachine $Virtualmachine

            }

}

}

foreach ($CurrentController in $ControllertoUse) {

        $ConnectedDevices = $CurrentController.ExtensionData.Device

        $UsedPort = @()

        foreach ($Device in $ConnectedDevices) {

            $DevObj = $PrimaryVirtualMachine.ExtensionData.Config.Hardware.Device | ? {$_.Key -eq $Device}

            $UsedPort += $DevObj.UnitNumber

        }

        for ($i = 0; $i -le 15; $i++) {

            if (($i -ne 7) -and ($UsedPort -notcontains $i)) {

                $PortInfo = New-Object -TypeName PSObject

                $PortInfo | Add-Member -MemberType NoteProperty -name "BusNumber" -Value $CurrentController.ExtensionData.BusNumber

                $PortInfo | add-member -MemberType NoteProperty -name "PortNumber" -value $i

                $AvailablePorts += $PortInfo

            }

        }

}

return $AvailablePorts

}

 

  • Now the function to add the RDM to the shared machine.

 

function AddRDM {

param (

        $VirtualMachine,

        [String]$DeviceName,

[Int]$ControllerKey,

        [Int]$UnitNumber,

        [Int]$Size

)

$spec = New-Object VMware.Vim.VirtualMachineConfigSpec

$spec.DeviceChange = @()

$spec.DeviceChange += New-Object VMware.Vim.VirtualDeviceConfigSpec

$spec.DeviceChange[0].FileOperation = 'create'

$spec.DeviceChange[0].Device = New-Object VMware.Vim.VirtualDisk

# $SIZE is available in objects returned by GetVMCustomObject, size will be in MB

$spec.DeviceChange[0].Device.CapacityInBytes = $Size*1204*1024

$spec.DeviceChange[0].Device.StorageIOAllocation = New-Object VMware.Vim.StorageIOAllocationInfo

$spec.DeviceChange[0].Device.StorageIOAllocation.Shares = New-Object VMware.Vim.SharesInfo

$spec.DeviceChange[0].Device.StorageIOAllocation.Shares.Shares = 1000

$spec.DeviceChange[0].Device.StorageIOAllocation.Shares.Level = 'normal'

$spec.DeviceChange[0].Device.StorageIOAllocation.Limit = -1

$spec.DeviceChange[0].Device.Backing = New-Object VMware.Vim.VirtualDiskRawDiskMappingVer1BackingInfo

$spec.DeviceChange[0].Device.Backing.CompatibilityMode = 'physicalMode'

$spec.DeviceChange[0].Device.Backing.FileName = ''

$spec.DeviceChange[0].Device.Backing.DiskMode = 'independent_persistent'

$spec.DeviceChange[0].Device.Backing.Sharing = 'sharingMultiWriter'

#Device name is in the format /vmfs/devices/disks/naa.<LUN ID>

$spec.DeviceChange[0].Device.Backing.DeviceName = $DeviceName

#Controller key to be retrieved at run time using controller bus number

$spec.DeviceChange[0].Device.ControllerKey = $ControllerKey

#Unit number is the controller port and will be provided by SCSiFreePorts function

$spec.DeviceChange[0].Device.UnitNumber = $UnitNumber

# $SIZE is available in objects returned by GetVMCustomObject, size will be in MB

$spec.DeviceChange[0].Device.CapacityInKB = $Size*1204

$spec.DeviceChange[0].Device.DeviceInfo = New-Object VMware.Vim.Description

$spec.DeviceChange[0].Device.DeviceInfo.Summary = 'New Hard disk'

$spec.DeviceChange[0].Device.DeviceInfo.Label = 'New Hard disk'

$spec.DeviceChange[0].Device.Key = -101

$spec.DeviceChange[0].Operation = 'add'

return $VirtualMachine.ExtensionData.ReconfigVM_Task($spec)

}

 

  • To share the RDM between Virtual Machines, we will use below function.

 

function ShareRDM {

param (

        $VirtualMachine,

        [String]$FileName,

        [Int]$ControllerKey,

        [Int]$UnitNumber,

        [Int]$Size

)

$spec = New-Object VMware.Vim.VirtualMachineConfigSpec

$spec.DeviceChange = @()

$spec.DeviceChange += New-Object VMware.Vim.VirtualDeviceConfigSpec

$spec.DeviceChange[0] = New-Object VMware.Vim.VirtualDeviceConfigSpec

$spec.DeviceChange[0].Device = New-Object VMware.Vim.VirtualDisk

# $SIZE is available in objects returned by GetVMCustomObject, size will be in MB

$spec.DeviceChange[0].Device.CapacityInBytes = $Size*1204*1024*1024

$spec.DeviceChange[0].Device.StorageIOAllocation = New-Object VMware.Vim.StorageIOAllocationInfo

$spec.DeviceChange[0].Device.StorageIOAllocation.Shares = New-Object VMware.Vim.SharesInfo

$spec.DeviceChange[0].Device.StorageIOAllocation.Shares.Shares = 1000

$spec.DeviceChange[0].Device.StorageIOAllocation.Shares.Level = 'normal'

$spec.DeviceChange[0].Device.StorageIOAllocation.Limit = -1

$spec.DeviceChange[0].Device.Backing = New-Object VMware.Vim.VirtualDiskRawDiskMappingVer1BackingInfo

#FileName is the disk filename to be shared in [<Datastore name>] VM Name/disk name.vmdk, to be retrieved at runtime using vm view and device bus number and Unit number

$spec.DeviceChange[0].Device.Backing.FileName = $FileName

$spec.DeviceChange[0].Device.Backing.DiskMode = 'persistent'

$spec.DeviceChange[0].Device.Backing.Sharing = 'sharingMultiWriter'

#Controller key to be retrieved at run time using controller bus number

$spec.DeviceChange[0].Device.ControllerKey = $ControllerKey

#Unit number is the controller port and will be provided by SCSiFreePorts function

$spec.DeviceChange[0].Device.UnitNumber = $UnitNumber

# $SIZE is available in objects returned by GetVMCustomObject, size will be in MB

$spec.DeviceChange[0].Device.CapacityInKB = $Size*1204*1024

$spec.DeviceChange[0].Device.DeviceInfo = New-Object VMware.Vim.Description

$spec.DeviceChange[0].Device.DeviceInfo.Summary = 'New Hard disk'

$spec.DeviceChange[0].Device.DeviceInfo.Label = 'New Hard disk'

$spec.DeviceChange[0].Device.Key = -101

$spec.DeviceChange[0].Operation = 'add'

return $VirtualMachine.ExtensionData.ReconfigVM_Task($spec)

}

 

To stitch it all together we just have to call the created functions as and when required, but a little pre-checks first.

Note : These pre-checks are not exhaustive, these were built in to satisfy customer specific requirements, more checks and balances could be added.

 

Now we do not want to just start modifying things without making sure that the virtual machines are powered off, do we?

 

$PrimaryVirtualMachine = Get-VM -Name $PrimaryVirtualMachineName

if($PrimaryVirtualMachine.PowerState -ne 'PoweredOff')

{

Read-Host -Prompt $PrimaryVirtualMachineName' is not Powered Off. Make sure all the Virtual Machines are Powered Off before running the script again. Press any key to exit.'

Exit

}

$SecondaryVirtualMachines = @()

foreach($VM in $SecondaryVirtualMachinesName)

{

$SecondaryVM = Get-VM -name $VM

if($SecondaryVM.PowerState -ne 'PoweredOff')

{

Read-Host -Prompt $VM' is not Powered Off. Make sure all the Virtual Machines are Powered Off before running the script again. Press any key to exit.'

Exit

}

$SecondaryVirtualMachines += $SecondaryVM

}

 

We also know that Virtual Machines support a total of 64  disks out of which 4 are IDE’s, so we will check if we have enough ports available to successfully attach all the given RDM’s.

$RDMS = Get-Content -path $PathtoRDMfile

$AttachedDisks = $PrimaryVirtualMachine | Get-HardDisk

if(($AttachedDisks.Count+$RDMS.count) -gt 60)

{

Read-Host -Prompt 'Configuration maximum for disks reached. Can not attach all provided disks. Press any key to exit.'

exit

}

 

Lets find out the Bus Numbers and Port Number that we will use to attach the RDM’s

 

$DeviceObjects = GetVMCustomObject -VirtualMachine $PrimaryVirtualMachine -RDMS $RDMS

$PortsAvailable = SCSiFreePorts -RequiredPorts $RDMS.Count -PrimaryVirtualMachine $PrimaryVirtualMachine -SecondaryVirtualMachines $SecondaryVirtualMachines

 

for($i = 0; $i -lt $RDMS.Count; $i++)

{

$CurrentObject = $DeviceObjects[$i]

$PorttoUse = $PortsAvailable[$i]

$CurrentObject.UnitNumber = $PorttoUse.PortNumber

$CurrentObject.BusNumber = $PorttoUse.BusNumber

}

 

Now we will use all this collected information and finish the job.

 

foreach($DiskObject in $DeviceObjects)

{

$Controller = Get-ScsiController -VM $PrimaryVirtualMachine | ? {$_.ExtensionData.BusNumber -eq $DiskObject.BusNumber}

$task = AddRDM -VirtualMachine $PrimaryVirtualMachine -DeviceName $DiskObject.DeviceName -ControllerKey $Controller.ExtensionData.Key -UnitNumber $DiskObject.UnitNumber -Size $DiskObject.SizeMB

Start-Sleep -Seconds 5

$PVM = Get-VM -Name $PrimaryVirtualMachineName

$Disk = $PVM.ExtensionData.Config.Hardware.Device | ? {($_.UnitNumber -eq $DiskObject.UnitNumber) -and ($_.ControllerKey -eq $Controller.ExtensionData.Key)}

$DiskObject.FileName = $Disk.Backing.FileName

foreach($VM in $SecondaryVirtualMachines)

{

        $SController = Get-ScsiController -VM $PrimaryVirtualMachine | ? {$_.ExtensionData.BusNumber -eq $DiskObject.BusNumber}

        ShareRDM -VirtualMachine $VM -FileName $Disk.Backing.FileName -ControllerKey $SController.ExtensionData.Key -UnitNumber $DiskObject.UnitNumber -Size $DiskObject.SizeMB

 

}

 

}

Write-Host "RDM's have been added on All VirtualMachines with Below Details"

Write-Host $DeviceObjects | Select NAAID,BusNumber,UnitNumber

 

Now just save the file and run is as below -

 

<path the script><scriptname.ps1> -PrimaryVirtualMachineName <VM Name> -SecondaryVirtualMachinesName  <VM Name>,<VM Name>,<VM Name> -PathtoRDMfile  <RDM File Path>

 

This script has been tested in Lab with 1 Primary and 2 Secondary Virtual Machines and upto 10 RDM devices. Below are the specific use cases tested.

 

  • RDM attachment with no existing Physical mode SCSI Controller.
  • RDM attachment with existing Physical mode SCSI Controller with no existing RDM.
  • RDM attachment with existing Physical mode SCSI controller with serially attached RDM.
  • RDM attachment with existing Physical mode SCSI controller with randomly attached RDM.
  • RDM attachment across multiple Physical mode SCSI controllers if the existing controller does not have enough ports available.

 

I understand there could be better and/or easier ways to do so, the script might also be modified to be more efficient, any suggestion is welcome, I have attached the completed script to the post, feel free to use/modify as deemed fit.