From 2020bfb6a3bf597d762e162a355ea3dfd26857eb Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 23 May 2019 11:47:53 +0530 Subject: [PATCH] server: allows compute offering with or without constraints (#3245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Custom compute offering does not allow setting min and max values for CPU and VRAM for custom VMs. Root Cause: Custom compute offerings cannot be created with a given range of CPU number and memory instead it allows only fixed values. Solution: createServiceOffering API has been modified to allow setting a defined range for CPU number and memory. Also, UI form for compute offering creation is provided with a new field named 'compute offering type’ with values - Fixed, Custom Constrained, Custom Constrained. It will allow the creation of compute offerings either with a fixed CPU speed and memory for fixed compute offering, or with a range of CPU number and memory for custom constrained compute offering or without predefined CPU number, CPU speed and memory for custom unconstrained compute offering. To allow the user to set CPU number, CPU speed and memory during VM deployment, UI form for VM deployment has been modified to provide controls to change these values. These controls are depicted in screenshots below for custom constrained and custom unconstrained compute offering types. Sample API calls using cmk to create a constrained service offering and deploying a VM using it, create serviceoffering name=Constrained displaytext=Constrained customized=true mincpunumber=2 maxcpunumber=4 cpuspeed=400 minmemory=256 maxmemory=1024 deploy virtualmachine displayname=ConstrainedVM serviceofferingid=60f3e500-6559-40b2-9a61-2192891c2bd6 templateid=8e0f4a3e-601b-11e9-9df4-a0afbd4a2d60 zoneid=9612a0c6-ed28-4fae-9a48-6eb207af29e3 details[0].cpuNumber=3 details[0].memory=800 Signed-off-by: Abhishek Kumar --- .../apache/cloudstack/api/ApiConstants.java | 4 + .../offering/CreateServiceOfferingCmd.java | 87 ++++++++++-- .../com/cloud/service/ServiceOfferingVO.java | 4 + .../cloud/capacity/CapacityManagerImpl.java | 24 +++- .../ConfigurationManagerImpl.java | 69 +++++++--- .../java/com/cloud/vm/UserVmManagerImpl.java | 26 ++-- ui/css/cloudstack3.css | 20 ++- ui/index.html | 29 +++- ui/l10n/en.js | 8 ++ ui/scripts/configuration.js | 125 +++++++++++++++++- ui/scripts/docs.js | 25 ++++ ui/scripts/instanceWizard.js | 11 ++ ui/scripts/ui-custom/instanceWizard.js | 88 +++++++++--- 13 files changed, 444 insertions(+), 76 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 71a201f5bd8..0d8cb03cd3e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -202,6 +202,10 @@ public class ApiConstants { public static final String MAX = "max"; public static final String MAC_ADDRESS = "macaddress"; public static final String MAX_SNAPS = "maxsnaps"; + public static final String MAX_CPU_NUMBER = "maxcpunumber"; + public static final String MAX_MEMORY = "maxmemory"; + public static final String MIN_CPU_NUMBER = "mincpunumber"; + public static final String MIN_MEMORY = "minmemory"; public static final String MEMORY = "memory"; public static final String MODE = "mode"; public static final String KEEPALIVE_ENABLED = "keepaliveenabled"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java index ed87ad86c40..60a55a5ffd7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java @@ -18,10 +18,8 @@ package org.apache.cloudstack.api.command.admin.offering; import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; -import com.cloud.storage.Storage; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -30,10 +28,14 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.commons.collections.MapUtils; import org.apache.log4j.Logger; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.offering.ServiceOffering; +import com.cloud.storage.Storage; import com.cloud.user.Account; +import com.google.common.base.Strings; @APICommand(name = "createServiceOffering", description = "Creates a service offering.", responseObject = ServiceOfferingResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -162,6 +164,37 @@ public class CreateServiceOfferingCmd extends BaseCmd { since = "4.4") private Integer hypervisorSnapshotReserve; + // Introduce 4 new optional paramaters to work custom compute offerings + @Parameter(name = ApiConstants.CUSTOMIZED, + type = CommandType.BOOLEAN, + since = "4.13", + description = "Whether service offering size is custom or not") + private Boolean customized; + + @Parameter(name = ApiConstants.MAX_CPU_NUMBER, + type = CommandType.INTEGER, + description = "The maximum number of CPUs to be set with Custom Computer Offering", + since = "4.13") + private Integer maxCPU; + + @Parameter(name = ApiConstants.MIN_CPU_NUMBER, + type = CommandType.INTEGER, + description = "The minimum number of CPUs to be set with Custom Computer Offering", + since = "4.13") + private Integer minCPU; + + @Parameter(name = ApiConstants.MAX_MEMORY, + type = CommandType.INTEGER, + description = "The maximum memroy size of the custom service offering in MB", + since = "4.13") + private Integer maxMemory; + + @Parameter(name = ApiConstants.MIN_MEMORY, + type = CommandType.INTEGER, + description = "The minimum memroy size of the custom service offering in MB", + since = "4.13") + private Integer minMemory; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -175,6 +208,9 @@ public class CreateServiceOfferingCmd extends BaseCmd { } public String getDisplayText() { + if (Strings.isNullOrEmpty(displayText)) { + throw new InvalidParameterValueException("Failed to create service offering because the offering display text has not been spified."); + } return displayText; } @@ -187,6 +223,9 @@ public class CreateServiceOfferingCmd extends BaseCmd { } public String getServiceOfferingName() { + if (Strings.isNullOrEmpty(serviceOfferingName)) { + throw new InvalidParameterValueException("Failed to create service offering because offering name has not been spified."); + } return serviceOfferingName; } @@ -234,18 +273,12 @@ public class CreateServiceOfferingCmd extends BaseCmd { return deploymentPlanner; } - public boolean isCustomized() { - return (cpuNumber == null || memory == null || cpuSpeed == null); - } - public Map getDetails() { - Map detailsMap = null; - if (details != null && !details.isEmpty()) { - detailsMap = new HashMap(); + Map detailsMap = new HashMap<>(); + if (MapUtils.isNotEmpty(details)) { Collection props = details.values(); - Iterator iter = props.iterator(); - while (iter.hasNext()) { - HashMap detail = (HashMap) iter.next(); + for (Object prop : props) { + HashMap detail = (HashMap) prop; detailsMap.put(detail.get("key"), detail.get("value")); } } @@ -316,6 +349,36 @@ public class CreateServiceOfferingCmd extends BaseCmd { return hypervisorSnapshotReserve; } + /** + * If customized parameter is true, then cpuNumber, memory and cpuSpeed must be null + * Check if the optional params min/max CPU/Memory have been specified + * @return true if the following conditions are satisfied; + * - cpuNumber, memory and cpuSpeed are all null when customized parameter is set to true + * - min/max CPU/Memory params are all null or all set + */ + public boolean isCustomized() { + if (customized != null){ + return customized; + } + return (cpuNumber == null || memory == null); + } + + public Integer getMaxCPUs() { + return maxCPU; + } + + public Integer getMinCPUs() { + return minCPU; + } + + public Integer getMaxMemory() { + return maxMemory; + } + + public Integer getMinMemory() { + return minMemory; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java index efaadcfae10..8cc834cecff 100644 --- a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java @@ -331,4 +331,8 @@ public class ServiceOfferingVO extends DiskOfferingVO implements ServiceOffering public void setDynamicFlag(boolean isdynamic) { isDynamic = isdynamic; } + + public boolean isCustomCpuSpeedSupported() { + return isCustomized() && getDetail("minCPU") != null; + } } diff --git a/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java b/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java index cc2d7a56b60..6775bfc6cff 100644 --- a/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java +++ b/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java @@ -612,9 +612,15 @@ public class CapacityManagerImpl extends ManagerBase implements CapacityManager, usedMemory += ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.memory.name())) * 1024L * 1024L) / ramOvercommitRatio) * clusterRamOvercommitRatio; - usedCpu += - ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())) * Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuSpeed.name()))) / cpuOvercommitRatio) * - clusterCpuOvercommitRatio; + if(vmDetails.containsKey(UsageEventVO.DynamicParameters.cpuSpeed.name())) { + usedCpu += + ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())) * Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuSpeed.name()))) / cpuOvercommitRatio) * + clusterCpuOvercommitRatio; + } else { + usedCpu += + ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())) * so.getSpeed()) / cpuOvercommitRatio) * + clusterCpuOvercommitRatio; + } usedCpuCore += Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())); } else { usedMemory += ((so.getRamSize() * 1024L * 1024L) / ramOvercommitRatio) * clusterRamOvercommitRatio; @@ -645,9 +651,15 @@ public class CapacityManagerImpl extends ManagerBase implements CapacityManager, reservedMemory += ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.memory.name())) * 1024L * 1024L) / ramOvercommitRatio) * clusterRamOvercommitRatio; - reservedCpu += - ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())) * Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuSpeed.name()))) / cpuOvercommitRatio) * - clusterCpuOvercommitRatio; + if(vmDetails.containsKey(UsageEventVO.DynamicParameters.cpuSpeed.name())) { + reservedCpu += + ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())) * Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuSpeed.name()))) / cpuOvercommitRatio) * + clusterCpuOvercommitRatio; + } else { + reservedCpu += + ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())) * so.getSpeed()) / cpuOvercommitRatio) * + clusterCpuOvercommitRatio; + } reservedCpuCore += Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())); } else { reservedMemory += ((so.getRamSize() * 1024L * 1024L) / ramOvercommitRatio) * clusterRamOvercommitRatio; diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 30f2f7cd1e2..a104bfb41aa 100755 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -35,12 +35,11 @@ import java.util.UUID; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.google.common.collect.Sets; - import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupService; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; @@ -232,6 +231,7 @@ import com.cloud.vm.dao.VMInstanceDao; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.Sets; public class ConfigurationManagerImpl extends ManagerBase implements ConfigurationManager, ConfigurationService, Configurable { public static final Logger s_logger = Logger.getLogger(ConfigurationManagerImpl.class); @@ -2218,6 +2218,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati @ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_CREATE, eventDescription = "creating service offering") public ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) { final Long userId = CallContext.current().getCallingUserId(); + final Map details = cmd.getDetails(); + final String offeringName = cmd.getServiceOfferingName(); final String name = cmd.getServiceOfferingName(); if (name == null || name.length() == 0) { @@ -2233,21 +2235,54 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati final Integer cpuSpeed = cmd.getCpuSpeed(); final Integer memory = cmd.getMemory(); - //restricting the createserviceoffering to allow setting all or none of the dynamic parameters to null - if (cpuNumber == null || cpuSpeed == null || memory == null) { - if (cpuNumber != null || cpuSpeed != null || memory != null) { - throw new InvalidParameterValueException("For creating a custom compute offering cpu, cpu speed and memory all should be null"); - } - } + // Optional Custom Parameters + Integer maxCPU = cmd.getMaxCPUs(); + Integer minCPU = cmd.getMinCPUs(); + Integer maxMemory = cmd.getMaxMemory(); + Integer minMemory = cmd.getMinMemory(); - if (cpuNumber != null && (cpuNumber.intValue() <= 0 || cpuNumber.longValue() > Integer.MAX_VALUE)) { - throw new InvalidParameterValueException("Failed to create service offering " + name + ": specify the cpu number value between 1 and " + Integer.MAX_VALUE); - } - if (cpuSpeed != null && (cpuSpeed.intValue() < 0 || cpuSpeed.longValue() > Integer.MAX_VALUE)) { - throw new InvalidParameterValueException("Failed to create service offering " + name + ": specify the cpu speed value between 0 and " + Integer.MAX_VALUE); - } - if (memory != null && (memory.intValue() < 32 || memory.longValue() > Integer.MAX_VALUE)) { - throw new InvalidParameterValueException("Failed to create service offering " + name + ": specify the memory value between 32 and " + Integer.MAX_VALUE + " MB"); + // Check if service offering is Custom, + // If Customized, the following conditions must hold + // 1. cpuNumber, cpuSpeed and memory should be all null + // 2. minCPU, maxCPU, minMemory and maxMemory should all be null or all specified + boolean isCustomized = cmd.isCustomized(); + if (isCustomized) { + // validate specs + //restricting the createserviceoffering to allow setting all or none of the dynamic parameters to null + if (cpuNumber != null || memory != null) { + throw new InvalidParameterValueException("For creating a custom compute offering cpu and memory all should be null"); + } + // if any of them is null, then all of them shoull be null + if (maxCPU == null || minCPU == null || maxMemory == null || minMemory == null) { + if (maxCPU != null || minCPU != null || maxMemory != null || minMemory != null) { + throw new InvalidParameterValueException("For creating a custom compute offering min/max cpu and min/max memory should all be specified"); + } + } else { + if (cpuSpeed != null && (cpuSpeed.intValue() < 0 || cpuSpeed.longValue() > Integer.MAX_VALUE)) { + throw new InvalidParameterValueException("Failed to create service offering " + offeringName + ": specify the cpu speed value between 1 and " + Integer.MAX_VALUE); + } + if ((maxCPU <= 0 || maxCPU.longValue() > Integer.MAX_VALUE) || (minCPU <= 0 || minCPU.longValue() > Integer.MAX_VALUE ) ) { + throw new InvalidParameterValueException("Failed to create service offering " + offeringName + ": specify the minimum or minimum cpu number value between 1 and " + Integer.MAX_VALUE); + } + if (minMemory < 32 || (minMemory.longValue() > Integer.MAX_VALUE) || (maxMemory.longValue() > Integer.MAX_VALUE)) { + throw new InvalidParameterValueException("Failed to create service offering " + offeringName + ": specify the memory value between 32 and " + Integer.MAX_VALUE + " MB"); + } + // Persist min/max CPU and Memory parameters in the service_offering_details table + details.put(ApiConstants.MIN_MEMORY, minMemory.toString()); + details.put(ApiConstants.MAX_MEMORY, maxMemory.toString()); + details.put(ApiConstants.MIN_CPU_NUMBER, minCPU.toString()); + details.put(ApiConstants.MAX_CPU_NUMBER, maxCPU.toString()); + } + } else { + if (cpuNumber != null && (cpuNumber.intValue() <= 0 || cpuNumber.longValue() > Integer.MAX_VALUE)) { + throw new InvalidParameterValueException("Failed to create service offering " + offeringName + ": specify the cpu number value between 1 and " + Integer.MAX_VALUE); + } + if (cpuSpeed != null && (cpuSpeed.intValue() < 0 || cpuSpeed.longValue() > Integer.MAX_VALUE)) { + throw new InvalidParameterValueException("Failed to create service offering " + offeringName + ": specify the cpu speed value between 0 and " + Integer.MAX_VALUE); + } + if (memory != null && (memory.intValue() < 32 || memory.longValue() > Integer.MAX_VALUE)) { + throw new InvalidParameterValueException("Failed to create service offering " + offeringName + ": specify the memory value between 32 and " + Integer.MAX_VALUE + " MB"); + } } // check if valid domain @@ -2330,7 +2365,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati return createServiceOffering(userId, cmd.isSystem(), vmType, cmd.getServiceOfferingName(), cpuNumber, memory, cpuSpeed, cmd.getDisplayText(), cmd.getProvisioningType(), localStorageRequired, offerHA, limitCpuUse, volatileVm, cmd.getTags(), cmd.getDomainId(), cmd.getHostTag(), - cmd.getNetworkRate(), cmd.getDeploymentPlanner(), cmd.getDetails(), isCustomizedIops, cmd.getMinIops(), cmd.getMaxIops(), + cmd.getNetworkRate(), cmd.getDeploymentPlanner(), details, isCustomizedIops, cmd.getMinIops(), cmd.getMaxIops(), cmd.getBytesReadRate(), cmd.getBytesReadRateMax(), cmd.getBytesReadRateMaxLength(), cmd.getBytesWriteRate(), cmd.getBytesWriteRateMax(), cmd.getBytesWriteRateMaxLength(), cmd.getIopsReadRate(), cmd.getIopsReadRateMax(), cmd.getIopsReadRateMaxLength(), diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 68b45e1af7c..3727ea69960 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -1017,14 +1017,18 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override public void validateCustomParameters(ServiceOfferingVO serviceOffering, Map customParameters) { + //TODO need to validate custom cpu, and memory against min/max CPU/Memory ranges from service_offering_details table if (customParameters.size() != 0) { + Map offeringDetails = serviceOfferingDetailsDao.listDetailsKeyPairs(serviceOffering.getId()); if (serviceOffering.getCpu() == null) { - String cpuNumber = customParameters.get(UsageEventVO.DynamicParameters.cpuNumber.name()); - if ((cpuNumber == null) || (NumbersUtil.parseInt(cpuNumber, -1) <= 0)) { - throw new InvalidParameterValueException("Invalid cpu cores value, specify a value between 1 and " + Integer.MAX_VALUE); + int minCPU = NumbersUtil.parseInt(offeringDetails.get(ApiConstants.MIN_CPU_NUMBER), 1); + int maxCPU = NumbersUtil.parseInt(offeringDetails.get(ApiConstants.MAX_CPU_NUMBER), Integer.MAX_VALUE); + int cpuNumber = NumbersUtil.parseInt(customParameters.get(UsageEventVO.DynamicParameters.cpuNumber.name()), -1); + if (cpuNumber < minCPU || cpuNumber > maxCPU) { + throw new InvalidParameterValueException(String.format("Invalid cpu cores value, specify a value between %d and %d", minCPU, maxCPU)); } } else if (customParameters.containsKey(UsageEventVO.DynamicParameters.cpuNumber.name())) { - throw new InvalidParameterValueException("The cpu cores of this offering id:" + serviceOffering.getId() + throw new InvalidParameterValueException("The cpu cores of this offering id:" + serviceOffering.getUuid() + " is not customizable. This is predefined in the template."); } @@ -1033,18 +1037,20 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if ((cpuSpeed == null) || (NumbersUtil.parseInt(cpuSpeed, -1) <= 0)) { throw new InvalidParameterValueException("Invalid cpu speed value, specify a value between 1 and " + Integer.MAX_VALUE); } - } else if (customParameters.containsKey(UsageEventVO.DynamicParameters.cpuSpeed.name())) { - throw new InvalidParameterValueException("The cpu speed of this offering id:" + serviceOffering.getId() + } else if (!serviceOffering.isCustomCpuSpeedSupported() && customParameters.containsKey(UsageEventVO.DynamicParameters.cpuSpeed.name())) { + throw new InvalidParameterValueException("The cpu speed of this offering id:" + serviceOffering.getUuid() + " is not customizable. This is predefined in the template."); } if (serviceOffering.getRamSize() == null) { - String memory = customParameters.get(UsageEventVO.DynamicParameters.memory.name()); - if (memory == null || (NumbersUtil.parseInt(memory, -1) < 32)) { - throw new InvalidParameterValueException("Invalid memory value, specify a value between 32 and " + Integer.MAX_VALUE + " MB"); + int minMemory = NumbersUtil.parseInt(offeringDetails.get(ApiConstants.MIN_MEMORY), 32); + int maxMemory = NumbersUtil.parseInt(offeringDetails.get(ApiConstants.MAX_MEMORY), Integer.MAX_VALUE); + int memory = NumbersUtil.parseInt(customParameters.get(UsageEventVO.DynamicParameters.memory.name()), -1); + if (memory < minMemory || memory > maxMemory) { + throw new InvalidParameterValueException(String.format("Invalid memory value, specify a value between %d and %d", minMemory, maxMemory)); } } else if (customParameters.containsKey(UsageEventVO.DynamicParameters.memory.name())) { - throw new InvalidParameterValueException("The memory of this offering id:" + serviceOffering.getId() + " is not customizable. This is predefined in the template."); + throw new InvalidParameterValueException("The memory of this offering id:" + serviceOffering.getUuid() + " is not customizable. This is predefined in the template."); } } else { throw new InvalidParameterValueException("Need to specify custom parameter values cpu, cpu speed and memory when using custom offering"); diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 9e35834de6d..d7b6159d2bf 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -6117,11 +6117,11 @@ label.error { margin-top: 9px !important; } -.multi-wizard.instance-wizard .custom-disk-size .select-container { +.multi-wizard.instance-wizard .custom-slider-container .select-container { height: 279px; } -.multi-wizard.instance-wizard .custom-disk-size .select-container { +.multi-wizard.instance-wizard .custom-slider-container .select-container { height: 213px; margin: -7px 6px 0 8px; /*+border-radius:6px;*/ @@ -6220,7 +6220,11 @@ label.error { font-size: 10px; } -.instance-wizard .step.data-disk-offering.custom-disk-size .select-container { +.instance-wizard .step.data-disk-offering.custom-slider-container .select-container { + height: 272px; +} + +.instance-wizard .step.service-offering.custom-slider-container .select-container { height: 272px; } @@ -6228,11 +6232,15 @@ label.error { height: 240px; } -.instance-wizard .step.data-disk-offering.custom-disk-size.custom-iops-do .select-container { +.instance-wizard .step.data-disk-offering.custom-slider-container.custom-iops-do .select-container { height: 176px; } -.instance-wizard .step.data-disk-offering.required.custom-disk-size .select-container { +.instance-wizard .step.service-offering.required.custom-slider-container .select-container { + height: 315px; +} + +.instance-wizard .step.data-disk-offering.required.custom-slider-container .select-container { height: 315px; } @@ -6240,7 +6248,7 @@ label.error { height: 295px; } -.instance-wizard .step.data-disk-offering.required.custom-disk-size.custom-iops-do .select-container { +.instance-wizard .step.data-disk-offering.required.custom-slider-container.custom-iops-do .select-container { height: 223px; } diff --git a/ui/index.html b/ui/index.html index a71d82395f0..1140d146577 100644 --- a/ui/index.html +++ b/ui/index.html @@ -206,7 +206,7 @@ -
+
@@ -221,6 +221,31 @@
+ +
+
+ + + +
+ + + +
+
+ + + +
+ + + +
+
+ +
+
+
@@ -248,7 +273,7 @@
-
+
diff --git a/ui/l10n/en.js b/ui/l10n/en.js index 0c8db8d68c0..c16a955695c 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -553,6 +553,10 @@ var dictionary = { "label.compute":"Compute", "label.compute.and.storage":"Compute and Storage", "label.compute.offering":"Compute offering", +"label.compute.offering.type":"Compute offering type", +"label.compute.offering.custom.constrained":"Custom Constrained", +"label.compute.offering.custom.unconstrained":"Custom Unconstrained", +"label.compute.offering.fixed":"Fixed Offering", "label.compute.offerings":"Compute Offerings", "label.configuration":"Configuration", "label.configure":"Configure", @@ -1037,6 +1041,8 @@ var dictionary = { "label.memory.allocated":"Memory Allocated", "label.memory.limits":"Memory limits (MiB)", "label.memory.mb":"Memory (in MB)", +"label.memory.minimum.mb":"Min Memory (in MB)", +"label.memory.maximum.mb":"Max Memory (in MB)", "label.memory.total":"Memory Total", "label.memory.used":"Memory Used", "label.menu.accounts":"Accounts", @@ -1219,6 +1225,8 @@ var dictionary = { "label.not.found":"Not Found", "label.notifications":"Notifications", "label.num.cpu.cores":"# of CPU Cores", +"label.min.cpu.cores":"Min CPU Cores", +"label.max.cpu.cores":"Max CPU Cores", "label.number.of.clusters":"Number of Clusters", "label.number.of.cpu.sockets":"The Number of CPU Sockets", "label.number.of.hosts":"Number of Hosts", diff --git a/ui/scripts/configuration.js b/ui/scripts/configuration.js index c81a632bd69..4003154710a 100644 --- a/ui/scripts/configuration.js +++ b/ui/scripts/configuration.js @@ -136,11 +136,70 @@ }); } }, - isCustomized: { - label: 'label.custom', - isBoolean: true, - isReverse: true, - isChecked: false + offeringType: { + label: 'label.compute.offering.type', + docID: 'helpComputeOfferingType', + select: function(args) { + var items = []; + items.push({ + id: 'fixed', + description: _l('label.compute.offering.fixed') + }); + items.push({ + id: 'customConstrained', + description: _l('label.compute.offering.custom.constrained') + }); + items.push({ + id: 'customUnconstrained', + description: _l('label.compute.offering.custom.unconstrained') + }); + + args.response.success({ + data: items + }); + + args.$select.change(function() { + var $form = $(this).closest('form'); + + var $cpuNumber = $form.find('.form-item[rel=cpuNumber]'); + var $cpuSpeed = $form.find('.form-item[rel=cpuSpeed]'); + var $memory = $form.find('.form-item[rel=memory]'); + + var $minCPUNumber = $form.find('.form-item[rel=minCPUNumber]'); + var $maxCPUNumber = $form.find('.form-item[rel=maxCPUNumber]'); + var $minMemory = $form.find('.form-item[rel=minMemory]'); + var $maxMemory = $form.find('.form-item[rel=maxMemory]'); + + var type = $(this).val(); + if (type == 'fixed') { + $minCPUNumber.hide(); + $maxCPUNumber.hide(); + $minMemory.hide(); + $maxMemory.hide(); + + $cpuNumber.css('display', 'inline-block'); + $cpuSpeed.css('display', 'inline-block'); + $memory.css('display', 'inline-block'); + } else if (type == 'customConstrained') { + $cpuNumber.hide(); + $memory.hide(); + + $cpuSpeed.css('display', 'inline-block'); + $minCPUNumber.css('display', 'inline-block'); + $maxCPUNumber.css('display', 'inline-block'); + $minMemory.css('display', 'inline-block'); + $maxMemory.css('display', 'inline-block'); + } else { + $cpuNumber.hide(); + $memory.hide(); + $cpuSpeed.hide(); + $minCPUNumber.hide(); + $maxCPUNumber.hide(); + $minMemory.hide(); + $maxMemory.hide(); + } + }); + } }, cpuNumber: { label: 'label.num.cpu.cores', @@ -169,6 +228,39 @@ number: true } }, + // Custom Compute Offering + minCPUNumber: { + label: 'label.min.cpu.cores', + docID: 'helpComputeOfferingMinCPUCores', + validation: { + required: true, + number: true + } + }, + maxCPUNumber: { + label: 'label.max.cpu.cores', + docID: 'helpComputeOfferingMaxCPUCores', + validation: { + required: true, + number: true + } + }, + minMemory: { + label: 'label.memory.minimum.mb', + docID: 'helpComputeOfferingMinMemory', + validation: { + required: true, + number: true + } + }, + maxMemory: { + label: 'label.memory.maximum.mb', + docID: 'helpComputeOfferingMaxMemory', + validation: { + required: true, + number: true + } + }, networkRate: { label: 'label.network.rate', docID: 'helpComputeOfferingNetworkRate', @@ -588,17 +680,18 @@ }, action: function(args) { + var isFixedOfferingType = args.data.offeringType == 'fixed'; var data = { issystem: false, name: args.data.name, displaytext: args.data.description, storageType: args.data.storageType, provisioningType :args.data.provisioningType, - customized: (args.data.isCustomized == "on") + customized: !isFixedOfferingType }; //custom fields (begin) - if (args.data.isCustomized != "on") { + if (isFixedOfferingType) { $.extend(data, { cpuNumber: args.data.cpuNumber }); @@ -608,6 +701,24 @@ $.extend(data, { memory: args.data.memory }); + } else { + if(args.data.cpuSpeed != null && args.data.minCPUNumber != null && args.data.maxCPUNumber != null && args.data.minMemory != null && args.data.maxMemory != null){ + $.extend(data, { + cpuSpeed: args.data.cpuSpeed + }); + $.extend(data, { + mincpunumber: args.data.minCPUNumber + }); + $.extend(data, { + maxcpunumber: args.data.maxCPUNumber + }); + $.extend(data, { + minmemory: args.data.minMemory + }); + $.extend(data, { + maxmemory: args.data.maxMemory + }); + } } //custom fields (end) diff --git a/ui/scripts/docs.js b/ui/scripts/docs.js index 016d806c3bf..9a737469ced 100755 --- a/ui/scripts/docs.js +++ b/ui/scripts/docs.js @@ -1326,5 +1326,30 @@ cloudStack.docs = { helpL2UserData: { desc: 'Pass user and meta data to VMs (via ConfigDrive)', externalLink: '' + }, + + helpComputeOfferingMinCPUCores: { + desc: 'This will be used for the setting the range (min-max) of the number of cpu cores that should be allowed for VMs using this custom offering.', + externalLink: '' + }, + + helpComputeOfferingMaxCPUCores: { + desc: 'This will be used for the setting the range (min-max) of the number of cpu cores that should be allowed for VMs using this custom offering.', + externalLink: '' + }, + + helpComputeOfferingMinMemory: { + desc: 'This will be used for the setting the range (min-max) amount of memory that should be allowed for VMs using this custom offering.', + externalLink: '' + }, + + helpComputeOfferingMaxMemory: { + desc: 'This will be used for the setting the range (min-max) amount of memory that should be allowed for VMs using this custom offering.', + externalLink: '' + }, + + helpComputeOfferingType: { + desc: 'This will be used for setting the type of compute offering - whether it is fixed, custom constrained or custom unconstrained.', + externalLink: '' } }; diff --git a/ui/scripts/instanceWizard.js b/ui/scripts/instanceWizard.js index 1234cfa7de3..d175f1f8dfd 100644 --- a/ui/scripts/instanceWizard.js +++ b/ui/scripts/instanceWizard.js @@ -811,6 +811,17 @@ 'details[0].memory' : args.$wizard.find('input[name=compute-memory]').val() }); } + } else if (args.$wizard.find('input[name=slider-compute-cpu-cores]').parent().parent().css('display') != 'none') { + if (args.$wizard.find('input[name=slider-compute-cpu-cores]').val().length > 0) { + $.extend(deployVmData, { + 'details[0].cpuNumber' : args.$wizard.find('input[name=slider-compute-cpu-cores]').val() + }); + } + if (args.$wizard.find('input[name=slider-compute-memory]').val().length > 0) { + $.extend(deployVmData, { + 'details[0].memory' : args.$wizard.find('input[name=slider-compute-memory]').val() + }); + } } if (args.$wizard.find('input[name=disk-min-iops]').parent().parent().css('display') != 'none') { diff --git a/ui/scripts/ui-custom/instanceWizard.js b/ui/scripts/ui-custom/instanceWizard.js index a219c69d444..706c0bfa4b5 100644 --- a/ui/scripts/ui-custom/instanceWizard.js +++ b/ui/scripts/ui-custom/instanceWizard.js @@ -431,7 +431,7 @@ if (custom) { $step.find('.section.custom-size').hide(); - $step.removeClass('custom-disk-size'); + $step.removeClass('custom-slider-container'); } $step.find('input[type=radio]').bind('change', function() { @@ -464,10 +464,10 @@ var hypervisor = item['hypervisor']; if (hypervisor == 'KVM' || hypervisor == 'XenServer' || hypervisor == 'VMware') { $step.find('.section.custom-size').show(); - $step.addClass('custom-disk-size'); + $step.addClass('custom-slider-container'); } else { $step.find('.section.custom-size').hide(); - $step.removeClass('custom-disk-size'); + $step.removeClass('custom-slider-container'); } return true; @@ -516,8 +516,65 @@ var custom = item[args.customFlag]; if (custom) { + // contains min/max CPU and Memory values $step.addClass('custom-size'); - } else { + var offeringDetails = item['serviceofferingdetails']; + var offeringCpuSpeed = item['cpuspeed']; + $step.find('.custom-no-limits').hide(); + $step.find('.custom-slider-container').hide(); + + var minCpuNumber = 0, maxCpuNumber = 0, minMemory = 0, maxMemory = 0; + if (offeringDetails){ + minCpuNumber = offeringDetails['mincpunumber']; + maxCpuNumber = offeringDetails['maxcpunumber']; + minMemory = offeringDetails['minmemory']; + maxMemory = offeringDetails['maxmemory']; + } + + if (minCpuNumber > 0 && maxCpuNumber > 0 && minMemory > 0 && maxMemory > 0) { + $step.find('.custom-slider-container.slider-cpu-speed input[type=text]').val(parseInt(offeringCpuSpeed)); + $step.find('.custom-slider-container').show(); + var setupSlider = function(sliderClassName, minVal, maxVal) { + $step.find('.custom-slider-container .' + sliderClassName + ' .size.min span').html(minVal); + $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').val(minVal); + $step.find('.custom-slider-container .' + sliderClassName + ' .size.max span').html(maxVal); + $step.find('.custom-slider-container .' + sliderClassName + ' .slider').each(function() { + var $slider = $(this); + $slider.slider({ + min: parseInt(minVal), + max: parseInt(maxVal), + slide: function(event, ui) { + $slider.closest('.section.custom-size .' + sliderClassName + '').find('input[type=text]').val(ui.value); + $step.find('span.custom-slider-container .' + sliderClassName + '').html(ui.value); + } + }); + }); + + $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').bind('change', function() { + var val = $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').val(); + if (val < minVal || val > maxVal) { + cloudStack.dialog.notice({ message: $.validator.format(_l('message.validate.range'), [minVal, maxVal]) }); + } + if (val < minVal) { + val = minVal; + $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').val(val); + } + if(val > maxVal) { + val = maxVal; + $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').val(val); + } + $step.find('span.custom-slider-container .' + sliderClassName).html(_s(val)); + }); + } + setupSlider('slider-cpu-cores', minCpuNumber, maxCpuNumber); + setupSlider('slider-memory-mb', minMemory, maxMemory); + } else { + $step.find('.custom-slider-container.slider-cpu-speed.slider-compute-cpu-speed').val(0); + $step.find('.custom-no-limits').show(); + } + } else { + $step.find('.custom-no-limits').hide(); + $step.find('.custom-slider-container').hide(); $step.removeClass('custom-size'); } @@ -557,7 +614,7 @@ var multiDisk = args.multiDisk; $step.find('.multi-disk-select-container').remove(); - $step.removeClass('custom-disk-size'); + $step.removeClass('custom-slider-container'); $step.find('.main-desc, p.no-datadisk').remove(); if (!multiDisk){ @@ -675,7 +732,7 @@ } else { // handle removal of custom size controls $step.find('.section.custom-size').hide(); - $step.removeClass('custom-disk-size'); + $step.removeClass('custom-slider-container'); // handle removal of custom IOPS controls $step.removeClass('custom-iops-do'); @@ -694,7 +751,7 @@ $('').addClass('custom-size-label') .append(': ') .append( - $('').addClass('custom-disk-size').html( + $('').addClass('custom-slider-container').html( $step.find('.custom-size input[name=size]').val() ) ) @@ -705,14 +762,14 @@ $('').addClass('custom-size-label') .append(', ') .append( - $('').addClass('custom-disk-size').html( + $('').addClass('custom-slider-container').html( $step.find('.custom-size input[name=size]').val() ) ) .append(' GB') ); $step.find('.section.custom-size').show(); - $step.addClass('custom-disk-size'); + $step.addClass('custom-slider-container'); $target.closest('.select-container').scrollTop( $target.position().top ); @@ -723,7 +780,7 @@ $(this).closest('.disk-select-group').removeClass('custom-size'); } else { $step.find('.section.custom-size').hide(); - $step.removeClass('custom-disk-size'); + $step.removeClass('custom-slider-container'); } } @@ -1333,9 +1390,9 @@ args.maxDiskOfferingSize() : 100; // Setup tabs and slider - $wizard.find('.section.custom-size.custom-disk-size .size.min span').html(minCustomDiskSize); - $wizard.find('.section.custom-size.custom-disk-size input[type=text]').val(minCustomDiskSize); - $wizard.find('.section.custom-size.custom-disk-size .size.max span').html(maxCustomDiskSize); + $wizard.find('.section.custom-size.custom-slider-container .size.min span').html(minCustomDiskSize); + $wizard.find('.section.custom-size.custom-slider-container input[type=text]').val(minCustomDiskSize); + $wizard.find('.section.custom-size.custom-slider-container .size.max span').html(maxCustomDiskSize); $wizard.find('.tab-view').tabs(); $wizard.find('.slider').each(function() { var $slider = $(this); @@ -1350,7 +1407,7 @@ $slider.closest('.section.custom-size').find('input[type=text]').val( ui.value ); - $slider.closest('.step').find('span.custom-disk-size').html( + $slider.closest('.step').find('span.custom-slider-container').html( ui.value ); } @@ -1359,9 +1416,8 @@ $wizard.find('div.data-disk-offering div.custom-size input[type=text]').bind('change', function() { var old = $wizard.find('div.data-disk-offering div.custom-size input[type=text]').val(); - $wizard.find('div.data-disk-offering span.custom-disk-size').html(_s(old)); + $wizard.find('div.data-disk-offering span.custom-slider-container').html(_s(old)); }); - var wizardDialog = $wizard.dialog({ title: _l('label.vm.add'),