Optimize drs plan generation (#12014)

This commit is contained in:
Vishesh 2025-12-10 17:54:39 +05:30 committed by GitHub
parent ba52db9b3e
commit 4348386970
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 2550 additions and 180 deletions

View File

@ -71,6 +71,8 @@ import com.cloud.alert.Alert;
import com.cloud.capacity.Capacity;
import com.cloud.dc.Pod;
import com.cloud.dc.Vlan;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.ManagementServerException;
import com.cloud.exception.ResourceUnavailableException;
@ -91,6 +93,7 @@ import com.cloud.utils.Ternary;
import com.cloud.vm.InstanceGroup;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachine.Type;
import com.cloud.vm.VirtualMachineProfile;
/**
* Hopefully this is temporary.
@ -452,6 +455,19 @@ public interface ManagementService {
Ternary<Pair<List<? extends Host>, Integer>, List<? extends Host>, Map<Host, Boolean>> listHostsForMigrationOfVM(VirtualMachine vm, Long startIndex, Long pageSize, String keyword, List<VirtualMachine> vmList);
/**
* Apply affinity group constraints and other exclusion rules for VM migration.
* This is a helper method that can be used independently for per-iteration affinity checks in DRS.
*
* @param vm The virtual machine to migrate
* @param vmProfile The VM profile
* @param plan The deployment plan
* @param vmList List of VMs with current/simulated placements for affinity processing
* @return ExcludeList containing hosts to avoid
*/
ExcludeList applyAffinityConstraints(VirtualMachine vm, VirtualMachineProfile vmProfile,
DeploymentPlan plan, List<VirtualMachine> vmList);
/**
* List storage pools for live migrating of a volume. The API returns list of all pools in the cluster to which the
* volume can be migrated. Current pool is not included in the list. In case of vSphere datastore cluster storage pools,

View File

@ -22,10 +22,10 @@ package org.apache.cloudstack.cluster;
import com.cloud.host.Host;
import com.cloud.offering.ServiceOffering;
import com.cloud.org.Cluster;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.component.Adapter;
import com.cloud.vm.VirtualMachine;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
@ -40,6 +40,9 @@ import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsMetricUs
public interface ClusterDrsAlgorithm extends Adapter {
Mean MEAN_CALCULATOR = new Mean();
StandardDeviation STDDEV_CALCULATOR = new StandardDeviation(false);
/**
* Determines whether a DRS operation is needed for a given cluster and host-VM
* mapping.
@ -59,79 +62,121 @@ public interface ClusterDrsAlgorithm extends Adapter {
boolean needsDrs(Cluster cluster, List<Ternary<Long, Long, Long>> cpuList,
List<Ternary<Long, Long, Long>> memoryList) throws ConfigurationException;
/**
* Determines the metrics for a given virtual machine and destination host in a DRS cluster.
*
* @param clusterId
* the ID of the cluster to check
* @param vm
* the virtual machine to check
* @param serviceOffering
* the service offering for the virtual machine
* @param destHost
* the destination host for the virtual machine
* @param hostCpuMap
* a map of host IDs to the Ternary of used, reserved and total CPU on each host
* @param hostMemoryMap
* a map of host IDs to the Ternary of used, reserved and total memory on each host
* @param requiresStorageMotion
* whether storage motion is required for the virtual machine
* Calculates the metrics (improvement, cost, benefit) for migrating a VM to a destination host. Improvement is
* calculated based on the change in cluster imbalance before and after the migration.
*
* @param cluster the cluster to check
* @param vm the virtual machine to check
* @param serviceOffering the service offering for the virtual machine
* @param destHost the destination host for the virtual machine
* @param hostCpuMap a map of host IDs to the Ternary of used, reserved and total CPU on each host
* @param hostMemoryMap a map of host IDs to the Ternary of used, reserved and total memory on each host
* @param requiresStorageMotion whether storage motion is required for the virtual machine
* @param preImbalance the pre-calculated cluster imbalance before migration (null to calculate it)
* @param baseMetricsArray pre-calculated array of all host metrics before migration
* @param hostIdToIndexMap mapping from host ID to index in the metrics array
* @return a ternary containing improvement, cost, benefit
*/
Ternary<Double, Double, Double> getMetrics(Cluster cluster, VirtualMachine vm, ServiceOffering serviceOffering,
Host destHost, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap,
Boolean requiresStorageMotion) throws ConfigurationException;
Boolean requiresStorageMotion, Double preImbalance,
double[] baseMetricsArray, Map<Long, Integer> hostIdToIndexMap) throws ConfigurationException;
/**
* Calculates the imbalance of the cluster after a virtual machine migration.
* Calculates the cluster imbalance after migrating a VM to a destination host.
*
* @param serviceOffering
* the service offering for the virtual machine
* @param vm
* the virtual machine being migrated
* @param destHost
* the destination host for the virtual machine
* @param hostCpuMap
* a map of host IDs to the Ternary of used, reserved and total CPU on each host
* @param hostMemoryMap
* a map of host IDs to the Ternary of used, reserved and total memory on each host
*
* @return a pair containing the CPU and memory imbalance of the cluster after the migration
* @param vm the virtual machine being migrated
* @param destHost the destination host for the virtual machine
* @param clusterId the cluster ID
* @param vmMetric the VM's resource consumption metric
* @param baseMetricsArray pre-calculated array of all host metrics before migration
* @param hostIdToIndexMap mapping from host ID to index in the metrics array
* @return the cluster imbalance after migration
*/
default Double getImbalancePostMigration(ServiceOffering serviceOffering, VirtualMachine vm,
Host destHost, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap) throws ConfigurationException {
Pair<Long, Map<Long, Ternary<Long, Long, Long>>> pair = getHostMetricsMapAndType(destHost.getClusterId(), serviceOffering, hostCpuMap, hostMemoryMap);
long vmMetric = pair.first();
Map<Long, Ternary<Long, Long, Long>> hostMetricsMap = pair.second();
default Double getImbalancePostMigration(VirtualMachine vm,
Host destHost, Long clusterId, long vmMetric, double[] baseMetricsArray,
Map<Long, Integer> hostIdToIndexMap, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap) {
// Create a copy of the base array and adjust only the two affected hosts
double[] adjustedMetrics = new double[baseMetricsArray.length];
System.arraycopy(baseMetricsArray, 0, adjustedMetrics, 0, baseMetricsArray.length);
List<Double> list = new ArrayList<>();
for (Long hostId : hostMetricsMap.keySet()) {
list.add(getMetricValuePostMigration(destHost.getClusterId(), hostMetricsMap.get(hostId), vmMetric, hostId, destHost.getId(), vm.getHostId()));
long destHostId = destHost.getId();
long vmHostId = vm.getHostId();
// Adjust source host (remove VM resources)
Integer sourceIndex = hostIdToIndexMap.get(vmHostId);
if (sourceIndex != null && sourceIndex < adjustedMetrics.length) {
Map<Long, Ternary<Long, Long, Long>> sourceMetricsMap = getClusterDrsMetric(clusterId).equals("cpu") ? hostCpuMap : hostMemoryMap;
Ternary<Long, Long, Long> sourceMetrics = sourceMetricsMap.get(vmHostId);
if (sourceMetrics != null) {
adjustedMetrics[sourceIndex] = getMetricValuePostMigration(clusterId, sourceMetrics, vmMetric, vmHostId, destHostId, vmHostId);
}
}
return getImbalance(list);
// Adjust destination host (add VM resources)
Integer destIndex = hostIdToIndexMap.get(destHostId);
if (destIndex != null && destIndex < adjustedMetrics.length) {
Map<Long, Ternary<Long, Long, Long>> destMetricsMap = getClusterDrsMetric(clusterId).equals("cpu") ? hostCpuMap : hostMemoryMap;
Ternary<Long, Long, Long> destMetrics = destMetricsMap.get(destHostId);
if (destMetrics != null) {
adjustedMetrics[destIndex] = getMetricValuePostMigration(clusterId, destMetrics, vmMetric, destHostId, destHostId, vmHostId);
}
}
return calculateImbalance(adjustedMetrics);
}
private Pair<Long, Map<Long, Ternary<Long, Long, Long>>> getHostMetricsMapAndType(Long clusterId,
ServiceOffering serviceOffering, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap) throws ConfigurationException {
/**
* Calculate imbalance from an array of metric values.
* Imbalance is defined as standard deviation divided by mean.
*
* Uses reusable stateless calculator objects to avoid object creation overhead.
* @param values array of metric values
* @return calculated imbalance
*/
private static double calculateImbalance(double[] values) {
if (values == null || values.length == 0) {
return 0.0;
}
double mean = MEAN_CALCULATOR.evaluate(values);
if (mean == 0.0) {
return 0.0; // Avoid division by zero
}
double stdDev = STDDEV_CALCULATOR.evaluate(values, mean);
return stdDev / mean;
}
/**
* Helper method to get VM metric based on cluster configuration.
*/
static long getVmMetric(ServiceOffering serviceOffering, Long clusterId) throws ConfigurationException {
String metric = getClusterDrsMetric(clusterId);
Pair<Long, Map<Long, Ternary<Long, Long, Long>>> pair;
switch (metric) {
case "cpu":
pair = new Pair<>((long) serviceOffering.getCpu() * serviceOffering.getSpeed(), hostCpuMap);
break;
return (long) serviceOffering.getCpu() * serviceOffering.getSpeed();
case "memory":
pair = new Pair<>(serviceOffering.getRamSize() * 1024L * 1024L, hostMemoryMap);
break;
return serviceOffering.getRamSize() * 1024L * 1024L;
default:
throw new ConfigurationException(
String.format("Invalid metric: %s for cluster: %d", metric, clusterId));
}
return pair;
}
/**
* Helper method to calculate metrics from pre and post imbalance values.
*/
default Ternary<Double, Double, Double> calculateMetricsFromImbalances(Double preImbalance, Double postImbalance) {
// This needs more research to determine the cost and benefit of a migration
// TODO: Cost should be a factor of the VM size and the host capacity
// TODO: Benefit should be a factor of the VM size and the host capacity and the number of VMs on the host
final double improvement = preImbalance - postImbalance;
final double cost = 0.0;
final double benefit = 1.0;
return new Ternary<>(improvement, cost, benefit);
}
private Double getMetricValuePostMigration(Long clusterId, Ternary<Long, Long, Long> metrics, long vmMetric,
@ -151,9 +196,26 @@ public interface ClusterDrsAlgorithm extends Adapter {
}
private static Double getImbalance(List<Double> metricList) {
Double clusterMeanMetric = getClusterMeanMetric(metricList);
Double clusterStandardDeviation = getClusterStandardDeviation(metricList, clusterMeanMetric);
return clusterStandardDeviation / clusterMeanMetric;
if (CollectionUtils.isEmpty(metricList)) {
return 0.0;
}
// Convert List<Double> to double[] once, avoiding repeated conversions
double[] values = new double[metricList.size()];
int index = 0;
for (Double value : metricList) {
if (value != null) {
values[index++] = value;
}
}
// Trim array if some values were null
if (index < values.length) {
double[] trimmed = new double[index];
System.arraycopy(values, 0, trimmed, 0, index);
values = trimmed;
}
return calculateImbalance(values);
}
static String getClusterDrsMetric(long clusterId) {
@ -181,36 +243,6 @@ public interface ClusterDrsAlgorithm extends Adapter {
return null;
}
/**
* Mean is the average of a collection or set of metrics. In context of a DRS
* cluster, the cluster metrics defined as the average metrics value for some
* metric (such as CPU, memory etc.) for every resource such as host.
* Cluster Mean Metric, mavg = (mi) / N, where mi is a measurable metric for a
* resource i in a cluster with total N number of resources.
*/
static Double getClusterMeanMetric(List<Double> metricList) {
return new Mean().evaluate(metricList.stream().mapToDouble(i -> i).toArray());
}
/**
* Standard deviation is defined as the square root of the absolute squared sum
* of difference of a metric from its mean for every resource divided by the
* total number of resources. In context of the DRS, the cluster standard
* deviation is the standard deviation based on a metric of resources in a
* cluster such as for the allocation or utilisation CPU/memory metric of hosts
* in a cluster.
* Cluster Standard Deviation, σc = sqrt((mimavg^2) / N), where mavg is the
* mean metric value and mi is a measurable metric for some resource i in the
* cluster with total N number of resources.
*/
static Double getClusterStandardDeviation(List<Double> metricList, Double mean) {
if (mean != null) {
return new StandardDeviation(false).evaluate(metricList.stream().mapToDouble(i -> i).toArray(), mean);
} else {
return new StandardDeviation(false).evaluate(metricList.stream().mapToDouble(i -> i).toArray());
}
}
static boolean getDrsMetricUseRatio(long clusterId) {
return ClusterDrsMetricUseRatio.valueIn(clusterId);
}

View File

@ -68,23 +68,26 @@ public class Balanced extends AdapterBase implements ClusterDrsAlgorithm {
return "balanced";
}
@Override
public Ternary<Double, Double, Double> getMetrics(Cluster cluster, VirtualMachine vm,
ServiceOffering serviceOffering, Host destHost,
Map<Long, Ternary<Long, Long, Long>> hostCpuMap, Map<Long, Ternary<Long, Long, Long>> hostMemoryMap,
Boolean requiresStorageMotion) throws ConfigurationException {
Double preImbalance = ClusterDrsAlgorithm.getClusterImbalance(cluster.getId(), new ArrayList<>(hostCpuMap.values()), new ArrayList<>(hostMemoryMap.values()), null);
Double postImbalance = getImbalancePostMigration(serviceOffering, vm, destHost, hostCpuMap, hostMemoryMap);
Boolean requiresStorageMotion, Double preImbalance,
double[] baseMetricsArray, Map<Long, Integer> hostIdToIndexMap) throws ConfigurationException {
// Use provided pre-imbalance if available, otherwise calculate it
if (preImbalance == null) {
preImbalance = ClusterDrsAlgorithm.getClusterImbalance(cluster.getId(), new ArrayList<>(hostCpuMap.values()), new ArrayList<>(hostMemoryMap.values()), null);
}
logger.debug("Cluster {} pre-imbalance: {} post-imbalance: {} Algorithm: {} VM: {} srcHost: {} destHost: {}",
// Use optimized post-imbalance calculation that adjusts only affected hosts
Double postImbalance = getImbalancePostMigration(vm, destHost,
cluster.getId(), ClusterDrsAlgorithm.getVmMetric(serviceOffering, cluster.getId()),
baseMetricsArray, hostIdToIndexMap, hostCpuMap, hostMemoryMap);
logger.trace("Cluster {} pre-imbalance: {} post-imbalance: {} Algorithm: {} VM: {} srcHost ID: {} destHost: {}",
cluster, preImbalance, postImbalance, getName(), vm, vm.getHostId(), destHost);
// This needs more research to determine the cost and benefit of a migration
// TODO: Cost should be a factor of the VM size and the host capacity
// TODO: Benefit should be a factor of the VM size and the host capacity and the number of VMs on the host
final double improvement = preImbalance - postImbalance;
final double cost = 0.0;
final double benefit = 1.0;
return new Ternary<>(improvement, cost, benefit);
return calculateMetricsFromImbalances(preImbalance, postImbalance);
}
}

View File

@ -43,6 +43,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getClusterDrsMetric;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getClusterImbalance;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getMetricValue;
import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsImbalanceThreshold;
import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsMetric;
import static org.junit.Assert.assertEquals;
@ -119,6 +122,48 @@ public class BalancedTest {
closeable.close();
}
/**
* Helper method to prepare metrics data for getMetrics calls with optimized signature.
* Calculates pre-imbalance and builds baseMetricsArray and hostIdToIndexMap.
*
* @return a Ternary containing preImbalance, baseMetricsArray, and hostIdToIndexMap
*/
private Ternary<Double, double[], Map<Long, Integer>> prepareMetricsData() throws ConfigurationException {
// Calculate pre-imbalance
Double preImbalance = getClusterImbalance(clusterId, new ArrayList<>(hostCpuFreeMap.values()),
new ArrayList<>(hostMemoryFreeMap.values()), null);
// Build baseMetricsArray and hostIdToIndexMap
String metricType = getClusterDrsMetric(clusterId);
Map<Long, Ternary<Long, Long, Long>> baseMetricsMap = "cpu".equals(metricType) ? hostCpuFreeMap : hostMemoryFreeMap;
double[] baseMetricsArray = new double[baseMetricsMap.size()];
Map<Long, Integer> hostIdToIndexMap = new HashMap<>();
int index = 0;
for (Map.Entry<Long, Ternary<Long, Long, Long>> entry : baseMetricsMap.entrySet()) {
Long hostId = entry.getKey();
Ternary<Long, Long, Long> metrics = entry.getValue();
long used = metrics.first();
long actualTotal = metrics.third() - metrics.second();
long free = actualTotal - metrics.first();
Double metricValue = getMetricValue(clusterId, used, free, actualTotal, null);
if (metricValue != null) {
baseMetricsArray[index] = metricValue;
hostIdToIndexMap.put(hostId, index);
index++;
}
}
// Trim array if some values were null
if (index < baseMetricsArray.length) {
double[] trimmed = new double[index];
System.arraycopy(baseMetricsArray, 0, trimmed, 0, index);
baseMetricsArray = trimmed;
}
return new Ternary<>(preImbalance, baseMetricsArray, hostIdToIndexMap);
}
/**
* <b>needsDrs tests</b>
* <p>Scenarios to test for needsDrs
@ -183,8 +228,14 @@ public class BalancedTest {
@Test
public void getMetricsWithCpu() throws NoSuchFieldException, IllegalAccessException, ConfigurationException {
overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "cpu");
Ternary<Double, double[], Map<Long, Integer>> metricsData = prepareMetricsData();
Double preImbalance = metricsData.first();
double[] baseMetricsArray = metricsData.second();
Map<Long, Integer> hostIdToIndexMap = metricsData.third();
Ternary<Double, Double, Double> result = balanced.getMetrics(cluster, vm3, serviceOffering, destHost,
hostCpuFreeMap, hostMemoryFreeMap, false);
hostCpuFreeMap, hostMemoryFreeMap, false, preImbalance, baseMetricsArray, hostIdToIndexMap);
assertEquals(0.0, result.first(), 0.01);
assertEquals(0.0, result.second(), 0.0);
assertEquals(1.0, result.third(), 0.0);
@ -197,8 +248,14 @@ public class BalancedTest {
@Test
public void getMetricsWithMemory() throws NoSuchFieldException, IllegalAccessException, ConfigurationException {
overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "memory");
Ternary<Double, double[], Map<Long, Integer>> metricsData = prepareMetricsData();
Double preImbalance = metricsData.first();
double[] baseMetricsArray = metricsData.second();
Map<Long, Integer> hostIdToIndexMap = metricsData.third();
Ternary<Double, Double, Double> result = balanced.getMetrics(cluster, vm3, serviceOffering, destHost,
hostCpuFreeMap, hostMemoryFreeMap, false);
hostCpuFreeMap, hostMemoryFreeMap, false, preImbalance, baseMetricsArray, hostIdToIndexMap);
assertEquals(0.4, result.first(), 0.01);
assertEquals(0, result.second(), 0.0);
assertEquals(1, result.third(), 0.0);

View File

@ -75,20 +75,22 @@ public class Condensed extends AdapterBase implements ClusterDrsAlgorithm {
public Ternary<Double, Double, Double> getMetrics(Cluster cluster, VirtualMachine vm,
ServiceOffering serviceOffering, Host destHost,
Map<Long, Ternary<Long, Long, Long>> hostCpuMap, Map<Long, Ternary<Long, Long, Long>> hostMemoryMap,
Boolean requiresStorageMotion) throws ConfigurationException {
Double preImbalance = ClusterDrsAlgorithm.getClusterImbalance(cluster.getId(), new ArrayList<>(hostCpuMap.values()),
new ArrayList<>(hostMemoryMap.values()), null);
Double postImbalance = getImbalancePostMigration(serviceOffering, vm, destHost, hostCpuMap, hostMemoryMap);
Boolean requiresStorageMotion, Double preImbalance,
double[] baseMetricsArray, Map<Long, Integer> hostIdToIndexMap) throws ConfigurationException {
// Use provided pre-imbalance if available, otherwise calculate it
if (preImbalance == null) {
preImbalance = ClusterDrsAlgorithm.getClusterImbalance(cluster.getId(), new ArrayList<>(hostCpuMap.values()),
new ArrayList<>(hostMemoryMap.values()), null);
}
logger.debug("Cluster {} pre-imbalance: {} post-imbalance: {} Algorithm: {} VM: {} srcHost: {} destHost: {}",
// Use optimized post-imbalance calculation that adjusts only affected hosts
Double postImbalance = getImbalancePostMigration(vm, destHost,
cluster.getId(), ClusterDrsAlgorithm.getVmMetric(serviceOffering, cluster.getId()),
baseMetricsArray, hostIdToIndexMap, hostCpuMap, hostMemoryMap);
logger.trace("Cluster {} pre-imbalance: {} post-imbalance: {} Algorithm: {} VM: {} srcHost ID: {} destHost: {}",
cluster, preImbalance, postImbalance, getName(), vm, vm.getHostId(), destHost);
// This needs more research to determine the cost and benefit of a migration
// TODO: Cost should be a factor of the VM size and the host capacity
// TODO: Benefit should be a factor of the VM size and the host capacity and the number of VMs on the host
final double improvement = postImbalance - preImbalance;
final double cost = 0;
final double benefit = 1;
return new Ternary<>(improvement, cost, benefit);
return calculateMetricsFromImbalances(postImbalance, preImbalance);
}
}

View File

@ -43,6 +43,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getClusterDrsMetric;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getClusterImbalance;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getMetricValue;
import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsImbalanceThreshold;
import static org.apache.cloudstack.cluster.ClusterDrsService.ClusterDrsMetric;
import static org.junit.Assert.assertEquals;
@ -121,6 +124,48 @@ public class CondensedTest {
closeable.close();
}
/**
* Helper method to prepare metrics data for getMetrics calls with optimized signature.
* Calculates pre-imbalance and builds baseMetricsArray and hostIdToIndexMap.
*
* @return a Ternary containing preImbalance, baseMetricsArray, and hostIdToIndexMap
*/
private Ternary<Double, double[], Map<Long, Integer>> prepareMetricsData() throws ConfigurationException {
// Calculate pre-imbalance
Double preImbalance = getClusterImbalance(clusterId, new ArrayList<>(hostCpuFreeMap.values()),
new ArrayList<>(hostMemoryFreeMap.values()), null);
// Build baseMetricsArray and hostIdToIndexMap
String metricType = getClusterDrsMetric(clusterId);
Map<Long, Ternary<Long, Long, Long>> baseMetricsMap = "cpu".equals(metricType) ? hostCpuFreeMap : hostMemoryFreeMap;
double[] baseMetricsArray = new double[baseMetricsMap.size()];
Map<Long, Integer> hostIdToIndexMap = new HashMap<>();
int index = 0;
for (Map.Entry<Long, Ternary<Long, Long, Long>> entry : baseMetricsMap.entrySet()) {
Long hostId = entry.getKey();
Ternary<Long, Long, Long> metrics = entry.getValue();
long used = metrics.first();
long actualTotal = metrics.third() - metrics.second();
long free = actualTotal - metrics.first();
Double metricValue = getMetricValue(clusterId, used, free, actualTotal, null);
if (metricValue != null) {
baseMetricsArray[index] = metricValue;
hostIdToIndexMap.put(hostId, index);
index++;
}
}
// Trim array if some values were null
if (index < baseMetricsArray.length) {
double[] trimmed = new double[index];
System.arraycopy(baseMetricsArray, 0, trimmed, 0, index);
baseMetricsArray = trimmed;
}
return new Ternary<>(preImbalance, baseMetricsArray, hostIdToIndexMap);
}
/**
* <p>needsDrs tests
* <p>Scenarios to test for needsDrs
@ -185,8 +230,14 @@ public class CondensedTest {
@Test
public void getMetricsWithCpu() throws NoSuchFieldException, IllegalAccessException, ConfigurationException {
overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "cpu");
Ternary<Double, double[], Map<Long, Integer>> metricsData = prepareMetricsData();
Double preImbalance = metricsData.first();
double[] baseMetricsArray = metricsData.second();
Map<Long, Integer> hostIdToIndexMap = metricsData.third();
Ternary<Double, Double, Double> result = condensed.getMetrics(cluster, vm3, serviceOffering, destHost,
hostCpuFreeMap, hostMemoryFreeMap, false);
hostCpuFreeMap, hostMemoryFreeMap, false, preImbalance, baseMetricsArray, hostIdToIndexMap);
assertEquals(0.0, result.first(), 0.0);
assertEquals(0, result.second(), 0.0);
assertEquals(1, result.third(), 0.0);
@ -199,8 +250,14 @@ public class CondensedTest {
@Test
public void getMetricsWithMemory() throws NoSuchFieldException, IllegalAccessException, ConfigurationException {
overrideDefaultConfigValue(ClusterDrsMetric, "_defaultValue", "memory");
Ternary<Double, double[], Map<Long, Integer>> metricsData = prepareMetricsData();
Double preImbalance = metricsData.first();
double[] baseMetricsArray = metricsData.second();
Map<Long, Integer> hostIdToIndexMap = metricsData.third();
Ternary<Double, Double, Double> result = condensed.getMetrics(cluster, vm3, serviceOffering, destHost,
hostCpuFreeMap, hostMemoryFreeMap, false);
hostCpuFreeMap, hostMemoryFreeMap, false, preImbalance, baseMetricsArray, hostIdToIndexMap);
assertEquals(-0.4, result.first(), 0.01);
assertEquals(0, result.second(), 0.0);
assertEquals(1, result.third(), 0.0);

View File

@ -692,6 +692,7 @@ import com.cloud.dc.dao.HostPodDao;
import com.cloud.dc.dao.PodVlanMapDao;
import com.cloud.dc.dao.VlanDao;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.deploy.DeploymentPlanner;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.deploy.DeploymentPlanningManager;
@ -1454,17 +1455,27 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
return false;
}
@Override
public Ternary<Pair<List<? extends Host>, Integer>, List<? extends Host>, Map<Host, Boolean>> listHostsForMigrationOfVM(final VirtualMachine vm, final Long startIndex, final Long pageSize,
final String keyword, List<VirtualMachine> vmList) {
validateVmForHostMigration(vm);
/**
* Get technically compatible hosts for VM migration (storage, hypervisor, UEFI filtering).
* This determines which hosts are technically capable of hosting the VM based on
* storage requirements, hypervisor capabilities, and UEFI requirements.
*
* @param vm The virtual machine to migrate
* @param startIndex Starting index for pagination
* @param pageSize Page size for pagination
* @param keyword Keyword filter for host search
* @return Ternary containing: (all hosts with count, filtered compatible hosts, storage motion requirements map)
*/
Ternary<Pair<List<? extends Host>, Integer>, List<? extends Host>, Map<Host, Boolean>> getTechnicallyCompatibleHosts(
final VirtualMachine vm,
final Long startIndex,
final Long pageSize,
final String keyword) {
// GPU check
if (_serviceOfferingDetailsDao.findDetail(vm.getServiceOfferingId(), GPU.Keys.pciDevice.toString()) != null) {
logger.info(" Live Migration of GPU enabled VM : " + vm.getInstanceName() + " is not supported");
// Return empty list.
return new Ternary<>(new Pair<>(new ArrayList<>(), 0),
new ArrayList<>(), new HashMap<>());
logger.info("Live Migration of GPU enabled VM : {} is not supported", vm);
return new Ternary<>(new Pair<>(new ArrayList<>(), 0), new ArrayList<>(), new HashMap<>());
}
final long srcHostId = vm.getHostId();
@ -1478,6 +1489,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
ex.addProxyObject(vm.getUuid(), "vmId");
throw ex;
}
String srcHostVersion = srcHost.getHypervisorVersion();
if (HypervisorType.KVM.equals(srcHost.getHypervisorType()) && srcHostVersion == null) {
srcHostVersion = "";
@ -1513,7 +1525,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
List<HostVO> allHosts = null;
List<HostVO> filteredHosts = null;
final Map<Host, Boolean> requiresStorageMotion = new HashMap<>();
DataCenterDeployment plan = null;
if (canMigrateWithStorage) {
Long podId = !VirtualMachine.Type.User.equals(vm.getType()) ? srcHost.getPodId() : null;
allHostsPair = searchForServers(startIndex, pageSize, null, hostType, null, srcHost.getDataCenterId(), podId, null, null, keyword,
@ -1562,7 +1574,6 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
if (CollectionUtils.isEmpty(filteredHosts)) {
return new Ternary<>(new Pair<>(allHosts, allHostsPair.second()), new ArrayList<>(), new HashMap<>());
}
plan = new DataCenterDeployment(srcHost.getDataCenterId(), podId, null, null, null, null);
} else {
final Long cluster = srcHost.getClusterId();
if (logger.isDebugEnabled()) {
@ -1571,22 +1582,38 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
allHostsPair = searchForServers(startIndex, pageSize, null, hostType, null, null, null, cluster, null, keyword, null, null, null,
null, srcHost.getId());
allHosts = allHostsPair.first();
plan = new DataCenterDeployment(srcHost.getDataCenterId(), srcHost.getPodId(), srcHost.getClusterId(), null, null, null);
filteredHosts = allHosts;
}
final Pair<List<? extends Host>, Integer> otherHosts = new Pair<>(allHosts, allHostsPair.second());
final Pair<List<? extends Host>, Integer> allHostsPairResult = new Pair<>(allHosts, allHostsPair.second());
Pair<Boolean, List<HostVO>> uefiFilteredResult = filterUefiHostsForMigration(allHosts, filteredHosts, vm);
if (!uefiFilteredResult.first()) {
return new Ternary<>(otherHosts, new ArrayList<>(), new HashMap<>());
return new Ternary<>(allHostsPairResult, new ArrayList<>(), new HashMap<>());
}
filteredHosts = uefiFilteredResult.second();
List<Host> suitableHosts = new ArrayList<>();
return new Ternary<>(allHostsPairResult, filteredHosts, requiresStorageMotion);
}
/**
* Apply affinity group constraints and other exclusion rules for VM migration.
* This builds an ExcludeList based on affinity groups, DPDK requirements, and dedicated resources.
*
* @param vm The virtual machine to migrate
* @param vmProfile The VM profile
* @param plan The deployment plan
* @param vmList List of VMs with current/simulated placements for affinity processing
* @return ExcludeList containing hosts to avoid
*/
@Override
public ExcludeList applyAffinityConstraints(VirtualMachine vm, VirtualMachineProfile vmProfile, DeploymentPlan plan, List<VirtualMachine> vmList) {
final ExcludeList excludes = new ExcludeList();
excludes.addHost(srcHostId);
excludes.addHost(vm.getHostId());
if (dpdkHelper.isVMDpdkEnabled(vm.getId())) {
excludeNonDPDKEnabledHosts(plan, excludes);
if (plan instanceof DataCenterDeployment) {
excludeNonDPDKEnabledHosts((DataCenterDeployment) plan, excludes);
}
}
// call affinitygroup chain
@ -1599,13 +1626,37 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
}
if (vm.getType() == VirtualMachine.Type.User || vm.getType() == VirtualMachine.Type.DomainRouter) {
final DataCenterVO dc = _dcDao.findById(srcHost.getDataCenterId());
final DataCenterVO dc = _dcDao.findById(plan.getDataCenterId());
_dpMgr.checkForNonDedicatedResources(vmProfile, dc, excludes);
}
return excludes;
}
/**
* Get hosts with available capacity using host allocators, and apply architecture filtering.
*
* @param vm The virtual machine (for architecture filtering)
* @param vmProfile The VM profile
* @param plan The deployment plan
* @param compatibleHosts List of technically compatible hosts
* @param excludes ExcludeList with hosts to avoid
* @param srcHost Source host (for architecture filtering)
* @return List of suitable hosts with capacity
*/
protected List<Host> getCapableSuitableHosts(
final VirtualMachine vm,
final VirtualMachineProfile vmProfile,
final DataCenterDeployment plan,
final List<? extends Host> compatibleHosts,
final ExcludeList excludes,
final Host srcHost) {
List<Host> suitableHosts = new ArrayList<>();
for (final HostAllocator allocator : hostAllocators) {
if (CollectionUtils.isNotEmpty(filteredHosts)) {
suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, filteredHosts, HostAllocator.RETURN_UPTO_ALL, false);
if (CollectionUtils.isNotEmpty(compatibleHosts)) {
suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, compatibleHosts, HostAllocator.RETURN_UPTO_ALL, false);
} else {
suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, HostAllocator.RETURN_UPTO_ALL, false);
}
@ -1631,6 +1682,43 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
}
}
return suitableHosts;
}
@Override
public Ternary<Pair<List<? extends Host>, Integer>, List<? extends Host>, Map<Host, Boolean>> listHostsForMigrationOfVM(final VirtualMachine vm, final Long startIndex, final Long pageSize,
final String keyword, List<VirtualMachine> vmList) {
validateVmForHostMigration(vm);
// Get technically compatible hosts (storage, hypervisor, UEFI)
Ternary<Pair<List<? extends Host>, Integer>, List<? extends Host>, Map<Host, Boolean>> compatibilityResult =
getTechnicallyCompatibleHosts(vm, startIndex, pageSize, keyword);
Pair<List<? extends Host>, Integer> allHostsPair = compatibilityResult.first();
List<? extends Host> filteredHosts = compatibilityResult.second();
Map<Host, Boolean> requiresStorageMotion = compatibilityResult.third();
// If no compatible hosts, return early
if (CollectionUtils.isEmpty(filteredHosts)) {
final Pair<List<? extends Host>, Integer> otherHosts = new Pair<>(allHostsPair.first(), allHostsPair.second());
return new Ternary<>(otherHosts, new ArrayList<>(), requiresStorageMotion);
}
// 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 VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(
vm, null, _offeringDao.findById(vm.getId(), vm.getServiceOfferingId()), null, null);
// Apply affinity constraints
final ExcludeList excludes = applyAffinityConstraints(vm, vmProfile, plan, vmList);
// Get hosts with capacity
List<Host> suitableHosts = getCapableSuitableHosts(vm, vmProfile, plan, filteredHosts, excludes, srcHost);
final Pair<List<? extends Host>, Integer> otherHosts = new Pair<>(allHostsPair.first(), allHostsPair.second());
return new Ternary<>(otherHosts, suitableHosts, requiresStorageMotion);
}
@ -1923,7 +2011,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
return suitablePools;
}
private Pair<List<HostVO>, Integer> searchForServers(final Long startIndex, final Long pageSize, final Object name, final Object type,
Pair<List<HostVO>, Integer> searchForServers(final Long startIndex, final Long pageSize, final Object name, final Object type,
final Object state, final Object zone, final Object pod, final Object cluster, final Object id, final Object keyword,
final Object resourceState, final Object haHosts, final Object hypervisorType, final Object hypervisorVersion, final Object... excludes) {
final Filter searchFilter = new Filter(HostVO.class, "id", Boolean.TRUE, startIndex, pageSize);

View File

@ -24,6 +24,8 @@ import com.cloud.api.query.dao.HostJoinDao;
import com.cloud.api.query.vo.HostJoinVO;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.domain.Domain;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes;
@ -51,6 +53,8 @@ import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.exception.CloudRuntimeException;
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.VMInstanceDao;
import org.apache.cloudstack.api.ApiCommandResourceType;
@ -62,6 +66,8 @@ import org.apache.cloudstack.api.command.admin.vm.MigrateVMCmd;
import org.apache.cloudstack.api.response.ClusterDrsPlanMigrationResponse;
import org.apache.cloudstack.api.response.ClusterDrsPlanResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.affinity.AffinityGroupVMMapVO;
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
import org.apache.cloudstack.cluster.dao.ClusterDrsPlanDao;
import org.apache.cloudstack.cluster.dao.ClusterDrsPlanMigrationDao;
import org.apache.cloudstack.context.CallContext;
@ -71,6 +77,7 @@ import org.apache.cloudstack.framework.jobs.AsyncJobManager;
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;
@ -81,13 +88,18 @@ import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.stream.Collectors;
import static com.cloud.org.Grouping.AllocationState.Disabled;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getClusterImbalance;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getClusterDrsMetric;
import static org.apache.cloudstack.cluster.ClusterDrsAlgorithm.getMetricValue;
public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsService, PluggableService {
@ -125,6 +137,9 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ
@Inject
ManagementServer managementServer;
@Inject
AffinityGroupVMMapDao affinityGroupVMMapDao;
List<ClusterDrsAlgorithm> drsAlgorithms = new ArrayList<>();
Map<String, ClusterDrsAlgorithm> drsAlgorithmMap = new HashMap<>();
@ -318,19 +333,14 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ
* @throws ConfigurationException
* If there is an error in the DRS configuration.
*/
List<Ternary<VirtualMachine, Host, Host>> getDrsPlan(Cluster cluster,
int maxIterations) throws ConfigurationException {
List<Ternary<VirtualMachine, Host, Host>> migrationPlan = new ArrayList<>();
List<Ternary<VirtualMachine, Host, Host>> getDrsPlan(Cluster cluster, int maxIterations) throws ConfigurationException {
if (cluster.getAllocationState() == Disabled || maxIterations <= 0) {
return Collections.emptyList();
}
ClusterDrsAlgorithm algorithm = getDrsAlgorithm(ClusterDrsAlgorithm.valueIn(cluster.getId()));
List<HostVO> hostList = hostDao.findByClusterId(cluster.getId());
List<VirtualMachine> vmList = new ArrayList<>(vmInstanceDao.listByClusterId(cluster.getId()));
int iteration = 0;
Map<Long, Host> hostMap = hostList.stream().collect(Collectors.toMap(HostVO::getId, host -> host));
Map<Long, List<VirtualMachine>> hostVmMap = getHostVmMap(hostList, vmList);
@ -357,10 +367,39 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ
serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId()));
}
Pair<Map<Long, List<? extends Host>>, Map<Long, Map<Host, Boolean>>> hostCache = getCompatibleHostAndVmStorageMotionCache(vmList);
Map<Long, List<? extends Host>> vmToCompatibleHostsCache = hostCache.first();
Map<Long, Map<Host, Boolean>> vmToStorageMotionCache = hostCache.second();
Set<Long> vmsWithAffinityGroups = getVmsWithAffinityGroups(vmList, vmToCompatibleHostsCache);
return getMigrationPlans(maxIterations, cluster, hostMap, vmList, vmsWithAffinityGroups, vmToCompatibleHostsCache,
vmToStorageMotionCache, vmIdServiceOfferingMap, originalHostIdVmIdMap, hostVmMap, hostCpuMap, hostMemoryMap);
}
private List<Ternary<VirtualMachine, Host, Host>> getMigrationPlans(
long maxIterations, Cluster cluster, Map<Long, Host> hostMap, List<VirtualMachine> vmList,
Set<Long> vmsWithAffinityGroups, Map<Long, List<? extends Host>> vmToCompatibleHostsCache,
Map<Long, Map<Host, Boolean>> vmToStorageMotionCache, Map<Long, ServiceOffering> vmIdServiceOfferingMap,
Map<Long, List<Long>> originalHostIdVmIdMap, Map<Long, List<VirtualMachine>> hostVmMap,
Map<Long, Ternary<Long, Long, Long>> hostCpuMap, Map<Long, Ternary<Long, Long, Long>> hostMemoryMap
) throws ConfigurationException {
ClusterDrsAlgorithm algorithm = getDrsAlgorithm(ClusterDrsAlgorithm.valueIn(cluster.getId()));
int iteration = 0;
List<Ternary<VirtualMachine, Host, Host>> migrationPlan = new ArrayList<>();
while (iteration < maxIterations && algorithm.needsDrs(cluster, new ArrayList<>(hostCpuMap.values()),
new ArrayList<>(hostMemoryMap.values()))) {
logger.debug("Starting DRS iteration {} for cluster {}", iteration + 1, cluster);
// Re-evaluate affinity constraints with current (simulated) VM placements
Map<Long, ExcludeList> vmToExcludesMap = getVmToExcludesMap(vmList, hostMap, vmsWithAffinityGroups,
vmToCompatibleHostsCache, vmIdServiceOfferingMap);
logger.debug("Completed affinity evaluation for DRS iteration {} for cluster {}", iteration + 1, cluster);
Pair<VirtualMachine, Host> bestMigration = getBestMigration(cluster, algorithm, vmList,
vmIdServiceOfferingMap, hostCpuMap, hostMemoryMap);
vmIdServiceOfferingMap, hostCpuMap, hostMemoryMap,
vmToCompatibleHostsCache, vmToStorageMotionCache, vmToExcludesMap);
VirtualMachine vm = bestMigration.first();
Host destHost = bestMigration.second();
if (destHost == null || vm == null || originalHostIdVmIdMap.get(destHost.getId()).contains(vm.getId())) {
@ -372,8 +411,6 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ
ServiceOffering serviceOffering = vmIdServiceOfferingMap.get(vm.getId());
migrationPlan.add(new Ternary<>(vm, hostMap.get(vm.getHostId()), hostMap.get(destHost.getId())));
hostVmMap.get(vm.getHostId()).remove(vm);
hostVmMap.get(destHost.getId()).add(vm);
hostVmMap.get(vm.getHostId()).remove(vm);
hostVmMap.get(destHost.getId()).add(vm);
@ -391,6 +428,106 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ
return migrationPlan;
}
private Map<Long, ExcludeList> getVmToExcludesMap(List<VirtualMachine> vmList, Map<Long, Host> hostMap,
Set<Long> vmsWithAffinityGroups, Map<Long, List<? extends Host>> vmToCompatibleHostsCache,
Map<Long, ServiceOffering> vmIdServiceOfferingMap) {
Map<Long, ExcludeList> vmToExcludesMap = new HashMap<>();
for (VirtualMachine vm : vmList) {
if (vmToCompatibleHostsCache.containsKey(vm.getId())) {
Host srcHost = hostMap.get(vm.getHostId());
if (srcHost != null) {
// Only call expensive applyAffinityConstraints for VMs with affinity groups
// For VMs without affinity groups, create minimal ExcludeList (just source host)
ExcludeList excludes;
if (vmsWithAffinityGroups.contains(vm.getId())) {
DataCenterDeployment plan = new DataCenterDeployment(
srcHost.getDataCenterId(), srcHost.getPodId(), srcHost.getClusterId(),
null, null, null);
VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(vm, null,
vmIdServiceOfferingMap.get(vm.getId()), null, null);
excludes = managementServer.applyAffinityConstraints(
vm, vmProfile, plan, vmList);
} else {
// VM has no affinity groups - create minimal ExcludeList (just source host)
excludes = new ExcludeList();
excludes.addHost(vm.getHostId());
}
vmToExcludesMap.put(vm.getId(), excludes);
}
}
}
return vmToExcludesMap;
}
/**
* Pre-compute suitable hosts (once per eligible VM - never changes)
* Use listHostsForMigrationOfVM to get hosts validated by getCapableSuitableHosts
* This ensures DRS uses the same validation as "find host for migration" command
*
* @param vmList List of VMs to pre-compute suitable hosts for
* @return Pair of VM to compatible hosts map and VM to storage motion requirement map
*/
private Pair<Map<Long, List<? extends Host>>, Map<Long, Map<Host, Boolean>>> getCompatibleHostAndVmStorageMotionCache(
List<VirtualMachine> vmList
) {
Map<Long, List<? extends Host>> vmToCompatibleHostsCache = new HashMap<>();
Map<Long, Map<Host, Boolean>> vmToStorageMotionCache = new HashMap<>();
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)))) {
continue;
}
try {
// Use listHostsForMigrationOfVM to get suitable hosts (validated by getCapableSuitableHosts)
// This ensures the same validation as the "find host for migration" command
Ternary<Pair<List<? extends Host>, Integer>, List<? extends Host>, Map<Host, Boolean>> hostsForMigration =
managementServer.listHostsForMigrationOfVM(vm, 0L, 500L, null, vmList);
List<? extends Host> suitableHosts = hostsForMigration.second(); // Get suitable hosts (validated by HostAllocator)
Map<Host, Boolean> requiresStorageMotion = hostsForMigration.third();
if (suitableHosts != null && !suitableHosts.isEmpty()) {
vmToCompatibleHostsCache.put(vm.getId(), suitableHosts);
vmToStorageMotionCache.put(vm.getId(), requiresStorageMotion);
}
} catch (Exception e) {
logger.debug("Could not get suitable hosts for VM {}: {}", vm, e.getMessage());
}
}
return new Pair<>(vmToCompatibleHostsCache, vmToStorageMotionCache);
}
/**
* Pre-fetch affinity group mappings for all eligible VMs (once, before iterations)
* This allows us to skip expensive affinity processing for VMs without affinity groups
*
* @param vmList List of VMs to check for affinity groups
* @param vmToCompatibleHostsCache Cached map of VM IDs to their compatible hosts
* @return Set of VM IDs that have affinity groups
*/
private Set<Long> getVmsWithAffinityGroups(
List<VirtualMachine> vmList, Map<Long, List<? extends Host>> vmToCompatibleHostsCache
) {
Set<Long> vmsWithAffinityGroups = new HashSet<>();
for (VirtualMachine vm : vmList) {
if (vmToCompatibleHostsCache.containsKey(vm.getId())) {
// Check if VM has any affinity groups - if list is empty, VM has no affinity groups
List<AffinityGroupVMMapVO> affinityGroupMappings = affinityGroupVMMapDao.listByInstanceId(vm.getId());
if (CollectionUtils.isNotEmpty(affinityGroupMappings)) {
vmsWithAffinityGroups.add(vm.getId());
}
}
}
return vmsWithAffinityGroups;
}
private ClusterDrsAlgorithm getDrsAlgorithm(String algoName) {
if (drsAlgorithmMap.containsKey(algoName)) {
return drsAlgorithmMap.get(algoName);
@ -429,6 +566,12 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ
* @param hostMemoryCapacityMap
* a map of host IDs to their corresponding memory
* capacity
* @param vmToCompatibleHostsCache
* cached map of VM IDs to their compatible hosts
* @param vmToStorageMotionCache
* cached map of VM IDs to storage motion requirements
* @param vmToExcludesMap
* map of VM IDs to their ExcludeList (affinity constraints)
*
* @return a pair of the virtual machine and host that represent the best
* migration, or null if no migration is
@ -438,33 +581,46 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ
List<VirtualMachine> vmList,
Map<Long, ServiceOffering> vmIdServiceOfferingMap,
Map<Long, Ternary<Long, Long, Long>> hostCpuCapacityMap,
Map<Long, Ternary<Long, Long, Long>> hostMemoryCapacityMap) throws ConfigurationException {
Map<Long, Ternary<Long, Long, Long>> hostMemoryCapacityMap,
Map<Long, List<? extends Host>> vmToCompatibleHostsCache,
Map<Long, Map<Host, Boolean>> vmToStorageMotionCache,
Map<Long, ExcludeList> vmToExcludesMap) throws ConfigurationException {
// Pre-calculate cluster imbalance once per iteration (same for all VM-host combinations)
Double preImbalance = getClusterImbalance(cluster.getId(),
new ArrayList<>(hostCpuCapacityMap.values()),
new ArrayList<>(hostMemoryCapacityMap.values()),
null);
// Pre-calculate base metrics array once per iteration for optimized imbalance calculation
String metricType = getClusterDrsMetric(cluster.getId());
Map<Long, Ternary<Long, Long, Long>> baseMetricsMap = "cpu".equals(metricType) ? hostCpuCapacityMap : hostMemoryCapacityMap;
Pair<double[], Map<Long, Integer>> baseMetricsAndIndexMap = getBaseMetricsArrayAndHostIdIndexMap(cluster, baseMetricsMap);
double[] baseMetricsArray = baseMetricsAndIndexMap.first();
Map<Long, Integer> hostIdToIndexMap = baseMetricsAndIndexMap.second();
double improvement = 0;
Pair<VirtualMachine, Host> bestMigration = new Pair<>(null, null);
for (VirtualMachine vm : vmList) {
if (vm.getType().isUsedBySystem() || vm.getState() != VirtualMachine.State.Running ||
(MapUtils.isNotEmpty(vm.getDetails()) &&
vm.getDetails().get(VmDetailConstants.SKIP_DRS).equalsIgnoreCase("true"))
) {
List<? extends Host> compatibleHosts = vmToCompatibleHostsCache.get(vm.getId());
Map<Host, Boolean> requiresStorageMotion = vmToStorageMotionCache.get(vm.getId());
ExcludeList excludes = vmToExcludesMap.get(vm.getId());
ServiceOffering serviceOffering = vmIdServiceOfferingMap.get(vm.getId());
if (skipDrs(vm, compatibleHosts, serviceOffering)) {
continue;
}
Ternary<Pair<List<? extends Host>, Integer>, List<? extends Host>, Map<Host, Boolean>> hostsForMigrationOfVM = managementServer
.listHostsForMigrationOfVM(
vm, 0L, 500L, null, vmList);
List<? extends Host> compatibleDestinationHosts = hostsForMigrationOfVM.first().first();
List<? extends Host> suitableDestinationHosts = hostsForMigrationOfVM.second();
Map<Host, Boolean> requiresStorageMotion = hostsForMigrationOfVM.third();
long vmCpu = (long) serviceOffering.getCpu() * serviceOffering.getSpeed();
long vmMemory = serviceOffering.getRamSize() * 1024L * 1024L;
for (Host destHost : compatibleDestinationHosts) {
if (!suitableDestinationHosts.contains(destHost) || cluster.getId() != destHost.getClusterId()) {
for (Host destHost : compatibleHosts) {
Ternary<Double, Double, Double> metrics = getMetricsForMigration(cluster, algorithm, vm, vmCpu,
vmMemory, serviceOffering, destHost, hostCpuCapacityMap, hostMemoryCapacityMap,
requiresStorageMotion, preImbalance, baseMetricsArray, hostIdToIndexMap, excludes);
if (metrics == null) {
continue;
}
Ternary<Double, Double, Double> metrics = algorithm.getMetrics(cluster, vm,
vmIdServiceOfferingMap.get(vm.getId()), destHost, hostCpuCapacityMap, hostMemoryCapacityMap,
requiresStorageMotion.get(destHost));
Double currentImprovement = metrics.first();
Double cost = metrics.second();
Double benefit = metrics.third();
@ -477,6 +633,86 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ
return bestMigration;
}
private boolean skipDrs(VirtualMachine vm, List<? extends Host> compatibleHosts, ServiceOffering serviceOffering) {
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;
}
private Pair<double[], Map<Long, Integer>> getBaseMetricsArrayAndHostIdIndexMap(
Cluster cluster, Map<Long, Ternary<Long, Long, Long>> baseMetricsMap
) {
double[] baseMetricsArray = new double[baseMetricsMap.size()];
Map<Long, Integer> hostIdToIndexMap = new HashMap<>();
int index = 0;
for (Map.Entry<Long, Ternary<Long, Long, Long>> entry : baseMetricsMap.entrySet()) {
Long hostId = entry.getKey();
Ternary<Long, Long, Long> metrics = entry.getValue();
long used = metrics.first();
long actualTotal = metrics.third() - metrics.second();
long free = actualTotal - metrics.first();
Double metricValue = getMetricValue(cluster.getId(), used, free, actualTotal, null);
if (metricValue != null) {
baseMetricsArray[index] = metricValue;
hostIdToIndexMap.put(hostId, index);
index++;
}
}
// Trim array if some values were null
if (index < baseMetricsArray.length) {
double[] trimmed = new double[index];
System.arraycopy(baseMetricsArray, 0, trimmed, 0, index);
baseMetricsArray = trimmed;
}
return new Pair<>(baseMetricsArray, hostIdToIndexMap);
}
private Ternary<Double, Double, Double> getMetricsForMigration(
Cluster cluster, ClusterDrsAlgorithm algorithm, VirtualMachine vm, long vmCpu, long vmMemory,
ServiceOffering serviceOffering, Host destHost, Map<Long, Ternary<Long, Long, Long>> hostCpuCapacityMap,
Map<Long, Ternary<Long, Long, Long>> hostMemoryCapacityMap, Map<Host, Boolean> requiresStorageMotion,
Double preImbalance, double[] baseMetricsArray, Map<Long, Integer> hostIdToIndexMap, ExcludeList excludes
) throws ConfigurationException {
if (cluster.getId() != destHost.getClusterId()) {
return null;
}
// Check affinity constraints
if (excludes != null && excludes.shouldAvoid(destHost)) {
return null;
}
// Quick capacity pre-filter: skip hosts that don't have enough capacity
Ternary<Long, Long, Long> destHostCpu = hostCpuCapacityMap.get(destHost.getId());
Ternary<Long, Long, Long> destHostMemory = hostMemoryCapacityMap.get(destHost.getId());
if (destHostCpu == null || destHostMemory == null) {
return null;
}
long destHostAvailableCpu = (destHostCpu.third() - destHostCpu.second()) - destHostCpu.first();
long destHostAvailableMemory = (destHostMemory.third() - destHostMemory.second()) - destHostMemory.first();
if (destHostAvailableCpu < vmCpu || destHostAvailableMemory < vmMemory) {
return null; // Skip hosts without sufficient capacity
}
return algorithm.getMetrics(cluster, vm, serviceOffering, destHost, hostCpuCapacityMap, hostMemoryCapacityMap,
requiresStorageMotion.getOrDefault(destHost, false), preImbalance, baseMetricsArray, hostIdToIndexMap);
}
/**
* Saves a DRS plan for a given cluster and returns the saved plan along with the list of migrations to be executed.

View File

@ -42,7 +42,9 @@ import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.VMInstanceDao;
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
import org.apache.cloudstack.api.command.admin.cluster.GenerateClusterDrsPlanCmd;
import org.apache.cloudstack.api.response.ClusterDrsPlanMigrationResponse;
import org.apache.cloudstack.api.response.ClusterDrsPlanResponse;
@ -116,6 +118,9 @@ public class ClusterDrsServiceImplTest {
@Mock
private VMInstanceDao vmInstanceDao;
@Mock
private AffinityGroupVMMapDao affinityGroupVMMapDao;
@Spy
@InjectMocks
private ClusterDrsServiceImpl clusterDrsService = new ClusterDrsServiceImpl();
@ -168,9 +173,14 @@ public class ClusterDrsServiceImplTest {
VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
Mockito.when(vm1.getId()).thenReturn(1L);
Mockito.when(vm1.getHostId()).thenReturn(1L);
Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
Mockito.when(vm2.getHostId()).thenReturn(2L);
Mockito.when(vm2.getId()).thenReturn(2L);
Mockito.when(vm2.getType()).thenReturn(VirtualMachine.Type.User);
Mockito.when(vm2.getState()).thenReturn(VirtualMachine.State.Running);
List<HostVO> hostList = new ArrayList<>();
hostList.add(host1);
@ -201,10 +211,11 @@ public class ClusterDrsServiceImplTest {
Mockito.when(vmInstanceDao.listByClusterId(1L)).thenReturn(vmList);
Mockito.when(balancedAlgorithm.needsDrs(Mockito.any(), Mockito.anyList(), Mockito.anyList())).thenReturn(
true, false);
Mockito.when(
clusterDrsService.getBestMigration(Mockito.any(Cluster.class), Mockito.any(ClusterDrsAlgorithm.class),
Mockito.anyList(), Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap())).thenReturn(
new Pair<>(vm1, host2));
Mockito.doReturn(new Pair<>(vm1, host2)).when(clusterDrsService).getBestMigration(
Mockito.any(Cluster.class), Mockito.any(ClusterDrsAlgorithm.class),
Mockito.anyList(), Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap(),
Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap());
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), Mockito.anyLong())).thenReturn(
serviceOffering);
Mockito.when(hostJoinDao.searchByIds(host1.getId(), host2.getId())).thenReturn(List.of(hostJoin1, hostJoin2));
@ -219,6 +230,445 @@ public class ClusterDrsServiceImplTest {
assertEquals(1, iterations.size());
}
@Test
public void testGetDrsPlanWithDisabledCluster() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Disabled);
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(0, result.size());
}
@Test
public void testGetDrsPlanWithZeroMaxIterations() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 0);
assertEquals(0, result.size());
}
@Test
public void testGetDrsPlanWithNegativeMaxIterations() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, -1);
assertEquals(0, result.size());
}
@Test
public void testGetDrsPlanWithSystemVMs() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getId()).thenReturn(1L);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
HostVO host1 = Mockito.mock(HostVO.class);
Mockito.when(host1.getId()).thenReturn(1L);
VMInstanceVO systemVm = Mockito.mock(VMInstanceVO.class);
Mockito.when(systemVm.getId()).thenReturn(1L);
Mockito.when(systemVm.getHostId()).thenReturn(1L);
Mockito.when(systemVm.getType()).thenReturn(VirtualMachine.Type.SecondaryStorageVm);
List<HostVO> hostList = new ArrayList<>();
hostList.add(host1);
List<VMInstanceVO> vmList = new ArrayList<>();
vmList.add(systemVm);
HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin1.getId()).thenReturn(1L);
Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getCpus()).thenReturn(4);
Mockito.when(hostJoin1.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
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);
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1));
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(0, result.size());
}
@Test
public void testGetDrsPlanWithNonRunningVMs() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getId()).thenReturn(1L);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
HostVO host1 = Mockito.mock(HostVO.class);
Mockito.when(host1.getId()).thenReturn(1L);
VMInstanceVO stoppedVm = Mockito.mock(VMInstanceVO.class);
Mockito.when(stoppedVm.getId()).thenReturn(1L);
Mockito.when(stoppedVm.getHostId()).thenReturn(1L);
Mockito.when(stoppedVm.getType()).thenReturn(VirtualMachine.Type.User);
Mockito.when(stoppedVm.getState()).thenReturn(VirtualMachine.State.Stopped);
List<HostVO> hostList = new ArrayList<>();
hostList.add(host1);
List<VMInstanceVO> vmList = new ArrayList<>();
vmList.add(stoppedVm);
HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin1.getId()).thenReturn(1L);
Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getCpus()).thenReturn(4);
Mockito.when(hostJoin1.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
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);
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1));
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(0, result.size());
}
@Test
public void testGetDrsPlanWithSkipDrsFlag() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getId()).thenReturn(1L);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
HostVO host1 = Mockito.mock(HostVO.class);
Mockito.when(host1.getId()).thenReturn(1L);
VMInstanceVO skippedVm = Mockito.mock(VMInstanceVO.class);
Mockito.when(skippedVm.getId()).thenReturn(1L);
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);
List<VMInstanceVO> vmList = new ArrayList<>();
vmList.add(skippedVm);
HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin1.getId()).thenReturn(1L);
Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getCpus()).thenReturn(4);
Mockito.when(hostJoin1.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
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);
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1));
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(0, result.size());
}
@Test
public void testGetDrsPlanWithNoCompatibleHosts() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getId()).thenReturn(1L);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
HostVO host1 = Mockito.mock(HostVO.class);
Mockito.when(host1.getId()).thenReturn(1L);
VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
Mockito.when(vm1.getId()).thenReturn(1L);
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);
List<VMInstanceVO> vmList = new ArrayList<>();
vmList.add(vm1);
HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin1.getId()).thenReturn(1L);
Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getCpus()).thenReturn(4);
Mockito.when(hostJoin1.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class);
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);
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), Mockito.anyLong())).thenReturn(serviceOffering);
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1));
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(0, result.size());
Mockito.verify(managementServer, Mockito.times(1)).listHostsForMigrationOfVM(Mockito.eq(vm1), Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.anyList());
}
@Test
public void testGetDrsPlanWithExceptionInCompatibilityCheck() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getId()).thenReturn(1L);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
HostVO host1 = Mockito.mock(HostVO.class);
Mockito.when(host1.getId()).thenReturn(1L);
VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
Mockito.when(vm1.getId()).thenReturn(1L);
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);
List<VMInstanceVO> vmList = new ArrayList<>();
vmList.add(vm1);
HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin1.getId()).thenReturn(1L);
Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getCpus()).thenReturn(4);
Mockito.when(hostJoin1.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class);
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);
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), Mockito.anyLong())).thenReturn(serviceOffering);
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1));
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(0, result.size());
// Exception should be caught and logged, not propagated
Mockito.verify(managementServer, Mockito.times(1)).listHostsForMigrationOfVM(Mockito.eq(vm1), Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.anyList());
}
@Test
public void testGetDrsPlanWithNoBestMigration() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getId()).thenReturn(1L);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
HostVO host1 = Mockito.mock(HostVO.class);
Mockito.when(host1.getId()).thenReturn(1L);
VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
Mockito.when(vm1.getId()).thenReturn(1L);
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);
List<VMInstanceVO> vmList = new ArrayList<>();
vmList.add(vm1);
HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin1.getId()).thenReturn(1L);
Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getCpus()).thenReturn(4);
Mockito.when(hostJoin1.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class);
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);
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), Mockito.anyLong())).thenReturn(serviceOffering);
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1));
HostVO compatibleHost = Mockito.mock(HostVO.class);
// Return null migration (no best migration found)
Mockito.doReturn(new Pair<>(null, null)).when(clusterDrsService).getBestMigration(
Mockito.any(Cluster.class), Mockito.any(ClusterDrsAlgorithm.class),
Mockito.anyList(), Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap(),
Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap());
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(0, result.size());
}
@Test
public void testGetDrsPlanWithMultipleIterations() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getId()).thenReturn(1L);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
HostVO host1 = Mockito.mock(HostVO.class);
Mockito.when(host1.getId()).thenReturn(1L);
HostVO host2 = Mockito.mock(HostVO.class);
Mockito.when(host2.getId()).thenReturn(2L);
VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
Mockito.when(vm1.getId()).thenReturn(1L);
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);
hostList.add(host2);
List<VMInstanceVO> vmList = new ArrayList<>();
vmList.add(vm1);
vmList.add(vm2);
HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin1.getId()).thenReturn(1L);
Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getCpus()).thenReturn(4);
Mockito.when(hostJoin1.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
HostJoinVO hostJoin2 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin2.getId()).thenReturn(2L);
Mockito.when(hostJoin2.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin2.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin2.getCpus()).thenReturn(4);
Mockito.when(hostJoin2.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin2.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin2.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin2.getTotalMemory()).thenReturn(8192L);
ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class);
Mockito.when(serviceOffering.getCpu()).thenReturn(1);
Mockito.when(serviceOffering.getRamSize()).thenReturn(1024);
Mockito.when(serviceOffering.getSpeed()).thenReturn(1000);
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, true, false);
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), Mockito.anyLong())).thenReturn(serviceOffering);
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1, hostJoin2));
HostVO compatibleHost = Mockito.mock(HostVO.class);
// Return migrations for first two iterations, then null
Mockito.doReturn(new Pair<>(vm1, host2), new Pair<>(vm2, host2), new Pair<>(null, null))
.when(clusterDrsService).getBestMigration(
Mockito.any(Cluster.class), Mockito.any(ClusterDrsAlgorithm.class),
Mockito.anyList(), Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap(),
Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap());
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(2, result.size());
Mockito.verify(balancedAlgorithm, Mockito.times(3)).needsDrs(Mockito.any(), Mockito.anyList(), Mockito.anyList());
}
@Test
public void testGetDrsPlanWithMigrationToOriginalHost() throws ConfigurationException {
ClusterVO cluster = Mockito.mock(ClusterVO.class);
Mockito.when(cluster.getId()).thenReturn(1L);
Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
HostVO host1 = Mockito.mock(HostVO.class);
Mockito.when(host1.getId()).thenReturn(1L);
HostVO host2 = Mockito.mock(HostVO.class);
Mockito.when(host2.getId()).thenReturn(2L);
VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
Mockito.when(vm1.getId()).thenReturn(1L);
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);
hostList.add(host2);
List<VMInstanceVO> vmList = new ArrayList<>();
vmList.add(vm1);
HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin1.getId()).thenReturn(1L);
Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getCpus()).thenReturn(4);
Mockito.when(hostJoin1.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin1.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin1.getTotalMemory()).thenReturn(8192L);
HostJoinVO hostJoin2 = Mockito.mock(HostJoinVO.class);
Mockito.when(hostJoin2.getId()).thenReturn(2L);
Mockito.when(hostJoin2.getCpuUsedCapacity()).thenReturn(1000L);
Mockito.when(hostJoin2.getCpuReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin2.getCpus()).thenReturn(4);
Mockito.when(hostJoin2.getSpeed()).thenReturn(1000L);
Mockito.when(hostJoin2.getMemUsedCapacity()).thenReturn(1024L);
Mockito.when(hostJoin2.getMemReservedCapacity()).thenReturn(0L);
Mockito.when(hostJoin2.getTotalMemory()).thenReturn(8192L);
ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class);
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);
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), Mockito.anyLong())).thenReturn(serviceOffering);
Mockito.when(hostJoinDao.searchByIds(Mockito.any())).thenReturn(List.of(hostJoin1, hostJoin2));
HostVO compatibleHost = Mockito.mock(HostVO.class);
// Return migration to original host (host1) - should break the loop
Mockito.doReturn(new Pair<>(vm1, host1)).when(clusterDrsService).getBestMigration(
Mockito.any(Cluster.class), Mockito.any(ClusterDrsAlgorithm.class),
Mockito.anyList(), Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap(),
Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap());
List<Ternary<VirtualMachine, Host, Host>> result = clusterDrsService.getDrsPlan(cluster, 5);
assertEquals(0, result.size());
// Should break early when VM would migrate to original host
}
@Test(expected = InvalidParameterValueException.class)
public void testGenerateDrsPlanClusterNotFound() {
Mockito.when(clusterDao.findById(1L)).thenReturn(null);
@ -387,18 +837,41 @@ public class ClusterDrsServiceImplTest {
vmIdServiceOfferingMap.put(vm.getId(), serviceOffering);
}
Mockito.when(managementServer.listHostsForMigrationOfVM(vm1, 0L, 500L, null, vmList)).thenReturn(
new Ternary<>(new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, false)));
Mockito.when(managementServer.listHostsForMigrationOfVM(vm2, 0L, 500L, null, vmList)).thenReturn(
new Ternary<>(new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, false)));
Mockito.when(balancedAlgorithm.getMetrics(cluster, vm1, serviceOffering, destHost, new HashMap<>(),
new HashMap<>(), false)).thenReturn(new Ternary<>(1.0, 0.5, 1.5));
// Create caches for the new method signature
Map<Long, List<? extends Host>> vmToCompatibleHostsCache = new HashMap<>();
vmToCompatibleHostsCache.put(vm1.getId(), List.of(destHost));
vmToCompatibleHostsCache.put(vm2.getId(), List.of(destHost));
Mockito.when(balancedAlgorithm.getMetrics(cluster, vm2, serviceOffering, destHost, new HashMap<>(),
new HashMap<>(), false)).thenReturn(new Ternary<>(1.0, 2.5, 1.5));
Map<Long, Map<Host, Boolean>> vmToStorageMotionCache = new HashMap<>();
vmToStorageMotionCache.put(vm1.getId(), Map.of(destHost, false));
vmToStorageMotionCache.put(vm2.getId(), Map.of(destHost, false));
Pair<VirtualMachine, Host> bestMigration = clusterDrsService.getBestMigration(cluster, balancedAlgorithm,
vmList, vmIdServiceOfferingMap, new HashMap<>(), new HashMap<>());
Map<Long, com.cloud.deploy.DeploymentPlanner.ExcludeList> vmToExcludesMap = new HashMap<>();
vmToExcludesMap.put(vm1.getId(), Mockito.mock(com.cloud.deploy.DeploymentPlanner.ExcludeList.class));
vmToExcludesMap.put(vm2.getId(), Mockito.mock(com.cloud.deploy.DeploymentPlanner.ExcludeList.class));
// Create capacity maps with dummy data for getClusterImbalance (include both source and dest hosts)
Map<Long, Ternary<Long, Long, Long>> hostCpuCapacityMap = new HashMap<>();
hostCpuCapacityMap.put(host.getId(), new Ternary<>(2000L, 0L, 3000L)); // Source host
hostCpuCapacityMap.put(destHost.getId(), new Ternary<>(1000L, 0L, 2000L)); // Dest host
Map<Long, Ternary<Long, Long, Long>> hostMemoryCapacityMap = new HashMap<>();
hostMemoryCapacityMap.put(host.getId(), new Ternary<>(2L * 1024L * 1024L * 1024L, 0L, 3L * 1024L * 1024L * 1024L)); // Source host
hostMemoryCapacityMap.put(destHost.getId(), new Ternary<>(1024L * 1024L * 1024L, 0L, 2L * 1024L * 1024L * 1024L)); // Dest host
// Mock getMetrics for the optimized 10-parameter version used by getBestMigration
// Return better improvement for vm1, worse for vm2
Mockito.doReturn(new Ternary<>(1.0, 0.5, 1.5)).when(balancedAlgorithm).getMetrics(
Mockito.eq(cluster), Mockito.eq(vm1), Mockito.any(ServiceOffering.class),
Mockito.eq(destHost), Mockito.eq(hostCpuCapacityMap), Mockito.eq(hostMemoryCapacityMap), Mockito.any(Boolean.class),
Mockito.any(Double.class), Mockito.any(double[].class), Mockito.any(Map.class));
Mockito.doReturn(new Ternary<>(0.5, 2.5, 1.5)).when(balancedAlgorithm).getMetrics(
Mockito.eq(cluster), Mockito.eq(vm2), Mockito.any(ServiceOffering.class),
Mockito.eq(destHost), Mockito.eq(hostCpuCapacityMap), Mockito.eq(hostMemoryCapacityMap), Mockito.any(Boolean.class),
Mockito.any(Double.class), Mockito.any(double[].class), Mockito.any(Map.class));
Pair<VirtualMachine, Host> bestMigration = clusterDrsService.getBestMigration(cluster, balancedAlgorithm,
vmList, vmIdServiceOfferingMap, hostCpuCapacityMap, hostMemoryCapacityMap,
vmToCompatibleHostsCache, vmToStorageMotionCache, vmToExcludesMap);
assertEquals(destHost, bestMigration.second());
assertEquals(vm1, bestMigration.first());
@ -443,12 +916,28 @@ public class ClusterDrsServiceImplTest {
vmIdServiceOfferingMap.put(vm.getId(), serviceOffering);
}
Mockito.when(managementServer.listHostsForMigrationOfVM(vm1, 0L, 500L, null, vmList)).thenReturn(
new Ternary<>(new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, false)));
Mockito.when(managementServer.listHostsForMigrationOfVM(vm2, 0L, 500L, null, vmList)).thenReturn(
new Ternary<>(new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, false)));
// Create caches for the new method signature
Map<Long, List<? extends Host>> vmToCompatibleHostsCache = new HashMap<>();
vmToCompatibleHostsCache.put(vm1.getId(), List.of(destHost));
vmToCompatibleHostsCache.put(vm2.getId(), List.of(destHost));
Map<Long, Map<Host, Boolean>> vmToStorageMotionCache = new HashMap<>();
vmToStorageMotionCache.put(vm1.getId(), Map.of(destHost, false));
vmToStorageMotionCache.put(vm2.getId(), Map.of(destHost, false));
Map<Long, com.cloud.deploy.DeploymentPlanner.ExcludeList> vmToExcludesMap = new HashMap<>();
vmToExcludesMap.put(vm1.getId(), Mockito.mock(com.cloud.deploy.DeploymentPlanner.ExcludeList.class));
vmToExcludesMap.put(vm2.getId(), Mockito.mock(com.cloud.deploy.DeploymentPlanner.ExcludeList.class));
// Create capacity maps with dummy data for getClusterImbalance
Map<Long, Ternary<Long, Long, Long>> hostCpuCapacityMap = new HashMap<>();
hostCpuCapacityMap.put(destHost.getId(), new Ternary<>(1000L, 0L, 2000L));
Map<Long, Ternary<Long, Long, Long>> hostMemoryCapacityMap = new HashMap<>();
hostMemoryCapacityMap.put(destHost.getId(), new Ternary<>(1024L * 1024L * 1024L, 0L, 2L * 1024L * 1024L * 1024L));
Pair<VirtualMachine, Host> bestMigration = clusterDrsService.getBestMigration(cluster, balancedAlgorithm,
vmList, vmIdServiceOfferingMap, new HashMap<>(), new HashMap<>());
vmList, vmIdServiceOfferingMap, hostCpuCapacityMap, hostMemoryCapacityMap,
vmToCompatibleHostsCache, vmToStorageMotionCache, vmToExcludesMap);
assertNull(bestMigration.second());
assertNull(bestMigration.first());