Generating HTML Reports in PowerShell - Part 1

HTMLReport.jpg

The problemPowerShell is an amazing tool for gathering, collecting, slicing, grouping, filtering and collating data.  However, trying to show that information or several sets of it on one report is not as easy.  A few years we ago built our own solution, we created a set of HTML reporting functions.  I have been using these functions for years to help myself, my team and customers to deliver Powershell data to people that just need the details and not a CSV file or a code snippet. We've now decided to make these available to the rest of you.

This solution This is developed as a Powershell module.  It is now available through the PowerShell Gallery or can be installed from PowerShell using install-module -name ReportHTML. Additionally it can be accessed from github ReportHTML, download and deploy to an appropriate module directory for example 'C:\Users\User\Documents\WindowsPowerShell\Modules\' It can be deployed and run ad-hoc or with your scheduled report Powershell jobs.  There are several ways to build up a report. In this post we will build five example reports based around an Azure VMs array.

The report at a glance This screenshot of a report that shows information about patching results grouped in two sections, successful and failed patches. ReportingSS1

This screenshot of a report that shows multiple collapsible sections with an array of functions from within a file displayed in an open section. ReportingSS2

Here is a more complete report I have created at the end of this series in More, Part 5 just to give you an idea of what is possible.  I provide the script and this sample in the link above.  Please note there is java script to enable hiding of sections which does generate a warning in your browser.  I do explain this later in this post. SystemReportScreenShot

This Blog Series There are lots of different ways to leverage this module including changing the logos, highlighting rows, creating different sections with code loops and much more.  This first article is going to work through 5 examples to get you started.  Here are two versions of the same code detailed below or you can create your own script with the code snippets as we work through the examples. You will need to save the file to a local folder or provide a report output path parameter. - A version without all the comments Report-AzureVMsExamples_Part1 - A version with all the comments Report-AzureVMsExamples_Part1WithComments

Reporting functions summary There are a handful of functions that generate HTML code, you string this code together and then save the content as a file.  This code was originally borrowed from Alan Renouf for a vSphere healthcheck report by Andrew Storrs and myself for a more dynamic reporting style, being able to create reports on the fly with minimal effort. In addition these reports once built can be scheduled to run, dropped on a file share or emailed.  I will outline the main functions and then build a report collecting information about virtual machines from an Azure subscription.  will walk through several examples of how to use the functions to generate different types of reports.

  • Get-HtmlOpen
  • Get-HtmlClose
  • Get-HtmlContentOpen
  • Get-HtmlContentClose
  • Get-HtmlContentTable
  • Get-HtmlContentText
  • Set-TableRowColor
  • New-HTMLPieChartObject
  • New-HTMLPieChart
  • New-HTMLBarChartObject
  • New-HTMLBarChart
  • Get-HTMLColumn1of2
  • Get-HTMLColumn2of2
  • Get-HTMLColumnClose

Let's get started First let's create the header section. This contains some parameters, report file path, reportname, loads the ReportHTML module and checks for an Azure Account and if one isn't present asks displays a login prompt.

[powershell] param ( $ReportOutputPath )

Import-Module ReportHtml Get-Command -Module ReportHtml

$ReportName = "Azure VMs Report"

if (!$ReportOutputPath) { $ReportOutputPath = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent }

# see if we already have a session. If we don't don't re-authN if (!$AzureRMAccount.Context.Tenant) { $AzureRMAccount = Add-AzureRmAccount } [/powershell]

Building a recordset We will need a record set to work with.  I am going to take some code Barry Shilmover shared here and add resource group name as a property to or an array of VMs

[powershell] # Get arrary of VMs from ARM $RMVMs = get-azurermvm

$RMVMArray = @() ; $TotalVMs = $RMVMs.Count; $i =1

# Loop through VMs foreach ($vm in $RMVMs) { # Tracking progress Write-Progress -PercentComplete ($i / $TotalVMs * 100) -Activity "Building VM array" -CurrentOperation ($vm.Name + " in resource group " + $vm.ResourceGroupName)

# Get VM Status (for Power State) $vmStatus = Get-AzurermVM -Name $vm.Name -ResourceGroupName $vm.ResourceGroupName -Status

# Generate Array $RMVMArray += New-Object PSObject -Property @{`

# Collect Properties ResourceGroup = $vm.ResourceGroupName Name = $vm.Name; PowerState = (get-culture).TextInfo.ToTitleCase(($vmStatus.statuses)[1].code.split("/")[1]); Location = $vm.Location; Tags = $vm.Tags Size = $vm.HardwareProfile.VmSize; ImageSKU = $vm.StorageProfile.ImageReference.Sku; OSType = $vm.StorageProfile.OsDisk.OsType; OSDiskSizeGB = $vm.StorageProfile.OsDisk.DiskSizeGB; DataDiskCount = $vm.StorageProfile.DataDisks.Count; DataDisks = $vm.StorageProfile.DataDisks; } $I++ } [/powershell]

Testing the report We are just going to write a short function to generate and invoke the report file

[powershell] Function Test-Report { param ($TestName) $rptFile = join-path $ReportOutputPath ($ReportName.replace(" ","") + "-$TestName" + ".mht") $rpt | Set-Content -Path $rptFile -Force Invoke-Item $rptFile sleep 1 } [/powershell]

The process of building the report Let's run this line to see what happens and what output we get.

[powershell] Get-HtmlContentOpen -HeaderText "Virtual Machines" [/powershell]

You should see this HTML as output. Each function generates HTML code, incorporating the parameters you pass it. We are going to collect all this code into an array variable called $rpt.

[html]

<div class="section">

<div class="header"> <a name="Virtual Machines">Virtual Machines</a> </div>

<div class="content" style="background-color:#ffffff;"> [/html]

Building a Basic Report (Example 1) Let's generate a very quick report before we dive into some of the other features and functions.  We want to display the VM array in an table.

[powershell] ####### Example 1 ####### # Create an empty array for HTML strings $rpt = @()

# note from here on we always append to the $rpt array variable. # First, let's add the HTML header information including report title $rpt += Get-HtmlOpen -TitleText $ReportName

# This content open function add a section header $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines"

# This creates an HTML table of whatever array you pass into the function $rpt += Get-HtmlContentTable $RMVMArray

# This content close function closes the section $rpt += Get-HtmlContentClose

# This HTML close adds HTML footer $rpt += Get-HtmlClose

# Now let's test what we have Test-Report -TestName Example1 [/powershell]

RptExample1

Allow Blocked Content Depending on your browser settings you may receive an warning asking if you want to 'Allow blocked content'.  This is the Javascript function to optionally hide sections of the report. You can click allow blocked content or change your IE settings. ReportBlockContent

Order and grouping data (Example 2) Here we will select specific columns from the array, with the column we are grouping by being the first in the select statement

[powershell] ####### Example 2 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText $ReportName $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines"

# here we are going to filter the recordset, reorder the columns and group the results by location. # The value you group by must be first in the select statement $rpt += Get-HtmlContentTable ($RMVMArray | select Location, ResourceGroup, Name, Size,PowerState,DataDiskCount, ImageSKU ) -GroupBy Location $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example2 [/powershell]

ReportExample2

Creating more sections and hiding them (Example 3) Let's create a summary section and a section about VM size counts and we will hide two sections

[powershell] ####### Example 3 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText $ReportName

# adding the summary section $rpt += Get-HtmlContentOpen -HeaderText "Summary Information" $rpt += Get-HtmlContenttext -Heading "Total VMs" -Detail ( $RMVMArray.Count) $rpt += Get-HtmlContenttext -Heading "VM Power State" -Detail ("Running " + ($RMVMArray | ? {$_.PowerState -eq 'Running'} | measure ).count + " / Deallocated " + ($RMVMArray | ? {$_.PowerState -eq 'Deallocated'} | measure ).count) $rpt += Get-HtmlContenttext -Heading "Total Data Disks" -Detail $RMVMArray.datadisks.count $rpt += Get-HtmlContentClose

# adding the VM size section. Note the -IsHidden switch $rpt += Get-HtmlContentOpen -HeaderText "VM Size Summary" -IsHidden $rpt += Get-HtmlContenttable ($RMVMArray | group size | select Name, Count | sort count -Descending ) -Fixed $rpt += Get-HtmlContentClose

# Note I have also added the -IsHidden Switch here $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines" -IsHidden $rpt += Get-HtmlContentTable ($RMVMArray | select Location, ResourceGroup, Name, Size,PowerState, DataDiskCount, ImageSKU ) -GroupBy Location $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example3 [/powershell]

ReportExample3

Looping with foreach and section background shading (Example 4) We are going group the recordset by location and add a foreach loop.

[powershell] ####### Example 4 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText $ReportName $rpt += Get-HtmlContentOpen -HeaderText "Summary Information" $rpt += Get-HtmlContenttext -Heading "Total VMs" -Detail ( $RMVMArray.Count) $rpt += Get-HtmlContenttext -Heading "VM Power State" -Detail ("Running " + ($RMVMArray | ? {$_.PowerState -eq 'Running'} | measure ).count + " / Deallocated " + ($RMVMArray | ? {$_.PowerState -eq 'Deallocated'} | measure ).count) $rpt += Get-HtmlContenttext -Heading "Total Data Disks" -Detail $RMVMArray.datadisks.count $rpt += Get-HtmlContentClose $rpt += Get-HtmlContentOpen -HeaderText "VM Size Summary" -IsHidden $rpt += Get-HtmlContenttable ($RMVMArray | group size | select Name, Count | sort count -Descending ) -Fixed $rpt += Get-HtmlContentClose

# We are introducing -BackgroundShade 2 so that we can clearly see the sections. # This helps with larger reports and many when there are many levels to the sections $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines by location" -IsHidden

# adding the foreach loop for the group recordset. foreach ($Group in ($RMVMArray | select Location, ResourceGroup, Name, Size,PowerState, DataDiskCount, ImageSKU | group location ) ) {

#for every group that exists for a location we will create an HTML section. I have also specified the -BackgroupShade to 1 $rpt += Get-HtmlContentOpen -HeaderText ("Virtual Machines for location '" + $group.Name +"'") -IsHidden -BackgroundShade 1

# Each recordset may have different data in the columns and therefore create different width in the table columns. # We would like it to look the same. We can use the -Fixed switch to produce evenly space columns for the table $rpt += Get-HtmlContentTable ($Group.Group | select ResourceGroup, Name, Size,PowerState, DataDiskCount, ImageSKU ) -Fixed $rpt += Get-HtmlContentClose } $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example4 [/powershell]

ReportExample4

Filtering Sections based on Conditions (Example 5) This will cover adding some IF conditions to the syntax to display a section or not.

[powershell] ####### Example 5 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + "Example 5") $rpt += Get-HtmlContentOpen -HeaderText "Summary Information" -BackgroundShade 1 $rpt += Get-HtmlContenttext -Heading "Total VMs" -Detail ( $RMVMArray.Count) $rpt += Get-HtmlContenttext -Heading "VM Power State" -Detail ("Running " + ($RMVMArray | ? {$_.PowerState -eq 'Running'} | measure ).count + " / Deallocated " + ($RMVMArray | ? {$_.PowerState -eq 'Deallocated'} | measure ).count) $rpt += Get-HtmlContenttext -Heading "Total Data Disks" -Detail $RMVMArray.datadisks.count $rpt += Get-HtmlContentClose $rpt += Get-HtmlContentOpen -HeaderText "VM Size Summary" -IsHidden -BackgroundShade 1 $rpt += Get-HtmlContenttable ($RMVMArray | group size | select Name, Count | sort count -Descending ) -Fixed $rpt += Get-HtmlContentClose $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines by location" -BackgroundShade 3 foreach ($Group in ($RMVMArray | select Location, ResourceGroup, Name, Size,PowerState, DataDiskCount, ImageSKU | group location ) ) {

# Here we are creating a group to use for the IF condition, so we can create sections for VMs by powerstate, Running or Deallocated $PowerState = $Group.Group | group PowerState $rpt += Get-HtmlContentOpen -HeaderText ("Virtual Machines for location '" + $group.Name +"' - " + $Group.Group.Count + " VMs") -IsHidden -BackgroundShade 2

# If there are VMs in the running group, continue and create a section for them if (($PowerState | ? {$_.name -eq 'running'})) { $rpt += Get-HtmlContentOpen -HeaderText ("Running Virtual Machines") -BackgroundShade 1 $rpt += Get-HtmlContentTable ($Group.Group | where {$_.PowerState -eq "Running"} | select ResourceGroup, Name, Size, DataDiskCount, ImageSKU ) -Fixed $rpt += Get-HtmlContentClose }

# If there are VMs in the running group, continue and create a section for them if (($PowerState | ? {$_.name -eq 'Deallocated'})) { $rpt += Get-HtmlContentOpen -HeaderText ("Deallocated") -BackgroundShade 1 -IsHidden $rpt += Get-HtmlContentTable ($Group.Group | where {$_.PowerState -eq "Deallocated"} | select ResourceGroup, Name, Size, DataDiskCount, ImageSKU)-Fixed $rpt += Get-HtmlContentClose } $rpt += Get-HtmlContentClose } $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example5 [/powershell]

HTML Reporting Example 5

Summary I hope you have had success working through these examples and can find a use for this code.  Part 2 in this series will move into some more techniques and reporting functions. Please share any questions or issues you have executing this content.

Part 1 | Part 2 | Part 3 | Part 4 | More