Export (0) Print
Expand All
Windows PowerShell Best Inventory Tool Ever!
Don Jones


In the past few installments of this column, I've demonstrated various techniques for using Windows PowerShell to collect computer inventory information. In this installment, I will assemble everything into a ready-to-run tool that can gather any kind of information from Windows Management Instrumentation (WMI).
Video
Don Jones wraps up his series on building a very powerful, full-featured inventory tool. In this streaming video, he demonstrates the final touches you'll need to add.

And the tool will be designed to work with either a text list of computer names or computer names queried from Active Directory.

Exploring the Tool
I will begin with the script itself (see Figure 1), stepping through the elements of its structure and looking at some of the tricks it uses. Some of these techniques may seem a bit odd or out of place, but I'm lumping them in here so I can demonstrate a variety of cool tricks being used.

1. function Get-WmiInventory {
2.  param (
3.  $wmiclass = "Win32_OperatingSystem"
4.  )
5.  PROCESS {
6.   $ErrorActionPreference = "SilentlyContinue"
7.   $computer = $_
8.   trap {
9.    $computer | out-file c:\errors.txt -append
10.    set-variable skip ($true) -scope 1
11.    continue
12.   }
13.   $skip = $false
14.   $wmi = Get-WmiObject -class $wmiclass -computer $computer -ea stop
15.   if (-not $skip) {
16.    foreach ($obj in $wmi) {
17.     $obj | Add-Member NoteProperty ComputerName $computer
18.     write $obj
19.    }
20.   }
21.  }
22. }  

On line 1, you can see that the function is named Get-WmiInventory. Right below that, I define an input parameter named $wmiclass and give it a default value of "Win32_OperatingSystem". The PROCESS scriptblock indicates that this is a filtering function, one designed to accept a collection of computer names from the pipeline—you'll see in a moment how I pass that information into the function.
Line 6 turns off the shell's normal error-reporting behavior since I want to provide my own error logging. And line 7 simply grabs the current computer name (as piped into the function) into the variable $computer.
Now go to line 13, where I create a variable named $skip and set it to the Boolean value False (more on that in a moment). On line 14, I then attempt to grab the desired WMI information from the current computer by using the Get-WmiObject cmdlet. Note that I've specified the –ErrorAction (or –EA) parameter, which tells the shell to generate an exception if the WMI retrieval fails for any reason.
Line 15 checks to see if the $skip variable still contains False. If it does, then line 16 enumerates through whatever came back from WMI and adds (on line 17) a ComputerName property to each WMI object. That way, when the script is returning WMI objects from multiple computers, each one is labeled with its parent computer name in a convenient property. Line 18 outputs each object to the pipeline so another cmdlet can deal with it or so the shell's formatting subsystem can take over to display some of the object's properties.
But what if something goes wrong? Since I specified –EA Stop, the shell will execute the trap on line 8. First, it writes the computer name to a text file so that I have a log of computer names that failed the inventory. Then, on line 10, I set the $skip variable to the Boolean value True. This prevents the script from attempting to output anything at line 15—if I hadn't done this, then each computer that fails would result in the previous computer's inventory information being output again.
Windows PowerShell Q&A
Q Can I use Windows PowerShell to manage Windows Server 2008 Server Core?
A Absolutely. Oh, you've probably heard that Windows PowerShell can't be installed on Server Core because Server Core doesn't currently support the .NET Framework, which the shell requires. But this isn't a problem. A lot of wise admins don't install anything on their servers if they can help it, and Windows PowerShell is no exception. Instead, you can install Windows PowerShell on your client computer and use it to remotely manage Server Core by using such technologies as Windows Management Instrumentation (WMI), Active Directory, and much more. You can, for instance, easily manage most aspects of Active Directory on a Server Core-based domain controller, all without leaving the comfort of your desk.
Line 10 uses a different technique to set the variable: The trap itself is a private variable scope and it doesn't contain a $skip variable. The $skip variable I want is from the trap's parent scope, one level up. The Set-Variable cmdlet allows me to modify the variable by using the -scope parameter, indicating that the $skip I'm interested in is one level up. Also note that when you use Set-Variable, the variable name doesn't include the $ symbol in front of it.

Test Drive
A simple way to test this is to just pipe in a single computer name, like this:
"localhost" | Get-WmiInventory 

Because I have not specified a WMI class name, the default of Win32_­OperatingSystem is used. To specify a different class, do this:
"localhost" | Get-WmiInventory "Win32_LogicalDisk"

Figure 2 shows the results. And if you want to include more computer names, you simply make a comma-separated list of them like so:
"localhost","server2","client17" | 
Get-WmiInventory "Win32_LogicalDisk"

Figure 2 Get-WmiInventory results

Computers En Masse
Specifying individual computer names is pretty tedious if you have a long list. If you already have a list of computer names in a text file—one computer name per line—then you can pipe those names right to the inventory function by doing this:
Get-Content c:\names.txt | Get-WmiInventory "Win32_Service"

Another technique involves the use of the Get-QADComputer cmd­let, which is provided as part of the free Active Directory cmdlets available from quest.com/powershell. You need to install the Active Directory cmdlets, open Windows PowerShell, and run:
Add-PSSnapin Quest.ActiveRoles.ADManagement

Run Get-QADComputer to retrieve all of the computers from Active Directory. But be aware that in a large domain, this will take a while! You can run Help Get-QADComputer to see some of the options available for filtering the list of computers, such as only grabbing those computers that reside in a specific organizational unit (OU). You also need to make a minor change to the script, changing line 7 from
$computer = $_

$computer = $_.Name

This is necessary because the objects produced by Get-QADComputer store the computer name in a Name property rather than as a simple string object.

What Do You Do with It?
This script's default output will take whatever form the Windows PowerShell formatting subsystem will create by default. For example, the Win32_OperatingSystem class is a small subset (just six properties) of the information actually available from that class. You can pipe this script's output to any standard shell cmdlet to customize that output. So, to display build number and service pack information, along with the computer names, do this:
Get-Content c:\names.txt | Get-WmiInventory | 
Format-List BuildNumber,ServicePackMajor­Version,­ComputerName

If you want to get an inventory of installed services and produce an HTML table, do this:
Get-Content c:\names.txt | Get-WmiInventory "Win32_Service" | 
ConvertTo-HTML | Out-File c:\services_inventory.html

And if you want to get a complete inventory of logical disk information and write it to a comma separated values (CSV) file, do this:
Get-Content c:\names.txt | 
Get-WmiInventory "Win32_LogicalDisk" | Export-CSV c:\disks.csv

Note that because the Get-WmiInventory function produces objects, not simple text, the shell can do quite a bit to help you get the data into the form you need, whether it's for statistical trending, management reports, or just a quick look at critical information.

Going Further
One weakness in the current script is that it retrieves all of the WMI objects from the class you specify. With some classes, that may be too much data. I typically prefer to restrict Win32_LogicalDisk so that I only retrieve local disks, which have a DriveType property of 3. I can easily modify the script to include filter criteria like that—the revised script is shown in Figure 3. To call this new script, I use this:
Get-Content c:\names.txt | 
Get-WmiInventory "Win32_LogicalDisk" "DriveType='3'"


1. function Get-WmiInventory {
2.  param (
3.   $wmiclass = "Win32_OperatingSystem"
4.   $filter = ""
5.  )
6.  PROCESS {
7.   $ErrorActionPreference = "SilentlyContinue"
8.   $computer = $_
9.   trap {
10.    $computer | out-file c:\errors.txt -append
11.    set-variable skip ($true) -scope 1
12.    continue
13.  }
14.  $skip = $false
15.  $wmi = Get-WmiObject -class $wmiclass -computer $computer -ea stop –filter $filter
16.  if (-not $skip) {
17.    foreach ($obj in $wmi) {
18.     $obj | Add-Member NoteProperty ComputerName $computer
19.     write $obj
20.    }
21.   }
22.  }
23. }

The changes, at lines 4 and 15, simply collect a new parameter named $filter and pass it to the –filter parameter of Get-WmiObject. When calling the function, the $filter parameter is second, so it's passed as a second value after the WMI class name. There's plenty more that you can do to customize this function for your specific environment, but there are a few things you should avoid doing:
  • Don't have the function send its output to anything but the pipeline, which is done on line 19 of the revised script. In other words, don't have the function output information to a file. Instead, take the pipeline output of the function and send it to a cmdlet, such as Out-File, that moves the output to a file or other medium. This will ensure that the function retains its flexibility.
  • Don't filter out information within the function—such as removing WMI object properties or only outputting a subset of properties. This too would reduce the function's flexibility for future scenarios. Instead, if you don't need all of the information being output, pipe that output to a Format cmdlet or to Select-Object so that you can get just the information you need for the task at hand.
  • Don't add additional tasks to the function. Keep each function single-tasked, rather than building super-functions that do a lot of things. If you have a complex process, accomplish it by building each discrete task as a standalone function and then piping information from one to another. This keeps each function more flexible, easier to maintain, and much easier to debug.

Powerful Do-It-Yourself Tools
While a script like this is no replacement for a powerful configuration management system such as System Center Configuration Manager, it does provide an easy way to inventory select information from a set of remote computers. This script is a good example of one of the things Windows PowerShell is best at: providing a quick, ad hoc solution to problems that Microsoft didn't foresee when creating Windows or whatever other product you're using.
Because this particular script uses WMI, it works with computers all the way back to Windows NT 4.0—even though Windows PowerShell itself can't be installed on a computer that old! And, because many Microsoft products expose management information through WMI, you can collect information from various products. I like to use a WMI explorer tool (a free one is at scriptinganswers.com in the "Tools Zone") to browse the WMI information available on a computer and then use a script like the one I've shown here to collect that information from multiple computers.

Don Jones a partner at Concentrated Technology and co-author of Windows PowerShell: TFM (SAPIEN Press). Get weekly Windows PowerShell tips, and contact Don, at ConcentratedTech.com.

Was this page helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft