Skip to content

Commit 2fae1e4

Browse files
committed
fixup
1 parent 0d0c1c1 commit 2fae1e4

9 files changed

Lines changed: 101 additions & 44 deletions

File tree

engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleVO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public ResourceScheduleVO(ApiCommandResourceType resourceType, long resourceId,
105105
@Override
106106
public String toString() {
107107
return String.format("ResourceSchedule %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
108-
this, "id", "uuid", "resourceType", "actionName", "description"));
108+
this, "id", "uuid", "resourceType", "actionName", "description", "enabled"));
109109
}
110110

111111
@Override

server/src/main/java/org/apache/cloudstack/schedule/BaseScheduleWorker.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ protected final String getResourceTypeName() {
105105
*/
106106
protected abstract Long processJob(ResourceScheduledJobVO job);
107107

108+
/**
109+
* Get a human-readable description of the schedule, for use in the UI and audit logs.
110+
*/
111+
public String getDescription(ResourceSchedule.Action parsedAction, CronExpression cronExpression, Map<String, String> details) {
112+
return String.format("%s - %s", parsedAction.name(), DateUtil.getHumanReadableSchedule(cronExpression));
113+
}
114+
108115
// -------------------------------------------------------------------------
109116
// Lifecycle
110117
// -------------------------------------------------------------------------
@@ -234,6 +241,11 @@ public Date scheduleNextJob(ResourceScheduleVO schedule, Date timestamp) {
234241
return null;
235242
}
236243

244+
if (zonedEnd != null && ts.isAfter(zonedEnd)) {
245+
logger.info("Next schedule time {} is after end time {}. No more jobs to schedule for {}.", ts, zonedEnd, schedule);
246+
return null;
247+
}
248+
237249
Date scheduledDateTime = Date.from(ts.toInstant());
238250
ResourceScheduledJobVO existingJob = resourceScheduledJobDao.findByScheduleAndTimestamp(schedule.getId(), scheduledDateTime);
239251
if (existingJob != null) {
@@ -250,8 +262,9 @@ public Date scheduleNextJob(ResourceScheduleVO schedule, Date timestamp) {
250262
ActionEventUtils.onScheduledActionEvent(
251263
User.UID_SYSTEM, accountId,
252264
parseAction(schedule.getActionName()).getEventType(),
253-
String.format("Scheduled action (%s) [resource: %d, schedule: %s] at %s",
254-
schedule.getActionName(), schedule.getResourceId(), schedule, scheduledDateTime),
265+
String.format("Scheduled action %s as part of Resource Schedule (uuid=%s description='%s' schedule='%s') for %s (id=%d) at %s",
266+
schedule.getActionName(), schedule.getUuid(), schedule.getDescription(),
267+
schedule.getSchedule(), getResourceTypeName(), schedule.getResourceId(), scheduledDateTime),
255268
schedule.getResourceId(), getResourceTypeName(), true, 0);
256269
} catch (EntityExistsException e) {
257270
logger.debug("Job already scheduled (concurrent insert).");

server/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleManagerImpl.java

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.apache.cloudstack.framework.config.Configurable;
4747
import org.apache.cloudstack.schedule.dao.ResourceScheduleDao;
4848
import org.apache.cloudstack.schedule.dao.ResourceScheduleDetailsDao;
49+
import org.apache.commons.collections4.MapUtils;
4950
import org.apache.commons.lang.time.DateUtils;
5051
import org.apache.commons.lang3.ObjectUtils;
5152
import org.apache.commons.lang3.StringUtils;
@@ -193,10 +194,11 @@ public ResourceScheduleResponse createSchedule(ApiCommandResourceType resourceTy
193194
validateStartDateEndDate(startDate, endDate, timeZone);
194195

195196
if (StringUtils.isBlank(description)) {
196-
description = String.format("%s - %s", parsedAction.name(), DateUtil.getHumanReadableSchedule(cronExpression));
197+
description = worker.getDescription(parsedAction, cronExpression, details);
197198
}
198199

199-
logger.warn("Using timezone [{}] for running the schedule for resource [{}], as an equivalent of [{}].", timeZoneId, resourceUuid, timeZoneStr);
200+
logger.warn("Using timezone [{}] for running the schedule for resource [{}], as an equivalent of [{}].",
201+
timeZoneId, resourceUuid, timeZoneStr);
200202

201203
String finalDescription = description;
202204
String finalAction = parsedAction.name();
@@ -209,7 +211,7 @@ public ResourceScheduleResponse createSchedule(ApiCommandResourceType resourceTy
209211
finalDescription, cronExpression.toString(), timeZoneId,
210212
finalAction, finalStartDate, finalEndDate, enabled));
211213

212-
if (details != null && !details.isEmpty()) {
214+
if (MapUtils.isNotEmpty(details)) {
213215
List<ResourceScheduleDetailVO> detailVOs = new ArrayList<>();
214216
for (Map.Entry<String, String> entry : details.entrySet()) {
215217
detailVOs.add(new ResourceScheduleDetailVO(scheduleVO.getId(), entry.getKey(), entry.getValue(), true));
@@ -221,6 +223,7 @@ public ResourceScheduleResponse createSchedule(ApiCommandResourceType resourceTy
221223

222224
CallContext.current().setEventResourceId(internalResourceId);
223225
CallContext.current().setEventResourceType(worker.getApiResourceType());
226+
CallContext.current().setEventDetails(String.format("Created resource schedule %s", scheduleVO));
224227
return createResponse(scheduleVO, details);
225228
});
226229
}
@@ -298,8 +301,9 @@ public ResourceScheduleResponse updateSchedule(Long id, String description, Stri
298301
long ownerId = worker.getEntityOwnerId(scheduleVO.getResourceId());
299302
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, false, accountManager.getAccount(ownerId));
300303

301-
if (details != null && !details.isEmpty()) {
302-
worker.validateDetails(worker.parseAction(scheduleVO.getActionName()), details);
304+
ResourceSchedule.Action parsedAction = worker.parseAction(scheduleVO.getActionName());
305+
if (MapUtils.isNotEmpty(details)) {
306+
worker.validateDetails(parsedAction, details);
303307
}
304308

305309
CronExpression cronExpression = Objects.requireNonNullElse(
@@ -308,7 +312,10 @@ public ResourceScheduleResponse updateSchedule(Long id, String description, Stri
308312
);
309313

310314
if (description == null && scheduleVO.getDescription() == null) {
311-
description = String.format("%s - %s", scheduleVO.getActionName(), DateUtil.getHumanReadableSchedule(cronExpression));
315+
Map<String, String> effectiveDetails = MapUtils.isNotEmpty(details)
316+
? details
317+
: resourceScheduleDetailsDao.listDetailsKeyPairs(id, true);
318+
description = worker.getDescription(parsedAction, cronExpression, effectiveDetails);
312319
}
313320

314321
final String originalTimeZone = scheduleVO.getTimeZone();
@@ -365,23 +372,21 @@ public ResourceScheduleResponse updateSchedule(Long id, String description, Stri
365372
return Transaction.execute((TransactionCallback<ResourceScheduleResponse>) status -> {
366373
resourceScheduleDao.update(id, scheduleVO);
367374

368-
if (details != null) {
369-
if (details.isEmpty()) {
370-
resourceScheduleDetailsDao.removeDetails(id);
371-
} else {
372-
List<ResourceScheduleDetailVO> detailVOs = new ArrayList<>();
373-
for (Map.Entry<String, String> entry : details.entrySet()) {
374-
detailVOs.add(new ResourceScheduleDetailVO(id, entry.getKey(), entry.getValue(), true));
375-
}
376-
resourceScheduleDetailsDao.saveDetails(detailVOs);
375+
if (MapUtils.isNotEmpty(details)) {
376+
List<ResourceScheduleDetailVO> detailVOs = new ArrayList<>();
377+
for (Map.Entry<String, String> entry : details.entrySet()) {
378+
detailVOs.add(new ResourceScheduleDetailVO(id, entry.getKey(), entry.getValue(), true));
377379
}
380+
resourceScheduleDetailsDao.saveDetails(detailVOs);
378381
}
379382

380383
worker.updateScheduledJob(scheduleVO);
381384

382385
CallContext.current().setEventResourceId(scheduleVO.getResourceId());
383386
CallContext.current().setEventResourceType(worker.getApiResourceType());
384387

388+
CallContext.current().setEventDetails(String.format("Updated resource schedule %s", scheduleVO));
389+
385390
// Re-load details if they weren't fully replaced
386391
Map<String, String> currentDetails = resourceScheduleDetailsDao.listDetailsKeyPairs(id, true);
387392
return createResponse(scheduleVO, currentDetails);

server/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleWorker.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.cloud.network.as.AutoScaleVmGroupVO;
2424
import com.cloud.network.as.dao.AutoScaleVmGroupDao;
2525
import com.cloud.user.User;
26+
import com.cloud.utils.DateUtil;
2627
import org.apache.cloudstack.api.ApiCommandResourceType;
2728
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmGroupCmd;
2829
import org.apache.cloudstack.schedule.BaseScheduleWorker;
@@ -32,6 +33,7 @@
3233
import org.apache.commons.collections4.MapUtils;
3334
import org.apache.commons.lang3.EnumUtils;
3435
import org.apache.commons.lang3.StringUtils;
36+
import org.springframework.scheduling.support.CronExpression;
3537

3638
import javax.inject.Inject;
3739
import java.util.Arrays;
@@ -57,6 +59,11 @@ public ApiCommandResourceType getApiResourceType() {
5759
return ApiCommandResourceType.AutoScaleVmGroup;
5860
}
5961

62+
@Override
63+
public String getDescription(ResourceSchedule.Action parsedAction, CronExpression cronExpression, Map<String, String> details) {
64+
return String.format("%s Min (%s) Max (%s) - %s", parsedAction.name(), details.get(MIN_MEMBERS), details.get(MAX_MEMBERS), DateUtil.getHumanReadableSchedule(cronExpression));
65+
}
66+
6067
@Override
6168
public boolean isResourceValid(long resourceId) {
6269
AutoScaleVmGroupVO group = autoScaleVmGroupDao.findById(resourceId);
@@ -122,15 +129,18 @@ protected Long processJob(ResourceScheduledJobVO job) {
122129
Map<String, String> details = resourceScheduleDetailsDao.listDetailsKeyPairs(job.getScheduleId(), true);
123130
validateDetails(action, details);
124131

132+
String minMembers = details.get(MIN_MEMBERS);
133+
String maxMembers = details.get(MAX_MEMBERS);
125134
long eventId = ActionEventUtils.onCompletedActionEvent(
126135
User.UID_SYSTEM, group.getAccountId(), null,
127136
action.getEventType(), true,
128-
String.format("Executing action (%s) for AutoScaleVmGroup: %s", action, group.getUuid()),
137+
String.format("Executing action %s (min=%s max=%s) for AutoScaleVmGroup: %s (min=%s max=%s)",
138+
action, minMembers, maxMembers, group.getUuid(), group.getMinMembers(), group.getMaxMembers()),
129139
group.getId(), ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
130140

131141
Map<String, String> params = new HashMap<>();
132-
params.put(MIN_MEMBERS, details.get(MIN_MEMBERS));
133-
params.put(MAX_MEMBERS, details.get(MAX_MEMBERS));
142+
params.put(MIN_MEMBERS, minMembers);
143+
params.put(MAX_MEMBERS, maxMembers);
134144
return submitAsyncJob(UpdateAutoScaleVmGroupCmd.class, group.getAccountId(), group.getId(), eventId, params);
135145
}
136146
}

server/src/main/java/org/apache/cloudstack/schedule/vm/VMScheduleWorker.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020

2121
import com.cloud.event.ActionEventUtils;
2222
import com.cloud.exception.InvalidParameterValueException;
23+
import com.cloud.network.as.AutoScaleVmGroupVmMapVO;
24+
import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao;
2325
import com.cloud.user.User;
26+
import com.cloud.uservm.UserVm;
2427
import com.cloud.vm.UserVmManager;
2528
import com.cloud.vm.VirtualMachine;
2629
import org.apache.cloudstack.api.ApiCommandResourceType;
@@ -31,26 +34,36 @@
3134
import org.apache.cloudstack.schedule.BaseScheduleWorker;
3235
import org.apache.cloudstack.schedule.ResourceSchedule;
3336
import org.apache.cloudstack.schedule.ResourceScheduledJobVO;
37+
import org.apache.commons.collections4.CollectionUtils;
3438
import org.apache.commons.lang3.EnumUtils;
3539

3640
import javax.inject.Inject;
3741
import java.util.Arrays;
3842
import java.util.Collections;
43+
import java.util.List;
3944
import java.util.Map;
4045

4146
public class VMScheduleWorker extends BaseScheduleWorker {
4247

4348
@Inject
4449
private UserVmManager userVmManager;
4550

51+
@Inject
52+
private AutoScaleVmGroupVmMapDao autoScaleVmGroupVmMapDao;
53+
4654
@Override
4755
public ApiCommandResourceType getApiResourceType() {
4856
return ApiCommandResourceType.VirtualMachine;
4957
}
5058

5159
@Override
5260
public boolean isResourceValid(long resourceId) {
53-
return userVmManager.getUserVm(resourceId) != null;
61+
UserVm userVm = userVmManager.getUserVm(resourceId);
62+
if (userVm != null) {
63+
List<AutoScaleVmGroupVmMapVO> autoScalingGroups = autoScaleVmGroupVmMapDao.listByVm(resourceId);
64+
return CollectionUtils.isEmpty(autoScalingGroups);
65+
}
66+
return false;
5467
}
5568

5669
@Override

server/src/test/java/org/apache/cloudstack/schedule/vm/VMScheduleWorkerTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.cloud.event.ActionEventUtils;
2222
import com.cloud.exception.InvalidParameterValueException;
23+
import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao;
2324
import com.cloud.uservm.UserVm;
2425
import com.cloud.utils.component.ComponentContext;
2526
import com.cloud.vm.UserVmManager;
@@ -68,6 +69,9 @@ public class VMScheduleWorkerTest {
6869
@Mock
6970
private UserVmManager userVmManager;
7071

72+
@Mock
73+
private AutoScaleVmGroupVmMapDao autoScaleVmGroupVmMapDao;
74+
7175
@Mock
7276
private ResourceScheduleDao resourceScheduleDao;
7377

ui/src/components/view/ListView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@
800800
{{ text && $toLocaleDate(text) }}
801801
</template>
802802
<template
803-
v-if="['startdate', 'enddate'].includes(column.key) && ['vm', 'vnfapp'].includes($route.path.split('/')[1])"
803+
v-if="['startdate', 'enddate'].includes(column.key) && ['vm', 'vnfapp', 'autoscalevmgroup'].includes($route.path.split('/')[1])"
804804
>
805805
{{ getDateAtTimeZone(text, record.timezone) }}
806806
</template>

ui/src/views/compute/InstanceTab.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
<a-tab-pane
9191
:tab="$t('label.schedules')"
9292
key="schedules"
93-
v-if="'listResourceSchedule' in $store.getters.apis"
93+
v-if="'listResourceSchedule' in $store.getters.apis && !dataResource.autoscalevmgroupid"
9494
>
9595
<ResourceSchedules
9696
:resource="vm"

ui/src/views/compute/ResourceSchedules.vue

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,20 @@
4343
icon="edit-outlined"
4444
@onClick="updateSchedule(record)"
4545
/>
46-
<tooltip-button
47-
:tooltip="$t('label.remove')"
48-
:disabled="!('deleteResourceSchedule' in $store.getters.apis)"
49-
icon="delete-outlined"
50-
:danger="true"
51-
type="primary"
52-
@onClick="removeSchedule(record)"
53-
/>
46+
<a-popconfirm
47+
:title="$t('label.delete') + ' ' + $t('label.schedule') + '?'"
48+
:okText="$t('label.yes')"
49+
:cancelText="$t('label.no')"
50+
@confirm="removeSchedule(record)"
51+
>
52+
<tooltip-button
53+
:tooltip="$t('label.remove')"
54+
:disabled="!('deleteResourceSchedule' in $store.getters.apis)"
55+
icon="delete-outlined"
56+
:danger="true"
57+
type="primary"
58+
/>
59+
</a-popconfirm>
5460
</template>
5561
</list-view>
5662
<a-pagination
@@ -399,6 +405,12 @@ export default {
399405
},
400406
created () {
401407
this.selectedColumnKeys = this.columnKeys
408+
if (this.resourceType === 'AutoScaleVmGroup') {
409+
this.columnKeys = ['enabled', 'description', 'schedule', 'minmembers',
410+
'maxmembers', 'timezone', 'startdate', 'enddate', 'created',
411+
'scheduleActions']
412+
this.selectedColumnKeys = [...this.columnKeys]
413+
}
402414
this.updateColumns()
403415
this.pageSize = this.pageSizeOptions[0] * 1
404416
this.initForm()
@@ -525,7 +537,6 @@ export default {
525537
if (error.errorFields !== undefined) {
526538
this.formRef.value.scrollToField(error.errorFields[0].name)
527539
}
528-
}).finally(() => {
529540
this.isSubmitted = false
530541
})
531542
},
@@ -568,7 +579,12 @@ export default {
568579
getAPI('listResourceSchedule', params).then(json => {
569580
this.schedules = []
570581
this.totalCount = json?.listresourcescheduleresponse?.count || 0
571-
this.schedules = json?.listresourcescheduleresponse?.resourceschedule || []
582+
const rawSchedules = json?.listresourcescheduleresponse?.resourceschedule || []
583+
this.schedules = rawSchedules.map(s => ({
584+
...s,
585+
minmembers: s.details?.minmembers,
586+
maxmembers: s.details?.maxmembers
587+
}))
572588
}).catch(error => {
573589
console.error(error)
574590
this.$notifyError(error)
@@ -594,20 +610,16 @@ export default {
594610
},
595611
updateColumns () {
596612
this.columns = []
613+
const columnTitleMap = {
614+
enabled: this.$t('label.state'),
615+
startdate: this.$t('label.start.date.and.time'),
616+
enddate: this.$t('label.end.date.and.time')
617+
}
597618
for (var columnKey of this.columnKeys) {
598619
if (!this.selectedColumnKeys.includes(columnKey)) continue
599620
this.columns.push({
600621
key: columnKey,
601-
// If columnKey is 'enabled', then title is 'state'
602-
// If columnKey is 'startdate', then the title is `start.date.and.time`
603-
// else title is columnKey
604-
title: columnKey === 'enabled'
605-
? this.$t('label.state')
606-
: columnKey === 'startdate'
607-
? this.$t('label.start.date.and.time')
608-
: columnKey === 'enddate'
609-
? this.$t('label.end.date.and.time')
610-
: this.$t('label.' + String(columnKey).toLowerCase()),
622+
title: columnTitleMap[columnKey] || this.$t('label.' + String(columnKey).toLowerCase()),
611623
dataIndex: columnKey
612624
})
613625
}

0 commit comments

Comments
 (0)