UPDATE 4-11-23:
Ended up using PoshRSJob module instead of my runspacepool. Now my script runs in under 10 seconds. I don't think it can be any more optimized than that. I'll update the main runspace bit below.
UPDATE 4-5-23:
I updated and optimized my code even further. I am able to run this in under a minute. There were 2 major changes from my previous code.
- When I pull the list of servers from AD, I was using ADSISearcher. This is generally pretty fast, however I have come across even a faster method and it required less code. The new method I use is DSQUERY.
- The 2nd big update is how I calculate uptime on each server. I was trying to find a clever way to get the value. I tried several ways using get-wmiobject, ciminstance, reading from event viewer logs, trying various commands and sorting through the output, and the tick function. All of these were "acceptable", but still made my script take anywhere from 1+ min(s) to run. Then I thought of a different way of calculating this.
My theory was to grab a file or directory that we know changes at every boot time and no sooner. This wasn't an easy task because I came across a lot of false positives/negatives and couldn't find a file or directory that worked... until I did. What I noticed is the file located at "C:\Windows\Tasks\SA.DAT" seems to be exactly what I was looking for. So I calculated the time from when it was last written to to get the amount of days the server has been online and it's been working really well for me. It's also significantly faster. I cut off at least 20 seconds from my previous code.
From all the troubleshooting and searching I have done, I haven't come across a strategy like this even though if you think about it, it's pretty trivial. Anyway, I wanted to share with the community and hopefully you find it helpful!
I got my code working. I'm still open to any suggestions for optimization and tweaks, but the final code works for me. I can query 1000 servers and my script takes just over 1 minute to run.
I ended up using this code to do the comparison between my spreadsheet and what's in AD.
$FilteredServers = ($FilteredServers | Where-Object {($_.servername -in $Servers.name) -or ($_.DMZ -eq $True)})
Ironically the runspace to calculate the server uptime takes the longest amount of time, otherwise my script runs in 3-4 seconds.
If there is a more optimized way to check uptime, I'm all ears. By the way, I know cimInstance is better than using wmi-object, but I was having trouble using ciminstance on some servers and get-wmiobject seems to work without a hitch.
([timespan]::FromMilliseconds([Math]::Abs([Environment]::TickCount))).days
The code above runs fastest locally, but there isn't a good way to run it remotely on a bunch of servers.
Here is the whole script for anyone interested.
CLS
$ErrorActionPreference = 'SilentlyContinue'
$ErrorFile = (Split-Path $script:MyInvocation.MyCommand.Path) + "\ERROR.txt"
#Install the module that will let us perform certain tasks in Excel
#Install PSExcel Module for powershell
#Check if Module is imported
if(Get-Module -ListAvailable -Name "ImportExcel")
{
Import-Module ImportExcel
}
else
{
#Install NuGet (Prerequisite) first
Install-PackageProvider -Name NuGet -Scope CurrentUser -Force -Confirm:$False
Install-Module -Name ImportExcel -Scope CurrentUser -Force -Confirm:$False
Import-Module ImportExcel
}
#----------------------------------
#Clear screen again
CLS
#Start Timestamp
Get-Date
Write-Host " "
#------------------------------------ Global Variables
$Results = @()
$Output = @()
$Path = (Split-Path $script:MyInvocation.MyCommand.Path)
#------------------------------------ Setup Excel Variables
#The file we will be reading from
$ExcelFile = (Get-ChildItem -Path "$Path\*.xlsx").FullName
#Worksheet we are working on (by default this is the 1st tab)
$worksheet = (((New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList (New-Object -TypeName System.IO.FileStream -ArgumentList $ExcelFile,'Open','Read','ReadWrite')).Workbook).Worksheets[1]).Name
$Servers = Import-Excel -Path $ExcelFile -WorkSheetname $worksheet -StartRow 1
#------------------------------------ Populate our variable with data from spreadsheet
$ServersList = foreach($Server in $Servers) {
$Server | Select-Object @{Name="ServerName";Expression={$_.Child}}, "Primary", @{Name="PatchWindow";Expression={$_."Patch Window"}}, @{Name="TestServer";Expression={$_."Test Server"}}, "DMZ"
}
#------------------------------------ Remove Duplicate entries
$ServersList = ($ServersList | Sort-Object -Property ServerName -Unique)
#------------------------------------ Seperate Servers from DMZ Servers
$FilteredServers = $ServersList | ForEach-Object -Process {
if($($_.DMZ) -eq $true)
{
$_.ServerName = [System.String]::Concat("$($_.ServerName)",".DMZ.com")
}
if($($_.PatchWindow) -like "*PVS*")
{
$_.ServerName = [System.String]::Concat("$($_.ServerName)","- PVS *SKIP*")
}
$_
}
#------------------------------------ Grab all servers from AD so we can use to compare against our list
#Get Servers
$searcher = [adsisearcher]::new()
$searcher.Sort.PropertyName = "name"
#Search root is the OU level we want to search at
$searcher.SearchRoot = [adsi]"LDAP://DC=blah,DC=com"
#Make this any non zero value to expand the default result size.
$searcher.PageSize = 100
#Filter Computers only and enabled
$searcher.Filter = "(&(objectCategory=computer)(objectClass=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
#List the properties we are interested in
$searcher.PropertiesToLoad.AddRange(('name','operatingSystem'))
$Servers = Foreach ($Computer in $searcher.FindAll() | where { ($_.properties["operatingSystem"] -like "*Server*") }){ ($Computer.Properties) }
#------------------------------------ Compare our list to servers in AD and filter out appliances
$FilteredServers = ($FilteredServers | Where-Object {($_.servername -in $Servers.name) -or ($_.DMZ -eq $True)})
#------------------------------------ Script Block
[System.Management.Automation.ScriptBlock]$ScriptBlock = {
Param ( $FilteredServer)
$RebootNeeded = $Check = $NULL
#Ping servers to make sure they're responsive
if([bool](Get-CimInstance -ClassName Win32_PingStatus -Filter "Address='$($FilteredServer.ServerName)' AND Timeout=100") -eq $True)
{
$Check = ((Get-Date) - (Get-WmiObject win32_operatingsystem -ComputerName $FilteredServer.ServerName | select csname, @{LABEL='LastBootUpTime';EXPRESSION={$_.ConverttoDateTime($_.lastbootuptime)}}).LastBootUpTime).days
#Check if server hasn't been rebooted in greater than or equal to 20 days.
if($Check -ge 20)
{
$RebootNeeded = $FilteredServer | Add-Member -NotePropertyMembers @{"Day's Online" = $Check} -PassThru
}
}
return $RebootNeeded
}
function Invoke-AsyncJob
{
$AllJobs = New-Object System.Collections.ArrayList
$HostRunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(20,100,$Host)
$HostRunspacePool.Open()
foreach($FilteredServer in $FilteredServers)
{
$asyncJob = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock).AddArgument($FilteredServer)
$asyncJob.RunspacePool = $HostRunspacePool
$asyncJobObj = @{ JobHandle = $asyncJob
AsyncHandle = $asyncJob.BeginInvoke() }
$AllJobs.Add($asyncJobObj) > $NULL
}
$ProcessingJobs = $true
Do
{
$CompletedJobs = $AllJobs | Where-Object { $_.AsyncHandle.IsCompleted }
if($null -ne $CompletedJobs)
{
foreach($job in $CompletedJobs)
{
$RebootNeeded = $job.JobHandle.EndInvoke($job.AsyncHandle)
if($null -ne $RebootNeeded)
{
$Results += $RebootNeeded
}
$job.JobHandle.Dispose()
$AllJobs.Remove($job)
}
}
else
{
if($AllJobs.Count -eq 0)
{
$ProcessingJobs = $false
}
else
{
Start-Sleep -Milliseconds 1000
}
}
}
While ($ProcessingJobs)
$HostRunspacePool.Close()
$HostRunspacePool.Dispose()
$Output = ($Results | Sort-Object -Property ServerName)
#Output to File
$Output | Export-Csv -Path "$Path\Results.csv" -NoTypeInformation -Force
#Output to screen
return $Output
Write-Host "`n`n"
}
Invoke-AsyncJob
Write-Host " "
Get-Date
UPDATED CODE 4-5-23
CLS
#Install the module that will let us perform certain tasks in Excel
#Install PSExcel Module for powershell
#Check if Module is imported
if(Get-Module -ListAvailable -Name "ImportExcel")
{
Import-Module ImportExcel
}
else
{
#Install NuGet (Prerequisite) first
Install-PackageProvider -Name NuGet -Scope CurrentUser -Force -Confirm:$False
Install-Module -Name ImportExcel -Scope CurrentUser -Force -Confirm:$False
Import-Module ImportExcel
}
#----------------------------------
#Clear screen again
CLS
#Start Timestamp
$Start = Get-Date
#------------------------------------ Global Variables
#Global Variables
$Path = (Split-Path $script:MyInvocation.MyCommand.Path)
$ErrorFile = (Split-Path $script:MyInvocation.MyCommand.Path) + "\ERROR.txt"
$Results = @()
#------------------------------------ Setup Excel Variables
#The file we will be reading from
$ExcelFile = (Get-ChildItem -Path "$Path\*.xlsx").FullName
#Worksheet we are working on (by default this is the 1st tab)
$worksheet = (((New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList (New-Object -TypeName System.IO.FileStream -ArgumentList $ExcelFile,'Open','Read','ReadWrite')).Workbook).Worksheets[1]).Name
$ExcelServers = Import-Excel -Path $ExcelFile -WorkSheetname $worksheet -StartRow 1
#------------------------------------ Populate our variable with data from spreadsheet
$ExcelServersList = foreach($ExcelServer in $ExcelServers) {
$ExcelServer | Select-Object @{Name="ServerName";Expression={$_.Child}}, "Primary", @{Name="PatchWindow";Expression={$_."Patch Window"}}, @{Name="TestServer";Expression={$_."Test Server"}}, "DMZ"
}
#------------------------------------ Remove Duplicate entries
$SortedExcelServersList = ($ExcelServersList | Sort-Object -Property ServerName -Unique)
#------------------------------------ Seperate Servers from DMZ Servers
$FilteredServers = ForEach($SortedExcelServerList in $SortedExcelServersList) {
if($($SortedExcelServerList.DMZ) -eq $true)
{
$SortedExcelServerList.ServerName = [System.String]::Concat("$($SortedExcelServerList.ServerName)",".DMZ.com")
}
$SortedExcelServerList
}
#------------------------------------ Grab all servers from AD so we can use to compare against our list - also trim any whitespaces from output
$Servers = (dsquery * -filter "(&(objectClass=Computer)(objectCategory=Computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(operatingSystem=*Server*))" -limit 0 -attr Name | sort).trim()
#------------------------------------ Compare our list to servers in AD and filter out appliances
$FilteredServersResult = $Null
$FilteredServersResult = ForEach ($Item in $FilteredServers)
{
If (($item.servername -in $Servers) -or ($item.DMZ -eq $True))
{
$Item
}
}
#------------------------------------ Script Block
[System.Management.Automation.ScriptBlock]$ScriptBlock = {
Param ( $FilteredServer)
$RebootNeeded = $Check = $NULL
$ServerPath = "\\$($FilteredServer.ServerName)\c$\Windows\Tasks\SA.DAT"
#Ping servers to make sure they're responsive
if($NULL -ne (Get-CimInstance -ClassName Win32_PingStatus -Filter "Address='$($FilteredServer.ServerName)' AND Timeout=100").ResponseTime)
{
#Instead of calculating server uptime, we check by getting the lastwrittenTime for a file that generally updates @ every boot. You can calculate the difference to get the # of days server is up
$Check = ((Get-Date) - (Get-ChildItem $ServerPath -Force).LastWriteTime).Days
#Check if server hasn't been rebooted in greater than or equal to 25 days.
if($Check -ge 20)
{
$RebootNeeded = $FilteredServer | Add-Member -NotePropertyMembers @{"Day's Online" = $Check} -PassThru
}
}
return $RebootNeeded
}
function Invoke-AsyncJob
{
$AllJobs = New-Object System.Collections.ArrayList
$HostRunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(20,100,$Host)
$HostRunspacePool.Open()
foreach($FilteredServer in $FilteredServersResult)
{
$asyncJob = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock).AddArgument($FilteredServer)
$asyncJob.RunspacePool = $HostRunspacePool
$asyncJobObj = @{ JobHandle = $asyncJob
AsyncHandle = $asyncJob.BeginInvoke() }
$AllJobs.Add($asyncJobObj) > $NULL
}
$ProcessingJobs = $true
Do
{
$CompletedJobs = $AllJobs | Where-Object { $_.AsyncHandle.IsCompleted }
if($null -ne $CompletedJobs)
{
foreach($job in $CompletedJobs)
{
$RebootNeeded = $job.JobHandle.EndInvoke($job.AsyncHandle)
if($null -ne $RebootNeeded)
{
$Results += $RebootNeeded
}
$job.JobHandle.Dispose()
$AllJobs.Remove($job)
}
}
else
{
if($AllJobs.Count -eq 0)
{
$ProcessingJobs = $false
}
else
{
Start-Sleep -Milliseconds 1000
}
}
}
While ($ProcessingJobs)
$HostRunspacePool.Close()
$HostRunspacePool.Dispose()
($Results | Sort-Object -Property ServerName) | Export-Csv -Path "$Path\Results.csv" -NoTypeInformation -Force
#Output to screen
return $Results
Write-Host "`n`n"
}
Invoke-AsyncJob
#End Timestamp
$End = Get-Date
$End - $Start
UPDATED CODE 4-11-23
CLS
#Install the module that will let us perform certain tasks in Excel
#Install PSExcel Module for powershell
#Check if Module is imported
#------------- PoshRSJob (multitasking)
if((Get-Module -ListAvailable -Name "PoshRSJob") -or (Get-Module -Name "PoshRSJob"))
{
Import-Module PoshRSJob
}
else
{
Install-Module -Name PoshRSJob -Scope CurrentUser -Force -Confirm:$False
Import-Module PoshRSJob
}
#------------- ImportExcel Module
if((Get-Module -ListAvailable -Name "ImportExcel") -or (Get-Module -Name "ImportExcel"))
{
Import-Module ImportExcel
}
else
{
#Install NuGet (Prerequisite) first
Install-PackageProvider -Name NuGet -Scope CurrentUser -Force -Confirm:$False
Install-Module -Name ImportExcel -Scope CurrentUser -Force -Confirm:$False
Import-Module ImportExcel
}
#----------------------------------
#Clear screen again
CLS
#Start Timestamp
$Start = Get-Date
#------------------------------------ Global Variables
#Global Variables
$Path = (Split-Path $script:MyInvocation.MyCommand.Path)
$ErrorFile = (Split-Path $script:MyInvocation.MyCommand.Path) + "\ERROR.txt"
$Results = @()
#------------------------------------ Setup Excel Variables
#The file we will be reading from
$ExcelFile = (Get-ChildItem -Path "$Path\*.xlsx").FullName
#Worksheet we are working on (by default this is the 1st tab)
$worksheet = (((New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList (New-Object -TypeName System.IO.FileStream -ArgumentList $ExcelFile,'Open','Read','ReadWrite')).Workbook).Worksheets[1]).Name
$ExcelServers = Import-Excel -Path $ExcelFile -WorkSheetname $worksheet -StartRow 1
#------------------------------------ Populate our variable with data from spreadsheet
$ExcelServersList = foreach($ExcelServer in $ExcelServers) {
$ExcelServer | Select-Object @{Name="ServerName";Expression={$_.Child}}, "Primary", @{Name="PatchWindow";Expression={$_."Patch Window"}}, @{Name="TestServer";Expression={$_."Test Server"}}, "DMZ"
}
#------------------------------------ Remove Duplicate entries
$SortedExcelServersList = ($ExcelServersList | Sort-Object -Property ServerName -Unique)
#------------------------------------ Seperate Servers from DMZ Servers
$FilteredServers = ForEach($SortedExcelServerList in $SortedExcelServersList) {
if($($SortedExcelServerList.DMZ) -eq $true)
{
$SortedExcelServerList.ServerName = [System.String]::Concat("$($SortedExcelServerList.ServerName)",".DMZ.com")
}
$SortedExcelServerList
}
#------------------------------------ Grab all servers from AD so we can use to compare against our list - also trim any whitespaces from output
$Servers = (dsquery * -filter "(&(objectClass=Computer)(objectCategory=Computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(operatingSystem=*Server*))" -limit 0 -attr Name | sort).trim()
#------------------------------------ Compare our list to servers in AD and filter out appliances
$FilteredServersResult = $Null
$FilteredServersResult = ForEach ($Item in $FilteredServers)
{
If (($item.servername -in $Servers) -or ($item.DMZ -eq $True))
{
$Item
}
}
#------------------------------------ Multithreading Magic using Posh-RSJob
$FilteredServersResult | Start-RSJob -Throttle 25 -ScriptBlock {
Param($Server)
#Ping servers to make sure they're responsive
if($NULL -ne (Get-CimInstance -ClassName Win32_PingStatus -Filter "Address='$($Server.servername)' AND Timeout=100").ResponseTime)
{
Try
{
$ServerPath = "\\$($Server.ServerName)\c$\Windows\ServiceProfiles\LocalService\NTUSER.DAT"
#If you're checking file date
$Check = ((Get-Date) - (dir $ServerPath -force -ErrorAction Stop).LastWriteTime).days
}
Catch
{
Try
{
$Check = ((Get-Date) - (Get-WmiObject win32_operatingsystem -ComputerName $($Server.ServerName) -ErrorAction Stop | select csname, @{LABEL='LastBootUpTime';EXPRESSION={$_.ConverttoDateTime($_.lastbootuptime)}}).LastBootUpTime).days
}
Catch
{
if($Error.exception -like "*")
{
($Server | Add-Member -NotePropertyMembers @{"Error" = [string]$Error} -PassThru) | Export-Csv -Path $using:ErrorFile -NoTypeInformation -Force -Append
}
#$Error | Select –Property *
}
}
#Check if server hasn't been rebooted in greater than or equal to 20 days.
if($Check -ge 20)
{
$Server | Add-Member -NotePropertyMembers @{"Day's Online" = $Check} -PassThru
}
}
} | Wait-RSJob -ShowProgress | Receive-RSJob | Export-Csv -Path "$Path\Results.csv" -NoTypeInformation -Force
#End Timestamp
$End = Get-Date
$End - $Start