feature: VM Scheduler (#7397)

This PR adds a feature to setup schedules to stop/start/restart your VMs.

Documentation PR: apache/cloudstack-documentation#313
Related issue: #3387
This commit is contained in:
Vishesh 2023-06-26 13:36:46 +05:30 committed by GitHub
parent 409e3202a3
commit fa3f2a75eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 4559 additions and 72 deletions

View File

@ -84,6 +84,7 @@ import com.cloud.user.User;
import com.cloud.vm.Nic; import com.cloud.vm.Nic;
import com.cloud.vm.NicSecondaryIp; import com.cloud.vm.NicSecondaryIp;
import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.vm.schedule.VMSchedule;
public class EventTypes { public class EventTypes {
@ -111,6 +112,17 @@ public class EventTypes {
public static final String EVENT_VM_UNMANAGE = "VM.UNMANAGE"; public static final String EVENT_VM_UNMANAGE = "VM.UNMANAGE";
public static final String EVENT_VM_RECOVER = "VM.RECOVER"; public static final String EVENT_VM_RECOVER = "VM.RECOVER";
// VM Schedule
public static final String EVENT_VM_SCHEDULE_CREATE = "VM.SCHEDULE.CREATE";
public static final String EVENT_VM_SCHEDULE_UPDATE = "VM.SCHEDULE.UPDATE";
public static final String EVENT_VM_SCHEDULE_DELETE = "VM.SCHEDULE.DELETE";
public static final String EVENT_VM_SCHEDULE_START = "VM.SCHEDULE.START";
public static final String EVENT_VM_SCHEDULE_STOP = "VM.SCHEDULE.STOP";
public static final String EVENT_VM_SCHEDULE_REBOOT = "VM.SCHEDULE.REBOOT";
public static final String EVENT_VM_SCHEDULE_FORCE_STOP = "VM.SCHEDULE.FORCE.STOP";
public static final String EVENT_VM_SCHEDULE_FORCE_REBOOT = "VM.SCHEDULE.FORCE.REBOOT";
// Domain Router // Domain Router
public static final String EVENT_ROUTER_CREATE = "ROUTER.CREATE"; public static final String EVENT_ROUTER_CREATE = "ROUTER.CREATE";
public static final String EVENT_ROUTER_DESTROY = "ROUTER.DESTROY"; public static final String EVENT_ROUTER_DESTROY = "ROUTER.DESTROY";
@ -716,6 +728,16 @@ public class EventTypes {
entityEventDetails.put(EVENT_VM_IMPORT, VirtualMachine.class); entityEventDetails.put(EVENT_VM_IMPORT, VirtualMachine.class);
entityEventDetails.put(EVENT_VM_UNMANAGE, VirtualMachine.class); entityEventDetails.put(EVENT_VM_UNMANAGE, VirtualMachine.class);
// VMSchedule
entityEventDetails.put(EVENT_VM_SCHEDULE_CREATE, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_DELETE, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_UPDATE, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_START, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_STOP, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_REBOOT, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_STOP, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_REBOOT, VMSchedule.class);
entityEventDetails.put(EVENT_ROUTER_CREATE, VirtualRouter.class); entityEventDetails.put(EVENT_ROUTER_CREATE, VirtualRouter.class);
entityEventDetails.put(EVENT_ROUTER_DESTROY, VirtualRouter.class); entityEventDetails.put(EVENT_ROUTER_DESTROY, VirtualRouter.class);
entityEventDetails.put(EVENT_ROUTER_START, VirtualRouter.class); entityEventDetails.put(EVENT_ROUTER_START, VirtualRouter.class);

View File

@ -0,0 +1,152 @@
/*
* 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.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import javax.inject.Inject;
import java.util.Date;
@APICommand(name = "createVMSchedule", description = "Create VM Schedule", responseObject = VMScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class CreateVMScheduleCmd extends BaseCmd {
@Inject
VMScheduleManager vmScheduleManager;
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
type = CommandType.UUID,
entityType = UserVmResponse.class,
required = true,
description = "ID of the VM for which schedule is to be defined")
private Long vmId;
@Parameter(name = ApiConstants.DESCRIPTION,
type = CommandType.STRING,
required = false,
description = "Description of the schedule")
private String description;
@Parameter(name = ApiConstants.SCHEDULE,
type = CommandType.STRING,
required = true,
description = "Schedule for action on VM in cron format. e.g. '0 15 10 * *' for 'at 15:00 on 10th day of every month'")
private String schedule;
@Parameter(name = ApiConstants.TIMEZONE,
type = CommandType.STRING,
required = true,
description = "Specifies a timezone for this command. For more information on the timezone parameter, see TimeZone Format.")
private String timeZone;
@Parameter(name = ApiConstants.ACTION,
type = CommandType.STRING,
required = true,
description = "Action to take on the VM (start/stop/restart/force_stop/force_reboot).")
private String action;
@Parameter(name = ApiConstants.START_DATE,
type = CommandType.DATE,
required = false,
description = "start date from which the schedule becomes active. Defaults to current date plus 1 minute."
+ "Use format \"yyyy-MM-dd hh:mm:ss\")")
private Date startDate;
@Parameter(name = ApiConstants.END_DATE,
type = CommandType.DATE,
required = false,
description = "end date after which the schedule becomes inactive"
+ "Use format \"yyyy-MM-dd hh:mm:ss\")")
private Date endDate;
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
required = false,
description = "Enable VM schedule. Defaults to true")
private Boolean enabled;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getVmId() {
return vmId;
}
public String getDescription() {
return description;
}
public String getSchedule() {
return schedule;
}
public String getTimeZone() {
return timeZone;
}
public String getAction() {
return action;
}
public Date getStartDate() {
return startDate;
}
public Date getEndDate() {
return endDate;
}
public Boolean getEnabled() {
if (enabled == null) {
enabled = true;
}
return enabled;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
VMScheduleResponse response = vmScheduleManager.createSchedule(this);
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, getVmId());
if (vm == null) {
throw new InvalidParameterValueException(String.format("Unable to find VM by id=%d", getVmId()));
}
return vm.getAccountId();
}
}

View File

@ -0,0 +1,112 @@
/*
* 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.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import javax.inject.Inject;
import java.util.Collections;
import java.util.List;
@APICommand(name = "deleteVMSchedule", description = "Delete VM Schedule.", responseObject = SuccessResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class DeleteVMScheduleCmd extends BaseCmd {
@Inject
VMScheduleManager vmScheduleManager;
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
type = CommandType.UUID,
entityType = UserVmResponse.class,
required = true,
description = "ID of VM")
private Long vmId;
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
entityType = VMScheduleResponse.class,
required = false,
description = "ID of VM schedule")
private Long id;
@Parameter(name = ApiConstants.IDS,
type = CommandType.LIST,
collectionType = CommandType.UUID,
entityType = VMScheduleResponse.class,
required = false,
description = "IDs of VM schedule")
private List<Long> ids;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public List<Long> getIds() {
if (ids == null) {
return Collections.emptyList();
}
return ids;
}
public Long getVmId() {
return vmId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
long rowsRemoved = vmScheduleManager.removeSchedule(this);
if (rowsRemoved > 0) {
final SuccessResponse response = new SuccessResponse();
response.setResponseName(getCommandName());
response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase());
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete VM Schedules");
}
}
@Override
public long getEntityOwnerId() {
VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, getVmId());
if (vm == null) {
throw new InvalidParameterValueException(String.format("Unable to find VM by id=%d", getVmId()));
}
return vm.getAccountId();
}
}

View File

@ -0,0 +1,97 @@
/*
* 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.api.command.user.vm;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import javax.inject.Inject;
@APICommand(name = "listVMSchedule", description = "List VM Schedules.", responseObject = VMScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class ListVMScheduleCmd extends BaseListCmd {
@Inject
VMScheduleManager vmScheduleManager;
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
type = CommandType.UUID,
entityType = UserVmResponse.class,
required = true,
description = "ID of the VM for which schedule is to be defined")
private Long vmId;
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
entityType = VMScheduleResponse.class,
required = false,
description = "ID of VM schedule")
private Long id;
@Parameter(name = ApiConstants.ACTION,
type = CommandType.STRING,
required = false,
description = "Action taken by schedule")
private String action;
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
required = false,
description = "ID of VM schedule")
private Boolean enabled;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getVmId() {
return vmId;
}
public Long getId() {
return id;
}
public String getAction() {
return action;
}
public Boolean getEnabled() {
return enabled;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
ListResponse<VMScheduleResponse> response = vmScheduleManager.listSchedule(this);
response.setResponseName(getCommandName());
response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase());
setResponseObject(response);
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import javax.inject.Inject;
import java.util.Date;
@APICommand(name = "updateVMSchedule", description = "Update VM Schedule.", responseObject = VMScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class UpdateVMScheduleCmd extends BaseCmd {
@Inject
VMScheduleManager vmScheduleManager;
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
entityType = VMScheduleResponse.class,
required = true,
description = "ID of VM schedule")
private Long id;
@Parameter(name = ApiConstants.DESCRIPTION,
type = CommandType.STRING,
required = false,
description = "Name of the schedule")
private String description;
@Parameter(name = ApiConstants.SCHEDULE,
type = CommandType.STRING,
required = false,
description = "Schedule for action on VM in cron format. e.g. '0 15 10 * *' for 'at 15:00 on 10th day of every month'")
private String schedule;
@Parameter(name = ApiConstants.TIMEZONE,
type = CommandType.STRING,
required = false,
description = "Specifies a timezone for this command. For more information on the timezone parameter, see TimeZone Format.")
private String timeZone;
@Parameter(name = ApiConstants.START_DATE,
type = CommandType.DATE,
required = false,
description = "start date from which the schedule becomes active"
+ "Use format \"yyyy-MM-dd hh:mm:ss\")")
private Date startDate;
@Parameter(name = ApiConstants.END_DATE,
type = CommandType.DATE,
required = false,
description = "end date after which the schedule becomes inactive"
+ "Use format \"yyyy-MM-dd hh:mm:ss\")")
private Date endDate;
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
required = false,
description = "Enable VM schedule")
private Boolean enabled;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public String getDescription() {
return description;
}
public String getSchedule() {
return schedule;
}
public String getTimeZone() {
return timeZone;
}
public Date getStartDate() {
return startDate;
}
public Date getEndDate() {
return endDate;
}
public Boolean getEnabled() {
return enabled;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
VMScheduleResponse response = vmScheduleManager.updateSchedule(this);
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
VMSchedule vmSchedule = _entityMgr.findById(VMSchedule.class, getId());
if (vmSchedule == null) {
throw new InvalidParameterValueException(String.format("Unable to find vmSchedule by id=%d", getId()));
}
VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, vmSchedule.getVmId());
return vm.getAccountId();
}
}

View File

@ -0,0 +1,109 @@
/*
* 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.api.response;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import java.util.Date;
@EntityReference(value = VMSchedule.class)
public class VMScheduleResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "the ID of VM schedule")
private String id;
@SerializedName(ApiConstants.VIRTUAL_MACHINE_ID)
@Param(description = "ID of virtual machine")
private String vmId;
@SerializedName(ApiConstants.DESCRIPTION)
@Param(description = "Description of VM schedule")
private String description;
@SerializedName(ApiConstants.SCHEDULE)
@Param(description = "Cron formatted VM schedule")
private String schedule;
@SerializedName(ApiConstants.TIMEZONE)
@Param(description = "Timezone of the schedule")
private String timeZone;
@SerializedName(ApiConstants.ACTION)
@Param(description = "Action")
private VMSchedule.Action action;
@SerializedName(ApiConstants.ENABLED)
@Param(description = "VM schedule is enabled")
private boolean enabled;
@SerializedName(ApiConstants.START_DATE)
@Param(description = "Date from which the schedule is active")
private Date startDate;
@SerializedName(ApiConstants.END_DATE)
@Param(description = "Date after which the schedule becomes inactive")
private Date endDate;
@SerializedName(ApiConstants.CREATED)
@Param(description = "Date when the schedule was created")
private Date created;
public void setId(String id) {
this.id = id;
}
public void setVmId(String vmId) {
this.vmId = vmId;
}
public void setDescription(String description) {
this.description = description;
}
public void setSchedule(String schedule) {
this.schedule = schedule;
}
public void setTimeZone(String timeZone) {
this.timeZone = timeZone;
}
public void setAction(VMSchedule.Action action) {
this.action = action;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
public void setCreated(Date created) {this.created = created;}
}

View File

@ -0,0 +1,51 @@
/*
* 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.vm.schedule;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import java.time.ZoneId;
import java.util.Date;
public interface VMSchedule extends Identity, InternalIdentity {
enum Action {
START, STOP, REBOOT, FORCE_STOP, FORCE_REBOOT
}
long getVmId();
String getDescription();
String getSchedule();
String getTimeZone();
Action getAction();
boolean getEnabled();
Date getStartDate();
Date getEndDate();
ZoneId getTimeZoneId();
Date getCreated();
}

View File

@ -0,0 +1,40 @@
/*
* 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.vm.schedule;
import org.apache.cloudstack.api.command.user.vm.CreateVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.DeleteVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.ListVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVMScheduleCmd;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
public interface VMScheduleManager {
VMScheduleResponse createSchedule(CreateVMScheduleCmd createVMScheduleCmd);
VMScheduleResponse createResponse(VMSchedule vmSchedule);
ListResponse<VMScheduleResponse> listSchedule(ListVMScheduleCmd listVMScheduleCmd);
VMScheduleResponse updateSchedule(UpdateVMScheduleCmd updateVMScheduleCmd);
long removeScheduleByVmId(long vmId, boolean expunge);
Long removeSchedule(DeleteVMScheduleCmd deleteVMScheduleCmd);
}

View File

@ -0,0 +1,38 @@
/*
* 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.vm.schedule;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import java.util.Date;
public interface VMScheduledJob extends Identity, InternalIdentity {
long getVmId();
long getVmScheduleId();
Long getAsyncJobId();
void setAsyncJobId(long asyncJobId);
VMSchedule.Action getAction();
Date getScheduledTime();
}

View File

@ -0,0 +1,96 @@
/*
* 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.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.db.EntityManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.security.InvalidParameterException;
public class CreateVMScheduleCmdTest {
@Mock
public VMScheduleManager vmScheduleManager;
@Mock
public EntityManager entityManager;
@InjectMocks
private CreateVMScheduleCmd createVMScheduleCmd = new CreateVMScheduleCmd();
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
/**
* given: "We have a VMScheduleManager and CreateVMScheduleCmd"
* when: "CreateVMScheduleCmd is executed successfully"
* then: "a VMSchedule response is created"
*/
@Test
public void testSuccessfulExecution() {
VMScheduleResponse vmScheduleResponse = Mockito.mock(VMScheduleResponse.class);
Mockito.when(vmScheduleManager.createSchedule(createVMScheduleCmd)).thenReturn(vmScheduleResponse);
createVMScheduleCmd.execute();
Assert.assertEquals(vmScheduleResponse, createVMScheduleCmd.getResponseObject());
}
/**
* given: "We have a VMScheduleManager and CreateVMScheduleCmd"
* when: "CreateVMScheduleCmd is executed with an invalid parameter"
* then: "an InvalidParameterException is thrown"
*/
@Test(expected = InvalidParameterException.class)
public void testInvalidParameterException() {
Mockito.when(vmScheduleManager.createSchedule(createVMScheduleCmd)).thenThrow(InvalidParameterException.class);
createVMScheduleCmd.execute();
}
/**
* given: "We have an EntityManager and CreateVMScheduleCmd"
* when: "CreateVMScheduleCmd.getEntityOwnerId is executed for a VM which does exist"
* then: "owner of that VM is returned"
*/
@Test
public void testSuccessfulGetEntityOwnerId() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(entityManager.findById(VirtualMachine.class, createVMScheduleCmd.getVmId())).thenReturn(vm);
long ownerId = createVMScheduleCmd.getEntityOwnerId();
Assert.assertEquals(vm.getAccountId(), ownerId);
}
/**
* given: "We have an EntityManager and CreateVMScheduleCmd"
* when: "CreateVMScheduleCmd.getEntityOwnerId is executed for a VM which doesn't exist"
* then: "an InvalidParameterException is thrown"
*/
@Test(expected = InvalidParameterValueException.class)
public void testFailureGetEntityOwnerId() {
Mockito.when(entityManager.findById(VirtualMachine.class, createVMScheduleCmd.getVmId())).thenReturn(null);
long ownerId = createVMScheduleCmd.getEntityOwnerId();
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.db.EntityManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.security.InvalidParameterException;
public class DeleteVMScheduleCmdTest {
@Mock
public VMScheduleManager vmScheduleManager;
@Mock
public EntityManager entityManager;
@InjectMocks
private DeleteVMScheduleCmd deleteVMScheduleCmd = new DeleteVMScheduleCmd();
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
/**
* given: "We have a VMScheduleManager and DeleteVMScheduleCmd"
* when: "VMScheduleManager.removeSchedule() is executed returning 1 row"
* then: "a Success response is created"
*/
@Test
public void testSuccessfulExecution() {
final SuccessResponse response = new SuccessResponse();
response.setResponseName(deleteVMScheduleCmd.getCommandName());
response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase());
Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenReturn(1L);
deleteVMScheduleCmd.execute();
SuccessResponse actualResponse = (SuccessResponse) deleteVMScheduleCmd.getResponseObject();
Assert.assertEquals(response.getResponseName(), actualResponse.getResponseName());
Assert.assertEquals(response.getObjectName(), actualResponse.getObjectName());
}
/**
* given: "We have a VMScheduleManager and DeleteVMScheduleCmd"
* when: "VMScheduleManager.removeSchedule() is executed returning 0 row"
* then: "ServerApiException is thrown"
*/
@Test(expected = ServerApiException.class)
public void testServerApiException() {
Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenReturn(0L);
deleteVMScheduleCmd.execute();
}
/**
* given: "We have a VMScheduleManager and DeleteVMScheduleCmd"
* when: "DeleteVMScheduleCmd is executed with an invalid parameter"
* then: "an InvalidParameterException is thrown"
*/
@Test(expected = InvalidParameterException.class)
public void testInvalidParameterException() {
Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenThrow(InvalidParameterException.class);
deleteVMScheduleCmd.execute();
}
/**
* given: "We have an EntityManager and DeleteVMScheduleCmd"
* when: "DeleteVMScheduleCmd.getEntityOwnerId is executed for a VM which does exist"
* then: "owner of that VM is returned"
*/
@Test
public void testSuccessfulGetEntityOwnerId() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(entityManager.findById(VirtualMachine.class, deleteVMScheduleCmd.getVmId())).thenReturn(vm);
long ownerId = deleteVMScheduleCmd.getEntityOwnerId();
Assert.assertEquals(vm.getAccountId(), ownerId);
}
/**
* given: "We have an EntityManager and DeleteVMScheduleCmd"
* when: "DeleteVMScheduleCmd.getEntityOwnerId is executed for a VM which doesn't exist"
* then: "an InvalidParameterException is thrown"
*/
@Test(expected = InvalidParameterValueException.class)
public void testFailureGetEntityOwnerId() {
Mockito.when(entityManager.findById(VirtualMachine.class, deleteVMScheduleCmd.getVmId())).thenReturn(null);
long ownerId = deleteVMScheduleCmd.getEntityOwnerId();
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.api.command.user.vm;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collections;
public class ListVMScheduleCmdTest {
@Mock
public VMScheduleManager vmScheduleManager;
@InjectMocks
private ListVMScheduleCmd listVMScheduleCmd = new ListVMScheduleCmd();
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
/**
* given: "We have a VMScheduleManager with 0 schedules and ListVMScheduleCmd"
* when: "ListVMScheduleCmd is executed"
* then: "a list of size 0 is returned"
*/
@Test
public void testEmptyResponse() {
ListResponse<VMScheduleResponse> response = new ListResponse<VMScheduleResponse>();
response.setResponses(new ArrayList<VMScheduleResponse>());
Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenReturn(response);
listVMScheduleCmd.execute();
ListResponse<VMScheduleResponse> actualResponseObject = (ListResponse<VMScheduleResponse>) listVMScheduleCmd.getResponseObject();
Assert.assertEquals(response, actualResponseObject);
Assert.assertEquals(0L, actualResponseObject.getResponses().size());
}
/**
* given: "We have a VMScheduleManager with 1 schedule and ListVMScheduleCmd"
* when: "ListVMScheduleCmd is executed"
* then: "a list of size 1 is returned"
*/
@Test
public void testNonEmptyResponse() {
ListResponse<VMScheduleResponse> listResponse = new ListResponse<VMScheduleResponse>();
VMScheduleResponse response = Mockito.mock(VMScheduleResponse.class);
listResponse.setResponses(Collections.singletonList(response));
Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenReturn(listResponse);
listVMScheduleCmd.execute();
ListResponse<VMScheduleResponse> actualResponseObject = (ListResponse<VMScheduleResponse>) listVMScheduleCmd.getResponseObject();
Assert.assertEquals(listResponse, actualResponseObject);
Assert.assertEquals(1L, actualResponseObject.getResponses().size());
Assert.assertEquals(response, actualResponseObject.getResponses().get(0));
}
/**
* given: "We have a VMScheduleManager and ListVMScheduleCmd"
* when: "ListVMScheduleCmd is executed with an invalid parameter"
* then: "an InvalidParameterException is thrown"
*/
@Test(expected = InvalidParameterException.class)
public void testInvalidParameterException() {
Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenThrow(InvalidParameterException.class);
listVMScheduleCmd.execute();
ListResponse<VMScheduleResponse> actualResponseObject = (ListResponse<VMScheduleResponse>) listVMScheduleCmd.getResponseObject();
}
}

View File

@ -0,0 +1,102 @@
/*
* 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.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.db.EntityManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.security.InvalidParameterException;
public class UpdateVMScheduleCmdTest {
@Mock
public VMScheduleManager vmScheduleManager;
@Mock
public EntityManager entityManager;
@InjectMocks
private UpdateVMScheduleCmd updateVMScheduleCmd = new UpdateVMScheduleCmd();
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
/**
* given: "We have a VMScheduleManager and UpdateVMScheduleCmd"
* when: "UpdateVMScheduleCmd is executed successfully"
* then: "a VMSchedule response is created"
*/
@Test
public void testSuccessfulExecution() {
VMScheduleResponse vmScheduleResponse = Mockito.mock(VMScheduleResponse.class);
Mockito.when(vmScheduleManager.updateSchedule(updateVMScheduleCmd)).thenReturn(vmScheduleResponse);
updateVMScheduleCmd.execute();
Assert.assertEquals(vmScheduleResponse, updateVMScheduleCmd.getResponseObject());
}
/**
* given: "We have a VMScheduleManager and UpdateVMScheduleCmd"
* when: "UpdateVMScheduleCmd is executed with an invalid parameter"
* then: "an InvalidParameterException is thrown"
*/
@Test(expected = InvalidParameterException.class)
public void testInvalidParameterException() {
Mockito.when(vmScheduleManager.updateSchedule(updateVMScheduleCmd)).thenThrow(InvalidParameterException.class);
updateVMScheduleCmd.execute();
}
/**
* given: "We have an EntityManager and UpdateVMScheduleCmd"
* when: "UpdateVMScheduleCmd.getEntityOwnerId is executed for a VM which does exist"
* then: "owner of that VM is returned"
*/
@Test
public void testSuccessfulGetEntityOwnerId() {
VMSchedule vmSchedule = Mockito.mock(VMSchedule.class);
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(entityManager.findById(VMSchedule.class, updateVMScheduleCmd.getId())).thenReturn(vmSchedule);
Mockito.when(entityManager.findById(VirtualMachine.class, vmSchedule.getVmId())).thenReturn(vm);
long ownerId = updateVMScheduleCmd.getEntityOwnerId();
Assert.assertEquals(vm.getAccountId(), ownerId);
}
/**
* given: "We have an EntityManager and UpdateVMScheduleCmd"
* when: "UpdateVMScheduleCmd.getEntityOwnerId is executed for a VM Schedule which doesn't exist"
* then: "an InvalidParameterException is thrown"
*/
@Test(expected = InvalidParameterValueException.class)
public void testFailureGetEntityOwnerId() {
VMSchedule vmSchedule = Mockito.mock(VMSchedule.class);
Mockito.when(entityManager.findById(VMSchedule.class, updateVMScheduleCmd.getId())).thenReturn(null);
long ownerId = updateVMScheduleCmd.getEntityOwnerId();
}
}

View File

@ -0,0 +1,183 @@
/*
* 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.vm.schedule;
import com.cloud.utils.db.GenericDao;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.time.ZoneId;
import java.util.Date;
import java.util.TimeZone;
import java.util.UUID;
@Entity
@Table(name = "vm_schedule")
public class VMScheduleVO implements VMSchedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
Long id;
@Column(name = "uuid", nullable = false)
String uuid;
@Column(name = "description")
String description;
@Column(name = "vm_id", nullable = false)
long vmId;
@Column(name = "schedule", nullable = false)
String schedule;
@Column(name = "timezone", nullable = false)
String timeZone;
@Column(name = "action", nullable = false)
@Enumerated(value = EnumType.STRING)
Action action;
@Column(name = "enabled", nullable = false)
boolean enabled;
@Column(name = "start_date", nullable = false)
@Temporal(value = TemporalType.TIMESTAMP)
Date startDate;
@Column(name = "end_date", nullable = true)
@Temporal(value = TemporalType.TIMESTAMP)
Date endDate;
@Column(name = GenericDao.CREATED_COLUMN)
Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
Date removed;
public VMScheduleVO() {
uuid = UUID.randomUUID().toString();
}
public VMScheduleVO(long vmId, String description, String schedule, String timeZone, Action action, Date startDate, Date endDate, boolean enabled) {
uuid = UUID.randomUUID().toString();
this.vmId = vmId;
this.description = description;
this.schedule = schedule;
this.timeZone = timeZone;
this.action = action;
this.startDate = startDate;
this.endDate = endDate;
this.enabled = enabled;
}
@Override
public String getUuid() {
return uuid;
}
@Override
public long getId() {
return id;
}
public long getVmId() {
return vmId;
}
public void setVmId(long vmId) {
this.vmId = vmId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getSchedule() {
return schedule.substring(2);
}
public void setSchedule(String schedule) {
this.schedule = schedule;
}
@Override
public String getTimeZone() {
return timeZone;
}
public void setTimeZone(String timeZone) {
this.timeZone = timeZone;
}
public Action getAction() {
return action;
}
public void setAction(Action action) {
this.action = action;
}
public boolean getEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
public Date getStartDate() {
return startDate;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
@Override
public Date getEndDate() {
return endDate;
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
@Override
public ZoneId getTimeZoneId() {
return TimeZone.getTimeZone(getTimeZone()).toZoneId();
}
public Date getCreated() {
return created;
}
}

View File

@ -0,0 +1,113 @@
/*
* 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.vm.schedule;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.util.Date;
import java.util.UUID;
@Entity
@Table(name = "vm_scheduled_job")
public class VMScheduledJobVO implements VMScheduledJob {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Long id;
@Column(name = "uuid", nullable = false)
String uuid;
@Column(name = "vm_id", nullable = false)
long vmId;
@Column(name = "vm_schedule_id", nullable = false)
long vmScheduleId;
@Column(name = "async_job_id")
Long asyncJobId;
@Column(name = "action", nullable = false)
@Enumerated(value = EnumType.STRING)
VMSchedule.Action action;
@Column(name = "scheduled_timestamp")
@Temporal(value = TemporalType.TIMESTAMP)
Date scheduledTime;
public VMScheduledJobVO() {
uuid = UUID.randomUUID().toString();
}
public VMScheduledJobVO(long vmId, long vmScheduleId, VMSchedule.Action action, Date scheduledTime) {
uuid = UUID.randomUUID().toString();
this.vmId = vmId;
this.vmScheduleId = vmScheduleId;
this.action = action;
this.scheduledTime = scheduledTime;
}
@Override
public String getUuid() {
return uuid;
}
@Override
public long getId() {
return id;
}
@Override
public long getVmId() {
return vmId;
}
@Override
public long getVmScheduleId() {
return vmScheduleId;
}
@Override
public Long getAsyncJobId() {
return asyncJobId;
}
@Override
public void setAsyncJobId(long asyncJobId) {
this.asyncJobId = asyncJobId;
}
@Override
public VMSchedule.Action getAction() {
return action;
}
@Override
public Date getScheduledTime() {
return scheduledTime;
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.vm.schedule.dao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.GenericDao;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleVO;
import java.util.List;
public interface VMScheduleDao extends GenericDao<VMScheduleVO, Long> {
List<VMScheduleVO> listAllActiveSchedules();
long removeSchedulesForVmIdAndIds(Long vmId, List<Long> ids);
Pair<List<VMScheduleVO>, Integer> searchAndCount(Long id, Long vmId, VMSchedule.Action action, Boolean enabled, Long offset, Long limit);
SearchCriteria<VMScheduleVO> getSearchCriteriaForVMId(Long vmId);
}

View File

@ -0,0 +1,108 @@
/*
* 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.vm.schedule.dao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleVO;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
public class VMScheduleDaoImpl extends GenericDaoBase<VMScheduleVO, Long> implements VMScheduleDao {
private final SearchBuilder<VMScheduleVO> activeScheduleSearch;
private final SearchBuilder<VMScheduleVO> scheduleSearchByVmIdAndIds;
private final SearchBuilder<VMScheduleVO> scheduleSearch;
public VMScheduleDaoImpl() {
super();
activeScheduleSearch = createSearchBuilder();
activeScheduleSearch.and(ApiConstants.ENABLED, activeScheduleSearch.entity().getEnabled(), SearchCriteria.Op.EQ);
activeScheduleSearch.and().op(activeScheduleSearch.entity().getEndDate(), SearchCriteria.Op.NULL);
activeScheduleSearch.or(ApiConstants.END_DATE, activeScheduleSearch.entity().getEndDate(), SearchCriteria.Op.GT);
activeScheduleSearch.cp();
activeScheduleSearch.done();
scheduleSearchByVmIdAndIds = createSearchBuilder();
scheduleSearchByVmIdAndIds.and(ApiConstants.ID, scheduleSearchByVmIdAndIds.entity().getId(), SearchCriteria.Op.IN);
scheduleSearchByVmIdAndIds.and(ApiConstants.VIRTUAL_MACHINE_ID, scheduleSearchByVmIdAndIds.entity().getVmId(), SearchCriteria.Op.EQ);
scheduleSearchByVmIdAndIds.done();
scheduleSearch = createSearchBuilder();
scheduleSearch.and(ApiConstants.ID, scheduleSearch.entity().getId(), SearchCriteria.Op.EQ);
scheduleSearch.and(ApiConstants.VIRTUAL_MACHINE_ID, scheduleSearch.entity().getVmId(), SearchCriteria.Op.EQ);
scheduleSearch.and(ApiConstants.ACTION, scheduleSearch.entity().getAction(), SearchCriteria.Op.EQ);
scheduleSearch.and(ApiConstants.ENABLED, scheduleSearch.entity().getEnabled(), SearchCriteria.Op.EQ);
scheduleSearch.done();
}
@Override
public List<VMScheduleVO> listAllActiveSchedules() {
// WHERE enabled = true AND (end_date IS NULL OR end_date > current_date)
SearchCriteria<VMScheduleVO> sc = activeScheduleSearch.create();
sc.setParameters(ApiConstants.ENABLED, true);
sc.setParameters(ApiConstants.END_DATE, new Date());
return search(sc, null);
}
@Override
public long removeSchedulesForVmIdAndIds(Long vmId, List<Long> ids) {
SearchCriteria<VMScheduleVO> sc = scheduleSearchByVmIdAndIds.create();
sc.setParameters(ApiConstants.ID, ids.toArray());
sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId);
return remove(sc);
}
@Override
public Pair<List<VMScheduleVO>, Integer> searchAndCount(Long id, Long vmId, VMSchedule.Action action, Boolean enabled, Long offset, Long limit) {
SearchCriteria<VMScheduleVO> sc = scheduleSearch.create();
if (id != null) {
sc.setParameters(ApiConstants.ID, id);
}
if (enabled != null) {
sc.setParameters(ApiConstants.ENABLED, enabled);
}
if (action != null) {
sc.setParameters(ApiConstants.ACTION, action);
}
sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId);
Filter filter = new Filter(VMScheduleVO.class, ApiConstants.ID, false, offset, limit);
return searchAndCount(sc, filter);
}
@Override
public SearchCriteria<VMScheduleVO> getSearchCriteriaForVMId(Long vmId) {
SearchCriteria<VMScheduleVO> sc = scheduleSearch.create();
sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId);
return sc;
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.vm.schedule.dao;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.vm.schedule.VMScheduledJobVO;
import java.util.Date;
import java.util.List;
public interface VMScheduledJobDao extends GenericDao<VMScheduledJobVO, Long> {
List<VMScheduledJobVO> listJobsToStart(Date currentTimestamp);
int expungeJobsForSchedules(List<Long> scheduleId, Date dateAfter);
int expungeJobsBefore(Date currentTimestamp);
}

View File

@ -0,0 +1,95 @@
/*
* 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.vm.schedule.dao;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.vm.schedule.VMScheduledJobVO;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@Component
public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long> implements VMScheduledJobDao {
private final SearchBuilder<VMScheduledJobVO> jobsToStartSearch;
private final SearchBuilder<VMScheduledJobVO> expungeJobsBeforeSearch;
private final SearchBuilder<VMScheduledJobVO> expungeJobForScheduleSearch;
static final String SCHEDULED_TIMESTAMP = "scheduled_timestamp";
static final String VM_SCHEDULE_ID = "vm_schedule_id";
public VMScheduledJobDaoImpl() {
super();
jobsToStartSearch = createSearchBuilder();
jobsToStartSearch.and(SCHEDULED_TIMESTAMP, jobsToStartSearch.entity().getScheduledTime(), SearchCriteria.Op.EQ);
jobsToStartSearch.and("async_job_id", jobsToStartSearch.entity().getAsyncJobId(), SearchCriteria.Op.NULL);
jobsToStartSearch.done();
expungeJobsBeforeSearch = createSearchBuilder();
expungeJobsBeforeSearch.and(SCHEDULED_TIMESTAMP, expungeJobsBeforeSearch.entity().getScheduledTime(), SearchCriteria.Op.LT);
expungeJobsBeforeSearch.done();
expungeJobForScheduleSearch = createSearchBuilder();
expungeJobForScheduleSearch.and(VM_SCHEDULE_ID, expungeJobForScheduleSearch.entity().getVmScheduleId(), SearchCriteria.Op.IN);
expungeJobForScheduleSearch.and(SCHEDULED_TIMESTAMP, expungeJobForScheduleSearch.entity().getScheduledTime(), SearchCriteria.Op.GTEQ);
expungeJobForScheduleSearch.done();
}
/**
* Execution of job wouldn't be at exact seconds. So, we round off and then execute.
*/
@Override
public List<VMScheduledJobVO> listJobsToStart(Date currentTimestamp) {
if (currentTimestamp == null) {
currentTimestamp = new Date();
}
Date truncatedTs = DateUtils.round(currentTimestamp, Calendar.MINUTE);
SearchCriteria<VMScheduledJobVO> sc = jobsToStartSearch.create();
sc.setParameters(SCHEDULED_TIMESTAMP, truncatedTs);
Filter filter = new Filter(VMScheduledJobVO.class, "vmScheduleId", true, null, null);
return search(sc, filter);
}
@Override
public int expungeJobsForSchedules(List<Long> vmScheduleIds, Date dateAfter) {
SearchCriteria<VMScheduledJobVO> sc = expungeJobForScheduleSearch.create();
sc.setParameters(VM_SCHEDULE_ID, vmScheduleIds.toArray());
if (dateAfter != null) {
sc.setParameters(SCHEDULED_TIMESTAMP, dateAfter);
}
return expunge(sc);
}
@Override
public int expungeJobsBefore(Date date) {
SearchCriteria<VMScheduledJobVO> sc = expungeJobsBeforeSearch.create();
sc.setParameters(SCHEDULED_TIMESTAMP, date);
return expunge(sc);
}
}

View File

@ -275,4 +275,6 @@
<bean id="UserVmDeployAsIsDetailsDaoImpl" class="com.cloud.deployasis.dao.UserVmDeployAsIsDetailsDaoImpl" /> <bean id="UserVmDeployAsIsDetailsDaoImpl" class="com.cloud.deployasis.dao.UserVmDeployAsIsDetailsDaoImpl" />
<bean id="NetworkPermissionDaoImpl" class="org.apache.cloudstack.network.dao.NetworkPermissionDaoImpl" /> <bean id="NetworkPermissionDaoImpl" class="org.apache.cloudstack.network.dao.NetworkPermissionDaoImpl" />
<bean id="PassphraseDaoImpl" class="org.apache.cloudstack.secret.dao.PassphraseDaoImpl" /> <bean id="PassphraseDaoImpl" class="org.apache.cloudstack.secret.dao.PassphraseDaoImpl" />
<bean id="VMScheduleDaoImpl" class="org.apache.cloudstack.vm.schedule.dao.VMScheduleDaoImpl" />
<bean id="VMScheduledJobDaoImpl" class="org.apache.cloudstack.vm.schedule.dao.VMScheduledJobDaoImpl" />
</beans> </beans>

View File

@ -135,5 +135,44 @@ CREATE VIEW `cloud`.`async_job_view` AS
UPDATE `cloud`.`console_session` SET removed=now(); UPDATE `cloud`.`console_session` SET removed=now();
-- Modify acquired column in console_session to datetime type -- Modify acquired column in console_session to datetime type
ALTER TABLE `cloud`.`console_session` DROP `acquired`, ADD `acquired` datetime COMMENT 'When the session was acquired' AFTER `host_id`; ALTER TABLE `cloud`.`console_session` DROP `acquired`, ADD `acquired` datetime COMMENT 'When the session was acquired' AFTER `host_id`;
-- create_public_parameter_on_roles. #6960 -- create_public_parameter_on_roles. #6960
ALTER TABLE `cloud`.`roles` ADD COLUMN `public_role` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Indicates whether the role will be visible to all users (public) or only to root admins (private). If this parameter is not specified during the creation of the role its value will be defaulted to true (public).'; ALTER TABLE `cloud`.`roles` ADD COLUMN `public_role` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Indicates whether the role will be visible to all users (public) or only to root admins (private). If this parameter is not specified during the creation of the role its value will be defaulted to true (public).';
-- Add tables for VM Scheduler
DROP TABLE IF EXISTS `cloud`.`vm_schedule`;
CREATE TABLE `cloud`.`vm_schedule` (
`id` bigint unsigned NOT NULL auto_increment COMMENT 'id',
`vm_id` bigint unsigned NOT NULL,
`uuid` varchar(40) NOT NULL COMMENT 'schedule uuid',
`description` varchar(1024) COMMENT 'description of the vm schedule',
`schedule` varchar(255) NOT NULL COMMENT 'schedule frequency in cron format',
`timezone` varchar(100) NOT NULL COMMENT 'the timezone in which the schedule time is specified',
`action` varchar(20) NOT NULL COMMENT 'action to perform',
`enabled` int(1) NOT NULL COMMENT 'Enabled or disabled',
`start_date` datetime NOT NULL COMMENT 'start time for this schedule',
`end_date` datetime COMMENT 'end time for this schedule',
`created` datetime NOT NULL COMMENT 'date created',
`removed` datetime COMMENT 'date removed if not null',
PRIMARY KEY (`id`),
INDEX `i_vm_schedule__vm_id`(`vm_id`),
INDEX `i_vm_schedule__enabled_end_date`(`enabled`, `end_date`),
CONSTRAINT `fk_vm_schedule__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `vm_instance`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `cloud`.`vm_scheduled_job`;
CREATE TABLE `cloud`.`vm_scheduled_job` (
`id` bigint unsigned NOT NULL auto_increment COMMENT 'id',
`vm_id` bigint unsigned NOT NULL,
`vm_schedule_id` bigint unsigned NOT NULL,
`uuid` varchar(40) NOT NULL COMMENT 'scheduled job uuid',
`action` varchar(20) NOT NULL COMMENT 'action to perform',
`scheduled_timestamp` datetime NOT NULL COMMENT 'Time at which the action is taken',
`async_job_id` bigint unsigned DEFAULT NULL COMMENT 'If this schedule is being executed, it is the id of the create aysnc_job. Before that it is null',
PRIMARY KEY (`id`),
UNIQUE KEY (`vm_schedule_id`, `scheduled_timestamp`),
INDEX `i_vm_scheduled_job__scheduled_timestamp`(`scheduled_timestamp`),
INDEX `i_vm_scheduled_job__vm_id`(`vm_id`),
CONSTRAINT `fk_vm_scheduled_job__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `vm_instance`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_vm_scheduled_job__vm_schedule_id` FOREIGN KEY (`vm_schedule_id`) REFERENCES `vm_schedule`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -133,6 +133,7 @@
<cs.bcprov.version>1.70</cs.bcprov.version> <cs.bcprov.version>1.70</cs.bcprov.version>
<cs.cglib.version>3.3.0</cs.cglib.version> <cs.cglib.version>3.3.0</cs.cglib.version>
<cs.checkstyle-lib.version>8.18</cs.checkstyle-lib.version> <cs.checkstyle-lib.version>8.18</cs.checkstyle-lib.version>
<cs.cron-utils.version>9.2.0</cs.cron-utils.version>
<cs.cxf.version>3.2.14</cs.cxf.version> <cs.cxf.version>3.2.14</cs.cxf.version>
<cs.ehcache.version>2.6.11</cs.ehcache.version> <cs.ehcache.version>2.6.11</cs.ehcache.version>
<cs.globodns-client.version>0.0.27</cs.globodns-client.version> <cs.globodns-client.version>0.0.27</cs.globodns-client.version>

View File

@ -125,6 +125,7 @@ import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.cloudstack.utils.security.ParserUtils;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
@ -582,6 +583,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Inject @Inject
private AutoScaleManager autoScaleManager; private AutoScaleManager autoScaleManager;
@Inject
VMScheduleManager vmScheduleManager;
private ScheduledExecutorService _executor = null; private ScheduledExecutorService _executor = null;
private ScheduledExecutorService _vmIpFetchExecutor = null; private ScheduledExecutorService _vmIpFetchExecutor = null;
private int _expungeInterval; private int _expungeInterval;
@ -3272,6 +3276,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
autoScaleManager.removeVmFromVmGroup(vmId); autoScaleManager.removeVmFromVmGroup(vmId);
vmScheduleManager.removeScheduleByVmId(vmId, expunge);
deleteVolumesFromVm(volumesToBeDeleted, expunge); deleteVolumesFromVm(volumesToBeDeleted, expunge);
if (getDestroyRootVolumeOnVmDestruction(vm.getDomainId())) { if (getDestroyRootVolumeOnVmDestruction(vm.getDomainId())) {

View File

@ -0,0 +1,317 @@
/*
* 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.vm.schedule;
import com.cloud.api.query.MutualExclusiveIdsManagerBase;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.user.AccountManager;
import com.cloud.utils.DateUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.UserVmManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.command.user.vm.CreateVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.DeleteVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.ListVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVMScheduleCmd;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.vm.schedule.dao.VMScheduleDao;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.scheduling.support.CronExpression;
import javax.inject.Inject;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.TimeZone;
public class VMScheduleManagerImpl extends MutualExclusiveIdsManagerBase implements VMScheduleManager, PluggableService {
private static Logger LOGGER = Logger.getLogger(VMScheduleManagerImpl.class);
@Inject
private VMScheduleDao vmScheduleDao;
@Inject
private UserVmManager userVmManager;
@Inject
private VMScheduler vmScheduler;
@Inject
private AccountManager accountManager;
@Override
public List<Class<?>> getCommands() {
final List<Class<?>> cmdList = new ArrayList<>();
cmdList.add(CreateVMScheduleCmd.class);
cmdList.add(ListVMScheduleCmd.class);
cmdList.add(UpdateVMScheduleCmd.class);
cmdList.add(DeleteVMScheduleCmd.class);
return cmdList;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VM_SCHEDULE_CREATE, eventDescription = "Creating VM Schedule", create = true)
public VMScheduleResponse createSchedule(CreateVMScheduleCmd cmd) {
VirtualMachine vm = userVmManager.getUserVm(cmd.getVmId());
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, false, vm);
if (vm == null) {
throw new InvalidParameterValueException(String.format("Invalid value for vmId: %s", cmd.getVmId()));
}
VMSchedule.Action action = null;
if (cmd.getAction() != null) {
try {
action = VMSchedule.Action.valueOf(cmd.getAction().toUpperCase());
} catch (IllegalArgumentException exception) {
throw new InvalidParameterValueException(String.format("Invalid value for action: %s", cmd.getAction()));
}
}
Date cmdStartDate = cmd.getStartDate();
Date cmdEndDate = cmd.getEndDate();
String cmdTimeZone = cmd.getTimeZone();
TimeZone timeZone = TimeZone.getTimeZone(cmdTimeZone);
String timeZoneId = timeZone.getID();
Date startDate = DateUtils.addMinutes(new Date(), 1);
if (cmdStartDate != null) {
startDate = Date.from(DateUtil.getZoneDateTime(cmdStartDate, timeZone.toZoneId()).toInstant());
}
Date endDate = null;
if (cmdEndDate != null) {
endDate = Date.from(DateUtil.getZoneDateTime(cmdEndDate, timeZone.toZoneId()).toInstant());
}
CronExpression cronExpression = DateUtil.parseSchedule(cmd.getSchedule());
validateStartDateEndDate(startDate, endDate, timeZone);
String description = null;
if (StringUtils.isBlank(cmd.getDescription())) {
description = String.format("%s - %s", action, DateUtil.getHumanReadableSchedule(cronExpression));
} else description = cmd.getDescription();
LOGGER.warn(String.format("Using timezone [%s] for running the schedule for VM [%s], as an equivalent of [%s].", timeZoneId, vm.getUuid(), cmdTimeZone));
String finalDescription = description;
VMSchedule.Action finalAction = action;
Date finalStartDate = startDate;
Date finalEndDate = endDate;
return Transaction.execute((TransactionCallback<VMScheduleResponse>) status -> {
VMScheduleVO vmSchedule = vmScheduleDao.persist(new VMScheduleVO(cmd.getVmId(), finalDescription, cronExpression.toString(), timeZoneId, finalAction, finalStartDate, finalEndDate, cmd.getEnabled()));
vmScheduler.scheduleNextJob(vmSchedule);
CallContext.current().setEventResourceId(vm.getId());
CallContext.current().setEventResourceType(ApiCommandResourceType.VirtualMachine);
return createResponse(vmSchedule);
});
}
@Override
public VMScheduleResponse createResponse(VMSchedule vmSchedule) {
VirtualMachine vm = userVmManager.getUserVm(vmSchedule.getVmId());
VMScheduleResponse response = new VMScheduleResponse();
response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase());
response.setId(vmSchedule.getUuid());
response.setVmId(vm.getUuid());
response.setDescription(vmSchedule.getDescription());
response.setSchedule(vmSchedule.getSchedule());
response.setTimeZone(vmSchedule.getTimeZone());
response.setAction(vmSchedule.getAction());
response.setEnabled(vmSchedule.getEnabled());
response.setStartDate(vmSchedule.getStartDate());
response.setEndDate(vmSchedule.getEndDate());
response.setCreated(vmSchedule.getCreated());
return response;
}
@Override
public ListResponse<VMScheduleResponse> listSchedule(ListVMScheduleCmd cmd) {
Long id = cmd.getId();
Boolean enabled = cmd.getEnabled();
Long vmId = cmd.getVmId();
VirtualMachine vm = userVmManager.getUserVm(vmId);
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, false, vm);
VMSchedule.Action action = null;
if (cmd.getAction() != null) {
try {
action = VMSchedule.Action.valueOf(cmd.getAction());
} catch (IllegalArgumentException exception) {
throw new InvalidParameterValueException("Invalid value for action: " + cmd.getAction());
}
}
Pair<List<VMScheduleVO>, Integer> result = vmScheduleDao.searchAndCount(id, vmId, action, enabled, cmd.getStartIndex(), cmd.getPageSizeVal());
ListResponse<VMScheduleResponse> response = new ListResponse<>();
List<VMScheduleResponse> responsesList = new ArrayList<>();
for (VMSchedule vmSchedule : result.first()) {
responsesList.add(createResponse(vmSchedule));
}
response.setResponses(responsesList, result.second());
return response;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VM_SCHEDULE_UPDATE, eventDescription = "Updating VM Schedule")
public VMScheduleResponse updateSchedule(UpdateVMScheduleCmd cmd) {
Long id = cmd.getId();
VMScheduleVO vmSchedule = vmScheduleDao.findById(id);
if (vmSchedule == null) {
throw new CloudRuntimeException("VM schedule doesn't exist");
}
VirtualMachine vm = userVmManager.getUserVm(vmSchedule.getVmId());
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, false, vm);
CronExpression cronExpression = Objects.requireNonNullElse(
DateUtil.parseSchedule(cmd.getSchedule()),
DateUtil.parseSchedule(vmSchedule.getSchedule())
);
String description = cmd.getDescription();
if (description == null && vmSchedule.getDescription() == null) {
description = String.format("%s - %s", vmSchedule.getAction(), DateUtil.getHumanReadableSchedule(cronExpression));
}
String cmdTimeZone = cmd.getTimeZone();
Date cmdStartDate = cmd.getStartDate();
Date cmdEndDate = cmd.getEndDate();
Boolean enabled = cmd.getEnabled();
TimeZone timeZone;
String timeZoneId;
if (cmdTimeZone != null) {
timeZone = TimeZone.getTimeZone(cmdTimeZone);
timeZoneId = timeZone.getID();
if (!timeZoneId.equals(cmdTimeZone)) {
LOGGER.warn(String.format("Using timezone [%s] for running the schedule [%s] for VM %s, as an equivalent of [%s].",
timeZoneId, vmSchedule.getSchedule(), vmSchedule.getVmId(), cmdTimeZone));
}
vmSchedule.setTimeZone(timeZoneId);
} else {
timeZoneId = vmSchedule.getTimeZone();
timeZone = TimeZone.getTimeZone(timeZoneId);
}
Date startDate = vmSchedule.getStartDate().before(DateUtils.addMinutes(new Date(), 1)) ? DateUtils.addMinutes(new Date(), 1) : vmSchedule.getStartDate();
Date endDate = vmSchedule.getEndDate();
if (cmdEndDate != null) {
endDate = Date.from(DateUtil.getZoneDateTime(cmdEndDate, timeZone.toZoneId()).toInstant());
}
if (cmdStartDate != null) {
startDate = Date.from(DateUtil.getZoneDateTime(cmdStartDate, timeZone.toZoneId()).toInstant());
}
validateStartDateEndDate(Objects.requireNonNullElse(startDate, DateUtils.addMinutes(new Date(), 1)), endDate, timeZone);
if (enabled != null) {
vmSchedule.setEnabled(enabled);
}
if (description != null) {
vmSchedule.setDescription(description);
}
if (cmdEndDate != null) {
vmSchedule.setEndDate(endDate);
}
if (cmdStartDate != null) {
vmSchedule.setStartDate(startDate);
}
vmSchedule.setSchedule(cronExpression.toString());
return Transaction.execute((TransactionCallback<VMScheduleResponse>) status -> {
vmScheduleDao.update(cmd.getId(), vmSchedule);
vmScheduler.updateScheduledJob(vmSchedule);
CallContext.current().setEventResourceId(vm.getId());
CallContext.current().setEventResourceType(ApiCommandResourceType.VirtualMachine);
return createResponse(vmSchedule);
});
}
void validateStartDateEndDate(Date startDate, Date endDate, TimeZone tz) {
ZonedDateTime now = ZonedDateTime.now(tz.toZoneId());
ZonedDateTime zonedStartDate = ZonedDateTime.ofInstant(startDate.toInstant(), tz.toZoneId());
if (zonedStartDate.isBefore(now)) {
throw new InvalidParameterValueException(String.format("Invalid value for start date. Start date [%s] can't be before current time [%s].", zonedStartDate, now));
}
if (endDate != null) {
ZonedDateTime zonedEndDate = ZonedDateTime.ofInstant(endDate.toInstant(), tz.toZoneId());
if (zonedEndDate.isBefore(now)) {
throw new InvalidParameterValueException(String.format("Invalid value for end date. End date [%s] can't be before current time [%s].", zonedEndDate, now));
}
if (zonedEndDate.isBefore(zonedStartDate)) {
throw new InvalidParameterValueException(String.format("Invalid value for end date. End date [%s] can't be before start date [%s].", zonedEndDate, zonedStartDate));
}
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VM_SCHEDULE_DELETE, eventDescription = "Deleting VM Schedule for VM")
public long removeScheduleByVmId(long vmId, boolean expunge) {
SearchCriteria<VMScheduleVO> sc = vmScheduleDao.getSearchCriteriaForVMId(vmId);
List<VMScheduleVO> vmSchedules = vmScheduleDao.search(sc, null);
List<Long> ids = new ArrayList<>();
for (final VMScheduleVO vmSchedule : vmSchedules) {
ids.add(vmSchedule.getId());
}
vmScheduler.removeScheduledJobs(ids);
if (expunge) {
return vmScheduleDao.expunge(sc);
}
CallContext.current().setEventResourceId(vmId);
CallContext.current().setEventResourceType(ApiCommandResourceType.VirtualMachine);
return vmScheduleDao.remove(sc);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VM_SCHEDULE_DELETE, eventDescription = "Deleting VM Schedule")
public Long removeSchedule(DeleteVMScheduleCmd cmd) {
VirtualMachine vm = userVmManager.getUserVm(cmd.getVmId());
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, false, vm);
List<Long> ids = getIdsListFromCmd(cmd.getId(), cmd.getIds());
if (ids.isEmpty()) {
throw new InvalidParameterValueException("Either id or ids parameter must be specified");
}
return Transaction.execute((TransactionCallback<Long>) status -> {
vmScheduler.removeScheduledJobs(ids);
CallContext.current().setEventResourceId(vm.getId());
CallContext.current().setEventResourceType(ApiCommandResourceType.VirtualMachine);
return vmScheduleDao.removeSchedulesForVmIdAndIds(vm.getId(), ids);
});
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.vm.schedule;
import com.cloud.utils.component.Manager;
import com.cloud.utils.concurrency.Scheduler;
import org.apache.cloudstack.framework.config.ConfigKey;
import java.util.Date;
import java.util.List;
public interface VMScheduler extends Manager, Scheduler {
ConfigKey<Integer> VMScheduledJobExpireInterval = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Integer.class, "vmscheduler.jobs.expire.interval", "30", "VM Scheduler expire interval in days", true);
void removeScheduledJobs(List<Long> vmScheduleIds);
void updateScheduledJob(VMScheduleVO vmSchedule);
Date scheduleNextJob(VMScheduleVO vmSchedule);
}

View File

@ -0,0 +1,405 @@
/*
* 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.vm.schedule;
import com.cloud.api.ApiGsonHelper;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes;
import com.cloud.user.User;
import com.cloud.utils.DateUtil;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.db.GlobalLock;
import com.cloud.vm.UserVmManager;
import com.cloud.vm.VirtualMachine;
import com.google.common.primitives.Longs;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.command.user.vm.RebootVMCmd;
import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
import org.apache.cloudstack.api.command.user.vm.StopVMCmd;
import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
import org.apache.cloudstack.managed.context.ManagedContextTimerTask;
import org.apache.cloudstack.vm.schedule.dao.VMScheduleDao;
import org.apache.cloudstack.vm.schedule.dao.VMScheduledJobDao;
import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Logger;
import org.springframework.scheduling.support.CronExpression;
import javax.inject.Inject;
import javax.persistence.EntityExistsException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
public class VMSchedulerImpl extends ManagerBase implements VMScheduler {
private static Logger LOGGER = Logger.getLogger(VMSchedulerImpl.class);
@Inject
private VMScheduledJobDao vmScheduledJobDao;
@Inject
private VMScheduleDao vmScheduleDao;
@Inject
private UserVmManager userVmManager;
@Inject
private AsyncJobManager asyncJobManager;
private AsyncJobDispatcher asyncJobDispatcher;
private Timer vmSchedulerTimer;
private Date currentTimestamp;
private EnumMap<VMSchedule.Action, String> actionEventMap = new EnumMap<>(VMSchedule.Action.class);
public AsyncJobDispatcher getAsyncJobDispatcher() {
return asyncJobDispatcher;
}
public void setAsyncJobDispatcher(final AsyncJobDispatcher dispatcher) {
asyncJobDispatcher = dispatcher;
}
@Override
public void removeScheduledJobs(List<Long> vmScheduleIds) {
if (vmScheduleIds == null || vmScheduleIds.isEmpty()) {
LOGGER.debug("Removed 0 scheduled jobs");
return;
}
Date now = new Date();
int rowsRemoved = vmScheduledJobDao.expungeJobsForSchedules(vmScheduleIds, now);
LOGGER.debug(String.format("Removed %s VM scheduled jobs", rowsRemoved));
}
@Override
public void updateScheduledJob(VMScheduleVO vmSchedule) {
removeScheduledJobs(Longs.asList(vmSchedule.getId()));
scheduleNextJob(vmSchedule);
}
public Date scheduleNextJob(Long vmScheduleId) {
VMScheduleVO vmSchedule = vmScheduleDao.findById(vmScheduleId);
if (vmSchedule != null) {
return scheduleNextJob(vmSchedule);
}
LOGGER.debug(String.format("VM Schedule [id=%s] is removed. Not scheduling next job.", vmScheduleId));
return null;
}
@Override
public Date scheduleNextJob(VMScheduleVO vmSchedule) {
if (!vmSchedule.getEnabled()) {
LOGGER.debug(String.format("VM Schedule [id=%s] for VM [id=%s] is disabled. Not scheduling next job.", vmSchedule.getUuid(), vmSchedule.getVmId()));
return null;
}
CronExpression cron = DateUtil.parseSchedule(vmSchedule.getSchedule());
Date startDate = vmSchedule.getStartDate();
Date endDate = vmSchedule.getEndDate();
VirtualMachine vm = userVmManager.getUserVm(vmSchedule.getVmId());
if (vm == null) {
LOGGER.info(String.format("VM [id=%s] is removed. Disabling VM schedule [id=%s].", vmSchedule.getVmId(), vmSchedule.getUuid()));
vmSchedule.setEnabled(false);
vmScheduleDao.persist(vmSchedule);
return null;
}
ZonedDateTime now = ZonedDateTime.now(vmSchedule.getTimeZoneId());
ZonedDateTime zonedStartDate = ZonedDateTime.ofInstant(startDate.toInstant(), vmSchedule.getTimeZoneId());
ZonedDateTime zonedEndDate = null;
if (endDate != null) {
zonedEndDate = ZonedDateTime.ofInstant(endDate.toInstant(), vmSchedule.getTimeZoneId());
}
if (zonedEndDate != null && now.isAfter(zonedEndDate)) {
LOGGER.info(String.format("End time is less than current time. Disabling VM schedule [id=%s] for VM [id=%s].", vmSchedule.getUuid(), vmSchedule.getVmId()));
vmSchedule.setEnabled(false);
vmScheduleDao.persist(vmSchedule);
return null;
}
ZonedDateTime ts = null;
if (zonedStartDate.isAfter(now)) {
ts = cron.next(zonedStartDate);
} else {
ts = cron.next(now);
}
if (ts == null) {
LOGGER.info(String.format("No next schedule found. Disabling VM schedule [id=%s] for VM [id=%s].", vmSchedule.getUuid(), vmSchedule.getVmId()));
vmSchedule.setEnabled(false);
vmScheduleDao.persist(vmSchedule);
return null;
}
Date scheduledDateTime = Date.from(ts.toInstant());
VMScheduledJobVO scheduledJob = new VMScheduledJobVO(vmSchedule.getVmId(), vmSchedule.getId(), vmSchedule.getAction(), scheduledDateTime);
try {
vmScheduledJobDao.persist(scheduledJob);
ActionEventUtils.onScheduledActionEvent(User.UID_SYSTEM, vm.getAccountId(), actionEventMap.get(vmSchedule.getAction()),
String.format("Scheduled action (%s) [vmId: %s scheduleId: %s] at %s", vmSchedule.getAction(), vm.getUuid(), vmSchedule.getUuid(), scheduledDateTime),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(), true, 0);
} catch (EntityExistsException exception) {
LOGGER.debug("Job is already scheduled.");
}
return scheduledDateTime;
}
@Override
public boolean start() {
actionEventMap.put(VMSchedule.Action.START, EventTypes.EVENT_VM_SCHEDULE_START);
actionEventMap.put(VMSchedule.Action.STOP, EventTypes.EVENT_VM_SCHEDULE_STOP);
actionEventMap.put(VMSchedule.Action.REBOOT, EventTypes.EVENT_VM_SCHEDULE_REBOOT);
actionEventMap.put(VMSchedule.Action.FORCE_STOP, EventTypes.EVENT_VM_SCHEDULE_FORCE_STOP);
actionEventMap.put(VMSchedule.Action.FORCE_REBOOT, EventTypes.EVENT_VM_SCHEDULE_FORCE_REBOOT);
// Adding 1 minute to currentTimestamp to ensure that
// jobs which were to be run at current time, doesn't cause issues
currentTimestamp = DateUtils.addMinutes(new Date(), 1);
scheduleNextJobs();
final TimerTask schedulerPollTask = new ManagedContextTimerTask() {
@Override
protected void runInContext() {
try {
poll(new Date());
} catch (final Throwable t) {
LOGGER.warn("Catch throwable in VM scheduler ", t);
}
}
};
vmSchedulerTimer = new Timer("VMSchedulerPollTask");
vmSchedulerTimer.schedule(schedulerPollTask, 5000L, 60 * 1000L);
return true;
}
@Override
public void poll(Date timestamp) {
currentTimestamp = DateUtils.round(timestamp, Calendar.MINUTE);
String displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, currentTimestamp);
LOGGER.debug(String.format("VM scheduler.poll is being called at %s", displayTime));
GlobalLock scanLock = GlobalLock.getInternLock("vmScheduler.poll");
try {
if (scanLock.lock(30)) {
try {
scheduleNextJobs();
} finally {
scanLock.unlock();
}
}
} finally {
scanLock.releaseRef();
}
scanLock = GlobalLock.getInternLock("vmScheduler.poll");
try {
if (scanLock.lock(30)) {
try {
startJobs(); // Create async job and update scheduled job
} finally {
scanLock.unlock();
}
}
} finally {
scanLock.releaseRef();
}
try {
cleanupVMScheduledJobs();
} catch (Exception e) {
LOGGER.warn("Error in cleaning up vm scheduled jobs", e);
}
}
private void scheduleNextJobs() {
for (final VMScheduleVO schedule : vmScheduleDao.listAllActiveSchedules()) {
try {
scheduleNextJob(schedule);
} catch (Exception e) {
LOGGER.warn("Error in scheduling next job for schedule " + schedule.getUuid(), e);
}
}
}
/**
* Delete scheduled jobs before vm.scheduler.expire.interval days
*/
private void cleanupVMScheduledJobs() {
Date deleteBeforeDate = DateUtils.addDays(currentTimestamp, -1 * VMScheduledJobExpireInterval.value());
int rowsRemoved = vmScheduledJobDao.expungeJobsBefore(deleteBeforeDate);
LOGGER.info(String.format("Cleaned up %d VM scheduled job entries", rowsRemoved));
}
void executeJobs(Map<Long, VMScheduledJob> jobsToExecute) {
String displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, currentTimestamp);
for (Map.Entry<Long, VMScheduledJob> entry : jobsToExecute.entrySet()) {
VMScheduledJob vmScheduledJob = entry.getValue();
VirtualMachine vm = userVmManager.getUserVm(vmScheduledJob.getVmId());
VMScheduledJobVO tmpVMScheduleJob = null;
try {
if (LOGGER.isDebugEnabled()) {
final Date scheduledTimestamp = vmScheduledJob.getScheduledTime();
displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, scheduledTimestamp);
LOGGER.debug(String.format("Executing %s for VM id %d for schedule id: %d at %s", vmScheduledJob.getAction(), vmScheduledJob.getVmId(), vmScheduledJob.getVmScheduleId(), displayTime));
}
tmpVMScheduleJob = vmScheduledJobDao.acquireInLockTable(vmScheduledJob.getId());
Long jobId = processJob(vmScheduledJob, vm);
if (jobId != null) {
tmpVMScheduleJob.setAsyncJobId(jobId);
vmScheduledJobDao.update(vmScheduledJob.getId(), tmpVMScheduleJob);
}
} catch (final Exception e) {
LOGGER.warn(String.format("Executing scheduled job id: %s failed due to %s", vmScheduledJob.getId(), e));
} finally {
if (tmpVMScheduleJob != null) {
vmScheduledJobDao.releaseFromLockTable(vmScheduledJob.getId());
}
}
}
}
Long processJob(VMScheduledJob vmScheduledJob, VirtualMachine vm) {
if (!Arrays.asList(VirtualMachine.State.Running, VirtualMachine.State.Stopped).contains(vm.getState())) {
LOGGER.info(String.format("Skipping action (%s) for [vmId:%s scheduleId: %s] because VM is invalid state: %s", vmScheduledJob.getAction(), vm.getUuid(), vmScheduledJob.getVmScheduleId(), vm.getState()));
return null;
}
final Long eventId = ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), null,
actionEventMap.get(vmScheduledJob.getAction()), true,
String.format("Executing action (%s) for VM Id:%s", vmScheduledJob.getAction(), vm.getUuid()),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(), 0);
if (vm.getState() == VirtualMachine.State.Running) {
switch (vmScheduledJob.getAction()) {
case STOP:
return executeStopVMJob(vm, false, eventId);
case FORCE_STOP:
return executeStopVMJob(vm, true, eventId);
case REBOOT:
return executeRebootVMJob(vm, false, eventId);
case FORCE_REBOOT:
return executeRebootVMJob(vm, true, eventId);
}
} else if (vm.getState() == VirtualMachine.State.Stopped && vmScheduledJob.getAction() == VMSchedule.Action.START) {
return executeStartVMJob(vm, eventId);
}
LOGGER.warn(String.format("Skipping action (%s) for [vmId:%s scheduleId: %s] because VM is in state: %s",
vmScheduledJob.getAction(), vm.getUuid(), vmScheduledJob.getVmScheduleId(), vm.getState()));
return null;
}
private void skipJobs(Map<Long, VMScheduledJob> jobsToExecute, Map<Long, List<VMScheduledJob>> jobsNotToExecute) {
for (Map.Entry<Long, List<VMScheduledJob>> entry : jobsNotToExecute.entrySet()) {
Long vmId = entry.getKey();
List<VMScheduledJob> skippedVmScheduledJobVOS = entry.getValue();
VirtualMachine vm = userVmManager.getUserVm(vmId);
for (final VMScheduledJob skippedVmScheduledJobVO : skippedVmScheduledJobVOS) {
VMScheduledJob scheduledJob = jobsToExecute.get(vmId);
LOGGER.info(String.format("Skipping scheduled job [id: %s, vmId: %s] because of conflict with another scheduled job [id: %s]", skippedVmScheduledJobVO.getUuid(), vm.getUuid(), scheduledJob.getUuid()));
}
}
}
/**
* Create async jobs for VM scheduled jobs
*/
private void startJobs() {
String displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, currentTimestamp);
final List<VMScheduledJobVO> vmScheduledJobs = vmScheduledJobDao.listJobsToStart(currentTimestamp);
LOGGER.debug(String.format("Got %d scheduled jobs to be executed at %s", vmScheduledJobs.size(), displayTime));
Map<Long, VMScheduledJob> jobsToExecute = new HashMap<>();
Map<Long, List<VMScheduledJob>> jobsNotToExecute = new HashMap<>();
for (final VMScheduledJobVO vmScheduledJobVO : vmScheduledJobs) {
long vmId = vmScheduledJobVO.getVmId();
if (jobsToExecute.get(vmId) == null) {
jobsToExecute.put(vmId, vmScheduledJobVO);
} else {
jobsNotToExecute.computeIfAbsent(vmId, k -> new ArrayList<>()).add(vmScheduledJobVO);
}
}
executeJobs(jobsToExecute);
skipJobs(jobsToExecute, jobsNotToExecute);
}
long executeStartVMJob(VirtualMachine vm, long eventId) {
final Map<String, String> params = new HashMap<>();
params.put(ApiConstants.ID, String.valueOf(vm.getId()));
params.put("ctxUserId", "1");
params.put("ctxAccountId", String.valueOf(vm.getAccountId()));
params.put(ApiConstants.CTX_START_EVENT_ID, String.valueOf(eventId));
final StartVMCmd cmd = new StartVMCmd();
ComponentContext.inject(cmd);
AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, vm.getAccountId(), StartVMCmd.class.getName(), ApiGsonHelper.getBuilder().create().toJson(params), vm.getId(), cmd.getApiResourceType() != null ? cmd.getApiResourceType().toString() : null, null);
job.setDispatcher(asyncJobDispatcher.getName());
return asyncJobManager.submitAsyncJob(job);
}
long executeStopVMJob(VirtualMachine vm, boolean isForced, long eventId) {
final Map<String, String> params = new HashMap<>();
params.put(ApiConstants.ID, String.valueOf(vm.getId()));
params.put("ctxUserId", "1");
params.put("ctxAccountId", String.valueOf(vm.getAccountId()));
params.put(ApiConstants.CTX_START_EVENT_ID, String.valueOf(eventId));
params.put(ApiConstants.FORCED, String.valueOf(isForced));
final StopVMCmd cmd = new StopVMCmd();
ComponentContext.inject(cmd);
AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, vm.getAccountId(), StopVMCmd.class.getName(), ApiGsonHelper.getBuilder().create().toJson(params), vm.getId(), cmd.getApiResourceType() != null ? cmd.getApiResourceType().toString() : null, null);
job.setDispatcher(asyncJobDispatcher.getName());
return asyncJobManager.submitAsyncJob(job);
}
long executeRebootVMJob(VirtualMachine vm, boolean isForced, long eventId) {
final Map<String, String> params = new HashMap<>();
params.put(ApiConstants.ID, String.valueOf(vm.getId()));
params.put("ctxUserId", "1");
params.put("ctxAccountId", String.valueOf(vm.getAccountId()));
params.put(ApiConstants.CTX_START_EVENT_ID, String.valueOf(eventId));
params.put(ApiConstants.FORCED, String.valueOf(isForced));
final RebootVMCmd cmd = new RebootVMCmd();
ComponentContext.inject(cmd);
AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, vm.getAccountId(), RebootVMCmd.class.getName(), ApiGsonHelper.getBuilder().create().toJson(params), vm.getId(), cmd.getApiResourceType() != null ? cmd.getApiResourceType().toString() : null, null);
job.setDispatcher(asyncJobDispatcher.getName());
return asyncJobManager.submitAsyncJob(job);
}
}

View File

@ -347,4 +347,9 @@
<bean id="resourceIconManager" class="com.cloud.resourceicon.ResourceIconManagerImpl" /> <bean id="resourceIconManager" class="com.cloud.resourceicon.ResourceIconManagerImpl" />
<bean id="VMScheduleManagerImpl" class="org.apache.cloudstack.vm.schedule.VMScheduleManagerImpl" />
<bean id="VMSchedulerImpl" class="org.apache.cloudstack.vm.schedule.VMSchedulerImpl">
<property name="asyncJobDispatcher" ref="ApiAsyncJobDispatcher" />
</bean>
</beans> </beans>

View File

@ -0,0 +1,274 @@
/*
* 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.vm.schedule;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.User;
import com.cloud.uservm.UserVm;
import com.cloud.utils.Pair;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.vm.UserVmManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.command.user.vm.CreateVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.DeleteVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.ListVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVMScheduleCmd;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.vm.schedule.dao.VMScheduleDao;
import org.apache.commons.lang.time.DateUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.UUID;
import static org.junit.Assert.assertNotNull;
public class VMScheduleManagerImplTest {
@Spy
@InjectMocks
VMScheduleManagerImpl vmScheduleManager = new VMScheduleManagerImpl();
@Mock
VMScheduleDao vmScheduleDao;
@Mock
VMScheduler vmScheduler;
@Mock
UserVmManager userVmManager;
@Mock
AccountManager accountManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
Account callingAccount = Mockito.mock(Account.class);
User callingUser = Mockito.mock(User.class);
CallContext.register(callingUser, callingAccount);
}
private void validateResponse(VMScheduleResponse response, VMSchedule vmSchedule, VirtualMachine vm) {
assertNotNull(response);
Assert.assertEquals(ReflectionTestUtils.getField(response, "id"), vmSchedule.getUuid());
Assert.assertEquals(ReflectionTestUtils.getField(response, "vmId"), vm.getUuid());
Assert.assertEquals(ReflectionTestUtils.getField(response, "schedule"), vmSchedule.getSchedule());
Assert.assertEquals(ReflectionTestUtils.getField(response, "timeZone"), vmSchedule.getTimeZone());
Assert.assertEquals(ReflectionTestUtils.getField(response, "action"), vmSchedule.getAction());
Assert.assertEquals(ReflectionTestUtils.getField(response, "startDate"), vmSchedule.getStartDate());
Assert.assertEquals(ReflectionTestUtils.getField(response, "endDate"), vmSchedule.getEndDate());
}
@Test
public void createSchedule() {
UserVm vm = Mockito.mock(UserVm.class);
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
CreateVMScheduleCmd cmd = Mockito.mock(CreateVMScheduleCmd.class);
Mockito.when(cmd.getVmId()).thenReturn(1L);
Mockito.when(cmd.getSchedule()).thenReturn("0 0 * * *");
Mockito.when(cmd.getTimeZone()).thenReturn("UTC");
Mockito.when(cmd.getAction()).thenReturn("start");
Mockito.when(cmd.getStartDate()).thenReturn(DateUtils.addDays(new Date(), 1));
Mockito.when(cmd.getEndDate()).thenReturn(DateUtils.addDays(new Date(), 2));
Mockito.when(vm.getUuid()).thenReturn(UUID.randomUUID().toString());
Mockito.when(vmScheduleDao.persist(Mockito.any(VMScheduleVO.class))).thenReturn(vmSchedule);
Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
Mockito.doNothing().when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.eq(false), Mockito.eq(vm));
VMScheduleResponse response = vmScheduleManager.createSchedule(cmd);
Mockito.verify(vmScheduleDao, Mockito.times(1)).persist(Mockito.any(VMScheduleVO.class));
validateResponse(response, vmSchedule, vm);
}
@Test
public void createResponse() {
VMSchedule vmSchedule = Mockito.mock(VMSchedule.class);
UserVm vm = Mockito.mock(UserVm.class);
Mockito.when(vmSchedule.getVmId()).thenReturn(1L);
Mockito.when(userVmManager.getUserVm(vmSchedule.getVmId())).thenReturn(vm);
VMScheduleResponse response = vmScheduleManager.createResponse(vmSchedule);
validateResponse(response, vmSchedule, vm);
}
@Test
public void listSchedule() {
UserVm vm = Mockito.mock(UserVm.class);
VMScheduleVO vmSchedule1 = Mockito.mock(VMScheduleVO.class);
VMScheduleVO vmSchedule2 = Mockito.mock(VMScheduleVO.class);
List<VMScheduleVO> vmScheduleList = new ArrayList<>();
vmScheduleList.add(vmSchedule1);
vmScheduleList.add(vmSchedule2);
Mockito.when(vm.getUuid()).thenReturn(UUID.randomUUID().toString());
Mockito.when(userVmManager.getUserVm(1L)).thenReturn(vm);
Mockito.when(
vmScheduleDao.searchAndCount(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(),
Mockito.anyBoolean(), Mockito.anyLong(), Mockito.anyLong())
).thenReturn(new Pair<>(vmScheduleList, vmScheduleList.size()));
Mockito.when(vmSchedule1.getVmId()).thenReturn(1L);
Mockito.when(vmSchedule2.getVmId()).thenReturn(1L);
ListVMScheduleCmd cmd = Mockito.mock(ListVMScheduleCmd.class);
Mockito.when(cmd.getVmId()).thenReturn(1L);
ListResponse<VMScheduleResponse> responseList = vmScheduleManager.listSchedule(cmd);
Mockito.verify(vmScheduleDao, Mockito.times(1)).searchAndCount(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(),
Mockito.anyBoolean(), Mockito.anyLong(), Mockito.anyLong());
assertNotNull(responseList);
Assert.assertEquals(2, (int) responseList.getCount());
Assert.assertEquals(2, responseList.getResponses().size());
for (int i = 0; i < responseList.getResponses().size(); i++) {
VMScheduleResponse response = responseList.getResponses().get(i);
VMScheduleVO vmSchedule = vmScheduleList.get(i);
validateResponse(response, vmSchedule, vm);
}
}
@Test
public void updateSchedule() {
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
UpdateVMScheduleCmd cmd = Mockito.mock(UpdateVMScheduleCmd.class);
UserVm vm = Mockito.mock(UserVm.class);
Mockito.when(cmd.getId()).thenReturn(1L);
Mockito.when(cmd.getSchedule()).thenReturn("0 0 * * *");
Mockito.when(cmd.getTimeZone()).thenReturn("UTC");
Mockito.when(cmd.getStartDate()).thenReturn(DateUtils.addDays(new Date(), 1));
Mockito.when(cmd.getEndDate()).thenReturn(DateUtils.addDays(new Date(), 2));
Mockito.when(vmScheduleDao.findById(Mockito.anyLong())).thenReturn(vmSchedule);
Mockito.when(vmScheduleDao.update(Mockito.eq(cmd.getId()), Mockito.any(VMScheduleVO.class))).thenReturn(true);
Mockito.when(vmSchedule.getVmId()).thenReturn(1L);
Mockito.when(vmSchedule.getStartDate()).thenReturn(DateUtils.addDays(new Date(), 1));
Mockito.when(userVmManager.getUserVm(vmSchedule.getVmId())).thenReturn(vm);
VMScheduleResponse response = vmScheduleManager.updateSchedule(cmd);
Mockito.verify(vmScheduleDao, Mockito.times(1)).update(Mockito.eq(cmd.getId()), Mockito.any(VMScheduleVO.class));
validateResponse(response, vmSchedule, vm);
}
@Test
public void removeScheduleByVmId() {
UserVm vm = Mockito.mock(UserVm.class);
VMScheduleVO vmSchedule1 = Mockito.mock(VMScheduleVO.class);
VMScheduleVO vmSchedule2 = Mockito.mock(VMScheduleVO.class);
List<VMScheduleVO> vmScheduleList = new ArrayList<>();
vmScheduleList.add(vmSchedule1);
vmScheduleList.add(vmSchedule2);
SearchCriteria<VMScheduleVO> sc = Mockito.mock(SearchCriteria.class);
Mockito.when(vm.getId()).thenReturn(1L);
Mockito.when(vmScheduleDao.getSearchCriteriaForVMId(vm.getId())).thenReturn(sc);
Mockito.when(vmScheduleDao.search(sc, null)).thenReturn(vmScheduleList);
Mockito.when(vmSchedule1.getId()).thenReturn(1L);
Mockito.when(vmSchedule2.getId()).thenReturn(2L);
Mockito.when(vmScheduleDao.remove(sc)).thenReturn(2);
long rowsRemoved = vmScheduleManager.removeScheduleByVmId(vm.getId(), false);
Mockito.verify(vmScheduleDao, Mockito.times(1)).remove(sc);
Assert.assertEquals(2, rowsRemoved);
}
@Test
public void removeSchedule() {
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
UserVm vm = Mockito.mock(UserVm.class);
DeleteVMScheduleCmd cmd = Mockito.mock(DeleteVMScheduleCmd.class);
Mockito.when(cmd.getId()).thenReturn(1L);
Mockito.when(cmd.getVmId()).thenReturn(1L);
Mockito.when(vmSchedule.getVmId()).thenReturn(1L);
Mockito.when(userVmManager.getUserVm(cmd.getVmId())).thenReturn(vm);
Mockito.when(vmScheduleDao.findById(Mockito.anyLong())).thenReturn(vmSchedule);
Mockito.when(vmScheduleDao.removeSchedulesForVmIdAndIds(Mockito.anyLong(), Mockito.anyList())).thenReturn(1L);
Long rowsRemoved = vmScheduleManager.removeSchedule(cmd);
Mockito.verify(vmScheduleDao, Mockito.times(1)).removeSchedulesForVmIdAndIds(Mockito.anyLong(), Mockito.anyList());
Assert.assertEquals(1L, (long) rowsRemoved);
}
@Test
public void validateStartDateEndDate() {
// Valid scenario 1
// Start date is before end date
Date startDate = DateUtils.addDays(new Date(), 1);
Date endDate = DateUtils.addDays(new Date(), 2);
vmScheduleManager.validateStartDateEndDate(startDate, endDate, TimeZone.getDefault());
// Valid Scenario 2
// Start date is before current date and end date is null
endDate = null;
vmScheduleManager.validateStartDateEndDate(startDate, endDate, TimeZone.getDefault());
// Invalid Scenario 2
// Start date is before current date
startDate = DateUtils.addDays(new Date(), -1);
try {
vmScheduleManager.validateStartDateEndDate(startDate, endDate, TimeZone.getDefault());
Assert.fail("Should have thrown InvalidParameterValueException");
} catch (InvalidParameterValueException e) {
Assert.assertTrue(e.getMessage().contains("Invalid value for start date. Start date") &&
e.getMessage().contains("can't be before current time"));
}
// Invalid Scenario 2
// Start date is after end date
startDate = DateUtils.addDays(new Date(), 2);
endDate = DateUtils.addDays(new Date(), 1);
try {
vmScheduleManager.validateStartDateEndDate(startDate, endDate, TimeZone.getDefault());
Assert.fail("Should have thrown InvalidParameterValueException");
} catch (InvalidParameterValueException e) {
Assert.assertTrue(e.getMessage().contains("Invalid value for end date. End date") &&
e.getMessage().contains("can't be before start date"));
}
// Invalid Scenario 3
// End date is before current date
startDate = DateUtils.addDays(new Date(), 1);
endDate = DateUtils.addDays(new Date(), -1);
try {
vmScheduleManager.validateStartDateEndDate(startDate, endDate, TimeZone.getDefault());
Assert.fail("Should have thrown InvalidParameterValueException");
} catch (InvalidParameterValueException e) {
Assert.assertTrue(e.getMessage().contains("Invalid value for end date. End date") &&
e.getMessage().contains("can't be before current time"));
}
}
}

View File

@ -0,0 +1,397 @@
/*
* 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.vm.schedule;
import com.cloud.event.ActionEventUtils;
import com.cloud.user.User;
import com.cloud.uservm.UserVm;
import com.cloud.utils.component.ComponentContext;
import com.cloud.vm.UserVmManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.command.user.vm.RebootVMCmd;
import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
import org.apache.cloudstack.api.command.user.vm.StopVMCmd;
import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
import org.apache.cloudstack.vm.schedule.dao.VMScheduleDao;
import org.apache.cloudstack.vm.schedule.dao.VMScheduledJobDao;
import org.apache.commons.lang.time.DateUtils;
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.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.test.util.ReflectionTestUtils;
import javax.persistence.EntityExistsException;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import static org.mockito.Mockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest({ActionEventUtils.class, ComponentContext.class})
public class VMSchedulerImplTest {
@Spy
@InjectMocks
private VMSchedulerImpl vmScheduler = new VMSchedulerImpl();
@Mock
private UserVmManager userVmManager;
@Mock
private VMScheduleDao vmScheduleDao;
@Mock
private VMScheduledJobDao vmScheduledJobDao;
@Mock
private EnumMap<VMSchedule.Action, String> actionEventMap;
@Mock
private AsyncJobManager asyncJobManager;
@Mock
private AsyncJobDispatcher asyncJobDispatcher;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
PowerMockito.mockStatic(ActionEventUtils.class);
Mockito.when(ActionEventUtils.onScheduledActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyLong())).thenReturn(1L);
Mockito.when(ActionEventUtils.onCompletedActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(),
Mockito.anyString(), Mockito.anyBoolean(),
Mockito.anyString(),
Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong())).thenReturn(1L);
}
private void prepareMocksForProcessJob(VirtualMachine vm, VMScheduledJob vmScheduledJob, VirtualMachine.State vmState, VMSchedule.Action action, Long executeJobReturnValue) {
Mockito.when(vm.getState()).thenReturn(vmState);
Mockito.when(vmScheduledJob.getAction()).thenReturn(action);
if (executeJobReturnValue != null) {
Mockito.doReturn(executeJobReturnValue).when(vmScheduler).executeStartVMJob(Mockito.any(VirtualMachine.class), Mockito.anyLong());
Mockito.doReturn(executeJobReturnValue).when(vmScheduler).executeStopVMJob(Mockito.any(VirtualMachine.class), Mockito.anyBoolean(), Mockito.anyLong());
Mockito.doReturn(executeJobReturnValue).when(vmScheduler).executeRebootVMJob(Mockito.any(VirtualMachine.class), Mockito.anyBoolean(), Mockito.anyLong());
}
}
private void executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State state, VMSchedule.Action action) {
VMScheduledJob vmScheduledJob = Mockito.mock(VMScheduledJob.class);
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Long expectedValue = 1L;
prepareMocksForProcessJob(vm, vmScheduledJob, state, action, expectedValue);
Long jobId = vmScheduler.processJob(vmScheduledJob, vm);
PowerMockito.verifyStatic(ActionEventUtils.class);
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), null,
actionEventMap.get(action), true,
String.format("Executing action (%s) for VM Id:%s", vmScheduledJob.getAction(), vm.getUuid()),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(), 0);
Assert.assertEquals(expectedValue, jobId);
}
@Test
public void testProcessJobRunning() {
executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Running, VMSchedule.Action.STOP);
executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Running, VMSchedule.Action.FORCE_STOP);
executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Running, VMSchedule.Action.REBOOT);
executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Running, VMSchedule.Action.FORCE_REBOOT);
executeProcessJobWithVMStateAndActionNonSkipped(VirtualMachine.State.Stopped, VMSchedule.Action.START);
}
@Test
public void testProcessJobInvalidAction() {
VMScheduledJob vmScheduledJob = Mockito.mock(VMScheduledJob.class);
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
prepareMocksForProcessJob(vm, vmScheduledJob, VirtualMachine.State.Running, VMSchedule.Action.START, null);
Long jobId = vmScheduler.processJob(vmScheduledJob, vm);
Assert.assertNull(jobId);
}
@Test
public void testProcessJobVMInInvalidState() {
VMScheduledJob vmScheduledJob = Mockito.mock(VMScheduledJob.class);
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
prepareMocksForProcessJob(vm, vmScheduledJob, VirtualMachine.State.Unknown, VMSchedule.Action.START, null);
Long jobId = vmScheduler.processJob(vmScheduledJob, vm);
Assert.assertNull(jobId);
}
@Test
public void testScheduleNextJobScheduleScheduleExists() {
Date now = DateUtils.setSeconds(new Date(), 0);
Date startDate = DateUtils.addDays(now, 1);
Date expectedScheduledTime = DateUtils.round(DateUtils.addMinutes(startDate, 1), Calendar.MINUTE);
UserVm vm = Mockito.mock(UserVm.class);
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
Mockito.when(vmSchedule.getSchedule()).thenReturn("* * * * *");
Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("UTC").toZoneId());
Mockito.when(vmSchedule.getStartDate()).thenReturn(startDate);
Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
Mockito.when(vmScheduledJobDao.persist(Mockito.any())).thenThrow(EntityExistsException.class);
Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule);
Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
}
@Test
public void testScheduleNextJobScheduleFutureSchedule() {
Date now = DateUtils.setSeconds(new Date(), 0);
Date startDate = DateUtils.addDays(now, 1);
Date expectedScheduledTime = DateUtils.round(DateUtils.addMinutes(startDate, 1), Calendar.MINUTE);
UserVm vm = Mockito.mock(UserVm.class);
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
Mockito.when(vmSchedule.getSchedule()).thenReturn("* * * * *");
Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("UTC").toZoneId());
Mockito.when(vmSchedule.getStartDate()).thenReturn(startDate);
Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule);
Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
}
@Test
public void testScheduleNextJobScheduleFutureScheduleWithTimeZoneChecks() throws Exception {
// Ensure that Date vmSchedulerImpl.scheduleNextJob(VMScheduleVO vmSchedule) generates
// the correct scheduled time on basis of schedule(cron), start date
// and the timezone of the user. The system running the test can have any timezone.
String cron = "30 5 * * *";
Date now = DateUtils.setSeconds(new Date(), 0);
Date startDate = DateUtils.addDays(now, 1);
UserVm vm = Mockito.mock(UserVm.class);
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
Mockito.when(vmSchedule.getSchedule()).thenReturn(cron);
Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("EST").toZoneId());
Mockito.when(vmSchedule.getStartDate()).thenReturn(startDate);
Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
// The timezone of the user is EST. The cron expression is 30 5 * * *.
// The start date is 1 day from now. The expected scheduled time is 5:30 AM EST.
// The actual scheduled time is 10:30 AM UTC.
// The actual scheduled time is 5:30 AM EST.
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(startDate.toInstant(), vmSchedule.getTimeZoneId());
zonedDateTime = zonedDateTime.withHour(5).withMinute(30).withSecond(0).withNano(0);
Date expectedScheduledTime = Date.from(zonedDateTime.toInstant());
if (expectedScheduledTime.before(startDate)) {
expectedScheduledTime = DateUtils.addDays(expectedScheduledTime, 1);
}
Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule);
Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
}
@Test
public void testScheduleNextJobScheduleCurrentSchedule() {
Date now = DateUtils.setSeconds(new Date(), 0);
Date expectedScheduledTime = DateUtils.round(DateUtils.addMinutes(now, 1), Calendar.MINUTE);
UserVm vm = Mockito.mock(UserVm.class);
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
Mockito.when(vmSchedule.getSchedule()).thenReturn("* * * * *");
Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("UTC").toZoneId());
Mockito.when(vmSchedule.getStartDate()).thenReturn(DateUtils.addDays(now, -1));
Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule);
Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
}
@Test
public void testScheduleNextJobScheduleCurrentScheduleWithTimeZoneChecks() throws Exception {
// Ensure that Date vmSchedulerImpl.scheduleNextJob(VMScheduleVO vmSchedule) generates
// the correct scheduled time on basis of schedule(cron), start date
// and the timezone of the user. The system running the test can have any timezone.
String cron = "30 5 * * *";
Date now = DateUtils.setSeconds(new Date(), 0);
UserVm vm = Mockito.mock(UserVm.class);
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
Mockito.when(vmSchedule.getSchedule()).thenReturn(cron);
Mockito.when(vmSchedule.getTimeZoneId()).thenReturn(TimeZone.getTimeZone("EST").toZoneId());
Mockito.when(vmSchedule.getStartDate()).thenReturn(DateUtils.addDays(now, -1));
Mockito.when(userVmManager.getUserVm(Mockito.anyLong())).thenReturn(vm);
// The timezone of the user is EST. The cron expression is 30 5 * * *.
// The start date is 1 day ago. The expected scheduled time is 5:30 AM EST.
// The actual scheduled time is 10:30 AM UTC.
// The actual scheduled time is 5:30 AM EST.
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(now.toInstant(), vmSchedule.getTimeZoneId());
zonedDateTime = zonedDateTime.withHour(5).withMinute(30).withSecond(0).withNano(0);
Date expectedScheduledTime = Date.from(zonedDateTime.toInstant());
if (expectedScheduledTime.before(now)) {
expectedScheduledTime = DateUtils.addDays(expectedScheduledTime, 1);
}
Date actualScheduledTime = vmScheduler.scheduleNextJob(vmSchedule);
Assert.assertEquals(expectedScheduledTime, actualScheduledTime);
}
@Test
public void testScheduleNextJobScheduleExpired() {
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
Mockito.when(vmSchedule.getEndDate()).thenReturn(DateUtils.addMinutes(new Date(), -5));
Mockito.when(vmSchedule.getEnabled()).thenReturn(true);
Date actualDate = vmScheduler.scheduleNextJob(vmSchedule);
Assert.assertNull(actualDate);
}
@Test
public void testScheduleNextJobScheduleDisabled() {
VMScheduleVO vmSchedule = Mockito.mock(VMScheduleVO.class);
Mockito.when(vmSchedule.getEnabled()).thenReturn(false);
Date actualDate = vmScheduler.scheduleNextJob(vmSchedule);
Assert.assertNull(actualDate);
}
@Test
public void testScheduleNextJobScheduleIdNotExists() {
long vmScheduleId = 1;
Mockito.when(vmScheduleDao.findById(vmScheduleId)).thenReturn(null);
Date actualDate = vmScheduler.scheduleNextJob(vmScheduleId);
Assert.assertNull(actualDate);
}
@Test
public void testScheduleNextJobScheduleIdExists() {
long vmScheduleId = 1;
VMScheduleVO vmScheduleVO = Mockito.mock(VMScheduleVO.class);
Date date = Mockito.mock(Date.class);
Mockito.when(vmScheduleDao.findById(vmScheduleId)).thenReturn(vmScheduleVO);
Mockito.doReturn(date).when(vmScheduler).scheduleNextJob(vmScheduleVO);
Date actualDate = vmScheduler.scheduleNextJob(vmScheduleId);
Assert.assertEquals(date, actualDate);
}
@Test
public void testExecuteJobs() {
/*
Test VMSchedulerImpl.executeJobs() method with a map of VMScheduledJob objects
covering all the possible scenarios
1. When the job is executed successfully
2. When the job is skipped (processJob returns null)
*/
VMScheduledJobVO job1 = Mockito.mock(VMScheduledJobVO.class);
VMScheduledJobVO job2 = Mockito.mock(VMScheduledJobVO.class);
UserVm vm1 = Mockito.mock(UserVm.class);
UserVm vm2 = Mockito.mock(UserVm.class);
Mockito.when(job1.getVmId()).thenReturn(1L);
Mockito.when(job1.getScheduledTime()).thenReturn(new Date());
Mockito.when(job1.getAction()).thenReturn(VMSchedule.Action.START);
Mockito.when(job1.getVmScheduleId()).thenReturn(1L);
Mockito.when(job2.getVmId()).thenReturn(2L);
Mockito.when(job2.getScheduledTime()).thenReturn(new Date());
Mockito.when(job2.getAction()).thenReturn(VMSchedule.Action.STOP);
Mockito.when(job2.getVmScheduleId()).thenReturn(2L);
Mockito.when(userVmManager.getUserVm(1L)).thenReturn(vm1);
Mockito.when(userVmManager.getUserVm(2L)).thenReturn(vm2);
Mockito.doReturn(1L).when(vmScheduler).processJob(job1, vm1);
Mockito.doReturn(null).when(vmScheduler).processJob(job2, vm2);
Mockito.when(vmScheduledJobDao.acquireInLockTable(job1.getId())).thenReturn(job1);
Mockito.when(vmScheduledJobDao.acquireInLockTable(job2.getId())).thenReturn(job2);
Map<Long, VMScheduledJob> jobs = new HashMap<>();
jobs.put(1L, job1);
jobs.put(2L, job2);
ReflectionTestUtils.setField(vmScheduler, "currentTimestamp", new Date());
vmScheduler.executeJobs(jobs);
Mockito.verify(vmScheduler, Mockito.times(2)).processJob(Mockito.any(), Mockito.any());
Mockito.verify(vmScheduledJobDao, Mockito.times(2)).acquireInLockTable(Mockito.anyLong());
}
@Test
public void testExecuteStopVMJob() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(asyncJobManager.submitAsyncJob(Mockito.any(AsyncJobVO.class))).thenReturn(1L);
PowerMockito.mockStatic(ComponentContext.class);
when(ComponentContext.inject(StopVMCmd.class)).thenReturn(Mockito.mock(StopVMCmd.class));
long jobId = vmScheduler.executeStopVMJob(vm, false, 1L);
Assert.assertEquals(1L, jobId);
}
@Test
public void testExecuteRebootVMJob() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(asyncJobManager.submitAsyncJob(Mockito.any(AsyncJobVO.class))).thenReturn(1L);
PowerMockito.mockStatic(ComponentContext.class);
when(ComponentContext.inject(RebootVMCmd.class)).thenReturn(Mockito.mock(RebootVMCmd.class));
long jobId = vmScheduler.executeRebootVMJob(vm, false, 1L);
Assert.assertEquals(1L, jobId);
}
@Test
public void testExecuteStartVMJob() {
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(asyncJobManager.submitAsyncJob(Mockito.any(AsyncJobVO.class))).thenReturn(1L);
PowerMockito.mockStatic(ComponentContext.class);
when(ComponentContext.inject(StartVMCmd.class)).thenReturn(Mockito.mock(StartVMCmd.class));
long jobId = vmScheduler.executeStartVMJob(vm, 1L);
Assert.assertEquals(1L, jobId);
}
}

View File

@ -0,0 +1,606 @@
# 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.
""" P1 tests for VM Schedule
"""
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.lib.base import Account, ServiceOffering, VirtualMachine, VMSchedule
from marvin.lib.common import get_domain, get_zone, get_template
from marvin.lib.utils import cleanup_resources
# Import Local Modules
from nose.plugins.attrib import attr
import datetime
import time
class Services:
"""Test Snapshots Services"""
def __init__(self):
self.services = {
"account": {
"email": "test@test.com",
"firstname": "Test",
"lastname": "User",
"username": "test",
# Random characters are appended for unique
# username
"password": "password",
},
"service_offering": {
"name": "Tiny Instance",
"displaytext": "Tiny Instance",
"cpunumber": 1,
"cpuspeed": 200, # in MHz
"memory": 256, # In MBs
},
"disk_offering": {
"displaytext": "Small Disk",
"name": "Small Disk",
"disksize": 1,
},
"server": {
"displayname": "TestVM",
"username": "root",
"password": "password",
"ssh_port": 22,
"privateport": 22,
"publicport": 22,
"protocol": "TCP",
},
"mgmt_server": {
"ipaddress": "192.168.100.21",
"username": "root",
"password": "password",
"port": 22,
},
"templates": {
"displaytext": "Template",
"name": "Template",
"ostype": "CentOS 5.3 (64-bit)",
"templatefilter": "self",
},
"ostype": "CentOS 5.3 (64-bit)",
}
class TestVMSchedule(cloudstackTestCase):
@classmethod
def setUpClass(cls):
cls.testClient = super(TestVMSchedule, cls).getClsTestClient()
cls.api_client = cls.testClient.getApiClient()
cls._cleanup = []
cls.hypervisor = cls.testClient.getHypervisorInfo()
cls.services = Services().services
# Get Zone, Domain and templates
cls.domain = get_domain(cls.api_client)
cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests())
cls.services["mode"] = cls.zone.networktype
template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"])
cls.services["domainid"] = cls.domain.id
cls.services["server"]["zoneid"] = cls.zone.id
cls.services["templates"]["ostypeid"] = template.ostypeid
cls.services["zoneid"] = cls.zone.id
# Create VMs, NAT Rules etc
cls.account = Account.create(
cls.api_client, cls.services["account"], domainid=cls.domain.id
)
cls._cleanup.append(cls.account)
cls.services["account"] = cls.account.name
cls.service_offering = ServiceOffering.create(
cls.api_client, cls.services["service_offering"]
)
cls._cleanup.append(cls.service_offering)
cls.virtual_machine = VirtualMachine.create(
cls.api_client,
cls.services["server"],
templateid=template.id,
accountid=cls.account.name,
domainid=cls.account.domainid,
serviceofferingid=cls.service_offering.id,
)
return
@classmethod
def tearDownClass(cls):
super(TestVMSchedule, cls).tearDownClass()
def setUp(self):
self.apiclient = self.testClient.getApiClient()
self.dbclient = self.testClient.getDbConnection()
self.cleanup = []
return
def tearDown(self):
super(TestVMSchedule, self).tearDown()
@attr(tags=["advanced", "basic"], required_hardware="false")
def test_01_vmschedule_create(self):
"""Test VM Schedule Creation in cron format and validate responses"""
# Validate the following
# 1. Create VM Schedule in cron format
# 2. List VM Schedule and verify the response
# 3. Delete VM Schedule and verify the response
# Create VM Schedule
schedule = "0 0 1 * *"
vmschedule = VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"start",
schedule,
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(minutes=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
enabled=True,
)
self.cleanup.append(vmschedule)
self.debug("Created VM Schedule with ID: %s" % vmschedule.id)
# List VM Schedule
vmschedules = VMSchedule.list(
self.apiclient, self.virtual_machine.id, id=vmschedule.id
)
self.assertEqual(
isinstance(vmschedules, list),
True,
"Check list response returns a valid list",
)
self.assertNotEqual(len(vmschedules), 0, "Check VM Schedule list")
self.debug("List VM Schedule response: %s" % vmschedules[0].__dict__)
self.assertEqual(
vmschedules[0].id,
vmschedule.id,
"Check VM Schedule ID in list resources call",
)
self.assertEqual(
vmschedules[0].virtualmachineid,
self.virtual_machine.id,
"Check VM ID in list resources call",
)
self.assertEqual(
vmschedules[0].schedule,
schedule,
"Check VM Schedule in list resources call",
)
self.assertEqual(
vmschedules[0].timezone,
str(datetime.datetime.now().astimezone().tzinfo),
"Check VM Schedule timezone in list resources call",
)
# Check for entry in vm_scheduled_job in db
vmscheduled_job = self.dbclient.execute(
"select * from vm_scheduled_job where vm_schedule_id IN (SELECT id FROM vm_schedule WHERE uuid = '%s')"
% vmschedule.id,
db="cloud",
)
self.assertIsInstance(
vmscheduled_job,
list,
"Check if VM Schedule exists in vm_scheduled_job table",
)
self.assertGreater(
len(vmscheduled_job),
0,
"Check if VM Schedule exists in vm_scheduled_job table",
)
return
@attr(tags=["advanced", "basic"], required_hardware="false")
def test_02_vmschedule_create_parameter_exceptions(self):
"""Test VM Schedule Creation exceptions with invalid parameters"""
# Validate the following
# 1. Create VM Schedule with invalid virtual machine ID
# 2. Create VM Schedule with invalid schedule
# 3. Create VM Schedule with invalid start date
# 5. Create VM Schedule with invalid action
# 6. Create VM Schedule with invalid end date
# Create VM Schedule with invalid virtual machine ID
with self.assertRaises(Exception):
VMSchedule.create(
self.apiclient,
"invalid",
"start",
"0 0 1 * *",
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(minutes=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
)
# Create VM Schedule with invalid schedule
with self.assertRaises(Exception):
VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"start",
"invalid",
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(minutes=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
)
# Create VM Schedule with invalid start date
with self.assertRaises(Exception):
VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"start",
"0 0 1 * *",
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
"invalid",
)
# Create VM Schedule with invalid action
with self.assertRaises(Exception):
VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"invalid",
"0 0 1 * *",
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(minutes=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
)
# test invalid end date
with self.assertRaises(Exception):
VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"start",
"0 0 1 * *",
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(minutes=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
enddate="invalid",
)
return
@attr(tags=["advanced", "basic"], required_hardware="false")
def test_03_vmschedule_update(self):
"""Test VM Schedule Update in cron format and validate responses"""
# Validate the following
# 1. Create VM Schedule in cron format
# 2. Update VM Schedule and verify the response
# Create VM Schedule
schedule = "0 0 1 * *"
vmschedule = VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"start",
schedule,
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(minutes=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
)
self.cleanup.append(vmschedule)
self.debug("Created VM Schedule with ID: %s" % vmschedule.id)
# Update VM Schedule
new_schedule = "0 0 2 * *"
vmschedule.update(
self.apiclient,
id=vmschedule.id,
virtualmachineid=self.virtual_machine.id,
description="TestVM",
schedule=new_schedule,
timezone=datetime.datetime.now().astimezone().tzinfo,
startdate=(
datetime.datetime.now() + datetime.timedelta(minutes=10)
).strftime("%Y-%m-%d %H:%M:%S"),
enddate=(datetime.datetime.now() + datetime.timedelta(hours=10)).strftime(
"%Y-%m-%d %H:%M:%S"
),
)
self.debug("Updated VM Schedule with ID: %s" % vmschedule.id)
# List VM Schedule
vmschedules = VMSchedule.list(
self.apiclient, self.virtual_machine.id, id=vmschedule.id
)
self.assertEqual(
isinstance(vmschedules, list),
True,
"Check list response returns a valid list",
)
self.assertNotEqual(len(vmschedules), 0, "Check VM Schedule list")
self.debug("List VM Schedule response: %s" % vmschedules[0].__dict__)
self.assertEqual(
vmschedules[0].id,
vmschedule.id,
"Check VM Schedule ID in list resources call",
)
self.assertEqual(
vmschedules[0].virtualmachineid,
self.virtual_machine.id,
"Check VM ID in list resources call",
)
self.assertEqual(
vmschedules[0].schedule,
new_schedule,
"Check VM Schedule in list resources call",
)
return
@attr(tags=["advanced", "basic"], required_hardware="false")
def test_04_vmschedule_update_parameter_exceptions(self):
"""Test VM Schedule Update exceptions with invalid parameters"""
# Validate the following
# 1. Update VM Schedule with invalid schedule
# 2. Update VM Schedule with invalid start date
# 3. Update VM Schedule with invalid ID
# 4. Update VM Schedule with invalid end date
# Create VM Schedule
schedule = "0 0 1 * *"
vmschedule = VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"start",
schedule,
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(minutes=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
)
self.cleanup.append(vmschedule)
self.debug("Created VM Schedule with ID: %s" % vmschedule.id)
# Update VM Schedule with invalid schedule
with self.assertRaises(Exception):
vmschedule.update(
self.apiclient,
id=vmschedule.id,
virtualmachineid=self.virtual_machine.id,
description="TestVM",
schedule="invalid",
timezone=datetime.datetime.now().astimezone().tzinfo,
startdate=(
datetime.datetime.now() + datetime.timedelta(minutes=5)
).strftime("%Y-%m-%d %H:%M:%S"),
)
# Update VM Schedule with invalid start date
with self.assertRaises(Exception):
vmschedule.update(
self.apiclient,
id=vmschedule.id,
virtualmachineid=self.virtual_machine.id,
description="TestVM",
schedule=schedule,
timezone=datetime.datetime.now().astimezone().tzinfo,
startdate=(
datetime.datetime.now() - datetime.timedelta(days=1)
).strftime("%Y-%m-%d %H:%M:%S"),
)
# Update VM Schedule with invalid ID
with self.assertRaises(Exception):
vmschedule.update(
self.apiclient,
id="invalid",
virtualmachineid=self.virtual_machine.id,
description="TestVM",
schedule=schedule,
timezone=datetime.datetime.now().astimezone().tzinfo,
startdate=(
datetime.datetime.now() + datetime.timedelta(minutes=5)
).strftime("%Y-%m-%d %H:%M:%S"),
)
# Update VM Schedule with invalid end date
with self.assertRaises(Exception):
vmschedule.update(
self.apiclient,
id=vmschedule.id,
virtualmachineid=self.virtual_machine.id,
description="TestVM",
schedule=schedule,
timezone=datetime.datetime.now().astimezone().tzinfo,
startdate=(
datetime.datetime.now() + datetime.timedelta(minutes=5)
).strftime("%Y-%m-%d %H:%M:%S"),
enddate=(
datetime.datetime.now() - datetime.timedelta(minutes=5)
).strftime("%Y-%m-%d %H:%M:%S"),
)
return
@attr(tags=["advanced", "basic"], required_hardware="false")
def test_05_vmschedule_test_e2e(self):
# Validate the following
# 1. Create 2 VM Schedules - start and stop
# 2. Verify VM Schedule is created
# 3. Verify VM is stopped after schedule time
# 4. Verify VM is started after schedule time
# 5. Delete VM Schedule
# 6. Verify VM Schedule is deleted
# 7. Verify VM is not stopped after schedule time
# 8. Verify VM is not started after schedule time
# Create VM Schedule - start
start_schedule = "*/2 * * * *"
start_vmschedule = VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"start",
start_schedule,
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(seconds=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
enabled=True,
)
self.debug("Created VM Schedule with ID: %s" % start_vmschedule.id)
# Create VM Schedule - stop
stop_schedule = "*/1 * * * *"
stop_vmschedule = VMSchedule.create(
self.apiclient,
self.virtual_machine.id,
"stop",
stop_schedule,
datetime.datetime.now().astimezone().tzinfo,
# Current date minutes in format "2014-01-01 00:00:00"
(datetime.datetime.now() + datetime.timedelta(seconds=5)).strftime(
"%Y-%m-%d %H:%M:%S"
),
enabled=True,
)
self.debug("Created VM Schedule with ID: %s" % stop_vmschedule.id)
# Verify VM Schedule is created
vmschedules = VMSchedule.list(
self.apiclient, self.virtual_machine.id, id=start_vmschedule.id
)
self.assertEqual(
isinstance(vmschedules, list),
True,
"Check list response returns a valid list",
)
self.assertNotEqual(len(vmschedules), 0, "Check VM Schedule is created")
# poll every 20 seconds (max waiting time is 4 minutes ) and check VM's state for changes
previous_state = self.virtual_machine.state
self.debug("VM state: %s" % self.virtual_machine.state)
is_stop_schedule_working = False
is_start_schedule_working = False
for i in range(0, 12):
time.sleep(20)
current_state = self.virtual_machine.update(self.apiclient).state
self.debug("Polling VM state: %s" % current_state)
if previous_state in ("Running", "Starting") and current_state in (
"Stopped",
"Stopping",
):
is_stop_schedule_working = True
elif previous_state in ("Stopped", "Stopping") and current_state in (
"Running",
"Starting",
):
is_start_schedule_working = True
if is_start_schedule_working and is_stop_schedule_working:
break
previous_state = current_state
self.debug("Is stop schedule working: %s" % is_stop_schedule_working)
self.debug("Is start schedule working: %s" % is_start_schedule_working)
self.assertTrue(
is_stop_schedule_working,
"VM switched states from Running to Stopped at least once",
)
self.assertTrue(
is_start_schedule_working,
"VM switched states from Stopped to Running at least once",
)
# Delete VM Schedule
start_vmschedule.delete(self.apiclient)
stop_vmschedule.delete(self.apiclient)
# Verify VM Schedule is deleted
self.assertEqual(
VMSchedule.list(
self.apiclient, self.virtual_machine.id, id=start_vmschedule.id
),
None,
"Check VM Schedule is deleted",
)
self.assertEqual(
VMSchedule.list(
self.apiclient, self.virtual_machine.id, id=stop_vmschedule.id
),
None,
"Check VM Schedule is deleted",
)
# Verify VM does not switch states after deleting schedules at least for 1.5 minutes
previous_state = self.virtual_machine.update(self.apiclient).state
state_changed = False
for i in range(0, 3):
time.sleep(30)
current_state = self.virtual_machine.update(self.apiclient).state
if previous_state != current_state:
self.debug(
"VM changed state from %s to %s" % (previous_state, current_state)
)
state_changed = True
break
self.assertFalse(
state_changed,
"VM did not switch states after schedule time",
)
return

View File

@ -6521,3 +6521,43 @@ class PolicyRule:
cmd.policyuuid = policyuuid cmd.policyuuid = policyuuid
cmd.ruleuuid = ruleuuid cmd.ruleuuid = ruleuuid
return apiclient.removeTungstenFabricPolicyRule(cmd) return apiclient.removeTungstenFabricPolicyRule(cmd)
class VMSchedule:
def __init__(self, items):
self.__dict__.update(items)
@classmethod
def create(cls, apiclient, virtualmachineid, action, schedule, timezone, startdate, enabled=False, description=None, enddate=None):
cmd = createVMSchedule.createVMScheduleCmd()
cmd.virtualmachineid = virtualmachineid
cmd.description = description
cmd.action = action
cmd.schedule = schedule
cmd.timezone = timezone
cmd.startdate = startdate
cmd.enddate = enddate
cmd.enabled = enabled
return VMSchedule(apiclient.createVMSchedule(cmd).__dict__)
@classmethod
def list(cls, apiclient, virtualmachineid, id=None, enabled=None, action=None):
cmd = listVMSchedule.listVMScheduleCmd()
cmd.virtualmachineid = virtualmachineid
cmd.id = id
cmd.enabled = enabled
cmd.action = action
return apiclient.listVMSchedule(cmd)
def update(self, apiclient, **kwargs):
cmd = updateVMSchedule.updateVMScheduleCmd()
cmd.id = self.id
[setattr(cmd, k, v) for k, v in list(kwargs.items())]
return apiclient.updateVMSchedule(cmd)
def delete(self, apiclient):
cmd = deleteVMSchedule.deleteVMScheduleCmd()
cmd.id = self.id
cmd.virtualmachineid = self.virtualmachineid
return (apiclient.deleteVMSchedule(cmd))

View File

@ -42,7 +42,7 @@ module.exports = {
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
], ],
transformIgnorePatterns: [ transformIgnorePatterns: [
'<rootDir>/node_modules/(?!ant-design-vue|vue|@babel/runtime|lodash-es|@ant-design)' '<rootDir>/node_modules/(?!ant-design-vue|vue|@babel/runtime|lodash-es|@ant-design|@vue-js-cron)'
], ],
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: [ collectCoverageFrom: [

192
ui/package-lock.json generated
View File

@ -1604,67 +1604,6 @@
"postcss": "^7.0.0" "postcss": "^7.0.0"
} }
}, },
"@intlify/core-base": {
"version": "9.1.10",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.10.tgz",
"integrity": "sha512-So9CNUavB/IsZ+zBmk2Cv6McQp6vc2wbGi1S0XQmJ8Vz+UFcNn9MFXAe9gY67PreIHrbLsLxDD0cwo1qsxM1Nw==",
"requires": {
"@intlify/devtools-if": "9.1.10",
"@intlify/message-compiler": "9.1.10",
"@intlify/message-resolver": "9.1.10",
"@intlify/runtime": "9.1.10",
"@intlify/shared": "9.1.10",
"@intlify/vue-devtools": "9.1.10"
}
},
"@intlify/devtools-if": {
"version": "9.1.10",
"resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.10.tgz",
"integrity": "sha512-SHaKoYu6sog3+Q8js1y3oXLywuogbH1sKuc7NSYkN3GElvXSBaMoCzW+we0ZSFqj/6c7vTNLg9nQ6rxhKqYwnQ==",
"requires": {
"@intlify/shared": "9.1.10"
}
},
"@intlify/message-compiler": {
"version": "9.1.10",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.10.tgz",
"integrity": "sha512-+JiJpXff/XTb0EadYwdxOyRTB0hXNd4n1HaJ/a4yuV960uRmPXaklJsedW0LNdcptd/hYUZtCkI7Lc9J5C1gxg==",
"requires": {
"@intlify/message-resolver": "9.1.10",
"@intlify/shared": "9.1.10",
"source-map": "0.6.1"
}
},
"@intlify/message-resolver": {
"version": "9.1.10",
"resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.10.tgz",
"integrity": "sha512-5YixMG/M05m0cn9+gOzd4EZQTFRUu8RGhzxJbR1DWN21x/Z3bJ8QpDYj6hC4FwBj5uKsRfKpJQ3Xqg98KWoA+w=="
},
"@intlify/runtime": {
"version": "9.1.10",
"resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.10.tgz",
"integrity": "sha512-7QsuByNzpe3Gfmhwq6hzgXcMPpxz8Zxb/XFI6s9lQdPLPe5Lgw4U1ovRPZTOs6Y2hwitR3j/HD8BJNGWpJnOFA==",
"requires": {
"@intlify/message-compiler": "9.1.10",
"@intlify/message-resolver": "9.1.10",
"@intlify/shared": "9.1.10"
}
},
"@intlify/shared": {
"version": "9.1.10",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.10.tgz",
"integrity": "sha512-Om54xJeo1Vw+K1+wHYyXngE8cAbrxZHpWjYzMR9wCkqbhGtRV5VLhVc214Ze2YatPrWlS2WSMOWXR8JktX/IgA=="
},
"@intlify/vue-devtools": {
"version": "9.1.10",
"resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.10.tgz",
"integrity": "sha512-5l3qYARVbkWAkagLu1XbDUWRJSL8br1Dj60wgMaKB0+HswVsrR6LloYZTg7ozyvM621V6+zsmwzbQxbVQyrytQ==",
"requires": {
"@intlify/message-resolver": "9.1.10",
"@intlify/runtime": "9.1.10",
"@intlify/shared": "9.1.10"
}
},
"@istanbuljs/load-nyc-config": { "@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -2962,6 +2901,49 @@
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
"dev": true "dev": true
}, },
"@vue-js-cron/ant": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@vue-js-cron/ant/-/ant-1.1.3.tgz",
"integrity": "sha512-E7PbZX/Fwb4w4GYYllnhDx8ywTce3OQ3WS20BPS8EbPlmShmkdvtU9dyNC+C+bGyzXfUwPIdLbtQ1vKzLrMm0A==",
"requires": {
"@vue-js-cron/core": "3.7.1",
"ant-design-vue": "^3.2.12"
},
"dependencies": {
"ant-design-vue": {
"version": "3.2.20",
"resolved": "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-3.2.20.tgz",
"integrity": "sha512-YWpMfGaGoRastIXEYfCoJiaRiDHk4chqtYhlKQM5GqPt6NfvrM1Vg2e60yHtjxlZjed91wCMm0rAmyUr7Hwzdg==",
"requires": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-vue": "^6.1.0",
"@babel/runtime": "^7.10.5",
"@ctrl/tinycolor": "^3.4.0",
"@simonwep/pickr": "~1.8.0",
"array-tree-filter": "^2.1.0",
"async-validator": "^4.0.0",
"dayjs": "^1.10.5",
"dom-align": "^1.12.1",
"dom-scroll-into-view": "^2.0.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.15",
"resize-observer-polyfill": "^1.5.1",
"scroll-into-view-if-needed": "^2.2.25",
"shallow-equal": "^1.0.0",
"vue-types": "^3.0.0",
"warning": "^4.0.0"
}
}
}
},
"@vue-js-cron/core": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@vue-js-cron/core/-/core-3.7.1.tgz",
"integrity": "sha512-aWCkbfCbwpEJwmptY0tRFlxH4ZaVtnmR2Q3f0L8SzSC58cn45VYMe2/eSK53221cBmk/w+18Hq563u+W7Jp9Eg==",
"requires": {
"mustache": "^4.2.0"
}
},
"@vue/babel-helper-vue-jsx-merge-props": { "@vue/babel-helper-vue-jsx-merge-props": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
@ -8083,6 +8065,11 @@
"sha.js": "^2.4.8" "sha.js": "^2.4.8"
} }
}, },
"cronstrue": {
"version": "2.26.0",
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.26.0.tgz",
"integrity": "sha512-M1VdV3hpBAsd1Zzvqcvf63wgDpcwCuS4WiNEVFpJ0s33MGO2sVDTfswYq0EPypCmESrCzmgL8h68DTzJuSDbVA=="
},
"cross-spawn": { "cross-spawn": {
"version": "6.0.5", "version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@ -16088,6 +16075,21 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
}, },
"moment-timezone": {
"version": "0.5.43",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
"integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
"requires": {
"moment": "^2.29.4"
},
"dependencies": {
"moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
}
}
},
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -16143,6 +16145,11 @@
"integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==",
"dev": true "dev": true
}, },
"mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="
},
"mute-stream": { "mute-stream": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
@ -22450,14 +22457,63 @@
"dev": true "dev": true
}, },
"vue-i18n": { "vue-i18n": {
"version": "9.1.10", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.10.tgz", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
"integrity": "sha512-jpr7gV5KPk4n+sSPdpZT8Qx3XzTcNDWffRlHV/cT2NUyEf+sEgTTmLvnBAibjOFJ0zsUyZlVTAWH5DDnYep+1g==", "integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
"requires": { "requires": {
"@intlify/core-base": "9.1.10", "@intlify/core-base": "9.2.2",
"@intlify/shared": "9.1.10", "@intlify/shared": "9.2.2",
"@intlify/vue-devtools": "9.1.10", "@intlify/vue-devtools": "9.2.2",
"@vue/devtools-api": "^6.0.0-beta.7" "@vue/devtools-api": "^6.2.1"
},
"dependencies": {
"@intlify/core-base": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
"integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
"requires": {
"@intlify/devtools-if": "9.2.2",
"@intlify/message-compiler": "9.2.2",
"@intlify/shared": "9.2.2",
"@intlify/vue-devtools": "9.2.2"
}
},
"@intlify/devtools-if": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
"integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
"requires": {
"@intlify/shared": "9.2.2"
}
},
"@intlify/message-compiler": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
"integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
"requires": {
"@intlify/shared": "9.2.2",
"source-map": "0.6.1"
}
},
"@intlify/shared": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
"integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q=="
},
"@intlify/vue-devtools": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
"integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
"requires": {
"@intlify/core-base": "9.2.2",
"@intlify/shared": "9.2.2"
}
},
"@vue/devtools-api": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz",
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
}
} }
}, },
"vue-jest": { "vue-jest": {

View File

@ -38,6 +38,8 @@
"@fortawesome/free-brands-svg-icons": "^5.15.2", "@fortawesome/free-brands-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/vue-fontawesome": "^3.0.0-4", "@fortawesome/vue-fontawesome": "^3.0.0-4",
"@vue-js-cron/ant": "^1.1.3",
"@vue-js-cron/core": "^3.7.1",
"ant-design-vue": "^3.2.9", "ant-design-vue": "^3.2.9",
"antd": "^4.21.4", "antd": "^4.21.4",
"antd-theme-webpack-plugin": "^1.3.9", "antd-theme-webpack-plugin": "^1.3.9",
@ -46,12 +48,14 @@
"chart.js": "^3.7.1", "chart.js": "^3.7.1",
"chartjs-adapter-moment": "^1.0.0", "chartjs-adapter-moment": "^1.0.0",
"core-js": "^3.21.1", "core-js": "^3.21.1",
"cronstrue": "^2.26.0",
"enquire.js": "^2.1.6", "enquire.js": "^2.1.6",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"md5": "^2.2.1", "md5": "^2.2.1",
"mitt": "^2.1.0", "mitt": "^2.1.0",
"moment": "^2.26.0", "moment": "^2.26.0",
"moment-timezone": "^0.5.43",
"npm-check-updates": "^6.0.1", "npm-check-updates": "^6.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"qrious": "^4.0.2", "qrious": "^4.0.2",

View File

@ -556,6 +556,7 @@
"label.creating": "Creating", "label.creating": "Creating",
"label.creating.iprange": "Creating IP ranges", "label.creating.iprange": "Creating IP ranges",
"label.credit": "Credit", "label.credit": "Credit",
"label.cron": "Cron expression",
"label.crosszones": "Cross zones", "label.crosszones": "Cross zones",
"label.currency": "Currency", "label.currency": "Currency",
"label.current": "Current", "label.current": "Current",
@ -857,6 +858,8 @@
"label.for": "for", "label.for": "for",
"label.forbidden": "Forbidden", "label.forbidden": "Forbidden",
"label.forced": "Force", "label.forced": "Force",
"label.force.stop": "Force stop",
"label.force.reboot": "Force reboot",
"label.forceencap": "Force UDP encapsulation of ESP packets", "label.forceencap": "Force UDP encapsulation of ESP packets",
"label.forgedtransmits": "Forged transmits", "label.forgedtransmits": "Forged transmits",
"label.format": "Format", "label.format": "Format",
@ -1710,8 +1713,10 @@
"label.scaleup.policies": "ScaleUp policies", "label.scaleup.policies": "ScaleUp policies",
"label.scaleup.policy": "ScaleUp policy", "label.scaleup.policy": "ScaleUp policy",
"label.schedule": "Schedule", "label.schedule": "Schedule",
"label.schedule.add": "Add schedule",
"label.scheduled.backups": "Scheduled backups", "label.scheduled.backups": "Scheduled backups",
"label.scheduled.snapshots": "Scheduled snapshots", "label.scheduled.snapshots": "Scheduled snapshots",
"label.schedules": "Schedules",
"label.scope": "Scope", "label.scope": "Scope",
"label.search": "Search", "label.search": "Search",
"label.secondary.isolated.vlan.type.isolated": "Isolated", "label.secondary.isolated.vlan.type.isolated": "Isolated",
@ -2134,6 +2139,7 @@
"label.vmlimit": "Instance limits", "label.vmlimit": "Instance limits",
"label.vmname": "VM name", "label.vmname": "VM name",
"label.vms": "VMs", "label.vms": "VMs",
"label.vmscheduleactions": "Actions",
"label.vmstate": "VM state", "label.vmstate": "VM state",
"label.vmtotal": "Total of VMs", "label.vmtotal": "Total of VMs",
"label.vmware.storage.policy": "VMWare storage policy", "label.vmware.storage.policy": "VMWare storage policy",
@ -2638,6 +2644,7 @@
"message.error.remove.nic": "There was an error", "message.error.remove.nic": "There was an error",
"message.error.remove.secondary.ipaddress": "There was an error removing the secondary IP Address", "message.error.remove.secondary.ipaddress": "There was an error removing the secondary IP Address",
"message.error.remove.tungsten.routing.policy": "Removing Tungsten-Fabric Routing Policy from network failed", "message.error.remove.tungsten.routing.policy": "Removing Tungsten-Fabric Routing Policy from network failed",
"message.error.remove.vm.schedule": "Removing Instance Schedule failed",
"message.error.required.input": "Please enter input", "message.error.required.input": "Please enter input",
"message.error.reset.config": "Unable to reset config to default value", "message.error.reset.config": "Unable to reset config to default value",
"message.error.retrieve.kubeconfig": "Unable to retrieve Kubernetes cluster config", "message.error.retrieve.kubeconfig": "Unable to retrieve Kubernetes cluster config",
@ -2940,6 +2947,7 @@
"message.success.config.backup.schedule": "Successfully configured VM backup schedule", "message.success.config.backup.schedule": "Successfully configured VM backup schedule",
"message.success.config.health.monitor": "Successfully Configure Health Monitor", "message.success.config.health.monitor": "Successfully Configure Health Monitor",
"message.success.config.sticky.policy": "Successfully configured sticky policy", "message.success.config.sticky.policy": "Successfully configured sticky policy",
"message.success.config.vm.schedule": "Successfully configured instance schedule",
"message.success.copy.clipboard": "Successfully copied to clipboard", "message.success.copy.clipboard": "Successfully copied to clipboard",
"message.success.create.account": "Successfully created account", "message.success.create.account": "Successfully created account",
"message.success.create.internallb": "Successfully created Internal Load Balancer", "message.success.create.internallb": "Successfully created Internal Load Balancer",

View File

@ -99,6 +99,12 @@
<span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(text)">{{ $t(text.toLowerCase()) }}</span> <span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(text)">{{ $t(text.toLowerCase()) }}</span>
<span v-else>{{ text }}</span> <span v-else>{{ text }}</span>
</template> </template>
<template v-if="column.key === 'schedule'">
{{ text }}
<br/>
({{ generateHumanReadableSchedule(text) }})
</template>
<template v-if="column.key === 'displayname'"> <template v-if="column.key === 'displayname'">
<QuickView <QuickView
style="margin-left: 5px" style="margin-left: 5px"
@ -296,11 +302,15 @@
<template v-if="column.key === 'current'"> <template v-if="column.key === 'current'">
<status :text="record.current ? record.current.toString() : 'false'" /> <status :text="record.current ? record.current.toString() : 'false'" />
</template> </template>
<template v-if="column.key === 'created'"> <template v-if="column.key === 'enabled'">
<status :text="record.enabled ? record.enabled.toString() : 'false'" />
{{ record.enabled ? 'Enabled' : 'Disabled' }}
</template>
<template v-if="['created', 'sent'].includes(column.key)">
{{ $toLocaleDate(text) }} {{ $toLocaleDate(text) }}
</template> </template>
<template v-if="column.key === 'sent'"> <template v-if="['startdate', 'enddate'].includes(column.key) && ['vm'].includes($route.path.split('/')[1])">
{{ $toLocaleDate(text) }} {{ getDateAtTimeZone(text, record.timezone) }}
</template> </template>
<template v-if="column.key === 'order'"> <template v-if="column.key === 'order'">
<div class="shift-btns"> <div class="shift-btns">
@ -387,6 +397,20 @@
@onClick="editTariffValue(record)" /> @onClick="editTariffValue(record)" />
<slot></slot> <slot></slot>
</template> </template>
<template v-if="column.key === 'vmScheduleActions'">
<tooltip-button
:tooltip="$t('label.edit')"
:disabled="!('updateVMSchedule' in $store.getters.apis)"
icon="edit-outlined"
@onClick="updateVMSchedule(record)" />
<tooltip-button
:tooltip="$t('label.remove')"
:disabled="!('deleteVMSchedule' in $store.getters.apis)"
icon="delete-outlined"
:danger="true"
type="primary"
@onClick="removeVMSchedule(record)" />
</template>
</template> </template>
<template #footer> <template #footer>
<span v-if="hasSelected"> <span v-if="hasSelected">
@ -405,6 +429,8 @@ import TooltipButton from '@/components/widgets/TooltipButton'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
import ResourceLabel from '@/components/widgets/ResourceLabel' import ResourceLabel from '@/components/widgets/ResourceLabel'
import { createPathBasedOnVmType } from '@/utils/plugins' import { createPathBasedOnVmType } from '@/utils/plugins'
import cronstrue from 'cronstrue/i18n'
import moment from 'moment-timezone'
export default { export default {
name: 'ListView', name: 'ListView',
@ -522,6 +548,9 @@ export default {
'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment' 'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment'
].includes(this.$route.name) ].includes(this.$route.name)
}, },
getDateAtTimeZone (date, timezone) {
return date ? moment(date).tz(timezone).format('YYYY-MM-DD HH:mm:ss') : null
},
fetchColumns () { fetchColumns () {
if (this.isOrderUpdatable()) { if (this.isOrderUpdatable()) {
return this.columns return this.columns
@ -686,6 +715,12 @@ export default {
editTariffValue (record) { editTariffValue (record) {
this.$emit('edit-tariff-action', true, record) this.$emit('edit-tariff-action', true, record)
}, },
updateVMSchedule (record) {
this.$emit('update-vm-schedule', record)
},
removeVMSchedule (record) {
this.$emit('remove-vm-schedule', record)
},
ipV6Address (text, record) { ipV6Address (text, record) {
if (!record || !record.nic || record.nic.length === 0) { if (!record || !record.nic || record.nic.length === 0) {
return '' return ''
@ -730,6 +765,9 @@ export default {
default: return record.entitytype.toLowerCase().replace('_', '') default: return record.entitytype.toLowerCase().replace('_', '')
} }
}, },
generateHumanReadableSchedule (schedule) {
return cronstrue.toString(schedule, { locale: this.$i18n.locale })
},
entityTypeToPath (entitytype) { entityTypeToPath (entitytype) {
switch (entitytype) { switch (entitytype) {
case 'VM' : return 'vm' case 'VM' : return 'vm'

View File

@ -68,6 +68,9 @@ import {
import VueClipboard from 'vue3-clipboard' import VueClipboard from 'vue3-clipboard'
import VueCropper from 'vue-cropper' import VueCropper from 'vue-cropper'
import cronAnt from '@vue-js-cron/ant'
import '@vue-js-cron/ant/dist/ant.css'
export default { export default {
install: (app) => { install: (app) => {
app.config.globalProperties.$confirm = Modal.confirm app.config.globalProperties.$confirm = Modal.confirm
@ -77,6 +80,7 @@ export default {
app.config.globalProperties.$error = Modal.error app.config.globalProperties.$error = Modal.error
app.config.globalProperties.$warning = Modal.warning app.config.globalProperties.$warning = Modal.warning
app.use(cronAnt)
app.use(VueClipboard, { autoSetContainer: true }) app.use(VueClipboard, { autoSetContainer: true })
app.use(VueCropper) app.use(VueCropper)
app.use(ConfigProvider) app.use(ConfigProvider)

View File

@ -0,0 +1,461 @@
// 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.
<template>
<div>
<a-button
type="dashed"
style="width: 100%; margin-bottom: 10px"
@click="showAddModal"
:loading="loading"
:disabled="!('createVMSchedule' in $store.getters.apis)">
<template #icon><plus-outlined/></template> {{ $t('label.schedule.add') }}
</a-button>
<list-view
:loading="tabLoading"
:columns="columns"
:items="schedules"
:columnKeys="columnKeys"
:selectedColumns="selectedColumnKeys"
ref="listview"
@update-selected-columns="updateSelectedColumns"
@update-vm-schedule="updateVMSchedule"
@remove-vm-schedule="removeVMSchedule"
@refresh="this.fetchData"/>
<a-pagination
class="row-element"
style="margin-top: 10px"
size="small"
:current="page"
:pageSize="pageSize"
:total="totalCount"
:showTotal="total => `${$t('label.showing')} ${Math.min(total, 1+((page-1)*pageSize))}-${Math.min(page*pageSize, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
:pageSizeOptions="pageSizeOptions"
@change="changePage"
@showSizeChange="changePage"
showSizeChanger
showQuickJumper>
<template #buildOptionText="props">
<span>{{ props.value }} / {{ $t('label.page') }}</span>
</template>
</a-pagination>
</div>
<a-modal
:visible="showModal"
:title="$t('label.schedule')"
:maskClosable="false"
:closable="true"
:footer="null"
@cancel="closeModal">
<a-form
layout="vertical"
:ref="formRef"
:model="form"
:rules="rules"
@finish="submitForm"
v-ctrl-enter="submitForm">
<a-form-item name="description" ref="description">
<template #label>
<tooltip-label :title="$t('label.description')" :tooltip="apiParams.description.description"/>
</template>
<a-input
v-model:value="form.description"
v-focus="true" />
</a-form-item>
<a-form-item name="action" ref="action">
<template #label>
<tooltip-label :title="$t('label.action')" :tooltip="apiParams.action.description"/>
</template>
<a-radio-group
v-model:value="form.action"
button-style="solid"
:disabled="isEdit">
<a-radio-button v-for="action in actions" :key="action.id" :value="action.value">
{{ $t(action.label) }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item name="schedule" ref="schedule">
<template #label>
<tooltip-label :title="$t('label.schedule')" :tooltip="apiParams.schedule.description"/>
</template>
<label>{{ $t('label.advanced.mode') }}</label>
<a-switch
v-model:checked="form.useCronFormat"
>
</a-switch>
<br/>
<span v-if="!form.useCronFormat">
<cron-ant
v-model="form.schedule"
:periods="periods"
:button-props="{ type: 'primary', size: 'small', disabled: form.useCronFormat }"
@error="error=$event"/>
</span>
<span v-if="form.useCronFormat">
<label>{{ generateHumanReadableSchedule(form.schedule) }}</label>
<br/>
</span>
<a-input
:addonBefore="$t('label.cron')"
v-model:value="form.schedule"
:disabled="!form.useCronFormat"
v-focus="true" />
</a-form-item>
<a-form-item name="timezone" ref="timezone">
<template #label>
<tooltip-label :title="$t('label.timezone')" :tooltip="apiParams.timezone.description"/>
</template>
<a-select
showSearch
v-model:value="form.timezone"
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="fetching">
<a-select-option v-for="opt in timeZoneMap" :key="opt.id" :label="opt.name || opt.description">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="startDate" ref="startDate">
<template #label>
<tooltip-label :title="$t('label.start.date.and.time')" :tooltip="apiParams.startdate.description"/>
</template>
<a-date-picker
v-model:value="form.startDate"
show-time
:locale="this.$i18n.locale"
:placeholder="$t('message.select.start.date.and.time')"/>
</a-form-item>
<a-form-item name="endDate" ref="endDate">
<template #label>
<tooltip-label :title="$t('label.end.date.and.time')" :tooltip="apiParams.enddate.description"/>
</template>
<a-date-picker
v-model:value="form.endDate"
show-time
:locale="this.$i18n.locale"
:placeholder="$t('message.select.end.date.and.time')"/>
</a-form-item>
<a-form-item name="enabled" ref="enabled">
<template #label>
<tooltip-label :title="$t('label.enabled')" :tooltip="apiParams.enabled.description"/>
</template>
<a-switch
v-model:checked="form.enabled">
</a-switch>
</a-form-item>
<div :span="24" class="action-button">
<a-button
:loading="loading"
@click="closeModal">
{{ $t('label.cancel') }}
</a-button>
<a-button
:loading="loading"
ref="submit"
type="primary"
htmlType="submit">
{{ $t('label.ok') }}
</a-button>
</div>
</a-form>
</a-modal>
</template>
<script>
import { reactive, ref, toRaw } from 'vue'
import { api } from '@/api'
import ListView from '@/components/view/ListView'
import Status from '@/components/widgets/Status'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import { mixinForm } from '@/utils/mixin'
import { timeZone } from '@/utils/timezone'
import debounce from 'lodash/debounce'
import cronstrue from 'cronstrue/i18n'
import moment from 'moment-timezone'
export default {
name: 'InstanceSchedules',
mixins: [mixinForm],
components: {
Status,
ListView,
TooltipLabel
},
props: {
virtualmachine: {
type: Object,
required: true
},
loading: {
type: Boolean,
required: true
}
},
data () {
this.fetchTimeZone = debounce(this.fetchTimeZone, 800)
return {
tabLoading: false,
columnKeys: ['action', 'enabled', 'description', 'schedule', 'timezone', 'startdate', 'enddate', 'created', 'vmScheduleActions'],
selectedColumnKeys: [],
columns: [],
schedules: [],
timeZoneMap: [],
actions: [
{ value: 'START', label: 'label.start' },
{ value: 'STOP', label: 'label.stop' },
{ value: 'REBOOT', label: 'label.reboot' },
{ value: 'FORCE_STOP', label: 'label.force.stop' },
{ value: 'FORCE_REBOOT', label: 'label.force.reboot' }
],
periods: [
{ id: 'year', value: ['month', 'day', 'dayOfWeek', 'hour', 'minute'] },
{ id: 'month', value: ['day', 'dayOfWeek', 'hour', 'minute'] },
{ id: 'week', value: ['dayOfWeek', 'hour', 'minute'] },
{ id: 'day', value: ['hour', 'minute'] }
],
page: 1,
pageSize: 20,
totalCount: 0,
showModal: false,
isSubmitted: false,
isEdit: false,
error: '',
pattern: 'YYYY-MM-DD HH:mm:ss'
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('createVMSchedule')
},
computed: {
pageSizeOptions () {
var sizes = [20, 50, 100, 200, this.$store.getters.defaultListViewPageSize]
if (this.device !== 'desktop') {
sizes.unshift(10)
}
return [...new Set(sizes)].sort(function (a, b) {
return a - b
}).map(String)
}
},
created () {
this.selectedColumnKeys = this.columnKeys
this.updateColumns()
this.pageSize = this.pageSizeOptions[0] * 1
this.initForm()
this.fetchData()
this.fetchTimeZone()
},
watch: {
virtualmachine: {
handler () {
this.fetchSchedules()
}
}
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({
action: 'START',
schedule: '* * * * *',
description: '',
timezone: 'UTC',
startDate: '',
endDate: '',
enabled: true,
useCronFormat: false
})
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') }],
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')}` }]
})
},
createVMSchedule (schedule) {
this.resetForm()
this.showAddModal()
},
removeVMSchedule (schedule) {
api('deleteVMSchedule', {
id: schedule.id,
virtualmachineid: this.virtualmachine.id
}).then(() => {
if (this.totalCount - 1 === this.pageSize * (this.page - 1)) {
this.page = this.page - 1 > 0 ? this.page - 1 : 1
}
const message = `${this.$t('label.removing')} ${schedule.description}`
this.$message.success(message)
}).catch(error => {
console.error(error)
this.$message.error(this.$t('message.error.remove.vm.schedule'))
this.$notification.error({
message: this.$t('label.error'),
description: this.$t('message.error.remove.vm.schedule')
})
}).finally(() => {
this.fetchData()
})
},
updateVMSchedule (schedule) {
this.resetForm()
this.isEdit = true
Object.assign(this.form, schedule)
// Some weird issue when we directly pass in the moment with tz object
this.form.startDate = moment(moment(schedule.startdate).tz(schedule.timezone).format(this.pattern))
this.form.endDate = schedule.enddate ? moment(moment(schedule.enddate).tz(schedule.timezone).format(this.pattern)) : ''
this.showAddModal()
},
showAddModal () {
this.showModal = true
},
submitForm () {
if (this.isSubmitted) return
this.isSubmitted = true
this.formRef.value.validate().then(() => {
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
var params = {
description: values.description,
schedule: values.schedule,
timezone: values.timezone,
action: values.action,
virtualmachineid: this.virtualmachine.id,
enabled: values.enabled,
startdate: (values.startDate) ? values.startDate.format(this.pattern) : null,
enddate: (values.endDate) ? values.endDate.format(this.pattern) : null
}
let command = null
if (this.form.id === null || this.form.id === undefined) {
command = 'createVMSchedule'
} else {
params.id = this.form.id
command = 'updateVMSchedule'
}
api(command, params).then(response => {
this.$notification.success({
message: this.$t('label.schedule'),
description: this.$t('message.success.config.vm.schedule')
})
this.isSubmitted = false
this.fetchData()
this.closeModal()
}).catch(error => {
this.$notifyError(error)
this.isSubmitted = false
})
}).catch(error => {
this.$notifyError(error)
if (error.errorFields !== undefined) {
this.formRef.value.scrollToField(error.errorFields[0].name)
}
}).finally(() => {
this.isSubmitted = false
})
},
resetForm () {
this.isEdit = false
if (this.formRef.value) {
this.formRef.value.resetFields()
}
},
fetchTimeZone (value) {
this.timeZoneMap = []
this.fetching = true
timeZone(value).then(json => {
this.timeZoneMap = json
this.fetching = false
})
},
closeModal () {
this.resetForm()
this.initForm()
this.showModal = false
},
fetchData () {
this.fetchSchedules()
},
fetchSchedules () {
this.schedules = []
if (!this.virtualmachine.id) {
return
}
const params = {
page: this.page,
pagesize: this.pageSize,
virtualmachineid: this.virtualmachine.id,
listall: true
}
this.tabLoading = true
api('listVMSchedule', params).then(json => {
this.schedules = []
this.totalCount = json?.listvmscheduleresponse?.count || 0
this.schedules = json?.listvmscheduleresponse?.vmschedule || []
this.tabLoading = false
})
},
changePage (page, pageSize) {
this.page = page
this.pageSize = pageSize
this.fetchData()
},
updateSelectedColumns (key) {
if (this.selectedColumnKeys.includes(key)) {
this.selectedColumnKeys = this.selectedColumnKeys.filter(x => x !== key)
} else {
this.selectedColumnKeys.push(key)
}
this.updateColumns()
},
generateHumanReadableSchedule (schedule) {
return cronstrue.toString(schedule, { locale: this.$i18n.locale, throwExceptionOnParseError: false })
},
updateColumns () {
this.columns = []
for (var columnKey of this.columnKeys) {
if (!this.selectedColumnKeys.includes(columnKey)) continue
this.columns.push({
key: columnKey,
// If columnKey is 'enabled', then title is 'state'
// If columnKey is 'startdate', then the title is `start.date.and.time`
// else title is columnKey
title: columnKey === 'enabled'
? this.$t('label.state')
: columnKey === 'startdate'
? this.$t('label.start.date.and.time')
: columnKey === 'enddate'
? this.$t('label.end.date.and.time')
: this.$t('label.' + String(columnKey).toLowerCase()),
dataIndex: columnKey
})
}
if (this.columns.length > 0) {
this.columns[this.columns.length - 1].customFilterDropdown = true
}
}
}
}
</script>

View File

@ -116,6 +116,11 @@
:routerlinks="(record) => { return { name: '/securitygroups/' + record.id } }" :routerlinks="(record) => { return { name: '/securitygroups/' + record.id } }"
:showSearch="false"/> :showSearch="false"/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane :tab="$t('label.schedules')" key="schedules" v-if="'listVMSchedule' in $store.getters.apis">
<InstanceSchedules
:virtualmachine="vm"
:loading="loading"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.settings')" key="settings"> <a-tab-pane :tab="$t('label.settings')" key="settings">
<DetailSettings :resource="dataResource" :loading="loading" /> <DetailSettings :resource="dataResource" :loading="loading" />
</a-tab-pane> </a-tab-pane>
@ -290,6 +295,7 @@ import StatsTab from '@/components/view/StatsTab'
import EventsTab from '@/components/view/EventsTab' import EventsTab from '@/components/view/EventsTab'
import DetailSettings from '@/components/view/DetailSettings' import DetailSettings from '@/components/view/DetailSettings'
import NicsTable from '@/views/network/NicsTable' import NicsTable from '@/views/network/NicsTable'
import InstanceSchedules from '@/views/compute/InstanceSchedules.vue'
import ListResourceTable from '@/components/view/ListResourceTable' import ListResourceTable from '@/components/view/ListResourceTable'
import TooltipButton from '@/components/widgets/TooltipButton' import TooltipButton from '@/components/widgets/TooltipButton'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
@ -305,6 +311,7 @@ export default {
EventsTab, EventsTab,
DetailSettings, DetailSettings,
NicsTable, NicsTable,
InstanceSchedules,
ListResourceTable, ListResourceTable,
TooltipButton, TooltipButton,
ResourceIcon, ResourceIcon,

View File

@ -118,6 +118,11 @@
<artifactId>javax.servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>${cs.cron-utils.version}</version>
</dependency>
<!-- Test dependency in mysql for db tests --> <!-- Test dependency in mysql for db tests -->
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>

View File

@ -22,6 +22,9 @@ package com.cloud.utils;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; import java.util.TimeZone;
@ -31,6 +34,13 @@ import java.time.format.DateTimeParseException;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.CloudRuntimeException;
import com.cronutils.descriptor.CronDescriptor;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinition;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.parser.CronParser;
import org.springframework.scheduling.support.CronExpression;
public class DateUtil { public class DateUtil {
public static final int HOURS_IN_A_MONTH = 30 * 24; public static final int HOURS_IN_A_MONTH = 30 * 24;
@ -295,4 +305,35 @@ public class DateUtil {
return (dateCalendar1.getTimeInMillis() - dateCalendar2.getTimeInMillis() )/1000; return (dateCalendar1.getTimeInMillis() - dateCalendar2.getTimeInMillis() )/1000;
} }
public static CronExpression parseSchedule(String schedule) {
if (schedule != null) {
// CronExpression's granularity is in seconds. Prepending "0 " to change the granularity to minutes.
return CronExpression.parse(String.format("0 %s", schedule));
} else {
return null;
}
}
public static String getHumanReadableSchedule(CronExpression schedule) {
CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING);
CronParser parser = new CronParser(cronDefinition);
CronDescriptor descriptor = CronDescriptor.instance();
return descriptor.describe(parser.parse(schedule.toString()));
}
public static ZonedDateTime getZoneDateTime(Date date, ZoneId tzId) {
if (date == null) {
return null;
}
ZonedDateTime zonedDate = ZonedDateTime.ofInstant(date.toInstant(), tzId);
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), TimeZone.getDefault().toZoneId());
zonedDate = zonedDate.withYear(localDateTime.getYear())
.withMonth(localDateTime.getMonthValue())
.withDayOfMonth(localDateTime.getDayOfMonth())
.withHour(localDateTime.getHour())
.withMinute(localDateTime.getMinute())
.withSecond(localDateTime.getSecond());
return zonedDate;
}
} }