You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by GitBox <gi...@apache.org> on 2018/05/21 07:19:10 UTC

[GitHub] rhtyd closed pull request #2630: Host Affinity plugin

rhtyd closed pull request #2630: Host Affinity plugin
URL: https://github.com/apache/cloudstack/pull/2630
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/api/src/com/cloud/deploy/DataCenterDeployment.java b/api/src/com/cloud/deploy/DataCenterDeployment.java
index f046b66ef06..76faf25f726 100644
--- a/api/src/com/cloud/deploy/DataCenterDeployment.java
+++ b/api/src/com/cloud/deploy/DataCenterDeployment.java
@@ -19,6 +19,9 @@
 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 @@
     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 ReservationContext getReservationContext() {
         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 456d5b85899..b57fec0cf41 100644
--- a/api/src/com/cloud/deploy/DeploymentPlan.java
+++ b/api/src/com/cloud/deploy/DeploymentPlan.java
@@ -19,6 +19,8 @@
 import com.cloud.deploy.DeploymentPlanner.ExcludeList;
 import com.cloud.vm.ReservationContext;
 
+import java.util.List;
+
 /**
  */
 public interface DeploymentPlan {
@@ -65,4 +67,8 @@
     Long getPhysicalNetworkId();
 
     ReservationContext getReservationContext();
+
+    void setPreferredHosts(List<Long> hostIds);
+
+    List<Long> getPreferredHosts();
 }
diff --git a/client/pom.xml b/client/pom.xml
index 9907d8cc2ac..5653f536ac6 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -438,6 +438,11 @@
       <artifactId>cloud-plugin-host-anti-affinity</artifactId>
       <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>
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 4ec917e3419..1f70e526147 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 00000000000..6b58322270b
--- /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 00000000000..fe0d91b7c12
--- /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 00000000000..3d42e80b277
--- /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 00000000000..055a6442e1a
--- /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 00000000000..5dc9270e69e
--- /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 2cf9d3ccf71..67ec0784eec 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 5d8ad0a7051..64fabb99dd5 100644
--- a/server/src/com/cloud/deploy/DeploymentPlanningManagerImpl.java
+++ b/server/src/com/cloud/deploy/DeploymentPlanningManagerImpl.java
@@ -33,6 +33,7 @@
 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 @@ public DeployDestination planDeployment(VirtualMachineProfile vmProfile, Deploym
                     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 @@ public DeployDestination planDeployment(VirtualMachineProfile vmProfile, Deploym
                                 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 @@ private DeployDestination checkClustersforDestination(List<Long> clusterList, Vi
                     // 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 @@ private boolean canAvoidCluster(Cluster clusterVO, ExcludeList avoids, ExcludeLi
     }
 
     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 @@ public int compare(Volume v1, Volume v2) {
                 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 @@ public int compare(Volume v1, Volume v2) {
                     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 @@ public int compare(Volume v1, Volume v2) {
         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 272a4fc300d..5d8f9ad2159 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 @@
     @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 void testSetUp() {
         planners.add(_planner);
         _dpm.setPlanners(planners);
 
+        Mockito.when(host.getId()).thenReturn(hostId);
     }
 
     @Test
@@ -222,6 +230,26 @@ public void emptyClusterListTest() throws InsufficientServerCapacityException, A
         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 64ec8ae8df3..f58f3d91cdc 100644
--- a/test/integration/smoke/test_affinity_groups.py
+++ b/test/integration/smoke/test_affinity_groups.py
@@ -21,8 +21,10 @@
 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 @@ def setUpClass(cls):
         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 @@ def setUpClass(cls):
         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 @@ def test_DeployVmAntiAffinityGroup(self):
         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):


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services