linstor: use sparse/discard qemu-img convert on thin devices (#11787)

diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md
index c0991a9..7da3516 100644
--- a/plugins/storage/volume/linstor/CHANGELOG.md
+++ b/plugins/storage/volume/linstor/CHANGELOG.md
@@ -5,6 +5,12 @@
 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 511b5a4..98b8bf0 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 @@
 {
     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 @@
                 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 4210008..c269878 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;
@@ -571,40 +569,6 @@
     }
 
     /**
-     * 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<ResourceWithVolumes> 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
      * we now need to know if the systemvm template is used, while copying.
@@ -674,7 +638,7 @@
         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 60d0659..9a6151e 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 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<ResourceWithVolumes> 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;
+    }
 }