Prepare VM: Programmatically Add ISO. Hyper-V, C# version

[Sergei Meleshchuk. http://blogs.msdn.com/sergeim/] 

Imagine I want prepare VM fully automatically (programmatically, via code). Say I need a fully functional VM. That is, OS and applications loaded, ready for use. Why adding (mounting) ISO is important here?

Here is why. To set up a VM, I need 3 or 4 steps:

-          Create VM

-          Add (mount) installation disk. That is, add ISO

-          Boot from this ISO image

-          Next depends on ISO. If this is normal OS image, you will need to finish installation with some manual actions. However, ISO may contain PXE which will just do un-attended installation from say network share or via WDS (this is for future post).

About this document

I outline steps (of adding ISO), briefly comment on implementation of each step, than provide complete code.

Credits

Inspired by James O’Neill’s PowerShell code. See his site.

Steps outline

Step 1 will be grabbing an empty “template” of the ISO image. This template is pre-existent in Hyper-V WMI-based management infrastructure.

Step 2 is to obtain management object representing our VM; the VM is likely to be just created. We only need this VM object to get access to the disk controller. To cut down number of command line parameters, we will hard-code the controller ID: it will be 1 (naturally).

Step 3 will be obtaining the controller object. That is, the WMI object representing controller 1 on specific VM.

Step 4, somewhat lengthy to explain. There is a pre-existing disk object for each disk controller 1 on each VM. This disk object has parent field, which is the controller’s identifier in WMI meaning of the word “identifier”. That is, the parent field is the WMI object path, usually pretty long string. Thus we will search for the disk object by scanning controllers, and comparing their Path with the parent field in each disk object. Little clumsy, yet I don’t know of another way.

Step 5. Here important work starts. We set values on template (from step 1). There will be 2 values to set: a) path to ISO image, which we originally obtain from the command line and b) the disk object from step 4.

Step 6 is to ask the management service to add the template (from step 4) to the VM (from step 2).

Step 0. Preparation

Use abbreviations and get data from command line. This is simple.

    using MO = ManagementObject;

    using MBO = ManagementBaseObject;

    using MOS = ManagementObjectCollection;

 

. . .

            const string CONTROLLER_ID_1 = "1";

            string vmDisplayName;

            string iso;

       // e.g. @"H:\ISO\Virtual Server 2008\amd64fre.iso";

 

            // get arguments

 

            if (args.Length < 2)

            {

                Console.WriteLine("usage: addiso <vmElementName> <isoPath>");

                return;

            }

            vmDisplayName = args[0];

            iso = args[1];

 

Step 1. Get ISO template object

One line:

MO moTemplateRasd = GetISOTemplate();

-          where the GetISOTemplate we have to write ourselves:

private MO GetISOTemplate()

{

    string where;

 

    MO capability = GetWmiObject(

        "MsVM_AllocationCapabilities",

        "ResourceType = 21 and ResourceSubType = 'Microsoft Virtual CD/DVD Disk'");

    AssertWmi(capability);

 

    // -- MsVM_SettingsDefineCapabilities

 

    where = string.Format(

        "valuerange=0 and Groupcomponent = \"{0}\"",

        FixPath(capability.Path.Path));

    Log(capability.Path.Path);

    Log(where);

    MO define = GetWmiObject("MsVM_SettingsDefineCapabilities", where);

    string partComponent = define["partcomponent"] as string;

    Log("*{0}", partComponent);

 

    // -- MsVM_ResourceAllocationSettingData for IDE drive (RASD/IDE)

 

    MO component = new MO(partComponent);

    Logc("COMP", "#{0}", component.Path.Path);

    // this ws the one we want to set parent of

    DumpMO("COMP", component);

    return component;

}

 

Step 2. Get VM object.

This is straight:

MO moVmComputerSystem = GetVM(vmDisplayName);

string vmGuidName = moVmComputerSystem["name"] as string;

 

with the GetVM function being:

MO GetVM(string vmElementName)

{

    string where;

    where = string.Format(

        CultureInfo.InvariantCulture,

        "elementname='{0}'",

        vmElementName);

    MO theVm = GetWmiObject("msvm_computersystem", where);

    return theVm;

}

 

Step 3. Get controller.

This is little difficult. We look for controller, having a) VM object and b) controller ID, which is traditionally (at the time of writing) 1.

 

MO moControllerRasd = GetController(moVmComputerSystem, CONTROLLER_ID_1);

GetController is our function that we have to write. The code is below.

MO GetController(MO theVm, string controllerId)

{

    string vmName = theVm["name"] as string;

    string where = string.Format(

        CultureInfo.InvariantCulture,

        "resourceSubtype='{0}' and instanceId Like 'Microsoft:{1}%' and address='{2}'",

        "Microsoft Emulated IDE Controller",

        vmName,

        controllerId);

    MO moControllerRasd = GetWmiObject("MsVM_ResourceAllocationSettingData", where);

    return moControllerRasd;

}

 

Step 4. Get disk object – child of the controller

MO moDiskRasd = GetControllerChild(

    vmGuidName,

    moControllerRasd.Path.Path);

 

Again, the GetControllerChild is what we have to write:

MO GetControllerChild(string vmName, string controllerPath)

{

    string where = string.Format(

        CultureInfo.InvariantCulture,

        "parent Like '%Microsoft:{0}%'",

        vmName);

    MOS childrenDisks = GetWmiObjects(Constants.RASD_CLASS, where);

    foreach (MO disk in childrenDisks)

    {

        string parentPath = disk["parent"] as string;

        if (parentPath != null)

        {

            if (parentPath.Equals(controllerPath, StringComparison.OrdinalIgnoreCase))

            {

                DumpMO("child", disk);

                return disk;

            }

        }

    }

    throw new InvalidOperationException("Cant locate child drive for controller " + controllerPath);

}

 

Step 5. Update template

// Step 5. Now actual work: 

//     - update template with disk resource data (by setting template's parent), and

//     - set ISO location

 

moTemplateRasd["parent"] = moDiskRasd.Path.Path;

moTemplateRasd["connection"] = new string[] { iso };

moTemplateRasd.Put();

 

Step 6. Ask the management service to apply filled-in template

 

MO moManagementService = GetMsVM_virtualSystemManagementService();

 MBO mboCallInParams = moManagementService.GetMethodParameters("AddVirtualSystemResources");

 mboCallInParams["TargetSystem"] = moVmComputerSystem;

 string componentText = moTemplateRasd.GetText(TextFormat.WmiDtd20);

 mboCallInParams["ResourceSettingData"] = new string[] { componentText };

 ManagementBaseObject mboCallOutParams = moManagementService.InvokeMethod(

     "AddVirtualSystemResources",

     mboCallInParams,

     null);

 

 

Full code

// auth: Sergei Meleshchuk, June 2008.

// Based in part on powershell code by James O'Naill

using System;

using System.Management;

using System.Globalization;

namespace Hyperv.Misc

{

    using MO = ManagementObject;

    using MBO = ManagementBaseObject;

    using MOS = ManagementObjectCollection;

 

    class MainAddISO3

    {

        static void Main(string[] args)

        {

            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(Oops);

            MainAddISO3 main = new MainAddISO3();

            main.AddISO(args);

        }

        private void AddISO(string[] args)

        {

            const string CONTROLLER_ID_1 = "1";

            string vmDisplayName;

            string iso;              // e.g. @"H:\ISO\Virtual Server 2008\6001.18000.080118-1840_amd64fre_Server_en-us_VL-KRMSXVOL_EN_DVD.iso";

 

            // get arguments

 

            if (args.Length < 2)

            {

                Console.WriteLine("usage: addiso <vmElementName> <isoPath>");

                return;

            }

            vmDisplayName = args[0];

            iso = args[1];

 

            // Step 1. Get template (of type: MsVM_ResourceAllocationSettingData)

 

            MO moTemplateRasd = GetISOTemplate();

 

            // Step 2. Get VM (needed when locating controller, of type msvm_ComputerSystem)

 

            MO moVmComputerSystem = GetVM(vmDisplayName);

            string vmGuidName = moVmComputerSystem["name"] as string;

 

            // Step 3. Get disk controller (of type: MsVM_ResourceAllocationSettingData)

 

            MO moControllerRasd = GetController(moVmComputerSystem, CONTROLLER_ID_1);

 

            // Step 4. Get Disk - the RASD object to which the controller is a parent

 

            MO moDiskRasd = GetControllerChild(

                vmGuidName,

                moControllerRasd.Path.Path);

 

            // Step 5. Now actual work: 

            //     - update template with disk resource data (by setting template's parent), and

            //     - set ISO location

 

            moTemplateRasd["parent"] = moDiskRasd.Path.Path;

            moTemplateRasd["connection"] = new string[] { iso };

            moTemplateRasd.Put();

 

            // Step 6. Now really actual work: Add actual resource from info in the template

 

            MO moManagementService = GetMsVM_virtualSystemManagementService();

            MBO mboCallInParams = moManagementService.GetMethodParameters("AddVirtualSystemResources");

            mboCallInParams["TargetSystem"] = moVmComputerSystem;

            string componentText = moTemplateRasd.GetText(TextFormat.WmiDtd20);

            mboCallInParams["ResourceSettingData"] = new string[] { componentText };

            ManagementBaseObject mboCallOutParams = moManagementService.InvokeMethod(

                "AddVirtualSystemResources",

                mboCallInParams,

                null);

            DumpMBO("AddVirtualSystemResources:", mboCallOutParams);

            // Check result here

        }

 

        // Main helpers

#region helpers

        MO GetVM(string vmElementName)

        {

            string where;

            where = string.Format(

                CultureInfo.InvariantCulture,

                "elementname='{0}'",

                vmElementName);

            MO theVm = GetWmiObject("msvm_computersystem", where);

            return theVm;

        }

        MO GetController(MO theVm, string controllerId)

        {

            string vmName = theVm["name"] as string;

            string where = string.Format(

                CultureInfo.InvariantCulture,

                "resourceSubtype='{0}' and instanceId Like 'Microsoft:{1}%' and address='{2}'",

                "Microsoft Emulated IDE Controller",

                vmName,

                controllerId);

            MO moControllerRasd = GetWmiObject("MsVM_ResourceAllocationSettingData", where);

            return moControllerRasd;

        }

        MO GetControllerChild(string vmName, string controllerPath)

        {

            string where = string.Format(

                CultureInfo.InvariantCulture,

                "parent Like '%Microsoft:{0}%'",

                vmName);

            MOS childrenDisks = GetWmiObjects(Constants.RASD_CLASS, where);

            foreach (MO disk in childrenDisks)

            {

                string parentPath = disk["parent"] as string;

                if (parentPath != null)

                {

                    if (parentPath.Equals(controllerPath, StringComparison.OrdinalIgnoreCase))

                    {

                        DumpMO("child", disk);

                        return disk;

                    }

                }

            }

            throw new InvalidOperationException("Cant locate child drive for controller " + controllerPath);

        }

        private MO GetMsVM_virtualSystemManagementService()

        {

            return GetWmiObject("MsVM_virtualSystemManagementService", null);

        }

        private MO GetISOTemplate()

        {

            string where;

 

            MO capability = GetWmiObject(

                "MsVM_AllocationCapabilities",

                "ResourceType = 21 and ResourceSubType = 'Microsoft Virtual CD/DVD Disk'");

            AssertWmi(capability);

 

            // -- MsVM_SettingsDefineCapabilities

 

            where = string.Format(

                "valuerange=0 and Groupcomponent = \"{0}\"",

                FixPath(capability.Path.Path));

            Log(capability.Path.Path);

            Log(where);

            MO define = GetWmiObject("MsVM_SettingsDefineCapabilities", where);

            string partComponent = define["partcomponent"] as string;

            Log("*{0}", partComponent);

 

            // -- MsVM_ResourceAllocationSettingData for IDE drive (RASD/IDE)

 

            MO component = new MO(partComponent);

            Logc("COMP", "#{0}", component.Path.Path);

            // this ws the one we want to set parent of

            DumpMO("COMP", component);

            return component;

        }

        #endregion helpers

 

        #region generic wmi helpers

        static string FixPath(string path)

        {

            string result = path.Replace("\\", "\\\\").Replace("\"", "'");

            Console.ForegroundColor = ConsoleColor.DarkYellow;

            Log("FixPath returns\n" + result);

            Console.ResetColor();

            return result;

        }

        private MO GetWmiObject(string classname, string where)

        {

            MOS resultset = GetWmiObjects(classname, where);

            if (resultset.Count != 1)

                throw new InvalidOperationException(

                    string.Format(

                        "Cannot locate {0} where {1}",

                        classname,

                        where));

            MOS.ManagementObjectEnumerator en = resultset.GetEnumerator();

            en.MoveNext();

            MO result = en.Current as MO;

            if (result == null)

                throw new InvalidOperationException("Failure retrieving " + classname + " where " + where);

            return result;

        }

 

        private MOS GetWmiObjects(string classname, string where)

        {

            string query;

            ManagementScope scope = new ManagementScope(@"root\virtualization", null);

            if (where != null)

            {

                query = string.Format(

                   "select * from {0} where {1}",

                   classname,

                   where);

            }

            else

            {

                query = string.Format(

                    CultureInfo.InvariantCulture,

                    "select * from {0}",

                    classname);

            }

            ManagementObjectSearcher searcher = new ManagementObjectSearcher(

                scope,

                new ObjectQuery(query));

 

            ManagementObjectCollection resultset = searcher.Get();

            return resultset;

        }

 

        void AssertWmi(MO mo)

        {

            if (mo == null)

                throw new NullReferenceException("dfasd");

        }

        void AssertNotNull(string tag, object o)

        {

            if (o == null)

                throw new NullReferenceException("AssertFailed with tag " + tag);

        }

        #endregion generic wmi helpers

        // -- end of main helpers

 

        // ---- Logging functions -----

        #region logging

        private static void Log(string message, params object[] data)

        {

            Console.WriteLine(message, data);

        }

        private static void Logc(string title, string message, params object[] data)

        {

            Console.ForegroundColor = ConsoleColor.Green;

            Console.Write(title);

            Console.ResetColor();

            Console.WriteLine(message, data);

        }

       private static void DumpMO(

           string message,

           MO mo,

           params object[] data)

       {

           Console.ForegroundColor = ConsoleColor.White;

           Console.WriteLine(message, data);

           Console.ResetColor();

           PropertyDataCollection props = mo.Properties;

           foreach (PropertyData prop in props)

           {

               Log("{0:20} {1}", prop.Name, prop.Value);

           }

       }

       private static void DumpMBO(

           string message,

           ManagementBaseObject mo,

           params object[] data)

       {

           Console.ForegroundColor = ConsoleColor.White;

           Console.WriteLine(message, data);

           Console.ResetColor();

           PropertyDataCollection props = mo.Properties;

           foreach (PropertyData prop in props)

           {

               Log("{0:20} {1}", prop.Name, prop.Value);

           }

       }

        #endregion logging

       // expection handler

        private static void Oops(object sender, UnhandledExceptionEventArgs e)

        {

            Console.BackgroundColor = ConsoleColor.White;

            Console.ForegroundColor = ConsoleColor.Black;

            Exception ex = e.ExceptionObject as Exception;

            Log(ex.Message);

            Console.ResetColor();

            Log(ex.ToString());

        }

    } // class MainCreateVm

 

    class Constants

    {

        internal const string DefineVirtualSystem = "DefineVirtualSystem";

        internal const string ModifyVirtualSystem = "ModifyVirtualSystem";

        internal const uint ERROR_SUCCESS = 0;

        internal const uint ERROR_INV_ARGUMENTS = 87;

        internal const string RASD_CLASS = "MsVM_ResourceAllocationSettingData";

    }

}