diff --git a/api/src/main/java/com/cloud/deploy/DataCenterDeployment.java b/api/src/main/java/com/cloud/deploy/DataCenterDeployment.java index c7471bd1b30..f2f94faab4c 100644 --- a/api/src/main/java/com/cloud/deploy/DataCenterDeployment.java +++ b/api/src/main/java/com/cloud/deploy/DataCenterDeployment.java @@ -21,7 +21,9 @@ import com.cloud.vm.ReservationContext; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class DataCenterDeployment implements DeploymentPlan { long _dcId; @@ -35,6 +37,7 @@ public class DataCenterDeployment implements DeploymentPlan { ReservationContext _context; List preferredHostIds = new ArrayList<>(); boolean migrationPlan; + Map hostPriorities = new HashMap<>(); public DataCenterDeployment(long dataCenterId) { this(dataCenterId, null, null, null, null, null); @@ -124,4 +127,32 @@ public class DataCenterDeployment implements DeploymentPlan { "migrationPlan"); } + @Override + public void adjustHostPriority(Long hostId, HostPriorityAdjustment adjustment) { + Integer currentPriority = hostPriorities.get(hostId); + if (currentPriority == null) { + currentPriority = DEFAULT_HOST_PRIORITY; + } else if (currentPriority.equals(PROHIBITED_HOST_PRIORITY)) { + return; + } + if (HostPriorityAdjustment.HIGHER.equals(adjustment)) { + hostPriorities.put(hostId, currentPriority + ADJUST_HOST_PRIORITY_BY); + } else if (HostPriorityAdjustment.LOWER.equals(adjustment)) { + hostPriorities.put(hostId, currentPriority - ADJUST_HOST_PRIORITY_BY); + } else if (HostPriorityAdjustment.DEFAULT.equals(adjustment)) { + hostPriorities.put(hostId, DEFAULT_HOST_PRIORITY); + } else if (HostPriorityAdjustment.PROHIBIT.equals(adjustment)) { + hostPriorities.put(hostId, PROHIBITED_HOST_PRIORITY); + } + } + + @Override + public Map getHostPriorities() { + return hostPriorities; + } + + @Override + public void setHostPriorities(Map priorities) { + this.hostPriorities = priorities; + } } diff --git a/api/src/main/java/com/cloud/deploy/DeploymentPlan.java b/api/src/main/java/com/cloud/deploy/DeploymentPlan.java index c71bf3e9311..fa3785e766b 100644 --- a/api/src/main/java/com/cloud/deploy/DeploymentPlan.java +++ b/api/src/main/java/com/cloud/deploy/DeploymentPlan.java @@ -20,10 +20,23 @@ import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.vm.ReservationContext; import java.util.List; +import java.util.Map; /** */ public interface DeploymentPlan { + + Integer DEFAULT_HOST_PRIORITY = 0; + Integer PROHIBITED_HOST_PRIORITY = Integer.MIN_VALUE; + Integer ADJUST_HOST_PRIORITY_BY = 1; + + enum HostPriorityAdjustment { + HIGHER, + DEFAULT, + LOWER, + PROHIBIT + } + // TODO: This interface is not fully developed. It really // number of parameters to be specified. @@ -73,4 +86,10 @@ public interface DeploymentPlan { List getPreferredHosts(); boolean isMigrationPlan(); + + void adjustHostPriority(Long hostId, HostPriorityAdjustment priority); + + Map getHostPriorities(); + + void setHostPriorities(Map priorities); } diff --git a/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java b/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java index f87939a13f4..62519412fdb 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachineProfile.java @@ -75,6 +75,7 @@ public interface VirtualMachineProfile { public static final Param BootType = new Param("BootType"); public static final Param BootIntoSetup = new Param("enterHardwareSetup"); public static final Param PreserveNics = new Param("PreserveNics"); + public static final Param ConsiderLastHost = new Param("ConsiderLastHost"); private String name; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 96110dbb57f..e43572ae489 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -634,6 +634,7 @@ public class ApiConstants { public static final String PURPOSE = "purpose"; public static final String IS_TAGGED = "istagged"; public static final String INSTANCE_NAME = "instancename"; + public static final String CONSIDER_LAST_HOST = "considerlasthost"; public static final String START_VM = "startvm"; public static final String HA_HOST = "hahost"; public static final String CUSTOM_DISK_OFF_MIN_SIZE = "customdiskofferingminsize"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java index 2e7f5a6f0d5..033a338fa7d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java @@ -82,6 +82,13 @@ public class StartVMCmd extends BaseAsyncCmd implements UserCmd { since = "3.0.1") private Long hostId; + @Parameter(name = ApiConstants.CONSIDER_LAST_HOST, + type = CommandType.BOOLEAN, + description = "True by default, CloudStack will firstly try to start the VM on the last host where it run on before stopping, if destination host is not specified. " + + "If false, CloudStack will not consider the last host and start the VM by normal process.", + since = "4.18.0") + private Boolean considerLastHost; + @Parameter(name = ApiConstants.DEPLOYMENT_PLANNER, type = CommandType.STRING, description = "Deployment planner to use for vm allocation. Available to ROOT admin only", since = "4.4", authorized = { RoleType.Admin }) private String deploymentPlanner; @@ -112,6 +119,10 @@ public class StartVMCmd extends BaseAsyncCmd implements UserCmd { return bootIntoSetup; } + public Boolean getConsiderLastHost() { + return considerLastHost; + } + // /////////////////////////////////////////////////// // ///////////// API Implementation/////////////////// // /////////////////////////////////////////////////// diff --git a/api/src/test/java/com/cloud/deploy/DataCenterDeploymentTest.java b/api/src/test/java/com/cloud/deploy/DataCenterDeploymentTest.java new file mode 100644 index 00000000000..793ccce7b22 --- /dev/null +++ b/api/src/test/java/com/cloud/deploy/DataCenterDeploymentTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.cloud.deploy; + +import junit.framework.TestCase; +import org.junit.Assert; + +public class DataCenterDeploymentTest extends TestCase { + + private long zoneId = 1L; + private long hostId = 2L; + + DataCenterDeployment plan = new DataCenterDeployment(zoneId); + + private void verifyHostPriority(Integer priority) { + Assert.assertEquals(priority, plan.getHostPriorities().get(hostId)); + } + + public void testHostPriorities() { + verifyHostPriority(null); + + plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.DEFAULT); + verifyHostPriority(0); + + plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER); + verifyHostPriority(1); + + plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.LOWER); + verifyHostPriority(0); + + plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.LOWER); + verifyHostPriority(-1); + + plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER); + verifyHostPriority(0); + + plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER); + verifyHostPriority(1); + } +} diff --git a/client/pom.xml b/client/pom.xml index 921a4adbf88..5934b340dd1 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -478,6 +478,16 @@ cloud-plugin-host-affinity ${project.version} + + org.apache.cloudstack + cloud-plugin-non-strict-host-anti-affinity + ${project.version} + + + org.apache.cloudstack + cloud-plugin-non-strict-host-affinity + ${project.version} + org.apache.cloudstack cloud-plugin-api-solidfire-intg-test diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index cb5e9652165..fa386bb1541 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -248,7 +248,7 @@ class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry"> + value="HostAntiAffinityProcessor,ExplicitDedicationProcessor,HostAffinityProcessor,NonStrictHostAntiAffinityProcessor,NonStrictHostAffinityProcessor" /> diff --git a/engine/components-api/src/main/java/com/cloud/deploy/DeploymentPlanningManager.java b/engine/components-api/src/main/java/com/cloud/deploy/DeploymentPlanningManager.java index d3a5f7f7376..ce40287bf03 100644 --- a/engine/components-api/src/main/java/com/cloud/deploy/DeploymentPlanningManager.java +++ b/engine/components-api/src/main/java/com/cloud/deploy/DeploymentPlanningManager.java @@ -20,10 +20,14 @@ import com.cloud.dc.DataCenter; import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.exception.AffinityConflictException; import com.cloud.exception.InsufficientServerCapacityException; +import com.cloud.host.Host; import com.cloud.utils.component.Manager; import com.cloud.vm.VirtualMachineProfile; import org.apache.cloudstack.framework.config.ConfigKey; +import java.util.List; +import java.util.Map; + public interface DeploymentPlanningManager extends Manager { @@ -60,4 +64,6 @@ public interface DeploymentPlanningManager extends Manager { DeploymentPlanner getDeploymentPlannerByName(String plannerName); void checkForNonDedicatedResources(VirtualMachineProfile vmProfile, DataCenter dc, ExcludeList avoids); + + void reorderHostsByPriority(Map priorities, List hosts); } diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index a66b6cb2530..d9117703751 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -1412,6 +1412,10 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac msgBuf.append(String.format("Boot into Setup: %s ", params.get(VirtualMachineProfile.Param.BootIntoSetup))); log = true; } + if (params.get(VirtualMachineProfile.Param.ConsiderLastHost) != null) { + msgBuf.append(String.format("Consider last host: %s ", params.get(VirtualMachineProfile.Param.ConsiderLastHost))); + log = true; + } if (log) { s_logger.info(msgBuf.toString()); } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/VMEntityManagerImpl.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/VMEntityManagerImpl.java index 74e743088ce..896b55734d7 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/VMEntityManagerImpl.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/VMEntityManagerImpl.java @@ -158,6 +158,9 @@ public class VMEntityManagerImpl implements VMEntityManager { vmProfile.getParameters().put(VirtualMachineProfile.Param.BootMode, details.get(VirtualMachineProfile.Param.BootMode.getName())); vmProfile.getParameters().put(VirtualMachineProfile.Param.UefiFlag, details.get(VirtualMachineProfile.Param.UefiFlag.getName())); } + if (MapUtils.isNotEmpty(vmEntityVO.getDetails()) && vmEntityVO.getDetails().containsKey(VirtualMachineProfile.Param.ConsiderLastHost.getName())) { + vmProfile.getParameters().put(VirtualMachineProfile.Param.ConsiderLastHost, vmEntityVO.getDetails().get(VirtualMachineProfile.Param.ConsiderLastHost.getName())); + } DataCenterDeployment plan = new DataCenterDeployment(vm.getDataCenterId(), vm.getPodIdToDeployIn(), null, null, null, null); if (planToDeploy != null && planToDeploy.getDataCenterId() != 0) { plan = diff --git a/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml b/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml new file mode 100644 index 00000000000..ffbe5cd2e5b --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-affinity/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + cloud-plugin-non-strict-host-affinity + Apache CloudStack Plugin - Non-Strict Host Affinity Processor + + org.apache.cloudstack + cloudstack-plugins + 4.18.0.0-SNAPSHOT + ../../pom.xml + + diff --git a/plugins/affinity-group-processors/non-strict-host-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessor.java b/plugins/affinity-group-processors/non-strict-host-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessor.java new file mode 100644 index 00000000000..a51102105eb --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessor.java @@ -0,0 +1,133 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.affinity; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMReservationDao; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; + +import com.cloud.configuration.Config; +import com.cloud.deploy.DeployDestination; +import com.cloud.deploy.DeploymentPlan; +import com.cloud.deploy.DeploymentPlanner.ExcludeList; +import com.cloud.exception.AffinityConflictException; +import com.cloud.utils.DateUtil; +import com.cloud.utils.NumbersUtil; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDao; + +public class NonStrictHostAffinityProcessor extends AffinityProcessorBase implements AffinityGroupProcessor { + + private final Logger logger = Logger.getLogger(this.getClass().getName()); + @Inject + protected UserVmDao vmDao; + @Inject + protected VMInstanceDao vmInstanceDao; + @Inject + protected AffinityGroupDao affinityGroupDao; + @Inject + protected AffinityGroupVMMapDao affinityGroupVMMapDao; + @Inject + protected ConfigurationDao configDao; + + @Inject + protected VMReservationDao reservationDao; + + private int vmCapacityReleaseInterval; + + @Override + public void process(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid) throws AffinityConflictException { + VirtualMachine vm = vmProfile.getVirtualMachine(); + List vmGroupMappings = affinityGroupVMMapDao.findByVmIdType(vm.getId(), getType()); + + for (AffinityGroupVMMapVO vmGroupMapping : vmGroupMappings) { + if (vmGroupMapping != null) { + processAffinityGroup(vmGroupMapping, plan, vm); + } + } + + } + + protected void processAffinityGroup(AffinityGroupVMMapVO vmGroupMapping, DeploymentPlan plan, VirtualMachine vm) { + AffinityGroupVO group = affinityGroupDao.findById(vmGroupMapping.getAffinityGroupId()); + + if (logger.isDebugEnabled()) { + logger.debug("Processing affinity group " + group.getName() + " for VM Id: " + vm.getId()); + } + + List groupVMIds = affinityGroupVMMapDao.listVmIdsByAffinityGroup(group.getId()); + groupVMIds.remove(vm.getId()); + + for (Long groupVMId : groupVMIds) { + VMInstanceVO groupVM = vmInstanceDao.findById(groupVMId); + if (groupVM != null && !groupVM.isRemoved()) { + processVmInAffinityGroup(plan, groupVM); + } + } + } + + protected void processVmInAffinityGroup(DeploymentPlan plan, VMInstanceVO groupVM) { + if (groupVM.getHostId() != null) { + Integer priority = adjustHostPriority(plan, groupVM.getHostId()); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Updated host %s priority to %s , since VM %s is present on the host", + groupVM.getHostId(), priority, groupVM.getId())); + } + } else if (Arrays.asList(VirtualMachine.State.Starting, VirtualMachine.State.Stopped).contains(groupVM.getState()) && groupVM.getLastHostId() != null) { + long secondsSinceLastUpdate = (DateUtil.currentGMTTime().getTime() - groupVM.getUpdateTime().getTime()) / 1000; + if (secondsSinceLastUpdate < vmCapacityReleaseInterval) { + Integer priority = adjustHostPriority(plan, groupVM.getLastHostId()); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Updated host %s priority to %s , since VM %s" + + " is present on the host, in %s state but has reserved capacity", + groupVM.getLastHostId(), priority, groupVM.getId(), groupVM.getState())); + } + } + } + } + + protected Integer adjustHostPriority(DeploymentPlan plan, Long hostId) { + plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.HIGHER); + return plan.getHostPriorities().get(hostId); + } + + @Override + public boolean configure(final String name, final Map params) throws ConfigurationException { + super.configure(name, params); + vmCapacityReleaseInterval = NumbersUtil.parseInt(configDao.getValue(Config.CapacitySkipcountingHours.key()), 3600); + return true; + } + + @Override + public boolean check(VirtualMachineProfile vmProfile, DeployDestination plannedDestination) throws AffinityConflictException { + return true; + } + +} diff --git a/plugins/affinity-group-processors/non-strict-host-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-affinity/module.properties b/plugins/affinity-group-processors/non-strict-host-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-affinity/module.properties new file mode 100644 index 00000000000..98c83112f17 --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-affinity/src/main/resources/META-INF/cloudstack/non-strict-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=non-strict-host-affinity +parent=planner diff --git a/plugins/affinity-group-processors/non-strict-host-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-affinity/spring-non-strict-host-affinity-context.xml b/plugins/affinity-group-processors/non-strict-host-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-affinity/spring-non-strict-host-affinity-context.xml new file mode 100644 index 00000000000..a80ddb1e3bb --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-affinity/spring-non-strict-host-affinity-context.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/plugins/affinity-group-processors/non-strict-host-affinity/src/test/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessorTest.java b/plugins/affinity-group-processors/non-strict-host-affinity/src/test/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessorTest.java new file mode 100644 index 00000000000..0bf62a21bbc --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-affinity/src/test/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessorTest.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.affinity; + +import com.cloud.deploy.DataCenterDeployment; +import com.cloud.deploy.DeploymentPlan; +import com.cloud.deploy.DeploymentPlanner.ExcludeList; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +public class NonStrictHostAffinityProcessorTest { + + @Spy + @InjectMocks + NonStrictHostAffinityProcessor processor = new NonStrictHostAffinityProcessor(); + + @Mock + AffinityGroupVMMapDao _affinityGroupVMMapDao; + @Mock + AffinityGroupDao affinityGroupDao; + @Mock + VMInstanceDao vmInstanceDao; + + long vmId = 10L; + long vm2Id = 11L; + long vm3Id = 12L; + long affinityGroupId = 20L; + long zoneId = 2L; + long host2Id = 3L; + long host3Id = 4L; + + @Test + public void testProcessWithEmptyPlan() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + when(vm.getId()).thenReturn(vmId); + VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class); + when(vmProfile.getVirtualMachine()).thenReturn(vm); + + List vmGroupMappings = new ArrayList<>(); + vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId)); + when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings); + + DataCenterDeployment plan = new DataCenterDeployment(zoneId); + ExcludeList avoid = new ExcludeList(); + + AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class); + when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO); + when(affinityGroupVO.getId()).thenReturn(affinityGroupId); + List groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id)); + when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds); + VMInstanceVO vm2 = new VMInstanceVO(); + when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2); + vm2.setHostId(host2Id); + + processor.process(vmProfile, plan, avoid); + + Assert.assertEquals(1, plan.getHostPriorities().size()); + Assert.assertNotNull(plan.getHostPriorities().get(host2Id)); + Assert.assertEquals(Integer.valueOf(1), plan.getHostPriorities().get(host2Id)); + } + + @Test + public void testProcessWithPlan() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + when(vm.getId()).thenReturn(vmId); + VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class); + when(vmProfile.getVirtualMachine()).thenReturn(vm); + + List vmGroupMappings = new ArrayList<>(); + vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId)); + when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings); + + DataCenterDeployment plan = new DataCenterDeployment(zoneId); + plan.adjustHostPriority(host2Id, DeploymentPlan.HostPriorityAdjustment.DEFAULT); + plan.adjustHostPriority(host3Id, DeploymentPlan.HostPriorityAdjustment.LOWER); + ExcludeList avoid = new ExcludeList(); + + AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class); + when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO); + when(affinityGroupVO.getId()).thenReturn(affinityGroupId); + List groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id, vm3Id)); + when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds); + VMInstanceVO vm2 = new VMInstanceVO(); + when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2); + vm2.setHostId(host2Id); + VMInstanceVO vm3 = new VMInstanceVO(); + when(vmInstanceDao.findById(vm3Id)).thenReturn(vm3); + vm3.setHostId(host3Id); + + processor.process(vmProfile, plan, avoid); + + Assert.assertEquals(2, plan.getHostPriorities().size()); + Assert.assertNotNull(plan.getHostPriorities().get(host2Id)); + Assert.assertEquals(Integer.valueOf(1), plan.getHostPriorities().get(host2Id)); + Assert.assertNotNull(plan.getHostPriorities().get(host3Id)); + Assert.assertEquals(Integer.valueOf(0), plan.getHostPriorities().get(host3Id)); + } + + @Test + public void testProcessWithNotRunningVM() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + when(vm.getId()).thenReturn(vmId); + VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class); + when(vmProfile.getVirtualMachine()).thenReturn(vm); + + List vmGroupMappings = new ArrayList<>(); + vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId)); + when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings); + + DataCenterDeployment plan = new DataCenterDeployment(zoneId); + ExcludeList avoid = new ExcludeList(); + + AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class); + when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO); + when(affinityGroupVO.getId()).thenReturn(affinityGroupId); + List groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id)); + when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds); + VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class); + when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2); + when(vm2.getHostId()).thenReturn(null); + when(vm2.getLastHostId()).thenReturn(host2Id); + when(vm2.getState()).thenReturn(VirtualMachine.State.Starting); + when(vm2.getUpdateTime()).thenReturn(new Date()); + + ReflectionTestUtils.setField(processor, "vmCapacityReleaseInterval", 3600); + processor.process(vmProfile, plan, avoid); + + Assert.assertEquals(1, plan.getHostPriorities().size()); + Assert.assertNotNull(plan.getHostPriorities().get(host2Id)); + Assert.assertEquals(Integer.valueOf(1), plan.getHostPriorities().get(host2Id)); + } +} diff --git a/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml b/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml new file mode 100644 index 00000000000..90eb4fde013 --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-anti-affinity/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + cloud-plugin-non-strict-host-anti-affinity + Apache CloudStack Plugin - Non-Strict Host Anti-Affinity Processor + + + org.apache.cloudstack + cloud-plugin-non-strict-host-affinity + 4.18.0.0-SNAPSHOT + compile + + + + org.apache.cloudstack + cloudstack-plugins + 4.18.0.0-SNAPSHOT + ../../pom.xml + + diff --git a/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAntiAffinityProcessor.java b/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAntiAffinityProcessor.java new file mode 100644 index 00000000000..727faa929c3 --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAntiAffinityProcessor.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.affinity; + +import com.cloud.deploy.DeploymentPlan; + +public class NonStrictHostAntiAffinityProcessor extends NonStrictHostAffinityProcessor { + + @Override + protected Integer adjustHostPriority(DeploymentPlan plan, Long hostId) { + plan.adjustHostPriority(hostId, DeploymentPlan.HostPriorityAdjustment.LOWER); + return plan.getHostPriorities().get(hostId); + } +} diff --git a/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-anti-affinity/module.properties b/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-anti-affinity/module.properties new file mode 100644 index 00000000000..1d8cb21d44d --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-anti-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=non-strict-host-anti-affinity +parent=planner diff --git a/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-anti-affinity/spring-non-strict-host-anti-affinity-context.xml b/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-anti-affinity/spring-non-strict-host-anti-affinity-context.xml new file mode 100644 index 00000000000..0f42019b269 --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/main/resources/META-INF/cloudstack/non-strict-host-anti-affinity/spring-non-strict-host-anti-affinity-context.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/test/java/org/apache/cloudstack/affinity/NonStrictHostAntiAffinityProcessorTest.java b/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/test/java/org/apache/cloudstack/affinity/NonStrictHostAntiAffinityProcessorTest.java new file mode 100644 index 00000000000..5ea423b374b --- /dev/null +++ b/plugins/affinity-group-processors/non-strict-host-anti-affinity/src/test/java/org/apache/cloudstack/affinity/NonStrictHostAntiAffinityProcessorTest.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.affinity; + +import com.cloud.deploy.DataCenterDeployment; +import com.cloud.deploy.DeploymentPlan; +import com.cloud.deploy.DeploymentPlanner.ExcludeList; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +public class NonStrictHostAntiAffinityProcessorTest { + + @Spy + @InjectMocks + NonStrictHostAntiAffinityProcessor processor = new NonStrictHostAntiAffinityProcessor(); + + @Mock + AffinityGroupVMMapDao _affinityGroupVMMapDao; + @Mock + AffinityGroupDao affinityGroupDao; + @Mock + VMInstanceDao vmInstanceDao; + + long vmId = 10L; + long vm2Id = 11L; + long vm3Id = 12L; + long affinityGroupId = 20L; + long zoneId = 2L; + long host2Id = 3L; + long host3Id = 4L; + + @Test + public void testProcessWithEmptyPlan() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + when(vm.getId()).thenReturn(vmId); + VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class); + when(vmProfile.getVirtualMachine()).thenReturn(vm); + + List vmGroupMappings = new ArrayList<>(); + vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId)); + when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings); + + DataCenterDeployment plan = new DataCenterDeployment(zoneId); + ExcludeList avoid = new ExcludeList(); + + AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class); + when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO); + when(affinityGroupVO.getId()).thenReturn(affinityGroupId); + List groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id)); + when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds); + VMInstanceVO vm2 = new VMInstanceVO(); + when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2); + vm2.setHostId(host2Id); + + processor.process(vmProfile, plan, avoid); + + Assert.assertEquals(1, plan.getHostPriorities().size()); + Assert.assertNotNull(plan.getHostPriorities().get(host2Id)); + Assert.assertEquals(Integer.valueOf(-1), plan.getHostPriorities().get(host2Id)); + } + + @Test + public void testProcessWithPlan() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + when(vm.getId()).thenReturn(vmId); + VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class); + when(vmProfile.getVirtualMachine()).thenReturn(vm); + + List vmGroupMappings = new ArrayList<>(); + vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId)); + when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings); + + DataCenterDeployment plan = new DataCenterDeployment(zoneId); + plan.adjustHostPriority(host2Id, DeploymentPlan.HostPriorityAdjustment.DEFAULT); + plan.adjustHostPriority(host3Id, DeploymentPlan.HostPriorityAdjustment.HIGHER); + ExcludeList avoid = new ExcludeList(); + + AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class); + when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO); + when(affinityGroupVO.getId()).thenReturn(affinityGroupId); + List groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id, vm3Id)); + when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds); + VMInstanceVO vm2 = new VMInstanceVO(); + when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2); + vm2.setHostId(host2Id); + VMInstanceVO vm3 = new VMInstanceVO(); + when(vmInstanceDao.findById(vm3Id)).thenReturn(vm3); + vm3.setHostId(host3Id); + + processor.process(vmProfile, plan, avoid); + + Assert.assertEquals(2, plan.getHostPriorities().size()); + Assert.assertNotNull(plan.getHostPriorities().get(host2Id)); + Assert.assertEquals(Integer.valueOf(-1), plan.getHostPriorities().get(host2Id)); + Assert.assertNotNull(plan.getHostPriorities().get(host3Id)); + Assert.assertEquals(Integer.valueOf(0), plan.getHostPriorities().get(host3Id)); + } + + @Test + public void testProcessWithNotRunningVM() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + when(vm.getId()).thenReturn(vmId); + VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class); + when(vmProfile.getVirtualMachine()).thenReturn(vm); + + List vmGroupMappings = new ArrayList<>(); + vmGroupMappings.add(new AffinityGroupVMMapVO(affinityGroupId, vmId)); + when(_affinityGroupVMMapDao.findByVmIdType(eq(vmId), nullable(String.class))).thenReturn(vmGroupMappings); + + DataCenterDeployment plan = new DataCenterDeployment(zoneId); + ExcludeList avoid = new ExcludeList(); + + AffinityGroupVO affinityGroupVO = Mockito.mock(AffinityGroupVO.class); + when(affinityGroupDao.findById(affinityGroupId)).thenReturn(affinityGroupVO); + when(affinityGroupVO.getId()).thenReturn(affinityGroupId); + List groupVMIds = new ArrayList<>(Arrays.asList(vmId, vm2Id)); + when(_affinityGroupVMMapDao.listVmIdsByAffinityGroup(affinityGroupId)).thenReturn(groupVMIds); + VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class); + when(vmInstanceDao.findById(vm2Id)).thenReturn(vm2); + when(vm2.getHostId()).thenReturn(null); + when(vm2.getLastHostId()).thenReturn(host2Id); + when(vm2.getState()).thenReturn(VirtualMachine.State.Starting); + when(vm2.getUpdateTime()).thenReturn(new Date()); + + ReflectionTestUtils.setField(processor, "vmCapacityReleaseInterval", 3600); + processor.process(vmProfile, plan, avoid); + + Assert.assertEquals(1, plan.getHostPriorities().size()); + Assert.assertNotNull(plan.getHostPriorities().get(host2Id)); + Assert.assertEquals(Integer.valueOf(-1), plan.getHostPriorities().get(host2Id)); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index 8534000afc0..461ec0e02ce 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -50,6 +50,8 @@ affinity-group-processors/explicit-dedication affinity-group-processors/host-affinity affinity-group-processors/host-anti-affinity + affinity-group-processors/non-strict-host-affinity + affinity-group-processors/non-strict-host-anti-affinity alert-handlers/snmp-alerts alert-handlers/syslog-alerts diff --git a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java index 40f6667faec..e99f3c88956 100644 --- a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java +++ b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java @@ -17,6 +17,7 @@ package com.cloud.deploy; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -382,6 +383,7 @@ StateListener, Configurable { if (s_logger.isDebugEnabled()) { s_logger.debug("Deploy avoids pods: " + avoids.getPodsToAvoid() + ", clusters: " + avoids.getClustersToAvoid() + ", hosts: " + avoids.getHostsToAvoid()); + s_logger.debug("Deploy hosts with priorities " + plan.getHostPriorities() + " , hosts have NORMAL priority by default"); } // call planners @@ -406,7 +408,10 @@ StateListener, Configurable { planner = getDeploymentPlannerByName(plannerName); } - if (vm.getLastHostId() != null && haVmTag == null) { + String considerLastHostStr = (String)vmProfile.getParameter(VirtualMachineProfile.Param.ConsiderLastHost); + boolean considerLastHost = vm.getLastHostId() != null && haVmTag == null && + (considerLastHostStr == null || Boolean.TRUE.toString().equalsIgnoreCase(considerLastHostStr)); + if (considerLastHost) { s_logger.debug("This VM has last host_id specified, trying to choose the same host: " + vm.getLastHostId()); HostVO host = _hostDao.findById(vm.getLastHostId()); @@ -1222,6 +1227,7 @@ StateListener, Configurable { // cluster. DataCenterDeployment potentialPlan = new DataCenterDeployment(plan.getDataCenterId(), clusterVO.getPodId(), clusterVO.getId(), null, plan.getPoolId(), null, plan.getReservationContext()); + potentialPlan.setHostPriorities(plan.getHostPriorities()); Pod pod = _podDao.findById(clusterVO.getPodId()); if (CollectionUtils.isNotEmpty(avoid.getPodsToAvoid()) && avoid.getPodsToAvoid().contains(pod.getId())) { @@ -1588,9 +1594,35 @@ StateListener, Configurable { if (suitableHosts.isEmpty()) { s_logger.debug("No suitable hosts found"); } + + // re-order hosts by priority + reorderHostsByPriority(plan.getHostPriorities(), suitableHosts); + return suitableHosts; } + @Override + public void reorderHostsByPriority(Map priorities, List hosts) { + s_logger.info("Re-ordering hosts " + hosts + " by priorities " + priorities); + + hosts.removeIf(host -> DataCenterDeployment.PROHIBITED_HOST_PRIORITY.equals(getHostPriority(priorities, host.getId()))); + + Collections.sort(hosts, new Comparator<>() { + @Override + public int compare(Host host1, Host host2) { + int res = getHostPriority(priorities, host1.getId()).compareTo(getHostPriority(priorities, host2.getId())); + return -res; + } + } + ); + + s_logger.info("Hosts after re-ordering are: " + hosts); + } + + private Integer getHostPriority(Map priorities, Long hostId) { + return priorities.get(hostId) != null ? priorities.get(hostId) : DeploymentPlan.DEFAULT_HOST_PRIORITY; + } + protected Pair>, List> findSuitablePoolsForVolumes(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid, int returnUpTo) { List volumesTobeCreated = _volsDao.findUsableVolumesForInstance(vmProfile.getId()); diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index d18c369c494..aa362b2dc63 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -1508,6 +1508,9 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } } + // re-order hosts by priority + _dpMgr.reorderHostsByPriority(plan.getHostPriorities(), suitableHosts); + if (s_logger.isDebugEnabled()) { if (suitableHosts.isEmpty()) { s_logger.debug("No suitable hosts found"); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index a4ab8fb74d6..06b772ce08b 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -3225,6 +3225,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (uefiDetail != null) { addVmUefiBootOptionsToParams(additonalParams, uefiDetail.getName(), uefiDetail.getValue()); } + if (cmd.getConsiderLastHost() != null) { + additonalParams.put(VirtualMachineProfile.Param.ConsiderLastHost, cmd.getConsiderLastHost().toString()); + } return startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), additonalParams, cmd.getDeploymentPlanner()).first(); } diff --git a/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java index 076740965c3..48600ddc0cc 100644 --- a/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java @@ -127,8 +127,9 @@ public class AffinityGroupServiceImpl extends ManagerBase implements AffinityGro AffinityGroupProcessor processor = typeProcessorMap.get(affinityGroupType); - if(processor == null){ - throw new InvalidParameterValueException("Unable to create affinity group, invalid affinity group type" + affinityGroupType); + if (processor == null) { + throw new InvalidParameterValueException(String.format("Unable to create affinity group, invalid affinity group type: %s. " + + "Valid values are %s", affinityGroupType, String.join(",", typeProcessorMap.keySet()))); } Account caller = CallContext.current().getCallingAccount(); diff --git a/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java b/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java index 262aab44784..d79010ee5c0 100644 --- a/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java +++ b/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -1029,4 +1030,49 @@ public class DeploymentPlanningManagerImplTest { } } } + + @Test + public void testReorderHostsByPriority() { + Map priorities = new LinkedHashMap<>(); + priorities.put(1L, 3); + priorities.put(2L, -6); + priorities.put(3L, 5); + priorities.put(5L, 8); + priorities.put(6L, -1); + priorities.put(8L, 5); + priorities.put(9L, DataCenterDeployment.PROHIBITED_HOST_PRIORITY); + + Host host1 = Mockito.mock(Host.class); + Mockito.when(host1.getId()).thenReturn(1L); + Host host2 = Mockito.mock(Host.class); + Mockito.when(host2.getId()).thenReturn(2L); + Host host3 = Mockito.mock(Host.class); + Mockito.when(host3.getId()).thenReturn(3L); + Host host4 = Mockito.mock(Host.class); + Mockito.when(host4.getId()).thenReturn(4L); + Host host5 = Mockito.mock(Host.class); + Mockito.when(host5.getId()).thenReturn(5L); + Host host6 = Mockito.mock(Host.class); + Mockito.when(host6.getId()).thenReturn(6L); + Host host7 = Mockito.mock(Host.class); + Mockito.when(host7.getId()).thenReturn(7L); + Host host8 = Mockito.mock(Host.class); + Mockito.when(host8.getId()).thenReturn(8L); + Host host9 = Mockito.mock(Host.class); + Mockito.when(host9.getId()).thenReturn(9L); + + List hosts = new ArrayList<>(Arrays.asList(host1, host2, host3, host4, host5, host6, host7, host8, host9)); + _dpm.reorderHostsByPriority(priorities, hosts); + + Assert.assertEquals(8, hosts.size()); + + Assert.assertEquals(5, hosts.get(0).getId()); + Assert.assertEquals(3, hosts.get(1).getId()); + Assert.assertEquals(8, hosts.get(2).getId()); + Assert.assertEquals(1, hosts.get(3).getId()); + Assert.assertEquals(4, hosts.get(4).getId()); + Assert.assertEquals(7, hosts.get(5).getId()); + Assert.assertEquals(6, hosts.get(6).getId()); + Assert.assertEquals(2, hosts.get(7).getId()); + } } diff --git a/test/integration/smoke/test_nonstrict_affinity_group.py b/test/integration/smoke/test_nonstrict_affinity_group.py new file mode 100644 index 00000000000..27b7d341bd3 --- /dev/null +++ b/test/integration/smoke/test_nonstrict_affinity_group.py @@ -0,0 +1,448 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Tests of Non-Strict (host anti-affinity and host affinity) affinity groups +""" + +import logging + +from nose.plugins.attrib import attr +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.cloudstackAPI import startVirtualMachine, stopVirtualMachine, destroyVirtualMachine + +from marvin.lib.base import (Account, + AffinityGroup, + Domain, + Host, + ServiceOffering, + VirtualMachine, + Zone, + Network, + NetworkOffering) + +from marvin.lib.common import (get_domain, + get_zone, + get_template) + + +class TestNonStrictAffinityGroups(cloudstackTestCase): + """ + Test Non-Strict (host anti-affinity and host affinity) affinity groups + """ + @classmethod + def setUpClass(cls): + cls.testClient = super( + TestNonStrictAffinityGroups, + cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + + zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.zone = Zone(zone.__dict__) + cls.template = get_template(cls.apiclient, cls.zone.id) + cls._cleanup = [] + + cls.logger = logging.getLogger("TestNonStrictAffinityGroups") + cls.stream_handler = logging.StreamHandler() + cls.logger.setLevel(logging.DEBUG) + cls.logger.addHandler(cls.stream_handler) + + cls.skipTests = False + hosts = Host.list( + cls.apiclient, + zoneid=cls.zone.id, + state='Up', + resourcestate='Enabled' + ) + if not hosts or not isinstance(hosts, list) or len(hosts) < 2: + cls.logger.debug("This test requires at least two (Up and Enabled) hosts in the zone") + cls.skipTests = True + return + + cls.domain = get_domain(cls.apiclient) + + # 1. Create small service offering + cls.service_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["small"] + ) + cls._cleanup.append(cls.service_offering) + + # 3. Create network offering for isolated networks + cls.network_offering_isolated = NetworkOffering.create( + cls.apiclient, + cls.services["isolated_network_offering"] + ) + cls.network_offering_isolated.update(cls.apiclient, state='Enabled') + cls._cleanup.append(cls.network_offering_isolated) + + # 4. Create sub-domain + cls.sub_domain = Domain.create( + cls.apiclient, + cls.services["acl"]["domain1"] + ) + cls._cleanup.append(cls.sub_domain) + + # 5. Create regular user + cls.regular_user = Account.create( + cls.apiclient, + cls.services["acl"]["accountD11A"], + domainid=cls.sub_domain.id + ) + cls._cleanup.append(cls.regular_user) + + # 5. Create api clients for regular user + cls.regular_user_user = cls.regular_user.user[0] + cls.regular_user_apiclient = cls.testClient.getUserApiClient( + cls.regular_user_user.username, cls.sub_domain.name + ) + + # 7. Create network for regular user + cls.services["network"]["name"] = "Test Network Isolated - Regular user" + cls.user_network = Network.create( + cls.regular_user_apiclient, + cls.services["network"], + networkofferingid=cls.network_offering_isolated.id, + zoneid=cls.zone.id + ) + + @classmethod + def tearDownClass(cls): + super(TestNonStrictAffinityGroups, cls).tearDownClass() + + def setUp(self): + if self.skipTests: + self.skipTest("This test requires at least two (Up and Enabled) hosts in the zone") + self.apiclient = self.testClient.getApiClient() + self.cleanup = [] + + def tearDown(self): + super(TestNonStrictAffinityGroups, self).tearDown() + + @classmethod + def get_vm_host_id(cls, vm_id): + list_vms = VirtualMachine.list( + cls.apiclient, + id=vm_id + ) + vm = list_vms[0] + return vm.hostid + + @attr(tags=["advanced"], required_hardware="false") + def test_01_non_strict_host_anti_affinity(self): + """ Verify Non-Strict host anti-affinity """ + + # 1. Create Non-Strict host anti-affinity + # 2. Deploy vm-1 with the group + # 3. Deploy vm-2 with the group. It will be started on different host if there are multiple hosts. + # 4. Migrate vm-2 to same host as vm-1 + # 5. Stop vm-2, start vm-2. It will be started on same host as vm-1 + # 6. Stop vm-2, start vm-2 with considerlasthost=false. It will be started on different host as vm-1 + # 7. Deploy vm-3 with same host, vm-3 should be started on specified host. + # 8. Deploy vm-4 with startvm=false, then start the VM. + # vm-4 should be started on different host if there are multiple hosts. + + self.logger.debug("=== Running test_01_non_strict_host_anti_affinity ===") + + # 1. Create Non-Strict host anti-affinity + affinity_group_params = { + "name": "Test affinity group", + "type": "non-strict host anti-affinity", + } + self.affinity_group = AffinityGroup.create( + self.regular_user_apiclient, + affinity_group_params + ) + self.cleanup.append(self.affinity_group) + + # 2. Deploy vm-1 with the group + self.services["virtual_machine"]["name"] = "virtual-machine-1" + self.services["virtual_machine"]["displayname"] = "virtual-machine-1" + self.virtual_machine_1 = VirtualMachine.create( + self.regular_user_apiclient, + self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id, + networkids=self.user_network.id, + affinitygroupids=self.affinity_group.id + ) + self.cleanup.append(self.virtual_machine_1) + vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) + + # 3. Deploy vm-2 with the group. It will be started on different host if there are multiple hosts. + self.services["virtual_machine"]["name"] = "virtual-machine-2" + self.services["virtual_machine"]["displayname"] = "virtual-machine-2" + self.virtual_machine_2 = VirtualMachine.create( + self.regular_user_apiclient, + self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id, + networkids=self.user_network.id, + affinitygroupids=self.affinity_group.id + ) + vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) + + self.assertNotEqual(vm_1_host_id, + vm_2_host_id, + msg="Both VMs of affinity group %s are on the same host" % self.affinity_group.name) + + # 4. Migrate vm-2 to same host as vm-1 + self.virtual_machine_2.migrate( + self.apiclient, + hostid=vm_1_host_id + ) + + # 5. Stop vm-2, start vm-2. It will be started on same host as vm-1 + stopCmd = stopVirtualMachine.stopVirtualMachineCmd() + stopCmd.id = self.virtual_machine_2.id + stopCmd.forced = True + self.apiclient.stopVirtualMachine(stopCmd) + + startCmd = startVirtualMachine.startVirtualMachineCmd() + startCmd.id = self.virtual_machine_2.id + self.apiclient.startVirtualMachine(startCmd) + + vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) + + self.assertEqual(vm_1_host_id, + vm_2_host_id, + msg="Both VMs of affinity group %s are on the different host" % self.affinity_group.name) + + # 6. Stop vm-2, start vm-2 with considerlasthost=false. It will be started on different host as vm-1 + stopCmd.id = self.virtual_machine_2.id + stopCmd.forced = True + self.apiclient.stopVirtualMachine(stopCmd) + + startCmd = startVirtualMachine.startVirtualMachineCmd() + startCmd.id = self.virtual_machine_2.id + startCmd.considerlasthost = False + self.apiclient.startVirtualMachine(startCmd) + + vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) + + self.assertNotEqual(vm_1_host_id, + vm_2_host_id, + msg="Both VMs of affinity group %s are on the same host" % self.affinity_group.name) + + destroyCmd = destroyVirtualMachine.destroyVirtualMachineCmd() + destroyCmd.id = self.virtual_machine_2.id + destroyCmd.expunge = True + self.apiclient.destroyVirtualMachine(destroyCmd) + + # 7. Deploy vm-3 with same host, vm-3 should be started on specified host. + self.services["virtual_machine"]["name"] = "virtual-machine-3" + self.services["virtual_machine"]["displayname"] = "virtual-machine-3" + self.virtual_machine_3 = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id, + networkids=self.user_network.id, + affinitygroupids=self.affinity_group.id, + domainid=self.sub_domain.id, + accountid=self.regular_user.name, + hostid=vm_1_host_id + ) + vm_3_host_id = self.get_vm_host_id(self.virtual_machine_3.id) + + self.assertEqual(vm_1_host_id, + vm_3_host_id, + msg="virtual-machine-3 should be started on %s" % vm_1_host_id) + + destroyCmd.id = self.virtual_machine_3.id + destroyCmd.expunge = True + self.apiclient.destroyVirtualMachine(destroyCmd) + + # 8. Deploy vm-4 with startvm=false, then start the VM. + # vm-4 should be started on different host if there are multiple hosts. + self.services["virtual_machine"]["name"] = "virtual-machine-4" + self.services["virtual_machine"]["displayname"] = "virtual-machine-4" + self.virtual_machine_4 = VirtualMachine.create( + self.regular_user_apiclient, + self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id, + networkids=self.user_network.id, + affinitygroupids=self.affinity_group.id, + startvm=False + ) + self.cleanup.append(self.virtual_machine_4) + + startCmd.id = self.virtual_machine_4.id + startCmd.considerlasthost = None + self.apiclient.startVirtualMachine(startCmd) + + vm_4_host_id = self.get_vm_host_id(self.virtual_machine_4.id) + + self.assertNotEqual(vm_1_host_id, + vm_4_host_id, + msg="virtual-machine-4 should be not started on %s" % vm_1_host_id) + + @attr(tags=["advanced"], required_hardware="false") + def test_02_non_strict_host_affinity(self): + """ Verify Non-Strict host affinity """ + + # 1. Create Non-Strict host affinity + # 2. Deploy vm-11 with the group + # 3. Deploy vm-12 with the group. It will be started on same host. + # 4. Migrate vm-12 to different host as vm-11 + # 5. Stop vm-12, start vm-12. It will be started on different host as vm-11 + # 6. Stop vm-12, start vm-12 with considerlasthost=false. It will be started on same host as vm-11 + # 7. Deploy vm-13 with different host, vm-13 should be started on specified host. + # 8. Deploy vm-14 with startvm=false, then start the VM. vm-14 should be started on same host. + + self.logger.debug("=== Running test_02_non_strict_host_affinity ===") + + # 1. Create Non-Strict host affinity + affinity_group_params = { + "name": "Test affinity group", + "type": "non-strict host affinity", + } + self.affinity_group = AffinityGroup.create( + self.regular_user_apiclient, + affinity_group_params + ) + self.cleanup.append(self.affinity_group) + + # 2. Deploy vm-11 with the group + self.services["virtual_machine"]["name"] = "virtual-machine-11" + self.services["virtual_machine"]["displayname"] = "virtual-machine-11" + self.virtual_machine_11 = VirtualMachine.create( + self.regular_user_apiclient, + self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id, + networkids=self.user_network.id, + affinitygroupids=self.affinity_group.id + ) + self.cleanup.append(self.virtual_machine_11) + vm_11_host_id = self.get_vm_host_id(self.virtual_machine_11.id) + + # 3. Deploy vm-12 with the group. It will be started on same host. + self.services["virtual_machine"]["name"] = "virtual-machine-12" + self.services["virtual_machine"]["displayname"] = "virtual-machine-12" + self.virtual_machine_12 = VirtualMachine.create( + self.regular_user_apiclient, + self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id, + networkids=self.user_network.id, + affinitygroupids=self.affinity_group.id + ) + vm_12_host_id = self.get_vm_host_id(self.virtual_machine_12.id) + + self.assertEqual(vm_11_host_id, + vm_12_host_id, + msg="Both VMs of affinity group %s are on the different host" % self.affinity_group.name) + + # 4. Migrate vm-12 to different host as vm-11 + self.virtual_machine_12.migrate( + self.apiclient + ) + + # 5. Stop vm-12, start vm-12. It will be started on different host as vm-11 + stopCmd = stopVirtualMachine.stopVirtualMachineCmd() + stopCmd.id = self.virtual_machine_12.id + stopCmd.forced = True + self.apiclient.stopVirtualMachine(stopCmd) + + startCmd = startVirtualMachine.startVirtualMachineCmd() + startCmd.id = self.virtual_machine_12.id + self.apiclient.startVirtualMachine(startCmd) + + vm_12_host_id = self.get_vm_host_id(self.virtual_machine_12.id) + + self.assertNotEqual(vm_11_host_id, + vm_12_host_id, + msg="Both VMs of affinity group %s are on the same host" % self.affinity_group.name) + + # 6. Stop vm-12, start vm-12 with considerlasthost=false. It will be started on same host as vm-11 + stopCmd.id = self.virtual_machine_12.id + stopCmd.forced = True + self.apiclient.stopVirtualMachine(stopCmd) + + startCmd.id = self.virtual_machine_12.id + startCmd.considerlasthost = False + self.apiclient.startVirtualMachine(startCmd) + + vm_12_host_id = self.get_vm_host_id(self.virtual_machine_12.id) + + self.assertEqual(vm_11_host_id, + vm_12_host_id, + msg="Both VMs of affinity group %s are on the different host" % self.affinity_group.name) + + destroyCmd = destroyVirtualMachine.destroyVirtualMachineCmd() + destroyCmd.id = self.virtual_machine_12.id + destroyCmd.expunge = True + self.apiclient.destroyVirtualMachine(destroyCmd) + + # 7. Deploy vm-13 with different host, vm-13 should be started on specified host. + self.services["virtual_machine"]["name"] = "virtual-machine-13" + self.services["virtual_machine"]["displayname"] = "virtual-machine-13" + self.virtual_machine_13 = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id, + networkids=self.user_network.id, + affinitygroupids=self.affinity_group.id, + domainid=self.sub_domain.id, + accountid=self.regular_user.name, + hostid=vm_12_host_id + ) + vm_13_host_id = self.get_vm_host_id(self.virtual_machine_13.id) + + self.assertEqual(vm_12_host_id, + vm_13_host_id, + msg="virtual-machine-13 should be started on %s" % vm_12_host_id) + + destroyCmd.id = self.virtual_machine_13.id + destroyCmd.expunge = True + self.apiclient.destroyVirtualMachine(destroyCmd) + + # 8. Deploy vm-14 with startvm=false, then start the VM. vm-14 should be started on same host. + self.services["virtual_machine"]["name"] = "virtual-machine-14" + self.services["virtual_machine"]["displayname"] = "virtual-machine-14" + self.virtual_machine_14 = VirtualMachine.create( + self.regular_user_apiclient, + self.services["virtual_machine"], + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id, + networkids=self.user_network.id, + affinitygroupids=self.affinity_group.id, + startvm=False + ) + self.cleanup.append(self.virtual_machine_14) + + startCmd.id = self.virtual_machine_14.id + startCmd.considerlasthost = None + self.apiclient.startVirtualMachine(startCmd) + + vm_14_host_id = self.get_vm_host_id(self.virtual_machine_14.id) + + self.assertEqual(vm_11_host_id, + vm_14_host_id, + msg="virtual-machine-4 should be started on %s" % vm_11_host_id) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 56ce92654e7..1b215e2bcec 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -438,6 +438,7 @@ "label.confirmpassword.description": "Please type the same password again.", "label.connectiontimeout": "Connection timeout", "label.conservemode": "Conserve mode", +"label.considerlasthost": "Consider Last Host", "label.consoleproxy": "Console proxy", "label.console.proxy": "Console proxy", "label.console.proxy.vm": "Console proxy VM", diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index dc68d57f85a..591c312d73d 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -117,7 +117,8 @@ export default { dataView: true, groupAction: true, popup: true, - groupMap: (selection) => { return selection.map(x => { return { id: x } }) }, + groupMap: (selection, values) => { return selection.map(x => { return { id: x, considerlasthost: values.considerlasthost } }) }, + args: ['considerlasthost'], show: (record) => { return ['Stopped'].includes(record.state) }, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/StartVirtualMachine.vue'))) }, @@ -850,7 +851,7 @@ export default { args: ['name', 'description', 'type'], mapping: { type: { - options: ['host anti-affinity', 'host affinity'] + options: ['host anti-affinity (Strict)', 'host affinity (Strict)', 'host anti-affinity (Non-Strict)', 'host affinity (Non-Strict)'] } } }, diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index 3b89cb49530..8ce9c5d7dbc 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -1405,6 +1405,17 @@ export default { } if (action.mapping && key in action.mapping && action.mapping[key].options) { params[key] = action.mapping[key].options[input] + if (['createAffinityGroup'].includes(action.api) && key === 'type') { + if (params[key] === 'host anti-affinity (Strict)') { + params[key] = 'host anti-affinity' + } else if (params[key] === 'host affinity (Strict)') { + params[key] = 'host affinity' + } else if (params[key] === 'host anti-affinity (Non-Strict)') { + params[key] = 'non-strict host anti-affinity' + } else if (params[key] === 'host affinity (Non-Strict)') { + params[key] = 'non-strict host affinity' + } + } } else if (param.type === 'list') { params[key] = input.map(e => { return param.opts[e].id }).reduce((str, name) => { return str + ',' + name }) } else if (param.name === 'account' || param.name === 'keypair') { diff --git a/ui/src/views/compute/StartVirtualMachine.vue b/ui/src/views/compute/StartVirtualMachine.vue index 5d5ff83346c..828a4a31f59 100644 --- a/ui/src/views/compute/StartVirtualMachine.vue +++ b/ui/src/views/compute/StartVirtualMachine.vue @@ -97,6 +97,13 @@ + + + + +
{{ $t('label.cancel') }} {{ $t('label.ok') }} @@ -147,7 +154,9 @@ export default { methods: { initForm () { this.formRef = ref() - this.form = reactive({}) + this.form = reactive({ + considerlasthost: true + }) this.rules = reactive({}) }, fetchPods () { diff --git a/ui/src/views/compute/wizard/AffinityGroupSelection.vue b/ui/src/views/compute/wizard/AffinityGroupSelection.vue index 38646c9ce4b..5c1ff2a7722 100644 --- a/ui/src/views/compute/wizard/AffinityGroupSelection.vue +++ b/ui/src/views/compute/wizard/AffinityGroupSelection.vue @@ -92,12 +92,17 @@ export default { { dataIndex: 'name', title: this.$t('label.affinity.groups'), - width: '40%' + width: '30%' + }, + { + dataIndex: 'type', + title: this.$t('label.type'), + width: '30%' }, { dataIndex: 'description', title: this.$t('label.description'), - width: '60%' + width: '40%' } ], selectedRowKeys: [],