diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index cc230268c42..c9d55870ede 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -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); diff --git a/api/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleAction.java b/api/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleAction.java new file mode 100644 index 00000000000..c05dafe8879 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleAction.java @@ -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; + } + } +} diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManager.java b/server/src/main/java/com/cloud/network/as/AutoScaleManager.java index eec1eec2ff1..cb1c577b934 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManager.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManager.java @@ -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); diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java index 805ac4aed86..f237354d1eb 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -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 passedScaleUpPolicyIds, final List 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)) { diff --git a/server/src/main/java/org/apache/cloudstack/schedule/BaseScheduleWorker.java b/server/src/main/java/org/apache/cloudstack/schedule/BaseScheduleWorker.java index 6fd33fcd949..11bf88298fd 100644 --- a/server/src/main/java/org/apache/cloudstack/schedule/BaseScheduleWorker.java +++ b/server/src/main/java/org/apache/cloudstack/schedule/BaseScheduleWorker.java @@ -366,7 +366,8 @@ public abstract class BaseScheduleWorker extends ManagerBase { */ public long submitAsyncJob( Class cmdClass, long accountId, long resourceId, long eventId, - Map extra) { + Map extra + ) { Map params = new HashMap<>(extra); params.put(ApiConstants.ID, String.valueOf(resourceId)); params.put("ctxUserId", "1"); diff --git a/server/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleWorker.java b/server/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleWorker.java new file mode 100644 index 00000000000..e90373a6e24 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleWorker.java @@ -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 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 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 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); + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index a9459b680eb..f4dcdf790ba 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -376,6 +376,9 @@ + + + diff --git a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java index 215a0e784bc..e36166bf927 100644 --- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java @@ -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); + } } diff --git a/server/src/test/java/org/apache/cloudstack/schedule/ResourceScheduleManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/schedule/ResourceScheduleManagerImplTest.java index e87459c2482..ab7233345a3 100644 --- a/server/src/test/java/org/apache/cloudstack/schedule/ResourceScheduleManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/schedule/ResourceScheduleManagerImplTest.java @@ -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 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 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) ReflectionTestUtils.getField(response, "details")).get("minmembers")); + Assert.assertEquals("5", ((Map) 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); diff --git a/server/src/test/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleWorkerTest.java b/server/src/test/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleWorkerTest.java new file mode 100644 index 00000000000..b3106d6371d --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleWorkerTest.java @@ -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 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 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 details = new HashMap<>(); + details.put("minmembers", "1"); + worker.validateDetails(AutoScaleScheduleAction.UPDATE, details); + } +} diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index bc029e732a2..431da22e079 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -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", diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index dd17116df02..922361a3937 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -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', diff --git a/ui/src/views/compute/ResourceSchedules.vue b/ui/src/views/compute/ResourceSchedules.vue index b8cb3379ef6..2b8797848e1 100644 --- a/ui/src/views/compute/ResourceSchedules.vue +++ b/ui/src/views/compute/ResourceSchedules.vue @@ -130,6 +130,41 @@ + + + + + + + + + + + + + +