Hey, Scripting Guy!The Return of WinRM
The Microsoft Scripting Guys
When the Scripting Guys set out to create a two-part series on Windows® Remote Management (WinRM), one important issue immediately jumped to the forefront: security. After all, we Scripting Guys were well aware of the problems that accompanied the release of the final installment in the Harry Potter series: pre-ordered copies of the books were inadvertently shipped out early and delivered before the book's official release date. (Did that turn out to be a problem? No, of course not; the publishing company just asked people not to read the book until the release date.)
And that's not the half of it. The book and series ending was leaked well before the books were available. (If you haven't read the book yet, here's what happens: it turns out that Harry Potter is some kind of wizard or something!) Likewise, scanned copies of all the pages were instantly available on the Internet, in some cases before author J. K. Rowling had even written those pages. All in all, it was a bit of a security debacle, and the Scripting Guys were determined not to let that happen to them. After all, if people were so determined to leak the finale of the Harry Potter series, imagine the lengths they might go to in order to get their hands on the finale to the Scripting Guys' two-part series on WinRM.
Fortunately, the Scripting Guys were able to clamp down and keep their secrets, well, secret. Granted, this was primarily due to the fact that they didn't actually write the second part of this series until well after the deadline for submitting the article. But hey, the Scripting Guy who writes this column was on vacation during the month of August and, unlike a surprisingly large percentage of Microsoft employees, he never even looks at, let alone uses, a computer while he's on vacation.
OK, so: he rarely looks at, let alone uses, a computer even when he's not on vacation. But that's another story.
At any rate, we know that many of you have been unable to sleep during the past four weeks, tossing and turning and worrying about how the WinRM saga will end. Well, the good news is that those painfully long weeks of sleepless anticipation are over. Here, for the first time ever, is the exciting conclusion to the Scripting Guys' two-part series on WinRM. Well, actually, it's over there in Figure 1.
Figure 1 The denouement
strComputer = "atl-fs-01.fabrikam.com" Set objWRM = CreateObject("WSMan.Automation") Set objSession = objWRM.CreateSession("http://" & strComputer) strDialect = "http://schemas.microsoft.com/wbem/wsman/1/WQL" strResource = "http://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/*" strFilter = "Select Name, DisplayName From Win32_Service Where State = 'Running'" Set objResponse = objSession.Enumerate(strResource, strFilter, strDialect) Do Until objResponse.AtEndOfStream strXML = objResponse.ReadItem Set objXMLDoc = CreateObject("Microsoft.XMLDom") objXMLDoc.async=False objXMLDoc.loadXML(strXML) Set objElement = objXMLDoc.documentElement Wscript.Echo "Name: " & objElement.ChildNodes(0).ChildNodes(0).NodeValue Wscript.Echo "Display Name: " & objElement.ChildNodes(1).ChildNodes(0).NodeValue Wscript.Echo Loop
Yes, we know: the stunning plot twist sent chills up and down our spines as well. objElement.ChildNodes(0).ChildNodes(0).NodeValue! Who saw that coming? Of course, that's because, as part of our security lockdown, even the Scripting Guys themselves had no idea how the series would end. But now the secret is out.
Before we go any further we should quickly recap the series as a whole, just in case anyone out there inexplicably failed to read Part 1 (which can be found at technetmagazine.com/issues/2007/11/HeyScriptingGuy). Back in Part 1, we introduced WinRM, a new technology, found in Windows Server® 2003 R2, Windows Vista®, and Windows Server 2008, that makes it much easier to manage computers over the Internet, even through firewalls. Granted, Windows Management Instrumentation (WMI) has always had the ability to manage computers remotely; however, WMI relies on Distributed COM (DCOM) as its remote management technology. That's fine, except that by default, many firewalls block DCOM traffic. True, you can open the appropriate ports and allow DCOM traffic, but many network administrators are reluctant to do that; their main concern is that opening the door to DCOM will simultaneously open the door to all sorts of malicious mischief as well.
Hence WinRM, "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." Which is just a fancy way of saying that you can now perform remote management using standard Internet protocols like HTTP and HTTPS.
As we noted last month, WinRM makes it easy to connect to and retrieve WMI information from remote computers. So does that mean that this is an absolutely perfect technology? Well, no, not exactly. As we pointed out, when WinRM sends data back to the calling script, that data comes back in XML format. Needless to say, XML can be a little tricky to parse and make use of, especially for system administrators with limited experience in that area. Because of that, WinRM ships with an XSL transform that converts the returned data to a more readable format.
That's great, except that it also means that your output will always look something like this:
Win32_Service AcceptPause = false AcceptStop = true Caption = User Profile Service CheckPoint = 0 CreationClassName = Win32_Service
That's not necessarily a bad thing, except that this also means that your output will always be written to the command window; by default, you can't readily save data to a text file, write that data to a database or Microsoft® Excel® spreadsheet, or do much of anything to that data other than display the information on the screen. That's not good enough.
On top of that, the problem is exacerbated if you opt to return only a selected number of properties for a WMI class (something you might do to help cut down on network traffic). When working with just a few properties of a class—as opposed to all the properties of a class—you get output similar to this:
XmlFragment DisplayName = Windows Event Log Name = EventLog XmlFragment DisplayName = COM+ Event System Name = EventSystem
Interesting, but not the most aesthetically pleasing display of information we've ever seen (especially the heading XmlFragment that appears throughout the report).
But what can you do about that? Write custom code to parse and format that XML data? That doesn't sound like anything a system administration scripter is likely to do. Or is it?
If You Want Something Done Right, Do It Yourself
As it turns out, working with the raw XML data returned by WinRM is nowhere near as daunting a task as it might seem. This month we're going to show you one simple way to parse and format XML data. The approach we'll use is by no means the only way you can work with XML, but that's OK; our primary goal here is just to demonstrate that you don't have to rely on the XSLT transform. Once you grasp that basic idea, well, from then on the sky's the limit as to what you can do with WinRM data.
There is one slight catch here: in order to fit this column into the allotted space, we're going to skip over most of the script we just showed you in Figure 1. That shouldn't be too much of a problem, however, because the first two-thirds of the script were described in detail in last month's column. Instead, we're going to focus on the last third of the script, which is the part where we actually work with the returned data.
As you can see, this episode of the WinRM Chronicles picks up after we've created an instance of the WSMan.Automation object, queried a remote computer (in this case, atl-fs-01.fabrikam.com), and received information about all the services running on that computer. That brings us to the line in the script where we set up a Do Until loop for reading and processing the returned XML data; this loop will continue until there's nothing left to read and process. (Or, to make it sound as though we know what we're talking about, this loop will continue until the time when the XML file's AtEndOfStream property is True.)
This is the line of code we're talking about, and this is where this month's story begins:
Do Until objResponse.AtEndOfStream
Inside the loop, the first thing we do is use the ReadItem method to read the first section of the returned XML data. (Because we're working with WinRM and because we're retrieving service information, this first section will consist of all the data returned for the first service in our collection.) After storing that data (which, again, is in XML format) in a variable named strXML, we then create an instance of the Microsoft.XMLDom object. In effect, that gives us a blank XML document that we can work with:
Set objXMLDoc = _ CreateObject("Microsoft.XMLDom")
As soon as our blank document is ready, we set the value of the Async property to False, then call the loadXML method.
That brings us to—what's that you said? Why do we set the value of the Async property to False? And why do we call the loadXML method? Good questions; if only we'd thought of them ourselves!
For starters, the Async property indicates whether the script allows an asynchronous download of XML information. If this property is True, the download will start and control will be returned to the script immediately, even if the download has not completed. Granted, that sounds like a pretty good deal. Unfortunately, though, your script will then proceed as if it has all the information it needs. If it doesn't have all the information it needs, well, then you're headed for trouble. Thus we set the Async property to False.
Note: OK, you could write code to periodically monitor the status of the download, thus ensuring that the script doesn't rush off prematurely. That works, but a far easier approach is to simply set the value of the Async property to False. When you do that, the script blocks until the download is complete; in other words, once the download begins, the script will wait patiently for the download to finish before it does anything else.
As for the loadXML method, this does pretty much what the name suggests: it loads a well-formed XML document (or document fragment) into our blank document. All we have to do is call the loadXML method, passing the variable strXML as the sole method parameter:
The net result? We've now turned our WinRM data into a virtual XML document. And that means we can start using standard XML methods to parse that virtual document.
To that end, we then must use the following line of code in order to create an object reference to the root element in the XML file:
Set objElement = objXMLDoc.documentElement
At this point we're ready to have some fun. (Assuming, of course, that your idea of fun is parsing an XML file. That's definitely what we Scripting Guys consider fun.)
As you might recall, our WMI Query Language (WQL) query (or, using WinRM terminology, our Filter) looks like this:
strFilter = "Select Name, DisplayName " & _ "From Win32_Service Where State = 'Running'"
As you can see, we've requested two properties from the Win32_Service class: Name and DisplayName. (We've also included a Where clause that limits our returned data to services that are running. However, that's nothing we have to worry about here.) Is it important that we've requested two properties? Is the order of that request important? Maybe. How the heck are we supposed to know that?
Oh, good point. Maybe, as the authors of this article, we really should know the answers to those questions. That's fine. As it turns out, we know that the answer to both of those questions is yes. Is it important that we've requested two properties in our WQL query? Yes it is; after all, those will be the only property values returned to us. (As opposed to doing a Select * From query, which returns values for all the properties of a class.)
So then, is the order of those two properties also important? You bet it is. The order in which we specify property names is the same order in which the property values are returned. That's important because each property value is going to be returned as a child node (or section) of our root element. Which property value will come back as the first child node? That's an easy question. In this case the first child node will be the Name property, because that's the first property listed in our WQL query. Which property will come back as the second child node? That's right—it will be the DisplayName property, because it's the second item listed in our query.
Wait a second. Did you figure that out on your own, or did someone leak an advance copy of this column to you? Hmmmm ...
At any rate, this makes it easy to echo back the value of the Name property. All we have to do is reference the NodeValue of the first item (item 0) in the first ChildNodes (item 0) collection, like so:
Wscript.Echo "Name: " & _ objElement.ChildNodes(0).ChildNodes(0).NodeValue
And how do we reference the value of the DisplayName property? In this case we reference the NodeValue of the first item in the second ChildNodes (item 1) collection:
Wscript.Echo "Display Name: " & _ objElement.ChildNodes(1).ChildNodes(0).NodeValue
And what if we had a third property (say, Status) in our WQL query? In that case we'd simply reference the NodeValue of the first item in the third ChildNodes (item 2) collection:
Wscript.Echo "Status: " & _ objElement.ChildNodes(2).ChildNodes(0).NodeValue
And so it goes until you get to the last property.
So what will our script output look like now? Something like this:
Display Name: Windows Event Log Name: EventLog Display Name: COM+ Event System Name: EventSystem
Granted, this doesn't appear to be all that different from the default WinRM output (although we did get rid of that silly XmlFragment heading). The difference is this: by working with the individual property values, we're no longer limited to the default formatting (note that we use the label Display Name rather than DisplayName), nor are we limited to displaying information only in the command window.
Would you prefer instead to write this data to an Excel spreadsheet? That's easy enough to do. To begin with, insert the following block of code (which creates and configures a new Excel spreadsheet) immediately after the line in the WinRM script that calls the Enumerate method:
Set objExcel = _ CreateObject("Excel.Application") objExcel.Visible = True Set objWorkbook = objExcel.Workbooks.Add() Set objWorksheet = objWorkbook.Worksheets(1) i = 2 objWorksheet.Cells(1,1) = "Name" objWorksheet.Cells(1,2) = "Display Name"
Now, replace the original Do Until loop with the one you see in Figure 2. Give that a try and see what happens.
Figure 2 New Do Until loop
Do Until objResponse.AtEndOfStream strXML = objResponse.ReadItem Set objXMLDoc = CreateObject("Microsoft.XMLDom") objXMLDoc.async=False objXMLDoc.loadXML(strXML) Set objElement = objXMLDoc.documentElement objExcel.Cells(i, 1) = objElement.ChildNodes(0).ChildNodes(0).NodeValue objExcel.Cells(i, 2) = objElement.ChildNodes(1).ChildNodes(0).NodeValue i = i + 1 Loop
WinRM, Part 3?
Between this month's column and last, you should have enough to get started with WinRM. We hope so; WinRM is an intriguing new technology that promises to make remote management of your computers far easier (while maintaining safety and security). Which, of course, leads directly to the question on everyone's mind: does that mean there will be a third installment to the WinRM saga?
Sorry, but we can't reveal that information. Not because of security concerns, but simply because due to the Scripting Guys' usual planning and decision-making process, we have no idea whether we'll write anything else on WinRM. Like they say, stay tuned!
Dr. Scripto's Scripting Perplexer
Doctor Scripto has had a bit of an accident. He left one of his scripts lying around (instead of putting it away like a good scripter), and he accidentally tripped over it, sending scripting pieces flying everywhere. He's managed to find all the variables, keywords, symbols, and such, and he's arranged them nicely in alphabetical order, but now he needs to put them back together to make his script whole again. It's going to take him a while, but he expects to have the answer ready by the time next month's TechNet Magazine comes out. In the meantime, feel free to give it a try and see if you can turn this seemingly random set of scripting pieces into one complete script. Good luck!
Hint: OK, here's a little help. The final script will delete all files on the local computer older than a specified date.
Dr. Scripto's Scripting Perplexer
Answer: Clumsy Doctor Scripto, December 2007
Yes, Dr. Scripto managed to get his script put back together. Here's the reconstructed, working script, which deletes all files on the local computer older than a specified date:
strDate = "20060102000000.000000+000" strComputer = "." Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") Set colFiles = objWMIService.ExecQuery("Select * From CIM_DataFile Where CreationDate < '" & strDate & "'") For Each objFile in colFiles Wscript.Echo objFile.Name Next
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.
© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.