Test Run

Configuration Testing With Virtual Server, Part 2

Dr. James McCaffrey and Paul Despe

Code download available from the MSDN Code Gallery

Contents

Virtual Server Automation with Windows PowerShell
Creating a Virtual Machine
Configuration Tests on a Virtual Machine
Last Word

There are several ways you can perform software configuration testing. One effective approach for certain scenarios is to use Microsoft Virtual Server to create a library of virtual machines. Because Virtual Server is built on a set of COM modules, you can completely automate the process of creating and exercising virtual machines.

Although individual Virtual Server automation tasks are quite well documented, in discussions with our testing colleagues we learned that there is a need for a complete end-to-end example that puts together all the parts of automating Virtual Server for software configuration testing. Additionally, almost all current Virtual Server automation references use the old VBScript language rather than the much more powerful Windows PowerShell.

In this month's column, we're going to walk you through the process of automating software configuration testing using Virtual Server and Windows PowerShell.We will assume that you have a basic familiarity with them, but you should be able to follow our examples even if these technologies are new to you.

(If you do want more information, you can read about Windows PowerShell in the May 2007 installment of Test Run—" Lightweight Testing with Windows PowerShell "—and in " CMDLETS: Extend Windows PowerShell with Custom Commands " by Jim Truher. I also covered configuration testing with Virtual Server in my September 2008 Test run column " Configuration Testing With Virtual Server, Part 1 .")

The screenshot in Figure 1 gives you a good idea of where we're headed in this column. Our physical host machine is running Windows Server 2008; however, all the techniques described here also work with Windows Server 2003. The host machine has Virtual Server 2005 R2 SP1 installed. The screenshot in Figure 1 shows a virtual guest machine named VitualMachine-Test that is running Windows XP SP2. What is not apparent from the screenshot is that the virtual machine was created using a Windows PowerShell 1.0 script. In the background of the screenshot, you can see that a second Windows PowerShell script has set up a Scheduled Task on the virtual machine. This task is the test automation, which is running in the cmd.exe shell in the foreground of the virtual machine.

fig01.gif

Figure 1 Example of Configuration Testing (Click the image for a larger view)

In the sections of this column that follow, we'll explain in detail the script that created the virtual machine shown in Figure 1 and the script that set up and executed the test automation. We think you'll find the techniques we present here a useful addition to your software testing, development, and management skill sets.

Virtual Server Automation with Windows PowerShell

Virtual Server is built on a set of objects that use classic DCOM technology. You can automate Virtual Server using any COM-aware language, including VBScript, JavaScript, C#, Visual Basic .NET, and Windows PowerShell. Our preferred approach is to use Windows PowerShell. Its advantages include its ability to directly call into the Microsoft .NET Framework and its command-line capabilities, which allow you to develop scripts interactively. Because VBScript and JavaScript are native Win32 scripting languages, they can directly create and use Virtual Server automation objects. But because Windows PowerShell is a .NET-compliant language, in order to use Virtual Server automation remote objects with Windows PowerShell you must enable impersonation on each object.

There are several approaches you can use. The most straightforward approach is to use Visual Studio and C# to create a custom DLL that contains a method that allows you to set an impersonation level on a native Win32 object, and then call that custom method from Windows PowerShell. We use a somewhat surprising alternative approach, which is to write and compile a custom DLL on the fly using Windows PowerShell.

The script in Figure 2 creates a Windows PowerShell function named SetImpersonation, which we can use to set the impersonation level on Virtual Server automation objects. We decided to place our code inside the special Windows PowerShell $profile startup script. We begin our script with two write-host messages to alert users that a custom startup script is adding additional functionality to Windows PowerShell. Next, we set up references to the C# compiler and a temporary file:

$csc = (join-path ($env:windir) Microsoft.NET\Framework\v2.0.50727\csc.exe) $tempFile = [IO.Path]::GetTempFileName() + ".cs"

Figure 2 Custom SetImpersonation Function

write-host "Executing custom startup script"
write-host "Creating custom SetImpersonation function'n"
set-location D:\

$csc = (join-path ($env:windir) Microsoft.NET\Framework\v2.0.50727\csc.exe)

$tempFile = [IO.Path]::GetTempFileName() + ".cs"

$source = @"
  using System;
  using System.Runtime.InteropServices;

  public class MySecurity
  {
    [DllImport("Ole32.dll", CharSet=CharSet.Auto)]
    public static extern int CoSetProxyBlanket(IntPtr pProxy,
      uint dwAuthnSvc,
      uint dwAuthzSvcp2,
      IntPtr pServerPrincName,
      uint dwAuthnLevel,
      uint dwImpLevel,
      IntPtr pAuthInfo,
      uint dwCapabilities);

    public static int EnableImpersonation(object objDCOM) {
      return CoSetProxyBlanket(Marshal.GetIDispatchForObject(objDCOM), 
      10, 0, IntPtr.Zero, 0, 3, IntPtr.Zero, 0);
    }
  }
"@

set-content $tempFile $source
$targetDLL = [IO.Path]::GetTempFileName() + ".dll"

invoke-expression "$csc /nologo /target:library /out:$targetDLL $tempFile"
[System.Reflection.Assembly]::LoadFrom($targetDLL) | out-null

function SetImpersonation($obj)
{
  [MySecurity]::EnableImpersonation($obj) | out-null
}

We use the join-path cmdlet to create a full path to the .NET Framework 2.0 C# compiler, which we assume is on our host machine. Then we call directly into the System.IO namespace and use the static GetTempFileName method of the Path class to generate a unique file name with a .cs extension, located in the system Temp folder of our host machine.

Next we set up C# source code in order to create our custom impersonation function:

$source = @"
  using System;
  using System.Runtime.InteropServices;

  public class MySecurity
  {
    . . . 
  }
"@

We use the here-string feature of Windows PowerShell to create, in essence, a complete C# program as a single string. The idea is to use the C# P/Invoke mechanism to create a managed wrapper method named EnablePersonation around the Win32 CoSetProxyBlanket function. The details of the CoSetProxyBlanket function are outside the scope of this column, but the bottom line is that we create a method that allows objects to impersonate the client's security context on remote systems.

With our C# source code created, next we actually build a managed DLL on the fly:

set-content $tempFile $source
$targetDLL = [IO.Path]::GetTempFileName() + ".dll"
invoke-expression "$csc /nologo /target:library /out:$targetDLL $tempFile"
[System.Reflection.Assembly]::LoadFrom($targetDLL) | out-null

We use the set-content cmdlet to place the C# source code into our temp file. Next we use the GetTempFileName to create a unique name for our DLL file. We could have left this step out, because the C# compiler by default will create a DLL name based on the name of the source code file. Then we use the invoke-expression cmdlet to run the C# compiler.

Because our argument to invoke-expression is a double-quoted string, the objects inside the string will be evaluated. After the C# compiler builds our custom DLL with our EnableImpersonation method, we use the LoadFrom method to load the DLL into our current Windows PowerShell execution environment.

Our script then finishes by creating a Windows PowerShell wrapper function named SetImpersonation around the C# EnableImpersonation method:

function SetImpersonation($obj) 
{
  [MySecurity]::EnableImpersonation($obj) | out-null
}

We pipe our call to EnableImpersonation to the out-null cmdlet simply to suppress printing diagnostic information when the function is called. By placing this script in your Windows PowerShell startup program, every time you launch a new instance of Windows PowerShell, the script will execute and you will create a new custom DLL with a SetImpersonation function.

The disadvantage of this approach is that you are recreating the same DLL every time you launch an instance of Windows PowerShell. Alternatively, you can place this code directly into the beginning of any Windows PowerShell script that needs to access Virtual Server COM objects. But we do not recommend that you simply build the custom DLL once (with C# or on the fly with Windows PowerShell) and then place that DLL somewhere on your host machine.

Automation scripts often have a way of persisting much longer than you initially expect. You don't want to be in a position months or even years later where you've lost your DLL and have no idea how to recreate it.

Creating a Virtual Machine

The screenshot in Figure 3 summarizes the steps you perform when using automation to create a Virtual Server virtual machine. We begin by verifying that we have our custom SetImpersonation function available. Next we check to see if our virtual machine configuration files already exist from a previous automation run. If the configuration files exist, we stop the background Virtual Server service and delete the files, then restart the service.

fig03.gif

Figure 3 Creating a Virtual Machine (Click the image for a larger view)

Next we instantiate the primary Virtual Server automation object, which contains all automation functionality. Then we create a virtual machine and provision its RAM, hard disk drive (with operating system), CD/DVD drive, and network adapter. We finish by starting the virtual machine, determining when the virtual machine's operating system is up and running.

Let's walk through the createVM.ps1 Windows PowerShell script shown running in Figure 3 . Our script begins by looking for the SetImpersonation function described in the previous section:

write-host "'nChecking to see if SetImpersonation function exists"
$funcs = get-childitem function:\*
$found = $false
foreach ($f in $funcs) {
  if ($f.name -eq "SetImpersonation") {
    $found = $true
  }
}

We use the get-childitem cmdlet to retrieve a collection of all functions known to our current Windows PowerShell environment. We could also have used the get-command cmdlet with a "-commandtype function" argument. We iterate through every item in the collection looking for a function with a SetImpersonation name property. Alternatively, we could have used the Windows PowerShell break statement to exit our iteration loop early if SetImpersonation is found.

After examining the function:\ collection, we can use our Boolean $found variable to determine if the SetImpersonation function is in our current environment:

if ($found) {
 write-host "Found custom SetImpersonation function"
}
else {
 throw "No SetImpersonation function found"
}

Here we decide to throw an exception if the SetImpersonation function is not found, because the rest of our automation will certainly fail. The next phase of our automation prepares to delete a virtual machine configuration file if it already exists. To delete a virtual machine configuration file using Windows PowerShell, you must first stop the background Virtual Server service, so we create a StopService utility function to do so.

The helper function begins by retrieving information about the Virtual Server service:

function StopService
{
  write-host "'nStopping Virtual Server service"
  $s = get-service "Virtual Server"

  if ($s.status -ne "Stopped") {
    $s.stop()
  }
  . . .
}

We use the handy get-service cmdlet to fetch an object that contains a Status property and then call the object's Stop method. At this point we could pause our automation to give the Virtual Server service time to stop, but the problem is that we have no way of knowing how long to pause.

A much better approach is to go into a delay loop:

$ct = 0
while ( ((get-service "Virtual Server").status -ne "Stopped")
          -and ($ct -lt 100) ) {
  $ct++
  start-sleep -m 200
  write-host "Waiting for Virtual Server service to stop . . ."
}

We initialize a counter to keep track of how many times we pause our automation. Then we go into a delay loop that will exit as soon as the service Status is equal to Stopped or if we exceed 100 delays. Inside our delay loop, we use the start-sleep cmdlet to pause for 200 milliseconds, so we will delay at most 20,000 milliseconds (20 seconds). After our delay loop terminates, we check to see how it exited:

if ( (get-service "Virtual Server").status -ne "Stopped" ) {
  throw "Failed to stop Virtual Server service in allowed time"
}

If the Virtual Server status is not Stopped, that means the loop exited because it exceeded 100 iterations, so we throw an exception. We define a StartService helper function, which uses the same logic as the StopService function:

. . .
if ($s.status -ne "Running") {
  $s.start()
}
. . .
while ( ((get-service "Virtual Server").status -ne "Running")
        -and ($ct -lt 100) ) {
. . .
}
if ( (get-service "Virtual Server").status -ne "Running" ) {
  throw "Failed to start Virtual Server service in allowed time"

An alternative to placing the StopService and StartService helper functions directly inside your virtual machine creation script is to place these helper functions inside the Windows PowerShell $profile startup script. With our two helper functions defined, we can now determine if we need to delete any virtual machine configuration files left from previous automation:

$vmcFile = test-path "D:\VMs\VirtualMachine-Test.vmc"
$lnkFile = test-path "D:\Users\All Users\Microsoft\Virtual Server\Virtual Machines\VirtualMachine-Test.lnk"

We use the test-path cmdlet to return a true/false value depending upon whether the cmdlet's file argument exists or not.

When you create a virtual machine, you can place the .vmc configuration file anywhere on your host machine. Virtual Server also creates a shortcut .lnk file to the .vmc file at a standard location, which depends upon your host operating system—for example, %SystemRoot%\Users\All Users\Microsoft\Virtual Server\Virtual Machines\ in the location in Windows Server 2008.

To create a virtual machine with a particular name, you must delete both the .vmc and corresponding .lnk files or Virtual Server will complain that your virtual machine already exists. Notice that our virtual machine name is hardcoded into our script for clarity; in a production environment, you will likely want to parameterize this value and pass it to your script. Once we've determined if any old virtual machines exist we can delete them, as we do in Figure 4 .

Figure 4 Delete Old Virtual Machines

if ($vmcFile -or $lnkFile) {
  write-host "'nFound existing .vmc and/or .lnk file"
  StopService
  if ($vmcFile) {
    write-host "Deleting .vmc file"
    remove-item "D:\VMs\VirtualMachine-Test.vmc"
  }
  if ($lnkFile) {
    write-host "Deleting .lnk file"
    remove-item "D:\Users\All Users\Microsoft\Virtual Server\Virtual
      Machines\VirtualMachine-Test.lnk"
  }
  StartService
}
else {
 write-host "'nDid not find existing .vmc or .lnk files"
}

If either our target .vmc or .lnk files exist, we call our StopService helper function to halt the background Virtual Server service. Then we use the remove-item cmdlet to delete the appropriate files and then restart the service.

At this point in our script, we are ready to create the primary Virtual Server COM automation object:

if ((get-service "Virtual Server").status -ne "Running") {
  StartService
}
$vs = new-object -com "VirtualServer.Application"
if ($vs -eq $null) {
  throw "Failed to create main COM automation object"
}
SetImpersonation $vs

First we make sure that the Virtual Server service is running. Next we use the new-object cmdlet to instantiate an instance of the Virtual Server automation library, which has a ProgID of VirtualServer.Application. And if that creation fails, we throw an exception because our automation is doomed. Otherwise, we invoke our custom SetImpersonation function on the automation object so that the object can impersonate the client's security context through Virtual Server's underlying DCOM transport mechanisms.

Creating a virtual machine is almost too easy:

write-host "'nCreating virtual machine"
$vm = $vs.CreateVirtualMachine("VirtualMachine-Test", "D:\VMs")
if ($vs -eq $null) {
  throw "Failed to create virtual machine object"
}
SetImpersonation $vm

We simply call the CreateVirtualMachine method and pass the name of the virtual machine as a string, and the location of the host where we want the resulting .vmc file to reside—in this case a hardcoded D:\VMs\ directory. Because we need to manipulate the virtual machine object, we set its impersonation level.

At this point in the process, we can begin provisioning our newly created virtual machine:

write-host "Assigning 256 MB memory"
$vm.Memory = 256 

write-host "'nAdding existing virtual hard disk drive"
write-host "Copying .vhd file from library to working directory"
copy-item "D:\VirtualMachinesLibrary\VirtualMachine-WinXP-SP2.vhd"
  "D:\VirtualMachines\"

First we assign RAM for the guest machine, keeping in mind the fact that the total amount of RAM available for all running virtual machines plus the host machine is limited by the amount of RAM on the host machine. Next we prepare to attach a .vhd virtual hard disk drive file to our virtual machine. We copy a .vhd file that contains our OS and other key software from a backup location to a working directory. The idea is that we want to make sure that if our .vhd file becomes corrupted during configuration testing, we still have an original version of our .vhd file to use later.

Because .vhd files are typically about 2.0GB in size, this file copy operation usually takes a couple of minutes. When you create a .vhd file with Virtual Server, the .vhd file is placed in the same directory as its associated .vmc file. However, when attaching an existing .vhd file to a new virtual machine, the .vhd and .vmc files can be in different directories, as we're doing here. And once we have our working .vhd file, we can attach it to our virtual machine:

$loc = "D:\VirtualMachines\VirtualMachine-WinXP-SP2.vhd"
if (test-path $loc) {
  write-host "Found the .vhd file"
  $hd = $vm.AddHardDiskConnection("D:\VirtualMachines\VirtualMachine-WinXP-SP2.vhd", 0, 0, 0) 
}
else {
  throw "Failed to find .vhd file"
}

First we use the test-path cmdlet to make sure our working copy of the target .vhd file exists. Next we use the AddHardDiskConnection method to attach the virtual hard disk. The first parameter to the AddHardDiskConnection method is hard drive bus type, which is an enumeration where vmDriveBusType_IDE = 0 and vmDriveBusType_SCSI = 1.

Interestingly, even if your host machine has only IDE drives, you can specify a SCSI drive for your virtual machine for improved performance. The second parameter is the bus number where the value can be 0 or 1 for IDE drives, and 0 through the number of SCSCI controllers for SCSI drives. The third parameter is the device number where the value can be 0 or 1 for IDE drives and 0 through 6 for SCI drives.

After attaching a .vhd virtual hard drive, we are able to make that drive Undoable:

write-host "Making .vhd disks undoable"
$vm.undoable = $true

By default, Virtual Server automatically saves all changes made to a virtual machine while the machine is running. But if you configure Undo disks, then all changes made to the virtual machine while the machine is running are saved to a .vud virtual undo disk file. And when you shut down the virtual machine, you get the option either to discard the Undo disk (and revert back to the original .vhd disk) or commit the Undo disk (and save changes to the .vhd file.)

Now, with the scheme we're using here—copying a base configuration .vhd file to a working directory—it doesn't matter if our hard disk is undoable or not. But we show you this technique in case you need it for your particular scenario.

Now we provision the CD/DVD drive:

write-host "'nAdding CD/DVD drive"
$dvd = $vm.AddDVDROMDrive(0,1,0)
if ($dvd -eq $null) {
  write-host "Failed to add CD/DVD drive"
}
SetImpersonation $dvd

write-host "Attaching CD/DVD drive to F:"
$dvd.AttachHostDrive("F")

When you create a virtual machine using the Virtual Server GUI interface, Virtual Server automatically adds and attaches a CD/DVD drive to your guest machine if the host machine already has a CD/DVD drive. But when you create a virtual machine with automation, you must explicitly configure a CD/DVD drive.

The first parameter to the AddDVDROMDrive method is the bus type, which must be 0 for a CD/DVD drive. The second parameter is the bus number, which must be 0 or 1. The third parameter is the device number, which also must be 0 or 1. Next we use the AttachHostDrive method to associate the virtual CD/DVD drive on the guest machine with the physical drive on the host machine. Notice we've hardcoded "F:" in our script for clarity, but you may want to parameterize this value and pass it into your script. Now we're ready to configure our network adapters:

$vn = $vs.FindVirtualNetwork('External Network (Broadcom 802.11g Network
   Adapter)')
if ($vn -eq $null) {
  write-host "Failed to find virtual network adapter"
}
SetImpersonation $vn

We use the FindVirtualNetwork method to locate an existing network adapter. Notice that Virtual Server often uses the terms network and network adapter more or less interchangeably. When you install Virtual Server, the setup program scans the host machine for all active adapters and then names the adapters by placing the adapter name in parentheses, preceded by the string "External Network". However, if you add a network adapter after Virtual Server installation, you can name the adapter anything you wish.

After setting the impersonation level on the adapter object, we attach our virtual adapter to our virtual network:

$adapters = $vm.NetworkAdapters
SetImpersonation $adapters
SetImpersonation $adapters.Item(1)
$adapters.Item(1).AttachToVirtualNetwork($vn)

We use the NetworkAdapters property to fetch a collection of all adapters. We must set impersonation on the collection object and each adapter in the collection in order to access an adapter with Windows PowerShell. The first adapter, Item(0), is the built-in "Internal Network" adapter, so Item(1) is the first external adapter. We use the AttachToVirtualNetwork method to connect the adapter to the network. Our configuration is now complete, and we can start our virtual machine:

write-host "'nStarting Virtual Machine"
$startTask = $vm.StartUp()
SetImpersonation $startTask
$startTask.WaitForCompletion(60000) # wait up to 1 minute

The Startup method returns VMTask object that can be used to monitor the method's progress. The task object has a convenient WaitForCompletion method that accepts a maximum value for time, in milliseconds, to wait for the task to complete.

Next we must wait until the operating system on the guest virtual machine boots up:

   write-host "'nWaiting for guest virtu al machine to boot                up'n"
   $ct = 0
   while ( ((test-path "\\VM-017\Public") -eq $false) -and $ct 
          -lt 100 )
   {
     $ct++
     write-host "Waiting . . . $ct" 
     start-sleep -s 10
   }

The approach we take is to use a delay loop. In each iteration through the delay, we pause for 10 seconds. Our while loop condition probes into the virtual machine using test-path and exits if we discover a directory that we know exists on the guest, or if we exceed a maximum number of probing attempts. Our script concludes by determining how our delay loop terminated:

if ((test-path "\\VM-017\Public") -eq $false) {
  throw "'nFailed to discover guest machine"
}
else {
  write-host "'nVirtual machine is up and running"
}

If we cannot discover the virtual guest machine, we throw an exception. Otherwise we display a success message.

Configuration Tests on a Virtual Machine

The previous section describes how to create and provision a virtual machine using a script. At this point you can perform manual testing, but let's look at the key techniques for automating the process of configuration testing. If you look at the screenshot in Figure 5 , you can see we have a Windows PowerShell script named exerciseVM.ps1. Our script first verifies that the target virtual machine is running and then copies a test-related file from the host machine to the guest machine. Next our script creates a Windows task on the guest machine that will fire off an automated test at a specified time. The script concludes by turning the virtual machine off and discarding the machine's Undo disk.

fig05.gif

Figure 5 Placing Test Automation on a Virtual Machine (Click the image for a larger view)

The exerciseVM.ps1 script begins by using the test-path cmdlet to look for the target virtual machine:

if ((test-path "\\VM-017\C$") -eq $false) {
  throw "'nFailed to discover guest machine"
}
else {
  write-host "Found virtual guest machine"
}

If we can't find the system root of the virtual machine, we throw an exception to terminate the script. Next we verify that test automation file exists on the host:

if (test-path "D:\Public\testHarness.js") {
  write-host "Found testHarness.js file to copy"
}
else {
  throw "Did NOT find testHarness.js file to copy"
}

Then we verify that the guest has a public share to receive our test automation. This approach assumes that the virtual machine's .vhd file has been configured with a share that allows users to copy files to that share. There is no easy way for a script running on a host machine to perform such a security configuration on a guest.

if (test-path "\\VM-017\Public") {
  write-host "Found \\VM-017\Public directory to copy to"
}
else {
  throw "Did NOT find \\VM-017\Public directory to copy to"
}
copy-item "D:\Public\testHarness.js" "\\VM-017\Public"

We simply use the copy-item cmdlet to copy our single automation file from host to guest. Now we set up a Windows task to execute the test automation on the virtual guest machine:

$dt = new-object -com WbemScripting.SWbemDateTime
$now = [DateTime]::now 
$later = $now.addSeconds(90)
write-host "'nTest automation testHarness.js schedeuled to run at " $later
$later.ToUniversalTime() | out-null
$dt.SetVarDate($later)

We are using Windows Management Instrumentation (WMI)/Common Information Model (CIM) technology, so first we create an SWbemDateTime object (as opposed to a .NET DateTime object). We use the .NET static Now property to fetch the current date and time on the host. Then we use the AddSeconds method to create a .NET Date time object that is 90 seconds later. Next we convert the .NET execution time to a Coordinated Universal Time (UTC) object. Then we use the SetVarDate to configure the SWbemDateTime date-time object.

Now we schedule our test automation task:

[System.Management.ManagementClass] $mc =
  new-object System.Management.ManagementClass("\\VM-017\root\cimv2",
    "Win32_ScheduledJob", $null) 
$mc.Create("cscript.exe //nologo C:\Public\testHarness.js",
           $dt.value, $false, 0, 0, $true) | out-null

Instead of wrestling directly with WMI technology using the Windows PowerShell get-wmiobject cmdlet, we use the fact that Windows PowerShell can directly call into the .NET Framework, and we use the ManagementClass of the System.Management namespace. You can think of this class as a friendly .NET wrapper around WMI functions.

The first argument to the object's constructor is the scope of the object and essentially identifies the target machine. The second argument is the WMI class name, and the third argument is an object that contains any optional information to use when retrieving the WMI class.

We use the Create method to set up the scheduled test automation. The first parameter is a string that is the command to execute. The second parameter is an object that specifies when to run the task. The third parameter is a Boolean value that specifies if the task is to run more than once or not. The next two parameters specify when multiple jobs should run. The sixth parameter is a Boolean that specifies if the job should run in interactive mode (in other words, visible) or non-interactive mode.

No script can arbitrarily set up a scheduled task on a remote/guest machine without the cooperation of the guest machine. The details of allowing remote job scheduling vary quite a bit depending upon the operating system being used. In the case of the example shown in Figure 1 , the guest machine required four modifications. First, the Windows Firewall on the Windows XP guest must be disabled to allow remote WMI access. Additionally, the Local Security Policy on the guest required three changes in the Security Options area of the Local Policies section of the Security Settings item. The "DCOM: Machine Access Restrictions in Security Descriptor Definition Language syntax" settings must be modified to "Allow for Anonymous Login and Everyone users." The "Network Access: Let Everyone permissions apply to anonymous users" setting must be enabled. And the "Network Access: Sharing and security model for local accounts" setting must be modified to "Classic—local users authenticate as themselves".

In short, if you want to use a Virtual Server host machine to schedule test automation on a guest machine, you need to make several security changes to the underlying .vhd file. Of course, there's no requirement that you schedule tasks from your host machine; an alternative is simply to configure the guest .vhd file with recurring test automation tasks built in.

With our test automation scheduled, we can wait for the tests to run and then shut down our guest machine:

$dummy = read-host -prompt "'nPress <Enter> to continue"
write-host "'nPreparing to turn off guest machine"
write-host "Creating main COM automation object"
$vs = new-object -com "VirtualServer.Application"
if ($vs -eq $null) {
  throw "Failed to create main COM automation object"
}
SetImpersonation $vs

We use the read-host cmdlet to halt our script execution so that we can observe our test automation using the Virtual Server Web-based administration page and determine when all tests are complete. This technique is useful when developing your automation, but in a production environment you will likely want to use some other mechanism to determine when all your tests are finished. For example, one approach is to use a delay loop and use the test-path cmdlet to probe for a test results file.

Our test automation script now has the ability to connect to the virtual machine:

write-host "Looking for virtual machine"
$vm = $vs.FindVirtualMachine("VirtualMachine-Test")
if ($vm -eq $null) {
  throw "Failed to find virtual machine"
}
else {
  write-host "Found virtual machine"
}
SetImpersonation $vm

We use the FindVirtualMachine method to locate the virtual machine, which was created by our createVM.ps1 script and, we assume, is running as a guest. Notice that we do not use the .vmc extension on the name of the target virtual machine.

After setting the impersonation level on the virtual machine object, we can turn off the virtual machine:

write-host "'nTurning virtual machine off"
$offTask = $vm.TurnOff()
SetImpersonation $offTask
$offTask.WaitForCompletion(5000)
write-host "'nDiscarding undo disk"
$vm.DiscardUndoDisks()

The TurnOff method returns a VMTask object that we can use to wait until the virtual machine shuts down or exceeds a timeout value. In this situation, because our virtual hard disks are undoable, we discard the Undo disk that holds changes made to the state of the virtual machine so that the next time we use the machine's .vhd file, we will be starting with a clean state.

Last Word

There are several ways you can adapt and extend the Virtual Server automation techniques we've described in this column. In order to demonstrate as many key principles as possible, we organized our examples into two relatively large, un-parameterized scripts. With the explanations we've presented, you should be able to modify and reorganize our two scripts to suit your own needs and parameterize some or all of the hardcoded values in the script as needed.

Because our scripts are primarily intended for demonstration purposes, we removed some of the error-checking code that you'd normally put into production code. However, our examples have included several error traps, so you shouldn't have too much trouble making your automation robust enough for production scenarios.

Although the new Microsoft Hyper-V virtualization technology may someday replace Virtual Server, Virtual Server is going to be useful for many years to come. Additionally, the Virtual Server automation principles and techniques we've presented here, in particular, the use of Windows PowerShell, will provide you with a nice transition to Hyper-V.

Send your questions and comments to testrun@microsoft.com .

Dr. James McCaffrey works for Volt Information Sciences, Inc., where he manages technical training for software engineers working for Microsoft in Redmond. He has worked on several Microsoft products including Internet Explorer and MSN Search. James is the author of .NET Test Automation Recipes (Apress, 2006). James can be reached at jmccaffrey@volt.com or v-jammc@microsoft.com .

Paul Despe is a Program Manager on the Hyper-V team. Paul has worked as a Software Design Engineer in Test on both the Virtual PC and Virtual Server products. Before joining Microsoft, Paul worked in Japan and at Connectix, a virtualization software company acquired by Microsoft in 2003. Paul can be reached at paulde@microsoft.com .