For the final section of this chapter we'll discuss a platform-dependent directory service framework that is heavily based on the material we've just covered.
Microsoft created a sophisticated LDAP-based directory service called Active Directory for use at the heart of their Windows 2000 administration framework. Active Directory serves as the repository for all of the important configuration information (users, groups, system policies, software installation support, etc.) used in a network of Windows 2000 machines.
During the development of Active Directory, Microsoft realized a higher-level applications interface to this service was needed. ADSI, or Active Directory Service Interfaces, was invented to provide this interface. To their credit, the developers at Microsoft also realized that their new ADSI framework could be extended to cover other system administration realms like printers and NT services. This coverage makes ADSI immensely useful to people who script and automate system administration tasks. Before we show this power in action, there are a few basic concepts and terms we need to cover.
ADSI can be thought of as a wrapper around any directory service that wishes to participate in the ADSI framework. There are providers, as these ADSI glue implementations are called, for LDAP, WinNT 4.0, and Novell Directory Service among others. In ADSI-speak, each of these directory services and data domains (WinNT isn't a directory service) are called namespaces. ADSI gives you a uniform way to query and change the data found in these namespaces.
To understand ADSI, you have to know a little about the Microsoft Component Object Model (COM) upon which ADSI is built. There are many books about COM, but we can distill down to these key points:
Everything we want to work with via COM is an object.[4]
[4]COM is in fact the protocol used to communicate with these objects as part of the larger framework called OLE, for Object Linking and Embedding. In this section, I've tried to keep us out of the Microsoft morass of acronyms, but if you want to dig deeper, there are some good resources that are available at http://www.microsoft.com/com.
Objects have interfaces that provide a set of methods for us to use to interact with these objects. From Perl, we can use the methods provided by or inherited from the interface called IDispatch. Luckily most of the ADSI methods provided by the ADSI interfaces and their children (e.g., IADsUser, IADsComputer, IADsPrintQueue) are inherited from IDispatch.
The values encapsulated by an object, which is queried and changed through these methods, are called properties. We'll refer to two kinds of properties in this chapter: interface-defined properties (those that are defined as part of an interface), and schema-defined properties (those that are defined in a schema object, more on this in just a moment). Unless we refer explicitly to "schema properties" in the following dicussion, we'll only be using interface properties.
This is standard object-oriented programming fare. It starts to get tricky when the nomenclature for ADSI/COM and other object-oriented worlds like LDAP collide.
For instance, in ADSI we speak of two different kinds of objects: leaf and container. Leaf objects encapsulate real data; container objects hold, or parent, other objects. In LDAP-speak a close translation for these terms might be "entry" and "branching point." On one hand we talk about objects with properties and on the other, entries with attributes. So how do you deal with this discrepancy, since both names refer to the exact same data?
Here's one way to think about it: an LDAP server does indeed provide access to a tree full of entries and their associated attributes. When you use ADSI instead of native LDAP to get at an entry in that tree, ADSI sucks the entry out of the LDAP server, wraps it up in a few layers of shiny wrapping paper, and hands it to you as a COM object. You use the necessary methods to get the contents of that parcel, which are now called "properties." If you make any changes to the properties of this object, you can hand the object back to ADSI, which will take care of unwrapping the information and putting it back in the LDAP tree for you.
A reasonable question at this point is "Why not go directly to the LDAP server?" Two good answers: once we know how to use ADSI to communicate with one kind of directory service, we know how to communicate with them all (or at least the ones that have ADSI providers). The second answer will be demonstrated in a few moments when we see how ADSI's encapsulation can make directory service programming a little easier.
To head in the direction of ADSI programming from Perl, we need to introduce ADsPaths. ADsPaths give us a unique way to refer to objects in any of our namespaces. They look like this:
<progID>:<path to object>
<progID> is the programmatic identifier for a provider (e.g., WinNT or LDAP), and <path to object> is a provider-specific way of finding the object in its namespace. The <progID> portion is case-sensitive. Using winnt, ldap, or WINNT instead of WinNT and LDAP will cause your programs to fail.
Here are some ADsPath examples taken from the ADSI SDK documentation:
WinNT://MyDomain/MyServer/User WinNT://MyDomain/JohnSmith,user LDAP://ldapsvr/CN=TopHat,DC=DEV,DC=MSFT,DC=COM,O=Internet LDAP://MyDomain.microsoft.com/CN=TopH,DC=DEV,DC=MSFT,DC=COM,O=Internet
It's no coincidence that these look like URLs, since both URLs and ADsPaths serve roughly the same purpose. They both try to provide an unambiguous way to reference a piece of data made available by different data services. In the case of LDAP ADsPaths, we are using the LDAP URL syntax from the RFC we mention in Appendix B, "The Ten-Minute LDAP Tutorial" (RFC2255).
We'll look more closely at ADsPaths when we discuss the two namespaces, WinNT and LDAP, referenced earlier. Before we get there, let's see how ADSI in general is used from Perl.
The Win32::OLE family of modules maintained by Jan Dubois and Gurusamy Sarathy give us the Perl bridge to ADSI (which is built on COM as part of OLE). After loading the main module, we use it to request an ADSI object:
use Win32::OLE; $adsobj = Win32::OLE->GetObject($ADsPath) or die "Unable to retrieve the object for $ADsPath\n";
Win32::OLE->GetObject( ) takes an OLE moniker (a unique identifier to an object, which in this case is an ADsPath) and returns an ADSI object for us. This call also handles the process of binding to the object, a process you should be familiar with from our LDAP discussion. By default we bind to the object using the credentials of the user running the script.
TIP
Here's a tip that may save you some consternation. If you run these two lines of code in the Perl debugger and examine the contents of the returned object reference, you might see something like this:
DB<3> x $adsobj 0 Win32::OLE=HASH(0x10fe0d4) empty hashDon't panic. Win32::OLE uses the power of tied variables. The seemingly empty data structure you see here will magically yield information from our object when we access it properly.
Perl's hash reference syntax is used to access the interface property values of an ADSI object:
$value = $adsobj->{key}
For instance, if that object had a Name property defined as part of its interface (and they all do), you could:
print $adsobj->{Name}."\n";
Interface property values can be assigned using the same notation:
$adsobj->{FullName}= "Oog"; # set the property in the cache
An ADSI object's properties are stored in an in-memory cache (called the property cache). The first request for an object's properties populates this cache. Subsequent queries for the same property will retrieve the information from this cache, not the directory service. If you want to populate the cache by hand, you can call that object instance's GetInfo( ) or GetInfoEx( ) (an extended version of GetInfo( )) method using the syntax we'll see in a moment.
Because the initial fetch is automatic, GetInfo( ) and GetInfoEx( ) are often overlooked. Though we won't see any in this book, there are cases where you will need them. Two example cases:
Some object properties are only fetched by an explicit GetInfoEx( ) call. Microsoft Exchange 5.5's LDAP provider offers a particularly egregious example because many of its properties are not available without calling GetInfoEx( ) first. See http://opensource.activestate.com/authors/tobyeverett for more details on this inconsistency.
If you have a directory that multiple people can change, an object you may have just retrieved could be changed while you are still working with it. If this happens, the data in your property cache for that object will be stale. GetInfo( ) and GetInfoEx( ) will refresh this cache for you.
To actually update the backend directory service and data source provided through ADSI, you must call the special method SetInfo( ) after changing an object. SetInfo( ) flushes the changes from the property cache to the actual directory service and data source. (This should remind you of our need in Mozilla::LDAP to call the update( ) method. It's the same concept.)
Calling methods from an ADSI object instance is easy:
$adsobj->Method($arguments...)
So, if we changed an object's properties as mentioned in the previous warning, we might use this line right after the code that made the change:
$adsobj->SetInfo( );
This would flush the data from the property cache back into the underlying directory service or data source.
One Win32::OLE call you'll want to use often is Win32::OLE->LastError( ). This will return the error, if any, generated by the last OLE operation. Using the -w switch with Perl (e.g., perl -w script) also causes any OLE failures to complain in a verbose manner. Often these error messages are all the debugging help you have, so be sure to make good use of them.
The ADSI code we've shown so far should look like fairly standard Perl to you, because on the surface, it is. Now let's introduce a few of the plot complications.
Early in this section we mentioned there are two kinds of ADSI objects: leaf and container. Leaf objects represent pure data, whereas container objects (also called collection objects in OLE/COM terms) contain other objects. Another way to distinguish between the two in the ADSI context is by noting that leaf objects have no children in a hierarchy, but container objects do.
Container objects require special handling, since most of the time we're interested in the data encapsulated by their child objects. There are two ways to access these objects from Perl. Win32::OLE offers a special function called in( ), which is not available by default when the module is loaded in the standard fashion. We have to use the following at the beginning of our code to make use of it:
use Win32::OLE 'in';
in( ) will return a list of references to the child objects held by that container. This allows us to write easy-to-read Perl code like:
foreach $child (in $adsobj){ print $child->{Name} }
Alternatively, we can load one of Win32::OLE's helpful progeny, which is called Win32::OLE::Enum. Win32::OLE::Enum->new( ) will create an enumerator object from one of our container objects:
use Win32::OLE::Enum; $enobj = Win32::OLE::Enum->new($adsobj);
We can then call a few methods on this enumerator object to get at $adsobj's children. These methods should remind you of the methods we used with Mozilla::LDAP's search operations; it is the same process.
$enobj->Next( ) will return a reference to the next child object instance (or the next X objects if given an optional parameter). $enobj->All returns a list of object instance references. Win32::OLE::Enum offers a few more methods (see the documentation for details), but these are the ones you'll use most often.
You can't know if an object is a container object a priori. There is no way to ask an object itself about its "containerness" from Perl. The closest you can come is to try to create an enumerator object and fail gracefully if this does not succeed. Here's some code that does just that:
use Win32::OLE; use Win32::OLE::Enum; eval {$enobj = Win32::OLE::Enum->new($adsobj)}; print "object is " . ($@ ? "not " : "") . "a container\n";
Alternatively, you can look to other sources that describe the object. This segues nicely into our third plot complication.
We've avoided the biggest and perhaps the most important question until now. In a moment we'll be dealing with objects in two of our namespaces. We understand how to retrieve and set object properties and how to call object methods for these objects, but only if we already know the names of these properties and methods. Where did these names come from? How did we find them in the first place?
There's no single place to find an answer to these questions, but there are a few sources we can draw upon to get most of the picture. The first place is the ADSI documentation, especially the help file mentioned in the earlier sidebar, the sidebar "The Tools of the ADSI Trade". This file has an huge amount of material. For the answer to our question about property and methods names, the place to start in the file is Active Directory Service Interfaces 2.5ADSI ReferenceADSI System Providers.
The documentation is sometimes the only place to find method names, but there's a second, more interesting approach we can take when looking for property names. We can use metadata provided by ADSI itself. This is where the schema properties concept we mentioned earlier comes into the picture.
Every ADSI object has a property called Schema that yields an ADsPath to its schema object. For instance, the following code:
use Win32::OLE; $ADsPath = "WinNT://BEESKNEES,computer"; $adsobj = Win32::OLE->GetObject($ADsPath) or die "Unable to retrieve the object for $ADsPath\n"; print "This is a ".$adsobj->{Class}."object, schema is at:\n". $adsobj->{Schema},"\n";
will print:
This is a Computer object, schema is at: WinNT://DomainName/Schema/Computer
The value of $adsobj{Schema} is an ADsPath to an object that describes the schema for the objects of class Computer in that domain. Here we're using the term "schema" in the same way we used it when talking about LDAP schemas. In LDAP, schemas define which attributes can and must be present in entries of specific object classes. In ADSI, a schema object holds the same information about objects of a certain class and their schema properties.
If we want to see the possible attribute names for an object, we can look at the values of two properties in its schema object: MandatoryProperties and OptionalProperties. Let's change the print statement above to the following:
$schmobj = Win32::OLE->GetObject($adsobj->{Schema}) or die "Unable to retrieve the object for $ADsPath\n"; print join("\n",@{$schmobj->{MandatoryProperties}}, @{$schmobj->{OptionalProperties}}),"\n";
This prints:
Owner Division OperatingSystem OperatingSystemVersion Processor ProcessorCount
Now we know the possible schema interface property names in the WinNT namespace for our Computer objects. Pretty nifty.
Schema properties are retrieved and set in a slightly different manner than interface properties. You recall that interface properties are retrieved and set like this:
# retrieving and setting INTERFACE properties $value = $obj->{property}; $obj->{property} = $value;
Schema properties are retrieved and set using special methods:
# retrieving and setting SCHEMA properties $value = $obj->Get("property"); $obj->Put("property","value");
Everything we've talked about so far regarding interface properties holds true for schema properties as well (i.e., property cache, SetInfo( ), etc). Besides the need to use special methods to retrieve and set values, the only other place where you'll need to distinguish between the two is in their names. Sometimes the same object may have two different names for essentially the same property, one for the interface property and one for the schema property. For example, these two retrieve the same basic setting for a user:
$len = $userobj->{PasswordMinimumLength}; # the interface property $len = $userobj->Get("MinPasswordLength"); # the same schema property
There are two kinds of properties because interface properties exist as part of the underlying COM model. When developers define an interface as part of developing a program, they also define the interface properties. Later on, if they want to extend the property set, they have to modify both the COM interface and any code that uses that interface. In ADSI, developers can change the schema properties in a provider without having to modify the underlying COM interface for that provider. It is important to become comfortable with dealing with both kinds of properties because sometimes a piece of data in an object is only made available from within one kind of property and not in the other.
On a practical note, if you are just looking for interface or schema property names and don't want to bother writing a program to find them, I recommend using the Toby Everett ADSI browser mentioned earlier. Figure 6-2 is a sample screen shot of this browser in action.
Alternatively, there is a program called ADSIDump in the General folder of the SDK samples that can dump the contents of an entire ADSI tree for you.
This is the last complication we'll discuss before moving on. In Section 6.4, "LDAP: A Sophisticated Directory Service", we spent considerable time talking about LDAP searches. But here in ADSI-land, we've breathed hardly a word about the subject. This is because from Perl (and any other language that uses the same OLE automation interface), searching with ADSI is a pain -- that is, sub-tree searches, or searches that entail anything but the simplest of search filters are excruciatingly painful. (Others are not so bad.) Complex searches are troublesome because they require you to step out of the ADSI framework and use a whole different methodology to get at your data (not to mention learn more Microsoft acronyms).
But people who do system administration are trained to laugh at pain, so let's start with simple searches before tackling the hard stuff. Simple searches that encompass one object (scope of base) or an object's immediate children (scope of one) can be handled manually with Perl. Here's how:
For a single object, retrieve the properties of interest and use the normal Perl comparison operators to determine if this object is a match:
if ($adsobj->{cn} eq "Mark Sausville" and $adsobj->{State} eq "CA"){...}
To search the children of an object, use the container object access techniques we discussed previously and then examine each child object in turn. We'll see some examples of this type of search in a moment.
If you want to do more complex searches like those that entail searching a whole directory tree or sub-tree, you need to switch to using a different "middleware" technology called ADO (ActiveX Data Objects). ADO offers scripting languages an interface to Microsoft's OLE DB layer. OLE DB provides a common database-oriented interface to data sources like relational databases and directory services. In our case we'll be using ADO to talk to ADSI (which then talks to the actual directory service). Because ADO is a database-oriented methodology, the code you are about to see foreshadows the ODBC material we cover in Chapter 7, "SQL Database Administration".
TIP
ADO only works when talking to the LDAP ADSI provider. It will not work for the WinNT namespace.
ADO is a whole subject in itself that is only peripherally related to the subject of directory services, so we're only going to look at one example and provide a little bit of explanation before moving on to some more relevant ADSI examples. For more information on ADO itself, please see http://www.microsoft.com/ado.
Here's some code that displays the name of all of the groups to be found in a given domain. We'll go through this code piece by piece in a moment.
use Win32::OLE 'in'; # get ADO object, set the provider, open the connection $c = Win32::OLE->new("ADODB.Connection"); $c->{Provider} = "ADsDSOObject"; $c->Open("ADSI Provider"); die Win32::OLE->LastError() if Win32::OLE->LastError( ); # prepare and then execute the query $ADsPath = "LDAP://ldapserver/dc=example,dc=com"; $rs = $c->Execute("<$ADsPath>;(objectClass=Group);Name;SubTree"); die Win32::OLE->LastError() if Win32::OLE->LastError( ); until ($rs->EOF){ print $rs->Fields(0)->{Value},"\n"; $rs->MoveNext; } $rs->Close; $c->Close;
The block of code after the module load gets an ADO Connection object instance sets that object instance's provider name, and then instructs it to open the connection. This connection is opened on behalf of the user running the script, though we could have set some other object properties to change this.
We then perform the actual search using Execute( ). This search can be specified using one of two "dialects," SQL or ADSI.[5] The ADSI dialect, as shown, uses a command string consisting as four arguments, each separated by semicolons.[6] The arguments are:
[5]If you know SQL, you may find the SQL dialect a little easier. The SQL dialect offers some interesting possibilities. For instance, MS SQL Server 7 can be configured to know about ADSI providers in addition to normal databases. This means that you can execute SQL queries which simultaneously access ActiveDirectory objects via ADSI.
[6]Be careful of this ADSI ADO provider quirk: there cannot be any whitespace around the semicolons or the query will fail.
An ADsPath (in angle brackets) that sets the server and base DN for the search
A search filter (using the same LDAP filter syntax we saw before)
The name or names (separated by commas) of the properties to return
A search scope of either Base, OneLevel, or SubTree (as per the LDAP standard)
Execute( ) returns a reference to the first of the ADO RecordSet objects returned by our query. We ask for each RecordSet object in turn, unpacking the objects it holds and printing the Value property returned by the Fields( ) method for each of these objects. The Value property contains the value we requested in our command string (the name of the Group object). Here's some sample output from a Windows 2000 machine:
Administrators Users Guests Backup Operators Replicator Server Operators Account Operators Print Operators DHCP Users DHCP Administrators Domain Computers Domain Controllers Schema Admins Enterprise Admins Cert Publishers Domain Admins Domain Users Domain Guests Group Policy Admins RAS and IAS Servers DnsAdmins DnsUpdateProxy
Now that we've safely emerged from our list of complications, we can turn to performing some common administrative tasks using ADSI from Perl. The goal is to give you a taste of the things you can do with the ADSI information we've presented. Then you can use the code we're going to see as starter recipes for your own programming.
For these tasks, we'll use one of two namespaces. The first namespace is WinNT, which gives us access to Windows 4.0 objects like users, groups, printers, services, etc.
The second is our friend LDAP. LDAP becomes the provider of choice when we move on to Windows 2000 and its LDAP-based Active Directory. Most of the WinNT objects can be accessed via LDAP as well. But even with Windows 2000, there are still tasks that can only be performed using the WinNT namespace (like the creation of local machine accounts).
The code that works with these different namespaces looks similar (after all, that's part of the point of using ADSI), but you should note two important differences. First, the ADsPath format is slightly different. The WinNT ADsPath takes one of these forms according to the ADSI SDK:
WinNT:[//DomainName[/ComputerName[/ObjectName[,className]]]] WinNT:[//DomainName[/ObjectName[,className]]] WinNT:[//ComputerName,computer] WinNT:
The LDAP ADsPath looks like this:
LDAP://HostName[:PortNumber][/DistinguishedName]
Note that the LDAP ADsPath requires a server hostname under NT4 (this changes in Windows 2000). This means that the LDAP namespace isn't browsable from its top-level like the WinNT namespace, since you have to point it at a starting server. With the WinNT namespace, one can begin with an ADsPath of just WinNT: to start drilling down into the domain hierarchy.
Also note that the properties of the objects in the two namespaces are similar, but they are not the same. For instance, you can access the same user objects from both the WinNT and LDAP namespaces, but you can only get to some Active Directory properties for a particular user object through the LDAP namespace.
It's especially important to pay attention to the differences between the schema found in the two namespaces. For example, the User class for WinNT has no mandatory properties while the LDAP User class requires cn and samAccountName to be present in every user object.
With these differences in mind, let's look at some actual code. To save space, we're going to omit most of the error checking, but you'll want to run your scripts with the -w switch and liberally sprinkle lines like this throughout your code:
die "OLE error :".Win32::OLE->LastError() if Win32::OLE->LastError( );
To dump the list of users in a domain:
use Win32::OLE 'in'; $AdsPath = "WinNT://DomainName/PDCName,computer"; $c = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; foreach $adsobj (in $c){ print $adsobj->{Name},"\n" if ($adsobj->{Class} eq "User"); }
To create a user and set her or his Full Name:
use Win32::OLE; $ADsPath="WinNT://DomainName/ComputerName,computer"; $c = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; # create and return a User object $u = $c->Create("user",$username); $u->SetInfo( ); # we have to create the user before we modify it # no space between "Full" and "Name" allowed with WinNT: namespace $u->{FullName} = $fullname; $su->SetInfo( );
If ComputerName is a Primary Domain Controller, then a domain user is created. If not, that user is local to the specified machine.
The equivalent code to create a global user (you can't create local users using LDAP) in an Active Directory looks like this:
use Win32::OLE; $AdsPath = "LDAP://ldapserver,CN=Users,dc=example,dc=com"; $c = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; # create and return a User object $u=$c->Create("user","cn=".$commonname); $u->{samAccountName} = $username; # we have to create the user in the dir before we modify it $u->SetInfo( ); # space between "Full" and "Name" required with LDAP: namespace, sigh $u->{'Full Name'} = $fullname; $u->SetInfo( );
Deleting a user requires just a small change:
use Win32::OLE; $AdsPath = "WinNT://DomainName/ComputerName,computer"; $c = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; # delete the User object, note that we are bound to the container object $c->Delete("user",$username); $u->SetInfo( );
Changing a user's password is a single method's work:
use Win32::OLE; $AdsPath = "WinNT://DomainName/ComputerName/".$username; $u = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; $u->ChangePasssword($oldpassword,$newpassword); $u->SetInfo( );
You can enumerate the available groups with just a minor tweak of our user enumeration code above. The one changed line is:
print $adsobj->{Name},"\n" if ($adsobj->{Class} eq "Group");
Creation and deletion of groups is performed using the same Create( ) and Delete( ) methods we just saw for user account and creation. The only difference is the first argument needs to be "group." For example:
$g = $c->Create("group",$groupname);
To add a user to a group (specified as a GroupName) once you've created it:
use Win32::OLE; $AdsPath = "WinNT://DomainName/GroupName,group"; $g = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; # this uses the ADsPath to a specific user object $g->Add($userADsPath);
The same rules we saw before about local versus domain (global) users apply here as well. If we want to add a domain user to our group, our $userADsPath should reference the user at the PDC for that domain.
To remove a user from a group, use:
$c->Remove($userADsPath);
Now we start to get into some of the more interesting ADSI esoterica. It is possible to use ADSI to instruct a machine to start sharing a part of its local storage to other computers:
use Win32::OLE; $AdsPath = "WinNT://ComputerName/lanmanserver"; $c = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; $s = $c->Create("fileshare",$sharename); $s->{path} = 'C:\directory'; $s->{description} = "This is a Perl created share"; $s->SetInfo( );
File shares are deleted using the Delete( ) method.
Before we move on to other tasks, let me take this opportunity to remind you to closely consult the SDK documentation before using any of these ADSI objects. Sometimes, you'll find useful surprises. If you look at this section in the ADSI 2.5 help file: Active Directory Service Interfaces 2.5ADSI ReferenceADSI InterfacesPersistent Object InterfacesIADsFileShare, you'll see that a fileshare object has a CurrentUserCount property that shows how many users are currently connected to this file share. This could be a very handy detail.
Here's how to determine the names of the queues on a particular server and the models of the printers being used to serve those queues:
use Win32::OLE 'in'; $ADsPath="WinNT://DomainName/PrintServerName,computer"; $c = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; foreach $adsobj (in $c){ print $adsobj->{Name}.":".$adsobj->{Model}."\n" if ($adsobj->{Class} eq "PrintQueue"); }
Once you have the name of a print queue, you can bind to it directly to query and control it:
use Win32::OLE 'in'; # this table comes from this section in the ADSI 2.5 SDK: # 'Active Directory Service Interfaces 2.5->ADSI Reference-> # ADSI Interfaces->Dynamic Object Interfaces->IADsPrintQueueOperations-> # IADsPrintQueueOperations Property Methods' (phew) %status = (0x00000001 => 'PAUSED', 0x00000002 => 'PENDING_DELETION', 0x00000003 => 'ERROR' , 0x00000004 => 'PAPER_JAM', 0x00000005 => 'PAPER_OUT', 0x00000006 => 'MANUAL_FEED', 0x00000007 => 'PAPER_PROBLEM', 0x00000008 => 'OFFLINE', 0x00000100 => 'IO_ACTIVE', 0x00000200 => 'BUSY', 0x00000400 => 'PRINTING', 0x00000800 => 'OUTPUT_BIN_FULL', 0x00001000 => 'NOT_AVAILABLE', 0x00002000 => 'WAITING', 0x00004000 => 'PROCESSING', 0x00008000 => 'INITIALIZING', 0x00010000 => 'WARMING_UP', 0x00020000 => 'TONER_LOW', 0x00040000 => 'NO_TONER', 0x00080000 => 'PAGE_PUNT', 0x00100000 => 'USER_INTERVENTION', 0x00200000 => 'OUT_OF_MEMORY', 0x00400000 => 'DOOR_OPEN', 0x00800000 => 'SERVER_UNKNOWN', 0x01000000 => 'POWER_SAVE'); $ADsPath = "WinNT://PrintServerName/PrintQueueName"; $p = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; print "The printer status for " . $c->{Name} . " is " . ((exists $p->{status}) ? $status{$c->{status}} : "NOT ACTIVE") . "\n";
The PrintQueue object offers the set of print queue control methods you'd hope for: Pause( ), Resume( ), and Purge( ). These allow us to control the actions of the queue itself. But what if we want to examine or manipulate the actual jobs in this queue?
To get at the actual jobs, you call a PrintQueue object method called PrintJobs( ). PrintJobs( ) returns a collection of PrintJob objects, each of which has a set of properties and methods. For instance, here's how to show the jobs in a particular queue:
use Win32::OLE 'in'; # this table comes from this section in the ADSI 2.5 SDK: # 'Active Directory Service Interfaces 2.5->ADSI Reference-> # ADSI Interfaces->Dynamic Object Interfaces->IADsPrintJobOperations-> # IADsPrintJobOperations Property Methods' (double phew) %status = (0x00000001 => 'PAUSED', 0x00000002 => 'ERROR', 0x00000004 => 'DELETING',0x00000010 => 'PRINTING', 0x00000020 => 'OFFLINE', 0x00000040 => 'PAPEROUT', 0x00000080 => 'PRINTED', 0x00000100 => 'DELETED'); $ADsPath = "WinNT://PrintServerName/PrintQueueName"; $p = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; $jobs = $p->PrintJobs( ); foreach $job (in $jobs){ print $job->{User} . "\t" . $job->{Description} . "\t" . $status{$job->{status}} . "\n"; }
Each job can be Pause( )d and Resume( )d as well.
For our last set of examples, we're going to look at how to locate, start, and stop the services on an NT/2000 machine. Like the other examples in this chapter, these code snippets must be run from an account with sufficient privileges on the target computer to effect changes.
To list the services on a computer and their status, we could use this code:
use Win32::OLE 'in'; # this table comes from this section in the ADSI 2.5 SDK: # 'Active Directory Service Interfaces 2.5->ADSI Reference-> # ADSI Interfaces->Dynamic Object Interfaces->IADsServiceOperations-> # IADsServiceOperations Property Methods' %status = (0x00000001 => 'STOPPED', 0x00000002 => 'START_PENDING', 0x00000003 => 'STOP_PENDING', 0x00000004 => 'RUNNING', 0x00000005 => 'CONTINUE_PENDING',0x00000006 => 'PAUSE_PENDING', 0x00000007 => 'PAUSED', 0x00000008 => 'ERROR'); $ADsPath = "WinNT://DomainName/ComputerName,computer"; $c = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; foreach $adsobj (in $c){ print $adsobj->{DisplayName} . ":" . $status{$adsobj->{status}} . "\n" if ($adsobj->{Class} eq "Service"); }
To start, stop, pause, or continue a service, we call the obvious method (Start( ), Stop( ), etc.). Here's how we might start the Network Time service on a Windows 2000 machine if it were stopped:
use Win32::OLE; $ADsPath = "WinNT://DomainName/ComputerName/W32Time,service"; $s = Win32::OLE->GetObject($ADsPath) or die "Unable to get $ADsPath\n"; $s->Start( ); # may wish to check status at this point, looping until it is started
To avoid potential user- and computer name conflicts, the previous code can also be written as:
use Win32::OLE; $d = Win32::OLE->GetObject("WinNT://Domain"); $c = $d->GetObject("Computer", $computername); $s = $c->GetObject("Service", "W32Time"); $s->Start( );
Stopping it is just a matter of changing the last line to:
$s->Stop( ); # may wish to check status at this point, looping until it is stopped
These examples should give you some idea of the amount of control ADSI from Perl can give you over your system administration work. Directory services and their interfaces can be a very powerful part of your computing infrastructure.
Copyright © 2001 O'Reilly & Associates. All rights reserved.