Building the script one piece at a time
Frequently, when something just isn't right on a computer, you can trace the problem to a service that's stopped running. Checking the status of services to make sure they're running is a useful troubleshooting step. On a local machine, you simply open the Microsoft Management Console (MMC) Computer Management snap-in and browse to Services and Applications, Services. But how can you quickly and easily check services on remote computers? If you're using Windows 2000 Server Terminal Services in Remote Administration mode, you can use Microsoft's Remote Desktop Connection software, but doing so requires you to perform additional steps. Furthermore, this option is available only on computers that accept incoming RDP requests. Particularly if you want to inventory many computers, a better solution is to write a script that lets you check the status of a service on any computer that supports Windows Management Instrumentation (WMI).
In "Windows Management Instrumentation for Beginners," May 2001, http:www.winnetmag.com, InstantDoc ID 20376, I introduced WMI by using it to streamline your computer management tasks. WMI is an interface to many physical and logical parts of a computer that the WMI service exposes. Now, I want to show you how to build a script called GetServiceStatus.vbs that attaches to WMI on a server and uses it to enumerate the services on that machine. You can configure GetServiceStatus.vbs to check the status of the local server, a remote server, a group of remote servers, or all servers in the domain. As I explain the workings of GetServiceStatus.vbs, I also talk about ways to organize data, the use of arguments in scripts, and some constants you can use to format output with tabs and carriage returns.
Gather Service Status
GetServiceStatus.vbs's core objectives are to attach to a specific server, enumerate the available services on that server, and display the status of each service. Everything else the script does contributes to specifying a set of servers to check.
To accomplish the core tasks, I assign the name of a server to a variable (sComputerName), connect to the WMI namespace on that server, and log on with administrative privileges so that I can access the information I want. Then, I retrieve a complete list of all services on the server and display each service's name and current state (e.g., started, stopped, paused) separated by a tab. Listing 1 shows how the script checks for services. I've hard-coded the server name (i.e., Alien) into this code snippet, but the actual script is more flexible, as you'll see.
As long as you have administrative rights on the domain, the code in Listing 1 lets you connect to the WMI service on another computer simply by changing the value of sComputerName to the name of the computer on which you want to run the code. The script will take slightly longer to run on a remote computer than on a local one but not significantly longer over a LAN—just long enough to connect to the service on the remote computer. The computer on which you run this code will produce output that lists all the services that are running. Figure 1 shows a bit of sample output.
However, you don't want to edit the script every time you want to check service status on a different computer. If you configure the script to use computer names supplied as arguments, stored in a text file, or collected from Active Directory (AD), your script will be able to report service status for everything from one particular server to every computer running WMI that the script can communicate with.
Computer Names as Arguments
If you want to specify just a few server names at a time, the simplest approach is to have the script prompt you to supply those names as arguments. Keep in mind that, to VBScript, any collection of characters separated by any number of spaces is a separate argument, and an argument that contains a space must be within quotation marks.
VBScript stores every argument you supply to it—numbered in order starting with 0—in a collection called Wscript.Arguments. You specify the argument that you want to use from the collection by using the collection name followed by the argument's ordinal number in parentheses. Thus, Wscript.Arguments(0) specifies the first argument in the collection, and Wscript.Arguments(3) specifies the fourth argument. Even if the collection contains only one argument, you must still specify the index number of that argument when referring to it in the script. Your script doesn't need any special code to accept arguments or store them—I wrote GetServiceStatus.vbs to assign the argument's value to a variable only to make the argument easy to work with.
The script accepts input that you supply when you run the script, indexes the content, and stores it in the Arguments collection, as the code snippet in Listing 2 shows. This code sets sComputerName equal to each argument's value in turn and prints the name of the computer so that you can identify which output goes with which machine. The final script will output the computer's service information below the computer's name.
What if a supplied server name isn't valid—for example, if you misspell the name or if the server is offline? This question is a good one because you can't get service information for a computer that doesn't exist. For now, we'll simply use the On Error Resume Next statement to tell VBScript to ignore anything that goes wrong and continue running the script.
Alternative Input as Arguments
Server names aren't the only arguments you might want to supply. To tell the script where you want it to get its input, you can specify that you want the script to read its input from a text file in which you've stored a list of server names or that you want it to check all servers in the domain. Alternatively, you can tell the script that you need help. If you don't provide any arguments, the script can assume that supplied arguments are the names of the computers on which the script should run. Let's look at how to test for all these possibilities. (For now, we're simply organizing the script so that it can work differently according to the input it gets. Later, we'll make the script do something with this information.)
If the person running the script doesn't supply any arguments, the script should run on the local computer. To check services on the local computer without hard-coding or supplying the local server's name, you can use WMI syntax. You can specify the current computer by providing its name, but you can also use a period (.) to tell WMI to run this script on the local computer. To make the script run locally by default, have it check the value of Wscript.Arguments(0), which is the first item in the Arguments collection. If the item is empty, the script will set the value of sComputerName to a period.
Another option is to make the script get the list of server names from a file. The text file's location shouldn't change, so you can hard-code it into the script. However, you must specify that you want the script to read a file. To do so, you can test to determine whether the value of Wscript.Arguments(0) is file.
You can configure the script to run against all computers in AD or some subset of computers that you code into the script. For the sake of simplicity, let's run it against everybody. If a user wants to check service status on all computers running WMI, the user would type all for the argument. (Similarly, you could let users supply the name of an organizational unit—OU—as an argument, test the input to determine whether it's an OU, then check only those computers in the specified OU.)
Because this script can accept a variety of input types, it requires some instructions. In my previous columns about scripts that take arguments, the notion of taking no arguments whatsoever wasn't an option. If the person running the script didn't provide the necessary arguments, the script informed that person that it required input and stopped running. That approach won't work with this script because not providing arguments is now a valid option—the script will simply run locally. Therefore, let's give the person running the script the ability to use the familiar /? command to obtain help with the script. If the script sees this character combination in Wscript.Arguments(0), it will print out instructions for using the script.
I've used the Select Case statements in many previous columns, so you won't be surprised that I use it in this script to check the value of inputs. Listing 3 shows an abstract of the portion of the script that decides how to attack input. The code converts the input to lowercase (to avoid case discrepancies; ALL, All, and all aren't identical string values), then tests to determine the value of Wscript.Arguments(0) and executes some code according to this value. Obviously, the final script will do more than merely report the way the script will run, but Listing 3 shows the general structure. You now have the mechanism for providing input other than server names.
Reading from a Text File
If you have more than a few server names to check, or if you want to always check the same servers, storing the names in a file is easier and more accurate than typing them in each time you run the script. If you'll always run the script from the same computer, you can store the server names in a file called servers.txt (one server name to each line), then call that file from the script. To read the file, you use a VBScript COM object called a File System Object (FSO)—VBScript doesn't know how to interact with files, but it knows how to interact with COM objects. FSOs can represent files or folders; in this case, you would use the FSO to point to and read from the text file in which you've stored your server names. Because the name of the file won't change, you can store it in the script as a constant instead of a variable, as Listing 4 shows.
If you want to create a script that can read files from different sources without requiring you to edit the script, you can make the filename the first argument and the path to the file in question the second argument. Simply set the value of INPUT_FILE_NAME equal to Wscript.Arguments(1) because that argument will contain the path.
Before you can work with the string of names that make up the sComputerName variable, you must put the names into an array. In their simplest and most common form, arrays are one-column tables of data, the rows of which are identified by index numbers from 0 through the final row number—the upper bound of the array. You can create an array and populate it manually, or you can create an array and populate it from a chunk of existing data. To make the array from existing data, use the Split function to divide the sComputerName variable's content into chunks, using the carriage return/line feed (vbCrLf) as the delimiter. The code that converts the contents of sComputerName into the aComputers array looks like this:
aComputers = Split(sComputerName, vbCrLf)
Now that you have the array, you can use a For Each...Next statement on its contents to print each computer's name and display its services and their status.
Reading Computer Names from AD
To perform a complete inventory of the services running on all servers in AD (or a portion of AD), you can read the names directly from the domain controller (DC) instead of manually inputting the names or reading them from a text file. Connecting to the servers this way ensures that you inventory only present servers—and avoids the potential problem of misspelling server names—but results in a lot of output.
Obtaining the list of servers is easy: Connect to the Lightweight Directory Access Protocol (LDAP) service on the DC, retrieve the names of all computers in AD (or a portion of AD), and place those names into a collection. Next, run a For Each...Next statement against the collection to get each computer's common name (CN), then plug in that information to get the service status on each computer. For example, to query the contents of the Computers container, you'd use the code that Listing 5 shows. The rest of the For Each...Next statement enumerates the services on each computer and their status.
Web Listing 1 (http:www.winnetmag.com, InstantDoc ID 41670) shows the assembled script, GetServiceStatus.vbs. Because this script can produce a lot of output, if you plan to run the script against more than one or two servers, I recommend that you don't try to read the output from the command window. Instead, redirect the output to a file, as follows:
getservicestatus.vbs \[arguments\] > status.txt
Not only can you now check service status on local or remote computers, but you can also provide Help for your scripts, format script output, and work from a variety of input types. You'll probably find these abilities useful in other kinds of reporting scripts.