From afe6c869909459211314df234535655843fe7083 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 6 Jan 2026 11:14:27 -0500 Subject: [PATCH] add support for vpc & backup offerings to be cloned --- .../main/java/com/cloud/event/EventTypes.java | 1 + .../network/vpc/VpcProvisioningService.java | 3 + .../admin/backup/CloneBackupOfferingCmd.java | 99 +++++++++ .../admin/backup/ImportBackupOfferingCmd.java | 2 +- .../network/CloneNetworkOfferingCmd.java | 19 ++ .../network/CreateNetworkOfferingCmd.java | 6 +- .../admin/vpc/CloneVPCOfferingCmd.java | 95 ++++++++ .../cloudstack/backup/BackupManager.java | 7 + .../ConfigurationManagerImpl.java | 123 ++++++++++- .../com/cloud/network/vpc/VpcManagerImpl.java | 208 ++++++++++++++++++ .../cloud/server/ManagementServerImpl.java | 2 + .../cloudstack/backup/BackupManagerImpl.java | 50 +++++ 12 files changed, 604 insertions(+), 11 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 4ad0b2726fc..389fcc1da5c 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -631,6 +631,7 @@ public class EventTypes { // Backup and Recovery events public static final String EVENT_VM_BACKUP_IMPORT_OFFERING = "BACKUP.IMPORT.OFFERING"; + public static final String EVENT_VM_BACKUP_CLONE_OFFERING = "BACKUP.CLONE.OFFERING"; public static final String EVENT_VM_BACKUP_OFFERING_ASSIGN = "BACKUP.OFFERING.ASSIGN"; public static final String EVENT_VM_BACKUP_OFFERING_REMOVE = "BACKUP.OFFERING.REMOVE"; public static final String EVENT_VM_BACKUP_CREATE = "BACKUP.CREATE"; diff --git a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java index 97b95339ecf..2988a94f5fe 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java @@ -20,6 +20,7 @@ package com.cloud.network.vpc; import java.util.List; import java.util.Map; +import org.apache.cloudstack.api.command.admin.vpc.CloneVPCOfferingCmd; import org.apache.cloudstack.api.command.admin.vpc.CreateVPCOfferingCmd; import org.apache.cloudstack.api.command.admin.vpc.UpdateVPCOfferingCmd; import org.apache.cloudstack.api.command.user.vpc.ListVPCOfferingsCmd; @@ -34,6 +35,8 @@ public interface VpcProvisioningService { VpcOffering createVpcOffering(CreateVPCOfferingCmd cmd); + VpcOffering cloneVPCOffering(CloneVPCOfferingCmd cmd); + VpcOffering createVpcOffering(String name, String displayText, List supportedServices, Map> serviceProviders, Map serviceCapabilitystList, NetUtils.InternetProtocol internetProtocol, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java new file mode 100644 index 00000000000..9900b2f83d5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java @@ -0,0 +1,99 @@ +// 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.backup; + +import org.apache.cloudstack.acl.RoleType; +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.BackupOfferingResponse; +import org.apache.cloudstack.backup.BackupOffering; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "cloneBackupOffering", + description = "Clones an existing backup offering with updated values. " + + "All parameters are copied from the source offering unless explicitly overridden.", + responseObject = BackupOfferingResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class CloneBackupOfferingCmd extends ImportBackupOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupOfferingResponse.class, + required = true, + description = "The ID of the backup offering to clone") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, + ServerApiException, ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + try { + BackupOffering clonedOffering = backupManager.cloneBackupOffering(this); + if (clonedOffering != null) { + BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(clonedOffering); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone backup offering"); + } + } catch (InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_CLONE_OFFERING; + } + + @Override + public String getEventDescription() { + return "Cloning backup offering from ID: " + id + " to new offering: " + getName(); + } +} + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java index f852f7e2577..bc9a99f14c7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java @@ -54,7 +54,7 @@ import java.util.Set; public class ImportBackupOfferingCmd extends BaseAsyncCmd { @Inject - private BackupManager backupManager; + protected BackupManager backupManager; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// 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 index f20fb1f4c6a..da583674ab5 100644 --- 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 @@ -62,6 +62,7 @@ public class CloneNetworkOfferingCmd extends CreateNetworkOfferingCmd { "If specified along with 'supportedservices', this parameter is ignored.") private List dropServices; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -78,6 +79,24 @@ public class CloneNetworkOfferingCmd extends CreateNetworkOfferingCmd { return dropServices; } + /** + * Override to provide placeholder values that will be replaced with source offering values. + * This allows API validation to pass even though these are marked as required in the parent class. + */ + @Override + public String getGuestIpType() { + String value = super.getGuestIpType(); + // Return placeholder if not provided - will be overwritten from source offering + return value != null ? value : "Isolated"; + } + + @Override + public String getTraffictype() { + String value = super.getTraffictype(); + // Return placeholder if not provided - will be overwritten from source offering + return value != null ? value : "Guest"; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java index a0559f57dab..549929a1994 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java @@ -128,7 +128,7 @@ public class CreateNetworkOfferingCmd extends BaseCmd { private Map serviceProviderList; @Parameter(name = ApiConstants.SERVICE_CAPABILITY_LIST, type = CommandType.MAP, description = "Desired service capabilities as part of network offering") - private Map serviceCapabilitystList; + private Map serviceCapabilitiesList; @Parameter(name = ApiConstants.SPECIFY_IP_RANGES, type = CommandType.BOOLEAN, @@ -423,9 +423,9 @@ public class CreateNetworkOfferingCmd extends BaseCmd { public Map getServiceCapabilities(Service service) { Map capabilityMap = null; - if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) { + if (serviceCapabilitiesList != null && !serviceCapabilitiesList.isEmpty()) { capabilityMap = new HashMap(); - Collection serviceCapabilityCollection = serviceCapabilitystList.values(); + Collection serviceCapabilityCollection = serviceCapabilitiesList.values(); Iterator iter = serviceCapabilityCollection.iterator(); while (iter.hasNext()) { HashMap svcCapabilityMap = (HashMap) iter.next(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java new file mode 100644 index 00000000000..284f60bf4e8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java @@ -0,0 +1,95 @@ +// 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.vpc; + +import com.cloud.network.vpc.VpcOffering; +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.VpcOfferingResponse; + +import java.util.List; + +@APICommand(name = "cloneVPCOffering", + description = "Clones an existing VPC 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 = VpcOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class CloneVPCOfferingCmd extends CreateVPCOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = VpcOfferingResponse.class, + required = true, + description = "The ID of the VPC 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() { + VpcOffering result = _vpcProvSvc.cloneVPCOffering(this); + if (result != null) { + VpcOfferingResponse response = _responseGenerator.createVpcOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone VPC offering"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index e83db3a2589..6c0121a3e4d 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -22,6 +22,7 @@ import java.util.Map; import com.cloud.capacity.Capacity; import com.cloud.exception.ResourceAllocationException; +import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd; @@ -140,6 +141,12 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer List getBackupOfferingDomains(final Long offeringId); + /** + * Clone an existing backup offering with updated values + * @param cmd clone backup offering cmd + */ + BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd); + /** * List backup offerings * @param ListBackupOfferingsCmd API cmd diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index d323df798fb..055244a6a3c 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -8322,13 +8322,16 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati Map finalServiceProviderMap = resolveServiceProviderMap(cmd, sourceServiceProviderMap, finalServices); + // Reconstruct service capability list from source offering + Map sourceServiceCapabilityList = reconstructNetworkServiceCapabilityList(sourceOffering); + Map sourceDetailsMap = getSourceOfferingDetails(sourceOfferingId); List sourceDomainIds = networkOfferingDetailsDao.findDomainIds(sourceOfferingId); List sourceZoneIds = networkOfferingDetailsDao.findZoneIds(sourceOfferingId); applyResolvedValuesToCommand(cmd, sourceOffering, finalServices, finalServiceProviderMap, - sourceDetailsMap, sourceDomainIds, sourceZoneIds); + sourceServiceCapabilityList, sourceDetailsMap, sourceDomainIds, sourceZoneIds); } private Map getSourceOfferingDetails(Long sourceOfferingId) { @@ -8396,8 +8399,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati } private void applyResolvedValuesToCommand(CloneNetworkOfferingCmd cmd, NetworkOfferingVO sourceOffering, - List finalServices, Map finalServiceProviderMap, Map sourceDetailsMap, - List sourceDomainIds, List sourceZoneIds) { + List finalServices, Map finalServiceProviderMap, Map sourceServiceCapabilityList, + Map sourceDetailsMap, List sourceDomainIds, List sourceZoneIds) { try { Map requestParams = cmd.getFullUrlParams(); @@ -8409,6 +8412,14 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati setField(cmd, "serviceProviderList", finalServiceProviderMap); } + // Apply service capability list if not provided via request parameters + // Check if any servicecapabilitylist parameters were passed (e.g., servicecapabilitylist[0].service) + boolean hasCapabilityParams = requestParams.keySet().stream() + .anyMatch(key -> key.startsWith(ApiConstants.SERVICE_CAPABILITY_LIST)); + + if (!hasCapabilityParams && sourceServiceCapabilityList != null && !sourceServiceCapabilityList.isEmpty()) { + setField(cmd, "serviceCapabilitystList", sourceServiceCapabilityList); + } applyIfNotProvided(cmd, requestParams, "displayText", ApiConstants.DISPLAY_TEXT, cmd.getDisplayText(), sourceOffering.getDisplayText()); applyIfNotProvided(cmd, requestParams, "traffictype", ApiConstants.TRAFFIC_TYPE, cmd.getTraffictype(), sourceOffering.getTrafficType().toString()); @@ -8466,7 +8477,90 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati } } - private void applyIfNotProvided(Object cmd, Map requestParams, String fieldName, + /** + * Reconstructs the service capability list from the source network offering's stored capability flags. + * These capabilities were originally passed during creation and stored as boolean flags in the offering. + * + * Returns a Map in the format expected by CreateNetworkOfferingCmd.serviceCapabilitystList: + * Map with keys like "0.service", "0.capabilitytype", "0.capabilityvalue" + */ + private Map reconstructNetworkServiceCapabilityList(NetworkOfferingVO sourceOffering) { + Map capabilityList = new HashMap<>(); + int index = 0; + + // LB service capabilities + if (sourceOffering.isDedicatedLB()) { + capabilityList.put(index + ".service", Network.Service.Lb.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.SupportedLBIsolation.getName()); + capabilityList.put(index + ".capabilityvalue", "dedicated"); + index++; + } + if (sourceOffering.isElasticLb()) { + capabilityList.put(index + ".service", Network.Service.Lb.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.ElasticLb.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + } + if (sourceOffering.isInline()) { + capabilityList.put(index + ".service", Network.Service.Lb.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.InlineMode.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + } + if (sourceOffering.isPublicLb() || sourceOffering.isInternalLb()) { + List schemes = new ArrayList<>(); + if (sourceOffering.isPublicLb()) schemes.add("public"); + if (sourceOffering.isInternalLb()) schemes.add("internal"); + capabilityList.put(index + ".service", Network.Service.Lb.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.LbSchemes.getName()); + capabilityList.put(index + ".capabilityvalue", String.join(",", schemes)); + index++; + } + if (sourceOffering.isSupportsVmAutoScaling()) { + capabilityList.put(index + ".service", Network.Service.Lb.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.VmAutoScaling.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + } + + // SourceNat service capabilities + if (sourceOffering.isSharedSourceNat() || sourceOffering.isRedundantRouter()) { + capabilityList.put(index + ".service", Network.Service.SourceNat.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.SupportedSourceNatTypes.getName()); + capabilityList.put(index + ".capabilityvalue", sourceOffering.isSharedSourceNat() ? "perzone" : "peraccount"); + index++; + } + if (sourceOffering.isRedundantRouter()) { + capabilityList.put(index + ".service", Network.Service.SourceNat.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.RedundantRouter.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + + // Also add to Gateway service + capabilityList.put(index + ".service", Network.Service.Gateway.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.RedundantRouter.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + } + + // StaticNat service capabilities + if (sourceOffering.isElasticIp()) { + capabilityList.put(index + ".service", Network.Service.StaticNat.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.ElasticIp.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + } + if (sourceOffering.isAssociatePublicIP()) { + capabilityList.put(index + ".service", Network.Service.StaticNat.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.AssociatePublicIP.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + } + + return capabilityList; + } + + public static 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) { @@ -8475,19 +8569,34 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati // If parameter WAS provided in request, the framework already set it correctly } - private void applyBooleanIfNotProvided(Object cmd, Map requestParams, + public static 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); + public static void setField(Object obj, String fieldName, Object value) throws Exception { + java.lang.reflect.Field field = findField(obj.getClass(), fieldName); + if (field == null) { + throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy of " + obj.getClass().getName()); + } field.setAccessible(true); field.set(obj, value); } + public static java.lang.reflect.Field findField(Class clazz, String fieldName) { + Class currentClass = clazz; + while (currentClass != null) { + try { + return currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + currentClass = currentClass.getSuperclass(); + } + } + return null; + } + @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/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index 86d1fba038b..527a5cdfc29 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -71,6 +71,7 @@ import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.admin.vpc.CloneVPCOfferingCmd; import org.apache.cloudstack.api.command.admin.vpc.CreatePrivateGatewayByAdminCmd; import org.apache.cloudstack.api.command.admin.vpc.CreateVPCCmdByAdmin; import org.apache.cloudstack.api.command.admin.vpc.CreateVPCOfferingCmd; @@ -811,6 +812,213 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis } } + @Override + public VpcOffering cloneVPCOffering(CloneVPCOfferingCmd cmd) { + Long sourceVpcOfferingId = cmd.getSourceOfferingId(); + + final VpcOffering sourceVpcOffering = _vpcOffDao.findById(sourceVpcOfferingId); + if (sourceVpcOffering == null) { + throw new InvalidParameterValueException("Unable to find source VPC offering by id " + sourceVpcOfferingId); + } + + String name = cmd.getVpcOfferingName(); + if (name == null || name.isEmpty()) { + throw new InvalidParameterValueException("Name is required when cloning a VPC offering"); + } + + VpcOfferingVO vpcOfferingVO = _vpcOffDao.findByUniqueName(name); + if (vpcOfferingVO != null) { + throw new InvalidParameterValueException(String.format("A VPC offering with name %s already exists", name)); + + } + + logger.info("Cloning VPC offering {} (id: {}) to new offering with name: {}", + sourceVpcOffering.getName(), sourceVpcOfferingId, name); + + applySourceOfferingValuesToCloneCmd(cmd, sourceVpcOffering); + + return createVpcOffering(cmd); + } + + private void applySourceOfferingValuesToCloneCmd(CloneVPCOfferingCmd cmd, VpcOffering sourceVpcOffering) { + Long sourceOfferingId = sourceVpcOffering.getId(); + + Map> sourceServiceProviderMap = getVpcOffSvcProvidersMap(sourceOfferingId); + + List finalServices = resolveFinalServicesList(cmd, sourceServiceProviderMap); + + Map finalServiceProviderMap = resolveServiceProviderMap(cmd, sourceServiceProviderMap, finalServices); + + List sourceDomainIds = vpcOfferingDetailsDao.findDomainIds(sourceOfferingId); + List sourceZoneIds = vpcOfferingDetailsDao.findZoneIds(sourceOfferingId); + + Map sourceServiceCapabilityList = reconstructServiceCapabilityList(sourceVpcOffering); + + applyResolvedValuesToCommand(cmd, (VpcOfferingVO)sourceVpcOffering, finalServices, finalServiceProviderMap, + sourceDomainIds, sourceZoneIds, sourceServiceCapabilityList); + } + + /** + * Reconstructs the service capability list from the source VPC offering's stored capability flags. + * These capabilities were originally passed during creation and stored as boolean flags in the offering. + * + * Returns a Map in the format expected by CreateVPCOfferingCmd.serviceCapabilityList: + * Map with keys like "0.service", "0.capabilitytype", "0.capabilityvalue" + */ + private Map reconstructServiceCapabilityList(VpcOffering sourceOffering) { + Map capabilityList = new HashMap<>(); + int index = 0; + + if (sourceOffering.isOffersRegionLevelVPC()) { + capabilityList.put(index + ".service", Network.Service.Connectivity.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.RegionLevelVpc.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + } + + if (sourceOffering.isSupportsDistributedRouter()) { + capabilityList.put(index + ".service", Network.Service.Connectivity.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.DistributedRouter.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + index++; + } + + if (sourceOffering.isRedundantRouter()) { + Map> serviceProviderMap = getVpcOffSvcProvidersMap(sourceOffering.getId()); + + // Check which service has VPCVirtualRouter provider - SourceNat takes precedence + Network.Service redundantRouterService = null; + for (Network.Service service : Arrays.asList(Network.Service.SourceNat, Network.Service.Gateway, Network.Service.StaticNat)) { + Set providers = serviceProviderMap.get(service); + if (providers != null && providers.contains(Network.Provider.VPCVirtualRouter)) { + redundantRouterService = service; + break; + } + } + + if (redundantRouterService != null) { + capabilityList.put(index + ".service", redundantRouterService.getName()); + capabilityList.put(index + ".capabilitytype", Network.Capability.RedundantRouter.getName()); + capabilityList.put(index + ".capabilityvalue", "true"); + } + } + + return capabilityList; + } + + private List resolveFinalServicesList(CloneVPCOfferingCmd 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(CloneVPCOfferingCmd 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(CloneVPCOfferingCmd cmd, VpcOfferingVO sourceOffering, + List finalServices, Map finalServiceProviderMap, + List sourceDomainIds, List sourceZoneIds, + Map sourceServiceCapabilityList) { + try { + Map requestParams = cmd.getFullUrlParams(); + + if (cmd.getSupportedServices() == null || cmd.getSupportedServices().isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "supportedServices", finalServices); + } + if (cmd.getServiceProviders() == null || cmd.getServiceProviders().isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "serviceProviderList", finalServiceProviderMap); + } + + if ((cmd.getServiceCapabilityList() == null || cmd.getServiceCapabilityList().isEmpty()) + && sourceServiceCapabilityList != null && !sourceServiceCapabilityList.isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "serviceCapabilityList", sourceServiceCapabilityList); + } + + ConfigurationManagerImpl.applyIfNotProvided(cmd, requestParams, "displayText", ApiConstants.DISPLAY_TEXT, cmd.getDisplayText(), sourceOffering.getDisplayText()); + ConfigurationManagerImpl.applyIfNotProvided(cmd, requestParams, "serviceOfferingId", ApiConstants.SERVICE_OFFERING_ID, cmd.getServiceOfferingId(), sourceOffering.getServiceOfferingId()); + + + ConfigurationManagerImpl.applyBooleanIfNotProvided(cmd, requestParams, "enable", ApiConstants.ENABLE, sourceOffering.getState() == VpcOffering.State.Enabled); + ConfigurationManagerImpl.applyBooleanIfNotProvided(cmd, requestParams, "specifyAsNumber", ApiConstants.SPECIFY_AS_NUMBER, sourceOffering.isSpecifyAsNumber()); + + if (!requestParams.containsKey(ApiConstants.INTERNET_PROTOCOL)) { + String internetProtocol = vpcOfferingDetailsDao.getDetail(sourceOffering.getId(), ApiConstants.INTERNET_PROTOCOL); + if (internetProtocol != null) { + ConfigurationManagerImpl.setField(cmd, "internetProtocol", internetProtocol); + } + } + + if (!requestParams.containsKey(ApiConstants.NETWORK_MODE) && sourceOffering.getNetworkMode() != null) { + ConfigurationManagerImpl.setField(cmd, "networkMode", sourceOffering.getNetworkMode().toString()); + } + + if (!requestParams.containsKey(ApiConstants.ROUTING_MODE) && sourceOffering.getRoutingMode() != null) { + ConfigurationManagerImpl.setField(cmd, "routingMode", sourceOffering.getRoutingMode().toString()); + } + + + if (cmd.getDomainIds() == null || cmd.getDomainIds().isEmpty()) { + if (sourceDomainIds != null && !sourceDomainIds.isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "domainIds", sourceDomainIds); + } + } + if (cmd.getZoneIds() == null || cmd.getZoneIds().isEmpty()) { + if (sourceZoneIds != null && !sourceZoneIds.isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "zoneIds", sourceZoneIds); + } + } + + } catch (Exception e) { + logger.warn("Failed to apply some source offering parameters during clone: {}", e.getMessage()); + } + } + private void validateConnectivtyServiceCapabilities(final Set providers, final Map serviceCapabilitystList) { if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) { final Collection serviceCapabilityCollection = serviceCapabilitystList.values(); diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 59ff09961c3..aafd220c60f 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -327,6 +327,7 @@ import org.apache.cloudstack.api.command.admin.volume.RecoverVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.ResizeVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.UpdateVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.UploadVolumeCmdByAdmin; +import org.apache.cloudstack.api.command.admin.vpc.CloneVPCOfferingCmd; import org.apache.cloudstack.api.command.admin.vpc.CreatePrivateGatewayByAdminCmd; import org.apache.cloudstack.api.command.admin.vpc.CreateVPCCmdByAdmin; import org.apache.cloudstack.api.command.admin.vpc.CreateVPCOfferingCmd; @@ -3967,6 +3968,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(RecoverVMCmd.class); cmdList.add(CreatePrivateGatewayCmd.class); cmdList.add(CreateVPCOfferingCmd.class); + cmdList.add(CloneVPCOfferingCmd.class); cmdList.add(DeletePrivateGatewayCmd.class); cmdList.add(DeleteVPCOfferingCmd.class); cmdList.add(UpdateVPCOfferingCmd.class); diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index ed8391fe0c6..085ad6b8db3 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -42,6 +42,7 @@ import com.cloud.utils.DomainHelper; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.DeleteBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.ListBackupProviderOfferingsCmd; @@ -334,6 +335,55 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { return backupOfferingDetailsDao.findDomainIds(offeringId); } + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_CLONE_OFFERING, eventDescription = "cloning backup offering", create = true) + public BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd) { + final BackupOfferingVO sourceOffering = backupOfferingDao.findById(cmd.getId()); + if (sourceOffering == null) { + throw new InvalidParameterValueException("Unable to find backup offering with ID: " + cmd.getId()); + } + + validateBackupForZone(sourceOffering.getZoneId()); + + if (backupOfferingDao.findByName(cmd.getName(), sourceOffering.getZoneId()) != null) { + throw new CloudRuntimeException("A backup offering with the name '" + cmd.getName() + "' already exists in this zone"); + } + + final String description = cmd.getDescription() != null ? cmd.getDescription() : sourceOffering.getDescription(); + final String externalId = cmd.getExternalId() != null ? cmd.getExternalId() : sourceOffering.getExternalId(); + final boolean userDrivenBackups = cmd.getUserDrivenBackups() != null ? cmd.getUserDrivenBackups() : sourceOffering.isUserDrivenBackupAllowed(); + + if (!externalId.equals(sourceOffering.getExternalId())) { + final BackupProvider provider = getBackupProvider(sourceOffering.getZoneId()); + if (!provider.isValidProviderOffering(sourceOffering.getZoneId(), externalId)) { + throw new CloudRuntimeException("Backup offering '" + externalId + "' does not exist on provider " + provider.getName() + " on zone " + sourceOffering.getZoneId()); + } + } + + if (!externalId.equals(sourceOffering.getExternalId())) { + final BackupOffering existingOffering = backupOfferingDao.findByExternalId(externalId, sourceOffering.getZoneId()); + if (existingOffering != null) { + throw new CloudRuntimeException("A backup offering with external ID '" + externalId + "' already exists in this zone"); + } + } + + final BackupOfferingVO clonedOffering = new BackupOfferingVO( + sourceOffering.getZoneId(), + externalId, + sourceOffering.getProvider(), + cmd.getName(), + description, + userDrivenBackups + ); + + final BackupOfferingVO savedOffering = backupOfferingDao.persist(clonedOffering); + if (savedOffering == null) { + throw new CloudRuntimeException("Unable to clone backup offering from ID: " + cmd.getId()); + } + + logger.debug("Successfully cloned backup offering '" + sourceOffering.getName() + "' (ID: " + cmd.getId() + ") to '" + cmd.getName() + "' (ID: " + savedOffering.getId() + ")"); + return savedOffering; + } + @Override public Pair, Integer> listBackupOfferings(final ListBackupOfferingsCmd cmd) { final Long offeringId = cmd.getOfferingId();