/* 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 Disks; private readonly List 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 readonly string cloudConfigDriveTemplateText; private SR firstSR = null; private Action _startDiagnosisForm; private Action _warningDialogHAInvalidConfig; private bool PointOfNoReturn; private bool assignOrRemoveVgpu; /// /// These are the RBAC dependencies that you always need to create a VM. Check CreateVMAction constructor for runtime dependent dependencies. /// 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 disks, SR fullCopySR, List vifs, bool startAfter, Action warningDialogHAInvalidConfig, Action startDiagnosisForm, GPU_group gpuGroup, VGPU_type vgpuType, bool modifyVgpuSettings, long coresPerSocket, string cloudConfigDriveTemplateText) : 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; this.cloudConfigDriveTemplateText = cloudConfigDriveTemplateText; 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 || modifyVgpuSettings && Helpers.GpuCapability(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(Result)); CopyBiosStrings(); SetXenCenterProperties(); ProvisionVM(); SetVMParams(); SetVMBootParams(); AddCdDrive(); AddDisks(); AddNetworks(); XenAdminConfigManager.Provider.ShowObject(VM.opaque_ref); PointOfNoReturn = true; CloudCreateConfigDrive(); AssignVgpu(); if (StartAfter) { Description = Messages.STARTING_VM; var startAction = new VMStartAction(VM, _warningDialogHAInvalidConfig, _startDiagnosisForm); startAction.RunAsync(); } Description = Messages.VM_SUCCESSFULLY_CREATED; } private void CloudCreateConfigDrive() { if (Template.CanHaveCloudConfigDrive && !string.IsNullOrEmpty(cloudConfigDriveTemplateText)) { Description = Messages.CREATING_CLOUD_CONFIG_DRIVE; var parameters = new Dictionary(); parameters.Add("vmuuid", VM.uuid); parameters.Add("sruuid", firstSR.uuid); parameters.Add("configuration", cloudConfigDriveTemplateText.Replace("\r\n", "\n")); var action = new ExecutePluginAction(Connection, HomeServer ?? Helpers.GetMaster(Connection), "xscontainer",//plugin "create_config_drive",//function parameters, true); //hidefromlogs action.RunExternal(Connection.Session); var result = action.Result.Replace("\n", Environment.NewLine); } } 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 platform = VM.platform == null ? new Dictionary() : new Dictionary(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 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 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 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; // set the new vm's provision xml: remove "disks" entry, as we are going to explicitly create all the disks Dictionary other_config = VM.other_config; 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 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.opaque_ref); vbd.VDI = null; RelatedTask = VBD.async_create(Session, vbd); PollToCompletion(60, 65); return Connection.WaitForCache(new XenRef(Result)); } private void AddDisks() { Description = Messages.CREATING_DISKS; List 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(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); this.firstSR = vdiSR; 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 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(Result))); VDI new_vdi = Connection.Resolve(new XenRef(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; } /// /// Helper: Check if the disk is at the zeroth position in the VBD list /// /// /// private bool IsDeviceAtPositionZero(DiskDescription disk) { return disk.Device.userdevice == "0"; } /// /// 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) /// /// /// /// /// 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) && InsMethod != InstallMethod.CD; } AddVMHint(vdi); CreateVbd(disk, vdi, progress + 0.75 * step, progress + step, bootable); return vdi; } private void AddVMHint(VDI vdi) { Dictionary 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; vdi.sm_config = disk.Disk.sm_config; RelatedTask = XenAPI.VDI.async_create(Session, vdi); PollToCompletion(progress1, progress2); return Connection.WaitForCache(new XenRef(Result)); } /// /// Create a VBD /// /// ** vbd.bootable ** /// 1. Windows ignores bootable flag /// 2. Eliloader changes the device "0" to bootable when booting linux /// /// /// /// /// /// Set VBD.bootable to this value - see comments above private void CreateVbd(DiskDescription disk, VDI vdi, double progress1, double progress2, bool bootable) { List 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.opaque_ref); vbd.VDI = new XenRef(vdi.opaque_ref); RelatedTask = VBD.async_create(Session, vbd); PollToCompletion(progress1, progress2); Connection.WaitForCache(new XenRef(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 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 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.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(Result)); } } private string HiddenVmName { get { return string.Format("{0}{1}", Helpers.GuiTempObjectPrefix, NameLabel); } } private List AllowedVBDs { get { return new List(XenAPI.VM.get_allowed_VBD_devices(Session, VM.opaque_ref)); } } private List AllowedVIFs { get { return new List(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; } } }