diff --git a/api/src/main/java/com/cloud/configuration/ConfigurationService.java b/api/src/main/java/com/cloud/configuration/ConfigurationService.java index 438283136d2c..1bf3cc19162c 100644 --- a/api/src/main/java/com/cloud/configuration/ConfigurationService.java +++ b/api/src/main/java/com/cloud/configuration/ConfigurationService.java @@ -24,6 +24,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; +import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd; import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; @@ -33,6 +34,8 @@ import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -105,6 +108,33 @@ public interface ConfigurationService { */ ServiceOffering createServiceOffering(CreateServiceOfferingCmd cmd); + /** + * Clones a service offering with optional parameter overrides + * + * @param cmd + * the command object that specifies the source offering ID and optional parameter overrides + * @return the newly created service offering cloned from source, null otherwise + */ + ServiceOffering cloneServiceOffering(CloneServiceOfferingCmd cmd); + + /** + * Clones a disk offering with optional parameter overrides + * + * @param cmd + * the command object that specifies the source offering ID and optional parameter overrides + * @return the newly created disk offering cloned from source, null otherwise + */ + DiskOffering cloneDiskOffering(CloneDiskOfferingCmd cmd); + + /** + * Clones a network offering with optional parameter overrides + * + * @param cmd + * the command object that specifies the source offering ID and optional parameter overrides + * @return the newly created network offering cloned from source, null otherwise + */ + NetworkOffering cloneNetworkOffering(CloneNetworkOfferingCmd cmd); + /** * Updates a service offering * diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 38e601c790a7..e3ee2720f59f 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -374,6 +374,7 @@ public class EventTypes { // Service Offerings public static final String EVENT_SERVICE_OFFERING_CREATE = "SERVICE.OFFERING.CREATE"; + public static final String EVENT_SERVICE_OFFERING_CLONE = "SERVICE.OFFERING.CLONE"; public static final String EVENT_SERVICE_OFFERING_EDIT = "SERVICE.OFFERING.EDIT"; public static final String EVENT_SERVICE_OFFERING_DELETE = "SERVICE.OFFERING.DELETE"; 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 8fca652518f2..67d2d57eb3bc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -552,6 +552,7 @@ public class ApiConstants { public static final String USE_STORAGE_REPLICATION = "usestoragereplication"; public static final String SOURCE_CIDR_LIST = "sourcecidrlist"; + public static final String SOURCE_OFFERING_ID = "sourceofferingid"; public static final String SOURCE_ZONE_ID = "sourcezoneid"; public static final String SSL_VERIFICATION = "sslverification"; public static final String START_ASN = "startasn"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java new file mode 100644 index 000000000000..f20fb1f4c6a6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java @@ -0,0 +1,97 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.network; + +import java.util.List; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NetworkOfferingResponse; + +import com.cloud.offering.NetworkOffering; + +@APICommand(name = "cloneNetworkOffering", + description = "Clones a network offering. All parameters are copied from the source offering unless explicitly overridden. " + + "Use 'addServices' and 'dropServices' to modify the service list without respecifying everything.", + responseObject = NetworkOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class CloneNetworkOfferingCmd extends CreateNetworkOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = NetworkOfferingResponse.class, + required = true, + description = "The ID of the network offering to clone") + private Long sourceOfferingId; + + @Parameter(name = "addservices", + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services to add to the cloned offering (in addition to source offering services). " + + "If specified along with 'supportedservices', this parameter is ignored.") + private List addServices; + + @Parameter(name = "dropservices", + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services to remove from the cloned offering (that exist in source offering). " + + "If specified along with 'supportedservices', this parameter is ignored.") + private List dropServices; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + public List getAddServices() { + return addServices; + } + + public List getDropServices() { + return dropServices; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + NetworkOffering result = _configService.cloneNetworkOffering(this); + if (result != null) { + NetworkOfferingResponse response = _responseGenerator.createNetworkOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone network offering"); + } + } +} + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java new file mode 100644 index 000000000000..7e576d1b3b20 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java @@ -0,0 +1,72 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.offering; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DiskOfferingResponse; + +import com.cloud.offering.DiskOffering; + +@APICommand(name = "cloneDiskOffering", + description = "Clones a disk offering. All parameters from createDiskOffering are available. If not specified, values will be copied from the source offering.", + responseObject = DiskOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class CloneDiskOfferingCmd extends CreateDiskOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = DiskOfferingResponse.class, + required = true, + description = "The ID of the disk offering to clone") + private Long sourceOfferingId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + DiskOffering result = _configService.cloneDiskOffering(this); + if (result != null) { + DiskOfferingResponse response = _responseGenerator.createDiskOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone disk offering"); + } + } +} + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java new file mode 100644 index 000000000000..2515b873e3e6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java @@ -0,0 +1,72 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.offering; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ServiceOfferingResponse; + +import com.cloud.offering.ServiceOffering; + +@APICommand(name = "cloneServiceOffering", + description = "Clones a service offering. All parameters from createServiceOffering are available. If not specified, values will be copied from the source offering.", + responseObject = ServiceOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class CloneServiceOfferingCmd extends CreateServiceOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = CommandType.UUID, + entityType = ServiceOfferingResponse.class, + required = true, + description = "The ID of the service offering to clone") + private Long sourceOfferingId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + + @Override + public void execute() { + ServiceOffering result = _configService.cloneServiceOffering(this); + if (result != null) { + ServiceOfferingResponse response = _responseGenerator.createServiceOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone service offering"); + } + } +} + diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 21995d5ae650..3a47b2c47b88 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -66,6 +66,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; +import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd; import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; @@ -75,6 +76,8 @@ import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -3843,6 +3846,458 @@ protected boolean serviceOfferingExternalDetailsNeedUpdate(final Map requestParams = cmd.getFullUrlParams(); + + final String name = cmd.getServiceOfferingName(); + final String displayText = getOrDefault(cmd.getDisplayText(), sourceOffering.getDisplayText()); + final Integer cpuNumber = getOrDefault(cmd.getCpuNumber(), sourceOffering.getCpu()); + final Integer cpuSpeed = getOrDefault(cmd.getCpuSpeed(), sourceOffering.getSpeed()); + final Integer memory = getOrDefault(cmd.getMemory(), sourceOffering.getRamSize()); + final String provisioningType = resolveProvisioningType(cmd, sourceDiskOffering); + + final Boolean offerHa = resolveBooleanParam(requestParams, ApiConstants.OFFER_HA, cmd::isOfferHa, sourceOffering.isOfferHA()); + final Boolean limitCpuUse = resolveBooleanParam(requestParams, ApiConstants.LIMIT_CPU_USE, cmd::isLimitCpuUse, sourceOffering.getLimitCpuUse()); + final Boolean isVolatile = resolveBooleanParam(requestParams, ApiConstants.IS_VOLATILE, cmd::isVolatileVm, sourceOffering.isVolatileVm()); + final Boolean isCustomized = resolveBooleanParam(requestParams, ApiConstants.CUSTOMIZED, cmd::isCustomized, sourceOffering.isCustomized()); + final Boolean dynamicScalingEnabled = resolveBooleanParam(requestParams, ApiConstants.DYNAMIC_SCALING_ENABLED, cmd::getDynamicScalingEnabled, sourceOffering.isDynamicScalingEnabled()); + final Boolean diskOfferingStrictness = resolveBooleanParam(requestParams, ApiConstants.DISK_OFFERING_STRICTNESS, cmd::getDiskOfferingStrictness, sourceOffering.getDiskOfferingStrictness()); + final Boolean encryptRoot = resolveBooleanParam(requestParams, ApiConstants.ENCRYPT_ROOT, cmd::getEncryptRoot, sourceDiskOffering != null && sourceDiskOffering.getEncrypt()); + final Boolean gpuDisplay = resolveBooleanParam(requestParams, ApiConstants.GPU_DISPLAY, cmd::getGpuDisplay, sourceOffering.getGpuDisplay()); + + final String storageType = resolveStorageType(cmd, sourceDiskOffering); + final String tags = getOrDefault(cmd.getTags(), sourceDiskOffering != null ? sourceDiskOffering.getTags() : null); + final List domainIds = resolveDomainIds(cmd, sourceOffering); + final List zoneIds = resolveZoneIds(cmd, sourceOffering); + final String hostTag = getOrDefault(cmd.getHostTag(), sourceOffering.getHostTag()); + final Integer networkRate = getOrDefault(cmd.getNetworkRate(), sourceOffering.getRateMbps()); + final String deploymentPlanner = getOrDefault(cmd.getDeploymentPlanner(), sourceOffering.getDeploymentPlanner()); + + final ClonedDiskOfferingParams diskParams = resolveDiskOfferingParams(cmd, sourceDiskOffering); + + final CustomOfferingParams customParams = resolveCustomOfferingParams(cmd, sourceOffering, isCustomized); + + final Long vgpuProfileId = getOrDefault(cmd.getVgpuProfileId(), sourceOffering.getVgpuProfileId()); + final Integer gpuCount = getOrDefault(cmd.getGpuCount(), sourceOffering.getGpuCount()); + + final Boolean purgeResources = resolvePurgeResources(cmd, requestParams, sourceOffering); + final LeaseParams leaseParams = resolveLeaseParams(cmd, sourceOffering); + + if (cmd.getCacheMode() != null) { + validateCacheMode(cmd.getCacheMode()); + } + final Integer finalGpuCount = validateVgpuProfileAndGetGpuCount(vgpuProfileId, gpuCount); + + final Map mergedDetails = mergeOfferingDetails(cmd, sourceOffering, customParams); + + final boolean localStorageRequired = ServiceOffering.StorageType.local.toString().equalsIgnoreCase(storageType); + + final boolean systemUse = sourceOffering.isSystemUse(); + final VirtualMachine.Type vmType = resolveVmType(sourceOffering); + + final Long diskOfferingId = getOrDefault(cmd.getDiskOfferingId(), sourceOffering.getDiskOfferingId()); + + return createServiceOffering(userId, systemUse, vmType, + name, cpuNumber, memory, cpuSpeed, displayText, provisioningType, localStorageRequired, + offerHa, limitCpuUse, isVolatile, tags, domainIds, zoneIds, hostTag, networkRate, + deploymentPlanner, mergedDetails, diskParams.rootDiskSize, diskParams.isCustomizedIops, + diskParams.minIops, diskParams.maxIops, + diskParams.bytesReadRate, diskParams.bytesReadRateMax, diskParams.bytesReadRateMaxLength, + diskParams.bytesWriteRate, diskParams.bytesWriteRateMax, diskParams.bytesWriteRateMaxLength, + diskParams.iopsReadRate, diskParams.iopsReadRateMax, diskParams.iopsReadRateMaxLength, + diskParams.iopsWriteRate, diskParams.iopsWriteRateMax, diskParams.iopsWriteRateMaxLength, + diskParams.hypervisorSnapshotReserve, diskParams.cacheMode, customParams.storagePolicy, dynamicScalingEnabled, + diskOfferingId, diskOfferingStrictness, isCustomized, encryptRoot, + vgpuProfileId, finalGpuCount, gpuDisplay, purgeResources, leaseParams.leaseDuration, leaseParams.leaseExpiryAction); + } + + private ServiceOfferingVO getAndValidateSourceOffering(Long sourceOfferingId) { + final ServiceOfferingVO sourceOffering = _serviceOfferingDao.findById(sourceOfferingId); + if (sourceOffering == null) { + throw new InvalidParameterValueException("Unable to find service offering with ID: " + sourceOfferingId); + } + return sourceOffering; + } + + private DiskOfferingVO getSourceDiskOffering(ServiceOfferingVO sourceOffering) { + final Long sourceDiskOfferingId = sourceOffering.getDiskOfferingId(); + return sourceDiskOfferingId != null ? _diskOfferingDao.findById(sourceDiskOfferingId) : null; + } + + private T getOrDefault(T cmdValue, T defaultValue) { + return cmdValue != null ? cmdValue : defaultValue; + } + + private Boolean resolveBooleanParam(Map requestParams, String paramKey, + java.util.function.Supplier cmdValueSupplier, Boolean defaultValue) { + return requestParams != null && requestParams.containsKey(paramKey) ? cmdValueSupplier.get() : defaultValue; + } + + private String resolveProvisioningType(CloneServiceOfferingCmd cmd, DiskOfferingVO sourceDiskOffering) { + if (cmd.getProvisioningType() != null) { + return cmd.getProvisioningType(); + } + if (sourceDiskOffering != null) { + return sourceDiskOffering.getProvisioningType().toString(); + } + return Storage.ProvisioningType.THIN.toString(); + } + + private String resolveStorageType(CloneServiceOfferingCmd cmd, DiskOfferingVO sourceDiskOffering) { + if (cmd.getStorageType() != null) { + return cmd.getStorageType(); + } + if (sourceDiskOffering != null && sourceDiskOffering.isUseLocalStorage()) { + return ServiceOffering.StorageType.local.toString(); + } + return ServiceOffering.StorageType.shared.toString(); + } + + private List resolveDomainIds(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering) { + List domainIds = cmd.getDomainIds(); + if (domainIds == null || domainIds.isEmpty()) { + domainIds = _serviceOfferingDetailsDao.findDomainIds(sourceOffering.getId()); + } + return domainIds; + } + + private List resolveZoneIds(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering) { + List zoneIds = cmd.getZoneIds(); + if (zoneIds == null || zoneIds.isEmpty()) { + zoneIds = _serviceOfferingDetailsDao.findZoneIds(sourceOffering.getId()); + } + return zoneIds; + } + + private ClonedDiskOfferingParams resolveDiskOfferingParams(CloneServiceOfferingCmd cmd, DiskOfferingVO sourceDiskOffering) { + final ClonedDiskOfferingParams params = new ClonedDiskOfferingParams(); + + params.rootDiskSize = getOrDefault(cmd.getRootDiskSize(), sourceDiskOffering != null ? sourceDiskOffering.getDiskSize() : null); + params.bytesReadRate = getOrDefault(cmd.getBytesReadRate(), sourceDiskOffering != null ? sourceDiskOffering.getBytesReadRate() : null); + params.bytesReadRateMax = getOrDefault(cmd.getBytesReadRateMax(), sourceDiskOffering != null ? sourceDiskOffering.getBytesReadRateMax() : null); + params.bytesReadRateMaxLength = getOrDefault(cmd.getBytesReadRateMaxLength(), sourceDiskOffering != null ? sourceDiskOffering.getBytesReadRateMaxLength() : null); + params.bytesWriteRate = getOrDefault(cmd.getBytesWriteRate(), sourceDiskOffering != null ? sourceDiskOffering.getBytesWriteRate() : null); + params.bytesWriteRateMax = getOrDefault(cmd.getBytesWriteRateMax(), sourceDiskOffering != null ? sourceDiskOffering.getBytesWriteRateMax() : null); + params.bytesWriteRateMaxLength = getOrDefault(cmd.getBytesWriteRateMaxLength(), sourceDiskOffering != null ? sourceDiskOffering.getBytesWriteRateMaxLength() : null); + params.iopsReadRate = getOrDefault(cmd.getIopsReadRate(), sourceDiskOffering != null ? sourceDiskOffering.getIopsReadRate() : null); + params.iopsReadRateMax = getOrDefault(cmd.getIopsReadRateMax(), sourceDiskOffering != null ? sourceDiskOffering.getIopsReadRateMax() : null); + params.iopsReadRateMaxLength = getOrDefault(cmd.getIopsReadRateMaxLength(), sourceDiskOffering != null ? sourceDiskOffering.getIopsReadRateMaxLength() : null); + params.iopsWriteRate = getOrDefault(cmd.getIopsWriteRate(), sourceDiskOffering != null ? sourceDiskOffering.getIopsWriteRate() : null); + params.iopsWriteRateMax = getOrDefault(cmd.getIopsWriteRateMax(), sourceDiskOffering != null ? sourceDiskOffering.getIopsWriteRateMax() : null); + params.iopsWriteRateMaxLength = getOrDefault(cmd.getIopsWriteRateMaxLength(), sourceDiskOffering != null ? sourceDiskOffering.getIopsWriteRateMaxLength() : null); + params.isCustomizedIops = getOrDefault(cmd.isCustomizedIops(), sourceDiskOffering != null ? sourceDiskOffering.isCustomizedIops() : null); + params.minIops = getOrDefault(cmd.getMinIops(), sourceDiskOffering != null ? sourceDiskOffering.getMinIops() : null); + params.maxIops = getOrDefault(cmd.getMaxIops(), sourceDiskOffering != null ? sourceDiskOffering.getMaxIops() : null); + params.hypervisorSnapshotReserve = getOrDefault(cmd.getHypervisorSnapshotReserve(), sourceDiskOffering != null ? sourceDiskOffering.getHypervisorSnapshotReserve() : null); + + if (cmd.getCacheMode() != null) { + params.cacheMode = cmd.getCacheMode(); + } else if (sourceDiskOffering != null && sourceDiskOffering.getCacheMode() != null) { + params.cacheMode = sourceDiskOffering.getCacheMode().toString(); + } + + return params; + } + + private CustomOfferingParams resolveCustomOfferingParams(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering, Boolean isCustomized) { + final CustomOfferingParams params = new CustomOfferingParams(); + + params.maxCPU = resolveDetailParameter(cmd.getMaxCPUs(), sourceOffering.getId(), ApiConstants.MAX_CPU_NUMBER); + params.minCPU = resolveDetailParameter(cmd.getMinCPUs(), sourceOffering.getId(), ApiConstants.MIN_CPU_NUMBER); + params.maxMemory = resolveDetailParameter(cmd.getMaxMemory(), sourceOffering.getId(), ApiConstants.MAX_MEMORY); + params.minMemory = resolveDetailParameter(cmd.getMinMemory(), sourceOffering.getId(), ApiConstants.MIN_MEMORY); + params.storagePolicy = resolveDetailParameterAsLong(cmd.getStoragePolicy(), sourceOffering.getId(), ApiConstants.STORAGE_POLICY); + + return params; + } + + private Integer resolveDetailParameter(Integer cmdValue, Long offeringId, String detailKey) { + if (cmdValue != null) { + return cmdValue; + } + String detailValue = _serviceOfferingDetailsDao.getDetail(offeringId, detailKey); + return detailValue != null ? Integer.parseInt(detailValue) : null; + } + + private Long resolveDetailParameterAsLong(Long cmdValue, Long offeringId, String detailKey) { + if (cmdValue != null) { + return cmdValue; + } + String detailValue = _serviceOfferingDetailsDao.getDetail(offeringId, detailKey); + return detailValue != null ? Long.parseLong(detailValue) : null; + } + + private Boolean resolvePurgeResources(CloneServiceOfferingCmd cmd, Map requestParams, ServiceOfferingVO sourceOffering) { + if (requestParams != null && requestParams.containsKey(ApiConstants.PURGE_RESOURCES)) { + return cmd.isPurgeResources(); + } + String purgeResourcesStr = _serviceOfferingDetailsDao.getDetail(sourceOffering.getId(), ServiceOffering.PURGE_DB_ENTITIES_KEY); + return Boolean.parseBoolean(purgeResourcesStr); + } + + private LeaseParams resolveLeaseParams(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering) { + final LeaseParams params = new LeaseParams(); + + params.leaseDuration = resolveDetailParameter(cmd.getLeaseDuration(), sourceOffering.getId(), ApiConstants.INSTANCE_LEASE_DURATION); + + if (cmd.getLeaseExpiryAction() != null) { + params.leaseExpiryAction = cmd.getLeaseExpiryAction(); + } else { + String leaseExpiryActionStr = _serviceOfferingDetailsDao.getDetail(sourceOffering.getId(), ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION); + if (leaseExpiryActionStr != null) { + params.leaseExpiryAction = VMLeaseManager.ExpiryAction.valueOf(leaseExpiryActionStr); + } + } + + params.leaseExpiryAction = validateAndGetLeaseExpiryAction(params.leaseDuration, params.leaseExpiryAction); + return params; + } + + private Map mergeOfferingDetails(CloneServiceOfferingCmd cmd, ServiceOfferingVO sourceOffering, CustomOfferingParams customParams) { + final Map cmdDetails = cmd.getDetails(); + final Map mergedDetails = new HashMap<>(); + + if (cmdDetails == null || cmdDetails.isEmpty()) { + Map sourceDetails = _serviceOfferingDetailsDao.listDetailsKeyPairs(sourceOffering.getId()); + if (sourceDetails != null) { + mergedDetails.putAll(sourceDetails); + } + } else { + mergedDetails.putAll(cmdDetails); + } + + if (customParams.minCPU != null && customParams.maxCPU != null && + customParams.minMemory != null && customParams.maxMemory != null) { + mergedDetails.put(ApiConstants.MIN_MEMORY, customParams.minMemory.toString()); + mergedDetails.put(ApiConstants.MAX_MEMORY, customParams.maxMemory.toString()); + mergedDetails.put(ApiConstants.MIN_CPU_NUMBER, customParams.minCPU.toString()); + mergedDetails.put(ApiConstants.MAX_CPU_NUMBER, customParams.maxCPU.toString()); + } + + return mergedDetails; + } + + private VirtualMachine.Type resolveVmType(ServiceOfferingVO sourceOffering) { + if (sourceOffering.getVmType() == null) { + return null; + } + try { + return VirtualMachine.Type.valueOf(sourceOffering.getVmType()); + } catch (IllegalArgumentException e) { + logger.warn("Invalid VM type in source offering: {}", sourceOffering.getVmType()); + return null; + } + } + + private static class ClonedDiskOfferingParams { + Long rootDiskSize; + Long bytesReadRate; + Long bytesReadRateMax; + Long bytesReadRateMaxLength; + Long bytesWriteRate; + Long bytesWriteRateMax; + Long bytesWriteRateMaxLength; + Long iopsReadRate; + Long iopsReadRateMax; + Long iopsReadRateMaxLength; + Long iopsWriteRate; + Long iopsWriteRateMax; + Long iopsWriteRateMaxLength; + Boolean isCustomizedIops; + Long minIops; + Long maxIops; + Integer hypervisorSnapshotReserve; + String cacheMode; + } + + private static class CustomOfferingParams { + Integer maxCPU; + Integer minCPU; + Integer maxMemory; + Integer minMemory; + Long storagePolicy; + } + + private static class LeaseParams { + Integer leaseDuration; + VMLeaseManager.ExpiryAction leaseExpiryAction; + } + + @Override + public DiskOffering cloneDiskOffering(final CloneDiskOfferingCmd cmd) { + final long userId = CallContext.current().getCallingUserId(); + final DiskOfferingVO sourceOffering = getAndValidateSourceDiskOffering(cmd.getSourceOfferingId()); + final Map requestParams = cmd.getFullUrlParams(); + + final String name = cmd.getOfferingName(); + final String displayText = getOrDefault(cmd.getDisplayText(), sourceOffering.getDisplayText()); + final String provisioningType = getOrDefault(cmd.getProvisioningType(), sourceOffering.getProvisioningType().toString()); + final Long diskSize = getOrDefault(cmd.getDiskSize(), sourceOffering.getDiskSize()); + final String tags = getOrDefault(cmd.getTags(), sourceOffering.getTags()); + + final Boolean isCustomized = resolveBooleanParam(requestParams, ApiConstants.CUSTOMIZED, cmd::isCustomized, sourceOffering.isCustomized()); + final Boolean displayOffering = resolveBooleanParam(requestParams, ApiConstants.DISPLAY_OFFERING, cmd::getDisplayOffering, sourceOffering.getDisplayOffering()); + final Boolean isCustomizedIops = getOrDefault(cmd.isCustomizedIops(), sourceOffering.isCustomizedIops()); + final Boolean diskSizeStrictness = resolveBooleanParam(requestParams, ApiConstants.DISK_SIZE_STRICTNESS, cmd::getDiskSizeStrictness, sourceOffering.getDiskSizeStrictness()); + final Boolean encrypt = resolveBooleanParam(requestParams, ApiConstants.ENCRYPT, cmd::getEncrypt, sourceOffering.getEncrypt()); + + final List domainIds = resolveDomainIdsForDiskOffering(cmd, sourceOffering); + final List zoneIds = resolveZoneIdsForDiskOffering(cmd, sourceOffering); + + final boolean localStorageRequired = resolveLocalStorageRequired(cmd, sourceOffering); + + final ClonedDiskIopsParams iopsParams = resolveDiskIopsParams(cmd, sourceOffering); + + final ClonedDiskRateParams rateParams = resolveDiskRateParams(cmd, sourceOffering); + + final Integer hypervisorSnapshotReserve = getOrDefault(cmd.getHypervisorSnapshotReserve(), sourceOffering.getHypervisorSnapshotReserve()); + final String cacheMode = resolveCacheMode(cmd, sourceOffering); + final Long storagePolicy = resolveStoragePolicyForDiskOffering(cmd, sourceOffering); + + final Map mergedDetails = mergeDiskOfferingDetails(cmd, sourceOffering); + + if (cmd.getCacheMode() != null) { + validateCacheMode(cmd.getCacheMode()); + } + + validateMaxRateEqualsOrGreater(iopsParams.iopsReadRate, iopsParams.iopsReadRateMax, IOPS_READ_RATE); + validateMaxRateEqualsOrGreater(iopsParams.iopsWriteRate, iopsParams.iopsWriteRateMax, IOPS_WRITE_RATE); + validateMaxRateEqualsOrGreater(rateParams.bytesReadRate, rateParams.bytesReadRateMax, BYTES_READ_RATE); + validateMaxRateEqualsOrGreater(rateParams.bytesWriteRate, rateParams.bytesWriteRateMax, BYTES_WRITE_RATE); + validateMaximumIopsAndBytesLength(iopsParams.iopsReadRateMaxLength, iopsParams.iopsWriteRateMaxLength, + rateParams.bytesReadRateMaxLength, rateParams.bytesWriteRateMaxLength); + + return createDiskOffering(userId, domainIds, zoneIds, name, displayText, provisioningType, diskSize, tags, + isCustomized, localStorageRequired, displayOffering, isCustomizedIops, iopsParams.minIops, iopsParams.maxIops, + rateParams.bytesReadRate, rateParams.bytesReadRateMax, rateParams.bytesReadRateMaxLength, + rateParams.bytesWriteRate, rateParams.bytesWriteRateMax, rateParams.bytesWriteRateMaxLength, + iopsParams.iopsReadRate, iopsParams.iopsReadRateMax, iopsParams.iopsReadRateMaxLength, + iopsParams.iopsWriteRate, iopsParams.iopsWriteRateMax, iopsParams.iopsWriteRateMaxLength, + hypervisorSnapshotReserve, cacheMode, mergedDetails, storagePolicy, diskSizeStrictness, encrypt); + } + + private DiskOfferingVO getAndValidateSourceDiskOffering(Long sourceOfferingId) { + final DiskOfferingVO sourceOffering = _diskOfferingDao.findById(sourceOfferingId); + if (sourceOffering == null) { + throw new InvalidParameterValueException("Unable to find disk offering with ID: " + sourceOfferingId); + } + return sourceOffering; + } + + private List resolveDomainIdsForDiskOffering(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + List domainIds = cmd.getDomainIds(); + if (domainIds == null || domainIds.isEmpty()) { + domainIds = diskOfferingDetailsDao.findDomainIds(sourceOffering.getId()); + } + return domainIds; + } + + private List resolveZoneIdsForDiskOffering(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + List zoneIds = cmd.getZoneIds(); + if (zoneIds == null || zoneIds.isEmpty()) { + zoneIds = diskOfferingDetailsDao.findZoneIds(sourceOffering.getId()); + } + return zoneIds; + } + + private boolean resolveLocalStorageRequired(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + if (cmd.getStorageType() != null) { + return ServiceOffering.StorageType.local.toString().equalsIgnoreCase(cmd.getStorageType()); + } + return sourceOffering.isUseLocalStorage(); + } + + private String resolveCacheMode(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + if (cmd.getCacheMode() != null) { + return cmd.getCacheMode(); + } + if (sourceOffering.getCacheMode() != null) { + return sourceOffering.getCacheMode().toString(); + } + return null; + } + + private Long resolveStoragePolicyForDiskOffering(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + Long storagePolicy = cmd.getStoragePolicy(); + if (storagePolicy == null) { + String storagePolicyStr = diskOfferingDetailsDao.getDetail(sourceOffering.getId(), ApiConstants.STORAGE_POLICY); + if (storagePolicyStr != null) { + storagePolicy = Long.parseLong(storagePolicyStr); + } + } + return storagePolicy; + } + + private ClonedDiskIopsParams resolveDiskIopsParams(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + final ClonedDiskIopsParams params = new ClonedDiskIopsParams(); + + params.minIops = getOrDefault(cmd.getMinIops(), sourceOffering.getMinIops()); + params.maxIops = getOrDefault(cmd.getMaxIops(), sourceOffering.getMaxIops()); + params.iopsReadRate = getOrDefault(cmd.getIopsReadRate(), sourceOffering.getIopsReadRate()); + params.iopsReadRateMax = getOrDefault(cmd.getIopsReadRateMax(), sourceOffering.getIopsReadRateMax()); + params.iopsReadRateMaxLength = getOrDefault(cmd.getIopsReadRateMaxLength(), sourceOffering.getIopsReadRateMaxLength()); + params.iopsWriteRate = getOrDefault(cmd.getIopsWriteRate(), sourceOffering.getIopsWriteRate()); + params.iopsWriteRateMax = getOrDefault(cmd.getIopsWriteRateMax(), sourceOffering.getIopsWriteRateMax()); + params.iopsWriteRateMaxLength = getOrDefault(cmd.getIopsWriteRateMaxLength(), sourceOffering.getIopsWriteRateMaxLength()); + + return params; + } + + private ClonedDiskRateParams resolveDiskRateParams(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + final ClonedDiskRateParams params = new ClonedDiskRateParams(); + + params.bytesReadRate = getOrDefault(cmd.getBytesReadRate(), sourceOffering.getBytesReadRate()); + params.bytesReadRateMax = getOrDefault(cmd.getBytesReadRateMax(), sourceOffering.getBytesReadRateMax()); + params.bytesReadRateMaxLength = getOrDefault(cmd.getBytesReadRateMaxLength(), sourceOffering.getBytesReadRateMaxLength()); + params.bytesWriteRate = getOrDefault(cmd.getBytesWriteRate(), sourceOffering.getBytesWriteRate()); + params.bytesWriteRateMax = getOrDefault(cmd.getBytesWriteRateMax(), sourceOffering.getBytesWriteRateMax()); + params.bytesWriteRateMaxLength = getOrDefault(cmd.getBytesWriteRateMaxLength(), sourceOffering.getBytesWriteRateMaxLength()); + + return params; + } + + private Map mergeDiskOfferingDetails(CloneDiskOfferingCmd cmd, DiskOfferingVO sourceOffering) { + final Map cmdDetails = cmd.getDetails(); + final Map mergedDetails = new HashMap<>(); + + if (cmdDetails == null || cmdDetails.isEmpty()) { + Map sourceDetails = diskOfferingDetailsDao.listDetailsKeyPairs(sourceOffering.getId()); + if (sourceDetails != null) { + mergedDetails.putAll(sourceDetails); + } + } else { + mergedDetails.putAll(cmdDetails); + } + + return mergedDetails; + } + + // Helper classes for disk offering parameters + private static class ClonedDiskIopsParams { + Long minIops; + Long maxIops; + Long iopsReadRate; + Long iopsReadRateMax; + Long iopsReadRateMaxLength; + Long iopsWriteRate; + Long iopsWriteRateMax; + Long iopsWriteRateMaxLength; + } + + private static class ClonedDiskRateParams { + Long bytesReadRate; + Long bytesReadRateMax; + Long bytesReadRateMaxLength; + Long bytesWriteRate; + Long bytesWriteRateMax; + Long bytesWriteRateMaxLength; + } + @Override @ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_EDIT, eventDescription = "updating service offering") public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) { @@ -7809,6 +8264,212 @@ public boolean deleteNetworkOffering(final DeleteNetworkOfferingCmd cmd) { } } + @Override + @ActionEvent(eventType = EventTypes.EVENT_NETWORK_OFFERING_CREATE, eventDescription = "cloning network offering") + public NetworkOffering cloneNetworkOffering(final CloneNetworkOfferingCmd cmd) { + final Long sourceOfferingId = cmd.getSourceOfferingId(); + + final NetworkOfferingVO sourceOffering = _networkOfferingDao.findById(sourceOfferingId); + if (sourceOffering == null) { + throw new InvalidParameterValueException("Unable to find network offering with id " + sourceOfferingId); + } + + String name = cmd.getNetworkOfferingName(); + if (name == null || name.isEmpty()) { + throw new InvalidParameterValueException("Name is required when cloning a network offering"); + } + + NetworkOfferingVO existing = _networkOfferingDao.findByUniqueName(name); + if (existing != null) { + throw new InvalidParameterValueException("Network offering with name '" + name + "' already exists"); + } + + logger.info("Cloning network offering {} (id: {}) to new offering with name: {}", + sourceOffering.getName(), sourceOfferingId, name); + + // Resolve parameters from source offering and apply add/drop logic + applySourceOfferingValuesToCloneCmd(cmd, sourceOffering); + + return createNetworkOffering(cmd); + } + + private void applySourceOfferingValuesToCloneCmd(CloneNetworkOfferingCmd cmd, NetworkOfferingVO sourceOffering) { + Long sourceOfferingId = sourceOffering.getId(); + + Map> sourceServiceProviderMap = + _networkModel.getNetworkOfferingServiceProvidersMap(sourceOfferingId); + + // Build final services list with add/drop support + List finalServices = resolveFinalServicesList(cmd, sourceServiceProviderMap); + + Map finalServiceProviderMap = resolveServiceProviderMap(cmd, sourceServiceProviderMap, finalServices); + + Map sourceDetailsMap = getSourceOfferingDetails(sourceOfferingId); + + List sourceDomainIds = networkOfferingDetailsDao.findDomainIds(sourceOfferingId); + List sourceZoneIds = networkOfferingDetailsDao.findZoneIds(sourceOfferingId); + + applyResolvedValuesToCommand(cmd, sourceOffering, finalServices, finalServiceProviderMap, + sourceDetailsMap, sourceDomainIds, sourceZoneIds); + } + + private Map getSourceOfferingDetails(Long sourceOfferingId) { + List sourceDetailsVOs = networkOfferingDetailsDao.listDetails(sourceOfferingId); + Map sourceDetailsMap = new HashMap<>(); + for (NetworkOfferingDetailsVO detailVO : sourceDetailsVOs) { + sourceDetailsMap.put(detailVO.getName(), detailVO.getValue()); + } + return sourceDetailsMap; + } + + private List resolveFinalServicesList(CloneNetworkOfferingCmd cmd, + Map> sourceServiceProviderMap) { + + List cmdServices = cmd.getSupportedServices(); + List addServices = cmd.getAddServices(); + List dropServices = cmd.getDropServices(); + + if (cmdServices != null && !cmdServices.isEmpty()) { + return cmdServices; + } + + List finalServices = new ArrayList<>(); + for (Network.Service service : sourceServiceProviderMap.keySet()) { + finalServices.add(service.getName()); + } + + if (dropServices != null && !dropServices.isEmpty()) { + finalServices.removeAll(dropServices); + logger.debug("Dropped services from clone: {}", dropServices); + } + + if (addServices != null && !addServices.isEmpty()) { + for (String service : addServices) { + if (!finalServices.contains(service)) { + finalServices.add(service); + } + } + logger.debug("Added services to clone: {}", addServices); + } + + return finalServices; + } + + private Map> resolveServiceProviderMap(CloneNetworkOfferingCmd cmd, + Map> sourceServiceProviderMap, List finalServices) { + + if (cmd.getServiceProviders() != null && !cmd.getServiceProviders().isEmpty()) { + return cmd.getServiceProviders(); + } + + Map> finalMap = new HashMap<>(); + for (Map.Entry> entry : sourceServiceProviderMap.entrySet()) { + String serviceName = entry.getKey().getName(); + if (finalServices.contains(serviceName)) { + List providers = new ArrayList<>(); + for (Network.Provider provider : entry.getValue()) { + providers.add(provider.getName()); + } + finalMap.put(serviceName, providers); + } + } + + return finalMap; + } + + private void applyResolvedValuesToCommand(CloneNetworkOfferingCmd cmd, NetworkOfferingVO sourceOffering, + List finalServices, Map finalServiceProviderMap, Map sourceDetailsMap, + List sourceDomainIds, List sourceZoneIds) { + + try { + Map requestParams = cmd.getFullUrlParams(); + + if (cmd.getSupportedServices() == null || cmd.getSupportedServices().isEmpty()) { + setField(cmd, "supportedServices", finalServices); + } + if (cmd.getServiceProviders() == null || cmd.getServiceProviders().isEmpty()) { + setField(cmd, "serviceProviderList", finalServiceProviderMap); + } + + + applyIfNotProvided(cmd, requestParams, "displayText", ApiConstants.DISPLAY_TEXT, cmd.getDisplayText(), sourceOffering.getDisplayText()); + applyIfNotProvided(cmd, requestParams, "traffictype", ApiConstants.TRAFFIC_TYPE, cmd.getTraffictype(), sourceOffering.getTrafficType().toString()); + applyIfNotProvided(cmd, requestParams, "tags", ApiConstants.TAGS, cmd.getTags(), sourceOffering.getTags()); + applyIfNotProvided(cmd, requestParams, "availability", ApiConstants.AVAILABILITY, cmd.getAvailability(), sourceOffering.getAvailability().toString()); + applyIfNotProvided(cmd, requestParams, "networkRate", ApiConstants.NETWORKRATE, cmd.getNetworkRate(), sourceOffering.getRateMbps()); + applyIfNotProvided(cmd, requestParams, "serviceOfferingId", ApiConstants.SERVICE_OFFERING_ID, cmd.getServiceOfferingId(), sourceOffering.getServiceOfferingId()); + applyIfNotProvided(cmd, requestParams, "guestIptype", ApiConstants.GUEST_IP_TYPE, cmd.getGuestIpType(), sourceOffering.getGuestType().toString()); + applyIfNotProvided(cmd, requestParams, "maxConnections", ApiConstants.MAX_CONNECTIONS, cmd.getMaxconnections(), sourceOffering.getConcurrentConnections()); + + applyBooleanIfNotProvided(cmd, requestParams, "specifyVlan", ApiConstants.SPECIFY_VLAN, sourceOffering.isSpecifyVlan()); + applyBooleanIfNotProvided(cmd, requestParams, "conserveMode", ApiConstants.CONSERVE_MODE, sourceOffering.isConserveMode()); + applyBooleanIfNotProvided(cmd, requestParams, "specifyIpRanges", ApiConstants.SPECIFY_IP_RANGES, sourceOffering.isSpecifyIpRanges()); + applyBooleanIfNotProvided(cmd, requestParams, "isPersistent", ApiConstants.IS_PERSISTENT, sourceOffering.isPersistent()); + applyBooleanIfNotProvided(cmd, requestParams, "forVpc", ApiConstants.FOR_VPC, sourceOffering.isForVpc()); + applyBooleanIfNotProvided(cmd, requestParams, "egressDefaultPolicy", ApiConstants.EGRESS_DEFAULT_POLICY, sourceOffering.isEgressDefaultPolicy()); + applyBooleanIfNotProvided(cmd, requestParams, "keepAliveEnabled", ApiConstants.KEEPALIVE_ENABLED, sourceOffering.isKeepAliveEnabled()); + applyBooleanIfNotProvided(cmd, requestParams, "enable", ApiConstants.ENABLE, sourceOffering.getState() == NetworkOffering.State.Enabled); + applyBooleanIfNotProvided(cmd, requestParams, "specifyAsNumber", ApiConstants.SPECIFY_AS_NUMBER, sourceOffering.isSpecifyAsNumber()); + + if (!requestParams.containsKey(ApiConstants.INTERNET_PROTOCOL)) { + String internetProtocol = networkOfferingDetailsDao.getDetail(sourceOffering.getId(), Detail.internetProtocol); + if (internetProtocol != null) { + setField(cmd, "internetProtocol", internetProtocol); + } + } + + if (!requestParams.containsKey(ApiConstants.NETWORK_MODE) && sourceOffering.getNetworkMode() != null) { + setField(cmd, "networkMode", sourceOffering.getNetworkMode().toString()); + } + + if (!requestParams.containsKey(ApiConstants.ROUTING_MODE) && sourceOffering.getRoutingMode() != null) { + setField(cmd, "routingMode", sourceOffering.getRoutingMode().toString()); + } + + if (cmd.getDetails() == null || cmd.getDetails().isEmpty()) { + if (!sourceDetailsMap.isEmpty()) { + setField(cmd, "details", sourceDetailsMap); + } + } + + if (cmd.getDomainIds() == null || cmd.getDomainIds().isEmpty()) { + if (sourceDomainIds != null && !sourceDomainIds.isEmpty()) { + setField(cmd, "domainIds", sourceDomainIds); + } + } + if (cmd.getZoneIds() == null || cmd.getZoneIds().isEmpty()) { + if (sourceZoneIds != null && !sourceZoneIds.isEmpty()) { + setField(cmd, "zoneIds", sourceZoneIds); + } + } + + } catch (Exception e) { + logger.warn("Failed to apply some source offering parameters during clone: {}", e.getMessage()); + } + } + + private void applyIfNotProvided(Object cmd, Map requestParams, String fieldName, + String apiConstant, Object currentValue, Object sourceValue) throws Exception { + // If parameter was not provided in request and source has a value, use source value + if (!requestParams.containsKey(apiConstant) && sourceValue != null) { + setField(cmd, fieldName, sourceValue); + } + // If parameter WAS provided in request, the framework already set it correctly + } + + private void applyBooleanIfNotProvided(Object cmd, Map requestParams, + String fieldName, String apiConstant, Boolean sourceValue) throws Exception { + if (!requestParams.containsKey(apiConstant) && sourceValue != null) { + setField(cmd, fieldName, sourceValue); + } + } + + private void setField(Object obj, String fieldName, Object value) throws Exception { + java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(obj, value); + } + @Override @ActionEvent(eventType = EventTypes.EVENT_NETWORK_OFFERING_EDIT, eventDescription = "updating network offering") public NetworkOffering updateNetworkOffering(final UpdateNetworkOfferingCmd cmd) { diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 47dcf60eb32d..6909577f5a6a 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -130,6 +130,7 @@ import org.apache.cloudstack.api.command.admin.management.RemoveManagementServerCmd; import org.apache.cloudstack.api.command.admin.network.AddNetworkDeviceCmd; import org.apache.cloudstack.api.command.admin.network.AddNetworkServiceProviderCmd; +import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.CreateNetworkCmdByAdmin; import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; @@ -160,6 +161,8 @@ import org.apache.cloudstack.api.command.admin.network.UpdatePhysicalNetworkCmd; import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.UpdateStorageNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -3840,6 +3843,7 @@ public List> getCommands() { cmdList.add(AddNetworkDeviceCmd.class); cmdList.add(AddNetworkServiceProviderCmd.class); cmdList.add(CreateNetworkOfferingCmd.class); + cmdList.add(CloneNetworkOfferingCmd.class); cmdList.add(CreatePhysicalNetworkCmd.class); cmdList.add(CreateStorageNetworkIpRangeCmd.class); cmdList.add(DeleteNetworkDeviceCmd.class); @@ -3860,7 +3864,9 @@ public List> getCommands() { cmdList.add(ListDedicatedGuestVlanRangesCmd.class); cmdList.add(ReleaseDedicatedGuestVlanRangeCmd.class); cmdList.add(CreateDiskOfferingCmd.class); + cmdList.add(CloneDiskOfferingCmd.class); cmdList.add(CreateServiceOfferingCmd.class); + cmdList.add(CloneServiceOfferingCmd.class); cmdList.add(DeleteDiskOfferingCmd.class); cmdList.add(DeleteServiceOfferingCmd.class); cmdList.add(IsAccountAllowedToCreateOfferingsWithTagsCmd.class); diff --git a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java index 2982c19ccdd4..dc587c6cb197 100644 --- a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java @@ -51,6 +51,7 @@ import com.cloud.utils.net.NetUtils; import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; +import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd; import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; @@ -60,6 +61,8 @@ import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -117,6 +120,24 @@ public ServiceOffering createServiceOffering(CreateServiceOfferingCmd cmd) { return null; } + @Override + public ServiceOffering cloneServiceOffering(CloneServiceOfferingCmd cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public DiskOffering cloneDiskOffering(CloneDiskOfferingCmd cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public NetworkOffering cloneNetworkOffering(CloneNetworkOfferingCmd cmd) { + // TODO Auto-generated method stub + return null; + } + /* (non-Javadoc) * @see com.cloud.configuration.ConfigurationService#updateServiceOffering(org.apache.cloudstack.api.commands.UpdateServiceOfferingCmd) */