Speedy Network Information Query Using PowerShell

Taking a page from my Advanced Event 2 script that I used in my Expert Commentary on Hey, Scripting Guy!, I wanted to put something together to query for basic networking information from both local and remote systems and accomplish this is a decent amount of time. When I say a decent amount of time, I really mean as fast as possible.

So with that, I need to figure out the means to accomplish this simple, yet sometimes long task. To get the network information from the systems, I am choosing to do this the only useful way I know how: WMI. WMI has all sorts of information in it that you can use for reporting and anything else you can think of.

The speedy portion that I mention in my title and hint at by reference my past script is by setting up background runspaces (not background jobs) to handle the multiple asynchronous runspaces and then use a helper function to handle those runspaces and grab the data I need to display on the screen.

    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline = $True,ValueFromPipeLineByPropertyName = $True)]
        [Alias('CN','__Server','IPAddress','Server')]
        [string[]]$Computername = $Env:Computername,
        
        [parameter()]
        [Alias('RunAs')]
        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,       
        
        [parameter()]
        [int]$Throttle = 15
    )

The script accepts pipeline input both by value and by property name. You can set the number of runspaces being used at a time by defining the number with the –Throttle parameter. It is important to keep in mind that you shouldn’t go crazy with this as your system could be impacted due to lack of resources. The default value is 15 for –throttle. Another thing to note is the [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty which allows you to supply either a pscredential object or you can specify the domain\username and it will open up the credential window to type in a password. This method works a lot better than just specifying a type of System.Management.Automation.PSCredential for your parameter as it is more flexible. If you don’t specify the [System.Management.Automation.PSCredential]::Empty piece, you will ALWAYS get the popup to enter credentials, essentially making it a mandatory parameter.

    Begin {
        #Function that will be used to process runspace jobs
        Function Get-RunspaceData {
            [cmdletbinding()]
            param(
                [switch]$Wait
            )
            Do {
                $more = $false         
                Foreach($runspace in $runspaces) {
                    If ($runspace.Runspace.isCompleted) {
                        $runspace.powershell.EndInvoke($runspace.Runspace)
                        $runspace.powershell.dispose()
                        $runspace.Runspace = $null
                        $runspace.powershell = $null
                        $Script:i++                  
                    } ElseIf ($runspace.Runspace -ne $null) {
                        $more = $true
                    }
                }
                If ($more -AND $PSBoundParameters['Wait']) {
                    Start-Sleep -Milliseconds 100
                }   
                #Clean out unused runspace jobs
                $temphash = $runspaces.clone()
                $temphash | Where {
                    $_.runspace -eq $Null
                } | ForEach {
                    Write-Verbose ("Removing {0}" -f $_.computer)
                    $Runspaces.remove($_)
                }             
            } while ($more -AND $PSBoundParameters['Wait'])
        }
            
        Write-Verbose ("Performing inital Administrator check")
        $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
        $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")                   
        
        #Main collection to hold all data returned from runspace jobs
        $Script:report = @()    
        
        Write-Verbose ("Building hash table for WMI parameters")
        $WMIhash = @{
            Class = "Win32_NetworkAdapterConfiguration"
            Filter = "IPEnabled='$True'"
            ErrorAction = "Stop"
        } 
        
        #Supplied Alternate Credentials?
        If ($PSBoundParameters['Credential']) {
            $wmihash.credential = $Credential
        }
        
        #Define hash table for Get-RunspaceData function
        $runspacehash = @{}

        #Define Scriptblock for runspaces
        $scriptblock = {
            Param (
                $Computer,
                $wmihash
            )           
            Write-Verbose ("{0}: Checking network connection" -f $Computer)
            If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
                #Check if running against local system and perform necessary actions
                Write-Verbose ("Checking for local system")
                If ($Computer -eq $Env:Computername) {
                    $wmihash.remove('Credential')
                } Else {
                    $wmihash.Computername = $Computer
                }
                Try {
                        Get-WmiObject @WMIhash | ForEach {
                            $IpHash =  @{
                                Computername = $_.DNSHostName
                                DNSDomain = $_.DNSDomain
                                IPAddress = $_.IpAddress
                                SubnetMask = $_.IPSubnet
                                DefaultGateway = $_.DefaultIPGateway
                                DNSServer = $_.DNSServerSearchOrder
                                DHCPEnabled = $_.DHCPEnabled
                                MACAddress  = $_.MACAddress
                                WINSPrimary = $_.WINSPrimaryServer
                                WINSSecondary = $_.WINSSecondaryServer
                                NICName = $_.ServiceName
                                NICDescription = $_.Description
                            }
                            $IpStack = New-Object PSObject -Property $IpHash
                            #Add a unique object typename
                            $IpStack.PSTypeNames.Insert(0,"IPStack.Information")
                            $IpStack 
                        }
                    } Catch {
                        Write-Warning ("{0}: {1}" -f $Computer,$_.Exception.Message)
                        Break
                }
            } Else {
                Write-Warning ("{0}: Unavailable!" -f $Computer)
                Break
            }        
        }
        
        Write-Verbose ("Creating runspace pool and session states")
        $sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host)
        $runspacepool.Open()  
        
        Write-Verbose ("Creating empty collection to hold runspace jobs")
        $Script:runspaces = New-Object System.Collections.ArrayList        
    }

A lot of code here in the Begin block holds most of the code that sets up the actual query portion of the function as well as holding a helper function to handle the background runspaces when they have completed. I also use some hashtables for splatting the WMI query.  The $Scriptblock holds a scriptblock that will be applied to the background runspace in the Process block of the function.

    Process {        
        $totalcount = $computername.count
        Write-Verbose ("Validating that current user is Administrator or supplied alternate credentials")        
        If (-Not ($Computername.count -eq 1 -AND $Computername[0] -eq $Env:Computername)) {
            #Now check that user is either an Administrator or supplied Alternate Credentials
            If (-Not ($IsAdmin -OR $PSBoundParameters['Credential'])) {
                Write-Warning ("You must be an Administrator to perform this action against remote systems!")
                Break
            }
        }
        ForEach ($Computer in $Computername) {
           #Create the powershell instance and supply the scriptblock with the other parameters 
           $powershell = [powershell]::Create().AddScript($ScriptBlock).AddArgument($computer).AddArgument($wmihash)
           
           #Add the runspace into the powershell instance
           $powershell.RunspacePool = $runspacepool
           
           #Create a temporary collection for each runspace
           $temp = "" | Select-Object PowerShell,Runspace,Computer
           $Temp.Computer = $Computer
           $temp.PowerShell = $powershell
           
           #Save the handle output when calling BeginInvoke() that will be used later to end the runspace
           $temp.Runspace = $powershell.BeginInvoke()
           Write-Verbose ("Adding {0} collection" -f $temp.Computer)
           $runspaces.Add($temp) | Out-Null
           
           Write-Verbose ("Checking status of runspace jobs")
           Get-RunspaceData @runspacehash
        }                        
    }

Not as much code here in the Process block compared to the Begin block. This is where the background runspaces will be kicked off to perform the actual network query. It also will do a administrator check as well to make sure that you have enough rights to perform a remote wmi query. After each runspace is kicked off, a quick check using the helper function GetRunspaceData is called to see if it is already finished or to check other runspaces and gather any data from those runspaces.

    End {                     
        Write-Verbose ("Finish processing the remaining runspace jobs: {0}" -f (@(($runspaces | Where {$_.Runspace -ne $Null}).Count)))
        $runspacehash.Wait = $true
        Get-RunspaceData @runspacehash
        
        Write-Verbose ("Closing the runspace pool")
        $runspacepool.close()               
    }

The End block wraps up the function by waiting for the remaining runspaces to finish before finally closing p the runspace pool at the end.

Examples

A couple of examples of using this against a local system, remote system and many remote systems.

Get-NetworkInfo

image

Just running the command by itself will return the local system network information. This is a reminder to everyone that you should always use a default value for a parameter such a –Computername.

'DC1','Boe-PC' | Get-NetworkInfo

image

measure-command {$servers | Get-NetworkInfo}

image

Running against 200 systems took approximately 19.5 to completed.

 

In conclusion, this script provides a nice way to quickly query your domain for the network information on all of your systems in a relatively short amount of time depending on how large your system is and the resources available on your system that you are running the function from.

You can download the script below. Give it a run and let me know what you think!

Download

Script Repository

This entry was posted in powershell, scripts and tagged , , , . Bookmark the permalink.

19 Responses to Speedy Network Information Query Using PowerShell

  1. Pingback: How to limit a number of PowerShell Jobs running simultaneously | Microsoft Technologies and Stuff

  2. Hey Boe, great idea, i was actually trying to take this runspace approach and use it for the Win32_QuickFixEngineering wmi class, i modifed your script a bit just enough for me to be able to do measuring against my starting technique of synchronously, and not that i had any doubts it is a great gap, for only 6 servers to reduce run time from 32 seconds to 9 seconds, amazing, anyway here is the modified bit – i basically just made it possible to name the wmi class i want to question and add a hash table to extract the data, hope it’s okay (i still haven’t changed the description and instruction but you run it with
    get-wmiinfo -computername X -wmiclass win32_Y -filter “name=’z'”
    or
    $computers | get-wmiinfo -wmiclass win32_Y -filter “name=’z'”

    #set up the series of computers to check on
    $name_format = “srv2k8r2”
    $domain_format = “contoso.com”
    $computers = 1..50 | ForEach-Object { if ($_ -ge 10) { $name_format + ‘{0}.’ -f $_ + $domain_format} else { $name_format + ‘{0}.’ -f $_ + $domain_format}}

    #begin boe’s modified function
    Function Get-WmiInfo {
    <#
    .SYNOPSIS
    Retrieves the network configuration from a local or remote client.

        .DESCRIPTION  
            Retrieves the network configuration from a local or remote client.        
    
        .PARAMETER Computername
            A single or collection of systems to perform the query against
    
        .PARAMETER Credential
            Alternate credentials to use for query of network information        
    
        .PARAMETER Throttle
            Number of asynchonous jobs that will run at a time
    
        .NOTES  
            Name: Get-NetworkInfo.ps1
            Author: Boe Prox
            Version: 1.0
    
        .EXAMPLE
             Get-NetworkInfo -Computername 'System1'
    
            NICDescription : Ethernet Network Adapter
            MACAddress     : 00:11:22:33:aa:bb
            NICName        : enthad
            Computername   : System1.domain.com
            DHCPEnabled    : True
            WINSPrimary    : 192.0.0.25
            SubnetMask     : {255.255.255.255}
            WINSSecondary  : 192.0.0.26
            DNSServer      : {192.0.0.31, 192.0.0.30}
            IPAddress      : {192.0.0.5}
            DefaultGateway : {192.0.0.1}        
    
            Description
            -----------
            Retrieves the network information from 'System1'      
    
        .EXAMPLE
            $Servers = Get-Content Servers.txt
            $Servers | Get-NetworkInfo -Throttle 10
    
            Description
            -----------
            Retrieves all of network information from the remote servers while running 10 runspace jobs at a time.  
    
        .EXAMPLE
            (Get-Content Servers.txt) | Get-NetworkInfo -Credential domain\adminuser -Throttle 10
    
            Description
            -----------
            Gathers all of the network information from the systems in the text file. Also uses alternate administrator credentials provided.                                            
    #>
    #Requires -Version 2.0
    
    
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline = $True,ValueFromPipeLineByPropertyName = $True)]
        [Alias('CN','__Server','IPAddress','Server')]
        [string[]]$Computername = $Env:Computername,
    
        [parameter(ValueFromPipeline = $True,ValueFromPipeLineByPropertyName = $True)]
        [Alias('Class')]
        [string]$WMIClass,        
    
        [parameter(ValueFromPipeline = $True,ValueFromPipeLineByPropertyName = $True)]
        [Alias('Filter')]
        [string]$WMIFilter,  
    
        [parameter()]
        [Alias('RunAs')]
        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,      
    
        [parameter()]
        [int]$Throttle = 15
    )
    Begin {
        #Function that will be used to process runspace jobs
        Function Get-RunspaceData {
            [cmdletbinding()]
            param(
                [switch]$Wait
            )
            Do {
                $more = $false        
                Foreach($runspace in $runspaces) {
                    If ($runspace.Runspace.isCompleted) {
                        $runspace.powershell.EndInvoke($runspace.Runspace)
                        $runspace.powershell.dispose()
                        $runspace.Runspace = $null
                        $runspace.powershell = $null
                        $Script:i++                  
                    } ElseIf ($runspace.Runspace -ne $null) {
                        $more = $true
                    }
                }
                If ($more -AND $PSBoundParameters['Wait']) {
                    Start-Sleep -Milliseconds 100
                }  
                #Clean out unused runspace jobs
                $temphash = $runspaces.clone()
                $temphash | Where {
                    $_.runspace -eq $Null
                } | ForEach {
                    Write-Verbose ("Removing {0}" -f $_.computer)
                    $Runspaces.remove($_)
                }            
            } while ($more -AND $PSBoundParameters['Wait'])
        }
    
        Write-Verbose ("Performing inital Administrator check")
        $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
        $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")                  
    
        #Main collection to hold all data returned from runspace jobs
        $Script:report = @()    
    
        Write-Verbose ("Building hash table for WMI parameters")
        $WMIhash = @{
            Class = $WMIClass
            Filter = $WMIFilter
            ErrorAction = "Stop"
        }
        Write-Verbose ("Hash Table used: $WMIClass")
        #Supplied Alternate Credentials?
        If ($PSBoundParameters['Credential']) {
            $wmihash.credential = $Credential
        }
    
        #Define hash table for Get-RunspaceData function
        $runspacehash = @{}
    
        #Define Scriptblock for runspaces
        $scriptblock = {
            Param (
                $Computer,
                $wmihash
            )          
            Write-Verbose ("{0}: Checking network connection" -f $Computer)
            If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
                #Check if running against local system and perform necessary actions
                Write-Verbose ("Checking for local system")
                If ($Computer -eq $Env:Computername) {
                    $wmihash.remove('Credential')
                } Else {
                    $wmihash.Computername = $Computer
                }
                # Hash Tables for Specific Classes
                # Start of Hash Table
                if ($wmihash.class -eq "Win32_NetworkAdapterConfiguration")
                {
                Try {
                        Get-WmiObject @WMIhash | ForEach {
                            $PropertiesHash =  @{
                                Computername = $_.DNSHostName
                                DNSDomain = $_.DNSDomain
                                IPAddress = $_.IpAddress
                                SubnetMask = $_.IPSubnet
                                DefaultGateway = $_.DefaultIPGateway
                                DNSServer = $_.DNSServerSearchOrder
                                DHCPEnabled = $_.DHCPEnabled
                                MACAddress  = $_.MACAddress
                                WINSPrimary = $_.WINSPrimaryServer
                                WINSSecondary = $_.WINSSecondaryServer
                                NICName = $_.ServiceName
                                NICDescription = $_.Description
                            }
                            $PropertiesStack = New-Object PSObject -Property $PropertiesHash
                            #Add a unique object typename
                            $PropertiesStack.PSTypeNames.Insert(0,"PropertiesStack.Information")
                            $PropertiesStack
                        }
                    } Catch {
                        Write-Warning ("{0}: {1}" -f $Computer,$_.Exception.Message)
                        Break
                }                    
                }
                # End of Hash Table
                # Start of Hash Table
                if ($wmihash.class -eq "Win32_QuickFixEngineering")
                {
                Try {
                        Get-WmiObject @WMIhash | ForEach {
                            $PropertiesHash =  @{
                                Computername = $_.PSComputerName
                                Type = $_.Description
                                DisplayName = $_.HotFixID
                                InstallDate = $_.InstallDate
                                InstallLocation = $_.Caption
                                'Publisher/User' = $_.InstalledBy
    
                            }
                            $PropertiesStack = New-Object PSObject -Property $PropertiesHash
                            #Add a unique object typename
                            $PropertiesStack.PSTypeNames.Insert(0,"PropertiesStack.Information")
                            $PropertiesStack
                        }
                    } Catch {
                        Write-Warning ("{0}: {1}" -f $Computer,$_.Exception.Message)
                        Break
                }                    
                }
                # End of Hash Table            
            } Else {
                Write-Warning ("{0}: Unavailable!" -f $Computer)
                Break
            }        
        }
    
        Write-Verbose ("Creating runspace pool and session states")
        $sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host)
        $runspacepool.Open()  
    
        Write-Verbose ("Creating empty collection to hold runspace jobs")
        $Script:runspaces = New-Object System.Collections.ArrayList        
    }
    Process {        
        $totalcount = $computername.count
        Write-Verbose ("Validating that current user is Administrator or supplied alternate credentials")        
        If (-Not ($Computername.count -eq 1 -AND $Computername[0] -eq $Env:Computername)) {
            #Now check that user is either an Administrator or supplied Alternate Credentials
            If (-Not ($IsAdmin -OR $PSBoundParameters['Credential'])) {
                Write-Warning ("You must be an Administrator to perform this action against remote systems!")
                Break
            }
        }
        ForEach ($Computer in $Computername) {
           #Create the powershell instance and supply the scriptblock with the other parameters
           $powershell = [powershell]::Create().AddScript($ScriptBlock).AddArgument($computer).AddArgument($wmihash)
    
           #Add the runspace into the powershell instance
           $powershell.RunspacePool = $runspacepool
    
           #Create a temporary collection for each runspace
           $temp = "" | Select-Object PowerShell,Runspace,Computer
           $Temp.Computer = $Computer
           $temp.PowerShell = $powershell
    
           #Save the handle output when calling BeginInvoke() that will be used later to end the runspace
           $temp.Runspace = $powershell.BeginInvoke()
           Write-Verbose ("Adding {0} collection" -f $temp.Computer)
           $runspaces.Add($temp) | Out-Null
    
           Write-Verbose ("Checking status of runspace jobs")
           Get-RunspaceData @runspacehash
        }                        
    }
    End {                    
        Write-Verbose ("Finish processing the remaining runspace jobs: {0}" -f (@(($runspaces | Where {$_.Runspace -ne $Null}).Count)))
        $runspacehash.Wait = $true
        Get-RunspaceData @runspacehash
    
        Write-Verbose ("Closing the runspace pool")
        $runspacepool.close()              
    }
    

    }

    method 1 measure using get-wmiobject synchronously

    Measure-Command {foreach ($computer in $computers) {get-wmiobject -class win32_quickfixengineering -ComputerName $computer | Select-Object -Property HotFixID,Caption,InstallDate,InstalledBy,Description,PSComputerName}}

    method 2 measure using runspaces
    Measure-Command {$computers | Get-WmiInfo -WMIClass Win32_QuickFixEngineering}

    #endcode

  3. Pingback: Powershell – выполняем скрипт еще быстрее | ILYA Sazonov: ITPro

  4. noel blanc says:

    Hello Sir,
    Is it too late to ask you ?
    Your script is very helpfully.
    I try to change the scriptblock. I need to connect to many ( more 1,000) remote computers (in workgroup) with WMI using different credentials and create a process. It woks fine if Thottlelimite is less than 25. If a replace wmi requests with simple commands “net use ip user pwd”, it work fine with Thottlelimite = 250. So i think about “fan out” of WMI… Have you test in this context ( wmi requests connectserver and createprocess on more 30 remote computers ) ?
    Respectfully.

  5. Pingback: Parallel PowerShell: Part II | rambling cookie monster

  6. techibee.com says:

    This helped me to get some level of multi threading using powershell. But the disappointing factor is, 1) I couldn’t find a way for inter thread communication 2) Threads are unable to recognize variables and functions from main thread — passing everything to thread is a headache.

  7. Pingback: Parallel PowerShell | rambling cookie monster

  8. This was incredibly helpful. Far more efficient than spinning up new processes and tying up my CPU with jobs. Thank you Boe!

    Will look into this on my own, but do you have any tips on implementing a timeout on runspaces to ensure something doesn’t get hung and potentially hold up the process?

    Regards,

    CM

  9. DJ says:

    How would you send the results to file instead of outputting to the screen? Thanks!

  10. Justus Thane says:

    Thanks! Great post. Do you have any suggested reading for further understanding runspaces? I would like to start using them in my own scripts, and while your post is enough to get me excited about runspaces, I still can’t quite wrap my head around it.

  11. mjolinor says:

    Very nice. Can you explain why you chose to manually created runspaces, rather than use background jobs?

    On a side note, in reference to the Scripting Games, do you know if any of the participant entries used this solution?

    • Boe Prox says:

      Thanks! I am actually working on a follow-up blog to post either tonight or tomorrow to talk a little more about why I chose runspaces vs PSJobs (less overhead,better performance,etc…) with some performance testing that I did to back it up.
      As far as I could tell when I was grading, there were no background jobs or runspaces that were used in AE2.

Leave a Reply to Boe Prox Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s