Hey, Scripting Guy!Desktop Management from beyond the Grave
The Microsoft Scripting Guys
Download the code for this article: HeyScriptingGuy2007_11.exe (151KB)
Due to overwhelming popular demand, we thought we'd do something a little different this month: rather than start out by talking about system administration scripting, we're going to start out by—cue the ominous music—telling a ghost story instead!
Note: OK, so technically, if we really wanted to do something a little different this month we'd start out by actually talking about system administration scripting for a change. But just play along with us, OK? Thanks!
Many years ago, one of the Scripting Great-Great-Great Grandmothers passed away. Shortly after Grandmother was laid to rest in her simple wooden coffin, Grandfather began having terrible nightmares, nightmares in which his beloved wife was desperately trying to claw her way out of the grave. After repeated nightmares, and after repeated entreaties, Grandfather finally convinced the local authorities to exhume the body. When the coffin was opened, everyone was horrified to see that Grandmother's nails had been bent back and the inside of the coffin was covered with scratches!
OK, so this story might not be entirely true; in fact, the more we think about it, the more we realize that it isn't the least bit true. Nevertheless, the story has an important lesson to teach us. We have no idea what it is, but it's in there somewhere.
Wait a second, now we remember! Coffins were originally designed to protect the deceased from the elements and to help prevent the body from decomposing. Unfortunately, coffins had an unintended consequence: they make it possible, in theory anyway, for you to bury a person alive and ensure that he can never get out. As the story of the Scripting Great-Great-Great Grandmother clearly shows, even the best-laid plans of mice and men can result in disaster, and in people being buried alive! (Cue the ominous music one more time.)
Note: Unless, of course, you opt for the Improved Burial Case, invented by Franz Vester in the 1860s. This coffin included a string connected to a bell that remained aboveground; in the case of premature burial, the "deceased" could simply ring the bell and summon help. The Improved Burial Case also included a folding ladder, although it's not entirely clear to us how a folding ladder will help you escape from a coffin buried six feet underground. If you happened to be buried on top of someone's garage, sure, a folding ladder would be useful. Otherwise ....
This very same thing (the thing about best-laid plans leading to disaster) is true of Internet firewalls. (Well, sort of.) Firewalls were originally designed to keep the bad guys out: they block incoming network traffic, which helps to keep hackers and intruders away from your computers. That's great, but—like the problem of being buried alive—there's also an unintended consequence here: firewalls can also keep the good guys out. This is especially true in the case of Windows® Management Instrumentation (WMI), which relies on DCOM to perform administrative tasks on remote computers. Firewalls tend to block all incoming DCOM traffic, something that makes it very difficult (if not downright impossible) to programmatically manage computers over the Internet. In fact, without opening up additional ports on the firewall and thus making you more vulnerable to hackers and crackers, this is downright impossible. Unless, of course, you opt for WinRM: Windows Remote Management (folding ladder not included).
What Is Windows Remote Management?
According to the WinRM SDK (msdn2.microsoft.com/aa384426), Windows Remote Management is "the Microsoft implementation of WS-Management Protocol, a standard SOAP-based, firewall-friendly protocol that allows hardware and operating systems from different vendors to interoperate." Impressive, huh? We're not going to discuss the details of the WS-Management Protocol in this month's column, so we recommend reading the WinRM SDK for the details. For now, all we care about is that WinRM is available on Windows Server® 2003 R2, Windows Vista®, and Windows Server 2008, and that it enables you to manage computers over the Internet. WinRM does this using port 80, a standard Internet services port that most firewalls leave open. (However, the port used by WinRM and the default transport mechanism, HTTP, can be changed as needed.)
We also won't spend any time in this month's column discussing how to install and configure WinRM. There's already plenty of information available to help you with that (msdn2.microsoft.com/aa384372). However, we will take a moment to emphasize one important point: if you want to use WinRM to retrieve information from a remote computer (which, of course, is the primary reason for using WinRM in the first place), then both your local machine and the remote computer must be running WinRM.
What does that mean? Well, it means that if you haven't upgraded your client computers to Windows Vista (say it isn't so!), or you haven't upgraded your servers to Windows Server 2003 R2 or Windows Server 2008, you won't find WinRM to be particularly useful, at least not today. Needless to say, however, that probably won't be the case tomorrow. (And, of course, assuming your firewall allows it, you can always use WMI and DCOM to manage remote computers.)
Returning All the Properties and Instances of a Class
But who cares about caveats and disclaimers, right? Instead of all that mumbo jumbo, let's see if we can figure out how to write a script that takes advantage of WinRM. Coincidentally enough, we just happened to have a simple little script that, using the HTTP protocol and port 80, connects to a computer named atl-fs-01.fabrikam.com and then returns complete information about all the services installed on that computer. See Figure 1 for the script in all its glory.
Figure 1 Listing services on a remote machine
strComputer = "atl-fs-01.fabrikam.com" Set objWRM = CreateObject("WSMan.Automation") Set objSession = objWRM.CreateSession("http://" & strComputer) strResource = "http://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_Service" Set objResponse = objSession.Enumerate(strResource) Do Until objResponse.AtEndOfStream DisplayOutput(objResponse.ReadItem) Loop Sub DisplayOutput(strWinRMXml) Set xmlFile = CreateObject("MSXml2.DOMDocument.3.0") Set xslFile = CreateObject("MSXml2.DOMDocument.3.0") xmlFile.LoadXml(strWinRMXml) xslFile.Load("WsmTxt.xsl") Wscript.Echo xmlFile.TransformNode(xslFile) End Sub
As you can see, we start out by assigning the DNS name of the computer (atl-fs-01.fabrikam.com) to a variable named strComputer. Alternatively, we could make the connection using the computer's IP address (or even its IPv6 address). For example:
strComputer = "192.168.1.1"
After assigning a value to strComputer we next create an instance of the WSMan.Automation object, then we call the CreateSession method to connect to the remote machine, in this case using the HTTP protocol (just like we said we were going to do):
Set objSession = objWRM.CreateSession _ ("http://" & strComputer)
As we noted, we want to return information about the services installed on the remote computer. In addition, and at least for this first example, we want information about all the properties of all the services. What does all that mean? That means that we need to specify a URI Resource that binds us to the Win32_Service class on the remote computer:
strResource = _ "http://schemas.microsoft.com" & _ "/wbem/wsman/1/wmi/root/cimv2" & _ "/Win32_Service"
Granted, that's not the prettiest URI we've ever seen. (Although, come to think of it, we're not sure we've ever seen a pretty URI.) Fortunately, though, most of the URI is boilerplate; all you need to worry about is the WMI path at the very end:
That should be pretty straightforward. What if you wanted to connect to the root/cimv2/Win32_Process class? Well, then you just modify the URI path accordingly:
Interested in the root/default/SystemRestore class? Well, once again, just modify the URI class, taking care to specify the default namespace (rather than the cimv2 namespace):
And so on.... It's a bit of a shame that you need to include the http://schemas.microsoft.com/wbem/wsman/1/wmi portion of the URI as well, but....
At this point we're ready to get back some data. To do that, we simply call the Enumerate method, passing the variable strResource as the sole method parameter:
Set objResponse = objSession.Enumerate(strResource)
Will that line of code truly populate objResponse with information about the services installed on the computer atl-fs-01? You bet it will. However, unlike standard WMI scripts, you don't get back a series of objects, each with its own properties and property methods. Instead, you'll get back a big old XML blob that looks a little like what you see in Figure 2.
Figure 2 Big old XML blob
<p:Win32_Service xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p=" http://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_Service" xmlns:ci m="http://schemas.dmtf.org/wbem/wscim/1/common" xsi:type="p:Win32_Service_Type" xml:lang="en-US"><p:AcceptPause>false</p:AcceptPause><p:AcceptStop>false</p:Acce ptStop><p:Caption>Windows Media Center Service Launcher</p:Caption><p:CheckPoint >0</p:CheckPoint><p:CreationClassName>Win32_Service</p:CreationClassName><p:Desc ription>Starts Windows Media Center Scheduler and Windows Media Center Receiver services at startup if TV is enabled within Windows Media Center.</p:Description ><p:DesktopInteract>false</p:DesktopInteract><p:DisplayName>Windows Media Center
If you're an XML whiz, that's no big deal; anyone familiar with XML should be able to parse and output this information without too much trouble (even though, in the words of the WinRM SDK, this information is not in "human-readable format"). But what if you're not an XML whiz? In that case, you have two choices. One, you can wait until next month, when we'll show you a few tricks for working with WinRM's XML. Or two, you can do what we did in our sample script: employ the XSL transform that gets installed alongside WinRM.
The XSL What-form?
An XSL transform is nothing more than a template that describes how an XML file should be displayed. A complete discussion of XSL files goes way beyond what we can do in this month's column—for that matter, even a cursory discussion of XSL files goes way beyond what we have the capacity to do in this month's column. Therefore, we won't try to explain how WsmTxt.xsl (the name of the built-in transform) actually works. Instead, we'll simply show you how you can use that transform in your script.
When you call the Enumerate method, WinRM sends back a stream of XML data. The easiest way to work with this data is to set up a Do Until loop that continues to run until you reach the end of the data stream. That's what we do here:
Do Until objResponse.AtEndOfStream DisplayOutput(objResponse.ReadItem) Loop
As you can see, inside our loop we call a subroutine named DisplayOutput. When we call that subroutine, we pass along the value of the stream's ReadItem method as the subroutine parameter. (As this whole approach implies, the XML stream is sent back in separate pieces rather than as one large blob of data. Our script, in turn, reads the XML data one piece, or one item, at a time.)
Meanwhile, the DisplayOutput subroutine looks like this:
Sub DisplayOutput(strWinRMXml) Set xmlFile = _ CreateObject("MSXml2.DOMDocument.3.0") Set xslFile = _ CreateObject("MSXml2.DOMDocument.3.0") xmlFile.LoadXml(strWinRMXml) xslFile.Load("WsmTxt.xsl") Wscript.Echo xmlFile.TransformNode(xslFile) End Sub
In brief, we start out by creating two instances of the MSXml2.DOMDocument.3.0 object. We load the XML data stream (strWinRMXML) into one object, then we load the XSL file (WsmTxt.xsl) into the other object. At that point we call the TransformNode method to use the information in the XSL file to format and display the data grabbed from the XML stream.
Yes, it's a little confusing. But at least the output (while far from perfect) is a bit easier to read (see Figure 3).
Figure 3 A tidier version of the XML
Win32_Service AcceptPause = false AcceptStop = true Caption = User Profile Service CheckPoint = 0 CreationClassName = Win32_Service Description = This service is responsible for loading and unloading user profiles. If this service is stopped or disabled, users will no longer be able to successfully logon or logoff, applications may have problems getting to users' data, and components registered to receive profile event notifications will not receive them.
Like we said, this is good, but it's not necessarily great, which is all the more reason to tune in next month, when we'll show you a few ways to manipulate the XML output by yourself.
Returning Selected Instances and Properties of a Class
Needless to say, this is all very cool, except for one thing: it might not be fully reflective of the way you typically do your job. Yes, there will be times when you want to return all the properties of all the instances of a class; however, there will also be times (perhaps many more times) when you want to return only selected properties or instances of a class. For example, you might want to return only information about services that are running, something you do in a regular WMI script by using code similar to this:
Set colItems = objWMIService.ExecQuery _ ("Select * From Win32_Service " & _ "Where State = 'Running'")
That's nice. But how in the world are you going to modify your Resource string to make it equivalent to that?
Well, to be perfectly honest, you aren't going to modify your Resource string to make it equivalent to the ExecQuery statement. You will definitely need to modify the Resource string, but you'll need to do a few other things as well.
With that in mind, let's take a peek at Figure 4. This is a WinRM script that returns information about the services that are running on a computer (as opposed to all the services installed on that computer).
Figure 4 Finding running services
strComputer = "atl-fs-01.fabrikam.com" Set objWRM = CreateObject("WSMan.Automation") Set objSession = objWRM.CreateSession("http://" & strComputer) strResource = "http://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/*" strFilter = "Select * From Win32_Service Where State = 'Running'" strDialect = "http://schemas.microsoft.com/wbem/wsman/1/WQL" Set objResponse = objSession.Enumerate(strResource, strFilter, strDialect) Do Until objResponse.AtEndOfStream DisplayOutput(objResponse.ReadItem) Loop Sub DisplayOutput(strWinRMXml) Set xmlFile = CreateObject("MSXml2.DOMDocument.3.0") Set xslFile = CreateObject("MSXml2.DOMDocument.3.0") xmlFile.LoadXml(strWinRMXml) xslFile.Load("WsmTxt.xsl") Wscript.Echo xmlFile.TransformNode(xslFile) End Sub
At first glance this might appear all but identical to the first WinRM script we showed you; however, there are some very important differences. For one thing, take a look at the value we assign to the Resource string:
strResource = _ "http://schemas.microsoft.com" & _ "/wbem/wsman/1/wmi/root/cimv2/*"
Notice that, when writing a filtered query, we don't specify the actual name of the class (Win32_Service) that we want to work with; instead, we simply connect to the namespace (root/cimv2) where that class resides. Just remember to tack the asterisk (*) onto the end. If you don't, the script will fail with the resulting message "... the class name must be '*' (star)," which simply means that you need to make the class name *.
In addition, we also need to define a Filter and a Dialect:
strFilter = _ "Select * From Win32_Service " & _ "Where State = 'Running'" strDialect = _ "http://schemas.microsoft.com" & _ "/wbem/wsman/1/WQL"
The Filter should be easy enough to figure out; that's where we put our Windows Management Instrumentation Query Language (WQL) query ("Select * From Win32_Service Where State = 'Running'"). The Dialect, meanwhile, is the query language used when creating the Filter. At the moment there's only one query language allowed: WQL. Nevertheless, the Dialect must be specified or the script will fail with the note that the "filter dialect ... is not supported."
Note: interestingly enough, the error message suggests that you remove the Dialect when you call the Enumerate method. That's a recommendation that you shouldn't follow. When doing a filtered query, the Dialect must be specified and must be WQL. Period.
The only other change we need to make occurs when we call the Enumerate method. At that point we need to pass along the variables representing the Filter (strFilter) and the Dialect (strDialect) as well as the variable representing the Resource (strResource):
Set objResponse = _ objSession.Enumerate _ (strResource, strFilter, strDialect)
Give that a try and see what happens.
Now, what about returning only selected properties of a class? For example, suppose you are interested in returning just the Name and DisplayName of all the services running on a computer. What then?
Well, in a case like that you can try to manipulate the XML so that only the Name and DisplayName are displayed. That's possible, but definitely a bit tricky. An easier way to do this is to specify just those properties when assigning the Filter:
strFilter = _ "Select Name, DisplayName " & _ "From Win32_Service " & _ "Where State = 'Running'"
Do that and all you'll get back are the Name and DisplayName of each service, like so:
XmlFragment DisplayName = Windows Event Log Name = EventLog XmlFragment DisplayName = COM+ Event System Name = EventSystem
Granted, the formatting is a bit goofy. (What is the deal with that XmlFragment stuff?) All the more reason to tune in next month.
Wait for It
With any luck, that should be enough to get you started with the wild and wonderful world of WinRM. Of course, we couldn't end a column on WinRM without mentioning the "waiting mortuaries" that were once prevalent throughout Germany. In cities with waiting mortuaries, corpses weren't immediately buried. Instead, they were placed in warm rooms with a number of strings and wires attached to their fingers and toes. The idea, of course, was that the slightest movement would trigger an alarm and summon help. The bodies were kept in these waiting mortuaries until it became evident that these people were truly beyond hope and would never do anything ever again.
Now that you mention it, a waiting mortuary is very similar to being assigned to the Scripting Guys team, isn't it? Of course, no one ever came back to life in any of the waiting mortuaries, whereas the people assigned to the Scripting Guys team ...
Dr. Scripto's Scripting Perplexer
In June 2007, the Scripting Guys attended the Tech•Ed 2007 conference in Orlando, Florida. Not content to just attend the conference, we decided to have some fun. Not only that, but we thought everyone else could use some fun, too. With that in mind we came up with Dr. Scripto's Fun Book, a booklet filled with scripting-related puzzles and various other bits of information. We teamed up with TechNet Magazine—meaning we talked them into giving up a tiny corner of their booth in the Expo Hall—and handed out Fun Books to anyone who wandered by.
As it turned out, the Fun Book was a pretty popular item (maybe not as popular as the Dr. Scripto bobblehead dolls, but close). The fine and opportunistic people at TechNet Magazine, seeing one more way they could capitalize on the Scripting Guys's success (since the Scripting Guys never seem to be able to capitalize on their own success), asked us to create some puzzles for them. When Scripting Guy Jean Ross turned her back for just a moment, Scripting Guy Greg Stemp said, "Sure, we'll do that!" And here we are: Dr. Scripto's Scripting Perplexer. Enjoy.
In this puzzle, all the letters in the top section unscramble to create a script (in VBScript). But don't worry, you don't have to unscramble the whole thing; instead, you simply need to unscramble one column at a time. The letters in each column of the top section fill in the blank spaces in the same column of the bottom section. Here's an example:
As you can see, in column 1 we have the letters S, C, and T. Those three letters belong in the grid below in some unknown order. But when all the letters are dropped down in the proper order, the bottom grid—read left-to-right—becomes something logical. Take a look at the solution:
You can see that the letters S, C, and T in column 1 move down in the order T, S, and C. These turned out to be the first letter in each word of "The Script Center." The actual puzzle is a little more difficult because, well, it's longer and because the final result is a full script.
Hint: the final script starts out with a full path to a file then parses out and displays just the file name.
Dr. Scripto's Scripting Perplexer
Answer: Drop-In Scripting, November 2007
In this puzzle, you needed to drop characters within a column into the correct boxes below so that the bottom rows created a script. Here's the standalone script:
name = "C:\Scripts\Test.txt" arr = Split(name, "\") index = Ubound(arr) Wscript.Echo "Filename: " _ & arr(index)
Here's what it looks like in the puzzle grid:
The Microsoft Scripting Guys work for—well, are employed by—Microsoft. When not playing/coaching/watching baseball (and various other activities) they run the TechNet Script Center. Check it out at www.scriptingguys.com.