This is an automated email from the ASF dual-hosted git repository.

weizhouapache pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.20 by this push:
     new a4b102fb3f9 Fix skip DRS for a VM (#12994)
a4b102fb3f9 is described below

commit a4b102fb3f99cfe45e3aab813d43e161efe59866
Author: Vishesh <[email protected]>
AuthorDate: Thu Jul 9 18:25:54 2026 +0530

    Fix skip DRS for a VM (#12994)
---
 .../resourcedetail/ResourceDetailsDao.java         |  2 +
 .../resourcedetail/ResourceDetailsDaoBase.java     | 14 +++++++
 .../cloudstack/cluster/ClusterDrsServiceImpl.java  | 34 ++++++++--------
 .../cluster/ClusterDrsServiceImplTest.java         | 46 ++++++++++++----------
 4 files changed, 57 insertions(+), 39 deletions(-)

diff --git 
a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java
 
b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java
index 6b6fe200c10..e3241731f71 100644
--- 
a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java
+++ 
b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java
@@ -103,4 +103,6 @@ public interface ResourceDetailsDao<R extends 
ResourceDetail> extends GenericDao
     long batchExpungeForResources(List<Long> ids, Long batchSize);
 
     String getActualValue(ResourceDetail resourceDetail);
+
+    List<R> listDetailsForResourceIdsAndKey(List<Long> resourceIds, String 
key);
 }
diff --git 
a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java
 
b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java
index 29d3f88fd90..74c4f7bcfe0 100644
--- 
a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java
+++ 
b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java
@@ -16,11 +16,13 @@
 // under the License.
 package org.apache.cloudstack.resourcedetail;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import com.cloud.utils.StringUtils;
 import org.apache.commons.collections.CollectionUtils;
 
 import com.cloud.utils.crypt.DBEncryptionUtil;
@@ -47,6 +49,7 @@ public abstract class ResourceDetailsDaoBase<R extends 
ResourceDetail> extends G
     public ResourceDetailsDaoBase() {
         AllFieldsSearch = createSearchBuilder();
         AllFieldsSearch.and("resourceId", 
AllFieldsSearch.entity().getResourceId(), SearchCriteria.Op.EQ);
+        AllFieldsSearch.and("resourceIdIn", 
AllFieldsSearch.entity().getResourceId(), SearchCriteria.Op.IN);
         AllFieldsSearch.and("name", AllFieldsSearch.entity().getName(), 
SearchCriteria.Op.EQ);
         AllFieldsSearch.and("value", AllFieldsSearch.entity().getValue(), 
SearchCriteria.Op.EQ);
         // FIXME SnapshotDetailsVO doesn't have a display field
@@ -246,4 +249,15 @@ public abstract class ResourceDetailsDaoBase<R extends 
ResourceDetail> extends G
         }
         return resourceDetail.getValue();
     }
+
+    @Override
+    public List<R> listDetailsForResourceIdsAndKey(List<Long> resourceIds, 
String key) {
+        if (CollectionUtils.isEmpty(resourceIds) || StringUtils.isBlank(key)) {
+            return Collections.emptyList();
+        }
+        SearchCriteria<R> sc = AllFieldsSearch.create();
+        sc.setParameters("name", key);
+        sc.setParameters("resourceIdIn", resourceIds.toArray());
+        return search(sc, null);
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java 
b/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java
index 7e1011ff931..fbfed03aa1e 100644
--- 
a/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java
+++ 
b/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java
@@ -51,11 +51,13 @@ import com.cloud.utils.db.GlobalLock;
 import com.cloud.utils.db.Transaction;
 import com.cloud.utils.db.TransactionCallback;
 import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.UserVmDetailVO;
 import com.cloud.vm.VMInstanceVO;
 import com.cloud.vm.VirtualMachine;
 import com.cloud.vm.VirtualMachineProfile;
 import com.cloud.vm.VirtualMachineProfileImpl;
 import com.cloud.vm.VmDetailConstants;
+import com.cloud.vm.dao.UserVmDetailsDao;
 import com.cloud.vm.dao.VMInstanceDao;
 import org.apache.cloudstack.api.ApiCommandResourceType;
 import org.apache.cloudstack.api.ApiConstants;
@@ -78,7 +80,6 @@ import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
 import org.apache.cloudstack.jobs.JobInfo;
 import org.apache.cloudstack.managed.context.ManagedContextTimerTask;
 import org.apache.commons.collections.CollectionUtils;
-import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.time.DateUtils;
 
 import javax.inject.Inject;
@@ -134,6 +135,9 @@ public class ClusterDrsServiceImpl extends ManagerBase 
implements ClusterDrsServ
     @Inject
     ServiceOfferingDao serviceOfferingDao;
 
+    @Inject
+    UserVmDetailsDao userVmDetailsDao;
+
     @Inject
     ManagementServer managementServer;
 
@@ -475,12 +479,16 @@ public class ClusterDrsServiceImpl extends ManagerBase 
implements ClusterDrsServ
         Map<Long, List<? extends Host>> vmToCompatibleHostsCache = new 
HashMap<>();
         Map<Long, Map<Host, Boolean>> vmToStorageMotionCache = new HashMap<>();
 
+        List<Long> vmIds = 
vmList.stream().map(VirtualMachine::getId).collect(Collectors.toList());
+        Set<Long> skipDrsVmIds = 
userVmDetailsDao.listDetailsForResourceIdsAndKey(vmIds, 
VmDetailConstants.SKIP_DRS)
+                .stream().filter(d -> "true".equalsIgnoreCase(d.getValue()))
+                .map(UserVmDetailVO::getResourceId)
+                .collect(Collectors.toSet());
+
         for (VirtualMachine vm : vmList) {
             // Skip ineligible VMs
-            if (vm.getType().isUsedBySystem() ||
-                vm.getState() != VirtualMachine.State.Running ||
-                (MapUtils.isNotEmpty(vm.getDetails()) &&
-                 
"true".equalsIgnoreCase(vm.getDetails().get(VmDetailConstants.SKIP_DRS)))) {
+            if (shouldSkipVMForDRS(vm, skipDrsVmIds)) {
+                logger.debug("Skipping VM {} for DRS as it is ineligible.", 
vm);
                 continue;
             }
 
@@ -607,7 +615,7 @@ public class ClusterDrsServiceImpl extends ManagerBase 
implements ClusterDrsServ
             ExcludeList excludes = vmToExcludesMap.get(vm.getId());
 
             ServiceOffering serviceOffering = 
vmIdServiceOfferingMap.get(vm.getId());
-            if (skipDrs(vm, compatibleHosts, serviceOffering)) {
+            if (CollectionUtils.isEmpty(compatibleHosts) || serviceOffering == 
null) {
                 continue;
             }
 
@@ -633,21 +641,11 @@ public class ClusterDrsServiceImpl extends ManagerBase 
implements ClusterDrsServ
         return bestMigration;
     }
 
-    private boolean skipDrs(VirtualMachine vm, List<? extends Host> 
compatibleHosts, ServiceOffering serviceOffering) {
+    private boolean shouldSkipVMForDRS(VirtualMachine vm, Set<Long> 
skipDrsVmIds) {
         if (vm.getType().isUsedBySystem() || vm.getState() != 
VirtualMachine.State.Running) {
             return true;
         }
-        if (MapUtils.isNotEmpty(vm.getDetails()) &&
-            
"true".equalsIgnoreCase(vm.getDetails().get(VmDetailConstants.SKIP_DRS))) {
-            return true;
-        }
-        if (CollectionUtils.isEmpty(compatibleHosts)) {
-            return true;
-        }
-        if (serviceOffering == null) {
-            return true;
-        }
-        return false;
+        return skipDrsVmIds.contains(vm.getId());
     }
 
     private Pair<double[], Map<Long, Integer>> 
getBaseMetricsArrayAndHostIdIndexMap(
diff --git 
a/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java
 
b/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java
index b76ef51c523..6dc73b7a630 100644
--- 
a/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java
+++ 
b/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java
@@ -40,9 +40,11 @@ import com.cloud.utils.Pair;
 import com.cloud.utils.Ternary;
 import com.cloud.utils.db.GlobalLock;
 import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.UserVmDetailVO;
 import com.cloud.vm.VMInstanceVO;
 import com.cloud.vm.VirtualMachine;
 import com.cloud.vm.VmDetailConstants;
+import com.cloud.vm.dao.UserVmDetailsDao;
 import com.cloud.vm.dao.VMInstanceDao;
 import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
 import 
org.apache.cloudstack.api.command.admin.cluster.GenerateClusterDrsPlanCmd;
@@ -121,6 +123,9 @@ public class ClusterDrsServiceImplTest {
     @Mock
     private AffinityGroupVMMapDao affinityGroupVMMapDao;
 
+    @Mock
+    private UserVmDetailsDao userVmDetailsDao;
+
     @Spy
     @InjectMocks
     private ClusterDrsServiceImpl clusterDrsService = new 
ClusterDrsServiceImpl();
@@ -294,6 +299,8 @@ public class ClusterDrsServiceImplTest {
 
         List<Ternary<VirtualMachine, Host, Host>> result = 
clusterDrsService.getDrsPlan(cluster, 5);
         assertEquals(0, result.size());
+        Mockito.verify(managementServer, 
Mockito.never()).listHostsForMigrationOfVM(
+                Mockito.eq(systemVm), Mockito.anyLong(), Mockito.anyLong(), 
Mockito.any(), Mockito.anyList());
     }
 
     @Test
@@ -334,6 +341,8 @@ public class ClusterDrsServiceImplTest {
 
         List<Ternary<VirtualMachine, Host, Host>> result = 
clusterDrsService.getDrsPlan(cluster, 5);
         assertEquals(0, result.size());
+        Mockito.verify(managementServer, 
Mockito.never()).listHostsForMigrationOfVM(
+                Mockito.eq(stoppedVm), Mockito.anyLong(), Mockito.anyLong(), 
Mockito.any(), Mockito.anyList());
     }
 
     @Test
@@ -350,9 +359,6 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(skippedVm.getHostId()).thenReturn(1L);
         Mockito.when(skippedVm.getType()).thenReturn(VirtualMachine.Type.User);
         
Mockito.when(skippedVm.getState()).thenReturn(VirtualMachine.State.Running);
-        Map<String, String> details = new HashMap<>();
-        details.put(VmDetailConstants.SKIP_DRS, "true");
-        Mockito.when(skippedVm.getDetails()).thenReturn(details);
 
         List<HostVO> hostList = new ArrayList<>();
         hostList.add(host1);
@@ -370,6 +376,11 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
         Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
 
+        // Return the SKIP_DRS detail for skippedVm so the flag is actually 
honoured
+        UserVmDetailVO skipDrsDetail = new UserVmDetailVO(1L, 
VmDetailConstants.SKIP_DRS, "true", true);
+        
Mockito.when(userVmDetailsDao.listDetailsForResourceIdsAndKey(Mockito.anyList(),
+                
Mockito.eq(VmDetailConstants.SKIP_DRS))).thenReturn(List.of(skipDrsDetail));
+
         Mockito.when(hostDao.findByClusterId(1L)).thenReturn(hostList);
         Mockito.when(vmInstanceDao.listByClusterId(1L)).thenReturn(vmList);
         Mockito.when(balancedAlgorithm.needsDrs(Mockito.any(), 
Mockito.anyList(), Mockito.anyList())).thenReturn(true);
@@ -377,6 +388,9 @@ public class ClusterDrsServiceImplTest {
 
         List<Ternary<VirtualMachine, Host, Host>> result = 
clusterDrsService.getDrsPlan(cluster, 5);
         assertEquals(0, result.size());
+        // Verify the VM was skipped before any host-compatibility lookup was 
attempted
+        Mockito.verify(managementServer, 
Mockito.never()).listHostsForMigrationOfVM(
+                Mockito.eq(skippedVm), Mockito.anyLong(), Mockito.anyLong(), 
Mockito.any(), Mockito.anyList());
     }
 
     @Test
@@ -393,7 +407,6 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(vm1.getHostId()).thenReturn(1L);
         Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
         Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());
 
         List<HostVO> hostList = new ArrayList<>();
         hostList.add(host1);
@@ -418,6 +431,10 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(balancedAlgorithm.needsDrs(Mockito.any(), 
Mockito.anyList(), Mockito.anyList())).thenReturn(true);
         
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), 
Mockito.anyLong())).thenReturn(serviceOffering);
         
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1));
+        // Return a Ternary with an empty suitable-hosts list to exercise the 
"no compatible hosts" path
+        
Mockito.when(managementServer.listHostsForMigrationOfVM(Mockito.eq(vm1), 
Mockito.anyLong(),
+                Mockito.anyLong(), Mockito.any(), Mockito.anyList()))
+                .thenReturn(new Ternary<>(new Pair<>(Collections.emptyList(), 
0), Collections.emptyList(), Collections.emptyMap()));
 
         List<Ternary<VirtualMachine, Host, Host>> result = 
clusterDrsService.getDrsPlan(cluster, 5);
         assertEquals(0, result.size());
@@ -438,7 +455,6 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(vm1.getHostId()).thenReturn(1L);
         Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
         Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());
 
         List<HostVO> hostList = new ArrayList<>();
         hostList.add(host1);
@@ -463,6 +479,10 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(balancedAlgorithm.needsDrs(Mockito.any(), 
Mockito.anyList(), Mockito.anyList())).thenReturn(true);
         
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), 
Mockito.anyLong())).thenReturn(serviceOffering);
         
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1));
+        // Throw an explicit exception so the catch-and-log path is exercised 
intentionally
+        
Mockito.when(managementServer.listHostsForMigrationOfVM(Mockito.eq(vm1), 
Mockito.anyLong(),
+                Mockito.anyLong(), Mockito.any(), Mockito.anyList()))
+                .thenThrow(new RuntimeException("Simulated host compatibility 
check failure"));
 
         List<Ternary<VirtualMachine, Host, Host>> result = 
clusterDrsService.getDrsPlan(cluster, 5);
         assertEquals(0, result.size());
@@ -484,7 +504,6 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(vm1.getHostId()).thenReturn(1L);
         Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
         Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());
 
         List<HostVO> hostList = new ArrayList<>();
         hostList.add(host1);
@@ -539,14 +558,12 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(vm1.getHostId()).thenReturn(1L);
         Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
         Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());
 
         VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
         Mockito.when(vm2.getId()).thenReturn(2L);
         Mockito.when(vm2.getHostId()).thenReturn(1L);
         Mockito.when(vm2.getType()).thenReturn(VirtualMachine.Type.User);
         Mockito.when(vm2.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm2.getDetails()).thenReturn(Collections.emptyMap());
 
         List<HostVO> hostList = new ArrayList<>();
         hostList.add(host1);
@@ -619,7 +636,6 @@ public class ClusterDrsServiceImplTest {
         Mockito.when(vm1.getHostId()).thenReturn(1L);
         Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
         Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());
 
         List<HostVO> hostList = new ArrayList<>();
         hostList.add(host1);
@@ -811,15 +827,9 @@ public class ClusterDrsServiceImplTest {
 
         VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
         Mockito.when(vm1.getId()).thenReturn(1L);
-        Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
-        Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());
 
         VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
         Mockito.when(vm2.getId()).thenReturn(2L);
-        Mockito.when(vm2.getType()).thenReturn(VirtualMachine.Type.User);
-        Mockito.when(vm2.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm2.getDetails()).thenReturn(Collections.emptyMap());
 
         List<VirtualMachine> vmList = new ArrayList<>();
         vmList.add(vm1);
@@ -890,15 +900,9 @@ public class ClusterDrsServiceImplTest {
 
         VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
         Mockito.when(vm1.getId()).thenReturn(1L);
-        Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
-        Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());
 
         VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
         Mockito.when(vm2.getId()).thenReturn(2L);
-        Mockito.when(vm2.getType()).thenReturn(VirtualMachine.Type.User);
-        Mockito.when(vm2.getState()).thenReturn(VirtualMachine.State.Running);
-        Mockito.when(vm2.getDetails()).thenReturn(Collections.emptyMap());
 
         List<VirtualMachine> vmList = new ArrayList<>();
         vmList.add(vm1);

Reply via email to