Implement simple server monitoring with PowerShell

As your server inventory grows, you’ll need some help to make sure you can avoid any issues.

There are many server monitoring and reporting options on the market, but some situations may require a lightweight solution. Monitoring with PowerShell is a way to use native Windows functionality to create scripts that check your systems and send out regular updates.

This article explains how to create a simple framework for Windows Server controls with the added benefit of generating reports at different intervals to aid in your monitoring efforts. Although PowerShell 7 is available, this tutorial is based on Windows PowerShell 5.1 due to the ease of its default PowerShell remote configuration process.

Server monitoring with PowerShell

While there are many server checks you can perform, this article will focus on a few to demonstrate the capabilities of a simple PowerShell monitoring framework. This tutorial will cover:

  • Disk space. Check local disks and trigger a warning if the percentage of free space drops below 10%.
  • Operating system version. Check if the version number is lower than 6.2. If so, the server operating system is older than Windows Server 2012, 2016, and 2019 and is no longer supported by Microsoft.
  • Expired certificates. Compile a list of certificates that are within 30 days of expiration.
  • Use of license. Check the licensed states and report the unlicensed ones.

Script framework for monitoring servers with PowerShell

The general idea behind this server monitoring framework is to run a series of checks on one or more servers, record the results, and then review the results. To do this, we will create functions in the PowerShell script.

  • Call ServerCheck. This function will take care of a series of checks and computers on which to run these scripts, and then export the results to an XML file.
  • New-Server Report. This function will take the XML files and generate different formats depending on the type of report requested, daily or monthly.

Build server controls

There are many ways to structure a potential script to perform server checks, but this tutorial will place all the checks in a hash table that uses script blocks, making it easier to run the code on the requested computers, by local or remote.

In addition, the script uses a state key and associated scriptblock to report whether the check succeeded or failed. Each condition must return a Boolean value.

Adding new checks is as easy as adding a new top-level key with a subkey of Check and state. Both are scriptblocks executed by Summon 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 The function handles most of the server monitoring with PowerShell. This function takes an array of checks from the $ Checks variable, each set of checks will be run on the servers or on the local computer.

The script performs the following steps:

  1. iterates over each check in the $ Checks variable;
  2. runs Summon Command on the script block of the Checks key;
  3. store the result in a $ CheckResults variable; and
  4. saves the XML of the $ Exit variable to the requested path, which allows easier manipulation of this variable in subsequent 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 showing which checks passed or failed for any of the given servers.

How the New-ServerReport function works

To better understand which checks have been performed on which servers, we will use the New-Server Report function.

The function performs the following steps:

  1. searches for XML files matching the name Server Results;
  2. performs checks depending on whether Daily Where Monthly the report exists;
  3. look at him Creation time to determine whether to remove all files on the given day or 30 days back;
  4. after having gathered the results, regroups them according to the servers then outputs a table of controls; and
  5. saves 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 monitoring with the PowerShell script

There are several ways to use this script, but the easiest is to add the servers to check and then use the New-Server Report to determine which checks have been run over time.


@("Server1","Server2","Server3") | Invoke-ServerCheck
New-ServerReport

Modular structure provides flexibility when monitoring with PowerShell

By using PowerShell to create a modular framework for easily creating and running controls on servers, a system administrator can quickly gain greater visibility and control of their environment. While these checks are straightforward, there are many ways to expand the capabilities to include more in-depth investigations into your server inventory.

With the new cross-platform capabilities in PowerShell 7, you can extend these checks to work on Linux systems to handle things like necessary operating system-specific updates.


Source link