16—
Windows NT Administration

Administering Windows NT networks is an onerous task, requiring a combination of technical expertise to set up and maintain the machines, and also extreme patience when performing the laborious manual task of dealing with accounts, user groups, and other details. Scripting languages such as Python can help automate this process, leaving the administrator to focus on the important tasks.

This chapter shows how Python can access the Windows NT functionality often sought by an NT administrator. We show how to deal with user accounts, user groups, and information about the servers on the network, and demonstrate how a Windows NT machine can be rebooted programmatically from a remote machine. In addition to the topics discussed in this chapter, you should see Chapter 18, Windows NT Services, for a description of how to use the Windows NT Event Log and Performance Monitor from Python programs. Finally, if you have an existing command-line tool that provides administration functionality Python doesn't remember that you can still execute command-line tools from within Python to help complement the tools it does have. See Chapter 17, Processes and Files, for more details on executing external programs.

All examples in this chapter assume you are running on a Windows NT machine. Many of these examples also will require some degree of administrator access; for example, creating a new user or initiating a server restart all require differing levels of access. If you are in doubt, please see your network administrator.

Working with Users and Groups

The Windows NT API provides a rich set of functions to control Windows NT users and groups. To cope with the large number of attributes stored for users and groups, Windows NT defines different information levels for these objects. Pro-

grams that require only minimal information, such as the user or group name, can specify an information level that returns only this information. Other programs may wish to view all information available for a user, and will specify a different information level when accessing user information.

The Python support for NT users, groups, shares, servers, and so forth is in the module win32net. This module uses dictionaries to set and obtain information about these-objects. When you request information about an object, a dictionary is returned; the information level you specify determines the items in the dictionary. When you create or modify information about a user, you pass a dictionary; the information level you specify determines which dictionary elements are expected to exist.

For example, when working with users, if you specify an information level of 1, the data is in the format defined for PyUSER_INFO_1. If you specify an information level of 102 when dealing with servers, the data is in the format defined for PySERVER_INFO_102. Appendix B, Win32 Extensions Reference, describes the different information levels and the data.

Most of the Windows NT administration functions take as their first parameter the name of a server on which to execute the command. You can pass None if you want to apply the changes to the local machine, but if you are working within a Windows NT domain, you may need to specify the name of a domain controller for that domain. This obviously means you will also need the appropriate permissions on that domain. All examples in this chapter use the local machine, and hence pass None as the first parameter. Also be aware that in a typical Windows NT network, you may find Windows NT Primary Domain Controllers (PDCs), Backup Domain Controllers (BDCs), Windows NT servers, Windows NT workstations, and Windows 2000 machines. Although the APIs are all exposed, you may need to ensure the changes are applied as you expect. For example, as we shall see later in this chapter, working with NT users or groups differs slightly if the changes are applied to the local database, or to the domain.

All strings returned from the Windows NT API functions are Unicode, so for Python 1.5 you may need to convert them to Python strings, using the str() function. When you pass a dictionary to these functions, the strings can be normal Python strings or Unicode objects; Python converts them to Unicode if necessary.

Obtaining Information about a User or Group

To get a feel for this, let's start by querying information about a current user. First, obtain your username:

>>> import win32api
>>> userName=win32api.GetUserName()

And to assist working with the Python dictionaries, you can define a simple helper function to pretty-print the data:

>>> def dump(dict):
�     for key, value in dict.items():
�         print key, "=", str(value)

>>>

So now you can get the user information and pass it to your function to print. Pass None for the first parameter, so this function obtains the information from the local machine. Pass your current username in the second parameter, and request information level 1 in the last parameter, giving the data defined in PyUSER_INFO_1:

>>> import win32net
>>> info=win32net.NetUserGetInfo(None, userName, 1)
>>> print info['name'] # print just the user name
skip
>>> dump(info)
priv = 2
home_dir = c:\winnt\profiles\skip\personal
password = None
script_path =
name = skip
flags = 66049
password_age = 23792806
comment =
>>>

By referring to Appendix B, you can determine the information returned for each information level; however, for a thorough description, you should refer to the Win32 documentation for these functions. Let's experiment with this a little from the interactive prompt:

>>> len(info)
8

Level 1 (PyUSER_INFO_1) has eight items of data. You can try some other levels:

>>> info=win32net.NetUserGetInfo(None, userName, 2)
>>> len(info)
24
>>> info=win32net.NetUserGetInfo(None, userName, 3)
>>> len(info)
29
>>> info=win32net.NetUserGetInfo(None, userName, 4)
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
ValueError: This information level is not supported
>>>

Level 2 provides 24 pieces of data, while level 3 provides 29. There is no information level 4, but if you refer to Appendix B, you will find other information levels supported we don't demonstrate here.

The win32net.NetGroupGetInfo() function is the equivalent for obtaining the information about a group, but instead deals with PyGROUP_INFO structures.

You can also enumerate (i.e., loop over) all users using the win32net.NetUserEnum() function.

entries, total, resume = win32net.NetUserEnum(server, level, filter, resume, len=4096)

server
The name of the server to execute on, or None for the current machine.

level
An integer specifying the level of information requested.

filter
An integer defining the type of accounts to list. A common value for this is FILTER_NORMAL_ACCOUNT, although Appendix B lists the valid values for Windows NT 4.

resume
A value used to control the iteration when there is a large number of users to list. Zero should be passed the first time it is called, and while a nonzero resume result is returned from the function, it can be called again with the new resume value to obtain the next set of the users. An example of this is shown in the following code.

len = 4096
A hint to the Win32 function about how much data to allocate. See the Win32 documentation for more details.

The NetUserEnum() function returns three items:

entries
A list of dictionaries; one for each user returned.

total
The total number of items left to read before making the call. Thus, total-len (entries) is the number of entries left to read after this call.

resume
A resume handle, that can obtain the next set of users. When resume is zero, there are no more items to read.

The parameters and arguments to this function are probably not obvious, and all the win32net enumeration functions follow this pattern, so it's worth discussing at this point. The general idea is that you call this function multiple times, with each call obtaining only a portion of the total data. The resume parameter controls the looping and indicates when there's no more data available. These functions are designed to allow programs to process large sets of data without consuming all the memory on the local machine; however, the key drawback is that the code becomes slightly more complex.

To demonstrate the use of this function, let's write a function that loops over all users, and prints their username and the date and time they last logged on.

If you consult Appendix B, you'll notice that an information level of 2 (PyUSER_INFO_2) includes the fields name and last_logon. So you can do an enumeration at level 2. Also, you should exploit the fact that the Win32 Networking API time values are all integers holding the number of seconds since January 1, 1970, and that this is the same system the standard Python time module uses.

As we mentioned, the looping makes the code more complex than most code working with users and groups, but it's still small enough to type interactively:

>>> import win32netcon
>>> import time
>>> def ReportUsers():
�  resume = 0
�  while 1:
�    filter = win32netcon.FILTER_NORMAL_ACCOUNT
�    data, total, resume = win32net.NetUserEnum(None, 2, filter, resume)
�    for user in data:
�      lastlogon= time.strftime("%c", time.localtime(user['last_logon']))
�      print user['name'], lastlogon
�    if resume==0
�      break
>>> ReportUsers()
Administrator 04/15/99 14:57:13
Guest 01/01/70 11:00:00
skip 04/15/99 15:07:26
VUSR_BOBCAT 10/09/98 15:33:55
>>>

Note the use of the resume parameter. Initialize this to zero before you start the loop. Each time you repeat the loop, you pass the resume result from the previous call. When the result returns zero, you're done.

Creating, Changing, and Deleting Users and Groups

Creating users and groups is a simple process. All you need to do is create a dictionary with the information for the user and call win32net.NetUserAdd() or win32net.NetGroupAdd(). Depending on the information you need to set for the user, the information level can be 1, 2, or 3, corresponding to PyGROUP_INFO_1, PyGROUP_INFO_2, and PyGROUP_INFO_3 respectively. Refer to Appendix B for the dictionary elements in these information levels.

win32net.NetUserAdd(server, level, data)

server
The name of the server to execute on or None for the current machine.

level
The level of information provided in the data parameter.

data
A dictionary with data corresponding to the level.

The simplest way to start is to create a dictionary corresponding to information level 1. Some of the attributes are optional. You can create a new user with the following code:

>>> d={}
>>> d['name'] = "PythonTestUser"
>>> d['password'] = "Top Secret"
>>> d['comment'] = "A user created by some Python demo code"
>>> d['flags'] = win32netcon.UF_NORMAL_ACCOUNT | win32netcon.UF_SCRIPT
>>> d['priv'] = win32netcon.USER_PRIV_USER
>>> win32net.NetUserAdd(None, 1, d)
>>>

Most of the attributes are self-explanatory. The flags attribute specifies the type of account to create. The Win32 documentation states that the use of the UF_SCRIPT flag is necessary. The priv attribute controls the privileges of the new user; you are creating a normal user. See Appendix B for more details on these attributes.

You've now created a user. Let's see if you can read the data for the new user:

>>> win32net.NetUserGetInfo(None, "PythonTestUser", 0)
{'name' : L'PythonTestUser'}

Information level 0 (PyUSER_INFO_0) provides only the username, so the user does exist.

To modify the details for a user, use the function win32net.NetUserSetInfo().

win32net.NetUserSetInfo(server, userName, level, data)

server
The name of the server to execute on or None for the current machine.

userName
The username to change. The name element in the dictionary is ignored.

level
The level of information provided in the data parameter.

data
A dictionary with data corresponding to the level.

The simplest way to fill this dictionary is to call win32net.NetUserGetInfo() with the appropriate information level, modify the returned dictionary, then pass it to win32net.NetUserSetInfo(). This is simple to demonstrate. Let's modify the comment field for the new user:

>>> d = win32net.NetUserGetInfo(None, "PythonTestUser", 1)
>>> d['comment']
L'A user created by some Python demo code'
>>>

Now you have a dictionary, and the comment is just as you created it. Now, update the dictionary and update the user:

>>> d['comment'] = "The new comment for our user"
>>> d = win32net.NetUserSetInfo(None, "PythonTestUser", 1, d)
>>>

Finally, you can check that your data made it by rereading the user information:

>>> win32net.NetUserGetInfo(None, "PythonTestUser", 1) ['comment']
L'The new comment for our user'
>>>

Working with Groups

Working with groups is similar to working with users. The concepts are identical; only the specific data that is used changes. Again, you need to refer to Appendix B to find the exact attributes required in the dictionary for the specific call. We will now make a slight diversion and play with some Windows NT groups while we have a test user.

Add the new user to the standard group named Users. Windows NT doesn't automatically add users to this group, so you need to do it for all new users.

owl.gif Although new users are aren't added to local groups, any new users you create in a domain are automatically added to the Domain Users group.

Windows NT defines two types of groups: local groups are local to the machine, while groups are domain groups. Rather than assume your Windows NT machine is in a domain, you can use local groups with the function win32net.NetLocalGroupAddMembers(). The process for using domain groups is similar, but it employs win32net.NetGroupAddUser().

win32net.NetLocalGroupAddMembers(server, group, level, members_data)

server
The name of the server on which to apply the changes, or None for the current machine.

group
The name of the local group to which the members should be added.

level
The level of the data contained in each element of members_data.

members_data
A list of dictionaries, one for each member to be added. The dictionaries must be one of the LOCALGROUP_MEMBERS_INFO structures, depending on the level parameter.

If you refer to Appendix B, notice that the LOCALGROUP_MEMBERS_INFO levels 0, 1, and 2 all require a user SID, an NT Security Identifier. Although you can work with SIDs via the win32security module, LOCALGROUP_MEMBERS_INFO_3 allows you to specify a domain and username. So the first step is to obtain the current domain name:

>>> import win32api
>>> domain = win32api.GetDomainName()

Now build a dictionary with a single element, domainandname, that is a string in the standard Windows NT username format, Domain\User. This dictionary is stored as the only item in a list:

>>> data = [ {"domainandname" : domain+"\\PythonTestUser"} ]

You can now add the member to the group. Note that you must specify information level 3, since this is the format of the data in your dictionary:

>>> win32net.NetLocalGroupAddMembers(None, "Users", 3, data)

If you wish, you could use the win32net.NetLocalGroupGetMembers() function to list the members in the group to prove the new member is indeed in the group.

Deleting Users

Finally, you can delete the new user with the win32net.NetUserDel() function.

win32net.NetUserDel(server, userName)

server
The name of the server on which to delete the user or None for the current machine.

userName
The name of the user to delete.

Can't get much simpler than that. Let's delete the new user:

>>> win32net.NetUserDel(None, "PythonTestUser")
>>>

Now, check the deletion by trying to read the user's information back in:

>>> win32net.NetUserGetInfo(None, "PythonTestUser", 1)
Traceback (innermost last):
  File "<interactive input>", line 0, in ?
api_error: (2221, 'NetUserGetInfo', 'The user name could not be found.')
>>>

As you can see, the user has been deleted.

Server and Share Information

In many administrative tasks, it's handy to be able to query and change information about particular servers and the resources these servers publish. Working with server and share information is identical in concept to working with users and groups. Each function defines an information level that determines the specific data requested or being set.

Querying Information about Servers

Server information is provided by the PySERVER_INFO_* structures, as defined in Appendix B. PySERVER_INFO_100 provides the lowest level of detail, so let's see what this includes.

First import the necessary modules and read the data for the server. Then print the dictionary:

>>> import win32net, win32netcon
>>> data=win32net.NetServerGetInfo(None, 100)
>>> data
{'name' : L'BOBCAT', 'platform_id' : 500

Notice the platform_id is 500. Windows defines only two platform IDs, one for Windows NT and one for OS/2:

>>> win32netcon.SV_PLATFORM_ID_NT
500
>>>

My workstation is indeed an NT machine: what a relief!

You can also obtain a list of the Windows servers on your network with the win32net.NetServerEnum() function.

entries, total, resume =  win32net.NetServerEnum(server, level, serverTypes= win32netcon.SV_TYPE_ALL, resume = 0, len=4096)

server
The name of the server to execute on or None for the current machine.

level
An integer specifying the level of information requested.

serverTypes
A bitmask of flags indicating the types of servers to list. Appendix B lists common values for this flag.

We don't discuss the resume or len parameters, or the result. Check the previous example for win32net.NetUserEnum() to see how to use these enumerators.

Working-with Share Information

Windows NT defines the concept of a share. A share is a resource published by a machine designed for sharing with multiple users. Shares are usually disk-based shares or printers.

To obtain information about a share or to enumerate the shares available on a Windows NT server, the win32net.NetShare*() family of functions are used, with pySHARE_INFO_* as the corresponding data structures. The process for shares is identical to the process for working with users and servers, as we described previously.

For example, you can use the win32net.NetShareEnum() function to view the shares published by a server. This function is almost identical to the other enumerator functions described in this chapter, so you can use the following code to read the first few shares at your local machine at information level 0:

>>> data, total, resume = win32net.NetShareEnum(None, 0)
>>> for share in data:
�     print share ['netname']


ADMIN$
IPC$
cdrom
C$
c_drive
1_drive
L$
>>>

Note that you haven't looped calling the function, so you get only the first few shares that may be available.

A new share can be created on a server using win32net.NetShareAdd(). This function requires data in information level 2, a pyNET_SHARE_INFO_2 structure. By referring to Appendix B, you can find the data necessary to create a share. The following code shows how to create a share to the local C:\TEMP directory:

>>> data={}
>>> data['netname']="TEMPSHARE"
>>> data['type']=Win32netcon.STYPE_DISKTREE
>>> data['path']="C:\\TEMP"

>>> data['max_uses']=-1
>>> win32net.NetShareAdd(None, 2, data)

The only nonobvious part may be the max_uses element. If you left this at zero, no users could connect to your share. The Win32 documentation states that this should be set to -1 to allow for unlimited uses.

User and Share Sample

We now present a fairly advanced sample of using some of these administrative tools in a real-world scenario.

The problem is that our company has just merged with another medium-sized company. The merger means 250 new user accounts need to be created on the network. This is clearly too many to perform manually when there is a tool such as Python available. It's not a problem to get a text file with the names of the new users, but you need a way to automate the process. The requirements for creating the new users are to:

� Create a new NT user with a default password that must be changed at first logon.

� Create a home directory for the new user with the user information reflecting this as their home directory.

� Create a new share for the user's home directory with the appropriate default permissions. Windows NT also supports connecting the user's home directory to a drive letter, so you need to set the user info to nominate P: to connect to this share at logon.

Of course, this is still a contrived example. Any real-world job will have additional requirements when creating many new users; for example, creating an email account for each new user. In addition, the error-handling requirements depend on the particular task at hand. To this end, and to keep the size of the sample code down, no error handling exists at all. Notwithstanding these restrictions, you should still find the sample valuable when developing your own customized scripts.

Before jumping into the code, there are a few things worth mentioning:

� The requirements state you should specify the home directory as P: and set the password as expired when creating the user. Looking in Appendix B, you'll see that you must use information level 3 (pyUSER_INFO_3) to obtain access to this information. Creating a user at this information level requires you to also set the primary_group_id element to a default value.

� The requirements state that the share must have special security settings. This requires the use of information level 502 (pySHARE_INFO_502). The directory is created without special security. If necessary, you could use win32file. CreateDirectory(), passing a security object similar to that used for the share.

� The handling of the NT security objects is not covered in detail in this book; for further information on Windows NT security, refer to the Windows NT documentation.

� Unlike local groups, new users are automatically added to the domain users group when they are created. Therefore, it's unnecessary to add the user to any additional groups when this code is run against a Windows NT domain.

� You should also provide some code to delete the new users. This is particularly helpful when developing and testing the script; you can delete the users created by the previous run, then re-create them with different settings.

The following code is quite large by Python standards—just over 100 lines, including comments and blank lines:

# BatchUserCreate.py
#
# A sample administrative script to perform a batch
# creation of many users.

# Input to this program is a text file with one user per
# line. Each line contains the new username and the
# user's full name.

# Creates the new user, and a new share on the server
# for the user. The new share is secure, and can only
# be accessed by the new user.
import win32security, win32net, win32file, win32api
import win32netcon, ntsecuritycon
import os, sys, string

# The name of the server to use to create the user.
serverName = None

# The logic for naming the home_drive assumes we have
# a server name. If we don't, get the current machine name
if serverName is None:
    serverName = "\\\\" + win32api.GetComputerName()

# The root of each users personal directory.
# This is a local reference to the directory where each
# personal directory is created.
homeRoot = "C:\\Users"

def CreateUserAndShare(userName, fullName):
    homeDir = "%s\\%s" % (serverName, userName)
    # Create user data in information level 3 (pyUSER_INFO_3) format.
    userData = {}
    userData['name'] = userName

    userData['full_name'] = fullName
    userData['password'] = userName
    userData['flags'] = win32netcon.UF_NORMAL_ACCOUNT | win32netcon.UF_SCRIPT
    userData['priv'] = win32netcon.USER_PRIV_USER
    userData['home_dir'] = homeDir
    userData['home_dir_drive'] = "p:"
    userData['primary_group_id'] = ntsecuritycon.DOMAIN_GROUP_RID_USERS
    userData['password_expired'] = 1 # User must change password next logon.

    # Create the user
    win32net.NetUserAdd(serverName, 3, userData)

    # Create the new directory, then the share
    dirName = os.path.join(homeRoot, userName)
    os.mkdir(dirName)
    shareData = {}
    shareData['netname'] = userName
    shareData['type'] = win32netcon.STYPE_DISKTREE
    shareData['path'] = dirName
    shareData['max_uses'] = -1
    # The security setting for the share.
    sd = CreateUsersSecurityDescriptor(userName)
    shareData['security_descriptor'] = sd
    # And finally create it.
    win32net.NetShareAdd(serverName, 502, shareData)

# A utility function that creates an NT security object for a user.
def CreateUserSecurityDescriptor(userName):
    sidUser = win32security.LookupAccountName(serverName, userName) [0]
    sd = win32security.SECURITY_DESCRIPTOR()

    # Create the "well known" SID for the administrators group
    subAuths = ntsecuritycon.SECURITY_BUILTIN_DOMAIN_RID, \
               ntsecuritycon.DOMAIN_ALIAS_RID_ADMINS
    sidAdmins = win32security.SID(ntsecuritycon.SECURITY_NT_AUTHORITY, subAuths)

    # Now set the ACL, giving user and admin full access.
    acl = win32security.ACL(128)
    acl.AddAccessAllowedAce(win32file.FILE_ALL_ACCESS, sidUser)
    acl.AddAccessAllowedAce(win32file.FILE_ALL_ACCESS, sidAdmins)

    sd.SetSecurityDescriptorDacl(1, acl, 0)
    return sd

# Debug helper to delete our test accounts and shares.
def DeleteUser(name):
    try:    win32net.NetUserDel(serverName, name)
    except win32net.error: pass

    try: win32net.NetShareDel(serverName, name)
    except win32net.error: pass

    try: os.rmdir(os.path.join(homeRoot, name))
    except os.error: pass

if__name__=='__main__':
    import fileinput # Helper for reading files line by line
    if len(sys.argv)<2:
        print "you must specify an options file"
        sys.exit(1)
    if sys.argv[1]=="-delete":
        for line in fileinput.input(sys.argv[2:]):
            DeleteUser(string.split(line,",")[0])
    else:
        for line in fileinput.input(sys.argv[1:]):
            userName, fullName = string.split(string.strip(line), ",")
            CreateUserAndShare(userName, fullName)
            print "Created', userName

To test this code, use a simple data file:

tul,Test User 1
tu2,Test User 2
tu3,Test User 3

To run this script, start a command prompt on an NT server and change to the directory with the script and data file, and execute the command:

C:\Scripts>BatchUserCreate.py userdata.txt
Created tu1
Created tu2
Created tu3
C:\Scripts>

There are now three new users. You can remove them by executing:

C:\Scripts>BatchUserCreate.py -delete userdata.txt

Rebooting a Machine

Occasionally, it's necessary to force a Windows NT computer to reboot programmatically. You may need to perform a scheduled reboot of the current machine or force a reboot of a remote PC programmatically.

The function win32api.InitiateSystemShutdown() appears perfect for the job.

win32api.InitiateSystemShutdown(machine, message, timeout, bForce, bReboot)

machine
The name of the machine to shutdown or None for the current machine.

message
A message to be displayed to the user in dialog while the timeout period expires.

timeout
A timeout in seconds, during which time a dialog is displayed warning the user of the pending shutdown. After the timeout expires, the shutdown process begins. If this is zero, the shutdown commences immediately.

bForce
Specifies whether applications with unsaved changes are to be forcibly closed. If this parameter is true, such applications are closed. If this parameter is false, a dialog box is displayed prompting the user to close the applications. Note that this implies the user could cancel the shutdown process by selecting Cancel in the dialog his application displays for unsaved data.

bReboot
Specifies whether the machine is rebooted after the shutdown process.

Let's try this function. Start by rebooting the current machine with a 30-second timeout (without forcing applications shut) and finally restart after shutdown:

>>> import win32api
>>> message = "This machine is being rebooted because it has been naughty"
>>> win32api.InitiateSystemShutdown(None, message, 30, 0, 1)
Traceback (innermost last):
  File "<interactive input>", line 0, in ?
api_error: (5, 'InitiateSystemShutdown', 'Access is denied.')

This, will no doubt, lead you into messing with the Windows NT User Manager, etc., to try to determine how to get permission to do so. You won't have much luck: everything will indicate you should be allowed to restart the machine. The Win32 documentation for this function briefly mentions:

To stop the local computer from shutting down, the calling process must have the SE_SHUTDOWN_NAME privilege. To stop a remote computer from shutting down, the calling process must have the SE_REMOTE_SHUTDOWN_NAME privilege on the remote computer.

But as far as can be seen, you should have the correct privilege. The answer lies in the fact that user rights and privileges are different things. Your user rights typically allow direct access to securable resources, such as files, printers, or the registry, but access to other system resources requires privileges. Your user rights determine the privileges you hold; but by default, most privileges aren't enabled. Programs must explicitly enable the privilege before they perform an operation that requires the privilege.

Privileges are required for fairly obscure tasks, such as rebooting the local or remote machine, changing the system time, creating machine accounts on the network, or loading device drivers. Our example of rebooting a machine is the only place in this book where you encounter privileges, so we will make a slight diversion at this point. Although we discuss only the privileges required to reboot the local or remote machine, the same concept applies when you perform any operation that requires you to enable special privileges.

Obtaining the Necessary Privileges

The process of enabling new privileges is simple. We will discuss briefly the concepts and the code necessary to enable privileges, but for an in-depth discussion of privileges refer to the Microsoft Windows NT Security documentation.

You use the win32security module to gain access to the necessary functions. The process for enabling a privilege is:

� Obtain the current access token using the win32security.OpenAccessToken() function.

� Obtain the ID for the privilege using win32security.LookupPrivilegeValue().

� Enable the privilege using win32security.AdjustTokenPrivileges().

When you've performed the operation, you need to disable the privilege again. The same process is used: win32security.AdjustTokenPrivileges() supports a flag that allows you to enable or disable the privilege. An example of this code is presented in the next section.

Sample Code to Reboot the Current Machine

You now have the knowledge to successfully make a win32api.InitiateSystemShutdown() call.

The code obtains the necessary privileges to reboot the machine, then makes the call to win32api.InitiateSystemShutdown(). Unfortunately, the dialog displayed by Windows NT doesn't include any way to disable the shutdown operation. Once the shutdown has begun, the only way to stop it is programmatically.

To cater to this, the sample application, shown in the following code, initiates a shutdown with a 30-second delay. The code then sleeps for 10 seconds before programmatically aborting the shutdown using win32api.AbortSystemShutdown(). If you refer to the Windows NT documentation or Appendix B, you'll notice that this function requires the same privileges needed to initiate the shutdown in the first place. So before aborting the shutdown, you must jump through the same privilege hoops. To assist the process, let's move the code that manages the privileges to a helper function that should be suitable for managing any type of privilege:

# RebootServer.py - Reboots a remove server
import win32security
import win32api
import sys

import time
from ntsecuritycon import

def AdjustPrivilege(priv, enable = 1):
    # Get the process token.
    flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY
    htoken = win32security.OpenProcessToken(win32api.GetCurrentProcess(), flags)
    # Get the ID for the system shutdown privilege.
    id = win32security.LookupPrivilegeValue(None, priv)
    # Now obtain the privilege for this process.
    # Create a list of the privileges to be added.
    if enable:
        newPrivileges = [(id, SE_PRIVILEGE_ENABLED)]
    else:
        newPrivileges = [(id, o)]
    # and make the adjustment.
    win32security.AdjustTokenPrivileges(htoken, o, newPrivileges)

def Rebootserver(message="Server Rebooting", timeout=30, bForce=0, bReboot=1):
    AdjustPrivilege(SE_SHUTDOWN_NAME)
    try:
        win32api.InitiateSystemShutdown(None, message, timeout, bForce, bReboot)
    finally:
        # Now we remove the privilege we just added.
        AdjustPrivilege(SE_SHUTDOWN_NAME, 0)

def AbortReboot():
    AdjustPrivilege(SE_SHUTDOWN_NAME)
    try:
        win32api.AbortSystemShutdown(None)
    finally:
        # Now we remove the privilege we just added.
        AdjustPrivilege(SE_SHUTDOWN_NAME, 0)

if __name__=='__main__':
        message = "This server is pretending to reboot\r\n"
        message = message + "The shutdown will stop in 10 seconds"
        RebootServer(message)
        print "Sleeping for 10 seconds"
        time.sleep(10)
        print "Aborting shutdown"
        AbortReboot()

The function AdjustPrivilege() is where you enable the necessary privilege. Notice the specific privilege is passed as a parameter. This makes the function general purpose and so, can be used for any of the Windows NT privileges. Specifically, if you must reboot a remote machine, you should use the privilege SEREMOTE_SHUTDOWN_NAME.

Running this script from Windows NT, you should see the dialog shown in Figure 16-1. Once the countdown timer reaches 20 seconds before shutdown, the dialog should disappear as the shutdown is aborted.

0307-01.gif
Figure 16-1.
Dialog displayed when RebootServer.py runs

Conclusion

Although Windows NT comes with many GUI tools to assist with machine administration, the process is often complicated by site-specific requirements or by the sheer volume of operations that must be performed. This chapter demonstrated some techniques that help automate the administration of a Windows NT Network. We demonstrated how users, user groups, share, and server information can be perused and maintained using Python, and provided example Python programs that can be tailored by an administrator for their particular task at hand.

References

The Microsoft Developers Network (MSDN) is an excellent resource. It's available online at http://msdn.microsoft.com.


Back