Skip to content

Commit f7317b2

Browse files
affinity: add soft host-tag placement preference
Adds a host-tag affinity processor. An affinity group of type "host tag affinity" takes the group name as a host tag; when a member VM is deployed, routing hosts carrying that tag in the VM's zone get their deployment priority raised. It is a preference and not a constraint: no host is ever added to the avoid set, so deployment still succeeds when no tagged host is available, and check() never fails a planned destination. The raised priority is not consulted by automatic DRS. Unit tests cover the three paths: tagged hosts get priority raised (nothing excluded), an empty match is a no-op, and check() returns true.
1 parent d87c804 commit f7317b2

7 files changed

Lines changed: 318 additions & 0 deletions

File tree

client/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,11 @@
572572
<artifactId>cloud-plugin-non-strict-host-affinity</artifactId>
573573
<version>${project.version}</version>
574574
</dependency>
575+
<dependency>
576+
<groupId>org.apache.cloudstack</groupId>
577+
<artifactId>cloud-plugin-host-tag-affinity</artifactId>
578+
<version>${project.version}</version>
579+
</dependency>
575580
<dependency>
576581
<groupId>org.apache.cloudstack</groupId>
577582
<artifactId>cloud-plugin-api-solidfire-intg-test</artifactId>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
<artifactId>cloud-plugin-host-tag-affinity</artifactId>
23+
<name>Apache CloudStack Plugin - Host Tag Affinity Processor</name>
24+
<parent>
25+
<groupId>org.apache.cloudstack</groupId>
26+
<artifactId>cloudstack-plugins</artifactId>
27+
<version>24.0.0-SNAPSHOT</version>
28+
<relativePath>../../pom.xml</relativePath>
29+
</parent>
30+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.affinity;
18+
19+
import java.util.List;
20+
21+
import javax.inject.Inject;
22+
23+
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
24+
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
25+
import org.apache.commons.collections.CollectionUtils;
26+
import org.apache.commons.lang3.StringUtils;
27+
28+
import com.cloud.deploy.DeploymentPlan;
29+
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
30+
import com.cloud.exception.AffinityConflictException;
31+
import com.cloud.host.Host;
32+
import com.cloud.host.HostVO;
33+
import com.cloud.host.dao.HostDao;
34+
import com.cloud.vm.VirtualMachine;
35+
import com.cloud.vm.VirtualMachineProfile;
36+
37+
/**
38+
* Soft VM-to-host placement preference: the affinity group name is treated as a host tag, and hosts
39+
* carrying that tag in the VM's zone have their deployment priority raised. A preference, not a
40+
* constraint — no host is excluded. Note: the priority channel is not honored by automatic DRS.
41+
*/
42+
public class HostTagAffinityProcessor extends AffinityProcessorBase implements AffinityGroupProcessor {
43+
44+
@Inject
45+
protected AffinityGroupDao affinityGroupDao;
46+
@Inject
47+
protected AffinityGroupVMMapDao affinityGroupVMMapDao;
48+
@Inject
49+
protected HostDao hostDao;
50+
51+
@Override
52+
public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid, List<VirtualMachine> vmList) throws AffinityConflictException {
53+
VirtualMachine vm = vmProfile.getVirtualMachine();
54+
List<AffinityGroupVMMapVO> vmGroupMappings = affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType());
55+
56+
for (AffinityGroupVMMapVO vmGroupMapping : vmGroupMappings) {
57+
if (vmGroupMapping != null) {
58+
processAffinityGroup(vmGroupMapping, plan, vm);
59+
}
60+
}
61+
}
62+
63+
protected void processAffinityGroup(AffinityGroupVMMapVO vmGroupMapping, DeploymentPlan plan, VirtualMachine vm) {
64+
AffinityGroupVO group = affinityGroupDao.findById(vmGroupMapping.getAffinityGroupId());
65+
if (group == null || StringUtils.isBlank(group.getName())) {
66+
return;
67+
}
68+
69+
String hostTag = group.getName();
70+
List<HostVO> preferredHosts = hostDao.listByHostTag(Host.Type.Routing, null, null, vm.getDataCenterId(), hostTag);
71+
if (CollectionUtils.isEmpty(preferredHosts)) {
72+
if (logger.isDebugEnabled()) {
73+
logger.debug(String.format("No hosts carry tag [%s] in zone %s for VM %s; host-tag affinity is a no-op.",
74+
hostTag, vm.getDataCenterId(), vm));
75+
}
76+
return;
77+
}
78+
79+
for (HostVO host : preferredHosts) {
80+
Integer priority = adjustHostPriority(plan, host.getId());
81+
if (logger.isDebugEnabled()) {
82+
logger.debug(String.format("Raised host %s priority to %s (VM %s prefers hosts tagged [%s]).",
83+
host.getId(), priority, vm, hostTag));
84+
}
85+
}
86+
}
87+
88+
protected Integer adjustHostPriority(DeploymentPlan plan, Long hostId) {
89+
plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER);
90+
return plan.getHostPriorities().get(hostId);
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
name=host-tag-affinity
18+
parent=planner
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
<beans xmlns="http://www.springframework.org/schema/beans"
20+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21+
xmlns:context="http://www.springframework.org/schema/context"
22+
xmlns:aop="http://www.springframework.org/schema/aop"
23+
xsi:schemaLocation="http://www.springframework.org/schema/beans
24+
http://www.springframework.org/schema/beans/spring-beans.xsd
25+
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
26+
http://www.springframework.org/schema/context
27+
http://www.springframework.org/schema/context/spring-context.xsd"
28+
>
29+
30+
<bean id="HostTagAffinityProcessor"
31+
class="org.apache.cloudstack.affinity.HostTagAffinityProcessor">
32+
<property name="name" value="HostTagAffinityProcessor" />
33+
<property name="type" value="host tag affinity" />
34+
</bean>
35+
36+
37+
</beans>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.cloudstack.affinity;
21+
22+
import static org.mockito.ArgumentMatchers.eq;
23+
import static org.mockito.ArgumentMatchers.isNull;
24+
import static org.mockito.ArgumentMatchers.nullable;
25+
import static org.mockito.Mockito.when;
26+
27+
import java.util.ArrayList;
28+
import java.util.Arrays;
29+
import java.util.Collections;
30+
import java.util.List;
31+
32+
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
33+
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
34+
import org.junit.Assert;
35+
import org.junit.Test;
36+
import org.junit.runner.RunWith;
37+
import org.mockito.InjectMocks;
38+
import org.mockito.Mock;
39+
import org.mockito.Mockito;
40+
import org.mockito.Spy;
41+
import org.mockito.junit.MockitoJUnitRunner;
42+
43+
import com.cloud.deploy.DataCenterDeployment;
44+
import com.cloud.deploy.DeployDestination;
45+
import com.cloud.deploy.DeploymentPlanner.ExcludeList;
46+
import com.cloud.host.Host;
47+
import com.cloud.host.HostVO;
48+
import com.cloud.host.dao.HostDao;
49+
import com.cloud.vm.VirtualMachine;
50+
import com.cloud.vm.VirtualMachineProfile;
51+
52+
@RunWith(MockitoJUnitRunner.class)
53+
public class HostTagAffinityProcessorTest {
54+
55+
@Spy
56+
@InjectMocks
57+
HostTagAffinityProcessor processor = new HostTagAffinityProcessor();
58+
59+
@Mock
60+
AffinityGroupVMMapDao affinityGroupVMMapDao;
61+
@Mock
62+
AffinityGroupDao affinityGroupDao;
63+
@Mock
64+
HostDao hostDao;
65+
66+
long vmId = 10L;
67+
long affinityGroupId = 20L;
68+
long zoneId = 2L;
69+
long host2Id = 3L;
70+
long host3Id = 4L;
71+
String groupName = "gold";
72+
73+
private VirtualMachineProfile mockVmProfile() {
74+
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
75+
when(vm.getId()).thenReturn(vmId);
76+
when(vm.getDataCenterId()).thenReturn(zoneId);
77+
VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class);
78+
when(vmProfile.getVirtualMachine()).thenReturn(vm);
79+
return vmProfile;
80+
}
81+
82+
private void stubGroupMembership() {
83+
List<AffinityGroupVMMapVO> vmGroupMappings = new ArrayList<>();
84+
vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId));
85+
when(affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings);
86+
AffinityGroupVO group = Mockito.mock(AffinityGroupVO.class);
87+
when(affinityGroupDao.findById(affinityGroupId)).thenReturn(group);
88+
when(group.getName()).thenReturn(groupName);
89+
}
90+
91+
@Test
92+
public void testProcessRaisesPriorityForTaggedHosts() {
93+
VirtualMachineProfile vmProfile = mockVmProfile();
94+
stubGroupMembership();
95+
96+
HostVO host2 = Mockito.mock(HostVO.class);
97+
when(host2.getId()).thenReturn(host2Id);
98+
HostVO host3 = Mockito.mock(HostVO.class);
99+
when(host3.getId()).thenReturn(host3Id);
100+
when(hostDao.listByHostTag(eq(Host.Type.Routing), isNull(), isNull(), eq(zoneId), eq(groupName)))
101+
.thenReturn(Arrays.asList(host2, host3));
102+
103+
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
104+
ExcludeList avoid = new ExcludeList();
105+
106+
processor.process(vmProfile, plan, avoid);
107+
108+
// Both tagged hosts get raised to priority 1 (DEFAULT 0 -> HIGHER +1); nothing is excluded.
109+
Assert.assertEquals(2, plan.getHostPriorities().size());
110+
Assert.assertEquals(Integer.valueOf(1), plan.getHostPriorities().get(host2Id));
111+
Assert.assertEquals(Integer.valueOf(1), plan.getHostPriorities().get(host3Id));
112+
Assert.assertFalse("soft preference: no host may be excluded", avoid.shouldAvoid(host2));
113+
}
114+
115+
@Test
116+
public void testProcessNoMatchingHostsIsNoOp() {
117+
VirtualMachineProfile vmProfile = mockVmProfile();
118+
stubGroupMembership();
119+
when(hostDao.listByHostTag(eq(Host.Type.Routing), isNull(), isNull(), eq(zoneId), eq(groupName)))
120+
.thenReturn(Collections.emptyList());
121+
122+
DataCenterDeployment plan = new DataCenterDeployment(zoneId);
123+
ExcludeList avoid = new ExcludeList();
124+
125+
processor.process(vmProfile, plan, avoid);
126+
127+
Assert.assertTrue(plan.getHostPriorities().isEmpty());
128+
}
129+
130+
@Test
131+
public void testCheckAlwaysTrue() throws Exception {
132+
// A soft preference must never fail a planned destination.
133+
Assert.assertTrue(processor.check(Mockito.mock(VirtualMachineProfile.class), Mockito.mock(DeployDestination.class)));
134+
}
135+
}

plugins/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<module>affinity-group-processors/host-anti-affinity</module>
5353
<module>affinity-group-processors/non-strict-host-affinity</module>
5454
<module>affinity-group-processors/non-strict-host-anti-affinity</module>
55+
<module>affinity-group-processors/host-tag-affinity</module>
5556

5657
<module>alert-handlers/snmp-alerts</module>
5758
<module>alert-handlers/syslog-alerts</module>

0 commit comments

Comments
 (0)