From 5397375caf8b8593adcb7459133723bdc939faa0 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 19 Sep 2025 10:28:58 +0530 Subject: [PATCH 1/6] Support restore from NAS backup to Ceph storage pool(s) --- .../cloudstack/backup/BackupManager.java | 2 +- .../backup/RestoreBackupCommand.java | 10 ++ .../cloudstack/backup/NASBackupProvider.java | 56 +++++-- .../LibvirtRestoreBackupCommandWrapper.java | 138 +++++++++++++++--- .../apache/cloudstack/utils/qemu/QemuImg.java | 7 + 5 files changed, 178 insertions(+), 35 deletions(-) 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 37d21613c3dd..f1f0c3c31ee0 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -55,7 +55,7 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer ConfigKey BackupProviderPlugin = new ConfigKey<>("Advanced", String.class, "backup.framework.provider.plugin", "dummy", - "The backup and recovery provider plugin.", true, ConfigKey.Scope.Zone, BackupFrameworkEnabled.key()); + "The backup and recovery provider plugin. Valid plugin values: dummy, veeam, networker and nas", true, ConfigKey.Scope.Zone, BackupFrameworkEnabled.key()); ConfigKey BackupSyncPollingInterval = new ConfigKey<>("Advanced", Long.class, "backup.framework.sync.interval", diff --git a/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java index 453b236df6b9..9cbb87da19a9 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java @@ -22,6 +22,7 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.LogLevel; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import java.util.List; @@ -31,6 +32,7 @@ public class RestoreBackupCommand extends Command { private String backupRepoType; private String backupRepoAddress; private List backupVolumesUUIDs; + private List restoreVolumePools; private List restoreVolumePaths; private String diskType; private Boolean vmExists; @@ -74,6 +76,14 @@ public void setBackupRepoAddress(String backupRepoAddress) { this.backupRepoAddress = backupRepoAddress; } + public List getRestoreVolumePools() { + return restoreVolumePools; + } + + public void setRestoreVolumePools(List restoreVolumePools) { + this.restoreVolumePools = restoreVolumePools; + } + public List getRestoreVolumePaths() { return restoreVolumePaths; } 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 9cd2f20e3862..54f74dffef38 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 @@ -26,6 +26,7 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.offering.DiskOffering; import com.cloud.resource.ResourceManager; +import com.cloud.storage.DataStoreRole; import com.cloud.storage.ScopeType; import com.cloud.storage.Storage; import com.cloud.storage.StoragePoolHostVO; @@ -49,10 +50,13 @@ import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -106,6 +110,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private PrimaryDataStoreDao primaryDataStoreDao; + @Inject + DataStoreManager dataStoreMgr; + @Inject private AgentManager agentManager; @@ -203,7 +210,8 @@ public Pair takeBackup(final VirtualMachine vm, Boolean quiesce if (VirtualMachine.State.Stopped.equals(vm.getState())) { List vmVolumes = volumeDao.findByInstance(vm.getId()); vmVolumes.sort(Comparator.comparing(Volume::getDeviceId)); - List volumePaths = getVolumePaths(vmVolumes); + Pair, List> volumePoolsAndPaths = getVolumePoolsAndPaths(vmVolumes); + List volumePaths = volumePoolsAndPaths.second(); command.setVolumePaths(volumePaths); } @@ -303,7 +311,9 @@ private Pair restoreVMBackup(VirtualMachine vm, Backup backup) restoreCommand.setMountOptions(backupRepository.getMountOptions()); restoreCommand.setVmName(vm.getName()); restoreCommand.setBackupVolumesUUIDs(backedVolumesUUIDs); - restoreCommand.setRestoreVolumePaths(getVolumePaths(restoreVolumes)); + Pair, List> volumePoolsAndPaths = getVolumePoolsAndPaths(restoreVolumes); + restoreCommand.setRestoreVolumePools(volumePoolsAndPaths.first()); + restoreCommand.setRestoreVolumePaths(volumePoolsAndPaths.second()); restoreCommand.setVmExists(vm.getRemoved() == null); restoreCommand.setVmState(vm.getState()); restoreCommand.setMountTimeout(NASBackupRestoreMountTimeout.value()); @@ -319,32 +329,42 @@ private Pair restoreVMBackup(VirtualMachine vm, Backup backup) return new Pair<>(answer.getResult(), answer.getDetails()); } - private List getVolumePaths(List volumes) { + private Pair, List> getVolumePoolsAndPaths(List volumes) { + List volumePools = new ArrayList<>(); List volumePaths = new ArrayList<>(); for (VolumeVO volume : volumes) { StoragePoolVO storagePool = primaryDataStoreDao.findById(volume.getPoolId()); if (Objects.isNull(storagePool)) { throw new CloudRuntimeException("Unable to find storage pool associated to the volume"); } - String volumePathPrefix; - if (ScopeType.HOST.equals(storagePool.getScope())) { - volumePathPrefix = storagePool.getPath(); - } else if (Storage.StoragePoolType.SharedMountPoint.equals(storagePool.getPoolType())) { - volumePathPrefix = storagePool.getPath(); - } else { - volumePathPrefix = String.format("/mnt/%s", storagePool.getUuid()); - } + String volumePathPrefix = getVolumePathPrefix(storagePool); + DataStore dataStore = dataStoreMgr.getDataStore(storagePool.getId(), DataStoreRole.Primary); + volumePools.add(dataStore != null ? (PrimaryDataStoreTO)dataStore.getTO() : null); volumePaths.add(String.format("%s/%s", volumePathPrefix, volume.getPath())); } - return volumePaths; + return new Pair<>(volumePools, volumePaths); + } + + private String getVolumePathPrefix(StoragePoolVO storagePool) { + String volumePathPrefix; + if (ScopeType.HOST.equals(storagePool.getScope()) || + Storage.StoragePoolType.SharedMountPoint.equals(storagePool.getPoolType()) || + Storage.StoragePoolType.RBD.equals(storagePool.getPoolType())) { + volumePathPrefix = storagePool.getPath(); + } else { + // Should be Storage.StoragePoolType.NetworkFilesystem + volumePathPrefix = String.format("/mnt/%s", storagePool.getUuid()); + } + return volumePathPrefix; } @Override public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { final VolumeVO volume = volumeDao.findByUuid(backupVolumeInfo.getUuid()); final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); - final StoragePoolHostVO dataStore = storagePoolHostDao.findByUuid(dataStoreUuid); + final StoragePoolVO pool = primaryDataStoreDao.findByUuid(dataStoreUuid); final HostVO hostVO = hostDao.findByIp(hostIp); + final StoragePoolHostVO storagePoolHost = storagePoolHostDao.findByPoolHost(pool.getId(), hostVO.getId()); LOG.debug("Restoring vm volume {} from backup {} on the NAS Backup Provider", backupVolumeInfo, backup); BackupRepository backupRepository = getBackupRepository(backup); @@ -360,10 +380,14 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI restoredVolume.setUuid(volumeUUID); restoredVolume.setRemoved(null); restoredVolume.setDisplayVolume(true); - restoredVolume.setPoolId(dataStore.getPoolId()); + restoredVolume.setPoolId(pool.getId()); + restoredVolume.setPoolType(pool.getPoolType()); restoredVolume.setPath(restoredVolume.getUuid()); restoredVolume.setState(Volume.State.Copying); restoredVolume.setFormat(Storage.ImageFormat.QCOW2); + if (pool.getPoolType() == Storage.StoragePoolType.RBD) { + restoredVolume.setFormat(Storage.ImageFormat.RAW); + } restoredVolume.setSize(backupVolumeInfo.getSize()); restoredVolume.setDiskOfferingId(diskOffering.getId()); @@ -372,7 +396,9 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI restoreCommand.setBackupRepoType(backupRepository.getType()); restoreCommand.setBackupRepoAddress(backupRepository.getAddress()); restoreCommand.setVmName(vmNameAndState.first()); - restoreCommand.setRestoreVolumePaths(Collections.singletonList(String.format("%s/%s", dataStore.getLocalPath(), volumeUUID))); + restoreCommand.setRestoreVolumePaths(Collections.singletonList(String.format("%s%s", storagePoolHost != null ? storagePoolHost.getLocalPath() + "/" : "", volumeUUID))); + DataStore dataStore = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); + restoreCommand.setRestoreVolumePools(Collections.singletonList(dataStore != null ? (PrimaryDataStoreTO)dataStore.getTO() : null)); restoreCommand.setDiskType(backupVolumeInfo.getType().name().toLowerCase(Locale.ROOT)); restoreCommand.setMountOptions(backupRepository.getMountOptions()); restoreCommand.setVmExists(null); 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 243cf2efa031..21b29c6fad19 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 @@ -21,15 +21,25 @@ import com.cloud.agent.api.Answer; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.backup.BackupAnswer; import org.apache.cloudstack.backup.RestoreBackupCommand; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.libvirt.LibvirtException; import java.io.File; import java.io.IOException; @@ -45,7 +55,9 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper backedVolumeUUIDs = command.getBackupVolumesUUIDs(); + List restoreVolumePools = command.getRestoreVolumePools(); List restoreVolumePaths = command.getRestoreVolumePaths(); String restoreVolumeUuid = command.getRestoreVolumeUUID(); Integer mountTimeout = command.getMountTimeout() * 1000; + int timeout = command.getWait(); + KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); String newVolumeId = null; try { String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions, mountTimeout); if (Objects.isNull(vmExists)) { + PrimaryDataStoreTO volumePool = restoreVolumePools.get(0); String volumePath = restoreVolumePaths.get(0); int lastIndex = volumePath.lastIndexOf("/"); newVolumeId = volumePath.substring(lastIndex + 1); - restoreVolume(backupPath, volumePath, diskType, restoreVolumeUuid, - new Pair<>(vmName, command.getVmState()), mountDirectory); + restoreVolume(storagePoolMgr, backupPath, volumePool, volumePath, diskType, restoreVolumeUuid, + new Pair<>(vmName, command.getVmState()), mountDirectory, timeout); } else if (Boolean.TRUE.equals(vmExists)) { - restoreVolumesOfExistingVM(restoreVolumePaths, backedVolumeUUIDs, backupPath, mountDirectory); + restoreVolumesOfExistingVM(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backedVolumeUUIDs, backupPath, mountDirectory, timeout); } else { - restoreVolumesOfDestroyedVMs(restoreVolumePaths, vmName, backupPath, mountDirectory); + restoreVolumesOfDestroyedVMs(storagePoolMgr, restoreVolumePools, restoreVolumePaths, vmName, backupPath, mountDirectory, timeout); } } catch (CloudRuntimeException e) { String errorMessage = e.getMessage() != null ? e.getMessage() : ""; @@ -94,17 +110,18 @@ private void verifyBackupFile(String backupPath, String volUuid) { } } - private void restoreVolumesOfExistingVM(List restoreVolumePaths, List backedVolumesUUIDs, - String backupPath, String mountDirectory) { + private void restoreVolumesOfExistingVM(KVMStoragePoolManager storagePoolMgr, List restoreVolumePools, List restoreVolumePaths, List backedVolumesUUIDs, + String backupPath, String mountDirectory, int timeout) { String diskType = "root"; try { for (int idx = 0; idx < restoreVolumePaths.size(); idx++) { + PrimaryDataStoreTO restoreVolumePool = restoreVolumePools.get(idx); String restoreVolumePath = restoreVolumePaths.get(idx); String backupVolumeUuid = backedVolumesUUIDs.get(idx); Pair bkpPathAndVolUuid = getBackupPath(mountDirectory, null, backupPath, diskType, backupVolumeUuid); diskType = "datadisk"; verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(restoreVolumePath, bkpPathAndVolUuid.first())) { + if (!replaceVolumeWithBackup(storagePoolMgr, restoreVolumePool, restoreVolumePath, bkpPathAndVolUuid.first(), timeout)) { throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); } } @@ -114,15 +131,16 @@ private void restoreVolumesOfExistingVM(List restoreVolumePaths, List volumePaths, String vmName, String backupPath, String mountDirectory) { + private void restoreVolumesOfDestroyedVMs(KVMStoragePoolManager storagePoolMgr, List volumePools, List volumePaths, String vmName, String backupPath, String mountDirectory, int timeout) { String diskType = "root"; try { for (int i = 0; i < volumePaths.size(); i++) { + PrimaryDataStoreTO volumePool = volumePools.get(i); 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())) { + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout)) { throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); } } @@ -132,17 +150,17 @@ private void restoreVolumesOfDestroyedVMs(List volumePaths, String vmNam } } - private void restoreVolume(String backupPath, String volumePath, String diskType, String volumeUUID, - Pair vmNameAndState, String mountDirectory) { + private void restoreVolume(KVMStoragePoolManager storagePoolMgr, String backupPath, PrimaryDataStoreTO volumePool, String volumePath, String diskType, String volumeUUID, + Pair vmNameAndState, String mountDirectory, int timeout) { Pair bkpPathAndVolUuid; try { bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID); verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) { + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout)) { throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); } if (VirtualMachine.State.Running.equals(vmNameAndState.second())) { - if (!attachVolumeToVm(vmNameAndState.first(), volumePath)) { + if (!attachVolumeToVm(storagePoolMgr, vmNameAndState.first(), volumePool, volumePath)) { throw new CloudRuntimeException(String.format("Failed to attach volume to VM: %s", vmNameAndState.first())); } } @@ -220,14 +238,58 @@ private boolean checkBackupPathExists(String backupPath) { return exitValue == 0; } - private boolean replaceVolumeWithBackup(String volumePath, String backupPath) { - int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath)); - return exitValue == 0; + private boolean replaceVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout) { + if (volumePool.getPoolType() != Storage.StoragePoolType.RBD) { + int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath)); + return exitValue == 0; + } + + return replaceRbdVolumeWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout); + } + + private boolean replaceRbdVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout) { + KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); + KVMPhysicalDisk rdbDisk = volumeStoragePool.getPhysicalDisk(volumePath); + logger.debug("RBD volume: {}", rdbDisk.toString()); + QemuImg qemu; + try { + qemu = new QemuImg(timeout * 1000, true, false); + qemu.setSkipTargetVolumeCreation(true); + } catch (LibvirtException ex) { + throw new CloudRuntimeException("Failed to create qemu-img command to replace RBD volume with backup", ex); + } + QemuImgFile srcBackupFile = null; + QemuImgFile destVolumeFile = null; + + try { + srcBackupFile = new QemuImgFile(backupPath, QemuImg.PhysicalDiskFormat.QCOW2); + String rbdDestVolumeFile = KVMPhysicalDisk.RBDStringBuilder(volumeStoragePool, volumePath); + destVolumeFile = new QemuImgFile(rbdDestVolumeFile, QemuImg.PhysicalDiskFormat.RAW); + + logger.debug("Starting convert backup {} to RBD volume {}", backupPath, volumePath); + qemu.convert(srcBackupFile, destVolumeFile); + logger.debug("Successfully converted backup {} to RBD volume {}", backupPath, volumePath); + } catch (QemuImgException | LibvirtException e) { + String srcFilename = srcBackupFile != null ? srcBackupFile.getFileName() : null; + String destFilename = destVolumeFile != null ? destVolumeFile.getFileName() : null; + logger.error("Failed to convert backup {} to volume {}, the error was: {}", srcFilename, destFilename, e.getMessage()); + return false; + } + + return true; } - private boolean attachVolumeToVm(String vmName, String volumePath) { + private boolean attachVolumeToVm(KVMStoragePoolManager storagePoolMgr, String vmName, PrimaryDataStoreTO volumePool, String volumePath) { String deviceToAttachDiskTo = getDeviceToAttachDisk(vmName); - int exitValue = Script.runSimpleBashScriptForExitValue(String.format(ATTACH_DISK_COMMAND, vmName, volumePath, deviceToAttachDiskTo)); + int exitValue; + if (volumePool.getPoolType() != Storage.StoragePoolType.RBD) { + exitValue = Script.runSimpleBashScriptForExitValue(String.format(ATTACH_QCOW2_DISK_COMMAND, vmName, volumePath, deviceToAttachDiskTo)); + } else { +// exitValue = Script.runSimpleBashScriptForExitValue(String.format(ATTACH_RBD_DISK_COMMAND, vmName, deviceToAttachDiskTo, volumePath)); + String xmlForRbdDisk = getXmlForRbdDisk(storagePoolMgr, volumePool, volumePath, deviceToAttachDiskTo); + logger.debug("RBD disk xml to attach: {}", xmlForRbdDisk); + exitValue = Script.runSimpleBashScriptForExitValue(String.format(ATTACH_RBD_DISK_XML_COMMAND, vmName, xmlForRbdDisk)); + } return exitValue == 0; } @@ -237,4 +299,42 @@ private String getDeviceToAttachDisk(String vmName) { char incrementedChar = (char) (lastChar + 1); return currentDevice.substring(0, currentDevice.length() - 1) + incrementedChar; } + + private String getXmlForRbdDisk(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String deviceToAttachDiskTo) { + StringBuilder diskBuilder = new StringBuilder(); + diskBuilder.append("\n\n"); + + diskBuilder.append("\n"); + for (String sourceHost : volumePool.getHost().split(",")) { + diskBuilder.append("\n"); + } + diskBuilder.append("\n"); + String authUserName = null; + final KVMStoragePool primaryPool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); + if (primaryPool != null) { + authUserName = primaryPool.getAuthUserName(); + } + if (StringUtils.isNotBlank(authUserName)) { + diskBuilder.append("\n"); + diskBuilder.append("\n"); + diskBuilder.append("\n"); + } + diskBuilder.append("\n"); + diskBuilder.append("\n"); + return diskBuilder.toString(); + } } diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java index beba08562415..0a8ea27cd490 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java @@ -61,6 +61,7 @@ public class QemuImg { private String cloudQemuImgPath = "cloud-qemu-img"; private int timeout; private boolean skipZero = false; + private boolean skipTargetVolumeCreation = false; private boolean noCache = false; private long version; @@ -435,6 +436,8 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, // with target-is-zero we skip zeros in 1M chunks for compatibility script.add("-S"); script.add("1M"); + } else if (skipTargetVolumeCreation) { + script.add("-n"); } script.add("-O"); @@ -881,6 +884,10 @@ public void setSkipZero(boolean skipZero) { this.skipZero = skipZero; } + public void setSkipTargetVolumeCreation(boolean skipTargetVolumeCreation) { + this.skipTargetVolumeCreation = skipTargetVolumeCreation; + } + public boolean supportsImageFormat(QemuImg.PhysicalDiskFormat format) { final Script s = new Script(_qemuImgPath, timeout); s.add("--help"); From 792b80fefb909196162e3a5a0e73820c752f2654 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 19 Sep 2025 13:52:44 +0530 Subject: [PATCH 2/6] Fix restore/attach backup volume as RBD volume in Ceph storage pool --- .../cloudstack/backup/NASBackupProvider.java | 8 +++---- .../LibvirtRestoreBackupCommandWrapper.java | 22 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) 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 54f74dffef38..c7dce73da38a 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 @@ -29,7 +29,6 @@ import com.cloud.storage.DataStoreRole; import com.cloud.storage.ScopeType; import com.cloud.storage.Storage; -import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeApiServiceImpl; import com.cloud.storage.VolumeVO; @@ -337,9 +336,11 @@ private Pair, List> getVolumePoolsAndPaths(List if (Objects.isNull(storagePool)) { throw new CloudRuntimeException("Unable to find storage pool associated to the volume"); } - String volumePathPrefix = getVolumePathPrefix(storagePool); + DataStore dataStore = dataStoreMgr.getDataStore(storagePool.getId(), DataStoreRole.Primary); volumePools.add(dataStore != null ? (PrimaryDataStoreTO)dataStore.getTO() : null); + + String volumePathPrefix = getVolumePathPrefix(storagePool); volumePaths.add(String.format("%s/%s", volumePathPrefix, volume.getPath())); } return new Pair<>(volumePools, volumePaths); @@ -364,7 +365,6 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); final StoragePoolVO pool = primaryDataStoreDao.findByUuid(dataStoreUuid); final HostVO hostVO = hostDao.findByIp(hostIp); - final StoragePoolHostVO storagePoolHost = storagePoolHostDao.findByPoolHost(pool.getId(), hostVO.getId()); LOG.debug("Restoring vm volume {} from backup {} on the NAS Backup Provider", backupVolumeInfo, backup); BackupRepository backupRepository = getBackupRepository(backup); @@ -396,7 +396,7 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI restoreCommand.setBackupRepoType(backupRepository.getType()); restoreCommand.setBackupRepoAddress(backupRepository.getAddress()); restoreCommand.setVmName(vmNameAndState.first()); - restoreCommand.setRestoreVolumePaths(Collections.singletonList(String.format("%s%s", storagePoolHost != null ? storagePoolHost.getLocalPath() + "/" : "", volumeUUID))); + restoreCommand.setRestoreVolumePaths(Collections.singletonList(String.format("%s/%s", getVolumePathPrefix(pool), volumeUUID))); DataStore dataStore = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); restoreCommand.setRestoreVolumePools(Collections.singletonList(dataStore != null ? (PrimaryDataStoreTO)dataStore.getTO() : null)); restoreCommand.setDiskType(backupVolumeInfo.getType().name().toLowerCase(Locale.ROOT)); 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 21b29c6fad19..cb76f4c5063d 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 @@ -56,7 +56,6 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper Date: Tue, 23 Sep 2025 14:37:56 +0530 Subject: [PATCH 3/6] addressed review comments --- .../org/apache/cloudstack/backup/NASBackupProvider.java | 9 +++++---- .../wrapper/LibvirtRestoreBackupCommandWrapper.java | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) 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 c7dce73da38a..a3856d24e20d 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 @@ -384,12 +384,13 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI restoredVolume.setPoolType(pool.getPoolType()); restoredVolume.setPath(restoredVolume.getUuid()); restoredVolume.setState(Volume.State.Copying); - restoredVolume.setFormat(Storage.ImageFormat.QCOW2); - if (pool.getPoolType() == Storage.StoragePoolType.RBD) { - restoredVolume.setFormat(Storage.ImageFormat.RAW); - } restoredVolume.setSize(backupVolumeInfo.getSize()); restoredVolume.setDiskOfferingId(diskOffering.getId()); + if (pool.getPoolType() != Storage.StoragePoolType.RBD) { + restoredVolume.setFormat(Storage.ImageFormat.QCOW2); + } else { + restoredVolume.setFormat(Storage.ImageFormat.RAW); + } RestoreBackupCommand restoreCommand = new RestoreBackupCommand(); restoreCommand.setBackupPath(backup.getExternalId()); 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 cb76f4c5063d..fd94013dd50c 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 @@ -257,11 +257,11 @@ private boolean replaceRbdVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, qemu = new QemuImg(timeout * 1000, true, false); if (!createTargetVolume) { KVMPhysicalDisk rdbDisk = volumeStoragePool.getPhysicalDisk(volumePath); - logger.debug("RBD volume: {}", rdbDisk.toString()); + logger.debug("Restoring RBD volume: {}", rdbDisk.toString()); qemu.setSkipTargetVolumeCreation(true); } } catch (LibvirtException ex) { - throw new CloudRuntimeException("Failed to create qemu-img command to replace RBD volume with backup", ex); + throw new CloudRuntimeException("Failed to create qemu-img command to restore RBD volume with backup", ex); } QemuImgFile srcBackupFile = null; From b85267b9b1966e9c7510e96c7db0eb778452c3f8 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 26 Sep 2025 13:30:36 +0530 Subject: [PATCH 4/6] Support take NAS backup for stopped instances with volumes on Ceph storage pool(s) --- .../cloudstack/backup/TakeBackupCommand.java | 10 +++++++++ .../cloudstack/backup/NASBackupProvider.java | 4 ++-- .../LibvirtTakeBackupCommandWrapper.java | 22 ++++++++++++++++++- scripts/vm/hypervisor/kvm/nasbackup.sh | 9 +++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java index ecebd57a178c..5402b6b24760 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java @@ -21,6 +21,7 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.LogLevel; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import java.util.List; @@ -29,6 +30,7 @@ public class TakeBackupCommand extends Command { private String backupPath; private String backupRepoType; private String backupRepoAddress; + private List volumePools; private List volumePaths; private Boolean quiesce; @LogLevel(LogLevel.Log4jLevel.Off) @@ -80,6 +82,14 @@ public void setMountOptions(String mountOptions) { this.mountOptions = mountOptions; } + public List getVolumePools() { + return volumePools; + } + + public void setVolumePools(List volumePools) { + this.volumePools = volumePools; + } + public List getVolumePaths() { return volumePaths; } 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 a3856d24e20d..3813cac0a33f 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 @@ -210,8 +210,8 @@ public Pair takeBackup(final VirtualMachine vm, Boolean quiesce List vmVolumes = volumeDao.findByInstance(vm.getId()); vmVolumes.sort(Comparator.comparing(Volume::getDeviceId)); Pair, List> volumePoolsAndPaths = getVolumePoolsAndPaths(vmVolumes); - List volumePaths = volumePoolsAndPaths.second(); - command.setVolumePaths(volumePaths); + command.setVolumePools(volumePoolsAndPaths.first()); + command.setVolumePaths(volumePoolsAndPaths.second()); } BackupAnswer answer; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java index c7a67080fbfe..38ea58e5956e 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java @@ -22,12 +22,17 @@ import com.amazonaws.util.CollectionUtils; import com.cloud.agent.api.Answer; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; import com.cloud.utils.Pair; import com.cloud.utils.script.Script; import org.apache.cloudstack.backup.BackupAnswer; import org.apache.cloudstack.backup.TakeBackupCommand; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import java.util.ArrayList; import java.util.Arrays; @@ -44,7 +49,22 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir final String backupRepoType = command.getBackupRepoType(); final String backupRepoAddress = command.getBackupRepoAddress(); final String mountOptions = command.getMountOptions(); - final List diskPaths = command.getVolumePaths(); + List volumePools = command.getVolumePools(); + final List volumePaths = command.getVolumePaths(); + KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + + List diskPaths = new ArrayList<>(); + for (int idx = 0; idx < volumePaths.size(); idx++) { + PrimaryDataStoreTO volumePool = volumePools.get(idx); + String volumePath = volumePaths.get(idx); + if (volumePool.getPoolType() != Storage.StoragePoolType.RBD) { + diskPaths.add(volumePath); + } else { + KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); + String rbdDestVolumeFile = KVMPhysicalDisk.RBDStringBuilder(volumeStoragePool, volumePath); + diskPaths.add(rbdDestVolumeFile); + } + } List commands = new ArrayList<>(); commands.add(new String[]{ diff --git a/scripts/vm/hypervisor/kvm/nasbackup.sh b/scripts/vm/hypervisor/kvm/nasbackup.sh index 588c3791769e..e298006f7a8c 100755 --- a/scripts/vm/hypervisor/kvm/nasbackup.sh +++ b/scripts/vm/hypervisor/kvm/nasbackup.sh @@ -165,7 +165,14 @@ backup_stopped_vm() { name="root" for disk in $DISK_PATHS; do - volUuid="${disk##*/}" + if [[ "$disk" == rbd:* ]]; then + # disk for rbd => rbd:/:mon_host=... + # sample: rbd:cloudstack/53d5c355-d726-4d3e-9422-046a503a0b12:mon_host=10.0.1.2... + beforeUuid="${disk#*/}" # Remove up to first slash after rbd: + volUuid="${beforeUuid%%:*}" # Remove everything after colon to get the uuid + else + volUuid="${disk##*/}" + fi output="$dest/$name.$volUuid.qcow2" if ! qemu-img convert -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then echo "qemu-img convert failed for $disk $output" From 9edae46fbabc65bde5974469f3c0cec960750b22 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 26 Sep 2025 17:32:35 +0530 Subject: [PATCH 5/6] test fixes --- ...ibvirtRestoreBackupCommandWrapperTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java index d120abd0a1bd..7bcd0bf18e6a 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java @@ -22,6 +22,7 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.backup.BackupAnswer; import org.apache.cloudstack.backup.RestoreBackupCommand; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -65,6 +66,8 @@ public void testExecuteWithVmExistsNull() throws Exception { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -105,6 +108,8 @@ public void testExecuteWithVmExistsTrue() throws Exception { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(true); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getBackupVolumesUUIDs()).thenReturn(Arrays.asList("volume-123")); when(command.getMountTimeout()).thenReturn(30); @@ -141,6 +146,8 @@ public void testExecuteWithVmExistsFalse() throws Exception { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(false); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getMountTimeout()).thenReturn(30); @@ -176,6 +183,8 @@ public void testExecuteWithCifsMountType() throws Exception { when(command.getMountOptions()).thenReturn("username=user,password=pass"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -215,6 +224,8 @@ public void testExecuteWithMountFailure() throws Exception { lenient().when(command.getMountOptions()).thenReturn("rw"); lenient().when(command.isVmExists()).thenReturn(null); lenient().when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); lenient().when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); lenient().when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); lenient().when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -249,6 +260,8 @@ public void testExecuteWithBackupFileNotFound() throws Exception { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -293,6 +306,8 @@ public void testExecuteWithCorruptBackupFile() throws Exception { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -339,6 +354,8 @@ public void testExecuteWithRsyncFailure() throws Exception { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -387,6 +404,8 @@ public void testExecuteWithAttachVolumeFailure() throws Exception { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -439,6 +458,8 @@ public void testExecuteWithTempDirectoryCreationFailure() throws Exception { lenient().when(command.getMountOptions()).thenReturn("rw"); lenient().when(command.isVmExists()).thenReturn(null); lenient().when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); lenient().when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); lenient().when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); lenient().when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -467,6 +488,12 @@ public void testExecuteWithMultipleVolumes() throws Exception { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(true); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore1 = Mockito.mock(PrimaryDataStoreTO.class); + PrimaryDataStoreTO primaryDataStore2 = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList( + primaryDataStore1, + primaryDataStore2 + )); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList( "/var/lib/libvirt/images/volume-123", "/var/lib/libvirt/images/volume-456" From ebd081aa4b6fbd1e8df6a7a2d4651ed707781ecb Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:10:04 +0530 Subject: [PATCH 6/6] Fix NPE for live vm backup --- .../LibvirtTakeBackupCommandWrapper.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java index 38ea58e5956e..11fa605908a6 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java @@ -54,15 +54,17 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); List diskPaths = new ArrayList<>(); - for (int idx = 0; idx < volumePaths.size(); idx++) { - PrimaryDataStoreTO volumePool = volumePools.get(idx); - String volumePath = volumePaths.get(idx); - if (volumePool.getPoolType() != Storage.StoragePoolType.RBD) { - diskPaths.add(volumePath); - } else { - KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); - String rbdDestVolumeFile = KVMPhysicalDisk.RBDStringBuilder(volumeStoragePool, volumePath); - diskPaths.add(rbdDestVolumeFile); + if (Objects.nonNull(volumePaths)) { + for (int idx = 0; idx < volumePaths.size(); idx++) { + PrimaryDataStoreTO volumePool = volumePools.get(idx); + String volumePath = volumePaths.get(idx); + if (volumePool.getPoolType() != Storage.StoragePoolType.RBD) { + diskPaths.add(volumePath); + } else { + KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); + String rbdDestVolumeFile = KVMPhysicalDisk.RBDStringBuilder(volumeStoragePool, volumePath); + diskPaths.add(rbdDestVolumeFile); + } } } @@ -76,7 +78,7 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir "-m", Objects.nonNull(mountOptions) ? mountOptions : "", "-p", backupPath, "-q", command.getQuiesce() != null && command.getQuiesce() ? "true" : "false", - "-d", (Objects.nonNull(diskPaths) && !diskPaths.isEmpty()) ? String.join(",", diskPaths) : "" + "-d", diskPaths.isEmpty() ? "" : String.join(",", diskPaths) }); Pair result = Script.executePipedCommands(commands, libvirtComputingResource.getCmdsTimeout());