From 2588ee445afd22fdbb273faf86d852ba2e1f3727 Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:39:09 +0530 Subject: [PATCH 01/17] draas initial changes --- .../com/cloud/vm/VirtualMachineProfile.java | 1 + .../cloudstack/backup/BackupProvider.java | 2 +- .../cloud/vm/VirtualMachineManagerImpl.java | 15 ++- .../backup/DummyBackupProvider.java | 4 +- .../cloudstack/backup/NASBackupProvider.java | 45 ++++++--- .../backup/NetworkerBackupProvider.java | 4 +- .../backup/VeeamBackupProvider.java | 4 +- .../LibvirtRestoreBackupCommandWrapper.java | 39 ++++++-- .../java/com/cloud/vm/UserVmManagerImpl.java | 19 ++-- .../cloudstack/backup/BackupManagerImpl.java | 34 ++++--- .../cloudstack/backup/BackupManagerTest.java | 4 +- ui/public/locales/en.json | 1 + ui/src/components/view/DeployVMFromBackup.vue | 95 ++++++++++++++++--- .../network/CreateIsolatedNetworkForm.vue | 2 +- ui/src/views/network/CreateL2NetworkForm.vue | 2 +- ui/src/views/network/CreateNetwork.vue | 2 +- .../views/network/CreateSharedNetworkForm.vue | 2 +- 17 files changed, 198 insertions(+), 77 deletions(-) diff --git a/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java b/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java index c67ee4eabc28..5c78d6bedd64 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java @@ -78,6 +78,7 @@ public static class Param { public static final Param BootIntoSetup = new Param("enterHardwareSetup"); public static final Param PreserveNics = new Param("PreserveNics"); public static final Param ConsiderLastHost = new Param("ConsiderLastHost"); + public static final Param ReturnAfterVolumePrepare = new Param("ReturnAfterVolumePrepare"); private String name; diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java index 1eb36f895565..60c92c2953b0 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java @@ -85,7 +85,7 @@ public interface BackupProvider { */ boolean deleteBackup(Backup backup, boolean forced); - boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid); + Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid); /** * Restore VM from backup diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 3a6e1b622774..098467b93722 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -1476,16 +1476,29 @@ public void orchestrateStart(final String vmUuid, final Map restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + return new Pair<>(true, null); } } diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index e5f98ad291be..5128aeeb8fdf 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -103,6 +103,12 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private VMSnapshotDao vmSnapshotDao; + @Inject + private VolumeDao _volsDao; + + @Inject + protected PrimaryDataStoreDao _storagePoolDao = null; + @Inject private VMSnapshotDetailsDao vmSnapshotDetailsDao; @@ -115,27 +121,38 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private DiskOfferingDao diskOfferingDao; + protected Long getClusterIdFromRootVolume(VirtualMachine vm) { + VolumeVO rootVolume = _volsDao.getInstanceRootVolume(vm.getId()); + StoragePoolVO rootDiskPool = _storagePoolDao.findById(rootVolume.getPoolId()); + return rootDiskPool.getClusterId(); + } + 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."); - return null; - } - Host host = hostDao.findById(hostId); + Long clusterId = null; - if (host.getStatus() == Status.Up) { - return host; + if (hostId != null) { + Host host = hostDao.findById(hostId); + if (host.getStatus() == Status.Up) { + return host; + } + clusterId = host.getClusterId(); } else { + clusterId = getClusterIdFromRootVolume(vm); + } + + if (clusterId != null) { // Try to find any Up host in the same cluster - for (final Host hostInCluster : hostDao.findHypervisorHostInCluster(host.getClusterId())) { + 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) { @@ -249,16 +266,16 @@ private BackupVO createBackupObject(VirtualMachine vm, String backupPath) { } @Override - public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair 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 restoreVMBackup(VirtualMachine vm, Backup backup) { List backedVolumesUUIDs = backup.getBackedUpVolumes().stream() .sorted(Comparator.comparingLong(Backup.VolumeInfo::getDeviceId)) .map(Backup.VolumeInfo::getUuid) @@ -291,7 +308,7 @@ private boolean restoreVMBackup(VirtualMachine vm, Backup backup) { } 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 getVolumePaths(List volumes) { diff --git a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java index f39aedb55f2a..84184a335070 100644 --- a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java +++ b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java @@ -630,7 +630,7 @@ public void syncBackupStorageStats(Long zoneId) { public boolean willDeleteBackupsOnOfferingRemoval() { return false; } @Override - public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { - return true; + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + return new Pair<>(true, null); } } diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index c81c5d34ea25..bf39f2d5836c 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -337,11 +337,11 @@ public List listRestorePoints(VirtualMachine vm) { } @Override - public boolean restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair 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 diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java index 0e5091ebcf49..ad4c39959bd4 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java @@ -76,17 +76,22 @@ public Answer execute(RestoreBackupCommand command, LibvirtComputingResource ser restoreVolumesOfDestroyedVMs(restoreVolumePaths, vmName, backupPath, backupRepoType, backupRepoAddress, mountOptions); } } 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 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 restoreVolumePaths, List backedVolumesUUIDs, String backupPath, String backupRepoType, String backupRepoAddress, String mountOptions) { String diskType = "root"; @@ -97,6 +102,7 @@ private void restoreVolumesOfExistingVM(List restoreVolumePaths, List 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())); } @@ -116,6 +122,7 @@ private void restoreVolumesOfDestroyedVMs(List volumePaths, String vmNam String volumePath = volumePaths.get(i); Pair 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())); } @@ -132,6 +139,7 @@ private void restoreVolume(String backupPath, String backupRepoType, String back Pair 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 +148,6 @@ private void restoreVolume(String backupPath, String backupRepoType, String back 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); @@ -167,7 +173,8 @@ private String mountBackupDirectory(String backupRepoAddress, String backupRepoT } Script.runSimpleBashScript(mount); } catch (Exception e) { - throw new CloudRuntimeException(String.format("Failed to mount %s to %s", backupRepoType, backupRepoAddress), e); + logger.error(String.format("Failed to mount repository {} of type {} to the directory {}", backupRepoAddress, backupRepoType, mountDirectory), e); + throw new CloudRuntimeException("Failed to mount the backup repository on the KVM host"); } return mountDirectory; } @@ -177,7 +184,8 @@ private void unmountBackupDirectory(String backupDirectory) { 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); + logger.error(String.format("Failed to unmount backup directory {}", backupDirectory), e); + throw new CloudRuntimeException("Failed to unmount the backup directory"); } } @@ -185,7 +193,8 @@ private void deleteTemporaryDirectory(String backupDirectory) { 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 +206,16 @@ private Pair getBackupPath(String mountDirectory, String volumeP 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; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index cd7d198eb611..426ef1a45a01 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -9473,9 +9473,6 @@ public UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws Insufficien 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); @@ -9612,20 +9609,15 @@ public UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws Insufficien @Override public UserVm restoreVMFromBackup(CreateVMFromBackupCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { long vmId = cmd.getEntityId(); + UserVm vm; Map diskOfferingMap = cmd.getDataDiskTemplateToDiskOfferingMap(); Map 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> vmParamPair = null; + vmParamPair = startVirtualMachine(vmId, null, null, null, additonalParams, null); + vm = vmParamPair.first(); Long isoId = vm.getIsoId(); if (isoId != null) { @@ -9663,6 +9655,7 @@ public UserVm restoreVMFromBackup(CreateVMFromBackupCmd cmd) throws ResourceUnav podId = adminCmd.getPodId(); clusterId = adminCmd.getClusterId(); } + additonalParams.remove(VirtualMachineProfile.Param.ReturnAfterVolumePrepare); vm = startVirtualMachine(vmId, podId, clusterId, cmd.getHostId(), diskOfferingMap, additonalParams, cmd.getDeploymentPlanner()); } return vm; diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 2682d53dd200..d95f4c222a06 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -1022,7 +1022,7 @@ public boolean restoreBackup(final Long backupId) { 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 Map getIpToNetworkMapFromBackup(Backup backup, 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); @@ -1293,7 +1301,8 @@ public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws Re 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 result = null; Long eventId = null; try { updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring); @@ -1310,18 +1319,21 @@ public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws Re 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, diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java index 61ae6cd018c7..2991653cd065 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -1275,7 +1275,7 @@ public void testRestoreBackupToVM() throws NoTransitionException { 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 utils = Mockito.mockStatic(ActionEventUtils.class)) { boolean result = backupManager.restoreBackupToVM(backupId, vmId); @@ -1331,7 +1331,7 @@ public void testRestoreBackupToVMException() throws NoTransitionException { 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 utils = Mockito.mockStatic(ActionEventUtils.class)) { CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 394de6ca6d26..6f35fb49596d 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2856,6 +2856,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.
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?", diff --git a/ui/src/components/view/DeployVMFromBackup.vue b/ui/src/components/view/DeployVMFromBackup.vue index 8d929a1fed0f..c3a981d39fcb 100644 --- a/ui/src/components/view/DeployVMFromBackup.vue +++ b/ui/src/components/view/DeployVMFromBackup.vue @@ -45,6 +45,68 @@ + + +
@@ -67,7 +128,6 @@ > @@ -82,7 +142,6 @@ > @@ -909,6 +968,9 @@ export default { isNormalAndDomainUser () { return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype) }, + isDifferentZoneFromBackup () { + return this.selectedZone !== this.dataPreFill.zoneid + }, isNormalUserOrProject () { return ['User'].includes(this.$store.getters.userInfo.roletype) || store.getters.project.id }, @@ -1488,21 +1550,15 @@ 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() @@ -2636,6 +2692,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; diff --git a/ui/src/views/network/CreateIsolatedNetworkForm.vue b/ui/src/views/network/CreateIsolatedNetworkForm.vue index d237759ad2a7..9c17427fe8f1 100644 --- a/ui/src/views/network/CreateIsolatedNetworkForm.vue +++ b/ui/src/views/network/CreateIsolatedNetworkForm.vue @@ -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.name === 'backup')) { params.id = this.resource.zoneid } params.showicon = true diff --git a/ui/src/views/network/CreateL2NetworkForm.vue b/ui/src/views/network/CreateL2NetworkForm.vue index a28548e2b6fd..9aa0c1f92276 100644 --- a/ui/src/views/network/CreateL2NetworkForm.vue +++ b/ui/src/views/network/CreateL2NetworkForm.vue @@ -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.name === 'backup')) { params.id = this.resource.zoneid } params.showicon = true diff --git a/ui/src/views/network/CreateNetwork.vue b/ui/src/views/network/CreateNetwork.vue index ad43ca7730d2..9f806885386f 100644 --- a/ui/src/views/network/CreateNetwork.vue +++ b/ui/src/views/network/CreateNetwork.vue @@ -106,7 +106,7 @@ export default { fetchActionZoneData () { this.loading = true const params = {} - if (this.$route.name === 'deployVirtualMachine' && this.resource.zoneid) { + if ((this.$route.name === 'deployVirtualMachine' || this.$route.name === 'backup') && this.resource.zoneid) { params.id = this.resource.zoneid } this.actionZoneLoading = true diff --git a/ui/src/views/network/CreateSharedNetworkForm.vue b/ui/src/views/network/CreateSharedNetworkForm.vue index b64be4aabaaf..deafcc3c8739 100644 --- a/ui/src/views/network/CreateSharedNetworkForm.vue +++ b/ui/src/views/network/CreateSharedNetworkForm.vue @@ -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.name === 'backup')) { params.id = this.resource.zoneid } params.showicon = true From 8f0ce460646b4f4f1c20b2f171ec73274283b00f Mon Sep 17 00:00:00 2001 From: abh1sar Date: Tue, 2 Sep 2025 16:46:35 +0530 Subject: [PATCH 02/17] Fix UT failure in UserVmManagerImplTest --- server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index e1efd87dcd8b..7dbb79f32934 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -3595,7 +3595,6 @@ public void testRestoreVMFromBackup() throws ResourceUnavailableException, Insuf 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); From 4eac151980de28b5b6c958534bf0a6fc9007f0c0 Mon Sep 17 00:00:00 2001 From: abh1sar Date: Wed, 3 Sep 2025 17:26:27 +0530 Subject: [PATCH 03/17] Added option to enable disaster recovery on a backup respository. Added UpdateBackupRepositoryCmd api. --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../repository/AddBackupRepositoryCmd.java | 6 + .../repository/UpdateBackupRepositoryCmd.java | 116 ++++++++++++++++++ .../response/BackupRepositoryResponse.java | 12 ++ .../cloudstack/backup/BackupManager.java | 2 + .../cloudstack/backup/BackupProvider.java | 2 + .../cloudstack/backup/BackupRepository.java | 3 + .../backup/BackupRepositoryService.java | 2 + .../cloudstack/backup/BackupRepositoryVO.java | 21 +++- .../META-INF/db/schema-42100to42200.sql | 3 + .../backup/DummyBackupProvider.java | 5 + .../cloudstack/backup/NASBackupProvider.java | 9 ++ .../backup/NASBackupProviderTest.java | 10 +- .../backup/NetworkerBackupProvider.java | 5 + .../backup/VeeamBackupProvider.java | 5 + .../java/com/cloud/api/ApiResponseHelper.java | 1 + .../java/com/cloud/vm/UserVmManagerImpl.java | 24 ++-- .../cloudstack/backup/BackupManagerImpl.java | 13 ++ .../backup/BackupRepositoryServiceImpl.java | 48 +++++++- ui/public/locales/en.json | 3 + ui/src/components/view/DeployVMFromBackup.vue | 4 + ui/src/config/section/config.js | 13 +- ui/src/views/storage/CreateVMFromBackup.vue | 37 +++--- 23 files changed, 313 insertions(+), 32 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/UpdateBackupRepositoryCmd.java diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 489d737b5bb9..e1b68e74526e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -209,6 +209,7 @@ public class ApiConstants { public static final String DOMAIN_ID = "domainid"; public static final String DOMAIN__ID = "domainId"; public static final String DURATION = "duration"; + public static final String DRAAS_ENABLED = "draasenabled"; public static final String ELIGIBLE = "eligible"; public static final String EMAIL = "email"; public static final String END_ASN = "endasn"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java index 5d0c838bc377..b7f2c744d167 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java @@ -69,6 +69,8 @@ public class AddBackupRepositoryCmd extends BaseCmd { @Parameter(name = ApiConstants.CAPACITY_BYTES, type = CommandType.LONG, description = "capacity of this backup repository") private Long capacityBytes; + @Parameter(name = ApiConstants.DRAAS_ENABLED, type = CommandType.BOOLEAN, description = "the backup repository is configured to be used for disaster recovery on other Zones", since = "4.22.0") + private Boolean draasEnabled; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -109,6 +111,10 @@ public Long getCapacityBytes() { return capacityBytes; } + public Boolean isDraasEnabled() { + return draasEnabled; + } + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/UpdateBackupRepositoryCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/UpdateBackupRepositoryCmd.java new file mode 100644 index 000000000000..57b1ca8bb9c3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/UpdateBackupRepositoryCmd.java @@ -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, entityType = BackupRepositoryResponse.class, description = "ID of the backup repository") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "name of the backup repository") + private String name; + + @Parameter(name = ApiConstants.ADDRESS, type = CommandType.STRING, required = true, 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.DRAAS_ENABLED, type = CommandType.BOOLEAN, description = "the backup repository is configured to be used for disaster recovery on other Zones") + private Boolean draasEnabled; + + ///////////////////////////////////////////////////// + /////////////////// 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 isDraasEnabled() { + return draasEnabled; + } + + ///////////////////////////////////////////////////// + ///////////////////////////////////////////////////// + /////////////////// 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 add backup repository"); + } + } catch (Exception ex4) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex4.getMessage()); + } + + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java index 0db51f040349..a0da6a324052 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java @@ -61,6 +61,10 @@ public class BackupRepositoryResponse extends BaseResponse { @Param(description = "capacity of the backup repository") private Long capacityBytes; + @SerializedName(ApiConstants.DRAAS_ENABLED) + @Param(description = "is the backup repository configured to be used for disaster recovery on other Zones") + private Boolean draasEnabled; + @SerializedName("created") @Param(description = "the date and time the backup repository was added") private Date created; @@ -132,6 +136,14 @@ public void setCapacityBytes(Long capacityBytes) { this.capacityBytes = capacityBytes; } + public Boolean getDraasEnabled() { + return draasEnabled; + } + + public void setDraasEnabled(Boolean draasEnabled) { + this.draasEnabled = draasEnabled; + } + public Date getCreated() { return created; } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index c4b92fc9e05c..37d21613c3dd 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -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 */ diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java index 60c92c2953b0..4edf9debf5ec 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java @@ -23,6 +23,8 @@ public interface BackupProvider { + Boolean isDraasEnabled(BackupOffering backupOffering); + /** * Returns the unique name of the provider * @return returns provider name diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupRepository.java b/api/src/main/java/org/apache/cloudstack/backup/BackupRepository.java index be539a0eb044..2ea67df17605 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupRepository.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupRepository.java @@ -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 isDraasEnabled(); + void setDraasEnabled(Boolean draasEnabled); Date getCreated(); } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java b/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java index ae71053e400d..875fc3b3d906 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java @@ -23,11 +23,13 @@ import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd; import org.apache.cloudstack.api.command.user.backup.repository.DeleteBackupRepositoryCmd; import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd; +import org.apache.cloudstack.api.command.user.backup.repository.UpdateBackupRepositoryCmd; import java.util.List; public interface BackupRepositoryService { BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd); + BackupRepository updateBackupRepository(UpdateBackupRepositoryCmd cmd); boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd); Pair, Integer> listBackupRepositories(ListBackupRepositoriesCmd cmd); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupRepositoryVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupRepositoryVO.java index 98efa94ceca2..6f00317fc372 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupRepositoryVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupRepositoryVO.java @@ -67,6 +67,9 @@ public class BackupRepositoryVO implements BackupRepository { @Column(name = "capacity_bytes", nullable = true) private Long capacityBytes; + @Column(name = "draas_enabled") + private Boolean draasEnabled; + @Column(name = "created") @Temporal(value = TemporalType.TIMESTAMP) private Date created; @@ -79,7 +82,7 @@ public BackupRepositoryVO() { this.uuid = UUID.randomUUID().toString(); } - public BackupRepositoryVO(final long zoneId, final String provider, final String name, final String type, final String address, final String mountOptions, final Long capacityBytes) { + public BackupRepositoryVO(final long zoneId, final String provider, final String name, final String type, final String address, final String mountOptions, final Long capacityBytes, final Boolean draasEnabled) { this(); this.zoneId = zoneId; this.provider = provider; @@ -88,6 +91,7 @@ public BackupRepositoryVO(final long zoneId, final String provider, final String this.address = address; this.mountOptions = mountOptions; this.capacityBytes = capacityBytes; + this.draasEnabled = draasEnabled; this.created = new Date(); } @@ -139,6 +143,11 @@ public String getMountOptions() { return mountOptions; } + @Override + public void setMountOptions(String mountOptions) { + this.mountOptions = mountOptions; + } + @Override public Long getUsedBytes() { return usedBytes; @@ -154,6 +163,16 @@ public Long getCapacityBytes() { return capacityBytes; } + @Override + public Boolean isDraasEnabled() { + return draasEnabled; + } + + @Override + public void setDraasEnabled(Boolean draasEnabled) { + this.draasEnabled = draasEnabled; + } + @Override public void setCapacityBytes(Long capacityBytes) { this.capacityBytes = capacityBytes; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql index 0f4e8b6f2a29..362bf29c4bfc 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql @@ -18,3 +18,6 @@ --; -- Schema upgrade from 4.21.0.0 to 4.22.0.0 --; + +-- Add the column draas_enabled to cloud.backup_repository. if enabled it means that new Instance can be created on another Zones from Backups on this Repository. +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_repository', 'draas_enabled', 'TINYINT(1) DEFAULT NULL COMMENT ''Backup Repository can be used for disaster recovery on another zone'''); diff --git a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java index cdadea0e991b..684816b8680b 100644 --- a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java +++ b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java @@ -53,6 +53,11 @@ public class DummyBackupProvider extends AdapterBase implements BackupProvider { @Inject private DiskOfferingDao diskOfferingDao; + @Override + public Boolean isDraasEnabled(BackupOffering backupOffering) { + return true; + } + @Override public String getName() { return "dummy"; diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index 5128aeeb8fdf..93f9e9319320 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -530,6 +530,15 @@ public boolean isValidProviderOffering(Long zoneId, String uuid) { return true; } + @Override + public Boolean isDraasEnabled(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.isDraasEnabled()); + } + @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[]{ diff --git a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java index d6f29dc1aac1..f59a9d82e999 100644 --- a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java +++ b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java @@ -94,7 +94,7 @@ public void testDeleteBackup() throws OperationTimedoutException, AgentUnavailab 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 +113,7 @@ public void testDeleteBackup() throws OperationTimedoutException, AgentUnavailab @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 +132,7 @@ public void testSyncBackupStorageStats() throws AgentUnavailableException, Opera @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 +146,11 @@ public void testListBackupOfferings() { @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")) diff --git a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java index 84184a335070..0858689c8e2c 100644 --- a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java +++ b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java @@ -158,6 +158,11 @@ public ConfigKey[] getConfigKeys() { }; } + @Override + public Boolean isDraasEnabled(BackupOffering backupOffering) { + return false; + } + @Override public String getName() { return "networker"; diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index bf39f2d5836c..3871b5fa6a4f 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -358,6 +358,11 @@ public Pair getBackupStorageStats(Long zoneId) { public void syncBackupStorageStats(Long zoneId) { } + @Override + public Boolean isDraasEnabled(BackupOffering backupOffering) { + return false; + } + @Override public String getConfigComponentName() { return BackupService.class.getSimpleName(); diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 64d6e8b6929d..f392d3f931f4 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -5553,6 +5553,7 @@ public BackupRepositoryResponse createBackupRepositoryResponse(BackupRepository response.setProviderName(backupRepository.getProvider()); response.setType(backupRepository.getType()); response.setCapacityBytes(backupRepository.getCapacityBytes()); + response.setDraasEnabled(backupRepository.isDraasEnabled()); response.setObjectName("backuprepository"); DataCenter zone = ApiDBUtils.findZoneById(backupRepository.getZoneId()); if (zone != null) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 426ef1a45a01..0c8d4524706c 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -9461,21 +9461,27 @@ private void checkRootDiskSizeAgainstBackup(Long instanceVolumeSize,DiskOffering @Override public UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws InsufficientCapacityException, ResourceAllocationException, ResourceUnavailableException { + BackupVO backup = backupDao.findById(cmd.getBackupId()); + if (backup == null) { + throw new InvalidParameterValueException("Backup " + cmd.getBackupId() + " does not exist"); + } + backupManager.validateBackupForZone(backup.getZoneId()); + 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) { + + DataCenter targetZone = _dcDao.findById(cmd.getZoneId()); + if (targetZone == 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 (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."); } - backupManager.validateBackupForZone(backup.getZoneId()); - backupDao.loadDetails(backup); + backupDao.loadDetails(backup); verifyDetails(cmd.getDetails()); UserVmVO backupVm = _vmDao.findByIdIncludingRemoved(backup.getVmId()); @@ -9574,7 +9580,7 @@ public UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws Insufficien List networkIds = cmd.getNetworkIds(); Account owner = _accountService.getActiveAccountById(cmd.getEntityOwnerId()); - LinkedHashMap userVmNetworkMap = getVmOvfNetworkMapping(zone, owner, template, cmd.getVmNetworkMap()); + LinkedHashMap userVmNetworkMap = getVmOvfNetworkMapping(targetZone, owner, template, cmd.getVmNetworkMap()); if (MapUtils.isNotEmpty(userVmNetworkMap)) { networkIds = new ArrayList<>(userVmNetworkMap.values()); } @@ -9585,7 +9591,7 @@ public UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws Insufficien 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) { diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index d95f4c222a06..c406cfdd9b68 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -62,6 +62,7 @@ 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; @@ -1258,6 +1259,17 @@ public Boolean canCreateInstanceFromBackup(final Long backupId) { return backupProvider.supportsInstanceFromBackup(); } + @Override + 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.isDraasEnabled(offering); + } + @Override public boolean restoreBackupToVM(final Long backupId, final Long vmId) throws ResourceUnavailableException { final BackupVO backup = backupDao.findById(backupId); @@ -1616,6 +1628,7 @@ public List> getCommands() { 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); diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupRepositoryServiceImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupRepositoryServiceImpl.java index 5eb6538eaf51..7df8f3bdfae4 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupRepositoryServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupRepositoryServiceImpl.java @@ -28,10 +28,12 @@ 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; @@ -52,10 +54,54 @@ public class BackupRepositoryServiceImpl extends ManagerBase implements BackupRe @Override 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.isDraasEnabled()); return repositoryDao.persist(repository); } + @Override + public BackupRepository updateBackupRepository(UpdateBackupRepositoryCmd cmd) { + Long id = cmd.getId(); + String name = cmd.getName(); + String address = cmd.getAddress(); + String mountOptions = cmd.getMountOptions(); + Boolean draasEnabled = cmd.isDraasEnabled(); + + 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 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 (draasEnabled != null){ + backupRepositoryVO.setDraasEnabled(draasEnabled); + fields.add("draasEnabled: " + draasEnabled); + } + + if (!repositoryDao.update(id, backupRepositoryVO)) { + logger.warn(String.format("Couldn't update Backup repository (%s) with [%s].", backupRepositoryVO, String.join(", ", fields))); + } + + BackupRepositoryVO repositoryVO = repositoryDao.findById(id); + CallContext.current().setEventDetails(String.format("Backup Repository updated [%s].", + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(repositoryVO, "id", "name", "description", "userDrivenBackupAllowed", "externalId"))); + return repositoryVO; + } + @Override public boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd) { BackupRepositoryVO backupRepositoryVO = repositoryDao.findById(cmd.getId()); diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 6f35fb49596d..c98ea644be97 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -456,6 +456,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", @@ -921,6 +922,7 @@ "label.download.setting": "Download setting", "label.download.state": "Download state", "label.dpd": "Dead peer detection", +"label.draasenabled": "DRaaS Enabled", "label.driver": "Driver", "label.drs": "DRS", "label.drsimbalance": "DRS imbalance", @@ -2911,6 +2913,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.", diff --git a/ui/src/components/view/DeployVMFromBackup.vue b/ui/src/components/view/DeployVMFromBackup.vue index c3a981d39fcb..d60a0b15567e 100644 --- a/ui/src/components/view/DeployVMFromBackup.vue +++ b/ui/src/components/view/DeployVMFromBackup.vue @@ -46,6 +46,7 @@