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 -------------------------------------------------------- -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 } 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-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 `n 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 `n 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] Specify date range" -ForegroundColor White Write-Host -------------------------------------------------------- -ForegroundColor Green $MenuChoice = Read-Host "Please select menu number" Write-Host `n # Builds a 24hour $StartDate and #EndDate unless date is provided. Switch ($MenuChoice) { 1 { $StartDate = (Get-Date).AddDays(-1) $EndDate = (Get-Date).AddDays(1) } 2 { $DateFormat = Get-Date -Format d Write-Host "The date format for this environment is '$DateFormat'." -ForegroundColor Yellow Write-Host `n $StartDate = Read-Host "Enter oldest search date." $EndDate = Read-Host "Enter latest search date." Write-Host `n } default { 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 `n # Filter for log collection. $Filter = @{ LogName = "*Hyper-V*" StartTime = $StartDate EndTime = $EndDate } # Builds $EventLogs variable used in report. if ($ClusterCheck) { # 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*" } Write-Host "Reviewing Hyper-V servers for eventlogs containing $Messagetxt. Please be patient." # 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 Clear-Host Write-Host -------------------------------------------------------------------------------------------------------------------------------------- -ForegroundColor Green Write-Host " Clustered Hyper-V Eventlog Search" -ForegroundColor White Write-Host -------------------------------------------------------------------------------------------------------------------------------------- -ForegroundColor Green Write-Host `n 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 "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 $ClusterNodes = Get-ClusterNode $Domain = (Get-WmiObject Win32_ComputerSystem).Domain $DomainNodes = foreach ($Node in $ClusterNodes) { $Node.Name + "." + $Domain } # Variable Setup $TotalVMHostMemory = $False $TotalUsableVMHostMemory = $False $VirtMemory = $False $NonClusteredVMs = $False 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 $ClusterNodes) { [PSCustomObject]@{ Name = $Node.Name TotalMemory = [math]::Round( (Get-WmiObject Win32_ComputerSystem -ComputerName $Node.Name).TotalPhysicalMemory /1GB ) AvailableMemory = [math]::Round(( (Get-WmiObject Win32_OperatingSystem -ComputerName $Node.Name).FreePhysicalMemory ) /1024 /1024 ) UsableMemory = [math]::Round( (Get-Counter -ComputerName $Node.Name -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 = $ClusterNodes.Count $SingleNodeVirtMemory = [math]::Round($VirtMemory/$Nodecount) $SingleNodeMemory = $VMHostMemory.TotalMemory[0] $Nodecheck = $TotalVMHostMemory / $SingleNodeMemory $UsableMemoryAfterFailure = ($TotalUsableVMHostMemory + $SingleNodeVirtMemory) $HAMemory = $SingleNodeMemory - $UsableMemoryAfterFailure # Clear any old jobs out related to this script. Get-Job | Where-Object Command -like *Get-VM* | Remove-Job # Setup ScriptBlock for Invoke-Command. $GetVMScriptBlock = { Get-VM | Where-Object IsClustered -EQ $False } # Use jobs to pull event logs from all cluster nodes at the same time. Invoke-Command -ComputerName $DomainNodes -ScriptBlock $GetVMScriptBlock -AsJob | Wait-Job | Out-Null # Collect eventlogs from jobs and assign to $EventLogs $NonClusteredVMs = Get-Job | Where-Object Command -like *Get-VM* | Receive-Job # Sort Nonclustered VMs by their state for readability. $NonClusteredVMsSorted = $NonClusteredVMs | Sort-Object State 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 has $Nodecount nodes." Write-Host "===========================================" -ForegroundColor DarkGray } # Print Node Memory Report Write-Host " $TotalVMHostMemory GB - Physical memory of cluster." Write-Host " $SingleNodeMemory GB - Physical memory of each node." Write-Host " $UsableMemoryAfterFailure GB - Useable 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 would survive single failure." -ForegroundColor Green } Write-Host "===========================================" -ForegroundColor DarkGray # Checks if nonclustered VMs exist and prints list. if ($Null -eq $NonClusteredVMs) { Write-Host " All VMs are clustered." -ForegroundColor Green Write-Host "-------------------------------------------" -ForegroundColor DarkGray } else { Write-Host " VMs NOT in cluster." -ForegroundColor Yellow Write-Host "-------------------------------------------" -ForegroundColor DarkGray } # Prints nonclustered VMs. foreach ($VM in $NonClusteredVMsSorted) { $VMOutput = " " + $VM.ComputerName + " - " + $VM.State + " - " + $VM.Name Write-Host $VMOutput -ForegroundColor Yellow } } 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) { # 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 $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 -like $ClusterPath | Select-Object -Property FriendlyVolumeName -ExpandProperty Partition $VolumeBlock = Get-Volume | Where-Object Path -like $AccessPathVolumeID $CSVState = (Get-ClusterSharedVolumeState | Where-Object VolumeFriendlyName -Like $FriendlyPath)[0] if ($OSVersion -eq 10) { $QOS = Get-StorageQosVolume | Where-Object MountPoint -Like *$ClusterPath* [PSCustomObject]@{ "#" = $CSV.DiskNumber Block = $VolumeBlock.AllocationUnitSize CSVName = $CSVState.Name ClusterPath = $ClusterPath "Used(GB)" = [math]::Round($ClusterSharedVolume.UsedSpace /1GB) "Size(GB)" = [math]::Round($ClusterSharedVolume.Size /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 = $CSVState.Name ClusterPath = $ClusterPath "Used(GB)" = [math]::Round($ClusterSharedVolume.UsedSpace /1GB) "Size(GB)" = [math]::Round($ClusterSharedVolume.Size /1GB) "Free %" = [math]::Round($ClusterSharedVolume.PercentFree, 1) } } } } catch { Write-Host "Couldn't process Cluster Shared Volume data!" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } } elseif ($MenuChoice -eq 4) { $Volumes = Get-Volume | Where-Object DriveLetter -NE $null $results = foreach ($disk in $Volumes) { [PSCustomObject]@{ Drive = $disk.DriveLetter Label = $disk.FileSystemLabel 'Free(GB)' = [math]::Round($disk.SizeRemaining /1GB) 'Size(GB)' = [math]::Round($disk.Size /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,"Used(GB)","Size(GB)","Free %" | Sort-Object "#" | Format-Table -AutoSize } 3 { $results | Select-Object "#",CSVName,ClusterPath,"Size(GB)",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. #> [CmdletBinding()] param() 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 -------------------------------------------------------- -ForegroundColor Green $MenuChoice = Read-Host "Menu Choice" # Filter for IPv4 addresses [Regex]$IPv4 = '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b' # Pull Cluster node data for script. Write-Host `n Write-Host "Gathering data from VMs... Please be patient." -ForegroundColor White if (Get-ClusterCheck) { $ClusterNodes = Get-ClusterNode -ErrorAction Stop $Domain = (Get-WmiObject Win32_ComputerSystem).Domain $DomainNodes = foreach ($Node in $ClusterNodes) { $Node.Name + "." + $Domain } # Clear any old jobs out related to this script. Get-Job | Where-Object Command -like *Get-VM* | Remove-Job # Setup ScriptBlock for Invoke-Command. $VMInfoPull = { Get-VM | Select-Object ComputerName,VMName,ProcessorCount,MemoryStartup } # Use psjobs to pull VM data from all cluster nodes at the same time. Invoke-Command -ComputerName $DomainNodes -ScriptBlock $VMInfoPull -AsJob | Wait-Job | Out-Null # Collect VM data from jobs and assign to $VMs $VMs = Get-Job | Where-Object Command -like *Get-VM* | Receive-Job } else { $VMs = Get-VM | Select-Object ComputerName,VMName,ProcessorCount,MemoryStartup } # Collects information from VMs and creates $VMInfo variable with all VM info. try{ $VMInfo = 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) { $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 } } } } } catch { Write-Host "Couldn't collect information from the VMs!" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } # Prints report based on $MenuChoice. switch ($MenuChoice) { 1 { $VMInfo | Sort-Object Host | Format-Table -AutoSize } 2 { $VMInfo | Sort-Object Host | Format-Table -AutoSize } 3 { $VMInfo | Sort-Object VMName | Format-Table -AutoSize } default { Write-Host "Incorrect Choice. Choose a number from the menu." Start-Sleep -Seconds 3 Get-HyperVStorageReport } } } |