Tales from the Script - November 2002
By The Scripting Guys
Running WMI Scripts Against Multiple Computers
Before we delve into this month's topic, the Scripting Guys want to say "Thank you!" The scripter alias has been flooded with great feedback, and its really nice to hear from so many of you. And even though we can't individually answer each e-mail that comes our way (although we try to answer as many as possible), we still need your suggestions to keep us on track. So far, you haven't let us down. Thanks again.
In this column, we're going to look at how you can modify the typical WMI scripts found in the Script Center, modifications that will enable these scripts to run against multiple computers. We're always pointing out that one of the primary advantages of these WMI scripts — which are usually designed to run only against the local computer — is that they can be easily modified to run against a given set of computers.
That's great, but as we all know, easily is a relative term; after all, nothing in scripting is easy until someone teaches you how to do it. And because we haven't gotten around to showing you how to do this, it's not surprisingly that we've received a number of questions like the following:
"If I have a script to find out all the running services on a remote computer, can I use the same script to prompt me to enter the remote computer name at the command prompt?"
"Is there any way that these scripts can run against more than one computer, maybe by getting computer names out of a file or something?"
"Can I get this script to run against all my domain controllers?"
The answer to all these questions is: you bet. So, with that in mind, let's begin this month's column.
On This Page
A Brief Review
Entering Computer Names as Command-Line Parameters
Entering Multiple Computer Names as Command-Line Arguments
Retrieving Computer Names from a Text File
Retrieving Computer Names from an Active Directory Container
A Brief Review
The best way to learn how to script is to actually do it. So let's start with a script from the script center that uses WMI to display the status of all the services installed on the local computer. Before we start modifying the script to get it to run against more than just the local computer, let's take a look at how the script appears in the Script Center:
strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colRunningServices = objWMIService.ExecQuery _
("Select * from Win32_Service")
For Each objService in colRunningServices
Wscript.Echo objService.DisplayName & VbTab & objService.State
Next
If you’re not familiar with WMI scripts this code might look a bit cryptic. If thats the case, you have a few options open to you. If you want to, you can rush off to MSDN and read the WMI Primer series that has been featured in the Scripting Clinic column; after you've been properly immersed in the world of WMI scripting, you can pick up where you left off with this column (don't worry, we'll wait for you). Or, you can stick with us for the time being, and we'll give you a really quick overview of what's going on here. You can then delve into the WMI Primer later on to learn the details.
Either way, here's how the preceding script works. This part of the script connects to WMI. To be more exact, it connects to something called a namespace: the root\cimv2 namespace.
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Again, if you are in need of details, go check out MSDN. In a nutshell, this section of the script just gets you connected to the WMI service on the computer whose name is specified in the strComputer variable (we'll talk about that in a second).
Note: So what is a namespace? In WMI, you work with things called classes. Classes are virtual representations of real, live things; for example, there is a Win32_Service class that represents all the services on a computer. A namespace is simply the location where a set of classes are stored; in this case, the Win32_Service class is stored in the root\cimv2 namespace.
OK, but what about the computer name? A minute ago we said the computer name was stored in the variable strComputer. But this variable has been set to a dot (.). Who the heck has a computer named dot?
Well, probably nobody. In WMI, however, setting the computer name to a dot is just a way of saying the local computer without committing yourself to any particular local computer. Suppose you have a single computer named TestComputer. In that case, either of these two lines of code will cause your script to run against TestComputer:
strComputer = "."
strComputer = "TestComputer"
In fact, if the IP address of TestComputer is 192.168.1.1, this line of code will also work:
strComputer = "192.168.1.1"
As implied above, we could have hard-coded the computer name into the script. For example, this line of code connects to the WMI service on a computer named HRServer01:
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\HRServer01\root\cimv2")
So why didn't we just hard-code in the name? Well, by using a variable, we're making it easy to run the script against any computer; all we have to do is figure out a way to change the value of the variable strComputer. And that is what this column is all about.
The next part of the script asks the WMI service for some information. It requests a list of all the services installed on the computer, and then stores that information in the colRunningServices variable. (If you want to impress your friends, tell them that colRunningServices is actually a special type of variable known as an object reference. Let's just hope that they don't ask you what an object reference is, because we don't have time to get into that.)
Set colRunningServices = objWMIService.ExecQuery _
("Select * from Win32_Service")
The final part of the script simply displays the name and state of each service that was stored in colRunningServices:.
For Each objService in colRunningServices
Wscript.Echo objService.DisplayName & VbTab & objService.State
Next
Go ahead and type the complete script into Notepad and save it as ListServices.vbs. It doesn't really matter where you save the script, but just so were all on the same page, you might want to save the script in the C:\Scripts directory. If you don't already have a C:\Scripts directory, create one before you attempt to save the script.
A Scripting Guys Freebie: OK, here's a way to really impress your friends; this script will create the folder C:\Scripts for you:
Set objFSO = CreateObject("Scripting.FileSystemObject")
objFSO.CreateFolder("C:\Scripts")
After you've saved the script, open up a command prompt window, navigate to the C:\Scripts directory, and then run the script by typing the following and pressing ENTER:
cscript ListServices.vbs
You should see output similar to the following, showing all the services on your local machine along with the current state of the service (such as Stopped or Running).
Now, there's nothing wrong with this, but what if you want to get that same service information from a remote machine? Well, you might remember that on the first line of the script the strComputer variable is set to a dot (.), indicating the local computer. Later in the script, strComputer is used to connect to the WMI service on that computer. If you change the dot to the name of another computer, the script will connect to the WMI service on that computer. In turn, the information that retrieved by the script will be information about the remote computer, not the computer on which the script is being run.
For example, let's say you want to modify the script to connect to a remote computer named HRServer01. To do that, simply change the value of the strComputer variable from . to "HRServer01". If you're playing along at home, choose a remote computer on your network and replace the dot with its name. Note that, by default, you must be in the local administrators group of the remote computer in order for this script to work. That's because only members of the local administrators group can use WMI to retrieve information from remote computers.
For example, here's the new script that retrieves service information from HRServer01:
strComputer = "HRServer01"
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colRunningServices = objWMIService.ExecQuery _
("Select * from Win32_Service")
For Each objService in colRunningServices
Wscript.Echo objService.DisplayName & VbTab & objService.State
Next
Run the modified script in the same way as you did before, by typing the following and pressing ENTER:
cscript ListServices.vbs.
You should see results similar to those produced by the last script (your actual results will vary, depending on the services installed on the remote computer).
The key difference between this script and the first script we wrote: this time the services listed are those running on the remote computer HRServer01. For example, you might have noticed that the Application Management Service that was running on the local computer (previous screenshot) is stopped on HRServer01.
Note: What if you chose a computer that doesn't exist, or that is currently offline? Well, in that case, the script will blow up. As a temporary workaround, make the first line in the script On Error Resume Next. Sometime soon will discuss better ways to work around this issue
Entering Computer Names as Command-Line Parameters
Changing the strComputer variable certainly works; it enables you to run the script against a remote computer. However, it would be nice if you didn't have to modify the script each time you had it to run against a different computer. In fact, it would be nice if you could just specify the computer's name as a command-line parameter.
That might sound hard, but it's actually remarkably easy. When you start a script from the command prompt (for example, by typing cscript ListServices.vbs) any characters you type after the script name are interpreted as command-line parameters. In fact, not only are they interpreted as command-line parameters, but they are automatically retrieved and stored so you can access them from within your script.
For example, suppose you type this to start a script:
cscript ListServices.vbs HRServer01 WebServer01
In this case, both HRServer01 and WebServer01will be recognized as command-line parameters. In turn, they will be stored in a special collection (the WSHArguments collection) so that you can access them from within the ListServices.vbs script. Arguments are stored in the collection along with an index number indicating the order in which they were entered. Because index numbers start with 0, the collection looks like this:
Index Number |
Argument |
---|---|
0 |
HRServer01 |
1 |
WebServer01 |
Within the script, you refer to the first parameter (HRServer01) using WScript.Arguments(0) and the second parameter (WebServer01) by using WScript.Arguments(1). If there were a third parameter, you'd refer to it by using WScript.Arguments(2) and so on. When you start a script using command-line arguments, the individual arguments must be separated by at least one space.
Note: Uh-oh; what if you have to enter an argument that includes a space, like Default Domain Policy? In that case, you must enclose the argument in quotation marks, like so: "Default Domain Policy". If you don't, the string Default Domain Policy will be interpreted as three separate arguments: Default, Domain, and Policy.
So let's try the simplest possible case: let's modify our script so that it runs against whatever computer we specify as a command-line argument. To do that, we simply need to set the strComputer variable equal to WScript.Arguments(0), the first command line parameter. Thus:
strComputer = WScript.Arguments(0)
WScript.Echo "Running Against Remote Computer Named: " & strComputer
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colRunningServices = objWMIService.ExecQuery _
("Select * from Win32_Service")
For Each objService in colRunningServices
Wscript.Echo objService.DisplayName & VbTab & objService.State
Next
Note: We also added a line that echoes the name of the computer that the script is running against, just so you don't get confused about what the script is doing.
Modify the script, and then run it. Be aware that if you try to run it as you did before, by just typing cscript ListServices.vbs, you will get this error message:
C:\scripts\ListServices.vbs(1, 1) Microsoft VBScript runtime error:
Subscript out of range.
Why? Because you didn't enter a command-line parameter following ListServices.vbs the reference to WScript.Arguments(0) doesn't make any sense; you told the script to set strComputer to the value of the first argument, but that argument is nowhere to be found. Consequently, VBScript gives you a hard time about it. (Incidentally, the first digit in (1,1) tells you the line number in the script where the error VBScript is complaining about is occurring.)
Lets try to appease VBScript by typing something like the following (replace HRServer01 with the name of an actual computer on your network):
cscript ListServices.vbs HRServer01
(If you just have one computer, type in the name of that machine. Even better, type a dot, and see if the script runs against the local machine, the way we keep saying it will.)
Once again you should once see a list of services and the current state of those services. The difference is that you can run this script against any computer simply by typing the appropriate name at the command-line. So, there you have it. Thank you, and good night.
Entering Multiple Computer Names as Command-Line Arguments
Oh, right; we still haven't touched on what we claimed this column was about: running scripts against multiple computers. Now, it's true that the preceding script could run against multiple computers, as long as you we're willing to run it multiple times. For example, typing the following commands will list the services and their status on 3 different computers: WebServer01, FileServer01 and SQLServer01.
cscript ListServices.vbs WebServer01
cscript ListServices.vbs FileServer01
cscript ListServices.vbs SQLServer01
But you're right: that's far too much repetition and work for a scripter! Shouldn't we be able to just type something like this and be done with it:
cscript ListServices.vbs WebServer01 FileServer01 SQLServer0
Well, of course we can. As we noted earlier, when command-line parameters are stored, they are stashed in a collection called the WSHArguments collection. A collection is just what it sounds like: a bunch things that go together. The WSHArguments collection is just a list of all the command-line arguments that were entered when a script was run.
So how does that help us? Well we can retrieve each of these arguments (each computer name) from the WSHArguments collection by using a For Each loop. Heres a script that runs against each computer included as a command-line argument:
For Each strArgument in WScript.Arguments
strComputer = strArgument
WScript.Echo "***** Computer ******" & vbCrLf & strComputer & vbCrLf
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colRunningServices = objWMIService.ExecQuery _
("Select * from Win32_Service")
For Each objService in colRunningServices
Wscript.Echo objService.DisplayName & vbTab & objService.State
Next
Next
And here's what the script does:
It takes the command-line arguments — in this example, WebServer01, FileServer01, SQLServer01 — and put them into the WSHArguments collection.
It takes the value of the first argument (WebServer01), and assigns that to the variable strComputer.
It connects to the computer represented by strComputer, and retrieves and displays information about all the services.
It loops back to the start of the For Each loop, takes the value of the second argument (FileServer01) and assigns that to the variable strComputer.
It connects to the computer represented by strComputer, and retrieves and displays information about all the services.
It loops back around, takes the value of the third argument (SQLServer01), etc. etc. The script continues repeating these steps over and over, until it has run through the entire collection of command-line arguments.
Incidentally, there's nothing magical about the number three. Try this with 1, 2, 3, or even 100 arguments. This code will work as long as there is at least 1 argument.
Note: OK, so what if someone does forget to supply an argument? If that's a concern, tack this code at the beginning of the script. It uses the WSHArgument's Count property to count the number of arguments in the collection. If the Count is 0, meaning no arguments were supplied, it echoes a message to that effect, and then terminates the script:
If WScript.Arguments.Count = 0 Then
Wscript.Echo "You must supply at least one computer name."
Wscript.Quit
End If
Retrieving Computer Names from a Text File
So, we started with a WMI script from the TechNet Script Center that retrieved service information about a single computer: the computer on which it was run. We're now at the point where we can enter any number of different computer names on the command-line when we run the script, and the result will be the display of service information from all of those computers.
Note: That could be an awful lot of information. In a future article, we tell you how to save that information in a variety of formats; for now, you can use the > symbol to redirect output to a file instead of the command-prompt window. After that, you can open the file with Notepad and browse it at your convenience instead of seeing it scroll by at lightening speed. To run the script and have its output redirected to a file named report.txt, type the following at the command-prompt:
cscript ListServices.vbs WebServer01 FileServer01 > report.txt.
Now, we know what you're thinking. You're thinking, Well, thanks, Scripting Guys, but, um, I have to run my script against 50 computers. Are you telling me I have to type all 50 computer names at the command prompt when I run the script?
Of course not. Instead, you can have your script read a text file of computer names and then run against each of those computers.
To do this, start by creating a simple text file of computer names, one computer name per line. For demonstration purposes, save the file as C:\Scripts\Computers.txt.
And now here's the script that reads the preceding file. To run this script, you don't need to use any command-line parameters; just type the following at the command prompt and press ENTER:
cscript ListServices.vbs.Const INPUT_FILE_NAME = "C:\Scripts\Computers.txt"
Const FOR_READING = 1
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile(INPUT_FILE_NAME, FOR_READING)
strComputers = objFile.ReadAll
objFile.Close
arrComputers = Split(strComputers, vbCrLf)
For Each strComputer In arrComputers
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colRunningServices = objWMIService.ExecQuery _
("Select * from Win32_Service")
For Each objService in colRunningServices
Wscript.Echo objService.DisplayName & vbTab & objService.State
Next
Next
The result is a display of information about the services on all of the computers whose names are listed in the file. So, we've got a script that does what we wanted to do; now we have to figure out how it works.
The first two lines of the script are just constants that, as you will see, make the script easier to read and understand. INPUT_FILE_NAME holds the path to the file that contains our computer names. FOR_READING is used to indicate that, when we open the file, we want to read from it. We'll discuss file manipulation in a future column; for now you just need to know that, when working with text files, you can either read from them or write to them. Because you can't do both simultaneously, you have to specify the desired mode in the script.
Another Scripting Guys' freebie: If this doesn't impress your friends, then you need to get new friends. Change the first line of the script to this:
INPUT_FILE = Wscript.Arguments(0)
What does that gain you? Well, suppose you have a bunch of text files that contain computer names: one with your DHCP servers, one with your domain controllers, one with your email servers. Do you need to create separate scripts for each of these? Heck no. In Windows Explorer, drag the appropriate text file onto the icon for your script (ListServices.vbs). The script will use the name of the text file as an argument, and then automatically open and read that file. Try it, and see what we mean.
The line: **Set objFSO = CreateObject("Scripting.FileSystemObject")**gets your script ready to work with files. VBScript doesn't know how to deal with files; instead, it needs assistance in the form of a COM object. In this case, we're using the FileSystemObject, a COM object that is installed along with VBScript and Windows Script Host and is great at reading from and writing to text files.
The line: Set objFile = objFSO.OpenTextFile(INPUT_FILE_NAME, FOR_READING) is fairly easy to decipher. It opens a text file, the one whose path is stored in the INPUT_FILE_NAME constant, for reading. By the way, this is why we used constants. This line of code — which uses the hard-coded value 1 — also opens a text file for reading, but is much less intuitive:
Set objFile = objFSO.OpenTextFile(INPUT_FILE_NAME, 1)
The line: strComputers = objFile.ReadAll is where the actual work of reading the file contents finally takes place. Those contents are read in their entirety (ReadAll) and stored in the strComputers variable. The next line of the script simply closes the file.
At this point, we're almost on familiar ground. When we used command-line parameters, we ended up with the computer names in a collection (they seem to be pretty popular in system administration scripting). We could then use a For Each loop to work our way through all the items in the collection. This time we have all of the computer names in the strComputers variable; in fact, if you were to echo the value of strComputers, you'd get output that looked exactly like the text file. Unfortunately, though, this is not a collection, so we can't just toss in a For Each loop and expect to get reasonable results.
The line: arrComputers = Split (strComputers, vbCrLf) takes the contents of strComputers (our list of computer names), extracts the individual computer names, and places them into something called an array. An array is similar to a collection (there are important differences, but for our purposes those differences don't really matter). What is important is the fact that arrays can be traversed using our old friend, For Each.
You might be wondering how the Split function can distinguish between individual computer names stored in the strComputers variable. As you might recall, each computer name is on a separate line in the file. At the end of every line of a text file like that there's an invisible marker that VBScript knows as vbCrLf (carriage return, linefeed). Basically we're telling the Split function that individual items strComputers are separated by carriage return, linefeeds. Split starts reading through the data. It finds the letters HRServer01, and then encounters a carriage return, linefeed. This is the signal that the first item — HRServer01 — has been found. It puts HRServer01 into the array, and then continues reading. It finds the letters WebServer01, and then encounters another carriage return, linefeed. Most likely you can figure out the rest of the story.
After we have our computer names in the arrComputers array, the remainder of the script is the same as it was previously. It simply uses a For Each loop to retrieve all of the computer names in the array (collection). The result is the display of corresponding information about services on each of those computers.
Retrieving Computer Names from an Active Directory Container
Stopping at this point wouldn't be a crime (so put down the phone and don't bother to dial 9-1-1). We do have a pretty good solution. However, think about how you might gather all of those computer names to put in your text file. Maybe, if you are in an Active Directory-based environment, you'd fire up Active Directory Users and Computers and check the directory. But you're a scripter! Why are you doing this manual labor? Shouldn't you instruct your script to do this work for you? Sure you should. And Microsoft has just the scripting library for you: Active Directory Service Interfaces (ADSI).
ADSI enables your scripts to talk to directory services like Active Directory. Explaining how ADSI works is well beyond the scope of this article. That said, this story wouldn't be complete without an ADSI-enabled script that grabs computer names right out of the directory. Therefore, we're going to give you a sample script, provide a very cursory explanation, and then make a sales pitch for our just about on the presses book from Microsoft Press, the Microsoft Windows 2000 Scripting Guide, where ADSI is explained in detail (Don't worry, we're not high pressure sales people; we're going to provide the book for free online in case you're saving your pennies to buy an X-Box.)
So, here you go. This script uses ADSI to attach to the Computers container in the fabrikam.com domain and grab a collection of the names of all the computers located there. The script then looks very familiar as it uses For Each to walk through the collection, gathering service information as it goes.
Set colComputers = GetObject("LDAP://CN=Computers, DC=fabrikam, DC=com")
For Each objComputer in colComputers
strComputer = objComputer.CN
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colRunningServices = objWMIService.ExecQuery _
("Select * from Win32_Service")
For Each objService in colRunningServices
Wscript.Echo objService.DisplayName & vbTab & objService.State
Next
Next
What if your computer accounts aren't stored in the Computers container? Hey, no problem; the string LDAP://CN=Computers, DC=fabrikam, DC=com can be modified to connect to other containers in the directory. For example, this line of code connects to the Finance OU (note that you must use the syntax OU= rather than CN=):
Set colComputers = GetObject("LDAP://OU=Finance, DC=fabrikam, DC=com")
That's all we have time (and space) for this month. Any questions or comments, please write to us at scripter@microsoft.com (in English, if possible).
For a list and additional information on all Tales from the Script columns, click here.