Skip to content

Commit 3459b70

Browse files
Re-check affinity before executing each DRS migration
A DRS plan is generated once and executed later, and nothing downstream re-checks it - migrateVirtualMachine does not enforce affinity groups. By execution time the cluster may have changed, so a plan that was valid when generated can violate anti-affinity when it runs. - validate each migration against current state before queueing it - skip and mark failed instead of migrating into a violation - track destinations already queued in this run, since the jobs are asynchronous and the database does not reflect them yet Signed-off-by: Brad House <bhouse@nexthop.ai>
1 parent 65d4fce commit 3459b70

2 files changed

Lines changed: 125 additions & 1 deletion

File tree

server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,29 @@ void processPlans() {
768768
* @param plan
769769
* the DRS plan to be executed
770770
*/
771+
/**
772+
* Checks a planned migration against the affinity rules as they stand now.
773+
*
774+
* A plan is generated once and executed later, so state can have moved on: VMs may have been
775+
* created, migrated or destroyed in between. Anti-affinity in particular is only meaningful
776+
* against current placements, and nothing downstream re-checks it - migrateVirtualMachine does
777+
* not enforce affinity groups.
778+
*
779+
* @param dispatched
780+
* migrations already queued by this run, which the database does not reflect yet
781+
*/
782+
protected boolean destinationViolatesAffinity(VirtualMachine vm, Host destHost, List<VirtualMachine> dispatched) {
783+
if (CollectionUtils.isEmpty(affinityGroupVMMapDao.listByInstanceId(vm.getId()))) {
784+
return false;
785+
}
786+
DataCenterDeployment plan = new DataCenterDeployment(destHost.getDataCenterId(), destHost.getPodId(),
787+
destHost.getClusterId(), null, null, null);
788+
VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(vm, null,
789+
serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId()), null, null);
790+
ExcludeList excludes = managementServer.applyAffinityConstraints(vm, vmProfile, plan, dispatched);
791+
return excludes.shouldAvoid(destHost);
792+
}
793+
771794
void executeDrsPlan(ClusterDrsPlanVO plan) {
772795
List<ClusterDrsPlanMigrationVO> planMigrations = drsPlanMigrationDao.listPlanMigrationsToExecute(plan.getId());
773796
if (planMigrations == null || planMigrations.isEmpty()) {
@@ -783,21 +806,36 @@ void executeDrsPlan(ClusterDrsPlanVO plan) {
783806
plan.setStatus(ClusterDrsPlan.Status.IN_PROGRESS);
784807
drsPlanDao.update(plan.getId(), plan);
785808

809+
List<VirtualMachine> dispatched = new ArrayList<>();
810+
786811
for (ClusterDrsPlanMigrationVO migration : planMigrations) {
787812
try {
788-
VirtualMachine vm = vmInstanceDao.findById(migration.getVmId());
813+
VMInstanceVO vm = vmInstanceDao.findById(migration.getVmId());
789814
Host host = hostDao.findById(migration.getDestHostId());
790815
if (vm == null || host == null) {
791816
throw new CloudRuntimeException(String.format("vm %s or host %s is not found", migration.getVmId(),
792817
migration.getDestHostId()));
793818
}
794819

820+
if (destinationViolatesAffinity(vm, host, dispatched)) {
821+
logger.warn("Skipping DRS migration of vm {} to host {}: it no longer satisfies the affinity " +
822+
"rules for that VM. The plan was generated against older state.", vm, host);
823+
migration.setStatus(JobInfo.Status.FAILED);
824+
drsPlanMigrationDao.update(migration.getId(), migration);
825+
continue;
826+
}
827+
795828
logger.debug("Executing DRS plan {} for vm {} to host {}", plan, vm, host);
796829
long jobId = createMigrateVMAsyncJob(vm, host, plan.getEventId());
797830
AsyncJobVO job = asyncJobManager.getAsyncJob(jobId);
798831
migration.setJobId(jobId);
799832
migration.setStatus(job.getStatus());
800833
drsPlanMigrationDao.update(migration.getId(), migration);
834+
835+
// the migration job has only been queued, so the database still shows the old host.
836+
// record where it is headed so later migrations in this plan see it.
837+
vm.setHostId(host.getId());
838+
dispatched.add(vm);
801839
} catch (Exception e) {
802840
logger.warn("Unable to execute DRS plan {} due to {}", plan, e.getMessage());
803841
migration.setStatus(JobInfo.Status.FAILED);

server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@
2929
import com.cloud.exception.InvalidParameterValueException;
3030
import com.cloud.host.Host;
3131
import com.cloud.host.HostVO;
32+
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
3233
import com.cloud.host.dao.HostDao;
3334
import com.cloud.offering.ServiceOffering;
3435
import com.cloud.org.Cluster;
3536
import com.cloud.org.Grouping;
3637
import com.cloud.server.ManagementServer;
3738
import com.cloud.service.ServiceOfferingVO;
39+
import org.apache.cloudstack.affinity.AffinityGroupVMMapVO;
3840
import com.cloud.service.dao.ServiceOfferingDao;
3941
import com.cloud.utils.Pair;
4042
import com.cloud.utils.Ternary;
@@ -70,13 +72,15 @@
7072
import java.lang.reflect.Field;
7173
import java.util.ArrayList;
7274
import java.util.Collections;
75+
import org.apache.cloudstack.jobs.JobInfo;
7376
import java.util.Date;
7477
import java.util.HashMap;
7578
import java.util.List;
7679
import java.util.Map;
7780

7881
import static org.junit.Assert.assertEquals;
7982
import static org.junit.Assert.assertFalse;
83+
import static org.junit.Assert.assertTrue;
8084
import static org.junit.Assert.assertNull;
8185

8286
@RunWith(MockitoJUnitRunner.class)
@@ -951,4 +955,86 @@ public void testProcessPlans() {
951955

952956
Mockito.verify(clusterDrsService, Mockito.times(2)).executeDrsPlan(Mockito.any(ClusterDrsPlanVO.class));
953957
}
958+
959+
@Test
960+
public void testDestinationViolatesAffinityWhenVmHasNoAffinityGroups() {
961+
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
962+
Mockito.when(vm.getId()).thenReturn(1L);
963+
Mockito.when(affinityGroupVMMapDao.listByInstanceId(1L)).thenReturn(Collections.emptyList());
964+
965+
HostVO destHost = Mockito.mock(HostVO.class);
966+
967+
assertFalse(clusterDrsService.destinationViolatesAffinity(vm, destHost, Collections.emptyList()));
968+
Mockito.verify(managementServer, Mockito.never())
969+
.applyAffinityConstraints(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
970+
}
971+
972+
@Test
973+
public void testDestinationViolatesAffinityWhenDestHostIsExcluded() {
974+
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
975+
Mockito.when(vm.getId()).thenReturn(1L);
976+
Mockito.when(vm.getServiceOfferingId()).thenReturn(5L);
977+
Mockito.when(affinityGroupVMMapDao.listByInstanceId(1L))
978+
.thenReturn(Collections.singletonList(Mockito.mock(AffinityGroupVMMapVO.class)));
979+
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(1L, 5L))
980+
.thenReturn(Mockito.mock(ServiceOfferingVO.class));
981+
982+
HostVO destHost = Mockito.mock(HostVO.class);
983+
Mockito.when(destHost.getId()).thenReturn(20L);
984+
985+
ExcludeList excludes = new ExcludeList();
986+
excludes.addHost(20L);
987+
Mockito.when(managementServer.applyAffinityConstraints(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
988+
.thenReturn(excludes);
989+
990+
assertTrue(clusterDrsService.destinationViolatesAffinity(vm, destHost, Collections.emptyList()));
991+
}
992+
993+
@Test
994+
public void testDestinationViolatesAffinityWhenDestHostIsAllowed() {
995+
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
996+
Mockito.when(vm.getId()).thenReturn(1L);
997+
Mockito.when(vm.getServiceOfferingId()).thenReturn(5L);
998+
Mockito.when(affinityGroupVMMapDao.listByInstanceId(1L))
999+
.thenReturn(Collections.singletonList(Mockito.mock(AffinityGroupVMMapVO.class)));
1000+
Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(1L, 5L))
1001+
.thenReturn(Mockito.mock(ServiceOfferingVO.class));
1002+
1003+
HostVO destHost = Mockito.mock(HostVO.class);
1004+
Mockito.when(destHost.getId()).thenReturn(20L);
1005+
1006+
ExcludeList excludes = new ExcludeList();
1007+
excludes.addHost(21L);
1008+
Mockito.when(managementServer.applyAffinityConstraints(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
1009+
.thenReturn(excludes);
1010+
1011+
assertFalse(clusterDrsService.destinationViolatesAffinity(vm, destHost, Collections.emptyList()));
1012+
}
1013+
1014+
@Test
1015+
public void testExecuteDrsPlanSkipsMigrationThatViolatesAffinity() {
1016+
ClusterDrsPlanVO plan = Mockito.mock(ClusterDrsPlanVO.class);
1017+
Mockito.when(plan.getId()).thenReturn(1L);
1018+
1019+
ClusterDrsPlanMigrationVO migration = Mockito.mock(ClusterDrsPlanMigrationVO.class);
1020+
Mockito.when(migration.getId()).thenReturn(7L);
1021+
Mockito.when(migration.getVmId()).thenReturn(1L);
1022+
Mockito.when(migration.getDestHostId()).thenReturn(20L);
1023+
Mockito.when(drsPlanMigrationDao.listPlanMigrationsToExecute(1L))
1024+
.thenReturn(Collections.singletonList(migration));
1025+
1026+
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
1027+
HostVO destHost = Mockito.mock(HostVO.class);
1028+
Mockito.when(vmInstanceDao.findById(1L)).thenReturn(vm);
1029+
Mockito.when(hostDao.findById(20L)).thenReturn(destHost);
1030+
1031+
Mockito.doReturn(true).when(clusterDrsService)
1032+
.destinationViolatesAffinity(Mockito.any(), Mockito.any(), Mockito.any());
1033+
1034+
clusterDrsService.executeDrsPlan(plan);
1035+
1036+
Mockito.verify(migration).setStatus(JobInfo.Status.FAILED);
1037+
Mockito.verify(clusterDrsService, Mockito.never())
1038+
.createMigrateVMAsyncJob(Mockito.any(), Mockito.any(), Mockito.anyLong());
1039+
}
9541040
}

0 commit comments

Comments
 (0)