Using PowerShell to List Domain Computers by Uptime

2015-11-18T20:30:00Z

This article introduces a short but powerful PowerShell snippet, the basis of which I find useful for performing a variety of ad-hoc tasks in my day-to-day work as a sysadmin.

I had recently applied a change to a series of workstations via Group Policy. It was a slightly tricky software upgrade. Generally, when a software package is installed using an MSI, deployment with Group Policy couldn't be simpler. However, this was an executable I'd wrapped into an MSI with custom actions to install/uninstall the underlying software defined as custom actions in the MSI's database. In my tests, attaching a new "wrapped" MSI for the new version of the software to an existing Group Policy Object had been successful. I'd specified that the new "wrapped" MSI should upgrade any existing installations.

Software Package GPOs are applied to domain workstations on start-up. I have a scheduled task set-up on a domain controller that reboots all the workstations within that OU on a two week rolling cycle, to ensure that software upgrades and patches do get applied.

When I've created or amended and then tested a GPO on a few select machines in a special org unit, I'll apply the tested GPO to other machines on the domain. I generally spot check a few machines just to make sure that the Group Policy changes have been applied successfully. It's not often that mistakes are made, but I always feel it's my duty to know about a problem before anyone else does. This time, my first spot check revealed a problem with the software upgrade. In this instance the previous version of the software had been removed and the new version only partially installed. The wrapped executable installer had been copied into it's target location on the machine, but it had not been executed.

I had to find out how many workstations out of those that were rebooted over night are affected? Was my software upgrade faulty or was the first machine I checked in an unknown state? I needed to find some more machines to check. I needed to find out how many machines had been restarted since I implemented the Group Policy change?

I first went hunting for the log file that the scheduled reboot task appends to each time it's run. I've used this many times before, but this time I couldn't find it. A concern in itself. Admittedly, it's the first time I'd gone hunting for it since upgrading the domain controllers from Windows Server 2008 to Windows Server 2012 r2. So in a step toward diagnosing one problem, I've found another.

I checked the scheduled task itself - it's last run was at 3am that morning with an exit code of 0. So the scheduled task is running, just not logging. I needed more confirmation than that. I needed to find out just how many machines, if any, had been rebooted by the scheduled task.

A quick PowerShell script would help answer that question, and also tell me what other machines I should check for the software upgrade. Here's what I chalked up:

$scriptBlock={ 
    $wmi = Get-WmiObject -Class Win32_OperatingSystem
    ($wmi.ConvertToDateTime($wmi.LocalDateTime) – $wmi.ConvertToDateTime($wmi.LastBootUpTime)).TotalHours
}

$UpTime = @()

Import-Module ActiveDirectory
Get-ADComputer -Filter 'ObjectClass -eq "Computer"' -SearchBase "OU=someOu,DC=someDomain,DC=someTld"  -SearchScope Subtree `
    | % { $Uptime += `
            (New-Object psobject -Property @{
                    "ComputerName" = $_.DNSHostName
                    "UpTimeHours" = (Invoke-Command -ComputerName $_.DNSHostName -ScriptBlock $scriptBlock)
                }
            )
        }

$UpTime | Where-Object {$_.UpTimeHours -ne ""} | sort-object -property @{Expression="UpTimeHours";Descending=$false} | `
    Select-Object -Property ComputerName,@{Name="UpTimeHours"; Expression = {$_.UpTimeHours.ToString("#.##")}} | `
        Format-Table -AutoSize

And the output? It looks something like this:

ComputerName                 UpTimeHours
------------                 -----------
marge.someDomain.someTld     1.96       
maggie.someDomain.someTld    4.34       
mel.someDomain.someTld       5.27       
apu.someDomain.someTld       6.35      
ralph.someDomain.someTld     6.35      
auto.someDomain.someTld      6.36      
ned.someDomain.someTld       6.36      
itchy.someDomain.someTld     26.03      
lisa.someDomain.someTld      29.04      
krusty.someDomain.someTld    32.16      
lenny.someDomain.someTld     32.18      
batman.someDomain.someTld    32.42      
homer.someDomain.someTld     45.28      
troy.someDomain.someTld      54.36      
wiggum.someDomain.someTld    54.36      
willie.someDomain.someTld    64.95      
patty.someDomain.someTld     125.01     
bart.someDomain.someTld      126.35     
selma.someDomain.someTld     126.35     
barney.someDomain.someTld    126.35     
todd.someDomain.someTld      150.87     
moe.someDomain.someTld       150.34     
hibbert.someDomain.someTld   174.35     
nelson.someDomain.someTld    174.55

The first 7 machines on the list were rebooted either by the nightly scheduled task that reboots all the workstations or by user intervention afterwards. Those are the machines that should have had the new software package deployed. Incidentally, the software upgrade was successful on all machines, except for the first one I spot-checked. I'm putting it down to something unexpected happening due to the machine being in an unknown state.

This output also makes it look like the reboot scheduled task is working consistently, even allowing for the weekend days on which no machines are rebooted.

So briefly then, here's an explanation of the PowerShell script I used, starting with the inital $scriptBlock.

$scriptBlock={ 
    $wmi = Get-WmiObject -Class Win32_OperatingSystem
    ($wmi.ConvertToDateTime($wmi.LocalDateTime) – $wmi.ConvertToDateTime($wmi.LastBootUpTime)).TotalHours
}

This script block is defined here and run later on in the script. It grabs an object representing Win32_OperatingSystem WMI class and then uses it to retrieve and subtract the last boot up time from the current time.

Then we initialise an associative array in which to store the results:

$UpTime = @()

Next we import the ActiveDirectory module so that we can use it's Get-ADComputer commandlet. We tell the commandlet that we're searching the domain tree for computers within an org unit called someOu. We also tell it we would like to search recursively for any computers in subordinate org units.

Import-Module ActiveDirectory
Get-ADComputer -Filter 'ObjectClass -eq "Computer"' -SearchBase "OU=someOu,DC=someDomain,DC=someTld"  -SearchScope Subtree `

Almost finally, for each computer found in active directory, we create a new object and add it to the $UpTime associative array. The object has two properties, the first is the ComputerName which is derived from the DNSHostName of the computer returned from Get-ADComputer. The second is UpTimeHours - this is where the script block we defined earlier comes in. The script block is run against each computer returned from Get-AdComputer commandlet, giving us the uptime for each computer.

    | % { $UpTime += ` to 
           (New-Object psobject -Property @{
                    "ComputerName" = $_.DNSHostName
                    "UpTimeHours" = (Invoke-Command -ComputerName $_.DNSHostName -ScriptBlock $scriptBlock)
                }

All that's left is to return, select, sort and format the output. We take the $UpTime associative array of results and pipe it into Where-Object just to filter out the computers for which we didn't obtain a result. These are machines not currently available on the network because they have been shutdown by the end users. Next up, Sort-Object is used to sort the results by the UpTime so that the more recently rebooted computers appear at the top of the list. Select-Object is then used to get both properties of each $UpTime object. This isn't strictly necessary, but it does mean I can format the UpTime to two decimal places using an expression. The finishing touch is Format-Table just to get columns of a reasonable size, thus making the output that bit more readable.

$UpTime | Where-Object {$_.UpTimeHours -ne ""} | Sort-Object -property @{Expression="UpTimeHours";Descending=$false} | `
    Select-Object -Property ComputerName,@{Name="UpTimeHoirs"; Expression = {$_.UpTimeHours.ToString("#.##")}} | `
        Format-Table -AutoSize

Get-ADComputer is a versatile and powerful commandlet because it's output can be used in so many different ways. I've used to enable, disable and even create new scheduled tasks on a suite of machines when I've been in a pinch. Or even to shutdown workstations as an energy saving measure at the start of the Christmas/New-Year break.

I hope you've found this useful.