Ping Multiple Computers Rapidly with a Custom PowerShell Class

Today I published a custom class for users of PowerShell 5 that allows you to ping multiple computers very quickly using a custom PS object.  It uses .Net runspaces to create a parallel processing environment to ensure speedy execution, and stores useful information about the ping results in the custom object, such as:

  • The full “Test-Connection” results
  • The list of online machines
  • The list of offline machines
  • The count and percentage of online and offline machines
  • The execution time of the command

capture

Check it out here:

https://smsagent.wordpress.com/posh-5-custom-classes/power-ping/

Creating a Simple Class Library for PowerShell 5

PowerShell 5 brings some nice capability to PowerShell, including support for the creation of custom classes.  Very simply, a class can be used to define a custom type, and allow you to create an object of that custom type.  This can be useful for example, if you want to create an object that has specific properties that you define, as well as some methods, or portions of code that do something specific to your need and relevant to that object.

Class libraries are used in programming so that instead of having to create code to do low-level, fundamental or often-repeated tasks, a collection of classes are provided for you so you can simply call a class and its properties and methods when you need it.

You can call .Net classes in Powershell using a type accelerator. For example, this code gives me the value of PI using the System.Math class. It is defined in the class as a static property.


[math]::PI

Since the value of PI is returned with 14 decimal places, I might want to reduce that say to 4 decimal places. So I can call a static method on the System.Math class to round the number of decimal places to 4:


[math]::Round([math]::PI,4)

Create a Custom Class

I won’t go into detail here about how to create a custom class (Stephane van Gulick gives a nice introduction), but below is a simple example of a class I created in PowerShell, which allows me to create an object representing a user account in Active Directory.  I have defined exactly what properties I want to have returned so I don’t have to pass a list of properties to the Get-ADUser cmdlet every time I run it.


class ADUser
{
    [string]$Username
    [string]$Enabled
    [string]$Displayname
    [string]$Title
    [string]$Department
    [string]$EmailAddress
    [string]$CanonicalName
    [string]$City
    [string]$co
    [string]$OfficePhone
    [string]$MobilePhone
    [string]$ipPhone
    [array]$MemberOf
    [array]$DirectReports
    [string]$homeMDB
    [string]$Created
    [string]$Modified
    [string]$LastBadPasswordAttempt
    [string]$PasswordLastSet

   # Constructor
   ADUser ([string] $Username)
   {
        $this.Username = $Username
        $Properties = @(
            'CanonicalName',
            'City',
            'co',
            'Created',
            'Department',
            'DirectReports',
            'Displayname',
            'EmailAddress',
            'Enabled',
            'homeMDB',
            'ipPhone',
            'LastBadPasswordAttempt',
            'MemberOf',
            'MobilePhone',
            'Modified',
            'OfficePhone',
            'PasswordLastSet',
            'Title'
        )
       try
        {
            $P = Get-ADUser $Username -Properties $Properties | Select $Properties
            $Properties | foreach {
                $this.$_ = $P.$_
                }
        }
       Catch
        {
            $_; continue
        }
   }
}

I can create a custom object from this class in a couple of ways:


# create a new object
$me = New-Object -TypeName ADUser -ArgumentList tjones

# Instantiate using the 'new' static method
$me = [ADUser]::new('tjones')

# Simply set the variable type
[ADUSer]$me = 'tjones'

In each case, I need to pass a username, or actually any of the properties in this list, as this is required by the class constructor and the Get-ADUser cmdlet:
— A distinguished name
— A GUID (objectGUID)
— A security identifier (objectSid)
— A SAM account name (sAMAccountName)

When I create the object, the code runs and gets the info from AD.  When I call the variable I can see that the properties I have defined are populated:

captures

I can view the list of properties on the object using the dot operator…

capture

…or view an individual property…

capture

…or even find the value of a property without first storing it to a variable:

capture

Scope

This custom class is quite handy to quickly find out information about a user, and I want to be able to call this class any time I need it.  Trouble is, in the current implementation of this, a custom class is limited in scope and I can only use this class in the same context I’ve created it in.  Or to say it another way, I can’t change the class scope in the same way you could a variable for example.  You can of course change the scope of the object you create from the class, for example:


New-Variable -Name me -Value ([ADuser]::new('tjones')) -Scope Script

But once my current session or script is closed, this custom class is no longer available to me.  To use it again, I must add the class code and run it in each session or script so it is available in the current context.  This can make your scripts fatter than they need to be.

So here’s an idea:

Create a Class Library (of sorts)

In C# for example, you can create a library of classes as a dll file, but for PowerShell I can’t do that.

So why not simply create a folder of class code saved into text files, then read and run the code in your session?

Here’s one way to do that.  In my PowerShell $Profile directory, I have two folders “Modules” and “Scripts”.  I add a folder called “Classes”.

capture

I create the code for my custom class using PowerShell ISE, then save it as a text file to the Classes folder.  Any time I create a new custom class that I want to reuse later, I save it as a text file in this location.

capture

Now I can simply read the contents of a class file and run it in my current session with a one-liner to give me instant access to this custom class.


Invoke-Expression $([System.IO.File]::ReadAllText('C:\Users\tjones\Documents\WindowsPowerShell\Classes\ADUser.txt'))

I can also load in all the custom classes in the Classes directory into my session like this:

 

# Import Custom Classes
Get-ChildItem '$env:USERPROFILE\Documents\WindowsPowerShell\Classes\' |
    Select -ExpandProperty FullName |
        foreach {
            $class = [System.IO.File]::ReadAllText($_)
            Invoke-Expression $Class
        }

Even better, I can add this code into my PowerShell profile scripts (the Microsoft.Powershell_profile.ps1 and Microsoft.PowershellISE_profile.ps1) and all these classes will be available for me to use every time I start a new PowerShell session 🙂

Get Creative

This capability for custom classes in PowerShell opens a door of creativity for IT administrators and developers alike.  You can create simple classes to use in a script for example, or create reusable classes in a kind of class library as we have discussed, and you can add custom code that is relevant to the custom object so you can simply call a method instead of writing out the code each time.

How would custom classes be useful to you?  What kinds of objects and methods would be useful in your day to day work?

I do a lot of troubleshooting on remote computers, so I wrote a custom class that gets lots of useful information from a remote computer, or performs certain tasks on the remote computer.

In the example below, I’ve created a custom computer object representing “PC001”. Creating the object runs a quick “Test-Connection” against the machine, so I call the “Online” property to see if it is.  Then I get the current user using the “GetCurrentUser()” method, then I output a list of methods in the object.  These methods allow me to get information from Active Directory for the computer, get hardware information, get installed hotfixes or software, get local administrators and other useful things.

I can also start / stop services, or kill running processes.

capture

The custom type also contains a couple of static methods that I can use without creating an object and assigning it to a variable.  “GetComputerFromUser()” checks my Configuration Manager database to find the computer/s that a user was last logged into, and “GetSerialTag()” gets the serial number (or asset tag) for the remote computer.

capture

Cool stuff 🙂

Get OSD Info Post-Deployment with PowerShell

In MDT-integrated Configuration Manager, a UDI task sequence contains a couple of steps called Branding to Reg that brands OSD variables to the registry.  This can be useful for reporting, and they can be inventoried with Configuration Manager using the handy RegKeyToMOF utility.

Capture

These steps can also be added manually to a ZTI task sequence as Kenneth van Surksum describes in his blog series.

The steps run a script called “OSDBranding.vbs” which defines which variables will be stamped to the following registry location:

HKLM\Software\Microsoft\MPSD\OSD

You can edit this script to add / remove any variables you want to save in the registry.  If you have custom variables that begin with “OSD” for example, these will be saved to the registry by default.

I wrote the following PowerShell script to retrieve these OSD registry values post-deployment from any remote computer.  The script includes a calculation of the deployment duration if the OSDStartTime and OSDEndTime variables are populated and adds it as OSDDuration.  It only returns properties that actually have populated values.

Example:


Get-OSDInfo -Computername PC001

Capture
 

Get-OSDInfo


function Get-OSDInfo
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true,
                ValueFromPipelineByPropertyName = $true,
                ValueFromPipeline = $true,
        Position = 0)]
        [ValidateScript({
                    Test-Connection -ComputerName $_ -Count 2 -Quiet
        })]
        [string]$ComputerName
    )

    # Define code to run
    $Code = {
        $results = @()

        # Check if the registry key exists, and get the property list if it does
        try
        {
            $Properties = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\MPSD\OSD' | Select-Object -ExpandProperty Property -ErrorAction Stop
        }
        catch
        {
            Write-Host -Object "$_" -ForegroundColor Red
            continue
        }

        # Get the property values for each key
        $Properties | ForEach-Object -Process {
            $value = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\MPSD\OSD' -Name $_ | Select-Object -ExpandProperty $_
            if ($value)
            {
                $obj = New-Object -TypeName psobject
                Add-Member -InputObject $obj -Name Property -Value $_ -MemberType NoteProperty
                Add-Member -InputObject $obj -Name Value -Value $value -MemberType NoteProperty
                $results += $obj
            }
        }

        # Calculate OSD duration if start time and end time exists
        $start = $results | Where-Object -FilterScript {
            $_.Property -eq 'OSDStartTime'
        }
        $end = $results | Where-Object -FilterScript {
            $_.Property -eq 'OSDEndTime'
        }
        if (($start.Value -ne $null) -and ($end.Value -ne $null))
        {
            $Hours = (($end.Value | Get-Date) - ($start.Value | Get-Date)).Hours
            $Minutes = (($end.Value  | Get-Date) - ($start.Value | Get-Date)).Minutes
            $Duration = "$Hours hours $Minutes minutes"
            $obj = New-Object -TypeName psobject
            Add-Member -InputObject $obj -Name Property -Value 'OSDDuration' -MemberType NoteProperty
            Add-Member -InputObject $obj -Name Value -Value $Duration -MemberType NoteProperty
            $results += $obj
        }

        # Sort and return results
        $results = $results | Sort-Object -Property Property
        if ($results)
        {
            return $results
        }
    }

    # Invoke the code remotely
    try
    {
        Invoke-Command -ComputerName $ComputerName -ScriptBlock $Code -ErrorAction Stop |
        Select-Object -Property Property, Value |
        Format-Table -AutoSize
    }
    catch
    {
        Write-Host -Object "$_" -ForegroundColor Red
    }
}

 

 

Get Incremental Collection Evaluation Results in ConfigMgr with PowerShell

Update 04-Mar-16: Added translation of CollectionIDs to CollectionNames

You may be aware that there is a ‘soft limit’ to the number of collections that can use incremental refresh in Configuration Manager 2012 onward, as described in this technet article.  It’s important to check that the time taken for the incremental collection evaluation cycle does not exceed the frequency with which the cycle runs, or the cycle will not complete before the next one begins, meaning some collections will never get incrementally refreshed.

There is a useful tool in the Configuration Manager toolkit called Collection Evaluation Viewer, which can display the evaluation queues in real-time as they run, as well as how long each collection took to evaluate and how many member changes were made.  But the tool does not tell you the total time taken for the evaluation cycle, which can be important to know.

To that end, I prepared a PowerShell script that reads the colleval.log to find various data about the most recent or currently running incremental collection evaluation, including the start and end time, how long the evaluation took to run, the number of collections evaluated, and the list of collections evaluated with the time and duration of each evaluation.

capture

Because the colleval.log is continually in use and will periodically rollover, getting information from the log is inherently dynamic.  Therefore it’s possible that incomplete results may be returned if the log file was rolled over during the most recent evaluation, or if the evaluation is currently processing when the script is run.

The script also has a parameter –TimePeriodInMinutes, which should be set to the same as (or possibly less than) your Collection Membership Evaluation cycle interval (default 5 minutes).  Setting it higher may return misleading results.

The script uses PSRemoting to the primary site server, so should not be run on the site server itself.

 

Finally, the list of collections evaluated is returned as an array, so to read the list save the results to a variable like this:

$Result = Get-IncrementalCollectionEvaluationResults -SiteServer SCCM-01
$Result.'Collections Evaluated List'

Get-IncrementalCollectionEvaluationResults

If you have PowerShell 5, you can download from the PowerShell Gallery:

Install-Script Get-IncrementalCollectionEvaluationResults

Or here’s the script:

 <#
.Synopsis
   Gets statistics for the most recent or currently running Incremental Evaluation Cycle (Express Evaluation) from the colleval.log in Configuration Manager
.NOTES
   Reads the colleval.log on a Configuration Manager Primary Site server to extract data and statistics from the Incremental Collection evaluation cycle.

   Since the log file may contain results from more than one cycle, you should limit the time period in which to search the log file using the -TimePeriodInMinutes parameter.  This should
   be set to the same value (or less) as your Collection Membership Evaluation Cycle interval in Configuration Manager (default 5).

   As this log file is frequently updated and periodically rolled over, it's possible that incomplete results may be returned if the current log file itself does not contain all the log
   entries for the most recent cycle.

   If the cmdlet is run while an incremental evaluation cycle is currently in progress, incomplete results will also be returned until the cycle has finished.

   The cmdlet returns the list of collections evaluated as an array, so save the output to a variable to view them.

   PSRemoting is used so the cmdlet should not be run on the site server itself.
.EXAMPLE
   Get-IncrementalCollectionEvaluation -SiteServer SCCM-01

   Returns statistics for the most recent incremental evaluation cycle on the site server SCCM-01
.EXAMPLE
   Get-IncrementalCollectionEvaluation -SiteServer SCCM-01 -TimePeriodInMInutes 10

   Returns statistics for the most recent incremental evaluation cycle on the site server SCCM-01 with an extended time period
.EXAMPLE
   $Result = Get-IncrementalCollectionEvaluation -SiteServer SCCM-01
   $Result.'Collections Evaluated List'

   Returns the list of collections that were evaluated in the time period with the collection ID, how long the evaluation took and the time of the evaluation.
#>
  
    [CmdletBinding()]
    Param
    (
        # ConfigMgr Primary Site Server name
        [Parameter(Mandatory = $true)]
        [string]$SiteServer,
        
        # Number of minutes past to check the colleval.log
        [Parameter(Mandatory = $false)]
        [int]$TimePeriodInMinutes = 5
    )


###################
# Get the results #
###################

# Set the earliest date/time to return results for
[datetime]$CurrentTime = Get-Date
$EarliestTime = $CurrentTime.AddMinutes(-$TimePeriodInMinutes) | Get-Date

# Open a new PS remoting session to the site server
if ($env:COMPUTERNAME -eq $SiteServer)
    {
        Write-host 'This script uses PSRemoting to the site server and cannot be run on the site server itself.' -ForegroundColor Red
        continue
    }
$s = New-PSSession -ComputerName $SiteServer

# Get the current number of collections in the incremental evaluation graph
[array]$CollectionsFound = Invoke-Command -Session $s -ScriptBlock {
$CollectionsFound = @()
$string = Select-String -Path "$env:SMS_LOG_PATH\colleval.log" -Pattern 'collections in incremental evaluation graph'
$string | ForEach-Object -Process {
    $obj = '' | Select-Object -Property String, Date
    $obj.String = $_.Line.Split('$')[0]
    [datetime]$obj.Date = $_.Line.Split('<')[2] -replace ".{4}$"
    $CollectionsFound += $obj
    }
$CollectionsFound = $CollectionsFound | Where-Object -FilterScript {
$_.date -ge $using:EarliestTime
}
$CollectionsFound
} | Select-Object -Property String, Date

# Get the time the evaluation cycle started
[array]$EvaluationStarted = Invoke-Command -Session $s -ScriptBlock {
$EvaluationStarted = @()
$string = Select-String -Path "$env:SMS_LOG_PATH\colleval.log" -Pattern 'Express Evaluator] Starting'
$string | ForEach-Object -Process {
    $obj = '' | Select-Object -Property String, Date
    $obj.String = $_.Line.Split('$')[0]
    [datetime]$obj.Date = $_.Line.Split('<')[2] -replace ".{4}$"
    $EvaluationStarted += $obj
    }
$EvaluationStarted = $EvaluationStarted | Where-Object -FilterScript {
$_.date -ge $using:EarliestTime
}
$EvaluationStarted
} | Select-Object -Property String, Date

# Get the list of collections that were evaluated
[array]$CollectionEvaluated = Invoke-Command -Session $s -ScriptBlock {
$CollectionEvaluated = @()
$string = Select-String -Path "$env:SMS_LOG_PATH\colleval.log" -Pattern 'Express Evaluator] successfully evaluated collection'
$string | ForEach-Object -Process {
    $obj = '' | Select-Object -Property CollectionID, SecondsToEvaluate, Date
    $obj.CollectionID = $_.Line.Split('$')[0].Split('[')[2].Split(']')[0]
    $obj.SecondsToEvaluate = $_.Line.Split('$')[0].Split()[9]
    [datetime]$obj.Date = $_.Line.Split('<')[2] -replace ".{4}$"
    $CollectionEvaluated += $obj
    }
$CollectionEvaluated = $CollectionEvaluated | Where-Object -FilterScript {
$_.date -ge $using:EarliestTime
}
$CollectionEvaluated
} | Select-Object -Property CollectionID, SecondsToEvaluate, Date

# Get the time the evaluation ended
[array]$EvaluationEnded = Invoke-Command -Session $s -ScriptBlock {
$EvaluationEnded = @()
$string = Select-String -Path "$env:SMS_LOG_PATH\colleval.log" -Pattern 'Express Evaluator] Exiting'
$string | ForEach-Object -Process {
    $obj = '' | Select-Object -Property String, Date
    $obj.String = $_.Line.Split('$')[0]
    [datetime]$obj.Date = $_.Line.Split('<')[2] -replace ".{4}$"
    $EvaluationEnded += $obj
    }
$EvaluationEnded = $EvaluationEnded | Where-Object -FilterScript {
$_.date -ge $using:EarliestTime
}
$EvaluationEnded
} | Select-Object -Property String, Date

# Close the remote session
Remove-PSSession $s


###############################
# Work the results for output #
###############################

# Collection count
if ($CollectionsFound -ne $null)
    {
        $CollectionCount = $CollectionsFound | Where-Object -FilterScript {
$_.String.Split(' ')[1] -gt 0
}
        $CollectionCount = $CollectionCount.String.Split(' ')[1]
    }
Else 
{
$CollectionCount = 'Not found'
}

# Evaluation Start Time
if ($EvaluationStarted -ne $null)
    {
[datetime]$EvaluationStartTime = $EvaluationStarted[0].Date
}

# Evaluation End Time
if ($EvaluationEnded -ne $null)
    {
[datetime]$EvaluationEndTime = $EvaluationEnded[0].Date
}

# Evaluation time taken
if ($EvaluationStartTime -and $EvaluationEndTime)
    {
        $TimeTaken = '' | Select-Object -Property Minutes, Seconds
        $TimeTaken.Minutes = ($EvaluationEndTime - $EvaluationStartTime).Minutes
        $TimeTaken.Seconds = ($EvaluationEndTime - $EvaluationStartTime).Seconds
    }
Else {$TimeTaken = 'N/A'}

# Collections Evaluated
$CollectionsEvaluatedCount = $CollectionEvaluated.Count
$measure = ($CollectionEvaluated.SecondsToEvaluate | Measure-Object -Sum -Average -Maximum -Minimum)
$CollectionsEvaluatedAverage = [math]::Round($measure.Average,2)
$CollectionsEvaluatedSum = [math]::Round(($measure.Sum / 60),2) # minutes
$CollectionsEvaluatedMaximum = [math]::Round($measure.Maximum,2)
$CollectionsEvaluatedMinimum = [math]::Round($measure.Minimum,2)
$NS = (Get-CimInstance -ComputerName $SiteServer -Namespace ROOT\SMS -ClassName __Namespace).Name
$Collections = Get-CimInstance -ComputerName $SiteServer -Namespace ROOT\SMS\$NS -Query "Select Name,CollectionID from SMS_Collection" | Select Name,CollectionID
$CollectionsEvaluatedWN = @()
$CollectionEvaluated | foreach {
    $a = $_
    $obj = '' | Select CollectionName, CollectionID, SecondsToEvaluate, Date
    $obj.CollectionName = $Collections | where {$_.CollectionID -eq $a.CollectionID} | Select -ExpandProperty Name
    $obj.CollectionID = $_.CollectionID
    $obj.SecondsToEvaluate = $_.SecondsToEvaluate
    $obj.Date = $_.Date
    $CollectionsEvaluatedWN += $obj
    }   


# Add the results to custom object
$Result = '' | Select-Object -Property 'Evaluation StartTime', 'Evaluation EndTime', 'Time Taken', `
    'Incremental Collections in graph', `
    'Collections Evaluated', 'Collection Eval TimeTaken (minutes)', 'Collection Eval Average Time (Seconds)', `
    'Longest Collection Eval (seconds)', 'Quickest Collection Eval (seconds)', 'Collections Evaluated List'
if ($EvaluationStarted)
    {
$Result.'Evaluation StartTime' = $EvaluationStartTime
}
Else 
{
$Result.'Evaluation StartTime' = 'Not found'
}
if ($EvaluationEnded)
    {
$Result.'Evaluation EndTime' = $EvaluationEndTime
}
Else 
{
$Result.'Evaluation EndTime' = 'Not found'
}
if ($EvaluationStarted -and $EvaluationEnded)
    {
$Result.'Time Taken' = "$($TimeTaken.Minutes) minutes $($TimeTaken.Seconds) seconds"
}
Else 
{
$Result.'Time Taken' = 'N/A'
}
$Result.'Incremental Collections in graph' = $CollectionCount
$Result.'Collections Evaluated' = $CollectionsEvaluatedCount
$Result.'Collection Eval TimeTaken (minutes)' = $CollectionsEvaluatedSum
$Result.'Collection Eval Average Time (Seconds)' = $CollectionsEvaluatedAverage
$Result.'Longest Collection Eval (seconds)' = $CollectionsEvaluatedMaximum
$Result.'Quickest Collection Eval (seconds)' = $CollectionsEvaluatedMinimum
$Result.'Collections Evaluated List' = $CollectionsEvaluatedWN

# Return the results
return $Result