Networking

SolutionBase: Improve your VBScript programming skills with these tips

If you don't do much programming as a part of your administration duties, you may not know the best way to organize and execute scripts for your servers. Here are some tips to help make VBScript applications better.

System administrators are continuing to expand their use of scripting. Although you may not come from a programming background, understanding basic programming makes a growing scripting infrastructure a lot easier to manage and optimize. I won't start at the very beginning with tips such as proper indentation for script readability and the use of comments to document your work. Instead, I'll share some tricks that have made a VBScript environment much more manageable.

Designing "black boxes" for commonly used functions

Getting the syntax just right on some of those trickier programming functions sometimes takes a bit of research. Once you've figured out how to get the results you want, why not turn the code into a "black box" that simply accepts one or more parameters and provides the appropriate result rather than reuse the same complicated code every time? For example, here's a bit of code that returns the serial number of a remote computer called ComputerA using a WMI connection over the network:

strComputer = "ComputerA"
Set ConnectToWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer )
Set rowSN = ConnectToWMI.InstancesOf("Win32_BIOS")
For Each SN in rowSN
ï¿?ï¿?ï¿?ï¿?ï¿? SerialNumber = SN.SerialNumber
Next
WScript.Echo SerialNumber
 

The following is an example of turning this code into a black box. The function accepts the computer name as a parameter and returns the appropriate serial number:

strComputer = "ComputerA"
WScript.Echo GetSNwithComputerName(strComputer)
Function GetSNwithComputerName(ComputerName)
ï¿?ï¿?ï¿?ï¿?ï¿? Set ConnectToWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & ï¿?ï¿?ï¿?ï¿?ï¿? ComputerName)
ï¿?ï¿?ï¿?ï¿?ï¿? Set rowSN = ConnectToWMI.InstancesOf("Win32_BIOS")
ï¿?ï¿?ï¿?ï¿?ï¿? For Each SN in rowSN
ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿? GetSNwithComputerName = SN.SerialNumber
ï¿?ï¿?ï¿?ï¿?ï¿? Next
End Function
 

That's already much better. Now, instead of having to run through the entire code every time, I can simply use the expression GetSNwithComputerName(ComputerName) to obtain the serial number. This code could stand one more improvement, though.

For performance reasons, be careful not to simply dump everything into a single function. That can easily backfire since there are probably certain components that won't benefit from being run with every call to this function. For instance, in this particular case, we'll need to negotiate the required WMI connection with the remote computer every time the function is called in order to get at the serial number. Perhaps we should make a function out of establishing the WMI connection itself so that it doesn't get repeated needlessly:

strComputer = "ComputerA"
Set ActiveWMIConnection = ConnectToWMI(strComputer)
WScript.Echo GetSNwithComputerName(ActiveWMIConnection)
Function ConnectToWMI(ComputerName)
ï¿?ï¿?ï¿?ï¿?ï¿? set ConnectToWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & ï¿?ï¿?ï¿?ï¿?ï¿? ComputerName)
End Function

Function GetSNwithComputerName(WMIConnection)
ï¿?ï¿?ï¿?ï¿?ï¿? Set rowSN = ConnectToWMI.InstancesOf("Win32_BIOS")
ï¿?ï¿?ï¿?ï¿?ï¿? For Each SN in rowSN
ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿?ï¿? GetSNwithComputerName = SN.SerialNumber
ï¿?ï¿?ï¿?ï¿?ï¿? Next
End Function
 

This way, even if we design additional functions that retrieve other types of information from the same computer, the mandatory WMI connection will need to be established only once.

You could argue that rather than using the function again and depending on the connection every time (as in the second code sample), the value could simply be assigned to a variable and used across the script. This is also a viable option.

Once you've accumulated several of these functions, the specific syntax won't matter anymore, since you'll need to provide the function only with the parameters it needs, and it will happily retrieve the serial number or whatever information you may require. There are three major advantages to this approach:

  • Since you assign function names, your code will suddenly look a lot more like English than it used to, making it much easier to understand and modify in the future. For example, a naming convention can easily be adopted to help categorize functions.
  • As a result of this, if you're in the habit of mapping out your programming ideas in "pseudo code" (i.e., writing out your ideas in plain English using what looks like the structure of a program), they will be a lot easier to translate since the code will already be much closer to English.
  • Debugging can also become much easier since this basically amounts to a divide-and-conquer strategy. Individual bits of code are simpler to deal with and can make the problem easier to find.

Store commonly used functions and subroutines in a public function library

As an extension of the black box concept, you may want to consider designing a central repository for all of these functions, much like "include" files in C++ or even DLLs. This approach will allow you to address only a single instance of any given function rather than having to reproduce it in every script that requires it. Furthermore, any modifications made to this single instance will be reflected across any script that calls it, eliminating the need to edit each individual script.

The following snippet of code consists of two components. The first checks whether the library file exists. (Because the script uses functions located in this file, it will not execute properly if it can't gain access to the library; therefore, the script shouldn't run at all if the library is unavailable.). The second part actually reads the entire file and stores the functions in memory the same way that any functions declared in the current script are made available:

LibraryFile = "\\ServerA\ScriptShare\Library.vbs"
Set fs = WScript.CreateObject("Scripting.FileSystemObject")
if fs.FileExists(LibraryFile) then
Set objTextStream = CreateObject("Scripting.FileSystemObject").OpenTextFile(LibraryFile, 1)
Execute (objTextStream.ReadAll)
objTextStream.Close
Else
WScript.Echo "Cannot run script. Library file missing."
End If
 

Again, it's not always a good idea to put all your eggs into one basket. For example, you could create a few types of library files for function types that could be grouped together by category. But I've honestly never found it necessary to do so for performance concerns.

Let's use our first example to implement this method:

Main.vbs LibraryFile = "\\ServerA\ScriptShare\Library.vbs"
Set fs = WScript.CreateObject("Scripting.FileSystemObject")
if fs.FileExists(LibraryFile) then
Set objTextStream = CreateObject("Scripting.FileSystemObject").OpenTextFile(LibraryFile, 1)
Execute (objTextStream.ReadAll)
objTextStream.Close
strComputer = "ComputerA"
Set ActiveWMIConnection = ConnectToWMI(strComputer)
WScript.Echo GetSNwithComputerName(ActiveWMIConnection)
Else
WScript.Echo ï¿?Cannot run script. Library file missing.ï¿?
End If
 Library.vbs Function ConnectToWMI(ComputerName)
set ConnectToWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" &
ComputerName)
End Function

Function GetSNwithComputerName(WMIConnection)
Set rowSN = ConnectToWMI.InstancesOf(ï¿?Win32_BIOSï¿?)
For Each SN in rowSN
GetSNwithComputerName = SN.SerialNumber
Next
End Function

Use global variables for key values

In the previous example, you may have noticed that rather than hard-coding the path to the library file every time I needed it, I simply assigned it to a variable that was used throughout the rest of the script. This can definitely be considered a best practice since things such as server names and file shares have a tendency to change over time. Should that ever happen, the only value that will need to be modified to ensure continued functionality is that of the original variable assignment. Any such values could also be strategically placed in the same area for every script (usually somewhere near the top) to make them easier to find.

Use a ping command exit code to check for the presence of a computer

Some scripts attempt to access remote computers over the network for information. This type of script is among the most useful, and quite a few of my personal favorites will attempt to sequentially connect to more than one computer. The downside with trying to establish a remote WMI connection, especially to multiple computers, is that if the target computer is currently offline for any reason, the WMI connection attempt will take sometimes up to a minute to time out. Therefore, it's a good idea to attempt to check for the computer's presence on the network before trying anything else. Take a look at the following morsel of code:

strComputer = ï¿?ComputerAï¿?
Set ws = WScript.CreateObject(ï¿?WScript.Shellï¿?)
If ws.run("ping -n 1 " & strComputer,0,"TRUE") Then
ï¿?
 

The ï¿?Ifï¿? command will evaluate the results of the ping command submitted to the command shell on the same line. If the computer replies, the expression returns 1 (or TRUE). If not, it returns 0 (or FALSE). Therefore, if you obtain a reply, you can be fairly certain that the target computer will respond to your requests. However, there are a few other conditions you'll want to respect before you trust your life to this technique:

  • Make sure the computer is DNS-reachable. If the computer uses DHCP to obtain an address, odds are that the DNS is updated automatically. If the computer uses a static address, make sure it's properly registered in the DNS. In any case, the bottom line is that the computer running the script needs to be able to resolve the host name using standard DNS services.
  • Personal firewall software may block ping replies. If the target computer uses any form of software firewall, it might not respond to ping requests even though it's present on the network and will respond to other types of requests.
  • High network latency can sometimes result in random ping requests timing out, which may also cause you to think that the computer isn't available.
  • As for properly configured local policies, the computer still needs to be configured to accept the type of connection you're requesting even though it responds to a ping. Ping requests are anonymous and check only for a pulse. Any kind of access to file or WMI services, for example, will require the user running the script to have the appropriate privileges on the target computer.

Editor's Picks