PowerCLI

 View Only

 Storage vMotion script for large virtual machines (Configure per disk)

Jump to  Best Answer
James Dougherty's profile image
James Dougherty posted Sep 30, 2024 12:42 AM

We have multiple virtual machines to migrate where the VMDK's need to be placed on separate datastores.  Through the vCenter interface we can manually migrate the VMDK's to different datastores using the "configure per disk" option.  Is there a way to do this with PowerCli?  I'm guessing VMware.Vim.VirtualMachineRelocateSpec would be used?  Any examples or input appreciated.

LucD's profile image
LucD  Best Answer

Did you try with Code Capture?
That will give you the underlying code for your action in the Web Client.

Note, that the returned code will need some tuning before it can be used in a general setting.

James Dougherty's profile image
James Dougherty

Thank you!  I used Code Capture and after messing with it for an hour or so I have a great solution!!

LostInScripting's profile image
LostInScripting

Can you please share your solution you found with code capture?

James Dougherty's profile image
James Dougherty

This is still a work in progress.  The first section creates a migrate plan which shows where each VMDK will be migrated to.

$VMList = "VMName" #VM to Migrate
$DestCluster = "Destination Cluster Name"
$DSPercentFree = 10 #The percent needed free on each datastore

#Check to see if a task is already running for the vm
$Tasks = Get-task | Where {$_.extensiondata.info.entityname -eq $VMList -and $_.PercentComplete -ne "100"}
If ($Tasks) {
    Write-host "There is a migrate task for this vm already" -ForegroundColor Red
    Break
}

$DoNotUseDS = "Datastore 1|Datastore 2|Datastore 3|Datastore 4" #Destination Datastores to exclude
$VM = Get-vm -name $VMlist
$VMDKs = $VM | Get-HardDisk
$DestCluster = get-cluster $DestCluster
$Datastores = $DestCluster | get-datastore | where-object {$_.name -notmatch $DoNotUseDS -and $_.extensiondata.summary.MultipleHostAccess -eq "True"} | sort FreeSpaceGB -Descending # Datastore with the most freespace
$DestHost = $DestCluster | Get-vmhost | where {$_.ConnectionState -eq "Connected"} | sort MemoryUsageGB | select -first 1 #Host with lowest memory usage

$DestResourcePool = ($DestCluster | get-resourcepool).ID

$MigratePlan = @()
$DSCounter = 0
$VMDKCount = 0
$ArrayCount = 0
$TotalVMDK = 0

#Create the migrate plan for each VMDK to Datastore
$MigratePlan = @()
Foreach ($VMDK in $VMDKs) {
    $DSSelected = $Datastores[$DSCounter]
    $NeededFreeSpaceDS = $DSSelected.CapacityGB / $DSPercentFree
    $TotalVMDK = $TotalVMDK + $VMDK.CapacityGB
    #$DestDSAvailable = $DestCluster | Get-datastore -name $DSSelected.name

    If ($VMDK.CapacityGB -lt $DSSelected.FreeSpaceGB){
        $MigratePlan += [pscustomobject] @{
            VM = $VMDK.parent;
            Item = $VMDKCount+1;
            DiskID = ($VMDK.Id).split("/")[1]
            VMDKCapacity = $VMDK.CapacityGB;
            DestinationDS = $DSSelected.name;
            DSFreeSpace = [math]::Round($Datastores[$DSCounter].FreeSpaceGB - $TotalVMDK);
            DSID = $DSSelected.Id -creplace "Datastore-";
            DestHost = ($DestHost.name).split(".")[0];
            DestHostID = $DestHost.Id -creplace "HostSystem-";
            DestCluster = $DestCluster.name;
            DestResourcePoool = $DestResourcePool -creplace "ResourcePool-"           
        }
    }
    If ($VMDKs[$VMDKCount + 1].CapacityGB + $TotalVMDK + $NeededFreeSpaceDS -gt $MigratePlan[$VMDKCount].DSFreeSpace){
        $DSCounter ++
        $TotalVMDK = 0
        
    }
    $ArrayCount ++
    $VMDKCount++
}
$MigratePlan | ft

Pause 
$VMDiskCount = $MigratePlan.Count


#Start Creating The Migration Job
$spec = New-Object VMware.Vim.VirtualMachineRelocateSpec
$spec.Disk = New-Object VMware.Vim.VirtualMachineRelocateSpecDiskLocator[] ($VMDiskCount)

#Loop Through each disk
$XCounter = 0
Foreach ($Migrate in $MigratePlan) {
    $spec.Disk[$XCounter] = New-Object VMware.Vim.VirtualMachineRelocateSpecDiskLocator
    $spec.Disk[$XCounter].Datastore = New-Object VMware.Vim.ManagedObjectReference
    $spec.Disk[$XCounter].Datastore.Type = 'Datastore'
    $spec.Disk[$XCounter].Datastore.Value = $Migrate.DSID
    $spec.Disk[$XCounter].DiskId = $Migrate.DiskID
    $XCounter ++
}
    $spec.Datastore = New-Object VMware.Vim.ManagedObjectReference
    $spec.Datastore.Type = 'Datastore'
    $spec.Datastore.Value = $MigratePlan.DSID[0]
    $spec.Host = New-Object VMware.Vim.ManagedObjectReference
    $spec.Host.Type = 'HostSystem'
    $spec.Host.Value = $MigratePlan.DestHostID[0]
    $spec.Pool = New-Object VMware.Vim.ManagedObjectReference
    $spec.Pool.Type = 'ResourcePool'
    $spec.Pool.Value = $MigratePlan.DestResourcePoool[0]
    $priority = 'highPriority'
    $_this = get-view -viewtype VirtualMachine -filter @{"Name"=$VMlist}
    $_this.RelocateVM_Task($spec, $priority)

#Display the migration task on screen
    $Tasks = Get-task | Where {$_.extensiondata.info.entityname -eq $VMList -and $_.PercentComplete -ne "100"} | select Name,State,PercentComplete,StartTime,FinishTime,@{n="VM"; e={$_.extensiondata.info.entityname}}
If ($Tasks.Name.count -gt "0"){
    DO
        {
        $Tasks = Get-task | Where {$_.extensiondata.info.entityname -eq $VMList -and $_.PercentComplete -ne "100"} | select Name,
        State,
        PercentComplete,
        StartTime,
        FinishTime,
        @{n="VM"; e={$_.extensiondata.info.entityname}},
        @{n="CurrentTime"; e={Get-Date -Format HH:mm}}

        $Tasks | ft
        $Tasks.count
        
        Sleep 5
    } While ($Tasks.count -ne 0)
}

#Check for task error
$TaskError = Get-task | Where {$_.extensiondata.info.entityname -eq $VMList}
If ($TaskError) {
    Write-host "The task for $($VMlist) failed" -ForegroundColor red
    Write-host "Gathering the error, this will take some time"
    $MigrateError = Get-VIevent -Types error -Maxsamples 1 -Entity $VMlist
    Write-host $MigrateError.FullFormattedMessage
    Write-host $MigrateError.Reason.LocalizedMessage
}



tbrock47's profile image
tbrock47

Here is my take on an operation like this that I have used to move multi-TB SQL servers on clusters that have multiple storage arrays separated using datastore clusters. It also relocates the config file to the same datastore as "Hard disk 1".

Adapt it as you like. It has some specific use case coding for my use.

####################
# NEW BY DISK METHOD
$ScriptBlock = {
    Function Move-VMConfig {
        Param(
            [Parameter(Mandatory, ValueFromPipeline)][VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine]$VM,
            [Parameter(Mandatory)][String]$DatastoreName
        )
        $hds = Get-HardDisk -VM $VM
        $spec = New-Object VMware.Vim.VirtualMachineRelocateSpec
        $spec.datastore = (Get-Datastore -Name $DatastoreName).Extensiondata.MoRef
        $hds | ForEach-Object {
            $disk = New-Object VMware.Vim.VirtualMachineRelocateSpecDiskLocator
            $disk.diskId = $_.Extensiondata.Key
            $disk.datastore = $_.Extensiondata.Backing.Datastore
            $spec.disk += $disk
        }
        $VM.Extensiondata.RelocateVM_Task($spec, 'defaultPriority')
    }
    $definitions = [PSCustomObject]@{
        vCenterName   = 'louvcslpa30'
        SourceDSCName = 'DSC01'
        DestDSCName   = 'DCS02'
        MinVMSizeGB   = (4 * 1024)
        VMName_regex  = 'W[BPS][CLM]\d+S[0-9][2468]$'
    }
    $Host.UI.RawUI.WindowTitle = $definitions.DestDSCName
    Import-Module -Name VMware.PowerCLI -Force -EA Ignore
    $VIServer = Connect-VIServer -Server $definitions.vCenterName -Credential $ViCredential
    $SourceDSC = Get-DatastoreCluster -Name $definitions.SourceDSCName
    $DestDSC = Get-DatastoreCluster -Name $definitions.DestDSCName
    $DatastoreList = $DestDSC | Get-Datastore -Refresh
    do {
        $VM = $SourceDSC | Get-VM | `
            Where-Object Name -Match $definitions.VMName_regex | `
            Where-Object Name -NotMatch '^Z-VRA' | `
            Where-Object ProvisionedSpaceGB -GE $definitions.MinVMSizeGB | `
            Sort-Object ProvisionedSpaceGB -Descending | Select-Object -First 1
        if (-not $VM) { break }
        Write-Host "[$(Get-Date -Format s)] $($VM.Name)"
        # Move all disks excluding disks on "PURNAS" arrays
        if ($HDs = $VM | Get-HardDisk | Where-Object { $_.Filename -notmatch 'PURNAS' -and ($_.ExtensionData.Backing.Datastore | Get-VIObjectByVIView) -notin $DatastoreList } | Sort-Object CapacityGB -Descending) {
            $Datastore = $DestDSC | Get-Datastore | Sort-Object FreeSpaceGB -Descending | Select-Object -First 1
            #get most disks that can be moved at once
            $DiskGroup = @()
            $HDs | ForEach-Object {
                #Add disks to the group until the total exceeds 15% free space on the destination datastore
                if ((Measure-Object -InputObject [decimal]($DiskGroup.CapacityGB + $_.CapacityGB) -Sum).Sum -lt ($Datastore.CapacityGB * 0.75)) {
                    $DiskGroup += $_
                } else { break }
            }
            Move-HardDisk -HardDisk $DiskGroup -Datastore $Datastore -Confirm:$false
        } elseif (-not($VM | Get-HardDisk | Where-Object { $_.Filename -notmatch 'PURNAS' -and ($_.ExtensionData.Backing.Datastore | Get-VIObjectByVIView) -notin $DatastoreList })) {
            Move-VMConfig -VM $VM -DatastoreName ($VM | Get-HardDisk -Name 'Hard Disk 1' | Get-Datastore).Name | Out-Null
        }
        $DestDSC | Get-Datastore -Refresh | Out-Null
        # Start-Sleep -Seconds 10
        $DestDSC = Get-DatastoreCluster -Name $DestDSC.Name
    } while (
        $DestDSC.FreeSpaceGB / $DestDSC.CapacityGB -gt 0.10
    )
    $VIServer | Disconnect-VIServer -Force -Confirm:$false
    Pause
}
Start-Process pwsh -ArgumentList "-Command $ScriptBlock"
return