Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2588ee4
draas initial changes
abh1sar Sep 2, 2025
8f0ce46
Fix UT failure in UserVmManagerImplTest
abh1sar Sep 2, 2025
4eac151
Added option to enable disaster recovery on a backup respository. Add…
abh1sar Sep 3, 2025
090c222
Added timeout for mount operation in backup restore configurable via …
abh1sar Sep 4, 2025
b6b8a0f
Merge remote-tracking branch 'upstream/main' into draas
abh1sar Sep 4, 2025
6c03978
Addressed review comments
abh1sar Sep 8, 2025
a58f6d4
fix for simulator test failures
abh1sar Sep 9, 2025
dae4f3b
Added UT for coverage
abh1sar Sep 9, 2025
7449646
Fix create instance from backup ui for other providers
abh1sar Sep 9, 2025
0df7f8d
Added events to add/update backup repository
abh1sar Sep 9, 2025
9dd75f2
Fix race in fetchZones
abh1sar Sep 15, 2025
5941150
One more fix in fetchZones in DeployVMFromBackup.vue
abh1sar Sep 15, 2025
0910dfe
Fix zone selection in createNetwork via Create Instance from backup f…
abh1sar Sep 15, 2025
1aa08b0
Allow template/iso selection in create instance from backup ui
abh1sar Sep 15, 2025
bc7055d
rename draasenabled to crosszoneinstancecreation
abh1sar Sep 15, 2025
6f39390
Added Cross-zone instance creation in test_backup_recovery_nas.py
abh1sar Sep 16, 2025
def5f8f
Added UT in BackupManagerTest and UserVmManagerImplTest
abh1sar Sep 16, 2025
faebe18
Integration test added for Cross-zone instance creation in test_backu…
abh1sar Sep 16, 2025
bdbfe54
Merge remote-tracking branch 'origin/draas' into draas
abh1sar Sep 16, 2025
ea8d8d6
Merge branch 'main' into draas
abh1sar Sep 22, 2025
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
9 changes: 9 additions & 0 deletions api/src/main/java/com/cloud/event/EventTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.cloudstack.api.response.HostResponse;
import org.apache.cloudstack.api.response.PodResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.backup.BackupRepositoryService;
import org.apache.cloudstack.config.Configuration;
import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet;
import org.apache.cloudstack.extension.Extension;
Expand Down Expand Up @@ -852,6 +853,10 @@ public class EventTypes {
// Custom Action
public static final String EVENT_CUSTOM_ACTION = "CUSTOM.ACTION";

// Backup Repository
public static final String EVENT_BACKUP_REPOSITORY_ADD = "BACKUP.REPOSITORY.ADD";
public static final String EVENT_BACKUP_REPOSITORY_UPDATE = "BACKUP.REPOSITORY.UPDATE";

static {

// TODO: need a way to force author adding event types to declare the entity details as well, with out braking
Expand Down Expand Up @@ -1385,6 +1390,10 @@ public class EventTypes {
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_ADD, ExtensionCustomAction.class);
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_UPDATE, ExtensionCustomAction.class);
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_DELETE, ExtensionCustomAction.class);

// Backup Repository
entityEventDetails.put(EVENT_BACKUP_REPOSITORY_ADD, BackupRepositoryService.class);
entityEventDetails.put(EVENT_BACKUP_REPOSITORY_UPDATE, BackupRepositoryService.class);
}

public static boolean isNetworkEvent(String eventType) {
Expand Down
1 change: 1 addition & 0 deletions api/src/main/java/com/cloud/vm/VirtualMachineProfile.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public static class Param {
public static final Param BootIntoSetup = new Param("enterHardwareSetup");
public static final Param PreserveNics = new Param("PreserveNics");
public static final Param ConsiderLastHost = new Param("ConsiderLastHost");
public static final Param ReturnAfterVolumePrepare = new Param("ReturnAfterVolumePrepare");

private String name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ public class ApiConstants {
public static final String CPU_SPEED = "cpuspeed";
public static final String CPU_LOAD_AVERAGE = "cpuloadaverage";
public static final String CREATED = "created";
public static final String CROSS_ZONE_INSTANCE_CREATION = "crosszoneinstancecreation";
public static final String CTX_ACCOUNT_ID = "ctxaccountid";
public static final String CTX_DETAILS = "ctxDetails";
public static final String CTX_USER_ID = "ctxuserid";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ public class AddBackupRepositoryCmd extends BaseCmd {
type = CommandType.UUID,
entityType = ZoneResponse.class,
required = true,
description = "ID of the zone where the backup repository is to be added")
description = "ID of the zone where the backup repository is to be added for taking backups")
private Long zoneId;

@Parameter(name = ApiConstants.CAPACITY_BYTES, type = CommandType.LONG, description = "capacity of this backup repository")
private Long capacityBytes;

@Parameter(name = ApiConstants.CROSS_ZONE_INSTANCE_CREATION, type = CommandType.BOOLEAN, description = "backups on this repository can be used to create Instances on all Zones", since = "4.22.0")
private Boolean crossZoneInstanceCreation;

/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
Expand Down Expand Up @@ -109,6 +111,10 @@ public Long getCapacityBytes() {
return capacityBytes;
}

public Boolean crossZoneInstanceCreationEnabled() {
return crossZoneInstanceCreation;
}

/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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.user.backup.repository;

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.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.BackupRepositoryResponse;
import org.apache.cloudstack.backup.BackupRepository;
import org.apache.cloudstack.backup.BackupRepositoryService;
import org.apache.cloudstack.context.CallContext;

import javax.inject.Inject;

@APICommand(name = "updateBackupRepository",
description = "Update a backup repository",
responseObject = BackupRepositoryResponse.class, since = "4.22.0",
authorized = {RoleType.Admin})
public class UpdateBackupRepositoryCmd extends BaseCmd {

@Inject
private BackupRepositoryService backupRepositoryService;

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

@Parameter(name = ApiConstants.ID, type = CommandType.UUID, required = true, entityType = BackupRepositoryResponse.class, description = "ID of the backup repository")
private Long id;

@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "name of the backup repository")
private String name;

@Parameter(name = ApiConstants.ADDRESS, type = CommandType.STRING, description = "address of the backup repository")
private String address;

@Parameter(name = ApiConstants.MOUNT_OPTIONS, type = CommandType.STRING, description = "shared storage mount options")
private String mountOptions;

@Parameter(name = ApiConstants.CROSS_ZONE_INSTANCE_CREATION, type = CommandType.BOOLEAN, description = "backups in this repository can be used to create Instances on all Zones")
private Boolean crossZoneInstanceCreation;

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

public BackupRepositoryService getBackupRepositoryService() {
return backupRepositoryService;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public String getAddress() {
return address;
}

public String getMountOptions() {
return mountOptions == null ? "" : mountOptions;
}

public Boolean crossZoneInstanceCreationEnabled() {
return crossZoneInstanceCreation;
}

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

@Override
public void execute() {
try {
BackupRepository result = backupRepositoryService.updateBackupRepository(this);
if (result != null) {
BackupRepositoryResponse response = _responseGenerator.createBackupRepositoryResponse(result);
response.setResponseName(getCommandName());
this.setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update the backup repository");
}
} catch (Exception ex4) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex4.getMessage());
}

}

@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ public class BackupRepositoryResponse extends BaseResponse {
@Param(description = "capacity of the backup repository")
private Long capacityBytes;

@SerializedName(ApiConstants.CROSS_ZONE_INSTANCE_CREATION)
@Param(description = "the backups in this repository can be used to create Instances on all Zones")
private Boolean crossZoneInstanceCreation;

@SerializedName("created")
@Param(description = "the date and time the backup repository was added")
private Date created;
Expand Down Expand Up @@ -132,6 +136,14 @@ public void setCapacityBytes(Long capacityBytes) {
this.capacityBytes = capacityBytes;
}

public Boolean getCrossZoneInstanceCreation() {
return crossZoneInstanceCreation;
}

public void setCrossZoneInstanceCreation(Boolean crossZoneInstanceCreation) {
this.crossZoneInstanceCreation = crossZoneInstanceCreation;
}

public Date getCreated() {
return created;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer

Boolean canCreateInstanceFromBackup(Long backupId);

Boolean canCreateInstanceFromBackupAcrossZones(Long backupId);

/**
* Restore a backup to a new Instance
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

public interface BackupProvider {

Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering);

/**
* Returns the unique name of the provider
* @return returns provider name
Expand Down Expand Up @@ -85,7 +87,7 @@ public interface BackupProvider {
*/
boolean deleteBackup(Backup backup, boolean forced);

boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid);
Pair<Boolean, String> restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid);

/**
* Restore VM from backup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ public interface BackupRepository extends InternalIdentity, Identity {
String getType();
String getAddress();
String getMountOptions();
void setMountOptions(String mountOptions);
void setUsedBytes(Long usedBytes);
Long getCapacityBytes();
Long getUsedBytes();
void setCapacityBytes(Long capacityBytes);
Boolean crossZoneInstanceCreationEnabled();
void setCrossZoneInstanceCreation(Boolean crossZoneInstanceCreation);
Date getCreated();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd;
import org.apache.cloudstack.api.command.user.backup.repository.DeleteBackupRepositoryCmd;
import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd;
import org.apache.cloudstack.api.command.user.backup.repository.UpdateBackupRepositoryCmd;

import java.util.List;

public interface BackupRepositoryService {
BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd);
BackupRepository updateBackupRepository(UpdateBackupRepositoryCmd cmd);
boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd);
Pair<List<BackupRepository>, Integer> listBackupRepositories(ListBackupRepositoriesCmd cmd);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class RestoreBackupCommand extends Command {
private Boolean vmExists;
private String restoreVolumeUUID;
private VirtualMachine.State vmState;
private Integer mountTimeout;

protected RestoreBackupCommand() {
super();
Expand Down Expand Up @@ -136,4 +137,12 @@ public List<String> getBackupVolumesUUIDs() {
public void setBackupVolumesUUIDs(List<String> backupVolumesUUIDs) {
this.backupVolumesUUIDs = backupVolumesUUIDs;
}

public Integer getMountTimeout() {
return this.mountTimeout;
}

public void setMountTimeout(Integer mountTimeout) {
this.mountTimeout = mountTimeout;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,21 @@ public void orchestrateStart(final String vmUuid, final Map<VirtualMachineProfil
volumeMgr.prepare(vmProfile, dest);
}

if (params != null) {
Boolean returnAfterVolumePrepare = (Boolean) params.get(VirtualMachineProfile.Param.ReturnAfterVolumePrepare);
if (Boolean.TRUE.equals(returnAfterVolumePrepare)) {
logger.info("Returning from VM start command execution for VM {} as requested. Volumes are prepared and ready.", vm.getUuid());

if (!changeState(vm, Event.AgentReportStopped, destHostId, work, Step.Done)) {
logger.error("Unable to transition to a new state. VM uuid: {}, VM oldstate: {}, Event: {}", vm, vm.getState(), Event.AgentReportStopped);
throw new ConcurrentOperationException(String.format("Failed to deploy VM %s", vm));
}

logger.debug("Volume preparation completed for VM {} (VM state set to Stopped)", vm);
return;
}
}

if (!reuseVolume) {
reuseVolume = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public class BackupRepositoryVO implements BackupRepository {
@Column(name = "capacity_bytes", nullable = true)
private Long capacityBytes;

@Column(name = "cross_zone_instance_creation")
private Boolean crossZoneInstanceCreation;

@Column(name = "created")
@Temporal(value = TemporalType.TIMESTAMP)
private Date created;
Expand All @@ -79,7 +82,7 @@ public BackupRepositoryVO() {
this.uuid = UUID.randomUUID().toString();
}

public BackupRepositoryVO(final long zoneId, final String provider, final String name, final String type, final String address, final String mountOptions, final Long capacityBytes) {
public BackupRepositoryVO(final long zoneId, final String provider, final String name, final String type, final String address, final String mountOptions, final Long capacityBytes, final Boolean crossZoneInstanceCreation) {
this();
this.zoneId = zoneId;
this.provider = provider;
Expand All @@ -88,6 +91,7 @@ public BackupRepositoryVO(final long zoneId, final String provider, final String
this.address = address;
this.mountOptions = mountOptions;
this.capacityBytes = capacityBytes;
this.crossZoneInstanceCreation = crossZoneInstanceCreation;
this.created = new Date();
}

Expand Down Expand Up @@ -139,6 +143,11 @@ public String getMountOptions() {
return mountOptions;
}

@Override
public void setMountOptions(String mountOptions) {
this.mountOptions = mountOptions;
}

@Override
public Long getUsedBytes() {
return usedBytes;
Expand All @@ -154,6 +163,16 @@ public Long getCapacityBytes() {
return capacityBytes;
}

@Override
public Boolean crossZoneInstanceCreationEnabled() {
return crossZoneInstanceCreation;
}

@Override
public void setCrossZoneInstanceCreation(Boolean crossZoneInstanceCreation) {
this.crossZoneInstanceCreation = crossZoneInstanceCreation;
}

@Override
public void setCapacityBytes(Long capacityBytes) {
this.capacityBytes = capacityBytes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('router_health_check', 'check_result', '

-- Increase length of scripts_version column to 128 due to md5sum to sha512sum change
CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.domain_router', 'scripts_version', 'scripts_version', 'VARCHAR(128)');

-- Add the column cross_zone_instance_creation to cloud.backup_repository. if enabled it means that new Instance can be created on all Zones from Backups on this Repository.
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_repository', 'cross_zone_instance_creation', 'TINYINT(1) DEFAULT NULL COMMENT ''Backup Repository can be used for disaster recovery on another zone''');
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public class DummyBackupProvider extends AdapterBase implements BackupProvider {
@Inject
private DiskOfferingDao diskOfferingDao;

@Override
public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) {
return true;
}

@Override
public String getName() {
return "dummy";
Expand Down Expand Up @@ -199,7 +204,7 @@ public void syncBackupStorageStats(Long zoneId) {
}

@Override
public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) {
return true;
public Pair<Boolean, String> restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) {
return new Pair<>(true, null);
}
}
Loading
Loading