-
-
Save jdhitsolutions/d23086d1365bd332ffef96c5ef2de9dd to your computer and use it in GitHub Desktop.
#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 |
Yes, I have tested and the commands run as expected in powershell. It creates a nice report but now I would like to schedule this to run automatically. I have also added an email function to your script and it also emails me the report when I run the command. If I could just get this schedule to work I would be set.
I started by creating this PowerShell script file.
# c:\scripts\diskreports.ps1
#dot source the script file with the function
. C:\scripts\New-DiskReport.ps1
#now run it
get-content C:\work\computers.txt | New-HTMLDiskReport -Path c:\work\diskreport.htm -logopath C:\scripts\db.png
I then created a scheduled job to run daily at 11:00PM. Look at help for New-JobTrigger
. I created the schedule job like this:
$trigger = New-JobTrigger -Daily -At 23:00
$runas = Get-Credential $env:USERNAME
$params = @{
FilePath = "C:\scripts\DiskReports.ps1"
Name = "DailyDiskReport"
Trigger = $trigger
Credential = $cred
MaxResultCount = 5
}
Register-ScheduledJob @params
You will need to specify a credential so that you can access the network. If you want to run it immediately you can run Start-Job -definitioname DailyDiskReport
or whatever you called it.
PowerShell scheduled jobs make it easy to run PowerShell scripts and commands as scheduled tasks. PowerShell does not need to be running.
Jeff, you are awesome and I really appreciate you sharing this information with me. Working from home this gives me something to do and pass the time. I appreciate it-stay safe my friend!
I use scheduled jobs all the time. It is the easiest way to run PowerShell commands via the Task scheduler.
help about_scheduled*
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!
Money! Now I have it working. You are the best, thanks!
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.
I just added the ability to attach the report to the email and I can open this in internet browser and view that way.
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.
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
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.
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.
You need to comment out the range of lines from 315 to 232.
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?
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.
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 = "
$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(''', "'")
`
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 = "
$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(''', "'")
$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`
I would create a PowerShell scheduled job but first, verify that the command runs correctly when run manually. Does it?