Skip to content

Instantly share code, notes, and snippets.

@jdhitsolutions
Last active November 8, 2024 20:34
Show Gist options
  • Save jdhitsolutions/d23086d1365bd332ffef96c5ef2de9dd to your computer and use it in GitHub Desktop.
Save jdhitsolutions/d23086d1365bd332ffef96c5ef2de9dd to your computer and use it in GitHub Desktop.
A PowerShell function to create an html report showing drive utilization. Includes tooltip popup details and a color gradient.
#requires -version 3.0
Function New-HTMLDiskReport {
<#
.Synopsis
Create a disk utilization report with gradient coloring
.Description
This command will create an HTML report depicting drive utilization through a gradient color bar.
.Parameter Computername
The name(s) of computers to query. They must be running PowerShell 3.0 or later and support CIM queries.
This parameter has an alias of CN.
.Parameter ReportTitle
The HTML title to be for your report. This parameter has an alias of Title.
.Parameter Path
The filename and path for your html report.
.Parameter PreContent
Add any HTML text to insert before the drive utilization table.
.Parameter PostContent
Add any HTML text to insert after the drive utilization table.
.Parameter LogoPath
Specify the path to a PNG or JPG file to use as a logo image. The image will be embedded into the html file.
.Example
PS C:\> New-HTMLDiskReport -passthru
Directory: C:\Users\Jeff\AppData\Local\Temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 10/10/2018 3:58 PM 2493 utilization.htm
Create a report for the local host using default settings.
.Example
PS C:\> get-content c:\work\computers.txt | New-HTMLDiskReport -path c:\work\diskreport.htm
Create a single report for every computer listed in computers.txt. The report will be saved to c:\work\diskreport.htm
.Example
PS C:\> New-HTMLDiskReport -Path c:\work\report.htm -Computername SRV1,SRV2,SRV3 -Precontent "<h3>Company Confidential</h3>" -PostContent "This report is offered as-is. You can verify results with a command like <b>Get-Volume</b>." -logopath c:\scripts\logo.png
Create a report for the specified servers and insert pre- and post-content.
.Notes
Learn more about PowerShell: http://jdhitsolutions.com/blog/essential-powershell-resources/
.Link
Get-CimInstance
.Link
Get-Volume
.Link
Get-Disk
.Inputs
System.String
#>
[cmdletbinding(SupportsShouldProcess)]
[OutputType("None", "System.IO.FileInfo")]
Param(
[Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[ValidateNotNullorEmpty()]
[Alias("cn")]
[string[]]$Computername = $env:Computername,
[ValidateNotNullorEmpty()]
[Parameter(HelpMessage = "The report title")]
[Alias("title")]
[string]$ReportTitle = "Drive Utilization Report",
[Parameter(HelpMessage = "The filename and path for the finished HTML report.")]
[ValidateNotNullorEmpty()]
[ValidateScript( {
#get parent
$parent = Split-Path $_
if (Test-Path $parent) {
$True
}
else {
Throw "Can't verify part of the file path $_"
}
})]
[string]$Path = "$env:temp\utilization.htm",
[string[]]$PreContent,
[string[]]$PostContent,
[string]$LogoPath,
[switch]$Passthru
)
Begin {
Write-Verbose "Starting $($MyInvocation.Mycommand)"
#define HTML header with style elements. If using a header the title must be inserted here.
#here strings must be left justified
$head = @"
<style>
body {
background-color: #FFFFFF;
font-family: Tahoma;
font-size: 12pt;
}
td,
th {
border: 0px solid black;
border-collapse: collapse;
}
th {
color: white;
background-color: black;
}
table,
tr,
td,
th {
padding: 2px;
margin: 0px;
}
tr:nth-child(even) {
background-color: lightgray
}
table {
width: 95%;
margin-left: 10px;
margin-bottom: 20px;
table-layout: fixed;
}
.meta {
width: 25%;
font-size: 8pt;
margin-left: 0px;
table-layout: auto;
}
.top {
width: 50%;
margin-left: 0px;
table-layout: auto;
}
tr.meta {
background-color: #FFFFFF;
}
.right {
text-align: right;
width: 20%;
}
caption {
background-color: #FFFF66;
text-align: left;
font-weight: bold;
font-size: 14pt;
}
td[tip]:hover {
color: #ff2283;
position: relative;
}
td[tip]:hover:after {
content: attr(tip);
left: 0;
top: 100%;
margin-left: 80px;
margin-top: 10px;
width: 400px;
padding: 3px 8px;
position: absolute;
color: #85003a;
font-family: 'Courier New', Courier, monospace;
font-size: 10pt;
background-color: gainsboro;
white-space: pre-wrap;
}
</style>
<Title>$reportTitle</Title>
"@
<#
Define a here string for coloring percentage cells.
The starting and ending percents will need to provided
using the -f operator.
#>
$gradient = @"
filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0,
StartColorStr=#0A802D, EndColorStr=#FF0011)
background-color: #376C46;
background-image: -mso-linear-gradient(left, #0A802D {0}%, #FF0011 {1}%);
background-image: -ms-linear-gradient(left, #0A802D {0}%, #FF0011 {1}%);
background-image: -moz-linear-gradient(left, #0A802D {0}%, #FF0011 {1}%);
background-image: -o-linear-gradient(left, #0A802D {0}%, #FF0011 {1}%);
background-image: -webkit-linear-gradient(left, #0A802D {0}%, #FF0011 {1}%);
background-image: linear-gradient(left, #0A802D {0}%, #FF0011 {1}%);
color:white;
"@
If ($LogoPath) {
if (Test-Path $LogoPath) {
#insert a graphic
$ImageBits = [Convert]::ToBase64String((Get-Content $LogoPath -Encoding Byte))
$ImageFile = Get-Item $LogoPath
$ImageType = $ImageFile.Extension.Substring(1) #strip off the leading .
$ImageTag = "<Img src='data:image/$ImageType;base64,$($ImageBits)' Alt='$($ImageFile.Name)' style='float:left' width='120' height='120' hspace=10>"
}
else {
Write-Warning "Could not find image file $LogoPath"
}
}
if ($ImageTag) {
$top = "<table class='top'><tr><td>$ImageTag</td><td><H1>$ReportTitle</H1></td></table><br>"
}
else {
$top = "<H1>$ReportTitle</H1><br>"
}
#define an array to hold HTML fragments
$fragments = @($top)
$fragments += $Precontent
$fragments += "<br><br>"
#define a parameter hashtable for Write-Progress
$progParam = @{
Activity = $MyInvocation.MyCommand
Status = "Gathering disk data"
CurrentOperation = ""
}
} #begin
Process {
#get the data for the report
foreach ($computer in $computername) {
Write-Verbose "Getting disk information for $Computer"
$progParam.CurrentOperation = $computer.toUpper()
Write-Progress @progParam
Try {
#create a temporary CIMSession
$cs = New-CimSession -ComputerName $computer -ErrorAction stop
#hashtable of parameters to splat to Get-Ciminstance
$paramHash = @{
Classname = "win32_logicaldisk"
filter = "drivetype=3"
CimSession = $cs
ErrorAction = "Stop"
}
if ($pscmdlet.ShouldProcess($Computer, "Get Disk Information")) {
$data = Get-CimInstance @paramHash
Write-Verbose "Formatting data"
#initialize a hashtable of for phsyical media
$hash = @{}
#Create a custom object for each drive
$drives = foreach ($item in $data) {
$Physical = $item | Get-CimAssociatedInstance -ResultClassName Win32_DiskPartition | Get-CimAssociatedInstance -ResultClassName Win32_DiskDrive
$hash.Add($item.DeviceID,$physical)
$prophash = [ordered]@{
Drive = $item.DeviceID
Volume = $item.VolumeName
SizeGB = $item.size / 1GB -as [int]
FreeGB = "{0:N4}" -f ($item.Freespace / 1GB)
PercentFree = [math]::Round(($item.Freespace / $item.size) * 100, 2)
}
New-Object PSObject -Property $prophash
} #foreach item
#convert drive objects to HTML but as an XML document
Write-Verbose "Converting to XML"
[xml]$html = $drives | ConvertTo-Html -Fragment
#add the computer name as the table caption
$caption = $html.CreateElement("caption")
$html.table.AppendChild($caption) | Out-Null
$html.table.caption = $data[0].SystemName
$pop = $html.CreateAttribute("title")
$pop.value = (Get-Ciminstance -ClassName Win32_OperatingSystem -Property caption -cimsession $cs).caption
$html.table.item("caption").attributes.append($pop) | Out-Null
#add physical media as a popup for each device
for ($i=1; $i -le $html.table.tr.count -1;$i++) {
$id = $html.table.tr[$i].ChildNodes[0]."#text"
$pop = $html.CreateAttribute("tip")
$props = ($hash.Item($id) | Select-Object -property Caption,SerialNumber,FirmwareRevision,Size,InterfaceType,SCSI* | Out-String).trim()
$pop.Value = $props
$html.table.tr[$i].ChildNodes[0].Attributes.append($pop) | Out-Null
}
#go through rows again and add gradient
Write-Verbose "Inserting gradient"
for ($i = 1; $i -le $html.table.tr.count - 1; $i++) {
$class = $html.CreateAttribute("style")
[int]$start = $html.table.tr[$i].td[-1]
#create the gradient using starting and ending values
#based on %free
$class.value = $Gradient -f $start, (100 - $start)
$html.table.tr[$i].ChildNodes[4].Attributes.Append($class) | Out-Null
} #for
#add the html to the fragments
$fragments += $html.InnerXml
} #should process
} #Try
Catch {
Write-Warning "Failed to get disk information for $computer. $($_.exception.message)"
} #Catch
remove-cimsession $cs
} #foreach computer
} #process
End {
$progParam.currentOperation = "Finalizing report"
Write-Progress @progParam
#only proceed if there is data in $fragments
if ($fragments) {
#add some metadata about this report
[xml]$metadata = [pscustomobject]@{
"Report Run" = "$((Get-Date).ToUniversalTime()) UTC"
"Run By" = "$($env:USERDOMAIN)\$env:username"
Originated = $env:Computername
Command = $($myinvocation.invocationname)
Version = "2.0"
} | ConvertTo-html -as List -Fragment
#insert css tags into this table
$class = $metadata.CreateAttribute("class")
$class.value = 'meta'
$metadata.table.Attributes.Append($class) | Out-Null
for ($i = 0; $i -le $metadata.table.tr.count - 1; $i++) {
$class = $metadata.CreateAttribute("class")
$class.value = 'meta'
$metadata.table.tr[$i].attributes.append($class) | Out-Null
$class = $metadata.CreateAttribute("class")
$class.value = 'right'
$metadata.table.tr[$i].item("td").attributes.append($class) | Out-Null
}
$postcontent += "<br><br>$($metadata.InnerXml)"
#create the final report
Write-Verbose "Creating HTML report"
$paramHash = @{
head = $head
Body = $($fragments | Out-String)
PostContent = $PostContent
}
if ($pscmdlet.ShouldProcess($path, "Creating HTML file")) {
ConvertTo-Html @paramHash | Out-File -FilePath $path -Encoding ascii
}
Write-Verbose "Report created to $path"
#if -Passthru write the file object to the pipeline
if ($Passthru) {
Get-Item $Path
}
}
Write-Verbose "Ending $($MyInvocation.Mycommand)"
} #end
} #close New-HTMLDiskReport
@jdhitsolutions
Copy link
Author

I use scheduled jobs all the time. It is the easiest way to run PowerShell commands via the Task scheduler.

help about_scheduled*

@johnczer
Copy link

I as well but for some reason I went off on a tangent where others were using a batch file, which I have successfully done with some powershell scripts in the past. Your scenario makes perfect sense for what I am trying to acomplish with HTMLDiskReports. Thanks again!

@johnczer
Copy link

Money! Now I have it working. You are the best, thanks!

@johnczer
Copy link

I am sure this is just an Outlook 2016 thing because reports appear normal when viewing the html report file itself and viewing it on my iPhone in Safari browser. But for some strange reason the reports in Outlook are missing some of the background color and gradients under percent free. Just another one of those two step forward-one step back moments. I will share if I get this figured out.

@johnczer
Copy link

I just added the ability to attach the report to the email and I can open this in internet browser and view that way.

@jdhitsolutions
Copy link
Author

The html file embeds the style sheet so it should work as an embedded message. It does for me using Thunderbird

PS C:\> send-mailmessage -to XXX@XXXX.com -Subject DiskReport -Body (get-content C:\work\diskreport.htm | out-string) -BodyAsHtml.

It could be an Outlook problem. But sounds like you have it figured out.

@ws65infl
Copy link

ws65infl commented Nov 5, 2024

Late to the show...what a fantastic script/function this is and thank you so much for the time you put into it. The only "issue" I'm having with it is in the gradient display; some of my drives show the gradient effect while others it's just red and green bar. For consistency, is there a way the gradient effect can be removed without destroying anything else in the script/function? I don't mind not having the gradient, or it can work across all my drives that would be great too, either way but I need it to be consistent. I've attached an image showing what I mean.
Edit: unsure why the image is not loading
gradient-difference

@jdhitsolutions
Copy link
Author

I get access denied on the image. I think if you comment out lines 315..323 that will keep the data without any fancy formatting. By the way, you might try to see if you can open the html file with a different browser.

@ws65infl
Copy link

ws65infl commented Nov 5, 2024

I do not know why it says access denied; I get the same. I commented out the two lines and I still see gradients. I have not tried another browser but I will when I have time tomorrow. Thank you for the quick response.

@jdhitsolutions
Copy link
Author

You need to comment out the range of lines from 315 to 232.

@ws65infl
Copy link

ws65infl commented Nov 6, 2024

Got back to it, commenting out 315 to 332 caused errors. Ended up commenting out 315 through 323 and it worked, however, now there are no bars of red and green at all and just the listing of free space as a number. I'll keep trying but wanted to give you an update.

On a side note, I am in awe of this. I've tried so many times in the past to learn PowerShell and it has been futile. I can generally look at other people's scripts and sort of figure out what the intentions are, but I have never been able to just sit down and write one of these from scratch. Can you give me any insight as to how I would get to anywhere near where you are at with PowerShell?

@jdhitsolutions
Copy link
Author

I re-read your initial comment. I mistook it that you wanted to hide the colors. I'll have to revisit the code to see how to keep the red and green but as blocks and not using a gradient. As for getting better with PowerShell, you just have to do it. And take the time to understand how and why it works. It is more than a scripting language. The only reason I'm good is because I have been using it constantly for 18+ years.

@jdhitsolutions
Copy link
Author

I tried to see what I could do, but I don't think I have any control over how the gradient style works. It looks great for some drives.
image

@ws65infl
Copy link

ws65infl commented Nov 8, 2024

Thanks for the advice. I found this in another script that creates a bar...maybe it can help?
`$newHtmlFragment = [System.Collections.ArrayList]::new()
foreach ($computer in $computers)
{
$disks = Get-DiskDetails -Computer $computer
$diskinfo = @()
foreach ($disk in $disks) {
[int]$percentUsage = ((($disk.Size - $disk.FreeSpace)/1gb -as [int]) / ($disk.Size/1gb -as [int])) * 100 #(50/100).tostring("P")
$bars = "

$percentUsage%
"
$diskInfo += [PSCustomObject]@{
Volume = $disk.DeviceID
VolumeName = $disk.VolumeName
TotalSize_GB = $disk.Size / 1gb -as [int]
UsedSpace_GB = ($disk.Size - $disk.FreeSpace)/1gb -as [int]
FreeSpace_GB = [System.Math]::Round($disk.FreeSpace/1gb)
Usage = "usage {0}" -f $bars #, $percentUsage
}
}
$htmlFragment = $diskInfo | ConvertTo-Html -Fragment
$newHtmlFragment += $htmlFragment[0]
$newHtmlFragment += "$($computer.ToUpper())"
$newHtmlFragment += $htmlFragment[2].Replace('',"")

$diskData =  $htmlFragment[3..($htmlFragment.count -2)]
for ($i = 0; $i -lt $diskData.Count; $i++) {
    if ($($i % 2) -eq 0)
    {
        $newHtmlFragment += $diskData[$i].Replace('<td>',"<td class='td0'>")
    }
    else 
    {
        $newHtmlFragment += $diskData[$i].Replace('<td>',"<td class='td1'>")
    }
}
$newHtmlFragment += $htmlFragment[-1]

}
$newHtmlFragment = $newHtmlFragment.Replace("usage ", "")
$newHtmlFragment = $newHtmlFragment.Replace("usage ", "")
$newHtmlFragment = $newHtmlFragment.Replace('<', '<')
$newHtmlFragment = $newHtmlFragment.Replace('>', '>')
$newHtmlFragment = $newHtmlFragment.Replace('&#39', "'")
`

@ws65infl
Copy link

ws65infl commented Nov 8, 2024

I thought it might be helpful to put the whole thing in and not just the section I thought would help. You will have to change the variable on line one for your environment. This script does everything I want, but it doesn't have your pop-up functions for server name and drive meta data.

`$computers = Get-Content "c:\bills\servers.txt"
function Get-DiskDetails
{
[CmdletBinding()]
param (
[string]$Computer = $env:COMPUTERNAME
)
$cimSessionOptions = New-CimSessionOption -Protocol Default
$query = "SELECT DeviceID, VolumeName, Size, FreeSpace FROM Win32_LogicalDisk WHERE DriveType = 3"
$cimsession = New-CimSession -Name $Computer -ComputerName $Computer -SessionOption $cimSessionOptions
Get-CimInstance -Query $query -CimSession $cimsession
}

$newHtmlFragment = [System.Collections.ArrayList]::new()
foreach ($computer in $computers)
{
$disks = Get-DiskDetails -Computer $computer
$diskinfo = @()
foreach ($disk in $disks) {
[int]$percentUsage = ((($disk.Size - $disk.FreeSpace)/1gb -as [int]) / ($disk.Size/1gb -as [int])) * 100 #(50/100).tostring("P")
$bars = "

$percentUsage%
"
$diskInfo += [PSCustomObject]@{
Volume = $disk.DeviceID
VolumeName = $disk.VolumeName
TotalSize_GB = $disk.Size / 1gb -as [int]
UsedSpace_GB = ($disk.Size - $disk.FreeSpace)/1gb -as [int]
FreeSpace_GB = [System.Math]::Round($disk.FreeSpace/1gb)
Usage = "usage {0}" -f $bars #, $percentUsage
}
}
$htmlFragment = $diskInfo | ConvertTo-Html -Fragment
$newHtmlFragment += $htmlFragment[0]
$newHtmlFragment += "$($computer.ToUpper())"
$newHtmlFragment += $htmlFragment[2].Replace('',"")

$diskData =  $htmlFragment[3..($htmlFragment.count -2)]
for ($i = 0; $i -lt $diskData.Count; $i++) {
    if ($($i % 2) -eq 0)
    {
        $newHtmlFragment += $diskData[$i].Replace('<td>',"<td class='td0'>")
    }
    else 
    {
        $newHtmlFragment += $diskData[$i].Replace('<td>',"<td class='td1'>")
    }
}
$newHtmlFragment += $htmlFragment[-1]

}
$newHtmlFragment = $newHtmlFragment.Replace("usage ", "")
$newHtmlFragment = $newHtmlFragment.Replace("usage ", "")
$newHtmlFragment = $newHtmlFragment.Replace('<', '<')
$newHtmlFragment = $newHtmlFragment.Replace('>', '>')
$newHtmlFragment = $newHtmlFragment.Replace('&#39', "'")

$html = @"

<title>Disk Usage Report</title> <style> body { font-family: Calibri, sans-serif, 'Gill Sans', 'Gill Sans MT', 'Trebuchet MS'; background-color: whitesmoke; } .mainhead { margin: auto; width: 100%; text-align: center; font-size: xx-large; font-weight: bolder; } table { margin: 10px auto; width: 70%; } .ServerName { font-size: x-large; margin: 10px; text-align: left; padding: 10 0; color: BlueViolet; } .tableheader { background-color: black; color: white; padding: 10px; text-align: left; /* font-size: large; */ border-bottom-style: solid; border-bottom-color: darkgray; } td { background-color: white; border-bottom: 1px; border-bottom-style: solid; border-bottom-color: #404040; }
        .usage {
            background-color: Lavender ;
            width: 70%;
            color:  black;
        }

        span {
            color: black;
        }

        .td1 {
            background-color: #F0F0F0;
        }
      
    </style>
</head>
<body>
    <div class='mainhead'>          
        <b><u>Production Servers Disk Usage Report</u></b>
    </div>
    <br>
    <div><i><b>Generated on: </b> $((Get-Date).DateTime)</i></div>
    $newHtmlFragment
</body>
"@

$html > disk_usage_report.html`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment