The PowerShell Morning Report

ZazuI love how easy it is to manage computers with Windows PowerShell. It is a great reporting tool, but often I find people getting locked into one approach. I’m a big believer in flexibility and re-use and using objects in the pipeline wherever I can. So I put together a PowerShell script that I can run every morning on my computer and get a quick summary about what is happening, or perhaps not happening. My script, MorningReport.ps1, relies on WMI to gather a variety of system information. By default it connects to the local host, but I’ve provided a computername parameter. I’m assuming current credentials are good enough for any remote system, but you can always add support for alternate credentials, assuming I don’t in some future version.

By default the script writes a custom object to the pipeline that contains all of the other WMI information like disk utilization, service status, and event logs. But I wanted flexibility and ease of use, so the script also supports parameters of -Text and -HTML. The former creates nicely formatted text suitable for printing or saving to a file. The latter creates HTML code from the original objects. I rely heavily on creating HTML fragments with ConvertTo-HTML and then assembling everything at the end. The script writes the HTML code to the pipeline so if you want to save results to a file, simply pipe to Out-File. I did this because there may be times when you want the “raw” HTML code. You might want to save the HTML and create an HTML mail message with Send-MailMessage. Or maybe further tweak the HTML before saving it to a file. Again, I didn’t want to lock myself in. Here’s the main part of the script.


[cmdletbinding(DefaultParameterSetName="object")]

Param(
[Parameter(Position=0,ValueFromPipeline=$True)]
[ValidateNotNullOrEmpty()]
[string]$Computername=$env:computername,
[ValidateNotNullOrEmpty()]
[alias("title")]
[string]$ReportTitle="System Report",
[ValidateScript({$_ -ge 1})]
[int]$Hours=24,
[Parameter(ParameterSetName="HTML")]
[switch]$HTML,
[Parameter(ParameterSetName="TEXT")]
[switch]$Text
)

#script internal version number used in output
[string]$reportVersion="1.0.8"

<# define some HTML style here's a source for HTML color codes http://www.immigration-usa.com/html_colors.html the code must be left justified #>
$head = @"

$ReportTitle
"@

If ($computername -eq $env:computername) {
#local computer so no ping test is necessary
$OK=$True
}
elseIf (($computername -ne $env:computername) -AND (Test-Connection -ComputerName $computername -quiet -Count 2)) {
#not local computer and it can be pinged so proceed
$OK=$True
}

If ($OK) {

Try {
$os=Get-WmiObject Win32_operatingSystem -ComputerName $computername -ErrorAction Stop
#set a variable to indicate WMI can be reached
$wmi=$True
}
Catch {
Write-Warning "WMI failed to connect to $($computername.ToUpper())"
}

if ($wmi) {
Write-Host "Preparing morning report for $($os.CSname)" -ForegroundColor Cyan

#OS Summary
Write-Host "...Operating System" -ForegroundColor Cyan
$osdata=$os | Select @{Name="Computername";Expression={$_.CSName}},
@{Name="OS";Expression={$_.Caption}},
@{Name="ServicePack";Expression={$_.CSDVersion}},
free*memory,totalv*,NumberOfProcesses,
@{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootupTime)}},
@{Name="Uptime";Expression={(Get-Date) - ($_.ConvertToDateTime($_.LastBootupTime))}}

#Computer system
Write-Host "...Computer System" -ForegroundColor Cyan
$cs=Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
$csdata=$cs | Select Status,Manufacturer,Model,SystemType,Number*

#services
Write-Host "...Services" -ForegroundColor Cyan
#get services via WMI and group into a hash table
$wmiservices=Get-WmiObject -class Win32_Service -ComputerName $computername
$services=$wmiservices | Group State -AsHashTable

#get services set to auto start that are not running
$failedAutoStart=$wmiservices | Where { ($_.startmode -eq "Auto") -AND ($_.state -ne "Running")}

#Disk Utilization
Write-Host "...Logical Disks" -ForegroundColor Cyan
$disks=Get-WmiObject -Class Win32_logicaldisk -Filter "Drivetype=3" -ComputerName $computername
$diskData=$disks | Select DeviceID,
@{Name="SizeGB";Expression={$_.size/1GB -as [int]}},
@{Name="FreeGB";Expression={"{0:N2}" -f ($_.Freespace/1GB)}},
@{Name="PercentFree";Expression={"{0:P2}" -f ($_.Freespace/$_.Size)}}

#NetworkAdapters
Write-Host "...Network Adapters" -ForegroundColor Cyan
#get NICS that have a MAC address only
$nics=Get-WmiObject -Class Win32_NetworkAdapter -filter "MACAddress Like '%'" -ComputerName $Computername
$nicdata=$nics | Foreach {
$tempHash=@{Name=$_.Name;DeviceID=$_.DeviceID;AdapterType=$_.AdapterType;MACAddress=$_.MACAddress}
#get related configuation information
$config=$_.GetRelated() | where {$_.__CLASS -eq "Win32_NetworkadapterConfiguration"}
#add to temporary hash
$tempHash.Add("IPAddress",$config.IPAddress)
$tempHash.Add("IPSubnet",$config.IPSubnet)
$tempHash.Add("DefaultGateway",$config.DefaultIPGateway)
$tempHash.Add("DHCP",$config.DHCPEnabled)
#convert lease information if found
if ($config.DHCPEnabled -AND $config.DHCPLeaseObtained) {
$tempHash.Add("DHCPLeaseExpires",($config.ConvertToDateTime($config.DHCPLeaseExpires)))
$tempHash.Add("DHCPLeaseObtained",($config.ConvertToDateTime($config.DHCPLeaseObtained)))
$tempHash.Add("DHCPServer",$config.DHCPServer)
}

New-Object -TypeName PSObject -Property $tempHash

}

#Event log errors and warnings in the last $Hours hours
$last=(Get-Date).AddHours(-$Hours)
#System Log
Write-Host "...System Event Log Error/Warning since $last" -ForegroundColor Cyan
$syslog=Get-EventLog -LogName System -ComputerName $computername -EntryType Error,Warning -After $last
$syslogdata=$syslog | Select TimeGenerated,EventID,Source,Message

#Application Log
Write-Host "...Application Event Log Error/Warning since $last" -ForegroundColor Cyan
$applog=Get-EventLog -LogName Application -ComputerName $computername -EntryType Error,Warning -After $last
$applogdata=$applog | Select TimeGenerated,EventID,Source,Message

} #if wmi is ok

#write results depending on parameter set
$footer="Report v{3} run {0} by {1}\{2}" -f (Get-Date),$env:USERDOMAIN,$env:USERNAME,$reportVersion

if ($HTML) {
#prepare HTML code
$fragments=@()
#insert a graphic header with one of the two following lines
#$fragments+="

"
$fragments+="Zazu

The Morning Report

"

#insert navigation bookmarks
$nav=@"
Services
Failed Auto Start
Disks
Network
System Log
Application Log
"@
$fragments+=$nav
$fragments+="
"

#add a link to the document top
$nav+="`nTop"

$fragments+="

System Summary

"
$fragments+=$osdata | ConvertTo-Html -as List -Fragment
$fragments+=$csdata | ConvertTo-Html -as List -Fragment
$fragments+=ConvertTo-Html -Fragment -PreContent "

Services

"
$services.keys | foreach {
$fragments+= ConvertTo-Html -Fragment -PreContent "

$_

"
$fragments+=$services.$_ | Select Name,Displayname,StartMode| ConvertTo-HTML -Fragment
#insert navigation link after each section
$fragments+=$nav
}

$fragments+=$failedAutoStart | Select Name,Displayname,StartMode,State |
ConvertTo-Html -Fragment -PreContent "

Failed Auto Start

"
$fragments+=$nav

$fragments+=$diskdata | ConvertTo-HTML -Fragment -PreContent "

Disk Utilization

"
$fragments+=$nav

#convert nested object array properties to strings
$fragments+=$nicdata | Select Name,DeviceID,DHCP*,AdapterType,MACAddress,
@{Name="IPAddress";Expression={$_.IPAddress | Out-String}},
@{Name="IPSubnet";Expression={$_.IPSubnet | Out-String}},
@{Name="IPGateway";Expression={$_.DefaultGateway | Out-String}} |
ConvertTo-HTML -Fragment -PreContent "

Network Adapters

"
$fragments+=$nav

$fragments+=$syslogData | ConvertTo-HTML -Fragment -PreContent "

System Event Log Summary

"
$fragments+=$nav

$fragments+=$applogData | ConvertTo-HTML -Fragment -PreContent "

Application Event Log Summary

"
$fragments+=$nav

Write $fragments | clip
ConvertTo-Html -Head $head -Title $ReportTitle -PreContent ($fragments | out-String) -PostContent "
$footer"
}
elseif ($TEXT) {
#prepare formatted text
$ReportTitle
"-"*($ReportTitle.Length)
"System Summary"
$osdata | Out-String
$csdata | format-List | Out-String
Write "Services"
$services.keys | foreach {
$services.$_ | Select Name,Displayname,StartMode,State
} | Format-List | Out-String
Write "Failed Autostart Services"
$failedAutoStart | Select Name,Displayname,StartMode,State
Write "Disk Utilization"
$diskdata | Format-table -AutoSize | Out-String
Write "Network Adapters"
$nicdata | Format-List | Out-String
Write "System Event Log Summary"
$syslogdata | Format-List | Out-String
Write "Application Event Log Summary"
$applogdata | Format-List | Out-String
Write $Footer
}
else {
#Write data to the pipeline as part of a custom object

New-Object -TypeName PSObject -Property @{
OperatingSystem=$osdata
ComputerSystem=$csdata
Services=$services.keys | foreach {$services.$_ | Select Name,Displayname,StartMode,State}
FailedAutoStart=$failedAutoStart | Select Name,Displayname,StartMode,State
Disks=$diskData
Network=$nicData
SystemLog=$syslogdata
ApplicationLog=$applogdata
ReportVersion=$reportVersion
RunDate=Get-Date
RunBy="$env:USERDOMAIN\$env:USERNAME"
}
}

} #if OK

else {
#can't ping computer so fail
Write-Warning "Failed to ping $computername"
}

As you can see, it is a lengthy script, but I’ve tried to include a fair amount of internal comments and documentation, so I won’t repeat it here, but I will touch on a few key points.

First, I’m embedding a style sheet directly in any HTML output so any files I create can stand alone. You could certainly modify the relevant sections and use the -CSSUri parameter with ConvertTo-HTML. Next, I use a combination of Test-Connection and Try/Catch to handle computers that are offline or I can’t access. This speeds up the script and makes it nicer for the script user. Assuming all is good, I create a number of variables that hold the WMI information I am interested in.


...
#OS Summary
Write-Host "...Operating System" -ForegroundColor Cyan
$osdata=$os | Select @{Name="Computername";Expression={$_.CSName}},
@{Name="OS";Expression={$_.Caption}},
@{Name="ServicePack";Expression={$_.CSDVersion}},
free*memory,totalv*,NumberOfProcesses,
@{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootupTime)}},
@{Name="Uptime";Expression={(Get-Date) - ($_.ConvertToDateTime($_.LastBootupTime))}}

#Computer system
Write-Host "...Computer System" -ForegroundColor Cyan
$cs=Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
$csdata=$cs | Select Status,Manufacturer,Model,SystemType,Number*
...

After everything is collected, then I can write output using an If/ElseIf statement depending on what parameters were passed. If no parameters were specified then a custom object is assembled and written to the pipeline.


else {
#Write data to the pipeline as part of a custom object

New-Object -TypeName PSObject -Property @{
OperatingSystem=$osdata
ComputerSystem=$csdata
Services=$services.keys | foreach {$services.$_ | Select Name,Displayname,StartMode,State}
FailedAutoStart=$failedAutoStart | Select Name,Displayname,StartMode,State
Disks=$diskData
Network=$nicData
SystemLog=$syslogdata
ApplicationLog=$applogdata
ReportVersion=$reportVersion
RunDate=Get-Date
RunBy="$env:USERDOMAIN\$env:USERNAME"
}

But perhaps you’d like to see this in action. Here’s a sample HTML SampleReport. In this report, there are no recent errors or warnings in the Application event log.

Or if you have a few minutes, here’s a short clip of the script in action.

I hope you find this a useful jumping off point for your own script, although I think I’ve already worked out information you are most likely interested in. Download MorningReport.ps1 and try it out for yourself.

Update: Read about a revised version that accepts pipelined input.

Friday Fun Drive Usage Console Graph

I think you’ll like this. Normally, I prefer my PowerShell commands to write objects to the pipeline. But there’s nothing wrong with sending output directly to the console, as long as you know that the output is intended only for the screen. What I find frustrating is the use of Write-Host when really, pipelined objects would be better. But for today, I’m going to revel in the beauty of the console and create a colorized drive utilization graph. Continue reading “Friday Fun Drive Usage Console Graph”

Get Shared Resource

I was poking around WMI the other day in PowerShell and was intrigued by the Win32_Share class. This is a great way to find out what items are shared on a server such as printers and folders, although it’s not limited to those types of resources. I thought this would make a useful function; one that you might use to audit a collection of computers. Continue reading “Get Shared Resource”

Get Local Administrators with WMI and PowerShell

Earlier this week I was helping someone out on a problem working with the local administrators group. There are a variety of ways to enumerate the members of a local group. The code he was using involved WMI. I hadn’t really worked with the WMI approach in any great detail so I thought I’d see how this might work in PowerShell. I ended up with a function to enumerate members of the local administrators group on a computer, as well as test if an account belongs to the group. Continue reading “Get Local Administrators with WMI and PowerShell”

Get WMI Namespace

PowerShell and WMI just seem to go together like peanut butter and jelly, beer and pretzels, or salt and pepper. However, discovering things about WMI isn’t always so easy. There are plenty of tools and scripts that will help you uncover WMI goodness, but here’s another one anyway. Today’s PowerShell function will get all namespaces on a computer. Continue reading “Get WMI Namespace”