Instance lease: Allow deployment of instances with lease duration and leaseexpiry action (#10560)

* FR-248: Instance lease, WIP commit

* insert lease expiry into db and use that to filter exiring vms, add asyncjobmanager

* Add leaseDuration and leaseExpiryAction in Service offering create flow

* Update listVM cmd to allow listing only leased instances

* Add methods to fetch instances for which lease is expiring in next days

* Changes included:
config key setup and configured for alert email
lease options in create and update vm screen
handle delete protection, edit vm, create vm
validated stop and detroy, delete protection

* Update UI screens for leased properties coming from config and service offering

* use global lock before running scheduler

* Unit tests

* Flow changes done in UI based on discussion

* Include view changes in schema upgrade files and use feature in various UI elements

* Added integration test for vm deployment, UI enhancements for user persona, bug fixes

* validate integration tests, minor ui changes and log messages

* fix build: moving configkey from setup to test itself

* Disable testAlert to unblock build and trim whitespaces in integration tests

* Address review comments

* Minor changes in EditVM screen

* Use ExecutorService instead of Timer and TimerTask

* Additional review comments

* Incorporate following changes:
1. Execute lease action once on the instance
2. Cancel lease on instance when feature is disabled
3. Relevant events when lease gets disabled, cancelled, executed
4. Disable associating lease after deployment
5. UI elements and flow changes
6. Changes based on feedback from demo

* Handle pr review comments

* address review comments

* move instance.lease.enabled config to VMLeaseManager interface

* bug fix in edit instance flow and reject api request for invalid values

* max allowed lease is for 100 years

* log instance ids for expired instance

* Fix config validation for value range and code coverage improvement

* fix lease expiry request failures in async

* dont use forced: true for StopVmCmd

* Update server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManager.java

Co-authored-by: Vishesh <vishesh92@gmail.com>

* handle review comments

---------

Co-authored-by: Rohit Yadav <rohityadav89@gmail.com>
Co-authored-by: Vishesh <vishesh92@gmail.com>
This commit is contained in:
Manoj Kumar 2025-05-28 17:40:09 +05:30 committed by GitHub
parent 650b5ec3da
commit 7632814cd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 2490 additions and 148 deletions

View File

@ -164,7 +164,8 @@ jobs:
component/test_cpu_limits component/test_cpu_limits
component/test_cpu_max_limits component/test_cpu_max_limits
component/test_cpu_project_limits component/test_cpu_project_limits
component/test_deploy_vm_userdata_multi_nic", component/test_deploy_vm_userdata_multi_nic
component/test_deploy_vm_lease",
"component/test_egress_fw_rules "component/test_egress_fw_rules
component/test_invalid_gw_nm component/test_invalid_gw_nm
component/test_ip_reservation", component/test_ip_reservation",

View File

@ -796,6 +796,11 @@ public class EventTypes {
// Resource Limit // Resource Limit
public static final String EVENT_RESOURCE_LIMIT_UPDATE = "RESOURCE.LIMIT.UPDATE"; public static final String EVENT_RESOURCE_LIMIT_UPDATE = "RESOURCE.LIMIT.UPDATE";
public static final String VM_LEASE_EXPIRED = "VM.LEASE.EXPIRED";
public static final String VM_LEASE_DISABLED = "VM.LEASE.DISABLED";
public static final String VM_LEASE_CANCELLED = "VM.LEASE.CANCELLED";
public static final String VM_LEASE_EXPIRING = "VM.LEASE.EXPIRING";
static { static {
// TODO: need a way to force author adding event types to declare the entity details as well, with out braking // TODO: need a way to force author adding event types to declare the entity details as well, with out braking
@ -1290,6 +1295,12 @@ public class EventTypes {
entityEventDetails.put(EVENT_SHAREDFS_DESTROY, SharedFS.class); entityEventDetails.put(EVENT_SHAREDFS_DESTROY, SharedFS.class);
entityEventDetails.put(EVENT_SHAREDFS_EXPUNGE, SharedFS.class); entityEventDetails.put(EVENT_SHAREDFS_EXPUNGE, SharedFS.class);
entityEventDetails.put(EVENT_SHAREDFS_RECOVER, SharedFS.class); entityEventDetails.put(EVENT_SHAREDFS_RECOVER, SharedFS.class);
// VM Lease
entityEventDetails.put(VM_LEASE_EXPIRED, VirtualMachine.class);
entityEventDetails.put(VM_LEASE_EXPIRING, VirtualMachine.class);
entityEventDetails.put(VM_LEASE_DISABLED, VirtualMachine.class);
entityEventDetails.put(VM_LEASE_CANCELLED, VirtualMachine.class);
} }
public static boolean isNetworkEvent(String eventType) { public static boolean isNetworkEvent(String eventType) {

View File

@ -110,4 +110,8 @@ public interface VmDetailConstants {
// CPU mode and model, ADMIN only // CPU mode and model, ADMIN only
String GUEST_CPU_MODE = "guest.cpu.mode"; String GUEST_CPU_MODE = "guest.cpu.mode";
String GUEST_CPU_MODEL = "guest.cpu.model"; String GUEST_CPU_MODEL = "guest.cpu.model";
String INSTANCE_LEASE_EXPIRY_DATE = "leaseexpirydate";
String INSTANCE_LEASE_EXPIRY_ACTION = "leaseexpiryaction";
String INSTANCE_LEASE_EXECUTION = "leaseactionexecution";
} }

View File

@ -269,7 +269,10 @@ public class ApiConstants {
public static final String INTERNAL_DNS2 = "internaldns2"; public static final String INTERNAL_DNS2 = "internaldns2";
public static final String INTERNET_PROTOCOL = "internetprotocol"; public static final String INTERNET_PROTOCOL = "internetprotocol";
public static final String INTERVAL_TYPE = "intervaltype"; public static final String INTERVAL_TYPE = "intervaltype";
public static final String LOCATION_TYPE = "locationtype"; public static final String INSTANCE_LEASE_DURATION = "leaseduration";
public static final String INSTANCE_LEASE_ENABLED = "instanceleaseenabled";
public static final String INSTANCE_LEASE_EXPIRY_ACTION = "leaseexpiryaction";
public static final String INSTANCE_LEASE_EXPIRY_DATE= "leaseexpirydate";
public static final String IOPS_READ_RATE = "iopsreadrate"; public static final String IOPS_READ_RATE = "iopsreadrate";
public static final String IOPS_READ_RATE_MAX = "iopsreadratemax"; public static final String IOPS_READ_RATE_MAX = "iopsreadratemax";
public static final String IOPS_READ_RATE_MAX_LENGTH = "iopsreadratemaxlength"; public static final String IOPS_READ_RATE_MAX_LENGTH = "iopsreadratemaxlength";
@ -317,11 +320,13 @@ public class ApiConstants {
public static final String LAST_BOOT = "lastboottime"; public static final String LAST_BOOT = "lastboottime";
public static final String LAST_SERVER_START = "lastserverstart"; public static final String LAST_SERVER_START = "lastserverstart";
public static final String LAST_SERVER_STOP = "lastserverstop"; public static final String LAST_SERVER_STOP = "lastserverstop";
public static final String LEASED = "leased";
public static final String LEVEL = "level"; public static final String LEVEL = "level";
public static final String LENGTH = "length"; public static final String LENGTH = "length";
public static final String LIMIT = "limit"; public static final String LIMIT = "limit";
public static final String LIMIT_CPU_USE = "limitcpuuse"; public static final String LIMIT_CPU_USE = "limitcpuuse";
public static final String LIST_HOSTS = "listhosts"; public static final String LIST_HOSTS = "listhosts";
public static final String LOCATION_TYPE = "locationtype";
public static final String LOCK = "lock"; public static final String LOCK = "lock";
public static final String LUN = "lun"; public static final String LUN = "lun";
public static final String LBID = "lbruleid"; public static final String LBID = "lbruleid";

View File

@ -34,7 +34,9 @@ import org.apache.cloudstack.api.response.ServiceOfferingResponse;
import org.apache.cloudstack.api.response.VsphereStoragePoliciesResponse; import org.apache.cloudstack.api.response.VsphereStoragePoliciesResponse;
import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.DiskOfferingResponse;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
@ -251,7 +253,15 @@ public class CreateServiceOfferingCmd extends BaseCmd {
since="4.20") since="4.20")
private Boolean purgeResources; private Boolean purgeResources;
@Parameter(name = ApiConstants.INSTANCE_LEASE_DURATION,
type = CommandType.INTEGER,
description = "Number of days instance is leased for.",
since = "4.21.0")
private Integer leaseDuration;
@Parameter(name = ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION, type = CommandType.STRING, since = "4.21.0",
description = "Lease expiry action, valid values are STOP and DESTROY")
private String leaseExpiryAction;
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////////// Accessors /////////////////////// /////////////////// Accessors ///////////////////////
@ -487,6 +497,22 @@ public class CreateServiceOfferingCmd extends BaseCmd {
return false; return false;
} }
public VMLeaseManager.ExpiryAction getLeaseExpiryAction() {
if (StringUtils.isBlank(leaseExpiryAction)) {
return null;
}
VMLeaseManager.ExpiryAction action = EnumUtils.getEnumIgnoreCase(VMLeaseManager.ExpiryAction.class, leaseExpiryAction);
if (action == null) {
throw new InvalidParameterValueException("Invalid value configured for leaseexpiryaction, valid values are: " +
com.cloud.utils.EnumUtils.listValues(VMLeaseManager.ExpiryAction.values()));
}
return action;
}
public Integer getLeaseDuration() {
return leaseDuration;
}
public boolean isPurgeResources() { public boolean isPurgeResources() {
return Boolean.TRUE.equals(purgeResources); return Boolean.TRUE.equals(purgeResources);
} }

View File

@ -72,6 +72,7 @@ public class ListCapabilitiesCmd extends BaseCmd {
response.setInstancesDisksStatsRetentionTime((Integer) capabilities.get(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME)); response.setInstancesDisksStatsRetentionTime((Integer) capabilities.get(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME));
response.setSharedFsVmMinCpuCount((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT)); response.setSharedFsVmMinCpuCount((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT));
response.setSharedFsVmMinRamSize((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE)); response.setSharedFsVmMinRamSize((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE));
response.setInstanceLeaseEnabled((Boolean) capabilities.get(ApiConstants.INSTANCE_LEASE_ENABLED));
response.setObjectName("capability"); response.setObjectName("capability");
response.setResponseName(getCommandName()); response.setResponseName(getCommandName());
this.setResponseObject(response); this.setResponseObject(response);

View File

@ -16,17 +16,24 @@
// under the License. // under the License.
package org.apache.cloudstack.api.command.user.vm; package org.apache.cloudstack.api.command.user.vm;
import java.util.ArrayList; import com.cloud.agent.api.LogLevel;
import java.util.Arrays; import com.cloud.event.EventTypes;
import java.util.Collection; import com.cloud.exception.ConcurrentOperationException;
import java.util.HashMap; import com.cloud.exception.InsufficientCapacityException;
import java.util.Iterator; import com.cloud.exception.InsufficientServerCapacityException;
import java.util.LinkedHashMap; import com.cloud.exception.InvalidParameterValueException;
import java.util.List; import com.cloud.exception.ResourceAllocationException;
import java.util.Map; import com.cloud.exception.ResourceUnavailableException;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import javax.annotation.Nonnull; import com.cloud.network.Network;
import com.cloud.network.Network.IpAddresses;
import com.cloud.offering.DiskOffering;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.uservm.UserVm;
import com.cloud.utils.net.Dhcp;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VmDetailConstants;
import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.affinity.AffinityGroupResponse;
import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.ACL;
@ -53,29 +60,22 @@ import org.apache.cloudstack.api.response.UserDataResponse;
import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.cloud.agent.api.LogLevel; import javax.annotation.Nonnull;
import com.cloud.event.EventTypes; import java.util.ArrayList;
import com.cloud.exception.ConcurrentOperationException; import java.util.Arrays;
import com.cloud.exception.InsufficientCapacityException; import java.util.Collection;
import com.cloud.exception.InsufficientServerCapacityException; import java.util.HashMap;
import com.cloud.exception.InvalidParameterValueException; import java.util.Iterator;
import com.cloud.exception.ResourceAllocationException; import java.util.LinkedHashMap;
import com.cloud.exception.ResourceUnavailableException; import java.util.List;
import com.cloud.hypervisor.Hypervisor.HypervisorType; import java.util.Map;
import com.cloud.network.Network;
import com.cloud.network.Network.IpAddresses;
import com.cloud.offering.DiskOffering;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.uservm.UserVm;
import com.cloud.utils.net.Dhcp;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VmDetailConstants;
@APICommand(name = "deployVirtualMachine", description = "Creates and automatically starts a virtual machine based on a service offering, disk offering, and template.", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, @APICommand(name = "deployVirtualMachine", description = "Creates and automatically starts a virtual machine based on a service offering, disk offering, and template.", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class},
requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) requestHasSensitiveInfo = false, responseHasSensitiveInfo = true)
@ -278,6 +278,14 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG
description = "Enable packed virtqueues or not.") description = "Enable packed virtqueues or not.")
private Boolean nicPackedVirtQueues; private Boolean nicPackedVirtQueues;
@Parameter(name = ApiConstants.INSTANCE_LEASE_DURATION, type = CommandType.INTEGER, since = "4.21.0",
description = "Number of days instance is leased for.")
private Integer leaseDuration;
@Parameter(name = ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION, type = CommandType.STRING, since = "4.21.0",
description = "Lease expiry action, valid values are STOP and DESTROY")
private String leaseExpiryAction;
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////////// Accessors /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -475,6 +483,22 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG
return password; return password;
} }
public Integer getLeaseDuration() {
return leaseDuration;
}
public VMLeaseManager.ExpiryAction getLeaseExpiryAction() {
if (StringUtils.isBlank(leaseExpiryAction)) {
return null;
}
VMLeaseManager.ExpiryAction action = EnumUtils.getEnumIgnoreCase(VMLeaseManager.ExpiryAction.class, leaseExpiryAction);
if (action == null) {
throw new InvalidParameterValueException("Invalid value configured for leaseexpiryaction, valid values are: " +
com.cloud.utils.EnumUtils.listValues(VMLeaseManager.ExpiryAction.values()));
}
return action;
}
public List<Long> getNetworkIds() { public List<Long> getNetworkIds() {
if (MapUtils.isNotEmpty(vAppNetworks)) { if (MapUtils.isNotEmpty(vAppNetworks)) {
if (CollectionUtils.isNotEmpty(networkIds) || ipAddress != null || getIp6Address() != null || MapUtils.isNotEmpty(ipToNetworkList)) { if (CollectionUtils.isNotEmpty(networkIds) || ipAddress != null || getIp6Address() != null || MapUtils.isNotEmpty(ipToNetworkList)) {

View File

@ -160,6 +160,11 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements
since = "4.20.1") since = "4.20.1")
private String arch; private String arch;
@Parameter(name = ApiConstants.LEASED, type = CommandType.BOOLEAN,
description = "Whether to return only leased instances",
since = "4.21.0")
private Boolean onlyLeasedInstances = false;
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////////// Accessors /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -303,6 +308,10 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements
return StringUtils.isBlank(arch) ? null : CPU.CPUArch.fromType(arch); return StringUtils.isBlank(arch) ? null : CPU.CPUArch.fromType(arch);
} }
public boolean getOnlyLeasedInstances() {
return BooleanUtils.toBoolean(onlyLeasedInstances);
}
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////// API Implementation/////////////////// /////////////// API Implementation///////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////

View File

@ -16,20 +16,19 @@
// under the License. // under the License.
package org.apache.cloudstack.api.command.user.vm; package org.apache.cloudstack.api.command.user.vm;
import java.util.Collection; import com.cloud.exception.InsufficientCapacityException;
import java.util.HashMap; import com.cloud.exception.InvalidParameterValueException;
import java.util.List; import com.cloud.exception.ResourceUnavailableException;
import java.util.Map; import com.cloud.user.Account;
import com.cloud.uservm.UserVm;
import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.Dhcp;
import org.apache.cloudstack.api.ApiArgValidator; import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.response.UserDataResponse;
import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiArgValidator;
import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ApiErrorCode;
@ -40,15 +39,17 @@ import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.GuestOSResponse; import org.apache.cloudstack.api.response.GuestOSResponse;
import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse;
import org.apache.cloudstack.api.response.UserDataResponse;
import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.exception.InsufficientCapacityException; import java.util.Collection;
import com.cloud.exception.ResourceUnavailableException; import java.util.HashMap;
import com.cloud.user.Account; import java.util.List;
import com.cloud.uservm.UserVm; import java.util.Map;
import com.cloud.utils.net.Dhcp;
import com.cloud.vm.VirtualMachine;
@APICommand(name = "updateVirtualMachine", description="Updates properties of a virtual machine. The VM has to be stopped and restarted for the " + @APICommand(name = "updateVirtualMachine", description="Updates properties of a virtual machine. The VM has to be stopped and restarted for the " +
"new properties to take effect. UpdateVirtualMachine does not first check whether the VM is stopped. " + "new properties to take effect. UpdateVirtualMachine does not first check whether the VM is stopped. " +
@ -154,6 +155,14 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction,
" autoscaling groups or CKS, delete protection will be ignored.") " autoscaling groups or CKS, delete protection will be ignored.")
private Boolean deleteProtection; private Boolean deleteProtection;
@Parameter(name = ApiConstants.INSTANCE_LEASE_DURATION, type = CommandType.INTEGER, since = "4.21.0",
description = "Number of days to lease the instance from now onward. Use -1 to remove the existing lease")
private Integer leaseDuration;
@Parameter(name = ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION, type = CommandType.STRING, since = "4.21.0",
description = "Lease expiry action, valid values are STOP and DESTROY")
private String leaseExpiryAction;
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////////// Accessors /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -324,4 +333,21 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction,
public ApiCommandResourceType getApiResourceType() { public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.VirtualMachine; return ApiCommandResourceType.VirtualMachine;
} }
public Integer getLeaseDuration() {
return leaseDuration;
}
public VMLeaseManager.ExpiryAction getLeaseExpiryAction() {
if (StringUtils.isBlank(leaseExpiryAction)) {
return null;
}
VMLeaseManager.ExpiryAction action = EnumUtils.getEnumIgnoreCase(VMLeaseManager.ExpiryAction.class, leaseExpiryAction);
if (action == null) {
throw new InvalidParameterValueException("Invalid value configured for leaseexpiryaction, valid values are: " +
com.cloud.utils.EnumUtils.listValues(VMLeaseManager.ExpiryAction.values()));
}
return action;
}
} }

View File

@ -136,6 +136,10 @@ public class CapabilitiesResponse extends BaseResponse {
@Param(description = "the min Ram size for the service offering used by the shared filesystem instance", since = "4.20.0") @Param(description = "the min Ram size for the service offering used by the shared filesystem instance", since = "4.20.0")
private Integer sharedFsVmMinRamSize; private Integer sharedFsVmMinRamSize;
@SerializedName(ApiConstants.INSTANCE_LEASE_ENABLED)
@Param(description = "true if instance lease feature is enabled", since = "4.21.0")
private Boolean instanceLeaseEnabled;
public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) { public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) {
this.securityGroupsEnabled = securityGroupsEnabled; this.securityGroupsEnabled = securityGroupsEnabled;
} }
@ -247,4 +251,8 @@ public class CapabilitiesResponse extends BaseResponse {
public void setSharedFsVmMinRamSize(Integer sharedFsVmMinRamSize) { public void setSharedFsVmMinRamSize(Integer sharedFsVmMinRamSize) {
this.sharedFsVmMinRamSize = sharedFsVmMinRamSize; this.sharedFsVmMinRamSize = sharedFsVmMinRamSize;
} }
public void setInstanceLeaseEnabled(Boolean instanceLeaseEnabled) {
this.instanceLeaseEnabled = instanceLeaseEnabled;
}
} }

View File

@ -238,6 +238,14 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations {
@Param(description = "Whether to cleanup VM and its associated resource upon expunge", since = "4.20") @Param(description = "Whether to cleanup VM and its associated resource upon expunge", since = "4.20")
private Boolean purgeResources; private Boolean purgeResources;
@SerializedName(ApiConstants.INSTANCE_LEASE_DURATION)
@Param(description = "Instance lease duration (in days) for service offering", since = "4.21.0")
private Integer leaseDuration;
@SerializedName(ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION)
@Param(description = "Action to be taken once lease is over", since = "4.21.0")
private String leaseExpiryAction;
public ServiceOfferingResponse() { public ServiceOfferingResponse() {
} }
@ -505,6 +513,22 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations {
this.cacheMode = cacheMode; this.cacheMode = cacheMode;
} }
public Integer getLeaseDuration() {
return leaseDuration;
}
public void setLeaseDuration(Integer leaseDuration) {
this.leaseDuration = leaseDuration;
}
public String getLeaseExpiryAction() {
return leaseExpiryAction;
}
public void setLeaseExpiryAction(String leaseExpiryAction) {
this.leaseExpiryAction = leaseExpiryAction;
}
public String getVsphereStoragePolicy() { public String getVsphereStoragePolicy() {
return vsphereStoragePolicy; return vsphereStoragePolicy;
} }

View File

@ -392,7 +392,7 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
@Param(description = "VNF details", since = "4.19.0") @Param(description = "VNF details", since = "4.19.0")
private Map<String, String> vnfDetails; private Map<String, String> vnfDetails;
@SerializedName((ApiConstants.VM_TYPE)) @SerializedName(ApiConstants.VM_TYPE)
@Param(description = "User VM type", since = "4.20.0") @Param(description = "User VM type", since = "4.20.0")
private String vmType; private String vmType;
@ -400,6 +400,18 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
@Param(description = "CPU arch of the VM", since = "4.20.1") @Param(description = "CPU arch of the VM", since = "4.20.1")
private String arch; private String arch;
@SerializedName(ApiConstants.INSTANCE_LEASE_DURATION)
@Param(description = "Instance lease duration in days", since = "4.21.0")
private Integer leaseDuration;
@SerializedName(ApiConstants.INSTANCE_LEASE_EXPIRY_DATE)
@Param(description = "Instance lease expiry date", since = "4.21.0")
private Date leaseExpiryDate;
@SerializedName(ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION)
@Param(description = "Instance lease expiry action", since = "4.21.0")
private String leaseExpiryAction;
public UserVmResponse() { public UserVmResponse() {
securityGroupList = new LinkedHashSet<>(); securityGroupList = new LinkedHashSet<>();
nics = new TreeSet<>(Comparator.comparingInt(x -> Integer.parseInt(x.getDeviceId()))); nics = new TreeSet<>(Comparator.comparingInt(x -> Integer.parseInt(x.getDeviceId())));
@ -1181,4 +1193,29 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
public void setArch(String arch) { public void setArch(String arch) {
this.arch = arch; this.arch = arch;
} }
public Integer getLeaseDuration() {
return leaseDuration;
}
public void setLeaseDuration(Integer leaseDuration) {
this.leaseDuration = leaseDuration;
}
public String getLeaseExpiryAction() {
return leaseExpiryAction;
}
public void setLeaseExpiryAction(String leaseExpiryAction) {
this.leaseExpiryAction = leaseExpiryAction;
}
public Date getLeaseExpiryDate() {
return leaseExpiryDate;
}
public void setLeaseExpiryDate(Date leaseExpiryDate) {
this.leaseExpiryDate = leaseExpiryDate;
}
} }

View File

@ -0,0 +1,61 @@
/*
* 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.lease;
import com.cloud.utils.component.Manager;
import org.apache.cloudstack.framework.config.ConfigKey;
import java.util.List;
public interface VMLeaseManager extends Manager {
int MAX_LEASE_DURATION_DAYS = 365_00; // 100 years
enum ExpiryAction {
STOP,
DESTROY
}
enum LeaseActionExecution {
PENDING,
DISABLED,
DONE,
CANCELLED
}
ConfigKey<Boolean> InstanceLeaseEnabled = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Boolean.class,
"instance.lease.enabled", "false", "Indicates whether to enable the Instance lease," +
" will be applicable only on instances created after lease is enabled. Disabling the feature cancels lease on existing instances with lease." +
" Re-enabling feature will not cause lease expiry actions on grandfathered instances",
true, List.of(ConfigKey.Scope.Global));
ConfigKey<Integer> InstanceLeaseSchedulerInterval = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Integer.class,
"instance.lease.scheduler.interval", "3600", "VM Lease Scheduler interval in seconds",
false, List.of(ConfigKey.Scope.Global));
ConfigKey<Integer> InstanceLeaseExpiryEventSchedulerInterval = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Integer.class,
"instance.lease.eventscheduler.interval", "86400", "Lease expiry event Scheduler interval in seconds",
false, List.of(ConfigKey.Scope.Global));
ConfigKey<Integer> InstanceLeaseExpiryEventDaysBefore = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Integer.class,
"instance.lease.expiryevent.daysbefore", "7", "Indicates how many days in advance, expiry events will be created before expiry.",
true, List.of(ConfigKey.Scope.Global));
void onLeaseFeatureToggle();
}

View File

@ -17,6 +17,8 @@
package org.apache.cloudstack.api.command.admin.offering; package org.apache.cloudstack.api.command.admin.offering;
import com.cloud.exception.InvalidParameterValueException;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -55,4 +57,25 @@ public class CreateServiceOfferingCmdTest {
ReflectionTestUtils.setField(createServiceOfferingCmd, "purgeResources", true); ReflectionTestUtils.setField(createServiceOfferingCmd, "purgeResources", true);
Assert.assertTrue(createServiceOfferingCmd.isPurgeResources()); Assert.assertTrue(createServiceOfferingCmd.isPurgeResources());
} }
@Test
public void testGetLeaseDuration() {
ReflectionTestUtils.setField(createServiceOfferingCmd, "leaseDuration", 10);
Assert.assertEquals(10, createServiceOfferingCmd.getLeaseDuration().longValue());
}
@Test
public void testGetLeaseExpiryAction() {
ReflectionTestUtils.setField(createServiceOfferingCmd, "leaseExpiryAction", "stop");
Assert.assertEquals(VMLeaseManager.ExpiryAction.STOP, createServiceOfferingCmd.getLeaseExpiryAction());
ReflectionTestUtils.setField(createServiceOfferingCmd, "leaseExpiryAction", "DESTROY");
Assert.assertEquals(VMLeaseManager.ExpiryAction.DESTROY, createServiceOfferingCmd.getLeaseExpiryAction());
}
@Test(expected = InvalidParameterValueException.class)
public void testGetLeaseExpiryActionInvalidValue() {
ReflectionTestUtils.setField(createServiceOfferingCmd, "leaseExpiryAction", "Unknown");
Assert.assertEquals(null, createServiceOfferingCmd.getLeaseExpiryAction());
}
} }

View File

@ -71,6 +71,8 @@ SELECT
`service_offering`.`dynamic_scaling_enabled` AS `dynamic_scaling_enabled`, `service_offering`.`dynamic_scaling_enabled` AS `dynamic_scaling_enabled`,
`service_offering`.`disk_offering_strictness` AS `disk_offering_strictness`, `service_offering`.`disk_offering_strictness` AS `disk_offering_strictness`,
`vsphere_storage_policy`.`value` AS `vsphere_storage_policy`, `vsphere_storage_policy`.`value` AS `vsphere_storage_policy`,
`lease_duration_details`.`value` AS `lease_duration`,
`lease_expiry_action_details`.`value` AS `lease_expiry_action`,
GROUP_CONCAT(DISTINCT(domain.id)) AS domain_id, GROUP_CONCAT(DISTINCT(domain.id)) AS domain_id,
GROUP_CONCAT(DISTINCT(domain.uuid)) AS domain_uuid, GROUP_CONCAT(DISTINCT(domain.uuid)) AS domain_uuid,
GROUP_CONCAT(DISTINCT(domain.name)) AS domain_name, GROUP_CONCAT(DISTINCT(domain.name)) AS domain_name,
@ -109,5 +111,11 @@ FROM
LEFT JOIN LEFT JOIN
`cloud`.`service_offering_details` AS `vsphere_storage_policy` ON `vsphere_storage_policy`.`service_offering_id` = `service_offering`.`id` `cloud`.`service_offering_details` AS `vsphere_storage_policy` ON `vsphere_storage_policy`.`service_offering_id` = `service_offering`.`id`
AND `vsphere_storage_policy`.`name` = 'storagepolicy' AND `vsphere_storage_policy`.`name` = 'storagepolicy'
LEFT JOIN
`cloud`.`service_offering_details` AS `lease_duration_details` ON `lease_duration_details`.`service_offering_id` = `service_offering`.`id`
AND `lease_duration_details`.`name` = 'leaseduration'
LEFT JOIN
`cloud`.`service_offering_details` AS `lease_expiry_action_details` ON `lease_expiry_action_details`.`service_offering_id` = `service_offering`.`id`
AND `lease_expiry_action_details`.`name` = 'leaseexpiryaction'
GROUP BY GROUP BY
`service_offering`.`id`; `service_offering`.`id`;

View File

@ -169,7 +169,10 @@ SELECT
`user_data`.`uuid` AS `user_data_uuid`, `user_data`.`uuid` AS `user_data_uuid`,
`user_data`.`name` AS `user_data_name`, `user_data`.`name` AS `user_data_name`,
`user_vm`.`user_data_details` AS `user_data_details`, `user_vm`.`user_data_details` AS `user_data_details`,
`vm_template`.`user_data_link_policy` AS `user_data_policy` `vm_template`.`user_data_link_policy` AS `user_data_policy`,
`lease_expiry_date`.`value` AS `lease_expiry_date`,
`lease_expiry_action`.`value` AS `lease_expiry_action`,
`lease_action_execution`.`value` AS `lease_action_execution`
FROM FROM
(((((((((((((((((((((((((((((((((((`user_vm` (((((((((((((((((((((((((((((((((((`user_vm`
JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`)
@ -216,4 +219,10 @@ FROM
LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`) LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`)
AND (`custom_speed`.`name` = 'CpuSpeed')))) AND (`custom_speed`.`name` = 'CpuSpeed'))))
LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`) LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`)
AND (`custom_ram_size`.`name` = 'memory')))); AND (`custom_ram_size`.`name` = 'memory')))
LEFT JOIN `user_vm_details` `lease_expiry_date` ON ((`lease_expiry_date`.`vm_id` = `vm_instance`.`id`)
AND (`lease_expiry_date`.`name` = 'leaseexpirydate'))
LEFT JOIN `user_vm_details` `lease_action_execution` ON ((`lease_action_execution`.`vm_id` = `vm_instance`.`id`)
AND (`lease_action_execution`.`name` = 'leaseactionexecution'))
LEFT JOIN `user_vm_details` `lease_expiry_action` ON (((`lease_expiry_action`.`vm_id` = `vm_instance`.`id`)
AND (`lease_expiry_action`.`name` = 'leaseexpiryaction'))));

View File

@ -174,6 +174,7 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.EnumUtils;
@ -1355,6 +1356,11 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
} }
} }
if (!VMLeaseManager.InstanceLeaseEnabled.value() && cmd.getOnlyLeasedInstances()) {
throw new InvalidParameterValueException(" Cannot list leased instances because the Instance Lease feature " +
"is disabled, please enable it to list leased instances");
}
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null);
accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false); accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false);
Long domainId = domainIdRecursiveListProject.first(); Long domainId = domainIdRecursiveListProject.first();
@ -1508,6 +1514,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
userVmSearchBuilder.join("tags", resourceTagSearch, resourceTagSearch.entity().getResourceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); userVmSearchBuilder.join("tags", resourceTagSearch, resourceTagSearch.entity().getResourceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER);
} }
if (cmd.getOnlyLeasedInstances()) {
SearchBuilder<UserVmDetailVO> leasedInstancesSearch = userVmDetailsDao.createSearchBuilder();
leasedInstancesSearch.and(leasedInstancesSearch.entity().getName(), SearchCriteria.Op.EQ).values(VmDetailConstants.INSTANCE_LEASE_EXECUTION);
leasedInstancesSearch.and(leasedInstancesSearch.entity().getValue(), SearchCriteria.Op.EQ).values(VMLeaseManager.LeaseActionExecution.PENDING.name());
userVmSearchBuilder.join("userVmToLeased", leasedInstancesSearch, leasedInstancesSearch.entity().getResourceId(),
userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER);
}
if (keyPairName != null) { if (keyPairName != null) {
SearchBuilder<UserVmDetailVO> vmDetailSearchKeys = userVmDetailsDao.createSearchBuilder(); SearchBuilder<UserVmDetailVO> vmDetailSearchKeys = userVmDetailsDao.createSearchBuilder();
SearchBuilder<UserVmDetailVO> vmDetailSearchVmIds = userVmDetailsDao.createSearchBuilder(); SearchBuilder<UserVmDetailVO> vmDetailSearchVmIds = userVmDetailsDao.createSearchBuilder();

View File

@ -33,6 +33,7 @@ import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -176,6 +177,11 @@ public class ServiceOfferingJoinDaoImpl extends GenericDaoBase<ServiceOfferingJo
} }
} }
if (VMLeaseManager.InstanceLeaseEnabled.value() && offering.getLeaseDuration() != null && offering.getLeaseDuration() > 0L) {
offeringResponse.setLeaseDuration(offering.getLeaseDuration());
offeringResponse.setLeaseExpiryAction(offering.getLeaseExpiryAction().name());
}
long rootDiskSizeInGb = (long) offering.getRootDiskSize() / GB_TO_BYTES; long rootDiskSizeInGb = (long) offering.getRootDiskSize() / GB_TO_BYTES;
offeringResponse.setRootDiskSize(rootDiskSizeInGb); offeringResponse.setRootDiskSize(rootDiskSizeInGb);
offeringResponse.setDiskOfferingStrictness(offering.getDiskOfferingStrictness()); offeringResponse.setDiskOfferingStrictness(offering.getDiskOfferingStrictness());

View File

@ -16,18 +16,17 @@
// under the License. // under the License.
package com.cloud.api.query.dao; package com.cloud.api.query.dao;
import java.util.List;
import java.util.Set;
import org.apache.cloudstack.api.ApiConstants.VMDetails;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.response.UserVmResponse;
import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.api.query.vo.UserVmJoinVO;
import com.cloud.user.Account; import com.cloud.user.Account;
import com.cloud.uservm.UserVm; import com.cloud.uservm.UserVm;
import com.cloud.utils.db.GenericDao; import com.cloud.utils.db.GenericDao;
import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.ApiConstants.VMDetails;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.response.UserVmResponse;
import java.util.List;
import java.util.Set;
public interface UserVmJoinDao extends GenericDao<UserVmJoinVO, Long> { public interface UserVmJoinDao extends GenericDao<UserVmJoinVO, Long> {
@ -46,4 +45,8 @@ public interface UserVmJoinDao extends GenericDao<UserVmJoinVO, Long> {
List<UserVmJoinVO> listByAccountServiceOfferingTemplateAndNotInState(long accountId, List<UserVmJoinVO> listByAccountServiceOfferingTemplateAndNotInState(long accountId,
List<VirtualMachine.State> states, List<Long> offeringIds, List<Long> templateIds); List<VirtualMachine.State> states, List<Long> offeringIds, List<Long> templateIds);
List<UserVmJoinVO> listEligibleInstancesWithExpiredLease();
List<UserVmJoinVO> listLeaseInstancesExpiringInDays(int days);
} }

View File

@ -16,38 +16,6 @@
// under the License. // under the License.
package com.cloud.api.query.dao; package com.cloud.api.query.dao;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.apache.cloudstack.affinity.AffinityGroupResponse;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiConstants.VMDetails;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.response.NicExtraDhcpOptionResponse;
import org.apache.cloudstack.api.response.NicResponse;
import org.apache.cloudstack.api.response.NicSecondaryIpResponse;
import org.apache.cloudstack.api.response.SecurityGroupResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VnfNicResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.query.QueryService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.cloud.api.ApiDBUtils; import com.cloud.api.ApiDBUtils;
import com.cloud.api.ApiResponseHelper; import com.cloud.api.ApiResponseHelper;
import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.api.query.vo.UserVmJoinVO;
@ -84,6 +52,42 @@ import com.cloud.vm.VmStats;
import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicExtraDhcpOptionDao;
import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.NicSecondaryIpVO;
import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.UserVmDetailsDao;
import org.apache.cloudstack.affinity.AffinityGroupResponse;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiConstants.VMDetails;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.response.NicExtraDhcpOptionResponse;
import org.apache.cloudstack.api.response.NicResponse;
import org.apache.cloudstack.api.response.NicSecondaryIpResponse;
import org.apache.cloudstack.api.response.SecurityGroupResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VnfNicResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.query.QueryService;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Component @Component
public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJoinVO, UserVmResponse> implements UserVmJoinDao { public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJoinVO, UserVmResponse> implements UserVmJoinDao {
@ -108,9 +112,13 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
VnfTemplateDetailsDao vnfTemplateDetailsDao; VnfTemplateDetailsDao vnfTemplateDetailsDao;
@Inject @Inject
VnfTemplateNicDao vnfTemplateNicDao; VnfTemplateNicDao vnfTemplateNicDao;
@Inject
ConfigurationDao configurationDao;
private final SearchBuilder<UserVmJoinVO> VmDetailSearch; private final SearchBuilder<UserVmJoinVO> VmDetailSearch;
private final SearchBuilder<UserVmJoinVO> activeVmByIsoSearch; private final SearchBuilder<UserVmJoinVO> activeVmByIsoSearch;
private final SearchBuilder<UserVmJoinVO> leaseExpiredInstanceSearch;
private final SearchBuilder<UserVmJoinVO> remainingLeaseInDaysSearch;
protected UserVmJoinDaoImpl() { protected UserVmJoinDaoImpl() {
@ -124,6 +132,29 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
activeVmByIsoSearch.and("isoId", activeVmByIsoSearch.entity().getIsoId(), SearchCriteria.Op.EQ); activeVmByIsoSearch.and("isoId", activeVmByIsoSearch.entity().getIsoId(), SearchCriteria.Op.EQ);
activeVmByIsoSearch.and("stateNotIn", activeVmByIsoSearch.entity().getState(), SearchCriteria.Op.NIN); activeVmByIsoSearch.and("stateNotIn", activeVmByIsoSearch.entity().getState(), SearchCriteria.Op.NIN);
activeVmByIsoSearch.done(); activeVmByIsoSearch.done();
leaseExpiredInstanceSearch = createSearchBuilder();
leaseExpiredInstanceSearch.selectFields(leaseExpiredInstanceSearch.entity().getId(), leaseExpiredInstanceSearch.entity().getState(),
leaseExpiredInstanceSearch.entity().isDeleteProtection(), leaseExpiredInstanceSearch.entity().getName(),
leaseExpiredInstanceSearch.entity().getUuid(), leaseExpiredInstanceSearch.entity().getLeaseExpiryAction());
leaseExpiredInstanceSearch.and(leaseExpiredInstanceSearch.entity().getLeaseActionExecution(), Op.EQ).values(VMLeaseManager.LeaseActionExecution.PENDING.name());
leaseExpiredInstanceSearch.and("leaseExpired", leaseExpiredInstanceSearch.entity().getLeaseExpiryDate(), Op.LT);
leaseExpiredInstanceSearch.and("leaseExpiryActions", leaseExpiredInstanceSearch.entity().getLeaseExpiryAction(), Op.IN);
leaseExpiredInstanceSearch.and("instanceStateNotIn", leaseExpiredInstanceSearch.entity().getState(), Op.NOTIN);
leaseExpiredInstanceSearch.done();
remainingLeaseInDaysSearch = createSearchBuilder();
remainingLeaseInDaysSearch.selectFields(remainingLeaseInDaysSearch.entity().getId(),
remainingLeaseInDaysSearch.entity().getUuid(), remainingLeaseInDaysSearch.entity().getName(),
remainingLeaseInDaysSearch.entity().getUserId(), remainingLeaseInDaysSearch.entity().getDomainId(),
remainingLeaseInDaysSearch.entity().getAccountId(), remainingLeaseInDaysSearch.entity().getLeaseExpiryAction());
remainingLeaseInDaysSearch.and(remainingLeaseInDaysSearch.entity().getLeaseActionExecution(), Op.EQ).values(VMLeaseManager.LeaseActionExecution.PENDING.name());
remainingLeaseInDaysSearch.and("leaseCurrentDate", remainingLeaseInDaysSearch.entity().getLeaseExpiryDate(), Op.GTEQ);
remainingLeaseInDaysSearch.and("leaseExpiryEndDate", remainingLeaseInDaysSearch.entity().getLeaseExpiryDate(), Op.LT);
remainingLeaseInDaysSearch.done();
} }
@Override @Override
@ -427,10 +458,10 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
userVmResponse.setDynamicallyScalable(userVm.isDynamicallyScalable()); userVmResponse.setDynamicallyScalable(userVm.isDynamicallyScalable());
} }
if (userVm.getDeleteProtection() == null) { if (userVm.isDeleteProtection() == null) {
userVmResponse.setDeleteProtection(false); userVmResponse.setDeleteProtection(false);
} else { } else {
userVmResponse.setDeleteProtection(userVm.getDeleteProtection()); userVmResponse.setDeleteProtection(userVm.isDeleteProtection());
} }
if (userVm.getAutoScaleVmGroupName() != null) { if (userVm.getAutoScaleVmGroupName() != null) {
@ -447,6 +478,15 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
userVmResponse.setUserDataPolicy(userVm.getUserDataPolicy()); userVmResponse.setUserDataPolicy(userVm.getUserDataPolicy());
} }
if (VMLeaseManager.InstanceLeaseEnabled.value() && userVm.getLeaseExpiryDate() != null &&
VMLeaseManager.LeaseActionExecution.PENDING.name().equals(userVm.getLeaseActionExecution())) {
userVmResponse.setLeaseExpiryAction(userVm.getLeaseExpiryAction());
userVmResponse.setLeaseExpiryDate(userVm.getLeaseExpiryDate());
int leaseDuration = (int) computeLeaseDurationFromExpiryDate(new Date(), userVm.getLeaseExpiryDate());
userVmResponse.setLeaseDuration(leaseDuration);
}
addVmRxTxDataToResponse(userVm, userVmResponse); addVmRxTxDataToResponse(userVm, userVmResponse);
if (TemplateType.VNF.equals(userVm.getTemplateType()) && (details.contains(VMDetails.all) || details.contains(VMDetails.vnfnics))) { if (TemplateType.VNF.equals(userVm.getTemplateType()) && (details.contains(VMDetails.all) || details.contains(VMDetails.vnfnics))) {
@ -456,6 +496,13 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
return userVmResponse; return userVmResponse;
} }
private long computeLeaseDurationFromExpiryDate(Date created, Date leaseExpiryDate) {
LocalDate createdDate = created.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate expiryDate = leaseExpiryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
return ChronoUnit.DAYS.between(createdDate, expiryDate);
}
private void addVnfInfoToserVmResponse(UserVmJoinVO userVm, UserVmResponse userVmResponse) { private void addVnfInfoToserVmResponse(UserVmJoinVO userVm, UserVmResponse userVmResponse) {
List<VnfTemplateNicVO> vnfNics = vnfTemplateNicDao.listByTemplateId(userVm.getTemplateId()); List<VnfTemplateNicVO> vnfNics = vnfTemplateNicDao.listByTemplateId(userVm.getTemplateId());
for (VnfTemplateNicVO nic : vnfNics) { for (VnfTemplateNicVO nic : vnfNics) {
@ -718,4 +765,43 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
sc.setParameters("displayVm", 1); sc.setParameters("displayVm", 1);
return customSearch(sc, null); return customSearch(sc, null);
} }
/**
* This method fetches instances where
* 1. lease has expired
* 2. leaseExpiryActions are valid, either STOP or DESTROY
* 3. instance State is eligible for expiry action
* @return list of instances, expiry action can be executed on
*/
@Override
public List<UserVmJoinVO> listEligibleInstancesWithExpiredLease() {
SearchCriteria<UserVmJoinVO> sc = leaseExpiredInstanceSearch.create();
sc.setParameters("leaseExpired", new Date());
sc.setParameters("leaseExpiryActions", VMLeaseManager.ExpiryAction.STOP.name(), VMLeaseManager.ExpiryAction.DESTROY.name());
sc.setParameters("instanceStateNotIn", State.Destroyed, State.Expunging, State.Error, State.Unknown, State.Migrating);
return listBy(sc);
}
/**
* This method will return instances which are expiring within days
* in case negative value is given, there won't be any endDate
*
* @param days
* @return
*/
@Override
public List<UserVmJoinVO> listLeaseInstancesExpiringInDays(int days) {
SearchCriteria<UserVmJoinVO> sc = remainingLeaseInDaysSearch.create();
Date currentDate = new Date();
sc.setParameters("leaseCurrentDate", currentDate);
if (days > 0) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(currentDate);
calendar.add(Calendar.DAY_OF_MONTH, days);
Date nextDate = calendar.getTime();
sc.setParameters("leaseExpiryEndDate", nextDate);
}
return listBy(sc);
}
} }

View File

@ -16,7 +16,12 @@
// under the License. // under the License.
package com.cloud.api.query.vo; package com.cloud.api.query.vo;
import java.util.Date; import com.cloud.offering.ServiceOffering.State;
import com.cloud.storage.Storage;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
@ -24,13 +29,7 @@ import javax.persistence.EnumType;
import javax.persistence.Enumerated; import javax.persistence.Enumerated;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.Date;
import com.cloud.offering.ServiceOffering.State;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import com.cloud.storage.Storage;
import com.cloud.utils.db.GenericDao;
@Entity @Entity
@Table(name = "service_offering_view") @Table(name = "service_offering_view")
@ -221,6 +220,13 @@ public class ServiceOfferingJoinVO extends BaseViewVO implements InternalIdentit
@Column(name = "encrypt_root") @Column(name = "encrypt_root")
private boolean encryptRoot; private boolean encryptRoot;
@Column(name = "lease_duration")
private Integer leaseDuration;
@Column(name = "lease_expiry_action")
@Enumerated(value = EnumType.STRING)
private VMLeaseManager.ExpiryAction leaseExpiryAction;
public ServiceOfferingJoinVO() { public ServiceOfferingJoinVO() {
} }
@ -459,4 +465,12 @@ public class ServiceOfferingJoinVO extends BaseViewVO implements InternalIdentit
} }
public boolean getEncryptRoot() { return encryptRoot; } public boolean getEncryptRoot() { return encryptRoot; }
public Integer getLeaseDuration() {
return leaseDuration;
}
public VMLeaseManager.ExpiryAction getLeaseExpiryAction() {
return leaseExpiryAction;
}
} }

View File

@ -28,6 +28,8 @@ import javax.persistence.EnumType;
import javax.persistence.Enumerated; import javax.persistence.Enumerated;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient; import javax.persistence.Transient;
import com.cloud.host.Status; import com.cloud.host.Status;
@ -442,6 +444,15 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
@Column(name = "arch") @Column(name = "arch")
protected String arch; protected String arch;
@Column(name = "lease_expiry_date")
@Temporal(value = TemporalType.TIMESTAMP)
private Date leaseExpiryDate;
@Column(name = "lease_expiry_action")
private String leaseExpiryAction;
@Column(name = "lease_action_execution")
private String leaseActionExecution;
public UserVmJoinVO() { public UserVmJoinVO() {
// Empty constructor // Empty constructor
@ -952,7 +963,7 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
return isDynamicallyScalable; return isDynamicallyScalable;
} }
public Boolean getDeleteProtection() { public Boolean isDeleteProtection() {
return deleteProtection; return deleteProtection;
} }
@ -984,4 +995,20 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
public String getArch() { public String getArch() {
return arch; return arch;
} }
public Date getLeaseExpiryDate() {
return leaseExpiryDate;
}
public String getLeaseExpiryAction() {
return leaseExpiryAction;
}
public void setLeaseExpiryAction(String leaseExpiryAction) {
this.leaseExpiryAction = leaseExpiryAction;
}
public String getLeaseActionExecution() {
return leaseActionExecution;
}
} }

View File

@ -137,6 +137,7 @@ import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import org.apache.cloudstack.vm.UnmanagedVMsManager; import org.apache.cloudstack.vm.UnmanagedVMsManager;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.EnumUtils;
@ -478,6 +479,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
NsxProviderDao nsxProviderDao; NsxProviderDao nsxProviderDao;
@Inject @Inject
ResourceManager resourceManager; ResourceManager resourceManager;
@Inject
VMLeaseManager vmLeaseManager;
// FIXME - why don't we have interface for DataCenterLinkLocalIpAddressDao? // FIXME - why don't we have interface for DataCenterLinkLocalIpAddressDao?
@Inject @Inject
@ -585,6 +588,9 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
configValuesForValidation.add(UserDataManager.VM_USERDATA_MAX_LENGTH_STRING); configValuesForValidation.add(UserDataManager.VM_USERDATA_MAX_LENGTH_STRING);
configValuesForValidation.add(UnmanagedVMsManager.RemoteKvmInstanceDisksCopyTimeout.key()); configValuesForValidation.add(UnmanagedVMsManager.RemoteKvmInstanceDisksCopyTimeout.key());
configValuesForValidation.add(UnmanagedVMsManager.ConvertVmwareInstanceToKvmTimeout.key()); configValuesForValidation.add(UnmanagedVMsManager.ConvertVmwareInstanceToKvmTimeout.key());
configValuesForValidation.add(VMLeaseManager.InstanceLeaseSchedulerInterval.key());
configValuesForValidation.add(VMLeaseManager.InstanceLeaseExpiryEventSchedulerInterval.key());
configValuesForValidation.add(VMLeaseManager.InstanceLeaseExpiryEventDaysBefore.key());
} }
protected void weightBasedParametersForValidation() { protected void weightBasedParametersForValidation() {
@ -638,6 +644,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
params.put(Config.RouterAggregationCommandEachTimeout.toString(), _configDao.getValue(Config.RouterAggregationCommandEachTimeout.toString())); params.put(Config.RouterAggregationCommandEachTimeout.toString(), _configDao.getValue(Config.RouterAggregationCommandEachTimeout.toString()));
params.put(Config.MigrateWait.toString(), _configDao.getValue(Config.MigrateWait.toString())); params.put(Config.MigrateWait.toString(), _configDao.getValue(Config.MigrateWait.toString()));
_agentManager.propagateChangeToAgents(params); _agentManager.propagateChangeToAgents(params);
} else if (VMLeaseManager.InstanceLeaseEnabled.key().equals(globalSettingUpdated)) {
vmLeaseManager.onLeaseFeatureToggle();
} }
} }
}); });
@ -3367,6 +3375,10 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
} }
} }
// validate lease properties and set leaseExpiryAction
Integer leaseDuration = cmd.getLeaseDuration();
VMLeaseManager.ExpiryAction leaseExpiryAction = validateAndGetLeaseExpiryAction(leaseDuration, cmd.getLeaseExpiryAction());
return createServiceOffering(userId, cmd.isSystem(), vmType, cmd.getServiceOfferingName(), cpuNumber, memory, cpuSpeed, cmd.getDisplayText(), return createServiceOffering(userId, cmd.isSystem(), vmType, cmd.getServiceOfferingName(), cpuNumber, memory, cpuSpeed, cmd.getDisplayText(),
cmd.getProvisioningType(), localStorageRequired, offerHA, limitCpuUse, volatileVm, cmd.getTags(), cmd.getDomainIds(), cmd.getZoneIds(), cmd.getHostTag(), cmd.getProvisioningType(), localStorageRequired, offerHA, limitCpuUse, volatileVm, cmd.getTags(), cmd.getDomainIds(), cmd.getZoneIds(), cmd.getHostTag(),
cmd.getNetworkRate(), cmd.getDeploymentPlanner(), details, cmd.getRootDiskSize(), isCustomizedIops, cmd.getMinIops(), cmd.getMaxIops(), cmd.getNetworkRate(), cmd.getDeploymentPlanner(), details, cmd.getRootDiskSize(), isCustomizedIops, cmd.getMinIops(), cmd.getMaxIops(),
@ -3375,20 +3387,20 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
cmd.getIopsReadRate(), cmd.getIopsReadRateMax(), cmd.getIopsReadRateMaxLength(), cmd.getIopsReadRate(), cmd.getIopsReadRateMax(), cmd.getIopsReadRateMaxLength(),
cmd.getIopsWriteRate(), cmd.getIopsWriteRateMax(), cmd.getIopsWriteRateMaxLength(), cmd.getIopsWriteRate(), cmd.getIopsWriteRateMax(), cmd.getIopsWriteRateMaxLength(),
cmd.getHypervisorSnapshotReserve(), cmd.getCacheMode(), storagePolicyId, cmd.getDynamicScalingEnabled(), diskOfferingId, cmd.getHypervisorSnapshotReserve(), cmd.getCacheMode(), storagePolicyId, cmd.getDynamicScalingEnabled(), diskOfferingId,
cmd.getDiskOfferingStrictness(), cmd.isCustomized(), cmd.getEncryptRoot(), cmd.isPurgeResources()); cmd.getDiskOfferingStrictness(), cmd.isCustomized(), cmd.getEncryptRoot(), cmd.isPurgeResources(), leaseDuration, leaseExpiryAction);
} }
protected ServiceOfferingVO createServiceOffering(final long userId, final boolean isSystem, final VirtualMachine.Type vmType, protected ServiceOfferingVO createServiceOffering(final long userId, final boolean isSystem, final VirtualMachine.Type vmType,
final String name, final Integer cpu, final Integer ramSize, final Integer speed, final String displayText, final String provisioningType, final boolean localStorageRequired, final String name, final Integer cpu, final Integer ramSize, final Integer speed, final String displayText, final String provisioningType, final boolean localStorageRequired,
final boolean offerHA, final boolean limitResourceUse, final boolean volatileVm, String tags, final List<Long> domainIds, List<Long> zoneIds, final String hostTag, final boolean offerHA, final boolean limitResourceUse, final boolean volatileVm, String tags, final List<Long> domainIds, List<Long> zoneIds, final String hostTag,
final Integer networkRate, final String deploymentPlanner, final Map<String, String> details, Long rootDiskSizeInGiB, final Boolean isCustomizedIops, Long minIops, Long maxIops, final Integer networkRate, final String deploymentPlanner, final Map<String, String> details, Long rootDiskSizeInGiB, final Boolean isCustomizedIops, Long minIops, Long maxIops,
Long bytesReadRate, Long bytesReadRateMax, Long bytesReadRateMaxLength, Long bytesReadRate, Long bytesReadRateMax, Long bytesReadRateMaxLength,
Long bytesWriteRate, Long bytesWriteRateMax, Long bytesWriteRateMaxLength, Long bytesWriteRate, Long bytesWriteRateMax, Long bytesWriteRateMaxLength,
Long iopsReadRate, Long iopsReadRateMax, Long iopsReadRateMaxLength, Long iopsReadRate, Long iopsReadRateMax, Long iopsReadRateMaxLength,
Long iopsWriteRate, Long iopsWriteRateMax, Long iopsWriteRateMaxLength, Long iopsWriteRate, Long iopsWriteRateMax, Long iopsWriteRateMaxLength,
final Integer hypervisorSnapshotReserve, String cacheMode, final Long storagePolicyID, final Integer hypervisorSnapshotReserve, String cacheMode, final Long storagePolicyID,
final boolean dynamicScalingEnabled, final Long diskOfferingId, final boolean diskOfferingStrictness, final boolean dynamicScalingEnabled, final Long diskOfferingId, final boolean diskOfferingStrictness,
final boolean isCustomized, final boolean encryptRoot, final boolean purgeResources) { final boolean isCustomized, final boolean encryptRoot, final boolean purgeResources, Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction) {
// Filter child domains when both parent and child domains are present // Filter child domains when both parent and child domains are present
List<Long> filteredDomainIds = filterChildSubDomains(domainIds); List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
@ -3495,6 +3507,12 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
} }
if ((serviceOffering = _serviceOfferingDao.persist(serviceOffering)) != null) { if ((serviceOffering = _serviceOfferingDao.persist(serviceOffering)) != null) {
//persist lease properties if leaseExpiryAction is valid
if (leaseExpiryAction != null) {
detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.INSTANCE_LEASE_DURATION, String.valueOf(leaseDuration), false));
detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION, leaseExpiryAction.name(), false));
}
for (Long domainId : filteredDomainIds) { for (Long domainId : filteredDomainIds) {
detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false));
} }
@ -3518,6 +3536,31 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
} }
} }
/**
* This method will return valid and non-empty expiryAction when
* "instance.lease.enabled" feature is enabled at global level
* leaseDuration is positive > 0 and has valid leaseExpiryAction provided
* @param leaseDuration
* @param cmdExpiryAction
* @return leaseExpiryAction
*/
public static VMLeaseManager.ExpiryAction validateAndGetLeaseExpiryAction(Integer leaseDuration, VMLeaseManager.ExpiryAction cmdExpiryAction) {
if (!VMLeaseManager.InstanceLeaseEnabled.value() || ObjectUtils.allNull(leaseDuration, cmdExpiryAction)) { // both are null
return null;
}
// one of them is non-null
if (ObjectUtils.anyNull(leaseDuration, cmdExpiryAction)) {
throw new InvalidParameterValueException("Provide values for both: leaseduration and leaseexpiryaction");
}
if (leaseDuration < 1L || leaseDuration > VMLeaseManager.MAX_LEASE_DURATION_DAYS) {
throw new InvalidParameterValueException("Invalid leaseduration: must be a natural number (>=1), max supported value is 36500");
}
return cmdExpiryAction;
}
@Override @Override
public void validateExtraConfigInServiceOfferingDetail(String detailName) { public void validateExtraConfigInServiceOfferingDetail(String detailName) {
if (!detailName.equals(DpdkHelper.DPDK_NUMA) && !detailName.equals(DpdkHelper.DPDK_HUGE_PAGES) if (!detailName.equals(DpdkHelper.DPDK_NUMA) && !detailName.equals(DpdkHelper.DPDK_HUGE_PAGES)

View File

@ -640,6 +640,7 @@ import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO;
import org.apache.cloudstack.userdata.UserDataManager; import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.cloudstack.utils.CloudStackVersion; import org.apache.cloudstack.utils.CloudStackVersion;
import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
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.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -4558,6 +4559,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
capabilities.put(ApiConstants.INSTANCES_STATS_USER_ONLY, StatsCollector.vmStatsCollectUserVMOnly.value()); capabilities.put(ApiConstants.INSTANCES_STATS_USER_ONLY, StatsCollector.vmStatsCollectUserVMOnly.value());
capabilities.put(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_ENABLED, StatsCollector.vmDiskStatsRetentionEnabled.value()); capabilities.put(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_ENABLED, StatsCollector.vmDiskStatsRetentionEnabled.value());
capabilities.put(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME, StatsCollector.vmDiskStatsMaxRetentionTime.value()); capabilities.put(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME, StatsCollector.vmDiskStatsMaxRetentionTime.value());
capabilities.put(ApiConstants.INSTANCE_LEASE_ENABLED, VMLeaseManager.InstanceLeaseEnabled.value());
if (apiLimitEnabled) { if (apiLimitEnabled) {
capabilities.put("apiLimitInterval", apiLimitInterval); capabilities.put("apiLimitInterval", apiLimitInterval);
capabilities.put("apiLimitMax", apiLimitMax); capabilities.put("apiLimitMax", apiLimitMax);

View File

@ -26,6 +26,9 @@ import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -39,6 +42,7 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TimeZone;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -135,11 +139,13 @@ import org.apache.cloudstack.storage.template.VnfTemplateManager;
import org.apache.cloudstack.userdata.UserDataManager; import org.apache.cloudstack.userdata.UserDataManager;
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.lease.VMLeaseManager;
import org.apache.cloudstack.vm.schedule.VMScheduleManager; import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.vm.UnmanagedVMsManager; import org.apache.cloudstack.vm.UnmanagedVMsManager;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
@ -2869,6 +2875,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} }
} else { } else {
if (MapUtils.isNotEmpty(details)) { if (MapUtils.isNotEmpty(details)) {
// error out if lease related keys are passed in details
if (details.containsKey(VmDetailConstants.INSTANCE_LEASE_EXECUTION)
|| details.containsKey(VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE)
|| details.containsKey(VmDetailConstants.INSTANCE_LEASE_EXPIRY_ACTION)) {
throw new InvalidParameterValueException("lease parameters should not be included in details as key");
}
if (details.containsKey("extraconfig")) { if (details.containsKey("extraconfig")) {
throw new InvalidParameterValueException("'extraconfig' should not be included in details as key"); throw new InvalidParameterValueException("'extraconfig' should not be included in details as key");
} }
@ -2916,6 +2929,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} }
} }
} }
if (VMLeaseManager.InstanceLeaseEnabled.value() && cmd.getLeaseDuration() != null) {
applyLeaseOnUpdateInstance(vmInstance, cmd.getLeaseDuration(), cmd.getLeaseExpiryAction());
}
return updateVirtualMachine(id, displayName, group, ha, isDisplayVm, return updateVirtualMachine(id, displayName, group, ha, isDisplayVm,
cmd.getDeleteProtection(), osTypeId, userData, cmd.getDeleteProtection(), osTypeId, userData,
userDataId, userDataDetails, isDynamicallyScalable, cmd.getHttpMethod(), userDataId, userDataDetails, isDynamicallyScalable, cmd.getHttpMethod(),
@ -6169,6 +6187,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} }
} }
boolean isLeaseFeatureEnabled = VMLeaseManager.InstanceLeaseEnabled.value();
if (isLeaseFeatureEnabled) {
validateLeaseProperties(cmd.getLeaseDuration(), cmd.getLeaseExpiryAction());
}
List<Long> networkIds = cmd.getNetworkIds(); List<Long> networkIds = cmd.getNetworkIds();
LinkedHashMap<Integer, Long> userVmNetworkMap = getVmOvfNetworkMapping(zone, owner, template, cmd.getVmNetworkMap()); LinkedHashMap<Integer, Long> userVmNetworkMap = getVmOvfNetworkMapping(zone, owner, template, cmd.getVmNetworkMap());
if (MapUtils.isNotEmpty(userVmNetworkMap)) { if (MapUtils.isNotEmpty(userVmNetworkMap)) {
@ -6267,9 +6290,117 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} }
} }
} }
if (isLeaseFeatureEnabled) {
applyLeaseOnCreateInstance(vm, cmd.getLeaseDuration(), cmd.getLeaseExpiryAction(), svcOffering);
}
return vm; return vm;
} }
protected void validateLeaseProperties(Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction) {
if (ObjectUtils.allNull(leaseDuration, leaseExpiryAction) // both are null
|| (leaseDuration != null && leaseDuration == -1)) { // special condition to disable lease for instance
return;
}
// any one of them have value
// validate leaseduration
if (leaseDuration == null || leaseDuration < 1 || leaseDuration > VMLeaseManager.MAX_LEASE_DURATION_DAYS) {
throw new InvalidParameterValueException("Invalid leaseduration: must be a natural number (>=1) or -1, max supported value is 36500");
}
if (leaseExpiryAction == null) {
throw new InvalidParameterValueException("Provide values for both: leaseduration and leaseexpiryaction");
}
}
/**
* if lease feature is enabled
* use leaseDuration and leaseExpiryAction passed in the cmd
* get leaseDuration from service_offering if leaseDuration is not passed
* @param vm
* @param leaseDuration
* @param leaseExpiryAction
* @param serviceOfferingJoinVO
*/
void applyLeaseOnCreateInstance(UserVm vm, Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction, ServiceOfferingJoinVO serviceOfferingJoinVO) {
if (leaseDuration == null) {
leaseDuration = serviceOfferingJoinVO.getLeaseDuration();
}
// if leaseDuration is null or < 1, instance will never expire, nothing to be done
if (leaseDuration == null || leaseDuration < 1) {
return;
}
leaseExpiryAction = leaseExpiryAction != null ? leaseExpiryAction : serviceOfferingJoinVO.getLeaseExpiryAction();
if (leaseExpiryAction == null) {
return;
}
addLeaseDetailsForInstance(vm, leaseDuration, leaseExpiryAction);
}
protected void applyLeaseOnUpdateInstance(UserVm instance, Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction) {
validateLeaseProperties(leaseDuration, leaseExpiryAction);
String instanceUuid = instance.getUuid();
// vm must have active lease associated during deployment
Map<String, String> vmDetails = userVmDetailsDao.listDetailsKeyPairs(instance.getId(),
List.of(VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE, VmDetailConstants.INSTANCE_LEASE_EXECUTION));
String leaseExecution = vmDetails.get(VmDetailConstants.INSTANCE_LEASE_EXECUTION);
String leaseExpiryDate = vmDetails.get(VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE);
if (StringUtils.isEmpty(leaseExpiryDate)) {
String errorMsg = "Lease can't be applied on instance with id: " + instanceUuid + ", it doesn't have lease associated during deployment";
logger.debug(errorMsg);
throw new CloudRuntimeException(errorMsg);
}
if (!VMLeaseManager.LeaseActionExecution.PENDING.name().equals(leaseExecution)) {
String errorMsg = "Lease can't be applied on instance with id: " + instanceUuid + ", it doesn't have active lease";
logger.debug(errorMsg);
throw new CloudRuntimeException(errorMsg);
}
// proceed if lease is yet to expire
long leaseExpiryTimeDiff;
try {
leaseExpiryTimeDiff = DateUtil.getTimeDifference(
DateUtil.parseDateString(TimeZone.getTimeZone("UTC"), leaseExpiryDate), new Date());
} catch (Exception ex) {
logger.error("Error occurred computing time difference for instance lease expiry, " +
"will skip applying lease for vm with id: {}", instanceUuid, ex);
return;
}
if (leaseExpiryTimeDiff < 0) {
logger.debug("Lease has expired for instance with id: {}, can't modify lease information", instanceUuid);
throw new CloudRuntimeException("Lease is not allowed to be redefined on expired leased instance");
}
if (leaseDuration < 1) {
userVmDetailsDao.addDetail(instance.getId(), VmDetailConstants.INSTANCE_LEASE_EXECUTION,
VMLeaseManager.LeaseActionExecution.DISABLED.name(), false);
ActionEventUtils.onActionEvent(CallContext.current().getCallingUserId(), instance.getAccountId(), instance.getDomainId(),
EventTypes.VM_LEASE_DISABLED, "Disabling lease on the instance", instance.getId(), ApiCommandResourceType.VirtualMachine.toString());
return;
}
addLeaseDetailsForInstance(instance, leaseDuration, leaseExpiryAction);
}
protected void addLeaseDetailsForInstance(UserVm vm, Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction) {
if (ObjectUtils.anyNull(vm, leaseDuration) || leaseDuration < 1) {
logger.debug("Lease can't be applied for given vm: {}, leaseduration: {} and leaseexpiryaction: {}", vm, leaseDuration, leaseExpiryAction);
return;
}
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
LocalDateTime leaseExpiryDateTime = now.plusDays(leaseDuration);
Date leaseExpiryDate = Date.from(leaseExpiryDateTime.atZone(ZoneOffset.UTC).toInstant());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String formattedLeaseExpiryDate = sdf.format(leaseExpiryDate);
userVmDetailsDao.addDetail(vm.getId(), VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE, formattedLeaseExpiryDate, false);
userVmDetailsDao.addDetail(vm.getId(), VmDetailConstants.INSTANCE_LEASE_EXPIRY_ACTION, leaseExpiryAction.name(), false);
userVmDetailsDao.addDetail(vm.getId(), VmDetailConstants.INSTANCE_LEASE_EXECUTION, "PENDING", false);
logger.debug("Instance lease for instanceId: {} is configured to expire on: {} with action: {}", vm.getUuid(), formattedLeaseExpiryDate, leaseExpiryAction);
}
/** /**
* Persist extra configuration data in the user_vm_details table as key/value pair * Persist extra configuration data in the user_vm_details table as key/value pair
* @param decodedUrl String consisting of the extra config data to appended onto the vmx file for VMware instances * @param decodedUrl String consisting of the extra config data to appended onto the vmx file for VMware instances

View File

@ -0,0 +1,381 @@
/*
* 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.lease;
import com.cloud.api.ApiGsonHelper;
import com.cloud.api.query.dao.UserVmJoinDao;
import com.cloud.api.query.vo.UserVmJoinVO;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes;
import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.utils.DateUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.GlobalLock;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.UserVmDetailsDao;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd;
import org.apache.cloudstack.api.command.user.vm.StopVMCmd;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.jobs.AsyncJob;
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.framework.messagebus.MessageBus;
import org.apache.cloudstack.framework.messagebus.MessageSubscriber;
import org.apache.cloudstack.jobs.JobInfo;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.time.DateUtils;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class VMLeaseManagerImpl extends ManagerBase implements VMLeaseManager, Configurable {
private static final int ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION = 5; // 5 seconds
@Inject
private UserVmDetailsDao userVmDetailsDao;
@Inject
private UserVmJoinDao userVmJoinDao;
@Inject
private AsyncJobManager asyncJobManager;
@Inject
private MessageBus messageBus;
private AsyncJobDispatcher asyncJobDispatcher;
ScheduledExecutorService vmLeaseExecutor;
ScheduledExecutorService vmLeaseExpiryEventExecutor;
Gson gson = ApiGsonHelper.getBuilder().create();
VMLeaseManagerSubscriber leaseManagerSubscriber;
public static final String JOB_INITIATOR = "jobInitiator";
@Override
public String getConfigComponentName() {
return VMLeaseManager.class.getSimpleName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey[]{
InstanceLeaseEnabled,
InstanceLeaseSchedulerInterval,
InstanceLeaseExpiryEventSchedulerInterval,
InstanceLeaseExpiryEventDaysBefore
};
}
public void setAsyncJobDispatcher(final AsyncJobDispatcher dispatcher) {
asyncJobDispatcher = dispatcher;
}
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
if (InstanceLeaseEnabled.value()) {
scheduleLeaseExecutors();
}
return true;
}
@Override
public boolean stop() {
shutDownLeaseExecutors();
return true;
}
/**
* This method will cancel lease on instances running under lease
* will be primarily used when feature gets disabled
*/
public void cancelLeaseOnExistingInstances() {
List<UserVmJoinVO> leaseExpiringForInstances = userVmJoinDao.listLeaseInstancesExpiringInDays(-1);
logger.debug("Total instances found for lease cancellation: {}", leaseExpiringForInstances.size());
for (UserVmJoinVO instance : leaseExpiringForInstances) {
userVmDetailsDao.addDetail(instance.getId(), VmDetailConstants.INSTANCE_LEASE_EXECUTION,
LeaseActionExecution.CANCELLED.name(), false);
String leaseCancellationMsg = String.format("Lease is cancelled for the instance: %s (id: %s) ", instance.getName(), instance.getUuid());
ActionEventUtils.onActionEvent(instance.getUserId(), instance.getAccountId(), instance.getDomainId(),
EventTypes.VM_LEASE_CANCELLED, leaseCancellationMsg, instance.getId(), ApiCommandResourceType.VirtualMachine.toString());
}
}
@Override
public void onLeaseFeatureToggle() {
boolean isLeaseFeatureEnabled = VMLeaseManager.InstanceLeaseEnabled.value();
if (isLeaseFeatureEnabled) {
scheduleLeaseExecutors();
} else {
cancelLeaseOnExistingInstances();
shutDownLeaseExecutors();
}
}
private void scheduleLeaseExecutors() {
if (vmLeaseExecutor == null || vmLeaseExecutor.isShutdown()) {
logger.debug("Scheduling lease executor");
vmLeaseExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("VMLeasePollExecutor"));
vmLeaseExecutor.scheduleAtFixedRate(new VMLeaseSchedulerTask(),5L, InstanceLeaseSchedulerInterval.value(), TimeUnit.SECONDS);
}
if (vmLeaseExpiryEventExecutor == null || vmLeaseExpiryEventExecutor.isShutdown()) {
logger.debug("Scheduling lease expiry event executor");
vmLeaseExpiryEventExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("VmLeaseExpiryEventExecutor"));
vmLeaseExpiryEventExecutor.scheduleAtFixedRate(new VMLeaseExpiryEventSchedulerTask(), 5L, InstanceLeaseExpiryEventSchedulerInterval.value(), TimeUnit.SECONDS);
}
addLeaseExpiryListener();
}
private void shutDownLeaseExecutors() {
if (vmLeaseExecutor != null) {
logger.debug("Shutting down lease executor");
vmLeaseExecutor.shutdown();
vmLeaseExecutor = null;
}
if (vmLeaseExpiryEventExecutor != null) {
logger.debug("Shutting down lease expiry event executor");
vmLeaseExpiryEventExecutor.shutdown();
vmLeaseExpiryEventExecutor = null;
}
removeLeaseExpiryListener();
}
class VMLeaseSchedulerTask extends ManagedContextRunnable {
@Override
protected void runInContext() {
Date currentTimestamp = DateUtils.round(new Date(), Calendar.MINUTE);
String displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, currentTimestamp);
logger.debug("VMLeaseSchedulerTask is being called at {}", displayTime);
if (!InstanceLeaseEnabled.value()) {
logger.debug("Instance lease feature is disabled, no action is required");
return;
}
GlobalLock scanLock = GlobalLock.getInternLock("VMLeaseSchedulerTask");
try {
if (scanLock.lock(ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION)) {
try {
reallyRun();
} finally {
scanLock.unlock();
}
}
} finally {
scanLock.releaseRef();
}
}
}
class VMLeaseExpiryEventSchedulerTask extends ManagedContextRunnable {
@Override
protected void runInContext() {
logger.debug("VMLeaseExpiryEventSchedulerTask is being called");
// as feature is disabled, no action is required
if (!InstanceLeaseEnabled.value()) {
return;
}
GlobalLock scanLock = GlobalLock.getInternLock("VMLeaseExpiryEventSchedulerTask");
try {
if (scanLock.lock(ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION)) {
try {
List<UserVmJoinVO> leaseExpiringForInstances = userVmJoinDao.listLeaseInstancesExpiringInDays(InstanceLeaseExpiryEventDaysBefore.value());
for (UserVmJoinVO instance : leaseExpiringForInstances) {
String leaseExpiryEventMsg = String.format("Lease expiring for for instance: %s (id: %s) with action: %s",
instance.getName(), instance.getUuid(), instance.getLeaseExpiryAction());
ActionEventUtils.onActionEvent(instance.getUserId(), instance.getAccountId(), instance.getDomainId(),
EventTypes.VM_LEASE_EXPIRING, leaseExpiryEventMsg, instance.getId(), ApiCommandResourceType.VirtualMachine.toString());
}
} finally {
scanLock.unlock();
}
}
} finally {
scanLock.releaseRef();
}
}
}
protected void reallyRun() {
// fetch user_instances having leaseDuration configured and has expired
List<UserVmJoinVO> leaseExpiredInstances = userVmJoinDao.listEligibleInstancesWithExpiredLease();
Set<Long> actionableInstanceIds = new HashSet<>();
for (UserVmJoinVO userVmVO : leaseExpiredInstances) {
// skip instance with delete protection for DESTROY action
if (ExpiryAction.DESTROY.name().equals(userVmVO.getLeaseExpiryAction())
&& userVmVO.isDeleteProtection() != null && userVmVO.isDeleteProtection()) {
logger.debug("Ignoring DESTROY action on instance: {} (id: {}) as deleteProtection is enabled", userVmVO.getName(), userVmVO.getUuid());
continue;
}
actionableInstanceIds.add(userVmVO.getId());
}
if (actionableInstanceIds.isEmpty()) {
logger.debug("Lease scheduler found no instance to work upon");
return;
}
List<Long> submittedJobIds = new ArrayList<>();
List<Long> successfulInstanceIds = new ArrayList<>();
List<Long> failedToSubmitInstanceIds = new ArrayList<>();
for (Long instanceId : actionableInstanceIds) {
UserVmJoinVO instance = userVmJoinDao.findById(instanceId);
ExpiryAction expiryAction = getLeaseExpiryAction(instance);
if (expiryAction == null) {
continue;
}
// for qualified vms, prepare Stop/Destroy(Cmd) and submit to Job Manager
final long eventId = ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, instance.getAccountId(), null,
EventTypes.VM_LEASE_EXPIRED, true,
String.format("Executing lease expiry action (%s) for instance: %s (id: %s)", instance.getLeaseExpiryAction(), instance.getName(), instance.getUuid()),
instance.getId(), ApiCommandResourceType.VirtualMachine.toString(), 0);
Long jobId = executeExpiryAction(instance, expiryAction, eventId);
if (jobId != null) {
submittedJobIds.add(jobId);
successfulInstanceIds.add(instanceId);
} else {
failedToSubmitInstanceIds.add(instanceId);
}
}
logger.debug("Successfully submitted lease expiry jobs with ids: {} and instance ids: {}", submittedJobIds, successfulInstanceIds);
if (!failedToSubmitInstanceIds.isEmpty()) {
logger.debug("Lease scheduler failed to submit jobs for instance ids: {}", failedToSubmitInstanceIds);
}
}
Long executeExpiryAction(UserVmJoinVO instance, ExpiryAction expiryAction, long eventId) {
// for qualified vms, prepare Stop/Destroy(Cmd) and submit to Job Manager
switch (expiryAction) {
case STOP: {
logger.debug("Stopping instance: {} (id: {}) on lease expiry", instance.getName(), instance.getUuid());
return executeStopInstanceJob(instance, eventId);
}
case DESTROY: {
logger.debug("Destroying instance: {} (id: {}) on lease expiry", instance.getName(), instance.getUuid());
return executeDestroyInstanceJob(instance, eventId);
}
default: {
logger.error("Invalid configuration for instance.lease.expiryaction for instance: {} (id: {}), " +
"valid values are: \"STOP\" and \"DESTROY\"", instance.getName(), instance.getUuid());
}
}
return null;
}
long executeStopInstanceJob(UserVmJoinVO vm, long eventId) {
final Map<String, String> params = new HashMap<>();
params.put(ApiConstants.ID, String.valueOf(vm.getId()));
params.put("ctxUserId", String.valueOf(User.UID_SYSTEM));
params.put("ctxAccountId", String.valueOf(Account.ACCOUNT_ID_SYSTEM));
params.put(ApiConstants.CTX_START_EVENT_ID, String.valueOf(eventId));
params.put(JOB_INITIATOR, VMLeaseManager.class.getSimpleName());
final StopVMCmd cmd = new StopVMCmd();
ComponentContext.inject(cmd);
AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, vm.getAccountId(), StopVMCmd.class.getName(), gson.toJson(params), vm.getId(),
cmd.getApiResourceType() != null ? cmd.getApiResourceType().toString() : null, null);
job.setDispatcher(asyncJobDispatcher.getName());
return asyncJobManager.submitAsyncJob(job);
}
long executeDestroyInstanceJob(UserVmJoinVO vm, long eventId) {
final Map<String, String> params = new HashMap<>();
params.put(ApiConstants.ID, String.valueOf(vm.getId()));
params.put("ctxUserId", String.valueOf(User.UID_SYSTEM));
params.put("ctxAccountId", String.valueOf(Account.ACCOUNT_ID_SYSTEM));
params.put(ApiConstants.CTX_START_EVENT_ID, String.valueOf(eventId));
params.put(JOB_INITIATOR, VMLeaseManager.class.getSimpleName());
final DestroyVMCmd cmd = new DestroyVMCmd();
ComponentContext.inject(cmd);
AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, vm.getAccountId(), DestroyVMCmd.class.getName(), gson.toJson(params), vm.getId(),
cmd.getApiResourceType() != null ? cmd.getApiResourceType().toString() : null, null);
job.setDispatcher(asyncJobDispatcher.getName());
return asyncJobManager.submitAsyncJob(job);
}
public ExpiryAction getLeaseExpiryAction(UserVmJoinVO instance) {
return EnumUtils.getEnumIgnoreCase(VMLeaseManager.ExpiryAction.class, instance.getLeaseExpiryAction());
}
private void addLeaseExpiryListener() {
logger.debug("Adding Lease subscriber for async job events");
if (this.leaseManagerSubscriber == null) {
this.leaseManagerSubscriber = new VMLeaseManagerSubscriber();
}
messageBus.subscribe(AsyncJob.Topics.JOB_EVENT_PUBLISH, this.leaseManagerSubscriber);
}
private void removeLeaseExpiryListener() {
logger.debug("Removing Lease subscriber for async job events");
messageBus.unsubscribe(AsyncJob.Topics.JOB_EVENT_PUBLISH, this.leaseManagerSubscriber);
this.leaseManagerSubscriber = null;
}
class VMLeaseManagerSubscriber implements MessageSubscriber {
@Override
public void onPublishMessage(String senderAddress, String subject, Object args) {
try {
@SuppressWarnings("unchecked")
Pair<AsyncJob, String> eventInfo = (Pair<AsyncJob, String>) args;
AsyncJob asyncExpiryJob = eventInfo.first();
if (!"ApiAsyncJobDispatcher".equalsIgnoreCase(asyncExpiryJob.getDispatcher()) || !"complete".equalsIgnoreCase(eventInfo.second())) {
return;
}
String cmd = asyncExpiryJob.getCmd();
if ((cmd.equalsIgnoreCase(StopVMCmd.class.getName()) || cmd.equalsIgnoreCase(DestroyVMCmd.class.getName()))
&& asyncExpiryJob.getStatus() == JobInfo.Status.SUCCEEDED && asyncExpiryJob.getInstanceId() != null) {
Map<String, String> params = gson.fromJson(asyncExpiryJob.getCmdInfo(), new TypeToken<Map<String, String>>() {
}.getType());
if (VMLeaseManager.class.getSimpleName().equals(params.get(JOB_INITIATOR))) {
logger.debug("Lease expiry job: {} successfully executed for instanceId: {}", asyncExpiryJob.getId(), asyncExpiryJob.getInstanceId());
userVmDetailsDao.addDetail(asyncExpiryJob.getInstanceId(), VmDetailConstants.INSTANCE_LEASE_EXECUTION, LeaseActionExecution.DONE.name(), false);
}
}
} catch (final Exception e) {
logger.error("Caught exception while executing lease expiry job", e);
}
}
}
}

View File

@ -385,4 +385,9 @@
<bean id="reconcileCommandServiceImpl" class="org.apache.cloudstack.command.ReconcileCommandServiceImpl"> <bean id="reconcileCommandServiceImpl" class="org.apache.cloudstack.command.ReconcileCommandServiceImpl">
</bean> </bean>
<bean id="vmLeaseManager" class="org.apache.cloudstack.vm.lease.VMLeaseManagerImpl" >
<property name="asyncJobDispatcher" ref="ApiAsyncJobDispatcher" />
</bean>
</beans> </beans>

View File

@ -80,6 +80,10 @@ import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeployDestination; import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlanner; import com.cloud.deploy.DeploymentPlanner;
import com.cloud.deploy.DeploymentPlanningManager; import com.cloud.deploy.DeploymentPlanningManager;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InsufficientServerCapacityException; import com.cloud.exception.InsufficientServerCapacityException;
@ -91,12 +95,28 @@ import com.cloud.host.Host;
import com.cloud.host.HostVO; import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel; import com.cloud.network.NetworkModel;
import com.cloud.network.dao.FirewallRulesDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.LoadBalancerVMMapDao;
import com.cloud.network.dao.LoadBalancerVMMapVO;
import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO; import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.guru.NetworkGuru;
import com.cloud.network.rules.FirewallRuleVO;
import com.cloud.network.rules.PortForwardingRule;
import com.cloud.network.rules.dao.PortForwardingRulesDao;
import com.cloud.network.security.SecurityGroupManager;
import com.cloud.network.security.SecurityGroupVO; import com.cloud.network.security.SecurityGroupVO;
import com.cloud.offering.DiskOffering; import com.cloud.offering.DiskOffering;
import com.cloud.offering.NetworkOffering;
import com.cloud.offering.ServiceOffering; import com.cloud.offering.ServiceOffering;
import com.cloud.offerings.NetworkOfferingVO;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.server.ManagementService; import com.cloud.server.ManagementService;
import com.cloud.service.ServiceOfferingVO; import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDao;
@ -136,31 +156,23 @@ import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.UserVmDetailsDao;
import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.VMSnapshotVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDao; import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import com.cloud.domain.DomainVO; import java.util.TimeZone;
import com.cloud.domain.dao.DomainDao; import java.util.UUID;
import com.cloud.event.UsageEventUtils;
import com.cloud.network.Network; import static org.mockito.ArgumentMatchers.anyBoolean;
import com.cloud.network.dao.FirewallRulesDao; import static org.mockito.ArgumentMatchers.anyList;
import com.cloud.network.dao.IPAddressDao; import static org.mockito.Mockito.verify;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.LoadBalancerVMMapDao;
import com.cloud.network.dao.LoadBalancerVMMapVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.guru.NetworkGuru;
import com.cloud.network.rules.FirewallRuleVO;
import com.cloud.network.rules.PortForwardingRule;
import com.cloud.network.rules.dao.PortForwardingRulesDao;
import com.cloud.network.security.SecurityGroupManager;
import com.cloud.offering.NetworkOffering;
import com.cloud.offerings.NetworkOfferingVO;
import com.cloud.offerings.dao.NetworkOfferingDao;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class UserVmManagerImplTest { public class UserVmManagerImplTest {
@ -3089,7 +3101,7 @@ public class UserVmManagerImplTest {
configureDoNothingForMethodsThatWeDoNotWantToTest(); configureDoNothingForMethodsThatWeDoNotWantToTest();
userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock, userVmVoMock, serviceOfferingVoMock, volumes, userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock, userVmVoMock, serviceOfferingVoMock, volumes,
virtualMachineTemplateMock, 1l); virtualMachineTemplateMock, 1L);
Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());
Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong());
@ -3211,4 +3223,176 @@ public class UserVmManagerImplTest {
Mockito.verify(storageManager, times(1)).getStorageAccessGroups(null, null, null, srcHost.getId()); Mockito.verify(storageManager, times(1)).getStorageAccessGroups(null, null, null, srcHost.getId());
Mockito.verify(storageManager, times(1)).getStorageAccessGroups(null, null, null, destHost.getId()); Mockito.verify(storageManager, times(1)).getStorageAccessGroups(null, null, null, destHost.getId());
} }
@Test(expected = InvalidParameterValueException.class)
public void testValidateLeasePropertiesInvalidDuration() {
userVmManagerImpl.validateLeaseProperties(-2, VMLeaseManager.ExpiryAction.STOP);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateLeasePropertiesNullActionValue() {
userVmManagerImpl.validateLeaseProperties(20, null);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateLeasePropertiesNullDurationValue() {
userVmManagerImpl.validateLeaseProperties(null, VMLeaseManager.ExpiryAction.STOP);
}
@Test
public void testValidateLeasePropertiesMinusOneDuration() {
userVmManagerImpl.validateLeaseProperties(-1, null);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateLeasePropertiesZeroDayDuration() {
userVmManagerImpl.validateLeaseProperties(0, VMLeaseManager.ExpiryAction.STOP);
}
@Test
public void testValidateLeasePropertiesValidValues() {
userVmManagerImpl.validateLeaseProperties(20, VMLeaseManager.ExpiryAction.STOP);
}
@Test
public void testValidateLeasePropertiesBothNUll() {
userVmManagerImpl.validateLeaseProperties(null, null);
}
@Test
public void testAddLeaseDetailsForInstance() {
UserVm userVm = mock(UserVm.class);
when(userVm.getId()).thenReturn(vmId);
when(userVm.getUuid()).thenReturn(UUID.randomUUID().toString());
userVmManagerImpl.addLeaseDetailsForInstance(userVm, 10, VMLeaseManager.ExpiryAction.STOP);
verify(userVmDetailsDao).addDetail(eq(vmId), eq(VmDetailConstants.INSTANCE_LEASE_EXPIRY_ACTION), eq(VMLeaseManager.ExpiryAction.STOP.name()), anyBoolean());
verify(userVmDetailsDao).addDetail(eq(vmId), eq(VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE), eq(getLeaseExpiryDate(10L)), anyBoolean());
}
@Test
public void testAddNullDurationLeaseDetailsForInstance() {
UserVm userVm = mock(UserVm.class);
userVmManagerImpl.addLeaseDetailsForInstance(userVm, null, VMLeaseManager.ExpiryAction.STOP);
Mockito.verify(userVmDetailsDao, Mockito.times(0)).removeDetail(vmId, VmDetailConstants.INSTANCE_LEASE_EXPIRY_ACTION);
Mockito.verify(userVmDetailsDao, Mockito.times(0)).removeDetail(vmId, VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE);
}
@Test
public void testApplyLeaseOnCreateInstanceFeatureEnabled() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class);
userVmManagerImpl.applyLeaseOnCreateInstance(userVm, 10, VMLeaseManager.ExpiryAction.DESTROY, svcOfferingMock);
Mockito.verify(userVmManagerImpl, Mockito.times(1)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test
public void testApplyLeaseOnCreateInstanceNegativeLease() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
userVmManagerImpl.applyLeaseOnCreateInstance(userVm, -1, VMLeaseManager.ExpiryAction.DESTROY, null);
Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test
public void testApplyLeaseOnCreateInstanceFromSvcOfferingWithoutLease() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class);
userVmManagerImpl.applyLeaseOnCreateInstance(userVm, null, VMLeaseManager.ExpiryAction.DESTROY, svcOfferingMock);
Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test
public void testApplyLeaseOnCreateInstanceFromSvcOfferingWithLease() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class);
when(svcOfferingMock.getLeaseDuration()).thenReturn(10);
userVmManagerImpl.applyLeaseOnCreateInstance(userVm, null, VMLeaseManager.ExpiryAction.DESTROY, svcOfferingMock);
Mockito.verify(userVmManagerImpl, Mockito.times(1)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test
public void testApplyLeaseOnCreateInstanceNullExpiryAction() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class);
userVmManagerImpl.applyLeaseOnCreateInstance(userVm, 10, null, svcOfferingMock);
Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test(expected = CloudRuntimeException.class)
public void testApplyLeaseOnUpdateInstanceForNoLease() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
when(userVm.getId()).thenReturn(vmId);
when(userVmDetailsDao.listDetailsKeyPairs(anyLong(), anyList())).thenReturn(getLeaseDetails(5, VMLeaseManager.LeaseActionExecution.DISABLED.name()));
userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, 10, VMLeaseManager.ExpiryAction.STOP);
Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test
public void testApplyLeaseOnUpdateInstanceForLease() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
when(userVm.getId()).thenReturn(vmId);
when(userVmDetailsDao.listDetailsKeyPairs(anyLong(), anyList())).thenReturn(getLeaseDetails(5, VMLeaseManager.LeaseActionExecution.PENDING.name()));
userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, 10, VMLeaseManager.ExpiryAction.STOP);
Mockito.verify(userVmManagerImpl, Mockito.times(1)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test(expected = CloudRuntimeException.class)
public void testApplyLeaseOnUpdateInstanceForDisabledLeaseInstance() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
when(userVm.getId()).thenReturn(vmId);
when(userVmDetailsDao.listDetailsKeyPairs(anyLong(), anyList())).thenReturn(getLeaseDetails(5, VMLeaseManager.LeaseActionExecution.DISABLED.name()));
userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, 10, VMLeaseManager.ExpiryAction.STOP);
Mockito.verify(userVmManagerImpl, Mockito.times(1)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test(expected = CloudRuntimeException.class)
public void testApplyLeaseOnUpdateInstanceForLeaseExpired() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
when(userVmDetailsDao.listDetailsKeyPairs(anyLong(), anyList())).thenReturn(getLeaseDetails(-2, VMLeaseManager.LeaseActionExecution.PENDING.name()));
userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, 10, VMLeaseManager.ExpiryAction.STOP);
Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any());
}
@Test
public void testApplyLeaseOnUpdateInstanceToRemoveLease() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
when(userVm.getId()).thenReturn(vmId);;
when(userVmDetailsDao.listDetailsKeyPairs(anyLong(), anyList())).thenReturn(getLeaseDetails(2, VMLeaseManager.LeaseActionExecution.PENDING.name()));
try (MockedStatic<ActionEventUtils> ignored = Mockito.mockStatic(ActionEventUtils.class)) {
Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(),
Mockito.anyLong(),
Mockito.anyString(), Mockito.anyString(),
Mockito.anyLong(), Mockito.anyString())).thenReturn(1L);
userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, -1, VMLeaseManager.ExpiryAction.STOP);
}
Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any());
Mockito.verify(userVmDetailsDao, Mockito.times(1)).
addDetail(vmId, VmDetailConstants.INSTANCE_LEASE_EXECUTION, VMLeaseManager.LeaseActionExecution.DISABLED.name(), false);
}
@Test(expected = CloudRuntimeException.class)
public void testApplyLeaseOnUpdateInstanceToRemoveLeaseForExpired() {
UserVmVO userVm = Mockito.mock(UserVmVO.class);
when(userVm.getId()).thenReturn(vmId);
when(userVmDetailsDao.listDetailsKeyPairs(anyLong(), anyList())).thenReturn(getLeaseDetails(-2, VMLeaseManager.LeaseActionExecution.PENDING.name()));
userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, -1, VMLeaseManager.ExpiryAction.STOP);
Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any());
Mockito.verify(userVmDetailsDao, Mockito.times(0)).removeDetail(vmId, VmDetailConstants.INSTANCE_LEASE_EXPIRY_ACTION);
Mockito.verify(userVmDetailsDao, Mockito.times(0)).removeDetail(vmId, VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE);
}
String getLeaseExpiryDate(long leaseDuration) {
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
LocalDateTime leaseExpiryDateTime = now.plusDays(leaseDuration);
Date leaseExpiryDate = Date.from(leaseExpiryDateTime.atZone(ZoneOffset.UTC).toInstant());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
return sdf.format(leaseExpiryDate);
}
Map<String, String> getLeaseDetails(int leaseDuration, String leaseExecution) {
Map<String, String> leaseDetails = new HashMap<>();
leaseDetails.put(VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE, getLeaseExpiryDate(leaseDuration));
leaseDetails.put(VmDetailConstants.INSTANCE_LEASE_EXECUTION, leaseExecution);
return leaseDetails;
}
} }

View File

@ -0,0 +1,314 @@
/*
* 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.lease;
import com.cloud.api.query.dao.UserVmJoinDao;
import com.cloud.api.query.vo.UserVmJoinVO;
import com.cloud.event.ActionEventUtils;
import com.cloud.user.User;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.UserVmDetailsDao;
import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd;
import org.apache.cloudstack.api.command.user.vm.StopVMCmd;
import org.apache.cloudstack.framework.config.ConfigKey;
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.framework.messagebus.MessageBus;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.context.ApplicationContext;
import javax.naming.ConfigurationException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class VMLeaseManagerImplTest {
public static final String DESTROY = "DESTROY";
public static final String VM_UUID = UUID.randomUUID().toString();
public static final String VM_NAME = "vm-name";
@Spy
@InjectMocks
private VMLeaseManagerImpl vmLeaseManager;
@Mock
private UserVmJoinDao userVmJoinDao;
@Mock
MessageBus messageBus;
@Mock
private UserVmDetailsDao userVmDetailsDao;
@Mock
private AsyncJobManager asyncJobManager;
@Mock
private AsyncJobDispatcher asyncJobDispatcher;
@Mock
private GlobalLock globalLock;
@Before
public void setUp() {
vmLeaseManager.setAsyncJobDispatcher(asyncJobDispatcher);
when(asyncJobDispatcher.getName()).thenReturn("AsyncJobDispatcher");
when(asyncJobManager.submitAsyncJob(any(AsyncJobVO.class))).thenReturn(1L);
doNothing().when(userVmDetailsDao).addDetail(
anyLong(), anyString(), anyString(), anyBoolean()
);
try {
vmLeaseManager.configure("VMLeaseManagerImpl", new HashMap<>());
} catch (ConfigurationException e) {
throw new CloudRuntimeException(e);
}
}
@Test
public void testReallyRunNoExpiredInstances() {
when(userVmJoinDao.listEligibleInstancesWithExpiredLease()).thenReturn(new ArrayList<>());
vmLeaseManager.reallyRun();
verify(asyncJobManager, never()).submitAsyncJob(any(AsyncJobVO.class));
}
@Test
public void testReallyRunWithDeleteProtection() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, true);
when(vm.getLeaseExpiryAction()).thenReturn("DESTROY");
List<UserVmJoinVO> expiredVms = Arrays.asList(vm);
when(userVmJoinDao.listEligibleInstancesWithExpiredLease()).thenReturn(expiredVms);
vmLeaseManager.reallyRun();
// Verify no jobs were submitted because of delete protection
verify(asyncJobManager, never()).submitAsyncJob(any(AsyncJobVO.class));
}
@Test
public void testReallyRunStopAction() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false);
List<UserVmJoinVO> expiredVms = Arrays.asList(vm);
when(userVmJoinDao.listEligibleInstancesWithExpiredLease()).thenReturn(expiredVms);
when(userVmJoinDao.findById(1L)).thenReturn(vm);
doReturn(1L).when(vmLeaseManager).executeStopInstanceJob(eq(vm), anyLong());
try (MockedStatic<ActionEventUtils> utilities = Mockito.mockStatic(ActionEventUtils.class)) {
utilities.when(() -> ActionEventUtils.onStartedActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyLong())).thenReturn(1L);
vmLeaseManager.reallyRun();
}
verify(vmLeaseManager).executeStopInstanceJob(eq(vm), anyLong());
}
@Test
public void testReallyRunDestroyAction() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false, DESTROY);
List<UserVmJoinVO> expiredVms = Arrays.asList(vm);
when(userVmJoinDao.listEligibleInstancesWithExpiredLease()).thenReturn(expiredVms);
when(userVmJoinDao.findById(1L)).thenReturn(vm);
doReturn(1L).when(vmLeaseManager).executeDestroyInstanceJob(eq(vm), anyLong());
try (MockedStatic<ActionEventUtils> utilities = Mockito.mockStatic(ActionEventUtils.class)) {
utilities.when(() -> ActionEventUtils.onStartedActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyLong())).thenReturn(1L);
vmLeaseManager.reallyRun();
}
verify(vmLeaseManager).executeDestroyInstanceJob(eq(vm), anyLong());
}
@Test
public void testExecuteExpiryActionStop() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false);
doReturn(1L).when(vmLeaseManager).executeStopInstanceJob(eq(vm), eq(123L));
Long jobId = vmLeaseManager.executeExpiryAction(vm, VMLeaseManager.ExpiryAction.STOP, 123L);
assertNotNull(jobId);
assertEquals(1L, jobId.longValue());
verify(vmLeaseManager).executeStopInstanceJob(eq(vm), eq(123L));
}
@Test
public void testExecuteExpiryActionDestroy() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false, DESTROY);
doReturn(1L).when(vmLeaseManager).executeDestroyInstanceJob(eq(vm), eq(123L));
Long jobId = vmLeaseManager.executeExpiryAction(vm, VMLeaseManager.ExpiryAction.DESTROY, 123L);
assertNotNull(jobId);
assertEquals(1L, jobId.longValue());
verify(vmLeaseManager).executeDestroyInstanceJob(eq(vm), eq(123L));
}
@Test
public void testExecuteStopInstanceJob() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false);
// Mock the static ComponentContext
try (MockedStatic<ComponentContext> mockedComponentContext = Mockito.mockStatic(ComponentContext.class)) {
ApplicationContext mockAppContext = mock(ApplicationContext.class);
mockedComponentContext.when(ComponentContext::getApplicationContext).thenReturn(mockAppContext);
mockedComponentContext.when(() -> ComponentContext.inject(any())).thenReturn(true);
long jobId = vmLeaseManager.executeStopInstanceJob(vm, 123L);
assertEquals(1L, jobId);
ArgumentCaptor<AsyncJobVO> jobCaptor = ArgumentCaptor.forClass(AsyncJobVO.class);
verify(asyncJobManager).submitAsyncJob(jobCaptor.capture());
AsyncJobVO capturedJob = jobCaptor.getValue();
assertEquals(User.UID_SYSTEM, capturedJob.getUserId());
assertEquals(vm.getAccountId(), capturedJob.getAccountId());
assertEquals(StopVMCmd.class.getName(), capturedJob.getCmd());
assertEquals(vm.getId(), capturedJob.getInstanceId().longValue());
assertEquals("AsyncJobDispatcher", capturedJob.getDispatcher());
}
}
@Test
public void testExecuteDestroyInstanceJob() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false, DESTROY);
try (MockedStatic<ComponentContext> mockedComponentContext = Mockito.mockStatic(ComponentContext.class)) {
ApplicationContext mockAppContext = mock(ApplicationContext.class);
mockedComponentContext.when(ComponentContext::getApplicationContext).thenReturn(mockAppContext);
mockedComponentContext.when(() -> ComponentContext.inject(any())).thenReturn(true);
long jobId = vmLeaseManager.executeDestroyInstanceJob(vm, 123L);
assertEquals(1L, jobId);
ArgumentCaptor<AsyncJobVO> jobCaptor = ArgumentCaptor.forClass(AsyncJobVO.class);
verify(asyncJobManager).submitAsyncJob(jobCaptor.capture());
AsyncJobVO capturedJob = jobCaptor.getValue();
assertEquals(User.UID_SYSTEM, capturedJob.getUserId());
assertEquals(vm.getAccountId(), capturedJob.getAccountId());
assertEquals(DestroyVMCmd.class.getName(), capturedJob.getCmd());
assertEquals(vm.getId(), capturedJob.getInstanceId().longValue());
assertEquals("AsyncJobDispatcher", capturedJob.getDispatcher());
}
}
@Test
public void testGetLeaseExpiryAction() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false);
VMLeaseManager.ExpiryAction action = vmLeaseManager.getLeaseExpiryAction(vm);
assertEquals(VMLeaseManager.ExpiryAction.STOP, action);
}
@Test
public void testGetLeaseExpiryActionNoAction() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false);
when(vm.getLeaseExpiryAction()).thenReturn(null);
vm.setLeaseExpiryAction(null);
assertNull(vmLeaseManager.getLeaseExpiryAction(vm));
}
@Test
public void testGetLeaseExpiryInvalidAction() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false);
when(vm.getLeaseExpiryAction()).thenReturn("Unknown");
assertNull(vmLeaseManager.getLeaseExpiryAction(vm));
}
@Test
public void testGetComponentName() {
assertEquals(vmLeaseManager.getConfigComponentName(), "VMLeaseManager");
}
@Test
public void testConfigKeys() {
assertEquals(vmLeaseManager.getConfigKeys().length, 4);
}
@Test
public void testConfigure() throws Exception {
overrideDefaultConfigValue(VMLeaseManager.InstanceLeaseEnabled, "true");
vmLeaseManager.configure("VMLeaseManagerImpl", new HashMap<>());
}
@Test
public void testStopShouldShutdownExecutors() {
assertTrue(vmLeaseManager.stop());
}
@Test
public void testCancelLeaseOnExistingInstances() {
UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, true);
when(userVmJoinDao.listLeaseInstancesExpiringInDays(-1)).thenReturn(List.of(vm));
try (MockedStatic<ActionEventUtils> utilities = Mockito.mockStatic(ActionEventUtils.class)) {
utilities.when(() -> ActionEventUtils.onStartedActionEvent(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyLong())).thenReturn(1L);
vmLeaseManager.cancelLeaseOnExistingInstances();
verify(userVmDetailsDao).addDetail(1L, VmDetailConstants.INSTANCE_LEASE_EXECUTION, VMLeaseManager.LeaseActionExecution.CANCELLED.name(), false);
}
}
@Test
public void testOnLeaseFeatureToggleEnabled() throws Exception {
overrideDefaultConfigValue(VMLeaseManager.InstanceLeaseEnabled, "true");
vmLeaseManager.onLeaseFeatureToggle();
}
@Test
public void testOnLeaseFeatureToggleDisabled() throws Exception {
overrideDefaultConfigValue(VMLeaseManager.InstanceLeaseEnabled, "false");
vmLeaseManager.onLeaseFeatureToggle();
}
private UserVmJoinVO createMockVm(Long id, String uuid, String name, VirtualMachine.State state, boolean deleteProtection) {
return createMockVm(id, uuid, name, state, deleteProtection, "STOP");
}
// Helper method to create mock VMs
private UserVmJoinVO createMockVm(Long id, String uuid, String name, VirtualMachine.State state, boolean deleteProtection, String expiryAction) {
UserVmJoinVO vm = mock(UserVmJoinVO.class);
when(vm.getId()).thenReturn(id);
when(vm.getUuid()).thenReturn(uuid);
when(vm.isDeleteProtection()).thenReturn(deleteProtection);
when(vm.getAccountId()).thenReturn(1L);
when(vm.getLeaseExpiryAction()).thenReturn(expiryAction);
return vm;
}
private void overrideDefaultConfigValue(final ConfigKey configKey, final String value) throws IllegalAccessException, NoSuchFieldException {
final Field f = ConfigKey.class.getDeclaredField("_defaultValue");
f.setAccessible(true);
f.set(configKey, value);
}
}

View File

@ -0,0 +1,358 @@
# 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.
# Import Local Modules
from nose.plugins.attrib import attr
from marvin.codes import FAILED
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.lib.utils import cleanup_resources
from marvin.lib.base import (Account,
VirtualMachine,
ServiceOffering,
DiskOffering,
Configurations)
from marvin.lib.common import (get_zone,
get_domain,
get_test_template,
is_config_suitable)
class TestDeployVMLease(cloudstackTestCase):
@classmethod
def setUpClass(cls):
cls.testClient = super(TestDeployVMLease, cls).getClsTestClient()
cls.api_client = cls.testClient.getApiClient()
cls.testdata = cls.testClient.getParsedTestDataConfig()
# Get Zone, Domain and templates
cls.domain = get_domain(cls.api_client)
cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests())
cls.hypervisor = cls.testClient.getHypervisorInfo()
cls.template = get_test_template(
cls.api_client,
cls.zone.id,
cls.hypervisor
)
if cls.template == FAILED:
assert False, "get_test_template() failed to return template"
# enable instance lease feature
Configurations.update(cls.api_client,
name="instance.lease.enabled",
value="true"
)
# Create service, disk offerings etc
cls.non_lease_svc_offering = ServiceOffering.create(
cls.api_client,
cls.testdata["service_offering"],
name="non-lease-svc-offering"
)
# Create service, disk offerings etc
cls.lease_svc_offering = ServiceOffering.create(
cls.api_client,
cls.testdata["service_offering"],
name="lease-svc-offering",
leaseduration=20,
leaseexpiryaction="DESTROY"
)
cls.disk_offering = DiskOffering.create(
cls.api_client,
cls.testdata["disk_offering"]
)
cls._cleanup = [
cls.lease_svc_offering,
cls.non_lease_svc_offering,
cls.disk_offering
]
return
@classmethod
def tearDownClass(cls):
try:
# disable instance lease feature
Configurations.update(cls.api_client,
name="instance.lease.enabled",
value="false"
)
cleanup_resources(cls.api_client, cls._cleanup)
except Exception as e:
raise Exception("Warning: Exception during cleanup : %s" % e)
def setUp(self):
self.apiclient = self.testClient.getApiClient()
self.hypervisor = self.testClient.getHypervisorInfo()
self.testdata["virtual_machine"]["zoneid"] = self.zone.id
self.testdata["virtual_machine"]["template"] = self.template.id
self.testdata["iso"]["zoneid"] = self.zone.id
self.account = Account.create(
self.apiclient,
self.testdata["account"],
domainid=self.domain.id
)
self.cleanup = [self.account]
return
def tearDown(self):
try:
self.debug("Cleaning up the resources")
cleanup_resources(self.apiclient, self.cleanup)
self.debug("Cleanup complete!")
except Exception as e:
self.debug("Warning! Exception in tearDown: %s" % e)
@attr(
tags=[
"advanced",
"basic"],
required_hardware="true")
def test_01_deploy_vm_no_lease_svc_offering(self):
"""Test Deploy Virtual Machine from non-lease-svc-offering
Validate the following:
1. deploy VM using non-lease-svc-offering
2. confirm vm has no lease configured
"""
non_lease_vm = VirtualMachine.create(
self.apiclient,
self.testdata["virtual_machine"],
accountid=self.account.name,
domainid=self.account.domainid,
templateid=self.template.id,
serviceofferingid=self.non_lease_svc_offering.id,
diskofferingid=self.disk_offering.id,
hypervisor=self.hypervisor
)
self.verify_no_lease_configured_for_vm(non_lease_vm.id)
return
@attr(
tags=[
"advanced",
"basic"],
required_hardware="true")
def test_02_deploy_vm_no_lease_svc_offering_with_lease_params(self):
"""Test Deploy Virtual Machine from non-lease-svc-offering and lease parameters are used to enabled lease for vm
Validate the following:
1. deploy VM using non-lease-svc-offering and passing leaseduration and leaseexpiryaction
2. confirm vm has lease configured
"""
lease_vm = VirtualMachine.create(
self.apiclient,
self.testdata["virtual_machine"],
accountid=self.account.name,
domainid=self.account.domainid,
templateid=self.template.id,
serviceofferingid=self.non_lease_svc_offering.id,
diskofferingid=self.disk_offering.id,
hypervisor=self.hypervisor,
leaseduration=10,
leaseexpiryaction="STOP"
)
self.verify_lease_configured_for_vm(lease_vm.id, lease_duration=10, lease_expiry_action="STOP")
return
@attr(
tags=[
"advanced",
"basic"],
required_hardware="true")
def test_03_deploy_vm_lease_svc_offering_with_no_param(self):
"""Test Deploy Virtual Machine from lease-svc-offering without lease params
expect vm to inherit svc_offering lease properties
Validate the following:
1. deploy VM using lease-svc-offering without passing leaseduration and leaseexpiryaction
2. confirm vm has lease configured
"""
lease_vm = VirtualMachine.create(
self.apiclient,
self.testdata["virtual_machine"],
accountid=self.account.name,
domainid=self.account.domainid,
templateid=self.template.id,
serviceofferingid=self.lease_svc_offering.id,
diskofferingid=self.disk_offering.id,
hypervisor=self.hypervisor
)
self.verify_lease_configured_for_vm(lease_vm.id, lease_duration=20, lease_expiry_action="DESTROY")
return
@attr(
tags=[
"advanced",
"basic"],
required_hardware="true")
def test_04_deploy_vm_lease_svc_offering_with_param(self):
"""Test Deploy Virtual Machine from lease-svc-offering with overridden lease properties
Validate the following:
1. confirm svc_offering has lease properties
2. deploy VM using lease-svc-offering and leaseduration and leaseexpiryaction passed
3. confirm vm has lease configured
"""
self.verify_svc_offering()
lease_vm = VirtualMachine.create(
self.apiclient,
self.testdata["virtual_machine"],
accountid=self.account.name,
domainid=self.account.domainid,
templateid=self.template.id,
serviceofferingid=self.lease_svc_offering.id,
diskofferingid=self.disk_offering.id,
hypervisor=self.hypervisor,
leaseduration=30,
leaseexpiryaction="STOP"
)
self.verify_lease_configured_for_vm(lease_vm.id, lease_duration=30, lease_expiry_action="STOP")
return
@attr(
tags=[
"advanced",
"basic"],
required_hardware="true")
def test_05_deploy_vm_lease_svc_offering_with_lease_param_disabled(self):
"""Test Deploy Virtual Machine from lease-svc-offering and passing -1 leaseduration to set no-expiry
Validate the following:
1. deploy VM using lease-svc-offering
2. leaseduration is set as -1 in the deploy vm request to disable lease
3. confirm vm has no lease configured
"""
lease_vm = VirtualMachine.create(
self.apiclient,
self.testdata["virtual_machine"],
accountid=self.account.name,
domainid=self.account.domainid,
templateid=self.template.id,
serviceofferingid=self.non_lease_svc_offering.id,
diskofferingid=self.disk_offering.id,
hypervisor=self.hypervisor,
leaseduration=-1
)
vms = VirtualMachine.list(
self.apiclient,
id=lease_vm.id
)
vm = vms[0]
self.verify_no_lease_configured_for_vm(vm.id)
return
@attr(
tags=[
"advanced",
"basic"],
required_hardware="true")
def test_06_deploy_vm_lease_svc_offering_with_disabled_lease(self):
"""Test Deploy Virtual Machine from lease-svc-offering with lease feature disabled
Validate the following:
1. Disable lease feature
2. deploy VM using lease-svc-offering
3. confirm vm has no lease configured
"""
Configurations.update(self.api_client,
name="instance.lease.enabled",
value="false"
)
lease_vm = VirtualMachine.create(
self.apiclient,
self.testdata["virtual_machine"],
accountid=self.account.name,
domainid=self.account.domainid,
templateid=self.template.id,
serviceofferingid=self.lease_svc_offering.id,
diskofferingid=self.disk_offering.id,
hypervisor=self.hypervisor
)
vms = VirtualMachine.list(
self.apiclient,
id=lease_vm.id
)
vm = vms[0]
self.verify_no_lease_configured_for_vm(vm.id)
return
def verify_svc_offering(self):
svc_offering_list = ServiceOffering.list(
self.api_client,
id=self.lease_svc_offering.id
)
svc_offering = svc_offering_list[0]
self.assertIsNotNone(
svc_offering.leaseduration,
"svc_offering has lease configured"
)
self.assertEqual(
20,
svc_offering.leaseduration,
"svc_offering has 20 days for lease"
)
def verify_lease_configured_for_vm(self, vm_id=None, lease_duration=None, lease_expiry_action=None):
vms = VirtualMachine.list(
self.apiclient,
id=vm_id
)
vm = vms[0]
self.assertEqual(
lease_duration,
vm.leaseduration,
"check to confirm leaseduration is configured"
)
self.assertEqual(
lease_expiry_action,
vm.leaseexpiryaction,
"check to confirm leaseexpiryaction is configured"
)
self.assertIsNotNone(vm.leaseexpirydate, "confirm leaseexpirydate is available")
def verify_no_lease_configured_for_vm(self, vm_id=None):
if vm_id == None:
return
vms = VirtualMachine.list(
self.apiclient,
id=vm_id
)
vm = vms[0]
self.assertIsNone(vm.leaseduration)
self.assertIsNone(vm.leaseexpiryaction)

View File

@ -35,7 +35,8 @@ from marvin.lib.common import (list_service_offering,
get_domain, get_domain,
get_zone, get_zone,
get_test_template, get_test_template,
list_hosts) list_hosts,
is_config_suitable)
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
import time import time
@ -356,6 +357,164 @@ class TestCreateServiceOffering(cloudstackTestCase):
) )
return return
@attr(
tags=[
"advanced",
"smoke",
"basic"],
required_hardware="false")
def test_06_create_service_offering_lease_enabled(self):
"""
1. Enable lease feature
2. Create a service_offering
3. Verify service offering lease properties
"""
self.update_lease_feature("true")
service_offering = ServiceOffering.create(
self.apiclient,
self.services["service_offerings"]["tiny"],
name="tiny-lease-svc-offering",
leaseduration=10,
leaseexpiryaction="STOP"
)
self.cleanup.append(service_offering)
self.debug(
"Created service offering with ID: %s" %
service_offering.id)
list_service_response = list_service_offering(
self.apiclient,
id=service_offering.id
)
self.assertNotEqual(
len(list_service_response),
0,
"Check Service offering is created"
)
self.assertEqual(
list_service_response[0].leaseduration,
10,
"Confirm leaseduration"
)
self.assertEqual(
list_service_response[0].leaseexpiryaction,
"STOP",
"Confirm leaseexpiryaction"
)
return
@attr(
tags=[
"advanced",
"smoke",
"basic"],
required_hardware="false")
def test_07_create_service_offering_without_lease_disabled_feature(self):
"""
1. Disable lease feature
2. Create a service_offering with lease option
3. Verify service offering for NO lease properties
"""
self.update_lease_feature("true")
service_offering = ServiceOffering.create(
self.apiclient,
self.services["service_offerings"]["tiny"],
name="tiny-svc-offering-novalue-lease"
)
self.cleanup.append(service_offering)
self.debug(
"Created service offering with ID: %s" %
service_offering.id)
list_service_response = list_service_offering(
self.apiclient,
id=service_offering.id
)
self.assertNotEqual(
len(list_service_response),
0,
"Check Service offering is created"
)
self.assertIsNone(
list_service_response[0].leaseduration,
"Confirm No leaseduration"
)
self.assertIsNone(
list_service_response[0].leaseexiryaction,
"Confirm leaseexpiryaction is not set"
)
return
@attr(
tags=[
"advanced",
"smoke",
"basic"],
required_hardware="false")
def test_08_create_service_offering_lease_disabled(self):
"""
1. Disable lease feature
2. Create a service_offering with lease option
3. Verify service offering for NO lease properties
"""
self.update_lease_feature("false")
service_offering = ServiceOffering.create(
self.apiclient,
self.services["service_offerings"]["tiny"],
name="tiny-lease-svc-offering-disabled",
leaseduration=10,
leaseexpiryaction="STOP"
)
self.cleanup.append(service_offering)
self.debug(
"Created service offering with ID: %s" %
service_offering.id)
list_service_response = list_service_offering(
self.apiclient,
id=service_offering.id
)
self.assertNotEqual(
len(list_service_response),
0,
"Check Service offering is created"
)
self.assertIsNone(
list_service_response[0].leaseduration,
"Confirm No leaseduration"
)
self.assertIsNone(
list_service_response[0].leaseexiryaction,
"Confirm leaseexpiryaction is not set"
)
return
def update_lease_feature(self, value=None):
# Update global setting for "instance.lease.enabled"
Configurations.update(self.apiclient,
name="instance.lease.enabled",
value=value
)
# Verify that the above mentioned settings are set to true
if not is_config_suitable(
apiclient=self.apiclient,
name='instance.lease.enabled',
value=value):
self.fail(f'instance.lease.enabled should be: {value}')
class TestServiceOfferings(cloudstackTestCase): class TestServiceOfferings(cloudstackTestCase):

View File

@ -527,7 +527,8 @@ class VirtualMachine:
customcpuspeed=None, custommemory=None, rootdisksize=None, customcpuspeed=None, custommemory=None, rootdisksize=None,
rootdiskcontroller=None, vpcid=None, macaddress=None, datadisktemplate_diskoffering_list={}, rootdiskcontroller=None, vpcid=None, macaddress=None, datadisktemplate_diskoffering_list={},
properties=None, nicnetworklist=None, bootmode=None, boottype=None, dynamicscalingenabled=None, properties=None, nicnetworklist=None, bootmode=None, boottype=None, dynamicscalingenabled=None,
userdataid=None, userdatadetails=None, extraconfig=None, size=None, overridediskofferingid=None): userdataid=None, userdatadetails=None, extraconfig=None, size=None, overridediskofferingid=None,
leaseduration=None, leaseexpiryaction=None):
"""Create the instance""" """Create the instance"""
cmd = deployVirtualMachine.deployVirtualMachineCmd() cmd = deployVirtualMachine.deployVirtualMachineCmd()
@ -691,6 +692,12 @@ class VirtualMachine:
if extraconfig: if extraconfig:
cmd.extraconfig = extraconfig cmd.extraconfig = extraconfig
if leaseduration:
cmd.leaseduration = leaseduration
if leaseexpiryaction:
cmd.leaseexpiryaction = leaseexpiryaction
virtual_machine = apiclient.deployVirtualMachine(cmd, method=method) virtual_machine = apiclient.deployVirtualMachine(cmd, method=method)
if 'password' in list(virtual_machine.__dict__.keys()): if 'password' in list(virtual_machine.__dict__.keys()):

View File

@ -1172,6 +1172,7 @@
"label.instancename": "Internal name", "label.instancename": "Internal name",
"label.instanceport": "Instance port", "label.instanceport": "Instance port",
"label.instances": "Instances", "label.instances": "Instances",
"label.leasedinstances": "Leased Instances",
"label.interface.route.table": "Interface Route Table", "label.interface.route.table": "Interface Route Table",
"label.interface.router.table": "Interface Router Table", "label.interface.router.table": "Interface Router Table",
"label.intermediate.certificate": "Intermediate certificate", "label.intermediate.certificate": "Intermediate certificate",
@ -2672,6 +2673,15 @@
"label.bucket.policy": "Bucket Policy", "label.bucket.policy": "Bucket Policy",
"label.usersecretkey": "Secret Key", "label.usersecretkey": "Secret Key",
"label.create.bucket": "Create Bucket", "label.create.bucket": "Create Bucket",
"label.lease.enable": "Enable Lease",
"label.lease.enable.tooltip": "The Instance Lease feature allows to set a lease duration (in days) for instances, after which they automatically expire. Upon expiry, the instance can either be stopped (powered off) or destroyed, based on the configured policy",
"label.instance.lease": "Instance lease",
"label.instance.lease.placeholder": "Lease duration in days ( > 0)",
"label.leaseduration": "Lease duration (in days)",
"label.leaseexpiry.date.and.time": "Lease expiry date",
"label.leaseexpiryaction": "Lease expiry action",
"label.remainingdays": "Lease",
"label.leased": "Leased",
"message.acquire.ip.failed": "Failed to acquire IP.", "message.acquire.ip.failed": "Failed to acquire IP.",
"message.action.acquire.ip": "Please confirm that you want to acquire new IP.", "message.action.acquire.ip": "Please confirm that you want to acquire new IP.",
"message.action.cancel.maintenance": "Your host has been successfully canceled for maintenance. This process can take up to several minutes.", "message.action.cancel.maintenance": "Your host has been successfully canceled for maintenance. This process can take up to several minutes.",

View File

@ -151,6 +151,13 @@
<div>{{ $toLocaleDate(dataResource[item]) }}</div> <div>{{ $toLocaleDate(dataResource[item]) }}</div>
</div> </div>
</a-list-item> </a-list-item>
<a-list-item v-else-if="item === 'leaseexpirydate' && dataResource[item]">
<div>
<strong>{{ $t('label.' + item.replace('date', '.date.and.time'))}}</strong>
<br/>
<div>{{ $toLocaleDate(dataResource[item]) }}</div>
</div>
</a-list-item>
<a-list-item v-else-if="item === 'details' && $route.meta.name === 'storagepool' && dataResource[item].rbd_default_data_pool"> <a-list-item v-else-if="item === 'details' && $route.meta.name === 'storagepool' && dataResource[item].rbd_default_data_pool">
<div> <div>
<strong>{{ $t('label.data.pool') }}</strong> <strong>{{ $t('label.data.pool') }}</strong>
@ -219,6 +226,9 @@ export default {
items.push('startdate') items.push('startdate')
items.push('enddate') items.push('enddate')
} }
if (this.$route.meta.name === 'vm') {
items.push('leaseexpirydate')
}
return items return items
}, },
vnfAccessMethods () { vnfAccessMethods () {

View File

@ -96,6 +96,9 @@
<a-tag v-if="resource.archived" :color="this.$config.theme['@warning-color']"> <a-tag v-if="resource.archived" :color="this.$config.theme['@warning-color']">
{{ $t('label.archived') }} {{ $t('label.archived') }}
</a-tag> </a-tag>
<a-tag v-if="resource.leaseduration != undefined">
{{ $t('label.remainingdays') + ': ' + (resource.leaseduration > -1 ? resource.leaseduration + 'd' : 'Over') }}
</a-tag>
<a-tooltip placement="right" > <a-tooltip placement="right" >
<template #title> <template #title>
<span>{{ $t('label.view.console') }}</span> <span>{{ $t('label.view.console') }}</span>
@ -226,6 +229,27 @@
</span> </span>
</div> </div>
</div> </div>
<div class="resource-detail-item" v-if="'leaseduration' in resource && resource.leaseduration !== undefined">
<div class="resource-detail-item__label">{{ $t('label.leaseduration') }}</div>
<div class="resource-detail-item__details">
<field-time-outlined
:style="{
color: $store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#888' },
fontSize: '20px'
}"/>
{{ resource.leaseduration + ' ' + $t('label.days') }}
</div>
</div>
<div class="resource-detail-item" v-if="'leaseexpiryaction' in resource && resource.leaseexpiryaction !== undefined">
<div class="resource-detail-item__label">{{ $t('label.leaseexpiryaction') }}</div>
<div class="resource-detail-item__details">
<font-awesome-icon
:icon="['fa-solid', 'fa-circle-xmark']"
class="anticon"
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#888' }]" />
{{ resource.leaseexpiryaction }}
</div>
</div>
<div class="resource-detail-item" v-if="'memory' in resource"> <div class="resource-detail-item" v-if="'memory' in resource">
<div class="resource-detail-item__label">{{ $t('label.memory') }}</div> <div class="resource-detail-item__label">{{ $t('label.memory') }}</div>
<div class="resource-detail-item__details"> <div class="resource-detail-item__details">

View File

@ -65,7 +65,6 @@
<span v-else :style="{ 'margin-right': record.ostypename ? '5px' : '0' }"> <span v-else :style="{ 'margin-right': record.ostypename ? '5px' : '0' }">
<os-logo v-if="record.ostypename" :osName="record.ostypename" size="xl" /> <os-logo v-if="record.ostypename" :osName="record.ostypename" size="xl" />
</span> </span>
<span v-if="record.hasannotations"> <span v-if="record.hasannotations">
<span v-if="record.id"> <span v-if="record.id">
<router-link :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link> <router-link :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
@ -101,6 +100,20 @@
</a-tooltip> </a-tooltip>
</span> </span>
</span> </span>
<span
v-if="record.leaseduration !== undefined"
:style="{
'margin-right': '5px',
'float': 'right'}">
<a-tooltip>
<template #title>{{ $t('label.remainingdays') + ": " + getRemainingLeaseText(record.leaseduration) }}</template>
<field-time-outlined
:style="{
color: getLeaseColor(record.leaseduration),
fontSize: '20px'
}"/>
</a-tooltip>
</span>
</span> </span>
</template> </template>
<template v-if="column.key === 'templatetype'"> <template v-if="column.key === 'templatetype'">
@ -1070,6 +1083,24 @@ export default {
} }
}) })
} }
},
getRemainingLeaseText (leaseDuration) {
if (leaseDuration > 0) {
return leaseDuration + (leaseDuration === 1 ? ' day' : ' days')
} else if (leaseDuration === 0) {
return 'expiring today'
} else {
return 'over'
}
},
getLeaseColor (leaseDuration) {
if (leaseDuration >= 7) {
return '#888'
} else if (leaseDuration >= 0) {
return '#ffbf00'
} else {
return '#fd7e14'
}
} }
} }
} }

View File

@ -44,6 +44,9 @@ export default {
if (!(store.getters.project && store.getters.project.id)) { if (!(store.getters.project && store.getters.project.id)) {
filters.unshift('self') filters.unshift('self')
} }
if (store.getters.features.instanceleaseenabled) {
filters.push('leased')
}
return filters return filters
}, },
columns: () => { columns: () => {
@ -83,7 +86,7 @@ export default {
var fields = ['name', 'displayname', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', var fields = ['name', 'displayname', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename',
'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'arch', 'boottype', 'bootmode', 'account', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'arch', 'boottype', 'bootmode', 'account',
'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy', 'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy',
'hostcontrolstate', 'deleteprotection'] 'hostcontrolstate', 'deleteprotection', 'leaseexpirydate', 'leaseexpiryaction']
const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true) const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true)
if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) { if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) {
return fields return fields

View File

@ -40,7 +40,7 @@ export default {
filters: ['active', 'inactive'], filters: ['active', 'inactive'],
columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'domain', 'zone', 'order'], columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'domain', 'zone', 'order'],
details: () => { details: () => {
var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources'] var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'leaseexpiryaction']
if (store.getters.apis.createServiceOffering && if (store.getters.apis.createServiceOffering &&
store.getters.apis.createServiceOffering.params.filter(x => x.name === 'storagepolicy').length > 0) { store.getters.apis.createServiceOffering.params.filter(x => x.name === 'storagepolicy').length > 0) {
fields.splice(6, 0, 'vspherestoragepolicy') fields.splice(6, 0, 'vspherestoragepolicy')

View File

@ -1724,6 +1724,7 @@ export default {
delete query.domainid delete query.domainid
delete query.state delete query.state
delete query.annotationfilter delete query.annotationfilter
delete query.leased
if (this.$route.name === 'template') { if (this.$route.name === 'template') {
query.templatefilter = filter query.templatefilter = filter
} else if (this.$route.name === 'iso') { } else if (this.$route.name === 'iso') {
@ -1773,6 +1774,8 @@ export default {
query.domainid = this.$store.getters.userInfo.domainid query.domainid = this.$store.getters.userInfo.domainid
} else if (['running', 'stopped'].includes(filter)) { } else if (['running', 'stopped'].includes(filter)) {
query.state = filter query.state = filter
} else if (filter === 'leased') {
query.leased = true
} }
} else if (this.$route.name === 'comment') { } else if (this.$route.name === 'comment') {
query.annotationfilter = filter query.annotationfilter = filter

View File

@ -603,6 +603,34 @@
@change="val => { dynamicscalingenabled = val }"/> @change="val => { dynamicscalingenabled = val }"/>
</a-form-item> </a-form-item>
</a-form-item> </a-form-item>
<a-form-item name="showLeaseOptions" ref="showLeaseOptions" v-if="isLeaseFeatureEnabled">
<template #label>
<tooltip-label :title="$t('label.lease.enable')" :tooltip="$t('label.lease.enable.tooltip')"/>
</template>
<a-switch v-model:checked="showLeaseOptions" @change="onToggleLeaseData"/>
</a-form-item>
<a-row :gutter="12" v-if="isLeaseFeatureEnabled && showLeaseOptions">
<a-col :md="12" :lg="12">
<a-form-item name="leaseduration" ref="leaseduration">
<template #label>
<tooltip-label :title="$t('label.leaseduration')" />
</template>
<a-input
v-model:value="form.leaseduration"
:placeholder="$t('label.instance.lease.placeholder')"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="leaseexpiryaction" ref="leaseexpiryaction">
<template #label>
<tooltip-label :title="$t('label.leaseexpiryaction')" />
</template>
<a-select v-model:value="form.leaseexpiryaction" :defaultValue="leaseexpiryaction">
<a-select-option v-for="action in expiryActions" :key="action" :label="action" />
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item :label="$t('label.userdata')"> <a-form-item :label="$t('label.userdata')">
<a-card> <a-card>
<div v-if="this.template && this.template.userdataid"> <div v-if="this.template && this.template.userdataid">
@ -1101,6 +1129,17 @@ export default {
description: 'ARM 64 bits (aarch64)' description: 'ARM 64 bits (aarch64)'
} }
] ]
},
isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled,
showLeaseOptions: false,
leaseduration: -1,
leaseexpiryaction: undefined,
expiryActions: ['STOP', 'DESTROY'],
defaultLeaseDuration: 90,
defaultLeaseExpiryAction: 'STOP',
naturalNumberRule: {
type: 'number',
validator: this.validateNumber
} }
} }
}, },
@ -1582,6 +1621,10 @@ export default {
if (this.sshKeyPairs && this.sshKeyPairs.length > 0) { if (this.sshKeyPairs && this.sshKeyPairs.length > 0) {
this.vm.keypairs = this.sshKeyPairs this.vm.keypairs = this.sshKeyPairs
} }
if (this.leaseduration < 1) {
this.vm.leaseduration = undefined
}
} }
} }
}, },
@ -1618,7 +1661,8 @@ export default {
this.form = reactive({}) this.form = reactive({})
this.rules = reactive({ this.rules = reactive({
zoneid: [{ required: true, message: `${this.$t('message.error.select')}` }], zoneid: [{ required: true, message: `${this.$t('message.error.select')}` }],
hypervisor: [{ required: true, message: `${this.$t('message.error.select')}` }] hypervisor: [{ required: true, message: `${this.$t('message.error.select')}` }],
leaseduration: [this.naturalNumberRule]
}) })
if (this.zoneSelected) { if (this.zoneSelected) {
@ -2193,6 +2237,12 @@ export default {
if (values.group) { if (values.group) {
deployVmData.group = values.group deployVmData.group = values.group
} }
if (values.leaseduration) {
deployVmData.leaseduration = values.leaseduration
}
if (values.leaseexpiryaction) {
deployVmData.leaseexpiryaction = values.leaseexpiryaction
}
// step 8: enter setup // step 8: enter setup
if ('properties' in values) { if ('properties' in values) {
const keys = Object.keys(values.properties) const keys = Object.keys(values.properties)
@ -2845,6 +2895,16 @@ export default {
this.rootDiskSizeFixed = offering.rootdisksize this.rootDiskSizeFixed = offering.rootdisksize
this.showRootDiskSizeChanger = false this.showRootDiskSizeChanger = false
} }
if (this.isLeaseFeatureEnabled) {
if (offering && offering.leaseduration > 0) {
this.showLeaseOptions = true
} else {
this.showLeaseOptions = false
}
this.onToggleLeaseData()
}
this.form.rootdisksizeitem = this.showRootDiskSizeChanger && this.rootDiskSizeFixed > 0 this.form.rootdisksizeitem = this.showRootDiskSizeChanger && this.rootDiskSizeFixed > 0
this.formModel = toRaw(this.form) this.formModel = toRaw(this.form)
}, },
@ -2888,6 +2948,23 @@ export default {
parent.$message.success(parent.$t('label.copied.clipboard')) parent.$message.success(parent.$t('label.copied.clipboard'))
} }
}) })
},
onToggleLeaseData () {
if (this.showLeaseOptions === false) {
this.leaseduration = -1
this.leaseexpiryaction = undefined
} else {
this.leaseduration = this.serviceOffering.leaseduration ? this.serviceOffering.leaseduration : this.defaultLeaseDuration
this.leaseexpiryaction = this.serviceOffering.leaseexpiryaction ? this.serviceOffering.leaseexpiryaction : this.defaultLeaseExpiryAction
}
this.form.leaseduration = this.leaseduration
this.form.leaseexpiryaction = this.leaseexpiryaction
},
async validateNumber (rule, value) {
if (value && (isNaN(value) || value <= 0)) {
return Promise.reject(this.$t('message.error.number'))
}
return Promise.resolve()
} }
} }
} }
@ -2940,7 +3017,7 @@ export default {
.vm-info-card { .vm-info-card {
.ant-card-body { .ant-card-body {
min-height: 250px; min-height: 250px;
max-height: calc(100vh - 150px); max-height: calc(100vh - 140px);
overflow-y: auto; overflow-y: auto;
scroll-behavior: smooth; scroll-behavior: smooth;
} }

View File

@ -117,6 +117,34 @@
</template> </template>
<a-switch v-model:checked="form.deleteprotection" /> <a-switch v-model:checked="form.deleteprotection" />
</a-form-item> </a-form-item>
<a-form-item name="showLeaseOptions" ref="showLeaseOptions" v-if="isLeaseEditable">
<template #label>
<tooltip-label :title="$t('label.lease.enable')" :tooltip="$t('label.lease.enable.tooltip')" />
</template>
<a-switch v-model:checked="showLeaseOptions" @change="onToggleLeaseData"/>
</a-form-item>
<a-row :gutter="12" v-if="showLeaseOptions">
<a-col :md="12" :lg="12">
<a-form-item name="leaseduration" ref="leaseduration">
<template #label>
<tooltip-label :title="$t('label.leaseduration')" />
</template>
<a-input
v-model:value="form.leaseduration"
:placeholder="$t('label.instance.lease.placeholder')"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="leaseexpiryaction" ref="leaseexpiryaction">
<template #label>
<tooltip-label :title="$t('label.leaseexpiryaction')" />
</template>
<a-select v-model:value="form.leaseexpiryaction" :defaultValue="expiryActions">
<a-select-option v-for="action in expiryActions" :key="action" :label="action" />
</a-select>
</a-form-item>
</a-col>
</a-row>
<div :span="24" class="action-button"> <div :span="24" class="action-button">
<a-button :loading="loading" @click="onCloseAction">{{ $t('label.cancel') }}</a-button> <a-button :loading="loading" @click="onCloseAction">{{ $t('label.cancel') }}</a-button>
@ -165,6 +193,15 @@ export default {
groups: { groups: {
loading: false, loading: false,
opts: [] opts: []
},
isLeaseEditable: this.$store.getters.features.instanceleaseenabled && this.resource.leaseduration > -1,
showLeaseOptions: false,
leaseduration: this.resource.leaseduration === undefined ? 90 : this.resource.leaseduration,
leaseexpiryaction: this.resource.leaseexpiryaction === undefined ? 'STOP' : this.resource.leaseexpiryaction,
expiryActions: ['STOP', 'DESTROY'],
naturalNumberRule: {
type: 'number',
validator: this.validateNumber
} }
} }
}, },
@ -186,9 +223,14 @@ export default {
deleteprotection: this.resource.deleteprotection, deleteprotection: this.resource.deleteprotection,
group: this.resource.group, group: this.resource.group,
userdata: '', userdata: '',
haenable: this.resource.haenable haenable: this.resource.haenable,
leaseduration: this.resource.leaseduration,
leaseexpiryaction: this.resource.leaseexpiryaction
}) })
this.rules = reactive({}) this.rules = reactive({
leaseduration: [this.naturalNumberRule]
})
this.showLeaseOptions = this.isLeaseEditable
}, },
fetchData () { fetchData () {
this.fetchZoneDetails() this.fetchZoneDetails()
@ -327,7 +369,6 @@ export default {
}) })
}) })
}, },
handleSubmit () { handleSubmit () {
this.formRef.value.validate().then(() => { this.formRef.value.validate().then(() => {
const values = toRaw(this.form) const values = toRaw(this.form)
@ -354,6 +395,12 @@ export default {
if (values.userdata && values.userdata.length > 0) { if (values.userdata && values.userdata.length > 0) {
params.userdata = this.$toBase64AndURIEncoded(values.userdata) params.userdata = this.$toBase64AndURIEncoded(values.userdata)
} }
if (values.leaseduration !== undefined && (values.leaseduration === -1 || values.leaseduration > 0)) {
params.leaseduration = values.leaseduration
if (values.leaseexpiryaction !== undefined) {
params.leaseexpiryaction = values.leaseexpiryaction
}
}
this.loading = true this.loading = true
api('updateVirtualMachine', {}, 'POST', params).then(json => { api('updateVirtualMachine', {}, 'POST', params).then(json => {
@ -372,6 +419,21 @@ export default {
}, },
onCloseAction () { onCloseAction () {
this.$emit('close-action') this.$emit('close-action')
},
onToggleLeaseData () {
if (this.showLeaseOptions === false) {
this.form.leaseduration = -1
this.form.leaseexpiryaction = undefined
} else {
this.form.leaseduration = this.leaseduration
this.form.leaseexpiryaction = this.leaseexpiryaction
}
},
async validateNumber (rule, value) {
if (value && (isNaN(value) || value <= 0)) {
return Promise.reject(this.$t('message.error.number'))
}
return Promise.resolve()
} }
} }
} }

View File

@ -36,6 +36,23 @@
<template v-if="column.key === 'cpu'"><appstore-outlined /> {{ $t('label.cpu') }}</template> <template v-if="column.key === 'cpu'"><appstore-outlined /> {{ $t('label.cpu') }}</template>
<template v-if="column.key === 'ram'"><bulb-outlined /> {{ $t('label.memory') }}</template> <template v-if="column.key === 'ram'"><bulb-outlined /> {{ $t('label.memory') }}</template>
</template> </template>
<template #displayText="{ record }">
<span>{{ record.name }}</span>
<span
v-if="record.leaseduration !== undefined"
:style="{
'margin-right': '10px',
'float': 'right'}">
<a-tooltip>
<template #title>{{ $t('label.remainingdays') + ": " + getRemainingLeaseText(record.leaseduration) }}</template>
<field-time-outlined
:style="{
color: $store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#888' },
fontSize: '20px'
}"/>
</a-tooltip>
</span>
</template>
</a-table> </a-table>
<div style="display: block; text-align: right;"> <div style="display: block; text-align: right;">
@ -119,7 +136,8 @@ export default {
key: 'name', key: 'name',
dataIndex: 'name', dataIndex: 'name',
title: this.$t('label.serviceofferingid'), title: this.$t('label.serviceofferingid'),
width: '40%' width: '40%',
slots: { customRender: 'displayText' }
}, },
{ {
key: 'cpu', key: 'cpu',
@ -191,7 +209,8 @@ export default {
name: item.name, name: item.name,
cpu: cpuNumberValue.length > 0 ? `${cpuNumberValue} CPU x ${cpuSpeedValue} Ghz` : '', cpu: cpuNumberValue.length > 0 ? `${cpuNumberValue} CPU x ${cpuSpeedValue} Ghz` : '',
ram: ramValue.length > 0 ? `${ramValue} MB` : '', ram: ramValue.length > 0 ? `${ramValue} MB` : '',
disabled: disabled disabled: disabled,
leaseduration: item.leaseduration
} }
}) })
}, },
@ -269,6 +288,15 @@ export default {
this.$emit('select-compute-item', record.key) this.$emit('select-compute-item', record.key)
} }
} }
},
getRemainingLeaseText (leaseDuration) {
if (leaseDuration > 0) {
return leaseDuration + (leaseDuration === 1 ? ' day' : ' days')
} else if (leaseDuration === 0) {
return 'expiring today'
} else {
return 'over'
}
} }
} }
} }

View File

@ -63,6 +63,18 @@
</a-statistic> </a-statistic>
</router-link> </router-link>
</a-col> </a-col>
<a-col :span="12" v-if="'listVirtualMachines' in $store.getters.apis && isLeaseFeatureEnabled">
<router-link :to="{ path: '/vm', query: { leased: true } }">
<a-statistic
:title="$t('label.leasedinstances')"
:value="data.leasedinstances"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<field-time-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12" v-if="'listKubernetesClusters' in $store.getters.apis"> <a-col :span="12" v-if="'listKubernetesClusters' in $store.getters.apis">
<router-link :to="{ path: '/kubernetes' }"> <router-link :to="{ path: '/kubernetes' }">
<a-statistic <a-statistic
@ -393,8 +405,10 @@ export default {
networks: 0, networks: 0,
vpcs: 0, vpcs: 0,
ips: 0, ips: 0,
templates: 0 templates: 0,
} leasedinstances: 0
},
isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled
} }
}, },
computed: { computed: {
@ -556,6 +570,15 @@ export default {
this.loading = false this.loading = false
this.data.stopped = json?.listvirtualmachinesresponse?.count this.data.stopped = json?.listvirtualmachinesresponse?.count
}) })
if (this.isLeaseFeatureEnabled) {
api('listVirtualMachines', { leased: true, listall: true, details: 'min', page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.leasedinstances = json?.listvirtualmachinesresponse?.count
if (!this.data.leasedinstances) {
this.data.leasedinstances = 0
}
})
}
}, },
listEvents () { listEvents () {
if (!('listEvents' in this.$store.getters.apis)) { if (!('listEvents' in this.$store.getters.apis)) {

View File

@ -349,6 +349,34 @@
</template> </template>
<a-switch v-model:checked="form.purgeresources"/> <a-switch v-model:checked="form.purgeresources"/>
</a-form-item> </a-form-item>
<a-form-item name="showLeaseOptions" ref="showLeaseOptions" v-if="isLeaseFeatureEnabled">
<template #label>
<tooltip-label :title="$t('label.lease.enable')" :tooltip="$t('label.lease.enable.tooltip')" />
</template>
<a-switch v-model:checked="showLeaseOptions" @change="onToggleLeaseData"/>
</a-form-item>
<a-row :gutter="12" v-if="isLeaseFeatureEnabled && showLeaseOptions">
<a-col :md="12" :lg="12">
<a-form-item name="leaseduration" ref="leaseduration">
<template #label>
<tooltip-label :title="$t('label.leaseduration')"/>
</template>
<a-input
v-model:value="form.leaseduration"
:placeholder="$t('label.instance.lease.placeholder')"/>
</a-form-item>
</a-col>
<a-col :md="12" :lg="12">
<a-form-item name="leaseexpiryaction" ref="leaseexpiryaction" v-if="form.leaseduration > 0">
<template #label>
<tooltip-label :title="$t('label.leaseexpiryaction')" />
</template>
<a-select v-model:value="form.leaseexpiryaction" :defaultValue="expiryActions">
<a-select-option v-for="action in expiryActions" :key="action" :label="action"/>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item name="computeonly" ref="computeonly"> <a-form-item name="computeonly" ref="computeonly">
<template #label> <template #label>
<tooltip-label :title="$t('label.computeonly.offering')" :tooltip="$t('label.computeonly.offering.tooltip')"/> <tooltip-label :title="$t('label.computeonly.offering')" :tooltip="$t('label.computeonly.offering.tooltip')"/>
@ -695,7 +723,14 @@ export default {
diskOfferings: [], diskOfferings: [],
selectedDiskOfferingId: '', selectedDiskOfferingId: '',
qosType: '', qosType: '',
isDomainAdminAllowedToInformTags: false isDomainAdminAllowedToInformTags: false,
isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled,
showLeaseOptions: false,
expiryActions: ['STOP', 'DESTROY'],
defaultLeaseDuration: 90,
defaultLeaseExpiryAction: 'STOP',
leaseduration: undefined,
leaseexpiryaction: undefined
} }
}, },
beforeCreate () { beforeCreate () {
@ -734,7 +769,9 @@ export default {
iscustomizeddiskiops: this.isCustomizedDiskIops, iscustomizeddiskiops: this.isCustomizedDiskIops,
diskofferingid: this.selectedDiskOfferingId, diskofferingid: this.selectedDiskOfferingId,
diskofferingstrictness: this.diskofferingstrictness, diskofferingstrictness: this.diskofferingstrictness,
encryptdisk: this.encryptdisk encryptdisk: this.encryptdisk,
leaseduration: this.leaseduration,
leaseexpiryaction: this.leaseexpiryaction
}) })
this.rules = reactive({ this.rules = reactive({
name: [{ required: true, message: this.$t('message.error.required.input') }], name: [{ required: true, message: this.$t('message.error.required.input') }],
@ -785,7 +822,8 @@ export default {
} }
return Promise.resolve() return Promise.resolve()
} }
}] }],
leaseduration: [this.naturalNumberRule]
}) })
}, },
fetchData () { fetchData () {
@ -965,7 +1003,9 @@ export default {
dynamicscalingenabled: values.dynamicscalingenabled, dynamicscalingenabled: values.dynamicscalingenabled,
diskofferingstrictness: values.diskofferingstrictness, diskofferingstrictness: values.diskofferingstrictness,
encryptroot: values.encryptdisk, encryptroot: values.encryptdisk,
purgeresources: values.purgeresources purgeresources: values.purgeresources,
leaseduration: values.leaseduration,
leaseexpiryaction: values.leaseexpiryaction
} }
if (values.diskofferingid) { if (values.diskofferingid) {
params.diskofferingid = values.diskofferingid params.diskofferingid = values.diskofferingid
@ -1061,6 +1101,15 @@ export default {
if ('systemvmtype' in values && values.systemvmtype !== undefined) { if ('systemvmtype' in values && values.systemvmtype !== undefined) {
params.systemvmtype = values.systemvmtype params.systemvmtype = values.systemvmtype
} }
if ('leaseduration' in values && values.leaseduration !== undefined) {
params.leaseduration = values.leaseduration
}
if ('leaseexpiryaction' in values && values.leaseexpiryaction !== undefined) {
params.leaseexpiryaction = values.leaseexpiryaction
}
if (values.ispublic !== true) { if (values.ispublic !== true) {
var domainIndexes = values.domainid var domainIndexes = values.domainid
var domainId = null var domainId = null
@ -1112,6 +1161,17 @@ export default {
return Promise.reject(this.$t('message.error.number')) return Promise.reject(this.$t('message.error.number'))
} }
return Promise.resolve() return Promise.resolve()
},
onToggleLeaseData () {
if (this.showLeaseOptions === false) {
this.leaseduration = undefined
this.leaseexpiryaction = undefined
} else {
this.leaseduration = this.leaseduration !== undefined ? this.leaseduration : this.defaultLeaseDuration
this.leaseexpiryaction = this.leaseexpiryaction !== undefined ? this.leaseexpiryaction : this.defaultLeaseExpiryAction
}
this.form.leaseduration = this.leaseduration
this.form.leaseexpiryaction = this.leaseexpiryaction
} }
} }
} }