Creating client-side building blocks to run script updates
In "Scripting a Corporate Update System," October 2002, InstantDoc ID 26360, I began to discuss how you can use scripts to build a large solution that keeps your corporate clients up-to-date. I explained the architecture of the corporate update system and described several scripts that reside in the central script repository—the server-side component of the corporate update system. Clients use these scripts to perform software updates. The corporate update system uses a client targetter script to upload to the clients necessary files that instruct the clients to download update scripts from the central script repository. This month, let's look at the client-side building blocks of the corporate update system that you use to run the central-script-repository update scripts.
As I mentioned last time, the corporate update system adds to a special Active Directory (AD) group each client (e.g., workstation, server) that successfully runs the client targetter script. After the corporate update system adds a client to the AD group, it uploads to the client the first two files that Table 1 lists.
The first file, CUSClientUpdater.job, is a scheduled task in the client's Tasks folder that runs every 5 minutes. This file calls a second file, a Windows Script File (WSF) titled CUSClientUpdater.wsf, which is located in a client folder that stores corporate-update-system scripts. CUSClientUpdater.wsf checks the central script repository for any new updates. The third client file in Table 1, CUSClientLastUpdateNumber.txt, stores the number of the last update the client successfully ran. You don't have to install the text file on the client when you install the .job file and .wsf file—if the text file is missing, CUSClientUpdater.wsf simply assumes that the absence of a file means that the update scripts should start from number 1 and creates the text file.
How the Client Scripts Work
Listing 1 shows CUSClientUpdater.wsf, which checks the central script repository every 5 minutes for new updates. The script begins by importing GenericLibrary.vbs and CUSLibrary.vbs. These libraries contain several corporate-update-systemspecific constants, some generic constants, and a generic function called GetPCGroupMembership that calls the function GetCurrentComputerDN in GenericLibrary.vbs. Web Listing 1 and Web Listing 2 (http://www.winscriptingsolutions.com, InstantDoc ID 26632) present excerpts of the two libraries that include the components for this month's scripts; I'll present additional content for these libraries during the rest of this article series.
The script in Listing 1 checks the CUSClientLastUpdateNumber.txt file to determine the number (x) of the last update the client successfully executed. The next update will be x+1. (If CUSClientLastUpdateNumber.txt doesn't exist, x equals 0 and the script attempts to run update 1.wsf.) If the central script repository doesn't have a script titled x+1.wsf (the next script), CUSClientUpdater.wsf ends and runs again in 5 minutes. If a new x+1.wsf script exists, you might have a script to execute. I say "might" because each x+1.wsf script can have an associated x+1.txt file that specifies the update should run only on clients within certain AD groups.
If no x+1.txt file exists (i.e., the update applies to all clients) or the x+1.txt file exists and the client is a member of one of the AD groups listed in that file, x+1.wsf executes on the client. However, if the client isn't a member of an AD group listed in the x+1.txt file, CUSClientUpdater.wsf deems the update to have been successful (i.e., the text file successfully ignores the update) and skips the update. Assuming that the x+1.wsf script is executed successfully or is skipped, CUSClientUpdater.wsf starts again by checking for an x+2.wsf script on the central script repository. This process of checking for update scripts and text files continues until no additional scripts are available to run or a script fails to run successfully. CUSClientUpdater.wsf then overwrites CUSClientLastUpdateNumber.txt with the number of the last successful script that the client executed or skipped. If a script execution fails, the corporate update system retries automatically in 5 minutes when the CUSClientUpdater.job scheduled task starts CUSClientUpdater.wsf all over again.
The Library Code
Web Listing 1 and Web Listing 2 define 10 constants and detail four functions. (I won't take time here to discuss the constants I covered in the previous article.) The first new constant in Web Listing 1 is a corporate-update-system definition for the standard file that stores the number of the last update script the client successfully executed. Web Listing 2 contains four new constants that represent generic parameters to the FileSystemObject::OpenTextFile method. These parameters are for opening a file for reading, writing, and appending, and creating the file if it doesn't exist. Finally, Web Listing 2 includes a constant that defines the email address to use to send administrative alerts in case CUSClientUpdater.wsf encounters an error. The ADMIN_EMAIL constant is in Microsoft Outlook format, but you can just as easily use a standard email address (e.g., firstname.lastname@example.org). You must ensure that all clients can resolve this email name or address; otherwise, clients won't be able to send messages to the administrator.
Web Listing 2 contains two short functions called GetCurrentComputerDN and GetCurrentComputerName. Although creating a function for a two-line piece of code might seem odd, I decided to make functions from these snippets to aid code readability. The GetCurrentComputerDN function first declares a variable that it uses to store an Active Directory Service Interfaces (ADSI) ADSystemInfo object. (For information about ADSystemInfo, visit http://msdn.microsoft.com/library/default.asp?url=/library/en-us/netdir/adsi/iadsadsysteminfo.asp.) The function then uses the IADsADSystemInfo::ComputerName property to retrieve the full distinguished name (DN) for the current computer and return the DN to the calling script. The GetCurrentComputerName function simply uses the Network::ComputerName method to retrieve the computer name (e.g., mypc087) from the current client.
Web Listing 2 also includes the SendSimpleMailToAdmins function for sending administrative alerts when errors occur. This simple function doesn't handle attachments or urgency stamps. The function accepts parameters for the subject and body and uses techniques that I introduced in "Automate Outlook Messaging," December 1998 (see "Related Articles in Previous Issues") to format and send messages.
The first function in Web Listing 2, GetPCGroupMembership, identifies the AD groups of which the client computer account is a member. If the client computer isn't a member of any groups, the function returns a FALSE value. However, if the client computer is a member of any AD groups, the function returns a TRUE value and places the resulting list of AD groups (i.e., ADsPaths) into an array that the function returns to the calling function.
The GetPCGroupMembership function first uses the Lightweight Directory Access Protocol (LDAP) to connect to the AD computer object that represents the current client. The function calls the GetCurrentComputerDN function I explained earlier to obtain the client's DN and prefixes the DN with the programmatic identifier (ProgID) string LDAP:// to create a full AD path, such as the paths that Web Listing 3 shows. The function binds to this string as an IADsUser object, uses the IADsUser::Groups method to obtain an ADSI collection of AD groups that this computer belongs to, and stores this collection in the adsGroups variable. The function then uses the IADsMembers interface to access the collection.
The function uses the IADsMembers::Count property on the adsGroups object to determine whether the computer object is a member of any AD groups. If the computer object isn't a member of any groups (i.e., the count is 0), the entire function returns a FALSE value to the calling code. If the IADsMembers::Count property isn't zero, the function assembles the array to be returned to the calling script. The function uses VBScript's ReDim function to dimension the array to be the same size as the IADsMembers::Count property so that it can populate the array with the ADSI collection's ADsPaths. Next, the GetPCGroupMembership function uses the intIndex variable as the array index, counting up from 0, and a For Each...Next loop to retrieve each group path from the collection and place the path into the array at the correct index. After the GetPCGroupMembership function populates the array, the function returns a TRUE value to indicate that the array contains the required values. The function then ends.
Listing 1 starts with the usual Option Explicit and On Error Resume Next statements. (The libraries don't use the Option Explicit statement because functions and subprocedures can't embed the statement.) The next lines of the script declare the variables and initialize intUpdNum to 1 (intUpdNum is a variable indicating the number of the central-script-repository update the script should process). After a quick use of CreateObject to gain access to a Scripting::FileSystemObject object in fso, the script ascertains the update number it should start with.
Ascertaining the update number. The script needs to determine whether the CUSClientLastUpdateNumber.txt file exists. If the file doesn't exist, the script leaves intUpdNum set to 1 to ensure that the script executes the first central-script-repository update. If CUSClientLastUpdateNumber.txt exists, the script opens the text file to determine the number of the last central-script-repository update that the client successfully executed. The FileSystemObject::OpenTextFile method opens the text file for reading. If no error occurs, the File::ReadLine method obtains the integer value from the file. If the result is numeric, the script increments intUpdNum by 1, then closes the file. If the result isn't numeric, the script raises an error message and ends.
Checking for errors and raising errors. The script uses a CheckError subprocedure that I described in a May 2001 article (see "Related Articles in Previous Issues") to determine whether an error has occurred. The subprocedure runs once when the script opens CUSClientLastUpdateNumber.txt and again when the script reads information from the text file. If no error has occurred (i.e., Err.Number = 0), the script exits the subprocedure; otherwise, the script addresses the error, then quits. The script uses a RaiseError subprocedure to indicate whether the data in the text file isn't numeric. RaiseError lets the script explicitly denote that an error has occurred without referencing the Err object. If any of these three errors occurs, the script stops the update process. Continuing that process wouldn't make sense because the script would be relying on useless data and would automatically attempt to apply update 1, which could be bad if the script should be running a later update. Leaving a client in this state isn't desirable if you intend to perform future updates.
CheckError and RaiseError use parameters that aid in tracking the error and let the user review where in the code the error is located. The sample script uses integers 1 and 2 for CheckError and integer 3 for RaiseError, but you can use whatever alphanumeric codes you like.
The big difference between the RaiseError and CheckError subprocedures is that RaiseError doesn't cause the script to end. Instead, RaiseError gives the calling function the option to end the script in the calling function, should you so choose. CheckError always terminates the script if the subroutine identifies an error.
To report an error, the CheckError subroutine creates an email message and sends it to the ADMIN_EMAIL constant defined in GenericLibrary.vbs. The message body contains the key error information, and the message subject line contains the current computer name, the error code parameter, and the current time and date. The subroutine uses the GetCurrentComputerName function to obtain the current computer name and uses VBScript's Now function to obtain the current time and date. The subject looks similar to the following example:
The RaiseError subroutine also sends an email message to report an error, but the message contains no body text because there's no Err text to include. The subject line is similar to the sample above, except that it uses the word Raised rather than Checked.
Be aware that if a serious error goes uncorrected, the script continues to mail one of these messages every 5 minutes. As a result, an error condition can cause the script to flood the administrator's mailbox.
Identifying the appropriate update. Now that the script has ascertained the appropriate update number and checked for errors, it searches the central script repository for the appropriate update. You want the script to execute as many updates that apply to the client as are available on the central script repository, so a Do While...Loop checks whether an update exists. If an update doesn't exist, the client is up-to-date and script processing continues to the next section.
Even if an update exists, it might not apply to this client. To determine whether an update applies to the client, the script checks for a companion text file similar to Web Listing 3. Remember that this text file contains the AD paths to the AD groups that this client belongs to. A Boolean variable called bolDoUpdate indicates whether to run this update. First, the script determines whether the text file exists. If the text file doesn't exist, the update applies to all clients and bolDoUpdate is set to TRUE so that the script knows to execute the update. If the text file exists, the script passes the text file path to the IsUpdateToBeRunOnThisPC function and places the result in the bolDoUpdate variable.
Determining whether the client needs the update. The script's IsUpdateToBeOnThisPC function is simple and takes only the text file path as input. The function calls the GetPCGroupMembership function in GenericLibrary.vbs to retrieve all the AD groups that this PC is a member of. If this call returns a FALSE value, the PC isn't a member of any AD groups and can't be a member of any group in the input text file. Thus, the function returns a FALSE value to bolDoUpdate in the main script.
Alternatively, if the GetPCGroupMembership call returns a value of TRUE, the IsUpdateToBeOnThisPC function returns an array of the AD groups the PC belongs to into an array variable called arrGroups. The function opens the input text file and starts a loop that iterates through every line in the file. For each line, the function uses a For Each...Next loop to go through the arrGroups array, checking whether any array elements match that specific line of the input text file. If no match occurs, the loop finishes and the function returns a FALSE value to bolDoUpdate in the main script. However, if a match occurs, the PC is a member of an AD group that's listed in the input text file. In this case, the function returns a TRUE value to bolDoUpdate in the main script and the script exits the function immediately.
Running the update on the client. If bolDoUpdate is FALSE, the script doesn't need to apply the update to the client. In this instance, the script deems not applying an update as a successful processing of the update. As a result, the script needs to increment the update counter (i.e., intUpdNum) by one and update the strUpdGroupsFilePath and strUpdFilePath variables that use this new number. The script then immediately processes the Do While...Loop again in search of the next update.
If bolDoUpdate is TRUE, the script uses the Shell::Run method, in the same way as it did in the first article of this series, to attempt to run the update and wait until processing has finished. If the intResult variable is 0, the update succeeded. This result means that the script needs to increment the update counter (i.e., intUpdNum) by 1 and update the strUpdGroupsFilePath and strUpdFilePath variables that use this new number. The script then processes the Do While...Loop again in search of the next update.
However, if the intResult variable isn't 0, an error occurred during the update. In that case, the script uses RaiseError (error code 4 this time) to ensure that the script notifies the administrator of the problem. An Exit Do statement then makes the script drop out of the Do While...Loop. The script can't just end because it might have executed any number of updates successfully up to this point and only now experienced a failure. If it quits now, the script won't record the value of the last successful update in CUSClientLastUpdateNumber.txt and the client will be in an unmanageable state. Therefore, the script doesn't quit but instead drops out of the loop and continues to the final section of the script.
Incrementing the update counter. The final major section of CUSClientUpdater.wsf updates the text file with the last successful update. When the script has gone through all available updates (i.e., checks for the next update and finds that it's missing or fails to process an existing update), processing of updates stops and the script opens CUSClientLastUpdateNumber.txt and writes the last successful update to the file or creates the text file if it doesn't exist. The script uses intUpd to test for the existence of the next update; as a result, the script must write intUpdNum-1 to the file because this was the last successful update. Note that if intUpdNum is still set to 1, either no update 1 exists on the central script repository or the script has yet to complete update 1 successfully. Therefore, the script must delete any CUSClientLastUpdateNumber.txt file to ensure that the script attempts to apply update 1 next time the script runs. And that's it. Even if the script did use RaiseError error code 4 earlier, the script still needs to complete this section to ensure that it writes the correct update number to the text file. Please be aware that if the loop continues and this update continues to fail, the script will continue to send error messages to the administrator.
You've now built scripts in the central script repository and run them from the client by using a task scheduler job. Next month, I'll continue to expand the corporate update system by looking at how to deploy key client files to the client.