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
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
measure-command {$servers | Get-NetworkInfo}
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!
Pingback: How to limit a number of PowerShell Jobs running simultaneously | Microsoft Technologies and Stuff
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.
}
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
Pingback: Powershell – выполняем скрипт еще быстрее | ILYA Sazonov: ITPro
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.
Pingback: Parallel PowerShell: Part II | rambling cookie monster
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.
It is a little more painful to work with other runspaces, but it is possible to share variables between threads using a thread safe collection such as a synchronized hash table or array. Sharing functions between runspaces will probably require more work to do.
But for the shared variables, you can check out the following article I wrote that deals with that subject for working with GUIs, but still applies to sharing data between threads.
https://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/
Pingback: Parallel PowerShell | rambling cookie monster
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
Hi all,
I ended up running into a situation where certain queries would freeze up, artificially limiting the throttle and preventing anything from running after the parallel code.
http://ramblingcookiemonster.wordpress.com/2012/11/25/parallel-powershell-part-ii/
Boe – any tips on a better way to implement a timeout? I basically just added a startTime property to $temp, and use this to calculate the runtime for each item in $runspaces. I assume there is a bit of computational overhead with this and that a built in timeout (if such a thing exists) would be more efficient.
Hi! I think I might have an idea that will work to allow a timeout with the runspaces pools. It involves working with the object that is outputted after calling BeginInvoke(). I’m currently on vacation and am typing this via my iPhone but will be back tomorrow so I can do some testing. I was pinged by Doug Finke to look at your post here: http://powershell.org/discuss/viewtopic.php?f=6&t=801&sid=fae92b13f32fc4903b3d9d50c225c6bc and will leave some feedback there as well.
As a followup to this, I did post my response on the Powershell.org forums.
http://powershell.org/discuss/viewtopic.php?f=6&t=801#p3300
How would you send the results to file instead of outputting to the screen? Thanks!
You should be able to just pipe it to something like Get-NetworkInfo Server1 | Export-CSV report.csv -notype
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.
Thanks, Justus!
There are not a lot of blogs out there that provide a lot of detail for runspaces, but here is one from a PowerShell MVP, Oisin Grehan that provides some more info:
http://www.nivot.org/nivot2/post/2009/01/22/CTP3TheRunspaceFactoryAndPowerShellAccelerators.aspx
Thanks for the tip; I had already found that one actually. I’m able to follow his script better than yours (no offense intended! Yours is just way beyond me), but unfortunately, it fails at line 40 regardless of whether I run it in the ISE or with PowerShell.exe.
I’m playing around with PowerShell Workflows now. So far, I am completely blown away by the speed.
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?
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.