diff --git a/PendingReleaseNotes b/PendingReleaseNotes index 9670b6e7c13a..0fe17afa85f4 100644 --- a/PendingReleaseNotes +++ b/PendingReleaseNotes @@ -39,3 +39,11 @@ example.ver.1 > example.ver.2: which can now be attached to Instances. This is to prevent the Secondary Storage to grow to enormous sizes as Linux Distributions keep growing in size while a stripped down Linux should fit on a 2.88MB floppy. + +4.23.0.0: + * VMware-to-KVM import using VDDK can import powered-off VMware VMs directly + into Ceph RBD primary storage when forceconverttopool is enabled and the + selected KVM conversion host supports qemu RBD access and in-place virt-v2v + finalization. Hosts without in-place finalization support can still use the + staged VDDK flow, converting to temporary qcow2 storage before copying the + finalized disks into RBD. diff --git a/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java b/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java index 7daeb9649177..81fc7f5ad7fe 100644 --- a/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java @@ -38,6 +38,7 @@ public class RemoteInstanceTO implements Serializable { private String datacenterName; private String clusterName; private String hostName; + private String vmwareMoref; public RemoteInstanceTO() { } @@ -100,4 +101,12 @@ public String getClusterName() { public String getHostName() { return hostName; } + + public String getVmwareMoref() { + return vmwareMoref; + } + + public void setVmwareMoref(String vmwareMoref) { + this.vmwareMoref = vmwareMoref; + } } diff --git a/api/src/main/java/com/cloud/agent/api/to/VmwareVddkSourceDiskTO.java b/api/src/main/java/com/cloud/agent/api/to/VmwareVddkSourceDiskTO.java new file mode 100644 index 000000000000..b980c611abc9 --- /dev/null +++ b/api/src/main/java/com/cloud/agent/api/to/VmwareVddkSourceDiskTO.java @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.agent.api.to; + +import java.io.Serializable; + +public class VmwareVddkSourceDiskTO implements Serializable { + + private String diskId; + private String sourceDiskPath; + private long capacityBytes; + private Integer position; + + public VmwareVddkSourceDiskTO() { + } + + public VmwareVddkSourceDiskTO(String diskId, String sourceDiskPath, long capacityBytes, Integer position) { + this.diskId = diskId; + this.sourceDiskPath = sourceDiskPath; + this.capacityBytes = capacityBytes; + this.position = position; + } + + public String getDiskId() { + return diskId; + } + + public String getSourceDiskPath() { + return sourceDiskPath; + } + + public long getCapacityBytes() { + return capacityBytes; + } + + public Integer getPosition() { + return position; + } +} diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index b52348201516..1e66c90f2d41 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -62,6 +62,10 @@ public static String[] toStrings(Host.Type... types) { String HOST_VDDK_VERSION = "host.vddk.version"; String HOST_OVFTOOL_VERSION = "host.ovftool.version"; String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; + String HOST_VDDK_RBD_DIRECT_IMPORT_SUPPORT = "host.vddk.rbd.direct.import.support"; + String HOST_VIRTV2V_INPLACE_SUPPORT = "host.virt.v2v.inplace.support"; + String HOST_QEMU_IMG_RBD_SUPPORT = "host.qemu.img.rbd.support"; + String HOST_RBD_QEMU_COPY_SUPPORT = "host.rbd.qemu.copy.support"; String HOST_SSH_PORT = "host.ssh.port"; int DEFAULT_SSH_PORT = 22; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java index db7dcc3fb44f..02a2a6cc3ef2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java @@ -153,7 +153,8 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { private Long importInstanceHostId; @Parameter(name = ApiConstants.CONVERT_INSTANCE_STORAGE_POOL_ID, type = CommandType.UUID, entityType = StoragePoolResponse.class, - description = "(only for importing VMs from VMware to KVM) optional - the temporary storage pool to perform the virt-v2v migration from VMware to KVM.") + description = "(only for importing VMs from VMware to KVM) optional - the temporary storage pool to perform the virt-v2v migration from VMware to KVM. " + + "When " + ApiConstants.USE_VDDK + " and " + ApiConstants.FORCE_CONVERT_TO_POOL + " are true, this can be an RBD primary storage pool for direct RBD import.") private Long convertStoragePoolId; @Parameter(name = ApiConstants.FORCE_MS_TO_IMPORT_VM_FILES, type = CommandType.BOOLEAN, @@ -183,7 +184,9 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { type = CommandType.BOOLEAN, since = "4.22.1", description = "(only for importing VMs from VMware to KVM) optional - if true, uses VDDK on the KVM conversion host for converting the VM. " + - "This parameter is mutually exclusive with " + ApiConstants.FORCE_MS_TO_IMPORT_VM_FILES + ".") + "This parameter is mutually exclusive with " + ApiConstants.FORCE_MS_TO_IMPORT_VM_FILES + ". " + + "With " + ApiConstants.FORCE_CONVERT_TO_POOL + "=true and an RBD conversion pool, the source VMware VM must be powered off and " + + "the conversion host must support qemu RBD access and in-place virt-v2v finalization.") private Boolean useVddk; diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java index cbb7c4de698a..60f4d959ebe4 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java +++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java @@ -66,6 +66,7 @@ public enum PowerState { private String bootType; private String bootMode; + private String vmwareMoref; public String getName() { return name; @@ -234,6 +235,14 @@ public void setBootMode(String bootMode) { this.bootMode = bootMode; } + public String getVmwareMoref() { + return vmwareMoref; + } + + public void setVmwareMoref(String vmwareMoref) { + this.vmwareMoref = vmwareMoref; + } + public static class Disk { private String diskId; diff --git a/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java index c46fb697a3c1..94318033bc1c 100644 --- a/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java +++ b/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java @@ -19,6 +19,7 @@ public class CheckConvertInstanceCommand extends Command { boolean checkWindowsGuestConversionSupport = false; boolean useVddk = false; + boolean checkVddkRbdDirectImportSupport = false; String vddkLibDir; public CheckConvertInstanceCommand() { @@ -50,6 +51,14 @@ public void setUseVddk(boolean useVddk) { this.useVddk = useVddk; } + public boolean isCheckVddkRbdDirectImportSupport() { + return checkVddkRbdDirectImportSupport; + } + + public void setCheckVddkRbdDirectImportSupport(boolean checkVddkRbdDirectImportSupport) { + this.checkVddkRbdDirectImportSupport = checkVddkRbdDirectImportSupport; + } + public String getVddkLibDir() { return vddkLibDir; } diff --git a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java index 38e0dca7736c..dd7139e8bd7d 100644 --- a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java +++ b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java @@ -18,8 +18,11 @@ import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.RemoteInstanceTO; +import com.cloud.agent.api.to.VmwareVddkSourceDiskTO; import com.cloud.hypervisor.Hypervisor; +import java.util.List; + public class ConvertInstanceCommand extends Command { private RemoteInstanceTO sourceInstance; @@ -35,6 +38,7 @@ public class ConvertInstanceCommand extends Command { private String vddkLibDir; private String vddkTransports; private String vddkThumbprint; + private List vmwareVddkSourceDisks; public ConvertInstanceCommand() { } @@ -126,6 +130,14 @@ public void setVddkThumbprint(String vddkThumbprint) { this.vddkThumbprint = vddkThumbprint; } + public List getVmwareVddkSourceDisks() { + return vmwareVddkSourceDisks; + } + + public void setVmwareVddkSourceDisks(List vmwareVddkSourceDisks) { + this.vmwareVddkSourceDisks = vmwareVddkSourceDisks; + } + @Override public boolean executeInSequence() { return false; diff --git a/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java index eadfa6556f86..e5b970814fab 100644 --- a/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java +++ b/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java @@ -18,6 +18,7 @@ import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.RemoteInstanceTO; +import com.cloud.storage.Storage; import java.util.List; @@ -25,6 +26,7 @@ public class ImportConvertedInstanceCommand extends Command { private RemoteInstanceTO sourceInstance; private List destinationStoragePools; + private List destinationStoragePoolTypes; private DataStoreTO conversionTemporaryLocation; private String temporaryConvertUuid; private boolean forceConvertToPool; @@ -34,15 +36,24 @@ public ImportConvertedInstanceCommand() { public ImportConvertedInstanceCommand(RemoteInstanceTO sourceInstance, List destinationStoragePools, + List destinationStoragePoolTypes, DataStoreTO conversionTemporaryLocation, String temporaryConvertUuid, boolean forceConvertToPool) { this.sourceInstance = sourceInstance; this.destinationStoragePools = destinationStoragePools; + this.destinationStoragePoolTypes = destinationStoragePoolTypes; this.conversionTemporaryLocation = conversionTemporaryLocation; this.temporaryConvertUuid = temporaryConvertUuid; this.forceConvertToPool = forceConvertToPool; } + public ImportConvertedInstanceCommand(RemoteInstanceTO sourceInstance, + List destinationStoragePools, + DataStoreTO conversionTemporaryLocation, String temporaryConvertUuid, + boolean forceConvertToPool) { + this(sourceInstance, destinationStoragePools, null, conversionTemporaryLocation, temporaryConvertUuid, forceConvertToPool); + } + public RemoteInstanceTO getSourceInstance() { return sourceInstance; } @@ -51,6 +62,10 @@ public List getDestinationStoragePools() { return destinationStoragePools; } + public List getDestinationStoragePoolTypes() { + return destinationStoragePoolTypes; + } + public DataStoreTO getConversionTemporaryLocation() { return conversionTemporaryLocation; } diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java index 1215829d92f8..2c0fab1ac6e2 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java @@ -808,8 +808,13 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi String vddkSupport = detailsMap.get(Host.HOST_VDDK_SUPPORT); String vddkLibDir = detailsMap.get(Host.HOST_VDDK_LIB_DIR); String vddkVersion = detailsMap.get(Host.HOST_VDDK_VERSION); + String virtv2vInPlaceSupport = detailsMap.get(Host.HOST_VIRTV2V_INPLACE_SUPPORT); + String qemuImgRbdSupport = detailsMap.get(Host.HOST_QEMU_IMG_RBD_SUPPORT); + String rbdQemuCopySupport = detailsMap.get(Host.HOST_RBD_QEMU_COPY_SUPPORT); + String vddkRbdDirectImportSupport = detailsMap.get(Host.HOST_VDDK_RBD_DIRECT_IMPORT_SUPPORT); logger.debug("Got HOST_UEFI_ENABLE [{}] for host [{}]:", uefiEnabled, host); - if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion)) { + if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion, + virtv2vInPlaceSupport, qemuImgRbdSupport, rbdQemuCopySupport, vddkRbdDirectImportSupport)) { _hostDao.loadDetails(host); boolean updateNeeded = false; if (StringUtils.isNotBlank(uefiEnabled) && !uefiEnabled.equals(host.getDetails().get(Host.HOST_UEFI_ENABLE))) { @@ -844,6 +849,22 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi } updateNeeded = true; } + if (StringUtils.isNotBlank(virtv2vInPlaceSupport) && !virtv2vInPlaceSupport.equals(host.getDetails().get(Host.HOST_VIRTV2V_INPLACE_SUPPORT))) { + host.getDetails().put(Host.HOST_VIRTV2V_INPLACE_SUPPORT, virtv2vInPlaceSupport); + updateNeeded = true; + } + if (StringUtils.isNotBlank(qemuImgRbdSupport) && !qemuImgRbdSupport.equals(host.getDetails().get(Host.HOST_QEMU_IMG_RBD_SUPPORT))) { + host.getDetails().put(Host.HOST_QEMU_IMG_RBD_SUPPORT, qemuImgRbdSupport); + updateNeeded = true; + } + if (StringUtils.isNotBlank(rbdQemuCopySupport) && !rbdQemuCopySupport.equals(host.getDetails().get(Host.HOST_RBD_QEMU_COPY_SUPPORT))) { + host.getDetails().put(Host.HOST_RBD_QEMU_COPY_SUPPORT, rbdQemuCopySupport); + updateNeeded = true; + } + if (StringUtils.isNotBlank(vddkRbdDirectImportSupport) && !vddkRbdDirectImportSupport.equals(host.getDetails().get(Host.HOST_VDDK_RBD_DIRECT_IMPORT_SUPPORT))) { + host.getDetails().put(Host.HOST_VDDK_RBD_DIRECT_IMPORT_SUPPORT, vddkRbdDirectImportSupport); + updateNeeded = true; + } if (updateNeeded) { _hostDao.saveDetails(host); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 06b668b801d3..a1b7e747dc6f 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -18,9 +18,13 @@ import static com.cloud.host.Host.HOST_INSTANCE_CONVERSION; import static com.cloud.host.Host.HOST_OVFTOOL_VERSION; +import static com.cloud.host.Host.HOST_QEMU_IMG_RBD_SUPPORT; +import static com.cloud.host.Host.HOST_RBD_QEMU_COPY_SUPPORT; +import static com.cloud.host.Host.HOST_VDDK_RBD_DIRECT_IMPORT_SUPPORT; import static com.cloud.host.Host.HOST_VDDK_LIB_DIR; import static com.cloud.host.Host.HOST_VDDK_SUPPORT; import static com.cloud.host.Host.HOST_VDDK_VERSION; +import static com.cloud.host.Host.HOST_VIRTV2V_INPLACE_SUPPORT; import static com.cloud.host.Host.HOST_VIRTV2V_VERSION; import static com.cloud.host.Host.HOST_VOLUME_ENCRYPTION; import static org.apache.cloudstack.utils.linux.KVMHostInfo.isHostS390x; @@ -361,6 +365,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final String TUNGSTEN_PATH = "scripts/vm/network/tungsten"; public static final String INSTANCE_CONVERSION_SUPPORTED_CHECK_CMD = "virt-v2v --version"; + public static final String INSTANCE_CONVERSION_IN_PLACE_BINARY_SUPPORTED_CHECK_CMD = "virt-v2v-in-place --version"; + public static final String INSTANCE_CONVERSION_IN_PLACE_OPTION_SUPPORTED_CHECK_CMD = "virt-v2v --help 2>&1 | grep -q -- '--in-place'"; + public static final String QEMU_IMG_RBD_SUPPORTED_CHECK_CMD = "qemu-img --help 2>&1 | grep -Eq '(^|[[:space:]])rbd([[:space:]]|$)'"; // virt-v2v --version => sample output: virt-v2v 1.42.0rhel=8,release=22.module+el8.10.0+1590+a67ab969 public static final String OVF_EXPORT_SUPPORTED_CHECK_CMD = "ovftool --version"; // ovftool --version => sample output: VMware ovftool 4.6.0 (build-21452615) @@ -4345,6 +4352,10 @@ public StartupCommand[] initialize() { boolean instanceConversionSupported = hostSupportsInstanceConversion(); cmd.getHostDetails().put(HOST_INSTANCE_CONVERSION, String.valueOf(instanceConversionSupported)); cmd.getHostDetails().put(HOST_VDDK_SUPPORT, String.valueOf(hostSupportsVddk())); + cmd.getHostDetails().put(HOST_VIRTV2V_INPLACE_SUPPORT, String.valueOf(hostSupportsVirtV2vInPlace())); + cmd.getHostDetails().put(HOST_QEMU_IMG_RBD_SUPPORT, String.valueOf(hostSupportsQemuImgRbd())); + cmd.getHostDetails().put(HOST_RBD_QEMU_COPY_SUPPORT, String.valueOf(hostSupportsRbdQemuCopy())); + cmd.getHostDetails().put(HOST_VDDK_RBD_DIRECT_IMPORT_SUPPORT, String.valueOf(hostSupportsVddkRbdDirectImport())); if (StringUtils.isNotBlank(vddkLibDir)) { cmd.getHostDetails().put(HOST_VDDK_LIB_DIR, vddkLibDir); } @@ -6087,6 +6098,34 @@ public boolean hostSupportsVddk(String overriddenVddkLibDir) { return hostSupportsInstanceConversion() && isVddkLibDirValid(effectiveVddkLibDir) && StringUtils.isNotBlank(detectVddkVersion()); } + public boolean hostSupportsVirtV2vInPlace() { + return hostSupportsVirtV2vInPlaceBinary() || hostSupportsVirtV2vInPlaceOption(); + } + + public boolean hostSupportsVirtV2vInPlaceBinary() { + return Script.runSimpleBashScriptForExitValue(INSTANCE_CONVERSION_IN_PLACE_BINARY_SUPPORTED_CHECK_CMD) == 0; + } + + public boolean hostSupportsVirtV2vInPlaceOption() { + return Script.runSimpleBashScriptForExitValue(INSTANCE_CONVERSION_IN_PLACE_OPTION_SUPPORTED_CHECK_CMD) == 0; + } + + public boolean hostSupportsQemuImgRbd() { + return Script.runSimpleBashScriptForExitValue(QEMU_IMG_RBD_SUPPORTED_CHECK_CMD) == 0; + } + + public boolean hostSupportsRbdQemuCopy() { + return hostSupportsQemuImgRbd(); + } + + public boolean hostSupportsVddkRbdDirectImport() { + return hostSupportsVddkRbdDirectImport(null); + } + + public boolean hostSupportsVddkRbdDirectImport(String overriddenVddkLibDir) { + return hostSupportsVddk(overriddenVddkLibDir) && hostSupportsQemuImgRbd() && hostSupportsVirtV2vInPlace(); + } + protected boolean isVddkLibDirValid(String path) { if (StringUtils.isBlank(path)) { return false; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapper.java index de9341715f02..e3f8dec19b16 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapper.java @@ -31,6 +31,13 @@ public class LibvirtCheckConvertInstanceCommandWrapper extends CommandWrapper sourceDisks = cmd.getVmwareVddkSourceDisks(); + if (sourceDisks == null || sourceDisks.isEmpty()) { + logger.error("({}) Direct RBD VDDK import requires VMware source disk metadata", originalVMName); + return false; + } + + probeRbdQemuAccess(targetPool, temporaryConvertUuid); + + String vcenterPassword = vmwareInstance.getVcenterPassword(); + if (StringUtils.isBlank(vcenterPassword)) { + logger.error("({}) Could not determine vCenter password for {}", originalVMName, vmwareInstance.getVcenterHost()); + return false; + } + + String vddkThumbprint = StringUtils.trimToNull(configuredVddkThumbprint); + if (StringUtils.isBlank(vddkThumbprint)) { + vddkThumbprint = getVcenterThumbprint(vmwareInstance.getVcenterHost(), timeout, originalVMName); + } + if (StringUtils.isBlank(vddkThumbprint)) { + logger.error("({}) Could not determine vCenter thumbprint for {}", originalVMName, vmwareInstance.getVcenterHost()); + return false; + } + + String passwordFilePath = String.format("/tmp/v2v.rbd.pass.cloud.%s.%s", + StringUtils.defaultIfBlank(vmwareInstance.getVcenterHost(), "unknown"), + UUID.randomUUID()); + List createdImages = new ArrayList<>(); + try { + Files.writeString(Path.of(passwordFilePath), vcenterPassword); + Files.setPosixFilePermissions(Path.of(passwordFilePath), Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + + for (int i = 0; i < sourceDisks.size(); i++) { + VmwareVddkSourceDiskTO sourceDisk = sourceDisks.get(i); + String imageName = buildRbdImageName(temporaryConvertUuid, i); + createdImages.add(imageName); + copyVddkSourceDiskToRbd(vmwareInstance, sourceDisk, targetPool, imageName, passwordFilePath, + vddkLibDir, vddkTransports, vddkThumbprint, timeout, originalVMName); + } + + Path inputXml = Files.createTempFile("cloudstack-vddk-rbd-" + temporaryConvertUuid, ".xml"); + Path outputXml = Files.createTempFile("cloudstack-vddk-rbd-" + temporaryConvertUuid + "-out", ".xml"); + Files.writeString(inputXml, buildDirectRbdLibvirtXml(temporaryConvertUuid, targetPool, createdImages)); + + boolean finalized = runInPlaceFinalization(inputXml, outputXml, libguestfsBackend, timeout, verboseModeEnabled, originalVMName, serverResource); + if (!finalized) { + cleanupRbdImages(targetPool, createdImages, originalVMName); + } + Files.deleteIfExists(inputXml); + Files.deleteIfExists(outputXml); + return finalized; + } catch (Exception e) { + logger.error("({}) Direct RBD VDDK import failed: {}", originalVMName, e.getMessage(), e); + cleanupRbdImages(targetPool, createdImages, originalVMName); + return false; + } finally { + try { + Files.deleteIfExists(Path.of(passwordFilePath)); + } catch (Exception e) { + logger.warn("({}) Failed to delete password file {}: {}", originalVMName, passwordFilePath, e.getMessage()); + } + } + } + + private void copyVddkSourceDiskToRbd(RemoteInstanceTO vmwareInstance, VmwareVddkSourceDiskTO sourceDisk, + KVMStoragePool targetPool, String imageName, String passwordFilePath, + String vddkLibDir, String vddkTransports, String vddkThumbprint, + long timeout, String originalVMName) { + if (StringUtils.isBlank(sourceDisk.getSourceDiskPath())) { + throw new CloudRuntimeException(String.format("VMware source disk %s does not have a VMDK path", sourceDisk.getDiskId())); + } + String rbdImagePath = targetPool.getSourceDir() + "/" + imageName; + String qemuRbdTarget = KVMPhysicalDisk.RBDStringBuilder(targetPool, rbdImagePath); + StringBuilder command = new StringBuilder(); + command.append("nbdkit -r -U - vddk "); + command.append("file=").append(shellQuote(sourceDisk.getSourceDiskPath())).append(" "); + command.append("server=").append(shellQuote(vmwareInstance.getVcenterHost())).append(" "); + command.append("user=").append(shellQuote(vmwareInstance.getVcenterUsername())).append(" "); + command.append("password=+").append(shellQuote(passwordFilePath)).append(" "); + if (StringUtils.isNotBlank(vmwareInstance.getVmwareMoref())) { + command.append("vm=").append(shellQuote("moref=" + vmwareInstance.getVmwareMoref())).append(" "); + } else { + command.append("vm=").append(shellQuote(vmwareInstance.getInstanceName())).append(" "); + } + command.append("libdir=").append(shellQuote(vddkLibDir)).append(" "); + command.append("thumbprint=").append(shellQuote(vddkThumbprint)).append(" "); + if (StringUtils.isNotBlank(vddkTransports)) { + command.append("transports=").append(shellQuote(vddkTransports)).append(" "); + } + String runCommand = "qemu-img convert -f raw -O raw \"$uri\" " + shellQuote(qemuRbdTarget); + command.append("--run ").append(shellQuote(runCommand)); + + Script script = new Script("/bin/bash", timeout, logger); + script.add("-c"); + script.add(command.toString()); + String logPrefix = String.format("(%s) vddk to rbd disk %s", originalVMName, sourceDisk.getDiskId()); + OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix); + logger.info("({}) Copying VMware disk {} to RBD image {}", originalVMName, sourceDisk.getSourceDiskPath(), rbdImagePath); + script.execute(outputLogger); + if (script.getExitValue() != 0) { + throw new CloudRuntimeException(String.format("Failed to copy VMware disk %s to RBD image %s", sourceDisk.getSourceDiskPath(), rbdImagePath)); + } + } + + private boolean runInPlaceFinalization(Path inputXml, Path outputXml, String libguestfsBackend, long timeout, + boolean verboseModeEnabled, String originalVMName, + LibvirtComputingResource serverResource) { + StringBuilder command = new StringBuilder(); + command.append("export LIBGUESTFS_BACKEND=").append(shellQuote(libguestfsBackend)).append(" && "); + if (serverResource.hostSupportsVirtV2vInPlaceBinary()) { + command.append("virt-v2v-in-place --root first -i libvirtxml ") + .append(shellQuote(inputXml.toString())).append(" -O ") + .append(shellQuote(outputXml.toString())).append(" "); + } else if (serverResource.hostSupportsVirtV2vInPlaceOption()) { + command.append("virt-v2v --root first -i libvirtxml ") + .append(shellQuote(inputXml.toString())).append(" --in-place "); + } else { + logger.error("({}) No virt-v2v in-place finalization method is available", originalVMName); + return false; + } + if (verboseModeEnabled) { + command.append("-v "); + } + + Script script = new Script("/bin/bash", timeout, logger); + script.add("-c"); + script.add(command.toString()); + OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, + String.format("(%s) virt-v2v rbd in-place", originalVMName)); + script.execute(outputLogger); + return script.getExitValue() == 0; + } + + private String buildDirectRbdLibvirtXml(String temporaryConvertUuid, KVMStoragePool targetPool, List imageNames) { + StringBuilder xml = new StringBuilder(); + xml.append("\n"); + xml.append(" ").append(xmlEscape("cloudstack-vddk-rbd-" + temporaryConvertUuid)).append("\n"); + xml.append(" 1048576\n"); + xml.append(" 1\n"); + xml.append(" hvm\n"); + xml.append(" \n"); + for (int i = 0; i < imageNames.size(); i++) { + String imageName = imageNames.get(i); + xml.append(" \n"); + xml.append(" \n"); + xml.append(" \n"); + for (String sourceHost : StringUtils.split(StringUtils.defaultString(targetPool.getSourceHost()), ",")) { + xml.append(" \n"); + } + xml.append(" \n"); + if (StringUtils.isNotBlank(targetPool.getAuthUserName())) { + xml.append(" \n"); + xml.append(" \n"); + xml.append(" \n"); + } + xml.append(" \n"); + xml.append(" \n"); + } + xml.append(" \n"); + xml.append("\n"); + return xml.toString(); + } + + private String buildRbdImageName(String temporaryConvertUuid, int position) { + return String.format("%s-disk-%03d", temporaryConvertUuid, position); + } + + private String diskTargetName(int index) { + StringBuilder target = new StringBuilder("sd"); + int value = index; + do { + target.insert(2, (char) ('a' + (value % 26))); + value = value / 26 - 1; + } while (value >= 0); + return target.toString(); + } + + private void probeRbdQemuAccess(KVMStoragePool pool, String temporaryConvertUuid) { + String probeName = temporaryConvertUuid + "-probe-" + UUID.randomUUID(); + String rbdImagePath = pool.getSourceDir() + "/" + probeName; + String qemuRbdPath = KVMPhysicalDisk.RBDStringBuilder(pool, rbdImagePath); + try { + Script qemuImg = new Script("qemu-img", 120000, logger); + qemuImg.add("create", "-f", "raw", qemuRbdPath, "4194304"); + qemuImg.execute(); + if (qemuImg.getExitValue() != 0) { + throw new CloudRuntimeException(String.format("qemu-img could not create RBD probe image %s", rbdImagePath)); + } + + Script qemuIo = new Script("qemu-io", 120000, logger); + qemuIo.add("-f", "raw", "-c", "write -P 0x5a 0 4k", "-c", "read -P 0x5a 0 4k", qemuRbdPath); + qemuIo.execute(); + if (qemuIo.getExitValue() != 0) { + throw new CloudRuntimeException(String.format("qemu-io could not verify RBD probe image %s", rbdImagePath)); + } + } finally { + try { + pool.deletePhysicalDisk(probeName, Storage.ImageFormat.RAW); + } catch (Exception e) { + logger.warn("Failed to delete RBD probe image {} from pool {}: {}", probeName, pool.getUuid(), e.getMessage()); + } + } + } + + private void cleanupRbdImages(KVMStoragePool pool, List imageNames, String originalVMName) { + for (String imageName : imageNames) { + try { + logger.info("({}) Cleaning up RBD image {} after failed direct import", originalVMName, imageName); + pool.deletePhysicalDisk(imageName, Storage.ImageFormat.RAW); + } catch (Exception e) { + logger.warn("({}) Failed to delete RBD image {} from pool {}: {}", originalVMName, imageName, pool.getUuid(), e.getMessage()); + } + } + } + + private String shellQuote(String value) { + return "'" + StringUtils.defaultString(value).replace("'", "'\"'\"'") + "'"; + } + + private String xmlEscape(String value) { + return StringUtils.defaultString(value) + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + protected String getVcenterThumbprint(String vcenterHost, long timeout, String originalVMName) { if (StringUtils.isBlank(vcenterHost)) { return null; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java index 5602da156799..d783b27c7b46 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -65,6 +66,7 @@ public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResour Hypervisor.HypervisorType sourceHypervisorType = sourceInstance.getHypervisorType(); String sourceInstanceName = sourceInstance.getInstanceName(); List destinationStoragePools = cmd.getDestinationStoragePools(); + List destinationStoragePoolTypes = cmd.getDestinationStoragePoolTypes(); DataStoreTO conversionTemporaryLocation = cmd.getConversionTemporaryLocation(); final String temporaryConvertUuid = cmd.getTemporaryConvertUuid(); final boolean forceConvertToPool = cmd.isForceConvertToPool(); @@ -74,6 +76,12 @@ public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResour final String temporaryConvertPath = temporaryStoragePool.getLocalPath(); try { + if (isForcedRbdConversion(conversionTemporaryLocation, forceConvertToPool)) { + List disks = getTemporaryDisksWithPrefixFromTemporaryPool(temporaryStoragePool, temporaryConvertPath, temporaryConvertUuid); + UnmanagedInstanceTO convertedInstanceTO = getConvertedUnmanagedInstance(temporaryConvertUuid, disks, null); + return new ImportConvertedInstanceAnswer(cmd, convertedInstanceTO); + } + String convertedBasePath = String.format("%s/%s", temporaryConvertPath, temporaryConvertUuid); LibvirtDomainXMLParser xmlParser = parseMigratedVMXmlDomain(convertedBasePath); @@ -87,7 +95,7 @@ public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResour disks = temporaryDisks; } else { disks = moveTemporaryDisksToDestination(temporaryDisks, - destinationStoragePools, storagePoolMgr); + destinationStoragePools, destinationStoragePoolTypes, storagePoolMgr); cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid); } @@ -107,6 +115,11 @@ public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResour } } + private boolean isForcedRbdConversion(DataStoreTO conversionTemporaryLocation, boolean forceConvertToPool) { + return forceConvertToPool && conversionTemporaryLocation instanceof PrimaryDataStoreTO && + ((PrimaryDataStoreTO) conversionTemporaryLocation).getPoolType() == Storage.StoragePoolType.RBD; + } + protected KVMStoragePool getTemporaryStoragePool(DataStoreTO conversionTemporaryLocation, KVMStoragePoolManager storagePoolMgr) { if (conversionTemporaryLocation instanceof NfsTO) { NfsTO nfsTO = (NfsTO) conversionTemporaryLocation; @@ -146,6 +159,7 @@ protected List getTemporaryDisksWithPrefixFromTemporaryPool(KVM List disksWithPrefix = pool.listPhysicalDisks() .stream() .filter(x -> x.getName().startsWith(prefix) && !x.getName().endsWith(".xml")) + .sorted(Comparator.comparing(KVMPhysicalDisk::getName)) .collect(Collectors.toList()); if (CollectionUtils.isEmpty(disksWithPrefix)) { msg = String.format("Could not find any converted disk with prefix %s on temporary location %s", prefix, path); @@ -177,6 +191,13 @@ protected void sanitizeDisksPath(List disks) { protected List moveTemporaryDisksToDestination(List temporaryDisks, List destinationStoragePools, KVMStoragePoolManager storagePoolMgr) { + return moveTemporaryDisksToDestination(temporaryDisks, destinationStoragePools, null, storagePoolMgr); + } + + protected List moveTemporaryDisksToDestination(List temporaryDisks, + List destinationStoragePools, + List destinationStoragePoolTypes, + KVMStoragePoolManager storagePoolMgr) { List targetDisks = new ArrayList<>(); if (temporaryDisks.size() != destinationStoragePools.size()) { String warn = String.format("Discrepancy between the converted instance disks (%s) " + @@ -185,16 +206,12 @@ protected List moveTemporaryDisksToDestination(List moveTemporaryDisksToDestination(List moveTemporaryDisksToDestination(List destinationStoragePoolTypes, int index) { + if (CollectionUtils.isEmpty(destinationStoragePoolTypes) || destinationStoragePoolTypes.size() <= index || destinationStoragePoolTypes.get(index) == null) { + return Storage.StoragePoolType.NetworkFilesystem; + } + return destinationStoragePoolTypes.get(index); + } + + private void probeRbdQemuAccess(KVMStoragePool pool, String destinationName) { + String probeName = destinationName + "-probe"; + String rbdImagePath = pool.getSourceDir() + "/" + probeName; + String qemuRbdPath = KVMPhysicalDisk.RBDStringBuilder(pool, rbdImagePath); + try { + Script qemuImg = new Script("qemu-img", 120000, logger); + qemuImg.add("create", "-f", "raw", qemuRbdPath, "4194304"); + qemuImg.execute(); + if (qemuImg.getExitValue() != 0) { + throw new CloudRuntimeException(String.format("qemu-img could not create RBD probe image %s", rbdImagePath)); + } + + Script qemuIo = new Script("qemu-io", 120000, logger); + qemuIo.add("-f", "raw", "-c", "write -P 0x5a 0 4k", "-c", "read -P 0x5a 0 4k", qemuRbdPath); + qemuIo.execute(); + if (qemuIo.getExitValue() != 0) { + throw new CloudRuntimeException(String.format("qemu-io could not verify RBD probe image %s", rbdImagePath)); + } + } finally { + try { + pool.deletePhysicalDisk(probeName, Storage.ImageFormat.RAW); + } catch (Exception e) { + logger.warn("Failed to delete RBD probe image {} from pool {}: {}", probeName, pool.getUuid(), e.getMessage()); + } + } + } + private UnmanagedInstanceTO getConvertedUnmanagedInstance(String baseName, List vmDisks, LibvirtDomainXMLParser xmlParser) { @@ -244,9 +298,15 @@ protected List getUnmanagedInstanceDisks(List storagePoolHostAndPath = getNfsStoragePoolHostAndPath(storagePool); - disk.setDatastoreHost(storagePoolHostAndPath.first()); - disk.setDatastorePath(storagePoolHostAndPath.second()); + if (storagePool.getType() == Storage.StoragePoolType.RBD) { + disk.setDatastoreHost(storagePool.getSourceHost()); + disk.setDatastorePath(storagePool.getSourceDir()); + disk.setImagePath(physicalDisk.getName()); + } else { + Pair storagePoolHostAndPath = getNfsStoragePoolHostAndPath(storagePool); + disk.setDatastoreHost(storagePoolHostAndPath.first()); + disk.setDatastorePath(storagePoolHostAndPath.second()); + } disk.setDatastoreName(storagePool.getUuid()); disk.setDatastoreType(storagePool.getType().name()); disk.setCapacity(physicalDisk.getVirtualSize()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java index 5a7d6d2c203a..4f66b2898125 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java @@ -54,6 +54,10 @@ public Answer execute(final ReadyCommand command, final LibvirtComputingResource hostDetails.put(Host.HOST_VDDK_SUPPORT, Boolean.toString(libvirtComputingResource.hostSupportsVddk())); hostDetails.put(Host.HOST_VDDK_LIB_DIR, StringUtils.defaultString(libvirtComputingResource.getVddkLibDir())); hostDetails.put(Host.HOST_VDDK_VERSION, StringUtils.defaultString(libvirtComputingResource.getVddkVersion())); + hostDetails.put(Host.HOST_VIRTV2V_INPLACE_SUPPORT, Boolean.toString(libvirtComputingResource.hostSupportsVirtV2vInPlace())); + hostDetails.put(Host.HOST_QEMU_IMG_RBD_SUPPORT, Boolean.toString(libvirtComputingResource.hostSupportsQemuImgRbd())); + hostDetails.put(Host.HOST_RBD_QEMU_COPY_SUPPORT, Boolean.toString(libvirtComputingResource.hostSupportsRbdQemuCopy())); + hostDetails.put(Host.HOST_VDDK_RBD_DIRECT_IMPORT_SUPPORT, Boolean.toString(libvirtComputingResource.hostSupportsVddkRbdDirectImport())); if (libvirtComputingResource.hostSupportsOvfExport()) { hostDetails.put(Host.HOST_OVFTOOL_VERSION, libvirtComputingResource.getHostOvfToolVersion()); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapperTest.java index 74090723331a..75147b3fd50a 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapperTest.java @@ -78,6 +78,32 @@ public void testCheckInstanceCommand_vddkSuccess() { assertTrue(answer.getResult()); } + @Test + public void testCheckInstanceCommand_vddkDirectRbdSuccess() { + Mockito.when(checkConvertInstanceCommandMock.isUseVddk()).thenReturn(true); + Mockito.when(checkConvertInstanceCommandMock.isCheckVddkRbdDirectImportSupport()).thenReturn(true); + Mockito.when(checkConvertInstanceCommandMock.getVddkLibDir()).thenReturn("/opt/vmware-vddk/vmware-vix-disklib-distrib"); + Mockito.when(libvirtComputingResourceMock.hostSupportsVddkRbdDirectImport("/opt/vmware-vddk/vmware-vix-disklib-distrib")).thenReturn(true); + Mockito.when(libvirtComputingResourceMock.hostSupportsVddk("/opt/vmware-vddk/vmware-vix-disklib-distrib")).thenReturn(true); + + Answer answer = checkConvertInstanceCommandWrapper.execute(checkConvertInstanceCommandMock, libvirtComputingResourceMock); + + assertTrue(answer.getResult()); + } + + @Test + public void testCheckInstanceCommand_vddkDirectRbdFailure() { + Mockito.when(checkConvertInstanceCommandMock.isUseVddk()).thenReturn(true); + Mockito.when(checkConvertInstanceCommandMock.isCheckVddkRbdDirectImportSupport()).thenReturn(true); + Mockito.when(checkConvertInstanceCommandMock.getVddkLibDir()).thenReturn("/opt/vmware-vddk/vmware-vix-disklib-distrib"); + Mockito.when(libvirtComputingResourceMock.hostSupportsVddkRbdDirectImport("/opt/vmware-vddk/vmware-vix-disklib-distrib")).thenReturn(false); + + Answer answer = checkConvertInstanceCommandWrapper.execute(checkConvertInstanceCommandMock, libvirtComputingResourceMock); + + assertFalse(answer.getResult()); + assertTrue(StringUtils.isNotBlank(answer.getDetails())); + } + @Test public void testCheckInstanceCommand_vddkFailure() { Mockito.when(checkConvertInstanceCommandMock.isUseVddk()).thenReturn(true); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapperTest.java index c30fa2f49482..dc74d4dd5e5f 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapperTest.java @@ -44,6 +44,7 @@ import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.storage.Storage; import com.cloud.utils.script.Script; @RunWith(MockitoJUnitRunner.class) @@ -151,6 +152,26 @@ public void testExecuteConvertUnsupportedHypervisors() { Assert.assertFalse(answer.getResult()); } + @Test + public void testExecuteDirectRbdVddkFailsWhenHostLacksDirectRbdSupport() { + RemoteInstanceTO remoteInstanceTO = getRemoteInstanceTO(Hypervisor.HypervisorType.VMware); + ConvertInstanceCommand cmd = getConvertInstanceCommand(remoteInstanceTO, Hypervisor.HypervisorType.KVM, false); + Mockito.when(cmd.isUseVddk()).thenReturn(true); + Mockito.when(cmd.getVddkLibDir()).thenReturn("/opt/vddk"); + Mockito.when(cmd.getConversionTemporaryLocation()).thenReturn(primaryDataStore); + Mockito.when(primaryDataStore.getPoolType()).thenReturn(Storage.StoragePoolType.RBD); + Mockito.when(primaryDataStore.getUuid()).thenReturn("rbd-pool-uuid"); + Mockito.when(storagePoolManager.getStoragePool(Storage.StoragePoolType.RBD, "rbd-pool-uuid")).thenReturn(destinationPool); + Mockito.when(destinationPool.getLocalPath()).thenReturn("/rbd"); + Mockito.when(libvirtComputingResourceMock.getVddkLibDir()).thenReturn("/opt/vddk"); + Mockito.when(libvirtComputingResourceMock.hostSupportsVddkRbdDirectImport("/opt/vddk")).thenReturn(false); + + Answer answer = convertInstanceCommandWrapper.execute(cmd, libvirtComputingResourceMock); + + Assert.assertFalse(answer.getResult()); + Assert.assertTrue(answer.getDetails().contains("Direct RBD VDDK import requires")); + } + @Test public void testExecuteConvertFailure() { RemoteInstanceTO remoteInstanceTO = getRemoteInstanceTO(Hypervisor.HypervisorType.VMware); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapperTest.java index 343a15b367d4..a65e14c7e6b2 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapperTest.java @@ -39,6 +39,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.Spy; @@ -167,6 +168,40 @@ public void testMoveTemporaryDisksToDestination() { Assert.assertEquals("xyz", movedDisks.get(0).getPath()); } + @Test + public void testMoveTemporaryDisksToRbdDestinationUsesDestinationPoolType() { + KVMPhysicalDisk sourceDisk = Mockito.mock(KVMPhysicalDisk.class); + Mockito.lenient().when(sourceDisk.getPool()).thenReturn(temporaryPool); + List disks = List.of(sourceDisk); + String destinationPoolUuid = UUID.randomUUID().toString(); + List destinationPools = List.of(destinationPoolUuid); + List destinationPoolTypes = List.of(Storage.StoragePoolType.RBD); + + KVMPhysicalDisk destDisk = Mockito.mock(KVMPhysicalDisk.class); + Mockito.when(destDisk.getPath()).thenReturn("rbd/image"); + Mockito.when(storagePoolManager.getStoragePool(Storage.StoragePoolType.RBD, destinationPoolUuid)) + .thenReturn(destinationPool); + Mockito.when(destinationPool.getType()).thenReturn(Storage.StoragePoolType.RBD); + Mockito.when(destinationPool.getSourceDir()).thenReturn("cloudstack"); + Mockito.when(destinationPool.getSourceHost()).thenReturn("10.0.0.1,10.0.0.2"); + Mockito.when(destinationPool.getSourcePort()).thenReturn(6789); + Mockito.when(storagePoolManager.copyPhysicalDisk(Mockito.eq(sourceDisk), Mockito.anyString(), Mockito.eq(destinationPool), Mockito.anyInt())) + .thenReturn(destDisk); + + try (MockedConstruction