Creating Dashboards for Vester


In my first post about Vester, I ended the post with a number of items that needs further investigation. On top of my list is some kind of reporting function. After submitting an Invoke-Vester command lots of information scrolls over the screen.

Figure 1. – Output Invoke-Vester

Most administrators will not agree with an unseen remediation of the errors found and desire some kind of overview. It would also be nice to have some kind of overview while running Invoke-Vester as a scheduled job. Fortunately, one of my colleagues (Thank you Alex!) gave me the idea to create a dashboard. As there are many monitoring and dashboards product available like Grafana and Graphite there is also the PowerShell Universal Dashboard module. The PowerShell Universal Dashboard comes in a licensed Enterprise Edition and a free Community edition, documentation can be found here.

Installation is done by installing the module:

Install-Module UniversalDashboard.Community -AcceptLicense

To test UD, run the following code

$MyDashboard = New-UDDashboard -Title "Hello, World" -Content {

New-UDCard -Title "Hello, my first universal dashboard!"


Start-UDDashboard -Port 10000 -Dashboard $MyDashboard -Name 'HelloDashboard'

Start a browser and enter URL: http://localhost:10000, this should show this:

Figure 2.

For a nice introduction in Universal Dashboard, please read this post by Nicolas Prigent.


The following code is my first version for Vester Dashboards. Some features:

  • Support for multiple vCenters and/or vCenters with multiple configuration files
  • The Home page shows the status of a vCenter in colored tiles (Green = OK, Yellow = Errors found, Red = VC not responding)
  • A separate page shows an overview of the errors found with the actual and desired values. Am effective filter function is included.

The homepage looks like this.

Figure 3. Home page

From the home page, you can jump to the corresponding Errors.

Figure 4. Error page

The function New-VesterDashboard desires three input parameters; $Labels, $VCs and $Configs.
$Labels is used to create a unique instance for every combination of a vCenter Server and a configuration file.
$VCs contains the FQDN or IP addresses of the vCenter Server(s) and
$Configs contains the path to .json files used by Vester.
In the first loop (row 44-84), after a successful connection to the vCenter Server (row48), Invoke-Vester will run (row 56-61). This version supports one set of credentials. Unsuccessful connections are handled in row 49-55.

After disconnecting from vCenter (row 61) the output of Invoke-Vester ($Vester) is used to create a data grid for the errors found (row 67-81).

Before creating a new dashboard, the current one will be stopped (row 86).

The homepage of the new dashboard is created in row 94-124.
The Error pages are created in row 126-141. Finally the new dashboard is started in row 143-145.

# Functions
function New-VesterDashboard {
    <# .SYNOPSIS 
        This function runs the Invoke-Vester command on one or more combinations of 
        a vCenter Server and a Vester Configuration file and creates a Dashboard.
        This version expects the input in the form of 3 arrays: 
        [Array]Labels contains an unique label 
        [Array]VCs contains the FQDN or IP address of a vCenter Server 
        [Array]Configs contains the path to the Vester Config file 
        Vester config files must be created using New-VesterConfig 
    .EXAMPLE PS> $Labels = @("VC_A","VC_B")
        PS> $VCs = @("vcA.virtual.local","vcB.virtual.local")
        PS> $Configs = @("C:\Temp\ConfigA.json","C:\Temp\ConfigB.json")
        PS> New-VesterDashboard $Labels $VCs $Configs
        This example creates a new dahboard for two vCenter / Configurations.
        Open a browser and add URL: http://localhost:10000
        To stop a dahboard run the following command
        PS> Get-UDDashboard -Name 'VesterDashboard' | Stop-UDDashboard

	param (
    process {
        [Array]$ConnectionStatus = @() # Result of Connect-VISeerver (Boolean)
        [Array]$Vesters = @()          # Result of Invoke-Vester
        [Array]$DateTimes = @()        # Date & Time Invoke-Vester completed
        [Array]$VCColors = @()         # Background color VCS on Home page
        [Array]$GridDatas = @()        # Grids with Errors
        [Array]$Pages = @()            # Error pages for each vCenter
        $i = 0                         # counter used in foreach loop
        foreach ($Label in $Labels) {
            # Run Invoke-Vester
            if (!$creds) {$creds = Get-Credential "administrator@vsphere.local"}
            if (!(Connect-VIServer -Server $VCs[$i] -Credential $creds -ErrorAction SilentlyContinue)) {
                Write-Host "vCenter Server not reachable: " $VCs[$i]
                $ConnectionStatus += $false
                $Vesters += , $false
                $DateTimes += Get-Date
                $VCColor = 'red'
                $VCColors += $VCColor
                $GridDatas += , $false
            } else {
                $ConnectionStatus += $true
                $Vester = Invoke-Vester -Config $Configs[$i] -PassThru
                $Vesters += , $Vester
                $DateTimes += Get-Date
                Disconnect-VIServer -Server $VCs[$i] -Force -Confirm:$false
                # Process Data
                if ($Vester.FailedCount -ne 0) { $VCColor = 'yellow' }
                if ($Vester.FailedCount -eq 0) { $VCColor = 'green' }
                $VCColors += $VCColor
                # Create DataGrid
                $GridData = $Vester.TestResult | Where-Object {$_.Passed -eq $false} |
                ForEach-Object {
                    # Split Name field
                    Type     = $_.Name.Split(" ")[0]
                    Name     = $_.Name.Split(" ")[1]
                    Test     = $_.Name.Split("-")[1].Substring(1)
                    # Convert Errorcode to String, Split on linebreak and remove first part
                    Desired  = $_.ErrorRecord.ToString().Split([Environment]::NewLine)[0].Substring(11)
                    Actual   = $_.ErrorRecord.ToString().Split([Environment]::NewLine)[1].Substring(11)
                    Synopsis = $_.ErrorRecord.ToString().Split([Environment]::NewLine)[2].Substring(11)
                $GridDatas += , $GridData
        # Stop old dashboard
        Get-UDDashboard -Name 'VesterDashboard' | Stop-UDDashboard
        # Create New Dashboard
        $FontColor = 'Black'
        # Footer
        $NavBarLinks = @((New-UDLink -Text "About" -Url "" -Icon question_circle),
                         (New-UDLink -Text "More info" -Url "" -Icon book))
        $Footer = New-UDFooter -Copyright "Copyright: Paul Grevink"
        $i = 0 # Index used in $Homepage
        $HomePage = New-UDPage -Name "Home" -Icon home -DefaultHomePage -Content { 
          New-UDRow {
            New-UDColumn -Size 12 {
              New-UDLayout -Columns 2 -Content {
                foreach ($Label in $Labels) {
                    New-UDCard -Title $VCs[$i] -Content {
                    if ($ConnectionStatus[$i]) {
                      New-UDParagraph -Text "Label : $($Labels[$i])" -Color $FontColor
                      New-UDParagraph -Text "Config: $($Configs[$i])" -Color $FontColor
                      New-UDParagraph -Text "=== Test Results of $($DateTimes[$i]) ==="  -Color $FontColor
                      New-UDParagraph -Text "Total   : $($Vesters[$i].TotalCount)"    -Color 'Black'
                      New-UDParagraph -Text "Passed  : $($Vesters[$i].PassedCount)"   -Color 'Black'
                      New-UDParagraph -Text "Failed  : $($Vesters[$i].FailedCount)"   -Color 'Red'
                      New-UDParagraph -Text "Skipped : $($Vesters[$i].SkippedCount)"  -Color 'Black'
                      New-UDParagraph -Text "Pending : $($Vesters[$i].PendingCount)"  -Color 'Black'} else {
                      New-UDParagraph -Text "Label : $($Labels[$i])" -Color $FontColor
                      New-UDParagraph -Text "Config: $($Configs[$i])" -Color $FontColor
                      New-UDParagraph -Text "=== Connection Failed at $($DateTimes[$i]) ==="  -Color $FontColor
                      New-UDParagraph -Text "."    -Color 'Black'
                      New-UDParagraph -Text "."    -Color 'Black'
                      New-UDParagraph -Text "."    -Color 'Black'
                      New-UDParagraph -Text "."    -Color 'Black'
                      New-UDParagraph -Text "."    -Color 'Black'
                    } -FontColor $FontColor -BackgroundColor $VCColors[$i] -Links (New-UDLink -Text "See Errors" -Url $Labels[$i] -Icon book)
                  $i ++
        # Error pages
        $Pages += , $HomePage # $HomePage is the first page
        $i = 0 # Index used in $Pages
        foreach ($Label in $Labels) {
            $Page = New-UDPage -Name $Labels[$i] -Icon grav -Content {
                if($ConnectionStatus[$i]) {
                    New-UDRow {
                        New-UDColumn -Size 12 {
                            New-UDGrid -Title "Errors $($VCs[$i])"  -Headers @("Type", "Name", "Test", "Desired", "Actual", "Synopsis") -Properties @("Type", "Name", "Test", "Desired", "Actual", "Synopsis") -Endpoint {
                            $GridDatas[$i] | Out-UDGridData } -FontColor "black"  
            $Pages += , $Page
            $i ++
        #Start presenting Dashboard
        Start-UDDashboard -Content { 
            New-UDDashboard -NavbarLinks $NavBarLinks -Title "Vester Dashboard" -NavBarColor 'Orange' -NavBarFontColor 'Ẃhite' -BackgroundColor "#FF333333" -FontColor "#FFFFFFF" -Pages $Pages -Footer $Footer
        } -Port 10000 -Name 'VesterDashboard'
} # eof function

# Main
# Labels MUST be unique values
$Labels = @("VC_A","VC_B")
$VCs = @("vcA.virtual.local","vcB.virtual.local")
$Configs = @("C:\Temp\ConfigA.json","C:\Temp\ConfigB.json")

New-VesterDashboard $Labels $VCs $Configs

As this is only the first version of the script, many improvements and additions are possible and needed (validation!). Universal Dashboards offers a large amount of possibilities, many of which I haven’t tried out yet.

Suppose, in an Error page, you can select items and run remediation?

As always, I thank you for reading and welcome your comments.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: