Chapter 13. Using Scripts for Consistent Administration

The Microsoft Windows scripting infrastructure has grown dramatically since the late 1990s, and available documentation, administrator knowledge, and the number of books dedicated to the subject have grown along with it. Given the breadth of documentation available, it might be argued that a one-chapter discussion of how to script is superfluous, and we agree—we have no intention here to teach you how to script. But scripting is fundamentally important to Windows administration, and our goal is to show you ways it can simplify and improve the quality of your system administration by allowing you to implement customized, highly automated solutions to many problems. Furthermore, we want to distinguish the ways that server scripting is different from, and has different needs than, the general run of Windows scripting.

We’ll start by showing how some scripting elements fit together. We’ll introduce the elements of the Windows Server 2003 scripting infrastructure in general terms and point out significant enhancements—notably Perl and Services for UNIX (or Subsystem for UNIX Applications in Windows Server 2003 R2)—to the base installation of Windows Server 2003 that add useful functionality and extend your options. The heart of our discussion is core conceptual practices for writing and using server scripts. Although most of our focus is on the core command shell and WSH scripting environments, many of the key concepts and scripting examples also apply to using Perl and UNIX-derived commands.

People often speak of scripting without defining it explicitly. Because it is used in so many contexts and usually includes some concept of intent, it is difficult to provide a technically precise definition that is always true. For practical purposes, we define scripting as a "gluing" activity: although superficially similar to programming, it focuses on gluing together pre-existing tools to solve problems rather than building solutions from the ground up.

Scripting on Windows Server 2003

Windows Server 2003 scripting is not a monolithic structure; it consists of several elements that have converged, usually including additions that didn’t come with Windows. To make it easier for you to understand how it all fits together, we’ll look at the infrastructure and some additions separately and then cover the extensions specific to Windows Server 2003.

Windows Server 2003 Scripting Infrastructure

The Windows Server 2003 scripting infrastructure, as we view it and use it, has three major elements that provide progressively deeper reach into the operating system:

  • Command shell

  • Active Scripting

  • Active-Scripting-accessible Component Object Model (COM) interfaces, including Active Directory Services Interface (ADSI) and Windows Management Instrumentation (WMI)

Command Shell

The familiar Windows command shell (Cmd.exe) is a distinct shell from the Windows Explorer interface. While it is still sometimes called the "DOS" prompt, it has been changed and improved well beyond its original DOS roots. In addition to having a significantly improved internal command set, it even supports primitive looping and error detection and can invoke any console-mode application available. While not as cute or obviously full featured as Windows Explorer, the command shell provides critical interactive task composition abilities missing from the GUI. You can rapidly recall and repeat commands, and you can create new tools by chaining old tools together instantly using its internal pipelining system.

To see a list of internal commands supported by Cmd.exe, open a command shell window and type help. External command-line tools are not shown in this listing but can typically be investigated using standard command-line help switches; although the help switches vary, most console applications on Windows use /?, /h, or -? to invoke help.

Note

For a (nearly) complete list of command-line tools in Windows Server 2003, open Help and Support, Quick Guide for Finding Tools, Command-Line Reference A-Z.

In contrast, although windowed applications are easy to invoke by clicking and often have internal task-specific time-saving conveniences, there is no way to record individual command sequences and repeat them. Nor is there a way to chain commands together in an immediate, ad-hoc fashion. You can mimic this with some third-party applications, but there is no easy or standard method for combining multiple graphical applications or actions into a single, repeatable, sequence of steps—which is something that the command shell and batch files make simple and easy. The command shell can use any tool that will provide console output, including graphical applications.

Active Scripting

Active Scripting is the Microsoft COM-based open scripting interface technology. It enables users to mix scriptable applications and different scripting languages. Microsoft includes scripting engines for two Active Scripting languages with Windows Server 2003: VBScript and JScript. Windows Server 2003 also includes several host applications, including the Windows Scripting Host (WSH). WSH has both a console host, Cscript.exe, and a GUI mode host, Wscript.exe. WSH provides a core set of facilities to support application-like use, including input/output handling, argument parsing, and an optional help system.

COM Interfaces

Windows Script Host uses the Component Object Model, or COM, for real work. COM, from a scripting perspective, is a standard for software component interfaces. If a component implements the proper COM interfaces, scripts can talk to it without worrying about whether the component is a compiled library or application, a service, or another script. A typical Windows Server install has hundreds of scriptable components. Two technologies with COM interfaces are particularly critical. The Active Directory Services Interface (ADSI) acts as a scripting gateway to directory services. Windows Management Instrumentation (WMI) provides a standard model for accessing fine-grained information about system elements and, in many cases, lets you manipulate those elements as well.

Extending the Infrastructure

Many administrators find the built-in scripting infrastructure in Windows Server 2003 to be sufficient for their needs, and it certainly has the ability to accomplish many tasks without any enhancement. But one of its biggest strengths is that it can be extended and enhanced. The two scripting infrastructure elements that offer the most scope for enhancement are the command shell itself and the supported scripting languages.

Even though the Cmd.exe in Windows Server 2003 is a significant improvement over the MS-DOS-compatible command.com interpreter, it still is severely limited compared to the shells that a UNIX or Linux system administrator takes for granted. It’s also missing the rich set of console applications that UNIX provides. Windows Services for UNIX (SFU) includes the Interix subsystem and the UNIX Korn and C shells, in addition to many standard UNIX tools that are available from the command line. Windows Server 2003 R2 integrates and enhances this with the Subsystem for UNIX Applications (SUA), which supports both 32-bit and 64-bit Windows Server 2003. SFU is available for Windows 2000, Windows Server 2003, and Windows XP Professional as a free download from http://www.microsoft.com/windowsserversystem/sfu/default.mspx.

More Info

SUA and the interoperability tools built into Windows Server 2003 are covered in detail in Chapter 27.

The Active Scripting tools are pluggable, and you can install and run several other scripting engines for WSH. You can also install additional scripting hosts that happily run alongside WSH. The first addition you should probably install is Perl. Perl is included with Services for UNIX or available directly from http://www.activestate.com. For many years, Perl has been the UNIX administrator’s scripting language of choice and has been widely used by Windows administrators as well. Installing Perl gives you immediate access to a wealth of scripts and Perl packages.

Perl is not the only additional choice to enhance Windows Server scripting. While they might not have quite as many already tested and readily available scripts available, several other useful scripting engines are available that plug into Active Scripting, including those for Python, KiXtart, IBM Rexx, and Tcl. Alternative shells are also available, from the Cmd-like shells (such as JPSoft 4NT and TakeCommand) to classic UNIX-style shells (such as Cygwin). Table 13-1 lists some alternative shells and where they can be obtained. Many don’t require commercial license purchases, and even those that do are reasonably priced for a small number of servers if you consider the savings in administrative time. If you’re familiar with a scripting system that is sufficiently stable and secure for server use, you should consider adding it; familiarity is the primary factor in making it easy to automate administration.

Table 13-1. Some commonly used scripting systems or enhancements

What’s New in Windows Server 2003 Scripting

Although the conceptual architecture of Windows Server 2003 scripting has not changed from earlier versions of Windows, the details have. The command shell, scripting host, and COM-related systems have all had changes made to them. Documenting all the possibilities would require an entire book, so we’ll simply provide an overview and point you to appropriate sources of additional information. Following are the major areas of change:

  • The command shell—specifically, Cmd.exe and console-based tools

  • Windows Scripting Host (WSH)

  • Distributed COM (DCOM)

  • ADSI and WMI

On the CD

On the CD

The CD contains reference information regarding the command-line tools and changes in Windows Server 2003. "Windows Server 2003 Console Tools" provides summary information on the new applications, and "SFU’s Win32 Console Tools" also provides information on the command shell tools you’ll have available if you install SFU (or are running Windows Server 2003 R2).

The Windows Command Shell—Cmd.exe

The Windows Server 2003 command shell has additions, changes, and usage details documented in its help file. You can access these by typing %WINDIR%Help tcmds.chm at a command prompt. There are 64 documented new external command-line tools. These include several WSH scripts focused on IIS and printer management, a large set of Active Directory tools, and miscellaneous other tools, including sc (services); tasklist and taskkill (process management); and takeown, inuse, and openfiles (file management). Help for these and other commands is available by invoking the command with its help switch, usually /?. Some new tools use the POSIX-style -? for their help switch instead.

Windows Scripting Host

Windows Server 2003 uses version 5.6 of WSH, VBScript, and JScript. This version has been available since 2001 for all supported operating systems, and WSH and the scripting engines are part of the updateable core feature set of Windows, so it technically has no new unique features. WSH scripts have been affected, however, by significant changes in default Distributed COM (DCOM) security.

Distributed COM

All remote COM access, including ADSI and WMI, is affected by DCOM’s increased authentication security and the presence of firewalls. The default security level used for authenticating clients has changed from connection-level to packet-level. Because the authentication level is negotiated, the changes don’t affect tools unless one side insists on a lower security level than the other system will accept. Client-side firewalls affect asynchronous connections because they require a separate connection and an exclusion for the specific remote host or the ports used.

ADSI and WMI

There are roughly 350 new classes and 4000 distinct new properties and methods in WMI’s root/cimv2 namespace on a default Windows Server 2003 domain controller. Unfortunately, there is no system-based help on these new elements. We’ve included Scriptomatic on the CD, which will allow you to browse through classes and properties on a Windows Server 2003 system as a useful starting point. You can search class names on MSDN at http://msdn.microsoft.com for specifics of the class or search the TechNet Script Center at http://www.microsoft.com/technet/scriptcenter/default.mspx. The TechNet Script Center will usually provide code samples as well.

On the CD

On the CD

Look for the Scriptomatic tool on the CD in the Tools folder.

Scripting Practices

Writing and using scripts requires a clear understanding by both the script writer and the script user of the intended use and the designed use. A script that will be scheduled to run automatically, for example, can’t have any GUI elements in it because it will fail if it is run when no one is logged in. If you’re running a script interactively from the console, it will fail if you don’t provide the correct input and syntax to the script.

Think from the Command Prompt

The most important step you can take to improve your Windows automation work is to think in terms of the command prompt. Good command-prompt tools can use the command prompt’s special abilities such as pipelining and batching. They can run correctly from the Task Scheduler. They work properly when run without a graphical session. And they function properly when they are run unattended. By focusing on these requirements, you’ll create good scripts that can be used in a broad range of conditions.

Write WSH Scripts as Console Tools

Making WSH scripts act as true command-line tools that can write to both the standard output and standard error streams and even read an input pipeline is a commitment that involves both script writing and use, but the effort pays off immediately in both cases.

More Info

See the Input and Output Handling section for more on the standard streams.

Writing WSH scripts as console tools means designing them to run totally automated with no user intervention after invocation. This means users need to be able to completely configure the tool and supply data to it from the command line. The most convenient way to do this is to use general defaults for tool configuration and implement named arguments for them. If you need to accommodate a large quantity of data, you can read it from standard input (WScript.StdIn.ReadLine, for example).

Although WSH allows you to set the default script host to the console-mode cscript host, it still breaks console input pipes during the host selection. For example, suppose you use a script like this simple VBScript Len function wrapper that tells us the length of lines:

' Len.vbs
If WScript.Arguments.UnNamed.Count > 0 Then
    For Each uarg in WScript.Arguments.UnNamed
        WScript.StdOut.WriteLine Len(WScript.StdIn.ReadLine)
    Next
Else
    Do While Not WScript.StdIn.AtEndofStream
        WScript.StdOut.WriteLine Len(WScript.StdIn.ReadLine)
   Loop
End If

You first set the default WSH host to cscript by entering cscript //H:cscript //S at a command prompt, and then you perform a quick test using ipconfig | len. This will immediately return the console error len.vbs(7, 5) (null)—which indicates that the handle is invalid.

One solution is to invoke cscript explicitly with the script as an argument—a cumbersome and not terribly practical solution, especially if you need to chain multiple scripts together. Not only do you have to add cscript to each command line you type, but you also need an explicit path to the script file. A better solution is to use wrapper scripts that invoke the script properly. For the len.vbs script, we create a command shell script len.cmd with the following single line in it:

@cscript "len.vbs" %* || @exit

Important

For this to work properly, the command shell script must be in the same folder as your WSH script, and as it is by default, .cmd must precede .vbs in your %PATHEXT% shell variable.

The following script can be used to automatically create a shell wrapper for any WSH scripts you create:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<job>
<runtime>
<description><![CDATA[
Name: New-ConsoleWrapper.wsf

DESCRIPTION
Given one or more paths to WSH scripts as arguments or as standard input data,
this script will generate a wrapper CMD script in the same directory to allow
direct execution of the script as a pipelineable script tool and shut down the
pipeline if the script is terminated.

LIMITATIONS
+ This script should always be executed with cscript as the host.
+ If the %PATHEXT% environment variable has been modified so that the CMD
extension comes after the WSH script type you wish to wrap, wrapper scripts will
not work.
]]></description>
<example><![CDATA[
USAGE SCENARIO
To create wrapper scripts in the current directory and all directories underneath
 it for all files with the .wsf or .vbs extensions, use this:
    dir /s /b *.wsf *.vbs | cscript New-ConsoleWrapper.wsf
Drag-and-Drop:
Do NOT drag and drop onto this script. Drag and drop onto this script's wrapper
script (New-ConsoleWrapper.cmd or New-ConsoleWrapper.bat).
]]></example>

<named name="?" helpstring="returns this help message" type="simple"
required="false"/>
<named name="A"
helpstring="Always create wrapper script, whether or not the specified script
exists" type="string" required="false"/>
<named name="Force" helpstring="Overwrite pre-
existing wrapper script files. If not specified, prior batch scripts with the
same name will not be overwritten." type="simple" required="false"/>
<named name="B"
helpstring="Generates wrappers with a .bat extension instead of the default.cmd
extension." type="simple" required="false"/>
<named name="T"
helpstring="Turns on tracing.  This causes various extra information about the
process to be echoed to stderr." type="simple" required="false"/>
<unnamed name="scriptpath"
helpstring="one or more scripts to create wrapper scripts for." many="true"
required="false"/>

</runtime>

<object id="fso" progid="Scripting.FileSystemObject"/>
<reference object="Scripting.FileSystemObject"/>
<object id="sh" progid="Wscript.Shell"/>

<script language="VBScript"><![CDATA[
Option Explicit
Dim StdIn, StdOut, StdErr, UARGS(), Named, UnNamed, TRACING
set Named = WScript.Arguments.Named
Set UnNamed = WScript.Arguments.UnNamed

' declare and initialize some data we may change based on named args.
dim ALWAYS_CREATE, OVERWRITE, EXT
ALWAYS_CREATE = false: OVERWRITE = false: EXT = ".cmd"

Script_Initialize true
ProcessArguments
dim arg: for each arg in UARGS
    TraceLine "script to wrap from argument list: " & arg
    CreateWrapper arg
next
If UBound(UARGS) < 0 then
    TraceLine "No unnamed args found; iterating over the input stream"
    ' if we didn't have any scripts specified on the command line,
    ' read them from StdIn
    do while not StdIn.AtEndOfStream
        CreateWrapper StdIn.ReadLine
    loop
end if


Sub CreateWrapper(byval scriptname)
    if TRACING Then StdErr.WriteLine "wrapping " & scriptname
    if not(FSO.FileExists(scriptname)) or ALWAYS_CREATE then
        StdErr.WriteLine "script not found: " & scriptname
        exit sub
    end if
    dim WrapperPath
    WrapperPath = StemNameFromFileSpec(scriptname) & EXT
    dim WrapperCmd: WrapperCmd = "@cscript ""%~dpn0." _
        & FSO.GetExtensionName(scriptname) & """ %*"
    If FSO.FileExists(WrapperPath) and not(OVERWRITE) then
      StdErr.WriteLine "wrapper script found, not overwritten: " _
       & WrapperPath
        exit sub
    end if
    dim file: set file = FSO.OpenTextFile(WrapperPath, ForWriting, True)
file.Write WrapperCmd
    file.Close
    StdOut.WriteLine "wrapper script created: " & WrapperPath
end sub


Function StemNameFromFileSpec(byval filespec)
    ' given a filespec as an argument, turns it into a "stem" -
    ' full path to parent directory + the base name.
    StemNameFromFileSpec = FSO.BuildPath( _
        FSO.GetParentFolderName(filespec), _
          FSO.GetBaseName(filespec) )
end function

Sub ProcessArguments()
    ' General argument processor template routine.
    ' Insert other handlers below.
    Dim i

    If Named.Exists("A") Then
       ALWAYS_CREATE = True
    Else
      ALWAYS_CREATE = False
    End If
    If Named.Exists("Y") Then OVERWRITE = True Else OVERWRITE = False
    If Named.Exists("B") Then EXT = ".bat" Else EXT = ".cmd"
    If Named.Exists("T") Then TRACING = True Else TRACING = False
    TraceLine "Tracing script execution now."
    ' get unnamed arguments into an array
    redim UARGS(UnNamed.Length - 1)
    TraceLine "found " & UnNamed.Length & " unnamed arguments."
    for i = 0 to ubound( UARGS )
        UARGS(i) = UnNamed(i)
    next
End sub

Sub Script_Initialize(byval bFailOnGUI)
    ' Set up global StdIn, StdOut, StdErr
    if bFailOnGUI And _
      lcase( right(WScript.FullName, 11) ) <> "cscript.exe" then
      WScript.Echo "Must be run with cscript as the script host"
      WScript.Arguments.ShowUsage: WScript.Quit 1
    end if
    Set StdIn = WScript.StdIn
    Set StdOut = WScript.StdOut
    Set StdErr = WScript.StdErr
End Sub

Sub TraceLine( s )
    If TRACING then WScript.StdErr.WriteLine s
End Sub
]]></script>
</job>

On the CD

On the CD

This script (New-ConsoleWrapper) is on the companion CD.

The command shell wrapper for this script is a one liner:

@cscript "%~dpn0.wsf" %* || @exit

Some comments on the specifics of the command shell script are useful for understanding how to make WSH scripts work as console tools. The leading "@" turns off echoing for that line only. This is simpler than an "echo off" statement in a very short script. The quotes around the file name aren’t always required, but they ensure that script names with spaces in them are correctly invoked.

The "%~dpn0.wsf" is an example of using batch parameters in a script. The "%~" identifies that special batch parameter modifiers will be used to modify the "%0" parameter. "%0" is argument zero, the name of the called script, the "d" expands to the drive (as in "C:"), the "p" expands to the path, and the "n" to the base name of the file (without the extension). The ".wsf" is then appended to this.

The "%*" passes on all command-line arguments without affecting them. The rest of the line is technically a new command. The doubled vertical line characters, ||, mean "if the command fails to run properly" perform the command that follows. The net effect of || @exit is to tell the CMD script to exit if the command fails. The exit statement ensures that the script stops processing data immediately on a failure rather than repeatedly asking you whether you want to terminate the script until the end of the input stream.

Important

Remember, if you don’t use a workaround like the console wrapper, you must explicitly execute WSH scripts with cscript on the command line to make them read standard input properly. Setting the default WSH host to Cscript.exe isn’t sufficient.

Credentials and Scripting

One of the critical security and reliability issues for Windows scripts is credential use. There are two main problem areas:

  • Secure password handling. Where scripts require passwords, how do we manage those passwords and securely transmit them?

  • Session credentials. Administrators should normally operate with limited privileges and then elevate them as required to handle specific tasks. Handling privilege elevation with scripts introduces a new range of issues with both security and troubleshooting.

Use RunAs and Scheduled Tasks

One solution that has some fringe benefits is to not handle authentication within the script, but to use the RunAs command or schtasks alternate credentials arguments instead. By using this approach, you don’t need to write or maintain code for managing the user’s credentials, or worry about specifying the user’s credentials explicitly within the script. Troubleshooting problems is also simplified because it’s clear that everything is happening with permissions derived from the executing account.

Avoid Specifying Passwords on the Command Line

Many tools that handle alternative authentication allow you to specify a password on the command line. This is a bad practice and should be avoided. If someone has permission to enumerate processes, he or she can also read the entire command line of processes running on a server. Further, even momentary access to your console will let a user see the cleartext password as typed on the command line.

Most tools that provide this kind of support will automatically prompt for a password if it isn’t provided or if you enter the password switch with no argument.

Use Obfuscated Password Entry

Windows XP and Windows Server 2003 have a new COM object written specifically to allow hidden password entry. You must be running the script from the command line for this to work, but this new COM object allows you to enter the password on the command line with no visible text, as shown in the following three examples:

VBScript

Dim ScriptPW, pw
Set ScriptPW = CreateObject("ScriptPW.Password")
pw = ScriptPW.GetPassword

JScript

var ScriptPW, pw;
ScriptPW = new ActiveXObject("ScriptPW.Password");
pw = ScriptPW.GetPassword();

Perl

use Win32::OLE;
$ScriptPW = Win32::OLE->CreateObject("ScriptPW.Password");
$pw = $ScriptPW->GetPassword();

Path Management Practices

When you invoke an application by any means other than specifying an explicit qualified name for it, Windows performs a search for the tool. This includes every single tool you invoke in any context.

How Command Discovery Works

When you type a command such as regedit at the command prompt, the command processor (Cmd.exe) first checks for an internal match—this is how names of internal commands such as dir are found. The command processor next looks in the current directory for files with the base name regedit and one of the file extensions listed in the %PATHEXT% command shell variable, in the order they are listed in the variable. If it isn’t found, it looks through each folder listed in the %PATH% variable in the order they are listed in the variable, and in each folder it checks against that base name plus the list of file extensions listed in %PATHEXT%. This process continues until it finds a matching name or it runs out of folders to search.

Making Changes to the Shell Working Directory

Executables always use the shell’s search mechanism for discovery. This applies to WSH scripts launching an external process using WScript.Shell’s Run method, for example, because the external process is just inheriting the shell. Because the local directory will always be searched first for commands, you need to exercise caution when changing your working directory in a script. Any arbitrary path might contain malicious executables with names of commands you are likely to invoke. An equally serious problem is that there are often multiple versions of files on a system, and invoking an external command after a directory change can pick up the wrong version of a file. To avoid this problem, you have two basic solutions:

  • Always use a working directory that has controlled access so that only Administrators can write into it.

  • Use explicit, fully qualified command paths. (This is the preferred solution.)

Use Fully Qualified Command Paths

To prevent accidental executable substitution, you should qualify executable paths rather than using just bare names. At its simplest, qualification can just specify executable type by including the file suffix—for example, you would use Regedit.exe instead of regedit to ensure you don’t find regedit.com instead. For increased control, you can specify a fully qualified path and use well-known environment variables in the path to make the invocation work on nonstandard system configurations. For example, %SYSTEMROOT%system32 egedit.exe will always find regedit.exe, even if Windows is installed in D:WinSafe instead of C:Windows.

Monitor the %PATHEXT% Variable

The pathext variable is also occasionally modified by application installs, and it might not need some elements that it contains by default. For example, encoded JScript (.JSE) and VBScript (.VBE) files are considered executables, but Windows does not have any tools shipped in this form. If you don’t use encoded scripts, you can remove .JSE and .VBE from the %PATHEXT% variable to prevent invoking encoded scripts. Another simple security trick that can shut down some attack vectors is to reverse the order of .EXE and .COM on %PATHEXT%. This will prevent a malicious program from inserting itself higher in the executable order by adding a .COM version of a common .EXE file. As with removing encoded scripts from known executable types, you should check the effects of switching .COM and .EXE files against your supported systems; some application suites use a .COM file as a launch wrapper for their .EXE and might have critical impairments if the .EXE is launched directly.

Add a Tools Directory to Your Path

If you use many command-line tools and want them consistently available from a predictable location, you should put the standard tools you use in a folder that is added to your path. This isn’t an exception to the general practices for path management; rather, it should be a directory modifiable by administrators only and one that follows the core system directories in the path statement. For consistent access across several machines such as a group of servers, you can use Distributed File System (DFS) to synchronize content, or you can create the folder on a single server and then share the folder. Map the folder as a drive letter and then add the map to your logon script to make it consistently available. (We like to store our generally available scripts in a folder we share as "Tools" and have that automatically mapped to the T: drive.) As obsolete as we might wish drive-mapping to be, drive letters are still necessary for many command-line tools that refer to their own location; the command shell needs a drive location, not a Uniform Naming Convention (UNC) path for current directory references.

Input and Output Handling

Reading and presenting data is usually an afterthought in light-duty scripting, an oversight that causes scripts to misbehave or fail when used for batch processing or in noninteractive sessions. There are some simple practices you can apply to make scripts handle I/O properly in the console environment and even account for special output needs such as logging.

WSH: Use Text Streams for Input and Output

Console WSH scripts have three objects automatically available for input and output operations: WScript.StdIn, WScript.StdOut, and WScript.StdErr. These objects correspond to the familiar UNIX streams of STDIN, STDOUT, and STDERR, respectively. These objects are actually text streams, functionally identical to the TextStream object used for file reading and writing in WSH. Use WScript.StdIn to read data you are processing, WScript.StdOut to write new data you create, and WScript.StdErr to emit error or tracing information. These streams will work correctly in all contexts where you use console script invocation. Your script won’t care whether it is reading input from a file or a previous command; you can log output information by redirecting it to a file and still get console output. In the most extreme case of redirection, you’re actually getting to use three files without all the messy coding to open them from the Scripting.FileSystemObject:

cscript myscript.vbs < in.txt 2> err.log 1> out.log

Limit MsgBox Use

The MsgBox function in VBScript supports optional parameters for displaying different buttons, selecting a default, setting a window icon and window modality, and providing a clickable reference to a help file topic. This is the real point behind MsgBox: providing necessary context-sensitive information to a user that needs confirmation and possibly needs to make a decision. If you really need this functionality at multiple points in the script, MsgBox is the function you want and the script would be a poor candidate for full automation anyway.

In any other roles, MsgBox is bad news. If a script is running in batch processing mode with WSH’s //B option, MsgBox will always cause a failure when encountered. MsgBox will cause a script to hang or fail if it’s used when a GUI can’t be presented—such as when running as a scheduled task, in a service account, or remotely via a console session. With arbitrarily large amounts of data coming out of it, MsgBox is even useless for interactive roles; users must click repeatedly to continue processing.

If a MsgBox-using script doesn’t use the return button-click information for anything, you can generally fix it instantly by replacing MsgBox with WScript.StdOut.WriteLine. If the message was customized with other arguments, you can comment those out. If you don’t need the message for your output, just comment out the whole line.

WScript.Echo: Flexible but Suppressed in Batch Mode

If you want a script to write to your console window when run from Cscript.exe and pop up a box when run from Wscript.exe, and if you don’t care whether output is displayed when using scripts in batch mode, WSH’s native Echo method allows this. The WScript.Echo statement will pop up a message window if run from the GUI WSH host Wscript.exe, and it will write output to the standard output stream if run from the console WSH host Cscript.exe. If WSH is run in batch mode, WScript.Echo output is always suppressed no matter what host you are using; this makes it a bad choice for output redirection from a noninteractive script.

WScript.Echo statements are usually straightforward to convert to WScript.StdOut.WriteLine statements, but Echo has a special feature that the StdOut WriteLine doesn’t: Echo will take multiple comma-separated arguments and output them with an automatic space. To adapt WScript.Echo statements for use with WScript.StdOut.WriteLine, you need to replace each argument-separating comma with & " " & in VBScript or + " " + in JScript.

Use the WScript.Shell LogEvent for Critical Information Logging

Scripts that run periodically on a schedule or that make significant changes that might not be otherwise recorded explicitly are candidates for logging. Too much detail can be useless, but a single event containing a critical piece of information can be useful at each standard exit point from these key scripts; at the least, you will know when the script was run and where. LogEvent always writes to the application log; its first argument is the event type. Use 0 for an information event, 2 for a warning, and 1 for an actual error. You can use the following generic code for almost any script to track normal exit events by placing this at the end of the script code or before any explicit WScript.Quit statements:

VBScript:
Set WshShell = CreateObject("WScript.Shell")
WshShell.LogEvent 0, "script exit: " & WScript.ScriptFullName
JScript:
new ActiveXObject("WScript.Shell").LogEvent(0,
    "script exit: " + WScript.ScriptFullName);
Perl:
use Win32::OLE;
Win32::OLE->CreateObject("WScript.Shell")->
    LogEvent(0,  "script exit: " . Win32::GetFullPathName( $0 ));

Use Good Error Management

Error-handling standards for desktop scripts are generally more lax than for enterprise scripts, but all scripts benefit from good error handling except those quick "one off" scripts that you never intend for anyone but yourself to see.

Use VBScript’s On Error Resume Next Carefully

With VBScript in particular, it is common to see the statement On Error Resume Next at the start of scripts, which effectively tells WSH to ignore all runtime errors regardless of the consequences. This is a bad practice and lazy coding, and it can cause completely unpredictable consequences.

If you expect your script to encounter error events, handle them if at all possible. And if it will be normal to encounter reasonable errors in particular sections of the script, you should turn on error suppression immediately before it is needed, and then continue the current task only if errors are known and can clearly be handled. Whatever you do, you should turn error suppression back off immediately.

Console Scripts Should Handle Most Errors as Normal Events

GUI-focused or single-run scripts typically treat errors that prevent core operation as critical errors. Console scripts designed for batch use might not work well with this model. For example, a script for changing an account password on a specific computer needs to connect to the computer, authenticate, get an account reference, and then set the new password. Any error in this process means the password change failed, so there’s no point in continuing. However, if we’re handling dozens of password updates at once, we don’t want to halt the whole chain of events for reasonable errors such as specific machines being unavailable or the particular user not being present on one of the machines. The following simplified code for a password update script demonstrates this approach. We suppress errors just long enough to try connecting and get an error result. If there is no error, we set the password. If we get a normal error—such as insufficient permissions, no machine available, or the account doesn’t exist—we write a message to the error stream. If we get any other error whatsoever, it represents an unknown situation, possibly even a major problem, and we actually quit after writing an error.

Function ChangePassword (computer, name, newpass)
    On Error Resume Next
    Dim user: Set user = GetObject("WinNT://" & computer _
        & "/" & name & ",user")
    Dim result: result = Err.Number
    On Error Goto 0
    Select Case result
        Case 0
            user.SetPassword(newpass): user.SetInfo
        Case &H46, &H80070035, &H800708AD
            WScript.StdErr.WriteLine "Failure: " & computer
        Case Else
            WScript.StdErr.WriteLine computer_
                & " FAILURE Unknown error returned: 0x" & Hex(result)
            WScript.Quit 999
    End Select
    ChangePassword = result
End Function

Return Error Information to the Shell

If you choose to make a script terminate as a result of a critical failure, give it a non-zero exit code. In the case of WSH scripts, it’s a good idea to make the exit code something other than 1. Some invoking applications will handle more process shutdown details for you if they see a nonzero exit code. You should avoid using an exit code of 1 for a WSH script because WSH returns an error of 1 if it chooses to shut down on its own. One option is to pass along the script error, which we could have done by using the WScript.Quit result. Because high-numeric codes such as ADSI errors might not be rendered correctly, we used 999 as a generic unknown error code.

Log Errors in Noninteractive Scripts

When a script is running noninteractively, you won’t see what problems it encountered unless you take explicit steps to log the errors. If you want to log errors when a script is run noninteractively but just let them go to the screen from an interactive session, you can test the interactive status of your script. The WScript.Interactive property is false if the script is running noninteractively, so you can then use LogEvent as detailed in the Input and Output Handling section to log the error.

WMI Scripting Issues

The learning curve to get started with WMI scripting is fairly steep, but once you learn to use WMI, you’ll find it a very useful tool. A few practices can help with understanding it and preventing or resolving errors.

Use Scriptomatic to Explore WMI

WMI’s endless classes and properties change in every release of Windows; it’s impossible to document them in detail without creating an entire reference work. Fortunately, you can explore them directly using Scriptomatic from the Scripting Center. Version 2.0 of Scriptomatic is available at: http://www.microsoft.com/technet/scriptcenter/tools/scripto2.mspx and on the CD that accompanies this book. Scriptomatic gives you a complete list of all WMI classes available on a specific computer and can automatically generate scripts to sample output data.

Use WMIC for Interactive Exploration and One-Shot WMI Calls

One of the reasons WMI has a bad reputation is the sheer mass of code required for the simplest invocations. A better alternative for immediate use is the wmic command-line tool. WMIC provides many conveniences, including short aliases that help with the lengthy WMI calls, but at first glance it might appear difficult to use. If you’re used to script-based WMI calls and already know a specific class and property you want information from, you can get what you want with a command of the form wmic path <class> get <property>. For example, wmic path win32_operatingsystem get buildnumber would return the BuildNumber property exposed by the WMI Win32_OperatingSystem class.

Note

Even on a Windows Server 2003 x64 Edition computer, you still use "Win32_operatingsystem" as the class. In fact, all the familiar WMI classes in root/cimv2 that begin with Win32_ are still exactly the same in x64 Edition. That might make no sense, but that’s how it is.

Avoid Authentication and Impersonation Settings

Many WMI sample scripts explicitly set authentication and impersonation levels. In scripting examples, this is typically used following the winmgmts: moniker in curly brackets {}. For example, a call that sets both of these levels might look like this:

Set wmi = GetObject("winmgmts:{impersonationLevel=impersonate," _
    & "authenticationLevel=pktPrivacy}!root/cimv2")

Although you might occasionally want to change these security settings, if you don’t have a specific reason for doing so, it’s usually best to eliminate them. This allows WMI to use the systemwide default impersonation level settings and negotiate the authentication level with the remote system. If you specify levels too low (as some legacy scripts do), WMI scripts might fail. This also makes WMI calls much more compact, because the line of script just shown reduces to

Set wmi = GetObject("winmgmts:!root/cimv2")

Translating Script Languages

All good scripters learn early on that there are many excellent examples of good scripts to use as the basis for their own scripting needs. It’s almost never necessary to start completely from scratch—someone has usually had to do something similar before, and scripters have traditionally made many of their scripts available for others to read and use. Unfortunately, if the script you want to borrow from isn’t written in your preferred language, you’re going to have to do some translation. And that isn’t very simple.

There are good sample scripts in just about any language you might choose to use, but there are certainly more of them in VBScript than in any other. So, if your preferred language isn’t VBScript, sooner or later you’ll probably have to translate a script or section of script from VBScript to your preferred language. The most common need is when administrators are researching COM, WMI, or ADSI properties. The documentation on these properties is frequently arcane, but there are usually code samples available somewhere—and they are almost always written in VBScript. The result is usually an impromptu attempt to translate a couple of lines of working VBScript into JScript or Perl. Because VBScript syntax has several historic peculiarities, this can be almost as difficult as starting from scratch. While we can’t tell you everything you need to know to translate some unknown script, what we can do is provide a sample of how to access COM objects directly in VBScript, JScript, and Perl. This won’t equip you for high-powered COM use, but seeing the general process for all three languages side-by side should get you started.

Creating and Getting

Working with COM objects might involve either accessing a pre-existing instance of an object or creating a new instance. In general, most components are created by you when needed and then shut down when you’re done with them (sometimes automatically). ADSI and WMI are both exceptions because you access them through running services. Creating a new instance in any scripting language works out to calling an underlying CoCreateInstance API; getting access to a running object such as the WMI interface reduces to calling the GetActiveObject API. To illustrate how this is done, we’ll demonstrate working with both a common COM class and WMI.

The COM class we’ll use as an example is WshShell, which has a common name or programmatic identifier used in WSH scripts of "WScript.Shell". One of its properties is CurrentDirectory, which can be used to both read and set the current shell directory. It also has a method named LogEvent for writing to the Windows Application event log. To get the running instances of COM objects, we’ll use the WMI winmgmts moniker and the path to a WMI namespace. We’ll create a reference to the namespace used most commonly by administrators, the root/cimv2 namespace, and then query WMI for the name and startup mode of all services.

VBScript

First, let’s look at an example in VBScript:

Dim WshShell: Set WshShell = CreateObject("WScript.Shell")
Dim dir: dir = WshShell.CurrentDirectory
WshShell.CurrentDirectory = "C:	emp"
Dim wmi: Set wmi = GetObject("winmgmts://./root/cimv2")
Dim results: Set results = wmi.execQuery( _
  "select Name,PathName,ProcessId from Win32_Service")

Note that when not capturing a result from a method as shown next, VBScript does not use parentheses around the method’s argument list:

WshShell.LogEvent 0, "Started in directory " & dir

VBScript uses physical line endings for statement terminations. To put two statements on one line, you use the colon (:); and to break a single line across two physical lines, you insert an underscore (_) at the end of the physical line. It uses the ampersand (&) to append one string to another. One final peculiarity that is very important is that it is not case-sensitive. The WMI method ExecQuery was written in the preceding code as execQuery, but it still works.

JScript

The JScript requirements are somewhat different than VBScript’s. VBScript’s CreateObject maps to the new ActiveXObject syntax. In addition, JScript uses the backslash () as an escape character, so literal backslashes in strings need to be doubled to escape them. Either double quotes or single quotes can be used as quoting characters, as long as they are correctly paired. Finally, when you call a method it must have its arguments enclosed by parentheses.

var WshShell; WshShell = new ActiveXObject("WScript.Shell");
var dir = WshShell.CurrentDirectory;
WshShell.CurrentDirectory = 'C:\temp';
var wmi = GetObject('winmgmts://./root/cimv2'),
var results = wmi.ExecQuery(
'select Name,PathName,ProcessId from Win32_Service'),
WshShell.LogEvent(4, "Start in directory: " + dir);

Perl

Perl uses CreateObject (though with a rather different syntax) just as VBScript does, but first you need to "use" the Win32::OLE module. Backslashes are an escape character again, and statement termination uses semicolons just as JScript does. Finally, when setting a property on a COM object (such as WshShell’s CurrentDirectory property), you use the same bracket notation as you would with setting a hashtable value.

use Win32::OLE;
$WshShell = Win32::OLE->CreateObject("WScript.Shell");
$dir = $WshShell->CurrentDirectory;
$WshShell->{CurrentDirectory} = 'C:\temp';
$wmi = Win32::OLE->GetObject("winmgmts://./root/cimv2");
$results = $wmi->ExecQuery(
  "select Name,PathName,ProcessId from Win32_Service");
$WshShell->LogEvent(4, "Start in directory: $dir");

Noninteractive Scripts: Remote and Scheduled Use

Scripts that run as scheduled or remote tasks have many shared issues as a result of their lack of a console or other visible interactive session. Although a well-written script can control this behavior, if your script invokes a component which doesn’t behave correctly, you’ll have problems. Using scripting conventions that don’t require interaction with the GUI and that don’t require user input can simplify the problem. And designing your scripts to recognize when they are being run from a noninteractive session allows you to redirect output, for example, to a log file or to use an input file as a source.

Important

It’s possible to work around needing a graphical session on a remote system by using Remote Desktop, but be careful. If you disconnect, the GUI session halts to restrict resource use and continues its work only when you reconnect.

The Future of Windows Scripting

Windows has a broad repertoire of scripting tools and interfaces available. However, legacy support requirements within specific tools, new basic technologies such as .NET, convergence of environments such as WSH and the command shell, and the emergence of issues based on real-world use have all shown us clear limitations. The standard command shell has limited usability compared to UNIX command shells. The scripting environment lacks interactivity and is tied to using COM for interoperation, with no way to talk to the new .NET runtime environment. The plethora of command-line arguments used for invoking help in the console shell and the workarounds required for using WSH scripts as command-line tools highlight both poor standardization and major interoperability issues.

Since 2001, Microsoft has been working on a new hybrid command line/scriptable shell system codenamed Monad, or MSH. MSH allows users to create and manipulate objects and use scripts with data flow managed by pipelines that support objects instead of simple text. Although the msh.exe shell is console-based, this isn’t a requirement for Monad use. Tools such as Microsoft Management Console will be able to create queries as Monad scripts, for example, and these queries can be reused elsewhere and even modified. The shell language is heavily informed by several existing shells and scripting systems, notably including Perl and ksh, and also supports classic stream-based interaction with the current Windows command shell. It also has direct access to .NET types, allowing you to use objects interactively, much like you could if you could type WSH script code at a command prompt.

The management infrastructure we’ve accessed for the last few years via WMI is also being extended. Web Services for Management (also known as WS-Management or just WS-Man) is a new specification for management via Web services. From a scripter’s viewpoint, it brings new accessibility: you can use HTTPS connections for remote access to management data, and you can even (with appropriate hardware support) talk to a nonfunctioning system that cannot even boot Windows. WS-Man automatically exposes existing WMI providers for use. Most of the information about WS-Man that has been available up to now has been focused on developers. A good starting point is the MSDN portal site for WS-Man at http://msdn.microsoft.com/library/en-us/wsman/wsman/portal.asp.

Summary

In this chapter, we covered scripting in Windows Server 2003. We’ve covered both the reasons why we think scripting is an essential part of any good administrator’s toolbox, and some of the challenges of writing scripts. In the next chapter, we start our coverage of directory services by showing you how to install and configure Active Directory.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.196.175