Mark Roddy
asked on
PowerShell Scripting Help - Generate a System Uptime Report that will send an daily html email blast to my team : < 30 days critcal systems as RED, 21 to 29 days as Yellow, and 0 to 20 days GREEN
Hi PowerShell Gurus,
Happy New Year Guys!
Once again, I need your help.
The company I work for has tasked me with generating a daily report, html preferred sent via email, of the Uptime of Windows Servers in our environment.
1. Query only active servers in our domains (Get-AdComputer -Filter {OperatingSystem -like "*windows*server*"} but I am having issues with the syntax of only active\enabled systems)
2. Generate an "Uptime Report\Date of Report" in an excel-like html format with systems that have been up longer than 30 days as red, systems that will need a reboot soon (21 to 29 days) yellow, and systems that are still within the 0 to 20 days window, as green
3. Email a list of users, daily, with this report so they can remediate these systems during our scheduled maintenance window
I found the system uptime code on the internet:
I have crafted this from online research and trial and error:
These are my results (Beginning Stages) - still receiving RPC errors:
So, I am asking the experts what do I need to do to put this all together? I have an SMTP server that is accessible, sent messages to my work account today, but I am not sure how to set this script up as I need it and the business is wanting it. :(
Thanks guys!
Happy New Year Guys!
Once again, I need your help.
The company I work for has tasked me with generating a daily report, html preferred sent via email, of the Uptime of Windows Servers in our environment.
1. Query only active servers in our domains (Get-AdComputer -Filter {OperatingSystem -like "*windows*server*"} but I am having issues with the syntax of only active\enabled systems)
2. Generate an "Uptime Report\Date of Report" in an excel-like html format with systems that have been up longer than 30 days as red, systems that will need a reboot soon (21 to 29 days) yellow, and systems that are still within the 0 to 20 days window, as green
3. Email a list of users, daily, with this report so they can remediate these systems during our scheduled maintenance window
I found the system uptime code on the internet:
<#
.SYNOPSIS
Outputs the last bootup time and uptime for one or more computers.
.DESCRIPTION
Outputs the last bootup time and uptime for one or more computers.
.PARAMETER ComputerName
One or more computer names. The default is the current computer. Wildcards are not supported.
.PARAMETER Credential
Specifies credentials that have permission to connect to the remote computer. This parameter is ignored for the current computer.
.OUTPUTS
PSObjects containing the computer name, the last bootup time, and the uptime.
#>
[CmdletBinding()]
param(
[parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
$ComputerName,
[System.Management.Automation.PSCredential]
$Credential
)
begin {
function Out-Object {
param(
[System.Collections.Hashtable[]] $hashData
)
$order = @()
$result = @{}
$hashData | ForEach-Object {
$order += ($_.Keys -as [Array])[0]
$result += $_
}
New-Object PSObject -Property $result | Select-Object $order
}
function Format-TimeSpan {
process {
"{0:00} d {1:00} h {2:00} m {3:00} s" -f $_.Days,$_.Hours,$_.Minutes,$_.Seconds
}
}
function Get-Uptime {
param(
$computerName,
$credential
)
# In case pipeline input contains ComputerName property
if ( $computerName.ComputerName ) {
$computerName = $computerName.ComputerName
}
if ( (-not $computerName) -or ($computerName -eq ".") ) {
$computerName = [Net.Dns]::GetHostName()
}
$params = @{
"Class" = "Win32_OperatingSystem"
"ComputerName" = $computerName
"Namespace" = "root\CIMV2"
}
if ( $credential ) {
# Ignore -Credential for current computer
if ( $computerName -ne [Net.Dns]::GetHostName() ) {
$params.Add("Credential", $credential)
}
}
try {
$wmiOS = Get-WmiObject @params -ErrorAction Stop
}
catch {
Write-Error -Exception (New-Object $_.Exception.GetType().FullName `
("Cannot connect to the computer '$computerName' due to the following error: '$($_.Exception.Message)'",
$_.Exception))
return
}
$lastBootTime = [Management.ManagementDateTimeConverter]::ToDateTime($wmiOS.LastBootUpTime)
Out-Object `
@{"ComputerName" = $computerName},
@{"LastBootTime" = $lastBootTime},
@{"Uptime" = (Get-Date) - $lastBootTime | Format-TimeSpan}
}
}
process {
if ( $ComputerName ) {
foreach ( $computerNameItem in $ComputerName ) {
Get-Uptime $computerNameItem $Credential
}
}
else {
Get-Uptime "."
}
}
I have crafted this from online research and trial and error:
Get-ADComputer -Filter {(OperatingSystem -Like “*Windows*Server*”)} | Where {$_.Enabled -eq $true} | Select-Object -ExpandProperty Name | Sort-object | .\Get-Uptime.ps1
These are my results (Beginning Stages) - still receiving RPC errors:
PS G:\Status Report Script> Get-ADComputer -Filter {OperatingSystem -like "*windows*server*"}|Where {$_.Enabled -eq $true}|Select-Object -ExpandProperty Name | Sort-Object | .\Get-Uptime.ps1
ComputerName LastBootTime Uptime
------------ ------------ ------
********* 12/17/2018 5:35:38 AM 15 d 14 h 35 m 24 s
Get-Uptime : Cannot connect to the computer '***********' due to the following error: 'The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)'
At G:\Status Report Script\Get-Uptime.ps1:91 char:7
+ Get-Uptime $computerNameItem $Credential
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], COMException
+ FullyQualifiedErrorId : System.Runtime.InteropServices.COMException,Get-Uptime
Get-Uptime : Cannot connect to the computer '**********' due to the following error: 'The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)'
At G:\Status Report Script\Get-Uptime.ps1:91 char:7
+ Get-Uptime $computerNameItem $Credential
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], COMException
+ FullyQualifiedErrorId : System.Runtime.InteropServices.COMException,Get-Uptime
'**********' 12/26/2018 12:38:04 PM 06 d 07 h 32 m 59 s
Get-Uptime : Cannot connect to the computer '**********' due to the following error: 'The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)'
At G:\Status Report Script\Get-Uptime.ps1:91 char:7
+ Get-Uptime $computerNameItem $Credential
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], COMException
+ FullyQualifiedErrorId : System.Runtime.InteropServices.COMException,Get-Uptime
Get-Uptime : Cannot connect to the computer '**********' due to the following error: 'The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)'
At G:\Status Report Script\Get-Uptime.ps1:91 char:7
+ Get-Uptime $computerNameItem $Credential
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], COMException
+ FullyQualifiedErrorId : System.Runtime.InteropServices.COMException,Get-Uptime
'**********' 12/20/2018 6:08:48 AM 12 d 14 h 02 m 36 s
'**********' 12/20/2018 6:07:51 AM 12 d 14 h 03 m 33 s
Get-Uptime : Cannot connect to the computer '**********' due to the following error: 'The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)'
At G:\Status Report Script\Get-Uptime.ps1:91 char:7
+ Get-Uptime $computerNameItem $Credential
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], COMException
+ FullyQualifiedErrorId : System.Runtime.InteropServices.COMException,Get-Uptime
So, I am asking the experts what do I need to do to put this all together? I have an SMTP server that is accessible, sent messages to my work account today, but I am not sure how to set this script up as I need it and the business is wanting it. :(
Thanks guys!
for the RPC error, I strongly suspect the firewall. Have a look at http://techgenix.com/troubleshoot-rpc-server-unavailable/
ASKER
Hi Guys,
Thank you both for your input.
Coralon: Thanks a ton for writing that up for me! I will test it and let you know how it is working out for me!
Eric: I think your assumptions are correct. There are some firewall rules that need to be implemented to get WRM and RPC calls enabled. Thanks for that article. I will get with our network guy to make the necessary changes.
I will report back soon!
Mark
Thank you both for your input.
Coralon: Thanks a ton for writing that up for me! I will test it and let you know how it is working out for me!
Eric: I think your assumptions are correct. There are some firewall rules that need to be implemented to get WRM and RPC calls enabled. Thanks for that article. I will get with our network guy to make the necessary changes.
I will report back soon!
Mark
Just don't forget... the HTML sections need some work.. (I ran out of time to work it out, since I'm not much of an HTML guy).. :-)
Coralon
Coralon
ASKER
Hi Guys,
Unfortunately I am not a html guru myself. I ran it again today, tweaking your script a little bit, but it was throwing errors rendering the html pages.
I will post some more tomorrow. Getting ready to go to bed.
Mark
Unfortunately I am not a html guru myself. I ran it again today, tweaking your script a little bit, but it was throwing errors rendering the html pages.
I will post some more tomorrow. Getting ready to go to bed.
Mark
Yeah it would.. there's a lot more that needs to be done for this. I'm actually using my own answer here as a template for a very similar thing here at work.. once I get this worked out, I'll try to remember to post the sanitized version here..
I'm glad to help here if you'll post the errors.
Coralon
I'm glad to help here if you'll post the errors.
Coralon
ASKER
Hi Coralon,
Thanks for the assist! Still receiving RPC errors when trying to enumerate the found systems BUT I am still having issues with writing to the html array. Here is my current code:
Windows PowerShell
Copyright (C) 2014 Microsoft Corporation. All rights reserved.
PS C:\Windows\system32> Test-WSMan -computername **************
wsmid : http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.xsd
ProtocolVersion : http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
ProductVendor : Microsoft Corporation
ProductVersion : OS: 0.0.0 SP: 0.0 Stack: 3.0
It is properly flagging the systems that are inactive BUT still in AD (We need to cleanup a bit) - https://www.screencast.com/t/RshqwuMYxU
RPC Errors below...
Thanks for the assist! Still receiving RPC errors when trying to enumerate the found systems BUT I am still having issues with writing to the html array. Here is my current code:
[CmdletBinding()]
param(
#[parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
#[string[]]$ComputerName = (Get-ADComputer -Filter {(OperatingSystem -Like “*Windows*Server*”)} | Where {$_.Enabled -eq $true} | Select-Object -ExpandProperty Name),
[parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[System.Management.Automation.PSCredential]$Credential = (Get-Credential),
[parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string]$SMTPServer = '***.***.***.***',
#[parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
#[string]$SMTPTo = @('****@***.***','***@***.***'),
[parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string]$SMTPFrom = 'ServerUptimeReport@***.***',
[parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string]$SMTPSubject = 'Server Uptime Report',
[parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[int]$SMTPPort = 25
)
$ComputerName = Get-ADComputer -Filter {OperatingSystem -like "Windows Server*"} -Properties * | Select-Object -ExpandProperty Name
$SMTPto = "***@***.***","***@***.***"
$Now = [datetime]::Now
$GreenMinUpDays = 0
$GreenMaxUpDays = 20
$YellowMinUpDays = 21
$YellowMaxUpDays = 29
$RedMinUpDays = 30
$HTML = New-Object -TypeName System.Collections.ArrayList[string]
$Tab = "`t"
$CRLF = "`r`n"
if($files -ne $null){
$Null = $HTML.Add( ('<HTML>') )
$Null = $HTML.Add( ('{0}<HEAD>' -f $Tab) )
$Null = $HTML.Add( ('{0}<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=windows-1252">' -f $Tab) )
$Null = $HTML.Add( ('{0}<title>Crimson Editor Setup</title>' -f $Tab) )
$Null = $HTML.Add( ('{0}<style>' -f $tab) )
$Null = $HTML.Add( ('put in CSS rules here.. 1 per line'))
$Null = $HTML.Add( ('{0}</style>' -f $tab) )
$Null = $HTML.Add( ('{0}<body>' -f $tab) )
$Null = $HTML.Add( ('{0}{0}<table>' -f $Tab ) )
$Null = $HTML.Add( ('{0}{0}{0}<th note="put in table header row, etc. here">') )}
foreach ($Computer in $ComputerName)
{
if (Test-Connection -ComputerName $Computer -Quiet -Count 2)
{
try
{
$wmiObject = Get-WmiObject -Class Win32_OperatingSystem -Property LastBootUpTime -ComputerName $Computer -Credential $Credential
$UptimeString = $wmiObject.LastBootUpTime
$LastBootYear = $UptimeString.SubString(0,4)
$LastBootMonth = $UptimeString.SubString(4,2)
$LastBootDay = $UptimeString.SubString(6,2)
$LastBootHour = $UptimeString.SubString(8,2)
$LastBootMinute = $UptimeString.SubString(10,2)
$LastBootSecond = $UptimeString.SubString(12,2)
$LastBootTime = Get-Date -Year $LastBootYear -Month $LastBootMonth -Day $LastBootDay -Hour $LastBootHour -Minute
$LastBootMinute -Second $LastBootSecond
$UptimeSpan = New-TimeSpan -Start $LastBootTime -End $Now
$UptimeDays = [int]($UptimeSpan.TotalDays)
if ($UptimeDays -ge $GreenMinUpDays -and $UptimeDays -le $GreenMaxUpDays)
{
$BootState = 'Green'
}
elseif ($UptimeDays -ge $YellowMinUpDays -and $UptimeDays -le $YellowMaxUpDays)
{
$BootState = 'Yellow'
}
elseif ($UptimeDays -ge $RedMinUpDays)
{
$BootState = 'Red'
$UpTimeDays = -1
}
}
catch
{
Write-Error -Exception (New-Object $_.Exception.GetType().FullName, ('Cannot connect to the computer ({0}) due to the following error: {1}' -f $Computer, $_.Exception.Message), $_.Exception)
$BootState = 'FAILED'
}
}
else
{
Write-Warning -Message ('{0} is not reachable' -f $Computer)
$BootState = 'UNREACHABLE'
$UptimeDays = -1
}
if($files -ne $null){
$Null = $HTML.Add( ('{0}{0}{0}<tr>' -f $Tab) )
$Null = $HTML.Add( ('{0}{0}{0}{0}<td class="{1}">{1}</td>' -f $Tab, $BootState, $Computer) )
$null = $HTML.Add( ('{0}{0}{0}{0}<td class="{1}">{2}</td>' -f $Tab, $BootState, $UptimeDays) )
$Null = $HTML.Add( ('{0}{0}{0}</tr>' -f $Tab) )
}
if($files -ne $null){
$Null = $HTML.Add( ('{0}{0}</table>' -f $Tab ) )
$Null = $HTML.Add( ('{0}</body>' -f $tab) )
$Null = $HTML.Add( ('</html>' -f $tab) )}
}
Send-MailMessage -To $SMTPTo -From $SMTPFrom -SmtpServer $SMTPServer -Port $SMTPPort -Body ($HTML -join $CRLF) -BodyAsHtml
I was receiving an error with the original $HTML array. So I changed it and now I am receiving:New-Object : Cannot find type [System.Collections.ArrayList[string]]: verify that the assembly containing this type is loaded.
At line:33 char:9
+ $HTML = New-Object -TypeName System.Collections.ArrayList[string]
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidType: (:) [New-Object], PSArgumentException
+ FullyQualifiedErrorId : TypeNotFound,Microsoft.PowerShell.Commands.NewObjectCommand
RPC errors are still popping up when systems report back they are active. I ran the Test-WSMan on a few of them and they reported back clean. No issues BUT I do not know if it is erroring out on systems that were erroring out on before due to firewall issues Windows PowerShell
Copyright (C) 2014 Microsoft Corporation. All rights reserved.
PS C:\Windows\system32> Test-WSMan -computername **************
wsmid : http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.xsd
ProtocolVersion : http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
ProductVendor : Microsoft Corporation
ProductVersion : OS: 0.0.0 SP: 0.0 Stack: 3.0
It is properly flagging the systems that are inactive BUT still in AD (We need to cleanup a bit) - https://www.screencast.com/t/RshqwuMYxU
RPC Errors below...
Get-WmiObject : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
At line:55 char:26
+ $wmiObject = Get-WmiObject -Class Win32_OperatingSystem -Property La ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject], COMException
+ FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
New-Object : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'ComObject'. Specified method is not supported.
At line:84 char:48
+ Write-Error -Exception (New-Object $_.Exception.GetType().FullName, ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [New-Object], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.NewObjectCommand
And of course, it can't find anything in the array so it errors out on the sending the email..Send-MailMessage : Cannot validate argument on parameter 'Body'. The argument is null or empty. Provide an argument that is not null or empty, and then try the
command again.
At line:107 char:92
+ ... SMTPPort -Body ($HTML -join $CRLF) -BodyAsHtml
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Send-MailMessage], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.SendMailMessage
I will look at this more tomorrow.. but what exactly did you change about the ArrayList? (I'll need to dig in deeper)..
Coralon
Coralon
I did a lot of reworking on my own script today.. and I reworked most of the HTML pieces..
What I ended up doing was creating here strings for all the html major pieces, and added functions for each of them. Here's a pseudo code type of layout of the changes I made:
When the script is done, then you just output the $OutputString ($OutputString.ToString()) or if you want a file, you output it to a file.. like $OutputString -join "`r`n" | out-file c:\Temp.html
Coralon
What I ended up doing was creating here strings for all the html major pieces, and added functions for each of them. Here's a pseudo code type of layout of the changes I made:
Add-Type -AssemblyName System.Web
$HTMLBody = @'
<html>
<head>
<style>
</style>
</head>
<body>
<div>
<table>
'@
$HTMLRow = @'
<tr>
<th class="_CLASS_">_HEADER1_</th>
<td class="_CLASS_">_data1</td>
</tr>
'@
$HTMLClose = @'
</table>
</div>
</body>
</html>
$OutputList = New-Object -Type System.Collections.ArrayList
function New-HTML
{
$OutputList.Add($HTMLBody)
}
function New-HTMLRow
{
param(
[string]$HeaderString,
[string]$DataString
)
$HeaderString = [system.web.httputility]::HTMLEncode($HeaderString)
$DataString = [system.web.httputility]::HTMLEncode($HeaderString)
$TempData = $HTMLRow.Replace('_HEADER1_', $HeaderString)
$TempData = $TempData.Replace('_DATA1_', $DataString)
$HTMLBody.Add($TempData)
}
#And more functions for each of the HTML snippets, and then inside the function, just calling the function with the correct data.
When the script is done, then you just output the $OutputString ($OutputString.ToString())
Coralon
ASKER
Hi Coralon,
I will let you know my results. Thank you so much for your help.
Mark
I will let you know my results. Thank you so much for your help.
Mark
ASKER
Hi Coralon,
What parts do I need to replace in the original script you gave me?
Can you post a full example, if it needs to be sanitized that is ok by me!
Thank you,
Mark
What parts do I need to replace in the original script you gave me?
Can you post a full example, if it needs to be sanitized that is ok by me!
Thank you,
Mark
This question needs an answer!
Become an EE member today
7 DAY FREE TRIALMembers can start a 7-Day Free trial then enjoy unlimited access to the platform.
View membership options
or
Learn why we charge membership fees
We get it - no one likes a content blocker. Take one extra minute and find out why we block content.
I did some playing with that WMI call, and the date time isn't easily parsed as a datetime object, From what I can see you have to break it out to convert it to datetime object. Since it's a very straightforward string, it'd be easy to parse it out manually to get a date time object. Are all your machines in a single timezone? (that matters if they are going to be contacted from a different timezone).
I reworked it, but left some of it to do... (primarily building the CSS rules for the HTML body at line 41).. I'm sure there will be suggestions to optimize this, but it should get you going.. The main thing is that the ComputerName parameter will take multiple entries, and it will then process them. So, you could use your Get-ADComputer line and pipe it directly to this function, or gather the names into a single variable and use that as the input to -ComputerName, etc..
Open in new window
More error checking could be added, or the html could be dumped to a file and sent as an attachment, etc.. but it should be a good start..