From 6e93c5719e07106be88a1a63ea6389a3e289db95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20De=20Marco=20Gon=C3=A7alves?= Date: Tue, 7 Apr 2026 12:36:00 -0300 Subject: [PATCH] Live scaling for VMs with fixed service offerings on KVM --- .../cloud/agent/api/to/VirtualMachineTO.java | 8 +- .../com/cloud/agent/api/ScaleVmCommand.java | 9 +- .../com/cloud/capacity/CapacityManager.java | 14 ++ .../cloud/vm/VirtualMachineManagerImpl.java | 40 +++-- .../META-INF/db/schema-42210to42300.sql | 18 ++ .../resource/LibvirtComputingResource.java | 159 ++++++++++++++---- .../kvm/resource/LibvirtDomainXMLParser.java | 2 +- .../hypervisor/kvm/resource/LibvirtVMDef.java | 16 +- .../wrapper/LibvirtScaleVmCommandWrapper.java | 5 +- .../LibvirtComputingResourceTest.java | 116 +++++++++++-- .../cloud/capacity/CapacityManagerImpl.java | 2 +- .../java/com/cloud/hypervisor/KVMGuru.java | 148 ++++++++-------- .../main/java/com/cloud/vm/UserVmManager.java | 4 + .../java/com/cloud/vm/UserVmManagerImpl.java | 36 ++-- .../com/cloud/hypervisor/KVMGuruTest.java | 123 +++++++------- ui/public/locales/el_GR.json | 2 - ui/public/locales/en.json | 1 - ui/public/locales/ja_JP.json | 1 - ui/public/locales/pt_BR.json | 1 - ui/public/locales/te.json | 1 - ui/public/locales/zh_CN.json | 1 - ui/src/views/compute/ScaleVM.vue | 37 ++-- .../views/compute/wizard/ComputeSelection.vue | 12 +- 23 files changed, 499 insertions(+), 257 deletions(-) diff --git a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java index e26cc1e9f029..9af6c731fd24 100644 --- a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java @@ -51,6 +51,7 @@ public class VirtualMachineTO { private long minRam; private long maxRam; + private long requestedRam; private String hostName; private String arch; private String os; @@ -207,15 +208,20 @@ public long getMinRam() { return minRam; } - public void setRam(long minRam, long maxRam) { + public void setRam(long minRam, long maxRam, long requestedRam) { this.minRam = minRam; this.maxRam = maxRam; + this.requestedRam = requestedRam; } public long getMaxRam() { return maxRam; } + public long getRequestedRam() { + return requestedRam; + } + public String getHostName() { return hostName; } diff --git a/core/src/main/java/com/cloud/agent/api/ScaleVmCommand.java b/core/src/main/java/com/cloud/agent/api/ScaleVmCommand.java index dc63b1ee746d..e2953e9c7ad7 100644 --- a/core/src/main/java/com/cloud/agent/api/ScaleVmCommand.java +++ b/core/src/main/java/com/cloud/agent/api/ScaleVmCommand.java @@ -30,6 +30,7 @@ public class ScaleVmCommand extends Command { Integer maxSpeed; long minRam; long maxRam; + private boolean limitCpuUseChange; public VirtualMachineTO getVm() { return vm; @@ -43,7 +44,7 @@ public int getCpus() { return cpus; } - public ScaleVmCommand(String vmName, int cpus, Integer minSpeed, Integer maxSpeed, long minRam, long maxRam, boolean limitCpuUse) { + public ScaleVmCommand(String vmName, int cpus, Integer minSpeed, Integer maxSpeed, long minRam, long maxRam, boolean limitCpuUse, Double cpuQuotaPercentage, boolean limitCpuUseChange) { super(); this.vmName = vmName; this.cpus = cpus; @@ -52,6 +53,8 @@ public ScaleVmCommand(String vmName, int cpus, Integer minSpeed, Integer maxSpee this.minRam = minRam; this.maxRam = maxRam; this.vm = new VirtualMachineTO(1L, vmName, null, cpus, minSpeed, maxSpeed, minRam, maxRam, null, null, false, limitCpuUse, null); + this.vm.setCpuQuotaPercentage(cpuQuotaPercentage); + this.limitCpuUseChange = limitCpuUseChange; } public void setCpus(int cpus) { @@ -102,6 +105,10 @@ public VirtualMachineTO getVirtualMachine() { return vm; } + public boolean getLimitCpuUseChange() { + return limitCpuUseChange; + } + @Override public boolean executeInSequence() { return true; diff --git a/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java b/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java index 4c81c7359f25..abaf6ea967d0 100644 --- a/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java +++ b/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java @@ -133,6 +133,20 @@ public interface CapacityManager { "capacity.calculate.workers", "1", "Number of worker threads to be used for capacities calculation", true); + ConfigKey KvmMemoryDynamicScalingCapacity = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, + Integer.class, "kvm.memory.dynamic.scaling.capacity", "0", + "Defines the maximum memory capacity in MiB for which VMs can be dynamically scaled to with KVM. " + + "The 'kvm.memory.dynamic.scaling.capacity' setting's value will be used to define the value of the " + + "'' element of domain XMLs. If it is set to a value less than or equal to '0', then the host's memory capacity will be considered.", + true, ConfigKey.Scope.Cluster); + + ConfigKey KvmCpuDynamicScalingCapacity = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, + Integer.class, "kvm.cpu.dynamic.scaling.capacity", "0", + "Defines the maximum vCPU capacity for which VMs can be dynamically scaled to with KVM. " + + "The 'kvm.cpu.dynamic.scaling.capacity' setting's value will be used to define the value of the " + + "'' element of domain XMLs. If it is set to a value less than or equal to '0', then the host's CPU cores capacity will be considered.", + true, ConfigKey.Scope.Cluster); + public boolean releaseVmCapacity(VirtualMachine vm, boolean moveFromReserved, boolean moveToReservered, Long hostId); void allocateVmCapacity(VirtualMachine vm, boolean fromLastHost); diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index b20c06fc2c31..d184d062f0b0 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -51,6 +51,7 @@ import javax.persistence.EntityExistsException; +import com.cloud.hypervisor.KVMGuru; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -5183,7 +5184,7 @@ public VMInstanceVO reConfigureVm(final String vmUuid, final ServiceOffering old try { result = retrieveResultFromJobOutcomeAndThrowExceptionIfNeeded(outcome); } catch (Exception ex) { - throw new RuntimeException("Unhandled exception", ex); + throw new RuntimeException("Unable to reconfigure VM.", ex); } if (result != null) { @@ -5196,22 +5197,29 @@ public VMInstanceVO reConfigureVm(final String vmUuid, final ServiceOffering old private VMInstanceVO orchestrateReConfigureVm(String vmUuid, ServiceOffering oldServiceOffering, ServiceOffering newServiceOffering, boolean reconfiguringOnExistingHost) throws ResourceUnavailableException, ConcurrentOperationException { - final VMInstanceVO vm = _vmDao.findByUuid(vmUuid); + VMInstanceVO vm = _vmDao.findByUuid(vmUuid); HostVO hostVo = _hostDao.findById(vm.getHostId()); - Long clustedId = hostVo.getClusterId(); - Float memoryOvercommitRatio = CapacityManager.MemOverprovisioningFactor.valueIn(clustedId); - Float cpuOvercommitRatio = CapacityManager.CpuOverprovisioningFactor.valueIn(clustedId); - boolean divideMemoryByOverprovisioning = HypervisorGuruBase.VmMinMemoryEqualsMemoryDividedByMemOverprovisioningFactor.valueIn(clustedId); - boolean divideCpuByOverprovisioning = HypervisorGuruBase.VmMinCpuSpeedEqualsCpuSpeedDividedByCpuOverprovisioningFactor.valueIn(clustedId); + Long clusterId = hostVo.getClusterId(); + Float memoryOvercommitRatio = CapacityManager.MemOverprovisioningFactor.valueIn(clusterId); + Float cpuOvercommitRatio = CapacityManager.CpuOverprovisioningFactor.valueIn(clusterId); + boolean divideMemoryByOverprovisioning = HypervisorGuruBase.VmMinMemoryEqualsMemoryDividedByMemOverprovisioningFactor.valueIn(clusterId); + boolean divideCpuByOverprovisioning = HypervisorGuruBase.VmMinCpuSpeedEqualsCpuSpeedDividedByCpuOverprovisioningFactor.valueIn(clusterId); int minMemory = (int)(newServiceOffering.getRamSize() / (divideMemoryByOverprovisioning ? memoryOvercommitRatio : 1)); int minSpeed = (int)(newServiceOffering.getSpeed() / (divideCpuByOverprovisioning ? cpuOvercommitRatio : 1)); - ScaleVmCommand scaleVmCommand = - new ScaleVmCommand(vm.getInstanceName(), newServiceOffering.getCpu(), minSpeed, - newServiceOffering.getSpeed(), minMemory * 1024L * 1024L, newServiceOffering.getRamSize() * 1024L * 1024L, newServiceOffering.getLimitCpuUse()); + Double cpuQuotaPercentage = null; + if (newServiceOffering.getLimitCpuUse() && vm.getHypervisorType().equals(HypervisorType.KVM)) { + KVMGuru kvmGuru = (KVMGuru) _hvGuruMgr.getGuru(vm.getHypervisorType()); + cpuQuotaPercentage = kvmGuru.getCpuQuotaPercentage(minSpeed, hostVo.getSpeed()); + } + + boolean limitCpuUseChange = oldServiceOffering.getLimitCpuUse() != newServiceOffering.getLimitCpuUse(); + ScaleVmCommand scaleVmCommand = new ScaleVmCommand(vm.getInstanceName(), newServiceOffering.getCpu(), minSpeed, newServiceOffering.getSpeed(), + minMemory * 1024L * 1024L, newServiceOffering.getRamSize() * 1024L * 1024L, + newServiceOffering.getLimitCpuUse(), cpuQuotaPercentage, limitCpuUseChange); scaleVmCommand.getVirtualMachine().setId(vm.getId()); scaleVmCommand.getVirtualMachine().setUuid(vm.getUuid()); @@ -5240,16 +5248,20 @@ private VMInstanceVO orchestrateReConfigureVm(String vmUuid, ServiceOffering old throw new CloudRuntimeException("Unable to scale vm due to " + (reconfigureAnswer == null ? "" : reconfigureAnswer.getDetails())); } - upgradeVmDb(vm.getId(), newServiceOffering, oldServiceOffering); + if (reconfiguringOnExistingHost) { + _capacityMgr.releaseVmCapacity(vm, false, false, vm.getHostId()); + } + + boolean vmUpgraded = upgradeVmDb(vm.getId(), newServiceOffering, oldServiceOffering); + if (vmUpgraded) { + vm = _vmDao.findById(vm.getId()); + } if (vm.getType().equals(VirtualMachine.Type.User)) { _userVmMgr.generateUsageEvent(vm, vm.isDisplayVm(), EventTypes.EVENT_VM_DYNAMIC_SCALE); } if (reconfiguringOnExistingHost) { - vm.setServiceOfferingId(oldServiceOffering.getId()); - _capacityMgr.releaseVmCapacity(vm, false, false, vm.getHostId()); - vm.setServiceOfferingId(newServiceOffering.getId()); _capacityMgr.allocateVmCapacity(vm, false); } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 4cb9eb7cb2c4..abe013a9e4dd 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -117,3 +117,21 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tin --- Disable/enable NICs CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Indicates whether the NIC is enabled or not'' '); + +-- Creates the 'kvm.memory.dynamic.scaling.capacity' and, for already active ACS environments, +-- initializes it with the value of the setting 'vm.serviceoffering.ram.size.max' +INSERT INTO `cloud`.`configuration` (`category`, `instance`, `component`, `name`, `value`, `default_value`, `updated`, `scope`, `is_dynamic`, `group_id`, `subgroup_id`, `display_text`, `description`) +SELECT 'Advanced', 'DEFAULT', 'CapacityManager', 'kvm.memory.dynamic.scaling.capacity', `cfg`.`value`, 0, NULL, 4, 1, 6, 27, + 'KVM memory dynamic scaling capacity', 'Defines the maximum memory capacity in MiB for which VMs can be dynamically scaled to with KVM. The ''kvm.memory.dynamic.scaling.capacity'' setting''s value will be used to define the value of the '''' element of domain XMLs. If it is set to a value less than or equal to ''0'', then the host''s memory capacity will be considered.' +FROM `cloud`.`configuration` `cfg` +WHERE NOT EXISTS (SELECT 1 FROM `cloud`.`configuration` WHERE `name` = 'kvm.memory.dynamic.scaling.capacity') + AND `cfg`.`name` = 'vm.serviceoffering.ram.size.max'; + +-- Creates the 'kvm.cpu.dynamic.scaling.capacity' and, for already active ACS environments, +-- initializes it with the value of the setting 'vm.serviceoffering.cpu.cores.max' +INSERT INTO `cloud`.`configuration` (`category`, `instance`, `component`, `name`, `value`, `default_value`, `updated`, `scope`, `is_dynamic`, `group_id`, `subgroup_id`, `display_text`, `description`) +SELECT 'Advanced', 'DEFAULT', 'CapacityManager', 'kvm.cpu.dynamic.scaling.capacity', `cfg`.`value`, 0, NULL, 4, 1, 6, 27, + 'KVM CPU dynamic scaling capacity', 'Defines the maximum vCPU capacity for which VMs can be dynamically scaled to with KVM. The ''kvm.cpu.dynamic.scaling.capacity'' setting''s value will be used to define the value of the '''' element of domain XMLs. If it is set to a value less than or equal to ''0'', then the host''s CPU cores capacity will be considered.' +FROM `cloud`.`configuration` `cfg` +WHERE NOT EXISTS (SELECT 1 FROM `cloud`.`configuration` WHERE `name` = 'kvm.cpu.dynamic.scaling.capacity') + AND `cfg`.`name` = 'vm.serviceoffering.cpu.cores.max'; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 46cf1da461e7..e23b893181ff 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -121,6 +121,7 @@ import org.libvirt.LibvirtException; import org.libvirt.MemoryStatistic; import org.libvirt.Network; +import org.libvirt.SchedLongParameter; import org.libvirt.SchedParameter; import org.libvirt.SchedUlongParameter; import org.libvirt.Secret; @@ -451,6 +452,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv private static final int MINIMUM_QEMU_VERSION_FOR_INCREMENTAL_SNAPSHOT = 6001000; + public static final long MAX_CPU_QUOTA = 17592186044415L; + protected HypervisorType hypervisorType; protected String hypervisorURI; protected long hypervisorLibvirtVersion; @@ -879,6 +882,25 @@ protected enum HealthCheckResult { SUCCESS, FAILURE, IGNORE } + public enum CpuSchedulerParameter { + CPU_SHARES("cpu_shares"), PERIOD("vcpu_period"), QUOTA("vcpu_quota"); + + private String name; + + CpuSchedulerParameter(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return getName(); + } + } + protected BridgeType bridgeType; protected StorageSubsystemCommandHandler storageHandler; @@ -2909,23 +2931,61 @@ protected String getUuid(String uuid) { protected void setQuotaAndPeriod(VirtualMachineTO vmTO, CpuTuneDef ctd) { if (vmTO.isLimitCpuUse() && vmTO.getCpuQuotaPercentage() != null) { Double cpuQuotaPercentage = vmTO.getCpuQuotaPercentage(); - int period = CpuTuneDef.DEFAULT_PERIOD; - int quota = (int) (period * cpuQuotaPercentage); - if (quota < CpuTuneDef.MIN_QUOTA) { - LOGGER.info("Calculated quota (" + quota + ") below the minimum (" + CpuTuneDef.MIN_QUOTA + ") for VM domain " + vmTO.getUuid() + ", setting it to minimum " + - "and calculating period instead of using the default"); - quota = CpuTuneDef.MIN_QUOTA; - period = (int) ((double) quota / cpuQuotaPercentage); - if (period > CpuTuneDef.MAX_PERIOD) { - LOGGER.info("Calculated period (" + period + ") exceeds the maximum (" + CpuTuneDef.MAX_PERIOD + - "), setting it to the maximum"); - period = CpuTuneDef.MAX_PERIOD; - } + Pair periodAndQuota = getPeriodAndQuota(cpuQuotaPercentage); + ctd.setPeriod(periodAndQuota.first()); + ctd.setQuota(periodAndQuota.second()); + LOGGER.info("Setting quota = [{}] and period = [{}] to VM domain [{}].", periodAndQuota.second(), periodAndQuota.first(), vmTO.getUuid()); + } + } + + /** + * Calculates the CPU period and quota based on the quota percentage defined by the Management Server + * @param cpuQuotaPercentage CPU quota percentage defined by the Management Server + * @return The period and quota to be defined for the VM's domain + */ + protected Pair getPeriodAndQuota(double cpuQuotaPercentage) { + int period = CpuTuneDef.DEFAULT_PERIOD; + long quota = (long) (period * cpuQuotaPercentage); + if (quota < CpuTuneDef.MIN_QUOTA) { + LOGGER.info("Calculated quota ({}) below the minimum ({}), setting it to minimum and calculating period instead of using the default", quota, CpuTuneDef.MIN_QUOTA); + quota = CpuTuneDef.MIN_QUOTA; + period = (int) ((double) quota / cpuQuotaPercentage); + if (period > CpuTuneDef.MAX_PERIOD) { + LOGGER.info("Calculated period ({}) exceeds the maximum ({}), setting it to the maximum", period, CpuTuneDef.MAX_PERIOD); + period = CpuTuneDef.MAX_PERIOD; } - ctd.setQuota(quota); - ctd.setPeriod(period); - LOGGER.info("Setting quota=" + quota + ", period=" + period + " to VM domain " + vmTO.getUuid()); } + + LOGGER.info("Calculated period = [{}] and quota = [{}] given the [{}] quota percentage.", period, quota, cpuQuotaPercentage); + return new Pair<>(period, quota); + } + + /** + * Dynamically updates the domain's "vcpu_quota" and "period" fields of the CPU tune definition. + * This is required because the values of the fields must change according to the new CPU speed of the VM. + * When the CPU limitation is removed from the domain, the "vcpu_quota" field is set to 17,592,186,044,415. + * @param domain VM's domain. + * @param vmTO VM's transfer object, which contains the required fields to update the "vcpu_quota" and "period" fields. + * @param limitCpuUseChange Indicates whether the CPU limitation for the VM has changed. + * @throws org.libvirt.LibvirtException + **/ + public void updateCpuQuotaAndPeriod(Domain domain, VirtualMachineTO vmTO, boolean limitCpuUseChange) throws LibvirtException { + if (hypervisorLibvirtVersion < MIN_LIBVIRT_VERSION_FOR_GUEST_CPU_TUNE || (!limitCpuUseChange && !vmTO.isLimitCpuUse())) { + logger.info("Not updating the [{}] and [{}] for the [{}] domain, because [{}].", + CpuSchedulerParameter.QUOTA, CpuSchedulerParameter.PERIOD, domain.getName(), hypervisorLibvirtVersion < MIN_LIBVIRT_VERSION_FOR_GUEST_CPU_TUNE ? + "the current Libvirt version does not support CPU tune" : "it was not requested to remove, change or apply CPU limitation for the instance."); + return; + } + + if (limitCpuUseChange && !vmTO.isLimitCpuUse()) { + logger.info("Updating the [{}] of the [{}] domain to [{}], because CPU limitation has been removed.", CpuSchedulerParameter.QUOTA, domain.getName(), LibvirtComputingResource.MAX_CPU_QUOTA); + LibvirtComputingResource.setQuota(domain, LibvirtComputingResource.MAX_CPU_QUOTA); + return; + } + + Pair periodAndQuota = getPeriodAndQuota(vmTO.getCpuQuotaPercentage()); + LibvirtComputingResource.setPeriod(domain, periodAndQuota.first()); + LibvirtComputingResource.setQuota(domain, periodAndQuota.second()); } protected void enlightenWindowsVm(VirtualMachineTO vmTO, FeaturesDef features) { @@ -3389,10 +3449,10 @@ protected GuestResourceDef createGuestResourceDef(VirtualMachineTO vmTO){ grd.setMemBalloning(!noMemBalloon); - Long maxRam = ByteScaleUtils.bytesToKibibytes(vmTO.getMaxRam()); - - grd.setMemorySize(maxRam); - grd.setCurrentMem(getCurrentMemAccordingToMemBallooning(vmTO, maxRam)); + long requestedRam = ByteScaleUtils.bytesToKibibytes(vmTO.getRequestedRam()); + long minRam = ByteScaleUtils.bytesToKibibytes(vmTO.getMinRam()); + grd.setCurrentMem(getCurrentMemAccordingToMemBallooning(vmTO, requestedRam, minRam)); + grd.setMaxMemory(ByteScaleUtils.bytesToKibibytes(vmTO.getMaxRam())); int vcpus = vmTO.getCpus(); Integer maxVcpus = vmTO.getVcpuMaxLimit(); @@ -3403,18 +3463,19 @@ protected GuestResourceDef createGuestResourceDef(VirtualMachineTO vmTO){ return grd; } - protected long getCurrentMemAccordingToMemBallooning(VirtualMachineTO vmTO, long maxRam) { - long retVal = maxRam; + protected long getCurrentMemAccordingToMemBallooning(VirtualMachineTO vmTO, long requestedRam, long minRam) { if (noMemBalloon) { - LOGGER.warn(String.format("Setting VM's [%s] current memory as max memory [%s] due to memory ballooning is disabled. If you are using a custom service offering, verify if memory ballooning really should be disabled.", vmTO.toString(), maxRam)); - } else if (vmTO != null && vmTO.getType() != VirtualMachine.Type.User) { - LOGGER.warn(String.format("Setting System VM's [%s] current memory as max memory [%s].", vmTO.toString(), maxRam)); - } else { - long minRam = ByteScaleUtils.bytesToKibibytes(vmTO.getMinRam()); - LOGGER.debug(String.format("Setting VM's [%s] current memory as min memory [%s] due to memory ballooning is enabled.", vmTO.toString(), minRam)); - retVal = minRam; + LOGGER.warn("Setting VM's [{}] current memory as requested memory [{}] due to memory ballooning is disabled.", vmTO.toString(), requestedRam); + return requestedRam; + } + + if (vmTO != null && vmTO.getType() != VirtualMachine.Type.User) { + LOGGER.warn("Setting System VM's [{}] current memory as requested memory [{}].", vmTO.toString(), requestedRam); + return requestedRam; } - return retVal; + + LOGGER.debug("Setting VM's [{}] current memory as min memory [{}] due to memory ballooning is enabled.", vmTO.toString(), minRam); + return minRam; } /** @@ -6098,29 +6159,59 @@ public static long countDomainRunningVcpus(Domain dm) throws LibvirtException { **/ public static Integer getCpuShares(Domain dm) throws LibvirtException { for (SchedParameter c : dm.getSchedulerParameters()) { - if (c.field.equals("cpu_shares")) { + if (c.field.equals(CpuSchedulerParameter.CPU_SHARES.getName())) { return Integer.parseInt(c.getValueAsString()); } } - LOGGER.warn(String.format("Could not get cpu_shares of domain: [%s]. Returning default value of 0. ", dm.getName())); + LOGGER.warn("Could not get [{}] of domain: [{}]. Returning default value of 0. ", CpuSchedulerParameter.CPU_SHARES.getName(), dm.getName()); return 0; } /** - * Sets the cpu_shares (priority) of the running VM
+ * Updates the cpu_shares (priority) of the running VM. * @param dm domain of the VM. * @param cpuShares new priority of the running VM. - * @throws org.libvirt.LibvirtException **/ public static void setCpuShares(Domain dm, Integer cpuShares) throws LibvirtException { + LOGGER.info("Dynamically updating the [{}] of the [{}] VM to [{}].", CpuSchedulerParameter.CPU_SHARES.getName(), dm.getName(), cpuShares); SchedUlongParameter[] params = new SchedUlongParameter[1]; params[0] = new SchedUlongParameter(); - params[0].field = "cpu_shares"; + params[0].field = CpuSchedulerParameter.CPU_SHARES.getName(); params[0].value = cpuShares; dm.setSchedulerParameters(params); } + /** + * Updates the period of the running VM. + * @param domain domain of the VM. + * @param period new period of the running VM. + **/ + public static void setPeriod(Domain domain, int period) throws LibvirtException { + LOGGER.info("Dynamically updating the [{}] of the [{}] VM to [{}].", CpuSchedulerParameter.PERIOD.getName(), domain.getName(), period); + SchedUlongParameter[] params = new SchedUlongParameter[1]; + params[0] = new SchedUlongParameter(); + params[0].field = CpuSchedulerParameter.PERIOD.getName(); + params[0].value = period; + + domain.setSchedulerParameters(params); + } + + /** + * Updates the quota of the running VM. + * @param domain domain of the VM. + * @param quota new quota of the running VM. + **/ + public static void setQuota(Domain domain, long quota) throws LibvirtException { + LOGGER.info("Dynamically updating the [{}] of the [{}] VM to [{}].", CpuSchedulerParameter.QUOTA.getName(), domain.getName(), quota); + SchedLongParameter[] params = new SchedLongParameter[1]; + params[0] = new SchedLongParameter(); + params[0].field = CpuSchedulerParameter.QUOTA.getName(); + params[0].value = quota; + + domain.setSchedulerParameters(params); + } + /** * Set up a libvirt secret for a volume. If Libvirt says that a secret already exists for this volume path, we use its uuid. * The UUID of the secret needs to be prescriptive such that we can register the same UUID on target host during live migration diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java index 4d823783a99a..e114669b8b56 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java @@ -542,7 +542,7 @@ private void extractCpuTuneDef(final Element rootElement) { final String quota = getTagValue("quota", cpuTuneDefElement); if (StringUtils.isNotBlank(quota)) { - cpuTuneDef.setQuota((Integer.parseInt(quota))); + cpuTuneDef.setQuota((Long.parseLong(quota))); } final String period = getTagValue("period", cpuTuneDefElement); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java index 097a9b8dd322..224fcfd192c0 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java @@ -443,15 +443,15 @@ public String toString() { } public static class GuestResourceDef { - private long memory; + private long maxMemory; private long currentMemory = -1; private int vcpu = -1; private int maxVcpu = -1; private boolean memoryBalloning = false; private int memoryBalloonStatsPeriod = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VM_MEMBALLOON_STATS_PERIOD); - public void setMemorySize(long mem) { - this.memory = mem; + public void setMaxMemory(long mem) { + this.maxMemory = mem; } public void setCurrentMem(long currMem) { @@ -484,8 +484,8 @@ public String toString() { response.append(String.format("%s\n", this.currentMemory)); response.append(String.format("%s\n", this.currentMemory)); - if (this.memory > this.currentMemory) { - response.append(String.format("%s\n", this.memory)); + if (this.maxMemory > this.currentMemory) { + response.append(String.format("%s\n", this.maxMemory)); response.append(String.format(" \n", this.maxVcpu - 1, this.currentMemory)); } @@ -1920,7 +1920,7 @@ public String toString() { public static class CpuTuneDef { private int _shares = 0; - private int quota = 0; + private long quota = 0; private int period = 0; static final int DEFAULT_PERIOD = 10000; static final int MIN_QUOTA = 1000; @@ -1934,11 +1934,11 @@ public int getShares() { return _shares; } - public int getQuota() { + public long getQuota() { return quota; } - public void setQuota(int quota) { + public void setQuota(long quota) { this.quota = quota; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtScaleVmCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtScaleVmCommandWrapper.java index 1536984f2e85..b1f64c8c6dbb 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtScaleVmCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtScaleVmCommandWrapper.java @@ -49,10 +49,11 @@ public Answer execute(ScaleVmCommand command, LibvirtComputingResource libvirtCo conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); Domain dm = conn.domainLookupByName(vmName); - logger.debug(String.format("Scaling %s.", scalingDetails)); + logger.debug("Scaling {}.", scalingDetails); scaleMemory(dm, newMemory, vmDefinition); scaleVcpus(dm, newVcpus, vmDefinition); updateCpuShares(dm, newCpuShares); + libvirtComputingResource.updateCpuQuotaAndPeriod(dm, vmSpec, command.getLimitCpuUseChange()); return new ScaleVmAnswer(command, true, String.format("Successfully scaled %s.", scalingDetails)); } catch (LibvirtException | CloudRuntimeException e) { @@ -74,7 +75,7 @@ protected void updateCpuShares(Domain dm, int newCpuShares) throws LibvirtExcept if (oldCpuShares < newCpuShares) { LibvirtComputingResource.setCpuShares(dm, newCpuShares); - logger.info(String.format("Successfully increased cpu_shares of VM [%s] from [%s] to [%s].", dm.getName(), oldCpuShares, newCpuShares)); + logger.info("Successfully increased cpu_shares of VM [{}] from [{}] to [{}].", dm.getName(), oldCpuShares, newCpuShares); } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index b96295240076..c193dae784b8 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -5642,35 +5642,45 @@ public void testAddExtraConfigComponentNotEmptyExtraConfig() { Mockito.verify(vmDef, times(1)).addComp(any()); } - public void validateGetCurrentMemAccordingToMemBallooningWithoutMemBalooning(){ + @Test + public void getCurrentMemAccordingToMemBallooningTestValidateCurrentMemoryWithoutMemBallooning(){ VirtualMachineTO vmTo = Mockito.mock(VirtualMachineTO.class); - Mockito.when(vmTo.getType()).thenReturn(Type.User); LibvirtComputingResource libvirtComputingResource = new LibvirtComputingResource(); libvirtComputingResource.noMemBalloon = true; - long maxMemory = 2048; + long requestedMemory = 1024 * 1024; + long minMemory = 512 * 1024; - long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, maxMemory); - Assert.assertEquals(maxMemory, currentMemory); - Mockito.verify(vmTo, Mockito.times(0)).getMinRam(); + long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, requestedMemory, minMemory); + Assert.assertEquals(requestedMemory, currentMemory); } @Test - public void validateGetCurrentMemAccordingToMemBallooningWithtMemBalooning(){ + public void getCurrentMemAccordingToMemBallooningTestValidateCurrentMemoryWithMemoryBallooning(){ LibvirtComputingResource libvirtComputingResource = new LibvirtComputingResource(); libvirtComputingResource.noMemBalloon = false; - long maxMemory = 2048; - long minMemory = ByteScaleUtils.mebibytesToBytes(64); - VirtualMachineTO vmTo = Mockito.mock(VirtualMachineTO.class); Mockito.when(vmTo.getType()).thenReturn(Type.User); - Mockito.when(vmTo.getMinRam()).thenReturn(minMemory); + long requestedMemory = 1024 * 1024; + long minMemory = 512 * 1024; - long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, maxMemory); - Assert.assertEquals(ByteScaleUtils.bytesToKibibytes(minMemory), currentMemory); - Mockito.verify(vmTo).getMinRam(); + long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, requestedMemory, minMemory); + Assert.assertEquals(minMemory, currentMemory); } + @Test + public void getCurrentMemAccordingToMemBallooningTestValidateCurrentMemoryForSystemVms() { + LibvirtComputingResource libvirtComputingResource = new LibvirtComputingResource(); + libvirtComputingResource.noMemBalloon = false; + + VirtualMachineTO vmTo = Mockito.mock(VirtualMachineTO.class); + Mockito.when(vmTo.getType()).thenReturn(Type.SecondaryStorageVm); + long requestedMemory = 1024 * 1024; + long minMemory = 512 * 1024; + + long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, requestedMemory, minMemory); + Assert.assertEquals(requestedMemory, currentMemory); + } @Test public void validateCreateGuestResourceDefWithVcpuMaxLimit(){ LibvirtComputingResource libvirtComputingResource = new LibvirtComputingResource(); @@ -7234,4 +7244,82 @@ public void getInterfaceTestInvalidMacAddressThrowCloudRuntimeException() { libvirtComputingResourceSpy.getInterface(connMock, vmName, invalidMacAddress); } + + @Test + public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreNotUpdatedWhenLibvirtVersionIsLessThanTheMinimum() throws LibvirtException { + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 8999; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, null, false); + Mockito.verify(domainMock, Mockito.never()).setSchedulerParameters(Mockito.any()); + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreNotUpdatedWhenThereIsNoCapCapChangeAndNoCpuLimitationIsApplied() throws LibvirtException { + Mockito.when(vmTO.isLimitCpuUse()).thenReturn(false); + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 9000; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, vmTO, false); + Mockito.verify(domainMock, Mockito.never()).setSchedulerParameters(Mockito.any()); + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertQuotaIsRemovedWhenThereIsCpuCapChangeAndNoCpuLimitationIsApplied() throws LibvirtException { + Mockito.when(vmTO.isLimitCpuUse()).thenReturn(false); + Mockito.when(domainMock.getName()).thenReturn("i-2-10-VM"); + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 9000; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, vmTO, true); + Mockito.verify(domainMock, Mockito.times(1)).setSchedulerParameters(Mockito.any()); + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreUpdatedWhenThereIsNotCpuCapChangeAndCpuLimitationIsApplied() throws LibvirtException { + Mockito.when(vmTO.isLimitCpuUse()).thenReturn(true); + double cpuQuotaPercentage = 0.03; + Mockito.when(vmTO.getCpuQuotaPercentage()).thenReturn(cpuQuotaPercentage); + Mockito.doReturn(new Pair<>(1000, 300L)).when(libvirtComputingResourceSpy).getPeriodAndQuota(cpuQuotaPercentage); + Mockito.when(domainMock.getName()).thenReturn("i-2-10-VM"); + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 9000; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, vmTO, false); + Mockito.verify(domainMock, Mockito.times(2)).setSchedulerParameters(Mockito.any()); + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreUpdatedWhenThereIsCpuCapChangeAndCpuLimitationIsApplied() throws LibvirtException { + Mockito.when(vmTO.isLimitCpuUse()).thenReturn(true); + double cpuQuotaPercentage = 0.03; + Mockito.when(vmTO.getCpuQuotaPercentage()).thenReturn(cpuQuotaPercentage); + Mockito.doReturn(new Pair<>(1000, 300L)).when(libvirtComputingResourceSpy).getPeriodAndQuota(cpuQuotaPercentage); + Mockito.when(domainMock.getName()).thenReturn("i-2-10-VM"); + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 9000; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, vmTO, true); + Mockito.verify(domainMock, Mockito.times(2)).setSchedulerParameters(Mockito.any()); + } + + @Test + public void getPeriodAndQuotaTestAssertQuotaIsEqualToPeriodMultipliedByQuotaPercentage() { + double cpuQuotaPercentage = 0.3; + int expectedPeriod = CpuTuneDef.DEFAULT_PERIOD; + long expectedQuota = (long) (expectedPeriod * cpuQuotaPercentage); + Pair expectedResult = new Pair<>(expectedPeriod, expectedQuota); + Pair result = libvirtComputingResourceSpy.getPeriodAndQuota(cpuQuotaPercentage); + Assert.assertEquals(expectedResult, result); + } + + @Test + public void getPeriodAndQuotaTestQuotaIsEqualToMinimumWhenRequired() { + double cpuQuotaPercentage = 0.03; + long expectedQuota = CpuTuneDef.MIN_QUOTA; + int expectedPeriod = (int) ((double) expectedQuota / cpuQuotaPercentage); + Pair expectedResult = new Pair<>(expectedPeriod, expectedQuota); + Pair result = libvirtComputingResourceSpy.getPeriodAndQuota(cpuQuotaPercentage); + Assert.assertEquals(expectedResult, result); + } + + @Test + public void getPeriodAndQuotaTestPeriodIsEqualToMaximumWhenRequired() { + double cpuQuotaPercentage = 0.0003; + long expectedQuota = CpuTuneDef.MIN_QUOTA; + int expectedPeriod = CpuTuneDef.MAX_PERIOD; + Pair expectedResult = new Pair<>(expectedPeriod, expectedQuota); + Pair result = libvirtComputingResourceSpy.getPeriodAndQuota(cpuQuotaPercentage); + Assert.assertEquals(expectedResult, result); + } } diff --git a/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java b/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java index 2940f900b081..3be7384ffb71 100644 --- a/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java +++ b/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java @@ -1214,6 +1214,6 @@ public String getConfigComponentName() { public ConfigKey[] getConfigKeys() { return new ConfigKey[] {CpuOverprovisioningFactor, MemOverprovisioningFactor, StorageCapacityDisableThreshold, StorageOverprovisioningFactor, StorageAllocatedCapacityDisableThreshold, StorageOperationsExcludeCluster, ImageStoreNFSVersion, SecondaryStorageCapacityThreshold, - StorageAllocatedCapacityDisableThresholdForVolumeSize, CapacityCalculateWorkers }; + StorageAllocatedCapacityDisableThresholdForVolumeSize, CapacityCalculateWorkers, KvmMemoryDynamicScalingCapacity, KvmCpuDynamicScalingCapacity }; } } diff --git a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java index 3303bc029333..6c1c3424b1b9 100644 --- a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java +++ b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java @@ -20,7 +20,7 @@ import com.cloud.agent.api.to.DataObjectType; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; -import com.cloud.configuration.ConfigurationManagerImpl; +import com.cloud.capacity.CapacityManager; import com.cloud.event.EventTypes; import com.cloud.event.UsageEventUtils; import com.cloud.host.HostVO; @@ -44,6 +44,7 @@ import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.command.StorageSubSystemCommand; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; @@ -54,9 +55,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; -import org.apache.commons.lang3.math.NumberUtils; public class KVMGuru extends HypervisorGuruBase implements HypervisorGuru { @Inject @@ -130,30 +129,47 @@ protected double getVmSpeed(VirtualMachineTO to) { * @param vmProfile vm profile */ protected void setVmQuotaPercentage(VirtualMachineTO to, VirtualMachineProfile vmProfile) { - if (to.isLimitCpuUse()) { - VirtualMachine vm = vmProfile.getVirtualMachine(); - HostVO host = hostDao.findById(vm.getHostId()); - if (host == null) { - logger.warn("Host is not available. Skipping setting CPU quota percentage for VM: {}", vm); - return; - } - logger.debug("Limiting CPU usage for VM: {} on host: {}", vm, host); - double hostMaxSpeed = getHostCPUSpeed(host); - double maxSpeed = getVmSpeed(to); - try { - BigDecimal percent = new BigDecimal(maxSpeed / hostMaxSpeed); - percent = percent.setScale(2, RoundingMode.HALF_DOWN); - if (percent.compareTo(new BigDecimal(1)) == 1) { - logger.debug("VM {} CPU MHz exceeded host {} CPU MHz, limiting VM CPU to the host maximum", vm, host); - percent = new BigDecimal(1); - } - to.setCpuQuotaPercentage(percent.doubleValue()); - logger.debug("Host: {} max CPU speed = {} MHz, VM: {} max CPU speed = {} MHz. " + - "Setting CPU quota percentage as: {}", - host, hostMaxSpeed, vm, maxSpeed, percent.doubleValue()); - } catch (NumberFormatException e) { - logger.error("Error calculating VM: {} quota percentage, it will not be set. Error: {}", vm, e.getMessage(), e); + if (!to.isLimitCpuUse()) { + return; + } + + VirtualMachine vm = vmProfile.getVirtualMachine(); + HostVO host = hostDao.findById(vm.getHostId()); + if (host == null) { + logger.warn("Host is not available. Skipping setting CPU quota percentage for VM: [{}].", vm); + return; + } + + logger.debug("Limiting CPU usage for VM: [{}] on host: [{}].", vm, host); + double maxSpeed = getVmSpeed(to); + double hostMaxSpeed = getHostCPUSpeed(host); + Double cpuQuotaPercentage = getCpuQuotaPercentage(maxSpeed, hostMaxSpeed); + if (cpuQuotaPercentage != null) { + to.setCpuQuotaPercentage(cpuQuotaPercentage); + } + } + + /** + * Calculates the VM quota percentage based on the VM and host CPU speeds. + * @param vmSpeeed Speed of the VM. + * @param hostSpeed Speed of the host. + * @return The VM quota percentage. + */ + public Double getCpuQuotaPercentage(double vmSpeeed, double hostSpeed) { + logger.debug("Calculating CPU quota percentage for VM with speed [{}] on host with speed [{}].", vmSpeeed, hostSpeed); + try { + BigDecimal percent = new BigDecimal(vmSpeeed / hostSpeed); + percent = percent.setScale(2, RoundingMode.HALF_DOWN); + if (percent.compareTo(new BigDecimal(1)) > 0) { + logger.debug("VM CPU speed exceeded host CPU speed and, therefore, limiting VM CPU quota to the host maximum."); + percent = new BigDecimal(1); } + double quotaPercentage = percent.doubleValue(); + logger.info("Calculated CPU quota percentage for VM with speed [{}] on host with speed [{}] is [{}].", vmSpeeed, hostSpeed, quotaPercentage); + return quotaPercentage; + } catch (NumberFormatException e) { + logger.info("Could not calculate CPU quota percentage for VM with speed [{}] on host with speed [{}]. Therefore, CPU limitation will not be set for the domain.", vmSpeeed, hostSpeed); + return null; } } @@ -214,28 +230,31 @@ protected void configureVmMemoryAndCpuCores(VirtualMachineTO virtualMachineTo, H Pair max = getHostMaxMemoryAndCpuCores(hostVo, virtualMachine, vmDescription); Long maxHostMemory = max.first(); - Integer maxHostCpuCore = max.second(); + Integer maxHostCpuCores = max.second(); long minMemory = virtualMachineTo.getMinRam(); Long maxMemory = virtualMachineTo.getMaxRam(); + long requestedMemory = maxMemory; + int minCpuCores = virtualMachineTo.getCpus(); - Integer maxCpuCores = minCpuCores; + int maxCpuCores = minCpuCores; - ServiceOfferingVO serviceOfferingVO = serviceOfferingDao.findById(virtualMachineProfile.getId(), virtualMachineProfile.getServiceOfferingId()); - if (isVmDynamicScalable(serviceOfferingVO, virtualMachineTo, virtualMachine)) { + if (isVmDynamicScalable(virtualMachineTo, virtualMachine)) { + ServiceOfferingVO serviceOfferingVO = serviceOfferingDao.findById(virtualMachineProfile.getId(), virtualMachineProfile.getServiceOfferingId()); serviceOfferingDao.loadDetails(serviceOfferingVO); - maxMemory = getVmMaxMemory(serviceOfferingVO, vmDescription, maxHostMemory); - maxCpuCores = getVmMaxCpuCores(serviceOfferingVO, vmDescription, maxHostCpuCore); + Long clusterId = hostVo != null ? hostVo.getClusterId() : null; + maxMemory = getVmMaxMemory(serviceOfferingVO, vmDescription, maxHostMemory, clusterId); + maxCpuCores = getVmMaxCpuCores(serviceOfferingVO, vmDescription, maxHostCpuCores, clusterId); } - virtualMachineTo.setRam(minMemory, maxMemory); + virtualMachineTo.setRam(minMemory, maxMemory, requestedMemory); virtualMachineTo.setCpus(minCpuCores); virtualMachineTo.setVcpuMaxLimit(maxCpuCores); } - protected boolean isVmDynamicScalable(ServiceOfferingVO serviceOfferingVO, VirtualMachineTO virtualMachineTo, VirtualMachine virtualMachine) { - return serviceOfferingVO.isDynamic() && virtualMachineTo.isEnableDynamicallyScaleVm() && UserVmManager.EnableDynamicallyScaleVm.valueIn(virtualMachine.getDataCenterId()); + protected boolean isVmDynamicScalable(VirtualMachineTO virtualMachineTo, VirtualMachine virtualMachine) { + return virtualMachineTo.isEnableDynamicallyScaleVm() && UserVmManager.EnableDynamicallyScaleVm.valueIn(virtualMachine.getDataCenterId()); } protected Pair getHostMaxMemoryAndCpuCores(HostVO host, VirtualMachine virtualMachine, String vmDescription){ @@ -263,53 +282,34 @@ protected Pair getHostMaxMemoryAndCpuCores(HostVO host, VirtualMa return new Pair<>(maxHostMemory, maxHostCpuCore); } - protected Long getVmMaxMemory(ServiceOfferingVO serviceOfferingVO, String vmDescription, Long maxHostMemory) { - String serviceOfferingDescription = serviceOfferingVO.toString(); - + protected Long getVmMaxMemory(ServiceOfferingVO serviceOfferingVO, String vmDescription, Long maxHostMemory, Long clusterId) { Long maxMemory; - Integer customOfferingMaxMemory = NumberUtils.createInteger(serviceOfferingVO.getDetail(ApiConstants.MAX_MEMORY)); - Integer maxMemoryConfig = ConfigurationManagerImpl.VM_SERVICE_OFFERING_MAX_RAM_SIZE.value(); - if (customOfferingMaxMemory != null) { - logger.debug(String.format("Using 'Custom unconstrained' %s max memory value [%sMb] as %s memory.", serviceOfferingDescription, customOfferingMaxMemory, vmDescription)); - maxMemory = ByteScaleUtils.mebibytesToBytes(customOfferingMaxMemory); + ConfigKey maxMemoryConfig = CapacityManager.KvmMemoryDynamicScalingCapacity; + Integer maxMemoryConfigValue = maxMemoryConfig.valueIn(clusterId); + logger.info("[{}] is a dynamically scalable service offering. Using config [{}] value [{}] in cluster [ID: {}] as max [{}] memory.", + serviceOfferingVO.toString(), maxMemoryConfig.key(), maxMemoryConfigValue, clusterId, vmDescription); + if (maxMemoryConfigValue > 0) { + maxMemory = ByteScaleUtils.mebibytesToBytes(maxMemoryConfigValue); } else { - String maxMemoryConfigKey = ConfigurationManagerImpl.VM_SERVICE_OFFERING_MAX_RAM_SIZE.key(); - - logger.info(String.format("%s is a 'Custom unconstrained' service offering. Using config [%s] value [%s] as max %s memory.", - serviceOfferingDescription, maxMemoryConfigKey, maxMemoryConfig, vmDescription)); - - if (maxMemoryConfig > 0) { - maxMemory = ByteScaleUtils.mebibytesToBytes(maxMemoryConfig); - } else { - logger.info(String.format("Config [%s] has value less or equal '0'. Using %s host or last host max memory [%s] as VM max memory in the hypervisor.", maxMemoryConfigKey, vmDescription, maxHostMemory)); - maxMemory = maxHostMemory; - } + logger.info("Config [{}] in cluster [ID: {}] has value less or equal '0'. Using [{}] host or last host max memory [{}] as VM max memory in the hypervisor.", + maxMemoryConfig.key(), clusterId, vmDescription, maxHostMemory); + maxMemory = maxHostMemory; } return maxMemory; } - protected Integer getVmMaxCpuCores(ServiceOfferingVO serviceOfferingVO, String vmDescription, Integer maxHostCpuCore) { - String serviceOfferingDescription = serviceOfferingVO.toString(); - + protected Integer getVmMaxCpuCores(ServiceOfferingVO serviceOfferingVO, String vmDescription, Integer maxHostCpuCores, Long clusterId) { Integer maxCpuCores; - Integer customOfferingMaxCpuCores = NumberUtils.createInteger(serviceOfferingVO.getDetail(ApiConstants.MAX_CPU_NUMBER)); - Integer maxCpuCoresConfig = ConfigurationManagerImpl.VM_SERVICE_OFFERING_MAX_CPU_CORES.value(); - - if (customOfferingMaxCpuCores != null) { - logger.debug(String.format("Using 'Custom unconstrained' %s max cpu cores [%s] as %s cpu cores.", serviceOfferingDescription, customOfferingMaxCpuCores, vmDescription)); - maxCpuCores = customOfferingMaxCpuCores; + ConfigKey maxCpuCoresConfig = CapacityManager.KvmCpuDynamicScalingCapacity; + Integer maxCpuCoresConfigValue = maxCpuCoresConfig.valueIn(clusterId); + logger.info("[{}] is a dynamically scalable service offering. Using config [{}] value [{}] in cluster [ID: {}] as max [{}] CPU cores.", + serviceOfferingVO.toString(), maxCpuCoresConfig.key(), maxCpuCoresConfigValue, clusterId, vmDescription); + if (maxCpuCoresConfigValue > 0) { + maxCpuCores = maxCpuCoresConfigValue; } else { - String maxCpuCoreConfigKey = ConfigurationManagerImpl.VM_SERVICE_OFFERING_MAX_CPU_CORES.key(); - - logger.info(String.format("%s is a 'Custom unconstrained' service offering. Using config [%s] value [%s] as max %s cpu cores.", - serviceOfferingDescription, maxCpuCoreConfigKey, maxCpuCoresConfig, vmDescription)); - - if (maxCpuCoresConfig > 0) { - maxCpuCores = maxCpuCoresConfig; - } else { - logger.info(String.format("Config [%s] has value less or equal '0'. Using %s host or last host max cpu cores [%s] as VM cpu cores in the hypervisor.", maxCpuCoreConfigKey, vmDescription, maxHostCpuCore)); - maxCpuCores = maxHostCpuCore; - } + logger.info("Config [{}] in cluster [ID: {}] has value less or equal '0'. Using [{}] host or last host max CPU cores [{}] as VM CPU cores in the hypervisor.", + maxCpuCoresConfig.key(), clusterId, vmDescription, maxHostCpuCores); + maxCpuCores = maxHostCpuCores; } return maxCpuCores; } diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index 0a744709644c..c09916dd8a0a 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -108,6 +108,10 @@ public interface UserVmManager extends UserVmService { "Comma separated list of allowed additional VM settings if VM instance settings are read from OVA.", true, ConfigKey.Scope.Zone, null, null, null, null, null, ConfigKey.Kind.CSV, null); + ConfigKey AutoMigrateVmOnLiveScaleInsufficientCapacity = new ConfigKey<>("Advanced", Boolean.class, "auto.migrate.vm.on.live.scale.insufficient.capacity", + "true", "Defines whether a VM should be automatically migrated to a suitable host when the current host " + + "lacks sufficient compute capacity to live scale the instance. Defaults to true.", true, ConfigKey.Scope.Cluster); + static final int MAX_USER_DATA_LENGTH_BYTES = 2048; public static final String CKS_NODE = "cksnode"; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 9134be3d3bd9..d155b2709168 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2105,25 +2105,26 @@ private boolean upgradeRunningVirtualMachine(Long vmId, Long newServiceOfferingI int newCpu = newServiceOffering.getCpu(); int newMemory = newServiceOffering.getRamSize(); int newSpeed = newServiceOffering.getSpeed(); + boolean cpuCapEnabledForTheNewOffering = newServiceOffering.getLimitCpuUse(); int currentCpu = currentServiceOffering.getCpu(); int currentMemory = currentServiceOffering.getRamSize(); int currentSpeed = currentServiceOffering.getSpeed(); + boolean cpuCapEnabledForTheCurrentOffering = currentServiceOffering.getLimitCpuUse(); int memoryDiff = newMemory - currentMemory; int cpuDiff = newCpu * newSpeed - currentCpu * currentSpeed; - // Don't allow to scale when (Any of the new values less than current values) OR (All current and new values are same) - if ((newSpeed < currentSpeed || newMemory < currentMemory || newCpu < currentCpu) || (newSpeed == currentSpeed && newMemory == currentMemory && newCpu == currentCpu)) { - String message = String.format("While the VM is running, only scalling up it is supported. New service offering {\"memory\": %s, \"speed\": %s, \"cpu\": %s} should" - + " have at least one value (ram, speed or cpu) greater than the current values {\"memory\": %s, \"speed\": %s, \"cpu\": %s}.", newMemory, newSpeed, newCpu, - currentMemory, currentSpeed, currentCpu); - - throw new InvalidParameterValueException(message); + boolean scalingDown = newSpeed < currentSpeed || newMemory < currentMemory || newCpu < currentCpu; + if (scalingDown) { + throw new InvalidParameterValueException(String.format("Scaling down is not supported while the VM is running. The new service offering attributes " + + "{\"memory\": %s, \"CPU speed\": %s, \"vCPUs\": %s} must not be lower than the current values {\"memory\": %s, \"CPU speed\": %s, \"vCPUs\": %s}.", + newMemory, newSpeed, newCpu, currentMemory, currentSpeed, currentCpu)); } - if (vmHypervisorType.equals(HypervisorType.KVM) && !currentServiceOffering.isDynamic()) { - String message = String.format("Unable to live scale VM on KVM when current service offering is a \"Fixed Offering\". KVM needs the tag \"maxMemory\" to live scale and it is only configured when VM is deployed with a custom service offering and \"Dynamic Scalable\" is enabled."); - logger.info(message); - throw new InvalidParameterValueException(message); + boolean sameAmountOfResourcesAsThePreviousOffering = newSpeed == currentSpeed && newMemory == currentMemory && newCpu == currentCpu; + boolean cpuCapChange = cpuCapEnabledForTheCurrentOffering != cpuCapEnabledForTheNewOffering; + if (sameAmountOfResourcesAsThePreviousOffering && (vmHypervisorType != HypervisorType.KVM || !cpuCapChange)) { + throw new InvalidParameterValueException("While the VM is running, scaling to a service offering with the same attributes (memory, CPU speed and vCPUs) " + + "is only allowed when the CPU cap is changed."); } serviceOfferingDao.loadDetails(currentServiceOffering); @@ -2187,8 +2188,19 @@ private boolean upgradeRunningVirtualMachine(Long vmId, Long newServiceOfferingI excludes.addHost(vmInstance.getHostId()); } + boolean autoMigrateVmToASuitableHost = AutoMigrateVmOnLiveScaleInsufficientCapacity.valueIn(host.getClusterId()); + if (!existingHostHasCapacity && !autoMigrateVmToASuitableHost) { + logger.error("Unable to scale the VM [{}] because the host [{}] in which it is currently allocated does not " + + "have enough compute capacity to scale the instance and the VM should not be automatically migrated to another host " + + "([{}] setting is [false]).", vmInstance.getInstanceName(), host.getName(), AutoMigrateVmOnLiveScaleInsufficientCapacity.key()); + return false; + } + // #2 migrate the vm if host doesn't have capacity or is in avoid set if (!existingHostHasCapacity) { + logger.info("Host [{}] does not have enough compute capacity to scale the instance [{}]. Since the [{}] setting is " + + "[true], the VM will be migrated to a suitable host and, if succeeded, the VM will be live scaled to the requested " + + "compute offering.", host.getName(), vmInstance.getInstanceName(), AutoMigrateVmOnLiveScaleInsufficientCapacity.key()); _itMgr.findHostAndMigrate(vmInstance.getUuid(), newServiceOfferingId, customParameters, excludes); } @@ -9454,7 +9466,7 @@ public ConfigKey[] getConfigKeys() { VmIpFetchThreadPoolMax, VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties, KvmAdditionalConfigAllowList, XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, DestroyRootVolumeOnVmDestruction, EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm, VmDistinctHostNameScope, - VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva}; + VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva, AutoMigrateVmOnLiveScaleInsufficientCapacity}; } @Override diff --git a/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java b/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java index d8d62df1a723..95bdd3f5ac09 100644 --- a/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java +++ b/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java @@ -18,7 +18,6 @@ import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; -import com.cloud.configuration.ConfigurationManagerImpl; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.kvm.dpdk.DpdkHelper; @@ -36,6 +35,7 @@ import com.cloud.vm.VirtualMachineProfile; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.junit.Assert; import org.junit.Before; @@ -48,6 +48,8 @@ import org.mockito.junit.MockitoJUnitRunner; import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -170,79 +172,45 @@ public void testSetVmQuotaPercentageOverProvision() { } @Test - public void validateGetVmMaxMemoryReturnCustomOfferingMaxMemory(){ - int maxCustomOfferingMemory = 64; - Mockito.when(serviceOfferingVoMock.getDetail(ApiConstants.MAX_MEMORY)).thenReturn(String.valueOf(maxCustomOfferingMemory)); + public void getVmMaxMemoryTestConsiderKvmMemoryDynamicScalingCapacitySettingWhenItIsGreaterThanZero() { + int maxMemoryConfigValue = 64; - long result = guru.getVmMaxMemory(serviceOfferingVoMock, "Vm description", 1l); + ConfigDepotImpl configDepotMock = Mockito.mock(ConfigDepotImpl.class); + ConfigKey.init(configDepotMock); + Mockito.when(configDepotMock.getConfigStringValue(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(String.valueOf(maxMemoryConfigValue)); - Assert.assertEquals(ByteScaleUtils.mebibytesToBytes(maxCustomOfferingMemory), result); + long result = guru.getVmMaxMemory(serviceOfferingVoMock, "Vm description", 1L, 1L); + ConfigKey.init(null); + Assert.assertEquals(ByteScaleUtils.mebibytesToBytes(maxMemoryConfigValue), result); } @Test - public void validateGetVmMaxMemoryReturnVmServiceOfferingMaxRAMSize(){ - int maxMemoryConfig = 64; - Mockito.when(serviceOfferingVoMock.getDetail(ApiConstants.MAX_MEMORY)).thenReturn(null); - - ConfigKey vmServiceOfferingMaxRAMSize = Mockito.mock(ConfigKey.class); - ConfigurationManagerImpl.VM_SERVICE_OFFERING_MAX_RAM_SIZE = vmServiceOfferingMaxRAMSize; - - Mockito.when(vmServiceOfferingMaxRAMSize.value()).thenReturn(maxMemoryConfig); - long result = guru.getVmMaxMemory(serviceOfferingVoMock, "Vm description", 1l); - - Assert.assertEquals(ByteScaleUtils.mebibytesToBytes(maxMemoryConfig), result); - } - - @Test - public void validateGetVmMaxMemoryReturnMaxHostMemory(){ + public void getVmMaxMemoryTestConsiderHostMaxMemoryWhenKvmMemoryDynamicScalingCapacitySettingIsEqualToZero() { long maxHostMemory = ByteScaleUtils.mebibytesToBytes(2000); - Mockito.when(serviceOfferingVoMock.getDetail(ApiConstants.MAX_MEMORY)).thenReturn(null); - - ConfigKey vmServiceOfferingMaxRAMSize = Mockito.mock(ConfigKey.class); - ConfigurationManagerImpl.VM_SERVICE_OFFERING_MAX_RAM_SIZE = vmServiceOfferingMaxRAMSize; - - Mockito.when(vmServiceOfferingMaxRAMSize.value()).thenReturn(0); - - long result = guru.getVmMaxMemory(serviceOfferingVoMock, "Vm description", maxHostMemory); + long result = guru.getVmMaxMemory(serviceOfferingVoMock, "Vm description", maxHostMemory, 1L); Assert.assertEquals(maxHostMemory, result); } @Test - public void validateGetVmMaxCpuCoresReturnCustomOfferingMaxCpuCores(){ - int maxCustomOfferingCpuCores = 16; - Mockito.when(serviceOfferingVoMock.getDetail(ApiConstants.MAX_CPU_NUMBER)).thenReturn(String.valueOf(maxCustomOfferingCpuCores)); - - long result = guru.getVmMaxCpuCores(serviceOfferingVoMock, "Vm description", 1); - - Assert.assertEquals(maxCustomOfferingCpuCores, result); - } - - @Test - public void validateGetVmMaxCpuCoresVmServiceOfferingMaxCPUCores(){ + public void getVmMaxCpuCoresTestConsiderKvmCpuDynamicScalingCapacitySettingWhenItIsGreaterThanZero() { int maxCpuCoresConfig = 16; - Mockito.when(serviceOfferingVoMock.getDetail(ApiConstants.MAX_CPU_NUMBER)).thenReturn(null); - - ConfigKey vmServiceOfferingMaxCPUCores = Mockito.mock(ConfigKey.class); - ConfigurationManagerImpl.VM_SERVICE_OFFERING_MAX_CPU_CORES = vmServiceOfferingMaxCPUCores; + ConfigDepotImpl configDepotMock = Mockito.mock(ConfigDepotImpl.class); + ConfigKey.init(configDepotMock); + Mockito.when(configDepotMock.getConfigStringValue(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(String.valueOf(maxCpuCoresConfig)); - Mockito.when(vmServiceOfferingMaxCPUCores.value()).thenReturn(maxCpuCoresConfig); - long result = guru.getVmMaxCpuCores(serviceOfferingVoMock, "Vm description", 1); + long result = guru.getVmMaxCpuCores(serviceOfferingVoMock, "Vm description", 1, 1L); + ConfigKey.init(null); Assert.assertEquals(maxCpuCoresConfig, result); } @Test - public void validateGetVmMaxCpuCoresReturnMaxHostMemory(){ + public void getVmMaxCpuCoresTestConsiderHostMaxCpuWhenKvmCpuDynamicScalingCapacitySettingIsEqualToZero() { int maxHostCpuCores = 64; - Mockito.when(serviceOfferingVoMock.getDetail(ApiConstants.MAX_CPU_NUMBER)).thenReturn(null); - - ConfigKey vmServiceOfferingMaxCPUCores = Mockito.mock(ConfigKey.class); - ConfigurationManagerImpl.VM_SERVICE_OFFERING_MAX_CPU_CORES = vmServiceOfferingMaxCPUCores; - - Mockito.when(vmServiceOfferingMaxCPUCores.value()).thenReturn(0); - - long result = guru.getVmMaxCpuCores(serviceOfferingVoMock, "Vm description", maxHostCpuCores); + long result = guru.getVmMaxCpuCores(serviceOfferingVoMock, "Vm description", maxHostCpuCores, 1L); Assert.assertEquals(maxHostCpuCores, result); } @@ -305,39 +273,36 @@ public void validateConfigureVmMemoryAndCpuCoresServiceOfferingIsDynamicAndVmIsD guru.serviceOfferingDao = serviceOfferingDaoMock; Mockito.doReturn(serviceOfferingVoMock).when(serviceOfferingDaoMock).findById(Mockito.anyLong(), Mockito.anyLong()); - Mockito.doReturn(true).when(guru).isVmDynamicScalable(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(true).when(guru).isVmDynamicScalable(Mockito.any(), Mockito.any()); guru.configureVmMemoryAndCpuCores(vmTO, host, virtualMachineMock, vmProfile); - Mockito.verify(guru).getVmMaxMemory(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyLong()); - Mockito.verify(guru).getVmMaxCpuCores(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyInt()); + Mockito.verify(guru).getVmMaxMemory(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.verify(guru).getVmMaxCpuCores(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()); } @Test public void validateConfigureVmMemoryAndCpuCoresServiceOfferingIsNotDynamicAndVmIsDynamicDoNotCallGetMethods(){ guru.serviceOfferingDao = serviceOfferingDaoMock; - Mockito.doReturn(serviceOfferingVoMock).when(serviceOfferingDaoMock).findById(Mockito.anyLong(), Mockito.anyLong()); - Mockito.doReturn(false).when(guru).isVmDynamicScalable(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(false).when(guru).isVmDynamicScalable(Mockito.any(), Mockito.any()); guru.configureVmMemoryAndCpuCores(vmTO, host, virtualMachineMock, vmProfile); - Mockito.verify(guru, Mockito.never()).getVmMaxMemory(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyLong()); - Mockito.verify(guru, Mockito.never()).getVmMaxCpuCores(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyInt()); + Mockito.verify(guru, Mockito.never()).getVmMaxMemory(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.verify(guru, Mockito.never()).getVmMaxCpuCores(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()); } @Test public void validateConfigureVmMemoryAndCpuCoresServiceOfferingIsDynamicAndVmIsNotDynamicDoNotCallGetMethods(){ guru.serviceOfferingDao = serviceOfferingDaoMock; - Mockito.doReturn(serviceOfferingVoMock).when(serviceOfferingDaoMock).findById(Mockito.anyLong(), Mockito.anyLong()); - Mockito.doReturn(true).when(serviceOfferingVoMock).isDynamic(); Mockito.doReturn(false).when(vmTO).isEnableDynamicallyScaleVm(); guru.configureVmMemoryAndCpuCores(vmTO, host, virtualMachineMock, vmProfile); - Mockito.verify(guru, Mockito.never()).getVmMaxMemory(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyLong()); - Mockito.verify(guru, Mockito.never()).getVmMaxCpuCores(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyInt()); + Mockito.verify(guru, Mockito.never()).getVmMaxMemory(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong()); + Mockito.verify(guru, Mockito.never()).getVmMaxCpuCores(Mockito.any(ServiceOfferingVO.class), Mockito.anyString(), Mockito.anyInt(), Mockito.anyLong()); } @Test @@ -490,4 +455,30 @@ public void testGetNullWhenVMThereIsNoInformationOfUsedHosts() { Assert.assertNull(clusterId); } + + @Test + public void getCpuQuotaPercentageTestAssertQuotaEqualsVmSpeedDividedByHostSpeed() { + double hostSpeed = 3000; + double vmSpeed = 500; + double expectedQuota = new BigDecimal(vmSpeed / hostSpeed).setScale(2, RoundingMode.HALF_DOWN).doubleValue(); + double actualQuota = guru.getCpuQuotaPercentage(vmSpeed, hostSpeed); + Assert.assertEquals(expectedQuota, actualQuota, 0.0001); + } + + @Test + public void getCpuQuotaPercentageTestAssertQuotaEqualsOneWhenVmSpeedIsGreaterThanHostSpeed() { + double hostSpeed = 3000; + double vmSpeed = 6000; + double expectedQuota = 1; + double actualQuota = guru.getCpuQuotaPercentage(vmSpeed, hostSpeed); + Assert.assertEquals(expectedQuota, actualQuota, 0.0001); + } + + @Test + public void getCpuQuotaPercentageTestReturnNullWhenANumberFormatExceptionIsThrown() { + double hostSpeed = 0; + double vmSpeed = 6000; + Double actualQuota = guru.getCpuQuotaPercentage(vmSpeed, hostSpeed); + Assert.assertNull(actualQuota); + } } diff --git a/ui/public/locales/el_GR.json b/ui/public/locales/el_GR.json index bb90661d9d37..566e182b8fad 100644 --- a/ui/public/locales/el_GR.json +++ b/ui/public/locales/el_GR.json @@ -2270,8 +2270,6 @@ "message.error.enable.saml": "Δεν είναι δυνατή η εύρεση των id χρηστών για την ενεργοποίηση του Saml σύνδεση μιας φοράς, παρακαλούμε να το ενεργοποιήσετε με μη αυτόματο τρόπο.", "message.error.end.date.and.time": "Παρακαλώ, εισάγετε την τελική ημερομηνία και ώρα!", "message.error.endip": "Πληκτρολογήστε End IP", -"message.error.fixed.offering.kvm": "Δεν είναι δυνατό να κλιμωκαθούν οι εικονές μηχανές που χρησιμοποιούν τον επόπτη KVM με ένα σταθερό υπολογισμό προσφοράς υπηρεσίας.", -"message.error.fixed.offering.kvm": "Δεν είναι εφικτή η κλιμάκωση προς τα πάνω της εικονικής μηχανής ενώ ανήκει σε μία σταθερή προσφορά νέφους.", "message.error.gateway": "Πληκτρολογήστε Πύλη", "message.error.host.name": "Πληκτρολογήστε το όνομα του κεντρικού υπολογιστή", "message.error.host.password": "Πληκτρολογήστε τον κωδικό πρόσβασης κεντρικού υπολογιστή", diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 6f3a623b8092..7ff945ca0fbd 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -3591,7 +3591,6 @@ "message.error.zone.name": "Please enter Zone name.", "message.error.zone.type": "Please select Zone type.", "message.error.linstor.resourcegroup": "Please enter the Linstor Resource-Group.", -"message.error.fixed.offering.kvm": "It's not possible to scale up Instances that utilize KVM hypervisor with a fixed compute offering.", "message.error.create.webhook.local.account": "Account must be provided for creating a Webhook with Local scope.", "message.error.create.webhook.name": "Name must be provided for creating a Webhook.", "message.error.create.webhook.payloadurl": "Payload URL must be provided for creating a Webhook.", diff --git a/ui/public/locales/ja_JP.json b/ui/public/locales/ja_JP.json index 850264fb3411..b7f32d7a2e12 100644 --- a/ui/public/locales/ja_JP.json +++ b/ui/public/locales/ja_JP.json @@ -2932,7 +2932,6 @@ "message.error.domain": "ドメインを入力し、ROOTドメインは空のままにします", "message.error.enable.saml": "SAML SSOを有効にするユーザーIDが見つかりません。手動で有効にしてください。", "message.error.endip": "終了IPを入力してください", - "message.error.fixed.offering.kvm": "固定コンピューティングオファリングでKVMハイパーバイザーを利用するVMをスケールアップすることはできません。", "message.error.gateway": "ゲートウェイに入ってください", "message.error.host.name": "ホスト名を入力してください", "message.error.host.password": "ホストパスワードを入力してください", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index c7ca36c1278d..4a28a6166ca0 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -2225,7 +2225,6 @@ "message.error.zone.name": "Por favor, insira o nome da zona", "message.error.zone.type": "Por favor, selecione o tipo de zona", "message.error.linstor.resourcegroup": "Por favor, insira o Linstor Resource-Group", -"message.error.fixed.offering.kvm": "N\u00e3o \u00e9 poss\u00edvel escalar VMs que utilizam o virtualizador KVM com uma oferta de computa\u00e7\u00e3o fixa.", "message.fail.to.delete": "Falha ao deletar.", "message.failed.to.add": "Falha ao adicionar", "message.failed.to.assign.vms": "Falha ao atribuir VMs", diff --git a/ui/public/locales/te.json b/ui/public/locales/te.json index ed72626e0a67..f3b4c70ca2ed 100644 --- a/ui/public/locales/te.json +++ b/ui/public/locales/te.json @@ -3169,7 +3169,6 @@ "message.error.zone.name": "దయచేసి జోన్ పేరును నమోదు చేయండి.", "message.error.zone.type": "దయచేసి జోన్ రకాన్ని ఎంచుకోండి.", "message.error.linstor.resourcegroup": "దయచేసి Linstor Resource-Groupని నమోదు చేయండి.", - "message.error.fixed.offering.kvm": "ఫిక్స్‌డ్ కంప్యూట్ ఆఫర్‌తో KVM హైపర్‌వైజర్‌ను ఉపయోగించుకునే సందర్భాలను స్కేల్ చేయడం సాధ్యం కాదు.", "message.error.create.webhook.local.account": "స్థానిక స్కోప్‌తో వెబ్‌హుక్‌ని సృష్టించడానికి తప్పనిసరిగా ఖాతా అందించబడాలి.", "message.error.create.webhook.name": "వెబ్‌హుక్‌ని సృష్టించడానికి తప్పనిసరిగా పేరు అందించాలి.", "message.error.create.webhook.payloadurl": "Webhookని సృష్టించడానికి పేలోడ్ URL తప్పనిసరిగా అందించబడాలి.", diff --git a/ui/public/locales/zh_CN.json b/ui/public/locales/zh_CN.json index 4b68a2ef6d77..9536bab7ee43 100644 --- a/ui/public/locales/zh_CN.json +++ b/ui/public/locales/zh_CN.json @@ -3462,7 +3462,6 @@ "message.error.zone.name": "\u8BF7\u8F93\u5165\u533A\u57DF\u540D\u79F0", "message.error.zone.type": "\u8BF7\u9009\u62E9\u533A\u57DF\u7C7B\u578B", "message.error.linstor.resourcegroup": "\u8BF7\u8F93\u5165 Linstor \u8D44\u6E90\u7EC4", - "message.error.fixed.offering.kvm": "\u4E0D\u53EF\u80FD\u901A\u8FC7\u56FA\u5B9A\u7684\u8BA1\u7B97\u65B9\u6848\u6765\u6269\u5C55KVM\u865A\u62DF\u673A\u3002", "message.fail.to.delete": "\u5220\u9664\u5931\u8D25\u3002", "message.failed.to.add": "\u6DFB\u52A0\u5931\u8D25", "message.failed.to.assign.vms": "\u672A\u80FD\u5206\u914D\u865A\u62DF\u673A", diff --git a/ui/src/views/compute/ScaleVM.vue b/ui/src/views/compute/ScaleVM.vue index ce8dca5b43a6..b810e1327c6b 100644 --- a/ui/src/views/compute/ScaleVM.vue +++ b/ui/src/views/compute/ScaleVM.vue @@ -23,10 +23,6 @@ - - - - offering.id === this.resource.serviceofferingid) - this.currentOffer = this.offerings[0] - if (this.currentOffer === undefined) { - this.fixedOfferingKvm = true - } - } - this.offerings.map(i => { this.offeringsMap[i.id] = i }) + this.offerings.forEach(offering => { this.offeringsMap[offering.id] = offering }) }).finally(() => { this.loading = false }) }, getMinCpu () { - // We can only scale up while a VM is running - if (this.resource.state === 'Running') { - return this.resource.cpunumber - } return this.selectedOffering?.serviceofferingdetails?.mincpunumber * 1 || 1 }, - getMinMemory () { - // We can only scale up while a VM is running - if (this.resource.state === 'Running') { - return this.resource.memory + getInitialCpuValue () { + const offeringMinCpu = this.getMinCpu() + if (this.resource.cpunumber < offeringMinCpu) { + return offeringMinCpu } + return this.resource.cpunumber + }, + getMinMemory () { return this.selectedOffering?.serviceofferingdetails?.minmemory * 1 || 32 }, + getInitialMemoryValue () { + const offeringMinMemory = this.getMinMemory() + if (this.resource.memory < offeringMinMemory) { + return offeringMinMemory + } + return this.resource.memory + }, getCPUSpeed () { // We can only scale up while a VM is running if (this.resource.state === 'Running') { diff --git a/ui/src/views/compute/wizard/ComputeSelection.vue b/ui/src/views/compute/wizard/ComputeSelection.vue index 563e17984e35..9cf36153f5cd 100644 --- a/ui/src/views/compute/wizard/ComputeSelection.vue +++ b/ui/src/views/compute/wizard/ComputeSelection.vue @@ -111,6 +111,10 @@ export default { type: Number, default: 0 }, + initialCpuValue: { + type: Number, + default: 0 + }, minCpu: { type: Number, default: 0 @@ -119,6 +123,10 @@ export default { type: Number, default: 2 }, + initialMemoryValue: { + type: Number, + default: 0 + }, minMemory: { type: Number, default: 0 @@ -200,8 +208,8 @@ export default { }, methods: { fillValue () { - this.cpuNumberInputValue = this.minCpu - this.memoryInputValue = this.minMemory + this.cpuNumberInputValue = this.initialCpuValue > 0 ? this.initialCpuValue : this.minCpu + this.memoryInputValue = this.initialMemoryValue > 0 ? this.initialMemoryValue : this.minMemory this.cpuSpeedInputValue = this.cpuSpeed if (!this.preFillContent) {