Wednesday, October 02, 2013

Programmatically controlling Hyper-V Server 2012 R2 virtual machines from C#

Recently I've wanted to remote control some virtual machines on a Hyper-V server in aid of automated testing.
I've got a server set up running Hyper-V Server 2012 R2, so the code samples in this are likely to work against Server 2012 non-R2, but may not work against Server 2008.

All the virtual machines start off at a known clean snapshot, and have a startup script which runs on boot that goes and asks a central database for a testing job to execute. So, for our system to work, we need to accomplish the following:
  1. Roll back a VM to the last known snapshot / checkpoint
  2. Power it on
There are several other things we need to do for this to work at all, which are:
  1. Connect to the Hyper-V server (this includes authentication)
  2. List out or otherwise ask the server about it's VM's so that we can rollback the correct one

To achieve thisk I'm using Hyper-V's WMI provider (V2). I don't really know much about WMI, so these code samples are just things I've hacked together. They are NOT production quality and they don't handle errors well or anything else. Please use them as guidance, not for copy/pasting.

Pre-requisites

Using WMI from C# is done by the classes in the System.Management namespace. To access this you'll want using System.Management at the top of your C# file, as well as a reference to the System.Management assembly.

Connecting and Logging on


To do this we need to create a ManagementScope object set to use the appropriate server and using the Hyper-V virtualization v2 namespace, and we also need to supply the username and password of a windows account that has privileges to administer the Hyper-V server. I did it like this:

var connectionOptions = new ConnectionOptions(
    @"en-US",
    @"domain\user",
    @"password",
    null,
    ImpersonationLevel.Impersonate, // I don't know if this is correct, but it worked for me
    AuthenticationLevel.Default,
    false,
    null,
    TimeSpan.FromSeconds(5);

var scope = new ManagementScope(new ManagementPath { 
    Server = "hostnameOrIpAddress", 
    NamespacePath = @"root\virtualization\v2" }, connectionOptions);
scope.Connect();

Note: Most of the other documentation or sample code I found refers to the root\virtualization namespace. This didn't work at all for me in Server 2012 R2, and I had to use dotPeek to decompile the Hyper-V powershell commandlets to figure out to put \v2 on the end. Perhaps the non-v2 one is for Server 2008?

Note: If your PC and the target server are on the same domain, and your windows user account has privileges to administer the remote server, You don't need the ConnectionOptions object at all. WMI will use windows authentication, and it will magically work:

Listing out the VM's and finding the one we want

Virtual machines in Hyper-V get exposed via the Msvm_ComputerSystem WMI class. In order to list them out, we simply ask WMI to give us all the objects of that class in the virtualization namespace. To do this, I'm going to create two helper extension methods, that we'll use from hereon:

public static class WmiExtensionMethods
{
    public static ManagementObject GetObject(this ManagementScope scope, string serviceName)
    {
        return GetObjects(scope, serviceName).FirstOrDefault();
    }
    
    public static IEnumerable<ManagementObject> GetObjects(this ManagementScope scope, string serviceName)
    {
        return new ManagementClass(scope, new ManagementPath(serviceName), null)
            .GetInstances()
            .OfType<ManagementObject>();
    }
}

It turns out that the Host PC is also exposed via Msvm_ComputerSystem, so we need to filter out things that are not virtual machines. This helper method will return a list of all the virtual machines. Note: I like extension methods, so I'm using them a lot here:

public static IEnumerable<ManagementObject> GetVirtualMachines(this ManagementScope scope)
{
    return scope.GetObjects("Msvm_ComputerSystem").Where(x => "Virtual Machine" == (string)x["Caption"]);
}

You can use it to get a specific virtual machine as follows:

var vm = scope.GetVirtualMachines().First(vm => vm["ElementName"] as string == "myvmname");

Rolling the VM back to it's latest snapshot / checkpoint

In order to roll a Hyper-V VM back to a snapshot, we need to get a reference to the snapshot object itself.

There are ways to simply list all the snapshots for each VM, but there is also a special Msvm_MostCurrentSnapshotInBranch class which represents the "latest" snapshot. We can use it to create a helper method as follows:

public static ManagementObject GetLastSnapshot(this ManagementObject virtualMachine)
{
    return virtualMachine.GetRelated(
        "Msvm_VirtualSystemSettingData",
        "Msvm_MostCurrentSnapshotInBranch",
        null,
        null,
        "Dependent",
        "Antecedent",
        false,
        null).OfType<ManagementObject>().FirstOrDefault();
}

And use it like this:

var snapshot = vm.GetLastSnapshot();

Now, to actually roll the VM back, we need to call the ApplySnapshot method on the Msvm_VirtualSystemSnapshotService class.
Note: Logically I thought that the snapshot methods should be on the Virtual Machine object, but Hyper-V puts them all in their own service for some reason. There is also only one global Snapshot service - not a service per VM. I've no idea why they've designed it this way.

We can create a helper method:

public static uint ApplySnapshot(this ManagementScope scope, ManagementObject snapshot)
{
    var snapshotService = scope.GetObject("Msvm_VirtualSystemSnapshotService");

    var inParameters = snapshotService.GetMethodParameters("ApplySnapshot");
    inParameters["Snapshot"] = snapshot.Path.Path;
    var outParameters = snapshotService.InvokeMethod("ApplySnapshot", inParameters, null);
    return (uint)outParameters["ReturnValue"];
}

And use it like this:

scope.ApplySnapshot(snapshot);

When I execute this with valid parameters, the VM applied it's snapshot, and I always got a return value of 4096. According to the documentation, this indicates that there's a Job in progress to asynchronously track the actual snapshot applying and determine the final success or failure. We could fetch the job out of outParameters["Job"] and use it to determine when the apply completes, but I'm not going to worry about that here. Refer to the ApplySnapshot MSDN page for other possible return codes.

Note: If the VM is not powered off, you are likely to find the ApplySnapshot call fails. You must power the VM off (and wait for the Power Off job to complete) first.

Powering on the VM to boot it up


Turning on the VM is done by the RequestStateChange method on the Msvm_ComputerSystem class. We already have the Msvm_ComputerSystem object representing the virtual machine we found earlier, so we can create a helper method and enumeration like this:

public enum VmRequestedState : ushort
{
    Other = 1,
    Running = 2,
    Off = 3,
    Saved = 6,
    Paused = 9,
    Starting = 10,
    Reset = 11,
    Saving = 32773,
    Pausing = 32776,
    Resuming = 32777,
    FastSaved = 32779,
    FastSaving = 32780,
}

public static uint RequestStateChange(this ManagementObject virtualMachine, VmRequestedState targetState)
{
    var managementService = virtualMachine.Scope.GetObject("Msvm_VirtualSystemManagementService");

    var inParameters = managementService.GetMethodParameters("RequestStateChange");
    inParameters["RequestedState"] = (object)targetState;
    var outParameters = virtualMachine.InvokeMethod("RequestStateChange", inParameters, null);
    return (uint)outParameters["ReturnValue"];
}

And use it like this:

vm.RequestStateChange(VmRequestedState.Running);

As for ApplySnapshot, when things work, the return value is usually 4096, indicating there is a Job to asynchronously track the progress of the operation.

Note: Although we invoke the method on the Msvm_ComputerSystem object, we need to get the method parameters by asking the Msvm_VirtualSystemManagementService object (which represents the host server) instead. I've no idea why.

Final fixups


ApplySnapshot will fail if the virtual machine is running. To turn it off, we can simply call vm.RequestStateChange(VmRequestedState.Off); and wait a bit for Job to complete.

RequestStateChange will fail if the state doesn't make sense. For example, if you try and turn the vm Off when it's already Off, the method will fail. You can check the current state of a Vm by reading it's EnabledState property. Valid values for EnabledState are documented with the Msvm_ComputerSystem class. I created an enum, and used it as follows:

public enum VmState : ushort
{
    Unknown = 0,
    Other,
    Running, // Enabled
    Off, // Disabled
    ShuttingDown,
    NotApplicable,
    OnButOffline,
    InTest,
    Deferred,
    Quiesce,
    Starting
}

if ((VmState)vm["EnabledState"] != VmState.Off)
{
    vm.RequestStateChange(VmRequestedState.Off); // needs to be off to apply snapshot
    Thread.Sleep(2000); // todo wait for the state change properly
}

You can do many more things by using other methods and classes from the Hyper-V WMI API. Hopefully this gives you a decent starting point.

P.S. The Hyper-V powershell commandlets are all implemented on top of this WMI Api. Using a tool like .NET reflector or dotPeek is an interesting way to see how Microsoft calls the WMI API.

3 comments:

Babak said...

Have you ever managed to rename a snapshot by using v2? Can you let me know how that can be done?

Unknown said...

Want to learn C# from basics & in most professional Code development style. I'll have you writing real working applications in no time flat-before you reach the end of course.
Hire C# Teacher.

ebenofthecamera said...

Thanks for posting this solution. Have you ever attempted to attach passthrough disks? I'm having a bit of difficulty utilizing Msvm_AllocationCapabilities in attaching passthrough disks, finally figured out how to utilize RASD. Thanks for this.