This is an automated email from the ASF dual-hosted git repository. rohit pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push: new e234c3ccdc7 server: guard vm start inter-cluster migration with config (#7401) e234c3ccdc7 is described below commit e234c3ccdc794ebc6187a5bcaac0a2ab54fc6325 Author: Abhishek Kumar <abhishek.mr...@gmail.com> AuthorDate: Mon May 8 12:08:57 2023 +0530 server: guard vm start inter-cluster migration with config (#7401) During the start of a stopped VM when there is not enough capacity in the current cluster CloudStack can migrate it to a new cluster. This can be an expensive operation when Cluster scope storage is used as migration can be carried out using SSVM and secondary storage. This PR allows controlling this behaviour with the existing global config - `migrate.vm.across.clusters` Signed-off-by: Abhishek Kumar <abhishek.mr...@gmail.com> --- .../deploy/DeploymentPlanningManagerImpl.java | 65 +++++++-- .../deploy/DeploymentPlanningManagerImplTest.java | 155 ++++++++++++++++----- 2 files changed, 171 insertions(+), 49 deletions(-) diff --git a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java index cf4e7fdb2cc..f71f46fdc03 100644 --- a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java +++ b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.deploy; +import static com.cloud.utils.NumbersUtil.toHumanReadableSize; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -23,28 +25,17 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.Timer; import java.util.TreeSet; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; -import com.cloud.storage.VMTemplateVO; -import com.cloud.storage.dao.VMTemplateDao; -import com.cloud.user.AccountVO; -import com.cloud.user.dao.AccountDao; -import com.cloud.exception.StorageUnavailableException; -import com.cloud.utils.db.Filter; -import com.cloud.utils.fsm.StateMachine2; - -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.Configurable; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.log4j.Logger; import org.apache.cloudstack.affinity.AffinityGroupProcessor; import org.apache.cloudstack.affinity.AffinityGroupService; import org.apache.cloudstack.affinity.AffinityGroupVMMapVO; @@ -57,6 +48,8 @@ import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMReservationDao; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.MessageSubscriber; @@ -64,6 +57,9 @@ import org.apache.cloudstack.managed.context.ManagedContextTimerTask; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; import com.cloud.agent.Listener; @@ -95,6 +91,7 @@ import com.cloud.deploy.dao.PlannerHostReservationDao; import com.cloud.exception.AffinityConflictException; import com.cloud.exception.ConnectionException; import com.cloud.exception.InsufficientServerCapacityException; +import com.cloud.exception.StorageUnavailableException; import com.cloud.gpu.GPU; import com.cloud.host.DetailVO; import com.cloud.host.Host; @@ -115,26 +112,32 @@ import com.cloud.storage.ScopeType; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import com.cloud.storage.StoragePoolHostVO; +import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSCategoryDao; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.user.AccountManager; +import com.cloud.user.AccountVO; +import com.cloud.user.dao.AccountDao; import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.DB; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallback; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.fsm.StateListener; +import com.cloud.utils.fsm.StateMachine2; import com.cloud.vm.DiskProfile; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; @@ -144,8 +147,6 @@ import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; -import static com.cloud.utils.NumbersUtil.toHumanReadableSize; - public class DeploymentPlanningManagerImpl extends ManagerBase implements DeploymentPlanningManager, Manager, Listener, StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable { @@ -266,6 +267,35 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable { _affinityProcessors = affinityProcessors; } + protected void avoidOtherClustersForDeploymentIfMigrationDisabled(VirtualMachine vm, Host lastHost, ExcludeList avoids) { + if (lastHost == null || lastHost.getClusterId() == null || + ConfigurationManagerImpl.MIGRATE_VM_ACROSS_CLUSTERS.valueIn(vm.getDataCenterId())) { + return; + } + List<VolumeVO> volumes = _volsDao.findUsableVolumesForInstance(vm.getId()); + if (CollectionUtils.isEmpty(volumes)) { + return; + } + boolean storageMigrationNeededDuringClusterMigration = false; + for (Volume volume : volumes) { + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (List.of(ScopeType.HOST, ScopeType.CLUSTER).contains(pool.getScope())) { + storageMigrationNeededDuringClusterMigration = true; + break; + } + } + if (!storageMigrationNeededDuringClusterMigration) { + return; + } + final Long lastHostClusterId = lastHost.getClusterId(); + s_logger.warn(String.format("VM last host ID: %d belongs to zone ID: %s for which config - %s is false and storage migration would be needed for inter-cluster migration, therefore, adding all other clusters except ID: %d from this zone to avoid list", + lastHost.getId(), vm.getDataCenterId(), ConfigurationManagerImpl.MIGRATE_VM_ACROSS_CLUSTERS.key(), lastHostClusterId)); + List<Long> clusterIds = _clusterDao.listAllClusters(lastHost.getDataCenterId()); + Set<Long> existingAvoidedClusters = avoids.getClustersToAvoid(); + clusterIds = clusterIds.stream().filter(x -> !Objects.equals(x, lastHostClusterId) && (existingAvoidedClusters == null || !existingAvoidedClusters.contains(x))).collect(Collectors.toList()); + avoids.addClusterList(clusterIds); + } + @Override public DeployDestination planDeployment(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoids, DeploymentPlanner planner) throws InsufficientServerCapacityException, AffinityConflictException { @@ -408,6 +438,8 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable { planner = getDeploymentPlannerByName(plannerName); } + Host lastHost = null; + String considerLastHostStr = (String)vmProfile.getParameter(VirtualMachineProfile.Param.ConsiderLastHost); boolean considerLastHost = vm.getLastHostId() != null && haVmTag == null && (considerLastHostStr == null || Boolean.TRUE.toString().equalsIgnoreCase(considerLastHostStr)); @@ -415,6 +447,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable { s_logger.debug("This VM has last host_id specified, trying to choose the same host: " + vm.getLastHostId()); HostVO host = _hostDao.findById(vm.getLastHostId()); + lastHost = host; _hostDao.loadHostTags(host); _hostDao.loadDetails(host); ServiceOfferingDetailsVO offeringDetails = null; @@ -519,6 +552,8 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable { s_logger.debug("Cannot choose the last host to deploy this VM "); } + avoidOtherClustersForDeploymentIfMigrationDisabled(vm, lastHost, avoids); + DeployDestination dest = null; List<Long> clusterList = null; diff --git a/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java b/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java index d79010ee5c0..0b6004bf77e 100644 --- a/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java +++ b/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java @@ -21,39 +21,31 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.dc.ClusterDetailsVO; -import com.cloud.dc.DataCenter; -import com.cloud.gpu.GPU; -import com.cloud.host.Host; -import com.cloud.host.HostVO; -import com.cloud.host.Status; -import com.cloud.storage.DiskOfferingVO; -import com.cloud.storage.Storage; -import com.cloud.storage.StoragePool; -import com.cloud.storage.VMTemplateVO; -import com.cloud.storage.Volume; -import com.cloud.storage.VolumeVO; -import com.cloud.storage.dao.VMTemplateDao; -import com.cloud.user.AccountVO; -import com.cloud.user.dao.AccountDao; -import com.cloud.utils.Pair; -import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.VirtualMachine; -import com.cloud.vm.VirtualMachine.Type; -import com.cloud.vm.VirtualMachineProfile; -import com.cloud.vm.VirtualMachineProfileImpl; +import org.apache.cloudstack.affinity.AffinityGroupProcessor; +import org.apache.cloudstack.affinity.AffinityGroupService; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.affinity.dao.AffinityGroupDomainMapDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMReservationDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.messagebus.MessageBus; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.test.utils.SpringUtils; import org.apache.commons.collections.CollectionUtils; import org.junit.Assert; import org.junit.Before; @@ -79,22 +71,14 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader; -import org.apache.cloudstack.affinity.AffinityGroupProcessor; -import org.apache.cloudstack.affinity.AffinityGroupService; -import org.apache.cloudstack.affinity.dao.AffinityGroupDao; -import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; -import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMReservationDao; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.messagebus.MessageBus; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.test.utils.SpringUtils; - import com.cloud.agent.AgentManager; import com.cloud.capacity.CapacityManager; import com.cloud.capacity.dao.CapacityDao; +import com.cloud.configuration.ConfigurationManagerImpl; import com.cloud.dc.ClusterDetailsDao; +import com.cloud.dc.ClusterDetailsVO; import com.cloud.dc.ClusterVO; +import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; @@ -105,26 +89,46 @@ import com.cloud.deploy.DeploymentPlanner.PlannerResourceUsage; import com.cloud.deploy.dao.PlannerHostReservationDao; import com.cloud.exception.AffinityConflictException; import com.cloud.exception.InsufficientServerCapacityException; +import com.cloud.gpu.GPU; import com.cloud.gpu.dao.HostGpuGroupsDao; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.Status; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.host.dao.HostTagsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.resource.ResourceManager; import com.cloud.org.Grouping.AllocationState; +import com.cloud.resource.ResourceManager; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDetailsDao; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.ScopeType; +import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; +import com.cloud.storage.StoragePool; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSCategoryDao; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.user.AccountManager; +import com.cloud.user.AccountVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.utils.Pair; import com.cloud.utils.component.ComponentContext; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachine.Type; +import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.VirtualMachineProfileImpl; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; -import com.cloud.host.dao.HostDetailsDao; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = AnnotationConfigContextLoader.class) @@ -191,6 +195,9 @@ public class DeploymentPlanningManagerImplTest { @Inject ClusterDetailsDao clusterDetailsDao; + @Inject + PrimaryDataStoreDao primaryDataStoreDao; + @Mock Host host; @@ -1075,4 +1082,84 @@ public class DeploymentPlanningManagerImplTest { Assert.assertEquals(6, hosts.get(6).getId()); Assert.assertEquals(2, hosts.get(7).getId()); } + + private List<Long> prepareMockForAvoidOtherClustersForDeploymentIfMigrationDisabled(boolean configValue, boolean mockVolumes, boolean mockClusterStoreVolume) { + try { + Field f = ConfigKey.class.getDeclaredField("_defaultValue"); + f.setAccessible(true); + f.set(ConfigurationManagerImpl.MIGRATE_VM_ACROSS_CLUSTERS, String.valueOf(configValue)); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + List<Long> allClusters = List.of(101L, 102L, 103L, 104L); + Mockito.when(_clusterDao.listAllClusters(Mockito.anyLong())).thenReturn(allClusters); + if (mockVolumes) { + VolumeVO vol1 = Mockito.mock(VolumeVO.class); + Mockito.when(vol1.getPoolId()).thenReturn(1L); + VolumeVO vol2 = Mockito.mock(VolumeVO.class); + Mockito.when(vol2.getPoolId()).thenReturn(2L); + StoragePoolVO pool1 = Mockito.mock(StoragePoolVO.class); + Mockito.when(pool1.getScope()).thenReturn(ScopeType.ZONE); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(pool1); + StoragePoolVO pool2 = Mockito.mock(StoragePoolVO.class); + Mockito.when(pool2.getScope()).thenReturn(mockClusterStoreVolume ? ScopeType.CLUSTER : ScopeType.GLOBAL); + Mockito.when(primaryDataStoreDao.findById(2L)).thenReturn(pool2); + Mockito.when(volDao.findUsableVolumesForInstance(1L)).thenReturn(List.of(vol1, vol2)); + } else { + Mockito.when(volDao.findUsableVolumesForInstance(1L)).thenReturn(new ArrayList<>()); + } + return allClusters; + } + + @Test + public void avoidOtherClustersForDeploymentIfMigrationDisabledNonValidHost() { + prepareMockForAvoidOtherClustersForDeploymentIfMigrationDisabled(false, false, false); + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + ExcludeList excludeList = new ExcludeList(); + _dpm.avoidOtherClustersForDeploymentIfMigrationDisabled(vm, null, excludeList); + Assert.assertTrue(CollectionUtils.isEmpty(excludeList.getClustersToAvoid())); + + Host lastHost = Mockito.mock(Host.class); + Mockito.when(lastHost.getClusterId()).thenReturn(null); + _dpm.avoidOtherClustersForDeploymentIfMigrationDisabled(vm, lastHost, excludeList); + Assert.assertTrue(CollectionUtils.isEmpty(excludeList.getClustersToAvoid())); + } + + private Set<Long> runAvoidOtherClustersForDeploymentIfMigrationDisabledTest() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + Mockito.when(vm.getId()).thenReturn(1L); + ExcludeList excludeList = new ExcludeList(); + Host lastHost = Mockito.mock(Host.class); + Long sourceClusterId = 101L; + Mockito.when(lastHost.getClusterId()).thenReturn(sourceClusterId); + _dpm.avoidOtherClustersForDeploymentIfMigrationDisabled(vm, lastHost, excludeList); + return excludeList.getClustersToAvoid(); + } + + @Test + public void avoidOtherClustersForDeploymentIfMigrationDisabledConfigAllows() { + prepareMockForAvoidOtherClustersForDeploymentIfMigrationDisabled(true,false, false); + Assert.assertTrue(CollectionUtils.isEmpty(runAvoidOtherClustersForDeploymentIfMigrationDisabledTest())); + } + + @Test + public void avoidOtherClustersForDeploymentIfMigrationDisabledNoVmVolumes() { + prepareMockForAvoidOtherClustersForDeploymentIfMigrationDisabled(false,false, false); + Assert.assertTrue(CollectionUtils.isEmpty(runAvoidOtherClustersForDeploymentIfMigrationDisabledTest())); + } + + @Test + public void avoidOtherClustersForDeploymentIfMigrationDisabledVmVolumesNonValidScope() { + prepareMockForAvoidOtherClustersForDeploymentIfMigrationDisabled(false,true, false); + Assert.assertTrue(CollectionUtils.isEmpty(runAvoidOtherClustersForDeploymentIfMigrationDisabledTest())); + } + + @Test + public void avoidOtherClustersForDeploymentIfMigrationDisabledValid() { + List<Long> allClusters = prepareMockForAvoidOtherClustersForDeploymentIfMigrationDisabled(false,true, true); + Set<Long> avoidedClusters = runAvoidOtherClustersForDeploymentIfMigrationDisabledTest(); + Assert.assertTrue(CollectionUtils.isNotEmpty(avoidedClusters)); + Assert.assertEquals(allClusters.size()-1, avoidedClusters.size()); + Assert.assertFalse(avoidedClusters.contains(allClusters.get(0))); + } }