diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 91f89738e32..d10049d0129 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -2319,23 +2319,24 @@ public class KVMStorageProcessor implements StorageProcessor { logger.debug("Rebasing snapshot [{}] with parent [{}].", snapshotName, parentSnapshotPath); + long snapshotTimeoutInMillis = wait * 1000L; try { - QemuImg qemuImg = new QemuImg(wait); + QemuImg qemuImg = new QemuImg(snapshotTimeoutInMillis); qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false); } catch (LibvirtException | QemuImgException e) { if (!StringUtils.contains(e.getMessage(), "Is another process using the image")) { logger.error("Exception while rebasing incremental snapshot [{}] due to: [{}].", snapshotName, e.getMessage(), e); throw new CloudRuntimeException(e); } - retryRebase(snapshotName, wait, e, snapshotFile, parentSnapshotFile); + retryRebase(snapshotName, snapshotTimeoutInMillis, e, snapshotFile, parentSnapshotFile); } } - private void retryRebase(String snapshotName, int wait, Exception e, QemuImgFile snapshotFile, QemuImgFile parentSnapshotFile) { + private void retryRebase(String snapshotName, long waitInMilliseconds, Exception e, QemuImgFile snapshotFile, QemuImgFile parentSnapshotFile) { logger.warn("Libvirt still has not released the lock, will wait [{}] milliseconds and try again later.", incrementalSnapshotRetryRebaseWait); try { Thread.sleep(incrementalSnapshotRetryRebaseWait); - QemuImg qemuImg = new QemuImg(wait); + QemuImg qemuImg = new QemuImg(waitInMilliseconds); qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false); } catch (LibvirtException | QemuImgException | InterruptedException ex) { logger.error("Unable to rebase snapshot [{}].", snapshotName, ex); diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java index 80e44d8059a..e51c80e521c 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java @@ -927,7 +927,10 @@ public class QemuImg { } protected static boolean helpSupportsImageFormat(String text, QemuImg.PhysicalDiskFormat format) { - Pattern pattern = Pattern.compile("Supported\\sformats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE); + // QEMU >= 10.1.0 changed the qemu-img --help header from + // "Supported formats:" to "Supported image formats:", so the word + // "image" must be treated as optional here. + Pattern pattern = Pattern.compile("Supported\\s(image\\s)?formats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE); return pattern.matcher(text).find(); } diff --git a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java index 140302590ba..502474e7c6b 100644 --- a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java +++ b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java @@ -390,6 +390,21 @@ public class QemuImgTest { Assert.assertFalse("should not support http", QemuImg.helpSupportsImageFormat(partialHelp, PhysicalDiskFormat.SHEEPDOG)); } + @Test + public void testHelpSupportsImageFormatQemu101Header() throws QemuImgException, LibvirtException { + // qemu-img 10.1.0 (e.g. RHEL 9.8: qemu-kvm-10.1.0-17.el9_8.3) changed the + // help header from "Supported formats:" to "Supported image formats:" + String help = "Supported image formats:\n" + + " blkdebug blklogwrites blkverify compress copy-before-write copy-on-read\n" + + " file ftp ftps host_cdrom host_device http https io_uring luks nbd null-aio\n" + + " null-co nvme nvme-io_uring preallocate qcow2 quorum raw rbd\n" + + " snapshot-access throttle vdi vhdx virtio-blk-vfio-pci\n" + + " virtio-blk-vhost-user virtio-blk-vhost-vdpa vmdk vpc\n"; + Assert.assertTrue("should support luks", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.LUKS)); + Assert.assertTrue("should support qcow2", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.QCOW2)); + Assert.assertFalse("should not support sheepdog", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.SHEEPDOG)); + } + @Test public void testCheckAndRepair() throws LibvirtException { String filename = "/tmp/" + UUID.randomUUID() + ".qcow2"; diff --git a/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java b/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java index f2aa567e02f..fae94bb1bea 100644 --- a/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java +++ b/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java @@ -28,6 +28,8 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -63,6 +65,7 @@ import org.apache.http.ssl.SSLContextBuilder; import com.cloud.utils.exception.CloudRuntimeException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -87,6 +90,10 @@ public class FlashArrayAdapter implements ProviderAdapter { private static final String API_LOGIN_VERSION_DEFAULT = "1.19"; private static final String API_VERSION_DEFAULT = "2.23"; + // URLs for which the legacy-auth deprecation WARN has already been emitted, + // so we don't spam the logs once per refresh per pool while it's still configured. + private static final Set WARNED_LEGACY_URLS = ConcurrentHashMap.newKeySet(); + static final ObjectMapper mapper = new ObjectMapper(); public String pod = null; public String hostgroup = null; @@ -589,6 +596,91 @@ public class FlashArrayAdapter implements ProviderAdapter { return accessToken; } + /** + * Discover the latest supported Purity REST API version by hitting the unauthenticated + * {@code /api/api_version} endpoint (returns {@code {"version":["1.0",...,"2.36"]}}). + * The discovered version is stored on {@link #apiVersion}; on failure the caller-configured + * default remains in place. + */ + private void fetchApiVersionFromPurity(CloseableHttpClient client) { + HttpGet vReq = new HttpGet(url + "/api_version"); + CloseableHttpResponse vResp = null; + try { + vResp = client.execute(vReq); + if (vResp.getStatusLine().getStatusCode() == 200) { + JsonNode root = mapper.readTree(vResp.getEntity().getContent()); + JsonNode versions = root.get("version"); + if (versions != null && versions.isArray() && versions.size() > 0) { + apiVersion = versions.get(versions.size() - 1).asText(); + } + } else { + logger.warn("Unexpected HTTP " + vResp.getStatusLine().getStatusCode() + + " from FlashArray [" + url + "] /api_version, falling back to default " + + API_VERSION_DEFAULT); + } + } catch (Exception e) { + logger.warn("Failed to discover Purity REST API version from " + url + + "/api_version, falling back to default " + API_VERSION_DEFAULT, e); + } finally { + if (vResp != null) { + try { + vResp.close(); + } catch (IOException e) { + logger.debug("Error closing /api_version response from FlashArray [" + url + "]", e); + } + } + } + } + + /** + * Exchange the operator-configured username/password for a long-lived Purity api-token + * via REST 1.x {@code /auth/apitoken}. Emits the once-per-URL deprecation WARN. + * @return the api-token to feed into the REST 2.x /login exchange. + */ + private String getApiTokenUsingUserPass(CloseableHttpClient client) throws IOException { + if (WARNED_LEGACY_URLS.add(url)) { + logger.warn("FlashArray adapter at [" + url + "] is using deprecated username/password " + + "login against Purity REST 1.x. Replace with a pre-minted " + + ProviderAdapter.API_TOKEN_KEY + " detail; the username/password code path will be " + + "removed in a future release."); + } + HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken"); + ArrayList postParms = new ArrayList(); + postParms.add(new BasicNameValuePair("username", username)); + postParms.add(new BasicNameValuePair("password", password)); + request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8")); + CloseableHttpResponse response = null; + try { + response = client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200 || statusCode == 201) { + FlashArrayApiToken legacyToken = mapper.readValue(response.getEntity().getContent(), + FlashArrayApiToken.class); + if (legacyToken == null || legacyToken.getApiToken() == null) { + throw new CloudRuntimeException( + "Authentication responded successfully but no api token was returned"); + } + return legacyToken.getApiToken(); + } else if (statusCode == 401 || statusCode == 403) { + throw new CloudRuntimeException( + "Authentication or Authorization to FlashArray [" + url + "] with user [" + username + + "] failed, unable to retrieve session token"); + } else { + throw new CloudRuntimeException( + "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode + + "] - " + response.getStatusLine().getReasonPhrase()); + } + } finally { + if (response != null) { + try { + response.close(); + } catch (IOException e) { + logger.debug("Error closing legacy auth/apitoken response from FlashArray [" + url + "]", e); + } + } + } + } + private synchronized void refreshSession(boolean force) { try { if (force || keyExpiration < System.currentTimeMillis()) { @@ -663,9 +755,11 @@ public class FlashArrayAdapter implements ProviderAdapter { } apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION); - if (apiVersion == null) { + boolean apiVersionExplicit = apiVersion != null; + if (!apiVersionExplicit) { apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION); - if (apiVersion == null) { + apiVersionExplicit = apiVersion != null; + if (!apiVersionExplicit) { apiVersion = API_VERSION_DEFAULT; } } @@ -732,72 +826,66 @@ public class FlashArrayAdapter implements ProviderAdapter { skipTlsValidation = true; } + // Resolve the long-lived API token. Prefer a pre-minted api_token (Purity REST 2.x flow); + // fall back to legacy username/password auth via Purity REST 1.x for backward compatibility. + String apiToken = connectionDetails.get(ProviderAdapter.API_TOKEN_KEY); + if (apiToken != null && apiToken.isEmpty()) { + apiToken = null; + } + boolean usingLegacyUserPass = apiToken == null; + if (usingLegacyUserPass && (username == null || password == null)) { + throw new CloudRuntimeException("FlashArray adapter requires either " + ProviderAdapter.API_TOKEN_KEY + + " (preferred) or both " + ProviderAdapter.API_USERNAME_KEY + " and " + + ProviderAdapter.API_PASSWORD_KEY + " in the connection details"); + } + + CloseableHttpClient client = getClient(); CloseableHttpResponse response = null; try { - HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken"); - // request.addHeader("Content-Type", "application/json"); - // request.addHeader("Accept", "application/json"); - ArrayList postParms = new ArrayList(); - postParms.add(new BasicNameValuePair("username", username)); - postParms.add(new BasicNameValuePair("password", password)); - request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8")); - CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); - - int statusCode = response.getStatusLine().getStatusCode(); - FlashArrayApiToken apitoken = null; - if (statusCode == 200 | statusCode == 201) { - apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class); - if (apitoken == null) { - throw new CloudRuntimeException( - "Authentication responded successfully but no api token was returned"); - } - } else if (statusCode == 401 || statusCode == 403) { - throw new CloudRuntimeException( - "Authentication or Authorization to FlashArray [" + url + "] with user [" + username - + "] failed, unable to retrieve session token"); - } else { - throw new CloudRuntimeException( - "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode - + "] - " + response.getStatusLine().getReasonPhrase()); + // Discover the latest supported API version from the array unless one was explicitly configured. + // GET /api/api_version is unauthenticated and returns {"version":["1.0",...,"2.36"]}. + if (!apiVersionExplicit) { + fetchApiVersionFromPurity(client); } - // now we need to get the access token - request = new HttpPost(url + "/" + apiVersion + "/login"); - request.addHeader("api-token", apitoken.getApiToken()); - response = (CloseableHttpResponse) client.execute(request); + if (usingLegacyUserPass) { + apiToken = getApiTokenUsingUserPass(client); + } - statusCode = response.getStatusLine().getStatusCode(); - if (statusCode == 200 | statusCode == 201) { + // Exchange the long-lived api-token for a short-lived x-auth-token (REST 2.x). + HttpPost request = new HttpPost(url + "/" + apiVersion + "/login"); + request.addHeader("api-token", apiToken); + response = client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200 || statusCode == 201) { Header[] headers = response.getHeaders("x-auth-token"); if (headers == null || headers.length == 0) { throw new CloudRuntimeException( - "Getting access token responded successfully but access token was not available"); + "FlashArray /login responded successfully but no x-auth-token header was returned"); } accessToken = headers[0].getValue(); } else if (statusCode == 401 || statusCode == 403) { throw new CloudRuntimeException( - "Authentication or Authorization to FlashArray [" + url + "] with user [" + username - + "] failed, unable to retrieve session token"); + "FlashArray [" + url + "] rejected the api-token at /" + apiVersion + "/login"); } else { throw new CloudRuntimeException( - "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode - + "] - " + response.getStatusLine().getReasonPhrase()); + "Unexpected HTTP response code from FlashArray [" + url + "] /" + apiVersion + + "/login - [" + statusCode + "] - " + + response.getStatusLine().getReasonPhrase()); } - } catch (UnsupportedEncodingException e) { - throw new CloudRuntimeException("Error creating input for login, check username/password encoding"); + throw new CloudRuntimeException("Error encoding login form for FlashArray [" + url + "]", e); } catch (UnsupportedOperationException e) { throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e); } catch (IOException e) { throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e); } finally { - try { - if (response != null) { + if (response != null) { + try { response.close(); + } catch (IOException e) { + logger.debug("Error closing response from login attempt to FlashArray", e); } - } catch (IOException e) { - logger.debug("Error closing response from login attempt to FlashArray", e); } } } @@ -965,7 +1053,7 @@ public class FlashArrayAdapter implements ProviderAdapter { request.setEntity(new StringEntity(data)); CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); + response = client.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 || statusCode == 201) { @@ -1020,7 +1108,7 @@ public class FlashArrayAdapter implements ProviderAdapter { request.addHeader("X-auth-token", getAccessToken()); CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); + response = client.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { try { @@ -1062,7 +1150,7 @@ public class FlashArrayAdapter implements ProviderAdapter { request.addHeader("X-auth-token", getAccessToken()); CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); + response = client.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 || statusCode == 404 || statusCode == 400) { // this means the volume was deleted successfully, or doesn't exist (effective diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index aec333c8ee3..f32857d7cf0 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -1507,6 +1507,7 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen */ Ternary, Integer>, List, Map> getTechnicallyCompatibleHosts( final VirtualMachine vm, + final Host srcHost, final Long startIndex, final Long pageSize, final String keyword) { @@ -1517,31 +1518,6 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen return new Ternary<>(new Pair<>(new ArrayList<>(), 0), new ArrayList<>(), new HashMap<>()); } - final long srcHostId = vm.getHostId(); - final Host srcHost = _hostDao.findById(srcHostId); - if (srcHost == null) { - if (logger.isDebugEnabled()) { - logger.debug("Unable to find the host with ID: " + srcHostId + " of this Instance: " + vm); - } - final InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find the host (with specified ID) of instance with specified ID"); - ex.addProxyObject(String.valueOf(srcHostId), "hostId"); - ex.addProxyObject(vm.getUuid(), "vmId"); - throw ex; - } - - String srcHostVersion = srcHost.getHypervisorVersion(); - if (HypervisorType.KVM.equals(srcHost.getHypervisorType()) && srcHostVersion == null) { - srcHostVersion = ""; - } - - // Check if the vm can be migrated with storage. - boolean canMigrateWithStorage = false; - - List hypervisorTypes = Arrays.asList(new HypervisorType[]{HypervisorType.VMware, HypervisorType.KVM}); - if (VirtualMachine.Type.User.equals(vm.getType()) || hypervisorTypes.contains(vm.getHypervisorType())) { - canMigrateWithStorage = _hypervisorCapabilitiesDao.isStorageMotionSupported(srcHost.getHypervisorType(), srcHostVersion); - } - // Check if the vm is using any disks on local storage. final VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(vm, null, _offeringDao.findById(vm.getId(), vm.getServiceOfferingId()), null, null); final List volumes = _volumeDao.findCreatedByInstance(vmProfile.getId()); @@ -1555,12 +1531,14 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen } } + boolean canMigrateWithStorage = isStorageMigrationSupported(vm, srcHost); if (!canMigrateWithStorage && usesLocal) { throw new InvalidParameterValueException("Unsupported operation, instance uses Local storage, cannot migrate"); } validateVgpuProfileForVmMigration(vmProfile); + final String srcHostVersion = getHypervisorVersionOfHost(srcHost); final Type hostType = srcHost.getType(); Pair, Integer> allHostsPair; List allHosts; @@ -1636,6 +1614,23 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen return new Ternary<>(allHostsPairResult, filteredHosts, requiresStorageMotion); } + protected boolean isStorageMigrationSupported(final VirtualMachine vm, final Host srcHost) { + final List hypervisorTypes = Arrays.asList(HypervisorType.VMware, HypervisorType.KVM); + if (VirtualMachine.Type.User.equals(vm.getType()) || hypervisorTypes.contains(vm.getHypervisorType())) { + final String srcHostVersion = getHypervisorVersionOfHost(srcHost); + return _hypervisorCapabilitiesDao.isStorageMotionSupported(srcHost.getHypervisorType(), srcHostVersion); + } + return false; + } + + protected String getHypervisorVersionOfHost(final Host host) { + final String version = host.getHypervisorVersion(); + if (version == null && HypervisorType.KVM.equals(host.getHypervisorType())) { + return ""; + } + return version; + } + /** * Apply affinity group constraints and other exclusion rules for VM migration. * This builds an ExcludeList based on affinity groups, DPDK requirements, and dedicated resources. @@ -1727,9 +1722,19 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen validateVmForHostMigration(vm); + final long srcHostId = vm.getHostId(); + final Host srcHost = _hostDao.findById(srcHostId); + if (srcHost == null) { + logger.debug("Unable to find the host with ID: {} of this Instance: {}", srcHostId, vm); + final InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find the host (with specified ID) of instance with specified ID"); + ex.addProxyObject(String.valueOf(srcHostId), "hostId"); + ex.addProxyObject(vm.getUuid(), "vmId"); + throw ex; + } + // Get technically compatible hosts (storage, hypervisor, UEFI) Ternary, Integer>, List, Map> compatibilityResult = - getTechnicallyCompatibleHosts(vm, startIndex, pageSize, keyword); + getTechnicallyCompatibleHosts(vm, srcHost, startIndex, pageSize, keyword); Pair, Integer> allHostsPair = compatibilityResult.first(); List filteredHosts = compatibilityResult.second(); @@ -1742,9 +1747,7 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen } // Create deployment plan and VM profile - final Host srcHost = _hostDao.findById(vm.getHostId()); - final DataCenterDeployment plan = new DataCenterDeployment( - srcHost.getDataCenterId(), srcHost.getPodId(), srcHost.getClusterId(), null, null, null); + final DataCenterDeployment plan = createDeploymentPlanForMigrationListing(vm, srcHost); final VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl( vm, null, _offeringDao.findById(vm.getId(), vm.getServiceOfferingId()), null, null); @@ -1769,6 +1772,14 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen } } + protected DataCenterDeployment createDeploymentPlanForMigrationListing(final VirtualMachine vm, final Host srcHost) { + final boolean canMigrateWithStorage = isStorageMigrationSupported(vm, srcHost); + if (canMigrateWithStorage) { + return new DataCenterDeployment(srcHost.getDataCenterId(), srcHost.getPodId(), null, null, null, null); + } + return new DataCenterDeployment(srcHost.getDataCenterId(), srcHost.getPodId(), srcHost.getClusterId(), null, null, null); + } + /** * Add non DPDK enabled hosts to the avoid list */ diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index e69f927b41e..1ac03258bee 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -47,8 +47,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd; @@ -63,6 +65,7 @@ import org.apache.cloudstack.api.command.user.userdata.ListUserDataCmd; import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; import org.apache.cloudstack.framework.config.ConfigDepot; @@ -73,24 +76,45 @@ import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; import org.apache.cloudstack.userdata.UserDataManager; import com.cloud.cpu.CPU; +import com.cloud.dc.DataCenterVO; import com.cloud.dc.Vlan.VlanType; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.deploy.DataCenterDeployment; +import com.cloud.deploy.DeploymentPlanningManager; import com.cloud.domain.dao.DomainDao; import com.cloud.api.ApiDBUtils; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.gpu.GPU; +import com.cloud.gpu.VgpuProfileVO; +import com.cloud.gpu.dao.VgpuProfileDao; import com.cloud.host.DetailVO; import com.cloud.host.Host; +import com.cloud.host.Host.Type; import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDetailsDao; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; +import com.cloud.hypervisor.kvm.dpdk.DpdkHelper; import com.cloud.network.IpAddress; import com.cloud.network.IpAddressManagerImpl; import com.cloud.network.dao.IPAddressVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.service.dao.ServiceOfferingDetailsDao; +import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOSCategoryVO; import com.cloud.storage.GuestOSVO; import com.cloud.storage.GuestOsCategory; import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSCategoryDao; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VolumeDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.SSHKeyPair; @@ -101,14 +125,18 @@ import com.cloud.user.UserDataVO; import com.cloud.user.dao.SSHKeyPairDao; import com.cloud.user.dao.UserDataDao; import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceDetailVO; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.agent.manager.allocator.HostAllocator; @@ -160,6 +188,42 @@ public class ManagementServerImplTest { @Mock HostDetailsDao hostDetailsDao; + @Mock + VMInstanceDao vmInstanceDao; + + @Mock + HostDao hostDao; + + @Mock + ServiceOfferingDetailsDao serviceOfferingDetailsDao; + + @Mock + VolumeDao volumeDao; + + @Mock + ServiceOfferingDao offeringDao; + + @Mock + DiskOfferingDao diskOfferingDao; + + @Mock + HypervisorCapabilitiesDao hypervisorCapabilitiesDao; + + @Mock + DataStoreManager dataStoreManager; + + @Mock + DpdkHelper dpdkHelper; + + @Mock + AffinityGroupVMMapDao affinityGroupVMMapDao; + + @Mock + DeploymentPlanningManager dpMgr; + + @Mock + DataCenterDao dcDao; + @Mock ConfigurationDao configDao; @@ -185,6 +249,12 @@ public class ManagementServerImplTest { @Mock HostAllocator hostAllocator; + @Mock + VgpuProfileDao vgpuProfileDao; + + @Mock + VgpuProfileVO vgpuProfileVO; + @Mock VirtualMachine virtualMachineMock; @@ -200,9 +270,6 @@ public class ManagementServerImplTest { @Mock Host hostMock; - @Mock - DeploymentPlanningManager deploymentPlanningManagerMock; - private AutoCloseable closeable; private MockedStatic apiDBUtilsMock; @@ -224,7 +291,10 @@ public class ManagementServerImplTest { apiDBUtilsMock = Mockito.mockStatic(ApiDBUtils.class); // Return empty list to avoid architecture filtering in most tests apiDBUtilsMock.when(() -> ApiDBUtils.listZoneClustersArchs(Mockito.anyLong())) - .thenReturn(new ArrayList<>()); + .thenReturn(new ArrayList<>()); + + when(vgpuProfileDao.findById(any())).thenReturn(vgpuProfileVO); + when(vgpuProfileVO.getName()).thenReturn("test-vgpu-profile"); } @After @@ -243,7 +313,7 @@ public class ManagementServerImplTest { } @Test(expected = InvalidParameterValueException.class) - public void testDuplicateRegistraitons(){ + public void testDuplicateRegistrations() { String accountName = "account"; String publicKeyString = "ssh-rsa very public"; String publicKeyMaterial = spy.getPublicKeyFromKeyKeyMaterial(publicKeyString); @@ -1091,4 +1161,1338 @@ public class ManagementServerImplTest { apiDBUtilsMock.verify(() -> ApiDBUtils.listZoneClustersArchs(Mockito.anyLong()), Mockito.never()); } + + // ============= Tests for listHostsForMigrationOfVM ============= + + @Test(expected = PermissionDeniedException.class) + public void testListHostsForMigrationOfVMNonRootAdmin() { + mockRunningVM(1L, HypervisorType.KVM); + Account caller = Mockito.mock(Account.class); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(accountManager.isRootAdmin(caller.getId())).thenReturn(false); + + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListHostsForMigrationOfVMNullVM() { + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(null); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListHostsForMigrationOfVMNotRunning() { + VMInstanceVO vm = mockVM(1L, HypervisorType.KVM, State.Stopped); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListHostsForMigrationOfVMUnsupportedHypervisor() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.BareMetal); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListHostsForMigrationOfVMLxcUserVM() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.LXC); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.User); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + } + + @Test + public void testListHostsForMigrationOfVMGpuEnabled() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + long hostId = vm.getHostId(); + HostVO srcHost = mockHost(hostId, 4L, 5L, 6L, HypervisorType.KVM); + Account caller = mockRootAdminAccount(); + + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.doReturn(srcHost).when(hostDao).findById(hostId); + + // Mock GPU detail + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(Mockito.mock(com.cloud.service.ServiceOfferingDetailsVO.class)); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + Assert.assertNotNull(result); + Assert.assertEquals(0, result.first().first().size()); + Assert.assertEquals(Integer.valueOf(0), result.first().second()); + Assert.assertEquals(0, result.second().size()); + } + + @Test + public void testListHostsForMigrationOfVMWithSystemVM() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.VMware); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.ConsoleProxy); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.VMware); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // System VMs can use storage motion + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.VMware, null)) + .thenReturn(true); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers with zone-wide scope (storage motion enabled) + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.VMware); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.VMware); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.any(HypervisorType.class), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume, true); + + // Verify this doesn't throw exception - system VMs should be migratable + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify storage motion capability was checked + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); + + // Verify result structure and data + Assert.assertNotNull(result); + Assert.assertNotNull("All hosts list should not be null", result.first()); + Assert.assertNotNull("Suitable hosts list should not be null", result.second()); + Assert.assertNotNull("Storage motion map should not be null", result.third()); + + // Verify all hosts returned (from searchForServers) + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("All hosts list should contain 2 hosts", 2, result.first().first().size()); + + // Verify suitable hosts (from host allocator) + Assert.assertEquals("Should return 2 suitable hosts", 2, result.second().size()); + Assert.assertTrue("Suitable hosts should contain host1", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Suitable hosts should contain host2", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + @Test + public void testListHostsForMigrationOfVMWithDomainRouter() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers for cluster-scoped search + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume); + + // Verify domain router can be migrated + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify hypervisor capabilities were checked + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); + + // Verify result contains expected hosts + Assert.assertNotNull(result); + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should return 2 suitable hosts", 2, result.second().size()); + Assert.assertTrue("Result should contain host 101", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Result should contain host 102", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + @Test + public void testListHostsForMigrationOfVMWithMultipleVolumes() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + // Multiple volumes - root and data disk + VolumeVO rootVolume = mockVolume(1L, 1L); + VolumeVO dataVolume = mockVolume(2L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(rootVolume, dataVolume)); + + DiskOfferingVO sharedOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(1L)).thenReturn(sharedOffering); + + // Mock searchForServers for cluster-scoped search + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, rootVolume); + + // Verify multiple volumes are handled + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify all volumes were checked + Mockito.verify(volumeDao).findCreatedByInstance(vm.getId()); + Mockito.verify(diskOfferingDao, Mockito.times(2)).findById(1L); + + // Verify result + Assert.assertNotNull(result); + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should return 2 suitable hosts for migration", 2, result.second().size()); + Assert.assertTrue("Suitable hosts should include host 101", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Suitable hosts should include host 102", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListHostsForMigrationOfVMWithMixedStorage() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.XenServer); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.XenServer); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // No storage motion support + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.XenServer, null)) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + // Mixed storage - one shared, one local + VolumeVO sharedVolume = mockVolume(1L, 1L); + VolumeVO localVolume = mockVolume(2L, 2L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(sharedVolume, localVolume)); + + DiskOfferingVO sharedOffering = mockSharedDiskOffering(1L); + DiskOfferingVO localOffering = mockLocalDiskOffering(2L); + Mockito.when(diskOfferingDao.findById(sharedVolume.getDiskOfferingId())).thenReturn(sharedOffering); + Mockito.when(diskOfferingDao.findById(localVolume.getDiskOfferingId())).thenReturn(localOffering); + + // Should throw exception because we have local storage without storage motion support + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + } + + @Test + public void testListHostsForMigrationOfVMKVMWithNullHypervisorVersion() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + // KVM host with null hypervisor version + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // KVM null version should be treated as empty string + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(true); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers with zone-wide scope (storage motion enabled) + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.anyLong(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.any(HypervisorType.class), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume, true); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify KVM null version was converted to empty string + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); + + // Verify result data + Assert.assertNotNull(result); + Assert.assertEquals("Total hosts should be 2", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Suitable hosts should be 2", 2, result.second().size()); + Assert.assertTrue("Should contain host 101", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Should contain host 102", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + @Test + public void testListHostsForMigrationOfVMNonUefiVm() { + // Test VM migration for non-UEFI VM (regular VM migration flow) + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers for cluster-scoped search + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify hosts are returned for migration (non-UEFI VMs don't need UEFI-enabled hosts) + Assert.assertNotNull(result); + Assert.assertEquals("Should have 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should have 2 suitable hosts", 2, result.second().size()); + Assert.assertTrue("Should contain host 101", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Should contain host 102", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + @Test + public void testListHostsForMigrationOfVMWithUefiVmClusterScope() { + // Test UEFI VM migration with cluster-scoped search (no storage motion) + // This exercises the code path where filteredHosts is NULL and allocateTo without list is called + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // No storage motion support - cluster-scoped search + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers for cluster-scoped search + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + // Mock filterUefiHostsForMigration to return success with filtered hosts (only host1 is UEFI-compatible) + List uefiCompatibleHosts = List.of(host1); + Pair> uefiFilterResult = new Pair<>(true, uefiCompatibleHosts); + Mockito.doReturn(uefiFilterResult).when(spy).filterUefiHostsForMigration( + Mockito.anyList(), Mockito.anyList(), Mockito.any()); + + // Setup other mocks + Mockito.when(dpdkHelper.isVMDpdkEnabled(vm.getId())).thenReturn(false); + Mockito.when(affinityGroupVMMapDao.countAffinityGroupsForVm(vm.getId())).thenReturn(0L); + DataCenterVO dc = Mockito.mock(DataCenterVO.class); + Mockito.when(dcDao.findById(srcHost.getDataCenterId())).thenReturn(dc); + Mockito.doNothing().when(dpMgr).checkForNonDedicatedResources(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doNothing().when(dpMgr).reorderHostsByPriority(Mockito.any(), Mockito.anyList()); + + // After UEFI filtering, filteredHosts is set to uefiCompatibleHosts (line 1582) + // So allocateTo WITH list parameter is called (line 1608) even in cluster scope + Mockito.when(hostAllocator.allocateTo(Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean())) + .thenReturn(new ArrayList<>(uefiCompatibleHosts)); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify result structure + Assert.assertNotNull("Result should not be null", result); + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("All hosts list should contain 2 hosts", 2, result.first().first().size()); + + // Verify only UEFI-compatible hosts are in suitable list + Assert.assertEquals("Should have 1 UEFI-compatible suitable host", 1, result.second().size()); + Assert.assertTrue("Host 101 should be the only UEFI-compatible host", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertFalse("Host 102 should not be in suitable hosts (not UEFI-compatible)", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + @Test + public void testListHostsForMigrationOfVMWithUefiVmZoneWideScope() { + // Test UEFI VM migration with zone-wide search (storage motion enabled) + // This exercises the code path where filteredHosts IS populated and allocateTo WITH list is called + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.VMware); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.User); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.VMware); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // Storage motion supported - zone-wide search with filteredHosts + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.VMware, null)) + .thenReturn(true); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers for zone-wide search (storage motion enabled) + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.VMware); + HostVO host2 = mockHost(102L, 2L, 1L, 1L, HypervisorType.VMware); // Different cluster + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.anyLong(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.any(HypervisorType.class), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume, true); + + // Mock filterUefiHostsForMigration to return success with filtered hosts (only host1 is UEFI-compatible) + List uefiCompatibleHosts = List.of(host1); + Pair> uefiFilterResult = new Pair<>(true, uefiCompatibleHosts); + Mockito.doReturn(uefiFilterResult).when(spy).filterUefiHostsForMigration( + Mockito.anyList(), Mockito.anyList(), Mockito.any()); + + // Override hostAllocator to return only UEFI-compatible hosts + // Uses allocateTo WITH list parameter (filteredHosts is populated in zone-wide search) + Mockito.when(hostAllocator.allocateTo(Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean())) + .thenReturn(new ArrayList<>(uefiCompatibleHosts)); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify result structure + Assert.assertNotNull("Result should not be null", result); + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("All hosts list should contain 2 hosts", 2, result.first().first().size()); + + // Verify only UEFI-compatible hosts are in suitable list + Assert.assertEquals("Should have 1 UEFI-compatible suitable host", 1, result.second().size()); + Assert.assertTrue("Host 101 should be the only UEFI-compatible host", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertFalse("Host 102 should not be in suitable hosts (not UEFI-compatible)", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + + // Verify storage motion map is populated + Assert.assertNotNull("Storage motion map should not be null", result.third()); + } + + @Test + public void testListHostsForMigrationOfVMUefiFilteringReturnsEmpty() { + // Test case where UEFI filtering results in no suitable hosts + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers for cluster-scoped search + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + // Mock filterUefiHostsForMigration FIRST to return false (no UEFI-enabled hosts found) + // This simulates the scenario where UEFI VM has no compatible hosts + Pair> uefiFilterResult = new Pair<>(false, null); + Mockito.doReturn(uefiFilterResult).when(spy).filterUefiHostsForMigration( + Mockito.anyList(), Mockito.anyList(), Mockito.any()); + + // Note: No other mocks needed because when filterUefiHostsForMigration returns false, + // the method returns early and doesn't proceed to host allocation or other processing + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify result structure + Assert.assertNotNull("Result should not be null", result); + Assert.assertNotNull("All hosts list should not be null", result.first()); + Assert.assertNotNull("Suitable hosts list should not be null", result.second()); + Assert.assertNotNull("Storage motion map should not be null", result.third()); + + // Verify all hosts are still returned (from searchForServers) + Assert.assertEquals("Should still return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("All hosts list should contain 2 hosts", 2, result.first().first().size()); + + // Verify suitable hosts list is empty due to UEFI filtering + Assert.assertEquals("Should have 0 suitable hosts after UEFI filtering", 0, result.second().size()); + + // Verify storage motion map is empty + Assert.assertTrue("Storage motion map should be empty when no suitable hosts", result.third().isEmpty()); + } + + @Test + public void testListHostsForMigrationOfVMStorageMotionCapabilityCheck() { + // Test User VM with VMware - should check storage motion for User VMs + VMInstanceVO userVm = mockRunningVM(1L, HypervisorType.VMware); + Mockito.when(userVm.getType()).thenReturn(VirtualMachine.Type.User); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(userVm); + Mockito.when(serviceOfferingDetailsDao.findDetail(userVm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.VMware); + Mockito.when(hostDao.findById(userVm.getHostId())).thenReturn(srcHost); + + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.VMware, null)) + .thenReturn(true); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(userVm.getId(), userVm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(userVm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers with zone-wide scope (storage motion enabled) + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.VMware); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.VMware); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.anyLong(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.any(HypervisorType.class), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(userVm, srcHost, hosts, volume, true); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify storage motion capability was checked for User VM + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); + + // Verify response data + Assert.assertNotNull(result); + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should have 2 suitable hosts", 2, result.second().size()); + Assert.assertTrue("Host 101 should be in suitable list", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Host 102 should be in suitable list", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + @Test + public void testListHostsForMigrationOfVMWithAllSupportedHypervisors() { + // Test each supported hypervisor type + HypervisorType[] supportedTypes = { + HypervisorType.XenServer, + HypervisorType.VMware, + HypervisorType.KVM, + HypervisorType.Ovm, + HypervisorType.Hyperv, + HypervisorType.Ovm3 + }; + + for (HypervisorType hypervisorType : supportedTypes) { + VMInstanceVO vm = mockRunningVM(1L, hypervisorType); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, hypervisorType); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + String version = hypervisorType == HypervisorType.KVM ? "" : null; + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(hypervisorType, version)) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers for cluster-scoped search + HostVO host1 = mockHost(101L, 1L, 1L, 1L, hypervisorType); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, hypervisorType); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify hypervisor is in supported hypervisors list + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(hypervisorType, version); + + // Verify validation passed for this hypervisor + Assert.assertNotNull("Result should not be null for " + hypervisorType, result); + Assert.assertEquals("Should return 2 total hosts for " + hypervisorType, + Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should have 2 suitable hosts for " + hypervisorType, + 2, result.second().size()); + Assert.assertTrue("Host 101 should be available for " + hypervisorType, + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Host 102 should be available for " + hypervisorType, + result.second().stream().anyMatch(h -> h.getId() == 102L)); + + // Reset mocks for next iteration + Mockito.reset(vmInstanceDao, hostDao, serviceOfferingDetailsDao, volumeDao, + diskOfferingDao, hypervisorCapabilitiesDao, offeringDao, dpdkHelper, + affinityGroupVMMapDao, dpMgr, dcDao, hostAllocator, dataStoreManager); + Mockito.reset(spy); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void testListHostsForMigrationOfVMSourceHostNotFound() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(null); + + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListHostsForMigrationOfVMLocalStorageNoStorageMotion() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.XenServer); + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.XenServer); + Account caller = mockRootAdminAccount(); + + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // Mock storage motion not supported + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.XenServer, null)) + .thenReturn(false); + + // Mock local storage usage + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockLocalDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + } + + @Test + public void testListHostsForMigrationOfVMStorageMotionCheckForSystemVM() { + // Test that storage motion capability is checked for System VMs + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.VMware); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.ConsoleProxy); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.VMware); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // Storage motion supported for VMware + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.VMware, null)) + .thenReturn(true); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers with zone-wide scope (storage motion enabled) + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.VMware); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.VMware); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.any(HypervisorType.class), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume, true); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify that storage motion capability was checked for system VM (VMware is in hypervisorTypes list) + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); + + // Verify response structure + Assert.assertNotNull(result); + Assert.assertEquals("Should have 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertTrue("Should have suitable hosts", result.second().size() > 0); + } + + @Test + public void testListHostsForMigrationOfVMStorageMotionCheckForUserVM() { + // Test that storage motion capability is checked for User VMs + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.User); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // Storage motion supported for User VM with KVM + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(true); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers with zone-wide scope (storage motion enabled) + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.anyLong(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.any(HypervisorType.class), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume, true); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify User VM can migrate with storage (User VM type always checks) + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); + + // Verify response data + Assert.assertNotNull(result); + Assert.assertEquals("Should have 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertTrue("Should have suitable hosts", result.second().size() > 0); + } + + @Test + public void testListHostsForMigrationOfVMWithoutStorageMotionClusterScope() { + // When storage motion not supported, should search only in same cluster + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.XenServer); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.User); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.XenServer); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // No storage motion support + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.XenServer, null)) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers - verify cluster scope is used + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.XenServer); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.XenServer); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.eq(0L), Mockito.eq(20L), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.eq(1L), // cluster=1L + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.eq(100L)); + + setupMigrationMocks(vm, srcHost, hosts, volume); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify XenServer without storage motion was checked + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.XenServer, null); + // Verify cluster-scoped search was used (not zone-wide) + Mockito.verify(spy).searchForServers( + Mockito.eq(0L), Mockito.eq(20L), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.eq(1L), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.eq(100L)); + + // Verify response data + Assert.assertNotNull(result); + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should have 2 suitable hosts", 2, result.second().size()); + Assert.assertTrue("Should contain host 101", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Should contain host 102", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + @Test + public void testListHostsForMigrationOfVMWithNoVolumes() { + // Edge case: VM with no volumes + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + // No volumes + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(new ArrayList<>()); + + // Mock searchForServers for cluster-scoped search + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + // Set up mocks without volume since there are no volumes + Pair> uefiResult = new Pair<>(true, hosts); + Mockito.doReturn(uefiResult).when(spy).filterUefiHostsForMigration( + Mockito.anyList(), Mockito.anyList(), Mockito.any()); + Mockito.when(dpdkHelper.isVMDpdkEnabled(vm.getId())).thenReturn(false); + Mockito.when(affinityGroupVMMapDao.countAffinityGroupsForVm(vm.getId())).thenReturn(0L); + DataCenterVO dc = Mockito.mock(DataCenterVO.class); + Mockito.when(dcDao.findById(1L)).thenReturn(dc); + Mockito.doNothing().when(dpMgr).checkForNonDedicatedResources(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doNothing().when(dpMgr).reorderHostsByPriority(Mockito.any(), Mockito.anyList()); + + Mockito.when(hostAllocator.allocateTo(Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean())) + .thenReturn(new ArrayList<>(hosts)); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Should still process without throwing exception for usesLocal check + Mockito.verify(volumeDao).findCreatedByInstance(vm.getId()); + + // Verify response + Assert.assertNotNull(result); + Assert.assertEquals("Should have 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should have 2 suitable hosts even with no volumes", 2, result.second().size()); + Assert.assertTrue("Storage motion map should be empty for VM with no volumes", + result.third().isEmpty()); + } + + @Test + public void testListHostsForMigrationOfVMOverloadedMethod() { + // Test the overloaded method that takes vmId instead of vm object + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers for cluster-scoped search with keyword + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.eq("keyword-test"), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume); + + // Call overloaded method with vmId + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, "keyword-test"); + + // Verify VM was fetched by ID + Mockito.verify(vmInstanceDao).findById(1L); + + // Verify keyword was passed to searchForServers + Mockito.verify(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.eq("keyword-test"), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + // Verify response data + Assert.assertNotNull(result); + Assert.assertEquals("Should have 2 hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should have 2 suitable hosts", 2, result.second().size()); + } + + @Test + public void testListHostsForMigrationOfVMVmwareStorageMotionCheck() { + // VMware should check storage motion even for non-User VMs + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.VMware); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.VMware); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + // VMware with DomainRouter should still check storage motion + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.VMware, null)) + .thenReturn(true); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers with zone-wide scope (storage motion enabled) + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.VMware); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.VMware); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.any(HypervisorType.class), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume, true); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify VMware always checks storage motion (hypervisorTypes list includes VMware) + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); + + // Verify response + Assert.assertNotNull(result); + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should have 2 suitable hosts", 2, result.second().size()); + Assert.assertTrue("Host 101 should be in the list", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + } + + + @Test + public void testListHostsForMigrationOfVMWithNullKeyword() { + // Test with null keyword parameter + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + + Account caller = mockRootAdminAccount(); + Mockito.doReturn(caller).when(spy).getCaller(); + Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm); + Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) + .thenReturn(null); + + HostVO srcHost = mockHost(100L, 1L, 1L, 1L, HypervisorType.KVM); + Mockito.when(hostDao.findById(vm.getHostId())).thenReturn(srcHost); + + Mockito.when(hypervisorCapabilitiesDao.isStorageMotionSupported(HypervisorType.KVM, "")) + .thenReturn(false); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); + + VolumeVO volume = mockVolume(1L, 1L); + Mockito.when(volumeDao.findCreatedByInstance(vm.getId())).thenReturn(List.of(volume)); + + DiskOfferingVO diskOffering = mockSharedDiskOffering(1L); + Mockito.when(diskOfferingDao.findById(volume.getDiskOfferingId())).thenReturn(diskOffering); + + // Mock searchForServers for cluster-scoped search + HostVO host1 = mockHost(101L, 1L, 1L, 1L, HypervisorType.KVM); + HostVO host2 = mockHost(102L, 1L, 1L, 1L, HypervisorType.KVM); + List hosts = List.of(host1, host2); + Pair, Integer> hostsPair = new Pair<>(hosts, 2); + Mockito.doReturn(hostsPair).when(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + setupMigrationMocks(vm, srcHost, hosts, volume); + + Ternary, Integer>, List, Map> result = + spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); + + // Verify null keyword is handled + Mockito.verify(vmInstanceDao).findById(1L); + + // Verify searchForServers was called with null keyword + Mockito.verify(spy).searchForServers( + Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Type.class), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyLong(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), + Mockito.isNull(), Mockito.isNull(), Mockito.anyLong()); + + // Verify response data + Assert.assertNotNull(result); + Assert.assertEquals("Should return 2 total hosts", Integer.valueOf(2), result.first().second()); + Assert.assertEquals("Should have 2 suitable hosts", 2, result.second().size()); + Assert.assertTrue("Host 101 should be available", + result.second().stream().anyMatch(h -> h.getId() == 101L)); + Assert.assertTrue("Host 102 should be available", + result.second().stream().anyMatch(h -> h.getId() == 102L)); + } + + // Note: Tests for success scenarios with complex flows (managed storage, zone-wide volumes, + // DPDK exclusion, affinity groups, architecture filtering) require full setup with mocking + // private methods like hasSuitablePoolsForVolume(), excludeNonDPDKEnabledHosts(), and + // filterUefiHostsForMigration() which are better suited for integration tests. + + // ============= Helper methods for tests ============= + + /** + * Sets up common mocks for successful migration tests + * For storage motion tests, set forceStorageMotion=true to configure volume in same cluster + * (which avoids complex filtering logic for cross-cluster storage motion) + */ + private void setupMigrationMocks(VMInstanceVO vm, HostVO srcHost, + List targetHosts, VolumeVO volume) { + setupMigrationMocks(vm, srcHost, targetHosts, volume, false); + } + + private void setupMigrationMocks(VMInstanceVO vm, HostVO srcHost, + List targetHosts, VolumeVO volume, + boolean forceStorageMotion) { + // Mock dataStoreManager for volume pool lookup (lenient as not used in all paths) + // For storage motion tests, put volume in same cluster to avoid complex filtering + PrimaryDataStore primaryDataStore = Mockito.mock(PrimaryDataStore.class); + Mockito.when(dataStoreManager.getPrimaryDataStore(volume.getPoolId())).thenReturn(primaryDataStore); + // If not forceStorageMotion, volume is in same cluster (no storage motion needed) + // If forceStorageMotion, set volClusterId to null (zone-wide storage) + Mockito.when(primaryDataStore.getClusterId()).thenReturn(forceStorageMotion ? null : srcHost.getClusterId()); + + // Mock zoneWideVolumeRequiresStorageMotion for zone-wide volumes + if (forceStorageMotion) { + Mockito.doReturn(false).when(spy).zoneWideVolumeRequiresStorageMotion( + Mockito.any(), Mockito.any(), Mockito.any()); + } + + // Mock filterUefiHostsForMigration - must return hosts properly + Pair> uefiResult = new Pair<>(true, new ArrayList<>(targetHosts)); + Mockito.doReturn(uefiResult).when(spy).filterUefiHostsForMigration( + Mockito.anyList(), Mockito.anyList(), Mockito.any()); + + // Mock DPDK check + Mockito.when(dpdkHelper.isVMDpdkEnabled(vm.getId())).thenReturn(false); + + // Mock affinity group count + Mockito.when(affinityGroupVMMapDao.countAffinityGroupsForVm(vm.getId())).thenReturn(0L); + + // Mock datacenter + DataCenterVO dc = Mockito.mock(DataCenterVO.class); + Mockito.when(dcDao.findById(srcHost.getDataCenterId())).thenReturn(dc); + + // Mock dedicated resources check + Mockito.doNothing().when(dpMgr).checkForNonDedicatedResources( + Mockito.any(), Mockito.any(), Mockito.any()); + + // Mock priority reordering + Mockito.doNothing().when(dpMgr).reorderHostsByPriority(Mockito.any(), Mockito.anyList()); + + // Mock host allocators - both signatures + // 1. Version with filteredHosts list (used when canMigrateWithStorage = true) + Mockito.when(hostAllocator.allocateTo(Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean())) + .thenReturn(new ArrayList<>(targetHosts)); + } + + private VMInstanceVO mockRunningVM(Long id, HypervisorType hypervisorType) { + return mockVM(id, hypervisorType, State.Running); + } + + private VMInstanceVO mockVM(Long id, HypervisorType hypervisorType, State state) { + VMInstanceVO vm = Mockito.mock(VMInstanceVO.class); + when(vm.getId()).thenReturn(id); + when(vm.getState()).thenReturn(state); + when(vm.getHypervisorType()).thenReturn(hypervisorType); + when(vm.getHostId()).thenReturn(100L); + when(vm.getServiceOfferingId()).thenReturn(1L); + when(vm.getType()).thenReturn(VirtualMachine.Type.User); + when(vm.getUuid()).thenReturn("uuid-" + id); + when(vm.getDataCenterId()).thenReturn(1L); + return vm; + } + + private Account mockRootAdminAccount() { + Account account = Mockito.mock(Account.class); + Mockito.when(account.getId()).thenReturn(1L); + Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(true); + return account; + } + + private HostVO mockHost(Long id, Long clusterId, Long podId, Long dcId, HypervisorType hypervisorType) { + HostVO host = new HostVO("guid-" + id); + ReflectionTestUtils.setField(host, "id", id); + ReflectionTestUtils.setField(host, "clusterId", clusterId); + ReflectionTestUtils.setField(host, "podId", podId); + ReflectionTestUtils.setField(host, "dataCenterId", dcId); + ReflectionTestUtils.setField(host, "hypervisorType", hypervisorType); + ReflectionTestUtils.setField(host, "type", Host.Type.Routing); + ReflectionTestUtils.setField(host, "hypervisorVersion", null); + return host; + } + + private VolumeVO mockVolume(Long id, Long poolId) { + VolumeVO volume = Mockito.mock(VolumeVO.class); + when(volume.getId()).thenReturn(id); + when(volume.getPoolId()).thenReturn(poolId); + when(volume.getDiskOfferingId()).thenReturn(1L); + when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT); + return volume; + } + + private DiskOfferingVO mockLocalDiskOffering(Long id) { + DiskOfferingVO diskOffering = Mockito.mock(DiskOfferingVO.class); + Mockito.when(diskOffering.getId()).thenReturn(id); + Mockito.when(diskOffering.isUseLocalStorage()).thenReturn(true); + return diskOffering; + } + + private DiskOfferingVO mockSharedDiskOffering(Long id) { + DiskOfferingVO diskOffering = Mockito.mock(DiskOfferingVO.class); + Mockito.when(diskOffering.getId()).thenReturn(id); + Mockito.when(diskOffering.isUseLocalStorage()).thenReturn(false); + return diskOffering; + } + + @Test + public void createDeploymentPlanForMigrationListingTestAllocatesInAnyClusterWhenStorageMigrationIsSupported() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.KVM); + HostVO srcHost = mockHost(vm.getHostId(), 1L, 2L, 3L, HypervisorType.KVM); + + Mockito.doReturn(true).when(spy).isStorageMigrationSupported(vm, srcHost); + + DataCenterDeployment deploymentPlan = spy.createDeploymentPlanForMigrationListing(vm, srcHost); + + Assert.assertEquals(3L, deploymentPlan.getDataCenterId()); + Assert.assertEquals(2L, (long) deploymentPlan.getPodId()); + Assert.assertNull(deploymentPlan.getClusterId()); + } + + @Test + public void createDeploymentPlanForMigrationListingTestAllocatesInSourceClusterWhenStorageMigrationIsNotSupported() { + VMInstanceVO vm = mockRunningVM(1L, HypervisorType.XenServer); + HostVO srcHost = mockHost(vm.getHostId(), 4L, 5L, 6L, HypervisorType.XenServer); + + Mockito.doReturn(false).when(spy).isStorageMigrationSupported(vm, srcHost); + + DataCenterDeployment deploymentPlan = spy.createDeploymentPlanForMigrationListing(vm, srcHost); + + Assert.assertEquals(6L, deploymentPlan.getDataCenterId()); + Assert.assertEquals(5L, (long) deploymentPlan.getPodId()); + Assert.assertEquals(4L, (long) deploymentPlan.getClusterId()); + } }