From bc2c92ce178a8c1afe02610b7b68d71c3eb54e47 Mon Sep 17 00:00:00 2001 From: Rene Peinthor Date: Fri, 3 Oct 2025 13:51:42 +0200 Subject: [PATCH] linstor: use sparse/discard qemu-img convert on thin devices This reduces revert time and also makes the devices not thick allocated anymore. --- plugins/storage/volume/linstor/CHANGELOG.md | 6 +++ ...torRevertBackupSnapshotCommandWrapper.java | 21 ++++++++-- .../kvm/storage/LinstorStorageAdaptor.java | 38 +------------------ .../storage/datastore/util/LinstorUtil.java | 34 +++++++++++++++++ 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md index c0991a9aa2b8..7da3516955df 100644 --- a/plugins/storage/volume/linstor/CHANGELOG.md +++ b/plugins/storage/volume/linstor/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Linstor CloudStack plugin will be documented in this file The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2025-10-03] + +### Changed + +- Revert qcow2 snapshot now use sparse/discard options to convert on thin devices. + ## [2025-08-05] ### Fixed diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java index 511b5a40ca83..98b8bf0bb780 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java @@ -26,13 +26,16 @@ import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.storage.Storage; +import com.cloud.utils.script.Script; import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.apache.log4j.Logger; +import org.joda.time.Duration; import org.libvirt.LibvirtException; @ResourceWrapper(handles = LinstorRevertBackupSnapshotCommand.class) @@ -41,12 +44,23 @@ public final class LinstorRevertBackupSnapshotCommandWrapper { private static final Logger s_logger = Logger.getLogger(LinstorRevertBackupSnapshotCommandWrapper.class); - private void convertQCow2ToRAW(final String srcPath, final String dstPath, int waitMilliSeconds) + private void convertQCow2ToRAW( + KVMStoragePool pool, final String srcPath, final String dstUuid, int waitMilliSeconds) throws LibvirtException, QemuImgException { + final String dstPath = pool.getPhysicalDisk(dstUuid).getPath(); final QemuImgFile srcQemuFile = new QemuImgFile( srcPath, QemuImg.PhysicalDiskFormat.QCOW2); - final QemuImg qemu = new QemuImg(waitMilliSeconds); + boolean zeroedDevice = LinstorUtil.resourceSupportZeroBlocks(pool, LinstorUtil.RSC_PREFIX + dstUuid); + if (zeroedDevice) + { + // blockdiscard the device to ensure the device is filled with zeroes + Script blkDiscardScript = new Script("blkdiscard", Duration.millis(waitMilliSeconds)); + blkDiscardScript.add("-f"); + blkDiscardScript.add(dstPath); + blkDiscardScript.execute(); + } + final QemuImg qemu = new QemuImg(waitMilliSeconds, zeroedDevice, true); final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.RAW); qemu.convert(srcQemuFile, dstFile); } @@ -73,8 +87,9 @@ public CopyCmdAnswer execute(LinstorRevertBackupSnapshotCommand cmd, LibvirtComp srcDataStore.getUrl() + File.separator + srcFile.getParent()); convertQCow2ToRAW( + linstorPool, secondaryPool.getLocalPath() + File.separator + srcFile.getName(), - linstorPool.getPhysicalDisk(dst.getPath()).getPath(), + dst.getPath(), cmd.getWaitInMillSeconds()); final VolumeObjectTO dstVolume = new VolumeObjectTO(); diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java index 4210008f1c0a..c269878c8080 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java @@ -30,7 +30,6 @@ import com.cloud.storage.Storage; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; - import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; @@ -56,7 +55,6 @@ import com.linbit.linstor.api.model.ResourceMakeAvailable; import com.linbit.linstor.api.model.ResourceWithVolumes; import com.linbit.linstor.api.model.StoragePool; -import com.linbit.linstor.api.model.Volume; import com.linbit.linstor.api.model.VolumeDefinition; import java.io.File; @@ -570,40 +568,6 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMSt return copyPhysicalDisk(disk, name, destPool, timeout, null, null, null); } - /** - * Checks if all diskful resource are on a zeroed block device. - * @param destPool Linstor pool to use - * @param resName Linstor resource name - * @return true if all resources are on a provider with zeroed blocks. - */ - private boolean resourceSupportZeroBlocks(KVMStoragePool destPool, String resName) { - final DevelopersApi api = getLinstorAPI(destPool); - - try { - List resWithVols = api.viewResources( - Collections.emptyList(), - Collections.singletonList(resName), - Collections.emptyList(), - Collections.emptyList(), - null, - null); - - if (resWithVols != null) { - return resWithVols.stream() - .allMatch(res -> { - Volume vol0 = res.getVolumes().get(0); - return vol0 != null && (vol0.getProviderKind() == ProviderKind.LVM_THIN || - vol0.getProviderKind() == ProviderKind.ZFS || - vol0.getProviderKind() == ProviderKind.ZFS_THIN || - vol0.getProviderKind() == ProviderKind.DISKLESS); - } ); - } - } catch (ApiException apiExc) { - s_logger.error(apiExc.getMessage()); - } - return false; - } - /** * Checks if the given disk is the SystemVM template, by checking its properties file in the same directory. * The initial systemvm template resource isn't created on the management server, but @@ -674,7 +638,7 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMSt destFile.setFormat(dstDisk.getFormat()); destFile.setSize(disk.getVirtualSize()); - boolean zeroedDevice = resourceSupportZeroBlocks(destPools, getLinstorRscName(name)); + boolean zeroedDevice = LinstorUtil.resourceSupportZeroBlocks(destPools, getLinstorRscName(name)); try { final QemuImg qemu = new QemuImg(timeout, zeroedDevice, true); qemu.convert(srcFile, destFile); diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java index 60d065900065..9a6151efafc7 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java @@ -42,6 +42,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.log4j.Logger; @@ -430,4 +431,37 @@ public static ResourceDefinition findResourceDefinition(DevelopersApi api, Strin public static boolean isRscDiskless(ResourceWithVolumes rsc) { return rsc.getFlags() != null && rsc.getFlags().contains(ApiConsts.FLAG_DISKLESS); } + + /** + * Checks if all diskful resource are on a zeroed block device. + * @param pool Linstor pool to use + * @param resName Linstor resource name + * @return true if all resources are on a provider with zeroed blocks. + */ + public static boolean resourceSupportZeroBlocks(KVMStoragePool pool, String resName) { + final DevelopersApi api = getLinstorAPI(pool.getSourceHost()); + try { + List resWithVols = api.viewResources( + Collections.emptyList(), + Collections.singletonList(resName), + Collections.emptyList(), + Collections.emptyList(), + null, + null); + + if (resWithVols != null) { + return resWithVols.stream() + .allMatch(res -> { + Volume vol0 = res.getVolumes().get(0); + return vol0 != null && (vol0.getProviderKind() == ProviderKind.LVM_THIN || + vol0.getProviderKind() == ProviderKind.ZFS || + vol0.getProviderKind() == ProviderKind.ZFS_THIN || + vol0.getProviderKind() == ProviderKind.DISKLESS); + } ); + } + } catch (ApiException apiExc) { + s_logger.error(apiExc.getMessage()); + } + return false; + } }