Inventorying Windows XP Service Packs - Part 1

By The Microsoft Scripting Guys

Doctor Scripto at work

Bowing to the demands of an adoring public, the Scripting Guys have decided to launch a new column drawing on the talents of our virtual Scripting Guy, Dr. Scripto. You've heard him sing, you've heard him tell atrocious jokes: now meet the deeply technical side of the little guy in the lab coat. Walk with him through his vast virtual warehouse of spare script parts and widgets, located in a closet of our luxurious penthouse suite high atop the Microsoft campus in Redmond.

Doctor Scripto's Script Shop will talk about practical system administration scripting problems, often from the experiences of our readers, and develop scripts to solve them. Most of our examples in the Script Repository perform one simple task; in this column, by contrast, we'll put those pieces together into more complex scripts. They won't be comprehensive or bullet-proof. But they will show how to build scripts from reusable code modules, handle errors and return codes, get input and output from different sources, run against multiple machines, and do other things you might want to do in your production scripts.

Many of the topics discussed in this column will involve intermediate to advanced scripting techniques. But we will try to explain them clearly and, as always, leave no scripter behind. We hope you will find these columns and scripts useful – please let us know what you think of them, what solutions you've come up with for these problems, and what you'd like to see covered in the future.

On This Page

Inventorying Windows XP Service Packs – Part 1
Inventorying non-Active Directory networks
Getting a list of computers from a text file
Retrieving operating system version and Service Pack
Sorting the computers by service pack
Outputting data to a text file
The Big Enchilada
For more information

Inventorying Windows XP Service Packs – Part 1

April 12, 2005 is looming. It's not a holiday (well, it might be somewhere, but we don’t get the day off work so as far as we’re concerned it’s not), but it is a day freighted with significance. It looks like an ordinary Tuesday on most calendars, but for system administrators who manage Windows XP clients, it's a day of reckoning.

That's because as of April 12 you will no longer be able to disable delivery of Windows XP Service Pack 2 from Automatic Update and Windows Update Services (details at Temporarily Disabling Delivery of Windows XP Service Pack 2 Through Windows Update and Automatic Updates). Currently you can prevent Automatic Update from automatically downloading Service Pack 2 to Windows XP clients by setting a value in the registry – we talked about how to do this in Block Windows XP Service Pack 2.

From April 12 on, however, this registry setting will be ignored. Or, in the ominous words of Madame de Pompadour (mistress and system administrator of Louis XV), "Après nous, le Service Pack 2."

For scripters, on the other hand, April 12 is game time. It's a sterling opportunity to show how useful scripting can be in managing a potentially thorny mass upgrade. And believe me, this opportunity is not lost on Dr. Scripto.

The Scripting Guys' guru and consigliere has been rummaging through his collection of used and remanufactured code, looking for choice bits to weld together into the ultimate script to manage the irresistible arrival of SP2.

Now, don't get us wrong: Service Pack 2 has very useful features to strengthen security, such as Windows Firewall. But these same features can complicate the lives of system administrators until we figure out how to manage them. And the default settings of Service Pack 2 lock out remote administration with WMI and ADSI, a capability that many of our system administration scripts require. We've already talked about some of these issues in previous columns and webcasts, which are listed at the end of this article.

We got the idea for this column from Jeremy, a consultant for a county in southern California who had the foresight to send us e-mail on this topic a few months ago. He had installed Service Pack 2 on some of the clients running Windows XP on his network, and wanted to write a script to find the remaining machines that needed to be upgraded. So we began a discussion and came up with some ideas.

Regardless of how your IT organization plans to handle Service Pack 2, an inventory is a good place to start. Once you've figured out which computers are running which operating system versions and service packs and know the scope of the issue, you’ll have a better idea of the impact this update will have on your systems and you can handle it in an orderly way. For example, you could phase in SP2 installations so that your network is not swamped on April 12. And you could change the settings of the new firewall to meet the needs of your IT organization, rather than accepting the defaults.

To help you with this, Dr. Scripto has put together a couple of scripts that can hunt down the Service Pack status of all Windows XP clients on different kinds of networks.

After spec'ing out the scripts (don't you always write specifications before coding?), we realized that showing you how to prepare for Windows XP SP2 would require at least three columns. This first one shows how to inventory Windows XP computers on a workgroup or network that does not use Active Directory. In the next column, we'll show scripts that do the same work in Active Directory, using both Active Directory Service Interfaces (ADSI) and ActiveX Data Objects (ADO). Then, for the grand finale, we'll talk about how to use the inventory you've taken to deploy SP2 in ways that permit you to continue managing your network remotely.

After that, this will become a regularly updated column that will move on to tackle other scripting jobs.

Inventorying non-Active Directory networks

For starters, we’re going to explore the simplest scenario: taking inventory on a network that doesn't use Active Directory. In this case, we use WMI to find out the operating system and service pack versions. This method isn't as fast as those we can use with Active Directory, which stores this information in the directory so that we can retrieve it without connecting to each machine. But WMI has the virtue of being a technology that's familiar to most Windows scripters, and this approach will work fine on small networks.

Since we’re not using Active Directory, we have to feed the script a list of computers to search. We'll use a text file because it's simple, but we could also use a spreadsheet or database. And for symmetry, we'll also output the results to another text file.

Getting a list of computers from a text file

Doctor Scripto at work

Let's call our input text file hosts.txt and put one computer name per line. These computers need to be accessible on the network and you must have Administrator privileges on them (as is usually true when you run a script against a remote machine). Our file should look something like this:

client1
client2
client3
client4

We extract the computer names from the file with the trusty FileSystemObject, part of Script Runtime (included with Windows Script Host). Here's what the code looks like (we hope this will be sleep-inducingly familiar to many of you).

Const FOR_READING = 1
strFilename = "hosts.txt"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextStream = objFSO.OpenTextFile(strFilename, FOR_READING)
Do Until objTextStream.AtEndOfStream
  strComputer = objTextStream.ReadLine
  Wscript.Echo "Use WMI to get OS and SP versions from " & strComputer
Loop

What we get back from the OpenTextFile method of FileSystemObject is actually an object representing a text stream. This object has handy properties such as AtEndOfStream and methods such as ReadLine that we use here to pull out one line at a time.

Retrieving operating system version and Service Pack

To find out the operating system and service pack of computers, we use three properties of theWMI class Win32_OperatingSystem: Version, ServicePackMajorVersion and ServicePackMinorVersion.

To get these, we connect to WMI on the computer in question and query for instances of the Win32_OperatingSystem class. (This query always returns only one instance, the operating system that is currently running.) Then we display these three properties, concatenating together ServicePackMajorVersion and ServicePackMinorVersion with a period in between.

'Get strComputer from each line of text file.
Set objWMIService = GetObject("winmgmts://" & strComputer)
Set colOSes = objWMIService.ExecQuery _
 ("SELECT * FROM Win32_OperatingSystem")
For Each objOS in colOSes
  Wscript.Echo
  Wscript.Echo strComputer
  Wscript.Echo "OS Version: " & objOS.Version
  Wscript.Echo "Service Pack: " & objOS.ServicePackMajorVersion & _
   "." & objOS.ServicePackMinorVersion
Next

If we ran this, we would get output something like the following.

client1
OS Version: 5.1.2600
Service Pack: 2.0

If we wanted to display the name of the operating system rather than the version number, we could use the Caption property, which might be a bit more legible, rather than Version. In any case, in this script, we're only interested in Windows XP.

We could put the two components we've created together and get the OS version and service pack on those four machines. The output would look like this:

C:\scripts>xpsplist-wmi.vbs 

client1
OS Version: 5.1.2600
Service Pack: 1.0

client2
OS Version: 5.1.2600
Service Pack: 2.0

client3
OS Version: 5.1.2600
Service Pack: 2.0

client4
OS Version: 5.1.2600
Service Pack:

This is straightforward WMI, and many of you are no doubt yawning and craving that second cup of coffee at this point. But that's part of the beauty of scripting technologies: once you learn them, they're routine and easy to use. In this column, we're going to try to put the pieces of them together in slightly more complex and practical ways, but most of the building blocks are the same old stuff. Remember Dr. Scripto's timeless wisdom: "Be lazy (IT managers should read 'productive'). Don't reinvent the wheel."

Sorting the computers by service pack

Doctor Scripto at work

What we really want here is something a bit beyond a simple list of computers with their OS version and service pack. Rather, we want output broken out by Service Pack 2, Service Pack 1, and no service pack, with a list of computers running each. And it would be nice to have a count for each, because we want to know the magnitude of the job ahead.

Here's the code that gets us that information.

'Get strComputer from each line of text file.
Set objWMIService = GetObject("winmgmts://" & strComputer)
Set colOSes = objWMIService.ExecQuery _
 ("SELECT * FROM Win32_OperatingSystem")
For Each objOS in colOSes
  If objOS.Version = "5.1.2600" Then
    If objOS.ServicePackMajorVersion = "2" Then
      strSP2 = strSP2 & strComputer & vbCrLf
      intSP2 = intSP2 + 1
    ElseIf "1" = objOS.ServicePackMajorVersion Then
      strSP1 = strSP1 & strComputer & vbCrLf
      intSP1 = intSP1 + 1
    Else
      strSP0 = strSP0 & strComputer & vbCrLf
      intSP0 = intSP0 + 1
    End If
  Else
    intNotXP = intNotXP + 1
  End If
Next

We now have the names of the machines running each service pack of Windows XP in a separate variable, along with a count of each. We've also got a tally of non-Windows XP machines in another counter.

Incidentally, if you want to find out explicitly whether no service pack is installed, check for ServicePackMajorVersion = 0 (rather than Null, as you might expect). Here we handle this possibility with the Else clause, which works fine for now. If another service pack were released in the future, it would fall into the Else clause and be counted as non-Windows XP. But for this column's purposes we want to keep our code on the simple side, so we'll forego handling future contingencies.

Outputting data to a text file

We want to output this data to another text file, using a variation on the code that reads the input file. This time, we'll check whether the named file exists: if it does, we open it and append data to it; if not, we create a file and write to it. We add some very retro plain text formatting that brings on MS-DOS nostalgia for old-timers like Dr. Scripto. Note that we have to close the text stream we've been writing to when we're done, which saves and closes the text file.

Const FOR_APPENDING = 8
strOutputFile = "xpsp.txt"
Set objFSO = CreateObject("Scripting.FileSystemObject")
If objFSO.FileExists(strOutputFile) Then
  Set objTextStream = objFSO.OpenTextFile(strOutputFile, FOR_APPENDING)
Else
  Set objTextStream = objFSO.CreateTextFile(strOutputFile)
End If

objTextStream.WriteLine "Inventory of Windows XP Service Packs"
objTextStream.WriteLine "Taken " & Now
objTextStream.WriteLine vbCrLf & "Computers Running Windows XP"
objTextStream.WriteLine "============================"
objTextStream.WriteLine  "Total number: " & (intSP2 + intSP1 + intSP0)

objTextStream.WriteLine  vbCrLf & "Service Pack 2"
objTextStream.WriteLine  "--------------"
objTextStream.WriteLine  strSP2
objTextStream.WriteLine  "Total number: " & intSP2

objTextStream.WriteLine  vbCrLf & "Service Pack 1"
objTextStream.WriteLine  "--------------"
objTextStream.WriteLine  strSP1
objTextStream.WriteLine  "Total number: " & intSP1

objTextStream.WriteLine  vbCrLf & "No Service Pack"
objTextStream.WriteLine  "---------------"
objTextStream.WriteLine  strSP0
objTextStream.WriteLine  "Total number: " & intSP0

objTextStream.WriteLine  vbCrLf & "Computers Not Running Windows XP"
objTextStream.WriteLine  "================================"
objTextStream.WriteLine  "Total number: " & intNotXP

objTextStream.WriteLine  vbCrLf & "Could Not Connect To Computer"
objTextStream.WriteLine  "============================="
objTextStream.WriteLine  "Total number: " & intErr
objTextStream.WriteLine

objTextStream.Close

The Big Enchilada

OK, now that we've got the components, Dr. Scripto is ready to fire up his arc welder and oxyacetylene torch and fit those parts together into a high-performance inventory machine. Well, OK, how about a script that runs on my machine, anyway? What did you expect for free?

We're going to put the components we've discussed so far into separate procedures to make them more modular, maintainable and reusable. That's going to be an emphasis of this column, because it's a good way to structure scripts of any complexity.

The Scripting Guys often meditate on Dr. Scripto's saying: "Don't reinvent the wheel." Perhaps he means that we should recycle useful code into many scripts. If you're going to do that, putting reusable functionality into generic sub-routines or functions that you can paste into any script is a reasonable strategy. When you put such code snippets into a standard form, you can boil them down to their most efficient and compact version and let all the scripters in your organization take advantage of these improvements.

Another reason to put a section of code into a procedure is when you find you're using it in more than one place in a script. With such code in a procedure, you have to write it only once and maintain it in one place.

Some of us find that long, complex scripts are easier to read when they're divided up into chunks this way. But as the Scripting Guys have debated the virtues of functions and sub-routines, we've learned that we have different perspectives on them. Greg seems to think they're overrated (maybe we should let him speak for himself about them in a future column). So help us out here: let us know what you like or dislike about procedures and how you use them by sending us mail.

OK, cut to the script. We broke our code down into these sections:

  • A main routine that initializes constants and global variables, then gets an array of computer names from the input function (ReadTextFile) and calls the other procedures. The main routine also counts machines on which the script encountered an error while trying to connect to WMI. That "g_" prefixed to the names of some variables is our notation for global variables, whose scope is the whole script so that they can be used in more than one procedure.

  • An input function, ReadTextFile, that takes a file name as a parameter and returns the array of computer names. This involves dynamically redimensioning the array for each new name read, which is what ReDim Preserve does.

  • A function, GetSP, that does the inventory and count. We pass this function the name of each computer in turn as the main routine loops through the array. The function appends the computer name to the list of whichever service pack that computer is running and increments a global variable counting the number of computers running that service pack. The function returns a 0 if it succeeds and a 1 if it encounters an error.

  • An output sub-routine, WriteTextFile, that takes a file name as a parameter and writes the accumulated inventory data to that text file.

Notice in the GetSP function that we test for an error after trying to connect to WMI on the remote computer. This works fine for a small number of machines, and WMI will tell us if it can't find the computer in question. For larger numbers of machines, though, we might want to ping each machine to make sure it's connected to the network before trying to connect to WMI. This requires a few more lines of code, but pinging is faster than waiting for WMI to time out.

At last, here's Dr. Scripto's opus magnus:

'List the operating system and service pack of all computers in a text file.
On Error Resume Next

'Initialize constants and variables.
Const FOR_READING = 1
Const FOR_APPENDING = 8
strInputFile = "hosts.txt"
strOutputFile = "xpsp.txt"
g_strSP2 = ""
g_strSP1 = ""
g_strSP0 = ""
g_intSP2 = 0
g_intSP1 = 0
g_intSP0 = 0
g_intNotXP = 0
g_intErr = 0

'Get list of computers from text file.
arrComputers = ReadTextFile(strInputFile)
For Each strComputer In arrComputers
  intGetSP = GetSP(strComputer)
  If 1 = intGetSP Then
    g_intErr = g_intErr + 1
  End If
Next

WriteTextFile(strOutputFile)

WScript.Echo "Data written to " & strOutputFile

'******************************************************************************

'Read text file line by line and return array of lines.
Function ReadTextFile(strFileName)

On Error Resume Next

Dim arrLines()

Set objFSO = CreateObject("Scripting.FileSystemObject")
If objFSO.FileExists(strFilename) Then
  Set objTextStream = objFSO.OpenTextFile(strFilename, FOR_READING)
Else
  WScript.Echo "Input text file " & strFilename & " not found."
  WScript.Quit
End If
Do Until objTextStream.AtEndOfStream
  intLineNo = objTextStream.Line
  ReDim Preserve arrLines(intLineNo - 1)
  arrLines(intLineNo - 1) = objTextStream.ReadLine
Loop
objTextStream.Close
ReadTextFile = arrLines

End Function

'******************************************************************************

'Get list and count of computers by OS and SP.
Function GetSP(strComp)

On Error Resume Next

'Connect to WMI on remote computer.
Set objWMIService = GetObject("winmgmts:\\" & strComp)
If Err <> 0 Then
  WScript.Echo "  Unable to connect to WMI."
  WScript.Echo "    Error Number:" & Err.Number
  WScript.Echo "    Source:" & Err.Source
  WScript.Echo "    Description:" & Err.Description
  GetSP = 1
  Exit Function
End If
Set colOSes = objWMIService.ExecQuery _
 ("SELECT * FROM Win32_OperatingSystem")
For Each objOS in colOSes
  If objOS.Version = "5.1.2600" Then
    If objOS.ServicePackMajorVersion = "2" Then
      g_strSP2 = g_strSP2 & strComp & vbCrLf
      g_intSP2 = g_intSP2 + 1
    ElseIf objOS.ServicePackMajorVersion = "1" Then
      g_strSP1 = g_strSP1 & strComp & vbCrLf
      g_intSP1 = g_intSP1 + 1
    Else
      g_strSP0 = g_strSP0 & strComp & vbCrLf
      g_intSP0 = g_intSP0 + 1
    End If
    GetSP = 0
  Else
    g_intNotXP = g_intNotXP + 1
  End If
Next
GetSP = 0

End Function

'******************************************************************************

'Write or append data to text file.
Sub WriteTextFile(strFileName)

On Error Resume Next

'Open text file for output.
Set objFSO = CreateObject("Scripting.FileSystemObject")
If objFSO.FileExists(strFileName) Then
  Set objTextStream = objFSO.OpenTextFile(strFileName, FOR_APPENDING)
Else
  Set objTextStream = objFSO.CreateTextFile(strFileName)
End If

'Write data to file.
objTextStream.WriteLine "Inventory of Windows XP Service Packs"
objTextStream.WriteLine "Taken " & Now
objTextStream.WriteLine vbCrLf & "Computers Running Windows XP"
objTextStream.WriteLine "============================"
objTextStream.WriteLine  "Total number: " & (g_intSP2 + g_intSP1 + g_intSP0)

objTextStream.WriteLine  vbCrLf & "Service Pack 2"
objTextStream.WriteLine  "--------------"
objTextStream.WriteLine  g_strSP2
objTextStream.WriteLine  "Total number: " & g_intSP2

objTextStream.WriteLine  vbCrLf & "Service Pack 1"
objTextStream.WriteLine  "--------------"
objTextStream.WriteLine  g_strSP1
objTextStream.WriteLine  "Total number: " & g_intSP1

objTextStream.WriteLine  vbCrLf & "No Service Pack"
objTextStream.WriteLine  "---------------"
objTextStream.WriteLine  g_strSP0
objTextStream.WriteLine  "Total number: " & g_intSP0

objTextStream.WriteLine  vbCrLf & "Computers Not Running Windows XP"
objTextStream.WriteLine  "================================"
objTextStream.WriteLine  "Total number: " & g_intNotXP

objTextStream.WriteLine  vbCrLf & "Could Not Connect To Computer"
objTextStream.WriteLine  "============================="
objTextStream.WriteLine  "Total number: " & g_intErr
objTextStream.WriteLine

objTextStream.Close

End Sub

Here's an example of what the output text file, xpsp.txt, might contain:

Inventory of Windows XP Service Packs
Taken 3/15/2005 1:40:10 PM

Computers Running Windows XP
============================
Total number: 3

Service Pack 2
--------------
client1
client2
Total number: 2

Service Pack 1
--------------
client3
Total number: 1

No Service Pack
---------------
client4
Total number: 1

Computers Not Running Windows XP
================================
Total number: 0

Could Not Connect To Computer
=============================
Total number: 0

We chose not to get the names of machines not running Windows XP, but that would be easy to add if we needed a more complete inventory. And if we wanted to cover all updates, not just service packs, we could also get a list of patches with the WMI class Win32_QuickFixEngineering. A lot of patches may be installed, so in this case we might want to display an inventory per machine, as we did in the first code examples above.

Another thing we might want to do is to add code that kicks off the installation of Service Pack 2 on all the non-SP 2 machines. All we'd have to do is concatenate the strings g_strSP1 and g_strSP0, which contain the names of Windows XP machines without SP 2, back into one string and use Split(strSP1AndSP0, vbCrLf) to turn it into an array again. Then we could loop through that array with For Each and run the Service Pack 2 setup on each of those machines. This approach could allow us to deploy the service pack with default firewall settings allowing remote administration or, on a network with its own firewall, disabling the firewall. But we're getting ahead of ourselves here.

In the next column, we'll talk about how to get this same service pack information from computers joined to an Active Directory domain.

For more information