Get-HyperVReports.psm1
function Get-HyperVReports { <# .SYNOPSIS Get-HyperVReports prints the menu for selecting which report you would like to print. #> [CmdletBinding()] param() Get-AdminCheck # Sets Console to black background $Host.UI.RawUI.BackgroundColor = 'Black' # Prints the Menu. Accepts input. Clear-Host Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host ' Hyper-V Reports' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host '[1] Hyper-V Cluster Log Search' -ForegroundColor White Write-Host '[2] Maintenance QC' -ForegroundColor White Write-Host '[3] Cluster Aware Update History' -ForegroundColor White Write-Host '[4] Storage Reports' -ForegroundColor White Write-Host '[5] VM Reports' -ForegroundColor White Write-Host '[6] Storage Cleanup Analyzer' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green $MenuChoice = Read-Host 'Menu Choice' # Prints report based on $MenuChoice. switch ($MenuChoice) { 1 { Get-HyperVClusterLogs } 2 { Get-HyperVMaintenanceQC } 3 { Get-HyperVCAULogs } 4 { Get-HyperVStorageReport } 5 { Get-HyperVVMInfo } 6 { Get-HyperVStorageCleanupAnalyzer } default { Clear-Host Write-Host 'Incorrect Choice. Choose a number from the menu.' Start-Sleep -Seconds 3 Get-HyperVReports } } } function Get-ClusterCheck { <# .SYNOPSIS This function performs a check to see if this script is being executed on a clustered Hyper-V server. It converts that into a bool for use in the script. #> [CmdletBinding()] param() # Variable Setup $ErrorActionPreference = 'SilentlyContinue' $result = $False # Check to see if this is a functional cluster. If so, return $True. $BoolClusterCheck = Get-Cluster if ($BoolClusterCheck) { $result = $True } $result } function Get-AdminCheck { <# .SYNOPSIS This function performs a check to see if this script is being executed in an administrative prompt. Breaks if not. #> [CmdletBinding()] param() # Checks to see if it is being run in an administrative prompt. Breaks the script if not. if ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match 'S-1-5-32-544') -eq $False ) { Write-Error 'This script must be run with administrator privledges. Relaunch script in an administrative prompt.' break } } Function Get-DomainNodes { <# .SYNOPSIS Get-DomainNodes creates an object that contains all of the clusternodes with their FQDN. #> [CmdletBinding()] param() $ClusterNodes = Get-ClusterNode -ErrorAction Stop $Domain = (Get-WmiObject Win32_ComputerSystem).Domain $DomainNodes = foreach ($node in $ClusterNodes) { $node.Name + '.' + $Domain } $DomainNodes } function Get-HyperVVMs { <# .SYNOPSIS Get-HyperVVMs uses PSJobs to pull all of the VMs from all Hyper-V servers at the same time. #> [CmdletBinding()] param() if (Get-ClusterCheck) { $VMs = Get-VM -ComputerName (Get-DomainNodes) } else { $VMs = Get-VM } $VMs } function Get-HyperVCAULogs { <# .SYNOPSIS Get-HyperVCAULogs collects CAU event log data and hotfixes and prints a report. #> [CmdletBinding()] param() Get-AdminCheck # Verifying this is being run on a cluster. $ClusterCheck = Get-ClusterCheck if ($ClusterCheck -eq $False) { Write-host 'This script only works for clustered Hyper-V servers.' -ForegroundColor Red Start-Sleep -Seconds 3 Get-HyperVReports } # Collect Variables try { $Cluster = (Get-Cluster).Name $CAUDates = ( (Get-WinEvent -LogName *ClusterAwareUpdating*).TimeCreated | Get-Date -Format MM/dd/yyy) | Get-Unique $ClusterNodes = Get-ClusterNode -ErrorAction SilentlyContinue } catch { Write-Host "Couldn't process cluster nodes!" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } # Gathers CAU Dates from logs and prints for $StartDate input. Clear-Host Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host 'Dates CAU was performed:' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Output $CAUDates Write-Host -------------------------------------------------------- -ForegroundColor Green $StartDateRequest = Read-Host 'Which date would you like the logs from' Write-Host `r Write-Host 'Collecting CAU logs and hotfix information...' # Formatting provided startdate for use in filtering. $StartDate = $StartDateRequest | Get-Date -Format MM/dd/yyyy # Collects HotFixs from cluster nodes. try { $Hotfixes = $False $Hotfixes = foreach ($Node in $ClusterNodes) { Get-HotFix -ComputerName $Node.Name | Where-Object InstalledOn -Match $StartDate } } catch { Write-Host "Couldn't collect the hotfixes from cluster nodes!" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } # Collects eventlogs for cluster nodes. try { $EventLogs = $False $EventLogs = foreach ($Node in $ClusterNodes) { Get-WinEvent -ComputerName $Node.Name -LogName *ClusterAwareUpdating* | Where-Object TimeCreated -Match $StartDate | Select-Object TimeCreated,Message } } catch { Write-Host "Couldn't collect the eventlogs from cluster nodes!" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } Clear-Host # Prints CAU logs Write-Host `r Write-Host "CAU logs from $StartDate for $Cluster." -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green if ($Eventlogs) { $Eventlogs | Sort-Object TimeCreated | Format-Table -AutoSize } else { Write-Host "No Logs Found" } # Prints HotFix logs Write-Host 'Updates installed during this CAU run.' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green if ($Hotfixes) { $Hotfixes | Format-Table -AutoSize } else { Write-Host 'No Hotfixes Found' } } function Get-HyperVClusterLogs { <# .SYNOPSIS Get-HyperVClusterLogs searches the Hyper-V eventlogs of a Hyper-V cluster and prints a report. #> [CmdletBinding()] param() Get-AdminCheck # Setting up Variables. $ClusterCheck = Get-ClusterCheck if ($ClusterCheck) { $ClusterNodes = Get-ClusterNode -ErrorAction SilentlyContinue $Domain = (Get-WmiObject Win32_ComputerSystem).Domain $DomainNodes = foreach ($node in $ClusterNodes) { $node.Name + '.' + $Domain } } # Prints the Menu. Accepts input. Clear-Host Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host ' Clustered Hyper-V Eventlog Search' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host '[1] Search last 24 hours' -ForegroundColor White Write-Host '[2] Search last 48 hours' -ForegroundColor White Write-Host '[3] Search last 7 days' -ForegroundColor White Write-Host '[4] Specify date range to search' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green $MenuChoice = Read-Host 'Select menu number' Write-Host `r # Builds a 24, 48 or 7 day $StartDate and #EndDate unless date is provided. Switch ($MenuChoice) { 1 { $DaysBack = -1 } 2 { $DaysBack = -2 } 3 { $DaysBack = -7 } } if ($MenuChoice -eq '1' -or $MenuChoice -eq '2' -or $MenuChoice -eq '3' ) { $StartDate = (Get-Date).AddDays($DaysBack) $EndDate = (Get-Date).AddDays(1) } elseif ($MenuChoice -eq '4') { $DateFormat = Get-Date -Format d Write-Host "The date format for this environment is '$DateFormat'." -ForegroundColor Yellow Write-Host `r $StartDate = Read-Host 'Enter oldest search date' $EndDate = Read-Host 'Enter latest search date' if ($EndDate -eq '') { $EndDate = (Get-Date).AddDays(1) } Write-Host `r } else { Clear-Host Write-Host 'Incorrect Choice. Choose a number from the menu.' Start-Sleep -Seconds 3 Get-HyperVClusterLogs } # Collects text to filter the event log with. $Messagetxt = Read-Host 'Enter the text you would like to search the eventlogs for' Write-Host `r # Filter for log collection. $Filter = @{ LogName = "*Hyper-V*" StartTime = $StartDate EndTime = $EndDate } Write-Host 'Reviewing Hyper-V servers for eventlogs containing $Messagetxt. Please be patient.' Clear-Host Write-Host -------------------------------------------------------------------------------------------------------------------------------------- -ForegroundColor Green Write-Host ' Clustered Hyper-V Eventlog Search' -ForegroundColor White Write-Host -------------------------------------------------------------------------------------------------------------------------------------- -ForegroundColor Green Write-Host "Search results for: $Messagetxt" Write-Host `r # Builds $EventLogs variable used in report. if ($ClusterCheck -eq $True) { # Clear any old jobs out related to this script. Get-Job | Where-Object Command -like *Get-WinEvent* | Remove-Job # Setup ScriptBlock for Invoke-Command. $EventLogScriptBlock = { param($Filter,$Messagetxt) Get-WinEvent -FilterHashtable $Filter -ErrorAction SilentlyContinue | Where-Object -Property Message -like "*$Messagetxt*" } # Use jobs to pull event logs from all cluster nodes at the same time. Invoke-Command -ComputerName $DomainNodes -ScriptBlock $EventLogScriptBlock -ArgumentList $Filter,$Messagetxt -AsJob | Wait-Job | Out-Null # Collect eventlogs from jobs and assign to $EventLogs $EventLogs = Get-Job | Where-Object Command -like *Get-WinEvent* | Receive-Job $EventLogNodes = $EventLogs.PSComputerName | Get-Unique foreach ($node in $DomainNodes) { Write-Host $node.split(".")[0] -ForegroundColor Green if ($EventLogNodes -contains $node) { $EventLogs | Where-Object PSComputerName -EQ $node | Select-Object TimeCreated,ProviderName,Message | Sort-Object TimeCreated | Format-List } else { Write-Host `r Write-Host 'No Logs found.' Write-Host `n } } } elseif ($ClusterCheck -eq $False) { $EventLogs = $False Write-Host $env:COMPUTERNAME -ForegroundColor Green $EventLogs = Get-WinEvent -FilterHashtable $Filter | Where-Object -Property Message -Like "*$Messagetxt*" | Select-Object TimeCreated,ProviderName,Message if ($EventLogs) { $EventLogs | Sort-Object TimeCreated | Format-List } else { Write-Host 'No Logs Found.' } } } Function Get-HyperVMaintenanceQC { <# .SYNOPSIS Get-HyperVMaintenanceQC tests Hyper-V cluster to ensure single node failure and no unclustered VMS. #> [CmdletBinding()] param() Get-AdminCheck # Verifying this is being run on a cluster. $ClusterCheck = Get-ClusterCheck if ($ClusterCheck -eq $False) { Write-host 'This script only works for clustered Hyper-V servers.' -ForegroundColor Red Start-Sleep -Seconds 3 Get-HyperVReports } # Gather cluster variables $Cluster = Get-Cluster $DomainNodes = Get-DomainNodes Clear-Host Write-Host 'Calculating cluster memory usage...' -ForegroundColor Green -BackgroundColor Black # Building variable that has memory info for all of the cluster nodes. try { $VMHostMemory = foreach ($node in $DomainNodes) { [PSCustomObject]@{ Name = $node.Split('.')[0] TotalMemory = [math]::Round( (Get-WmiObject Win32_ComputerSystem -ComputerName $node).TotalPhysicalMemory /1GB ) AvailableMemory = [math]::Round(( (Get-WmiObject Win32_OperatingSystem -ComputerName $node).FreePhysicalMemory ) /1024 /1024 ) UsableMemory = [math]::Round( (Get-Counter -ComputerName $node -Counter '\Hyper-V Dynamic Memory Balancer(System Balancer)\Available Memory').Readings.Split(':')[1] / 1024 ) } } } catch { Write-Host "Couldn't collect Memory usage from cluster nodes!" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } # Adding the hosts memory values together. foreach ($VMHost in $VMHostMemory) { $TotalVMHostMemory += $VMHost.TotalMemory $TotalAvailableVMHostMemory += $VMHost.AvailableMemory $TotalUsableVMHostMemory += $VMHost.UsableMemory $VirtMemory += $VMHost.AvailableMemory - $VMHost.UsableMemory } # Calculate math for different variables. $Nodecount = $DomainNodes.Count $SingleNodeVirtMemory = $VirtMemory | Sort-Object -Descending | Select-Object -Last 1 # Reviews nodes and takes the node with the lowest virt memory. $SingleNodeMemory = $VMHostMemory.TotalMemory[0] $Nodecheck = $TotalVMHostMemory / $SingleNodeMemory $UsableMemoryAfterFailure = ($TotalUsableVMHostMemory + $SingleNodeVirtMemory) $HAMemory = $SingleNodeMemory - $UsableMemoryAfterFailure [decimal]$NPlusMath = $UsableMemoryAfterFailure / $SingleNodeMemory $Nplus = $NPlusMath.ToString().Split('.')[0] # Sort nonclustered VMs by their state for readability. $VMs = Get-HyperVVMs $NonClusteredVMsSorted = $VMs | Where-Object IsClustered -EQ $False | Sort-Object State # Print the top of the report. Clear-Host if ($Nodecount -eq '1') { Write-Host '===========================================' -ForegroundColor DarkGray Write-Host " $Cluster is a single node cluster" Write-Host '===========================================' -ForegroundColor DarkGray } else { Write-Host '===========================================' -ForegroundColor DarkGray Write-Host " $Cluster - $Nodecount Nodes" Write-Host '===========================================' -ForegroundColor DarkGray } # Print node memory report. Write-Host -NoNewline " $TotalVMHostMemory " -ForegroundColor Green; Write-Host 'GB - Total cluster memory' Write-Host -NoNewline " $SingleNodeMemory " -ForegroundColor Green ; Write-Host 'GB - Memory of each node' if ($NPlus -gt 0) { Write-Host -NoNewline " $UsableMemoryAfterFailure " -ForegroundColor Green ; Write-Host 'GB - Usable memory with 1 failure' } else { Write-Host -NoNewline " $UsableMemoryAfterFailure " -ForegroundColor Red ; Write-Host 'GB - Usable memory with 1 failure' } Write-Host '===========================================' -ForegroundColor DarkGray # Prints error if all nodes don't have the same amount of memory. if ($Nodecheck -ne $Nodecount) { Write-Host ' Nodes have different amounts of memory!' -ForegroundColor Red Write-Host '===========================================' -ForegroundColor DarkGray } # Checks if cluster is HA. if ($TotalUsableVMHostMemory -le $SingleNodeMemory -and $HAMemory -gt 0) { Write-host ' Cluster would NOT survive single failure!' -ForegroundColor Red Write-Host '===========================================' -ForegroundColor DarkGray Write-Host " More than $HAMemory GB of memory needed to be HA" } else { Write-Host " Cluster is N+$Nplus" -ForegroundColor Green } Write-Host '===========================================' -ForegroundColor DarkGray # Checks if nonclustered VMs exist and prints list. if ($Null -eq $NonClusteredVMsSorted) { Write-Host ' All VMs are clustered' -ForegroundColor Green } else { Write-Host ' Unclustered VMs Found!' -ForegroundColor Yellow Write-Host '--------------------------------------------' -ForegroundColor DarkGray } # Prints nonclustered VMs. foreach ($VM in $NonClusteredVMsSorted) { $VMOutput = ' ' + ($VM.ComputerName).Split('.')[0] + ' - ' + $VM.State + ' - ' + $VM.Name Write-Host $VMOutput -ForegroundColor Yellow } Write-Host '===========================================' -ForegroundColor DarkGray } function Get-HyperVStorageReport { <# .SYNOPSIS Get-HyperVStorageReport collects Cluster Shared Volumes and prints a report of their data. #> [CmdletBinding()] param() Get-AdminCheck # Prints the Menu. Accepts input. Clear-Host Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host ' Hyper-V Storage Reports' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host '[1] Cluster Storage - Full report' -ForegroundColor White Write-Host '[2] Cluster Storage - Utilization' -ForegroundColor White Write-Host '[3] Cluster Storage - IO (2016/2019 Only)' -ForegroundColor White Write-Host '[4] Local Storage - Utilization' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green $MenuChoice = Read-Host 'Menu Choice' if ($MenuChoice -eq 1 -or $MenuChoice -eq 2 -or $MenuChoice -eq 3) { Write-Host `r Write-Host 'Pulling information for Cluster Shared Volumes...' -ForegroundColor White # Builds $CSVINfo to gather disk info for final report. try { # Variable Setup $OSVersion = [environment]::OSVersion.Version.Major $CSVs = Get-Partition | Where-Object AccessPaths -like *ClusterStorage* | Select-Object AccessPaths,DiskNumber if (Get-ClusterSharedVolume) { $results = foreach ($csv in $CSVs) { # Collecting CSV information $AccessPathVolumeID = $csv.AccessPaths.Split('/')[1] $ClusterPath = $csv.AccessPaths[0].TrimEnd('\') $FriendlyPath = $ClusterPath.Split('\')[2] $ClusterSharedVolume = Get-ClusterSharedVolume | Select-Object -ExpandProperty SharedVolumeInfo | Where-Object FriendlyVolumeName -eq $ClusterPath | Select-Object -Property FriendlyVolumeName -ExpandProperty Partition $CSVName = (Get-ClusterSharedVolumeState | Where-Object VolumeFriendlyName -eq $FriendlyPath).Name | Get-Unique $VolumeBlock = Get-Volume | Where-Object Path -like $AccessPathVolumeID if ($OSVersion -ge 10) { $QOS = Get-StorageQosVolume | Where-Object MountPoint -eq ($ClusterPath + '\') [PSCustomObject]@{ '#' = $csv.DiskNumber Block = $VolumeBlock.AllocationUnitSize CSVName = $CSVName ClusterPath = $ClusterPath 'Size(GB)' = [math]::Round($ClusterSharedVolume.Size /1GB) 'Used(GB)' = [math]::Round($ClusterSharedVolume.UsedSpace /1GB) 'Free(GB)' = [math]::Round( ($ClusterSharedVolume.Size - $ClusterSharedVolume.UsedSpace) /1GB) '% Free' = [math]::Round($ClusterSharedVolume.PercentFree, 1) IOPS = $QOS.IOPS Latency = [math]::Round($QOS.Latency, 2) 'MB/s' = [math]::Round(($QOS.Bandwidth /1MB), 1) } } else { [PSCustomObject]@{ '#' = $csv.DiskNumber Block = (Get-CimInstance -ClassName Win32_Volume | Where-Object Label -Like $VolumeBlock.FileSystemLabel).BlockSize[0] CSVName = $CSVName ClusterPath = $ClusterPath 'Size(GB)' = [math]::Round($ClusterSharedVolume.Size /1GB) 'Used(GB)' = [math]::Round($ClusterSharedVolume.UsedSpace /1GB) 'Free(GB)' = [math]::Round( ($ClusterSharedVolume.Size - $ClusterSharedVolume.UsedSpace) /1GB) '% Free' = [math]::Round($ClusterSharedVolume.PercentFree, 1) } } } } else { Write-Host 'This environment does not have any clustered storage.' -ForegroundColor White } } catch { Write-Host "Couldn't process Cluster Shared Volume data!" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } } elseif ($MenuChoice -eq 4) { Write-Host `r Write-Host 'Pulling information from local storage...' -ForegroundColor White # Collect local disk information. $Volumes = Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.FileSystem -ne 'CSVFS' -and $_.FileSystemLabel -notlike "*quorum*" } $results = foreach ($disk in $Volumes) { [PSCustomObject]@{ Drive = $disk.DriveLetter Label = $disk.FileSystemLabel 'Size(GB)' = [math]::Round($disk.Size /1GB) 'Used(GB)' = [math]::Round( ($disk.Size - $disk.SizeRemaining) /1GB) 'Free(GB)' = [math]::Round($disk.SizeRemaining /1GB) '% Free' = [math]::Round(($disk.SizeRemaining / $disk.Size) * 100) } } } # Prints report based on $MenuChoice. switch ($MenuChoice) { 1 { $results | Sort-Object '#' | Format-Table * -AutoSize } 2 { $results | Select-Object '#',CSVName,ClusterPath,'Size(GB)','Used(GB)','Free(GB)','% Free' | Sort-Object '#' | Format-Table -AutoSize } 3 { $results | Select-Object '#',CSVName,ClusterPath,IOPS,Latency,MB/s | Sort-Object '#' | Format-Table -AutoSize } 4 { $results | Sort-Object Drive | Format-Table -AutoSize } default { Write-Host 'Incorrect Choice. Choose a number from the menu.' Start-Sleep -Seconds 3 Get-HyperVStorageReport } } } function Get-HyperVVMInfo { <# .SYNOPSIS Get-HyperVVMInfo collects Hyper-V VM info and prints report of their data. .PARAMETER ExportToCSV Exports the report to specified .CSV in path. .EXAMPLE C:\PS> Get-HyperVVMInfo -ExportToCSV C:\rs-pkgs\VMInfo.csv #> [CmdletBinding()] param( [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [string] # Exports the report to specified .CSV in path. $ExportToCSV ) Get-AdminCheck # Prints the Menu. Accepts input. Clear-Host Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host ' Hyper-V VM Reports' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green Write-Host '[1] VM vCPU and RAM' -ForegroundColor White Write-Host '[2] VM Networking' -ForegroundColor White Write-Host '[3] VM VHDX Size/Location/Type' -ForegroundColor White Write-Host '[4] VM VHDX IO/Latency (2016/2019 Only)' -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green $MenuChoice = Read-Host 'Menu Choice' Write-Host `r # Pull Cluster node data for script. Write-Host 'Gathering data from VMs... ' -ForegroundColor White Write-Host `r $VMs = Get-HyperVVMs # Collects information from VMs and creates $VMInfo variable with all VM info. try { $results = foreach ($vm in $VMs) { if ($MenuChoice -eq 1) { [PSCustomObject]@{ Host = $vm.ComputerName VMName = $vm.VMName vCPU = $vm.ProcessorCount RAM = [math]::Round($vm.MemoryStartup /1GB) } } elseif ($MenuChoice -eq 2) { [Regex]$IPv4 = '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b' $VMNetworkAdapters = Get-VMNetworkAdapter -ComputerName $vm.Computername -VMName $vm.VMName foreach ($adapter in $VMNetworkAdapters) { $VMNetworkAdapterVlans = Get-VMNetworkAdapterVlan -VMNetworkAdapter $Adapter foreach ($adapterVlan in $VMNetworkAdapterVlans) { [PSCustomObject]@{ Host = $vm.ComputerName VMName = $vm.VMName IPAddress = $adapter.Ipaddresses | Select-String -Pattern $IPv4 VLAN = $adapterVlan.AccessVlanId MAC = $adapter.MacAddress vSwitch = $adapter.SwitchName } } } } elseif ($MenuChoice -eq 3) { $Disks = Get-VMHardDiskDrive -ComputerName $vm.Computername -VMName $vm.VMName | Get-VHD -ComputerName $vm.Computername foreach ($disk in $Disks) { [PSCustomObject]@{ VMName = $vm.VMName Disk = $disk.Path Size = [math]::Round($disk.FileSize /1GB) PotentialSize = [math]::Round($disk.Size /1GB) 'VHDX Type' = $disk.VhdType } } } } if ($MenuChoice -eq 4) { $OSVersion = [environment]::OSVersion.Version.Major if ($OSVersion -ge 10) { $VHDXIO = Get-StorageQoSFlow $results = foreach($vhdxio in $VHDXIO) { $Latency = [math]::Round($vhdxio.InitiatorLatency, 2) $Bandwidth = [math]::Round($vhdxio.InitiatorBandwidth /1MB, 2) [PSCustomObject]@{ VMName = $vhdxio.InitiatorName FilePath = $vhdxio.FilePath IOPS = $vhdxio.InitiatorIOPS Latency = [string]$Latency + ' ms' Bandwidth = [string]$Bandwidth + ' MB/s' } } } else { Write-Host 'This is only supported on Windows Server 2016 and up. Returning to menu.' Start-Sleep -s 2 Get-HyperVVMInfo } } } catch { Write-Host "Couldn't collect information from the VMs!" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } # Prints report based on $MenuChoice. if ($MenuChoice -eq 1 -or $MenuChoice -eq 2) { if ($ExportToCSV) { $results | Sort-Object Host | Export-Csv -Path $ExportToCSV -NoTypeInformation } else { $results | Sort-Object Host | Format-Table -AutoSize } } elseif ($MenuChoice -eq 3) { if ($ExportToCSV) { $results | Sort-Object VMName | Export-Csv -Path $ExportToCSV -NoTypeInformation } else { $results | Sort-Object VMName | Format-Table -AutoSize } } elseif ($MenuChoice -eq 4) { if ($ExportToCSV) { $results | Sort-Object IOPS -Descending | Export-Csv -Path $ExportToCSV -NoTypeInformation } else { $results | Sort-Object IOPS -Descending | Format-Table -AutoSize } } else { Write-Host 'Incorrect Choice. Choose a number from the menu.' Start-Sleep -Seconds 3 Get-HyperVStorageReport } # Checks to see if a CSV exists at the export path. if ($ExportToCSV) { $TestExport = Test-Path $ExportToCSV if ($TestExport -eq $True) { Write-Host "Export to $ExportToCSV completed successfully." } else { Write-Host "Export to $ExportToCSV failed. Verify path and try again." } } } function Get-HyperVStorageCleanupAnalyzer { <# .SYNOPSIS Get-HyperVStorageCleanupAnalyzer goes through the Hyper-V environment looking for things taking up space. #> [CmdletBinding()] param() Get-AdminCheck # Script to pull the number of DiskShadows that are currently on the Hyp. $GetHyperVDiskShadows = { # HyperVDiskShadows $DiskShadowScript = $env:TEMP + '\Temp.dsh' 'list shadows all' | Set-Content $DiskShadowScript $DiskShadows = diskshadow /s $DiskShadowScript [String]$NoDiskShadowCheck = $DiskShadows | Select-String -SimpleMatch 'No shadow copies found in system.' if ($NoDiskShadowCheck -like '*No*') { [String]$NumberOfDiskShadows = 0 } else { [String]$DiskShadowInfo = $DiskShadows | Select-String -SimpleMatch 'Number of shadow copies listed:' [String]$NumberOfDiskShadows = $DiskShadowInfo.Split('')[5] } [PSCustomObject]@{ DiskShadows = $NumberOfDiskShadows } } # Script to check all VMs to verify they don't have Save as the Automatic Stop Action. $GetHyperVStopAction = { # HyperVStopAction [int]$SaveActionCheck = 0 foreach ($vm in $VMs) { if ($vm.AutomaticStopAction -eq 'Save') { $SaveActionCheck = $SaveActionCheck + 1 [PSCustomObject]@{ VMName = $vm.VMName StopAction = $vm.AutomaticStopAction } } } } # Script to check the environment for any Checkpoints that exist. $GetHyperVVMCheckpoints = { # HyperVCheckpoints if (Get-ClusterNode) { Get-VMSnapshot -ComputerName (Get-ClusterNode) -VMName * } else { Get-VMSnapshot -VMName * } } # Script to pull all VM disks and check to see if they are an avhdx. $GetHyperVVMAVHDX = { # GetHyperVVMAVHDX if (Get-ClusterNode) { $VMs = Get-VM -ComputerName (Get-ClusterNode) } else { $VMs = Get-VM } foreach ($vm in $VMs) { $VMDisks = Get-VMHardDiskDrive -ComputerName $vm.Computername -VMName $vm.VMName | Get-VHD -ComputerName $vm.Computername foreach ($disk in $VMDisks) { if ($disk.Path -like '*.avhdx' -or $disk.Path -like '*.avhd' ) { [PSCustomObject]@{ VMName = $vm.Name Path = $disk.Path } } } } } # Script to pull all VM disks and check to see if they are not being used. $GetHyperVUnusedVHDXs = { # HyperVUnusedVHDX $AllVMDiskRoots = [System.Collections.ArrayList]@() if (Get-ClusterNode) { $VMs = Get-VM -ComputerName (Get-ClusterNode) } else { $VMs = Get-VM } foreach ($vm in $VMs) { $VMDisks = Get-VMHardDiskDrive -ComputerName $vm.Computername -VMName $vm.VMName | Get-VHD -ComputerName $vm.Computername foreach ($disk in $VMDisks) { if ($disk.Path -like '*.vhdx' -or $disk.Path -like '*.vhd') { $AllVMDiskRoots += $disk.Path.ToLower() } elseif ($disk.Path -like '*.avhdx' -or $disk.Path -like '*.avhd') { $Path = $disk.Path while($Path = (Get-VHD -Path $Path).ParentPath) { $AllVMDiskRoots += $Path.ToLower() } } } } # Collect all VHDXs on clustered and unclustered storage. $LocalDriveLetters = (Get-Volume).DriveLetter $LocalVHDXDataPull = [System.Collections.ArrayList]@() $LocalVHDXDataPull += foreach ($driveLetter in $LocalDriveLetters) { (Get-ChildItem -Path ($driveLetter + ':\') -Include '*.vhdx','*.vhd' -Recurse -ErrorAction SilentlyContinue ).FullName } $AllVHDXs = [System.Collections.ArrayList]@() $AllVHDXs = $LocalVHDXDataPull.Where({$_ -ne $null}) # Compare the list of all VHDXs to the list of VHDXs in use and then create a list of VHDXs not in use by any VMs. foreach ($vhdx in $AllVHDXs) { if ( -not ( $AllVMDiskRoots.Contains($vhdx.ToLower()) )) { [PSCustomObject]@{ UnusedVHDX = $vhdx } } } } # Script to collect all HRLs on clustered and unclustered storage. $GetHyperVHRL = { # HyperVHRL $LocalHRLDataPull = [System.Collections.ArrayList]@() $AllHRLs = [System.Collections.ArrayList]@() $LocalDriveLetters = (Get-Volume).DriveLetter $LocalHRLDataPull += foreach ($driveLetter in $LocalDriveLetters) { Get-ChildItem -Path ($driveLetter + ':\') -Filter '*.hrl' -Recurse -ErrorAction SilentlyContinue } $AllHRLs = $LocalHRLDataPull.Where({$_ -ne $null}) # Check to see if any hrl files are larger than 5GB. foreach ($hrl in $AllHRLs) { if ( ($hrl.Length -gt 5368706371) -or ((Get-Date).AddDays(-7)) -ge ($hrl.LastWriteTime | Get-Date -Format d) ) { [PSCustomObject]@{ FullName = $hrl.FullName Size = [math]::Round($hrl.Length /1GB) LastWritten = $hrl.LastWriteTime | Get-Date -Format d } } } } # Collect all VHDX.tmp files on clustered and unclustered storage. $GetHyperVVHDXTemp = { # HyperVVHDXTmp $LocalTmpVHDXDataPull = [System.Collections.ArrayList]@() $LocalDriveLetters = (Get-Volume).DriveLetter $LocalTmpVHDXDataPull += foreach ($driveLetter in $LocalDriveLetters) { (Get-ChildItem -Path ($driveLetter + ':\') -Include '*.vhdx.tmp','*.vhd.tmp' -Recurse -ErrorAction SilentlyContinue ).FullName } # Clean the $null values out of the data pull and output the clean data as a PSObject. $AllTmpVHDXs = [System.Collections.ArrayList]@() $AllTmpVHDXs = $LocalTmpVHDXDataPull.Where({$_ -ne $null}) if ($AllTmpVHDXs) { foreach ($tmpVHDX in $AllTmpVHDXs) { [PSCustomObject]@{ 'VHDXtmp' = $tmpVHDX } } } } # Clear any old jobs out. Get-Job | Where-Object Command -like *HyperV* | Remove-Job -Force # Use PSJobs to launch all of the scripts at the same time. Start-Job -ScriptBlock $GetHyperVDiskShadows | Out-Null Start-Job -ScriptBlock $GetHyperVStopAction | Out-Null Start-Job -ScriptBlock $GetHyperVVMCheckpoints | Out-Null Start-Job -ScriptBlock $GetHyperVVMAVHDX | Out-Null Start-Job -ScriptBlock $GetHyperVUnusedVHDXs | Out-Null Start-Job -ScriptBlock $GetHyperVHRL | Out-Null Start-Job -ScriptBlock $GetHyperVVHDXTemp | Out-Null Write-Host `r Write-Host '-----------------------------------------------------------------' -ForegroundColor White Write-Host 'Checking for Disk Shadows...' -ForegroundColor White Write-Host '-----------------------------------------------------------------' -ForegroundColor White # Collect diskshadows from the job and assign to $HyperVDiskShadows. $HyperVDiskShadows = Get-Job | Where-Object Command -like *HyperVDiskShadows* | Wait-Job | Receive-Job $NumberOfDiskShadows = ($HyperVDiskShadows).Diskshadows if ( $NumberOfDiskShadows -eq '0' ) { Write-Host 'No Disk Shadows found.' -ForegroundColor Green } else { Write-Host "$NumberOfDiskShadows Disk Shadows found!" -ForegroundColor Yellow } Write-Host `r Write-Host `r Write-Host '-----------------------------------------------------------------' -ForegroundColor White Write-Host 'Checking for VMs with their Automatic Stop Action set to Save...' -ForegroundColor White Write-Host '-----------------------------------------------------------------' -ForegroundColor White # Collect VM Stop Action from the job and assign to $HyperVSaveAction. $HyperVStopAction = Get-Job | Where-Object Command -like *HyperVStopAction* | Wait-Job | Receive-Job $VMsWithStopAction = ($HyperVStopAction).VMName if ( $VMsWithStopAction.Count -eq '0' ) { Write-Host 'No VMs with Save set as the Automatic Stop Action found.' -ForegroundColor Green } else { $VMsWithStopAction | ForEach-Object { Write-Host $_ -ForegroundColor Yellow } } Write-Host `r Write-Host `r Write-Host '-----------------------------------------------------------------' -ForegroundColor White Write-Host 'Checking for Checkpoints...' -ForegroundColor White Write-Host '-----------------------------------------------------------------' -ForegroundColor White # Collect checkpoints from the job and assign to $HyperVDiskShadows. $HyperVCheckpoints = Get-Job | Where-Object Command -like *HyperVCheckpoints* | Wait-Job | Receive-Job $VMSnapshots = $HyperVCheckpoints if ($VMSnapshots) { $VMSnapshots | ForEach-Object { Write-Host "$($_.VMName) - $($_.CreationTime)" -ForegroundColor Yellow } } else { Write-Host 'No Checkpoints found.' -ForegroundColor Green } Write-Host `r Write-Host `r Write-Host '-----------------------------------------------------------------' -ForegroundColor White Write-Host 'Checking for AVHDXs...' -ForegroundColor White Write-Host '-----------------------------------------------------------------' -ForegroundColor White # Collect AVHDXs from the job and assign to $HyperVVMAVHDX. $HyperVVMAVHDX = Get-Job | Where-Object Command -like *GetHyperVVMAVHDX* | Wait-Job | Receive-Job $VMAVHDXs = $HyperVVMAVHDX if ($VMAVHDXs) { $VMAVHDXs | ForEach-Object { Write-Host "$($_.VMName) - $($_.Path)" -ForegroundColor Yellow } } else { Write-Host 'No AVHDXs found.' -ForegroundColor Green } Write-Host `r Write-Host `r Write-Host '-----------------------------------------------------------------' -ForegroundColor White Write-Host 'Checking for hrl files that are larger than 5GB...' -ForegroundColor White Write-Host '-----------------------------------------------------------------' -ForegroundColor White # Collect HRL files from the job and assign to $HyperVHRL. $HyperVHRL = Get-Job | Where-Object Command -like *HyperVHRL* | Wait-Job | Receive-Job $HRLs = $HyperVHRL | Where-Object Size -gt 5 if ($HRLs) { foreach ($hrl in $HRLs) { Write-Host "$($Hrl.Size) GB - $($hrl.FullName)" -ForegroundColor Yellow } } else { Write-Host 'All hrl files smaller than 5GBs.' -ForegroundColor Green } Write-Host `r Write-Host `r Write-Host '-----------------------------------------------------------------' -ForegroundColor White Write-Host 'Checking for hrl files that are older than a week...' -ForegroundColor White Write-Host '-----------------------------------------------------------------' -ForegroundColor White $HRLs = $HyperVHRL if ($HRLs) { foreach ($hrl in $HRLs) { if (((Get-Date).AddDays(-7)) -ge ($hrl.LastWritten | Get-Date -Format d)) { Write-Host "$($Hrl.LastWritten) - $($Hrl.Size) GB - $($hrl.FullName)" -ForegroundColor Yellow } } } else { Write-Host 'All hrls are newer than a week.' -ForegroundColor Green } Write-Host `r Write-Host `r Write-Host '-----------------------------------------------------------------' -ForegroundColor White Write-Host 'Checking for VHDX.tmp files...' -ForegroundColor White Write-Host '-----------------------------------------------------------------' -ForegroundColor White # Collect VHDX.tmp from the job and assign to $HyperVVHDXTmp $HyperVVHDXTmp = Get-Job | Where-Object Command -like *HyperVVHDXTmp* | Wait-Job | Receive-Job $TmpVHDXs = $HyperVVHDXTmp if ($TmpVHDXs) { foreach ($tmpVHDX in $TmpVHDXs) { Write-Host $tmpVHDX.vhdxtmp -ForegroundColor Yellow } } else { Write-Host 'No VHDX.tmp files found.' -ForegroundColor Green } Write-Host `r Write-Host `r Write-Host '-----------------------------------------------------------------' -ForegroundColor White Write-Host 'Checking for VHDXs that are not in use...' -ForegroundColor White Write-Host '-----------------------------------------------------------------' -ForegroundColor White # Collect unused vHDXs from the job and assign to $HyperVUnusedVHDX. $HyperVUnusedVHDX = Get-Job | Where-Object Command -like *HyperVUnusedVHDX* | Wait-Job | Receive-Job $UnusedVHDXs = ($HyperVUnusedVHDX).UnusedVHDX if ($UnusedVHDXs) { foreach ($unusedVHDX in $UnusedVHDXs) { Write-Host $unusedVHDX -ForegroundColor Yellow } } else { Write-Host 'No unused VHDXs found.' -ForegroundColor Green } } |