|Getting a script to process all the machines in Microsoft Active Directory domains can be a challenging task for several reasons, including having different Windows operating systems and old computer accounts in domains. ScriptTemplate.vbs can help you successfully automate a task on all the servers in your Active Directory domain running in a Windows Server 2003, Windows 2000 Server, or mixed environment. ScriptTemplate.vbs uses Windows Management Instrumentation, Active Directory Service Interfaces, Windows Script Host, and ActiveX Data Objects.|
Most systems administrators know that automating tasks is an important skill in Active Directory (AD) environments, especially in large enterprises. However, getting a script to process all the machines in an AD domain is a challenging task, especially when the AD domain includes 500 or more machines. Some machines are online and appear to be functioning properly, but when a script attempts to access them to perform a task, these machines cause the script to hang. As a result, the script won't process the rest of the machines on the list. This problem occurs because properly configuring a network is difficult. The difficulties include:
- The domain contains machines running different Windows OSs.
- The domain contains old computer accounts, which leads to an additional burden for the script.
- The network might not be properly configured for the script to run from a central location. For example, there might be blocked firewall ports across the WAN.
I created a template, ScriptTemplate.vbs, that can give you a better chance of successfully automating a task on all the servers in your AD domain. ScriptTemplate.vbs first obtains a list of the servers in a Windows Server 2003/Windows 2000 Server AD domain. Alternatively, you can use an input file to provide the list of servers. ScriptTemplate.vbs then tests to see whether the target servers are online and whether a Windows Management Instrumentation (WMI) connection can be established. If the servers are online and their WMI service is working, the script performs a specified task (e.g., obtain data, make a configuration change) on those servers. If a server is offline or its WMI service isn't working, the script won't hang. Instead, it records the failure in a log file, then continues to the next server.
ScriptTemplate.vbs is currently set up to produce four output log files so that you know when problems occur and why. One log file reports on the servers that were offline when the script ran (aka the Offline log). Another log file reports on servers that were online but unable to establish a WMI connection (aka the Error log). There's also a log file that reports on the servers whose computer accounts are no longer found in DNS but still remain in the AD database (aka the Unknown log). The last log file (aka the Main Report log file) reports on the success or failure of the specified task (e.g., a server query) that the script is supposed to perform. You can easily adapt the script to produce other output log files. For example, you can adapt the script to produce a Secondary Report log file if the script performs several tasks (e.g., a server query and a server configuration change).
All the log files are tab-delimited text files that have an .xls extension. Although I could have used Microsoft Excel to log the output data, most system administrators like to run scripts on servers, which usually don't have Excel installed. In addition, if a script uses Excel to write to the logs and the script is run on a workstation in which Excel is installed, the script will halt if a user launches Excel to open a worksheet. Therefore, I decided to use a text file format for the log files. By using tabs as delimiters and an .xls extension, you can open the log files with Excel by simply double-clicking the files.
Three subroutines—checkArguments, RuntheScript, and ProcessServers—constitute the heart of ScriptTemplate.vbs. They call on many self-contained functions that you can easily reuse in other scripts. Because a lot of the code in the script is written for reusability, the subroutines and functions declare their own variables and constants. However, there are five global variables:
- StrScriptName. This variable stores the script's friendly name (e.g., System Audit Script), which is used as the title of the help screen.
- StrMainReport. This variable stores the name of the Main Report log file.
- StrSecReport. This variable stores the name of the Secondary Report log file, if applicable.
- StrProgram. This variable stores the word script, which is used within the description of the help message in the help screen.
- StrProgramName. This variable stores the basic launch command for the script name (e.g., ScriptTemplate.vbs), which is also used in the help screen.
Let's look at how the checkArguments, RuntheScript, and ProcessServers subroutines use these global variables and how they work in concert with the functions to perform tasks on many servers without the usual problems.
The checkArguments Subroutine
The script uses the checkArguments subroutine in Listing 1 to retrieve and process any arguments you provide on the command line when you launch the script. It is important to note that although the script uses the checkArguments subroutine to obtain the command-line arguments, the arguments are optional. The available optional arguments are:
- The /?, /h, help, -h, or –help switches. You can specify any one of these switches to obtain help on how to run the script.
- The /e server1,server2 argument, where server1 specifies any server you want to exclude from being processed by the script when the script obtains its server list from an AD query. If you want to specify more than one server, you use a comma as a delimiter.
- The /f filename argument, where filename is the name of an input file that contains the names of the servers to be processed by the script. In the input file, each computer name should go on a separate line.
As callout A in Listing 1 shows, the checkArguments subroutine stores the command-line arguments in a dynamic array named arrArguments. If you don't supply any arguments, the checkArguments subroutine simply calls the RuntheScript subroutine and passes the strInputFile and arrExServers variables with empty strings as parameters. If you don't supply a proper argument (e.g., a /g argument, which doesn't exist) or the necessary arguments (e.g., the /f without a filename), the script calls the ShowUsage subroutine to display a popup dialog box that explains how to run the script.
If you include the /e argument on the command line, the script stores the specified server names in a dynamic array named arrExServers, as callout B in Listing 1 shows. If you include the /f argument, the script stores the specified filename in the strInputFile variable, as callout C in Listing 1 shows. After processing the /e and /f arguments, the checkArguments subroutine calls the RuntheScript subroutine with the strInputFile and arrExServers variables passed in as parameters, as callout D in Listing 1 shows.
The RuntheScript Subroutine
Listing 2 shows the RuntheScript subroutine. After declaring its variables, the subroutine calls upon three functions to get the data it needs to calculate the script's run time and name the output log files, as callout A in Listing 2 shows.
RuntheScript first calls VBScript's Now function to obtain the current date and time and sets it to the strStartTime variable. The subroutine later uses this variable to calculate how long it took the script to run.
Next, RuntheScript calls the fnDate function in Listing 3 to obtain the current date and put it in the format mm-dd-yyyy. The value returned by the fnDate function is stored in the strFNDate variable, which is used to construct the log files' names.
RuntheScript subroutine then calls the getDNSDomain function to obtain the DNS domain name, which is stored in the strDomain variable. Like strFNDate, strDomain is used to construct the log files' names. As Listing 4 shows, the getDNSDomain function first binds to the RootDSE object (a special object in LDAP 3.0). Using the Get method, the function reads this object's defaultNamingContext attribute to obtain the distinguished name (DN) of the domain in which the script was launched. The function converts this DN to a DNS domain name that follows the subroot domain format (e.g., mydomain.com).
After the RuntheScript subroutine has the data it needs, it constructs the names of the log files, as callout B in Listing 2 shows. The filenames follow the format
where Domain is the DNS domain name in the strDomain variable and LogPurpose is the log descriptor (i.e., the strMainReport variable's value or the word Error, Offline, or Unknown). Following the descriptor is the word Servers. The last two parts of the filename are the current date in the strFNDate variable and the .xls extension.
With the log files ready to go, the RuntheScript subroutine obtains the list of target servers from either the input file or from AD. The code at callout C obtains the server list from an input file, whereas the code at callout D obtains the server list from AD.
Obtaining the server list from an input file. The code at callout C uses VBScript's Len function to determine whether the strInputFile variable holds a value (i.e., the name of the input file that contains the server list). If there is a value, the subroutine calls the PrintMsg2 subroutine to display a screen message, then calls the getServerList function to read the server names in the input file. RuntheScript passes in two parameters to the getServerList function: the strInputFile variable and the strInRec variable (which is empty at this point).
As Listing 5 shows, the getServerList function instantiates the FileSystemObject object. The function uses the object's OpenTextFile method to obtain an instance of a TextStream object to represent the input file so that the file's contents can be read. Using VBScript's IsObject function, getServerList checks to see whether the strInputFile variable stores a valid filename.
When the input filename is valid, IsObject returns a True value. In response, getServerList creates a Dictionary object and assigns it to a variable named dicDataList. The Dictionary object requires key-item pairs. So, in a Do...Loop statement (aka Do loop), the function assigns each server name as an item in the dicDataList array. The Dictionary object's Count property (dicDataList.Count) is used as a placeholder for the key. The count increments by 1 in each iteration through the loop. After all the server names in the file are read in, the count in the Dictionary object’s key is set to the strInRec variable. The getServerList function returns this variable along with the server array in dicDataList to the RuntheScript subroutine, which stores it in the arrServerList variable.
When the input filename isn't valid, IsObject returns a False value. In this case, getServerList doesn't return any server names, and strInRec contains a null value. When the strInRec value is null, the RuntheScript subroutine calls the ShowUsageRec subroutine, which displays a message that states the input file is empty, then ends the script.
Obtaining the server list from AD. If an input file isn't supplied, the script calls the getADServerList function to obtain a server list from AD, passing in the strFNDate variable as a parameter. Listing 6 shows the getADServerList function. Like the getServerList function, the getADServerList function creates a Dictionary object to store the server names and assigns it to the dicData variable. Next, the function creates a RootDSE object and uses its Get method to read the domain's DN from the defaultNamingContext attribute. The DN is converted to a DNS domain name and stored in the strDomain variable. This domain name is used for a screen display and for a log file that's used only for logging the server list from AD. The log file's name follows the format
where strDomain is the DNS domain name in the strDomain variable, which is followed by the words Domain_Servers, the current date in the strFNDate variable, and the .xls extension. The getADServerList function uses the FileSystemObject and TextStream objects to create and prepare the Domain_Servers log file for writing.
To fill that log file with server names, the getADServerList function uses ActiveX Data Objects (ADO). As callout A in Listing 6 shows, the function uses ADO's Connection object to connect to AD and ADO's Command object to define the query to run against AD. The query uses three AD attributes to obtain the server data: Name, distinguishedName, and operatingSystem. The Name attribute returns the server name. The distinguishedName attribute returns the DN, which the getReverseOU function in Listing 7 converts to the organization unit (OU) where the server resides. The operatingSystem attribute returns the machine’s OS name, which is used to separate out the servers in the AD server list.
The code at callout B in Listing 6 creates a disconnected ADO Recordset object and assigns it to the DataList variable. In a disconnected recordset, you can work with the records after you terminate the connection to the database that generated the recordset. In this case, the getADServerList function iterates through the recordset returned by the query, adding only server records to the disconnected recordset in DataList. The function then sorts the server data by OU name, then server name.
After sorting the data, the getADServerList function uses a Do loop to iterate through DataList. For each server, getADServerList constructs a string that consists of four elements—a count and the server's name, OU, and OS—that are delimited with tabs. The tab-delimited string is set to the strHostData variable. Then, the string in strHostData is assigned as the item and Count property is the placeholder for the key in the dicData array.
Besides constructing the string, the getADServerList function writes the server data to the Domain_Servers log file during the Do loop. The function adds the headers "No.," "Host Name," "OU Container," and "Operating System" to the log file.
After the Do loop completes, the getADServerList function returns the server array in dicData to the RuntheScript subroutine. RuntheScript, in turn, stores the array in the arrServerList variable.
At this point, the arrServerList variable contains the server data, no matter whether the server list came from an input file or AD. The RuntheScript subroutine processes the servers in arrServerList by calling the ProcessServers subroutine. As callout E in Listing 2 shows, seven parameters are passed to the ProcessServers subroutine: the strInputFile variable, the arrServerList variable, the arrExServers variable, and the variables representing the four output log files (i.e., the Main Report, Error, Offline, and Unknown log files). As mentioned previously, the number of the log files can vary, depending on the tasks you want the script to perform on the servers. If the script needs more than one Main Report log file, you can add parameters for the extra log files.
The ProcessServers Subroutine
Listing 8 shows the ProcessServers subroutine. After declaring its variables, this subroutine defines the constant for and creates a FileSystemObject object. ProcessServers then uses the Windows Script Host (WSH) WshNetwork object's ComputerName property to obtain the name of the computer on which the script was launched. This computer name is assigned to the strLComputer variable, which will be used by the WMIConnection function to test for a WMI connection.
Next, the ProcessServers subroutine uses a For Each…Next statement (aka For Each loop) to iterate through the server array in arrServerList. Within this For Each loop, the script performs its tasks.
As callout A in Listing 8 shows, the ProcessServers subroutine uses the Len function to determine whether the strInputFile variable holds a value. If there is a value (which means an input file was provided), the subroutine sets the strComputer variable to each value in the array. If there isn't a value (which means an input file wasn't provided), the subroutine has to take a more roundabout way to obtain each server name because the name is part of a tab-delimited string. To obtain the name, ProcessServers uses VBScript's Split function with a tab delimiter to parse the string back into its four elements (i.e., the count and the server's name, OU, and OS) . The subroutine sets the strComputer variable to the second element and the strOU variable to the third element.
Next, the ProcessServers subroutine calls the excludedServer function to check the server name in the strComputer variable to make sure that it isn't a server that should be excluded. As Listing 9 shows, the excludedServer function accepts two parameters: the strComputer variable, which contains the target server's name, and the arrExServers variable, which contains the exclusion list. The function iterates through each server name in arrExServers and compares it with the target server name. If the names match, the value for the excludedServer function is set to True, which prompts the ProcessServers subroutine to skip that particular server and continue to the next one. If no match is found, the value for the excludedServer function is set to False. ProcessServers, in turn, logs a variable (h) to count the server for a message displayed by the PrintMsg3 subroutine, then calls the getPingIP function to ping the server, as callout B in Listing 8 shows.
The getPingIP function in Listing 10 accepts two parameters: the target server's name (strComputer) and a placeholder variable named strPingStatus, which returns the ping status to ProcessServers. After declaring its variables, getPingIP sets the variables for a WshShell object, Dictionary object, and Regular Expression (RegExp) object. It also sets the RegExp pattern being searched for.
The getPingIP function uses the WshShell object's Exec method to execute ping.exe against the target server. (Note that instead of using ping.exe, you can use WMI's Win32_PingStatus class to ping servers. However, because the Win32_PingStatus class isn't available in Win2K, I had to use ping.exe.)
The Dictionary object stores the target server's response to the ping. In a Do loop, the getPingIP function processes the ping response to obtain the server's IP address and determine the ping status. The function uses the RegExp pattern to search for the IP address. When one is found, the function returns the IP address back to the ProcessServers subroutine, along with the status of On line or Off line in the strPingStatus variable, depending on whether the server was online or offline. When the IP address isn't found (i.e., it no longer exists in DNS), the function returns the value of No IP Address instead of an IP address, along with the status of Unknown host in the strPingStatus variable.
When the strPingStatus value is Off line, the ProcessServers subroutine writes the server name to the Offline log file. When the strPingStatus value is Unknown host, the server name is written to the Unknown log file. When the strPingStatus value is On line, the subroutine calls the WMIConnection function, as callout C in Listing 8 shows.
The WMIConnection function accepts two parameters: the target server's name (strComputer) and the local computer name ( strLComputer). As Listing 11 shows, the function begins by comparing the target server name against the local computer name to see whether they're the same. When the two names are the same, the connection test is skipped and the function ends. This is done so that when the script is launched from a member server, there's no need to test the connection to the server on which the script is launched.
When the two names differ, the function creates an instance of the SWbemLocator object. As callout A in Listing 11 shows, it uses that object's ConnectServer method with the WBEM_FLAG_CONNECT_USE_MAX_WAIT flag to test the WMI connection. (For information about the ConnectServer method and its flags, see the SWbemLocator.ConnectServer Web page at http://msdn2.microsoft.com/en-us/library/aa393720.aspx.) With this flag, the ConnectServer method has a connection timeout of two minutes if the WMI service on the target machine isn't working. When this happens, the WMIConnection function returns an error and the connection to the target server is terminated. Using the timeout flag ensures that the script doesn't hang indefinitely, thereby ensuring that the script will process all the servers on the list.
The WMIConnection function's return value is set to the strConnection variable. When the WMI connection doesn't generate an error, the strConnection value is an empty string. When the WMI connection generates an error, strConnection holds a string that contains an error number and error description. What is written to the Error log file depends on whether the server name was supplied by an input file or by a server list from an AD query. When the server is from an input file, the Error log file includes the server name, IP address, error number, and error description. Otherwise, the Error log file includes the server name, IP address, server OU, error number, and error description.
When the WMIConnection test doesn't return an error, the ProcessServers subroutine continues to process the target server for information about its OS, service pack, domain role, and NetBIOS domain name. If the server name was obtained from the input file, the subroutine also determines the server's OU. To get this information, ProcessServers calls four functions, as callout D in Listing 8 shows. The output from the following functions are written to the Main Report log file:
- The getOSVersion function in Listing 12. The getOSVersion function accepts one input parameter: the target server's name. It uses WMI's Win32_OperatingSystem class to obtain the target server's OS name and the service pack level. It combines them to form a custom label (e.g., Windows Server 2003 , Datacenter Edition Service Pack 1), which is set to the strOSVersion variable.
- The getDomainRole function in Listing 13. Like the getOSVersion function, the getDomainRole function accepts one input parameter: the target server's name. The getDomainRole function uses the Win32_ComputerSystem class's DomainRole property to obtain the target server's domain role. The function's return value is set to the strDomainRole variable.
- The getNetBIOSDomain function in Listing 14. After accepting the target server's name as an input parameter, the getNetBIOSDomain function obtains the NetBIOS domain name for that server. Although you can use the Win32_ComputerSystem class's Domain property to obtain a computer's domain name, the return value can either be a NetBIOS domain name or a DNS domain name, depending on the computer's Windows platform. For example, this WMI class returns a NetBIOS domain name for Win2K computers and a DNS domain name for Windows 2003 and Windows XP computers. To be consistent across all the Windows platforms, I used WMI's StdRegProv class's GetStringValue method to obtain the NetBIOS domain name from the target machine's registry. The registry key that contains the NetBIOS domain name is HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon. The entry name is CachePrimaryDomain, which has a value type of REG_SZ. The function's return value is set to the strNTBDomain variable.
- The getOU function in Listing 15. The getOU function accepts two input parameters: the target server's name and the NetBIOS domain name obtained by the getNetBIOSDomain function. The getOU function uses Active Directory Service Interfaces' (ADSI's) IADsNameTranslate object to obtain the target server's DN. (For information about the IADsNameTranslate object, go to the IADsNameTranslate Web page at http://msdn2.microsoft.com/en-us/library/aa706046.aspx.) The getOU function then calls the getReverseOU function in Listing 7 to convert the DN into an OU name. The getOU function's return value is set to the strOU variable.
If the strOSVersion, strDomainRole, strNTBDomain, or strOU variable is returned to the ProcessServers subroutine empty, the subroutine sets that variable's value to "Unknown."
At this point, the ProcessServers subroutine is ready to perform a specific task in AD, as callout E in Listing 8 shows. I will discuss what you might include in this code in the "Using the Script" section.
After all the servers in the arrServerList array have been processed, the ProcessServers subroutine uses the IsObject function to determine the log files that were generated during the run. It then closes these open log files and cleans up the variables.
The last task that the script performs is to calculate the time (in seconds) it took to run. To perform this calculation, the RuntheScript subroutine uses VBScript's DateDiff function to subtract the current time from the starting time that was stored in the strStartTime variable at the beginning of the script. The result of this calculation is set to the strElapsedTime variable. The script then uses the convertTime function in Listing 16 to convert the elapsed number of seconds into a day hh:mm:ss time format, which is displayed on the screen along with a finish message. The script is also set up to display a finish message and the run time in a pop-up dialog box. Because this dialog box waits until you acknowledge the script has finished, the call to the ShowFinish subroutine that displays the pop-up dialog box is commented out. If you want the pop-up dialog box to appear, you need to remove the Rem statement from the call, which appears at the end of the RuntheScript subroutine.
Using the Script
Before you use ScriptTemplate.vbs, you need to know about several prerequisites and how to customize the script. Here are the prerequisites you need to be aware of:
- For ScriptTemplate.vbs work in the AD environment, you need to run it on a server or workstation that is a member of the AD domain. You must run the script using an account that has administrative privileges in that AD domain.
- When a custom server list is provided as an input for the script to run, all the servers in the list must be from the same domain of the computer on which the script is run.
- For Windows 2003 SP1, the Windows Firewall can't be enabled. (In Windows 2003, Windows Firewall is disabled by default.) Otherwise, the script generates an Access is denied error, which will be written to the Error log file.
- The network ports required for running the script must not be blocked by the network firewall. Otherwise, the following error message is written to the Error log file: The RPC server is unavailable.
- On Win2K servers, the script might hang indefinitely when it reaches the WMIConnection function because of a shortcoming in WMI in Win2K. According to the IWbemLocator::ConnectServer Web page at http://msdn2.microsoft.com/en-us/library/aa391769.aspx, the ConnectServer method's WBEM_FLAG_CONNECT_USE_MAX_WAIT flag isn't available for the Win2K platform. (Unfortunately, the SWbemLocator.ConnectServer Web page at http://msdn2.microsoft.com/en-us/library/aa393720.aspx doesn't mention this limitation.) When Win2K servers cause the script to hang in this manner, you can use the script's command-line option to exclude them from being processed by the script. In some instances, restarting the WMI service on the target server allows the script to continue to the next server.
Before you use ScriptTemplate.vbs, you need to make two customizations. First, you need to customize the values for the StrScriptName, StrMainReport, and StrSecReport (if applicable) global variables that I discussed earlier. You'll find these variables at the beginning of the script.
Second, you need to customize the section of code highlight by callout E in Listing 8. In the ProcessServers subroutine, you need to add code that performs a specific task on the target servers. Using the information stored in the strOSVersion, strDomainRole, or strOU variable, you can target servers on a certain Windows platform, in a certain OU, or that play a certain domain role. You can use custom functions and subroutines to perform the tasks that you want the script to perform. You can then use the return values from your custom routines in the Main Report log file.
A Useful Addition to Your Scripting Toolkit
I hope you'll find ScriptTemplate.vbs useful. If you add this script to your scripting toolkit, you’ll always have a template that you can easily adapt to automate tasks in your AD environment. In a future article, I'll demonstrate how to use this template to create a script that audits the system time in Windows servers.