From ec2d3ea1e6d186964760c8d2ab33d3164715e1b5 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Fri, 10 Jul 2026 13:22:58 -0300 Subject: [PATCH 1/5] Fix `findHostsForMigration` never returning hosts from other clusters (#13452) --- .../cloud/server/ManagementServerImpl.java | 69 +++++++++++-------- .../server/ManagementServerImplTest.java | 61 ++++++++++++---- 2 files changed, 86 insertions(+), 44 deletions(-) diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index bd4c311e3cd..1fc92ad0e8e 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -1469,6 +1469,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe */ Ternary, Integer>, List, Map> getTechnicallyCompatibleHosts( final VirtualMachine vm, + final Host srcHost, final Long startIndex, final Long pageSize, final String keyword) { @@ -1479,31 +1480,6 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe 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()); @@ -1517,10 +1493,12 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } } + boolean canMigrateWithStorage = isStorageMigrationSupported(vm, srcHost); if (!canMigrateWithStorage && usesLocal) { throw new InvalidParameterValueException("Unsupported operation, instance uses Local storage, cannot migrate"); } + final String srcHostVersion = getHypervisorVersionOfHost(srcHost); final Type hostType = srcHost.getType(); Pair, Integer> allHostsPair = null; List allHosts = null; @@ -1596,6 +1574,23 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe 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. @@ -1692,9 +1687,19 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe 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(); @@ -1707,9 +1712,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } // 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); @@ -1723,6 +1726,14 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe return new Ternary<>(otherHosts, suitableHosts, requiresStorageMotion); } + 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 da2005f6136..2f3e97716a0 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -20,6 +20,7 @@ import com.cloud.api.ApiDBUtils; 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.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; @@ -227,7 +228,7 @@ 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<>()); } @After @@ -246,7 +247,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); @@ -826,9 +827,13 @@ public class ManagementServerImplTest { @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())) @@ -888,7 +893,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify storage motion capability was checked - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.VMware, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); // Verify result structure and data Assert.assertNotNull(result); @@ -952,7 +957,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify hypervisor capabilities were checked - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.KVM, ""); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); // Verify result contains expected hosts Assert.assertNotNull(result); @@ -1097,7 +1102,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify KVM null version was converted to empty string - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.KVM, ""); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); // Verify result data Assert.assertNotNull(result); @@ -1416,7 +1421,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify storage motion capability was checked for User VM - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.VMware, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); // Verify response data Assert.assertNotNull(result); @@ -1481,7 +1486,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify hypervisor is in supported hypervisors list - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(hypervisorType, version); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(hypervisorType, version); // Verify validation passed for this hypervisor Assert.assertNotNull("Result should not be null for " + hypervisorType, result); @@ -1508,8 +1513,6 @@ public class ManagementServerImplTest { 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(null); spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); @@ -1589,7 +1592,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify that storage motion capability was checked for system VM (VMware is in hypervisorTypes list) - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.VMware, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); // Verify response structure Assert.assertNotNull(result); @@ -1642,7 +1645,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify User VM can migrate with storage (User VM type always checks) - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.KVM, ""); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.KVM, ""); // Verify response data Assert.assertNotNull(result); @@ -1695,7 +1698,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify XenServer without storage motion was checked - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.XenServer, null); + 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), @@ -1845,14 +1848,14 @@ public class ManagementServerImplTest { 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); + .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); + .thenReturn(true); ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); Mockito.when(offeringDao.findById(vm.getId(), vm.getServiceOfferingId())).thenReturn(offering); @@ -1880,7 +1883,7 @@ public class ManagementServerImplTest { spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); // Verify VMware always checks storage motion (hypervisorTypes list includes VMware) - Mockito.verify(hypervisorCapabilitiesDao).isStorageMotionSupported(HypervisorType.VMware, null); + Mockito.verify(hypervisorCapabilitiesDao, Mockito.atLeastOnce()).isStorageMotionSupported(HypervisorType.VMware, null); // Verify response Assert.assertNotNull(result); @@ -2074,4 +2077,32 @@ public class ManagementServerImplTest { 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()); + } } From 8225668688d15b776182d3849cd2c94167678bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= <89930804+erikbocks@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:53:11 -0300 Subject: [PATCH 2/5] Fix QEMU convert command timeout for incremental snapshots (#13212) --- .../hypervisor/kvm/storage/KVMStorageProcessor.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 b5e5f5939dc..bc82744dd85 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 @@ -2092,23 +2092,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); From e8df87e89be3a1834e163c01ef395fe220a72027 Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Fri, 10 Jul 2026 21:02:07 +0200 Subject: [PATCH 3/5] flasharray: authenticate via REST 2.x api-token and discover API version (#13060) Signed-off-by: Eugenio Grosso Co-authored-by: Eugenio Grosso --- .../adapter/flasharray/FlashArrayAdapter.java | 184 +++++++++++++----- 1 file changed, 136 insertions(+), 48 deletions(-) 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 dd554af36fe..3ffbeb1f9a3 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; @@ -588,6 +595,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()) { @@ -662,9 +754,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; } } @@ -731,72 +825,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); } } } @@ -964,7 +1052,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) { @@ -1019,7 +1107,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 { @@ -1061,7 +1149,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 From 63c142be261753314bb8d33446f20ab553aefec9 Mon Sep 17 00:00:00 2001 From: Nikolaus Eppinger Date: Mon, 13 Jul 2026 09:42:40 +0200 Subject: [PATCH 4/5] KVM: fix LUKS/volume-encryption detection for qemu-img >= 10.1.0 (#13587) qemu-img 10.1.0 changed the "qemu-img --help" supported-formats header from "Supported formats:" to "Supported image formats:". The regex in QemuImg.helpSupportsImageFormat() only matched the old header, so hostSupportsVolumeEncryption() returned false on affected hosts even though cryptsetup and the luks format were both available, blocking encrypted offerings. Make the "image" keyword optional in the regex so it matches both the legacy and current qemu-img help output. --- .../org/apache/cloudstack/utils/qemu/QemuImg.java | 5 ++++- .../apache/cloudstack/utils/qemu/QemuImgTest.java | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) 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 5a027425776..15f6785c1fd 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"; From 17e5947a6d2cea3ced8d892e13d04b4e838f8d8e Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 13 Jul 2026 11:00:37 +0200 Subject: [PATCH 5/5] server: add removed Tests for listHostsForMigrationOfVM and fix test failures --- .../server/ManagementServerImplTest.java | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index b0f274e6fc8..451e30005cf 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -43,8 +43,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; @@ -59,6 +61,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; @@ -69,24 +72,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; @@ -97,14 +121,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; @@ -156,6 +184,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; @@ -181,6 +245,12 @@ public class ManagementServerImplTest { @Mock HostAllocator hostAllocator; + @Mock + VgpuProfileDao vgpuProfileDao; + + @Mock + VgpuProfileVO vgpuProfileVO; + private AutoCloseable closeable; private MockedStatic apiDBUtilsMock; @@ -203,6 +273,9 @@ public class ManagementServerImplTest { // Return empty list to avoid architecture filtering in most tests apiDBUtilsMock.when(() -> ApiDBUtils.listZoneClustersArchs(Mockito.anyLong())) .thenReturn(new ArrayList<>()); + + when(vgpuProfileDao.findById(any())).thenReturn(vgpuProfileVO); + when(vgpuProfileVO.getName()).thenReturn("test-vgpu-profile"); } @After @@ -1067,7 +1140,7 @@ public class ManagementServerImplTest { mockRunningVM(1L, HypervisorType.KVM); Account caller = Mockito.mock(Account.class); Mockito.doReturn(caller).when(spy).getCaller(); - Mockito.when(_accountMgr.isRootAdmin(caller.getId())).thenReturn(false); + Mockito.when(accountManager.isRootAdmin(caller.getId())).thenReturn(false); spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); } @@ -1127,7 +1200,7 @@ public class ManagementServerImplTest { Mockito.when(serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString())) .thenReturn(Mockito.mock(com.cloud.service.ServiceOfferingDetailsVO.class)); - Ternary, Integer>, List, java.util.Map> result = + Ternary, Integer>, List, Map> result = spy.listHostsForMigrationOfVM(1L, 0L, 20L, null); Assert.assertNotNull(result); @@ -2327,7 +2400,7 @@ public class ManagementServerImplTest { private Account mockRootAdminAccount() { Account account = Mockito.mock(Account.class); Mockito.when(account.getId()).thenReturn(1L); - Mockito.when(_accountMgr.isRootAdmin(1L)).thenReturn(true); + Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(true); return account; }