Handling Job Objects with Hyper-V WMI scripting

Many of the methods in the Hyper-V WMI have an odd way of returning status.  They can either immediately return a return value - or instead return a job object - and it can be hard to predict which one you are going to get.  The reason for this is that some methods are always synchronous (and return a return value), some methods are always asynchronous (and return a job object) while other methods can be executed synchronously or asynchronously on the back end by WMI, and could return either.

The job object returned by asynchronous methods allows you to track the progress of the operation, and get detailed error information in the case of a failure.

Given this you have two options when working with our WMI interfaces:

  1. Ignore method results.

    This is simple and for many scripts is acceptable, as you can figure out whether the method succeeded or not through observing the results.

  2. Do the work to handle the results.

    Thankfully, Hyper-V does help a little here.  All methods return two objects: a return value and a job path.  If the return value is anything but "4096" then you know the method executed synchronously - and a return value of "1" means success.  If the return value is "4096" then you need to go and look at the job object.

    Methods that always complete asynchronously will always return "4096" in the return value.

Digging into the second option - and starting with this script for mounting a virtual hard disk - let's look at how to add proper support for handling the method results.

This is relatively simple with PowerShell where:

 #Specify the VHD to be mounted
 $VHDName = "F:\Windows.vhd" 
  
 #Get the MSVM_ImageManagementService
 $VHDService = get-wmiobject -class "Msvm_ImageManagementService" -namespace "root\virtualization" -computername "." 
  
 #Mount the VHD
 $Result = $VHDService.Mount($VHDName)

Becomes:

 #Specify the VHD to be mounted
 $VHDName = "F:\Windows.vhd" 
  
 #Get the MSVM_ImageManagementService
 $VHDService = get-wmiobject -class "Msvm_ImageManagementService" -namespace "root\virtualization" -computername "." 
  
 #Mount the VHD
 $Result = $VHDService.Mount($VHDName)
  
 #Return success if the return value is "0"
 if ($Result.ReturnValue -eq 0)
    {write-host "Operation completed sucessfully"}
  
 #If the return value is not "0" or "4096" then the operation failed
 ElseIf ($Result.ReturnValue -ne 4096)
    {write-host "Operation failed"}
  
  
 Else
    {#Get the job object
     $job=[WMI]$Result.job
     
     #Provide updates if the jobstate is "3" (starting) or "4" (running)
     while ($job.JobState -eq 3 -or $job.JobState -eq 4)
       {write-host $job.PercentComplete
        start-sleep 1
     
        #Refresh the job object
        $job=[WMI]$Result.job}
  
     #A jobstate of "7" means success
     if ($job.JobState -eq 7)
        {write-host "Operation completed sucessfully"}
       Else
        {write-host "Operation failed"
         write-host "ErrorCode:" $job.ErrorCode
         write-host "ErrorDescription" $job.ErrorDescription}
    }

As you can see, the only addition is code to inspect and respond to the return value and job object.

In VBScript life is harder:

 Option Explicit 
  
 Dim WMIService
 Dim VHDService
 Dim VHD 
  
 'Specify the VHD to be mounted
 VHD = "F:\Windows.vhd" 
  
 'Get instance of 'virtualization' WMI service on the local computer
 Set WMIService = GetObject("winmgmts:\\.\root\virtualization")  
  
 'Get the MSVM_ImageManagementService
 Set VHDService = WMIService.ExecQuery("SELECT * FROM Msvm_ImageManagementService").ItemIndex(0) 
  
 'Mount the VHD
 VHDService.Mount(VHD)

Becomes:

 Option Explicit
  
 Dim WMIService
 Dim VHDService
 Dim VHD 
 Dim Result
 Dim Job
 Dim InParam
 Dim OutParam
  
 'Specify the VHD to be mounted
 VHD = "C:\Hyper-V Virtual Machines\Virtual Hard Disks\SCVMM - Domain Controller.vhd" 
  
 'Get instance of 'virtualization' WMI service on the local computer
 Set WMIService = GetObject("winmgmts:\\.\root\virtualization")  
  
 'Get the MSVM_ImageManagementServiceSet 
 Set VHDService = WMIService.ExecQuery("SELECT * FROM Msvm_ImageManagementService").ItemIndex(0)
  
 'Setup the input parameter list
 Set InParam = VHDService.Methods_("mount").InParameters.SpawnInstance_()
 InParam.Path = VHD 
  
 'Execute the method and store the results in OutParam
 Set OutParam = VHDService.ExecMethod_("mount", InParam)
  
 'Check to see if the jub completed synchronously
 if (OutParam.ReturnValue = 0) then
    Wscript.Echo "Operation succeeded"
 elseif (OutParam.ReturnValue <> 4096) then
    Wscript.Echo "Operation failed"
 else
    
    'Get the job object
    set Job = WMIService.Get(OutParam.Job)
  
    'Wait for the job to complete (3 == starting, 4 == running)
    while (Job.JobState = 3) or (Job.JobState = 4)
       Wscript.Echo Job.PercentComplete
       WScript.Sleep(1000)
       
       'Refresh the job object
       set Job = WMIService.Get(OutParam.Job)
    Wend
  
    'Provide details if the job fails (7 == complete)
    if (Job.JobState <> 7) then
       Wscript.Echo "Operation failed"
       Wscript.Echo "ErrorCode:" & Job.ErrorCode
       Wscript.Echo "ErrorDescription:" & Job.ErrorDescription
    else
       Wscript.Echo "Operation succeeded"
    end If
 end if

Yikes!  The issue here is that when you call a method under VBScript directly you do not get both the return value and job path in the output.  You just get the return value.  In order to get both output parameters you need to use "ExecMethod_", which means you need to make an instance of the input parameters, populate it and pass it to ExecMethod - along with the name of the method you want to run.

Cheers,
Ben