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

View File

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

View File

@ -139,6 +139,7 @@ public class ApiConstants {
public static final String CPU_SPEED = "cpuspeed";
public static final String CPU_LOAD_AVERAGE = "cpuloadaverage";
public static final String CREATED = "created";
public static final String CROSS_ZONE_INSTANCE_CREATION = "crosszoneinstancecreation";
public static final String CTX_ACCOUNT_ID = "ctxaccountid";
public static final String CTX_DETAILS = "ctxDetails";
public static final String CTX_USER_ID = "ctxuserid";

View File

@ -63,12 +63,14 @@ public class AddBackupRepositoryCmd extends BaseCmd {
type = CommandType.UUID,
entityType = ZoneResponse.class,
required = true,
description = "ID of the zone where the backup repository is to be added")
description = "ID of the zone where the backup repository is to be added for taking backups")
private Long zoneId;
@Parameter(name = ApiConstants.CAPACITY_BYTES, type = CommandType.LONG, description = "capacity of this backup repository")
private Long capacityBytes;
@Parameter(name = ApiConstants.CROSS_ZONE_INSTANCE_CREATION, type = CommandType.BOOLEAN, description = "backups on this repository can be used to create Instances on all Zones", since = "4.22.0")
private Boolean crossZoneInstanceCreation;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
@ -109,6 +111,10 @@ public class AddBackupRepositoryCmd extends BaseCmd {
return capacityBytes;
}
public Boolean crossZoneInstanceCreationEnabled() {
return crossZoneInstanceCreation;
}
/////////////////////////////////////////////////////
/////////////////// 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")
private Long capacityBytes;
@SerializedName(ApiConstants.CROSS_ZONE_INSTANCE_CREATION)
@Param(description = "the backups in this repository can be used to create Instances on all Zones")
private Boolean crossZoneInstanceCreation;
@SerializedName("created")
@Param(description = "the date and time the backup repository was added")
private Date created;
@ -132,6 +136,14 @@ public class BackupRepositoryResponse extends BaseResponse {
this.capacityBytes = capacityBytes;
}
public Boolean getCrossZoneInstanceCreation() {
return crossZoneInstanceCreation;
}
public void setCrossZoneInstanceCreation(Boolean crossZoneInstanceCreation) {
this.crossZoneInstanceCreation = crossZoneInstanceCreation;
}
public Date getCreated() {
return created;
}

View File

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

View File

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

View File

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

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

View File

@ -36,6 +36,7 @@ public class RestoreBackupCommand extends Command {
private Boolean vmExists;
private String restoreVolumeUUID;
private VirtualMachine.State vmState;
private Integer mountTimeout;
protected RestoreBackupCommand() {
super();
@ -136,4 +137,12 @@ public class RestoreBackupCommand extends Command {
public void setBackupVolumesUUIDs(List<String> 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);
}
if (params != null) {
Boolean returnAfterVolumePrepare = (Boolean) params.get(VirtualMachineProfile.Param.ReturnAfterVolumePrepare);
if (Boolean.TRUE.equals(returnAfterVolumePrepare)) {
logger.info("Returning from VM start command execution for VM {} as requested. Volumes are prepared and ready.", vm.getUuid());
if (!changeState(vm, Event.AgentReportStopped, destHostId, work, Step.Done)) {
logger.error("Unable to transition to a new state. VM uuid: {}, VM oldstate: {}, Event: {}", vm, vm.getState(), Event.AgentReportStopped);
throw new ConcurrentOperationException(String.format("Failed to deploy VM %s", vm));
}
logger.debug("Volume preparation completed for VM {} (VM state set to Stopped)", vm);
return;
}
}
if (!reuseVolume) {
reuseVolume = true;
}

View File

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

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

View File

@ -70,9 +70,18 @@ import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.apache.cloudstack.backup.BackupManager.BackupFrameworkEnabled;
public class NASBackupProvider extends AdapterBase implements BackupProvider, Configurable {
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
private BackupDao backupDao;
@ -115,30 +124,45 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
@Inject
private DiskOfferingDao diskOfferingDao;
protected Host getLastVMHypervisorHost(VirtualMachine vm) {
Long hostId = vm.getLastHostId();
if (hostId == null) {
LOG.debug("Cannot find last host for vm. This should never happen, please check your database.");
private Long getClusterIdFromRootVolume(VirtualMachine vm) {
VolumeVO rootVolume = volumeDao.getInstanceRootVolume(vm.getId());
StoragePoolVO rootDiskPool = primaryDataStoreDao.findById(rootVolume.getPoolId());
if (rootDiskPool == null) {
return null;
}
Host host = hostDao.findById(hostId);
return rootDiskPool.getClusterId();
}
if (host.getStatus() == Status.Up) {
return host;
} else {
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) {
return host;
}
// 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) {
LOG.debug("Found Host {} in cluster {}", hostInCluster, host.getClusterId());
LOG.debug("Found Host {} in cluster {}", hostInCluster, clusterId);
return hostInCluster;
}
}
}
// 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();
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()));
@ -158,7 +182,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
@Override
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());
if (backupRepository == null) {
@ -249,16 +273,16 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
}
@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);
}
@Override
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()
.sorted(Comparator.comparingLong(Backup.VolumeInfo::getDeviceId))
.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);
BackupRepository backupRepository = getBackupRepository(backup);
final Host host = getLastVMHypervisorHost(vm);
final Host host = getVMHypervisorHost(vm);
RestoreBackupCommand restoreCommand = new RestoreBackupCommand();
restoreCommand.setBackupPath(backup.getExternalId());
restoreCommand.setBackupRepoType(backupRepository.getType());
@ -282,6 +306,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
restoreCommand.setRestoreVolumePaths(getVolumePaths(restoreVolumes));
restoreCommand.setVmExists(vm.getRemoved() == null);
restoreCommand.setVmState(vm.getState());
restoreCommand.setMountTimeout(NASBackupRestoreMountTimeout.value());
BackupAnswer answer;
try {
@ -291,7 +316,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
} catch (OperationTimedoutException e) {
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) {
@ -398,7 +423,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
final Host host;
final VirtualMachine vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId());
if (vm != null) {
host = getLastVMHypervisorHost(vm);
host = getVMHypervisorHost(vm);
} else {
host = resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, backup.getZoneId());
}
@ -513,9 +538,19 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
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
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey[]{
NASBackupRestoreMountTimeout
};
}

View File

@ -21,11 +21,9 @@ import static org.mockito.Mockito.mock;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
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.Test;
import org.junit.runner.RunWith;
@ -39,6 +37,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.agent.AgentManager;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
@ -51,6 +50,12 @@ import com.cloud.utils.Pair;
import com.cloud.vm.VMInstanceVO;
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)
public class NASBackupProviderTest {
@Spy
@ -84,6 +89,9 @@ public class NASBackupProviderTest {
@Mock
private ResourceManager resourceManager;
@Mock
private PrimaryDataStoreDao storagePoolDao;
@Test
public void testDeleteBackup() throws OperationTimedoutException, AgentUnavailableException {
Long hostId = 1L;
@ -94,7 +102,7 @@ public class NASBackupProviderTest {
ReflectionTestUtils.setField(backup, "id", 1L);
BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 1024L);
"nfs", "address", "sync", 1024L, null);
VMInstanceVO vm = mock(VMInstanceVO.class);
Mockito.when(vm.getLastHostId()).thenReturn(hostId);
@ -113,7 +121,7 @@ public class NASBackupProviderTest {
@Test
public void testSyncBackupStorageStats() throws AgentUnavailableException, OperationTimedoutException {
BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 1024L);
"nfs", "address", "sync", 1024L, null);
HostVO host = mock(HostVO.class);
Mockito.when(resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, 1L)).thenReturn(host);
@ -132,7 +140,7 @@ public class NASBackupProviderTest {
@Test
public void testListBackupOfferings() {
BackupRepositoryVO backupRepository = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 1024L);
"nfs", "address", "sync", 1024L, null);
ReflectionTestUtils.setField(backupRepository, "uuid", "uuid");
Mockito.when(backupRepositoryDao.listByZoneAndProvider(1L, "nas")).thenReturn(Collections.singletonList(backupRepository));
@ -146,11 +154,11 @@ public class NASBackupProviderTest {
@Test
public void testGetBackupStorageStats() {
BackupRepositoryVO backupRepository1 = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 1000L);
"nfs", "address", "sync", 1000L, null);
backupRepository1.setUsedBytes(500L);
BackupRepositoryVO backupRepository2 = new BackupRepositoryVO(1L, "nas", "test-repo",
"nfs", "address", "sync", 2000L);
"nfs", "address", "sync", 2000L, null);
backupRepository2.setUsedBytes(600L);
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(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
public String getName() {
return "networker";
@ -630,7 +635,7 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid
public boolean willDeleteBackupsOnOfferingRemoval() { return false; }
@Override
public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) {
return true;
public Pair<Boolean, String> restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) {
return new Pair<>(true, null);
}
}

View File

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

View File

@ -61,42 +61,49 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
List<String> backedVolumeUUIDs = command.getBackupVolumesUUIDs();
List<String> restoreVolumePaths = command.getRestoreVolumePaths();
String restoreVolumeUuid = command.getRestoreVolumeUUID();
Integer mountTimeout = command.getMountTimeout() * 1000;
String newVolumeId = null;
try {
String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions, mountTimeout);
if (Objects.isNull(vmExists)) {
String volumePath = restoreVolumePaths.get(0);
int lastIndex = volumePath.lastIndexOf("/");
newVolumeId = volumePath.substring(lastIndex + 1);
restoreVolume(backupPath, backupRepoType, backupRepoAddress, volumePath, diskType, restoreVolumeUuid,
new Pair<>(vmName, command.getVmState()), mountOptions);
restoreVolume(backupPath, volumePath, diskType, restoreVolumeUuid,
new Pair<>(vmName, command.getVmState()), mountDirectory);
} else if (Boolean.TRUE.equals(vmExists)) {
restoreVolumesOfExistingVM(restoreVolumePaths, backedVolumeUUIDs, backupPath, backupRepoType, backupRepoAddress, mountOptions);
restoreVolumesOfExistingVM(restoreVolumePaths, backedVolumeUUIDs, backupPath, mountDirectory);
} else {
restoreVolumesOfDestroyedVMs(restoreVolumePaths, vmName, backupPath, backupRepoType, backupRepoAddress, mountOptions);
restoreVolumesOfDestroyedVMs(restoreVolumePaths, vmName, backupPath, mountDirectory);
}
} catch (CloudRuntimeException e) {
String errorMessage = "Failed to restore backup for VM: " + vmName + ".";
if (e.getMessage() != null && !e.getMessage().isEmpty()) {
errorMessage += " Details: " + e.getMessage();
}
logger.error(errorMessage);
String errorMessage = e.getMessage() != null ? e.getMessage() : "";
return new BackupAnswer(command, false, errorMessage);
}
return new BackupAnswer(command, true, newVolumeId);
}
private void restoreVolumesOfExistingVM(List<String> restoreVolumePaths, List<String> backedVolumesUUIDs, String backupPath,
String backupRepoType, String backupRepoAddress, String mountOptions) {
private void verifyBackupFile(String backupPath, String volUuid) {
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 mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
try {
for (int idx = 0; idx < restoreVolumePaths.size(); idx++) {
String restoreVolumePath = restoreVolumePaths.get(idx);
String backupVolumeUuid = backedVolumesUUIDs.get(idx);
Pair<String, String> bkpPathAndVolUuid = getBackupPath(mountDirectory, null, backupPath, diskType, backupVolumeUuid);
diskType = "datadisk";
verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second());
if (!replaceVolumeWithBackup(restoreVolumePath, bkpPathAndVolUuid.first())) {
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,
String backupRepoType, String backupRepoAddress, String mountOptions) {
String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
private void restoreVolumesOfDestroyedVMs(List<String> volumePaths, String vmName, String backupPath, String mountDirectory) {
String diskType = "root";
try {
for (int i = 0; i < volumePaths.size(); i++) {
String volumePath = volumePaths.get(i);
Pair<String, String> bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, null);
diskType = "datadisk";
verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second());
if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) {
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,
String diskType, String volumeUUID, Pair<String, VirtualMachine.State> vmNameAndState, String mountOptions) {
String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
private void restoreVolume(String backupPath, String volumePath, String diskType, String volumeUUID,
Pair<String, VirtualMachine.State> vmNameAndState, String mountDirectory) {
Pair<String, String> bkpPathAndVolUuid;
try {
bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID);
verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second());
if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) {
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()));
}
}
} catch (Exception e) {
throw new CloudRuntimeException("Failed to restore volume", e);
} finally {
unmountBackupDirectory(mountDirectory);
deleteTemporaryDirectory(mountDirectory);
@ -149,35 +153,43 @@ 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 mountDirectory = String.format("%s.%s",BACKUP_TEMP_FILE_PREFIX , randomChars);
try {
mountDirectory = Files.createTempDirectory(mountDirectory).toString();
String mount = String.format(MOUNT_COMMAND, backupRepoType, backupRepoAddress, mountDirectory);
if ("cifs".equals(backupRepoType)) {
if (Objects.isNull(mountOptions) || mountOptions.trim().isEmpty()) {
mountOptions = "nobrl";
} else {
mountOptions += ",nobrl";
}
} 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);
if ("cifs".equals(backupRepoType)) {
if (Objects.isNull(mountOptions) || mountOptions.trim().isEmpty()) {
mountOptions = "nobrl";
} else {
mountOptions += ",nobrl";
}
if (Objects.nonNull(mountOptions) && !mountOptions.trim().isEmpty()) {
mount += " -o " + mountOptions;
}
Script.runSimpleBashScript(mount);
} catch (Exception e) {
throw new CloudRuntimeException(String.format("Failed to mount %s to %s", backupRepoType, backupRepoAddress), e);
}
if (Objects.nonNull(mountOptions) && !mountOptions.trim().isEmpty()) {
mount += " -o " + mountOptions;
}
int exitValue = Script.runSimpleBashScriptForExitValue(mount, mountTimeout, false);
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;
}
private void unmountBackupDirectory(String backupDirectory) {
try {
String umountCmd = String.format(UMOUNT_COMMAND, backupDirectory);
Script.runSimpleBashScript(umountCmd);
} catch (Exception e) {
throw new CloudRuntimeException(String.format("Failed to unmount backup directory: %s", backupDirectory), e);
String umountCmd = String.format(UMOUNT_COMMAND, backupDirectory);
int exitValue = Script.runSimpleBashScriptForExitValue(umountCmd);
if (exitValue != 0) {
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 {
Files.deleteIfExists(Paths.get(backupDirectory));
} 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);
}
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) {
int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath));
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.setType(backupRepository.getType());
response.setCapacityBytes(backupRepository.getCapacityBytes());
response.setCrossZoneInstanceCreation(backupRepository.crossZoneInstanceCreationEnabled());
response.setObjectName("backuprepository");
DataCenter zone = ApiDBUtils.findZoneById(backupRepository.getZoneId());
if (zone != null) {

View File

@ -9478,24 +9478,27 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Override
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());
if (backup == null) {
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());
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());
UserVmVO backupVm = _vmDao.findByIdIncludingRemoved(backup.getVmId());
@ -9594,7 +9597,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
List<Long> networkIds = cmd.getNetworkIds();
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)) {
networkIds = new ArrayList<>(userVmNetworkMap.values());
}
@ -9605,7 +9608,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
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);
if (vm != null && vmSettingsFromBackup != null) {
@ -9629,20 +9632,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Override
public UserVm restoreVMFromBackup(CreateVMFromBackupCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException {
long vmId = cmd.getEntityId();
UserVm vm;
Map<Long, DiskOffering> diskOfferingMap = cmd.getDataDiskTemplateToDiskOfferingMap();
Map<VirtualMachineProfile.Param, Object> additonalParams = new HashMap<>();
UserVm vm;
additonalParams.put(VirtualMachineProfile.Param.ReturnAfterVolumePrepare, true);
try {
vm = startVirtualMachine(vmId, null, null, null, diskOfferingMap, additonalParams, null);
boolean status = stopVirtualMachine(CallContext.current().getCallingUserId(), vm.getId()) ;
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");
}
Pair<UserVmVO, Map<VirtualMachineProfile.Param, Object>> vmParamPair = null;
vmParamPair = startVirtualMachine(vmId, null, null, null, additonalParams, null);
vm = vmParamPair.first();
Long isoId = vm.getIsoId();
if (isoId != null) {
@ -9653,7 +9651,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
backupManager.restoreBackupToVM(cmd.getBackupId(), vmId);
} catch (CloudRuntimeException e) {
} catch (CloudRuntimeException | ResourceUnavailableException | ResourceAllocationException | InsufficientCapacityException e) {
UserVmVO vmVO = _vmDao.findById(vmId);
try {
expunge(vmVO);
@ -9680,6 +9678,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
podId = adminCmd.getPodId();
clusterId = adminCmd.getClusterId();
}
additonalParams.remove(VirtualMachineProfile.Param.ReturnAfterVolumePrepare);
vm = startVirtualMachine(vmId, podId, clusterId, cmd.getHostId(), diskOfferingMap, additonalParams, cmd.getDeploymentPlanner());
}
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.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.api.command.user.vm.CreateVMFromBackupCmd;
import org.apache.cloudstack.api.response.BackupResponse;
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.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
@ -1022,7 +1022,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
if (offering == null) {
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);
updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready);
updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped);
@ -1239,6 +1239,14 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
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
public Boolean canCreateInstanceFromBackup(final Long backupId) {
final BackupVO backup = backupDao.findById(backupId);
@ -1251,7 +1259,18 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
}
@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);
if (backup == null) {
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.");
}
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;
try {
updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring);
@ -1310,18 +1330,21 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
host = restoreInfo.first().getPrivateIpAddress();
dataStore = restoreInfo.second().getUuid();
}
if (!backupProvider.restoreBackupToVM(vm, backup, host, dataStore)) {
throw new CloudRuntimeException(String.format("Error restoring backup [%s] to VM %s.", backupDetailsInMessage, vm.getUuid()));
}
result = backupProvider.restoreBackupToVM(vm, backup, host, dataStore);
} catch (Exception e) {
updateVolumeState(vm, Volume.Event.RestoreFailed, Volume.State.Ready);
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped);
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);
logger.error(String.format("Failed to create Instance [%s] from backup [%s] due to: [%s]", vm.getInstanceName(), backupDetailsInMessage, e.getMessage()), e);
processRestoreBackupToVMFailure(vm, backup, eventId);
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);
updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped);
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(RestoreVolumeFromBackupAndAttachToVMCmd.class);
cmdList.add(AddBackupRepositoryCmd.class);
cmdList.add(UpdateBackupRepositoryCmd.class);
cmdList.add(DeleteBackupRepositoryCmd.class);
cmdList.add(ListBackupRepositoriesCmd.class);
cmdList.add(CreateVMFromBackupCmd.class);

View File

@ -19,6 +19,8 @@
package org.apache.cloudstack.backup;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.user.AccountManager;
import com.cloud.utils.Pair;
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.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.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.inject.Inject;
import java.util.ArrayList;
@ -50,12 +54,59 @@ public class BackupRepositoryServiceImpl extends ManagerBase implements BackupRe
private AccountManager accountManager;
@Override
@ActionEvent(eventType = EventTypes.EVENT_BACKUP_REPOSITORY_ADD, eventDescription = "add backup repository")
public BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd) {
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);
}
@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
public boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd) {
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());
when(userVmDao.findById(vmId)).thenReturn(vm);
when(templateDao.findByIdIncludingRemoved(templateId)).thenReturn(mock(VMTemplateVO.class));
when(userVmManagerImpl.stopVirtualMachine(anyLong(), anyLong())).thenReturn(true);
UserVm result = userVmManagerImpl.restoreVMFromBackup(cmd);
@ -3908,4 +3907,129 @@ public class UserVmManagerImplTest {
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.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
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.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -1275,7 +1276,7 @@ public class BackupManagerTest {
when(rootVolume.getPoolId()).thenReturn(poolId);
when(volumeDao.findIncludingRemovedByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(List.of(rootVolume));
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)) {
boolean result = backupManager.restoreBackupToVM(backupId, vmId);
@ -1284,7 +1285,7 @@ public class BackupManagerTest {
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.RestoringSuccess, hostId);
} catch (ResourceUnavailableException e) {
} catch (CloudRuntimeException e) {
fail("Test failed due to exception" + e);
}
}
@ -1331,7 +1332,7 @@ public class BackupManagerTest {
when(rootVolume.getPoolId()).thenReturn(poolId);
when(volumeDao.findIncludingRemovedByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(List.of(rootVolume));
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)) {
CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class,
@ -1813,4 +1814,269 @@ public class BackupManagerTest {
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
# under the License.
from marvin.cloudstackAPI import listZones
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.lib.utils import (cleanup_resources)
from marvin.lib.base import (Account, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering,
BackupRepository, Backup, Configurations, Volume, StoragePool)
from marvin.lib.base import (Account, Network, ServiceOffering, DiskOffering, VirtualMachine, BackupOffering,
NetworkOffering, BackupRepository, Backup, Configurations, Volume, StoragePool)
from marvin.lib.common import (get_domain, get_zone, get_template)
from nose.plugins.attrib import attr
from marvin.codes import FAILED
@ -109,40 +110,7 @@ class TestNASBackupAndRecovery(cloudstackTestCase):
except Exception as e:
raise Exception("Warning: Exception during cleanup : %s" % e)
@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
"""
def vm_backup_create_vm_from_backup_int(self, templateid=None, networkids=None):
self.backup_offering.assignOffering(self.apiclient, self.vm.id)
# Create a file and take backup
@ -178,7 +146,9 @@ class TestNASBackupAndRecovery(cloudstackTestCase):
vmname=new_vm_name,
accountname=self.account.name,
domainid=self.account.domainid,
zoneid=self.zone.id
zoneid=self.destZone.id,
networkids=networkids,
templateid=templateid
)
self.cleanup.append(new_vm)
@ -194,7 +164,7 @@ class TestNASBackupAndRecovery(cloudstackTestCase):
"New VM should have the correct service offering")
# 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)
volumes = Volume.list(
@ -217,3 +187,81 @@ class TestNASBackupAndRecovery(cloudstackTestCase):
# Delete backups
Backup.delete(self.apiclient, backups[0].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))
@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
"""
cmd = createVMFromBackup.createVMFromBackupCmd()
@ -6275,6 +6275,10 @@ class Backup:
cmd.zoneid = zoneid
if vmname:
cmd.name = vmname
if networkids:
cmd.networkids = networkids
if templateid:
cmd.templateid = templateid
response = apiclient.createVMFromBackup(cmd)
virtual_machine = VirtualMachine(response.__dict__, [])
VirtualMachine.program_ssh_access(apiclient, services, mode, cmd.networkids, virtual_machine)
@ -6346,6 +6350,14 @@ class BackupRepository:
cmd.id = self.id
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):
"""List backup repository"""

View File

@ -460,6 +460,7 @@
"label.backupofferingid": "Backup Offering ID",
"label.backupofferingname": "Backup Offering Name",
"label.backup.repository.add": "Add Backup Repository",
"label.backup.repository.edit": "Edit Backup Repository",
"label.backup.repository.remove": "Remove Backup Repository",
"label.balance": "Balance",
"label.bandwidth": "Bandwidth",
@ -927,6 +928,7 @@
"label.download.setting": "Download setting",
"label.download.state": "Download state",
"label.dpd": "Dead peer detection",
"label.crosszoneinstancecreation": "Cross-Zone Instance Creation",
"label.driver": "Driver",
"label.drs": "DRS",
"label.drsimbalance": "DRS imbalance",
@ -2870,6 +2872,7 @@
"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.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.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?",
@ -2924,6 +2927,7 @@
"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.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.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.",

View File

@ -45,6 +45,69 @@
</div>
</template>
</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
v-if="!isNormalAndDomainUser"
:title="$t('label.select.deployment.infrastructure')"
@ -52,7 +115,6 @@
<template #description>
<div style="margin-top: 15px">
<a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.podid')"
name="podid"
ref="podid">
@ -67,7 +129,6 @@
></a-select>
</a-form-item>
<a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.clusterid')"
name="clusterid"
ref="clusterid">
@ -82,7 +143,6 @@
></a-select>
</a-form-item>
<a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.hostid')"
name="hostid"
ref="hostid">
@ -909,6 +969,12 @@ export default {
isNormalAndDomainUser () {
return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype)
},
isDifferentZoneFromBackup () {
return this.selectedZone !== this.dataPreFill.zoneid
},
crossZoneInstanceCreationEnabled () {
return this.dataPreFill.crosszoneinstancecreation
},
isNormalUserOrProject () {
return ['User'].includes(this.$store.getters.userInfo.roletype) || store.getters.project.id
},
@ -1488,21 +1554,12 @@ export default {
})
},
async fetchData () {
const zones = await this.fetchZoneByQuery()
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) => {
if (param.isLoad) {
this.fetchOptions(param, name)
}
})
}
this.fetchZones(null, null)
_.each(this.params, (param, name) => {
if (param.isLoad) {
this.fetchOptions(param, name)
}
})
this.fetchBootTypes()
this.fetchBootModes()
this.fetchInstaceGroups()
@ -1531,11 +1588,6 @@ export default {
}
this.showOverrideDiskOfferingOption = val
},
async fetchDataByZone (zoneId) {
this.fillValue('zoneid')
this.options.zones = await this.fetchZones(zoneId)
this.onSelectZoneId(zoneId)
},
fetchBootTypes () {
this.options.bootTypes = [
{ id: 'BIOS', description: 'BIOS' },
@ -2124,7 +2176,9 @@ export default {
if (name === 'zones') {
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
} else if (this.options.zones.length === 1) {
zoneid = this.options.zones[0].id
@ -2636,6 +2690,15 @@ export default {
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 {
.ant-card-body {
min-height: 250px;

View File

@ -141,7 +141,7 @@ export default {
permission: ['listBackupRepositories'],
searchFilters: ['zoneid'],
columns: ['name', 'provider', 'type', 'address', 'zonename'],
details: ['name', 'type', 'address', 'provider', 'zonename'],
details: ['name', 'type', 'address', 'provider', 'zonename', 'crosszoneinstancecreation'],
actions: [
{
api: 'addBackupRepository',
@ -149,7 +149,7 @@ export default {
label: 'label.backup.repository.add',
listView: true,
args: [
'name', 'provider', 'address', 'type', 'mountopts', 'zoneid'
'name', 'provider', 'address', 'type', 'mountopts', 'zoneid', 'crosszoneinstancecreation'
],
mapping: {
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',
icon: 'delete-outlined',

View File

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

View File

@ -443,7 +443,7 @@ export default {
fetchZoneData () {
this.zones = []
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.showicon = true

View File

@ -264,7 +264,7 @@ export default {
fetchZoneData () {
this.zones = []
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.showicon = true

View File

@ -106,7 +106,7 @@ export default {
fetchActionZoneData () {
this.loading = true
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
}
this.actionZoneLoading = true

View File

@ -640,7 +640,7 @@ export default {
}
} else {
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.showicon = true

View File

@ -92,39 +92,52 @@ export default {
}
},
created () {
this.fetchBackupVmDetails().then(() => {
this.fetchServiceOffering()
this.fetchServiceOffering()
this.fetchBackupOffering().then(() => {
this.fetchBackupRepository()
this.loading = false
})
},
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 () {
this.serviceOfferings = []
getAPI('listServiceOfferings', {
zoneid: this.resource.zoneid,
id: this.vmdetails.serviceofferingid,
id: this.resource.vmdetails.serviceofferingid,
listall: true
}).then(response => {
const serviceOfferings = response.listserviceofferingsresponse.serviceoffering || []
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 () {
this.vmdetails = this.resource.vmdetails
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.backupid = this.resource.id
this.dataPreFill.computeofferingid = this.vmdetails.serviceofferingid
this.dataPreFill.templateid = this.vmdetails.templateid
this.dataPreFill.allowtemplateisoselection = true
this.dataPreFill.isoid = this.vmdetails.templateid
this.dataPreFill.allowIpAddressesFetch = !this.resource.virtualmachineid
if (this.vmdetails.nics) {