Add support scheduling min & max for autoscaling groups

This commit is contained in:
vishesh92 2026-05-13 14:24:59 +05:30
parent cc2d5df079
commit f84692b257
No known key found for this signature in database
GPG Key ID: 4E395186CBFA790B
13 changed files with 454 additions and 18 deletions

View File

@ -677,6 +677,7 @@ public class EventTypes {
public static final String EVENT_AUTOSCALEVMGROUP_DISABLE = "AUTOSCALEVMGROUP.DISABLE";
public static final String EVENT_AUTOSCALEVMGROUP_SCALEDOWN = "AUTOSCALEVMGROUP.SCALEDOWN";
public static final String EVENT_AUTOSCALEVMGROUP_SCALEUP = "AUTOSCALEVMGROUP.SCALEUP";
public static final String EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE = "AUTOSCALEVMGROUP.SCHEDULE.UPDATE";
public static final String EVENT_BAREMETAL_DHCP_SERVER_ADD = "PHYSICAL.DHCP.ADD";
public static final String EVENT_BAREMETAL_DHCP_SERVER_DELETE = "PHYSICAL.DHCP.DELETE";
@ -895,6 +896,7 @@ public class EventTypes {
entityEventDetails.put(EVENT_VM_SCHEDULE_REBOOT, ResourceSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_STOP, ResourceSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_REBOOT, ResourceSchedule.class);
entityEventDetails.put(EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE, ResourceSchedule.class);
// Generic Resource Schedule
entityEventDetails.put(EVENT_SCHEDULE_CREATE, ResourceSchedule.class);

View File

@ -0,0 +1,31 @@
/*
* 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.schedule.autoscale;
import com.cloud.event.EventTypes;
import org.apache.cloudstack.schedule.ResourceSchedule;
public enum AutoScaleScheduleAction implements ResourceSchedule.Action {
UPDATE {
@Override
public String getEventType() {
return EventTypes.EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE;
}
}
}

View File

@ -48,6 +48,8 @@ public interface AutoScaleManager extends AutoScaleService {
void checkAutoScaleUser(Long autoscaleUserId, long accountId);
void validateMinMaxMembers(int minMembers, int maxMembers);
boolean deleteAutoScaleVmGroupsByAccount(Account account);
void cleanUpAutoScaleResources(Account account);

View File

@ -73,6 +73,7 @@ import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
@ -285,6 +286,8 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage
private VirtualMachineManager virtualMachineManager;
@Inject
GuestOSDao guestOSDao;
@Inject
private ResourceScheduleManager resourceScheduleManager;
private static final String PARAM_ROOT_DISK_SIZE = "rootdisksize";
private static final String PARAM_DISK_OFFERING_ID = "diskofferingid";
@ -1098,7 +1101,11 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage
if (autoScaleVmGroupVO.getState().equals(AutoScaleVmGroup.State.NEW)) {
/* This condition is for handling failures during creation command */
return autoScaleVmGroupDao.remove(id);
boolean removed = autoScaleVmGroupDao.remove(id);
if (removed) {
resourceScheduleManager.removeSchedulesForResource(ApiCommandResourceType.AutoScaleVmGroup, id);
}
return removed;
}
if (!autoScaleVmGroupVO.getState().equals(AutoScaleVmGroup.State.DISABLED) && !Boolean.TRUE.equals(cleanup)) {
@ -1168,6 +1175,8 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage
return false;
}
resourceScheduleManager.removeSchedulesForResource(ApiCommandResourceType.AutoScaleVmGroup, id);
logger.info("Successfully deleted autoscale vm group: {}", autoScaleVmGroupVO);
return success; // Successfull
}
@ -1231,6 +1240,22 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage
return searchWrapper.search();
}
@Override
public void validateMinMaxMembers(int minMembers, int maxMembers) {
if (minMembers <= 0) {
throw new InvalidParameterValueException(ApiConstants.MIN_MEMBERS + " is an invalid value: " + minMembers);
}
if (maxMembers <= 0) {
throw new InvalidParameterValueException(ApiConstants.MAX_MEMBERS + " is an invalid value: " + maxMembers);
}
if (minMembers > maxMembers) {
throw new InvalidParameterValueException(ApiConstants.MIN_MEMBERS + " (" + minMembers + ")cannot be greater than " + ApiConstants.MAX_MEMBERS + " (" +
maxMembers + ")");
}
}
@DB
protected AutoScaleVmGroupVO checkValidityAndPersist(final AutoScaleVmGroupVO vmGroup, final List<Long> passedScaleUpPolicyIds,
final List<Long> passedScaleDownPolicyIds) {
@ -1249,18 +1274,7 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage
ApiDBUtils.getAutoScaleVmGroupPolicyIds(vmGroup.getId(), currentScaleUpPolicyIds, currentScaleDownPolicyIds);
}
if (minMembers <= 0) {
throw new InvalidParameterValueException(ApiConstants.MIN_MEMBERS + " is an invalid value: " + minMembers);
}
if (maxMembers <= 0) {
throw new InvalidParameterValueException(ApiConstants.MAX_MEMBERS + " is an invalid value: " + maxMembers);
}
if (minMembers > maxMembers) {
throw new InvalidParameterValueException(ApiConstants.MIN_MEMBERS + " (" + minMembers + ")cannot be greater than " + ApiConstants.MAX_MEMBERS + " (" +
maxMembers + ")");
}
validateMinMaxMembers(minMembers, maxMembers);
if (interval <= 0) {
throw new InvalidParameterValueException("interval is an invalid value: " + interval);
@ -1341,10 +1355,10 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage
AutoScaleVmGroupVO vmGroupVO = getEntityInDatabase(CallContext.current().getCallingAccount(), "AutoScale Vm Group", vmGroupId, autoScaleVmGroupDao);
int currentInterval = vmGroupVO.getInterval();
boolean physicalParametersUpdate = (minMembers != null || maxMembers != null || (interval != null && interval != currentInterval) || CollectionUtils.isNotEmpty(scaleUpPolicyIds) || CollectionUtils.isNotEmpty(scaleDownPolicyIds));
boolean physicalParametersUpdate = ((interval != null && interval != currentInterval) || CollectionUtils.isNotEmpty(scaleUpPolicyIds) || CollectionUtils.isNotEmpty(scaleDownPolicyIds));
if (physicalParametersUpdate && !vmGroupVO.getState().equals(AutoScaleVmGroup.State.DISABLED)) {
throw new InvalidParameterValueException("An AutoScale Vm Group can be updated with minMembers/maxMembers/Interval only when it is in disabled state");
throw new InvalidParameterValueException("An AutoScale Vm Group can be updated with Interval/Policies only when it is in disabled state");
}
if (StringUtils.isNotBlank(name)) {

View File

@ -366,7 +366,8 @@ public abstract class BaseScheduleWorker extends ManagerBase {
*/
public <T extends BaseCmd> long submitAsyncJob(
Class<T> cmdClass, long accountId, long resourceId, long eventId,
Map<String, String> extra) {
Map<String, String> extra
) {
Map<String, String> params = new HashMap<>(extra);
params.put(ApiConstants.ID, String.valueOf(resourceId));
params.put("ctxUserId", "1");

View File

@ -0,0 +1,136 @@
// 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.schedule.autoscale;
import com.cloud.event.ActionEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.as.AutoScaleManager;
import com.cloud.network.as.AutoScaleVmGroup;
import com.cloud.network.as.AutoScaleVmGroupVO;
import com.cloud.network.as.dao.AutoScaleVmGroupDao;
import com.cloud.user.User;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmGroupCmd;
import org.apache.cloudstack.schedule.BaseScheduleWorker;
import org.apache.cloudstack.schedule.ResourceSchedule;
import org.apache.cloudstack.schedule.ResourceScheduledJobVO;
import org.apache.cloudstack.schedule.dao.ResourceScheduleDetailsDao;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.apache.cloudstack.api.ApiConstants.MAX_MEMBERS;
import static org.apache.cloudstack.api.ApiConstants.MIN_MEMBERS;
public class AutoScaleScheduleWorker extends BaseScheduleWorker {
@Inject
private AutoScaleManager autoScaleManager;
@Inject
private AutoScaleVmGroupDao autoScaleVmGroupDao;
@Inject
private ResourceScheduleDetailsDao resourceScheduleDetailsDao;
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.AutoScaleVmGroup;
}
@Override
public boolean isResourceValid(long resourceId) {
AutoScaleVmGroupVO group = autoScaleVmGroupDao.findById(resourceId);
return group != null && !AutoScaleVmGroup.State.REVOKE.equals(group.getState());
}
@Override
public long getEntityOwnerId(long resourceId) {
AutoScaleVmGroupVO group = autoScaleVmGroupDao.findById(resourceId);
return group != null ? group.getAccountId() : User.UID_SYSTEM;
}
@Override
public AutoScaleScheduleAction parseAction(String actionName) {
AutoScaleScheduleAction action = EnumUtils.getEnumIgnoreCase(AutoScaleScheduleAction.class, actionName);
if (action == null) {
throw new InvalidParameterValueException(String.format(
"Invalid action for AutoScaleVmGroup schedule: %s. Supported actions: %s",
actionName, Arrays.toString(AutoScaleScheduleAction.values())));
}
return action;
}
@Override
public void validateDetails(ResourceSchedule.Action action, Map<String, String> details) {
if (!(action instanceof AutoScaleScheduleAction)) {
throw new InvalidParameterValueException("Invalid action type for AutoScaleVmGroup schedule");
}
if (MapUtils.isEmpty(details)) {
throw new InvalidParameterValueException("Details are required for AutoScaleVmGroup schedule");
}
if (!details.keySet().stream().allMatch(key -> MIN_MEMBERS.equalsIgnoreCase(key) || MAX_MEMBERS.equalsIgnoreCase(key))) {
throw new InvalidParameterValueException("Only minmembers and maxmembers are supported for AutoScaleVmGroup schedule details");
}
String minMembersRaw = details.get(MIN_MEMBERS);
String maxMembersRaw = details.get(MAX_MEMBERS);
if (StringUtils.isBlank(minMembersRaw) || StringUtils.isBlank(maxMembersRaw)) {
throw new InvalidParameterValueException("Both minmembers and maxmembers are required for AutoScaleVmGroup schedule");
}
int minMembers;
int maxMembers;
try {
minMembers = Integer.parseInt(minMembersRaw);
maxMembers = Integer.parseInt(maxMembersRaw);
} catch (NumberFormatException e) {
throw new InvalidParameterValueException("minmembers and maxmembers must be valid integers");
}
autoScaleManager.validateMinMaxMembers(minMembers, maxMembers);
}
@Override
protected Long processJob(ResourceScheduledJobVO job) {
AutoScaleVmGroupVO group = autoScaleVmGroupDao.findById(job.getResourceId());
if (group == null || AutoScaleVmGroup.State.REVOKE.equals(group.getState())) {
logger.warn("AutoScaleVmGroup id={} not found/invalid; skipping scheduled job {}", job.getResourceId(), job);
return null;
}
AutoScaleScheduleAction action = parseAction(job.getActionName());
Map<String, String> details = resourceScheduleDetailsDao.listDetailsKeyPairs(job.getScheduleId(), true);
validateDetails(action, details);
long eventId = ActionEventUtils.onCompletedActionEvent(
User.UID_SYSTEM, group.getAccountId(), null,
action.getEventType(), true,
String.format("Executing action (%s) for AutoScaleVmGroup: %s", action, group.getUuid()),
group.getId(), ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
Map<String, String> params = new HashMap<>();
params.put(MIN_MEMBERS, details.get(MIN_MEMBERS));
params.put(MAX_MEMBERS, details.get(MAX_MEMBERS));
return submitAsyncJob(UpdateAutoScaleVmGroupCmd.class, group.getAccountId(), group.getId(), eventId, params);
}
}

View File

@ -376,6 +376,9 @@
<bean id="VMScheduleWorker" class="org.apache.cloudstack.schedule.vm.VMScheduleWorker">
<property name="asyncJobDispatcher" ref="ApiAsyncJobDispatcher" />
</bean>
<bean id="AutoScaleScheduleWorker" class="org.apache.cloudstack.schedule.autoscale.AutoScaleScheduleWorker">
<property name="asyncJobDispatcher" ref="ApiAsyncJobDispatcher" />
</bean>
<bean id="vnfTemplateManager" class="org.apache.cloudstack.storage.template.VnfTemplateManagerImpl" />

View File

@ -47,6 +47,7 @@ import org.apache.cloudstack.affinity.AffinityGroupVO;
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.command.admin.autoscale.CreateCounterCmd;
@ -62,6 +63,7 @@ import org.apache.cloudstack.api.command.user.vm.DeployVMCmd;
import org.apache.cloudstack.config.ApiServiceConfiguration;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.apache.cloudstack.userdata.UserDataManager;
import org.junit.After;
import org.junit.Assert;
@ -268,6 +270,8 @@ public class AutoScaleManagerImplTest {
VirtualMachineManager virtualMachineManager;
@Mock
GuestOSDao guestOSDao;
@Mock
ResourceScheduleManager resourceScheduleManager;
@Mock
NetworkOrchestrationService networkOrchestrationService;
@ -1036,7 +1040,6 @@ public class AutoScaleManagerImplTest {
when(asVmGroupMock.getInterval()).thenReturn(interval);
when(asVmGroupMock.getMaxMembers()).thenReturn(maxMembers);
when(asVmGroupMock.getMinMembers()).thenReturn(minMembers);
when(asVmGroupMock.getState()).thenReturn(AutoScaleVmGroup.State.DISABLED);
when(asVmGroupMock.getProfileId()).thenReturn(vmProfileId);
when(asVmGroupMock.getLoadBalancerId()).thenReturn(loadBalancerId);
@ -1086,7 +1089,6 @@ public class AutoScaleManagerImplTest {
when(autoScaleVmGroupDao.findById(vmGroupId)).thenReturn(asVmGroupMock);
when(asVmGroupMock.getInterval()).thenReturn(interval);
when(asVmGroupMock.getState()).thenReturn(AutoScaleVmGroup.State.ENABLED);
AutoScaleVmGroup vmGroup = autoScaleManagerImplSpy.updateAutoScaleVmGroup(cmd);
}
@ -1213,6 +1215,7 @@ public class AutoScaleManagerImplTest {
Mockito.verify(autoScaleManagerImplSpy).configureAutoScaleVmGroup(vmGroupId, AutoScaleVmGroup.State.ENABLED);
Mockito.verify(annotationDao).removeByEntityType(AnnotationService.EntityType.AUTOSCALE_VM_GROUP.name(), vmGroupUuid);
Mockito.verify(autoScaleManagerImplSpy).cancelMonitorTask(vmGroupId);
Mockito.verify(resourceScheduleManager).removeSchedulesForResource(ApiCommandResourceType.AutoScaleVmGroup, vmGroupId);
}
@Test
@ -2557,4 +2560,19 @@ public class AutoScaleManagerImplTest {
Assert.assertTrue(result.first().matches(vmHostNamePattern));
Assert.assertEquals(result.first(), result.second());
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateMinMaxMembersInvalidMin() {
autoScaleManagerImplSpy.validateMinMaxMembers(-1, 5);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateMinMaxMembersInvalidMax() {
autoScaleManagerImplSpy.validateMinMaxMembers(1, -1);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateMinMaxMembersInvalidRange() {
autoScaleManagerImplSpy.validateMinMaxMembers(5, 1);
}
}

View File

@ -19,6 +19,7 @@
package org.apache.cloudstack.schedule;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.as.AutoScaleVmGroup;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.User;
@ -34,6 +35,8 @@ import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.schedule.dao.ResourceScheduleDao;
import org.apache.cloudstack.schedule.dao.ResourceScheduleDetailsDao;
import org.apache.cloudstack.schedule.autoscale.AutoScaleScheduleAction;
import org.apache.cloudstack.schedule.autoscale.AutoScaleScheduleWorker;
import org.apache.cloudstack.schedule.vm.VMScheduleAction;
import org.apache.cloudstack.schedule.vm.VMScheduleWorker;
import org.apache.commons.lang.time.DateUtils;
@ -73,6 +76,9 @@ public class ResourceScheduleManagerImplTest {
@Mock
VMScheduleWorker vmScheduleWorker;
@Mock
AutoScaleScheduleWorker autoScaleScheduleWorker;
@Mock
UserVmManager userVmManager;
@ -92,8 +98,10 @@ public class ResourceScheduleManagerImplTest {
CallContext.register(callingUser, callingAccount);
Mockito.when(vmScheduleWorker.getApiResourceType()).thenReturn(ApiCommandResourceType.VirtualMachine);
Mockito.when(autoScaleScheduleWorker.getApiResourceType()).thenReturn(ApiCommandResourceType.AutoScaleVmGroup);
Map<ApiCommandResourceType, BaseScheduleWorker> workerMap = new HashMap<>();
workerMap.put(ApiCommandResourceType.VirtualMachine, vmScheduleWorker);
workerMap.put(ApiCommandResourceType.AutoScaleVmGroup, autoScaleScheduleWorker);
ReflectionTestUtils.setField(resourceScheduleManager, "workerMap", workerMap);
}
@ -226,6 +234,43 @@ public class ResourceScheduleManagerImplTest {
validateResponse(response, schedule, vm);
}
@Test
public void createScheduleAutoScale() {
AutoScaleVmGroup group = Mockito.mock(AutoScaleVmGroup.class);
ResourceScheduleVO schedule = Mockito.mock(ResourceScheduleVO.class);
Account ownerAccount = Mockito.mock(Account.class);
Map<String, String> details = new HashMap<>();
details.put("minmembers", "2");
details.put("maxmembers", "5");
Mockito.when(group.getId()).thenReturn(21L);
Mockito.when(group.getUuid()).thenReturn(UUID.randomUUID().toString());
Mockito.when(entityManager.findByUuid(AutoScaleVmGroup.class, "asg-uuid")).thenReturn(group);
Mockito.when(autoScaleScheduleWorker.isResourceValid(21L)).thenReturn(true);
Mockito.when(autoScaleScheduleWorker.getEntityOwnerId(21L)).thenReturn(2L);
Mockito.when(autoScaleScheduleWorker.parseAction("UPDATE")).thenReturn(AutoScaleScheduleAction.UPDATE);
Mockito.when(accountManager.getAccount(2L)).thenReturn(ownerAccount);
Mockito.when(resourceScheduleDao.persist(Mockito.any(ResourceScheduleVO.class))).thenReturn(schedule);
Mockito.when(schedule.getId()).thenReturn(99L);
Mockito.when(schedule.getResourceType()).thenReturn(ApiCommandResourceType.AutoScaleVmGroup);
Mockito.when(schedule.getResourceId()).thenReturn(21L);
Mockito.when(schedule.getActionName()).thenReturn("UPDATE");
Mockito.when(autoScaleScheduleWorker.parseAction("UPDATE")).thenReturn(AutoScaleScheduleAction.UPDATE);
Mockito.when(entityManager.findById(AutoScaleVmGroup.class, 21L)).thenReturn(group);
ResourceScheduleResponse response = resourceScheduleManager.createSchedule(
ApiCommandResourceType.AutoScaleVmGroup, "asg-uuid", null,
"0 0 * * *", "UTC", "UPDATE",
DateUtils.addDays(new Date(), 1), DateUtils.addDays(new Date(), 2),
true, details);
Assert.assertEquals(ApiCommandResourceType.AutoScaleVmGroup, ReflectionTestUtils.getField(response, "resourceType"));
Assert.assertEquals("21", ReflectionTestUtils.getField(response, "resourceId"));
Assert.assertEquals("2", ((Map<String, String>) ReflectionTestUtils.getField(response, "details")).get("minmembers"));
Assert.assertEquals("5", ((Map<String, String>) ReflectionTestUtils.getField(response, "details")).get("maxmembers"));
Mockito.verify(autoScaleScheduleWorker, Mockito.times(1)).validateDetails(Mockito.eq(AutoScaleScheduleAction.UPDATE), Mockito.eq(details));
}
@Test
public void removeSchedulesForResource() {
ResourceScheduleVO schedule1 = Mockito.mock(ResourceScheduleVO.class);

View File

@ -0,0 +1,129 @@
/*
* 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.schedule.autoscale;
import com.cloud.event.ActionEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.as.AutoScaleManager;
import com.cloud.network.as.AutoScaleVmGroup;
import com.cloud.network.as.AutoScaleVmGroupVO;
import com.cloud.network.as.dao.AutoScaleVmGroupDao;
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmGroupCmd;
import org.apache.cloudstack.schedule.ResourceScheduledJobVO;
import org.apache.cloudstack.schedule.dao.ResourceScheduleDao;
import org.apache.cloudstack.schedule.dao.ResourceScheduleDetailsDao;
import org.apache.cloudstack.schedule.dao.ResourceScheduledJobDao;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.HashMap;
import java.util.Map;
@RunWith(MockitoJUnitRunner.class)
public class AutoScaleScheduleWorkerTest {
@Spy
@InjectMocks
private AutoScaleScheduleWorker worker = new AutoScaleScheduleWorker();
@Mock
private AutoScaleVmGroupDao autoScaleVmGroupDao;
@Mock
private ResourceScheduleDetailsDao resourceScheduleDetailsDao;
@Mock
private ResourceScheduleDao resourceScheduleDao;
@Mock
private ResourceScheduledJobDao resourceScheduledJobDao;
@Mock
private AsyncJobManager asyncJobManager;
@Mock
private AutoScaleManager autoScaleManager;
private AutoCloseable closeable;
private MockedStatic<ActionEventUtils> actionEventUtilsMocked;
@Before
public void setUp() {
closeable = MockitoAnnotations.openMocks(this);
actionEventUtilsMocked = Mockito.mockStatic(ActionEventUtils.class);
Mockito.when(ActionEventUtils.onCompletedActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(),
Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString(),
Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong())).thenReturn(1L);
}
@After
public void tearDown() throws Exception {
actionEventUtilsMocked.close();
closeable.close();
}
@Test
public void testProcessJobWithValidDetailsSubmitsUpdateAutoScaleVmGroupCmd() {
ResourceScheduledJobVO job = Mockito.mock(ResourceScheduledJobVO.class);
AutoScaleVmGroupVO group = Mockito.mock(AutoScaleVmGroupVO.class);
Map<String, String> details = new HashMap<>();
details.put("minmembers", "2");
details.put("maxmembers", "5");
Mockito.when(job.getResourceId()).thenReturn(1L);
Mockito.when(job.getScheduleId()).thenReturn(10L);
Mockito.when(job.getActionName()).thenReturn(AutoScaleScheduleAction.UPDATE.name());
Mockito.when(autoScaleVmGroupDao.findById(1L)).thenReturn(group);
Mockito.when(group.getState()).thenReturn(AutoScaleVmGroup.State.ENABLED);
Mockito.when(group.getAccountId()).thenReturn(3L);
Mockito.when(group.getId()).thenReturn(1L);
Mockito.when(group.getUuid()).thenReturn("asg-uuid");
Mockito.when(resourceScheduleDetailsDao.listDetailsKeyPairs(10L, true)).thenReturn(details);
Mockito.doReturn(11L).when(worker).submitAsyncJob(
Mockito.eq(UpdateAutoScaleVmGroupCmd.class), Mockito.eq(3L), Mockito.eq(1L), Mockito.eq(1L), Mockito.anyMap());
Long asyncJobId = worker.processJob(job);
Assert.assertEquals(Long.valueOf(11L), asyncJobId);
Mockito.verify(worker).submitAsyncJob(Mockito.eq(UpdateAutoScaleVmGroupCmd.class), Mockito.eq(3L), Mockito.eq(1L), Mockito.eq(1L),
Mockito.argThat(map -> "2".equals(map.get("minmembers")) && "5".equals(map.get("maxmembers"))));
}
@Test
public void testProcessJobWithMissingGroupSkipsExecution() {
ResourceScheduledJobVO job = Mockito.mock(ResourceScheduledJobVO.class);
Mockito.when(job.getResourceId()).thenReturn(1L);
Mockito.when(autoScaleVmGroupDao.findById(1L)).thenReturn(null);
Long asyncJobId = worker.processJob(job);
Assert.assertNull(asyncJobId);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateDetailsMissingRequiredKeys() {
Map<String, String> details = new HashMap<>();
details.put("minmembers", "1");
worker.validateDetails(AutoScaleScheduleAction.UPDATE, details);
}
}

View File

@ -2240,6 +2240,7 @@
"label.schedule": "Schedule",
"label.scheduled": "Scheduled",
"label.schedule.add": "Add schedule",
"label.update.members": "Update members",
"label.scheduled.backups": "Scheduled backups",
"label.schedules": "Schedules",
"label.scope": "Scope",

View File

@ -909,6 +909,12 @@ export default {
name: 'scaledown.policy',
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/AutoScaleDownPolicyTab.vue')))
},
{
name: 'schedules',
resourceType: 'AutoScaleVmGroup',
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ResourceSchedules.vue'))),
show: () => { return 'listResourceSchedule' in store.getters.apis }
},
{
name: 'events',
resourceType: 'AutoScaleVmGroup',

View File

@ -130,6 +130,41 @@
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-row
v-if="resourceType === 'AutoScaleVmGroup'"
justify="space-between"
>
<a-col :span="11">
<a-form-item
name="minMembers"
ref="minMembers"
>
<template #label>
<tooltip-label :title="$t('label.minimum.members')" />
</template>
<a-input-number
v-model:value="form.minMembers"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item
name="maxMembers"
ref="maxMembers"
>
<template #label>
<tooltip-label :title="$t('label.maximum.members')" />
</template>
<a-input-number
v-model:value="form.maxMembers"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item
name="timezone"
ref="timezone"
@ -323,6 +358,9 @@ export default {
{ value: 'REBOOT', label: 'label.reboot' },
{ value: 'FORCE_STOP', label: 'label.force.stop' },
{ value: 'FORCE_REBOOT', label: 'label.force.reboot' }
],
AutoScaleVmGroup: [
{ value: 'UPDATE', label: 'label.update.members' }
]
},
periods: [
@ -382,6 +420,8 @@ export default {
schedule: '* * * * *',
description: '',
timezone: 'UTC',
minMembers: null,
maxMembers: null,
startDate: '',
endDate: '',
enabled: true,
@ -390,6 +430,8 @@ export default {
this.rules = reactive({
schedule: [{ type: 'string', required: true, message: this.$t('message.error.required.input') }],
action: [{ type: 'string', required: true, message: this.$t('message.error.required.input') }],
minMembers: [{ required: this.resourceType === 'AutoScaleVmGroup', message: this.$t('message.error.required.input') }],
maxMembers: [{ required: this.resourceType === 'AutoScaleVmGroup', message: this.$t('message.error.required.input') }],
timezone: [{ required: true, message: `${this.$t('message.error.select')}` }],
startDate: [{ required: false, message: `${this.$t('message.error.select')}` }],
endDate: [{ required: false, message: `${this.$t('message.error.select')}` }]
@ -425,6 +467,8 @@ export default {
this.resetForm()
this.isEdit = true
Object.assign(this.form, schedule)
this.form.minMembers = schedule?.details?.minmembers ? Number(schedule.details.minmembers) : null
this.form.maxMembers = schedule?.details?.maxmembers ? Number(schedule.details.maxmembers) : null
// Some weird issue when we directly pass in the moment with tz object
this.form.startDate = dayjs(schedule.startdate).tz(schedule.timezone)
this.form.endDate = schedule.enddate ? dayjs(dayjs(schedule.enddate).tz(schedule.timezone)) : null
@ -450,6 +494,10 @@ export default {
startdate: (values.startDate) ? values.startDate.format(this.pattern) : null,
enddate: (values.endDate) ? values.endDate.format(this.pattern) : null
}
if (this.resourceType === 'AutoScaleVmGroup') {
params['details[0].minmembers'] = values.minMembers
params['details[1].maxmembers'] = values.maxMembers
}
let command = null
if (this.form.id === null || this.form.id === undefined) {
command = 'createResourceSchedule'