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;
+ }
}