Andrew Clinick
Microsoft Corporation
June 12, 2000
Download the source code for this article (78.9K)
The following article originally appeared in the MSDN Online Voices "Scripting Clinic" column.
Luckily for me, this month's "Scripting Clinic" coincides with Tech·Ed, where the Windows Script Team is giving presentations on Windows Script Host (WSH) and Version 5.5 of JScript® and Visual Basic® Scripting Edition.
I thought I'd take this opportunity to expand on my "Administering Windows with Windows Script" Tech·Ed presentation, and give some more in-depth explanations of how the demos work. Windows Media shows of this session and Peter Torr's "Windows Script for Developers" presentation are available on the MSDN Web site, so you can take a look at them as well.
Just to round things off, I'm also including some photos I took of the script presentations and some key folks in the Windows Script community who flew into Orlando (some from as far away as New Zealand—now that's what I call dedication!).
This presentation is an overview of how Windows Script and the scriptable technologies in Windows 2000 make administration easier than ever. I've touched on these subjects in prior articles ("If It Moves, Script It" and "Windows 2000: Script Heaven"), so I recommend taking a look at those articles in conjunction with this column. The key to the presentation is the two major demos that we put together:
The slides are worth looking at—but this session was the last one of the week at Tech·Ed, and people were experiencing severe PowerPoint overload by that point. We needed to keep their attention somehow, and every one loves a good demo.
This script was developed to help administrators deal with the Monday morning rush when Human Resources (HR) sends them a list of 60 new hires that need accounts set up by 9 A.M., and the list arrives at 8:55 A.M. Each user needs:
This is quite a task, and it would require the administrator to move among a number of applications to achieve the desired result. It would certainly take longer than five minutes for 60 users—a perfect scenario for script to save the day.
There are two parts to the script: an Excel spreadsheet and the script that runs using the spreadsheet. You can get both by downloading the code for this article. I chose Excel because it's pretty simple for HR to use and Excel documents can easily be distributed (via e-mail for example). As an alternative, I considered a Web site that would result in some XML being sent to the administrator—but Excel is a simple way to do this, and it works pretty well.
View Figure 1. The Excel spreadsheet
When launched, the script first checks to see which domains and exchange servers it needs to talk to. We wanted to make our demo reasonably fool proof (you'd be amazed how your ability to remember things disappears once you're up in front of 6,500 people), so if you don't pass any arguments to the script, it prompts for the required ones with default values. The default values I have in the script are based on the setup I had for my demo domain, the infamous scripthappens.com. Be sure to change those to values that are appropriate for your organization. For more information on the machine setups, see the machine setup section.
Once all the parameters for the script are set up, the script checks to see which domain it's going to use, and stores that information for later use. It does this by using the Active Directory Services Interface (ADSI) Lightweight Directory Access Protocol (LDAP) provider. When calling LDAP with the name of the computer on which the script is being run, it returns an object that the script can query for the domain to be used. Once you have the account domain, you can then bind to the LDAP provider again to pull further information needed for adding users and so forth.
' ---------------------------------------------------------------------------------------------- ' Get the Default Domain name where we will create Windows 2000 users ' based on the computer name given Set objRoot = GetObject("LDAP://" & strAccountComputer & "/RootDSE") strAccountDomain = objRoot.Get("DefaultNamingContext") strRootDomain = objRoot.Get("RootDomainNamingContext") Set objRoot = Nothing ' Bind to the current account Domain to get its Netbios name Set objDomain = GetObject ("LDAP://" & strAccountDomain) ' The domain name will be used to set access rights on directories strAccountNBDomain = UCase (objDomain.get ("name")) Set objDomain = Nothing ' Bind to the root domain to get its canonical name for UPN contruction Set objRootDomain = GetObject("LDAP://" & strRootDomain) ' Retrieve a constructed property, so first we do a GetInfoEx (for UPN construction) objRootDomain.GetInfoEx Array("canonicalName"), 0 strRootDNSDomainName = objRootDomain.Get("canonicalName") ' Remove the / at the end strRootDNSDomainName = Mid (strRootDNSDomainName, 1, Len(strRootDNSDomainName) - 1)
Once the domain information has been gathered, the script has to load up the Excel spreadsheet that contains information about the new hires, then pull out the data to use in the script. This is achieved by creating an instance of Excel via createobject, then using the Excel object model to retrieve results from a predefined range in the spreadsheet.
' ---------------------------------------------------------------------------------------------- ' Start the Excel Worksheet reading ' Bind to an Excel worksheet object Set objXL = WScript.CreateObject("EXCEL.application") Wscript.Echo "Reading Excel datasheet '" & strExcelSheetPath & "'" Wscript.Echo ' Make it visible 'objXL.Visible = True ' Open the desired workbook containing the users list objXL.workbooks.open strExcelSheetPath ' Activate the named page "Export" objXL.sheets("Export").Activate ' Put the cursor on the A2 cell objXL.ActiveSheet.range("A2").Activate
Once the range has been returned, the script can iterate through the row in the range and pull the user information from each cell in the row:
' Until we run out of rows Do While objXL.activecell.Value <> "" ' -------------------------------------------------------------------------------------- ' Parameters for the user's mailbox creation under LDAP: namespace strFirstName = objXL.activecell.offset(0, 0).Value strLastName = objXL.activecell.offset(0, 1).Value '... More extraction of info here see the script for exact details
For each row, the script takes the information from the spreadsheet and uses ADSI to create a domain user. This is a reasonably simple process of calling the LDAP provider with the right information to create a new user. The key to this is the moniker for the user:
' Create the user in the given domain strUserPath = "LDAP://" & strAccountComputer & _ "/CN=Users," & strAccountDomain Set objUserContainer = GetObject(strUserPath)
With this information, ADSI can query the Active Directory to see whether the user already exists. If the user does exist, then HR made a mistake (surely not!) and the script gracefully exits. If no user exists, a new user can be created using the create method from ADSI and setting a whole bunch of properties on the user with the information from the spreadsheet. Once all the properties have been set, the script tells ADSI to commit the changes to the directory via the setinfo method, and the user is created.
Once the user has been created, the script checks to see whether Exchange 2000 has been installed on the domain. If it has, the script adds the user to Exchange as well. Adding a user to Exchange is pretty simple, because ADSI also provides a mechanism for talking to Exchange. The only real difference is that, rather than passing in a domain to the LDAP provider, you pass in the Exchange Organization. Once you query ADSI with the Exchange Organization, adding a user is pretty much identical to adding a user to Active Directory. In fact, it is creating a user in Active Directory, but this time with Exchange user information (the joys of an integrated directory!). The properties for an Exchange user are a little different, including X400 addresses (which this scripter hopes he never understands!), nicknames, and so forth. You can see how the script does this in the source code's EnableE-mailFunction.vbs include file.
Once users have been set up correctly, they need to have home folders and Web v-roots. These are set up by using a combination of ADSI and the FileSystemObject.
The script creates a folder using the createfolder method on the FileSystemObject, then calls ADSI to create a share. To create a share, the function uses the WINNT provider to ADSI, and calls the create method on it. This is just like calling the create method to add a user; the only difference is that you get a different ADSI object because you've passed in a different moniker. This time round, we pass in WINNT://machinename/lanmanserver rather than LDAP://domain/user, etc. Assuming that no share already exists, the script creates a share for the user.
Creating a Web share is just an extension of the file share, since it, too, uses FileSystemObject and ADSI. Each user will have a myweb folder in his home folder, which will be used to create a v-root with the naming convention http://machinename/username on the corporate Web server. This means the actual hierarchy of myweb is never exposed from the server—and, hopefully, it's a little easier for the user to understand. Setting up a v-root in Internet Information Services (IIS) is achieved by using the IIS handler for ADSI. Again, you just query ADSI with the path to the folder by using the IIS ADSI handler, which returns an object that you can query (e.g., IIS://servername/sharename). Assuming that no share already exists, the script calls the create method on the returned object.
Once the v-root has been set up, the script creates a home page for the user that displays information about the user (phone number, and so on). This information is the same data that came from the spreadsheet, which is now also in the Active Directory. You could write an Active Server Page (ASP) file that queried ADSI for the information every time you went to the vroot—but that's probably a bit of overkill, since the page is really just a placeholder and the user will probably replace it pretty quickly. As a result, the script creates a static Web page with the information and saves that into the myweb folder.
To get the information into a Web page, we use the resource feature of WSH 2.0 and the replace function in VBScript. The resource element contains the HTML that will be used to create the page with the fields that will be replaced with the user information; the fields are delimited by %'s. The great thing about using the resource element is that you can put the HTML in a separate file, allowing HR folks to update the page easily without having to touch the script. If you're planning to use strings within a WSH Script, I highly recommend the resource element. The script loads the string from the resource using the getResource function available within a .wsf file (and .wsc, if you're familiar with Windows Script Components). The script then uses the VBScript replace function to go through each user information field from the spreadsheet and update the HTML page. Once the replace has finished, the page is saved to the folder as default.htm.
Once the user has been successfully added to the domain and to Exchange, and the home page is set up, the script sends the user a message that everything is done. To send the mail, the script uses the resource element and Collaborative Data Objects (CDO). The text of the e-mail is contained in the "WelcomeLetter" resource, and uses the same format as the HTML version, with %-delimited fields. The fields are replaced to build the e-mail, which tells the user where to get help and provides a list of the information that has been entered for them. Once the mail is ready to send, the script creates an instance of CDO, and calls the send method to send the mail.
There are two people I can't thank enough for getting this script going for Tech·Ed. First of all, Allain Lissoir from Compaq did a fantastic job of building the base script with ADSI. He goes into much more depth with his white papers. Allain's script was the basis for this demo, and we're grateful to him for all his work in the Windows Scripting arena. Finally, Mike Whalen, a developer on the Windows Script Host team, helped me out considerably by implementing my vague ideas so elegantly in the script.
Setting up the user is only half the battle for system administrators. Once those new users can log on to the system, many of them will be phoning up helpdesk with questions about why their machine isn't working. To manage this and make it easier for a system administrator to keep some form of sanity, I put together a Web site using Windows Script, ASP, NetMeeting Remote Desktop sharing, and Windows Management Instrumentation. Some key things I'd like to point out here:
The Administrative Sanity Workbench is a Web site with a main page that divides the process of helping users into three steps:
The main page that shows up when you navigate to the Administrative Sanity Workbench is a simple HTML page with a table for layout, an input box for the machine name, an <IFRAME> (which is used to show the information about the user's machine), an object tag for embedding the Netmeeting control onto the page, and a plain old HTML button for rebooting the machine.
When the page is loaded, it uses some script event handlers to hook up script to the buttons on the page. The first script that runs is the onclick handler for the Get Machine Info button. When the user clicks the button, a simple VBScript (it could easily be JScript) calls the navigate method on the <IFRAME> (frmInfo), which is on the page passing in the URL for the machineinfo.asp file and giving the machine name as an argument to the page. The navigate method causes the <IFRAME> to reload itself with the content returned by machineinfo.asp, which will be the machine information for the specified machine in the input box.
The machineInfo.asp file utilizes WMI to query the user's machine for its current running information. WMI allows script to query remote machines, so the script is actually running on the administrator's Web server and communicating over the network to the end user's machine. The code in machineinfo.asp is actually pretty simple. First, it displays a title for the page using the machine name passed in when the <IFRAME> loaded in the page. It does this by using the Request.Querystring collection and, specifically, the machine variable.
Once the title has been displayed, the main part of the script runs. To query a machine about its current state, the script must first connect to the machine using WMI. This is achieved by using the WbemScripting.SwbemLocater object. This has a connectServer method, which returns an object that can be queried using standard SQL syntax, making the machine look like a database.
Note: There are a number of ways to query machines in WMI, but I like the SQL interface.
Once the script creates the WbemScripting object, it calls the connectServer method with the machine name passed into the page, then sets the security impersonation level to 3 (which means the calls to the object will impersonate the user who created the object). Security is obviously important with WMI, since you don't want just anyone looking at your machine—or worse, changing settings. To enforce security, WMI will allow only users with administrative privileges to query WMI information. The Administrative Sanity Workbench further enforces this by turning off anonymous access to the Web site and enforcing an admins-only policy. I suggest you do something similar if you build a similar workbench.
Set objLocator = CreateObject("WbemScripting.SWbemLocator") Set objService = objLocator.ConnectServer (Request.QueryString("machine"), "root/cimv2") ObjService.Security_.impersonationlevel = 3
Once the connection to the machine has been established, we can start to query it for the information that we require. First, machineinfo.asp will return some general operating system settings, including the OS that the machine is running, the specific OS version, and how much memory is available. To get this information, the script builds a simple SQL query:
Select * from Win32_OperatingSystem
It then calls the execute method on the objservice object returned earlier by the ConnectServer method call. Typically, there will be only one running operating system on a machine. (I can't imagine how you'd have two or more operating systems running simultaneously on one machine, but who knows in the future.) To make things reasonably easy, the script iterates through the collection of operating systems returned by the query and writes out the values for Caption (description of the OS), Version (version of the OS), Freememory, and Freevirtual memory. You'll notice that the script calls formatnumber on the memory results to make the return values a little more humanly readable.
The machineinfo page also returns information about the hard disks on the machine and the processes running. Getting this information is simply a case of changing the SQL query and executing it on the WMI object. The Disk query is:
Select FreeSpace, DeviceID From Win32_LogicalDisk
The Process query is:
Select processid, name, executablepath From Win32_Process.
The disk information is shown by iterating through the drive collection returns, and writing out the disk name and freespace properties on the drive object.
The processes list does essentially the same, but with the addition of a hyperlink at the end of each line to show a process. This hyperlink allows the administrator to kill a selected process. The hyperlink calls kill.asp, and passes in the machine name and the process ID. The kill.asp file uses the same WbemScripting object and queries the machine for the processes passed in. When the query is executed, the script calls the terminate method on the process object returned, and the process is killed.
Set objLocator = CreateObject("WbemScripting.SWbemLocator") Set objService = objLocator.ConnectServer (Request.QueryString("machine"), "root/cimv2") ObjService.Security_.impersonationlevel = 3 'Set the query string. strQuery = "Select processid, name, executablepath From Win32_Process where processid=" _ & request.querystring("pid") dim objProcesses Set objProcesses = objService.ExecQuery(strQuery) for each obj in objprocesses x = obj.terminate next
With some reasonably simple script, machineinfo.asp provides a pretty powerful page that can help to diagnose and solve users' problems.
Step One is great if there is an obvious problem on the machine, but quite often it's impossible to determine the problem without actually going to the machine. In the past, this meant physically getting up and going to the box. There are obvious downsides to this:
Luckily, Netmeeting 3.01 has a solution that makes this pretty simple: Remote Desktop Sharing. This feature allows you to share out your entire desktop and enables certain users to take control of it if you want them to. Netmeeting ships with Windows 2000, Window 98 Second Edition, and Windows Millenium Edition, and is downloadable for Windows 95, Windows NT 4.0, and Windows 98.
To take advantage of Remote Desktop Sharing, the Administrative Sanity Workbench integrates Netmeeting so that an admin can just click the call button, and Netmeeting automatically connects to the end user's machine. Integrating Netmeeting into an HTML page is simple: Just add the object, then add some script event handlers for the onclick event for the Call User and Hang up buttons. To call the user, only one line of script is required:
sub btnCall_onclick() netmeeting.callto(txtmachine.value & "+secure=true") end sub
To end the call, the script calls the leaveconference method on Netmeeting.
TIP: If you want to use Netmeeting from Windows Script Host, you need to create an instance of Netmeeting. To do this, call createobject("Netmeeting.application") in VBScript or create a new ActiveXObject("Netmeeting.application") in JScript. Once you've done that, the code is the same as in HTML.
Once a call has been accepted, you can control a user's desktop and take a look at what the problem might be. The great thing about this is that you can guide the user through the problems he or she is having. This is a boon for anyone who's tried to explain, over the phone, how to get things to work. I use it to help out some of my more technically challenged family members who call me for technical support. If you suffer from this, you might want to try it out, too.
There comes a time when a user just can't be helped, and some extreme measures must be taken for your sanity and quite possibly theirs as well. To implement this, the Admin Sanity Workbench includes the ultimate tool, reboot.asp. It's a simple page that just takes the machine name, calls WMI to get the operating system that's running, then calls the Shutdown method on it.
objInstance.Win32ShutDown(CONST_FORCE_REBOOT)
Simple, but deadly efficient.
There are some setup requirements to get these scripts to work on your machines. You'll need a Windows 2000 Domain Controller set up with Active Directory, and you'll need at least one machine running Windows with Windows Script 5.1 installed (Windows 2000 Professional already has this, as does any machine with Internet Explorer 5.01 installed). To take advantage of the Exchange features, you'll need the latest release candidate of Exchange 2000 (Beta 2 should work, but I ran with the latest release candidate to be safe). On the server, you need two shares, Home$ and profile$. To use the script unchanged, the server should be called ScriptDemo; the domain, scripthappens—and all the files should be placed in a folder c:\demo. Changing these values will be pretty simple, so you might want to do that rather than set up your infrastructure the same as mine.
We hope that these scripts give you a head start in managing your Windows installations, and that they provide some good pointers as to what's possible with a little script. Please feel free to enhance the scripts as you see fit and send in your improvements to the newsgroups at news://msnews.microsoft.com/microsoft.public.scripting.wsh.