mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 01:32:18 +02:00
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:
parent
b0c7719006
commit
23c9e83047
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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''');
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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])
|
||||
|
||||
@ -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"""
|
||||
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user