This post shares a PowerShell script to automatically live migrate clustered Hyper-V virtual machines to the host that owns the CSV that the VM is stored on. The example below should work nicely with a 2-node cluster, such as a cluster-in-a-box.
For lots of reasons, you get the best performance for VMs on a Hyper-V cluster if:
- Host X owns CSV Y AND
- The VMs that are stored on CSV Y are running on Host X.
This continues into WS2016, as we’ve seen by analysing the performance enhancements of ReFS for VHDX operations. In summary, the ODX-like enhancements work best when the CSV and VM placement are identical as above.
I wrote a script, with little bits taken from several places (scripting is the art of copy & paste), to analyse a cluster and then move virtual machines to the best location. The method of the script is:
- Move CSV ownership to what you have architected.
- Locate the VMs that need to move.
- Order that list of VMs based on RAM. I want to move the smallest VMs first in case there is memory contention.
- Live migrate VMs based on that ordered list.
What’s missing? Error handling 🙂
What do you need to do?
- You need to add variables for your CSVs and hosts.
- Modify/add lines to move CSV ownership to the required hosts.
- Balance the deployment of your VMs across your CSVs.
Here’s the script. I doubt the code is optimal, but it works. Note that the Live Migration command (Move-ClusterVirtualMachineRole) has been commented out so you can see what the script will do without it actually doing anything to your VM placement. Feel free to use, modify, etc.
#List your CSVs
$CSV1 = "CSV1"
$CSV2 = "CSV2"
#List your hosts
$CSV1Node = "Host01"
$CSV2Node = "Host02"
function ListVMs ()
{
Write-Host "`n`n`n`n`n`nAnalysing the cluster $Cluster ..."
$Cluster = Get-Cluster
$AllCSV = Get-ClusterSharedVolume -Cluster $Cluster | Sort-Object Name
$VMMigrationList = @()
ForEach ($CSV in $AllCSV)
{
$CSVVolumeInfo = $CSV | Select -Expand SharedVolumeInfo
$CSVPath = ($CSVVolumeInfo).FriendlyVolumeName
$FixedCSVPath = $CSVPath -replace '\\', '\\'
#Get the VMs where VM placement doesn't match CSV ownership
$VMsToMove = Get-ClusterGroup | ? {($_.GroupType –eq 'VirtualMachine') -and ( $_.OwnerNode -ne $CSV.OWnernode.Name)} | Get-VM | Where-object {($_.path -match $FixedCSVPath)}
#Build up a list of VMs including their memory size
ForEach ($VM in $VMsToMove)
{
$VMRAM = (Get-VM -ComputerName $VM.ComputerName -Name $VM.Name).MemoryAssigned
$VMMigrationList += ,@($VM.Name, $CSV.OWnernode.Name, $VMRAM)
}
}
#Order the VMs based on memory size, ascending
$VMMigrationList = $VMMigrationList | sort-object @{Expression={$_[2]}; Ascending=$true}
Return $VMMigrationList
}
function MoveVM ($TheVMs)
{
foreach ($VM in $TheVMs)
{
$VMName = $VM[0]
$VMDestination = $VM[1]
Write-Host "`nMove $VMName to $VMDestination"
#Move-ClusterVirtualMachineRole -Name $VMName -Node $VMDestination -MigrationType Live
}
}
cls
#Configure which node will own wich CSV
Move-ClusterSharedVolume -Name $CSV1 -Node $CSV1Node | Out-Null
Move-ClusterSharedVolume -Name $CSV2 -Node $CSV2Node | Out-Null
$SortedVMs = @{}
#Get a sorted list of VMs, ordered by assign memory
$SortedVMs = ListVMs
#Live Migrate the VMs, so that their host is also their CSV owner
MoveVM $SortedVMs
Possible improvements:
- My ListVMs algorithm probably can be improved.
- The Live Migration piece also can be improved. It only does 1 VM at a time, but you could implement parallelism using jobs.
- Quick Migration should be used for non-running VMs. I haven’t handles that situation.
- You could opt to use Quick Migration for low priority VMs – if that’s your policy.
- The script could be modified to start using parameters, e.g. Analyse (not move), QuickMigrateLow, QuickMigrate (instead of Live Migrate), etc.

