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.