Windows PowerShell Tip of the Week

Here’s a quick tip on working with Windows PowerShell. These are published every week for as long as we can come up with new tips. If you have a tip you’d like us to share or a question about how to do something, let us know.

Find more tips in the Windows PowerShell Tip of the Week archive.

Running Windows PowerShell Scripts Against Multiple Computers Part 2

So you say that last week only whet your appetite for more ways to run a single Windows PowerShell script against multiple computers? Well, you might as well say that, because it’s going to be more of the same this week; in this week’s tip, we’ll provide an in-depth look at running scripts against all the computers in the domain and against all the computers in an OU. And then, just to make sure you keep coming back for more, we’ll also provide a sneak peek at running a script against all the computers listed in a Microsoft Excel spreadsheet.

Running a Script Against All the Computers in a Domain

One question that the Scripting Guys get asked over and over again is this: how can I get a script to run against all the computers in a domain? Well, if you’re running Windows PowerShell (and we assume you must be if you’re reading this article) here’s one way you can do that:

$strFilter = "computer"

$objDomain = New-Object System.DirectoryServices.DirectoryEntry

$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.SearchScope = "Subtree"
$objSearcher.PageSize = 1000

$objSearcher.Filter = "(objectCategory=$strFilter)"

$colResults = $objSearcher.FindAll()

foreach ($i in $colResults)
    {
        $objComputer = $i.GetDirectoryEntry()
        Get-WMIObject Win32_BIOS -computername $objComputer.Name
    }

As you can see, what we’re doing here is using an Active Directory search script to return a collection of all the computers in the domain; in turn, we then call the Get-WMIObject cmdlet to retrieve BIOS information for each of those computers. Before trying this on your own, keep in mind that this script could take quite awhile to complete; that’s going to be especially true if: a) you have a lot of computer accounts in the domain; and/or b) at least some of these computers are offline. Ideally, you would ping each computer before trying to connect to it. But that’s a story for another day.

For now, we’ll simply note that we start things off by assigning the value computer to a variable named $strFilter. If you’re wondering where this value came from, this happens to be the official way to specify a computer’s objectCategory. (No big surprise there.) If you’re wondering why we even need this value, well, stay tuned; we’ll explain that in just a second.

After assigning a value to $strFilter, we then create a new instance of the .NET Framework class System.DirectoryServices.DirectoryEntry. (In case you’re wondering, yes, we do use the .NET Framework when we work with Active Directory.) Because we didn’t specify otherwise, our object reference $objDomain automatically connects us to the root of our Active Directory domain.

And yes, we thought that was kind of cool, too.

From there we create an instance of the System.DirectoryServices.DirectorySearcher class; that’s the object used to conduct an Active Directory search. Once we have that object in hand we then assign values to three of the DirectorySearcher’s properties:

  • SearchRoot. The SearchRoot property simply tells DirectorySearcher where to begin searching. We want to search the entire domain so we set SearchRoot to the object reference $objDomain.

  • SearchScope. This determines how “deep” we want our search to go. In this case we want to search not only the domain root, but all the OUs and containers (and sub-OUs and sub-containers) housed in the domain root as well. Thus we set the SearchScope value to Subtree.

  • PageSize. By default, an Active Directory search returns a maximum of 1,000 objects. That’s fine, unless you happen to have 1,001 computers in your domain. Because we want to get back all the computers in the domain we set the PageSize to 1,000. That means that the script will return the first 1,000 items it finds, go back and retrieve the next 1,000 items, and continue this process until every last computer account has been returned.

Next we assign a value to one more property, Filter:

$objSearcher.Filter = "(objectCategory=$strFilter)"

The Filter property is used to target a specific subset of the items found in Active Directory. Because the only thing we care about are computers, we tell the Filter property to bring back only those objects where the objectCategory attribute is equal to the variable $strFilter (which, in turn, is equal to computer).

And then, at long last, we call the FindAll method to actually kick off our search, with the complete collection of computer accounts being returned and stored in a variable named $colResults:

$colResults = $objSearcher.FindAll()

Got all that? Good. We should note that, by default, this search returns all the values of all the properties for our computer accounts. If you have a lot of computers, that’s likely to be way more data than you need, something which, in turn, could clog the network and slow down the script. Because of that you might want to limit the property values returned to the only one we’re interested in: Name. But that’s something else we’ll have to cover in a future tip.

Once we have our collection we set up a foreach loop to loop through all the computers in that collection. Inside that loop, we use this line of code and the GetDirectoryEntry method to create an object reference to the Active Directory account for the first computer in the collection:

$objComputer = $i.GetDirectoryEntry()

And then we simply pass the value of the Name property to the Get-WMIObject cmdlet and the –computername parameter:

Get-WMIObject Win32_BIOS -computername $objComputer.Name

From there we simply loop around and repeat the process with the next computer in the collection. And that, believe it or not, is all we have to do to get a script to run against all the computers in a domain.

Note. Actually, it was way easier than we thought it would be, too.

Running a Script Against All the Computers in an OU

You’re right: that was pretty cool. However, it might also be a bit more than you wanted; oftentimes you don’t want to run a script against all your computers, you only want to run the script against the computers found in a particular OU. So how are you supposed to do that?

That’s actually a good question, and one that’s a little tough to answer. As it turns out, this happens to be an area where VBScript might outshine PowerShell; with VBScript you can return a collection of all the computers in an OU using code similar to this:

Set colItems = GetObject _
    ("LDAP://ou=Servers, dc=fabrikam, dc=com")
colItems.Filter = Array("Computer")

With PowerShell it’s not that easy. When you bind to an OU using Windows PowerShell, you get back basic information about that OU itself; what you don’t get back is information about the objects stored in that OU. To get at that information you need to use the PSBase object, and then engage in a little programmatic sleight-of-hand in order to limit that information to computer objects. Can that be done? Sure; in fact, here’s one way to do that:

$objOU = [ADSI]"LDAP://OU=Workstations,DC=fabrikam,DC=com"
$colItems = $objOU.psbase.children

$colItems | ForEach-Object
{
    if ($_.objectCategory -eq "CN=Computer,CN=Schema,CN=Configuration,DC=fabrikam,DC=com")
        {Get-WMIObject Win32_BIOS -computername $_.Name}
}

So what’s going on here? Well, in the first line we’re using standard PowerShell syntax to connect to the Workstations OU in Active Directory. Note the “type adapter” [ADSI]; that’s simply a shorthand method built into PowerShell that makes it easy to connect to the .NET Framework and the DirectoryServices classes.

In the second line, we reference the PSBase object in order to get at the “children” of our target OU; as you might expect the Children property returns a collection of all the objects stored in the OU.

Note. What’s the PSBase class for? Well, without going into any of the technical details (mainly because we don’t really know all the technical details) the PSBase class allows us to access the underlying .NET Framework class. If we pipe the variable $objOU to the Get-Member cmdlet we won’t get back much; that’s because $objOU represents an “adapted view” of the actual OU. PowerShell often creates these adapted views as a way to present the most relevant information to the user. If you’d like to get at all the information that the object contains you’ll need to use the PSBase object.

Just like we did in this script.

Once we have a collection of all the objects found in our target OU (a collection named $colItems), we then pipe that information to the ForEach-Object cmdlet. As the name implies, ForEach-Object provides a way for us to loop through all the items in the collection, one-by-one. For each such item we then determine whether or not the objectCategory property is equal to CN=Computer,CN=Schema,CN=Configuration,DC=fabrikam,DC=com. (And yes, as near as we can tell the objectCategory must be expressed in this fashion.) If it is, we call the Get-WMIObject cmdlet, passing the value of the object’s Name property to the –computername parameter. If the objectCategory isn’t equal to CN=Computer, etc., etc. then we simply return to the top of the loop and repeat the process with the next item in the collection.

Granted, it’s a little clunky, but it seems to work. If you know of a more elegant way to retrieve a collection of computer accounts (and only computer accounts) from a specified OU, well, drop us a line at scripter@microsoft.com (in English, if possible). We’d love to hear about this.

Speaking of alternate approaches, another way to tackle this problem is to make two minor modifications to the search script we showed you (the one that works against all the computers in Active Directory). Make those modifications, and you’ll end up with a script similar to this:

$strFilter = "computer"

$objDomain = New-Object System.DirectoryServices.DirectoryEntry

$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = "LDAP://OU= Workstations,DC=fabrikam,DC=com"
$objSearcher.SearchScope = "Base"
$objSearcher.PageSize = 1000

$objSearcher.Filter = "(objectCategory=$strFilter)"
$colResults = $objSearcher.FindAll()

foreach ($i in $colResults)
    {
        $objComputer = $i.GetDirectoryEntry()
        Get-WMIObject Win32_BIOS -computername $objComputer.Name
    }

So what modifications did we make here? Like we said, we did two things. First, we set the SearchScope to Base. That tells the script to search only the target OU and to ignore any child OUs. (What if you want to search the child OUs? Then just leave the SearchScope set to Subtree.) Second, we set the value of the SearchRoot property to the ADsPath of the target OU:

$objSearch.SearchRoot = "LDAP://OU= Workstations,DC=fabrikam,DC=com"

Why? Ah, you’re way ahead of us: yes, we do that to tell the script that we want the search to begin in the Workstations OU.

This approach requires a little more code, but the script will also complete a bit faster, especially if you have a whole bunch of computers in your target OU.

Note. A bit faster? A whole bunch of computers? What does all that mean? Well, that’s difficult to say, particularly when you factor in network speed, network bandwidth, and all that other good stuff. However, an Active Directory search will complete in a matter of seconds, and you’ll then need to loop through only a subset of the objects found in the target OU. The enumeration script, by contrast, requires you to methodically wade through all the objects in the OU, separating the computer objects from everything else.

Running a Script Against All the Computers in an Excel Spreadsheet

Last week we promised we’d show you how to run a script against all the computers listed in an Excel spreadsheet; of course, we made that promise long before we found out how much time it would take to explain Active Directory searching. Therefore, we decided to compromise a little; we’ll show you a script that reads computer names from a spreadsheet (C:\Scripts\Test.xls), and then retrieves BIOS information for each of those computers. What we won’t do, however, is explain exactly how this script works. That’s one final thing we’ll have to do in the future.

In the meantime, here’s the script:

$a = New-Object -comobject Excel.Application

$a.Visible = $True

$b = $a.Workbooks.Open("C:\Scripts\Test.xls")
$c = $b.Worksheets.Item(1)

$i = 1

do
    {
        $d = $c.Cells.Item($i,1).Value()
        if ($d -ne $null)
            {Get-WMIObject Win32_BIOS -computername $d}
        $i++
    }
while ($d -ne $null)

Just make sure your computer names are listed in column A, with no blank rows between the names. This should tide you over until we get a chance to explain how to work with Excel while using Windows PowerShell.

This also means that it’s time to say goodbye. See you next week.