From 2c0473d997ee841c7e51f2373582b5ac7aff4ea0 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 19 Sep 2025 10:28:58 +0530 Subject: [PATCH 1/3] 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 | 141 +++++++++++++++--- .../apache/cloudstack/utils/qemu/QemuImg.java | 7 + 5 files changed, 179 insertions(+), 37 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 c4b92fc9e05c..7b4412f1b6cd 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 f447fbe3d008..80d69aa8c61d 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; @@ -73,6 +75,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 e5f98ad291be..a9fab3052798 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; @@ -97,6 +101,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private PrimaryDataStoreDao primaryDataStoreDao; + @Inject + DataStoreManager dataStoreMgr; + @Inject private AgentManager agentManager; @@ -179,7 +186,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); } @@ -279,7 +287,9 @@ private boolean 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()); @@ -294,32 +304,42 @@ private boolean restoreVMBackup(VirtualMachine vm, Backup backup) { return answer.getResult(); } - 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); @@ -335,10 +355,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()); @@ -347,7 +371,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 0e5091ebcf49..ace2341c181f 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(); + int timeout = command.getWait(); + KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); String newVolumeId = null; try { 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, backupRepoType, backupRepoAddress, volumePath, diskType, restoreVolumeUuid, - new Pair<>(vmName, command.getVmState()), mountOptions); + restoreVolume(storagePoolMgr, backupPath, backupRepoType, backupRepoAddress, volumePool, volumePath, diskType, restoreVolumeUuid, + new Pair<>(vmName, command.getVmState()), mountOptions, timeout); } else if (Boolean.TRUE.equals(vmExists)) { - restoreVolumesOfExistingVM(restoreVolumePaths, backedVolumeUUIDs, backupPath, backupRepoType, backupRepoAddress, mountOptions); + restoreVolumesOfExistingVM(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backedVolumeUUIDs, backupPath, backupRepoType, backupRepoAddress, mountOptions, timeout); } else { - restoreVolumesOfDestroyedVMs(restoreVolumePaths, vmName, backupPath, backupRepoType, backupRepoAddress, mountOptions); + restoreVolumesOfDestroyedVMs(storagePoolMgr, restoreVolumePools, restoreVolumePaths, vmName, backupPath, backupRepoType, backupRepoAddress, mountOptions, timeout); } } catch (CloudRuntimeException e) { String errorMessage = "Failed to restore backup for VM: " + vmName + "."; @@ -87,17 +103,18 @@ public Answer execute(RestoreBackupCommand command, LibvirtComputingResource ser return new BackupAnswer(command, true, newVolumeId); } - private void restoreVolumesOfExistingVM(List restoreVolumePaths, List backedVolumesUUIDs, String backupPath, - String backupRepoType, String backupRepoAddress, String mountOptions) { + private void restoreVolumesOfExistingVM(KVMStoragePoolManager storagePoolMgr, List restoreVolumePools, List restoreVolumePaths, List backedVolumesUUIDs, String backupPath, + String backupRepoType, String backupRepoAddress, String mountOptions, int timeout) { String diskType = "root"; String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions); 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"; - 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())); } } @@ -107,16 +124,17 @@ private void restoreVolumesOfExistingVM(List restoreVolumePaths, List volumePaths, String vmName, String backupPath, - String backupRepoType, String backupRepoAddress, String mountOptions) { + private void restoreVolumesOfDestroyedVMs(KVMStoragePoolManager storagePoolMgr, List volumePools, List volumePaths, String vmName, String backupPath, + String backupRepoType, String backupRepoAddress, String mountOptions, int timeout) { String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions); 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"; - 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())); } } @@ -126,17 +144,17 @@ private void restoreVolumesOfDestroyedVMs(List volumePaths, String vmNam } } - private void restoreVolume(String backupPath, String backupRepoType, String backupRepoAddress, String volumePath, - String diskType, String volumeUUID, Pair vmNameAndState, String mountOptions) { + private void restoreVolume(KVMStoragePoolManager storagePoolMgr, String backupPath, String backupRepoType, String backupRepoAddress, PrimaryDataStoreTO volumePool, String volumePath, + String diskType, String volumeUUID, Pair vmNameAndState, String mountOptions, int timeout) { String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions); Pair bkpPathAndVolUuid; try { bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID); - 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())); } } @@ -148,7 +166,6 @@ private void restoreVolume(String backupPath, String backupRepoType, String back } } - private String mountBackupDirectory(String backupRepoAddress, String backupRepoType, String mountOptions) { String randomChars = RandomStringUtils.random(5, true, false); String mountDirectory = String.format("%s.%s",BACKUP_TEMP_FILE_PREFIX , randomChars); @@ -197,14 +214,58 @@ private Pair getBackupPath(String mountDirectory, String volumeP return new Pair<>(bkpPath, volUuid); } - 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 attachVolumeToVm(String vmName, String volumePath) { + 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(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; } @@ -214,4 +275,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 93108c355a3d6202eb83b7a71a688e6a02f57be0 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 19 Sep 2025 13:52:44 +0530 Subject: [PATCH 2/3] 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 a9fab3052798..baa4db8a587f 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; @@ -312,9 +311,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); @@ -339,7 +340,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); @@ -371,7 +371,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 ace2341c181f..3598378de500 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 bkpPathAndVolUuid; try { bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID); - if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout)) { + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout, true)) { throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); } if (VirtualMachine.State.Running.equals(vmNameAndState.second())) { @@ -215,28 +214,34 @@ private Pair getBackupPath(String mountDirectory, String volumeP } private boolean replaceVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout) { + return replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, false); + } + + private boolean replaceVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume) { 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); + return replaceRbdVolumeWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume); } - private boolean replaceRbdVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout) { + private boolean replaceRbdVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume) { 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); + if (!createTargetVolume) { + KVMPhysicalDisk rdbDisk = volumeStoragePool.getPhysicalDisk(volumePath); + logger.debug("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); } + QemuImgFile srcBackupFile = null; QemuImgFile destVolumeFile = null; - try { srcBackupFile = new QemuImgFile(backupPath, QemuImg.PhysicalDiskFormat.QCOW2); String rbdDestVolumeFile = KVMPhysicalDisk.RBDStringBuilder(volumeStoragePool, volumePath); @@ -261,7 +266,6 @@ private boolean attachVolumeToVm(KVMStoragePoolManager storagePoolMgr, String vm 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)); From 8113584005b53fa120385022cd9ecc8807df8562 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Tue, 23 Sep 2025 14:37:56 +0530 Subject: [PATCH 3/3] 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 baa4db8a587f..cefedd890659 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 @@ -359,12 +359,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 3598378de500..fd1490132a08 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 @@ -233,11 +233,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;