From c0cdfa41da668507b8b4437cf171beea5b68e896 Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Wed, 22 Apr 2026 20:52:05 +0000 Subject: [PATCH] kvm: implement copyPhysicalDisk on MultipathNVMeOFAdapterBase The NVMe-oF KVM adapter refused every template copy request from the adaptive storage orchestrator with UnsupportedOperationException, which made it impossible to use an NVMe-TCP pool as primary storage for a VM root disk: every deploy that landed a root volume on the pool failed as soon as CloudStack tried to lay down the template. Implement it the same way FiberChannel (SCSI) does: the storage provider creates and connects a raw namespace ahead of time, then the adapter resolves the host-side /dev/disk/by-id/nvme-eui. path via the existing getPhysicalDisk plumbing (which will nvme ns-rescan and wait for the symlink if the kernel has not yet picked it up) and qemu-img converts the source image into the raw block device. User-space encrypted source or destination volumes are rejected: the FlashArray already encrypts at rest and layering qemu-img LUKS on top of a hostgroup-scoped namespace shared between hosts is not a sensible layering. Source encryption would also break on migration because the passphrase does not travel. With this change a CloudStack KVM VM can have its ROOT volume on an NVMe-TCP pool (tested end-to-end on 4.23-SNAPSHOT against Purity 6.7.7: template copy, first boot, live migrate with data disk, VM snapshot with quiesce, and revert all work). Signed-off-by: Eugenio Grosso --- .../storage/MultipathNVMeOFAdapterBase.java | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathNVMeOFAdapterBase.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathNVMeOFAdapterBase.java index 7ad0560fd56..59f1abd3bec 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathNVMeOFAdapterBase.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathNVMeOFAdapterBase.java @@ -25,6 +25,9 @@ import java.util.concurrent.TimeUnit; 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.libvirt.LibvirtException; import com.cloud.storage.Storage; import com.cloud.utils.exception.CloudRuntimeException; @@ -280,12 +283,61 @@ public abstract class MultipathNVMeOFAdapterBase implements StorageAdaptor { @Override public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout) { - throw new UnsupportedOperationException("Unimplemented method 'copyPhysicalDisk'"); + return copyPhysicalDisk(disk, name, destPool, timeout, null, null, null); } + /** + * Copy a template or source disk into a pre-provisioned NVMe namespace on + * this pool, so it can be consumed by a VM as a root or data volume. + * + * The destination namespace is expected to have already been created on + * the storage provider and connected to this host's hostgroup (that is + * the storage orchestrator's responsibility, not the KVM adapter's). All + * this method does is resolve the destination device path via + * {@link #getPhysicalDisk} - which will nvme ns-rescan and wait for the + * by-id/nvme-eui.<NGUID> symlink to show up if the kernel has not + * picked it up yet - and {@code qemu-img convert} the source image into + * the raw block device. + * + * User-space encryption passphrases are not supported: the provider + * already encrypts at rest and qemu-img LUKS on top of a shared + * hostgroup-scoped namespace is not a sensible layering. + */ @Override - public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout, byte[] srcPassphrase, byte[] destPassphrase, Storage.ProvisioningType provisioningType) { - throw new UnsupportedOperationException("Unimplemented method 'copyPhysicalDisk'"); + public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout, + byte[] srcPassphrase, byte[] destPassphrase, Storage.ProvisioningType provisioningType) { + if (disk == null || StringUtils.isEmpty(name) || destPool == null) { + throw new CloudRuntimeException("Unable to copy disk to NVMe-oF pool: source disk, destination volume name or destination pool not specified"); + } + if (srcPassphrase != null || destPassphrase != null) { + throw new CloudRuntimeException("NVMe-oF adapter does not support user-space encrypted source or destination volumes"); + } + + KVMPhysicalDisk destDisk = destPool.getPhysicalDisk(name); + if (destDisk == null || StringUtils.isEmpty(destDisk.getPath())) { + throw new CloudRuntimeException("Unable to resolve NVMe namespace for destination volume [" + name + "] on pool [" + destPool.getUuid() + "]"); + } + + destDisk.setFormat(QemuImg.PhysicalDiskFormat.RAW); + destDisk.setVirtualSize(disk.getVirtualSize()); + destDisk.setSize(disk.getSize()); + + LOGGER.info(String.format("Copying source disk [path=%s, format=%s, virtualSize=%d] to NVMe-oF namespace [path=%s] on pool [%s]", + disk.getPath(), disk.getFormat(), disk.getVirtualSize(), destDisk.getPath(), destPool.getUuid())); + + QemuImgFile srcFile = new QemuImgFile(disk.getPath(), disk.getFormat()); + QemuImgFile destFile = new QemuImgFile(destDisk.getPath(), destDisk.getFormat()); + + try { + QemuImg qemu = new QemuImg(timeout); + qemu.convert(srcFile, destFile, true); + } catch (QemuImgException | LibvirtException e) { + throw new CloudRuntimeException("Failed to copy source disk [" + disk.getPath() + "] to NVMe-oF namespace [" + + destDisk.getPath() + "] on pool [" + destPool.getUuid() + "]: " + e.getMessage(), e); + } + + LOGGER.info("Successfully copied source disk to NVMe-oF namespace [" + destDisk.getPath() + "] on pool [" + destPool.getUuid() + "]"); + return destDisk; } @Override