Skip to content

[Veeam] Restore only a specified volume #7221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java
Original file line number Diff line number Diff line change
@@ -95,7 +95,7 @@ public interface HypervisorGuru extends Adapter {
VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId,
String vmInternalName, Backup backup) throws Exception;

boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo,
boolean attachRestoredVolumeToVirtualMachine(long zoneId, String restoredVolumeLocation, Backup.VolumeInfo volumeInfo,
VirtualMachine vm, long poolId, Backup backup) throws Exception;
/**
* Will generate commands to migrate a vm to a pool. For now this will only work for stopped VMs on Vmware.
Original file line number Diff line number Diff line change
@@ -135,6 +135,12 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer

/**
* Restore a backed up volume and attach it to a VM
*
* @param backedUpVolumeUuid volume to be restored
* @param backupId backup containing the volume to be restored
* @param vmId VM to attach restored volume
* @return returns operation success
* @throws Exception
*/
boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId) throws Exception;

Original file line number Diff line number Diff line change
@@ -93,7 +93,7 @@ public interface BackupProvider {
/**
* Restore a volume from a backup
*/
Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeUuid, String hostIp, String dataStoreUuid);
Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeUuid, String host, String dataStore, VirtualMachine vm);

/**
* Returns backup metrics for a list of VMs in a zone
Original file line number Diff line number Diff line change
@@ -76,7 +76,7 @@ public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) {
}

@Override
public Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeUuid, String hostIp, String dataStoreUuid) {
public Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeUuid, String host, String dataStore, VirtualMachine vm) {
logger.debug("Restoring volume " + volumeUuid + "from backup " + backup.getUuid() + " on the Dummy Backup Provider");
throw new CloudRuntimeException("Dummy plugin does not support this feature");
}
Original file line number Diff line number Diff line change
@@ -372,12 +372,12 @@ public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) {
}

@Override
public Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeUuid, String hostIp, String dataStoreUuid) {
public Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeUuid, String host, String dataStore, VirtualMachine vm) {
String networkerServer;
VolumeVO volume = volumeDao.findByUuid(volumeUuid);
VMInstanceVO backupSourceVm = vmInstanceDao.findById(backup.getVmId());
StoragePoolHostVO dataStore = storagePoolHostDao.findByUuid(dataStoreUuid);
HostVO hostVO = hostDao.findByIp(hostIp);
StoragePoolHostVO datastore = storagePoolHostDao.findByUuid(dataStore);
HostVO hostVO = hostDao.findByIp(host);

final Long zoneId = backup.getZoneId();
final String externalBackupId = backup.getExternalId();
@@ -440,7 +440,7 @@ public Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeU
script.add("-n");
script.add(restoredVolume.getUuid());
script.add("-p");
script.add(dataStore.getLocalPath());
script.add(datastore.getLocalPath());
script.add("-a");
script.add(volume.getUuid());

Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
import java.util.Objects;
import java.util.stream.Collectors;

import com.google.gson.Gson;
import javax.inject.Inject;

import org.apache.cloudstack.api.ApiCommandResourceType;
@@ -37,7 +38,9 @@
import org.apache.cloudstack.backup.veeam.api.Job;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.BooleanUtils;

import com.cloud.agent.AgentManager;
@@ -51,6 +54,9 @@
import com.cloud.dc.dao.VmwareDatacenterDao;
import com.cloud.hypervisor.vmware.dao.VmwareDatacenterZoneMapDao;
import com.cloud.user.User;
import com.cloud.serializer.GsonHelper;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.utils.Pair;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.db.Transaction;
@@ -64,6 +70,7 @@

public class VeeamBackupProvider extends AdapterBase implements BackupProvider, Configurable {

private static final Gson GSON = GsonHelper.getGson();
public static final String BACKUP_IDENTIFIER = "-CSBKP-";

public ConfigKey<String> VeeamUrl = new ConfigKey<>("Advanced", String.class,
@@ -109,6 +116,8 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
private AgentManager agentMgr;
@Inject
private VirtualMachineManager virtualMachineManager;
@Inject
private VolumeDao volumeDao;

protected VeeamClient getClient(final Long zoneId) {
try {
@@ -291,10 +300,31 @@ private void prepareForBackupRestoration(VirtualMachine vm) {
}

@Override
public Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeUuid, String hostIp, String dataStoreUuid) {
public Pair<Boolean, String> restoreBackedUpVolume(Backup backup, String volumeUuid, String host, String dataStore, VirtualMachine vm) {
Pair<Boolean, String> result = new Pair<>(false, "");
final Long zoneId = backup.getZoneId();
final String restorePointId = backup.getExternalId();
return getClient(zoneId).restoreVMToDifferentLocation(restorePointId, hostIp, dataStoreUuid);

VMInstanceVO vmVO = vmInstanceDao.findById(backup.getVmId());
VolumeVO volumeVO = volumeDao.findByUuid(volumeUuid);
long totalDeviceIds = volumeDao.findByInstance(vm.getId()).stream().mapToLong(VolumeVO::getDeviceId).max().orElse(0L);
long newDeviceId = totalDeviceIds + 1;
logger.debug("VM [{}] has [{}] deviceIds. Trying to restore volume [{}] using restorePoint [{}] and with [{}] as the new deviceId.", vm.getUuid(),
totalDeviceIds, volumeUuid, restorePointId, newDeviceId);

VirtualMachineDiskInfo fromJson = GSON.fromJson(volumeVO.getChainInfo(), VirtualMachineDiskInfo.class);
String type = fromJson.getControllerFromDeviceBusName().toUpperCase();
String virtualDeviceNode = StringUtils.substringAfter(fromJson.getDiskDeviceBusName(), ":");
for (String name : fromJson.getDiskChain()) {
String diskName = StringUtils.substringAfter(name, "/");
try {
result = getClient(zoneId).restoreVolume(volumeUuid, vmVO.getUuid(), restorePointId, host, dataStore, type, virtualDeviceNode, diskName, newDeviceId, vm);
} catch (Exception e) {
logger.error("Failed to restore volume [{}] in VM [{}], with type [{}], node [{}] and disk name [{}], using target host [{}] and datastore [{}] due to [{}].",
volumeUuid, vmVO.getUuid(), type, virtualDeviceNode, diskName, host, dataStore, e.getMessage(), e);
}
}
return result;
}

@Override
Original file line number Diff line number Diff line change
@@ -84,9 +84,11 @@

import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.UuidUtils;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.nio.TrustAllManager;
import com.cloud.utils.ssh.SshHelper;
import com.cloud.vm.VirtualMachine;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
@@ -913,14 +915,25 @@ private Date formatDate(String date) throws ParseException {
return dateFormat.parse(StringUtils.substring(date, 0, 19));
}

public Pair<Boolean, String> restoreVMToDifferentLocation(String restorePointId, String hostIp, String dataStoreUuid) {
protected String removeDashesIfDatastoreNameIsUuid(String datastore) {
if (!UuidUtils.isUuid(datastore)) {
return datastore;
}
logger.trace("Removing the dash symbol of datastore name [{}] because this name is a valid UUID used by ACS. This happens because when a new NFS storage is created via ACS, "
+ "this storage is created in vCenter with a name that uses the UUID in ACS, but removing the dashes. "
+ "However, if the storage is created first in vCenter (e.g. VMFS), and its name contains dashes, if this name is not a valid UUID, the dashes will be keeped. But, if for some "
+ "reason the name is a valid UUID, this causes ACS to remove the dashes, and then the restore backup fails, because Veeam does not find the datastore.", datastore);
return datastore.replace("-","");
}

public Pair<Boolean, String> restoreVMToDifferentLocation(String restorePointId, String host, String datastore) {
final String restoreLocation = RESTORE_VM_SUFFIX + UUID.randomUUID().toString();
final String datastoreId = dataStoreUuid.replace("-","");
datastore = removeDashesIfDatastoreNameIsUuid(datastore);
final List<String> cmds = Arrays.asList(
"$points = Get-VBRRestorePoint",
String.format("foreach($point in $points) { if ($point.Id -eq '%s') { break; } }", restorePointId),
String.format("$server = Get-VBRServer -Name \"%s\"", hostIp),
String.format("$ds = Find-VBRViDatastore -Server:$server -Name \"%s\"", datastoreId),
String.format("$server = Get-VBRServer -Name \"%s\"", host),
String.format("$ds = Find-VBRViDatastore -Server:$server -Name \"%s\"", datastore),
String.format("$job = Start-VBRRestoreVM -RestorePoint:$point -Server:$server -Datastore:$ds -VMName \"%s\" -RunAsync", restoreLocation),
"while (-not (Get-VBRRestoreSession -Id $job.Id).IsCompleted) { Start-Sleep -Seconds 10 }"
);
@@ -934,4 +947,43 @@ public Pair<Boolean, String> restoreVMToDifferentLocation(String restorePointId,
private boolean isLegacyServer() {
return this.veeamServerVersion != null && (this.veeamServerVersion > 0 && this.veeamServerVersion < 11);
}

public Pair<Boolean, String> restoreVolume(String volumeUuid, String vmUuid, String restorePointId, String host, String datastore, String diskType, String diskDeviceNode,
String diskName, long device, VirtualMachine vm) {
datastore = removeDashesIfDatastoreNameIsUuid(datastore);
if (diskType.equals("IDE")) {
String veeamLink = "https://www.veeam.com/kb1100";
logger.warn("Veeam does not support restore of IDE virtual devices disks. We will use the type SCSI instead. "
+ "For more information, please see this link [{}].", veeamLink);
}
final List<String> cmds = Arrays.asList(
String.format("$point = Get-VBRRestorePoint -Id '%s'", restorePointId),
"if ($point) {",
String.format("$vm = Find-VBRViEntity -Name '%s'", vm.getInstanceName()),
String.format("$server = Get-VBRServer -Name \"%s\"", host),
String.format("$ds = Find-VBRViDatastore -Server:$server -Name \"%s\"", datastore),
String.format("$disk = Get-VBRViVirtualDevice -RestorePoint:$point ^| Where-Object { $_.Type -eq '%s' -and $_.VirtualDeviceNode -eq '%s' -and $_.Name -eq '%s' }", diskType, diskDeviceNode, diskName),
"if ($disk -and $ds -and $server -and $vm) { ",
String.format("$newdisk = Set-VBRViVirtualDevice -VirtualDevice $disk -VirtualDeviceNode %s -Type %s", device, diskType.equals("IDE") ? "SCSI" : diskType),
"$mapping = New-VBRViVirtualDeviceMappingRule -SourceVirtualDevice:$newdisk -Datastore:$ds",
"$job = Start-VBRViVirtualDiskRestore -RestorePoint:$point -VirtualDeviceMapping:$mapping -TargetVM $vm -RunAsync",
"while (-not (Get-VBRRestoreSession -Id $job.Id).IsCompleted) { Start-Sleep -Seconds 10 }",
"Write-Output $disk.Name",
"} else { ",
"Write-Output 'Cannot find disk to restore' ",
"Exit 1",
"}",
"} else { ",
"Write-Output 'Cannot find any restore point with this id'",
"Exit 1",
"}"
);
Pair<Boolean, String> result = executePowerShellCommands(cmds);
if (result != null && result.first()) {
return new Pair<>(result.first(), result.second().split("\r\n")[0]);
}
logger.error("Failed to restore volume [uuid: {}, name: {}, type: {}, deviceId: {}] of VM [{}] to VM [{}] using restore point [%s], host [%s] and datastore [%s].",
volumeUuid, diskName, diskType, diskDeviceNode, vmUuid, vm.getUuid(), restorePointId, host, datastore);
throw new CloudRuntimeException(String.format("Failed to restore volume [%s] of VM [%s] to VM [%s] using restore point [%s],", volumeUuid, vmUuid, vm.getUuid(), restorePointId));
}
}
Original file line number Diff line number Diff line change
@@ -24,30 +24,24 @@
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.times;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import com.cloud.utils.Pair;
import org.apache.cloudstack.backup.Backup;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.backup.veeam.api.RestoreSession;
import org.apache.http.HttpResponse;
import org.apache.logging.log4j.core.Logger;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;

import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.github.tomakehurst.wiremock.client.BasicCredentials;
import com.github.tomakehurst.wiremock.junit.WireMockRule;

@@ -105,71 +99,25 @@ public void testVeeamJobs() {
}

@Test
public void getRepositoryNameFromJobTestExceptionCmdWithoutResult() throws Exception {
String backupName = "TEST-BACKUP";
try {
Mockito.doReturn(null).when(mockClient).executePowerShellCommands(Mockito.anyList());
mockClient.getRepositoryNameFromJob(backupName);
fail();
} catch (Exception e) {
Assert.assertEquals(CloudRuntimeException.class, e.getClass());
Assert.assertEquals("Failed to get Repository Name from Job [name: TEST-BACKUP].", e.getMessage());
}
}

@Test
public void getRepositoryNameFromJobTestExceptionCmdWithFalseResult() {
String backupName = "TEST-BACKUP2";
Pair<Boolean, String> response = new Pair<Boolean, String>(Boolean.FALSE, "");
Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList());
try {
mockClient.getRepositoryNameFromJob(backupName);
fail();
} catch (Exception e) {
Assert.assertEquals(CloudRuntimeException.class, e.getClass());
Assert.assertEquals("Failed to get Repository Name from Job [name: TEST-BACKUP2].", e.getMessage());
}
}

@Test
public void getRepositoryNameFromJobTestExceptionWhenResultIsInWrongFormat() {
String backupName = "TEST-BACKUP3";
Pair<Boolean, String> response = new Pair<Boolean, String>(Boolean.TRUE, "\nName:\n\nName-test");
Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList());
try {
mockClient.getRepositoryNameFromJob(backupName);
fail();
} catch (Exception e) {
Assert.assertEquals(CloudRuntimeException.class, e.getClass());
Assert.assertEquals("Can't find any repository name for Job [name: TEST-BACKUP3].", e.getMessage());
}
public void removeDashesIfDatastoreNameIsUuidTestValidUuid() {
String validUuid = UUID.randomUUID().toString();
String expected = validUuid.replace("-","");
String result = client.removeDashesIfDatastoreNameIsUuid(validUuid);
Assert.assertEquals(expected, result);
}

@Test
public void getRepositoryNameFromJobTestSuccess() throws Exception {
String backupName = "TEST-BACKUP3";
Pair<Boolean, String> response = new Pair<Boolean, String>(Boolean.TRUE, "\r\nName : test");
Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList());
String repositoryNameFromJob = mockClient.getRepositoryNameFromJob(backupName);
Assert.assertEquals("test", repositoryNameFromJob);
public void removeDashesIfDatastoreNameIsUuidTestNameWithDashesButIsNotUuid() {
String datastore = UUID.randomUUID().toString() + "-test-extra-name";
String result = client.removeDashesIfDatastoreNameIsUuid(datastore);
Assert.assertEquals(datastore, result);
}

@Test
public void checkIfRestoreSessionFinishedTestTimeoutException() throws IOException {
try {
ReflectionTestUtils.setField(mockClient, "restoreTimeout", 10);
RestoreSession restoreSession = Mockito.mock(RestoreSession.class);
HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
Mockito.when(mockClient.get(Mockito.anyString())).thenReturn(httpResponse);
Mockito.when(mockClient.parseRestoreSessionResponse(httpResponse)).thenReturn(restoreSession);
Mockito.when(restoreSession.getResult()).thenReturn("No Success");
Mockito.when(mockClient.checkIfRestoreSessionFinished(Mockito.eq("RestoreTest"), Mockito.eq("any"))).thenCallRealMethod();
mockClient.checkIfRestoreSessionFinished("RestoreTest", "any");
fail();
} catch (Exception e) {
Assert.assertEquals("Related job type: RestoreTest was not successful", e.getMessage());
}
Mockito.verify(mockClient, times(10)).get(Mockito.anyString());
public void removeDashesIfDatastoreNameIsUuidTestNotUuidName() {
String name = "simple-datastore-name";
String result = client.removeDashesIfDatastoreNameIsUuid(name);
Assert.assertEquals(name, result);
}

private void verifyBackupMetrics(Map<String, Backup.Metric> metrics) {
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@
import java.util.Map;
import java.util.UUID;

import com.cloud.utils.LogUtils;
import javax.inject.Inject;

import com.cloud.hypervisor.vmware.mo.DatastoreMO;
@@ -93,7 +94,6 @@
import com.cloud.hypervisor.vmware.manager.VmwareManager;
import com.cloud.hypervisor.vmware.mo.DatacenterMO;
import com.cloud.hypervisor.vmware.mo.NetworkMO;
import com.cloud.hypervisor.vmware.mo.VirtualDiskManagerMO;
import com.cloud.hypervisor.vmware.mo.VirtualMachineDiskInfoBuilder;
import com.cloud.hypervisor.vmware.mo.VirtualMachineMO;
import com.cloud.hypervisor.vmware.resource.VmwareContextFactory;
@@ -147,6 +147,7 @@
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.vmware.vim25.ManagedObjectReference;
import com.vmware.vim25.VirtualDevice;
@@ -796,6 +797,12 @@ private VirtualMachineDiskInfo getDiskInfo(VirtualMachineMO vmMo, Long poolId, S
return diskInfoBuilder.getDiskInfoByBackingFileBaseName(volumeName, poolName);
}

private VirtualMachineDiskInfo getDiskInfo(VirtualMachineMO vmMo, Long poolId, String volumeName, String busName) throws Exception {
VirtualMachineDiskInfoBuilder diskInfoBuilder = vmMo.getDiskInfoBuilder();
String poolName = _storagePoolDao.findById(poolId).getUuid().replace("-", "");
return diskInfoBuilder.getDiskInfoByBackingFileBaseName(volumeName, poolName, busName);
}

private VolumeVO createVolume(VirtualDisk disk, VirtualMachineMO vmToImport, long domainId, long zoneId, long accountId, long instanceId, Long poolId, long templateId, Backup backup, boolean isImport) throws Exception {
VMInstanceVO vm = virtualMachineDao.findByIdIncludingRemoved(backup.getVmId());
if (vm == null) {
@@ -815,10 +822,11 @@ private VolumeVO createVolume(VirtualDisk disk, VirtualMachineMO vmToImport, lon
checkBackingInfo(backing);
VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo)backing;
String volumeName = getVolumeName(disk, vmToImport);
String deviceBusName = vmToImport.getDeviceBusName(vmToImport.getAllDeviceList(), disk);
Storage.ProvisioningType provisioningType = getProvisioningType(info);
long diskOfferingId = getDiskOfferingId(size, provisioningType);
Integer unitNumber = disk.getUnitNumber();
VirtualMachineDiskInfo diskInfo = getDiskInfo(vmToImport, poolId, volumeName);
VirtualMachineDiskInfo diskInfo = getDiskInfo(vmToImport, poolId, volumeName, deviceBusName);
return createVolumeRecord(type, volumeName, zoneId, domainId, accountId, diskOfferingId, provisioningType, size, instanceId, poolId, templateId, unitNumber, diskInfo);
}

@@ -1004,11 +1012,17 @@ private VirtualMachineMO findVM(DatacenterMO dcMo, String path) throws Exception
/**
* Find restored volume based on volume info
*/
private VirtualDisk findRestoredVolume(Backup.VolumeInfo volumeInfo, VirtualMachineMO vm) throws Exception {
List<VirtualDisk> virtualDisks = vm.getVirtualDisks();
protected VirtualDisk findRestoredVolume(Backup.VolumeInfo volumeInfo, VirtualMachineMO vm, String volumeName, int deviceId) throws Exception {
List<VirtualDisk> virtualDisks = Lists.reverse(vm.getVirtualDisks());
logger.debug(LogUtils.logGsonWithoutException("Trying to find restored volume with size [%s], name [%s] and deviceId (unitNumber in VMWare) [%s] "
+ "in VM [%s] disks [%s].", volumeInfo.getSize(), volumeName, deviceId, vm.getVmName(), virtualDisks));
for (VirtualDisk disk : virtualDisks) {
if (disk.getCapacityInBytes().equals(volumeInfo.getSize())) {
return disk;
VirtualDeviceBackingInfo backingInfo = disk.getBacking();
if(backingInfo instanceof VirtualDiskFlatVer2BackingInfo) {
VirtualDiskFlatVer2BackingInfo diskBackingInfo = (VirtualDiskFlatVer2BackingInfo)backingInfo;
if (disk.getCapacityInBytes().equals(volumeInfo.getSize()) && diskBackingInfo.getFileName().contains(volumeName) && disk.getUnitNumber() == deviceId) {
return disk;
}
}
}
throw new CloudRuntimeException("Volume to restore could not be found");
@@ -1076,54 +1090,15 @@ public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId,
}

@Override
public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup)
public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String restoredVolumeName, Backup.VolumeInfo volumeInfo, VirtualMachine vm, long poolId, Backup backup)
throws Exception {
DatacenterMO dcMo = getDatacenterMO(zoneId);
VirtualMachineMO vmRestored = findVM(dcMo, location);
VirtualMachineMO vmMo = findVM(dcMo, vm.getInstanceName());
VirtualDisk restoredDisk = findRestoredVolume(volumeInfo, vmRestored);
String diskPath = vmRestored.getVmdkFileBaseName(restoredDisk);

logger.debug("Restored disk size=" + toHumanReadableSize(restoredDisk.getCapacityInKB() * Resource.ResourceType.bytesToKiB) + " path=" + diskPath);

// Detach restored VM disks
vmRestored.detachDisk(String.format("%s/%s.vmdk", location, diskPath), false);

String srcPath = getVolumeFullPath(restoredDisk);
String destPath = getDestVolumeFullPath(vmMo);

VirtualDiskManagerMO virtualDiskManagerMO = new VirtualDiskManagerMO(dcMo.getContext());

// Copy volume to the VM folder
logger.debug(String.format("Moving volume from %s to %s", srcPath, destPath));
virtualDiskManagerMO.moveVirtualDisk(srcPath, dcMo.getMor(), destPath, dcMo.getMor(), true);

try {
// Attach volume to VM
vmMo.attachDisk(new String[] {destPath}, getDestStoreMor(vmMo));
} catch (Exception e) {
logger.error("Failed to attach the restored volume: " + diskPath, e);
return false;
} finally {
// Destroy restored VM
vmRestored.destroy();
}

logger.debug(String.format("Attaching disk %s to vm %s", destPath, vm.getId()));
VirtualDisk attachedDisk = getAttachedDisk(vmMo, destPath);
if (attachedDisk == null) {
logger.error("Failed to get the attached the (restored) volume " + destPath);
return false;
}
logger.debug(String.format("Creating volume entry for disk %s attached to vm %s", destPath, vm.getId()));
createVolume(attachedDisk, vmMo, vm.getDomainId(), vm.getDataCenterId(), vm.getAccountId(), vm.getId(), poolId, vm.getTemplateId(), backup, false);

if (vm.getBackupOfferingId() == null) {
return true;
}
VMInstanceVO vmVO = (VMInstanceVO)vm;
vmVO.setBackupVolumes(createVolumeInfoFromVolumes(_volumeDao.findByInstance(vm.getId())));
vmDao.update(vmVO.getId(), vmVO);
int newDeviceId = (int) (_volumeDao.findByInstance(vm.getId()).stream().mapToLong(VolumeVO::getDeviceId).max().orElse(0L) + 1);
VirtualDisk restoredDisk = findRestoredVolume(volumeInfo, vmMo, restoredVolumeName.split(".vmdk")[0], newDeviceId);
String diskPath = vmMo.getVmdkFileBaseName(restoredDisk);
logger.debug("Restored disk size={} path={}.", toHumanReadableSize(restoredDisk.getCapacityInKB()), diskPath);
createVolume(restoredDisk, vmMo, vm.getDomainId(), vm.getDataCenterId(), vm.getAccountId(), vm.getId(), poolId, vm.getTemplateId(), backup, false);
return true;
}

Original file line number Diff line number Diff line change
@@ -23,6 +23,10 @@
import java.util.List;
import java.util.Map;

import com.cloud.hypervisor.vmware.mo.VirtualMachineMO;
import com.vmware.vim25.VirtualDisk;
import com.vmware.vim25.VirtualDiskFlatVer2BackingInfo;
import org.apache.cloudstack.backup.Backup;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.junit.After;
@@ -44,7 +48,7 @@
import com.cloud.dc.ClusterDetailsDao;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.storage.Storage.ProvisioningType;
import com.cloud.storage.Storage;
import com.cloud.storage.StoragePool;
import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.Volume;
@@ -141,10 +145,10 @@ public void createVolumeInfoFromVolumesTestNullVolume() {
public void createVolumeInfoFromVolumesTestCorrectlyConvertOfVolumes() {
List<VolumeVO> volumesToTest = new ArrayList<>();

VolumeVO root = new VolumeVO("test", 1l, 1l, 1l, 1l, 1l, "test", "/root/dir", ProvisioningType.THIN, 555l, Volume.Type.ROOT);
VolumeVO root = new VolumeVO("test", 1l, 1l, 1l, 1l, 1l, "test", "/root/dir", Storage.ProvisioningType.THIN, 555l, Volume.Type.ROOT);
String rootUuid = root.getUuid();

VolumeVO data = new VolumeVO("test", 1l, 1l, 1l, 1l, 1l, "test", "/root/dir/data", ProvisioningType.THIN, 1111000l, Volume.Type.DATADISK);
VolumeVO data = new VolumeVO("test", 1l, 1l, 1l, 1l, 1l, "test", "/root/dir/data", Storage.ProvisioningType.THIN, 1111000l, Volume.Type.DATADISK);
String dataUuid = data.getUuid();

volumesToTest.add(root);
@@ -155,4 +159,35 @@ public void createVolumeInfoFromVolumesTestCorrectlyConvertOfVolumes() {

assertEquals(expected, result);
}

@Test
public void findRestoredVolumeTestNotFindRestoredVolume() throws Exception {
VirtualMachineMO vmInstanceVO = Mockito.mock(VirtualMachineMO.class);
Backup.VolumeInfo volumeInfo = Mockito.mock(Backup.VolumeInfo.class);
Mockito.when(volumeInfo.getSize()).thenReturn(52l);
Mockito.when(vmInstanceVO.getVirtualDisks()).thenReturn(new ArrayList<>());
try {
vMwareGuru.findRestoredVolume(volumeInfo, vmInstanceVO, null, 0);
} catch (Exception e) {
assertEquals("Volume to restore could not be found", e.getMessage());
}
}

@Test
public void findRestoredVolumeTestFindRestoredVolume() throws Exception {
Backup.VolumeInfo volumeInfo = Mockito.mock(Backup.VolumeInfo.class);
VirtualMachineMO vmInstanceVO = Mockito.mock(VirtualMachineMO.class);
VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class);
VirtualDiskFlatVer2BackingInfo info = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class);
ArrayList<VirtualDisk> disks = new ArrayList<>();
disks.add(virtualDisk);
Mockito.when(volumeInfo.getSize()).thenReturn(52l);
Mockito.when(virtualDisk.getCapacityInBytes()).thenReturn(52l);
Mockito.when(info.getFileName()).thenReturn("test.vmdk");
Mockito.when(virtualDisk.getBacking()).thenReturn(info);
Mockito.when(virtualDisk.getUnitNumber()).thenReturn(1);
Mockito.when(vmInstanceVO.getVirtualDisks()).thenReturn(disks);
VirtualDisk findRestoredVolume = vMwareGuru.findRestoredVolume(volumeInfo, vmInstanceVO, "test", 1);
Assert.assertNotNull(findRestoredVolume);
}
}
Original file line number Diff line number Diff line change
@@ -397,7 +397,7 @@ public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId,
}

@Override
public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo,
public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String restoredVolumeLocation, Backup.VolumeInfo volumeInfo,
VirtualMachine vm, long poolId, Backup backup) throws Exception {
return false;
}
Original file line number Diff line number Diff line change
@@ -750,6 +750,10 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid,
}
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vmFromBackup);

if (!VirtualMachine.PowerState.PowerOff.equals(vm.getPowerState())) {
throw new CloudRuntimeException(String.format("VM [%s] needs to be powered off to restore the volume [%s].", vm.getUuid(), backedUpVolumeUuid));
}

Pair<HostVO, StoragePoolVO> restoreInfo = getRestoreVolumeHostAndDatastore(vm);
HostVO host = restoreInfo.first();
StoragePoolVO datastore = restoreInfo.second();
@@ -763,12 +767,11 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid,
}

BackupProvider backupProvider = getBackupProvider(offering.getProvider());
logger.debug(String.format("Trying to restore volume using host private IP address: [%s].", host.getPrivateIpAddress()));

String[] hostPossibleValues = {host.getPrivateIpAddress(), host.getName()};
String[] datastoresPossibleValues = {datastore.getUuid(), datastore.getName()};

Pair<Boolean, String> result = restoreBackedUpVolume(backedUpVolumeUuid, backup, backupProvider, hostPossibleValues, datastoresPossibleValues);
Pair<Boolean, String> result = restoreBackedUpVolume(backedUpVolumeUuid, backup, backupProvider, hostPossibleValues, datastoresPossibleValues, vm);

if (BooleanUtils.isFalse(result.first())) {
throw new CloudRuntimeException(String.format("Error restoring volume [%s] of VM [%s] to host [%s] using backup provider [%s] due to: [%s].",
@@ -782,15 +785,15 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid,
}

protected Pair<Boolean, String> restoreBackedUpVolume(final String backedUpVolumeUuid, final BackupVO backup, BackupProvider backupProvider, String[] hostPossibleValues,
String[] datastoresPossibleValues) {
String[] datastoresPossibleValues, VMInstanceVO vm) {
Pair<Boolean, String> result = new Pair<>(false, "");
for (String hostData : hostPossibleValues) {
for (String datastoreData : datastoresPossibleValues) {
logger.debug(String.format("Trying to restore volume [UUID: %s], using host [%s] and datastore [%s].",
backedUpVolumeUuid, hostData, datastoreData));

try {
result = backupProvider.restoreBackedUpVolume(backup, backedUpVolumeUuid, hostData, datastoreData);
result = backupProvider.restoreBackedUpVolume(backup, backedUpVolumeUuid, hostData, datastoreData, vm);

if (BooleanUtils.isTrue(result.first())) {
return result;
@@ -870,12 +873,17 @@ private boolean attachVolumeToVM(Long zoneId, String restoredVolumeLocation, Lis
}
volumeInfo.setType(Volume.Type.DATADISK);

logger.debug("Attaching the restored volume to VM " + vm.getId());
String volumeData = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(volumeInfo, "uuid", "size", "path", "type", "deviceId");
String vmData = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "hostName", "instanceName");

logger.debug(String.format("Trying to attach restored volume [%s] to VM [%s] using datastore [%s].", volumeData, vmData, datastoreUuid));
StoragePoolVO pool = primaryDataStoreDao.findByUuid(datastoreUuid);
try {
return guru.attachRestoredVolumeToVirtualMachine(zoneId, restoredVolumeLocation, volumeInfo, vm, pool.getId(), backup);
} catch (Exception e) {
throw new CloudRuntimeException("Error attach restored volume to VM " + vm.getUuid() + " due to: " + e.getMessage());
String errorMsg = String.format("Failed to attach restored volume [%s] to VM [%s] due to [%s].", volumeData, vmData, e.getMessage());
logger.error(errorMsg, e);
throw new CloudRuntimeException(errorMsg);
}
}

Original file line number Diff line number Diff line change
@@ -161,14 +161,14 @@ public void restoreBackedUpVolumeTestHostIpAndDatastoreUuid() {
String volumeUuid = "5f4ed903-ac23-4f8a-b595-69c73c40593f";

Mockito.when(backupProvider.restoreBackedUpVolume(Mockito.any(), Mockito.eq(volumeUuid),
Mockito.eq("127.0.0.1"), Mockito.eq("e9804933-8609-4de3-bccc-6278072a496c"))).thenReturn(new Pair<Boolean, String>(Boolean.TRUE, "Success"));
Pair<Boolean,String> restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeUuid, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues);
Mockito.eq("127.0.0.1"), Mockito.eq("e9804933-8609-4de3-bccc-6278072a496c"), Mockito.any())).thenReturn(new Pair<Boolean, String>(Boolean.TRUE, "Success"));
Pair<Boolean,String> restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeUuid, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, null);

assertEquals(Boolean.TRUE, restoreBackedUpVolume.first());
assertEquals("Success", restoreBackedUpVolume.second());

Mockito.verify(backupProvider, times(1)).restoreBackedUpVolume(Mockito.any(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyString());
Mockito.anyString(), Mockito.anyString(), Mockito.any());
}

@Test
@@ -177,14 +177,14 @@ public void restoreBackedUpVolumeTestHostIpAndDatastoreName() {
String volumeUuid = "5f4ed903-ac23-4f8a-b595-69c73c40593f";

Mockito.when(backupProvider.restoreBackedUpVolume(Mockito.any(), Mockito.eq(volumeUuid),
Mockito.eq("127.0.0.1"), Mockito.eq("datastore-name"))).thenReturn(new Pair<Boolean, String>(Boolean.TRUE, "Success2"));
Pair<Boolean,String> restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeUuid, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues);
Mockito.eq("127.0.0.1"), Mockito.eq("datastore-name"), Mockito.any())).thenReturn(new Pair<Boolean, String>(Boolean.TRUE, "Success2"));
Pair<Boolean,String> restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeUuid, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, null);

assertEquals(Boolean.TRUE, restoreBackedUpVolume.first());
assertEquals("Success2", restoreBackedUpVolume.second());

Mockito.verify(backupProvider, times(2)).restoreBackedUpVolume(Mockito.any(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyString());
Mockito.anyString(), Mockito.anyString(), Mockito.any());
}

@Test
@@ -193,14 +193,14 @@ public void restoreBackedUpVolumeTestHostNameAndDatastoreUuid() {
String volumeUuid = "5f4ed903-ac23-4f8a-b595-69c73c40593f";

Mockito.when(backupProvider.restoreBackedUpVolume(Mockito.any(), Mockito.eq(volumeUuid),
Mockito.eq("hostname"), Mockito.eq("e9804933-8609-4de3-bccc-6278072a496c"))).thenReturn(new Pair<Boolean, String>(Boolean.TRUE, "Success3"));
Pair<Boolean,String> restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeUuid, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues);
Mockito.eq("hostname"), Mockito.eq("e9804933-8609-4de3-bccc-6278072a496c"), Mockito.any())).thenReturn(new Pair<Boolean, String>(Boolean.TRUE, "Success3"));
Pair<Boolean,String> restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeUuid, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, null);

assertEquals(Boolean.TRUE, restoreBackedUpVolume.first());
assertEquals("Success3", restoreBackedUpVolume.second());

Mockito.verify(backupProvider, times(3)).restoreBackedUpVolume(Mockito.any(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyString());
Mockito.anyString(), Mockito.anyString(), Mockito.any());
}

@Test
@@ -209,14 +209,14 @@ public void restoreBackedUpVolumeTestHostAndDatastoreName() {
String volumeUuid = "5f4ed903-ac23-4f8a-b595-69c73c40593f";

Mockito.when(backupProvider.restoreBackedUpVolume(Mockito.any(), Mockito.eq(volumeUuid),
Mockito.eq("hostname"), Mockito.eq("datastore-name"))).thenReturn(new Pair<Boolean, String>(Boolean.TRUE, "Success4"));
Pair<Boolean,String> restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeUuid, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues);
Mockito.eq("hostname"), Mockito.eq("datastore-name"), Mockito.any())).thenReturn(new Pair<Boolean, String>(Boolean.TRUE, "Success4"));
Pair<Boolean,String> restoreBackedUpVolume = backupManager.restoreBackedUpVolume(volumeUuid, backupVO, backupProvider, hostPossibleValues, datastoresPossibleValues, null);

assertEquals(Boolean.TRUE, restoreBackedUpVolume.first());
assertEquals("Success4", restoreBackedUpVolume.second());

Mockito.verify(backupProvider, times(4)).restoreBackedUpVolume(Mockito.any(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyString());
Mockito.anyString(), Mockito.anyString(), Mockito.any());
}

@Test
2 changes: 1 addition & 1 deletion utils/src/main/java/com/cloud/utils/UuidUtils.java
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@

public class UuidUtils {

private static final RegularExpression uuidRegex = new RegularExpression("[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}");
private static final RegularExpression uuidRegex = new RegularExpression("[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$");

public static String first(String uuid) {
return uuid.substring(0, uuid.indexOf('-'));
Original file line number Diff line number Diff line change
@@ -76,6 +76,19 @@ public VirtualMachineDiskInfo getDiskInfoByBackingFileBaseName(String diskBackin
return null;
}

public VirtualMachineDiskInfo getDiskInfoByBackingFileBaseName(String diskBackingFileBaseName, String dataStoreName, String busName) {
for (Map.Entry<String, List<String>> entry : disks.entrySet()) {
if (chainContains(entry.getValue(), diskBackingFileBaseName, dataStoreName) && entry.getKey().equals(busName)) {
VirtualMachineDiskInfo diskInfo = new VirtualMachineDiskInfo();
diskInfo.setDiskDeviceBusName(entry.getKey());
diskInfo.setDiskChain(entry.getValue().toArray(new String[1]));
return diskInfo;
}
}

return null;
}

private List<String> getDiskChainContainer(String diskDeviceBusName) {
assert (diskDeviceBusName != null);
List<String> chain = disks.get(diskDeviceBusName);
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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.vmware.mo;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class VirtualMachineDiskInfoBuilderTest {

@Test
public void getDiskInfoByBackingFileBaseNameTestFindDisk() {
VirtualMachineDiskInfoBuilder virtualMachineDiskInfoBuilder = new VirtualMachineDiskInfoBuilder();
Map<String, List<String>> disks = new HashMap<String, List<String>>();
String[] diskChain = new String[]{"[somedatastorename] i-3-VM-somePath/ROOT-1.vmdk"};
disks.put("scsi0:0", Arrays.asList(diskChain));
virtualMachineDiskInfoBuilder.disks = disks;
VirtualMachineDiskInfo findedDisk = virtualMachineDiskInfoBuilder.getDiskInfoByBackingFileBaseName("ROOT-1", "somedatastorename", "scsi0:0");
assertEquals("scsi", findedDisk.getControllerFromDeviceBusName());
assertArrayEquals(findedDisk.getDiskChain(), diskChain);
}

@Test
public void getDiskInfoByBackingFileBaseNameTestNotFindDisk() {
VirtualMachineDiskInfoBuilder virtualMachineDiskInfoBuilder = new VirtualMachineDiskInfoBuilder();
Map<String, List<String>> disks = new HashMap<String, List<String>>();
disks.put("scsi0:0", Arrays.asList(new String[]{"[somedatastorename] i-3-VM-somePath/ROOT-1.vmdk"}));
virtualMachineDiskInfoBuilder.disks = disks;
VirtualMachineDiskInfo findedDisk = virtualMachineDiskInfoBuilder.getDiskInfoByBackingFileBaseName("ROOT-1", "somedatastorename", "ide0:0");
assertEquals(null, findedDisk);
}
}