You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2018/05/21 07:19:16 UTC
[cloudstack] branch 4.11 updated: Host Affinity plugin (#2630)
This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch 4.11
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.11 by this push:
new 06f7e49 Host Affinity plugin (#2630)
06f7e49 is described below
commit 06f7e495dcc022e935622c24d326b527bbb73f63
Author: Nicolas Vazquez <ni...@gmail.com>
AuthorDate: Mon May 21 04:19:08 2018 -0300
Host Affinity plugin (#2630)
This implements a new host-affinity plugin.
---
api/src/com/cloud/deploy/DataCenterDeployment.java | 14 ++
api/src/com/cloud/deploy/DeploymentPlan.java | 6 +
client/pom.xml | 5 +
.../core/spring-core-registry-core-context.xml | 2 +-
.../host-affinity/pom.xml | 33 ++++
.../cloudstack/host-affinity/module.properties | 18 +++
.../host-affinity/spring-host-affinity-context.xml | 35 ++++
.../cloudstack/affinity/HostAffinityProcessor.java | 127 +++++++++++++++
.../affinity/HostAffinityProcessorTest.java | 176 +++++++++++++++++++++
plugins/pom.xml | 1 +
.../deploy/DeploymentPlanningManagerImpl.java | 31 +++-
.../vm/DeploymentPlanningManagerImplTest.java | 30 +++-
test/integration/smoke/test_affinity_groups.py | 95 ++++++++++-
13 files changed, 562 insertions(+), 11 deletions(-)
diff --git a/api/src/com/cloud/deploy/DataCenterDeployment.java b/api/src/com/cloud/deploy/DataCenterDeployment.java
index f046b66..76faf25 100644
--- a/api/src/com/cloud/deploy/DataCenterDeployment.java
+++ b/api/src/com/cloud/deploy/DataCenterDeployment.java
@@ -19,6 +19,9 @@ package com.cloud.deploy;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.vm.ReservationContext;
+import java.util.ArrayList;
+import java.util.List;
+
public class DataCenterDeployment implements DeploymentPlan {
long _dcId;
Long _podId;
@@ -29,6 +32,7 @@ public class DataCenterDeployment implements DeploymentPlan {
ExcludeList _avoids = null;
boolean _recreateDisks;
ReservationContext _context;
+ List<Long> preferredHostIds = new ArrayList<>();
public DataCenterDeployment(long dataCenterId) {
this(dataCenterId, null, null, null, null, null);
@@ -93,4 +97,14 @@ public class DataCenterDeployment implements DeploymentPlan {
return _context;
}
+ @Override
+ public void setPreferredHosts(List<Long> hostIds) {
+ this.preferredHostIds = new ArrayList<>(hostIds);
+ }
+
+ @Override
+ public List<Long> getPreferredHosts() {
+ return this.preferredHostIds;
+ }
+
}
diff --git a/api/src/com/cloud/deploy/DeploymentPlan.java b/api/src/com/cloud/deploy/DeploymentPlan.java
index 456d5b8..b57fec0 100644
--- a/api/src/com/cloud/deploy/DeploymentPlan.java
+++ b/api/src/com/cloud/deploy/DeploymentPlan.java
@@ -19,6 +19,8 @@ package com.cloud.deploy;
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
import com.cloud.vm.ReservationContext;
+import java.util.List;
+
/**
*/
public interface DeploymentPlan {
@@ -65,4 +67,8 @@ public interface DeploymentPlan {
Long getPhysicalNetworkId();
ReservationContext getReservationContext();
+
+ void setPreferredHosts(List<Long> hostIds);
+
+ List<Long> getPreferredHosts();
}
diff --git a/client/pom.xml b/client/pom.xml
index 9907d8c..5653f53 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -439,6 +439,11 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-plugin-host-affinity</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-api-solidfire-intg-test</artifactId>
<version>${project.version}</version>
diff --git a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
index 4ec917e..1f70e52 100644
--- a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
+++ b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
@@ -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" />
+ value="HostAntiAffinityProcessor,ExplicitDedicationProcessor,HostAffinityProcessor" />
<property name="excludeKey" value="affinity.processors.exclude" />
</bean>
diff --git a/plugins/affinity-group-processors/host-affinity/pom.xml b/plugins/affinity-group-processors/host-affinity/pom.xml
new file mode 100644
index 0000000..6b58322
--- /dev/null
+++ b/plugins/affinity-group-processors/host-affinity/pom.xml
@@ -0,0 +1,33 @@
+<!--
+ 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>
+ <name>Apache CloudStack Plugin - Host Affinity Processor</name>
+ <artifactId>cloud-plugin-host-affinity</artifactId>
+ <parent>
+ <artifactId>cloudstack-plugins</artifactId>
+ <groupId>org.apache.cloudstack</groupId>
+ <version>4.11.1.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <build>
+ <defaultGoal>install</defaultGoal>
+ <sourceDirectory>src</sourceDirectory>
+ </build>
+</project>
\ No newline at end of file
diff --git a/plugins/affinity-group-processors/host-affinity/resources/META-INF/cloudstack/host-affinity/module.properties b/plugins/affinity-group-processors/host-affinity/resources/META-INF/cloudstack/host-affinity/module.properties
new file mode 100644
index 0000000..fe0d91b
--- /dev/null
+++ b/plugins/affinity-group-processors/host-affinity/resources/META-INF/cloudstack/host-affinity/module.properties
@@ -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=host-affinity
+parent=planner
\ No newline at end of file
diff --git a/plugins/affinity-group-processors/host-affinity/resources/META-INF/cloudstack/host-affinity/spring-host-affinity-context.xml b/plugins/affinity-group-processors/host-affinity/resources/META-INF/cloudstack/host-affinity/spring-host-affinity-context.xml
new file mode 100644
index 0000000..3d42e80
--- /dev/null
+++ b/plugins/affinity-group-processors/host-affinity/resources/META-INF/cloudstack/host-affinity/spring-host-affinity-context.xml
@@ -0,0 +1,35 @@
+<!--
+ 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="HostAffinityProcessor"
+ class="org.apache.cloudstack.affinity.HostAffinityProcessor">
+ <property name="name" value="HostAffinityProcessor" />
+ <property name="type" value="host affinity" />
+ </bean>
+</beans>
\ No newline at end of file
diff --git a/plugins/affinity-group-processors/host-affinity/src/org/apache/cloudstack/affinity/HostAffinityProcessor.java b/plugins/affinity-group-processors/host-affinity/src/org/apache/cloudstack/affinity/HostAffinityProcessor.java
new file mode 100644
index 0000000..055a644
--- /dev/null
+++ b/plugins/affinity-group-processors/host-affinity/src/org/apache/cloudstack/affinity/HostAffinityProcessor.java
@@ -0,0 +1,127 @@
+// 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.List;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.ArrayList;
+
+import javax.inject.Inject;
+
+import com.cloud.vm.VMInstanceVO;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.log4j.Logger;
+
+import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
+import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
+
+import com.cloud.deploy.DeployDestination;
+import com.cloud.deploy.DeploymentPlan;
+import com.cloud.deploy.DeploymentPlanner.ExcludeList;
+import com.cloud.exception.AffinityConflictException;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VirtualMachineProfile;
+import com.cloud.vm.dao.VMInstanceDao;
+
+public class HostAffinityProcessor extends AffinityProcessorBase implements AffinityGroupProcessor {
+
+ private static final Logger s_logger = Logger.getLogger(HostAffinityProcessor.class);
+
+ @Inject
+ protected VMInstanceDao _vmInstanceDao;
+ @Inject
+ protected AffinityGroupDao _affinityGroupDao;
+ @Inject
+ protected AffinityGroupVMMapDao _affinityGroupVMMapDao;
+
+ @Override
+ public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException {
+ VirtualMachine vm = vmProfile.getVirtualMachine();
+ List<AffinityGroupVMMapVO> vmGroupMappings = _affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType());
+ if (CollectionUtils.isNotEmpty(vmGroupMappings)) {
+ for (AffinityGroupVMMapVO vmGroupMapping : vmGroupMappings) {
+ processAffinityGroup(vmGroupMapping, plan, vm);
+ }
+ }
+ }
+
+ /**
+ * Process Affinity Group for VM deployment
+ */
+ protected void processAffinityGroup(AffinityGroupVMMapVO vmGroupMapping, DeploymentPlan plan, VirtualMachine vm) {
+ AffinityGroupVO group = _affinityGroupDao.findById(vmGroupMapping.getAffinityGroupId());
+ s_logger.debug("Processing affinity group " + group.getName() + " for VM Id: " + vm.getId());
+
+ List<Long> groupVMIds = _affinityGroupVMMapDao.listVmIdsByAffinityGroup(group.getId());
+ groupVMIds.remove(vm.getId());
+
+ List<Long> preferredHosts = getPreferredHostsFromGroupVMIds(groupVMIds);
+ plan.setPreferredHosts(preferredHosts);
+ }
+
+ /**
+ * Get host ids set from vm ids list
+ */
+ protected Set<Long> getHostIdSet(List<Long> vmIds) {
+ Set<Long> hostIds = new HashSet<>();
+ for (Long groupVMId : vmIds) {
+ VMInstanceVO groupVM = _vmInstanceDao.findById(groupVMId);
+ hostIds.add(groupVM.getHostId());
+ }
+ return hostIds;
+ }
+
+ /**
+ * Get preferred host ids list from the affinity group VMs
+ */
+ protected List<Long> getPreferredHostsFromGroupVMIds(List<Long> vmIds) {
+ return new ArrayList<>(getHostIdSet(vmIds));
+ }
+
+ @Override
+ public boolean check(VirtualMachineProfile vmProfile, DeployDestination plannedDestination) throws AffinityConflictException {
+ if (plannedDestination.getHost() == null) {
+ return true;
+ }
+ long plannedHostId = plannedDestination.getHost().getId();
+ VirtualMachine vm = vmProfile.getVirtualMachine();
+ List<AffinityGroupVMMapVO> vmGroupMappings = _affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType());
+
+ if (CollectionUtils.isNotEmpty(vmGroupMappings)) {
+ for (AffinityGroupVMMapVO vmGroupMapping : vmGroupMappings) {
+ if (!checkAffinityGroup(vmGroupMapping, vm, plannedHostId)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check Affinity Group
+ */
+ protected boolean checkAffinityGroup(AffinityGroupVMMapVO vmGroupMapping, VirtualMachine vm, long plannedHostId) {
+ List<Long> groupVMIds = _affinityGroupVMMapDao.listVmIdsByAffinityGroup(vmGroupMapping.getAffinityGroupId());
+ groupVMIds.remove(vm.getId());
+
+ Set<Long> hostIds = getHostIdSet(groupVMIds);
+ return CollectionUtils.isEmpty(hostIds) || hostIds.contains(plannedHostId);
+ }
+
+}
diff --git a/plugins/affinity-group-processors/host-affinity/test/org/apache/cloudstack/affinity/HostAffinityProcessorTest.java b/plugins/affinity-group-processors/host-affinity/test/org/apache/cloudstack/affinity/HostAffinityProcessorTest.java
new file mode 100644
index 0000000..5dc9270
--- /dev/null
+++ b/plugins/affinity-group-processors/host-affinity/test/org/apache/cloudstack/affinity/HostAffinityProcessorTest.java
@@ -0,0 +1,176 @@
+// 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.DeployDestination;
+import com.cloud.deploy.DeploymentPlan;
+import com.cloud.host.Host;
+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.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(JUnit4.class)
+public class HostAffinityProcessorTest {
+
+ private static final long AFFINITY_GROUP_ID = 2L;
+ private static final String AFFINITY_GROUP_NAME = "Host affinity group";
+ private static final Long VM_ID = 3L;
+ private static final Long GROUP_VM_1_ID = 1L;
+ private static final Long GROUP_VM_2_ID = 2L;
+ private static final Long HOST_ID = 1L;
+ private static final Long HOST_2_ID = 2L;
+
+ @Mock
+ AffinityGroupDao affinityGroupDao;
+
+ @Mock
+ AffinityGroupVMMapDao affinityGroupVMMapDao;
+
+ @Mock
+ VMInstanceDao vmInstanceDao;
+
+ @Spy
+ @InjectMocks
+ HostAffinityProcessor processor = new HostAffinityProcessor();
+
+ @Mock
+ DeploymentPlan plan;
+
+ @Mock
+ VirtualMachine vm;
+
+ @Mock
+ VMInstanceVO groupVM1;
+
+ @Mock
+ VMInstanceVO groupVM2;
+
+ @Mock
+ AffinityGroupVO affinityGroupVO;
+
+ @Mock
+ AffinityGroupVMMapVO mapVO;
+
+ @Mock
+ DeployDestination dest;
+
+ @Mock
+ Host host;
+
+ @Mock
+ VirtualMachineProfile profile;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ when(groupVM1.getHostId()).thenReturn(HOST_ID);
+ when(groupVM2.getHostId()).thenReturn(HOST_ID);
+ when(vmInstanceDao.findById(GROUP_VM_1_ID)).thenReturn(groupVM1);
+ when(vmInstanceDao.findById(GROUP_VM_2_ID)).thenReturn(groupVM2);
+
+ when(affinityGroupVMMapDao.listVmIdsByAffinityGroup(AFFINITY_GROUP_ID)).thenReturn(new ArrayList<>(Arrays.asList(GROUP_VM_1_ID, GROUP_VM_2_ID, VM_ID)));
+
+ when(vm.getId()).thenReturn(VM_ID);
+
+ when(affinityGroupVO.getId()).thenReturn(AFFINITY_GROUP_ID);
+ when(affinityGroupVO.getName()).thenReturn(AFFINITY_GROUP_NAME);
+ when(mapVO.getAffinityGroupId()).thenReturn(AFFINITY_GROUP_ID);
+
+ when(affinityGroupDao.findById(AFFINITY_GROUP_ID)).thenReturn(affinityGroupVO);
+
+ when(dest.getHost()).thenReturn(host);
+ when(host.getId()).thenReturn(HOST_ID);
+ when(profile.getVirtualMachine()).thenReturn(vm);
+ when(affinityGroupVMMapDao.findByVmIdType(eq(VM_ID), any())).thenReturn(new ArrayList<>(Arrays.asList(mapVO)));
+ }
+
+ @Test
+ public void testProcessAffinityGroupMultipleVMs() {
+ processor.processAffinityGroup(mapVO, plan, vm);
+ verify(plan).setPreferredHosts(Arrays.asList(HOST_ID));
+ }
+
+ @Test
+ public void testProcessAffinityGroupEmptyGroup() {
+ when(affinityGroupVMMapDao.listVmIdsByAffinityGroup(AFFINITY_GROUP_ID)).thenReturn(new ArrayList<>());
+ processor.processAffinityGroup(mapVO, plan, vm);
+ verify(plan).setPreferredHosts(new ArrayList<>());
+ }
+
+ @Test
+ public void testGetPreferredHostsFromGroupVMIdsMultipleVMs() {
+ List<Long> list = new ArrayList<>(Arrays.asList(GROUP_VM_1_ID, GROUP_VM_2_ID));
+ List<Long> preferredHosts = processor.getPreferredHostsFromGroupVMIds(list);
+ assertNotNull(preferredHosts);
+ assertEquals(1, preferredHosts.size());
+ assertEquals(HOST_ID, preferredHosts.get(0));
+ }
+
+ @Test
+ public void testGetPreferredHostsFromGroupVMIdsEmptyVMsList() {
+ List<Long> list = new ArrayList<>();
+ List<Long> preferredHosts = processor.getPreferredHostsFromGroupVMIds(list);
+ assertNotNull(preferredHosts);
+ assertTrue(preferredHosts.isEmpty());
+ }
+
+ @Test
+ public void testCheckAffinityGroup() {
+ assertTrue(processor.checkAffinityGroup(mapVO, vm, HOST_ID));
+ }
+
+ @Test
+ public void testCheckAffinityGroupWrongHostId() {
+ assertFalse(processor.checkAffinityGroup(mapVO, vm, HOST_2_ID));
+ }
+
+ @Test
+ public void testCheck() {
+ assertTrue(processor.check(profile, dest));
+ }
+
+ @Test
+ public void testCheckWrongHostId() {
+ when(host.getId()).thenReturn(HOST_2_ID);
+ assertFalse(processor.check(profile, dest));
+ }
+}
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 2cf9d3c..67ec078 100755
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -109,6 +109,7 @@
<module>database/quota</module>
<module>integrations/cloudian</module>
<module>integrations/prometheus</module>
+ <module>affinity-group-processors/host-affinity</module>
</modules>
<dependencies>
diff --git a/server/src/com/cloud/deploy/DeploymentPlanningManagerImpl.java b/server/src/com/cloud/deploy/DeploymentPlanningManagerImpl.java
index 5d8ad0a..64fabb9 100644
--- a/server/src/com/cloud/deploy/DeploymentPlanningManagerImpl.java
+++ b/server/src/com/cloud/deploy/DeploymentPlanningManagerImpl.java
@@ -33,6 +33,7 @@ import javax.naming.ConfigurationException;
import com.cloud.utils.db.Filter;
import com.cloud.utils.fsm.StateMachine2;
+import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.apache.cloudstack.affinity.AffinityGroupProcessor;
import org.apache.cloudstack.affinity.AffinityGroupService;
@@ -321,7 +322,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine> {
suitableHosts.add(host);
Pair<Host, Map<Volume, StoragePool>> potentialResources = findPotentialDeploymentResources(
suitableHosts, suitableVolumeStoragePools, avoids,
- getPlannerUsage(planner, vmProfile, plan, avoids), readyAndReusedVolumes);
+ getPlannerUsage(planner, vmProfile, plan, avoids), readyAndReusedVolumes, plan.getPreferredHosts());
if (potentialResources != null) {
pod = _podDao.findById(host.getPodId());
cluster = _clusterDao.findById(host.getClusterId());
@@ -461,7 +462,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine> {
suitableHosts.add(host);
Pair<Host, Map<Volume, StoragePool>> potentialResources = findPotentialDeploymentResources(
suitableHosts, suitableVolumeStoragePools, avoids,
- getPlannerUsage(planner, vmProfile, plan, avoids), readyAndReusedVolumes);
+ getPlannerUsage(planner, vmProfile, plan, avoids), readyAndReusedVolumes, plan.getPreferredHosts());
if (potentialResources != null) {
Map<Volume, StoragePool> storageVolMap = potentialResources.second();
// remove the reused vol<->pool from
@@ -1077,7 +1078,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine> {
// choose the potential host and pool for the VM
if (!suitableVolumeStoragePools.isEmpty()) {
Pair<Host, Map<Volume, StoragePool>> potentialResources = findPotentialDeploymentResources(suitableHosts, suitableVolumeStoragePools, avoid,
- resourceUsageRequired, readyAndReusedVolumes);
+ resourceUsageRequired, readyAndReusedVolumes, plan.getPreferredHosts());
if (potentialResources != null) {
Host host = _hostDao.findById(potentialResources.first().getId());
@@ -1217,11 +1218,12 @@ StateListener<State, VirtualMachine.Event, VirtualMachine> {
}
protected Pair<Host, Map<Volume, StoragePool>> findPotentialDeploymentResources(List<Host> suitableHosts, Map<Volume, List<StoragePool>> suitableVolumeStoragePools,
- ExcludeList avoid, DeploymentPlanner.PlannerResourceUsage resourceUsageRequired, List<Volume> readyAndReusedVolumes) {
+ ExcludeList avoid, DeploymentPlanner.PlannerResourceUsage resourceUsageRequired, List<Volume> readyAndReusedVolumes, List<Long> preferredHosts) {
s_logger.debug("Trying to find a potenial host and associated storage pools from the suitable host/pool lists for this VM");
boolean hostCanAccessPool = false;
boolean haveEnoughSpace = false;
+ boolean hostAffinityCheck = false;
if (readyAndReusedVolumes == null) {
readyAndReusedVolumes = new ArrayList<Volume>();
@@ -1245,6 +1247,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine> {
s_logger.debug("Checking if host: " + potentialHost.getId() + " can access any suitable storage pool for volume: " + vol.getVolumeType());
List<StoragePool> volumePoolList = suitableVolumeStoragePools.get(vol);
hostCanAccessPool = false;
+ hostAffinityCheck = checkAffinity(potentialHost, preferredHosts);
for (StoragePool potentialSPool : volumePoolList) {
if (hostCanAccessSPool(potentialHost, potentialSPool)) {
hostCanAccessPool = true;
@@ -1273,8 +1276,12 @@ StateListener<State, VirtualMachine.Event, VirtualMachine> {
s_logger.warn("insufficient capacity to allocate all volumes");
break;
}
+ if (!hostAffinityCheck) {
+ s_logger.debug("Host affinity check failed");
+ break;
+ }
}
- if (hostCanAccessPool && haveEnoughSpace && checkIfHostFitsPlannerUsage(potentialHost.getId(), resourceUsageRequired)) {
+ if (hostCanAccessPool && haveEnoughSpace && hostAffinityCheck && checkIfHostFitsPlannerUsage(potentialHost.getId(), resourceUsageRequired)) {
s_logger.debug("Found a potential host " + "id: " + potentialHost.getId() + " name: " + potentialHost.getName() +
" and associated storage pools for this VM");
return new Pair<Host, Map<Volume, StoragePool>>(potentialHost, storage);
@@ -1286,6 +1293,20 @@ StateListener<State, VirtualMachine.Event, VirtualMachine> {
return null;
}
+ /**
+ * True if:
+ * - Affinity is not enabled (preferred host is empty)
+ * - Affinity is enabled and potential host is on the preferred hosts list
+ *
+ * False if not
+ */
+ @DB
+ public boolean checkAffinity(Host potentialHost, List<Long> preferredHosts) {
+ boolean hostAffinityEnabled = CollectionUtils.isNotEmpty(preferredHosts);
+ boolean hostAffinityMatches = hostAffinityEnabled && preferredHosts.contains(potentialHost.getId());
+ return !hostAffinityEnabled || hostAffinityMatches;
+ }
+
protected boolean hostCanAccessSPool(Host host, StoragePool pool) {
boolean hostCanAccessSPool = false;
diff --git a/server/test/com/cloud/vm/DeploymentPlanningManagerImplTest.java b/server/test/com/cloud/vm/DeploymentPlanningManagerImplTest.java
index 272a4fc..5d8f9ad 100644
--- a/server/test/com/cloud/vm/DeploymentPlanningManagerImplTest.java
+++ b/server/test/com/cloud/vm/DeploymentPlanningManagerImplTest.java
@@ -16,15 +16,19 @@
// under the License.
package com.cloud.vm;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
+import com.cloud.host.Host;
import org.apache.cloudstack.affinity.dao.AffinityGroupDomainMapDao;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -136,9 +140,12 @@ public class DeploymentPlanningManagerImplTest {
@Inject
UserVmDetailsDao vmDetailsDao;
- private static long domainId = 5L;
+ @Mock
+ Host host;
+ private static long domainId = 5L;
private static long dataCenterId = 1L;
+ private static long hostId = 1l;
@BeforeClass
public static void setUp() throws ConfigurationException {
@@ -172,6 +179,7 @@ public class DeploymentPlanningManagerImplTest {
planners.add(_planner);
_dpm.setPlanners(planners);
+ Mockito.when(host.getId()).thenReturn(hostId);
}
@Test
@@ -222,6 +230,26 @@ public class DeploymentPlanningManagerImplTest {
assertNull("Planner cannot handle, destination should be null! ", dest);
}
+ @Test
+ public void testCheckAffinityEmptyPreferredHosts() {
+ assertTrue(_dpm.checkAffinity(host, new ArrayList<>()));
+ }
+
+ @Test
+ public void testCheckAffinityNullPreferredHosts() {
+ assertTrue(_dpm.checkAffinity(host, null));
+ }
+
+ @Test
+ public void testCheckAffinityNotEmptyPreferredHostsContainingHost() {
+ assertTrue(_dpm.checkAffinity(host, Arrays.asList(3l, 4l, hostId, 2l)));
+ }
+
+ @Test
+ public void testCheckAffinityNotEmptyPreferredHostsNotContainingHost() {
+ assertFalse(_dpm.checkAffinity(host, Arrays.asList(3l, 4l, 2l)));
+ }
+
@Configuration
@ComponentScan(basePackageClasses = {DeploymentPlanningManagerImpl.class}, includeFilters = {@Filter(value = TestConfiguration.Library.class,
type = FilterType.CUSTOM)}, useDefaultFilters = false)
diff --git a/test/integration/smoke/test_affinity_groups.py b/test/integration/smoke/test_affinity_groups.py
index 64ec8ae..f58f3d9 100644
--- a/test/integration/smoke/test_affinity_groups.py
+++ b/test/integration/smoke/test_affinity_groups.py
@@ -21,8 +21,10 @@ from marvin.cloudstackTestCase import *
from marvin.cloudstackAPI import *
from marvin.lib.utils import *
from marvin.lib.base import *
-from marvin.lib.common import *
-from marvin.sshClient import SshClient
+from marvin.lib.common import (get_domain,
+ get_zone,
+ get_template,
+ list_virtual_machines)
from nose.plugins.attrib import attr
class TestDeployVmWithAffinityGroup(cloudstackTestCase):
@@ -42,14 +44,14 @@ class TestDeployVmWithAffinityGroup(cloudstackTestCase):
cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests())
cls.hypervisor = cls.testClient.getHypervisorInfo()
- cls.template = get_test_template(
+ cls.template = get_template(
cls.apiclient,
cls.zone.id,
cls.hypervisor
)
if cls.template == FAILED:
- assert False, "get_test_template() failed to return template"
+ assert False, "get_template() failed to return template"
cls.services["virtual_machine"]["zoneid"] = cls.zone.id
@@ -69,6 +71,16 @@ class TestDeployVmWithAffinityGroup(cloudstackTestCase):
cls.ag = AffinityGroup.create(cls.apiclient, cls.services["virtual_machine"]["affinity"],
account=cls.account.name, domainid=cls.domain.id)
+ host_affinity = {
+ "name": "marvin-host-affinity",
+ "type": "host affinity",
+ }
+ cls.affinity = AffinityGroup.create(
+ cls.apiclient,
+ host_affinity,
+ account=cls.account.name,
+ domainid=cls.domain.id
+ )
cls._cleanup = [
cls.service_offering,
cls.ag,
@@ -152,6 +164,81 @@ class TestDeployVmWithAffinityGroup(cloudstackTestCase):
self.assertNotEqual(host_of_vm1, host_of_vm2,
msg="Both VMs of affinity group %s are on the same host" % self.ag.name)
+ @attr(tags=["basic", "advanced", "multihost"], required_hardware="false")
+ def test_DeployVmAffinityGroup(self):
+ """
+ test DeployVM in affinity groups
+
+ deploy VM1 and VM2 in the same host-affinity groups
+ Verify that the vms are deployed on the same host
+ """
+ #deploy VM1 in affinity group created in setUp
+ vm1 = VirtualMachine.create(
+ self.apiclient,
+ self.services["virtual_machine"],
+ templateid=self.template.id,
+ accountid=self.account.name,
+ domainid=self.account.domainid,
+ serviceofferingid=self.service_offering.id,
+ affinitygroupnames=[self.affinity.name]
+ )
+
+ list_vm1 = list_virtual_machines(
+ self.apiclient,
+ id=vm1.id
+ )
+ self.assertEqual(
+ isinstance(list_vm1, list),
+ True,
+ "Check list response returns a valid list"
+ )
+ self.assertNotEqual(
+ len(list_vm1),
+ 0,
+ "Check VM available in List Virtual Machines"
+ )
+ vm1_response = list_vm1[0]
+ self.assertEqual(
+ vm1_response.state,
+ 'Running',
+ msg="VM is not in Running state"
+ )
+ host_of_vm1 = vm1_response.hostid
+
+ #deploy VM2 in affinity group created in setUp
+ vm2 = VirtualMachine.create(
+ self.apiclient,
+ self.services["virtual_machine"],
+ templateid=self.template.id,
+ accountid=self.account.name,
+ domainid=self.account.domainid,
+ serviceofferingid=self.service_offering.id,
+ affinitygroupnames=[self.affinity.name]
+ )
+ list_vm2 = list_virtual_machines(
+ self.apiclient,
+ id=vm2.id
+ )
+ self.assertEqual(
+ isinstance(list_vm2, list),
+ True,
+ "Check list response returns a valid list"
+ )
+ self.assertNotEqual(
+ len(list_vm2),
+ 0,
+ "Check VM available in List Virtual Machines"
+ )
+ vm2_response = list_vm2[0]
+ self.assertEqual(
+ vm2_response.state,
+ 'Running',
+ msg="VM is not in Running state"
+ )
+ host_of_vm2 = vm2_response.hostid
+
+ self.assertEqual(host_of_vm1, host_of_vm2,
+ msg="Both VMs of affinity group %s are on different hosts" % self.affinity.name)
@classmethod
def tearDownClass(cls):
--
To stop receiving notification emails like this one, please contact
rohit@apache.org.