xenadmin/XenModel/Actions/VM/CreateVMAction.cs

740 lines
28 KiB
C#
Raw Normal View History

/* Copyright (c) Citrix Systems Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms,
* with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using XenAdmin.Network;
using XenAPI;
using XenAdmin.Core;
using System.Xml;
namespace XenAdmin.Actions.VMActions
{
public enum InstallMethod
{
None,
CD,
Network
}
public class CreateVMAction : AsyncAction
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private readonly string NameLabel;
private readonly string NameDescription;
private readonly InstallMethod InsMethod;
private readonly string PvArgs;
private readonly VDI Cd;
private readonly string Url;
private readonly Host HomeServer;
private readonly long Vcpus;
private readonly long MemoryDynamicMin, MemoryDynamicMax, MemoryStaticMax;
private readonly List<DiskDescription> Disks;
private readonly List<VIF> Vifs;
private readonly bool StartAfter;
private readonly Host CopyBiosStringsFrom;
private readonly SR FullCopySR;
private readonly GPU_group GpuGroup;
private readonly VGPU_type VgpuType;
private readonly long CoresPerSocket;
private Action<VMStartAbstractAction, Failure> _startDiagnosisForm;
private Action<VM, bool> _warningDialogHAInvalidConfig;
private bool PointOfNoReturn;
private bool assignOrRemoveVgpu;
/// <summary>
/// These are the RBAC dependencies that you always need to create a VM. Check CreateVMAction constructor for runtime dependent dependencies.
/// </summary>
public static RbacMethodList StaticRBACDependencies = new RbacMethodList(
// provision VM
"vm.provision",
"vm.set_other_config",
// set VM Params
"vm.set_name_label",
"vm.set_name_description",
"vm.set_VCPUs_max",
"vm.set_VCPUs_at_startup",
// set VM Boot Params
"vm.set_HVM_boot_params",
"vm.set_PV_args",
"vm.set_other_config",
// Add CD Drive
"vbd.eject",
"vbd.insert",
// Create CD Drive
"vbd.create",
// Add disks
"vdi.destroy",
"vdi.create",
"vdi.set_sm_config",
"vbd.create",
"vbd.destroy",
"vdi.copy",
// Add networks
"vif.create",
"vm.set_platform"
);
public CreateVMAction(IXenConnection connection, VM template, Host copyBiosStringsFrom,
string name, string description, InstallMethod installMethod,
string pvArgs, VDI cd, string url, Host homeServer, long vcpus,
long memoryDynamicMin, long memoryDynamicMax, long memoryStaticMax,
List<DiskDescription> disks, SR fullCopySR, List<VIF> vifs, bool startAfter,
Action<VM, bool> warningDialogHAInvalidConfig,
Action<VMStartAbstractAction, Failure> startDiagnosisForm,
GPU_group gpuGroup, VGPU_type vgpuType, long coresPerSocket)
: base(connection, string.Format(Messages.CREATE_VM, name),
string.Format(Messages.CREATE_VM_FROM_TEMPLATE, name, Helpers.GetName(template)))
{
Template = template;
CopyBiosStringsFrom = copyBiosStringsFrom;
FullCopySR = fullCopySR;
NameLabel = name;
NameDescription = description;
InsMethod = installMethod;
PvArgs = pvArgs;
Cd = cd;
Url = url;
HomeServer = homeServer;
Vcpus = vcpus;
MemoryDynamicMin = memoryDynamicMin;
MemoryDynamicMax = memoryDynamicMax;
MemoryStaticMax = memoryStaticMax;
Disks = disks;
Vifs = vifs;
StartAfter = startAfter;
_warningDialogHAInvalidConfig = warningDialogHAInvalidConfig;
_startDiagnosisForm = startDiagnosisForm;
GpuGroup = gpuGroup;
VgpuType = vgpuType;
CoresPerSocket = coresPerSocket;
Pool pool_of_one = Helpers.GetPoolOfOne(Connection);
if (HomeServer != null || pool_of_one != null) // otherwise we have no where to put the action
AppliesTo.Add(HomeServer != null ? HomeServer.opaque_ref : pool_of_one.opaque_ref);
assignOrRemoveVgpu = (GpuGroup != null && VgpuType != null)
|| (!Helpers.FeatureForbidden(Connection, Host.RestrictVgpu)
&& Helpers.VgpuCapability(Connection));
#region RBAC Dependencies
if (StartAfter)
ApiMethodsToRoleCheck.Add("vm.start");
if (HomeServerChanged())
ApiMethodsToRoleCheck.Add("vm.set_affinity");
if (Template.memory_dynamic_min != MemoryDynamicMin || Template.memory_dynamic_max != MemoryDynamicMax || Template.memory_static_max != MemoryStaticMax)
ApiMethodsToRoleCheck.Add("vm.set_memory_limits");
if (assignOrRemoveVgpu)
{
ApiMethodsToRoleCheck.Add("VGPU.destroy");
ApiMethodsToRoleCheck.Add("VGPU.create");
}
ApiMethodsToRoleCheck.AddRange(StaticRBACDependencies);
ApiMethodsToRoleCheck.AddRange(Role.CommonTaskApiList);
ApiMethodsToRoleCheck.AddRange(Role.CommonSessionApiList);
#endregion
}
protected override void Run()
{
if (FullCopySR != null)
{
// VM.copy is the best call to make if all target disks are on the same SR.
// however, if the target disks are on the same SR as the source disks, then the user is
// given the choice of a fast-clone (VM.clone) or a full-copy (VM.copy) on the storage page of the wizard. If the
// user chose a VM.clone, then FullCopySR will be null.
RelatedTask = VM.async_copy(Session, Template.opaque_ref, HiddenVmName, FullCopySR.opaque_ref);
}
else
{
// if the target disks are on mixed storage or the user chose to a do a fast-clone on the storage
// page then we end up here.
RelatedTask = VM.async_clone(Session, Template.opaque_ref, HiddenVmName);
}
Description = string.Format(Messages.CLONING_TEMPLATE, Helpers.GetName(Template));
PollToCompletion(0, 10);
VM = Connection.WaitForCache(new XenRef<VM>(Result));
CopyBiosStrings();
SetXenCenterProperties();
ProvisionVM();
SetVMParams();
SetVMBootParams();
AddCdDrive();
AddDisks();
AddNetworks();
XenAdminConfigManager.Provider.ShowObject(VM.opaque_ref);
PointOfNoReturn = true;
AssignVgpu();
if (StartAfter)
{
Description = Messages.STARTING_VM;
var startAction = new VMStartAction(VM, _warningDialogHAInvalidConfig, _startDiagnosisForm);
startAction.RunAsync();
}
Description = Messages.VM_SUCCESSFULLY_CREATED;
}
private void AssignVgpu()
{
if (assignOrRemoveVgpu)
{
var action = new GpuAssignAction(VM, GpuGroup, VgpuType);
action.RunExternal(Session);
}
}
private void CopyBiosStrings()
{
if (CopyBiosStringsFrom != null && Template.DefaultTemplate)
{
VM.copy_bios_strings(Session, this.VM.opaque_ref, CopyBiosStringsFrom.opaque_ref);
}
}
private void SetXenCenterProperties()
{
XenAdminConfigManager.Provider.HideObject(VM.opaque_ref);
AppliesTo.Add(VM.opaque_ref);
}
private void SetVMParams()
{
Description = Messages.SETTING_VM_PROPERTIES;
XenAPI.VM.set_name_label(Session, VM.opaque_ref, NameLabel);
XenAPI.VM.set_name_description(Session, VM.opaque_ref, NameDescription);
ChangeVCPUSettingsAction vcpuAction = new ChangeVCPUSettingsAction(VM, Vcpus);
vcpuAction.RunExternal(Session);
// set cores-per-socket
Dictionary<string, string> platform = VM.platform == null ?
new Dictionary<string, string>() :
new Dictionary<string, string>(VM.platform);
platform["cores-per-socket"] = CoresPerSocket.ToString();
VM.set_platform(Session, VM.opaque_ref, platform);
// Check these values have changed before setting them, as they are RBAC protected
if (HomeServerChanged())
XenAPI.VM.set_affinity(Session, VM.opaque_ref, HomeServer != null ? HomeServer.opaque_ref : Helper.NullOpaqueRef);
if (Helpers.MidnightRideOrGreater(VM.Connection))
{
if (Template.memory_dynamic_min != MemoryDynamicMin || Template.memory_dynamic_max != MemoryDynamicMax || Template.memory_static_max != MemoryStaticMax)
XenAPI.VM.set_memory_limits(Session, VM.opaque_ref, Template.memory_static_min, MemoryStaticMax, MemoryDynamicMin, MemoryDynamicMax);
}
else
{
// For George and earlier hosts, we set them all the same.
// The order of operations doesn't matter, because George didn't enforce the ordering of values.
XenAPI.VM.set_memory_dynamic_min(Session, VM.opaque_ref, MemoryStaticMax);
XenAPI.VM.set_memory_dynamic_max(Session, VM.opaque_ref, MemoryStaticMax);
XenAPI.VM.set_memory_static_max(Session, VM.opaque_ref, MemoryStaticMax);
}
}
private bool HomeServerChanged()
{
if (HomeServer == null)
{
return Template.affinity.opaque_ref != Helper.NullOpaqueRef;
}
return HomeServer.opaque_ref != Template.affinity.opaque_ref;
}
private void SetVMBootParams()
{
if (Template.IsHVM && (Disks.Count == 0 || InsMethod == InstallMethod.Network)) // CA-46213
{
// boot from network
Dictionary<string, string> hvm_params = VM.HVM_boot_params;
hvm_params["order"] = GetBootOrderNetworkFirst();
XenAPI.VM.set_HVM_boot_params(Session, VM.opaque_ref, hvm_params);
}
else if (IsEli() && InsMethod == InstallMethod.Network)
{
Dictionary<string, string> other_config = VM.other_config;
string normal_url = IsRhel() ? NormalizeRepoUrlForRHEL(Url) : Url;
other_config["install-repository"] = normal_url;
XenAPI.VM.set_other_config(Session, VM.opaque_ref, other_config);
}
else if (IsEli() && InsMethod == InstallMethod.CD)
{
Dictionary<string, string> other_config = VM.other_config;
other_config["install-repository"] = "cdrom";
XenAPI.VM.set_other_config(Session, VM.opaque_ref, other_config);
}
if (!Template.IsHVM)
{
XenAPI.VM.set_PV_args(Session, VM.opaque_ref, PvArgs);
}
}
private bool IsEli()
{
return !Template.IsHVM && Template.PV_bootloader == "eliloader";
}
private bool IsRhel()
{
string distro = VM.InstallDistro;
return distro == "rhel41" || distro == "rhel44" || distro == "rhlike";
}
private string NormalizeRepoUrlForRHEL(string url)
{
Uri uri = new Uri(url);
return uri.Scheme == "nfs" ? string.Format("nfs:{0}:{1}", uri.Host, uri.PathAndQuery) : url;
}
private void ProvisionVM()
{
Description = Messages.PROVISIONING_VM;
RewriteProvisionXML();
RelatedTask = XenAPI.VM.async_provision(Session, VM.opaque_ref);
PollToCompletion(10, 60);
}
private void RewriteProvisionXML()
{
XmlNode xml = VM.ProvisionXml;
if (xml == null)
return;
List<DiskDescription> disksToProvision = Disks.Where(d => d != null && d.Type == DiskDescription.DiskType.New).ToList();
foreach (XmlNode diskNode in xml.ChildNodes)
{
foreach (DiskDescription disk in disksToProvision)
{
if (disk.Device.userdevice == diskNode.Attributes["device"].Value)
{
// write details for this disk
diskNode.Attributes["size"].Value = disk.Disk.virtual_size.ToString();
diskNode.Attributes["sr"].Value = XenAPI.SR.get_uuid(Session, disk.Disk.SR.opaque_ref);
diskNode.Attributes["bootable"].Value = disk.Device.userdevice == "0" && InsMethod != InstallMethod.CD ? "true" : "false";
}
}
}
// set the new vm's provision xml
Dictionary<string, string> other_config = VM.other_config;
if (Disks.Count > 0 && Disks[0] != null && Disks[0].Type == DiskDescription.DiskType.New)
other_config["disks"] = xml.OuterXml;
else
other_config.Remove("disks");
XenAPI.VM.set_other_config(Session, VM.opaque_ref, other_config);
}
private void AddCdDrive()
{
if (Helpers.CustomWithNoDVD(Template))
return; // we have skipped the install media page because we are a cutom template with no cd drive - the user doesnt want a cd drive
Description = Messages.CREATE_CD_DRIVE;
VBD cd_drive = null;
foreach (VBD vbd in Connection.ResolveAll(VM.VBDs))
{
if (vbd.type != vbd_type.CD)
continue;
if ("0123".IndexOf(vbd.userdevice) < 0) // userdevice is not 0, 1, 2 or 3: these are the valid positions for CD drives
continue;
cd_drive = vbd;
break;
}
if (cd_drive == null)
{
cd_drive = CreateCdDrive();
}
if (!cd_drive.empty)
{
RelatedTask = VBD.async_eject(Session, cd_drive.opaque_ref);
PollToCompletion(65, 67);
}
if (InsMethod == InstallMethod.CD && Cd != null) // obviously dont insert the empty cd
{
RelatedTask = VBD.async_insert(Session, cd_drive.opaque_ref, Cd.opaque_ref);
PollToCompletion(67, 70);
}
}
private VBD CreateCdDrive()
{
List<string> devices = AllowedVBDs;
if (devices.Count == 0)
throw new Exception(Messages.NO_MORE_USERDEVICES);
VBD vbd = new VBD();
vbd.bootable = InsMethod == InstallMethod.CD;
vbd.empty = true;
vbd.unpluggable = true;
vbd.mode = vbd_mode.RO;
vbd.type = vbd_type.CD;
vbd.userdevice = devices.Contains("3") ? "3" : devices[0];
vbd.device = "";
vbd.VM = new XenRef<VM>(VM.opaque_ref);
vbd.VDI = null;
RelatedTask = VBD.async_create(Session, vbd);
PollToCompletion(60, 65);
return Connection.WaitForCache(new XenRef<VBD>(Result));
}
private void AddDisks()
{
Description = Messages.CREATING_DISKS;
List<VBD> vbds = Connection.ResolveAll(VM.VBDs);
bool firstDisk = true;
string suspendSr = null;
double progress = 70;
double step = 20.0 / (double)Disks.Count;
foreach (DiskDescription disk in Disks)
{
VBD vbd = GetDiskVBD(disk, vbds);
VDI vdi = null;
if (vbd != null)
{
vdi = Connection.Resolve<VDI>(vbd.VDI);
}
if (!DiskOk(disk, vbd))
{
if (vbd != null)
vdi = MoveDisk(disk, vbd, progress, step);
else
vdi = CreateDisk(disk, progress, step);
}
if (vdi == null)
continue;
if (vdi.name_description != disk.Disk.name_description)
VDI.set_name_description(Session, vdi.opaque_ref, disk.Disk.name_description);
if (vdi.name_label != disk.Disk.name_label)
VDI.set_name_label(Session, vdi.opaque_ref, disk.Disk.name_label);
if (firstDisk && Helpers.BostonOrGreater(Connection))
{
//use the first disk to set the VM.suspend_SR
SR vdiSR = Connection.Resolve(vdi.SR);
if(vdiSR != null && !vdiSR.HBALunPerVDI)
suspendSr = vdi.SR;
firstDisk = false;
}
progress += step;
}
if (Helpers.BostonOrGreater(Connection))
VM.set_suspend_SR(Session, VM.opaque_ref, suspendSr);
}
private VBD GetDiskVBD(DiskDescription disk, List<VBD> vbds)
{
foreach (VBD vbd in vbds)
{
if (disk.Device.userdevice == vbd.userdevice)
return vbd;
}
return null;
}
private bool DiskOk(DiskDescription disk, VBD vbd)
{
if (vbd == null)
return false;
VDI vdi = Connection.Resolve(vbd.VDI);
return vdi != null && disk.Disk.SR.opaque_ref == vdi.SR.opaque_ref;
}
private VDI MoveDisk(DiskDescription disk, VBD vbd, double progress, double step)
{
string old_vdi_ref = vbd.VDI.opaque_ref;
RelatedTask = XenAPI.VDI.async_copy(Session, vbd.VDI.opaque_ref, disk.Disk.SR.opaque_ref);
PollToCompletion(progress, progress + 0.25 * step);
AddVMHint(Connection.WaitForCache(new XenRef<VDI>(Result)));
VDI new_vdi = Connection.Resolve(new XenRef<VDI>(Result));
RelatedTask = XenAPI.VBD.async_destroy(Session, vbd.opaque_ref);
PollToCompletion(progress + 0.25 * step, progress + 0.5 * step);
RelatedTask = XenAPI.VDI.async_destroy(Session, old_vdi_ref);
PollToCompletion(progress + 0.5 * step, progress + 0.75 * step);
CreateVbd(disk, new_vdi, progress + 0.75 * step, progress + step, IsDeviceAtPositionZero(disk));
return new_vdi;
}
/// <summary>
/// Helper: Check if the disk is at the zeroth position in the VBD list
/// </summary>
/// <param name="disk"></param>
/// <returns></returns>
private bool IsDeviceAtPositionZero(DiskDescription disk)
{
return disk.Device.userdevice == "0";
}
/// <summary>
/// Create a VDI/disk.
/// If disk type is existing use the VDI in disk description
/// Otherwise create a new disk (provision it from the SR)
/// </summary>
/// <param name="disk"></param>
/// <param name="progress"></param>
/// <param name="step"></param>
/// <returns></returns>
private VDI CreateDisk(DiskDescription disk, double progress, double step)
{
VDI vdi;
bool bootable = false;
if(disk.Type == DiskDescription.DiskType.Existing)
vdi = disk.Disk;
else
{
vdi = CreateVdi(disk, progress, progress + 0.75 * step);
bootable = IsDeviceAtPositionZero(disk);
}
AddVMHint(vdi);
CreateVbd(disk, vdi, progress + 0.75 * step, progress + step, bootable);
return vdi;
}
private void AddVMHint(VDI vdi)
{
Dictionary<string, string> sm_config = VDI.get_sm_config(Session, vdi.opaque_ref);
sm_config["vmhint"] = VM.opaque_ref;
VDI.set_sm_config(Session, vdi.opaque_ref, sm_config);
}
private VDI CreateVdi(DiskDescription disk, double progress1, double progress2)
{
VDI vdi = new VDI();
vdi.name_label = disk.Disk.name_label;
vdi.name_description = disk.Disk.name_description;
vdi.read_only = false;
vdi.sharable = false;
vdi.SR = disk.Disk.SR;
vdi.type = disk.Disk.type;
vdi.virtual_size = disk.Disk.virtual_size;
RelatedTask = XenAPI.VDI.async_create(Session, vdi);
PollToCompletion(progress1, progress2);
return Connection.WaitForCache(new XenRef<VDI>(Result));
}
/// <summary>
/// Create a VBD
///
/// ** vbd.bootable **
/// 1. Windows ignores bootable flag
/// 2. Eliloader changes the device "0" to bootable when booting linux
/// </summary>
/// <param name="disk"></param>
/// <param name="vdi"></param>
/// <param name="progress1"></param>
/// <param name="progress2"></param>
/// <param name="bootable">Set VBD.bootable to this value - see comments above</param>
private void CreateVbd(DiskDescription disk, VDI vdi, double progress1, double progress2, bool bootable)
{
List<string> devices = AllowedVBDs;
if (devices.Count == 0)
throw new Exception(Messages.NO_MORE_USERDEVICES);
VBD vbd = new VBD();
vbd.IsOwner = true;
vbd.bootable = bootable;
vbd.empty = false;
vbd.unpluggable = true;
vbd.mode = vbd_mode.RW;
vbd.type = vbd_type.Disk;
vbd.userdevice = devices.Contains(disk.Device.userdevice) ? disk.Device.userdevice : devices[0];
vbd.device = "";
vbd.VM = new XenRef<VM>(VM.opaque_ref);
vbd.VDI = new XenRef<VDI>(vdi.opaque_ref);
RelatedTask = VBD.async_create(Session, vbd);
PollToCompletion(progress1, progress2);
Connection.WaitForCache(new XenRef<VBD>(Result));
}
private void AddNetworks()
{
// first of all we need to clear any vifs that we have cloned from the template
double progress = 90;
VIF vif;
List<VIF> existingTemplateVifs = Connection.ResolveAll(VM.VIFs);
double step = 5.0 / (double)existingTemplateVifs.Count;
for (int i = 0; i < existingTemplateVifs.Count; i++)
{
vif = existingTemplateVifs[i];
RelatedTask = XenAPI.VIF.async_destroy(Session, vif.opaque_ref);
PollToCompletion(progress, progress + step);
progress += step;
}
// then we add the ones the user has specified
step = 5.0 / (double)Vifs.Count;
for (int i = 0; i < Vifs.Count; i++)
{
vif = Vifs[i];
List<string> devices = AllowedVIFs;
VIF new_vif = new VIF();
if (devices.Count < 1)
{
// If we have assigned more VIFs than we have space for then don't try to create them
log.Warn("Tried to create more VIFs than the server allows. Ignoring remaining vifs");
return;
}
new_vif.device = devices.Contains(vif.device) ? vif.device : devices[0];
new_vif.MAC = vif.MAC;
new_vif.network = vif.network;
new_vif.VM = new XenRef<VM>(VM.opaque_ref);
new_vif.qos_algorithm_type = vif.qos_algorithm_type;
new_vif.qos_algorithm_params = vif.qos_algorithm_params;
RelatedTask = XenAPI.VIF.async_create(Session, new_vif);
PollToCompletion(progress, progress + step);
progress += step;
Connection.WaitForCache(new XenRef<VIF>(Result));
}
}
private string HiddenVmName
{
get
{
return string.Format("{0}{1}", Helpers.GuiTempObjectPrefix, NameLabel);
}
}
private List<string> AllowedVBDs
{
get
{
return new List<String>(XenAPI.VM.get_allowed_VBD_devices(Session, VM.opaque_ref));
}
}
private List<string> AllowedVIFs
{
get
{
return new List<String>(XenAPI.VM.get_allowed_VIF_devices(Session, VM.opaque_ref));
}
}
protected override void CleanOnError()
{
if (VM != null && !PointOfNoReturn && Connection.IsConnected)
{
try
{
VMDestroyAction.DestroyVM(Session, VM, true);
}
catch (Exception e)
{
// if the clean up has failed for whatever reason we just log it and give up.
log.Error(e);
}
}
}
private string GetBootOrderNetworkFirst()
{
// add "n" at the beginning of the order string
if (VM.HVM_boot_params.ContainsKey("order"))
{
string order = VM.HVM_boot_params["order"].ToLower();
int i = order.IndexOf("n");
switch (i)
{
case -1: return order.Insert(0, "n");
case 0: return order;
default: return order.Remove(i, 1).Insert(0, "n");
}
}
else
{
return "ncd";
}
}
}
public class DiskDescription
{
public VDI Disk;
public VBD Device;
public DiskType Type;
public enum DiskType { New, Existing }
public DiskDescription(){}
public DiskDescription(VDI disk, VBD device)
{
Disk = disk;
Device = device;
Type = DiskType.New;
}
}
}