Get-VMotion.ps1
<#PSScriptInfo
.VERSION 1.2.0 .GUID e4945281-2135-4365-a194-739fcf54456b .AUTHOR Brian Bunke .DESCRIPTION Report on recent vMotion events in your VMware environment. .COMPANYNAME brianbunke .COPYRIGHT .TAGS vmware powercli vmotion vcenter .LICENSEURI https://github.com/brianbunke/vCmdlets/blob/master/LICENSE .PROJECTURI https://github.com/brianbunke/vCmdlets .ICONURI .EXTERNALMODULEDEPENDENCIES VMware.VimAutomation.Core .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 1.2.0 - 2019/05/09 - Add cluster output; replace ArrayList with Generic.List; apply ScriptAnalyzer recommendations 1.1.0 - 2017/10/24 - Support new Encrypted vMotion type in 6.5; localize time; add datacenter properties 1.0.1 - 2017/10/12 - Fix improper filtering on VCSA 6.5 1.0.0 - 2017/01/02 - Initial release #> #Requires -Version 3 -Module VMware.VimAutomation.Core function Get-VMotion { <# .SYNOPSIS Report on recent vMotion events in your VMware environment. .DESCRIPTION Use to check DRS history, or to help with troubleshooting. vMotion and Storage vMotion events are returned by default. Can filter to view only results from recent days, hours, or minutes (default is 1 day). For performance, "Get-VMotion" is good. "Get-VM | Get-VMotion" is very slow. The cmdlet gathers and parses each entity's events one by one. This means that while one VM and one datacenter will have similar speeds, a "Get-VM | Get-VMotion" that contains 50 VMs will take a while. Get-VMotion has been tested on Windows 6.0 and VCSA 6.5 vCenter servers. "Get-Help Get-VMotion -Examples" for some common usage tips. .NOTES Thanks to lucdekens/alanrenouf/sneddo for doing the hard work long ago. http://www.lucd.info/2013/03/31/get-the-vmotionsvmotion-history/ https://github.com/alanrenouf/vCheck-vSphere .EXAMPLE Get-VMotion By default, searches $global:DefaultVIServers (all open Connect-VIServer sessions). For all datacenters found by Get-Datacenter, view all s/vMotion events in the last 24 hours. VM name, vMotion type (compute/storage/both), start time, and duration are returned by default. .EXAMPLE Get-VMotion -Verbose | Format-List * For each s/vMotion event found in Example 1, show all properties instead of the default four. Verbose output tracks current progress, and helps when troubleshooting results. .EXAMPLE Get-Cluster '*arcade' | Get-VMotion -Hours 8 | Where-Object {$_.Type -eq 'vmotion'} For the cluster Flynn's Arcade, view all vMotions in the last eight hours. NOTE: Piping "Get-Datacenter" or "Get-Cluster" will be much faster than an unfiltered "Get-VM". .EXAMPLE Get-VM 'Sam' | Get-VMotion -Days 30 | Format-List * View hosts/datastores the VM "Sam" has been on in the last 30 days, when changes happened, and how long any migrations took to complete. When supplying VM objects, a generic warning displays once about result speed. .EXAMPLE $Grid = $global:DefaultVIServers | Where-Object {$_.Name -eq 'Grid'} PS C:\>Get-VM -Name 'Tron','Rinzler' | Get-VMotion -Days 7 -Server $Grid View all s/vMotion events for only VMs "Tron" and "Rinzler" in the last week. If connected to multiple servers, will only search for events on vCenter server Grid. .EXAMPLE Get-VMotion | Select-Object Name,Type,Duration | Sort-Object Duration For all s/vMotions in the last day, return only VM name, vMotion type, and total migration time. Sort all events from fastest to slowest. Selecting < 5 properties automatically formats output in a table, instead of a list. .INPUTS [VMware.VimAutomation.ViCore.Types.V1.Inventory.InventoryItem[]] PowerCLI cmdlets Get-Datacenter / Get-Cluster / Get-VM .OUTPUTS [System.Collections.ArrayList] [System.Management.Automation.PSCustomObject] [vMotion.Object] = arbitrary PSCustomObject typename, to enable default property display .LINK http://www.brianbunke.com/blog/2017/10/25/get-vmotion-65/ .LINK https://github.com/brianbunke/vCmdlets #> [CmdletBinding(DefaultParameterSetName='Days')] [OutputType([System.Collections.ArrayList])] param ( # Filter results to only the specified object(s) # Tested with datacenter, cluster, and VM entities [Parameter(ValueFromPipeline = $true)] [ValidateScript({$_.GetType().Name -match 'VirtualMachine|Cluster|Datacenter'})] [Alias('Name','VM','Cluster','Datacenter')] [VMware.VimAutomation.ViCore.Types.V1.Inventory.InventoryItem[]]$Entity, # Number of days to return results from. Defaults to 1 # Mutually exclusive from Hours, Minutes [Parameter(ParameterSetName='Days')] [ValidateRange(0,[int]::MaxValue)] [int]$Days = 1, # Number of hours to return results from # Mutually exclusive from Days, Minutes [Parameter(ParameterSetName='Hours')] [ValidateRange(0,[int]::MaxValue)] [int]$Hours, # Number of minutes to return results from # Mutually exclusive from Days, Hours [Parameter(ParameterSetName='Minutes')] [ValidateRange(0,[int]::MaxValue)] [int]$Minutes, # Specifies the vCenter Server system(s) on which you want to run the cmdlet. # If no value is passed to this parameter, the command runs on the default servers. # For more information about default servers, "Get-Help Connect-VIServer". [VMware.VimAutomation.Types.VIServer[]]$Server = $global:DefaultVIServers ) BEGIN { If (-not $Server) { throw 'Please open a vCenter session with Connect-VIServer first.' } Write-Verbose "Processing against vCenter server(s) $("'$Server'" -join ' | ')" # Based on parameter supplied, set $Time for $EventFilter below switch ($PSCmdlet.ParameterSetName) { 'Days' {$Time = (Get-Date).AddDays(-$Days).ToUniversalTime()} 'Hours' {$Time = (Get-Date).AddHours(-$Hours).ToUniversalTime()} 'Minutes' {$Time = (Get-Date).AddMinutes(-$Minutes).ToUniversalTime()} } Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)" Write-Verbose "Searching for all vMotion events since $($Time.ToLocalTime().ToString())" # Construct an empty array for events returned # Performs faster than @() when appending; matters if running against many VMs $Events = New-Object System.Collections.ArrayList # Build a vMotion-specific event filter query $EventFilter = New-Object VMware.Vim.EventFilterSpec $EventFilter.Entity = New-Object VMware.Vim.EventFilterSpecByEntity $EventFilter.Time = New-Object VMware.Vim.EventFilterSpecByTime $EventFilter.Time.BeginTime = $Time # After moving from Win 6.0 to VCSA 6.5, apparently the Category filter no longer works? # $EventFilter.Category = 'Info' $EventFilter.DisableFullMessage = $true $EventFilter.EventTypeID = @( 'com.vmware.vc.vm.VmHotMigratingWithEncryptionEvent', 'DrsVmMigratedEvent', 'VmBeingHotMigratedEvent', 'VmBeingMigratedEvent', 'VmMigratedEvent' ) } #Begin PROCESS { ForEach ($vCenter in $Server) { Write-Verbose "Searching for events in vCenter server '$vCenter'" Write-Verbose "Calling Get-View for EventManager against server '$vCenter'" $EventMgr = Get-View EventManager -Server $vCenter -Verbose:$false -Debug:$false If ($Entity) { # Acknowledge user-supplied inventory item(s) $InventoryObjects = $Entity } Else { # If -Entity was not specified, return datacenter object(s) Write-Verbose "Calling Get-Datacenter to process all objects in server '$vCenter'" $InventoryObjects = Get-Datacenter -Server $vCenter -Verbose:$false -Debug:$false } $InventoryObjects | ForEach-Object { Write-Verbose "Processing $($_.GetType().Name) inventory object $($_.Name)" # Warn once against using VMs in -Entity parameter If ($_.GetType().Name -match 'VirtualMachine' -and $null -eq $AlreadyWarned) { Write-Warning 'Get-VMotion must process VM objects one by one, which slows down results.' Write-Warning 'Consider supplying parent Cluster(s) or Datacenter(s) to -Entity parameter.' $AlreadyWarned = $true } # Add the entity details for the current loop of the Process block $EventFilter.Entity.Entity = $_.ExtensionData.MoRef $EventFilter.Entity.Recursion = &{ If ($_.ExtensionData.MoRef.Type -eq 'VirtualMachine') {'self'} Else {'all'} } # Create the event collector, and collect 100 events at a time Write-Verbose "Calling Get-View to gather event results for object $($_.Name)" $CollectorSplat = @{ Server = $vCenter Verbose = $false Debug = $false } $Collector = Get-View ($EventMgr).CreateCollectorForEvents($EventFilter) @CollectorSplat $Buffer = $Collector.ReadNextEvents(100) If (-not $Buffer) { Write-Verbose "No vMotion events found for object $($_.Name)" } While ($Buffer) { $EventCount = ($Buffer | Measure-Object).Count Write-Verbose "Processing $EventCount events from object $($_.Name)" # Append up to 100 results into the $Events array If ($EventCount -gt 1) { # .AddRange if more than one event $Events.AddRange($Buffer) | Out-Null } Else { # .Add if only one event; should never happen since gathering begin & end events $Events.Add($Buffer) | Out-Null } # Were there more than 100 results? Get the next batch and restart the While loop $Buffer = $Collector.ReadNextEvents(100) } # Destroy the collector after each entity to avoid running out of memory :) $Collector.DestroyCollector() } #ForEach $Entity $InventoryObjects = $null } #ForEach $vCenter } #Process END { # Construct an empty array for results within the ForEach $Results = New-Object System.Collections.Generic.List[object] # Group together by ChainID; each vMotion has begin/end events ForEach ($vMotion in ($Events | Sort-Object CreatedTime | Group-Object ChainID)) { # Each vMotion should have start and finish events # "% 2" correctly processes duplicate vMotion results # (duplicate results can occur, for example, if you have duplicate vCenter connections open) If ($vMotion.Group.Count % 2 -eq 0) { # New 6.5 migration event type is changing fields around on me If ($vMotion.Group[0].EventTypeID -eq 'com.vmware.vc.vm.VmHotMigratingWithEncryptionEvent') { $DstDC = ($vMotion.Group[0].Arguments | Where-Object {$_.Key -eq 'destDatacenter'}).Value $DstDS = ($vMotion.Group[0].Arguments | Where-Object {$_.Key -eq 'destDatastore'}).Value $DstHost = ($vMotion.Group[0].Arguments | Where-Object {$_.Key -eq 'destHost'}).Value } Else { $DstDC = $vMotion.Group[0].DestDatacenter.Name $DstDS = $vMotion.Group[0].DestDatastore.Name $DstHost = $vMotion.Group[0].DestHost.Name } #If 'com.vmware.vc.vm.VmHotMigratingWithEncryptionEvent' # Mark the current vMotion as vMotion / Storage vMotion / Both If ($vMotion.Group[0].Ds.Name -eq $DstDS) { $Type = 'vMotion' } ElseIf ($vMotion.Group[0].Host.Name -eq $DstHost) { $Type = 's-vMotion' } Else { $Type = 'Both' } # Add the current vMotion into the $Results array $Results.Add([PSCustomObject][Ordered]@{ PSTypeName = 'vMotion.Object' Name = $vMotion.Group[0].Vm.Name Type = $Type SrcHost = $vMotion.Group[0].Host.Name DstHost = $DstHost SrcDS = $vMotion.Group[0].Ds.Name DstDS = $DstDS SrcCluster = $vMotion.Group[0].ComputeResource.Name DstCluster = $vMotion.Group[1].ComputeResource.Name SrcDC = $vMotion.Group[0].Datacenter.Name DstDC = $DstDC # Hopefully people aren't performing vMotions that take >24 hours, because I'm ignoring days in the string Duration = (New-TimeSpan -Start $vMotion.Group[0].CreatedTime -End $vMotion.Group[1].CreatedTime).ToString('hh\:mm\:ss') StartTime = $vMotion.Group[0].CreatedTime.ToLocalTime() EndTime = $vMotion.Group[1].CreatedTime.ToLocalTime() # Making an assumption that all events with an empty username are DRS-initiated Username = &{If ($vMotion.Group[0].UserName) {$vMotion.Group[0].UserName} Else {'DRS'}} ChainID = $vMotion.Group[0].ChainID }) } #If vMotion Group % 2 ElseIf ($vMotion.Group.Count % 2 -eq 1) { Write-Debug "vMotion chain ID $($vMotion.Group[0].ChainID -join ', ') had an odd number of events; cannot match start/end times. Inspect `$vMotion for more details" # If you're here, try to gather some details and tell me what happened! @brianbunke } } #ForEach ChainID # Reduce default property set for readability $TypeData = @{ TypeName = 'vMotion.Object' DefaultDisplayPropertySet = 'Name','Duration','Type','StartTime' } # Include -Force to avoid errors after the first run Update-TypeData @TypeData -Force # Display all results found $Results } #End } |