Create Instance from backup on another Zone (DRaaS use case) (#11560)

* draas initial changes

* Added option to enable disaster recovery on a backup respository. Added UpdateBackupRepositoryCmd api.

* Added timeout for mount operation in backup restore configurable via global setting

* Addressed review comments

* fix for simulator test failures

* Added UT for coverage

* Fix create instance from backup ui for other providers

* Added events to add/update backup repository

* Fix race in fetchZones

* One more fix in fetchZones in DeployVMFromBackup.vue

* Fix zone selection in createNetwork via Create Instance from backup form.

* Allow template/iso selection in create instance from backup ui

* rename draasenabled to crosszoneinstancecreation

* Added Cross-zone instance creation in test_backup_recovery_nas.py

* Added UT in BackupManagerTest and UserVmManagerImplTest

* Integration test added for Cross-zone instance creation in test_backup_recovery_nas.py
This commit is contained in:
Abhisar Sinha 2025-09-25 13:28:29 +05:30 committed by GitHub
parent b0c7719006
commit 23c9e83047
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1961 additions and 206 deletions

View File

@ -27,6 +27,7 @@ import org.apache.cloudstack.api.response.ClusterResponse;
import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.HostResponse;
import org.apache.cloudstack.api.response.PodResponse; import org.apache.cloudstack.api.response.PodResponse;
import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.backup.BackupRepositoryService;
import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.config.Configuration;
import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet; import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet;
import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.Extension;
@ -852,6 +853,10 @@ public class EventTypes {
// Custom Action // Custom Action
public static final String EVENT_CUSTOM_ACTION = "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 { static {
// TODO: need a way to force author adding event types to declare the entity details as well, with out braking // TODO: need a way to force author adding event types to declare the entity details as well, with out braking
@ -1385,6 +1390,10 @@ public class EventTypes {
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_ADD, ExtensionCustomAction.class); entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_ADD, ExtensionCustomAction.class);
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_UPDATE, ExtensionCustomAction.class); entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_UPDATE, ExtensionCustomAction.class);
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_DELETE, 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) { public static boolean isNetworkEvent(String eventType) {

View File

@ -78,6 +78,7 @@ public interface VirtualMachineProfile {
public static final Param BootIntoSetup = new Param("enterHardwareSetup"); public static final Param BootIntoSetup = new Param("enterHardwareSetup");
public static final Param PreserveNics = new Param("PreserveNics"); public static final Param PreserveNics = new Param("PreserveNics");
public static final Param ConsiderLastHost = new Param("ConsiderLastHost"); public static final Param ConsiderLastHost = new Param("ConsiderLastHost");
public static final Param ReturnAfterVolumePrepare = new Param("ReturnAfterVolumePrepare");
private String name; private String name;

View File

@ -139,6 +139,7 @@ public class ApiConstants {
public static final String CPU_SPEED = "cpuspeed"; public static final String CPU_SPEED = "cpuspeed";
public static final String CPU_LOAD_AVERAGE = "cpuloadaverage"; public static final String CPU_LOAD_AVERAGE = "cpuloadaverage";
public static final String CREATED = "created"; 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_ACCOUNT_ID = "ctxaccountid";
public static final String CTX_DETAILS = "ctxDetails"; public static final String CTX_DETAILS = "ctxDetails";
public static final String CTX_USER_ID = "ctxuserid"; public static final String CTX_USER_ID = "ctxuserid";

View File

@ -63,12 +63,14 @@ public class AddBackupRepositoryCmd extends BaseCmd {
type = CommandType.UUID, type = CommandType.UUID,
entityType = ZoneResponse.class, entityType = ZoneResponse.class,
required = true, 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; private Long zoneId;
@Parameter(name = ApiConstants.CAPACITY_BYTES, type = CommandType.LONG, description = "capacity of this backup repository") @Parameter(name = ApiConstants.CAPACITY_BYTES, type = CommandType.LONG, description = "capacity of this backup repository")
private Long capacityBytes; 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 /////////////////////// /////////////////// Accessors ///////////////////////
@ -109,6 +111,10 @@ public class AddBackupRepositoryCmd extends BaseCmd {
return capacityBytes; return capacityBytes;
} }
public Boolean crossZoneInstanceCreationEnabled() {
return crossZoneInstanceCreation;
}
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////////// Accessors /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////

View File

@ -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();
}
}

View File

@ -61,6 +61,10 @@ public class BackupRepositoryResponse extends BaseResponse {
@Param(description = "capacity of the backup repository") @Param(description = "capacity of the backup repository")
private Long capacityBytes; 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") @SerializedName("created")
@Param(description = "the date and time the backup repository was added") @Param(description = "the date and time the backup repository was added")
private Date created; private Date created;
@ -132,6 +136,14 @@ public class BackupRepositoryResponse extends BaseResponse {
this.capacityBytes = capacityBytes; this.capacityBytes = capacityBytes;
} }
public Boolean getCrossZoneInstanceCreation() {
return crossZoneInstanceCreation;
}
public void setCrossZoneInstanceCreation(Boolean crossZoneInstanceCreation) {
this.crossZoneInstanceCreation = crossZoneInstanceCreation;
}
public Date getCreated() { public Date getCreated() {
return created; return created;
} }

View File

@ -205,6 +205,8 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer
Boolean canCreateInstanceFromBackup(Long backupId); Boolean canCreateInstanceFromBackup(Long backupId);
Boolean canCreateInstanceFromBackupAcrossZones(Long backupId);
/** /**
* Restore a backup to a new Instance * Restore a backup to a new Instance
*/ */

View File

@ -23,6 +23,8 @@ import com.cloud.vm.VirtualMachine;
public interface BackupProvider { public interface BackupProvider {
Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering);
/** /**
* Returns the unique name of the provider * Returns the unique name of the provider
* @return returns provider name * @return returns provider name
@ -85,7 +87,7 @@ public interface BackupProvider {
*/ */
boolean deleteBackup(Backup backup, boolean forced); 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 * Restore VM from backup

View File

@ -28,9 +28,12 @@ public interface BackupRepository extends InternalIdentity, Identity {
String getType(); String getType();
String getAddress(); String getAddress();
String getMountOptions(); String getMountOptions();
void setMountOptions(String mountOptions);
void setUsedBytes(Long usedBytes); void setUsedBytes(Long usedBytes);
Long getCapacityBytes(); Long getCapacityBytes();
Long getUsedBytes(); Long getUsedBytes();
void setCapacityBytes(Long capacityBytes); void setCapacityBytes(Long capacityBytes);
Boolean crossZoneInstanceCreationEnabled();
void setCrossZoneInstanceCreation(Boolean crossZoneInstanceCreation);
Date getCreated(); Date getCreated();
} }

View File

@ -23,11 +23,13 @@ import com.cloud.utils.Pair;
import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd; 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.DeleteBackupRepositoryCmd;
import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd; import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd;
import org.apache.cloudstack.api.command.user.backup.repository.UpdateBackupRepositoryCmd;
import java.util.List; import java.util.List;
public interface BackupRepositoryService { public interface BackupRepositoryService {
BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd); BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd);
BackupRepository updateBackupRepository(UpdateBackupRepositoryCmd cmd);
boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd); boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd);
Pair<List<BackupRepository>, Integer> listBackupRepositories(ListBackupRepositoriesCmd cmd); Pair<List<BackupRepository>, Integer> listBackupRepositories(ListBackupRepositoriesCmd cmd);

View File

@ -36,6 +36,7 @@ public class RestoreBackupCommand extends Command {
private Boolean vmExists; private Boolean vmExists;
private String restoreVolumeUUID; private String restoreVolumeUUID;
private VirtualMachine.State vmState; private VirtualMachine.State vmState;
private Integer mountTimeout;
protected RestoreBackupCommand() { protected RestoreBackupCommand() {
super(); super();
@ -136,4 +137,12 @@ public class RestoreBackupCommand extends Command {
public void setBackupVolumesUUIDs(List<String> backupVolumesUUIDs) { public void setBackupVolumesUUIDs(List<String> backupVolumesUUIDs) {
this.backupVolumesUUIDs = backupVolumesUUIDs; this.backupVolumesUUIDs = backupVolumesUUIDs;
} }
public Integer getMountTimeout() {
return this.mountTimeout;
}
public void setMountTimeout(Integer mountTimeout) {
this.mountTimeout = mountTimeout;
}
} }

View File

@ -1482,6 +1482,21 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
volumeMgr.prepare(vmProfile, dest); 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) { if (!reuseVolume) {
reuseVolume = true; reuseVolume = true;
} }

View File

@ -67,6 +67,9 @@ public class BackupRepositoryVO implements BackupRepository {
@Column(name = "capacity_bytes", nullable = true) @Column(name = "capacity_bytes", nullable = true)
private Long capacityBytes; private Long capacityBytes;
@Column(name = "cross_zone_instance_creation")
private Boolean crossZoneInstanceCreation;
@Column(name = "created") @Column(name = "created")
@Temporal(value = TemporalType.TIMESTAMP) @Temporal(value = TemporalType.TIMESTAMP)
private Date created; private Date created;
@ -79,7 +82,7 @@ public class BackupRepositoryVO implements BackupRepository {
this.uuid = UUID.randomUUID().toString(); 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();
this.zoneId = zoneId; this.zoneId = zoneId;
this.provider = provider; this.provider = provider;
@ -88,6 +91,7 @@ public class BackupRepositoryVO implements BackupRepository {
this.address = address; this.address = address;
this.mountOptions = mountOptions; this.mountOptions = mountOptions;
this.capacityBytes = capacityBytes; this.capacityBytes = capacityBytes;
this.crossZoneInstanceCreation = crossZoneInstanceCreation;
this.created = new Date(); this.created = new Date();
} }
@ -139,6 +143,11 @@ public class BackupRepositoryVO implements BackupRepository {
return mountOptions; return mountOptions;
} }
@Override
public void setMountOptions(String mountOptions) {
this.mountOptions = mountOptions;
}
@Override @Override
public Long getUsedBytes() { public Long getUsedBytes() {
return usedBytes; return usedBytes;
@ -154,6 +163,16 @@ public class BackupRepositoryVO implements BackupRepository {
return capacityBytes; return capacityBytes;
} }
@Override
public Boolean crossZoneInstanceCreationEnabled() {
return crossZoneInstanceCreation;
}
@Override
public void setCrossZoneInstanceCreation(Boolean crossZoneInstanceCreation) {
this.crossZoneInstanceCreation = crossZoneInstanceCreation;
}
@Override @Override
public void setCapacityBytes(Long capacityBytes) { public void setCapacityBytes(Long capacityBytes) {
this.capacityBytes = capacityBytes; this.capacityBytes = capacityBytes;

View File

@ -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 -- 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)'); 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''');

View File

@ -53,6 +53,11 @@ public class DummyBackupProvider extends AdapterBase implements BackupProvider {
@Inject @Inject
private DiskOfferingDao diskOfferingDao; private DiskOfferingDao diskOfferingDao;
@Override
public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) {
return true;
}
@Override @Override
public String getName() { public String getName() {
return "dummy"; return "dummy";
@ -199,7 +204,7 @@ public class DummyBackupProvider extends AdapterBase implements BackupProvider {
} }
@Override @Override
public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { public Pair<Boolean, String> restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) {
return true; return new Pair<>(true, null);
} }
} }

View File

@ -70,9 +70,18 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.cloudstack.backup.BackupManager.BackupFrameworkEnabled;
public class NASBackupProvider extends AdapterBase implements BackupProvider, Configurable { public class NASBackupProvider extends AdapterBase implements BackupProvider, Configurable {
private static final Logger LOG = LogManager.getLogger(NASBackupProvider.class); private static final Logger LOG = LogManager.getLogger(NASBackupProvider.class);
ConfigKey<Integer> NASBackupRestoreMountTimeout = new ConfigKey<>("Advanced", Integer.class,
"nas.backup.restore.mount.timeout",
"30",
"Timeout in seconds after which backup repository mount for restore fails.",
true,
BackupFrameworkEnabled.key());
@Inject @Inject
private BackupDao backupDao; private BackupDao backupDao;
@ -115,30 +124,45 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
@Inject @Inject
private DiskOfferingDao diskOfferingDao; private DiskOfferingDao diskOfferingDao;
protected Host getLastVMHypervisorHost(VirtualMachine vm) { private Long getClusterIdFromRootVolume(VirtualMachine vm) {
Long hostId = vm.getLastHostId(); VolumeVO rootVolume = volumeDao.getInstanceRootVolume(vm.getId());
if (hostId == null) { StoragePoolVO rootDiskPool = primaryDataStoreDao.findById(rootVolume.getPoolId());
LOG.debug("Cannot find last host for vm. This should never happen, please check your database."); if (rootDiskPool == null) {
return null; return null;
} }
Host host = hostDao.findById(hostId); return rootDiskPool.getClusterId();
}
protected Host getVMHypervisorHost(VirtualMachine vm) {
Long hostId = vm.getLastHostId();
Long clusterId = null;
if (hostId != null) {
Host host = hostDao.findById(hostId);
if (host.getStatus() == Status.Up) { if (host.getStatus() == Status.Up) {
return host; return host;
} else { }
// Try to find any Up host in the same cluster // Try to find any Up host in the same cluster
for (final Host hostInCluster : hostDao.findHypervisorHostInCluster(host.getClusterId())) { clusterId = host.getClusterId();
} else {
// Try to find any Up host in the same cluster as the root volume
clusterId = getClusterIdFromRootVolume(vm);
}
if (clusterId != null) {
for (final Host hostInCluster : hostDao.findHypervisorHostInCluster(clusterId)) {
if (hostInCluster.getStatus() == Status.Up) { if (hostInCluster.getStatus() == Status.Up) {
LOG.debug("Found Host {} in cluster {}", hostInCluster, host.getClusterId()); LOG.debug("Found Host {} in cluster {}", hostInCluster, clusterId);
return hostInCluster; return hostInCluster;
} }
} }
} }
// Try to find any Host in the zone // Try to find any Host in the zone
return resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, host.getDataCenterId()); return resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, vm.getDataCenterId());
} }
protected Host getVMHypervisorHost(VirtualMachine vm) { protected Host getVMHypervisorHostForBackup(VirtualMachine vm) {
Long hostId = vm.getHostId(); Long hostId = vm.getHostId();
if (hostId == null && VirtualMachine.State.Running.equals(vm.getState())) { if (hostId == null && VirtualMachine.State.Running.equals(vm.getState())) {
throw new CloudRuntimeException(String.format("Unable to find the hypervisor host for %s. Make sure the virtual machine is running", vm.getName())); throw new CloudRuntimeException(String.format("Unable to find the hypervisor host for %s. Make sure the virtual machine is running", vm.getName()));
@ -158,7 +182,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
@Override @Override
public Pair<Boolean, Backup> takeBackup(final VirtualMachine vm, Boolean quiesceVM) { public Pair<Boolean, Backup> takeBackup(final VirtualMachine vm, Boolean quiesceVM) {
final Host host = getVMHypervisorHost(vm); final Host host = getVMHypervisorHostForBackup(vm);
final BackupRepository backupRepository = backupRepositoryDao.findByBackupOfferingId(vm.getBackupOfferingId()); final BackupRepository backupRepository = backupRepositoryDao.findByBackupOfferingId(vm.getBackupOfferingId());
if (backupRepository == null) { if (backupRepository == null) {
@ -249,16 +273,16 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
} }
@Override @Override
public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { public Pair<Boolean, String> restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) {
return restoreVMBackup(vm, backup); return restoreVMBackup(vm, backup);
} }
@Override @Override
public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) {
return restoreVMBackup(vm, backup); return restoreVMBackup(vm, backup).first();
} }
private boolean restoreVMBackup(VirtualMachine vm, Backup backup) { private Pair<Boolean, String> restoreVMBackup(VirtualMachine vm, Backup backup) {
List<String> backedVolumesUUIDs = backup.getBackedUpVolumes().stream() List<String> backedVolumesUUIDs = backup.getBackedUpVolumes().stream()
.sorted(Comparator.comparingLong(Backup.VolumeInfo::getDeviceId)) .sorted(Comparator.comparingLong(Backup.VolumeInfo::getDeviceId))
.map(Backup.VolumeInfo::getUuid) .map(Backup.VolumeInfo::getUuid)
@ -271,7 +295,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
LOG.debug("Restoring vm {} from backup {} on the NAS Backup Provider", vm, backup); LOG.debug("Restoring vm {} from backup {} on the NAS Backup Provider", vm, backup);
BackupRepository backupRepository = getBackupRepository(backup); BackupRepository backupRepository = getBackupRepository(backup);
final Host host = getLastVMHypervisorHost(vm); final Host host = getVMHypervisorHost(vm);
RestoreBackupCommand restoreCommand = new RestoreBackupCommand(); RestoreBackupCommand restoreCommand = new RestoreBackupCommand();
restoreCommand.setBackupPath(backup.getExternalId()); restoreCommand.setBackupPath(backup.getExternalId());
restoreCommand.setBackupRepoType(backupRepository.getType()); restoreCommand.setBackupRepoType(backupRepository.getType());
@ -282,6 +306,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
restoreCommand.setRestoreVolumePaths(getVolumePaths(restoreVolumes)); restoreCommand.setRestoreVolumePaths(getVolumePaths(restoreVolumes));
restoreCommand.setVmExists(vm.getRemoved() == null); restoreCommand.setVmExists(vm.getRemoved() == null);
restoreCommand.setVmState(vm.getState()); restoreCommand.setVmState(vm.getState());
restoreCommand.setMountTimeout(NASBackupRestoreMountTimeout.value());
BackupAnswer answer; BackupAnswer answer;
try { try {
@ -291,7 +316,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
} catch (OperationTimedoutException e) { } catch (OperationTimedoutException e) {
throw new CloudRuntimeException("Operation to restore backup timed out, please try again"); throw new CloudRuntimeException("Operation to restore backup timed out, please try again");
} }
return answer.getResult(); return new Pair<>(answer.getResult(), answer.getDetails());
} }
private List<String> getVolumePaths(List<VolumeVO> volumes) { private List<String> getVolumePaths(List<VolumeVO> volumes) {
@ -398,7 +423,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
final Host host; final Host host;
final VirtualMachine vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); final VirtualMachine vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId());
if (vm != null) { if (vm != null) {
host = getLastVMHypervisorHost(vm); host = getVMHypervisorHost(vm);
} else { } else {
host = resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, backup.getZoneId()); host = resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, backup.getZoneId());
} }
@ -513,9 +538,19 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
return true; return true;
} }
@Override
public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) {
final BackupRepository backupRepository = backupRepositoryDao.findByBackupOfferingId(backupOffering.getId());
if (backupRepository == null) {
throw new CloudRuntimeException("Backup repository not found for the backup offering" + backupOffering.getName());
}
return Boolean.TRUE.equals(backupRepository.crossZoneInstanceCreationEnabled());
}
@Override @Override
public ConfigKey<?>[] getConfigKeys() { public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey[]{ return new ConfigKey[]{
NASBackupRestoreMountTimeout
}; };
} }

View File

@ -21,11 +21,9 @@ import static org.mockito.Mockito.mock;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import org.apache.cloudstack.backup.dao.BackupDao;
import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -39,6 +37,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.agent.AgentManager; import com.cloud.agent.AgentManager;
import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.host.HostVO; import com.cloud.host.HostVO;
import com.cloud.host.Status; import com.cloud.host.Status;
import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDao;
@ -51,6 +50,12 @@ import com.cloud.utils.Pair;
import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.VMInstanceDao;
import org.apache.cloudstack.backup.dao.BackupDao;
import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class NASBackupProviderTest { public class NASBackupProviderTest {
@Spy @Spy
@ -84,6 +89,9 @@ public class NASBackupProviderTest {
@Mock @Mock
private ResourceManager resourceManager; private ResourceManager resourceManager;
@Mock
private PrimaryDataStoreDao storagePoolDao;
@Test @Test
public void testDeleteBackup() throws OperationTimedoutException, AgentUnavailableException { public void testDeleteBackup() throws OperationTimedoutException, AgentUnavailableException {
Long hostId = 1L; Long hostId = 1L;
@ -94,7 +102,7 @@ public class NASBackupProviderTest {
ReflectionTestUtils.setField(backup, "id", 1L); ReflectionTestUtils.setField(backup, "id", 1L);
BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo", BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 1024L); "nfs", "address", "sync", 1024L, null);
VMInstanceVO vm = mock(VMInstanceVO.class); VMInstanceVO vm = mock(VMInstanceVO.class);
Mockito.when(vm.getLastHostId()).thenReturn(hostId); Mockito.when(vm.getLastHostId()).thenReturn(hostId);
@ -113,7 +121,7 @@ public class NASBackupProviderTest {
@Test @Test
public void testSyncBackupStorageStats() throws AgentUnavailableException, OperationTimedoutException { public void testSyncBackupStorageStats() throws AgentUnavailableException, OperationTimedoutException {
BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo", BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 1024L); "nfs", "address", "sync", 1024L, null);
HostVO host = mock(HostVO.class); HostVO host = mock(HostVO.class);
Mockito.when(resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, 1L)).thenReturn(host); Mockito.when(resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, 1L)).thenReturn(host);
@ -132,7 +140,7 @@ public class NASBackupProviderTest {
@Test @Test
public void testListBackupOfferings() { public void testListBackupOfferings() {
BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo", BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 1024L); "nfs", "address", "sync", 1024L, null);
ReflectionTestUtils.setField(backupRepository, "uuid", "uuid"); ReflectionTestUtils.setField(backupRepository, "uuid", "uuid");
Mockito.when(backupRepositoryDao.listByZoneAndProvider(1L, "nas")).thenReturn(Collections.singletonList(backupRepository)); Mockito.when(backupRepositoryDao.listByZoneAndProvider(1L, "nas")).thenReturn(Collections.singletonList(backupRepository));
@ -146,11 +154,11 @@ public class NASBackupProviderTest {
@Test @Test
public void testGetBackupStorageStats() { public void testGetBackupStorageStats() {
BackupRepositoryVO backupRepository1 = new BackupRepositoryVO(1L, "nas", "test-repo", BackupRepositoryVO backupRepository1 = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 1000L); "nfs", "address", "sync", 1000L, null);
backupRepository1.setUsedBytes(500L); backupRepository1.setUsedBytes(500L);
BackupRepositoryVO backupRepository2 = new BackupRepositoryVO(1L, "nas", "test-repo", BackupRepositoryVO backupRepository2 = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 2000L); "nfs", "address", "sync", 2000L, null);
backupRepository2.setUsedBytes(600L); backupRepository2.setUsedBytes(600L);
Mockito.when(backupRepositoryDao.listByZoneAndProvider(1L, "nas")) Mockito.when(backupRepositoryDao.listByZoneAndProvider(1L, "nas"))
@ -227,4 +235,118 @@ public class NASBackupProviderTest {
Mockito.verify(backupDao).update(Mockito.anyLong(), Mockito.any(BackupVO.class)); Mockito.verify(backupDao).update(Mockito.anyLong(), Mockito.any(BackupVO.class));
Mockito.verify(agentManager).send(anyLong(), Mockito.any(TakeBackupCommand.class)); Mockito.verify(agentManager).send(anyLong(), Mockito.any(TakeBackupCommand.class));
} }
@Test
public void testGetVMHypervisorHost() {
Long hostId = 1L;
Long vmId = 1L;
Long zoneId = 1L;
VMInstanceVO vm = mock(VMInstanceVO.class);
Mockito.when(vm.getLastHostId()).thenReturn(hostId);
HostVO host = mock(HostVO.class);
Mockito.when(host.getId()).thenReturn(hostId);
Mockito.when(host.getStatus()).thenReturn(Status.Up);
Mockito.when(hostDao.findById(hostId)).thenReturn(host);
Host result = nasBackupProvider.getVMHypervisorHost(vm);
Assert.assertNotNull(result);
Assert.assertTrue(Objects.equals(hostId, result.getId()));
Mockito.verify(hostDao).findById(hostId);
}
@Test
public void testGetVMHypervisorHostWithHostDown() {
Long hostId = 1L;
Long clusterId = 2L;
Long vmId = 1L;
Long zoneId = 1L;
VMInstanceVO vm = mock(VMInstanceVO.class);
Mockito.when(vm.getLastHostId()).thenReturn(hostId);
HostVO downHost = mock(HostVO.class);
Mockito.when(downHost.getStatus()).thenReturn(Status.Down);
Mockito.when(downHost.getClusterId()).thenReturn(clusterId);
Mockito.when(hostDao.findById(hostId)).thenReturn(downHost);
HostVO upHostInCluster = mock(HostVO.class);
Mockito.when(upHostInCluster.getId()).thenReturn(3L);
Mockito.when(upHostInCluster.getStatus()).thenReturn(Status.Up);
Mockito.when(hostDao.findHypervisorHostInCluster(clusterId)).thenReturn(List.of(upHostInCluster));
Host result = nasBackupProvider.getVMHypervisorHost(vm);
Assert.assertNotNull(result);
Assert.assertTrue(Objects.equals(Long.valueOf(3L), result.getId()));
Mockito.verify(hostDao).findById(hostId);
Mockito.verify(hostDao).findHypervisorHostInCluster(clusterId);
}
@Test
public void testGetVMHypervisorHostWithUpHostViaRootVolumeCluster() {
Long vmId = 1L;
Long zoneId = 1L;
Long clusterId = 2L;
Long poolId = 3L;
VMInstanceVO vm = mock(VMInstanceVO.class);
Mockito.when(vm.getLastHostId()).thenReturn(null);
Mockito.when(vm.getId()).thenReturn(vmId);
VolumeVO rootVolume = mock(VolumeVO.class);
Mockito.when(rootVolume.getPoolId()).thenReturn(poolId);
Mockito.when(volumeDao.getInstanceRootVolume(vmId)).thenReturn(rootVolume);
StoragePoolVO storagePool = mock(StoragePoolVO.class);
Mockito.when(storagePool.getClusterId()).thenReturn(clusterId);
Mockito.when(storagePoolDao.findById(poolId)).thenReturn(storagePool);
HostVO upHostInCluster = mock(HostVO.class);
Mockito.when(upHostInCluster.getId()).thenReturn(4L);
Mockito.when(upHostInCluster.getStatus()).thenReturn(Status.Up);
Mockito.when(hostDao.findHypervisorHostInCluster(clusterId)).thenReturn(List.of(upHostInCluster));
Host result = nasBackupProvider.getVMHypervisorHost(vm);
Assert.assertNotNull(result);
Assert.assertTrue(Objects.equals(Long.valueOf(4L), result.getId()));
Mockito.verify(volumeDao).getInstanceRootVolume(vmId);
Mockito.verify(storagePoolDao).findById(poolId);
Mockito.verify(hostDao).findHypervisorHostInCluster(clusterId);
}
@Test
public void testGetVMHypervisorHostFallbackToZoneWideKVMHost() {
Long hostId = 1L;
Long clusterId = 2L;
Long vmId = 1L;
Long zoneId = 1L;
VMInstanceVO vm = mock(VMInstanceVO.class);
Mockito.when(vm.getLastHostId()).thenReturn(hostId);
Mockito.when(vm.getDataCenterId()).thenReturn(zoneId);
HostVO downHost = mock(HostVO.class);
Mockito.when(downHost.getStatus()).thenReturn(Status.Down);
Mockito.when(downHost.getClusterId()).thenReturn(clusterId);
Mockito.when(hostDao.findById(hostId)).thenReturn(downHost);
Mockito.when(hostDao.findHypervisorHostInCluster(clusterId)).thenReturn(Collections.emptyList());
HostVO fallbackHost = mock(HostVO.class);
Mockito.when(fallbackHost.getId()).thenReturn(5L);
Mockito.when(resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, zoneId))
.thenReturn(fallbackHost);
Host result = nasBackupProvider.getVMHypervisorHost(vm);
Assert.assertNotNull(result);
Assert.assertTrue(Objects.equals(Long.valueOf(5L), result.getId()));
Mockito.verify(hostDao).findById(hostId);
Mockito.verify(hostDao).findHypervisorHostInCluster(clusterId);
Mockito.verify(resourceManager).findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, zoneId);
}
} }

View File

@ -158,6 +158,11 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid
}; };
} }
@Override
public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) {
return false;
}
@Override @Override
public String getName() { public String getName() {
return "networker"; return "networker";
@ -630,7 +635,7 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid
public boolean willDeleteBackupsOnOfferingRemoval() { return false; } public boolean willDeleteBackupsOnOfferingRemoval() { return false; }
@Override @Override
public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { public Pair<Boolean, String> restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) {
return true; return new Pair<>(true, null);
} }
} }

View File

@ -337,11 +337,11 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
} }
@Override @Override
public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { public Pair<Boolean, String> restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) {
final Long zoneId = backup.getZoneId(); final Long zoneId = backup.getZoneId();
final String restorePointId = backup.getExternalId(); final String restorePointId = backup.getExternalId();
final String restoreLocation = vm.getInstanceName(); final String restoreLocation = vm.getInstanceName();
return getClient(zoneId).restoreVMToDifferentLocation(restorePointId, restoreLocation, hostIp, dataStoreUuid).first(); return getClient(zoneId).restoreVMToDifferentLocation(restorePointId, restoreLocation, hostIp, dataStoreUuid);
} }
@Override @Override
@ -358,6 +358,11 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
public void syncBackupStorageStats(Long zoneId) { public void syncBackupStorageStats(Long zoneId) {
} }
@Override
public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) {
return false;
}
@Override @Override
public String getConfigComponentName() { public String getConfigComponentName() {
return BackupService.class.getSimpleName(); return BackupService.class.getSimpleName();

View File

@ -61,42 +61,49 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
List<String> backedVolumeUUIDs = command.getBackupVolumesUUIDs(); List<String> backedVolumeUUIDs = command.getBackupVolumesUUIDs();
List<String> restoreVolumePaths = command.getRestoreVolumePaths(); List<String> restoreVolumePaths = command.getRestoreVolumePaths();
String restoreVolumeUuid = command.getRestoreVolumeUUID(); String restoreVolumeUuid = command.getRestoreVolumeUUID();
Integer mountTimeout = command.getMountTimeout() * 1000;
String newVolumeId = null; String newVolumeId = null;
try { try {
String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions, mountTimeout);
if (Objects.isNull(vmExists)) { if (Objects.isNull(vmExists)) {
String volumePath = restoreVolumePaths.get(0); String volumePath = restoreVolumePaths.get(0);
int lastIndex = volumePath.lastIndexOf("/"); int lastIndex = volumePath.lastIndexOf("/");
newVolumeId = volumePath.substring(lastIndex + 1); newVolumeId = volumePath.substring(lastIndex + 1);
restoreVolume(backupPath, backupRepoType, backupRepoAddress, volumePath, diskType, restoreVolumeUuid, restoreVolume(backupPath, volumePath, diskType, restoreVolumeUuid,
new Pair<>(vmName, command.getVmState()), mountOptions); new Pair<>(vmName, command.getVmState()), mountDirectory);
} else if (Boolean.TRUE.equals(vmExists)) { } else if (Boolean.TRUE.equals(vmExists)) {
restoreVolumesOfExistingVM(restoreVolumePaths, backedVolumeUUIDs, backupPath, backupRepoType, backupRepoAddress, mountOptions); restoreVolumesOfExistingVM(restoreVolumePaths, backedVolumeUUIDs, backupPath, mountDirectory);
} else { } else {
restoreVolumesOfDestroyedVMs(restoreVolumePaths, vmName, backupPath, backupRepoType, backupRepoAddress, mountOptions); restoreVolumesOfDestroyedVMs(restoreVolumePaths, vmName, backupPath, mountDirectory);
} }
} catch (CloudRuntimeException e) { } catch (CloudRuntimeException e) {
String errorMessage = "Failed to restore backup for VM: " + vmName + "."; String errorMessage = e.getMessage() != null ? e.getMessage() : "";
if (e.getMessage() != null && !e.getMessage().isEmpty()) {
errorMessage += " Details: " + e.getMessage();
}
logger.error(errorMessage);
return new BackupAnswer(command, false, errorMessage); return new BackupAnswer(command, false, errorMessage);
} }
return new BackupAnswer(command, true, newVolumeId); return new BackupAnswer(command, true, newVolumeId);
} }
private void restoreVolumesOfExistingVM(List<String> restoreVolumePaths, List<String> backedVolumesUUIDs, String backupPath, private void verifyBackupFile(String backupPath, String volUuid) {
String backupRepoType, String backupRepoAddress, String mountOptions) { if (!checkBackupPathExists(backupPath)) {
throw new CloudRuntimeException(String.format("Backup file for the volume [%s] does not exist.", volUuid));
}
if (!checkBackupFileImage(backupPath)) {
throw new CloudRuntimeException(String.format("Backup qcow2 file for the volume [%s] is corrupt.", volUuid));
}
}
private void restoreVolumesOfExistingVM(List<String> restoreVolumePaths, List<String> backedVolumesUUIDs,
String backupPath, String mountDirectory) {
String diskType = "root"; String diskType = "root";
String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
try { try {
for (int idx = 0; idx < restoreVolumePaths.size(); idx++) { for (int idx = 0; idx < restoreVolumePaths.size(); idx++) {
String restoreVolumePath = restoreVolumePaths.get(idx); String restoreVolumePath = restoreVolumePaths.get(idx);
String backupVolumeUuid = backedVolumesUUIDs.get(idx); String backupVolumeUuid = backedVolumesUUIDs.get(idx);
Pair<String, String> bkpPathAndVolUuid = getBackupPath(mountDirectory, null, backupPath, diskType, backupVolumeUuid); Pair<String, String> bkpPathAndVolUuid = getBackupPath(mountDirectory, null, backupPath, diskType, backupVolumeUuid);
diskType = "datadisk"; diskType = "datadisk";
verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second());
if (!replaceVolumeWithBackup(restoreVolumePath, bkpPathAndVolUuid.first())) { if (!replaceVolumeWithBackup(restoreVolumePath, bkpPathAndVolUuid.first())) {
throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second()));
} }
@ -107,15 +114,14 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
} }
} }
private void restoreVolumesOfDestroyedVMs(List<String> volumePaths, String vmName, String backupPath, private void restoreVolumesOfDestroyedVMs(List<String> volumePaths, String vmName, String backupPath, String mountDirectory) {
String backupRepoType, String backupRepoAddress, String mountOptions) {
String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
String diskType = "root"; String diskType = "root";
try { try {
for (int i = 0; i < volumePaths.size(); i++) { for (int i = 0; i < volumePaths.size(); i++) {
String volumePath = volumePaths.get(i); String volumePath = volumePaths.get(i);
Pair<String, String> bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, null); Pair<String, String> bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, null);
diskType = "datadisk"; diskType = "datadisk";
verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second());
if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) { if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) {
throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second()));
} }
@ -126,12 +132,12 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
} }
} }
private void restoreVolume(String backupPath, String backupRepoType, String backupRepoAddress, String volumePath, private void restoreVolume(String backupPath, String volumePath, String diskType, String volumeUUID,
String diskType, String volumeUUID, Pair<String, VirtualMachine.State> vmNameAndState, String mountOptions) { Pair<String, VirtualMachine.State> vmNameAndState, String mountDirectory) {
String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
Pair<String, String> bkpPathAndVolUuid; Pair<String, String> bkpPathAndVolUuid;
try { try {
bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID); bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID);
verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second());
if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) { if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) {
throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second()));
} }
@ -140,8 +146,6 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
throw new CloudRuntimeException(String.format("Failed to attach volume to VM: %s", vmNameAndState.first())); throw new CloudRuntimeException(String.format("Failed to attach volume to VM: %s", vmNameAndState.first()));
} }
} }
} catch (Exception e) {
throw new CloudRuntimeException("Failed to restore volume", e);
} finally { } finally {
unmountBackupDirectory(mountDirectory); unmountBackupDirectory(mountDirectory);
deleteTemporaryDirectory(mountDirectory); deleteTemporaryDirectory(mountDirectory);
@ -149,11 +153,17 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
} }
private String mountBackupDirectory(String backupRepoAddress, String backupRepoType, String mountOptions) { private String mountBackupDirectory(String backupRepoAddress, String backupRepoType, String mountOptions, Integer mountTimeout) {
String randomChars = RandomStringUtils.random(5, true, false); String randomChars = RandomStringUtils.random(5, true, false);
String mountDirectory = String.format("%s.%s",BACKUP_TEMP_FILE_PREFIX , randomChars); String mountDirectory = String.format("%s.%s",BACKUP_TEMP_FILE_PREFIX , randomChars);
try { try {
mountDirectory = Files.createTempDirectory(mountDirectory).toString(); mountDirectory = Files.createTempDirectory(mountDirectory).toString();
} catch (IOException e) {
logger.error(String.format("Failed to create the tmp mount directory {} for restore", mountDirectory), e);
throw new CloudRuntimeException("Failed to create the tmp mount directory for restore on the KVM host");
}
String mount = String.format(MOUNT_COMMAND, backupRepoType, backupRepoAddress, mountDirectory); String mount = String.format(MOUNT_COMMAND, backupRepoType, backupRepoAddress, mountDirectory);
if ("cifs".equals(backupRepoType)) { if ("cifs".equals(backupRepoType)) {
if (Objects.isNull(mountOptions) || mountOptions.trim().isEmpty()) { if (Objects.isNull(mountOptions) || mountOptions.trim().isEmpty()) {
@ -165,19 +175,21 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
if (Objects.nonNull(mountOptions) && !mountOptions.trim().isEmpty()) { if (Objects.nonNull(mountOptions) && !mountOptions.trim().isEmpty()) {
mount += " -o " + mountOptions; mount += " -o " + mountOptions;
} }
Script.runSimpleBashScript(mount);
} catch (Exception e) { int exitValue = Script.runSimpleBashScriptForExitValue(mount, mountTimeout, false);
throw new CloudRuntimeException(String.format("Failed to mount %s to %s", backupRepoType, backupRepoAddress), e); if (exitValue != 0) {
logger.error(String.format("Failed to mount repository {} of type {} to the directory {}", backupRepoAddress, backupRepoType, mountDirectory));
throw new CloudRuntimeException("Failed to mount the backup repository on the KVM host");
} }
return mountDirectory; return mountDirectory;
} }
private void unmountBackupDirectory(String backupDirectory) { private void unmountBackupDirectory(String backupDirectory) {
try {
String umountCmd = String.format(UMOUNT_COMMAND, backupDirectory); String umountCmd = String.format(UMOUNT_COMMAND, backupDirectory);
Script.runSimpleBashScript(umountCmd); int exitValue = Script.runSimpleBashScriptForExitValue(umountCmd);
} catch (Exception e) { if (exitValue != 0) {
throw new CloudRuntimeException(String.format("Failed to unmount backup directory: %s", backupDirectory), e); logger.error(String.format("Failed to unmount backup directory {}", backupDirectory));
throw new CloudRuntimeException("Failed to unmount the backup directory");
} }
} }
@ -185,7 +197,8 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
try { try {
Files.deleteIfExists(Paths.get(backupDirectory)); Files.deleteIfExists(Paths.get(backupDirectory));
} catch (IOException e) { } catch (IOException e) {
throw new CloudRuntimeException(String.format("Failed to delete backup directory: %s", backupDirectory), e); logger.error(String.format("Failed to delete backup directory: %s", backupDirectory), e);
throw new CloudRuntimeException("Failed to delete the backup directory");
} }
} }
@ -197,6 +210,16 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
return new Pair<>(bkpPath, volUuid); return new Pair<>(bkpPath, volUuid);
} }
private boolean checkBackupFileImage(String backupPath) {
int exitValue = Script.runSimpleBashScriptForExitValue(String.format("qemu-img check %s", backupPath));
return exitValue == 0;
}
private boolean checkBackupPathExists(String backupPath) {
int exitValue = Script.runSimpleBashScriptForExitValue(String.format("ls %s", backupPath));
return exitValue == 0;
}
private boolean replaceVolumeWithBackup(String volumePath, String backupPath) { private boolean replaceVolumeWithBackup(String volumePath, String backupPath) {
int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath)); int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath));
return exitValue == 0; return exitValue == 0;

View File

@ -0,0 +1,499 @@
// 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 com.cloud.hypervisor.kvm.resource.wrapper;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.utils.script.Script;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.backup.BackupAnswer;
import org.apache.cloudstack.backup.RestoreBackupCommand;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class LibvirtRestoreBackupCommandWrapperTest {
private LibvirtRestoreBackupCommandWrapper wrapper;
private LibvirtComputingResource libvirtComputingResource;
private RestoreBackupCommand command;
@Before
public void setUp() {
wrapper = new LibvirtRestoreBackupCommandWrapper();
libvirtComputingResource = Mockito.mock(LibvirtComputingResource.class);
command = Mockito.mock(RestoreBackupCommand.class);
}
@Test
public void testExecuteWithVmExistsNull() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
when(command.getBackupRepoType()).thenReturn("nfs");
when(command.getMountOptions()).thenReturn("rw");
when(command.isVmExists()).thenReturn(null);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
when(command.getRestoreVolumeUUID()).thenReturn("volume-123");
when(command.getVmState()).thenReturn(VirtualMachine.State.Running);
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenReturn(0); // Other commands success
scriptMock.when(() -> Script.runSimpleBashScript(anyString()))
.thenReturn("vda"); // Current device
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertTrue(backupAnswer.getResult());
Assert.assertEquals("volume-123", backupAnswer.getDetails());
}
}
}
@Test
public void testExecuteWithVmExistsTrue() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
when(command.getBackupRepoType()).thenReturn("nfs");
when(command.getMountOptions()).thenReturn("rw");
when(command.isVmExists()).thenReturn(true);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
when(command.getBackupVolumesUUIDs()).thenReturn(Arrays.asList("volume-123"));
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenReturn(0); // Other commands success
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertTrue(backupAnswer.getResult());
}
}
}
@Test
public void testExecuteWithVmExistsFalse() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
when(command.getBackupRepoType()).thenReturn("nfs");
when(command.getMountOptions()).thenReturn("rw");
when(command.isVmExists()).thenReturn(false);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenReturn(0); // Other commands success
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertTrue(backupAnswer.getResult());
}
}
}
@Test
public void testExecuteWithCifsMountType() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("//192.168.1.100/backup");
when(command.getBackupRepoType()).thenReturn("cifs");
when(command.getMountOptions()).thenReturn("username=user,password=pass");
when(command.isVmExists()).thenReturn(null);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
when(command.getRestoreVolumeUUID()).thenReturn("volume-123");
when(command.getVmState()).thenReturn(VirtualMachine.State.Running);
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenReturn(0); // Other commands success
scriptMock.when(() -> Script.runSimpleBashScript(anyString()))
.thenReturn("vda"); // Current device
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertTrue(backupAnswer.getResult());
}
}
}
@Test
public void testExecuteWithMountFailure() throws Exception {
lenient().when(command.getVmName()).thenReturn("test-vm");
lenient().when(command.getBackupPath()).thenReturn("backup/path");
lenient().when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
lenient().when(command.getBackupRepoType()).thenReturn("nfs");
lenient().when(command.getMountOptions()).thenReturn("rw");
lenient().when(command.isVmExists()).thenReturn(null);
lenient().when(command.getDiskType()).thenReturn("root");
lenient().when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
lenient().when(command.getRestoreVolumeUUID()).thenReturn("volume-123");
lenient().when(command.getVmState()).thenReturn(VirtualMachine.State.Running);
lenient().when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(1); // Mount failure
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertFalse(backupAnswer.getResult());
Assert.assertTrue(backupAnswer.getDetails().contains("Failed to mount the backup repository"));
}
}
}
@Test
public void testExecuteWithBackupFileNotFound() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
when(command.getBackupRepoType()).thenReturn("nfs");
when(command.getMountOptions()).thenReturn("rw");
when(command.isVmExists()).thenReturn(null);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
when(command.getRestoreVolumeUUID()).thenReturn("volume-123");
when(command.getVmState()).thenReturn(VirtualMachine.State.Running);
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenAnswer(invocation -> {
String command = invocation.getArgument(0);
if (command.contains("ls ")) {
return 1; // File not found
}
return 0; // Other commands success
});
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertFalse(backupAnswer.getResult());
Assert.assertTrue(backupAnswer.getDetails().contains("Backup file for the volume [volume-123] does not exist"));
}
}
}
@Test
public void testExecuteWithCorruptBackupFile() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
when(command.getBackupRepoType()).thenReturn("nfs");
when(command.getMountOptions()).thenReturn("rw");
when(command.isVmExists()).thenReturn(null);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
when(command.getRestoreVolumeUUID()).thenReturn("volume-123");
when(command.getVmState()).thenReturn(VirtualMachine.State.Running);
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenAnswer(invocation -> {
String command = invocation.getArgument(0);
if (command.contains("ls ")) {
return 0; // File exists
} else if (command.contains("qemu-img check")) {
return 1; // Corrupt file
}
return 0; // Other commands success
});
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertFalse(backupAnswer.getResult());
Assert.assertTrue(backupAnswer.getDetails().contains("Backup qcow2 file for the volume [volume-123] is corrupt"));
}
}
}
@Test
public void testExecuteWithRsyncFailure() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
when(command.getBackupRepoType()).thenReturn("nfs");
when(command.getMountOptions()).thenReturn("rw");
when(command.isVmExists()).thenReturn(null);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
when(command.getRestoreVolumeUUID()).thenReturn("volume-123");
when(command.getVmState()).thenReturn(VirtualMachine.State.Running);
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenAnswer(invocation -> {
String command = invocation.getArgument(0);
if (command.contains("ls ")) {
return 0; // File exists
} else if (command.contains("qemu-img check")) {
return 0; // File is valid
} else if (command.contains("rsync")) {
return 1; // Rsync failure
}
return 0; // Other commands success
});
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertFalse(backupAnswer.getResult());
Assert.assertTrue(backupAnswer.getDetails().contains("Unable to restore contents from the backup volume [volume-123]"));
}
}
}
@Test
public void testExecuteWithAttachVolumeFailure() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
when(command.getBackupRepoType()).thenReturn("nfs");
when(command.getMountOptions()).thenReturn("rw");
when(command.isVmExists()).thenReturn(null);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
when(command.getRestoreVolumeUUID()).thenReturn("volume-123");
when(command.getVmState()).thenReturn(VirtualMachine.State.Running);
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenAnswer(invocation -> {
String command = invocation.getArgument(0);
if (command.contains("ls ")) {
return 0; // File exists
} else if (command.contains("qemu-img check")) {
return 0; // File is valid
} else if (command.contains("rsync")) {
return 0; // Rsync success
} else if (command.contains("virsh attach-disk")) {
return 1; // Attach failure
}
return 0; // Other commands success
});
scriptMock.when(() -> Script.runSimpleBashScript(anyString()))
.thenReturn("vda"); // Current device
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertFalse(backupAnswer.getResult());
Assert.assertTrue(backupAnswer.getDetails().contains("Failed to attach volume to VM: test-vm"));
}
}
}
@Test
public void testExecuteWithTempDirectoryCreationFailure() throws Exception {
lenient().when(command.getVmName()).thenReturn("test-vm");
lenient().when(command.getBackupPath()).thenReturn("backup/path");
lenient().when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
lenient().when(command.getBackupRepoType()).thenReturn("nfs");
lenient().when(command.getMountOptions()).thenReturn("rw");
lenient().when(command.isVmExists()).thenReturn(null);
lenient().when(command.getDiskType()).thenReturn("root");
lenient().when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123"));
lenient().when(command.getRestoreVolumeUUID()).thenReturn("volume-123");
lenient().when(command.getVmState()).thenReturn(VirtualMachine.State.Running);
lenient().when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
filesMock.when(() -> Files.createTempDirectory(anyString()))
.thenThrow(new IOException("Failed to create temp directory"));
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertFalse(backupAnswer.getResult());
Assert.assertTrue(backupAnswer.getDetails().contains("Failed to create the tmp mount directory for restore"));
}
}
@Test
public void testExecuteWithMultipleVolumes() throws Exception {
when(command.getVmName()).thenReturn("test-vm");
when(command.getBackupPath()).thenReturn("backup/path");
when(command.getBackupRepoAddress()).thenReturn("192.168.1.100:/backup");
when(command.getBackupRepoType()).thenReturn("nfs");
when(command.getMountOptions()).thenReturn("rw");
when(command.isVmExists()).thenReturn(true);
when(command.getDiskType()).thenReturn("root");
when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList(
"/var/lib/libvirt/images/volume-123",
"/var/lib/libvirt/images/volume-456"
));
when(command.getBackupVolumesUUIDs()).thenReturn(Arrays.asList("volume-123", "volume-456"));
when(command.getMountTimeout()).thenReturn(30);
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
Path tempPath = Mockito.mock(Path.class);
when(tempPath.toString()).thenReturn("/tmp/csbackup.abc123");
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenReturn(0); // All other commands success
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);
Answer result = wrapper.execute(command, libvirtComputingResource);
Assert.assertNotNull(result);
Assert.assertTrue(result instanceof BackupAnswer);
BackupAnswer backupAnswer = (BackupAnswer) result;
Assert.assertTrue(backupAnswer.getResult());
}
}
}
}

View File

@ -5563,6 +5563,7 @@ public class ApiResponseHelper implements ResponseGenerator {
response.setProviderName(backupRepository.getProvider()); response.setProviderName(backupRepository.getProvider());
response.setType(backupRepository.getType()); response.setType(backupRepository.getType());
response.setCapacityBytes(backupRepository.getCapacityBytes()); response.setCapacityBytes(backupRepository.getCapacityBytes());
response.setCrossZoneInstanceCreation(backupRepository.crossZoneInstanceCreationEnabled());
response.setObjectName("backuprepository"); response.setObjectName("backuprepository");
DataCenter zone = ApiDBUtils.findZoneById(backupRepository.getZoneId()); DataCenter zone = ApiDBUtils.findZoneById(backupRepository.getZoneId());
if (zone != null) { if (zone != null) {

View File

@ -9478,24 +9478,27 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Override @Override
public UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws InsufficientCapacityException, ResourceAllocationException, ResourceUnavailableException { public UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws InsufficientCapacityException, ResourceAllocationException, ResourceUnavailableException {
if (!backupManager.canCreateInstanceFromBackup(cmd.getBackupId())) {
throw new CloudRuntimeException("Create instance from backup is not supported for this provider.");
}
DataCenter zone = _dcDao.findById(cmd.getZoneId());
if (zone == null) {
throw new InvalidParameterValueException("Unable to find zone by id=" + cmd.getZoneId());
}
BackupVO backup = backupDao.findById(cmd.getBackupId()); BackupVO backup = backupDao.findById(cmd.getBackupId());
if (backup == null) { if (backup == null) {
throw new InvalidParameterValueException("Backup " + cmd.getBackupId() + " does not exist"); throw new InvalidParameterValueException("Backup " + cmd.getBackupId() + " does not exist");
} }
if (backup.getZoneId() != cmd.getZoneId()) {
throw new InvalidParameterValueException("Instance should be created in the same zone as the backup");
}
backupManager.validateBackupForZone(backup.getZoneId()); backupManager.validateBackupForZone(backup.getZoneId());
backupDao.loadDetails(backup);
if (!backupManager.canCreateInstanceFromBackup(cmd.getBackupId())) {
throw new CloudRuntimeException("Create instance from backup is not supported for this provider.");
}
DataCenter targetZone = _dcDao.findById(cmd.getZoneId());
if (targetZone == null) {
throw new InvalidParameterValueException("Unable to find zone by id=" + cmd.getZoneId());
}
if (cmd.getZoneId() != backup.getZoneId() &&
!backupManager.canCreateInstanceFromBackupAcrossZones(cmd.getBackupId())) {
throw new CloudRuntimeException("Create Instance from Backup on another Zone is not supported by this provider or the Backup Repository.");
}
backupDao.loadDetails(backup);
verifyDetails(cmd.getDetails()); verifyDetails(cmd.getDetails());
UserVmVO backupVm = _vmDao.findByIdIncludingRemoved(backup.getVmId()); UserVmVO backupVm = _vmDao.findByIdIncludingRemoved(backup.getVmId());
@ -9594,7 +9597,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
List<Long> networkIds = cmd.getNetworkIds(); List<Long> networkIds = cmd.getNetworkIds();
Account owner = _accountService.getActiveAccountById(cmd.getEntityOwnerId()); Account owner = _accountService.getActiveAccountById(cmd.getEntityOwnerId());
LinkedHashMap<Integer, Long> userVmNetworkMap = getVmOvfNetworkMapping(zone, owner, template, cmd.getVmNetworkMap()); LinkedHashMap<Integer, Long> userVmNetworkMap = getVmOvfNetworkMapping(targetZone, owner, template, cmd.getVmNetworkMap());
if (MapUtils.isNotEmpty(userVmNetworkMap)) { if (MapUtils.isNotEmpty(userVmNetworkMap)) {
networkIds = new ArrayList<>(userVmNetworkMap.values()); networkIds = new ArrayList<>(userVmNetworkMap.values());
} }
@ -9605,7 +9608,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
ipToNetworkMap = backupManager.getIpToNetworkMapFromBackup(backup, cmd.getPreserveIp(), networkIds); ipToNetworkMap = backupManager.getIpToNetworkMapFromBackup(backup, cmd.getPreserveIp(), networkIds);
} }
UserVm vm = createVirtualMachine(cmd, zone, owner, serviceOffering, template, hypervisorType, diskOfferingId, size, overrideDiskOfferingId, dataDiskInfoList, networkIds, ipToNetworkMap, null, null); UserVm vm = createVirtualMachine(cmd, targetZone, owner, serviceOffering, template, hypervisorType, diskOfferingId, size, overrideDiskOfferingId, dataDiskInfoList, networkIds, ipToNetworkMap, null, null);
String vmSettingsFromBackup = backup.getDetail(ApiConstants.VM_SETTINGS); String vmSettingsFromBackup = backup.getDetail(ApiConstants.VM_SETTINGS);
if (vm != null && vmSettingsFromBackup != null) { if (vm != null && vmSettingsFromBackup != null) {
@ -9629,20 +9632,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Override @Override
public UserVm restoreVMFromBackup(CreateVMFromBackupCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { public UserVm restoreVMFromBackup(CreateVMFromBackupCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException {
long vmId = cmd.getEntityId(); long vmId = cmd.getEntityId();
UserVm vm;
Map<Long, DiskOffering> diskOfferingMap = cmd.getDataDiskTemplateToDiskOfferingMap(); Map<Long, DiskOffering> diskOfferingMap = cmd.getDataDiskTemplateToDiskOfferingMap();
Map<VirtualMachineProfile.Param, Object> additonalParams = new HashMap<>(); Map<VirtualMachineProfile.Param, Object> additonalParams = new HashMap<>();
UserVm vm; additonalParams.put(VirtualMachineProfile.Param.ReturnAfterVolumePrepare, true);
try { try {
vm = startVirtualMachine(vmId, null, null, null, diskOfferingMap, additonalParams, null); Pair<UserVmVO, Map<VirtualMachineProfile.Param, Object>> vmParamPair = null;
vmParamPair = startVirtualMachine(vmId, null, null, null, additonalParams, null);
boolean status = stopVirtualMachine(CallContext.current().getCallingUserId(), vm.getId()) ; vm = vmParamPair.first();
if (!status) {
UserVmVO vmVO = _vmDao.findById(vmId);
expunge(vmVO);
logger.debug("Successfully cleaned up Instance {} after create Instance from backup failed", vmId);
throw new CloudRuntimeException("Unable to stop the Instance before restore");
}
Long isoId = vm.getIsoId(); Long isoId = vm.getIsoId();
if (isoId != null) { if (isoId != null) {
@ -9653,7 +9651,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
backupManager.restoreBackupToVM(cmd.getBackupId(), vmId); backupManager.restoreBackupToVM(cmd.getBackupId(), vmId);
} catch (CloudRuntimeException e) { } catch (CloudRuntimeException | ResourceUnavailableException | ResourceAllocationException | InsufficientCapacityException e) {
UserVmVO vmVO = _vmDao.findById(vmId); UserVmVO vmVO = _vmDao.findById(vmId);
try { try {
expunge(vmVO); expunge(vmVO);
@ -9680,6 +9678,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
podId = adminCmd.getPodId(); podId = adminCmd.getPodId();
clusterId = adminCmd.getClusterId(); clusterId = adminCmd.getClusterId();
} }
additonalParams.remove(VirtualMachineProfile.Param.ReturnAfterVolumePrepare);
vm = startVirtualMachine(vmId, podId, clusterId, cmd.getHostId(), diskOfferingMap, additonalParams, cmd.getDeploymentPlanner()); vm = startVirtualMachine(vmId, podId, clusterId, cmd.getHostId(), diskOfferingMap, additonalParams, cmd.getDeploymentPlanner());
} }
return vm; return vm;

View File

@ -62,6 +62,7 @@ import org.apache.cloudstack.api.command.user.backup.UpdateBackupScheduleCmd;
import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd; 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.DeleteBackupRepositoryCmd;
import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd; import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd;
import org.apache.cloudstack.api.command.user.backup.repository.UpdateBackupRepositoryCmd;
import org.apache.cloudstack.api.command.user.vm.CreateVMFromBackupCmd; import org.apache.cloudstack.api.command.user.vm.CreateVMFromBackupCmd;
import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.api.response.BackupResponse;
import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupDao;
@ -107,7 +108,6 @@ import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.HostVO; import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor;
@ -1022,7 +1022,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
if (offering == null) { if (offering == null) {
throw new CloudRuntimeException(errorMessage); throw new CloudRuntimeException(errorMessage);
} }
String backupDetailsInMessage = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "uuid", "externalId", "vmId", "type", "status", "date"); String backupDetailsInMessage = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "uuid", "externalId", "vmId", "name");
tryRestoreVM(backup, vm, offering, backupDetailsInMessage); tryRestoreVM(backup, vm, offering, backupDetailsInMessage);
updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready); updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready);
updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped); updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped);
@ -1239,6 +1239,14 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
return ipToNetworkMap; return ipToNetworkMap;
} }
private void processRestoreBackupToVMFailure(VMInstanceVO vm, Backup backup, Long eventId) {
updateVolumeState(vm, Volume.Event.RestoreFailed, Volume.State.Ready);
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped);
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_CREATE_FROM_BACKUP,
String.format("Failed to create Instance %s from backup %s", vm.getInstanceName(), backup.getUuid()),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(), eventId);
}
@Override @Override
public Boolean canCreateInstanceFromBackup(final Long backupId) { public Boolean canCreateInstanceFromBackup(final Long backupId) {
final BackupVO backup = backupDao.findById(backupId); final BackupVO backup = backupDao.findById(backupId);
@ -1251,7 +1259,18 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
} }
@Override @Override
public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws ResourceUnavailableException { public Boolean canCreateInstanceFromBackupAcrossZones(final Long backupId) {
final BackupVO backup = backupDao.findById(backupId);
BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId());
if (offering == null) {
throw new CloudRuntimeException("Failed to find backup offering");
}
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
return backupProvider.crossZoneInstanceCreationEnabled(offering);
}
@Override
public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws CloudRuntimeException {
final BackupVO backup = backupDao.findById(backupId); final BackupVO backup = backupDao.findById(backupId);
if (backup == null) { if (backup == null) {
throw new CloudRuntimeException("Backup " + backupId + " does not exist"); throw new CloudRuntimeException("Backup " + backupId + " does not exist");
@ -1293,7 +1312,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
throw new CloudRuntimeException("Create instance from backup is not supported by the " + offering.getProvider() + " provider."); throw new CloudRuntimeException("Create instance from backup is not supported by the " + offering.getProvider() + " provider.");
} }
String backupDetailsInMessage = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "uuid", "externalId", "newVMId", "type", "status", "date"); String backupDetailsInMessage = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "uuid", "externalId", "name");
Pair<Boolean, String> result = null;
Long eventId = null; Long eventId = null;
try { try {
updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring); updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring);
@ -1310,18 +1330,21 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
host = restoreInfo.first().getPrivateIpAddress(); host = restoreInfo.first().getPrivateIpAddress();
dataStore = restoreInfo.second().getUuid(); dataStore = restoreInfo.second().getUuid();
} }
if (!backupProvider.restoreBackupToVM(vm, backup, host, dataStore)) { result = backupProvider.restoreBackupToVM(vm, backup, host, dataStore);
throw new CloudRuntimeException(String.format("Error restoring backup [%s] to VM %s.", backupDetailsInMessage, vm.getUuid()));
}
} catch (Exception e) { } catch (Exception e) {
updateVolumeState(vm, Volume.Event.RestoreFailed, Volume.State.Ready); logger.error(String.format("Failed to create Instance [%s] from backup [%s] due to: [%s]", vm.getInstanceName(), backupDetailsInMessage, e.getMessage()), e);
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped); processRestoreBackupToVMFailure(vm, backup, eventId);
logger.error(String.format("Failed to create Instance [%s] from backup [%s] due to: [%s].", vm.getInstanceName(), backupDetailsInMessage, e.getMessage()), e);
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_CREATE_FROM_BACKUP,
String.format("Failed to create Instance %s from backup %s", vm.getInstanceName(), backup.getUuid()),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(), eventId);
throw new CloudRuntimeException(String.format("Error while creating Instance [%s] from backup [%s].", vm.getUuid(), backupDetailsInMessage)); throw new CloudRuntimeException(String.format("Error while creating Instance [%s] from backup [%s].", vm.getUuid(), backupDetailsInMessage));
} }
if (result != null && !result.first()) {
String error_msg = String.format("Failed to create Instance [%s] from backup [%s] due to: %s.", vm.getInstanceName(), backupDetailsInMessage, result.second());
logger.error(error_msg);
processRestoreBackupToVMFailure(vm, backup, eventId);
throw new CloudRuntimeException(error_msg);
}
updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready); updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready);
updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped); updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped);
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_VM_CREATE_FROM_BACKUP, ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_VM_CREATE_FROM_BACKUP,
@ -1604,6 +1627,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
cmdList.add(DeleteBackupCmd.class); cmdList.add(DeleteBackupCmd.class);
cmdList.add(RestoreVolumeFromBackupAndAttachToVMCmd.class); cmdList.add(RestoreVolumeFromBackupAndAttachToVMCmd.class);
cmdList.add(AddBackupRepositoryCmd.class); cmdList.add(AddBackupRepositoryCmd.class);
cmdList.add(UpdateBackupRepositoryCmd.class);
cmdList.add(DeleteBackupRepositoryCmd.class); cmdList.add(DeleteBackupRepositoryCmd.class);
cmdList.add(ListBackupRepositoriesCmd.class); cmdList.add(ListBackupRepositoriesCmd.class);
cmdList.add(CreateVMFromBackupCmd.class); cmdList.add(CreateVMFromBackupCmd.class);

View File

@ -19,6 +19,8 @@
package org.apache.cloudstack.backup; package org.apache.cloudstack.backup;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.user.AccountManager; import com.cloud.user.AccountManager;
import com.cloud.utils.Pair; import com.cloud.utils.Pair;
import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.ManagerBase;
@ -28,10 +30,12 @@ import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd; 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.DeleteBackupRepositoryCmd;
import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd; import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd;
import org.apache.cloudstack.api.command.user.backup.repository.UpdateBackupRepositoryCmd;
import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupDao;
import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.backup.dao.BackupRepositoryDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.ArrayList; import java.util.ArrayList;
@ -50,12 +54,59 @@ public class BackupRepositoryServiceImpl extends ManagerBase implements BackupRe
private AccountManager accountManager; private AccountManager accountManager;
@Override @Override
@ActionEvent(eventType = EventTypes.EVENT_BACKUP_REPOSITORY_ADD, eventDescription = "add backup repository")
public BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd) { public BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd) {
BackupRepositoryVO repository = new BackupRepositoryVO(cmd.getZoneId(), cmd.getProvider(), cmd.getName(), BackupRepositoryVO repository = new BackupRepositoryVO(cmd.getZoneId(), cmd.getProvider(), cmd.getName(),
cmd.getType(), cmd.getAddress(), cmd.getMountOptions(), cmd.getCapacityBytes()); cmd.getType(), cmd.getAddress(), cmd.getMountOptions(), cmd.getCapacityBytes(), cmd.crossZoneInstanceCreationEnabled());
return repositoryDao.persist(repository); return repositoryDao.persist(repository);
} }
@Override
@ActionEvent(eventType = EventTypes.EVENT_BACKUP_REPOSITORY_UPDATE, eventDescription = "update backup repository")
public BackupRepository updateBackupRepository(UpdateBackupRepositoryCmd cmd) {
Long id = cmd.getId();
String name = cmd.getName();
String address = cmd.getAddress();
String mountOptions = cmd.getMountOptions();
Boolean crossZoneInstanceCreation = cmd.crossZoneInstanceCreationEnabled();
BackupRepositoryVO backupRepository = repositoryDao.findById(id);
if (Objects.isNull(backupRepository)) {
logger.debug("Backup repository appears to already be deleted");
return null;
}
BackupRepositoryVO backupRepositoryVO = repositoryDao.createForUpdate(id);
List<String> fields = new ArrayList<>();
if (name != null) {
backupRepositoryVO.setName(name);
fields.add("name: " + name);
}
if (address != null) {
backupRepositoryVO.setAddress(address);
fields.add("address: " + address);
}
if (mountOptions != null) {
backupRepositoryVO.setMountOptions(mountOptions);
}
if (crossZoneInstanceCreation != null){
backupRepositoryVO.setCrossZoneInstanceCreation(crossZoneInstanceCreation);
fields.add("crossZoneInstanceCreation: " + crossZoneInstanceCreation);
}
if (!repositoryDao.update(id, backupRepositoryVO)) {
logger.warn(String.format("Couldn't update Backup repository (%s) with [%s].", backupRepositoryVO, String.join(", ", fields)));
return null;
}
BackupRepositoryVO repositoryVO = repositoryDao.findById(id);
CallContext.current().setEventDetails(String.format("Backup Repository updated [%s].",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(repositoryVO, "id", "name", "description", "userDrivenBackupAllowed", "externalId", "crossZoneInstanceCreation")));
return repositoryVO;
}
@Override @Override
public boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd) { public boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd) {
BackupRepositoryVO backupRepositoryVO = repositoryDao.findById(cmd.getId()); BackupRepositoryVO backupRepositoryVO = repositoryDao.findById(cmd.getId());

View File

@ -3595,7 +3595,6 @@ public class UserVmManagerImplTest {
doReturn(vmPair).when(userVmManagerImpl).startVirtualMachine(anyLong(), isNull(), isNull(), anyLong(), anyMap(), isNull()); doReturn(vmPair).when(userVmManagerImpl).startVirtualMachine(anyLong(), isNull(), isNull(), anyLong(), anyMap(), isNull());
when(userVmDao.findById(vmId)).thenReturn(vm); when(userVmDao.findById(vmId)).thenReturn(vm);
when(templateDao.findByIdIncludingRemoved(templateId)).thenReturn(mock(VMTemplateVO.class)); when(templateDao.findByIdIncludingRemoved(templateId)).thenReturn(mock(VMTemplateVO.class));
when(userVmManagerImpl.stopVirtualMachine(anyLong(), anyLong())).thenReturn(true);
UserVm result = userVmManagerImpl.restoreVMFromBackup(cmd); UserVm result = userVmManagerImpl.restoreVMFromBackup(cmd);
@ -3908,4 +3907,129 @@ public class UserVmManagerImplTest {
userVmManagerImpl.createVirtualMachine(deployVMCmd); userVmManagerImpl.createVirtualMachine(deployVMCmd);
} }
@Test
public void testAllocateVMFromBackupWithVmSettingsRestoration() throws InsufficientCapacityException, ResourceAllocationException, ResourceUnavailableException {
Long backupId = 10L;
Long vmId = 1L;
CreateVMFromBackupCmd cmd = new CreateVMFromBackupCmd();
cmd._accountService = accountService;
cmd._entityMgr = entityManager;
when(accountService.finalyzeAccountId(nullable(String.class), nullable(Long.class), nullable(Long.class), eq(true))).thenReturn(accountId);
when(accountService.getActiveAccountById(accountId)).thenReturn(account);
ReflectionTestUtils.setField(cmd, "serviceOfferingId", serviceOfferingId);
ReflectionTestUtils.setField(cmd, "templateId", templateId);
ReflectionTestUtils.setField(cmd, "backupId", backupId);
ReflectionTestUtils.setField(cmd, "zoneId", zoneId);
ServiceOfferingVO serviceOffering = mock(ServiceOfferingVO.class);
when(_serviceOfferingDao.findById(serviceOfferingId)).thenReturn(serviceOffering);
DataCenterVO zone = mock(DataCenterVO.class);
when(_dcDao.findById(zoneId)).thenReturn(zone);
BackupVO backup = mock(BackupVO.class);
when(backup.getZoneId()).thenReturn(zoneId);
when(backup.getVmId()).thenReturn(vmId);
when(backupDao.findById(backupId)).thenReturn(backup);
String vmSettingsJson = "{\"key1\":\"value1\",\"key2\":\"value2\",\"existingKey\":\"backupValue\"}";
when(backup.getDetail(ApiConstants.VM_SETTINGS)).thenReturn(vmSettingsJson);
UserVmVO userVmVO = new UserVmVO();
when(userVmDao.findByIdIncludingRemoved(vmId)).thenReturn(userVmVO);
VMTemplateVO template = mock(VMTemplateVO.class);
when(template.getFormat()).thenReturn(Storage.ImageFormat.QCOW2);
when(templateDao.findById(templateId)).thenReturn(template);
DiskOfferingVO diskOffering = mock(DiskOfferingVO.class);
VmDiskInfo rootVmDiskInfo = new VmDiskInfo(diskOffering, 10L, 1000L, 2000L);
when(backupManager.getRootDiskInfoFromBackup(backup)).thenReturn(rootVmDiskInfo);
when(backupManager.canCreateInstanceFromBackup(backupId)).thenReturn(true);
UserVmVO createdVm = mock(UserVmVO.class);
when(createdVm.getId()).thenReturn(2L);
Mockito.doReturn(createdVm).when(userVmManagerImpl).createAdvancedVirtualMachine(any(), any(), any(), any(), any(), any(), any(),
any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), nullable(Boolean.class), any(), any(), any(),
any(), any(), any(), any(), eq(true), any(), any(), any(), any());
Map<String, String> existingDetails = new HashMap<>();
existingDetails.put("existingKey", "existingValue");
when(vmInstanceDetailsDao.listDetailsKeyPairs(2L)).thenReturn(existingDetails);
UserVmVO vmVO = mock(UserVmVO.class);
when(userVmDao.findById(2L)).thenReturn(vmVO);
UserVm result = userVmManagerImpl.allocateVMFromBackup(cmd);
assertNotNull(result);
assertEquals(2L, result.getId());
verify(backup).getDetail(ApiConstants.VM_SETTINGS);
verify(vmInstanceDetailsDao).listDetailsKeyPairs(2L);
verify(userVmDao).findById(2L);
verify(userVmDao).saveDetails(any(UserVmVO.class));
}
@Test
public void testAllocateVMFromBackupWithOverrideDiskOfferingComputeOnly() throws InsufficientCapacityException, ResourceAllocationException, ResourceUnavailableException {
Long backupId = 11L;
Long vmId = 1L;
Long overrideDiskOfferingId = 5L;
CreateVMFromBackupCmd cmd = new CreateVMFromBackupCmd();
cmd._accountService = accountService;
cmd._entityMgr = entityManager;
when(accountService.finalyzeAccountId(nullable(String.class), nullable(Long.class), nullable(Long.class), eq(true))).thenReturn(accountId);
when(accountService.getActiveAccountById(accountId)).thenReturn(account);
ReflectionTestUtils.setField(cmd, "serviceOfferingId", serviceOfferingId);
ReflectionTestUtils.setField(cmd, "templateId", templateId);
ReflectionTestUtils.setField(cmd, "backupId", backupId);
ReflectionTestUtils.setField(cmd, "zoneId", zoneId);
ReflectionTestUtils.setField(cmd, "overrideDiskOfferingId", overrideDiskOfferingId);
ServiceOfferingVO serviceOffering = mock(ServiceOfferingVO.class);
when(_serviceOfferingDao.findById(serviceOfferingId)).thenReturn(serviceOffering);
DataCenterVO zone = mock(DataCenterVO.class);
when(_dcDao.findById(zoneId)).thenReturn(zone);
BackupVO backup = mock(BackupVO.class);
when(backup.getZoneId()).thenReturn(zoneId);
when(backup.getVmId()).thenReturn(vmId);
when(backupDao.findById(backupId)).thenReturn(backup);
UserVmVO userVmVO = new UserVmVO();
when(userVmDao.findByIdIncludingRemoved(vmId)).thenReturn(userVmVO);
VMTemplateVO template = mock(VMTemplateVO.class);
when(template.getFormat()).thenReturn(Storage.ImageFormat.QCOW2);
when(templateDao.findById(templateId)).thenReturn(template);
DiskOfferingVO overrideDiskOffering = mock(DiskOfferingVO.class);
when(overrideDiskOffering.isComputeOnly()).thenReturn(true);
when(diskOfferingDao.findById(overrideDiskOfferingId)).thenReturn(overrideDiskOffering);
DiskOfferingVO diskOffering = mock(DiskOfferingVO.class);
VmDiskInfo rootVmDiskInfo = new VmDiskInfo(diskOffering, 10L, 1000L, 2000L);
when(backupManager.getRootDiskInfoFromBackup(backup)).thenReturn(rootVmDiskInfo);
when(backupManager.canCreateInstanceFromBackup(backupId)).thenReturn(true);
UserVmVO createdVm = mock(UserVmVO.class);
when(createdVm.getId()).thenReturn(2L);
Mockito.doReturn(createdVm).when(userVmManagerImpl).createAdvancedVirtualMachine(any(), any(), any(), any(), any(), any(), any(),
any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), nullable(Boolean.class), any(), any(), any(),
any(), any(), any(), any(), eq(true), any(), any(), any(), any());
UserVm result = userVmManagerImpl.allocateVMFromBackup(cmd);
assertNotNull(result);
assertEquals(2L, result.getId());
verify(diskOfferingDao).findById(overrideDiskOfferingId);
verify(overrideDiskOffering).isComputeOnly();
}
} }

View File

@ -31,7 +31,6 @@ import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes; import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils; import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.HostVO; import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor;
@ -118,8 +117,10 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -1275,7 +1276,7 @@ public class BackupManagerTest {
when(rootVolume.getPoolId()).thenReturn(poolId); when(rootVolume.getPoolId()).thenReturn(poolId);
when(volumeDao.findIncludingRemovedByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(List.of(rootVolume)); when(volumeDao.findIncludingRemovedByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(List.of(rootVolume));
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool); when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
when(backupProvider.restoreBackupToVM(vm, backup, null, null)).thenReturn(true); when(backupProvider.restoreBackupToVM(vm, backup, null, null)).thenReturn(new Pair<>(true, null));
try (MockedStatic<ActionEventUtils> utils = Mockito.mockStatic(ActionEventUtils.class)) { try (MockedStatic<ActionEventUtils> utils = Mockito.mockStatic(ActionEventUtils.class)) {
boolean result = backupManager.restoreBackupToVM(backupId, vmId); boolean result = backupManager.restoreBackupToVM(backupId, vmId);
@ -1284,7 +1285,7 @@ public class BackupManagerTest {
verify(backupProvider, times(1)).restoreBackupToVM(vm, backup, null, null); verify(backupProvider, times(1)).restoreBackupToVM(vm, backup, null, null);
verify(virtualMachineManager, times(1)).stateTransitTo(vm, VirtualMachine.Event.RestoringRequested, hostId); verify(virtualMachineManager, times(1)).stateTransitTo(vm, VirtualMachine.Event.RestoringRequested, hostId);
verify(virtualMachineManager, times(1)).stateTransitTo(vm, VirtualMachine.Event.RestoringSuccess, hostId); verify(virtualMachineManager, times(1)).stateTransitTo(vm, VirtualMachine.Event.RestoringSuccess, hostId);
} catch (ResourceUnavailableException e) { } catch (CloudRuntimeException e) {
fail("Test failed due to exception" + e); fail("Test failed due to exception" + e);
} }
} }
@ -1331,7 +1332,7 @@ public class BackupManagerTest {
when(rootVolume.getPoolId()).thenReturn(poolId); when(rootVolume.getPoolId()).thenReturn(poolId);
when(volumeDao.findIncludingRemovedByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(List.of(rootVolume)); when(volumeDao.findIncludingRemovedByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(List.of(rootVolume));
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool); when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
when(backupProvider.restoreBackupToVM(vm, backup, null, null)).thenReturn(false); when(backupProvider.restoreBackupToVM(vm, backup, null, null)).thenReturn(new Pair<>(false, null));
try (MockedStatic<ActionEventUtils> utils = Mockito.mockStatic(ActionEventUtils.class)) { try (MockedStatic<ActionEventUtils> utils = Mockito.mockStatic(ActionEventUtils.class)) {
CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class,
@ -1813,4 +1814,269 @@ public class BackupManagerTest {
expectedAlertDetails expectedAlertDetails
); );
} }
@Test
public void testCanCreateInstanceFromBackupAcrossZonesSuccess() {
Long backupId = 1L;
Long backupOfferingId = 2L;
String providerName = "testbackupprovider";
BackupVO backup = mock(BackupVO.class);
when(backup.getBackupOfferingId()).thenReturn(backupOfferingId);
BackupOfferingVO offering = mock(BackupOfferingVO.class);
when(offering.getProvider()).thenReturn(providerName);
BackupProvider backupProvider = mock(BackupProvider.class);
when(backupProvider.crossZoneInstanceCreationEnabled(offering)).thenReturn(true);
when(backupDao.findById(backupId)).thenReturn(backup);
when(backupOfferingDao.findByIdIncludingRemoved(backupOfferingId)).thenReturn(offering);
when(backupManager.getBackupProvider(providerName)).thenReturn(backupProvider);
Boolean result = backupManager.canCreateInstanceFromBackupAcrossZones(backupId);
assertTrue(result);
verify(backupDao, times(1)).findById(backupId);
verify(backupOfferingDao, times(1)).findByIdIncludingRemoved(backupOfferingId);
verify(backupManager, times(1)).getBackupProvider(providerName);
verify(backupProvider, times(1)).crossZoneInstanceCreationEnabled(offering);
}
@Test
public void testCanCreateInstanceFromBackupAcrossZonesFalse() {
Long backupId = 1L;
Long backupOfferingId = 2L;
String providerName = "testbackupprovider";
BackupVO backup = mock(BackupVO.class);
when(backup.getBackupOfferingId()).thenReturn(backupOfferingId);
BackupOfferingVO offering = mock(BackupOfferingVO.class);
when(offering.getProvider()).thenReturn(providerName);
BackupProvider backupProvider = mock(BackupProvider.class);
when(backupProvider.crossZoneInstanceCreationEnabled(offering)).thenReturn(false);
when(backupDao.findById(backupId)).thenReturn(backup);
when(backupOfferingDao.findByIdIncludingRemoved(backupOfferingId)).thenReturn(offering);
when(backupManager.getBackupProvider(providerName)).thenReturn(backupProvider);
Boolean result = backupManager.canCreateInstanceFromBackupAcrossZones(backupId);
assertFalse(result);
verify(backupDao, times(1)).findById(backupId);
verify(backupOfferingDao, times(1)).findByIdIncludingRemoved(backupOfferingId);
verify(backupManager, times(1)).getBackupProvider(providerName);
verify(backupProvider, times(1)).crossZoneInstanceCreationEnabled(offering);
}
@Test
public void testCanCreateInstanceFromBackupAcrossZonesOfferingNotFound() {
Long backupId = 1L;
Long backupOfferingId = 2L;
BackupVO backup = mock(BackupVO.class);
when(backup.getBackupOfferingId()).thenReturn(backupOfferingId);
when(backupDao.findById(backupId)).thenReturn(backup);
when(backupOfferingDao.findByIdIncludingRemoved(backupOfferingId)).thenReturn(null);
CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class,
() -> backupManager.canCreateInstanceFromBackupAcrossZones(backupId));
assertEquals("Failed to find backup offering", exception.getMessage());
verify(backupDao, times(1)).findById(backupId);
verify(backupOfferingDao, times(1)).findByIdIncludingRemoved(backupOfferingId);
verify(backupManager, never()).getBackupProvider(any(String.class));
}
@Test
public void testRestoreBackupSuccess() throws NoTransitionException {
Long backupId = 1L;
Long vmId = 2L;
Long zoneId = 3L;
Long accountId = 4L;
Long domainId = 5L;
Long userId = 6L;
Long offeringId = 7L;
String vmInstanceName = "test-vm";
Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.KVM;
BackupVO backup = mock(BackupVO.class);
when(backup.getVmId()).thenReturn(vmId);
when(backup.getZoneId()).thenReturn(zoneId);
when(backup.getStatus()).thenReturn(Backup.Status.BackedUp);
when(backup.getBackupOfferingId()).thenReturn(offeringId);
Backup.VolumeInfo volumeInfo = new Backup.VolumeInfo("uuid", "path", Volume.Type.ROOT, 1024L, 0L, "disk-offering-uuid", 1000L, 2000L);
when(backup.getBackedUpVolumes()).thenReturn(List.of(volumeInfo));
when(backup.getUuid()).thenReturn("backup-uuid");
VMInstanceVO vm = mock(VMInstanceVO.class);
when(vm.getId()).thenReturn(vmId);
when(vm.getDataCenterId()).thenReturn(zoneId);
when(vm.getDomainId()).thenReturn(domainId);
when(vm.getAccountId()).thenReturn(accountId);
when(vm.getUserId()).thenReturn(userId);
when(vm.getInstanceName()).thenReturn(vmInstanceName);
when(vm.getHypervisorType()).thenReturn(hypervisorType);
when(vm.getState()).thenReturn(VirtualMachine.State.Stopped);
when(vm.getRemoved()).thenReturn(null);
when(vm.getBackupOfferingId()).thenReturn(offeringId);
BackupOfferingVO offering = mock(BackupOfferingVO.class);
when(offering.getProvider()).thenReturn("testbackupprovider");
VolumeVO volume = mock(VolumeVO.class);
when(volumeDao.findByInstance(vmId)).thenReturn(Collections.singletonList(volume));
BackupProvider backupProvider = mock(BackupProvider.class);
when(backupProvider.restoreVMFromBackup(vm, backup)).thenReturn(true);
when(backupDao.findById(backupId)).thenReturn(backup);
when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm);
when(backupOfferingDao.findByIdIncludingRemoved(offeringId)).thenReturn(offering);
when(backupManager.getBackupProvider("testbackupprovider")).thenReturn(backupProvider);
doReturn(true).when(backupManager).importRestoredVM(zoneId, domainId, accountId, userId, vmInstanceName, hypervisorType, backup);
doNothing().when(backupManager).validateBackupForZone(any());
when(virtualMachineManager.stateTransitTo(any(), any(), any())).thenReturn(true);
try (MockedStatic<ActionEventUtils> utils = Mockito.mockStatic(ActionEventUtils.class)) {
Mockito.when(ActionEventUtils.onStartedActionEvent(Mockito.anyLong(), Mockito.anyLong(),
Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(),
Mockito.eq(true), Mockito.eq(0))).thenReturn(1L);
boolean result = backupManager.restoreBackup(backupId);
assertTrue(result);
verify(backupDao, times(1)).findById(backupId);
verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(vmId);
verify(backupOfferingDao, times(2)).findByIdIncludingRemoved(offeringId);
verify(backupProvider, times(1)).restoreVMFromBackup(vm, backup);
verify(backupManager, times(1)).importRestoredVM(zoneId, domainId, accountId, userId, vmInstanceName, hypervisorType, backup);
}
}
@Test
public void testRestoreBackupBackupNotFound() {
Long backupId = 1L;
when(backupDao.findById(backupId)).thenReturn(null);
CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class,
() -> backupManager.restoreBackup(backupId));
assertEquals("Backup " + backupId + " does not exist", exception.getMessage());
verify(backupDao, times(1)).findById(backupId);
verify(vmInstanceDao, never()).findByIdIncludingRemoved(any());
}
@Test
public void testRestoreBackupBackupNotBackedUp() {
Long backupId = 1L;
BackupVO backup = mock(BackupVO.class);
when(backup.getStatus()).thenReturn(Backup.Status.BackingUp);
when(backupDao.findById(backupId)).thenReturn(backup);
CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class,
() -> backupManager.restoreBackup(backupId));
assertEquals("Backup should be in BackedUp state", exception.getMessage());
verify(backupDao, times(1)).findById(backupId);
verify(vmInstanceDao, never()).findByIdIncludingRemoved(any());
}
@Test
public void testRestoreBackupVmExpunging() {
Long backupId = 1L;
Long vmId = 2L;
Long zoneId = 3L;
BackupVO backup = mock(BackupVO.class);
when(backup.getVmId()).thenReturn(vmId);
when(backup.getZoneId()).thenReturn(zoneId);
when(backup.getStatus()).thenReturn(Backup.Status.BackedUp);
VMInstanceVO vm = mock(VMInstanceVO.class);
when(vm.getState()).thenReturn(VirtualMachine.State.Expunging);
when(backupDao.findById(backupId)).thenReturn(backup);
when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm);
doNothing().when(backupManager).validateBackupForZone(any());
CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class,
() -> backupManager.restoreBackup(backupId));
assertEquals("The Instance from which the backup was taken could not be found.", exception.getMessage());
verify(backupDao, times(1)).findById(backupId);
verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(vmId);
}
@Test
public void testRestoreBackupVmNotStopped() {
Long backupId = 1L;
Long vmId = 2L;
Long zoneId = 3L;
BackupVO backup = mock(BackupVO.class);
when(backup.getVmId()).thenReturn(vmId);
when(backup.getZoneId()).thenReturn(zoneId);
when(backup.getStatus()).thenReturn(Backup.Status.BackedUp);
VMInstanceVO vm = mock(VMInstanceVO.class);
when(vm.getState()).thenReturn(VirtualMachine.State.Running);
when(vm.getRemoved()).thenReturn(null);
when(backupDao.findById(backupId)).thenReturn(backup);
when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm);
doNothing().when(backupManager).validateBackupForZone(any());
CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class,
() -> backupManager.restoreBackup(backupId));
assertEquals("Existing VM should be stopped before being restored from backup", exception.getMessage());
verify(backupDao, times(1)).findById(backupId);
verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(vmId);
}
@Test
public void testRestoreBackupVolumeMismatch() {
Long backupId = 1L;
Long vmId = 2L;
Long zoneId = 3L;
BackupVO backup = mock(BackupVO.class);
when(backup.getVmId()).thenReturn(vmId);
when(backup.getZoneId()).thenReturn(zoneId);
when(backup.getStatus()).thenReturn(Backup.Status.BackedUp);
when(backup.getBackedUpVolumes()).thenReturn(Collections.emptyList());
VMInstanceVO vm = mock(VMInstanceVO.class);
when(vm.getId()).thenReturn(vmId);
when(vm.getState()).thenReturn(VirtualMachine.State.Destroyed);
when(vm.getRemoved()).thenReturn(null);
when(vm.getBackupVolumeList()).thenReturn(Collections.emptyList());
VolumeVO volume = mock(VolumeVO.class);
when(volumeDao.findByInstance(vmId)).thenReturn(Collections.singletonList(volume));
when(backupDao.findById(backupId)).thenReturn(backup);
when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm);
doNothing().when(backupManager).validateBackupForZone(any());
try (MockedStatic<ActionEventUtils> utils = Mockito.mockStatic(ActionEventUtils.class)) {
Mockito.when(ActionEventUtils.onStartedActionEvent(Mockito.anyLong(), Mockito.anyLong(),
Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(),
Mockito.eq(true), Mockito.eq(0))).thenReturn(1L);
CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class,
() -> backupManager.restoreBackup(backupId));
assertEquals("Unable to restore VM with the current backup as the backup has different number of disks as the VM", exception.getMessage());
}
verify(backupDao, times(1)).findById(backupId);
verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(vmId);
verify(volumeDao, times(1)).findByInstance(vmId);
}
} }

View File

@ -0,0 +1,243 @@
// 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.backup;
import com.cloud.user.AccountManager;
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 org.apache.cloudstack.backup.dao.BackupDao;
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
import org.apache.cloudstack.context.CallContext;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class BackupRepositoryServiceImplTest {
@InjectMocks
private BackupRepositoryServiceImpl backupRepositoryService;
@Mock
private BackupRepositoryDao repositoryDao;
@Mock
private BackupOfferingDao backupOfferingDao;
@Mock
private BackupDao backupDao;
@Mock
private AccountManager accountManager;
@Mock
private AddBackupRepositoryCmd addCmd;
@Mock
private UpdateBackupRepositoryCmd updateCmd;
@Mock
private DeleteBackupRepositoryCmd deleteCmd;
@Mock
private ListBackupRepositoriesCmd listCmd;
@Mock
private BackupRepositoryVO repositoryVO;
@Mock
private BackupOfferingVO offeringVO;
@Mock
private BackupVO backupVO;
@Mock
private CallContext callContext;
private Long zoneId = 2L;
private Long backupOfferingId = 3L;
@Test
public void testUpdateBackupRepository() {
when(updateCmd.getId()).thenReturn(1L);
when(updateCmd.getName()).thenReturn("updated-repo");
when(updateCmd.getAddress()).thenReturn("192.168.1.200:/backup");
when(updateCmd.getMountOptions()).thenReturn("rw,noexec");
when(updateCmd.crossZoneInstanceCreationEnabled()).thenReturn(false);
when(repositoryDao.findById(1L)).thenReturn(repositoryVO);
when(repositoryDao.createForUpdate(1L)).thenReturn(repositoryVO);
when(repositoryDao.update(eq(1L), any(BackupRepositoryVO.class))).thenReturn(true);
try (MockedStatic<CallContext> callContextMock = mockStatic(CallContext.class)) {
callContextMock.when(CallContext::current).thenReturn(callContext);
BackupRepository result = backupRepositoryService.updateBackupRepository(updateCmd);
Assert.assertEquals(repositoryVO, result);
verify(repositoryDao, Mockito.times(2)).findById(1L);
verify(repositoryDao).createForUpdate(1L);
verify(repositoryDao).update(eq(1L), any(BackupRepositoryVO.class));
verify(callContext).setEventDetails(anyString());
}
}
@Test
public void testUpdateBackupRepositoryWithNullRepository() {
when(updateCmd.getId()).thenReturn(1L);
when(updateCmd.getName()).thenReturn("updated-repo");
when(repositoryDao.findById(1L)).thenReturn(null);
BackupRepository result = backupRepositoryService.updateBackupRepository(updateCmd);
Assert.assertNull(result);
verify(repositoryDao).findById(1L);
verify(repositoryDao, never()).createForUpdate(anyLong());
verify(repositoryDao, never()).update(anyLong(), any(BackupRepositoryVO.class));
}
@Test
public void testUpdateBackupRepositoryWithUpdateFailure() {
when(updateCmd.getId()).thenReturn(1L);
when(updateCmd.getName()).thenReturn("updated-repo");
when(repositoryDao.findById(1L)).thenReturn(repositoryVO);
when(repositoryDao.createForUpdate(1L)).thenReturn(repositoryVO);
when(repositoryDao.update(eq(1L), any(BackupRepositoryVO.class))).thenReturn(false);
try (MockedStatic<CallContext> callContextMock = mockStatic(CallContext.class)) {
callContextMock.when(CallContext::current).thenReturn(callContext);
BackupRepository result = backupRepositoryService.updateBackupRepository(updateCmd);
Assert.assertNull(result);
verify(repositoryDao, Mockito.times(1)).findById(1L);
verify(repositoryDao).createForUpdate(1L);
verify(repositoryDao).update(eq(1L), any(BackupRepositoryVO.class));
}
}
@Test
public void testDeleteBackupRepository() {
when(deleteCmd.getId()).thenReturn(1L);
when(repositoryDao.findById(1L)).thenReturn(repositoryVO);
when(repositoryVO.getUuid()).thenReturn("repo-uuid");
when(repositoryVO.getZoneId()).thenReturn(zoneId);
when(repositoryVO.getId()).thenReturn(1L);
when(backupOfferingDao.findByExternalId("repo-uuid", zoneId)).thenReturn(offeringVO);
when(offeringVO.getId()).thenReturn(backupOfferingId);
when(backupDao.listByOfferingId(backupOfferingId)).thenReturn(new ArrayList<>());
when(repositoryDao.remove(1L)).thenReturn(true);
boolean result = backupRepositoryService.deleteBackupRepository(deleteCmd);
Assert.assertTrue(result);
verify(repositoryDao).findById(1L);
verify(backupOfferingDao).findByExternalId("repo-uuid", zoneId);
verify(backupDao).listByOfferingId(backupOfferingId);
verify(repositoryDao).remove(1L);
}
@Test
public void testDeleteBackupRepositoryWithNullRepository() {
when(deleteCmd.getId()).thenReturn(1L);
when(repositoryDao.findById(1L)).thenReturn(null);
boolean result = backupRepositoryService.deleteBackupRepository(deleteCmd);
Assert.assertFalse(result);
verify(repositoryDao).findById(1L);
verify(backupOfferingDao, never()).findByExternalId(anyString(), anyLong());
verify(backupDao, never()).listByOfferingId(anyLong());
verify(repositoryDao, never()).remove(anyLong());
}
@Test
public void testDeleteBackupRepositoryWithExistingBackups() {
when(deleteCmd.getId()).thenReturn(1L);
when(repositoryDao.findById(1L)).thenReturn(repositoryVO);
when(repositoryVO.getUuid()).thenReturn("repo-uuid");
when(repositoryVO.getZoneId()).thenReturn(zoneId);
when(backupOfferingDao.findByExternalId("repo-uuid", zoneId)).thenReturn(offeringVO);
when(offeringVO.getId()).thenReturn(backupOfferingId);
List<Backup> backups = Arrays.asList(backupVO);
when(backupDao.listByOfferingId(backupOfferingId)).thenReturn(backups);
try {
backupRepositoryService.deleteBackupRepository(deleteCmd);
Assert.fail("Expected CloudRuntimeException");
} catch (Exception e) {
Assert.assertTrue(e.getMessage().contains("Failed to delete backup repository as there are backups present on it"));
}
verify(repositoryDao).findById(1L);
verify(backupOfferingDao).findByExternalId("repo-uuid", zoneId);
verify(backupDao).listByOfferingId(backupOfferingId);
verify(repositoryDao, never()).remove(anyLong());
}
@Test
public void testDeleteBackupRepositoryWithNullOffering() {
when(deleteCmd.getId()).thenReturn(1L);
when(repositoryDao.findById(1L)).thenReturn(repositoryVO);
when(repositoryVO.getUuid()).thenReturn("repo-uuid");
when(repositoryVO.getId()).thenReturn(1L);
when(repositoryVO.getZoneId()).thenReturn(zoneId);
when(repositoryDao.remove(1L)).thenReturn(true);
boolean result = backupRepositoryService.deleteBackupRepository(deleteCmd);
Assert.assertTrue(result);
verify(repositoryDao).findById(1L);
verify(backupOfferingDao).findByExternalId("repo-uuid", zoneId);
verify(backupDao, never()).listByOfferingId(anyLong());
verify(repositoryDao).remove(1L);
}
}

View File

@ -16,10 +16,11 @@
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
from marvin.cloudstackAPI import listZones
from marvin.cloudstackTestCase import cloudstackTestCase from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.lib.utils import (cleanup_resources) from marvin.lib.utils import (cleanup_resources)
from marvin.lib.base import (Account, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering, from marvin.lib.base import (Account, Network, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering,
BackupRepository, Backup, Configurations, Volume, StoragePool) NetworkOffering, BackupRepository, Backup, Configurations, Volume, StoragePool)
from marvin.lib.common import (get_domain, get_zone, get_template) from marvin.lib.common import (get_domain, get_zone, get_template)
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from marvin.codes import FAILED from marvin.codes import FAILED
@ -109,40 +110,7 @@ class TestNASBackupAndRecovery(cloudstackTestCase):
except Exception as e: except Exception as e:
raise Exception("Warning: Exception during cleanup : %s" % e) raise Exception("Warning: Exception during cleanup : %s" % e)
@attr(tags=["advanced", "backup"], required_hardware="true") def vm_backup_create_vm_from_backup_int(self, templateid=None, networkids=None):
def test_vm_backup_lifecycle(self):
"""
Test VM backup lifecycle
"""
# Verify there are no backups for the VM
backups = Backup.list(self.apiclient, self.vm.id)
self.assertEqual(backups, None, "There should not exist any backup for the VM")
# Assign VM to offering and create ad-hoc backup
self.backup_offering.assignOffering(self.apiclient, self.vm.id)
Backup.create(self.apiclient, self.vm.id)
# Verify backup is created for the VM
backups = Backup.list(self.apiclient, self.vm.id)
self.assertEqual(len(backups), 1, "There should exist only one backup for the VM")
backup = backups[0]
# Delete backup
Backup.delete(self.apiclient, backup.id)
# Verify backup is deleted
backups = Backup.list(self.apiclient, self.vm.id)
self.assertEqual(backups, None, "There should not exist any backup for the VM")
# Remove VM from offering
self.backup_offering.removeOffering(self.apiclient, self.vm.id)
@attr(tags=["advanced", "backup"], required_hardware="true")
def test_vm_backup_create_vm_from_backup(self):
"""
Test creating a new VM from a backup
"""
self.backup_offering.assignOffering(self.apiclient, self.vm.id) self.backup_offering.assignOffering(self.apiclient, self.vm.id)
# Create a file and take backup # Create a file and take backup
@ -178,7 +146,9 @@ class TestNASBackupAndRecovery(cloudstackTestCase):
vmname=new_vm_name, vmname=new_vm_name,
accountname=self.account.name, accountname=self.account.name,
domainid=self.account.domainid, domainid=self.account.domainid,
zoneid=self.zone.id zoneid=self.destZone.id,
networkids=networkids,
templateid=templateid
) )
self.cleanup.append(new_vm) self.cleanup.append(new_vm)
@ -194,7 +164,7 @@ class TestNASBackupAndRecovery(cloudstackTestCase):
"New VM should have the correct service offering") "New VM should have the correct service offering")
# Verify the new VM has the correct zone # Verify the new VM has the correct zone
self.assertEqual(new_vm.zoneid, self.zone.id, "New VM should be in the correct zone") self.assertEqual(new_vm.zoneid, self.destZone.id, "New VM should be in the correct zone")
# Verify the new VM has the correct number of volumes (ROOT + DATADISK) # Verify the new VM has the correct number of volumes (ROOT + DATADISK)
volumes = Volume.list( volumes = Volume.list(
@ -217,3 +187,81 @@ class TestNASBackupAndRecovery(cloudstackTestCase):
# Delete backups # Delete backups
Backup.delete(self.apiclient, backups[0].id) Backup.delete(self.apiclient, backups[0].id)
Backup.delete(self.apiclient, backups[1].id) Backup.delete(self.apiclient, backups[1].id)
@attr(tags=["advanced", "backup"], required_hardware="true")
def test_vm_backup_lifecycle(self):
"""
Test VM backup lifecycle
"""
# Verify there are no backups for the VM
backups = Backup.list(self.apiclient, self.vm.id)
self.assertEqual(backups, None, "There should not exist any backup for the VM")
# Assign VM to offering and create ad-hoc backup
self.backup_offering.assignOffering(self.apiclient, self.vm.id)
Backup.create(self.apiclient, self.vm.id)
# Verify backup is created for the VM
backups = Backup.list(self.apiclient, self.vm.id)
self.assertEqual(len(backups), 1, "There should exist only one backup for the VM")
backup = backups[0]
# Delete backup
Backup.delete(self.apiclient, backup.id)
# Verify backup is deleted
backups = Backup.list(self.apiclient, self.vm.id)
self.assertEqual(backups, None, "There should not exist any backup for the VM")
# Remove VM from offering
self.backup_offering.removeOffering(self.apiclient, self.vm.id)
@attr(tags=["advanced", "backup"], required_hardware="true")
def test_vm_backup_create_vm_from_backup(self):
"""
Test creating a new VM from a backup
"""
self.destZone = self.zone
self.vm_backup_create_vm_from_backup_int()
@attr(tags=["advanced", "backup"], required_hardware="true")
def test_vm_backup_create_vm_from_backup_in_another_zone(self):
"""
Test creating a new VM from a backup in another zone
"""
cmd = listZones.listZonesCmd()
zones = self.apiclient.listZones(cmd)
if not isinstance(zones, list):
raise Exception("Failed to find zones.")
if len(zones) < 2:
self.skipTest("Skipping test due to there are less than two zones.")
return
self.destZone = zones[1]
template = get_template(self.api_client, self.destZone.id, self.services["ostype"])
list_isolated_network_offerings_response = NetworkOffering.list(
self.apiclient,
name="DefaultIsolatedNetworkOfferingWithSourceNatService"
)
isolated_network_offering_id = list_isolated_network_offerings_response[0].id
network = {
"name": "Network-",
"displaytext": "Network-"
}
network["name"] = self.account.name + " -destZone"
network["displaytext"] = self.account.name + " -destZone"
network = Network.create(
self.apiclient,
network,
accountid=self.account.name,
domainid=self.domain.id,
networkofferingid=isolated_network_offering_id,
zoneid=self.destZone.id
)
backup_repository = self.backup_repository.update(self.api_client, crosszoneinstancecreation=True)
self.assertEqual(backup_repository.crosszoneinstancecreation, True, "Cross-Zone Instance Creation could not be enabled on the backup repository")
self.vm_backup_create_vm_from_backup_int(template.id, [network.id])

View File

@ -6265,7 +6265,7 @@ class Backup:
return (apiclient.restoreVolumeFromBackupAndAttachToVM(cmd)) return (apiclient.restoreVolumeFromBackupAndAttachToVM(cmd))
@classmethod @classmethod
def createVMFromBackup(cls, apiclient, services, mode, backupid, accountname, domainid, zoneid, vmname=None): def createVMFromBackup(cls, apiclient, services, mode, backupid, accountname, domainid, zoneid, vmname=None, networkids=None, templateid=None):
"""Create new VM from backup """Create new VM from backup
""" """
cmd = createVMFromBackup.createVMFromBackupCmd() cmd = createVMFromBackup.createVMFromBackupCmd()
@ -6275,6 +6275,10 @@ class Backup:
cmd.zoneid = zoneid cmd.zoneid = zoneid
if vmname: if vmname:
cmd.name = vmname cmd.name = vmname
if networkids:
cmd.networkids = networkids
if templateid:
cmd.templateid = templateid
response = apiclient.createVMFromBackup(cmd) response = apiclient.createVMFromBackup(cmd)
virtual_machine = VirtualMachine(response.__dict__, []) virtual_machine = VirtualMachine(response.__dict__, [])
VirtualMachine.program_ssh_access(apiclient, services, mode, cmd.networkids, virtual_machine) VirtualMachine.program_ssh_access(apiclient, services, mode, cmd.networkids, virtual_machine)
@ -6346,6 +6350,14 @@ class BackupRepository:
cmd.id = self.id cmd.id = self.id
return (apiclient.deleteBackupRepository(cmd)) return (apiclient.deleteBackupRepository(cmd))
def update(self, apiclient, crosszoneinstancecreation):
"""Update backup repository"""
cmd = updateBackupRepository.updateBackupRepositoryCmd()
cmd.id = self.id
cmd.crosszoneinstancecreation = crosszoneinstancecreation
return (apiclient.updateBackupRepository(cmd))
def list(self, apiclient): def list(self, apiclient):
"""List backup repository""" """List backup repository"""

View File

@ -460,6 +460,7 @@
"label.backupofferingid": "Backup Offering ID", "label.backupofferingid": "Backup Offering ID",
"label.backupofferingname": "Backup Offering Name", "label.backupofferingname": "Backup Offering Name",
"label.backup.repository.add": "Add Backup Repository", "label.backup.repository.add": "Add Backup Repository",
"label.backup.repository.edit": "Edit Backup Repository",
"label.backup.repository.remove": "Remove Backup Repository", "label.backup.repository.remove": "Remove Backup Repository",
"label.balance": "Balance", "label.balance": "Balance",
"label.bandwidth": "Bandwidth", "label.bandwidth": "Bandwidth",
@ -927,6 +928,7 @@
"label.download.setting": "Download setting", "label.download.setting": "Download setting",
"label.download.state": "Download state", "label.download.state": "Download state",
"label.dpd": "Dead peer detection", "label.dpd": "Dead peer detection",
"label.crosszoneinstancecreation": "Cross-Zone Instance Creation",
"label.driver": "Driver", "label.driver": "Driver",
"label.drs": "DRS", "label.drs": "DRS",
"label.drsimbalance": "DRS imbalance", "label.drsimbalance": "DRS imbalance",
@ -2870,6 +2872,7 @@
"message.action.cancel.maintenance.mode": "Please confirm that you want to cancel this maintenance.", "message.action.cancel.maintenance.mode": "Please confirm that you want to cancel this maintenance.",
"message.action.create.snapshot.from.vmsnapshot": "Please confirm that you want to create Snapshot from Instance Snapshot", "message.action.create.snapshot.from.vmsnapshot": "Please confirm that you want to create Snapshot from Instance Snapshot",
"message.action.create.instance.from.backup": "Please confirm that you want to create a new Instance from the given Backup.<br>Click on configure to edit the parameters for the new Instance before creation.", "message.action.create.instance.from.backup": "Please confirm that you want to create a new Instance from the given Backup.<br>Click on configure to edit the parameters for the new Instance before creation.",
"message.create.instance.from.backup.different.zone": "Creating Instance from Backup on a different Zone. Please ensure that the backup repository is accessible in the selected Zone.",
"message.action.delete.asnrange": "Please confirm the AS range that you want to delete", "message.action.delete.asnrange": "Please confirm the AS range that you want to delete",
"message.action.delete.autoscale.vmgroup": "Please confirm that you want to delete this autoscaling group.", "message.action.delete.autoscale.vmgroup": "Please confirm that you want to delete this autoscaling group.",
"message.action.delete.backup.offering": "Please confirm that you want to delete this backup offering?", "message.action.delete.backup.offering": "Please confirm that you want to delete this backup offering?",
@ -2924,6 +2927,7 @@
"message.action.download.iso": "Please confirm that you want to download this ISO.", "message.action.download.iso": "Please confirm that you want to download this ISO.",
"message.action.download.snapshot": "Please confirm that you want to download this Snapshot.", "message.action.download.snapshot": "Please confirm that you want to download this Snapshot.",
"message.action.download.template": "Please confirm that you want to download this Template.", "message.action.download.template": "Please confirm that you want to download this Template.",
"message.action.edit.backup.repository": "Please confirm that you want to update the backup repository.",
"message.action.edit.nfs.mount.options": "Changes to NFS mount options will only take affect on cancelling maintenance mode which will cause the storage pool to be remounted on all KVM hosts with the new mount options.", "message.action.edit.nfs.mount.options": "Changes to NFS mount options will only take affect on cancelling maintenance mode which will cause the storage pool to be remounted on all KVM hosts with the new mount options.",
"message.action.enable.cluster": "Please confirm that you want to enable this Cluster.", "message.action.enable.cluster": "Please confirm that you want to enable this Cluster.",
"message.action.enable.disk.offering": "Please confirm that you want to enable this disk offering.", "message.action.enable.disk.offering": "Please confirm that you want to enable this disk offering.",

View File

@ -45,6 +45,69 @@
</div> </div>
</template> </template>
</a-step> </a-step>
<a-step
v-if="crossZoneInstanceCreationEnabled"
:title="$t('label.select.a.zone')"
status="process">
<template #description>
<div style="margin-top: 15px">
<a-form-item :label="$t('label.zoneid')" name="zoneid" ref="zoneid">
<div v-if="zones.length <= 8">
<a-row type="flex" :gutter="[16, 18]" justify="start">
<div v-for="(zoneItem, idx) in zones" :key="idx">
<a-radio-group
:key="idx"
v-model:value="form.zoneid"
@change="onSelectZoneId(zoneItem.id)">
<a-col :span="6">
<a-radio-button
:value="zoneItem.id"
style="border-width: 2px"
class="zone-radio-button">
<span>
<resource-icon
v-if="zoneItem && zoneItem.icon && zoneItem.icon.base64image"
:image="zoneItem.icon.base64image"
size="2x" />
<global-outlined size="2x" v-else />
{{ zoneItem.name }}
</span>
</a-radio-button>
</a-col>
</a-radio-group>
</div>
</a-row>
</div>
<a-select
v-else
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="onSelectZoneId"
:loading="loading.zones"
v-focus="true"
>
<a-select-option v-for="zone1 in zones" :key="zone1.id" :label="zone1.name">
<span>
<resource-icon v-if="zone1.icon && zone1.icon.base64image" :image="zone1.icon.base64image" size="2x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px" />
{{ zone1.name }}
</span>
</a-select-option>
</a-select>
</a-form-item>
<a-alert
v-if="isDifferentZoneFromBackup">
<template #message>
<div v-html="$t('message.create.instance.from.backup.different.zone')"></div>
</template>
</a-alert>
</div>
</template>
</a-step>
<a-step <a-step
v-if="!isNormalAndDomainUser" v-if="!isNormalAndDomainUser"
:title="$t('label.select.deployment.infrastructure')" :title="$t('label.select.deployment.infrastructure')"
@ -52,7 +115,6 @@
<template #description> <template #description>
<div style="margin-top: 15px"> <div style="margin-top: 15px">
<a-form-item <a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.podid')" :label="$t('label.podid')"
name="podid" name="podid"
ref="podid"> ref="podid">
@ -67,7 +129,6 @@
></a-select> ></a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.clusterid')" :label="$t('label.clusterid')"
name="clusterid" name="clusterid"
ref="clusterid"> ref="clusterid">
@ -82,7 +143,6 @@
></a-select> ></a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.hostid')" :label="$t('label.hostid')"
name="hostid" name="hostid"
ref="hostid"> ref="hostid">
@ -909,6 +969,12 @@ export default {
isNormalAndDomainUser () { isNormalAndDomainUser () {
return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype) return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype)
}, },
isDifferentZoneFromBackup () {
return this.selectedZone !== this.dataPreFill.zoneid
},
crossZoneInstanceCreationEnabled () {
return this.dataPreFill.crosszoneinstancecreation
},
isNormalUserOrProject () { isNormalUserOrProject () {
return ['User'].includes(this.$store.getters.userInfo.roletype) || store.getters.project.id return ['User'].includes(this.$store.getters.userInfo.roletype) || store.getters.project.id
}, },
@ -1488,21 +1554,12 @@ export default {
}) })
}, },
async fetchData () { async fetchData () {
const zones = await this.fetchZoneByQuery() this.fetchZones(null, null)
if (zones && zones.length === 1) {
this.selectedZone = zones[0]
this.dataPreFill.zoneid = zones[0]
}
if (this.dataPreFill.zoneid) {
this.fetchDataByZone(this.dataPreFill.zoneid)
} else {
this.fetchZones(null, zones)
_.each(this.params, (param, name) => { _.each(this.params, (param, name) => {
if (param.isLoad) { if (param.isLoad) {
this.fetchOptions(param, name) this.fetchOptions(param, name)
} }
}) })
}
this.fetchBootTypes() this.fetchBootTypes()
this.fetchBootModes() this.fetchBootModes()
this.fetchInstaceGroups() this.fetchInstaceGroups()
@ -1531,11 +1588,6 @@ export default {
} }
this.showOverrideDiskOfferingOption = val this.showOverrideDiskOfferingOption = val
}, },
async fetchDataByZone (zoneId) {
this.fillValue('zoneid')
this.options.zones = await this.fetchZones(zoneId)
this.onSelectZoneId(zoneId)
},
fetchBootTypes () { fetchBootTypes () {
this.options.bootTypes = [ this.options.bootTypes = [
{ id: 'BIOS', description: 'BIOS' }, { id: 'BIOS', description: 'BIOS' },
@ -2124,7 +2176,9 @@ export default {
if (name === 'zones') { if (name === 'zones') {
let zoneid = '' let zoneid = ''
if (this.$route.query.zoneid) { if (this.dataPreFill.zoneid) {
zoneid = this.dataPreFill.zoneid
} else if (this.$route.query.zoneid) {
zoneid = this.$route.query.zoneid zoneid = this.$route.query.zoneid
} else if (this.options.zones.length === 1) { } else if (this.options.zones.length === 1) {
zoneid = this.options.zones[0].id zoneid = this.options.zones[0].id
@ -2636,6 +2690,15 @@ export default {
margin: 0 0 1.2rem; margin: 0 0 1.2rem;
} }
.zone-radio-button {
width:100%;
min-width: 345px;
height: 60px;
display: flex;
padding-left: 20px;
align-items: center;
}
.vm-info-card { .vm-info-card {
.ant-card-body { .ant-card-body {
min-height: 250px; min-height: 250px;

View File

@ -141,7 +141,7 @@ export default {
permission: ['listBackupRepositories'], permission: ['listBackupRepositories'],
searchFilters: ['zoneid'], searchFilters: ['zoneid'],
columns: ['name', 'provider', 'type', 'address', 'zonename'], columns: ['name', 'provider', 'type', 'address', 'zonename'],
details: ['name', 'type', 'address', 'provider', 'zonename'], details: ['name', 'type', 'address', 'provider', 'zonename', 'crosszoneinstancecreation'],
actions: [ actions: [
{ {
api: 'addBackupRepository', api: 'addBackupRepository',
@ -149,7 +149,7 @@ export default {
label: 'label.backup.repository.add', label: 'label.backup.repository.add',
listView: true, listView: true,
args: [ args: [
'name', 'provider', 'address', 'type', 'mountopts', 'zoneid' 'name', 'provider', 'address', 'type', 'mountopts', 'zoneid', 'crosszoneinstancecreation'
], ],
mapping: { mapping: {
type: { type: {
@ -160,6 +160,15 @@ export default {
} }
} }
}, },
{
api: 'updateBackupRepository',
icon: 'edit-outlined',
label: 'label.backup.repository.edit',
message: 'message.action.edit.backup.repository',
args: ['name', 'address', 'mountopts', 'crosszoneinstancecreation'],
dataView: true,
popup: true
},
{ {
api: 'deleteBackupRepository', api: 'deleteBackupRepository',
icon: 'delete-outlined', icon: 'delete-outlined',

View File

@ -127,11 +127,15 @@ export default {
}, },
methods: { methods: {
onSelectTemplateIso () { onSelectTemplateIso () {
if (this.preFillContent?.allowtemplateisoselection) {
this.value = this.selected
} else {
if (this.inputDecorator === 'templateid') { if (this.inputDecorator === 'templateid') {
this.value = !this.preFillContent.templateid ? this.selected : this.preFillContent.templateid this.value = !this.preFillContent.templateid ? this.selected : this.preFillContent.templateid
} else { } else {
this.value = !this.preFillContent.isoid ? this.selected : this.preFillContent.isoid this.value = !this.preFillContent.isoid ? this.selected : this.preFillContent.isoid
} }
}
this.$emit('emit-update-template-iso', this.inputDecorator, this.value) this.$emit('emit-update-template-iso', this.inputDecorator, this.value)
}, },

View File

@ -443,7 +443,7 @@ export default {
fetchZoneData () { fetchZoneData () {
this.zones = [] this.zones = []
const params = {} const params = {}
if (this.resource.zoneid && this.$route.name === 'deployVirtualMachine') { if (this.resource.zoneid && (this.$route.name === 'deployVirtualMachine' || this.$route.path.startsWith('/backup'))) {
params.id = this.resource.zoneid params.id = this.resource.zoneid
} }
params.showicon = true params.showicon = true

View File

@ -264,7 +264,7 @@ export default {
fetchZoneData () { fetchZoneData () {
this.zones = [] this.zones = []
const params = {} const params = {}
if (this.resource.zoneid && this.$route.name === 'deployVirtualMachine') { if (this.resource.zoneid && (this.$route.name === 'deployVirtualMachine' || this.$route.path.startsWith('/backup'))) {
params.id = this.resource.zoneid params.id = this.resource.zoneid
} }
params.showicon = true params.showicon = true

View File

@ -106,7 +106,7 @@ export default {
fetchActionZoneData () { fetchActionZoneData () {
this.loading = true this.loading = true
const params = {} const params = {}
if (this.$route.name === 'deployVirtualMachine' && this.resource.zoneid) { if (this.resource.zoneid && (this.$route.name === 'deployVirtualMachine' || this.$route.path.startsWith('/backup'))) {
params.id = this.resource.zoneid params.id = this.resource.zoneid
} }
this.actionZoneLoading = true this.actionZoneLoading = true

View File

@ -640,7 +640,7 @@ export default {
} }
} else { } else {
const params = {} const params = {}
if (this.resource.zoneid && this.$route.name === 'deployVirtualMachine') { if (this.resource.zoneid && (this.$route.name === 'deployVirtualMachine' || this.$route.path.startsWith('/backup'))) {
params.id = this.resource.zoneid params.id = this.resource.zoneid
} }
params.showicon = true params.showicon = true

View File

@ -92,39 +92,52 @@ export default {
} }
}, },
created () { created () {
this.fetchBackupVmDetails().then(() => {
this.fetchServiceOffering() this.fetchServiceOffering()
this.fetchBackupOffering().then(() => {
this.fetchBackupRepository()
this.loading = false this.loading = false
}) })
}, },
methods: { methods: {
fetchBackupVmDetails () {
this.serviceOfferings = []
return getAPI('listBackups', {
id: this.resource.id,
listvmdetails: true
}).then(response => {
const backups = response.listbackupsresponse.backup || []
this.vmdetails = backups[0].vmdetails
})
},
fetchServiceOffering () { fetchServiceOffering () {
this.serviceOfferings = []
getAPI('listServiceOfferings', { getAPI('listServiceOfferings', {
zoneid: this.resource.zoneid, zoneid: this.resource.zoneid,
id: this.vmdetails.serviceofferingid, id: this.resource.vmdetails.serviceofferingid,
listall: true listall: true
}).then(response => { }).then(response => {
const serviceOfferings = response.listserviceofferingsresponse.serviceoffering || [] const serviceOfferings = response.listserviceofferingsresponse.serviceoffering || []
this.serviceOffering = serviceOfferings[0] this.serviceOffering = serviceOfferings[0]
}) })
}, },
fetchBackupOffering () {
return getAPI('listBackupOfferings', {
id: this.resource.backupofferingid,
listall: true
}).then(response => {
const backupOfferings = response.listbackupofferingsresponse.backupoffering || []
this.backupOffering = backupOfferings[0]
})
},
fetchBackupRepository () {
if (this.backupOffering.provider !== 'nas') {
return
}
getAPI('listBackupRepositories', {
id: this.backupOffering.externalid
}).then(response => {
const backupRepositories = response.listbackuprepositoriesresponse.backuprepository || []
this.backupRepository = backupRepositories[0]
})
},
populatePreFillData () { populatePreFillData () {
this.vmdetails = this.resource.vmdetails
this.dataPreFill.zoneid = this.resource.zoneid this.dataPreFill.zoneid = this.resource.zoneid
this.dataPreFill.crosszoneinstancecreation = this.backupRepository?.crosszoneinstancecreation || this.backupOffering.provider === 'dummy'
this.dataPreFill.isIso = (this.vmdetails.isiso === 'true') this.dataPreFill.isIso = (this.vmdetails.isiso === 'true')
this.dataPreFill.backupid = this.resource.id this.dataPreFill.backupid = this.resource.id
this.dataPreFill.computeofferingid = this.vmdetails.serviceofferingid this.dataPreFill.computeofferingid = this.vmdetails.serviceofferingid
this.dataPreFill.templateid = this.vmdetails.templateid this.dataPreFill.templateid = this.vmdetails.templateid
this.dataPreFill.allowtemplateisoselection = true
this.dataPreFill.isoid = this.vmdetails.templateid this.dataPreFill.isoid = this.vmdetails.templateid
this.dataPreFill.allowIpAddressesFetch = !this.resource.virtualmachineid this.dataPreFill.allowIpAddressesFetch = !this.resource.virtualmachineid
if (this.vmdetails.nics) { if (this.vmdetails.nics) {