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.<NGUID> 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 <eugenio.grosso@gmail.com>
This commit is contained in:
Eugenio Grosso 2026-04-22 20:52:05 +00:00
parent ff03d9f4f3
commit c0cdfa41da
1 changed files with 55 additions and 3 deletions

View File

@ -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.&lt;NGUID&gt; 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