This is an automated email from the ASF dual-hosted git repository.

pearl11594 pushed a commit to branch clone-edit-existing-offerings
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 2afc1b74562515a1730344003ad043771701c5da
Author: Pearl Dsilva <[email protected]>
AuthorDate: Wed Dec 31 11:18:01 2025 -0500

    Add support to clone existing offerings and update them
---
 .../cloud/configuration/ConfigurationService.java  |  30 +
 api/src/main/java/com/cloud/event/EventTypes.java  |   1 +
 .../org/apache/cloudstack/api/ApiConstants.java    |   1 +
 .../admin/network/CloneNetworkOfferingCmd.java     |  97 +++
 .../admin/offering/CloneDiskOfferingCmd.java       |  72 +++
 .../admin/offering/CloneServiceOfferingCmd.java    |  72 +++
 .../configuration/ConfigurationManagerImpl.java    | 661 +++++++++++++++++++++
 .../com/cloud/server/ManagementServerImpl.java     |   6 +
 .../cloud/vpc/MockConfigurationManagerImpl.java    |  21 +
 9 files changed, 961 insertions(+)

diff --git 
a/api/src/main/java/com/cloud/configuration/ConfigurationService.java 
b/api/src/main/java/com/cloud/configuration/ConfigurationService.java
index 438283136d2..1bf3cc19162 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 com.cloud.network.Network;
 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.DeleteNetworkOfferingCmd;
 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 38e601c790a..e3ee2720f59 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 8fca652518f..67d2d57eb3b 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 00000000000..f20fb1f4c6a
--- /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<String> 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<String> dropServices;
+
+    /////////////////////////////////////////////////////
+    /////////////////// Accessors ///////////////////////
+    /////////////////////////////////////////////////////
+
+    public Long getSourceOfferingId() {
+        return sourceOfferingId;
+    }
+
+    public List<String> getAddServices() {
+        return addServices;
+    }
+
+    public List<String> 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 00000000000..7e576d1b3b2
--- /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 00000000000..2515b873e3e
--- /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 21995d5ae65..3a47b2c47b8 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.ApiCommandResourceType;
 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.DeleteNetworkOfferingCmd;
 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 @@ public class ConfigurationManagerImpl extends 
ManagerBase implements Configurati
         return false;
     }
 
+    @Override
+    @ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_CLONE, 
eventDescription = "cloning service offering")
+    public ServiceOffering cloneServiceOffering(final CloneServiceOfferingCmd 
cmd) {
+        final long userId = CallContext.current().getCallingUserId();
+        final ServiceOfferingVO sourceOffering = 
getAndValidateSourceOffering(cmd.getSourceOfferingId());
+        final DiskOfferingVO sourceDiskOffering = 
getSourceDiskOffering(sourceOffering);
+        final Map<String, String> 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<Long> domainIds = resolveDomainIds(cmd, sourceOffering);
+        final List<Long> 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<String, String> 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> T getOrDefault(T cmdValue, T defaultValue) {
+        return cmdValue != null ? cmdValue : defaultValue;
+    }
+
+    private Boolean resolveBooleanParam(Map<String, String> requestParams, 
String paramKey,
+                                       java.util.function.Supplier<Boolean> 
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<Long> resolveDomainIds(CloneServiceOfferingCmd cmd, 
ServiceOfferingVO sourceOffering) {
+        List<Long> domainIds = cmd.getDomainIds();
+        if (domainIds == null || domainIds.isEmpty()) {
+            domainIds = 
_serviceOfferingDetailsDao.findDomainIds(sourceOffering.getId());
+        }
+        return domainIds;
+    }
+
+    private List<Long> resolveZoneIds(CloneServiceOfferingCmd cmd, 
ServiceOfferingVO sourceOffering) {
+        List<Long> 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<String, String> 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<String, String> mergeOfferingDetails(CloneServiceOfferingCmd 
cmd, ServiceOfferingVO sourceOffering, CustomOfferingParams customParams) {
+        final Map<String, String> cmdDetails = cmd.getDetails();
+        final Map<String, String> mergedDetails = new HashMap<>();
+
+        if (cmdDetails == null || cmdDetails.isEmpty()) {
+            Map<String, String> 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<String, String> 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<Long> domainIds = resolveDomainIdsForDiskOffering(cmd, 
sourceOffering);
+        final List<Long> 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<String, String> 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<Long> resolveDomainIdsForDiskOffering(CloneDiskOfferingCmd 
cmd, DiskOfferingVO sourceOffering) {
+        List<Long> domainIds = cmd.getDomainIds();
+        if (domainIds == null || domainIds.isEmpty()) {
+            domainIds = 
diskOfferingDetailsDao.findDomainIds(sourceOffering.getId());
+        }
+        return domainIds;
+    }
+
+    private List<Long> resolveZoneIdsForDiskOffering(CloneDiskOfferingCmd cmd, 
DiskOfferingVO sourceOffering) {
+        List<Long> 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<String, String> mergeDiskOfferingDetails(CloneDiskOfferingCmd 
cmd, DiskOfferingVO sourceOffering) {
+        final Map<String, String> cmdDetails = cmd.getDetails();
+        final Map<String, String> mergedDetails = new HashMap<>();
+
+        if (cmdDetails == null || cmdDetails.isEmpty()) {
+            Map<String, String> 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 class ConfigurationManagerImpl extends 
ManagerBase implements Configurati
         }
     }
 
+    @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<Network.Service, Set<Network.Provider>> sourceServiceProviderMap =
+            
_networkModel.getNetworkOfferingServiceProvidersMap(sourceOfferingId);
+
+        // Build final services list with add/drop support
+        List<String> finalServices = resolveFinalServicesList(cmd, 
sourceServiceProviderMap);
+
+        Map finalServiceProviderMap = resolveServiceProviderMap(cmd, 
sourceServiceProviderMap, finalServices);
+
+        Map<String, String> sourceDetailsMap = 
getSourceOfferingDetails(sourceOfferingId);
+
+        List<Long> sourceDomainIds = 
networkOfferingDetailsDao.findDomainIds(sourceOfferingId);
+        List<Long> sourceZoneIds = 
networkOfferingDetailsDao.findZoneIds(sourceOfferingId);
+
+        applyResolvedValuesToCommand(cmd, sourceOffering, finalServices, 
finalServiceProviderMap,
+            sourceDetailsMap, sourceDomainIds, sourceZoneIds);
+    }
+
+    private Map<String, String> getSourceOfferingDetails(Long 
sourceOfferingId) {
+        List<NetworkOfferingDetailsVO> sourceDetailsVOs = 
networkOfferingDetailsDao.listDetails(sourceOfferingId);
+        Map<String, String> sourceDetailsMap = new HashMap<>();
+        for (NetworkOfferingDetailsVO detailVO : sourceDetailsVOs) {
+            sourceDetailsMap.put(detailVO.getName(), detailVO.getValue());
+        }
+        return sourceDetailsMap;
+    }
+
+    private List<String> resolveFinalServicesList(CloneNetworkOfferingCmd cmd,
+            Map<Network.Service, Set<Network.Provider>> 
sourceServiceProviderMap) {
+
+        List<String> cmdServices = cmd.getSupportedServices();
+        List<String> addServices = cmd.getAddServices();
+        List<String> dropServices = cmd.getDropServices();
+
+        if (cmdServices != null && !cmdServices.isEmpty()) {
+            return cmdServices;
+        }
+
+        List<String> 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<String, List<String>> 
resolveServiceProviderMap(CloneNetworkOfferingCmd cmd,
+            Map<Network.Service, Set<Network.Provider>> 
sourceServiceProviderMap, List<String> finalServices) {
+
+        if (cmd.getServiceProviders() != null && 
!cmd.getServiceProviders().isEmpty()) {
+            return cmd.getServiceProviders();
+        }
+
+        Map<String, List<String>> finalMap = new HashMap<>();
+        for (Map.Entry<Network.Service, Set<Network.Provider>> entry : 
sourceServiceProviderMap.entrySet()) {
+            String serviceName = entry.getKey().getName();
+            if (finalServices.contains(serviceName)) {
+                List<String> 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<String> finalServices, Map finalServiceProviderMap, 
Map<String, String> sourceDetailsMap,
+            List<Long> sourceDomainIds, List<Long> sourceZoneIds) {
+
+        try {
+            Map<String, String> 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<String, String> 
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<String, String> 
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 47dcf60eb32..6909577f5a6 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.ListMgmtsCmd;
 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.UpdateNetworkServiceProvi
 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 class ManagementServerImpl extends ManagerBase 
implements ManagementServe
         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 class ManagementServerImpl extends ManagerBase 
implements ManagementServe
         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 2982c19ccdd..dc587c6cb19 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.component.ManagerBase;
 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.DeleteNetworkOfferingCmd;
 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 class MockConfigurationManagerImpl extends 
ManagerBase implements Configu
         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)
      */

Reply via email to