ra2 studio - Fotolia
Implement simple server monitoring with PowerShell
Administrators who build a server monitoring framework with PowerShell can develop their own customized checks for deeper insights into their environment.
As your server inventory expands, you will need assistance to ensure you can head off any problems.
There are many server monitoring and reporting options in the market, but certain situations may call for a lightweight solution. Monitoring with PowerShell is a way to use the native functionality in Windows to create scripts that check your systems and send regular updates.
This article explores how to create a simple framework for Windows Server checks with the added benefit of generating reports at different intervals to assist with your monitoring efforts. While PowerShell 7 is available, this tutorial is based on Windows PowerShell 5.1 due to the ease of its default PowerShell remoting setup process.
Server monitoring with PowerShell
While there are many server checks you can perform, this article will concentrate on just a few to demonstrate the capabilities of a simple PowerShell monitoring framework. This tutorial will cover:
- Disk space. Checking local disks and triggering a warning if the percentage of free space goes below 10%.
- OS version. Checking if the version number is lower than 6.2. If it is, then the server OS predates Windows Server 2012, 2016 and 2019 and is no longer supported by Microsoft.
- Expiring certificates. Compile a list of certificates that are within 30 days of expiration.
- License usage. Check licensed states and report any that are unlicensed.
Script structure for server monitoring with PowerShell
The overall idea behind this server monitoring framework is to run a series of checks on one or more servers, save the results and then review the findings. To do this, we will create functions in the PowerShell script.
- Invoke-ServerCheck. This function will take in a series of checks and computers to run those scripts against, then export the results to an XML file.
- New-ServerReport. This function will take the XML files and generate different formats based on the requested report type, either daily or monthly.
Constructing the server checks
There are many ways to structure a potential script to do server checks, but this tutorial will place all checks in a hashtable that uses scriptblocks, which makes it easy to run the code on the requested computers, either local or remotely.
Additionally, the script uses a condition key and associated scriptblock to report whether the check has passed or failed. Every condition should return a Boolean value.
Adding new checks is as easy as adding a new top-level key with a sub-key of check and condition. Both are scriptblocks run by Invoke-Command within the Invoke-ServerCheck function.
$Checks = @{
'OSVersion' = @{
'Check' = {
Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, ServicePackMajorVersion, OSArchitecture
}
'Condition' = {$_.Version -LT 6.2}
}
'Certificates' = @{
'Check' = {
Get-ChildItem -Path 'Cert:' -Recurse -ExpiringInDays 30 | Select-Object Subject, NotAfter
}
'Condition' = {$Result.Count -GT 0}
}
'DiskSpace' = @{
'Check' = {
Get-CIMInstance -Class 'Win32_logicaldisk' -Filter "DriveType = '3'" | Select-Object -Property DeviceID, @{L='FreeSpaceGB';E={"{0:N2}" -f ($_.FreeSpace /1GB)}}, @{L="Capacity";E={"{0:N2}" -f ($_.Size/1GB)}}
}
'Condition' = { ($Result | Where-Object { (($_.FreeSpaceGB / $_.Capacity) * 100) -LT 10 }).Count -GT 0 }
}
'License' = @{
'Check' = {
Enum Licensestatus {
Unlicensed = 0
Licensed = 1
OOBGrace = 2
OOTGrace = 3
NonGenuineGrace = 4
Notification = 5
ExtendedGrace = 6
}
Get-CimInstance -ClassName SoftwareLicensingProduct -Filter "PartialProductKey IS NOT NULL" | Select-Object Name, ApplicationId, @{N='LicenseStatus'; E={[LicenseStatus]$_.LicenseStatus} }
}
'Condition' = {($Results | Where-Object LicenseStatus -NE 'Licensed').Count -EQ 0}
}
}
How the Invoke-ServerCheck function works
The Invoke-ServerCheck function handles the bulk of the server monitoring with PowerShell. This function takes in an array of checks from the $Checks variable, then each set of checks will be run across the servers or the local computer.
The script executes the following steps:
- iterates over each check in the $Checks variable;
- runs Invoke-Command on the scriptblock from the Checks key;
- stores the result in a $CheckResults variable; and
- saves the XML of the $Output variable to the requested path, which allows for easier manipulation of this variable in later functions.
Function Invoke-ServerCheck {
[CmdletBinding()]
Param(
[Parameter(Position = 0, Mandatory = $True)]$Checks,
[Parameter(Position = 1, ValueFromPipeline = $True)]$ComputerName,
[Parameter(Position = 2)]$Path = $Env:TEMP
)
Process {
If ($ComputerName) {
$Computer = $ComputerName
} Else {
$Computer = $Env:COMPUTERNAME
}
$CheckResults = @()
$Checks.GetEnumerator() | ForEach-Object {
Write-Host "Running Check, $($_.Key), on $Computer" -ForegroundColor 'Green'
$Params = @{
"ScriptBlock" = $_.Value.Check
"Verbose" = $MyInvocation.BoundParameters.Verbose
}
If ($ComputerName) {
$Params.Add('ComputerName', $Computer)
}
$Result = Invoke-Command @Params
$CheckResults += ,[PSCustomObject]@{
"Check" = $_.Key
"Result" = $Result
"Condition" = (Invoke-Command -ScriptBlock $_.Value.Condition -ArgumentList $Result)
}
}
$Output = [PSCustomObject]@{
"Server" = $Computer
"Results" = $CheckResults
}
$FileName = "ServerResults-{0}-{1}.xml" -F $Computer, (Get-Date -Format "yyyy_MM_dd_HH_mm_ss")
Export-Clixml -Path (Join-Path -Path $Path -ChildPath $FileName) -InputObject $Output
}
}
Next, we will want to generate a report telling which checks have passed or failed for any of the given servers.
How the New-ServerReport function operates
To make better sense of which checks have run against which servers, we will use the New-ServerReport function.
The function performs the following steps:
- looks for XML files that match the name ServerResults;
- runs checks based on whether a Daily or Monthly report exists;
- looks at the CreationTime to determine whether to either pull all files on the given day or 30 days back;
- after gathering the results, groups them based on the servers and then outputs a table of the checks; and
- saves the results to a CSV file for later viewing.
Function New-ServerReport {
[CmdletBinding()]
Param(
[Parameter(Position = 0)]
[ValidateSet('Daily','Monthly')]
[String]$Type = 'Daily',
[Parameter(Position = 1)]$Path = $Env:TEMP,
[Parameter(Position = 2)]$ReportPath = $Env:TEMP
)
Process {
$Files = Get-ChildItem -Path $Path -Filter '*.xml' | Where-Object Name -Match 'ServerResults'
Switch ($Type) {
'Daily' {
$Results = $Files | Where-Object 'CreationTime' -GT (Get-Date -Hour 0 -Minute 00 -Second 00)
$ResultArray = @()
$Results | ForEach-Object {
$ResultArray += ,[PSCustomObject]@{
'Results' = (Import-Clixml -Path $_.FullName)
'DateTime' = $_.CreationTime
}
}
$Report = $ResultArray | Foreach-Object {
$DateTime = $_.DateTime
$_.Results | Group-Object -Property 'Server' | Foreach-Object {
$Server = $_.Name
$_.Group.Results | ForEach-Object {
$Object = [PSCustomObject]@{
"Server" = $Server
"Check" = $_.Check
"Result" = $_.Condition
"DateTime" = $DateTime
}
$Object
}
}
}
$FileName = "ServersReport-{0}.csv" -F (Get-Date -Format "yyyy_MM_dd_HH_mm_ss")
$Report | Export-CSV -Path (Join-Path -Path $ReportPath -ChildPath $FileName) -NoTypeInformation
$Report
Break
}
'Monthly' {
$Results = $Files | Where-Object 'CreationTime' -GT (Get-Date).AddDays(-30)
$ResultArray = @()
$Results | ForEach-Object {
$ResultArray += ,[PSCustomObject]@{
'Results' = (Import-Clixml -Path $_.FullName)
'DateTime' = $_.CreationTime
}
}
$Report = $ResultArray | Foreach-Object {
$DateTime = $_.DateTime
$_.Results | Group-Object -Property 'Server' | Foreach-Object {
$Server = $_.Name
$_.Group.Results | ForEach-Object {
$Object = [PSCustomObject]@{
"Server" = $Server
"Check" = $_.Check
"Result" = $_.Condition
"DateTime" = $DateTime
}
$Object
}
}
}
$FileName = "ServersReport-{0}.csv" -F (Get-Date -Format "yyyy_MM_dd_HH_mm_ss")
$Report | Export-CSV -Path (Join-Path -Path $ReportPath -ChildPath $FileName) -NoTypeInformation
$Report
Break
}
}
}
}
Running the monitoring with PowerShell script
There are several ways to use this script, but the simplest is to add the servers to check and then use the New-ServerReport command to determine which checks have run over time.
# Perform Checks on Requested Servers
@("Server1","Server2","Server3") | Invoke-ServerCheck
# Generate Daily Report
New-ServerReport
Modular structure provides flexibility when monitoring with PowerShell
By using PowerShell to create a modular framework to easily create and execute checks on servers, a system administrator can quickly gain better visibility and control of their environment. Although these checks are simple, there are many ways to extend the capabilities to include more in-depth inquiries into your server inventory.
With new cross-platform abilities of PowerShell 7, you can extend these checks to work on Linux systems to handle things such as necessary OS-specific updates.