new plugins: Add non-strict affinity groups (#6845)

This commit is contained in:
Wei Zhou 2022-12-20 15:09:52 +01:00 committed by GitHub
parent 440d7805cb
commit 889045fba5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1397 additions and 9 deletions

View File

@ -21,7 +21,9 @@ import com.cloud.vm.ReservationContext;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DataCenterDeployment implements DeploymentPlan {
long _dcId;
@ -35,6 +37,7 @@ public class DataCenterDeployment implements DeploymentPlan {
ReservationContext _context;
List<Long> preferredHostIds = new ArrayList<>();
boolean migrationPlan;
Map<Long, Integer> hostPriorities = new HashMap<>();
public DataCenterDeployment(long dataCenterId) {
this(dataCenterId, null, null, null, null, null);
@ -124,4 +127,32 @@ public class DataCenterDeployment implements DeploymentPlan {
"migrationPlan");
}
@Override
public void adjustHostPriority(Long hostId, HostPriorityAdjustment adjustment) {
Integer currentPriority = hostPriorities.get(hostId);
if (currentPriority == null) {
currentPriority = DEFAULT_HOST_PRIORITY;
} else if (currentPriority.equals(PROHIBITED_HOST_PRIORITY)) {
return;
}
if (HostPriorityAdjustment.HIGHER.equals(adjustment)) {
hostPriorities.put(hostId, currentPriority + ADJUST_HOST_PRIORITY_BY);
} else if (HostPriorityAdjustment.LOWER.equals(adjustment)) {
hostPriorities.put(hostId, currentPriority - ADJUST_HOST_PRIORITY_BY);
} else if (HostPriorityAdjustment.DEFAULT.equals(adjustment)) {
hostPriorities.put(hostId, DEFAULT_HOST_PRIORITY);
} else if (HostPriorityAdjustment.PROHIBIT.equals(adjustment)) {
hostPriorities.put(hostId, PROHIBITED_HOST_PRIORITY);
}
}
@Override
public Map<Long, Integer> getHostPriorities() {
return hostPriorities;
}
@Override
public void setHostPriorities(Map<Long, Integer> priorities) {
this.hostPriorities = priorities;
}
}

View File

@ -20,10 +20,23 @@ import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.vm.ReservationContext;
import java.util.List;
import java.util.Map;
/**
*/
public interface DeploymentPlan {
Integer DEFAULT_HOST_PRIORITY = 0;
Integer PROHIBITED_HOST_PRIORITY = Integer.MIN_VALUE;
Integer ADJUST_HOST_PRIORITY_BY = 1;
enum HostPriorityAdjustment {
HIGHER,
DEFAULT,
LOWER,
PROHIBIT
}
// TODO: This interface is not fully developed. It really
// number of parameters to be specified.
@ -73,4 +86,10 @@ public interface DeploymentPlan {
List<Long> getPreferredHosts();
boolean isMigrationPlan();
void adjustHostPriority(Long hostId, HostPriorityAdjustment priority);
Map<Long, Integer> getHostPriorities();
void setHostPriorities(Map<Long, Integer> priorities);
}

View File

@ -75,6 +75,7 @@ public interface VirtualMachineProfile {
public static final Param BootType = new Param("BootType");
public static final Param BootIntoSetup = new Param("enterHardwareSetup");
public static final Param PreserveNics = new Param("PreserveNics");
public static final Param ConsiderLastHost = new Param("ConsiderLastHost");
private String name;

View File

@ -634,6 +634,7 @@ public class ApiConstants {
public static final String PURPOSE = "purpose";
public static final String IS_TAGGED = "istagged";
public static final String INSTANCE_NAME = "instancename";
public static final String CONSIDER_LAST_HOST = "considerlasthost";
public static final String START_VM = "startvm";
public static final String HA_HOST = "hahost";
public static final String CUSTOM_DISK_OFF_MIN_SIZE = "customdiskofferingminsize";

View File

@ -82,6 +82,13 @@ public class StartVMCmd extends BaseAsyncCmd implements UserCmd {
since = "3.0.1")
private Long hostId;
@Parameter(name = ApiConstants.CONSIDER_LAST_HOST,
type = CommandType.BOOLEAN,
description = "True by default, CloudStack will firstly try to start the VM on the last host where it run on before stopping, if destination host is not specified. " +
"If false, CloudStack will not consider the last host and start the VM by normal process.",
since = "4.18.0")
private Boolean considerLastHost;
@Parameter(name = ApiConstants.DEPLOYMENT_PLANNER, type = CommandType.STRING, description = "Deployment planner to use for vm allocation. Available to ROOT admin only", since = "4.4", authorized = { RoleType.Admin })
private String deploymentPlanner;
@ -112,6 +119,10 @@ public class StartVMCmd extends BaseAsyncCmd implements UserCmd {
return bootIntoSetup;
}
public Boolean getConsiderLastHost() {
return considerLastHost;
}
// ///////////////////////////////////////////////////
// ///////////// API Implementation///////////////////
// ///////////////////////////////////////////////////

View File

@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.cloud.deploy;
import junit.framework.TestCase;
import org.junit.Assert;
public class DataCenterDeploymentTest extends TestCase {
private long zoneId = 1L;
private long hostId = 2L;
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
private void verifyHostPriority(Integer priority) {
Assert.assertEquals(priority, plan.getHostPriorities().get(hostId));
}
public void testHostPriorities() {
verifyHostPriority(null);
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.DEFAULT);
verifyHostPriority(0);
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER);
verifyHostPriority(1);
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.LOWER);
verifyHostPriority(0);
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.LOWER);
verifyHostPriority(-1);
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER);
verifyHostPriority(0);
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER);
verifyHostPriority(1);
}
}

View File

@ -478,6 +478,16 @@
<artifactId>cloud-plugin-host-affinity</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-non-strict-host-anti-affinity</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-non-strict-host-affinity</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-api-solidfire-intg-test</artifactId>

View File

@ -248,7 +248,7 @@
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
<property name="orderConfigKey" value="affinity.processors.order" />
<property name="orderConfigDefault"
value="HostAntiAffinityProcessor,ExplicitDedicationProcessor,HostAffinityProcessor" />
value="HostAntiAffinityProcessor,ExplicitDedicationProcessor,HostAffinityProcessor,NonStrictHostAntiAffinityProcessor,NonStrictHostAffinityProcessor" />
<property name="excludeKey" value="affinity.processors.exclude" />
</bean>

View File

@ -20,10 +20,14 @@ import com.cloud.dc.DataCenter;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.exception.AffinityConflictException;
import com.cloud.exception.InsufficientServerCapacityException;
import com.cloud.host.Host;
import com.cloud.utils.component.Manager;
import com.cloud.vm.VirtualMachineProfile;
import org.apache.cloudstack.framework.config.ConfigKey;
import java.util.List;
import java.util.Map;
public interface DeploymentPlanningManager extends Manager {
@ -60,4 +64,6 @@ public interface DeploymentPlanningManager extends Manager {
DeploymentPlanner getDeploymentPlannerByName(String plannerName);
void checkForNonDedicatedResources(VirtualMachineProfile vmProfile, DataCenter dc, ExcludeList avoids);
void reorderHostsByPriority(Map<Long, Integer> priorities, List<Host> hosts);
}

View File

@ -1412,6 +1412,10 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
msgBuf.append(String.format("Boot into Setup: %s ", params.get(VirtualMachineProfile.Param.BootIntoSetup)));
log = true;
}
if (params.get(VirtualMachineProfile.Param.ConsiderLastHost) != null) {
msgBuf.append(String.format("Consider last host: %s ", params.get(VirtualMachineProfile.Param.ConsiderLastHost)));
log = true;
}
if (log) {
s_logger.info(msgBuf.toString());
}

View File

@ -158,6 +158,9 @@ public class VMEntityManagerImpl implements VMEntityManager {
vmProfile.getParameters().put(VirtualMachineProfile.Param.BootMode, details.get(VirtualMachineProfile.Param.BootMode.getName()));
vmProfile.getParameters().put(VirtualMachineProfile.Param.UefiFlag, details.get(VirtualMachineProfile.Param.UefiFlag.getName()));
}
if (MapUtils.isNotEmpty(vmEntityVO.getDetails()) && vmEntityVO.getDetails().containsKey(VirtualMachineProfile.Param.ConsiderLastHost.getName())) {
vmProfile.getParameters().put(VirtualMachineProfile.Param.ConsiderLastHost, vmEntityVO.getDetails().get(VirtualMachineProfile.Param.ConsiderLastHost.getName()));
}
DataCenterDeployment plan = new DataCenterDeployment(vm.getDataCenterId(), vm.getPodIdToDeployIn(), null, null, null, null);
if (planToDeploy != null && planToDeploy.getDataCenterId() != 0) {
plan =

View File

@ -0,0 +1,30 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-plugin-non-strict-host-affinity</artifactId>
<name>Apache CloudStack Plugin - Non-Strict Host Affinity Processor</name>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-plugins</artifactId>
<version>4.18.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
</project>

View File

@ -0,0 +1,133 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.affinity;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.log4j.Logger;
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.framework.config.dao.ConfigurationDao;
import com.cloud.configuration.Config;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.exception.AffinityConflictException;
import com.cloud.utils.DateUtil;
import com.cloud.utils.NumbersUtil;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
public class NonStrictHostAffinityProcessor extends AffinityProcessorBase implements AffinityGroupProcessor {
private final Logger logger = Logger.getLogger(this.getClass().getName());
@Inject
protected UserVmDao vmDao;
@Inject
protected VMInstanceDao vmInstanceDao;
@Inject
protected AffinityGroupDao affinityGroupDao;
@Inject
protected AffinityGroupVMMapDao affinityGroupVMMapDao;
@Inject
protected ConfigurationDao configDao;
@Inject
protected VMReservationDao reservationDao;
private int vmCapacityReleaseInterval;
@Override
public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException {
VirtualMachine vm = vmProfile.getVirtualMachine();
List<AffinityGroupVMMapVO> vmGroupMappings = affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType());
for (AffinityGroupVMMapVO vmGroupMapping : vmGroupMappings) {
if (vmGroupMapping != null) {
processAffinityGroup(vmGroupMapping, plan, vm);
}
}
}
protected void processAffinityGroup(AffinityGroupVMMapVO vmGroupMapping, DeploymentPlan plan, VirtualMachine vm) {
AffinityGroupVO group = affinityGroupDao.findById(vmGroupMapping.getAffinityGroupId());
if (logger.isDebugEnabled()) {
logger.debug("Processing affinity group " + group.getName() + " for VM Id: " + vm.getId());
}
List<Long> groupVMIds = affinityGroupVMMapDao.listVmIdsByAffinityGroup(group.getId());
groupVMIds.remove(vm.getId());
for (Long groupVMId : groupVMIds) {
VMInstanceVO groupVM = vmInstanceDao.findById(groupVMId);
if (groupVM != null && !groupVM.isRemoved()) {
processVmInAffinityGroup(plan, groupVM);
}
}
}
protected void processVmInAffinityGroup(DeploymentPlan plan, VMInstanceVO groupVM) {
if (groupVM.getHostId() != null) {
Integer priority = adjustHostPriority(plan, groupVM.getHostId());
if (logger.isDebugEnabled()) {
logger.debug(String.format("Updated host %s priority to %s , since VM %s is present on the host",
groupVM.getHostId(), priority, groupVM.getId()));
}
} else if (Arrays.asList(VirtualMachine.State.Starting, VirtualMachine.State.Stopped).contains(groupVM.getState()) && groupVM.getLastHostId() != null) {
long secondsSinceLastUpdate = (DateUtil.currentGMTTime().getTime() - groupVM.getUpdateTime().getTime()) / 1000;
if (secondsSinceLastUpdate < vmCapacityReleaseInterval) {
Integer priority = adjustHostPriority(plan, groupVM.getLastHostId());
if (logger.isDebugEnabled()) {
logger.debug(String.format("Updated host %s priority to %s , since VM %s" +
" is present on the host, in %s state but has reserved capacity",
groupVM.getLastHostId(), priority, groupVM.getId(), groupVM.getState()));
}
}
}
}
protected Integer adjustHostPriority(DeploymentPlan plan, Long hostId) {
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER);
return plan.getHostPriorities().get(hostId);
}
@Override
public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
super.configure(name, params);
vmCapacityReleaseInterval = NumbersUtil.parseInt(configDao.getValue(Config.CapacitySkipcountingHours.key()), 3600);
return true;
}
@Override
public boolean check(VirtualMachineProfile vmProfile, DeployDestination plannedDestination) throws AffinityConflictException {
return true;
}
}

View File

@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name=non-strict-host-affinity
parent=planner

View File

@ -0,0 +1,37 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
>
<bean id="NonStrictHostAffinityProcessor"
class="org.apache.cloudstack.affinity.NonStrictHostAffinityProcessor">
<property name="name" value="NonStrictHostAffinityProcessor" />
<property name="type" value="non-strict host affinity" />
</bean>
</beans>

View File

@ -0,0 +1,172 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.affinity;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.VMInstanceDao;
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.when;
@RunWith(PowerMockRunner.class)
public class NonStrictHostAffinityProcessorTest {
@Spy
@InjectMocks
NonStrictHostAffinityProcessor processor = new NonStrictHostAffinityProcessor();
@Mock
AffinityGroupVMMapDao _affinityGroupVMMapDao;
@Mock
AffinityGroupDao affinityGroupDao;
@Mock
VMInstanceDao vmInstanceDao;
long vmId = 10L;
long vm2Id = 11L;
long vm3Id = 12L;
long affinityGroupId = 20L;
long zoneId = 2L;
long host2Id = 3L;
long host3Id = 4L;
@Test
public void testProcessWithEmptyPlan() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
when(vm.getId()).thenReturn(vmId);
VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class);
when(vmProfile.getVirtualMachine()).thenReturn(vm);
List<AffinityGroupVMMapVO> vmGroupMappings = new ArrayList<>();
vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId));
when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings);
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
ExcludeList avoid = new ExcludeList();
AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class);
when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO);
when(affinityGroupVO.getId()).thenReturn(affinityGroupId);
List<Long> groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id));
when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds);
VMInstanceVO vm2 = new VMInstanceVO();
when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2);
vm2.setHostId(host2Id);
processor.process(vmProfile, plan, avoid);
Assert.assertEquals(1, plan.getHostPriorities().size());
Assert.assertNotNull(plan.getHostPriorities().get(host2Id));
Assert.assertEquals(Integer.valueOf(1), plan.getHostPriorities().get(host2Id));
}
@Test
public void testProcessWithPlan() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
when(vm.getId()).thenReturn(vmId);
VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class);
when(vmProfile.getVirtualMachine()).thenReturn(vm);
List<AffinityGroupVMMapVO> vmGroupMappings = new ArrayList<>();
vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId));
when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings);
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
plan.adjustHostPriority(host2Id, DeploymentPlan.HostPriorityAdjustment.DEFAULT);
plan.adjustHostPriority(host3Id, DeploymentPlan.HostPriorityAdjustment.LOWER);
ExcludeList avoid = new ExcludeList();
AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class);
when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO);
when(affinityGroupVO.getId()).thenReturn(affinityGroupId);
List<Long> groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id, vm3Id));
when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds);
VMInstanceVO vm2 = new VMInstanceVO();
when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2);
vm2.setHostId(host2Id);
VMInstanceVO vm3 = new VMInstanceVO();
when(vmInstanceDao.findById(vm3Id)).thenReturn(vm3);
vm3.setHostId(host3Id);
processor.process(vmProfile, plan, avoid);
Assert.assertEquals(2, plan.getHostPriorities().size());
Assert.assertNotNull(plan.getHostPriorities().get(host2Id));
Assert.assertEquals(Integer.valueOf(1), plan.getHostPriorities().get(host2Id));
Assert.assertNotNull(plan.getHostPriorities().get(host3Id));
Assert.assertEquals(Integer.valueOf(0), plan.getHostPriorities().get(host3Id));
}
@Test
public void testProcessWithNotRunningVM() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
when(vm.getId()).thenReturn(vmId);
VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class);
when(vmProfile.getVirtualMachine()).thenReturn(vm);
List<AffinityGroupVMMapVO> vmGroupMappings = new ArrayList<>();
vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId));
when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings);
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
ExcludeList avoid = new ExcludeList();
AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class);
when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO);
when(affinityGroupVO.getId()).thenReturn(affinityGroupId);
List<Long> groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id));
when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds);
VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2);
when(vm2.getHostId()).thenReturn(null);
when(vm2.getLastHostId()).thenReturn(host2Id);
when(vm2.getState()).thenReturn(VirtualMachine.State.Starting);
when(vm2.getUpdateTime()).thenReturn(new Date());
ReflectionTestUtils.setField(processor, "vmCapacityReleaseInterval", 3600);
processor.process(vmProfile, plan, avoid);
Assert.assertEquals(1, plan.getHostPriorities().size());
Assert.assertNotNull(plan.getHostPriorities().get(host2Id));
Assert.assertEquals(Integer.valueOf(1), plan.getHostPriorities().get(host2Id));
}
}

View File

@ -0,0 +1,38 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-plugin-non-strict-host-anti-affinity</artifactId>
<name>Apache CloudStack Plugin - Non-Strict Host Anti-Affinity Processor</name>
<dependencies>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-non-strict-host-affinity</artifactId>
<version>4.18.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-plugins</artifactId>
<version>4.18.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
</project>

View File

@ -0,0 +1,28 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.affinity;
import com.cloud.deploy.DeploymentPlan;
public class NonStrictHostAntiAffinityProcessor extends NonStrictHostAffinityProcessor {
@Override
protected Integer adjustHostPriority(DeploymentPlan plan, Long hostId) {
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.LOWER);
return plan.getHostPriorities().get(hostId);
}
}

View File

@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name=non-strict-host-anti-affinity
parent=planner

View File

@ -0,0 +1,37 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
>
<bean id="NonStrictHostAntiAffinityProcessor"
class="org.apache.cloudstack.affinity.NonStrictHostAntiAffinityProcessor">
<property name="name" value="NonStrictHostAntiAffinityProcessor" />
<property name="type" value="non-strict host anti-affinity" />
</bean>
</beans>

View File

@ -0,0 +1,172 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.affinity;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.VMInstanceDao;
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.when;
@RunWith(PowerMockRunner.class)
public class NonStrictHostAntiAffinityProcessorTest {
@Spy
@InjectMocks
NonStrictHostAntiAffinityProcessor processor = new NonStrictHostAntiAffinityProcessor();
@Mock
AffinityGroupVMMapDao _affinityGroupVMMapDao;
@Mock
AffinityGroupDao affinityGroupDao;
@Mock
VMInstanceDao vmInstanceDao;
long vmId = 10L;
long vm2Id = 11L;
long vm3Id = 12L;
long affinityGroupId = 20L;
long zoneId = 2L;
long host2Id = 3L;
long host3Id = 4L;
@Test
public void testProcessWithEmptyPlan() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
when(vm.getId()).thenReturn(vmId);
VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class);
when(vmProfile.getVirtualMachine()).thenReturn(vm);
List<AffinityGroupVMMapVO> vmGroupMappings = new ArrayList<>();
vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId));
when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings);
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
ExcludeList avoid = new ExcludeList();
AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class);
when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO);
when(affinityGroupVO.getId()).thenReturn(affinityGroupId);
List<Long> groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id));
when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds);
VMInstanceVO vm2 = new VMInstanceVO();
when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2);
vm2.setHostId(host2Id);
processor.process(vmProfile, plan, avoid);
Assert.assertEquals(1, plan.getHostPriorities().size());
Assert.assertNotNull(plan.getHostPriorities().get(host2Id));
Assert.assertEquals(Integer.valueOf(-1), plan.getHostPriorities().get(host2Id));
}
@Test
public void testProcessWithPlan() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
when(vm.getId()).thenReturn(vmId);
VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class);
when(vmProfile.getVirtualMachine()).thenReturn(vm);
List<AffinityGroupVMMapVO> vmGroupMappings = new ArrayList<>();
vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId));
when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings);
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
plan.adjustHostPriority(host2Id, DeploymentPlan.HostPriorityAdjustment.DEFAULT);
plan.adjustHostPriority(host3Id, DeploymentPlan.HostPriorityAdjustment.HIGHER);
ExcludeList avoid = new ExcludeList();
AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class);
when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO);
when(affinityGroupVO.getId()).thenReturn(affinityGroupId);
List<Long> groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id, vm3Id));
when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds);
VMInstanceVO vm2 = new VMInstanceVO();
when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2);
vm2.setHostId(host2Id);
VMInstanceVO vm3 = new VMInstanceVO();
when(vmInstanceDao.findById(vm3Id)).thenReturn(vm3);
vm3.setHostId(host3Id);
processor.process(vmProfile, plan, avoid);
Assert.assertEquals(2, plan.getHostPriorities().size());
Assert.assertNotNull(plan.getHostPriorities().get(host2Id));
Assert.assertEquals(Integer.valueOf(-1), plan.getHostPriorities().get(host2Id));
Assert.assertNotNull(plan.getHostPriorities().get(host3Id));
Assert.assertEquals(Integer.valueOf(0), plan.getHostPriorities().get(host3Id));
}
@Test
public void testProcessWithNotRunningVM() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
when(vm.getId()).thenReturn(vmId);
VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class);
when(vmProfile.getVirtualMachine()).thenReturn(vm);
List<AffinityGroupVMMapVO> vmGroupMappings = new ArrayList<>();
vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId));
when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings);
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
ExcludeList avoid = new ExcludeList();
AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class);
when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO);
when(affinityGroupVO.getId()).thenReturn(affinityGroupId);
List<Long> groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id));
when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds);
VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2);
when(vm2.getHostId()).thenReturn(null);
when(vm2.getLastHostId()).thenReturn(host2Id);
when(vm2.getState()).thenReturn(VirtualMachine.State.Starting);
when(vm2.getUpdateTime()).thenReturn(new Date());
ReflectionTestUtils.setField(processor, "vmCapacityReleaseInterval", 3600);
processor.process(vmProfile, plan, avoid);
Assert.assertEquals(1, plan.getHostPriorities().size());
Assert.assertNotNull(plan.getHostPriorities().get(host2Id));
Assert.assertEquals(Integer.valueOf(-1), plan.getHostPriorities().get(host2Id));
}
}

View File

@ -50,6 +50,8 @@
<module>affinity-group-processors/explicit-dedication</module>
<module>affinity-group-processors/host-affinity</module>
<module>affinity-group-processors/host-anti-affinity</module>
<module>affinity-group-processors/non-strict-host-affinity</module>
<module>affinity-group-processors/non-strict-host-anti-affinity</module>
<module>alert-handlers/snmp-alerts</module>
<module>alert-handlers/syslog-alerts</module>

View File

@ -17,6 +17,7 @@
package com.cloud.deploy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@ -382,6 +383,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable {
if (s_logger.isDebugEnabled()) {
s_logger.debug("Deploy avoids pods: " + avoids.getPodsToAvoid() + ", clusters: " + avoids.getClustersToAvoid() + ", hosts: " + avoids.getHostsToAvoid());
s_logger.debug("Deploy hosts with priorities " + plan.getHostPriorities() + " , hosts have NORMAL priority by default");
}
// call planners
@ -406,7 +408,10 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable {
planner = getDeploymentPlannerByName(plannerName);
}
if (vm.getLastHostId() != null && haVmTag == null) {
String considerLastHostStr = (String)vmProfile.getParameter(VirtualMachineProfile.Param.ConsiderLastHost);
boolean considerLastHost = vm.getLastHostId() != null && haVmTag == null &&
(considerLastHostStr == null || Boolean.TRUE.toString().equalsIgnoreCase(considerLastHostStr));
if (considerLastHost) {
s_logger.debug("This VM has last host_id specified, trying to choose the same host: " + vm.getLastHostId());
HostVO host = _hostDao.findById(vm.getLastHostId());
@ -1222,6 +1227,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable {
// cluster.
DataCenterDeployment potentialPlan =
new DataCenterDeployment(plan.getDataCenterId(), clusterVO.getPodId(), clusterVO.getId(), null, plan.getPoolId(), null, plan.getReservationContext());
potentialPlan.setHostPriorities(plan.getHostPriorities());
Pod pod = _podDao.findById(clusterVO.getPodId());
if (CollectionUtils.isNotEmpty(avoid.getPodsToAvoid()) && avoid.getPodsToAvoid().contains(pod.getId())) {
@ -1588,9 +1594,35 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, Configurable {
if (suitableHosts.isEmpty()) {
s_logger.debug("No suitable hosts found");
}
// re-order hosts by priority
reorderHostsByPriority(plan.getHostPriorities(), suitableHosts);
return suitableHosts;
}
@Override
public void reorderHostsByPriority(Map<Long, Integer> priorities, List<Host> hosts) {
s_logger.info("Re-ordering hosts " + hosts + " by priorities " + priorities);
hosts.removeIf(host -> DataCenterDeployment.PROHIBITED_HOST_PRIORITY.equals(getHostPriority(priorities, host.getId())));
Collections.sort(hosts, new Comparator<>() {
@Override
public int compare(Host host1, Host host2) {
int res = getHostPriority(priorities, host1.getId()).compareTo(getHostPriority(priorities, host2.getId()));
return -res;
}
}
);
s_logger.info("Hosts after re-ordering are: " + hosts);
}
private Integer getHostPriority(Map<Long, Integer> priorities, Long hostId) {
return priorities.get(hostId) != null ? priorities.get(hostId) : DeploymentPlan.DEFAULT_HOST_PRIORITY;
}
protected Pair<Map<Volume, List<StoragePool>>, List<Volume>> findSuitablePoolsForVolumes(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid,
int returnUpTo) {
List<VolumeVO> volumesTobeCreated = _volsDao.findUsableVolumesForInstance(vmProfile.getId());

View File

@ -1508,6 +1508,9 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
}
}
// re-order hosts by priority
_dpMgr.reorderHostsByPriority(plan.getHostPriorities(), suitableHosts);
if (s_logger.isDebugEnabled()) {
if (suitableHosts.isEmpty()) {
s_logger.debug("No suitable hosts found");

View File

@ -3225,6 +3225,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
if (uefiDetail != null) {
addVmUefiBootOptionsToParams(additonalParams, uefiDetail.getName(), uefiDetail.getValue());
}
if (cmd.getConsiderLastHost() != null) {
additonalParams.put(VirtualMachineProfile.Param.ConsiderLastHost, cmd.getConsiderLastHost().toString());
}
return startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), additonalParams, cmd.getDeploymentPlanner()).first();
}

View File

@ -127,8 +127,9 @@ public class AffinityGroupServiceImpl extends ManagerBase implements AffinityGro
AffinityGroupProcessor processor = typeProcessorMap.get(affinityGroupType);
if(processor == null){
throw new InvalidParameterValueException("Unable to create affinity group, invalid affinity group type" + affinityGroupType);
if (processor == null) {
throw new InvalidParameterValueException(String.format("Unable to create affinity group, invalid affinity group type: %s. " +
"Valid values are %s", affinityGroupType, String.join(",", typeProcessorMap.keySet())));
}
Account caller = CallContext.current().getCallingAccount();

View File

@ -24,6 +24,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -1029,4 +1030,49 @@ public class DeploymentPlanningManagerImplTest {
}
}
}
@Test
public void testReorderHostsByPriority() {
Map<Long, Integer> priorities = new LinkedHashMap<>();
priorities.put(1L, 3);
priorities.put(2L, -6);
priorities.put(3L, 5);
priorities.put(5L, 8);
priorities.put(6L, -1);
priorities.put(8L, 5);
priorities.put(9L, DataCenterDeployment.PROHIBITED_HOST_PRIORITY);
Host host1 = Mockito.mock(Host.class);
Mockito.when(host1.getId()).thenReturn(1L);
Host host2 = Mockito.mock(Host.class);
Mockito.when(host2.getId()).thenReturn(2L);
Host host3 = Mockito.mock(Host.class);
Mockito.when(host3.getId()).thenReturn(3L);
Host host4 = Mockito.mock(Host.class);
Mockito.when(host4.getId()).thenReturn(4L);
Host host5 = Mockito.mock(Host.class);
Mockito.when(host5.getId()).thenReturn(5L);
Host host6 = Mockito.mock(Host.class);
Mockito.when(host6.getId()).thenReturn(6L);
Host host7 = Mockito.mock(Host.class);
Mockito.when(host7.getId()).thenReturn(7L);
Host host8 = Mockito.mock(Host.class);
Mockito.when(host8.getId()).thenReturn(8L);
Host host9 = Mockito.mock(Host.class);
Mockito.when(host9.getId()).thenReturn(9L);
List<Host> hosts = new ArrayList<>(Arrays.asList(host1, host2, host3, host4, host5, host6, host7, host8, host9));
_dpm.reorderHostsByPriority(priorities, hosts);
Assert.assertEquals(8, hosts.size());
Assert.assertEquals(5, hosts.get(0).getId());
Assert.assertEquals(3, hosts.get(1).getId());
Assert.assertEquals(8, hosts.get(2).getId());
Assert.assertEquals(1, hosts.get(3).getId());
Assert.assertEquals(4, hosts.get(4).getId());
Assert.assertEquals(7, hosts.get(5).getId());
Assert.assertEquals(6, hosts.get(6).getId());
Assert.assertEquals(2, hosts.get(7).getId());
}
}

View File

@ -0,0 +1,448 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Tests of Non-Strict (host anti-affinity and host affinity) affinity groups
"""
import logging
from nose.plugins.attrib import attr
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.cloudstackAPI import startVirtualMachine, stopVirtualMachine, destroyVirtualMachine
from marvin.lib.base import (Account,
AffinityGroup,
Domain,
Host,
ServiceOffering,
VirtualMachine,
Zone,
Network,
NetworkOffering)
from marvin.lib.common import (get_domain,
get_zone,
get_template)
class TestNonStrictAffinityGroups(cloudstackTestCase):
"""
Test Non-Strict (host anti-affinity and host affinity) affinity groups
"""
@classmethod
def setUpClass(cls):
cls.testClient = super(
TestNonStrictAffinityGroups,
cls).getClsTestClient()
cls.apiclient = cls.testClient.getApiClient()
cls.services = cls.testClient.getParsedTestDataConfig()
zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests())
cls.zone = Zone(zone.__dict__)
cls.template = get_template(cls.apiclient, cls.zone.id)
cls._cleanup = []
cls.logger = logging.getLogger("TestNonStrictAffinityGroups")
cls.stream_handler = logging.StreamHandler()
cls.logger.setLevel(logging.DEBUG)
cls.logger.addHandler(cls.stream_handler)
cls.skipTests = False
hosts = Host.list(
cls.apiclient,
zoneid=cls.zone.id,
state='Up',
resourcestate='Enabled'
)
if not hosts or not isinstance(hosts, list) or len(hosts) < 2:
cls.logger.debug("This test requires at least two (Up and Enabled) hosts in the zone")
cls.skipTests = True
return
cls.domain = get_domain(cls.apiclient)
# 1. Create small service offering
cls.service_offering = ServiceOffering.create(
cls.apiclient,
cls.services["service_offerings"]["small"]
)
cls._cleanup.append(cls.service_offering)
# 3. Create network offering for isolated networks
cls.network_offering_isolated = NetworkOffering.create(
cls.apiclient,
cls.services["isolated_network_offering"]
)
cls.network_offering_isolated.update(cls.apiclient, state='Enabled')
cls._cleanup.append(cls.network_offering_isolated)
# 4. Create sub-domain
cls.sub_domain = Domain.create(
cls.apiclient,
cls.services["acl"]["domain1"]
)
cls._cleanup.append(cls.sub_domain)
# 5. Create regular user
cls.regular_user = Account.create(
cls.apiclient,
cls.services["acl"]["accountD11A"],
domainid=cls.sub_domain.id
)
cls._cleanup.append(cls.regular_user)
# 5. Create api clients for regular user
cls.regular_user_user = cls.regular_user.user[0]
cls.regular_user_apiclient = cls.testClient.getUserApiClient(
cls.regular_user_user.username, cls.sub_domain.name
)
# 7. Create network for regular user
cls.services["network"]["name"] = "Test Network Isolated - Regular user"
cls.user_network = Network.create(
cls.regular_user_apiclient,
cls.services["network"],
networkofferingid=cls.network_offering_isolated.id,
zoneid=cls.zone.id
)
@classmethod
def tearDownClass(cls):
super(TestNonStrictAffinityGroups, cls).tearDownClass()
def setUp(self):
if self.skipTests:
self.skipTest("This test requires at least two (Up and Enabled) hosts in the zone")
self.apiclient = self.testClient.getApiClient()
self.cleanup = []
def tearDown(self):
super(TestNonStrictAffinityGroups, self).tearDown()
@classmethod
def get_vm_host_id(cls, vm_id):
list_vms = VirtualMachine.list(
cls.apiclient,
id=vm_id
)
vm = list_vms[0]
return vm.hostid
@attr(tags=["advanced"], required_hardware="false")
def test_01_non_strict_host_anti_affinity(self):
""" Verify Non-Strict host anti-affinity """
# 1. Create Non-Strict host anti-affinity
# 2. Deploy vm-1 with the group
# 3. Deploy vm-2 with the group. It will be started on different host if there are multiple hosts.
# 4. Migrate vm-2 to same host as vm-1
# 5. Stop vm-2, start vm-2. It will be started on same host as vm-1
# 6. Stop vm-2, start vm-2 with considerlasthost=false. It will be started on different host as vm-1
# 7. Deploy vm-3 with same host, vm-3 should be started on specified host.
# 8. Deploy vm-4 with startvm=false, then start the VM.
# vm-4 should be started on different host if there are multiple hosts.
self.logger.debug("=== Running test_01_non_strict_host_anti_affinity ===")
# 1. Create Non-Strict host anti-affinity
affinity_group_params = {
"name": "Test affinity group",
"type": "non-strict host anti-affinity",
}
self.affinity_group = AffinityGroup.create(
self.regular_user_apiclient,
affinity_group_params
)
self.cleanup.append(self.affinity_group)
# 2. Deploy vm-1 with the group
self.services["virtual_machine"]["name"] = "virtual-machine-1"
self.services["virtual_machine"]["displayname"] = "virtual-machine-1"
self.virtual_machine_1 = VirtualMachine.create(
self.regular_user_apiclient,
self.services["virtual_machine"],
serviceofferingid=self.service_offering.id,
templateid=self.template.id,
zoneid=self.zone.id,
networkids=self.user_network.id,
affinitygroupids=self.affinity_group.id
)
self.cleanup.append(self.virtual_machine_1)
vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id)
# 3. Deploy vm-2 with the group. It will be started on different host if there are multiple hosts.
self.services["virtual_machine"]["name"] = "virtual-machine-2"
self.services["virtual_machine"]["displayname"] = "virtual-machine-2"
self.virtual_machine_2 = VirtualMachine.create(
self.regular_user_apiclient,
self.services["virtual_machine"],
serviceofferingid=self.service_offering.id,
templateid=self.template.id,
zoneid=self.zone.id,
networkids=self.user_network.id,
affinitygroupids=self.affinity_group.id
)
vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id)
self.assertNotEqual(vm_1_host_id,
vm_2_host_id,
msg="Both VMs of affinity group %s are on the same host" % self.affinity_group.name)
# 4. Migrate vm-2 to same host as vm-1
self.virtual_machine_2.migrate(
self.apiclient,
hostid=vm_1_host_id
)
# 5. Stop vm-2, start vm-2. It will be started on same host as vm-1
stopCmd = stopVirtualMachine.stopVirtualMachineCmd()
stopCmd.id = self.virtual_machine_2.id
stopCmd.forced = True
self.apiclient.stopVirtualMachine(stopCmd)
startCmd = startVirtualMachine.startVirtualMachineCmd()
startCmd.id = self.virtual_machine_2.id
self.apiclient.startVirtualMachine(startCmd)
vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id)
self.assertEqual(vm_1_host_id,
vm_2_host_id,
msg="Both VMs of affinity group %s are on the different host" % self.affinity_group.name)
# 6. Stop vm-2, start vm-2 with considerlasthost=false. It will be started on different host as vm-1
stopCmd.id = self.virtual_machine_2.id
stopCmd.forced = True
self.apiclient.stopVirtualMachine(stopCmd)
startCmd = startVirtualMachine.startVirtualMachineCmd()
startCmd.id = self.virtual_machine_2.id
startCmd.considerlasthost = False
self.apiclient.startVirtualMachine(startCmd)
vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id)
self.assertNotEqual(vm_1_host_id,
vm_2_host_id,
msg="Both VMs of affinity group %s are on the same host" % self.affinity_group.name)
destroyCmd = destroyVirtualMachine.destroyVirtualMachineCmd()
destroyCmd.id = self.virtual_machine_2.id
destroyCmd.expunge = True
self.apiclient.destroyVirtualMachine(destroyCmd)
# 7. Deploy vm-3 with same host, vm-3 should be started on specified host.
self.services["virtual_machine"]["name"] = "virtual-machine-3"
self.services["virtual_machine"]["displayname"] = "virtual-machine-3"
self.virtual_machine_3 = VirtualMachine.create(
self.apiclient,
self.services["virtual_machine"],
serviceofferingid=self.service_offering.id,
templateid=self.template.id,
zoneid=self.zone.id,
networkids=self.user_network.id,
affinitygroupids=self.affinity_group.id,
domainid=self.sub_domain.id,
accountid=self.regular_user.name,
hostid=vm_1_host_id
)
vm_3_host_id = self.get_vm_host_id(self.virtual_machine_3.id)
self.assertEqual(vm_1_host_id,
vm_3_host_id,
msg="virtual-machine-3 should be started on %s" % vm_1_host_id)
destroyCmd.id = self.virtual_machine_3.id
destroyCmd.expunge = True
self.apiclient.destroyVirtualMachine(destroyCmd)
# 8. Deploy vm-4 with startvm=false, then start the VM.
# vm-4 should be started on different host if there are multiple hosts.
self.services["virtual_machine"]["name"] = "virtual-machine-4"
self.services["virtual_machine"]["displayname"] = "virtual-machine-4"
self.virtual_machine_4 = VirtualMachine.create(
self.regular_user_apiclient,
self.services["virtual_machine"],
serviceofferingid=self.service_offering.id,
templateid=self.template.id,
zoneid=self.zone.id,
networkids=self.user_network.id,
affinitygroupids=self.affinity_group.id,
startvm=False
)
self.cleanup.append(self.virtual_machine_4)
startCmd.id = self.virtual_machine_4.id
startCmd.considerlasthost = None
self.apiclient.startVirtualMachine(startCmd)
vm_4_host_id = self.get_vm_host_id(self.virtual_machine_4.id)
self.assertNotEqual(vm_1_host_id,
vm_4_host_id,
msg="virtual-machine-4 should be not started on %s" % vm_1_host_id)
@attr(tags=["advanced"], required_hardware="false")
def test_02_non_strict_host_affinity(self):
""" Verify Non-Strict host affinity """
# 1. Create Non-Strict host affinity
# 2. Deploy vm-11 with the group
# 3. Deploy vm-12 with the group. It will be started on same host.
# 4. Migrate vm-12 to different host as vm-11
# 5. Stop vm-12, start vm-12. It will be started on different host as vm-11
# 6. Stop vm-12, start vm-12 with considerlasthost=false. It will be started on same host as vm-11
# 7. Deploy vm-13 with different host, vm-13 should be started on specified host.
# 8. Deploy vm-14 with startvm=false, then start the VM. vm-14 should be started on same host.
self.logger.debug("=== Running test_02_non_strict_host_affinity ===")
# 1. Create Non-Strict host affinity
affinity_group_params = {
"name": "Test affinity group",
"type": "non-strict host affinity",
}
self.affinity_group = AffinityGroup.create(
self.regular_user_apiclient,
affinity_group_params
)
self.cleanup.append(self.affinity_group)
# 2. Deploy vm-11 with the group
self.services["virtual_machine"]["name"] = "virtual-machine-11"
self.services["virtual_machine"]["displayname"] = "virtual-machine-11"
self.virtual_machine_11 = VirtualMachine.create(
self.regular_user_apiclient,
self.services["virtual_machine"],
serviceofferingid=self.service_offering.id,
templateid=self.template.id,
zoneid=self.zone.id,
networkids=self.user_network.id,
affinitygroupids=self.affinity_group.id
)
self.cleanup.append(self.virtual_machine_11)
vm_11_host_id = self.get_vm_host_id(self.virtual_machine_11.id)
# 3. Deploy vm-12 with the group. It will be started on same host.
self.services["virtual_machine"]["name"] = "virtual-machine-12"
self.services["virtual_machine"]["displayname"] = "virtual-machine-12"
self.virtual_machine_12 = VirtualMachine.create(
self.regular_user_apiclient,
self.services["virtual_machine"],
serviceofferingid=self.service_offering.id,
templateid=self.template.id,
zoneid=self.zone.id,
networkids=self.user_network.id,
affinitygroupids=self.affinity_group.id
)
vm_12_host_id = self.get_vm_host_id(self.virtual_machine_12.id)
self.assertEqual(vm_11_host_id,
vm_12_host_id,
msg="Both VMs of affinity group %s are on the different host" % self.affinity_group.name)
# 4. Migrate vm-12 to different host as vm-11
self.virtual_machine_12.migrate(
self.apiclient
)
# 5. Stop vm-12, start vm-12. It will be started on different host as vm-11
stopCmd = stopVirtualMachine.stopVirtualMachineCmd()
stopCmd.id = self.virtual_machine_12.id
stopCmd.forced = True
self.apiclient.stopVirtualMachine(stopCmd)
startCmd = startVirtualMachine.startVirtualMachineCmd()
startCmd.id = self.virtual_machine_12.id
self.apiclient.startVirtualMachine(startCmd)
vm_12_host_id = self.get_vm_host_id(self.virtual_machine_12.id)
self.assertNotEqual(vm_11_host_id,
vm_12_host_id,
msg="Both VMs of affinity group %s are on the same host" % self.affinity_group.name)
# 6. Stop vm-12, start vm-12 with considerlasthost=false. It will be started on same host as vm-11
stopCmd.id = self.virtual_machine_12.id
stopCmd.forced = True
self.apiclient.stopVirtualMachine(stopCmd)
startCmd.id = self.virtual_machine_12.id
startCmd.considerlasthost = False
self.apiclient.startVirtualMachine(startCmd)
vm_12_host_id = self.get_vm_host_id(self.virtual_machine_12.id)
self.assertEqual(vm_11_host_id,
vm_12_host_id,
msg="Both VMs of affinity group %s are on the different host" % self.affinity_group.name)
destroyCmd = destroyVirtualMachine.destroyVirtualMachineCmd()
destroyCmd.id = self.virtual_machine_12.id
destroyCmd.expunge = True
self.apiclient.destroyVirtualMachine(destroyCmd)
# 7. Deploy vm-13 with different host, vm-13 should be started on specified host.
self.services["virtual_machine"]["name"] = "virtual-machine-13"
self.services["virtual_machine"]["displayname"] = "virtual-machine-13"
self.virtual_machine_13 = VirtualMachine.create(
self.apiclient,
self.services["virtual_machine"],
serviceofferingid=self.service_offering.id,
templateid=self.template.id,
zoneid=self.zone.id,
networkids=self.user_network.id,
affinitygroupids=self.affinity_group.id,
domainid=self.sub_domain.id,
accountid=self.regular_user.name,
hostid=vm_12_host_id
)
vm_13_host_id = self.get_vm_host_id(self.virtual_machine_13.id)
self.assertEqual(vm_12_host_id,
vm_13_host_id,
msg="virtual-machine-13 should be started on %s" % vm_12_host_id)
destroyCmd.id = self.virtual_machine_13.id
destroyCmd.expunge = True
self.apiclient.destroyVirtualMachine(destroyCmd)
# 8. Deploy vm-14 with startvm=false, then start the VM. vm-14 should be started on same host.
self.services["virtual_machine"]["name"] = "virtual-machine-14"
self.services["virtual_machine"]["displayname"] = "virtual-machine-14"
self.virtual_machine_14 = VirtualMachine.create(
self.regular_user_apiclient,
self.services["virtual_machine"],
serviceofferingid=self.service_offering.id,
templateid=self.template.id,
zoneid=self.zone.id,
networkids=self.user_network.id,
affinitygroupids=self.affinity_group.id,
startvm=False
)
self.cleanup.append(self.virtual_machine_14)
startCmd.id = self.virtual_machine_14.id
startCmd.considerlasthost = None
self.apiclient.startVirtualMachine(startCmd)
vm_14_host_id = self.get_vm_host_id(self.virtual_machine_14.id)
self.assertEqual(vm_11_host_id,
vm_14_host_id,
msg="virtual-machine-4 should be started on %s" % vm_11_host_id)

View File

@ -438,6 +438,7 @@
"label.confirmpassword.description": "Please type the same password again.",
"label.connectiontimeout": "Connection timeout",
"label.conservemode": "Conserve mode",
"label.considerlasthost": "Consider Last Host",
"label.consoleproxy": "Console proxy",
"label.console.proxy": "Console proxy",
"label.console.proxy.vm": "Console proxy VM",

View File

@ -117,7 +117,8 @@ export default {
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) },
groupMap: (selection, values) => { return selection.map(x => { return { id: x, considerlasthost: values.considerlasthost } }) },
args: ['considerlasthost'],
show: (record) => { return ['Stopped'].includes(record.state) },
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/StartVirtualMachine.vue')))
},
@ -850,7 +851,7 @@ export default {
args: ['name', 'description', 'type'],
mapping: {
type: {
options: ['host anti-affinity', 'host affinity']
options: ['host anti-affinity (Strict)', 'host affinity (Strict)', 'host anti-affinity (Non-Strict)', 'host affinity (Non-Strict)']
}
}
},

View File

@ -1405,6 +1405,17 @@ export default {
}
if (action.mapping && key in action.mapping && action.mapping[key].options) {
params[key] = action.mapping[key].options[input]
if (['createAffinityGroup'].includes(action.api) && key === 'type') {
if (params[key] === 'host anti-affinity (Strict)') {
params[key] = 'host anti-affinity'
} else if (params[key] === 'host affinity (Strict)') {
params[key] = 'host affinity'
} else if (params[key] === 'host anti-affinity (Non-Strict)') {
params[key] = 'non-strict host anti-affinity'
} else if (params[key] === 'host affinity (Non-Strict)') {
params[key] = 'non-strict host affinity'
}
}
} else if (param.type === 'list') {
params[key] = input.map(e => { return param.opts[e].id }).reduce((str, name) => { return str + ',' + name })
} else if (param.name === 'account' || param.name === 'keypair') {

View File

@ -97,6 +97,13 @@
<a-switch v-model:checked="form.bootintosetup" />
</a-form-item>
<a-form-item name="considerlasthost" ref="considerlasthost">
<template #label>
<tooltip-label :title="$t('label.considerlasthost')" :tooltip="apiParams.considerlasthost.description"/>
</template>
<a-switch v-model:checked="form.considerlasthost" />
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
@ -147,7 +154,9 @@ export default {
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({})
this.form = reactive({
considerlasthost: true
})
this.rules = reactive({})
},
fetchPods () {

View File

@ -92,12 +92,17 @@ export default {
{
dataIndex: 'name',
title: this.$t('label.affinity.groups'),
width: '40%'
width: '30%'
},
{
dataIndex: 'type',
title: this.$t('label.type'),
width: '30%'
},
{
dataIndex: 'description',
title: this.$t('label.description'),
width: '60%'
width: '40%'
}
],
selectedRowKeys: [],