Scripting Solutions

Automation and Scripting
How to use ADSI to configure NT's Account Policy

A common question that people new to scripting ask me is, "How do you know which scripting technology to use?" Unfortunately, no one answer exists. Many factors affect the selection of a scripting solution. Furthermore, you often must use multiple scripting technologies to achieve a comprehensive automation environment. However, you need to make certain your scripting toolkit includes a language that supports Object Linking and Embedding/component object model (OLE/COM) automation. For situations in which you once used batch files to call standalone executables, you can now use OLE-savvy scripting languages to bind automation-enabled applications and components. Take Windows NT's NET commands, for example. In the past, you needed to use NET commands to automate many of NT's computer, user, and environment settings. Now, you can use automation objects in Windows Scripting Host (WSH) or directory service (DS) objects in Active Directory Service Interfaces (ADSI). Although NET commands are still useful, automation and DS objects are more robust and flexible than NET commands.

In this column, I'll explain the basic concepts behind OLE/COM automation and their effect on scripting. I'll also demonstrate how to write scripts that use OLE/COM objects to automate tasks. Specifically, I'll show you how to use Perl and ADSI objects to automate the configuration of NT 4.0's Account Policy.

OLE/COM Automation 101
Automation is Microsoft's answer to open, reusable software components. In general, automation is the process through which one application uses the functionality that another application provides. From a scripting perspective, automation is the process through which Perl, Visual Basic Script (VBScript), and other interpreted languages that support automation can access and control the functionality that an automation application or component exposes.

Applications and components that expose automation interfaces are automation servers. Applications and languages that host or control objects are automation controllers. In the example script I will show you how to write, ADSI is the server and Perl is the controller.

Dispatch interfaces (e.g., IDispatch) make automation possible. IDispatch is one of the many COM interfaces available to application and component developers. What's unique about IDispatch is that it lets interpreted languages dynamically bind to and use the objects, methods, properties, and events IDispatch-enabled applications or components expose. Objects represent a class (i.e., a logical grouping of methods and properties that defines something meaningful). Examples of objects include ADSI Domain Objects and ADSI User Objects. Methods represent the behavior of an object. Methods are functions you invoke to make an object perform a task. For example, the ADSI User Object includes a SetPassword method that you can invoke to change a user's password. Properties represent an object's state. The ADSI Domain Object you'll be working with in your script includes properties that describe the state of an NT 4.0 domain's Account Policy. Events represent asynchronous messages that an object responds to. For example, an object might respond when a user selects a certain menu item.

OLE/COM Automation Servers
The two main types of OLE/COM automation servers are application- and component-based automation servers. Application- and component- based automation servers expose objects, methods, properties, and events. (Vendors typically identify which objects, methods, properties, and events a component or application provides in the documentation.)

Application-based automation servers are applications (.exe files) that expose their functionality via automation interfaces in addition to performing their primary role that a GUI accesses. Examples of such applications include Internet Explorer (IE), Office 97, Notes, and Visio. You can create instances of and use these applications without displaying the applications' user interface. Application-based automation servers run out-of-process, meaning they run in a separate process from that of the automation controller.

Component-based automation servers are usually .dll files. Unlike applications, components typically don't provide a user interface. Instead, the components' primary role is to abstract and provide access to various systems and application services, letting other applications use the underlying technology without regard to location, programming language, platform, and other factors. Examples of components include ADSI, ActiveX Data Objects (ADO), and Collaboration Data Objects (CDO). Components run in-process, meaning they share the same process with that of the controller.

TABLE 1: The Methods' Syntax
Method Syntax
new Win32::OLE->new(ProgID, [Destructor])
GetObject Win32::OLE->GetObject(Moniker)
GetActiveObject Win32::OLE->GetActiveObject(ProgID)
OLE Automation Controllers
You use OLE automation controllers to create and interact with automation objects. Active Server Pages (ASP), JavaScript, Delphi, Perl, Python, VBScript, Visual C++, WSH, and many other languages provide varying levels of support for automation objects. For example, Listings 1 and 2 show how VBScript's WSH syntax and Perl's Win32::OLE syntax support automation objects. By comparing Listings 1 and 2, you can see the differences and similarities between how these two scripting languages operate. I'll also note VBScript and Perl syntax differences and similarities in the following discussion.

Regardless of the language you choose, you follow three steps to create and interact with automation objects.

Create a reference to a registered object. OLE automation controllers provide multiple methods to create objects. Perl's Win32::OLE module provides three methods: new, GetObject, and GetActiveObject. Table 1 shows the syntax for these methods. If successful, all three methods return a reference to the target object. If unsuccessful, the three methods return the undef value.

Perl's new method closely resembles the CreateObject method in VBScript. The method has one mandatory argument (the programmatic identifier--ProgID--of the object) and one optional property (a property that behaves as a destructor if the script terminates abnormally). ProgID is the friendly name for the class identifier (CLSID) of a registered object; the CLSID is a 128-bit globally unique ID (GUID) that identifies the object. Examples of ProgIDs include Excel.Application and Visio.Application.

Perl's GetObject method is similar to VBScript's GetObject. This method creates a reference to an object, given a moniker. Monikers identify persistent objects the application or component understands, such as the name of an Excel spreadsheet file. In the case of ADSI, the moniker is one of the four namespaces that ADSI supports: Lightweight Directory Access Protocol (LDAP), Novell Directory Services (NDS), NWCOMPAT, and WinNT.

Perl's GetActiveObject method returns a reference to an already running instance of an application. ProgID identifies the application.

Interact with the object via its methods and properties. After the method successfully creates a reference to an automation object, you invoke and access the object's methods and properties. Like VBScript, Perl lets you access nested objects. Unlike VBScript in which you use the dot operator (.) to access an object's methods and properties, with Perl you use the arrow operator (->). Another difference is that with Perl, you can distinguish properties from methods. You express properties using Perl's hash syntax by enclosing property names in curly braces, such as myObjectReference->{myProperty}.

Destroy the object when you no longer need it. When the Perl script exits, Perl automatically destroys the object.

The AcntPlcy.pl Script

Now that you know the basic concepts in OLE/COM automation and Perl, I'll walk through the example script, AcntPlcy.pl, in Listing 3. This script uses Perl's Win32::OLE module to access and control an NT 4.0 domain's Account Policy via ADSI.

AcntPlcy.pl begins by including the Win32::OLE module, which provides the core OLE support you need to create instances of and interact with automation servers and components. The script then checks the default command-line argument array, @ARGV. If the first element of the array contains a question mark, the script calls the Usage subroutine (which prints usage instructions to STDOUT) and then exits. (The AcntPlcy.pl script in Listing 3 doesn't include the Usage subroutine; you can find this subroutine in the AcntPlcy.pl script on Windows NT Magazine's Web site at http://www.winntmag.com.)

If $ARGV[0] doesn't contain a question mark, the script defines and initializes two hashes: %OldAccountPolicy and %NewAccountPolicy. The script uses these hashes to store the target domain's Account Policy values before and after any changes. Notice the hash keys' names are identical to the properties in the underlying directory. Thus, in AcntPlcy.pl, the hash keys are identical to the ADSI Domain Object's property names. This approach has three benefits. First, by using Perl's keys operator, you can use the hash key to control the number of loop iterations. Second, you can use the hash key names to tell ADSI which property to fetch. Third, you can use the hash key to identify the storage location for the retrieved property.

At callout A in Listing 3, the foreach loop processes the command-line arguments. Not including the question mark, the script supports eight optional command-line switches: d=TargetDomain, A=MaxPasswordAge, a=MinPasswordAge, l=MinPasswordLength, h=PasswordHistoryLength, b=MaxBadPass-wordsAllowed, u=AutoUnlockInterval, and o=LockoutObservationInterval.

Through each loop iteration, the script uses Perl's default scalar, $_, to compare an element of the command-line argument array to a regular expression that corresponds to a specific command-line switch. When a match occurs, the script evaluates the second part of the expression. Except for the target domain scalar $strDomain, the second part of each expression creates and initializes a third hash, %AccountPolicy.

The %AccountPolicy hash stores the changes that you specify on the command line. Like the Old and New Account Policy hashes, the names of the %AccountPolicy's keys are identical to the ADSI Domain object property names. Unlike the other two hashes, the script creates and initializes elements (key ­> value pairs) for only those options defined on the command line. For example, if you specify only three command-line options, the hash will only contain three key ­> value pairs. This approach makes it easy to determine which properties to change.

The split operator is used in each expression to separate the switch from the value. The [1] subscript tells Perl's split operator to return only the value used to initialize the corresponding key in the %AccountPolicy hash.

After exiting the foreach loop, the script tests the target domain scalar $strDomain. If this scalar didn't get set via the command line, the script sets the scalar to the value of the USERDOMAIN environment variable. However, if the user has logged on locally to a workstation or member server, this value will contain the local machine name rather than the domain name, causing the subsequent GetObject call to fail because GetObject is expecting a domain name. The bottom line is that you must either specify the target domain using the d=TargetDomain switch or be logged on to the target domain. ADSI supports changing the Account Policy on domains, but not on member servers and workstations.

Next, the script calls the Win32::OLE module's GetObject method. Recall that this method takes only one argument in the form of a moniker. When you use ADSI, the moniker identifies an ADSI registered namespace. In AcntPlcy.pl, the moniker identifies the WinNT namespace. (The ADSI namespace identifiers are case sensitive.) On success, GetObject returns a reference to the object that the argument identifies, which initializes $oDomain.

After $oDomain contains a valid object reference, the script uses this reference to access the object's methods and properties. Recall that in the beginning of the script, you created two hashes (%OldAccountPolicy and %NewAccountPolicy) using the ADSI Domain Object's property names as the hash keys. The foreach loop at B in Listing 3 traverses the hash keys in the %OldAccountPolicy hash. Through each iteration, the foreach loop uses Perl's keys operator to assign the hash key name to the $key scalar. The $key scalar identifies the property to fetch and specifies where to store the property in the original hash. The $oDomain reference invokes the ADSI Domain Object's Get method, which retrieves the value of the property and stores it in the hash element referenced by the current value of $key references. The end result is a hash that contains the Account Policy settings before you make any changes.

The if defined statement that follows the foreach loop determines whether the %AccountPolicy hash has a value. Recall that the command-line argument loop at A created the %AccountPolicy hash and corresponding key ­> value pairs for only those options you specified on the command line. If you fail to provide a valid Account Policy argument, the script doesn't create a %AccountPolicy hash, which causes the if defined statement to fail. As a result, the script sends the message No change(s) specified via command line. Printing active account policy with the target domain's Account Policy to STDOUT. The script then exits, and Perl destroys the $oDomain object reference.

If you specify at least one valid Account Policy argument, the script creates an %AccountPolicy hash, satisfying the if defined statement. The script then updates the cached Account Policy properties it retrieved earlier. This time around, the foreach loop traverses the keys in the %AccountPolicy hash. Through each iteration, the script assigns a hash key name to the $key scalar and uses the $key scalar to update the appropriate $oDomain object property with the new value from the %AccountPolicy hash. At this point, the script updates only the locally cached copies of the $oDomain object's properties, not the underlying directory.

To write the changes to the directory, you need to invoke the SetInfo method. You then fetch the updated values using the same approach you used at B, storing the values in the %NewAccountPolicy hash.

You use the old and new hashes to print the changes to STDOUT. The code at C in Listing 3 produces output similar to Screen 1. Screen 2 shows the resulting changes made to the target domain's Account Policy. The script then exits, and Perl destroys the $oDomain object reference.

Not all the Account Policy properties behave as you might expect. The AcntPlcy.pl script on Windows NT Magazine's Web site identifies the properties you need to be aware of in the script's usage instructions. In addition, you might want to check out the Win32::OLE module, which features other methods and functions that I didn't discuss here. The module is part of ActiveState Tool's ActivePerl software. You can find information about this module in the online HTML documentation included with the ActivePerl distribution at http://www.activestate.com.

A New Frontier
Although command-line utilities will continue to play an important role in scripting, the applications and components that expose automation interfaces represent a new frontier for administrators of automated systems. Furthermore, automation represents an opportunity for independent software vendors looking for ways to enhance and extend their applications' functionality. Imagine if you could access and control your backup and systems management software from OLE-capable scripting languages. You just might find yourself automating tasks you never thought possible.