Save to My DOJO
In a recent article, I showed you how I built a PowerShell function to display information about the status of a Hyper-V host. The function alone I hope is useful. But what good would it be if that is all it does? Actually, let me rephrase that – what else can I do with that function? The whole point of PowerShell is that it provides a large set of building blocks that you can assemble to achieve the desired result.
It can be as simple as the illustrated command above. For more complex tasks and especially those that you want to repeat, you will take the same commands you would run at a prompt and put them into a PowerShell script. That’s what I want to show you today. Using the Get-VMHostStatus function, here are 3 different ways you might leverage it. I won’t go into great detail on the code as I’ve added internal comments. And as with all my articles, pay as much attention to the techniques and concepts as much as the end result. You may not have a need for any of these scripts as they exist, but you might pick up an idea that you can use in building your own Hyper-V PowerShell tools.
Creating an HTML Report
My first use is to take the output from Get-VMHostStatus and create an HTML report. Even though the function is creating an object with a lot of information, I can pick and choose what I want. I can even gather additional information. The script is essentially a wrapper for Get-VMHostStatus. It runs the command and creates a series of HTML fragments which is then assembled into a final file. The script even embeds a CSS style sheet.
#requires -version 5.1 [cmdletbinding()] Param( [Parameter(Position = 0 , Mandatory, HelpMessage = "Enter the name of a Hyper-V Host")] [string]$ComputerName, [Paramater(ParameterSetName = "Computername")] [PSCredential]$Credential, [ValidatePattern("w+.(html|htm)$")] [string]$Path = ".HyperVHostStatus.htm" ) #region get VM Host Status #dot source the required script with the Get-VMHostStatus function . C:scriptsGet-VMHostStatus.ps1 #remove the Path from boundparameters since it isn't part of the parameters #for Get-VMHostStatus and I want to eventually splat $PSBoundParameters if ($PSBoundParameters.ContainsKey("Path")) { $PSBoundParameters.Remove("Path") | Out-Null } #get the VMHost data passing bound parameters Try { $data = Get-VMHostStatus @PSBoundParameters -ErrorAction Stop } Catch { Throw $_ } #endregion #region define the CSS style in the head section #get sample CSS from https://github.com/jdhitsolutions/SampleCSS $head = @" <Title>Hyper-V Host Status</Title> <style> @charset "UTF-8"; body { background-color: rgb(233, 223, 223); font-family: Monospace; font-size: 12pt; } td,th { border: 0px solid black; border-collapse: collapse; white-space: pre; } th { color: white; background-color: black; } table,tr,td,th { padding: 3px; margin: 0px; white-space: pre; } tr:nth-child(odd) { background-color: lightgray } table { margin-left: 25px; width: 50%; } h2 { font-family: Tahoma; } .footer { color: green; margin-left: 25px; font-family: Tahoma font-size: 7pt; font-style: italic; } </style> "@ #endregion #region get some additional data $s = New-PSSession @PSBoundParameters $procdata = Invoke-command -ScriptBlock {Get-Ciminstance Win32_Computersystem -Property 'NumberOfLogicalProcessors', 'NumberOfProcessors'} -session $s $hostdetail = Invoke-Command -ScriptBlock {Get-Ciminstance Win32_OperatingSystem -property "Caption", "Version"} -session $s $detail = "{0} version {1}" -f $hostdetail.caption, $hostdetail.version Remove-PSSession $s #endregion #region define the pieces of the HTML report as fragments $fragments = @() $fragments += "<H1>Hyper-V Host Status</H1>" $fragments += "<H2 title = '$detail'>$($data.computername)<H2>" $fragments += "<H3>Memory</H3>" $fragments += $data | Select-object -Property *memory*, TotalPctDemand | ConvertTo-Html -Fragment -as List $fragments += "<H3>Processor</H3>" $fragments += $data | Select-Object -Property @{Name = "ProcessorCount"; Expression = {$procdata.NumberOfProcessors}}, @{Name = "LogicalProcessorCount"; Expression = {$procdata.NumberOfLogicalProcessors}}, PctProcessorTime, Logical* | ConvertTo-Html -Fragment -as list $fragments += "<H3>Virtual Machines</H3>" $fragments += $data | Select-Object -Property *VMs | ConvertTo-Html -Fragment -as List $fragments += "<H3>Virtual Machine Health</H3>" $fragments += $data | Select-Object -Property Critical, Healthy | ConvertTo-Html -Fragment -as table #a future version might include additional network-related values $fragments += "<H3>Networking</H3>" $fragments += $data | Select-Object VMSwitchBytesSec, VMSwitchPacketsSec | ConvertTo-Html -Fragment $fragments += "<H3>Other</H3>" $fragments += $data | Select-Object Uptime, TotalProcesses, PctFreeDisk| ConvertTo-Html -Fragment -as table #endregion #region create an object with footer information so it can be displayed neatly [xml]$meta = [pscustomobject]@{ "Report run" = (Get-Date) Author = "$env:USERDOMAIN$env:USERNAME" Script = (Convert-Path $MyInvocation.InvocationName).Replace("", "/") ScriptVersion = '0.9.3' Source = $env:COMPUTERNAME } | ConvertTo-Html -Fragment -As List $meta.CreateAttribute("Class") | Out-Null $meta.table.SetAttribute("class", "footer") #endregion #region assemble the final HTML report ConvertTo-Html -Head $head -body $fragments -postcontent "<br><br>$($meta.innerxml)" | Out-File -FilePath $Path -Encoding utf8 Write-Host "See $(Convert-Path $path) for the finished report file." -ForegroundColor green #endregion
The script will create a single HTML report per Hyper-V Host.
C:scriptsGet-VMHostStatusReport.ps1 -computername chi-p50 -path $env:tempchi-p50-status.html
The end result looks like this:
One thing I want to point out is that I’ve added some metadata at the bottom of the report to indicate how and where the file originated from. This is the type of script you could set up as a PowerShell Scheduled Job. Maybe even emailing the results. Over time and as staff turns over, people may forget where the report is coming from. Adding the metadata helps you keep tabs on where the script is running.
Extending the PowerShell Prompt
Let’s say managing Hyper-V is a primary job function. If you are like me you may have a PowerShell window open constantly. I’ve been writing on my blog a number of articles that demonstrate how to turn your PowerShell prompt into a monitoring tool. I realized I could do something similar with a Hyper-V emphasis.
Function prompt { #define a hashtable of symbols to use $charHash = @{ Up = [char]0x25b2 Down = [char]0x25bc Delta = [char]0x2206 Pointer = [char]0x25BA Pointerleft = [char]0x25C4 TopLeft = [char]0x250c TopRight = [char]0x2510 Border = [char]0x2500 BottomLeft = [char]0x2514 BottomRight = [char]0x2518 Ohm = [char]0x2126 Mu = [char]0x3bc disk = [char]0x058d bps = [char]0x20bf } Try { #verify there is a global hashtable variable Get-Variable -Name rsHash -Scope global -ErrorAction Stop | Out-Null } Catch { #create the runspace and synchronized hashtable $global:rsHash = [hashtable]::Synchronized(@{Computername = $env:computername; results = ""; date = (Get-Date); computers = @()}) $newRunspace = [runspacefactory]::CreateRunspace() #set apartment state if available if ($newRunspace.ApartmentState) { $newRunspace.ApartmentState = "STA" } $newRunspace.ThreadOptions = "ReuseThread" $newRunspace.Open() $newRunspace.SessionStateProxy.SetVariable("rsHash", $rsHash) $pscmd = [PowerShell]::Create().AddScript( { #define scriptblock to run in the background $sb = { Param($sessions) #turn off Write-Progress to speed things up a bit $ProgressPreference = "silentlycontinue" #dot source the script file . C:scriptsGet-VMHostStatus.ps1 #run the function using the PSSessions Get-VMHostStatus $sessions } #define the Hyper-V Hosts to query as a comma separated list $Computers= $env:computername #or pull names in from a text file # $computers = Get-Content c:scriptshvhosts.txt do { #reset results #make sure there are sessions for all computers in the list $computers | Where-Object {(get-pssession).where( {$_.state -eq 'opened'}).computername -notcontains $_} | ForEach-Object { New-PSSession -ComputerName $_ } Get-PSSession | Where-Object {$_.state -eq 'broken'} | foreach-object { Remove-PSSession $_ #attempt to recreate it New-PSsession -ComputerName $_.computername } $results = Invoke-Command -ScriptBlock $sb -ArgumentList @(, $(Get-PSSession)) $global:rsHash.results = $results $global:rsHash.date = Get-Date $global:rshash.computers = $computers #set a sleep interval between tests Start-Sleep -Seconds 10 } While ($True) }) # script $pscmd.runspace = $newrunspace [void]$psCmd.BeginInvoke() } #catch if (-Not $global:rsHash.results) { #define a working message that has the string: Working $workingmsg = " Working....please wait" $working = $True } else { $data = $global:rsHash.results $working = $False #get the length of the longest host name for padding purposes $pad = ($data.computername | Sort-object {$_.length} -Descending | Select-Object -first 1).length } #Take a guess at how wide to make the border $wide = 68 #display the results in the console Write-Host "`n " -NoNewline Write-Host "Hyper-V Host Status $(Get-Date)" -BackgroundColor Gray -ForegroundColor Black Write-Host $charHash.TopLeft -NoNewline Write-Host $($charHash.Border.tostring() * $wide) -NoNewline Write-Host $charHash.TopRight if ($working) { Write-Host $workingmsg -ForegroundColor Yellow } else { foreach ($name in $($global:rsHash.computers)) { $hvhost = $data | where {$_.computername -eq $name} if (-not $Hvhost) { #create an empty placeholder $hvhost = [psobject]@{ Uptime = New-timespan RunningVMs = 0 OffVMs = 0 PausedVMs = 0 SavedVMs = 0 PctMemoryFree = 0 TotalPctDemand = 0 PctFreeDisk = 0 } } $hostinfo =" $($charHash.Pointer) {0}" -f $name.Padright($pad," ") Write-Host $hostInfo -NoNewline if ($hvhost.Uptime.totalseconds -gt 0) { write-Host " $($charHash.up)" -ForegroundColor Green -NoNewline } else { Write-Host " $($charHash.Down)" -ForegroundColor Red -NoNewline } write-Host (" {0:ddd.hh:mm:ss}" -f $hvhost.Uptime) -NoNewline Write-Host " $($charhash.up) $($hvhost.runningVMs)" -ForegroundColor Green -NoNewline Write-Host " $($charhash.down) $($hvhost.offVMs)" -ForegroundColor red -NoNewline Write-Host " $($charhash.pointer)$($charHash.pointerleft) $($hvhost.pausedVMs)" -ForegroundColor magenta -NoNewline Write-Host " $($charHash.pointerleft)$($charhash.pointer) $($hvhost.savedVMs)" -ForegroundColor yellow -NoNewline #define some threshholds which will be reflected in color if ($hvhost.PctMemoryFree -le 20) { $fg = "red" } elseif ($hvhost.PctMemoryFree -le 60) { $fg = "yellow" } else { $fg = "green" } Write-Host " $($charHash.mu)$(($hvhost.pctMemoryFree.tostring()).Padleft(5,' '))%" -NoNewline -ForegroundColor $fg Write-Host " $($charhash.delta)$(($hvhost.TotalPctDemand.tostring()).Padleft(5,' '))" -NoNewline if ($hvhost.PctFreeDisk -le 20) { $fg = "red" } elseif ($hvhost.PctFreeDisk -le 40) { $fg = "yellow" } else { $fg = "green" } #format pctFreeDisk [string]$p =[math]::round($hvhost.pctFreeDisk,2) Write-Host (" $($charHash.disk){0}" -f $p.padleft(5,' ')) -ForegroundColor $fg } } # Add the bottom of the border Write-Host $charHash.BottomLeft -NoNewline Write-Host $($charHash.Border.tostring() * $wide) -NoNewline Write-Host $charHash.BottomRight "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) " } #close function
You will need to specify the location and filename of the script file with the Get-HVHostStatus prompt and you might need to define $computers with a comma-separated list of server names. The function defaults to the localhost. I would keep this list to a handful of server names.
To use this, save the script to a file and dot source it in your PowerShell session.
. C:scriptshvprompt.ps1
Or dot source it in your PowerShell profile script. When the prompt initially loads you’ll see a “Working” message. In the background the prompt is spinning up a separate runspace, running Get-VMHostStatus and passing the results via a synchronized hash table. Yeah, this is a bit advanced so if you feel lost that is to be expected. But once you make the necessary changes for your environment you should see something like this:
If the host is offline, you’ll see a red downward pointing indicator and 0 in all the values. Reading from left to right this is what the prompt is showing:
- Computername
- Up/Down indicator
- Uptime
- Number of running virtual machines
- Number of stopped virtual machines
- Number of paused virtual machines
- Number of saved virtual machines
- The percent free memory (color coded)
- The total percent memory demand
- The percent free disk space for the default storage drive (color coded)
This isn’t necessarily real-time data. In the background, the information is getting updated every 10 seconds. You may want to increase that value. Every time you press Enter you are getting the last obtained values.
Creating a Graphical Monitor
The last solution is a bit more complicated. I wanted to take advantage of my Convertto-WPFGrid function which I’ve written about before. I wanted to use it as a standalone graphical reporting tool that you could kick off from PowerShell yet wouldn’t block your PowerShell prompt. This was more complicated than I expected and ended up building a set of functions that start the WPF (Windows Presentation Foundation) code in a background runspace. The WPF display can be managed through the synchronized hashtable. But I didn’t want to force you to know how to modify the hashtable so I wrote a few helper functions. Here’s the complete script file.
# requires -version 5.1 <# MonitorHVHost.ps1 This is NOT a module so you will need to dot source the script file into your PowerShell session. You can then run Start-HVHostMonitor to kick off a WPF GUI. Use Set-HVHostMonitor to adjust it and Stop-HVHostMonitor to clean up. Allow a little time for changes to be updated. The properties are from Get-VMHostStatus Computername : CHI-P50 Uptime : 00:46:26.8179350 PctProcessorTime : 3.84799996455453 TotalMemoryGB : 64 PctMemoryFree : 77.16 TotalVMs : 13 RunningVMs : 4 OffVMs : 9 SavedVMs : 0 PausedVMs : 0 OtherVMs : 0 Critical : 0 Healthy : 13 TotalAssignedMemoryGB : 10.939453125 TotalDemandMemoryGB : 5.6279296875 TotalPctDemand : 8.81 PctFreeDisk : 27.3125310680432 VMSwitchBytesSec : 170332.7095065 VMSwitchPacketsSec : 263.120591389896 LogicalProcPctGuestRuntime : 4.11198442672914 LogicalProcPctHypervisorRuntime : 0.28358272876852 TotalProcesses : 88 #> Function Start-HVHostMonitor { [cmdletbinding()] Param( [Parameter(Position = 0, HelpMessage = "The names of the Hyper-V Hosts to monitor")] [ValidateNotNullorEmpty()] [alias("cn", "host")] #Hyper-V hosts to monitor [string[]]$Computername = $env:COMPUTERNAME, #properties to display from Get-VMHostStatus [Parameter(HelpMessage = "The properties from Get-VMHostStatus")] [ValidateNotNullorEmpty()] [string[]]$Properties = @("Computername", "Uptime", "*VMs", "Pct*"), [Parameter(HelpMessage = "The form height")] [int]$Height = 160, [Parameter(HelpMessage = "The form width")] [int]$Width = 970, #the windows title [Parameter(HelpMessage = "The form title")] [string]$Title = "Hyper-V Host Status", [Parameter(HelpMessage = "The refresh interval in seconds")] [int]$Timeout = 30 ) #define a synchronized hashtable that can be used to "communicate" with the background runspace $global:rsHash = [hashtable]::Synchronized(@{ #Hyper-V hosts to monitor computername = @($Computername) #properties to display properties = @($Properties) height = $height width = $width title = $Title timeout = $Timeout CenterScreen = $False #set to false to stop the display Run = $True #these properties are for troubleshooting and development Data = @() Updated = (Get-Date) Timer = "" JobID = 0 Job = "" }) $newRunspace = [RunspaceFactory]::CreateRunspace() $newRunspace.ApartmentState = "STA" $newRunspace.ThreadOptions = "ReuseThread" $newRunspace.Open() $newRunspace.SessionStateProxy.SetVariable("rsHash", $global:rsHash) $psCmd = [PowerShell]::Create().AddScript( { # It may not be necessary to add these types but it doesn't hurt to include them Add-Type -AssemblyName PresentationFramework Add-Type -assemblyName PresentationCore Add-Type -AssemblyName WindowsBase # !!! You will need to update the path to your copy of Get-VMHostStatus.ps1 !!!# # !!! The scriptblock should have a period then a space then the path to the ps1 file !!!# $script:Init = { . "C:scriptsGet-VMHostStatus.ps1"} Function _startjob { #this is a private internal function used to start a data gathering job in the background Start-Job -ScriptBlock { Param($hosts) #ignore errors for offline or bad hosts Get-VMHostStatus -Computername $hosts -ErrorAction SilentlyContinue } -InitializationScript $script:Init -ArgumentList @(, @($global:rsHash.computername)) $global:rsHash.jobid = $script:j.id } #get initial data $script:j = _startjob Do { #region WPF code # define a timer to automatically dismiss the form. The timer uses a 5 second interval tick if ($global:rsHash.Timeout -gt 0) { Write-Verbose "Creating a timer" $timer = new-object System.Windows.Threading.DispatcherTimer $terminate = (Get-Date).AddSeconds($global:rsHash.timeout) Write-verbose "Form will close at $terminate" $timer.Interval = [TimeSpan]"0:0:5.00" $timer.add_tick( { if ((Get-Date) -ge $terminate) { if ($global:rsHash.run) { $timer.stop() $data = $script:j | Wait-Job -ov w | Receive-Job | Select-Object $global:rsHash.Properties -OutVariable hash $global:rshash.job = $w $global:rsHash.data = $hash $global:rsHash.updated = (Get-Date) $datagrid.Clear() $DataGrid.ItemsSource = $hash $form.title = "$($global:rsHash.title) [Last Updated $(Get-Date)]" $form.Height = $global:rsHash.Height $form.Width = $global:rsHash.Width $terminate = (Get-Date).AddSeconds($global:rsHash.timeout) #kick off the next job $script:j = _startjob $global:rsHash.timer = (Get-Date) $timer.Start() $form.UpdateLayout() } else { $form.close() } } }) } $form = New-Object System.Windows.Window #define what it looks like $form.Title = "$($global:rsHash.title) [Last Updated $(Get-Date)]" $form.Height = $global:rsHash.Height $form.Width = $global:rsHash.Width if ($global:rsHash.CenterScreen) { Write-Verbose "Form will be center screen" $form.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen } #define a handler when the form is loaded. The scriptblock uses variables defined later #in the script $form.add_Loaded( { foreach ($col in $datagrid.Columns) { #because of the way I am loading data into the grid #it appears I need to set the sorting on each column $col.CanUserSort = $True $col.SortMemberPath = $col.Header } $datagrid.Items.Refresh() $form.focus }) #Create a stack panel to hold the datagrid $stack = New-object System.Windows.Controls.StackPanel #create a datagrid $datagrid = New-Object System.Windows.Controls.DataGrid $datagrid.VerticalAlignment = "Bottom" #adjust the size of the grid based on the form dimensions $datagrid.Height = $form.Height - 50 $datagrid.Width = $form.Width - 50 $datagrid.CanUserSortColumns = $True $datagrid.CanUserResizeColumns = $True $datagrid.CanUserReorderColumns = $True $datagrid.AutoGenerateColumns = $True #enable alternating color rows $datagrid.AlternatingRowBackground = "gainsboro" $stack.AddChild($datagrid) $form.AddChild($stack) #endregion #show the form $data = $script:j | Wait-Job -ov w | Receive-Job | Select-Object $global:rsHash.Properties -OutVariable hash $global:rshash.job = $w $global:rsHash.data = $hash $global:rsHash.updated = (Get-Date) $DataGrid.ItemsSource = $hash #kick off the next job $script:j = _startjob If ($global:rsHash.Timeout -gt 0) { Write-Verbose "Starting timer" $timer.IsEnabled = $True $Timer.Start() } Write-Verbose "Displaying form" $form.Title = "$($global:rsHash.title) [Last Updated $(Get-Date)]" $form.ShowDialog() | Out-Null } while ($global:rsHash.Run) }) $psCmd.Runspace = $newRunspace $psCmd.BeginInvoke() | Out-Null $msg = @" Please wait a moment for the results to be displayed. To terminate run this command at your prompt" `$rsHash.run = `$False or run: Stop-HVHostMonitor Using runspace id $($newRunspace.Id) "@ Write-host $msg -ForegroundColor cyan #set a global variable for the runspace so it can later be cleaned up $global:hvmonrun = $newrunspace.id } #close Start function Function Stop-HVHostMonitor { [cmdletbinding(SupportsShouldProcess)] Param() if ($pscmdlet.ShouldProcess("Hyper-VHost Monitor")) { $global:rsHash.run = $False Remove-Variable -Name rshash -Scope global } $run = Get-Runspace -id $global:hvmonrun if ($pscmdlet.ShouldProcess("Runspace id $($run.id)", "Clean up")) { $run.Close() $run.Dispose() Remove-Variable -Name hvmonrun -Scope global } } #close Stop function Function Set-HVHostMonitor { [cmdletbinding(SupportsShouldProcess)] Param( [Parameter(Position = 0, HelpMessage = "The names of the Hyper-V Hosts to monitor")] #Hyper-V hosts to monitor [ValidateNotNullOrEmpty()] [alias("cn", "host")] [string[]]$Computername, #properties to display [Parameter(HelpMessage = "Update the properties from Get-VMHostStatus")] [ValidateNotNullorEmpty()] [string[]]$Properties, [Parameter(HelpMessage = "Update the form height")] [int]$Height, [Parameter(HelpMessage = "Update the form width")] [int]$Width, #the windows title [Parameter(HelpMessage = "Update yhe form title")] [string]$Title, [Parameter(HelpMessage = "Update the refresh interval in seconds")] [int]$Timeout ) #remove common parameters from bound parameters "Whatif", "Verbose", "ErrorAction" | foreach-object { if ($PSBoundParameters.ContainsKey($_)) { [void]$PSBoundParameters.Remove($_) } } $PSBoundParameters.GetEnumerator() | Foreach-Object { if ($pscmdlet.ShouldProcess($_.key, "Update monitor value")) { $global:rsHash[$_.key] = $_.value } } } #close Set function
This is currently written as a .PS1 file. It would not take much to turn this into a module. If you have the experience, you might want to take that extra step. Once you dot source the script (or import it as a module) you will have these commands:
- Start-HVHostMonitor
- Set-HVHostMonitor
- Stop-HVHostMonitor
You should be able to get help on any of them to see the syntax. To begin monitoring you can use the default parameter values. Or run it like this.
Start-HVHostMonitor -Computername CHI-P50,Bovine320,Think51 -Title "VM Hosts" -Timeout 60
This will start the process and after a brief moment, display a graphical table with selected properties from Get-VMHostStatus. In this example, the display will be updated every minute.
You can use Set-HVHostMonitor to adjust display properties on the fly. Although you might not see the results until the next refresh.
As long as the PowerShell session where you started it from is running, the form will be displayed. To terminate it, use the Stop-HVHostMonitor.
Your Turn
And there you have it: 3 different ways to use a single PowerShell command. Behind the scenes I’m executing my Get-VMHostStatus function against one or more Hyper-V hosts. From there I can use the data in whatever way meets my business needs. If you are still getting started with PowerShell, don’t try to create projects as complicated as these. Start simple. With time and experience you’ll soon be creating PowerShell Hyper-V management tools that you’ll wonder how you ever managed without. When you do, I hope you’ll share. I definitely would like to know what you are up to!
Need Help
I’ve mentioned this before, but if you need help with PowerShell or writing a PowerShell script, I encourage you to use the free forums at PowerShell.org.
Not a DOJO Member yet?
Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!
14 thoughts on "3 Awesome Uses for the Get-VMHostStatus PowerShell Function"
very helpful .
can we get report for all VM
I’m not sure if you are referring to one of the commands in this post or something more general.