Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2afc1b7
Add support to clone existing offerings and update them
Pearl1594 Dec 31, 2025
1ad120f
add support for vpc & backup offerings to be cloned
Pearl1594 Jan 6, 2026
24ddfd2
fix capability list and mapping of params
Pearl1594 Jan 6, 2026
3787aaa
Add support to clone network and vpc offering with the right parameters
Pearl1594 Jan 6, 2026
db8fb3b
make fields non mandatory for clone offerings APIs
Pearl1594 Jan 7, 2026
d58fea2
Add UI support for cloning Compute and System Service offerings
Pearl1594 Jan 7, 2026
b9a1c02
remove unnecessary changes
Pearl1594 Jan 8, 2026
44d47ac
fix license and pre-ccommit issues
Pearl1594 Jan 8, 2026
7fd4567
Add UI support to clone disk and network offering
Pearl1594 Jan 8, 2026
cdd1250
vpc & backup offering clone api
Pearl1594 Jan 9, 2026
786372f
add unit tests
Pearl1594 Jan 9, 2026
210cb4c
fix pre-commit checks
Pearl1594 Jan 9, 2026
3aa1040
increase test coverage
Pearl1594 Jan 12, 2026
3981d42
combine add/clone disk/compute offering forms
Pearl1594 Jan 20, 2026
4e97874
update license
Pearl1594 Jan 20, 2026
231f22b
fix unit tests
Pearl1594 Jan 21, 2026
6e68f80
Merge branch 'main' of https://github.com/apache/cloudstack into clon…
Pearl1594 Jan 21, 2026
2057616
fix test failures
Pearl1594 Jan 21, 2026
cc4ada0
fix test failure - unnecessary stubbings
Pearl1594 Jan 21, 2026
3511525
pre-commit check failure
Pearl1594 Jan 21, 2026
10333df
add recently added domain id for bkp offering to be inherited in clon…
Pearl1594 Jan 21, 2026
7b2be10
extract common code wrt service capability in network & vpc offering …
Pearl1594 Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@
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;
import org.apache.cloudstack.api.command.admin.network.DeleteGuestNetworkIpv6PrefixCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd;
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.NetworkOfferingBaseCmd;
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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -282,7 +312,7 @@ Vlan updateVlanAndPublicIpRange(UpdateVlanIpRangeCmd cmd) throws ConcurrentOpera

boolean releasePublicIpRange(ReleasePublicIpRangeCmd cmd);

NetworkOffering createNetworkOffering(CreateNetworkOfferingCmd cmd);
NetworkOffering createNetworkOffering(NetworkOfferingBaseCmd cmd);

NetworkOffering updateNetworkOffering(UpdateNetworkOfferingCmd cmd);

Expand Down
2 changes: 2 additions & 0 deletions api/src/main/java/com/cloud/event/EventTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -629,6 +630,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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand All @@ -34,6 +35,8 @@ public interface VpcProvisioningService {

VpcOffering createVpcOffering(CreateVPCOfferingCmd cmd);

VpcOffering cloneVPCOffering(CloneVPCOfferingCmd cmd);

VpcOffering createVpcOffering(String name, String displayText, List<String> supportedServices,
Map<String, List<String>> serviceProviders,
Map serviceCapabilitystList, NetUtils.InternetProtocol internetProtocol,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// 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 javax.inject.Inject;

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.BaseAsyncCmd;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver;
import org.apache.cloudstack.api.response.BackupOfferingResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.backup.BackupManager;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.context.CallContext;

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;

import java.util.Arrays;
import java.util.List;
import java.util.function.LongFunction;

@APICommand(name = "cloneBackupOffering",
description = "Clones a backup offering from an existing offering",
responseObject = BackupOfferingResponse.class, since = "4.14.0",
authorized = {RoleType.Admin})
public class CloneBackupOfferingCmd extends BaseAsyncCmd implements DomainAndZoneIdResolver {

@Inject
protected BackupManager backupManager;

/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
////////////////////////////////////////////////////

@Parameter(name = ApiConstants.SOURCE_OFFERING_ID, type = BaseCmd.CommandType.UUID, entityType = BackupOfferingResponse.class,
required = true, description = "The ID of the source backup offering to clone from")
private Long sourceOfferingId;

@Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = false,
description = "The name of the cloned offering")
private String name;

@Parameter(name = ApiConstants.DESCRIPTION, type = BaseCmd.CommandType.STRING, required = false,
description = "The description of the cloned offering")
private String description;

@Parameter(name = ApiConstants.EXTERNAL_ID, type = BaseCmd.CommandType.STRING, required = false,
description = "The backup offering ID (from backup provider side)")
private String externalId;

@Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class,
description = "The zone ID", required = false)
private Long zoneId;

@Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.STRING,
description = "the ID of the containing domain(s) as comma separated string, public for public offerings",
since = "4.23.0",
length = 4096)
private String domainIds;

@Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = BaseCmd.CommandType.BOOLEAN,
description = "Whether users are allowed to create adhoc backups and backup schedules", required = false)
private Boolean userDrivenBackups;

/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////

public Long getSourceOfferingId() {
return sourceOfferingId;
}

public String getName() {
return name;
}

public String getExternalId() {
return externalId;
}

public Long getZoneId() {
return zoneId;
}

public String getDescription() {
return description;
}

public Boolean getUserDrivenBackups() {
return userDrivenBackups;
}

public List<Long> getDomainIds() {
if (domainIds != null && !domainIds.isEmpty()) {
return Arrays.asList(Arrays.stream(domainIds.split(",")).map(domainId -> Long.parseLong(domainId.trim())).toArray(Long[]::new));
}
LongFunction<List<Long>> defaultDomainsProvider = null;
if (backupManager != null) {
defaultDomainsProvider = backupManager::getBackupOfferingDomains;
}
return resolveDomainIds(domainIds, sourceOfferingId, defaultDomainsProvider, "backup offering");
}

/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
try {
BackupOffering policy = backupManager.cloneBackupOffering(this);
if (policy == null) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone backup offering");
}
BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(policy);
response.setResponseName(getCommandName());
setResponseObject(response);
} catch (InvalidParameterValueException e) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage());
} catch (CloudRuntimeException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
}
}

@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
}

@Override
public String getEventType() {
return EventTypes.EVENT_VM_BACKUP_CLONE_OFFERING;
}

@Override
public String getEventDescription() {
return "Cloning backup offering: " + name + " from source offering: " + (sourceOfferingId == null ? "" : sourceOfferingId.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
public class ImportBackupOfferingCmd extends BaseAsyncCmd {

@Inject
private BackupManager backupManager;
protected BackupManager backupManager;

/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
Expand Down Expand Up @@ -86,7 +86,8 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd {
type = CommandType.LIST,
collectionType = CommandType.UUID,
entityType = DomainResponse.class,
description = "the ID of the containing domain(s), null for public offerings")
description = "the ID of the containing domain(s), null for public offerings",
since = "4.23.0")
private List<Long> domainIds;

/////////////////////////////////////////////////////
Expand Down
Loading
Loading