This is an automated email from the ASF dual-hosted git repository.
dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new 685ee9e78f6 StorPool: support for direct download (#9833)
685ee9e78f6 is described below
commit 685ee9e78f63a41ec27fe04f75fc26639debe2a5
Author: slavkap <[email protected]>
AuthorDate: Sat Jun 14 12:19:37 2025 +0300
StorPool: support for direct download (#9833)
---
.../kvm/storage/StorPoolStorageAdaptor.java | 222 +++++++++++++++++++--
1 file changed, 203 insertions(+), 19 deletions(-)
diff --git
a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java
b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java
index d68e9cbd110..f6cc3c1e216 100644
---
a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java
+++
b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java
@@ -17,6 +17,29 @@
package com.cloud.hypervisor.kvm.storage;
+import com.cloud.agent.api.to.DiskTO;
+import com.cloud.storage.Storage;
+import com.cloud.storage.Storage.ImageFormat;
+import com.cloud.storage.Storage.ProvisioningType;
+import com.cloud.storage.Storage.StoragePoolType;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.utils.script.OutputInterpreter;
+import com.cloud.utils.script.Script;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import org.apache.cloudstack.storage.datastore.util.StorPoolUtil;
+import org.apache.cloudstack.utils.qemu.QemuImg;
+import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat;
+import org.apache.cloudstack.utils.qemu.QemuImgException;
+import org.apache.cloudstack.utils.qemu.QemuImgFile;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.libvirt.LibvirtException;
+
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
@@ -26,19 +49,7 @@ import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-
-import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.LogManager;
-
-import com.cloud.agent.api.to.DiskTO;
-import com.cloud.storage.Storage;
-import com.cloud.storage.Storage.ImageFormat;
-import com.cloud.storage.Storage.ProvisioningType;
-import com.cloud.storage.Storage.StoragePoolType;
-import com.cloud.utils.exception.CloudRuntimeException;
-import com.cloud.utils.script.OutputInterpreter;
-import com.cloud.utils.script.Script;
+import java.util.UUID;
public class StorPoolStorageAdaptor implements StorageAdaptor {
public static void SP_LOG(String fmt, Object... args) {
@@ -149,6 +160,10 @@ public class StorPoolStorageAdaptor implements
StorageAdaptor {
}
public static boolean attachOrDetachVolume(String command, String type,
String volumeUuid) {
+ if (volumeUuid == null) {
+ LOGGER.debug("Could not attach volume. The volume ID is null");
+ return false;
+ }
final String name = getVolumeNameFromPath(volumeUuid, true);
if (name == null) {
return false;
@@ -345,11 +360,85 @@ public class StorPoolStorageAdaptor implements
StorageAdaptor {
throw new UnsupportedOperationException("A folder cannot be created in
this configuration.");
}
- public KVMPhysicalDisk createTemplateFromDirectDownloadFile(String
templateFilePath, String destTemplatePath,
- KVMStoragePool destPool, ImageFormat format, int timeout) {
+ @Override
+ public KVMPhysicalDisk createDiskFromTemplateBacking(KVMPhysicalDisk
template, String name,
+ PhysicalDiskFormat
format, long size, KVMStoragePool destPool, int timeout, byte[] passphrase) {
return null;
}
+ @Override
+ public KVMPhysicalDisk createTemplateFromDirectDownloadFile(String
templateFilePath, String destTemplatePath,
+ KVMStoragePool
destPool, ImageFormat format, int timeout) {
+ if (StringUtils.isEmpty(templateFilePath) || destPool == null) {
+ throw new CloudRuntimeException(
+ "Unable to create template from direct download template
file due to insufficient data");
+ }
+
+ File sourceFile = new File(templateFilePath);
+ if (!sourceFile.exists()) {
+ throw new CloudRuntimeException(
+ "Direct download template file " + templateFilePath + "
does not exist on this host");
+ }
+
+ if (!StoragePoolType.StorPool.equals(destPool.getType())) {
+ throw new CloudRuntimeException("Unsupported storage pool type: "
+ destPool.getType().toString());
+ }
+
+ if (!Storage.ImageFormat.QCOW2.equals(format)) {
+ throw new CloudRuntimeException("Unsupported template format: " +
format.toString());
+ }
+
+ String srcTemplateFilePath = templateFilePath;
+ KVMPhysicalDisk destDisk = null;
+ QemuImgFile srcFile = null;
+ QemuImgFile destFile = null;
+ String templateName = UUID.randomUUID().toString();
+ String volume = null;
+ try {
+
+ srcTemplateFilePath = extractTemplate(templateFilePath,
sourceFile, srcTemplateFilePath, templateName);
+
+ QemuImg.PhysicalDiskFormat srcFileFormat =
QemuImg.PhysicalDiskFormat.QCOW2;
+
+ srcFile = new QemuImgFile(srcTemplateFilePath, srcFileFormat);
+
+ String spTemplate = destPool.getUuid().split(";")[0];
+
+ QemuImg qemu = new QemuImg(timeout);
+ OutputInterpreter.AllLinesParser parser =
createStorPoolVolume(destPool, srcFile, qemu, spTemplate);
+
+ String response = parser.getLines();
+
+ LOGGER.debug(response);
+ volume = StorPoolUtil.devPath(getNameFromResponse(response, false,
false));
+ attachOrDetachVolume("attach", "volume", volume);
+ destDisk = destPool.getPhysicalDisk(volume);
+ if (destDisk == null) {
+ throw new CloudRuntimeException(
+ "Failed to find the disk: " + volume + " of the
storage pool: " + destPool.getUuid());
+ }
+
+ destFile = new QemuImgFile(destDisk.getPath(),
QemuImg.PhysicalDiskFormat.RAW);
+
+ qemu.convert(srcFile, destFile);
+ parser =
volumeSnapshot(StorPoolStorageAdaptor.getVolumeNameFromPath(volume, true),
spTemplate);
+ response = parser.getLines();
+ LOGGER.debug(response);
+ String newPath =
StorPoolUtil.devPath(getNameFromResponse(response, false, true));
+ destDisk = destPool.getPhysicalDisk(newPath);
+ } catch (QemuImgException | LibvirtException e) {
+ destDisk = null;
+ } finally {
+ if (volume != null) {
+ attachOrDetachVolume("detach", "volume", volume);
+
volumeDelete(StorPoolStorageAdaptor.getVolumeNameFromPath(volume, true));
+ }
+ Script.runSimpleBashScript("rm -f " + srcTemplateFilePath);
+ }
+
+ return destDisk;
+ }
+
@Override
public boolean createFolder(String uuid, String path, String localPath) {
return false;
@@ -367,9 +456,104 @@ public class StorPoolStorageAdaptor implements
StorageAdaptor {
return null;
}
- @Override
- public KVMPhysicalDisk createDiskFromTemplateBacking(KVMPhysicalDisk
template, String name,
- PhysicalDiskFormat format, long size, KVMStoragePool destPool, int
timeout, byte[] passphrase) {
- return null;
+ private OutputInterpreter.AllLinesParser
createStorPoolVolume(KVMStoragePool destPool, QemuImgFile srcFile,
+ QemuImg
qemu, String templateUuid) throws QemuImgException, LibvirtException {
+ Map<String, String> info = qemu.info(srcFile);
+ Map<String, Object> reqParams = new HashMap<>();
+ reqParams.put("template", templateUuid);
+ reqParams.put("size", info.get("virtual_size"));
+ Map<String, String> tags = new HashMap<>();
+ tags.put("cs", "template");
+ reqParams.put("tags", tags);
+ Gson gson = new Gson();
+ String js = gson.toJson(reqParams);
+
+ Script sc = createStorPoolRequest(js, "VolumeCreate", null,true);
+ OutputInterpreter.AllLinesParser parser = new
OutputInterpreter.AllLinesParser();
+
+ String res = sc.execute(parser);
+ if (res != null) {
+ throw new CloudRuntimeException("Could not create volume due to: "
+ res);
+ }
+ return parser;
+ }
+
+ private OutputInterpreter.AllLinesParser volumeSnapshot(String
volumeName, String templateUuid) {
+ Map<String, String> reqParams = new HashMap<>();
+ reqParams.put("template", templateUuid);
+ Gson gson = new Gson();
+ String js = gson.toJson(reqParams);
+
+ Script sc = createStorPoolRequest(js, "VolumeSnapshot",
volumeName,true);
+ OutputInterpreter.AllLinesParser parser = new
OutputInterpreter.AllLinesParser();
+
+ String res = sc.execute(parser);
+ if (res != null) {
+ throw new CloudRuntimeException("Could not snapshot volume due to:
" + res);
+ }
+ return parser;
+ }
+
+ private OutputInterpreter.AllLinesParser volumeDelete(String volumeName) {
+ Script sc = createStorPoolRequest(null, "VolumeDelete", volumeName,
false);
+ OutputInterpreter.AllLinesParser parser = new
OutputInterpreter.AllLinesParser();
+
+ String res = sc.execute(parser);
+ if (res != null) {
+ throw new CloudRuntimeException("Could not delete volume due to: "
+ res);
+ }
+ return parser;
+ }
+ @NotNull
+ private static Script createStorPoolRequest(String js, String apiCall,
String param, boolean jsonRequired) {
+ Script sc = new Script("storpool_req", 0, LOGGER);
+ sc.add("-P");
+ sc.add("-M");
+ if (jsonRequired) {
+ sc.add("--json");
+ sc.add(js);
+ }
+ sc.add(apiCall);
+ if (param != null) {
+ sc.add(param);
+ }
+ return sc;
+ }
+
+ private String extractTemplate(String templateFilePath, File sourceFile,
String srcTemplateFilePath,
+ String templateName) {
+ if (isTemplateExtractable(templateFilePath)) {
+ srcTemplateFilePath = sourceFile.getParent() + "/" + templateName;
+ String extractCommand =
getExtractCommandForDownloadedFile(templateFilePath, srcTemplateFilePath);
+ Script.runSimpleBashScript(extractCommand);
+ Script.runSimpleBashScript("rm -f " + templateFilePath);
+ }
+ return srcTemplateFilePath;
+ }
+
+ private boolean isTemplateExtractable(String templatePath) {
+ String type = Script.runSimpleBashScript("file " + templatePath + " |
awk -F' ' '{print $2}'");
+ return type.equalsIgnoreCase("bzip2") || type.equalsIgnoreCase("gzip")
|| type.equalsIgnoreCase("zip");
+ }
+
+ private String getExtractCommandForDownloadedFile(String
downloadedTemplateFile, String templateFile) {
+ if (downloadedTemplateFile.endsWith(".zip")) {
+ return "unzip -p " + downloadedTemplateFile + " | cat > " +
templateFile;
+ } else if (downloadedTemplateFile.endsWith(".bz2")) {
+ return "bunzip2 -c " + downloadedTemplateFile + " > " +
templateFile;
+ } else if (downloadedTemplateFile.endsWith(".gz")) {
+ return "gunzip -c " + downloadedTemplateFile + " > " +
templateFile;
+ } else {
+ throw new CloudRuntimeException("Unable to extract template " +
downloadedTemplateFile);
+ }
+ }
+
+ private String getNameFromResponse(String resp, boolean tildeNeeded,
boolean isSnapshot) {
+ JsonParser jsonParser = new JsonParser();
+ JsonObject respObj = (JsonObject) jsonParser.parse(resp);
+ JsonPrimitive data = isSnapshot ?
respObj.getAsJsonPrimitive("snapshotGlobalId") :
respObj.getAsJsonPrimitive("globalId");
+ String name = data !=null ? data.getAsString() : null;
+ name = name != null ? name.startsWith("~") && !tildeNeeded ?
name.split("~")[1] : name : name;
+ return name;
}
}