From ee95f45df4e8846215426b412bc62390e1e48270 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 13 Mar 2019 14:24:39 +0530 Subject: [PATCH 1/7] server: allows compute offering with or without constraints Changes allow admin to create compute offerings with unconstrained CPU, Memory or constrained range CPU, memory which can be later set by user while deploying VM. Signed-off-by: Abhishek Kumar --- .../apache/cloudstack/api/ApiConstants.java | 4 + .../offering/CreateServiceOfferingCmd.java | 87 ++++++++++-- .../com/cloud/service/ServiceOfferingVO.java | 4 + .../ConfigurationManagerImpl.java | 69 +++++++--- .../java/com/cloud/vm/UserVmManagerImpl.java | 27 +++- 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 | 77 ++++++++--- 12 files changed, 424 insertions(+), 62 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 0c1f2b11f327..2f36dad18cc4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -201,6 +201,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 ed87ad86c409..4332a4d21cb6 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 @@ 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.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.11") + 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 Integer getCpuSpeed() { } 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 Integer getMemory() { } 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 String getDeploymentPlanner() { 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 Integer getHypervisorSnapshotReserve() { 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 efaadcfae106..8cc834cecff4 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 boolean isDynamic() { public void setDynamicFlag(boolean isdynamic) { isDynamic = isdynamic; } + + public boolean isCustomCpuSpeedSupported() { + return isCustomized() && getDetail("minCPU") != null; + } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 30f2f7cd1e22..a104bfb41aac 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 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.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 DataCenter createZone(final CreateZoneCmd cmd) { @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 ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) 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(); + + // 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"); } - } - - 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 valid domain @@ -2330,7 +2365,7 @@ public ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) 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 68b45e1af7c3..2552792db2c1 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -1017,6 +1017,7 @@ public UserVm upgradeVirtualMachine(UpgradeVMCmd cmd) throws ResourceAllocationE @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) { if (serviceOffering.getCpu() == null) { String cpuNumber = customParameters.get(UsageEventVO.DynamicParameters.cpuNumber.name()); @@ -1033,7 +1034,7 @@ public void validateCustomParameters(ServiceOfferingVO serviceOffering, Map details = serviceOfferingDetailsDao.listDetailsKeyPairs(offering.getId()); + + if (details.containsKey(ApiConstants.MAX_CPU_NUMBER) && details.containsKey(ApiConstants.MIN_CPU_NUMBER) + && details.containsKey(ApiConstants.MIN_MEMORY) && details.containsKey(ApiConstants.MAX_MEMORY)){ + + int cpu = NumbersUtil.parseInt(customParameters.get(UsageEventVO.DynamicParameters.cpuNumber.name()), -1); + + if (cpu < NumbersUtil.parseInt(details.get(ApiConstants.MIN_CPU_NUMBER), -1) && cpu > NumbersUtil.parseInt(details.get(ApiConstants.MAX_CPU_NUMBER), -1)) { + throw new InvalidParameterValueException("The provided cpu value: " + cpu + "is out of the range supported by this custom offering"); + } + + int memory = NumbersUtil.parseInt(customParameters.get(UsageEventVO.DynamicParameters.memory.name()), -1); + + if (memory < NumbersUtil.parseInt(details.get(ApiConstants.MIN_MEMORY), -1) && memory > NumbersUtil.parseInt(details.get(ApiConstants.MAX_MEMORY), -1)) { + throw new InvalidParameterValueException("The provided memory value: " + memory + "is out of the range supported by this custom offering"); + } + + } else { + resourceLimitCheck(owner, isDisplayVm, new Long(offering.getCpu()), new Long(offering.getRamSize())); + } _resourceLimitMgr.checkResourceLimit(owner, ResourceType.volume, (isIso || diskOfferingId == null ? 1 : 2)); _resourceLimitMgr.checkResourceLimit(owner, ResourceType.primary_storage, size); diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 5294f5658802..a95f48b77650 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -6304,11 +6304,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;*/ @@ -6393,7 +6393,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; } @@ -6401,11 +6405,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; } @@ -6413,7 +6421,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 6e33598ed9c4..547ea040583f 100644 --- a/ui/index.html +++ b/ui/index.html @@ -205,7 +205,7 @@

-
+
@@ -220,6 +220,31 @@

+ +
+
+ + + +
+ + + +
+
+ + + +
+ + + +
+
+ +
+
+
@@ -247,7 +272,7 @@

-
+
diff --git a/ui/l10n/en.js b/ui/l10n/en.js index eb7b82250891..685366091dc7 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -554,6 +554,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 Unsconstrained", +"label.compute.offering.fixed":"Fixed Offering", "label.compute.offerings":"Compute Offerings", "label.configuration":"Configuration", "label.configure":"Configure", @@ -1039,6 +1043,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", @@ -1223,6 +1229,8 @@ var dictionary = { "label.nuage.vpc.usedomaintemplate":"Use pre-configured Domain Template", "label.nuage.vpc.domaintemplatelist":"Domain Template", "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 de8f4726e66d..5a9555f3d796 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 bbe8f3e64b49..7daea6133c3d 100755 --- a/ui/scripts/docs.js +++ b/ui/scripts/docs.js @@ -1355,5 +1355,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 351ca7b30208..1eeae4288915 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 9b41e35e035c..63bb9e566444 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,54 @@ 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 old = $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').val(); + $step.find('span.custom-slider-container .' + sliderClassName).html(_s(old)); + }); + } + 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 +603,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 +721,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 +740,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 +751,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 +769,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 +1379,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 +1396,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 +1405,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'), From 2b89dfcac8e0eeaf8cf292680805e093df55405f Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 4 Apr 2019 11:34:05 +0530 Subject: [PATCH 2/7] server: fix capacity computaion for constrained offering VM VM deployed using constrained service offering won't be having CPU speed in the details therefore CPU speed for such VMs need to be retrieved from offering itself. This change adds check for the same. Signed-off-by: Abhishek Kumar --- .../cloud/capacity/CapacityManagerImpl.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java b/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java index cc2d7a56b601..6775bfc6cff0 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 void updateCapacityForHost(final Host host) { 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 void updateCapacityForHost(final Host host) { 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; From 15e6afeddc2da80de7e971524a2f79eaccf74083 Mon Sep 17 00:00:00 2001 From: dahn Date: Wed, 10 Apr 2019 12:41:14 +0200 Subject: [PATCH 3/7] Update en.js typo --- ui/l10n/en.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/l10n/en.js b/ui/l10n/en.js index 685366091dc7..5865ceb2e7f2 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -556,7 +556,7 @@ var dictionary = { "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 Unsconstrained", +"label.compute.offering.custom.unconstrained":"Custom Unconstrained", "label.compute.offering.fixed":"Fixed Offering", "label.compute.offerings":"Compute Offerings", "label.configuration":"Configuration", From c1a81afe6ea0625efb6cf092c46264fd349d1444 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 11 Apr 2019 10:42:30 +0530 Subject: [PATCH 4/7] server: fix for out of range CPU, memory for custom offering VM Signed-off-by: Abhishek Kumar --- .../java/com/cloud/vm/UserVmManagerImpl.java | 47 ++++++------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 2552792db2c1..3727ea699608 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -1019,13 +1019,16 @@ public UserVm upgradeVirtualMachine(UpgradeVMCmd cmd) throws ResourceAllocationE 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."); } @@ -1035,17 +1038,19 @@ public void validateCustomParameters(ServiceOfferingVO serviceOffering, Map 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"); @@ -3389,29 +3394,7 @@ private UserVm createVirtualMachine(DataCenter zone, ServiceOffering serviceOffe } size += _diskOfferingDao.findById(diskOfferingId).getDiskSize(); } - - // Check Limits from new parameters here - // Get custom offerring cpu and memory ranges frm service_offering_details Table; - Map details = serviceOfferingDetailsDao.listDetailsKeyPairs(offering.getId()); - - if (details.containsKey(ApiConstants.MAX_CPU_NUMBER) && details.containsKey(ApiConstants.MIN_CPU_NUMBER) - && details.containsKey(ApiConstants.MIN_MEMORY) && details.containsKey(ApiConstants.MAX_MEMORY)){ - - int cpu = NumbersUtil.parseInt(customParameters.get(UsageEventVO.DynamicParameters.cpuNumber.name()), -1); - - if (cpu < NumbersUtil.parseInt(details.get(ApiConstants.MIN_CPU_NUMBER), -1) && cpu > NumbersUtil.parseInt(details.get(ApiConstants.MAX_CPU_NUMBER), -1)) { - throw new InvalidParameterValueException("The provided cpu value: " + cpu + "is out of the range supported by this custom offering"); - } - - int memory = NumbersUtil.parseInt(customParameters.get(UsageEventVO.DynamicParameters.memory.name()), -1); - - if (memory < NumbersUtil.parseInt(details.get(ApiConstants.MIN_MEMORY), -1) && memory > NumbersUtil.parseInt(details.get(ApiConstants.MAX_MEMORY), -1)) { - throw new InvalidParameterValueException("The provided memory value: " + memory + "is out of the range supported by this custom offering"); - } - - } else { - resourceLimitCheck(owner, isDisplayVm, new Long(offering.getCpu()), new Long(offering.getRamSize())); - } + resourceLimitCheck(owner, isDisplayVm, new Long(offering.getCpu()), new Long(offering.getRamSize())); _resourceLimitMgr.checkResourceLimit(owner, ResourceType.volume, (isIso || diskOfferingId == null ? 1 : 2)); _resourceLimitMgr.checkResourceLimit(owner, ResourceType.primary_storage, size); From 04585fc0fa375a2b7e5de4e700fb7be11220b43d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 11 Apr 2019 13:00:45 +0530 Subject: [PATCH 5/7] ui: prevent out of range cpu, memory input for custom offering Signed-off-by: Abhishek Kumar --- ui/scripts/ui-custom/instanceWizard.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ui/scripts/ui-custom/instanceWizard.js b/ui/scripts/ui-custom/instanceWizard.js index 63bb9e566444..2e04a02ae11d 100644 --- a/ui/scripts/ui-custom/instanceWizard.js +++ b/ui/scripts/ui-custom/instanceWizard.js @@ -551,8 +551,16 @@ }); $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').bind('change', function() { - var old = $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').val(); - $step.find('span.custom-slider-container .' + sliderClassName).html(_s(old)); + var val = $step.find('.custom-slider-container .' + sliderClassName + ' input[type=text]').val(); + 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); From a5761f5a708844369a839eb416244dbaf89fbb81 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 11 Apr 2019 13:32:29 +0530 Subject: [PATCH 6/7] ui: show prompt for out of range cpu, memory values Signed-off-by: Abhishek Kumar --- ui/scripts/ui-custom/instanceWizard.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/scripts/ui-custom/instanceWizard.js b/ui/scripts/ui-custom/instanceWizard.js index 2e04a02ae11d..14cfe03b8c44 100644 --- a/ui/scripts/ui-custom/instanceWizard.js +++ b/ui/scripts/ui-custom/instanceWizard.js @@ -552,6 +552,9 @@ $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); From f953e6070dd394fd896d5f6bd35e5cd5d3c33f6a Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 15 Apr 2019 17:48:32 +0530 Subject: [PATCH 7/7] api: fixed maxMemory param since value Signed-off-by: Abhishek Kumar --- .../api/command/admin/offering/CreateServiceOfferingCmd.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4332a4d21cb6..60a55a5ffd7c 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 @@ -186,7 +186,7 @@ public class CreateServiceOfferingCmd extends BaseCmd { @Parameter(name = ApiConstants.MAX_MEMORY, type = CommandType.INTEGER, description = "The maximum memroy size of the custom service offering in MB", - since = "4.11") + since = "4.13") private Integer maxMemory; @Parameter(name = ApiConstants.MIN_MEMORY,