diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceffb42c79bb..8026bb2f4a00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,7 +164,8 @@ jobs: component/test_cpu_limits component/test_cpu_max_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_invalid_gw_nm component/test_ip_reservation", diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 815bd2363d5a..9c05b253a044 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -795,6 +795,11 @@ public class EventTypes { // Resource Limit 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 { // TODO: need a way to force author adding event types to declare the entity details as well, with out braking @@ -1289,6 +1294,12 @@ public class EventTypes { entityEventDetails.put(EVENT_SHAREDFS_DESTROY, SharedFS.class); entityEventDetails.put(EVENT_SHAREDFS_EXPUNGE, 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) { diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index 29803d5271b4..722d5f49e525 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -101,4 +101,8 @@ public interface VmDetailConstants { String VMWARE_HOST_NAME = String.format("%s-host", VMWARE_TO_KVM_PREFIX); String VMWARE_DISK = String.format("%s-disk", VMWARE_TO_KVM_PREFIX); String VMWARE_MAC_ADDRESSES = String.format("%s-mac-addresses", VMWARE_TO_KVM_PREFIX); + + String INSTANCE_LEASE_EXPIRY_DATE = "leaseexpirydate"; + String INSTANCE_LEASE_EXPIRY_ACTION = "leaseexpiryaction"; + String INSTANCE_LEASE_EXECUTION = "leaseactionexecution"; } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 627e7395e1e1..fa9a3c970a09 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -269,6 +269,7 @@ public class ApiConstants { public static final String INTERNAL_DNS2 = "internaldns2"; public static final String INTERNET_PROTOCOL = "internetprotocol"; public static final String INTERVAL_TYPE = "intervaltype"; + public static final String INSTANCE_LEASE_ENABLED = "instanceleaseenabled"; public static final String LOCATION_TYPE = "locationtype"; public static final String IOPS_READ_RATE = "iopsreadrate"; public static final String IOPS_READ_RATE_MAX = "iopsreadratemax"; @@ -520,6 +521,10 @@ public class ApiConstants { public static final String USED_SUBNETS = "usedsubnets"; public static final String USED_IOPS = "usediops"; public static final String USER_DATA = "userdata"; + public static final String INSTANCE_LEASE_DURATION = "leaseduration"; + public static final String INSTANCE_LEASE_EXPIRY_DATE= "leaseexpirydate"; + public static final String INSTANCE_LEASE_EXPIRY_ACTION = "leaseexpiryaction"; + public static final String LEASED = "leased"; public static final String USER_DATA_NAME = "userdataname"; public static final String USER_DATA_ID = "userdataid"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java index 8f6d5413d72d..af6a04766962 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java @@ -251,7 +251,15 @@ public class CreateServiceOfferingCmd extends BaseCmd { since="4.20") 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 /////////////////////// @@ -487,6 +495,14 @@ public boolean getEncryptRoot() { return false; } + public String getLeaseExpiryAction() { + return leaseExpiryAction; + } + + public Integer getLeaseDuration() { + return leaseDuration; + } + public boolean isPurgeResources() { return Boolean.TRUE.equals(purgeResources); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java index 0cecbb370202..77a7a7fd8eac 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java @@ -72,6 +72,7 @@ public void execute() { response.setInstancesDisksStatsRetentionTime((Integer) capabilities.get(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME)); response.setSharedFsVmMinCpuCount((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT)); response.setSharedFsVmMinRamSize((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE)); + response.setInstanceLeaseEnabled((Boolean) capabilities.get(ApiConstants.INSTANCE_LEASE_ENABLED)); response.setObjectName("capability"); response.setResponseName(getCommandName()); this.setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index 52d42a95d981..557609dc1474 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -16,17 +16,24 @@ // under the License. package org.apache.cloudstack.api.command.user.vm; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import javax.annotation.Nonnull; - +import com.cloud.agent.api.LogLevel; +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InsufficientServerCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +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.affinity.AffinityGroupResponse; import org.apache.cloudstack.api.ACL; @@ -58,24 +65,15 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; -import com.cloud.agent.api.LogLevel; -import com.cloud.event.EventTypes; -import com.cloud.exception.ConcurrentOperationException; -import com.cloud.exception.InsufficientCapacityException; -import com.cloud.exception.InsufficientServerCapacityException; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.exception.ResourceAllocationException; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.hypervisor.Hypervisor.HypervisorType; -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 javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; @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) @@ -278,6 +276,14 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG description = "Enable packed virtqueues or not.") 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 /////////////////////// ///////////////////////////////////////////////////// @@ -475,6 +481,14 @@ public String getPassword() { return password; } + public Integer getLeaseDuration() { + return leaseDuration; + } + + public String getLeaseExpiryAction() { + return leaseExpiryAction; + } + public List getNetworkIds() { if (MapUtils.isNotEmpty(vAppNetworks)) { if (CollectionUtils.isNotEmpty(networkIds) || ipAddress != null || getIp6Address() != null || MapUtils.isNotEmpty(ipToNetworkList)) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java index ac180b6d4569..c71c3edd1f6e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java @@ -153,6 +153,11 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements @Parameter(name = ApiConstants.USER_DATA_ID, type = CommandType.UUID, entityType = UserDataResponse.class, required = false, description = "the instances by userdata", since = "4.20.1") private Long userdataId; + @Parameter(name = ApiConstants.LEASED, type = CommandType.BOOLEAN, + description = "Whether to return only leased instances", + since = "4.21.0") + private Boolean onlyLeasedInstances = false; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -330,4 +335,8 @@ protected void updateVMResponse(List response) { vmResponse.setResourceIconResponse(iconResponse); } } + + public boolean getOnlyLeasedInstances() { + return BooleanUtils.toBoolean(onlyLeasedInstances); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java index 0f5dade96d25..519cb7995d3d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java @@ -16,20 +16,18 @@ // under the License. package org.apache.cloudstack.api.command.user.vm; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; import com.cloud.utils.exception.CloudRuntimeException; - -import org.apache.cloudstack.api.ApiArgValidator; -import org.apache.cloudstack.api.response.UserDataResponse; - +import com.cloud.utils.net.Dhcp; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -40,15 +38,14 @@ import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.response.GuestOSResponse; 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.context.CallContext; -import com.cloud.exception.InsufficientCapacityException; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.user.Account; -import com.cloud.uservm.UserVm; -import com.cloud.utils.net.Dhcp; -import com.cloud.vm.VirtualMachine; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @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. " + @@ -154,6 +151,14 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction, " autoscaling groups or CKS, delete protection will be ignored.") private Boolean deleteProtection; + @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 /////////////////////// ///////////////////////////////////////////////////// @@ -324,4 +329,13 @@ public Long getApiResourceId() { public ApiCommandResourceType getApiResourceType() { return ApiCommandResourceType.VirtualMachine; } + + public Integer getLeaseDuration() { + return leaseDuration; + } + + public String getLeaseExpiryAction() { + return leaseExpiryAction; + } + } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java index 3861ac455ed5..74dbfa15a431 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java @@ -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") 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) { this.securityGroupsEnabled = securityGroupsEnabled; } @@ -247,4 +251,8 @@ public void setSharedFsVmMinCpuCount(Integer sharedFsVmMinCpuCount) { public void setSharedFsVmMinRamSize(Integer sharedFsVmMinRamSize) { this.sharedFsVmMinRamSize = sharedFsVmMinRamSize; } + + public void setInstanceLeaseEnabled(Boolean instanceLeaseEnabled) { + this.instanceLeaseEnabled = instanceLeaseEnabled; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java index 0622b936f6e0..7ac8166d2e6f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java @@ -238,6 +238,14 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations { @Param(description = "Whether to cleanup VM and its associated resource upon expunge", since = "4.20") private Boolean purgeResources; + @SerializedName(ApiConstants.INSTANCE_LEASE_DURATION) + @Param(description = "Instance lease duration 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() { } @@ -505,6 +513,22 @@ public void setCacheMode(String 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() { return vsphereStoragePolicy; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index 1f4b493fba2f..5caece9ae6f0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -16,6 +16,18 @@ // under the License. package org.apache.cloudstack.api.response; +import com.cloud.network.router.VirtualRouter; +import com.cloud.serializer.Param; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.affinity.AffinityGroupResponse; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponseWithTagInformation; +import org.apache.cloudstack.api.EntityReference; +import org.apache.commons.collections.CollectionUtils; + import java.util.ArrayList; import java.util.Comparator; import java.util.Date; @@ -26,19 +38,6 @@ import java.util.Set; import java.util.TreeSet; -import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.affinity.AffinityGroupResponse; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseResponseWithTagInformation; -import org.apache.cloudstack.api.EntityReference; - -import com.cloud.network.router.VirtualRouter; -import com.cloud.serializer.Param; -import com.cloud.uservm.UserVm; -import com.cloud.vm.VirtualMachine; -import com.google.gson.annotations.SerializedName; -import org.apache.commons.collections.CollectionUtils; - @SuppressWarnings("unused") @EntityReference(value = {VirtualMachine.class, UserVm.class, VirtualRouter.class}) public class UserVmResponse extends BaseResponseWithTagInformation implements ControlledEntityResponse, SetResourceIconResponse { @@ -392,10 +391,22 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "VNF details", since = "4.19.0") private Map vnfDetails; - @SerializedName((ApiConstants.VM_TYPE)) + @SerializedName(ApiConstants.VM_TYPE) @Param(description = "User VM type", since = "4.20.0") private String vmType; + @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() { securityGroupList = new LinkedHashSet<>(); nics = new TreeSet<>(Comparator.comparingInt(x -> Integer.parseInt(x.getDeviceId()))); @@ -1169,4 +1180,29 @@ public String getVmType() { public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; } + + 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; + } + } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java index 6daa5de07cbf..bb7a81ba2d31 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java @@ -55,4 +55,19 @@ public void testIsPurgeResourcesTrue() { ReflectionTestUtils.setField(createServiceOfferingCmd, "purgeResources", true); 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("stop", createServiceOfferingCmd.getLeaseExpiryAction()); + + ReflectionTestUtils.setField(createServiceOfferingCmd, "leaseExpiryAction", "DESTROY"); + Assert.assertEquals("DESTROY", createServiceOfferingCmd.getLeaseExpiryAction()); + } } diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql index c894429adf80..18e6231ef89a 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql @@ -71,6 +71,8 @@ SELECT `service_offering`.`dynamic_scaling_enabled` AS `dynamic_scaling_enabled`, `service_offering`.`disk_offering_strictness` AS `disk_offering_strictness`, `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.uuid)) AS domain_uuid, GROUP_CONCAT(DISTINCT(domain.name)) AS domain_name, @@ -109,5 +111,11 @@ FROM LEFT JOIN `cloud`.`service_offering_details` AS `vsphere_storage_policy` ON `vsphere_storage_policy`.`service_offering_id` = `service_offering`.`id` 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 `service_offering`.`id`; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql index 97cb7b735cfc..a3941cd83224 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql @@ -168,7 +168,10 @@ SELECT `user_data`.`uuid` AS `user_data_uuid`, `user_data`.`name` AS `user_data_name`, `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 (((((((((((((((((((((((((((((((((((`user_vm` JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) @@ -215,4 +218,10 @@ FROM LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`) AND (`custom_speed`.`name` = 'CpuSpeed')))) 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')))); diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index a6ea75b47fea..95a5631ff58d 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -162,6 +162,7 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.vm.lease.VMLeaseManagerImpl; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.EnumUtils; @@ -1330,6 +1331,11 @@ private Pair, Integer> searchForUserVMIdsAndCount(ListVMsCmd cmd) { } } + if (!VMLeaseManagerImpl.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 domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false); Long domainId = domainIdRecursiveListProject.first(); @@ -1483,6 +1489,14 @@ private Pair, Integer> searchForUserVMIdsAndCount(ListVMsCmd cmd) { userVmSearchBuilder.join("tags", resourceTagSearch, resourceTagSearch.entity().getResourceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); } + if (cmd.getOnlyLeasedInstances()) { + SearchBuilder 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("PENDING"); + userVmSearchBuilder.join("userVmToLeased", leasedInstancesSearch, leasedInstancesSearch.entity().getResourceId(), + userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } + if (keyPairName != null) { SearchBuilder vmDetailSearchKeys = userVmDetailsDao.createSearchBuilder(); SearchBuilder vmDetailSearchVmIds = userVmDetailsDao.createSearchBuilder(); diff --git a/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java index d3c7a7decdea..e4b075e20c77 100644 --- a/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java @@ -33,6 +33,7 @@ import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.vm.lease.VMLeaseManagerImpl; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -176,6 +177,11 @@ public ServiceOfferingResponse newServiceOfferingResponse(ServiceOfferingJoinVO } } + if (VMLeaseManagerImpl.InstanceLeaseEnabled.value() && offering.getLeaseDuration() != null && offering.getLeaseDuration() > 0L) { + offeringResponse.setLeaseDuration(offering.getLeaseDuration()); + offeringResponse.setLeaseExpiryAction(offering.getLeaseExpiryAction()); + } + long rootDiskSizeInGb = (long) offering.getRootDiskSize() / GB_TO_BYTES; offeringResponse.setRootDiskSize(rootDiskSizeInGb); offeringResponse.setDiskOfferingStrictness(offering.getDiskOfferingStrictness()); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDao.java index 58b73096de80..79312460d2c0 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDao.java @@ -16,18 +16,17 @@ // under the License. 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.user.Account; import com.cloud.uservm.UserVm; import com.cloud.utils.db.GenericDao; 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 { @@ -46,4 +45,8 @@ UserVmResponse newUserVmResponse(ResponseView view, String objectName, UserVmJoi List listByAccountServiceOfferingTemplateAndNotInState(long accountId, List states, List offeringIds, List templateIds); + + List listEligibleInstancesWithExpiredLease(); + + List listLeaseInstancesExpiringInDays(int days); } diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 7e10df24e1b5..a0929e7454e9 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -16,38 +16,6 @@ // under the License. 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.ApiResponseHelper; import com.cloud.api.query.vo.UserVmJoinVO; @@ -84,6 +52,42 @@ import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicSecondaryIpVO; 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.VMLeaseManagerImpl; +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 public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation implements UserVmJoinDao { @@ -108,9 +112,13 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; private final SearchBuilder activeVmByIsoSearch; + private final SearchBuilder leaseExpiredInstanceSearch; + private final SearchBuilder remainingLeaseInDaysSearch; protected UserVmJoinDaoImpl() { @@ -124,6 +132,28 @@ protected UserVmJoinDaoImpl() { activeVmByIsoSearch.and("isoId", activeVmByIsoSearch.entity().getIsoId(), SearchCriteria.Op.EQ); activeVmByIsoSearch.and("stateNotIn", activeVmByIsoSearch.entity().getState(), SearchCriteria.Op.NIN); activeVmByIsoSearch.done(); + + leaseExpiredInstanceSearch = createSearchBuilder(); + leaseExpiredInstanceSearch.selectFields(leaseExpiredInstanceSearch.entity().getId(), leaseExpiredInstanceSearch.entity().getState(), + leaseExpiredInstanceSearch.entity().isDeleteProtection(), leaseExpiredInstanceSearch.entity().getUuid(), + leaseExpiredInstanceSearch.entity().getLeaseExpiryAction()); + + leaseExpiredInstanceSearch.and(leaseExpiredInstanceSearch.entity().getLeaseActionExecution(), Op.EQ).values("PENDING"); + 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().getUserId(), remainingLeaseInDaysSearch.entity().getDomainId(), + remainingLeaseInDaysSearch.entity().getAccountId(), remainingLeaseInDaysSearch.entity().getLeaseExpiryAction()); + + remainingLeaseInDaysSearch.and(remainingLeaseInDaysSearch.entity().getLeaseActionExecution(), Op.EQ).values("PENDING"); + remainingLeaseInDaysSearch.and("leaseCurrentDate", remainingLeaseInDaysSearch.entity().getLeaseExpiryDate(), Op.GTEQ); + remainingLeaseInDaysSearch.and("leaseExpiryEndDate", remainingLeaseInDaysSearch.entity().getLeaseExpiryDate(), Op.LT); + remainingLeaseInDaysSearch.done(); + } @Override @@ -426,10 +456,10 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us userVmResponse.setDynamicallyScalable(userVm.isDynamicallyScalable()); } - if (userVm.getDeleteProtection() == null) { + if (userVm.isDeleteProtection() == null) { userVmResponse.setDeleteProtection(false); } else { - userVmResponse.setDeleteProtection(userVm.getDeleteProtection()); + userVmResponse.setDeleteProtection(userVm.isDeleteProtection()); } if (userVm.getAutoScaleVmGroupName() != null) { @@ -446,6 +476,13 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us userVmResponse.setUserDataPolicy(userVm.getUserDataPolicy()); } + if (VMLeaseManagerImpl.InstanceLeaseEnabled.value() && userVm.getLeaseExpiryDate() != null && "PENDING".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); if (TemplateType.VNF.equals(userVm.getTemplateType()) && (details.contains(VMDetails.all) || details.contains(VMDetails.vnfnics))) { @@ -455,6 +492,13 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us 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) { List vnfNics = vnfTemplateNicDao.listByTemplateId(userVm.getTemplateId()); for (VnfTemplateNicVO nic : vnfNics) { @@ -717,4 +761,43 @@ public List listByAccountServiceOfferingTemplateAndNotInState(long sc.setParameters("displayVm", 1); 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 listEligibleInstancesWithExpiredLease() { + SearchCriteria sc = leaseExpiredInstanceSearch.create(); + sc.setParameters("leaseExpired", new Date()); + sc.setParameters("leaseExpiryActions", "STOP", "DESTROY"); + 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 listLeaseInstancesExpiringInDays(int days) { + SearchCriteria 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); + } } diff --git a/server/src/main/java/com/cloud/api/query/vo/ServiceOfferingJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/ServiceOfferingJoinVO.java index 01811c878fe5..4ecd5d1c3337 100644 --- a/server/src/main/java/com/cloud/api/query/vo/ServiceOfferingJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/ServiceOfferingJoinVO.java @@ -16,7 +16,11 @@ // under the License. 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 javax.persistence.Column; import javax.persistence.Entity; @@ -24,13 +28,7 @@ import javax.persistence.Enumerated; import javax.persistence.Id; import javax.persistence.Table; - -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; +import java.util.Date; @Entity @Table(name = "service_offering_view") @@ -221,6 +219,12 @@ public class ServiceOfferingJoinVO extends BaseViewVO implements InternalIdentit @Column(name = "encrypt_root") private boolean encryptRoot; + @Column(name = "lease_duration") + private Integer leaseDuration; + + @Column(name = "lease_expiry_action") + private String leaseExpiryAction; + public ServiceOfferingJoinVO() { } @@ -459,4 +463,12 @@ public String getDiskOfferingDisplayText() { } public boolean getEncryptRoot() { return encryptRoot; } + + public Integer getLeaseDuration() { + return leaseDuration; + } + + public String getLeaseExpiryAction() { + return leaseExpiryAction; + } } diff --git a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java index 701fa7d4f826..cdafbfe166c5 100644 --- a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java @@ -16,28 +16,14 @@ // under the License. package com.cloud.api.query.vo; -import java.net.URI; -import java.util.Date; -import java.util.Map; - -import javax.persistence.AttributeOverride; -import javax.persistence.Column; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Transient; - import com.cloud.host.Status; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.Network.GuestType; import com.cloud.network.Networks.TrafficType; import com.cloud.resource.ResourceState; import com.cloud.storage.Storage; -import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.Volume; import com.cloud.user.Account; import com.cloud.util.StoragePoolTypeConverter; @@ -46,6 +32,21 @@ import com.cloud.vm.VirtualMachine.State; import org.apache.cloudstack.util.HypervisorTypeConverter; +import javax.persistence.AttributeOverride; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Transient; +import java.net.URI; +import java.util.Date; +import java.util.Map; + @Entity @Table(name = "user_vm_view") @AttributeOverride( name="id", column = @Column(name = "id", updatable = false, nullable = false) ) @@ -439,6 +440,15 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro @Column(name = "delete_protection") protected Boolean deleteProtection; + @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() { // Empty constructor @@ -949,7 +959,7 @@ public Boolean isDynamicallyScalable() { return isDynamicallyScalable; } - public Boolean getDeleteProtection() { + public Boolean isDeleteProtection() { return deleteProtection; } @@ -977,4 +987,20 @@ public String getUserDataPolicy() { public String getUserDataDetails() { return userDataDetails; } + + public Date getLeaseExpiryDate() { + return leaseExpiryDate; + } + + public String getLeaseExpiryAction() { + return leaseExpiryAction; + } + + public void setLeaseExpiryAction(String leaseExpiryAction) { + this.leaseExpiryAction = leaseExpiryAction; + } + + public String getLeaseActionExecution() { + return leaseActionExecution; + } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 56a86e65da02..1006e02ab7c1 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -16,132 +16,6 @@ // under the License. package com.cloud.configuration; -import static com.cloud.configuration.Config.SecStorageAllowedInternalDownloadSites; -import static com.cloud.offering.NetworkOffering.RoutingMode.Dynamic; -import static com.cloud.offering.NetworkOffering.RoutingMode.Static; -import static org.apache.cloudstack.framework.config.ConfigKey.CATEGORY_SYSTEM; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.sql.Date; -import java.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.Vector; -import java.util.stream.Collectors; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; - -import org.apache.cloudstack.acl.SecurityChecker; -import org.apache.cloudstack.affinity.AffinityGroup; -import org.apache.cloudstack.affinity.AffinityGroupService; -import org.apache.cloudstack.affinity.dao.AffinityGroupDao; -import org.apache.cloudstack.agent.lb.IndirectAgentLB; -import org.apache.cloudstack.agent.lb.IndirectAgentLBServiceImpl; -import org.apache.cloudstack.annotation.AnnotationService; -import org.apache.cloudstack.annotation.dao.AnnotationDao; -import org.apache.cloudstack.api.ApiCommandResourceType; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; -import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; -import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd; -import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; -import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; -import org.apache.cloudstack.api.command.admin.network.DeleteGuestNetworkIpv6PrefixCmd; -import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd; -import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd; -import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd; -import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; -import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; -import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; -import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; -import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; -import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCmd; -import org.apache.cloudstack.api.command.admin.offering.IsAccountAllowedToCreateOfferingsWithTagsCmd; -import org.apache.cloudstack.api.command.admin.offering.UpdateDiskOfferingCmd; -import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCmd; -import org.apache.cloudstack.api.command.admin.pod.DeletePodCmd; -import org.apache.cloudstack.api.command.admin.pod.UpdatePodCmd; -import org.apache.cloudstack.api.command.admin.region.CreatePortableIpRangeCmd; -import org.apache.cloudstack.api.command.admin.region.DeletePortableIpRangeCmd; -import org.apache.cloudstack.api.command.admin.region.ListPortableIpRangesCmd; -import org.apache.cloudstack.api.command.admin.vlan.CreateVlanIpRangeCmd; -import org.apache.cloudstack.api.command.admin.vlan.DedicatePublicIpRangeCmd; -import org.apache.cloudstack.api.command.admin.vlan.DeleteVlanIpRangeCmd; -import org.apache.cloudstack.api.command.admin.vlan.ReleasePublicIpRangeCmd; -import org.apache.cloudstack.api.command.admin.vlan.UpdateVlanIpRangeCmd; -import org.apache.cloudstack.api.command.admin.zone.CreateZoneCmd; -import org.apache.cloudstack.api.command.admin.zone.DeleteZoneCmd; -import org.apache.cloudstack.api.command.admin.zone.UpdateZoneCmd; -import org.apache.cloudstack.api.command.user.network.ListNetworkOfferingsCmd; -import org.apache.cloudstack.cluster.ClusterDrsService; -import org.apache.cloudstack.config.ApiServiceConfiguration; -import org.apache.cloudstack.config.Configuration; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; -import org.apache.cloudstack.framework.config.ConfigDepot; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.Configurable; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.config.dao.ConfigurationGroupDao; -import org.apache.cloudstack.framework.config.dao.ConfigurationSubGroupDao; -import org.apache.cloudstack.framework.config.impl.ConfigurationGroupVO; -import org.apache.cloudstack.framework.config.impl.ConfigurationSubGroupVO; -import org.apache.cloudstack.framework.config.impl.ConfigurationVO; -import org.apache.cloudstack.framework.messagebus.MessageBus; -import org.apache.cloudstack.framework.messagebus.MessageSubscriber; -import org.apache.cloudstack.framework.messagebus.PublishScope; -import org.apache.cloudstack.network.RoutedIpv4Manager; -import org.apache.cloudstack.query.QueryService; -import org.apache.cloudstack.region.PortableIp; -import org.apache.cloudstack.region.PortableIpDao; -import org.apache.cloudstack.region.PortableIpRange; -import org.apache.cloudstack.region.PortableIpRangeDao; -import org.apache.cloudstack.region.PortableIpRangeVO; -import org.apache.cloudstack.region.PortableIpVO; -import org.apache.cloudstack.region.Region; -import org.apache.cloudstack.region.RegionVO; -import org.apache.cloudstack.region.dao.RegionDao; -import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO; -import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; -import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; -import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailVO; -import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailsDao; -import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.userdata.UserDataManager; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.apache.cloudstack.vm.UnmanagedVMsManager; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang3.EnumUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; - import com.cloud.agent.AgentManager; import com.cloud.alert.AlertManager; import com.cloud.api.ApiDBUtils; @@ -312,6 +186,132 @@ import com.google.common.collect.Sets; import com.googlecode.ipv6.IPv6Address; import com.googlecode.ipv6.IPv6Network; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.affinity.AffinityGroup; +import org.apache.cloudstack.affinity.AffinityGroupService; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.agent.lb.IndirectAgentLB; +import org.apache.cloudstack.agent.lb.IndirectAgentLBServiceImpl; +import org.apache.cloudstack.annotation.AnnotationService; +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; +import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; +import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd; +import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.network.DeleteGuestNetworkIpv6PrefixCmd; +import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd; +import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.IsAccountAllowedToCreateOfferingsWithTagsCmd; +import org.apache.cloudstack.api.command.admin.offering.UpdateDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.pod.DeletePodCmd; +import org.apache.cloudstack.api.command.admin.pod.UpdatePodCmd; +import org.apache.cloudstack.api.command.admin.region.CreatePortableIpRangeCmd; +import org.apache.cloudstack.api.command.admin.region.DeletePortableIpRangeCmd; +import org.apache.cloudstack.api.command.admin.region.ListPortableIpRangesCmd; +import org.apache.cloudstack.api.command.admin.vlan.CreateVlanIpRangeCmd; +import org.apache.cloudstack.api.command.admin.vlan.DedicatePublicIpRangeCmd; +import org.apache.cloudstack.api.command.admin.vlan.DeleteVlanIpRangeCmd; +import org.apache.cloudstack.api.command.admin.vlan.ReleasePublicIpRangeCmd; +import org.apache.cloudstack.api.command.admin.vlan.UpdateVlanIpRangeCmd; +import org.apache.cloudstack.api.command.admin.zone.CreateZoneCmd; +import org.apache.cloudstack.api.command.admin.zone.DeleteZoneCmd; +import org.apache.cloudstack.api.command.admin.zone.UpdateZoneCmd; +import org.apache.cloudstack.api.command.user.network.ListNetworkOfferingsCmd; +import org.apache.cloudstack.cluster.ClusterDrsService; +import org.apache.cloudstack.config.ApiServiceConfiguration; +import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; +import org.apache.cloudstack.framework.config.ConfigDepot; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.config.dao.ConfigurationGroupDao; +import org.apache.cloudstack.framework.config.dao.ConfigurationSubGroupDao; +import org.apache.cloudstack.framework.config.impl.ConfigurationGroupVO; +import org.apache.cloudstack.framework.config.impl.ConfigurationSubGroupVO; +import org.apache.cloudstack.framework.config.impl.ConfigurationVO; +import org.apache.cloudstack.framework.messagebus.MessageBus; +import org.apache.cloudstack.framework.messagebus.MessageSubscriber; +import org.apache.cloudstack.framework.messagebus.PublishScope; +import org.apache.cloudstack.network.RoutedIpv4Manager; +import org.apache.cloudstack.query.QueryService; +import org.apache.cloudstack.region.PortableIp; +import org.apache.cloudstack.region.PortableIpDao; +import org.apache.cloudstack.region.PortableIpRange; +import org.apache.cloudstack.region.PortableIpRangeDao; +import org.apache.cloudstack.region.PortableIpRangeVO; +import org.apache.cloudstack.region.PortableIpVO; +import org.apache.cloudstack.region.Region; +import org.apache.cloudstack.region.RegionVO; +import org.apache.cloudstack.region.dao.RegionDao; +import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO; +import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailVO; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailsDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.userdata.UserDataManager; +import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.cloudstack.vm.UnmanagedVMsManager; +import org.apache.cloudstack.vm.lease.VMLeaseManager; +import org.apache.cloudstack.vm.lease.VMLeaseManagerImpl; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.Vector; +import java.util.stream.Collectors; + +import static com.cloud.configuration.Config.SecStorageAllowedInternalDownloadSites; +import static com.cloud.offering.NetworkOffering.RoutingMode.Dynamic; +import static com.cloud.offering.NetworkOffering.RoutingMode.Static; +import static org.apache.cloudstack.framework.config.ConfigKey.CATEGORY_SYSTEM; public class ConfigurationManagerImpl extends ManagerBase implements ConfigurationManager, ConfigurationService, Configurable { public static final String PERACCOUNT = "peraccount"; @@ -473,6 +473,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati Ipv6Service ipv6Service; @Inject NsxProviderDao nsxProviderDao; + @Inject + VMLeaseManager vmLeaseManager; // FIXME - why don't we have interface for DataCenterLinkLocalIpAddressDao? @Inject @@ -626,6 +628,8 @@ public void onPublishMessage(String serderAddress, String subject, Object args) params.put(Config.RouterAggregationCommandEachTimeout.toString(), _configDao.getValue(Config.RouterAggregationCommandEachTimeout.toString())); params.put(Config.MigrateWait.toString(), _configDao.getValue(Config.MigrateWait.toString())); _agentManager.propagateChangeToAgents(params); + } else if (VMLeaseManagerImpl.InstanceLeaseEnabled.key().equals(globalSettingUpdated)) { + vmLeaseManager.onLeaseFeatureToggle(); } } }); @@ -3319,6 +3323,10 @@ public ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) } } + // validate lease properties and set leaseExpiryAction + Integer leaseDuration = cmd.getLeaseDuration(); + String leaseExpiryAction = validateAndGetLeaseExpiryAction(leaseDuration, cmd.getLeaseExpiryAction()); + 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.getNetworkRate(), cmd.getDeploymentPlanner(), details, cmd.getRootDiskSize(), isCustomizedIops, cmd.getMinIops(), cmd.getMaxIops(), @@ -3327,20 +3335,20 @@ public ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) cmd.getIopsReadRate(), cmd.getIopsReadRateMax(), cmd.getIopsReadRateMaxLength(), cmd.getIopsWriteRate(), cmd.getIopsWriteRateMax(), cmd.getIopsWriteRateMaxLength(), 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, - 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 domainIds, List zoneIds, final String hostTag, - final Integer networkRate, final String deploymentPlanner, final Map details, Long rootDiskSizeInGiB, final Boolean isCustomizedIops, Long minIops, Long maxIops, - Long bytesReadRate, Long bytesReadRateMax, Long bytesReadRateMaxLength, - Long bytesWriteRate, Long bytesWriteRateMax, Long bytesWriteRateMaxLength, - Long iopsReadRate, Long iopsReadRateMax, Long iopsReadRateMaxLength, - Long iopsWriteRate, Long iopsWriteRateMax, Long iopsWriteRateMaxLength, - final Integer hypervisorSnapshotReserve, String cacheMode, final Long storagePolicyID, - final boolean dynamicScalingEnabled, final Long diskOfferingId, final boolean diskOfferingStrictness, - final boolean isCustomized, final boolean encryptRoot, final boolean purgeResources) { + 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 domainIds, List zoneIds, final String hostTag, + final Integer networkRate, final String deploymentPlanner, final Map details, Long rootDiskSizeInGiB, final Boolean isCustomizedIops, Long minIops, Long maxIops, + Long bytesReadRate, Long bytesReadRateMax, Long bytesReadRateMaxLength, + Long bytesWriteRate, Long bytesWriteRateMax, Long bytesWriteRateMaxLength, + Long iopsReadRate, Long iopsReadRateMax, Long iopsReadRateMaxLength, + Long iopsWriteRate, Long iopsWriteRateMax, Long iopsWriteRateMaxLength, + final Integer hypervisorSnapshotReserve, String cacheMode, final Long storagePolicyID, + final boolean dynamicScalingEnabled, final Long diskOfferingId, final boolean diskOfferingStrictness, + final boolean isCustomized, final boolean encryptRoot, final boolean purgeResources, Integer leaseDuration, String leaseExpiryAction) { // Filter child domains when both parent and child domains are present List filteredDomainIds = filterChildSubDomains(domainIds); @@ -3447,6 +3455,12 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole } if ((serviceOffering = _serviceOfferingDao.persist(serviceOffering)) != null) { + //persist lease properties if leaseExpiryAction is valid + if (StringUtils.isNotEmpty(leaseExpiryAction)) { + 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, false)); + } + for (Long domainId : filteredDomainIds) { detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); } @@ -3470,6 +3484,39 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole } } + /** + * 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 String validateAndGetLeaseExpiryAction(Integer leaseDuration, String cmdExpiryAction) { + if (!VMLeaseManagerImpl.InstanceLeaseEnabled.value() || ObjectUtils.allNull(leaseDuration, cmdExpiryAction)) { // both are null + return null; + } + + // one of them is non-null + if (leaseDuration == null || StringUtils.isEmpty(cmdExpiryAction)) { + throw new InvalidParameterValueException("Provide values for both: leaseduration and leaseexpiryaction"); + } + + if (leaseDuration < 1L) { + throw new InvalidParameterValueException("Invalid value provided for leaseDuration, accepts only positive number"); + } + + if (StringUtils.isNotEmpty(cmdExpiryAction)) { + try { + VMLeaseManager.ExpiryAction.valueOf(cmdExpiryAction); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException("Invalid value configured for leaseexpiryaction, valid values are: " + + com.cloud.utils.EnumUtils.listValues(VMLeaseManager.ExpiryAction.values())); + } + } + return cmdExpiryAction.toUpperCase(); + } + @Override public void validateExtraConfigInServiceOfferingDetail(String detailName) { if (!detailName.equals(DpdkHelper.DPDK_NUMA) && !detailName.equals(DpdkHelper.DPDK_HUGE_PAGES) diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 89cfb3509cb0..1fcaf32019d9 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -639,6 +639,7 @@ import org.apache.cloudstack.userdata.UserDataManager; import org.apache.cloudstack.utils.CloudStackVersion; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.vm.lease.VMLeaseManagerImpl; import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -4505,6 +4506,7 @@ public Map listCapabilities(final ListCapabilitiesCmd cmd) { 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_TIME, StatsCollector.vmDiskStatsMaxRetentionTime.value()); + capabilities.put(ApiConstants.INSTANCE_LEASE_ENABLED, VMLeaseManagerImpl.InstanceLeaseEnabled.value()); if (apiLimitEnabled) { capabilities.put("apiLimitInterval", apiLimitInterval); capabilities.put("apiLimitMax", apiLimitMax); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 021c6ff62267..127af6f080cd 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -16,143 +16,6 @@ // under the License. package com.cloud.vm; -import static com.cloud.hypervisor.Hypervisor.HypervisorType.Functionality; -import static com.cloud.storage.Volume.IOPS_LIMIT; -import static com.cloud.utils.NumbersUtil.toHumanReadableSize; -import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS; -import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS; - -import java.io.IOException; -import java.io.StringReader; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.ParserConfigurationException; - -import org.apache.cloudstack.acl.ControlledEntity; -import org.apache.cloudstack.acl.ControlledEntity.ACLType; -import org.apache.cloudstack.acl.SecurityChecker.AccessType; -import org.apache.cloudstack.affinity.AffinityGroupService; -import org.apache.cloudstack.affinity.AffinityGroupVMMapVO; -import org.apache.cloudstack.affinity.AffinityGroupVO; -import org.apache.cloudstack.affinity.dao.AffinityGroupDao; -import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; -import org.apache.cloudstack.annotation.AnnotationService; -import org.apache.cloudstack.annotation.dao.AnnotationDao; -import org.apache.cloudstack.api.ApiCommandResourceType; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.BaseCmd.HTTPMethod; -import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; -import org.apache.cloudstack.api.command.admin.vm.DeployVMCmdByAdmin; -import org.apache.cloudstack.api.command.admin.vm.ExpungeVMCmd; -import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; -import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; -import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; -import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; -import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; -import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; -import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; -import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; -import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; -import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; -import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; -import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; -import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction; -import org.apache.cloudstack.api.command.user.vm.StartVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd; -import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd; -import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd; -import org.apache.cloudstack.api.command.user.vmgroup.DeleteVMGroupCmd; -import org.apache.cloudstack.api.command.user.volume.ChangeOfferingForVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; -import org.apache.cloudstack.backup.Backup; -import org.apache.cloudstack.backup.BackupManager; -import org.apache.cloudstack.backup.dao.BackupDao; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntity; -import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; -import org.apache.cloudstack.engine.service.api.OrchestrationService; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProviderManager; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; -import org.apache.cloudstack.framework.async.AsyncCallFuture; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.Configurable; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.messagebus.MessageBus; -import org.apache.cloudstack.framework.messagebus.PublishScope; -import org.apache.cloudstack.managed.context.ManagedContextRunnable; -import org.apache.cloudstack.query.QueryService; -import org.apache.cloudstack.reservation.dao.ReservationDao; -import org.apache.cloudstack.snapshot.SnapshotHelper; -import org.apache.cloudstack.storage.command.DeleteCommand; -import org.apache.cloudstack.storage.command.DettachCommand; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; -import org.apache.cloudstack.storage.template.VnfTemplateManager; -import org.apache.cloudstack.userdata.UserDataManager; -import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; -import org.apache.cloudstack.utils.security.ParserUtils; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; -import org.apache.cloudstack.vm.UnmanagedVMsManager; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.math.NumberUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; @@ -351,6 +214,7 @@ import com.cloud.user.dao.VmDiskStatisticsDao; import com.cloud.uservm.UserVm; import com.cloud.utils.DateUtil; +import com.cloud.utils.EnumUtils; import com.cloud.utils.Journal; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; @@ -388,6 +252,149 @@ import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.ControlledEntity.ACLType; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.affinity.AffinityGroupService; +import org.apache.cloudstack.affinity.AffinityGroupVMMapVO; +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.apache.cloudstack.annotation.AnnotationService; +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseCmd.HTTPMethod; +import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; +import org.apache.cloudstack.api.command.admin.vm.DeployVMCmdByAdmin; +import org.apache.cloudstack.api.command.admin.vm.ExpungeVMCmd; +import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; +import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; +import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; +import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; +import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; +import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction; +import org.apache.cloudstack.api.command.user.vm.StartVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd; +import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd; +import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd; +import org.apache.cloudstack.api.command.user.vmgroup.DeleteVMGroupCmd; +import org.apache.cloudstack.api.command.user.volume.ChangeOfferingForVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntity; +import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.engine.service.api.OrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProviderManager; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; +import org.apache.cloudstack.framework.async.AsyncCallFuture; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.messagebus.MessageBus; +import org.apache.cloudstack.framework.messagebus.PublishScope; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.query.QueryService; +import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.snapshot.SnapshotHelper; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.command.DettachCommand; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.cloudstack.storage.template.VnfTemplateManager; +import org.apache.cloudstack.userdata.UserDataManager; +import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; +import org.apache.cloudstack.utils.security.ParserUtils; +import org.apache.cloudstack.vm.UnmanagedVMsManager; +import org.apache.cloudstack.vm.lease.VMLeaseManager; +import org.apache.cloudstack.vm.lease.VMLeaseManagerImpl; +import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang.math.NumberUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.logging.log4j.util.Strings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.cloud.hypervisor.Hypervisor.HypervisorType.Functionality; +import static com.cloud.storage.Volume.IOPS_LIMIT; +import static com.cloud.utils.NumbersUtil.toHumanReadableSize; +import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS; +import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS; public class UserVmManagerImpl extends ManagerBase implements UserVmManager, VirtualMachineGuru, Configurable { @@ -2862,6 +2869,13 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx } } else { 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*' should not be included in details as key"); + } + if (details.containsKey("extraconfig")) { throw new InvalidParameterValueException("'extraconfig' should not be included in details as key"); } @@ -2909,6 +2923,12 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx } } } + + Integer leaseDuration = cmd.getLeaseDuration(); + String leaseExpiryAction = cmd.getLeaseExpiryAction(); + validateLeaseProperties(leaseDuration, leaseExpiryAction); + applyLeaseOnUpdateInstance(vmInstance, leaseDuration, leaseExpiryAction); + return updateVirtualMachine(id, displayName, group, ha, isDisplayVm, cmd.getDeleteProtection(), osTypeId, userData, userDataId, userDataDetails, isDynamicallyScalable, cmd.getHttpMethod(), @@ -6153,6 +6173,10 @@ public UserVm createVirtualMachine(DeployVMCmd cmd) throws InsufficientCapacityE } } + Integer leaseDuration = cmd.getLeaseDuration(); + String leaseExpiryAction = cmd.getLeaseExpiryAction(); + validateLeaseProperties(leaseDuration, leaseExpiryAction); + List networkIds = cmd.getNetworkIds(); LinkedHashMap userVmNetworkMap = getVmOvfNetworkMapping(zone, owner, template, cmd.getVmNetworkMap()); if (MapUtils.isNotEmpty(userVmNetworkMap)) { @@ -6251,9 +6275,117 @@ public UserVm createVirtualMachine(DeployVMCmd cmd) throws InsufficientCapacityE } } } + + applyLeaseOnCreateInstance(vm, leaseDuration, leaseExpiryAction, svcOffering); return vm; } + protected void validateLeaseProperties(Integer leaseDuration, String leaseExpiryAction) { + if (!VMLeaseManagerImpl.InstanceLeaseEnabled.value() + || ObjectUtils.allNull(leaseDuration, leaseExpiryAction) // both are null + || (leaseDuration != null && leaseDuration < 1)) { // special condition to disable lease for instance + return; + } + + boolean bothValuesSet = true; + if (leaseDuration != null) { + if (StringUtils.isEmpty(leaseExpiryAction)) { + bothValuesSet = false; + } + } else { + bothValuesSet = false; + } + + if (!bothValuesSet) { + throw new InvalidParameterValueException("Provide values for both: leaseduration and leaseexpiryaction"); + } + try { + VMLeaseManager.ExpiryAction.valueOf(leaseExpiryAction); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException("Invalid value provided for leaseexpiryaction, valid values are: " + + EnumUtils.listValues(VMLeaseManager.ExpiryAction.values())); + } + } + + /** + * 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, String leaseExpiryAction, ServiceOfferingJoinVO serviceOfferingJoinVO) { + if (!VMLeaseManagerImpl.InstanceLeaseEnabled.value()) { + return; + } + 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 = Strings.isNotEmpty(leaseExpiryAction) ? leaseExpiryAction : serviceOfferingJoinVO.getLeaseExpiryAction(); + addLeaseDetailsForInstance(vm, leaseDuration, leaseExpiryAction); + } + + protected void applyLeaseOnUpdateInstance(UserVm instance, Integer leaseDuration, String leaseExpiryAction) { + // lease feature must be enabled + if (!VMLeaseManagerImpl.InstanceLeaseEnabled.value() || leaseDuration == null) { + return; + } + + String instanceUuid = instance.getUuid(); + // vm must have associated lease during deployment + UserVmDetailVO vmDetail = userVmDetailsDao.findDetail(instance.getId(), VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE); + if (vmDetail == null || StringUtils.isEmpty(vmDetail.getValue())) { + logger.debug("Lease won't be applied on instance with id: {}, it doesn't have " + + "leased associated during deployment", instanceUuid); + return; + } + // proceed if lease is yet to expire + long leaseExpiryTimeDiff; + try { + leaseExpiryTimeDiff = DateUtil.getTimeDifference( + DateUtil.parseDateString(TimeZone.getTimeZone("UTC"), vmDetail.getValue()), 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); + return; + } + + if (leaseDuration < 1) { + userVmDetailsDao.addDetail(instance.getId(), VmDetailConstants.INSTANCE_LEASE_EXECUTION, "DISABLED", 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, String leaseExpiryAction) { + if (ObjectUtils.anyNull(vm, leaseDuration) || leaseDuration < 1 || !Arrays.asList("STOP", "DESTROY").contains(leaseExpiryAction)) { + 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, 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 * @param decodedUrl String consisting of the extra config data to appended onto the vmx file for VMware instances diff --git a/server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManager.java b/server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManager.java new file mode 100644 index 000000000000..ab472f27416b --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManager.java @@ -0,0 +1,46 @@ +/* + * 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 { + + enum ExpiryAction { + STOP, + DESTROY + } + + ConfigKey InstanceLeaseSchedulerInterval = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Long.class, + "instance.lease.scheduler.interval", "3600", "VM Lease Scheduler interval in seconds", + false, List.of(ConfigKey.Scope.Global)); + + ConfigKey InstanceLeaseAlertSchedule = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Long.class, + "instance.lease.alertscheduler.interval", "86400", "Lease Alert Scheduler interval in seconds", + false, List.of(ConfigKey.Scope.Global)); + + ConfigKey InstanceLeaseExpiryAlertDaysBefore = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Long.class, + "instance.lease.alert.daysbefore", "7", "Indicates how many days in advance the alert will be triggered before expiry.", + true, List.of(ConfigKey.Scope.Global)); + + void onLeaseFeatureToggle(); +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManagerImpl.java new file mode 100644 index 000000000000..4544a1cd0775 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManagerImpl.java @@ -0,0 +1,353 @@ +/* + * 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.alert.AlertManager; +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.StringUtils; +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 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.AsyncJobDispatcher; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +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.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class VMLeaseManagerImpl extends ManagerBase implements VMLeaseManager, Configurable { + public static final String INSTANCE_LEASE_ENABLED = "instance.lease.enabled"; + + public static ConfigKey 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)); + + private static final int ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION = 5; // 5 seconds + + @Inject + private UserVmDetailsDao userVmDetailsDao; + + @Inject + private UserVmJoinDao userVmJoinDao; + + @Inject + private AlertManager alertManager; + + @Inject + private AsyncJobManager asyncJobManager; + + private AsyncJobDispatcher asyncJobDispatcher; + + ScheduledExecutorService vmLeaseExecutor; + ScheduledExecutorService vmLeaseAlertExecutor; + + @Override + public String getConfigComponentName() { + return VMLeaseManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + InstanceLeaseEnabled, + InstanceLeaseSchedulerInterval, + InstanceLeaseAlertSchedule, + InstanceLeaseExpiryAlertDaysBefore + }; + } + + public void setAsyncJobDispatcher(final AsyncJobDispatcher dispatcher) { + asyncJobDispatcher = dispatcher; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + if (InstanceLeaseEnabled.value()) { + scheduleLeaseExecutors(); + } + return true; + } + + @Override + public boolean start() { + 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 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, "CANCELLED", false); + String leaseCancellationMsg = String.format("Lease is cancelled for the instancedId: %s ", 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 = VMLeaseManagerImpl.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 (vmLeaseAlertExecutor == null || vmLeaseAlertExecutor.isShutdown()) { + logger.debug("Scheduling lease alert executor"); + vmLeaseAlertExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("VMLeaseAlertPollExecutor")); + vmLeaseAlertExecutor.scheduleAtFixedRate(new VMLeaseAlertSchedulerTask(), 5L, InstanceLeaseAlertSchedule.value(), TimeUnit.SECONDS); + } + } + + private void shutDownLeaseExecutors() { + if (vmLeaseExecutor != null) { + logger.debug("Shutting down lease executor"); + vmLeaseExecutor.shutdown(); + vmLeaseExecutor = null; + } + + if (vmLeaseAlertExecutor != null) { + logger.debug("Shutting down lease alert executor"); + vmLeaseAlertExecutor.shutdown(); + vmLeaseAlertExecutor = null; + } + } + + 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 VMLeaseAlertSchedulerTask extends ManagedContextRunnable { + @Override + protected void runInContext() { + // as feature is disabled, no action is required + if (!InstanceLeaseEnabled.value()) { + return; + } + + GlobalLock scanLock = GlobalLock.getInternLock("VMLeaseAlertSchedulerTask"); + try { + if (scanLock.lock(ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION)) { + try { + List leaseExpiringForInstances = userVmJoinDao.listLeaseInstancesExpiringInDays(InstanceLeaseExpiryAlertDaysBefore.value().intValue()); + for (UserVmJoinVO instance : leaseExpiringForInstances) { + String leaseExpiryEventMsg = String.format("Lease expiring for for instanceId: %s with action: %s", 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(); + } + } + } + + @Override + public Map getConfigParams() { + return super.getConfigParams(); + } + + protected void reallyRun() { + // fetch user_instances having leaseDuration configured and has expired + List leaseExpiredInstances = userVmJoinDao.listEligibleInstancesWithExpiredLease(); + List actionableInstanceIds = new ArrayList<>(); + 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 with id: {} as deleteProtection is enabled", userVmVO.getUuid()); + continue; + } + actionableInstanceIds.add(userVmVO.getId()); + } + if (actionableInstanceIds.isEmpty()) { + logger.debug("Lease scheduler found no instance to work upon"); + return; + } + + List submittedJobIds = new ArrayList<>(); + List 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 instanceId: %s", instance.getLeaseExpiryAction(), instance.getUuid()), + instance.getId(), ApiCommandResourceType.VirtualMachine.toString(), 0); + + Long jobId = executeExpiryAction(instance, expiryAction, eventId); + if (jobId != null) { + submittedJobIds.add(jobId); + userVmDetailsDao.addDetail(instanceId, VmDetailConstants.INSTANCE_LEASE_EXECUTION, "DONE", false); + } else { + failedToSubmitInstanceIds.add(instanceId); + } + } + logger.debug("Successfully submitted lease expiry jobs with ids: {}", submittedJobIds); + 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 with id: {} on lease expiry", instance.getUuid()); + return executeStopInstanceJob(instance, true, eventId); + } + case DESTROY: { + logger.debug("Destroying instance with id: {} on lease expiry", instance.getUuid()); + return executeDestroyInstanceJob(instance, true, eventId); + } + default: { + logger.error("Invalid configuration for instance.lease.expiryaction for vm id: {}, " + + "valid values are: \"STOP\" and \"DESTROY\"", instance.getUuid()); + } + } + return null; + } + + long executeStopInstanceJob(UserVmJoinVO vm, boolean isForced, long eventId) { + final Map 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(ApiConstants.FORCED, String.valueOf(isForced)); + final StopVMCmd cmd = new StopVMCmd(); + ComponentContext.inject(cmd); + AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, vm.getAccountId(), StopVMCmd.class.getName(), + ApiGsonHelper.getBuilder().create().toJson(params), vm.getId(), + cmd.getApiResourceType() != null ? cmd.getApiResourceType().toString() : null, null); + job.setDispatcher(asyncJobDispatcher.getName()); + return asyncJobManager.submitAsyncJob(job); + } + + long executeDestroyInstanceJob(UserVmJoinVO vm, boolean isForced, long eventId) { + final Map 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(ApiConstants.FORCED, String.valueOf(isForced)); + + final DestroyVMCmd cmd = new DestroyVMCmd(); + ComponentContext.inject(cmd); + + AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, vm.getAccountId(), DestroyVMCmd.class.getName(), + ApiGsonHelper.getBuilder().create().toJson(params), vm.getId(), + cmd.getApiResourceType() != null ? cmd.getApiResourceType().toString() : null, null); + job.setDispatcher(asyncJobDispatcher.getName()); + return asyncJobManager.submitAsyncJob(job); + } + + public ExpiryAction getLeaseExpiryAction(UserVmJoinVO instance) { + String action = instance.getLeaseExpiryAction(); + if (StringUtils.isEmpty(action)) { + return null; + } + + ExpiryAction expiryAction = null; + try { + expiryAction = ExpiryAction.valueOf(action); + } catch (Exception ex) { + logger.error("Invalid expiry action configured for instance with id: {}", instance.getUuid(), ex); + } + return expiryAction; + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 60c2095d5f41..5b89e6dacf11 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -382,4 +382,9 @@ + + + + + diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index f07d2af21af2..75ce758ac519 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -16,58 +16,6 @@ // under the License. package com.cloud.vm; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.cloudstack.acl.ControlledEntity; -import org.apache.cloudstack.acl.SecurityChecker; -import org.apache.cloudstack.api.BaseCmd.HTTPMethod; -import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; -import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; -import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; -import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; -import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; -import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.template.VnfTemplateManager; -import org.apache.cloudstack.userdata.UserDataManager; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - import com.cloud.api.query.dao.ServiceOfferingJoinDao; import com.cloud.api.query.vo.ServiceOfferingJoinVO; import com.cloud.configuration.Resource; @@ -78,6 +26,10 @@ import com.cloud.deploy.DeployDestination; import com.cloud.deploy.DeploymentPlanner; 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.InsufficientCapacityException; import com.cloud.exception.InsufficientServerCapacityException; @@ -89,12 +41,28 @@ import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.Network; 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.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.offering.DiskOffering; +import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import com.cloud.offerings.NetworkOfferingVO; +import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.server.ManagementService; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; @@ -134,31 +102,72 @@ import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.BaseCmd.HTTPMethod; +import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; +import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; +import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.template.VnfTemplateManager; +import org.apache.cloudstack.userdata.UserDataManager; +import org.apache.cloudstack.vm.lease.VMLeaseManagerImpl; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; -import com.cloud.domain.DomainVO; -import com.cloud.domain.dao.DomainDao; -import com.cloud.event.UsageEventUtils; -import com.cloud.network.Network; -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.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; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +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 UserVmManagerImplTest { @@ -479,6 +488,7 @@ public void updateVirtualMachineTestDisplayChanged() throws ResourceUnavailableE Mockito.when(userVmVoMock.isDisplay()).thenReturn(true); Mockito.doNothing().when(userVmManagerImpl).updateDisplayVmFlag(false, vmId, userVmVoMock); Mockito.when(updateVmCommand.getUserdataId()).thenReturn(null); + Mockito.when(updateVmCommand.getLeaseDuration()).thenReturn(null); userVmManagerImpl.updateVirtualMachine(updateVmCommand); verifyMethodsThatAreAlwaysExecuted(); @@ -612,6 +622,8 @@ private void configureDoNothingForMethodsThatWeDoNotWantToTest() throws Resource Mockito.doNothing().when(userVmManagerImpl).updateVmNetwork(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Mockito.doNothing().when(userVmManagerImpl).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); + + Mockito.doNothing().when(userVmManagerImpl).validateLeaseProperties(Mockito.any(), Mockito.any()); } @Test @@ -3093,7 +3105,7 @@ public void executeStepsToChangeOwnershipOfVmTestResourceCountRunningVmsOnlyEnab configureDoNothingForMethodsThatWeDoNotWantToTest(); 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).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); @@ -3125,4 +3137,255 @@ public void executeStepsToChangeOwnershipOfVmTestResourceCountRunningVmsOnlyEnab Mockito.verify(userVmManagerImpl, Mockito.never()).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); } } + + @Test + public void testValidateLeasePropertiesInvalidDuration() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(-2, "STOP"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateLeasePropertiesNullActionValue() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(20, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateLeasePropertiesNullDurationValue() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(null, "STOP"); + } + + @Test + public void testValidateLeasePropertiesMinusOneDuration() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(-1, null); + } + + @Test + public void testValidateLeasePropertiesZeroDayDuration() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(0, "STOP"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateLeasePropertiesValidActionValue() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(20, "RUN"); + } + + @Test + public void testValidateLeasePropertiesValidValues() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(20, "STOP"); + } + + @Test + public void testValidateLeasePropertiesBothNUll() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(null, null); + } + + @Test + public void testValidateLeasePropertiesDisabledFeatureNullActionValue() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.FALSE); + userVmManagerImpl.validateLeaseProperties(20, null); + } + + @Test + public void testValidateLeasePropertiesDisabledFeatureInvalidDuration() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.FALSE); + userVmManagerImpl.validateLeaseProperties(null, "DESTROY"); + } + + @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, "STOP"); + verify(userVmDetailsDao).addDetail(eq(vmId), eq(VmDetailConstants.INSTANCE_LEASE_EXPIRY_ACTION), eq("STOP"), 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, "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 testApplyLeaseOnCreateInstanceFeatureDisabled() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.FALSE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class); + userVmManagerImpl.applyLeaseOnCreateInstance(userVm, 10, "STOP", svcOfferingMock); + Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnCreateInstanceFeatureEnabled() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class); + userVmManagerImpl.applyLeaseOnCreateInstance(userVm, 10, "DESTROY", svcOfferingMock); + Mockito.verify(userVmManagerImpl, Mockito.times(1)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnCreateInstanceNegativeLease() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + userVmManagerImpl.applyLeaseOnCreateInstance(userVm, -1, "DESTROY", null); + Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnCreateInstanceFromSvcOfferingWithoutLease() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class); + userVmManagerImpl.applyLeaseOnCreateInstance(userVm, null, "DESTROY", svcOfferingMock); + Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnCreateInstanceFromSvcOfferingWithLease() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class); + when(svcOfferingMock.getLeaseDuration()).thenReturn(10); + userVmManagerImpl.applyLeaseOnCreateInstance(userVm, null, "DESTROY", svcOfferingMock); + Mockito.verify(userVmManagerImpl, Mockito.times(1)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnCreateInstanceNullExpiryAction() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class); + userVmManagerImpl.applyLeaseOnCreateInstance(userVm, 10, null, svcOfferingMock); + Mockito.verify(userVmManagerImpl, Mockito.times(1)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnUpdateInstanceForNoLease() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + when(userVm.getId()).thenReturn(vmId); + when(userVmDetailsDao.findDetail(anyLong(), any())).thenReturn(null); + userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, 10, "STOP"); + Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnUpdateInstanceForLease() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + UserVmDetailVO userVmDetailVO = Mockito.mock(UserVmDetailVO.class); + when(userVm.getId()).thenReturn(vmId); + when(userVmDetailVO.getValue()).thenReturn(getLeaseExpiryDate(5)); + when(userVmDetailsDao.findDetail(anyLong(), any())).thenReturn(userVmDetailVO); + userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, 10, "STOP"); + Mockito.verify(userVmManagerImpl, Mockito.times(1)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnUpdateInstanceForLeaseExpired() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + UserVmDetailVO userVmDetailVO = Mockito.mock(UserVmDetailVO.class); + when(userVm.getId()).thenReturn(vmId); + when(userVmDetailVO.getValue()).thenReturn(getLeaseExpiryDate(-5)); + when(userVmDetailsDao.findDetail(anyLong(), any())).thenReturn(userVmDetailVO); + userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, 10, "STOP"); + Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any()); + } + + @Test + public void testApplyLeaseOnUpdateInstanceToRemoveLease() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + UserVmDetailVO userVmDetailVO = Mockito.mock(UserVmDetailVO.class); + when(userVm.getId()).thenReturn(vmId); + when(userVmDetailVO.getValue()).thenReturn(getLeaseExpiryDate(2)); + when(userVmDetailsDao.findDetail(anyLong(), any())).thenReturn(userVmDetailVO); + + try (MockedStatic 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, "STOP"); + } + Mockito.verify(userVmManagerImpl, Mockito.times(0)).addLeaseDetailsForInstance(any(), any(), any()); + Mockito.verify(userVmDetailsDao, Mockito.times(1)).addDetail(vmId, VmDetailConstants.INSTANCE_LEASE_EXECUTION, "DISABLED", false); + } + + @Test + public void testApplyLeaseOnUpdateInstanceToRemoveLeaseForExpired() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + UserVmVO userVm = Mockito.mock(UserVmVO.class); + UserVmDetailVO userVmDetailVO = Mockito.mock(UserVmDetailVO.class); + when(userVm.getId()).thenReturn(vmId); + when(userVmDetailVO.getValue()).thenReturn(getLeaseExpiryDate(-10)); + when(userVmDetailsDao.findDetail(anyLong(), any())).thenReturn(userVmDetailVO); + userVmManagerImpl.applyLeaseOnUpdateInstance(userVm, -1, "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); + } } diff --git a/server/src/test/java/org/apache/cloudstack/vm/lease/VMLeaseManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/lease/VMLeaseManagerImplTest.java new file mode 100644 index 000000000000..9e01c9161e26 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/lease/VMLeaseManagerImplTest.java @@ -0,0 +1,263 @@ +/* + * 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.alert.AlertManager; +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.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.dao.ConfigurationDao; +import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +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.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.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 + private ConfigurationDao configurationDao; + + @Mock + private AlertManager alertManager; + + @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 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 expiredVms = Arrays.asList(vm); + when(userVmJoinDao.listEligibleInstancesWithExpiredLease()).thenReturn(expiredVms); + when(userVmJoinDao.findById(1L)).thenReturn(vm); + doReturn(1L).when(vmLeaseManager).executeStopInstanceJob(eq(vm), eq(true), anyLong()); + try (MockedStatic 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), eq(true), anyLong()); + } + + @Test + public void testReallyRunDestroyAction() { + UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false, DESTROY); + List expiredVms = Arrays.asList(vm); + when(userVmJoinDao.listEligibleInstancesWithExpiredLease()).thenReturn(expiredVms); + when(userVmJoinDao.findById(1L)).thenReturn(vm); + doReturn(1L).when(vmLeaseManager).executeDestroyInstanceJob(eq(vm), eq(true), anyLong()); + try (MockedStatic 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), eq(true), 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(true), eq(123L)); + Long jobId = vmLeaseManager.executeExpiryAction(vm, VMLeaseManager.ExpiryAction.STOP, 123L); + assertNotNull(jobId); + assertEquals(1L, jobId.longValue()); + verify(vmLeaseManager).executeStopInstanceJob(eq(vm), eq(true), 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(true), eq(123L)); + Long jobId = vmLeaseManager.executeExpiryAction(vm, VMLeaseManager.ExpiryAction.DESTROY, 123L); + assertNotNull(jobId); + assertEquals(1L, jobId.longValue()); + verify(vmLeaseManager).executeDestroyInstanceJob(eq(vm), eq(true), eq(123L)); + } + + @Test + public void testExecuteStopInstanceJob() { + UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false); + // Mock the static ComponentContext + try (MockedStatic 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, true, 123L); + assertEquals(1L, jobId); + ArgumentCaptor 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 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, true, 123L); + assertEquals(1L, jobId); + ArgumentCaptor 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)); + } + + 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; + } +} diff --git a/test/integration/component/test_deploy_vm_lease.py b/test/integration/component/test_deploy_vm_lease.py new file mode 100644 index 000000000000..4fe084841966 --- /dev/null +++ b/test/integration/component/test_deploy_vm_lease.py @@ -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) diff --git a/test/integration/smoke/test_service_offerings.py b/test/integration/smoke/test_service_offerings.py index c6a14a64471d..a523cd0e6987 100644 --- a/test/integration/smoke/test_service_offerings.py +++ b/test/integration/smoke/test_service_offerings.py @@ -35,7 +35,8 @@ get_domain, get_zone, get_test_template, - list_hosts) + list_hosts, + is_config_suitable) from nose.plugins.attrib import attr import time @@ -356,6 +357,164 @@ def test_05_create_service_offering_with_root_encryption_type(self): ) 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): diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 557434ea2ee3..5df254ced6d9 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -527,7 +527,8 @@ def create(cls, apiclient, services, templateid=None, accountid=None, customcpuspeed=None, custommemory=None, rootdisksize=None, rootdiskcontroller=None, vpcid=None, macaddress=None, datadisktemplate_diskoffering_list={}, 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""" cmd = deployVirtualMachine.deployVirtualMachineCmd() @@ -691,6 +692,12 @@ def create(cls, apiclient, services, templateid=None, accountid=None, if extraconfig: cmd.extraconfig = extraconfig + if leaseduration: + cmd.leaseduration = leaseduration + + if leaseexpiryaction: + cmd.leaseexpiryaction = leaseexpiryaction + virtual_machine = apiclient.deployVirtualMachine(cmd, method=method) if 'password' in list(virtual_machine.__dict__.keys()): diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index b4ea945cd257..94dae64cead7 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1165,6 +1165,7 @@ "label.instancename": "Internal name", "label.instanceport": "Instance port", "label.instances": "Instances", +"label.leasedinstances": "Leased Instances", "label.interface.route.table": "Interface Route Table", "label.interface.router.table": "Interface Router Table", "label.intermediate.certificate": "Intermediate certificate", @@ -2659,6 +2660,15 @@ "label.bucket.policy": "Bucket Policy", "label.usersecretkey": "Secret Key", "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.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.", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index bbabd8c801d8..071daccc0a62 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -144,7 +144,7 @@
{{ dataResource[item] }}
- +
{{ $t('label.' + item.replace('date', '.date.and.time'))}}
@@ -219,6 +219,9 @@ export default { items.push('startdate') items.push('enddate') } + if (this.$route.meta.name === 'vm') { + items.push('leaseexpirydate') + } return items }, vnfAccessMethods () { diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 6ecf4885ce51..2de56f951fda 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -93,6 +93,9 @@ {{ $t('label.archived') }} + + {{ $t('label.remainingdays') + ': ' + (resource.leaseduration > -1 ? resource.leaseduration + 'd' : 'Over') }} + + + + + + + + + + + + + + + + + + + + +
{{ $t('label.cancel') }} @@ -165,6 +193,15 @@ export default { groups: { loading: false, 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 } } }, @@ -187,9 +224,14 @@ export default { group: this.resource.group, securitygroupids: this.resource.securitygroup.map(x => x.id), 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 () { this.fetchZoneDetails() @@ -328,7 +370,6 @@ export default { }) }) }, - handleSubmit () { this.formRef.value.validate().then(() => { const values = toRaw(this.form) @@ -357,6 +398,13 @@ export default { if (values.userdata && values.userdata.length > 0) { params.userdata = this.$toBase64AndURIEncoded(values.userdata) } + if (values.leaseduration && values.leaseduration !== undefined) { + params.leaseduration = values.leaseduration + } + if (values.leaseexpiryaction && values.leaseexpiryaction !== undefined) { + params.leaseexpiryaction = values.leaseexpiryaction + } + this.loading = true api('updateVirtualMachine', {}, 'POST', params).then(json => { @@ -375,6 +423,21 @@ export default { }, onCloseAction () { 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() } } } diff --git a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue index 4450ce1144cd..b52918f2b550 100644 --- a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue +++ b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue @@ -36,6 +36,23 @@ +
@@ -119,7 +136,8 @@ export default { key: 'name', dataIndex: 'name', title: this.$t('label.serviceofferingid'), - width: '40%' + width: '40%', + slots: { customRender: 'displayText' } }, { key: 'cpu', @@ -191,7 +209,8 @@ export default { name: item.name, cpu: cpuNumberValue.length > 0 ? `${cpuNumberValue} CPU x ${cpuSpeedValue} Ghz` : '', 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) } } + }, + getRemainingLeaseText (leaseDuration) { + if (leaseDuration > 0) { + return leaseDuration + (leaseDuration === 1 ? ' day' : ' days') + } else if (leaseDuration === 0) { + return 'expiring today' + } else { + return 'over' + } } } } diff --git a/ui/src/views/dashboard/UsageDashboard.vue b/ui/src/views/dashboard/UsageDashboard.vue index 18b90bb837a7..507c2955acfa 100644 --- a/ui/src/views/dashboard/UsageDashboard.vue +++ b/ui/src/views/dashboard/UsageDashboard.vue @@ -63,6 +63,18 @@ + + + + + + + { + this.loading = false + this.data.leasedinstances = json?.listvirtualmachinesresponse?.count + if (!this.data.leasedinstances) { + this.data.leasedinstances = 0 + } + }) + } }, listEvents () { if (!('listEvents' in this.$store.getters.apis)) { diff --git a/ui/src/views/offering/AddComputeOffering.vue b/ui/src/views/offering/AddComputeOffering.vue index 1fd600ae566e..640779acdc38 100644 --- a/ui/src/views/offering/AddComputeOffering.vue +++ b/ui/src/views/offering/AddComputeOffering.vue @@ -349,6 +349,34 @@ + + + + + + + + + + + + + + + + + + + +