Summary: Guest blogger, Brian Wilhite, shares his experience using Windows PowerShell jobs to speed up collecting information from WMI.
Microsoft Scripting Guy, Ed Wilson, is here. This weekend, we will start with guest blogger, Brian Wilhite.
Brian Wilhite works as a Windows system administrator for a large health-care provider in North Carolina. He has over 15 years of experience in IT. In his current capacity as a Windows system administrator, he leads a team of individuals who have responsibilities for Microsoft Exchange Server, Windows Server builds, and management and system performance. Brian also supports and participates in the Charlotte PowerShell Users Group.
Twitter: Brian Wilhite
Take it away Brian…
Here in North Carolina, it’s getting warmer, the rain is falling, flowers are blooming, and nature is giving us a blast of pollen to last a lifetime—yes, spring is in full swing. Like most things that are common with this time of year, so is my organization’s Microsoft Enterprise Agreement “True-Up.” If you are like us, we gather information from various data sources, such as SCCM, Active Directory and/or a CMDB. I was on a mission, given to me by my manager, to track down all Windows Server installations in my organization’s domain. I began by querying Active Directory for computer objects that have a server operating system by using the Active Directory module in Windows PowerShell. To import the Active Directory module, I will have to, you guessed it, use Import-Module. It looks like this:
Note Prior to running this cmdlet, to ensure that you have the Active Directory module available for import, you can run the following Windows PowerShell code:
Get-Module - ListAvailable
If you do not have the Active Directory module available, you need to download and install the Remote Server Administration Tools from the Microsoft Download Center. In addition, for them to work on any domain earlier than Windows Server 2008, you need to install Active Directory Management Gateway Service.
Next, I use Get-ADComputer to query Active Directory for all computer objects that have “Server” in the OperatingSystem property, and I stored it as $AllServers. I also wanted to export this to a CSV file for review. Notice that I use the NoTypeInformation parameter. In my opinion, this should be the default; however, this parameter will not display the object data at the top of the CSV.
$AllServers = Get-ADComputer -Filter {OperatingSystem -like "*Server*"} -Properties OperatingSystem | Select Name, OperatingSystem
$AllServers | Export-Csv -Path C:\Users\Brian\Desktop\AllServers.csv -NoTypeInformation
When reviewing the data shown in the following image, I realized that Windows Server 2008 populates the Standard, Enterprise, and Datacenter versions correctly within Active Directory. However, Windows Server 2003 does not:
Because the “True-Up” requires us to count what specific versions we’re running, I need to go one step further to gather that information. When I thought about how I was going to gather this data, I pondered for a bit, and then I decided that I would use my good old friend WMI. Because I already had the counts for the servers running Windows Server 2008, and I had the data filtering in Excel, I would shift my focus and effort to the servers running Windows Server 2003. To isolate them from the $AllServers variable that I created earlier, I ran the following code, selecting only the Name property:
$2003ServersOnly = $AllServers | Where-Object {$_.OperatingSystem -eq “Windows Server 2003”} | Select -ExpandProperty Name
Now that I had all the names of servers running Windows Server 2003 in a variable to itself, I ran the following code “trying” to export the data to a CSV file:
Get-WmiObject -Class Win32_OperatingSystem -ComputerName $2003ServersOnly | Select CSName, Caption | Export-Csv -Path C:\Users\Brian\Desktop\2003Servers.csv -NoTypeInformation
This should work because the ComputerName parameter will accept an array of computer names. It is also quicker than piping to a Foreach-Object. However, about 20 minutes later, I noticed that the CSV file was not growing in size like it had earlier, so I terminated the one-liner. I gave it more thought, and I decided that I would try the Foreach-Object route to see where that would take me. That code follows:
$2003ServersOnly | ForEach-Object {Get-WmiObject -Class Win32_OperatingSystem -ComputerName $_} | Select CSName, Caption | Export-Csv -Path C:\Users\Brian\Desktop\2003Servers.csv -NoTypeInformation
That didn’t work either. About 20 minutes later, I noticed that it stalled once again. Back to the drawing board…
I gave it further thought, and I remembered a conversation that I had with a good friend of mine about the AsJob parameter for Get-WmiObject. It came about because of a function I wrote, Get-ComputerInfo, which serially queries a set of 8 or 9 WMI classes. He said that he made some modifications specifically around running Get-WmiObject -AsJob, so that the queries would run asynchronously, making the function execute faster. So I put that method into practice for the mission at hand. Instead of running the objects one at a time through the pipeline, why not create a job for each server and see if my one-liner continues to stall? Boy, was I in for a surprise on this one. I ran the following code:
$2003ServersOnly | ForEach-Object {Get-WmiObject -Class Win32_OperatingSystem -ComputerName $_ -AsJob}
With the snap of my fingers the job creation was off and running on over 1100 servers. It took maybe 30 seconds to create all the jobs. “Oh my…,” I thought in disbelief, “Did that just happen that fast? No way!” As it turns out, it most certainly did.
After the jobs finished scrolling by, I ran Get-Job to check the status. To my surprise, other than a few “Running” and “Failed” jobs, all were “Completed”:
Because all the jobs completed, I ran the following code to capture the data from the jobs and then exported the data to a CSV file:
$Jobs = Get-Job | Receive-Job | Select CSName, Caption
$Jobs | Export-Csv -Path C:\Users\Brian\Desktop\2003Servers.csv -NoTypeInformation
Later I went back to find out exactly how long it took to kick the jobs off, and it appears that it ran in 17.267 seconds:
Now keep in mind that after the jobs finished scrolling by within Windows PowerShell, I ran Get-Job and most had finished. So the total time taken to query that many servers was less than 25 to 30 seconds, that is AWESOME!
With this information, I was able to complete the “True-Up” mission that my manager had given me in a timely manner. I was so happy with the sheer “Power” of Windows PowerShell that I reached out to my friend and shared the story. And now I have the privilege of sharing it with the community. I enjoy working daily in Windows PowerShell, and I hope you do too. Thanks for taking the time to listen to me ramble on about the “Shell.”
~Brian
I don’t think you are rambling, Brian. In fact, I appreciate your enthusiasm. I can also tell you that the longer you work with Windows PowerShell, the greater that enthusiasm grows. I am constantly shouting out loud, “Dude, that is awesome!” So much so that the Scripting Wife simply ignores me—at least I think that is why she ignores me sometimes. Anyway, Brian, thank you so very much for taking the time to share your experience with the Windows PowerShell scripting community. Join me tomorrow for more Windows PowerShell goodness.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy