Skip to content

Commit 8d965ab

Browse files
Eugenio Grossoclaude
andcommitted
kvm: support direct download templates on NVMe-oF pools
createTemplateFromDirectDownloadFile() threw UnsupportedOperationException, so a template registered with directdownload=true could not be deployed onto an NVMe-TCP pool: the download itself succeeded and the deployment then failed on the host. The two path arguments are different kinds of thing, and conflating them is what made this look harder than it is. templateFilePath is a plain local file produced by the direct-download helper, while destTemplatePath is a managed volume path of the form "type=NVMETCP;address=...". Only the destination may be resolved through KVMStoragePool.getPhysicalDisk(); handing the local file to that would pass a file name to parseAndValidatePath() and fail. KVMStorageProcessor has already issued connectPhysicalDisk() for the destination by the time we are called, so the namespace is present. Worth recording that the Fibre Channel implementation does conflate them. It calls destPool.getPhysicalDisk(templateFilePath), and FiberChannelAdapter's parseAndValidatePath() treats any string without a ";" as a bare WWN, so a local file path becomes "/dev/mapper/3" + that path and the lookup cannot succeed. This implementation follows ScaleIOStorageAdaptor instead, which is the working precedent for block-backed managed storage. The template is written as QCOW2 onto the raw namespace rather than as RAW. Three reasons: it matches ScaleIO; it agrees with the QCOW2 format the template is registered with, so stored content and declared format do not contradict each other; and KVMStorageProcessor runs Qcow2Inspector.validateQcow2File() on the path we return, which for a block device that exists would fail on RAW content and then try to delete the device. The disk returned to the caller is constructed rather than being the one getPhysicalDisk() handed back, because its *name* matters and not just its path. KVMStorageProcessor puts disk.getName() into the DirectDownloadAnswer, which becomes the template's install path in template_spool_ref and later its external name, and the provider interpolates that external name into array REST calls. getPhysicalDisk() names a disk AddressInfo.toString(), e.g. AddressInfo NVMETCP [address=006c..., connectionId=2172, path=/dev/disk/by-id/...] Recording that as the install path makes the first deployment from a freshly registered template fail: revokeAccess() -> detach() builds "/connections?host_names=kvm01&volume_names=cloudstack2::<external name>" and URI.create() rejects the spaces and brackets with "Illegal character in query". The surfaced error is a misleading "Failed to update state", because the resulting CloudRuntimeException is followed by applying OperationFailed to a template that has already reached Ready. Naming the returned disk with the managed volume path gives the same install path shape a volume records: type=NVMETCP; address=006c...; providerName=cloudstack2::tpl-9-0-2-210; providerID=... Also handles compressed templates via TemplateDownloaderUtil, since direct-download templates are commonly published that way, and refuses up front if the template will not fit the namespace rather than failing part way through the convert. Tested on a two-host KVM cluster against a FlashArray over NVMe-TCP: registered a QCOW2 template with directdownload=true and deployed a VM from it, on the first attempt, with a storage offering tagged for the NVMe-TCP pool: ROOT-32 Everpure X90 NVMe-TCP pool2 Ready type=NVMETCP; address=006c1b16ce1c034d24a9371c0623b125 Signed-off-by: Eugenio Grosso <eugenio.grosso@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 34edb22 commit 8d965ab

1 file changed

Lines changed: 103 additions & 7 deletions

File tree

‎plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathNVMeOFAdapterBase.java‎

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.File;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.UUID;
2324
import java.util.concurrent.ConcurrentHashMap;
2425
import java.util.concurrent.TimeUnit;
2526

@@ -33,6 +34,7 @@
3334
import com.cloud.utils.exception.CloudRuntimeException;
3435
import com.cloud.utils.script.OutputInterpreter;
3536
import com.cloud.utils.script.Script;
37+
import com.cloud.utils.storage.TemplateDownloaderUtil;
3638
import org.apache.commons.lang3.StringUtils;
3739
import org.apache.logging.log4j.LogManager;
3840
import org.apache.logging.log4j.Logger;
@@ -382,15 +384,109 @@ public KVMPhysicalDisk createDiskFromTemplateBacking(KVMPhysicalDisk template, S
382384
throw new UnsupportedOperationException("Unimplemented method 'createDiskFromTemplateBacking'");
383385
}
384386

387+
/**
388+
* Write a directly-downloaded template onto a namespace on this pool.
389+
*
390+
* The two path arguments are different kinds of thing, which is worth being explicit
391+
* about: {@code templateFilePath} is a plain local file produced by the direct-download
392+
* helper, while {@code destTemplatePath} is a managed volume path of the form
393+
* {@code type=NVMETCP;address=...}. Only the destination may be resolved through
394+
* {@link KVMStoragePool#getPhysicalDisk(String)}; passing the local file through it
395+
* would hand a file name to {@link #parseAndValidatePath(String)} and fail. The caller
396+
* has already issued connectPhysicalDisk() for the destination, so the namespace is
397+
* expected to be present.
398+
*
399+
* The template is written as QCOW2 onto the raw namespace rather than as RAW. That
400+
* matches the ScaleIO adaptor, is consistent with the QCOW2 format the template is
401+
* registered with, and is what lets the Qcow2Inspector check the caller runs on the
402+
* returned path succeed.
403+
*
404+
* Note what the caller does with the returned disk: KVMStorageProcessor puts
405+
* {@code disk.getName()} into the DirectDownloadAnswer, and that becomes the template's
406+
* install path and later its external name, which the provider interpolates into array
407+
* REST calls. {@link #getPhysicalDisk(String, KVMStoragePool)} names a disk
408+
* {@code AddressInfo.toString()}, which contains spaces and brackets and would produce
409+
* a name that cannot be placed in a URI. So the disk handed back here is named with the
410+
* managed volume path we were given, matching what a volume records.
411+
*/
385412
@Override
386413
public KVMPhysicalDisk createTemplateFromDirectDownloadFile(String templateFilePath, String destTemplatePath, KVMStoragePool destPool, Storage.ImageFormat format, int timeout) {
387-
// Not supported yet. The equivalent Fibre Channel path resolves the downloaded
388-
// file through destPool.getPhysicalDisk(), which on this adapter requires a
389-
// "type=NVMETCP;address=..." volume path and so cannot describe a plain local
390-
// file. Supporting direct-download templates needs a separate way to present
391-
// the local file as the copy source.
392-
throw new UnsupportedOperationException(
393-
"Direct download templates are not supported on NVMe-oF pools; register the template via secondary storage instead");
414+
if (StringUtils.isAnyEmpty(templateFilePath, destTemplatePath) || destPool == null) {
415+
throw new CloudRuntimeException("Unable to create a template from a direct download file on an NVMe-oF pool: "
416+
+ "template file path, destination template path or destination pool not specified");
417+
}
418+
419+
if (!Storage.ImageFormat.QCOW2.equals(format) && !Storage.ImageFormat.RAW.equals(format)) {
420+
throw new CloudRuntimeException("Unsupported direct download template format for NVMe-oF pools: " + format
421+
+ "; expected " + Storage.ImageFormat.QCOW2 + " or " + Storage.ImageFormat.RAW);
422+
}
423+
424+
File sourceFile = new File(templateFilePath);
425+
if (!sourceFile.exists()) {
426+
throw new CloudRuntimeException("Direct download template file " + templateFilePath + " does not exist on this host");
427+
}
428+
429+
LOGGER.debug("Creating a template on NVMe-oF pool [{}] from direct download file [{}] into [{}], format [{}]",
430+
destPool.getUuid(), templateFilePath, destTemplatePath, format);
431+
432+
String srcTemplateFilePath = templateFilePath;
433+
KVMPhysicalDisk destDisk;
434+
try {
435+
destDisk = destPool.getPhysicalDisk(destTemplatePath);
436+
if (destDisk == null || StringUtils.isEmpty(destDisk.getPath())) {
437+
throw new CloudRuntimeException("Unable to resolve the NVMe namespace for destination template path ["
438+
+ destTemplatePath + "] on pool [" + destPool.getUuid() + "]");
439+
}
440+
441+
// Direct-download templates are commonly published compressed.
442+
if (TemplateDownloaderUtil.isTemplateExtractable(templateFilePath)) {
443+
srcTemplateFilePath = sourceFile.getParent() + "/" + UUID.randomUUID().toString();
444+
LOGGER.debug("Extracting downloaded template [{}] to [{}]", templateFilePath, srcTemplateFilePath);
445+
Script.runSimpleBashScript(TemplateDownloaderUtil.getExtractCommandForDownloadedFile(templateFilePath, srcTemplateFilePath));
446+
Script.runSimpleBashScript("rm -f " + templateFilePath);
447+
}
448+
449+
QemuImg.PhysicalDiskFormat srcFormat = Storage.ImageFormat.RAW.equals(format)
450+
? QemuImg.PhysicalDiskFormat.RAW : QemuImg.PhysicalDiskFormat.QCOW2;
451+
452+
QemuImg qemu = new QemuImg(timeout);
453+
QemuImgFile srcFile = new QemuImgFile(srcTemplateFilePath, srcFormat);
454+
// Populates the virtual size, and fails early if the file is unreadable or is
455+
// not in the format the template claims to be.
456+
qemu.info(srcFile);
457+
458+
long namespaceSize = getPhysicalDiskSize(destDisk.getPath());
459+
if (namespaceSize > 0 && srcFile.getSize() > namespaceSize) {
460+
throw new CloudRuntimeException("Direct download template needs " + srcFile.getSize()
461+
+ " bytes but the NVMe namespace at " + destDisk.getPath() + " is only " + namespaceSize + " bytes");
462+
}
463+
464+
QemuImgFile destFile = new QemuImgFile(destDisk.getPath(), QemuImg.PhysicalDiskFormat.QCOW2);
465+
destFile.setSize(srcFile.getSize());
466+
467+
LOGGER.debug("Converting [{}] onto NVMe namespace [{}]", srcFile.getFileName(), destDisk.getPath());
468+
qemu.create(destFile);
469+
qemu.convert(srcFile, destFile);
470+
471+
KVMPhysicalDisk template = new KVMPhysicalDisk(destDisk.getPath(), destTemplatePath, destPool);
472+
template.setFormat(QemuImg.PhysicalDiskFormat.QCOW2);
473+
template.setVirtualSize(srcFile.getSize());
474+
template.setSize(srcFile.getSize());
475+
destDisk = template;
476+
LOGGER.info("Wrote direct download template onto NVMe namespace [{}] on pool [{}]",
477+
destDisk.getPath(), destPool.getUuid());
478+
} catch (QemuImgException | LibvirtException e) {
479+
throw new CloudRuntimeException("Failed to write the direct download template [" + templateFilePath
480+
+ "] onto the NVMe namespace for [" + destTemplatePath + "] on pool [" + destPool.getUuid()
481+
+ "]: " + e.getMessage(), e);
482+
} finally {
483+
// Only remove what we extracted; the original download belongs to the caller.
484+
if (!srcTemplateFilePath.equals(templateFilePath)) {
485+
Script.runSimpleBashScript("rm -f " + srcTemplateFilePath);
486+
}
487+
}
488+
489+
return destDisk;
394490
}
395491

396492
@Override

0 commit comments

Comments
 (0)