diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index 29803d5271b4..2761b7bcaad0 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -101,4 +101,7 @@ 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"; } 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 3e8b329cac78..7845968f4360 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 ONLY_LEASED_INSTANCES = "onlyleasedinstances"; 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..333c9aa33a65 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,14 @@ public class CreateServiceOfferingCmd extends BaseCmd { since="4.20") private Boolean purgeResources; + @Parameter(name = ApiConstants.INSTANCE_LEASE_DURATION, type = CommandType.LONG, + description = "Number of days instance is leased for.", + since = "4.21.0") + private Long leaseDuration; + @Parameter(name = ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION, type = CommandType.STRING, since = "4.21.0", + description = "Lease expiry action") + private String leaseExpiryAction; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -487,6 +494,14 @@ public boolean getEncryptRoot() { return false; } + public String getLeaseExpiryAction() { + return leaseExpiryAction; + } + + public Long 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..df259b2125d2 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 @@ -278,6 +278,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.LONG, since = "4.21.0", + description = "Number of days instance is leased for.") + private Long leaseDuration; + + @Parameter(name = ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION, type = CommandType.STRING, since = "4.21.0", + description = "Lease expiry action") + private String leaseExpiryAction; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -475,6 +483,14 @@ public String getPassword() { return password; } + public Long 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 50e1798112d2..4aa2f9dd1bc7 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 @@ -149,6 +149,11 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements @Parameter(name = ApiConstants.USER_DATA, type = CommandType.BOOLEAN, description = "Whether to return the VMs' user data or not. By default, user data will not be returned.", since = "4.18.0.0") private Boolean showUserData; + @Parameter(name = ApiConstants.ONLY_LEASED_INSTANCES, type = CommandType.BOOLEAN, + description = "Whether to return the Leased instances", + since = "4.21.0") + private Boolean onlyLeasedInstances = false; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -322,4 +327,10 @@ protected void updateVMResponse(List response) { vmResponse.setResourceIconResponse(iconResponse); } } + + + public Boolean getOnlyLeasedInstances() { + return 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..07ccfffaab0c 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 @@ -154,6 +154,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.LONG, since = "4.21.0", + description = "Number of days instance is leased for.") + private Long leaseDuration; + + @Parameter(name = ApiConstants.INSTANCE_LEASE_EXPIRY_ACTION, type = CommandType.STRING, since = "4.21.0", + description = "Lease expiry action") + private String leaseExpiryAction; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -324,4 +332,13 @@ public Long getApiResourceId() { public ApiCommandResourceType getApiResourceType() { return ApiCommandResourceType.VirtualMachine; } + + public Long 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..80c1c406c120 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 Long 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 Long getLeaseDuration() { + return leaseDuration; + } + + public void setLeaseDuration(Long 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..382b7852df09 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", since = "4.21.0") + private Long 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 Long getLeaseDuration() { + return leaseDuration; + } + + public void setLeaseDuration(Long 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..3347585b2c6b 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", 10L); + 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/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index b01243ad989d..3e7461d48f38 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -37,3 +37,315 @@ WHERE rp.rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaCreditsList'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host', 'last_mgmt_server_id', 'bigint unsigned DEFAULT NULL COMMENT "last management server this host is connected to" AFTER `mgmt_server_id`'); + +-- Add column lease_duration and lease_expiry_action to service_offering_view +DROP VIEW IF EXISTS `cloud`.`service_offering_view`; + +CREATE VIEW `cloud`.`service_offering_view` AS +SELECT + `service_offering`.`id` AS `id`, + `service_offering`.`uuid` AS `uuid`, + `service_offering`.`name` AS `name`, + `service_offering`.`state` AS `state`, + `service_offering`.`display_text` AS `display_text`, + `disk_offering`.`provisioning_type` AS `provisioning_type`, + `service_offering`.`created` AS `created`, + `disk_offering`.`tags` AS `tags`, + `service_offering`.`removed` AS `removed`, + `disk_offering`.`use_local_storage` AS `use_local_storage`, + `service_offering`.`system_use` AS `system_use`, + `disk_offering`.`id` AS `disk_offering_id`, + `disk_offering`.`name` AS `disk_offering_name`, + `disk_offering`.`uuid` AS `disk_offering_uuid`, + `disk_offering`.`display_text` AS `disk_offering_display_text`, + `disk_offering`.`customized_iops` AS `customized_iops`, + `disk_offering`.`min_iops` AS `min_iops`, + `disk_offering`.`max_iops` AS `max_iops`, + `disk_offering`.`hv_ss_reserve` AS `hv_ss_reserve`, + `disk_offering`.`bytes_read_rate` AS `bytes_read_rate`, + `disk_offering`.`bytes_read_rate_max` AS `bytes_read_rate_max`, + `disk_offering`.`bytes_read_rate_max_length` AS `bytes_read_rate_max_length`, + `disk_offering`.`bytes_write_rate` AS `bytes_write_rate`, + `disk_offering`.`bytes_write_rate_max` AS `bytes_write_rate_max`, + `disk_offering`.`bytes_write_rate_max_length` AS `bytes_write_rate_max_length`, + `disk_offering`.`iops_read_rate` AS `iops_read_rate`, + `disk_offering`.`iops_read_rate_max` AS `iops_read_rate_max`, + `disk_offering`.`iops_read_rate_max_length` AS `iops_read_rate_max_length`, + `disk_offering`.`iops_write_rate` AS `iops_write_rate`, + `disk_offering`.`iops_write_rate_max` AS `iops_write_rate_max`, + `disk_offering`.`iops_write_rate_max_length` AS `iops_write_rate_max_length`, + `disk_offering`.`cache_mode` AS `cache_mode`, + `disk_offering`.`disk_size` AS `root_disk_size`, + `disk_offering`.`encrypt` AS `encrypt_root`, + `service_offering`.`cpu` AS `cpu`, + `service_offering`.`speed` AS `speed`, + `service_offering`.`ram_size` AS `ram_size`, + `service_offering`.`nw_rate` AS `nw_rate`, + `service_offering`.`mc_rate` AS `mc_rate`, + `service_offering`.`ha_enabled` AS `ha_enabled`, + `service_offering`.`limit_cpu_use` AS `limit_cpu_use`, + `service_offering`.`host_tag` AS `host_tag`, + `service_offering`.`default_use` AS `default_use`, + `service_offering`.`vm_type` AS `vm_type`, + `service_offering`.`sort_key` AS `sort_key`, + `service_offering`.`is_volatile` AS `is_volatile`, + `service_offering`.`deployment_planner` AS `deployment_planner`, + `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, + GROUP_CONCAT(DISTINCT(domain.path)) AS domain_path, + GROUP_CONCAT(DISTINCT(zone.id)) AS zone_id, + GROUP_CONCAT(DISTINCT(zone.uuid)) AS zone_uuid, + GROUP_CONCAT(DISTINCT(zone.name)) AS zone_name, + IFNULL(`min_compute_details`.`value`, `cpu`) AS min_cpu, + IFNULL(`max_compute_details`.`value`, `cpu`) AS max_cpu, + IFNULL(`min_memory_details`.`value`, `ram_size`) AS min_memory, + IFNULL(`max_memory_details`.`value`, `ram_size`) AS max_memory +FROM + `cloud`.`service_offering` + INNER JOIN + `cloud`.`disk_offering` ON service_offering.disk_offering_id = disk_offering.id + LEFT JOIN + `cloud`.`service_offering_details` AS `domain_details` ON `domain_details`.`service_offering_id` = `service_offering`.`id` AND `domain_details`.`name`='domainid' + LEFT JOIN + `cloud`.`domain` AS `domain` ON FIND_IN_SET(`domain`.`id`, `domain_details`.`value`) + LEFT JOIN + `cloud`.`service_offering_details` AS `zone_details` ON `zone_details`.`service_offering_id` = `service_offering`.`id` AND `zone_details`.`name`='zoneid' + LEFT JOIN + `cloud`.`data_center` AS `zone` ON FIND_IN_SET(`zone`.`id`, `zone_details`.`value`) + LEFT JOIN + `cloud`.`service_offering_details` AS `min_compute_details` ON `min_compute_details`.`service_offering_id` = `service_offering`.`id` + AND `min_compute_details`.`name` = 'mincpunumber' + LEFT JOIN + `cloud`.`service_offering_details` AS `max_compute_details` ON `max_compute_details`.`service_offering_id` = `service_offering`.`id` + AND `max_compute_details`.`name` = 'maxcpunumber' + LEFT JOIN + `cloud`.`service_offering_details` AS `min_memory_details` ON `min_memory_details`.`service_offering_id` = `service_offering`.`id` + AND `min_memory_details`.`name` = 'minmemory' + LEFT JOIN + `cloud`.`service_offering_details` AS `max_memory_details` ON `max_memory_details`.`service_offering_id` = `service_offering`.`id` + AND `max_memory_details`.`name` = 'maxmemory' + 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`; + +-- Add lease_expiry_date and lease_expiry_action to user_vm_view + +DROP VIEW IF EXISTS `cloud`.`user_vm_view`; + +CREATE VIEW `user_vm_view` AS +SELECT + `vm_instance`.`id` AS `id`, + `vm_instance`.`name` AS `name`, + `user_vm`.`display_name` AS `display_name`, + `user_vm`.`user_data` AS `user_data`, + `user_vm`.`user_vm_type` AS `user_vm_type`, + `account`.`id` AS `account_id`, + `account`.`uuid` AS `account_uuid`, + `account`.`account_name` AS `account_name`, + `account`.`type` AS `account_type`, + `domain`.`id` AS `domain_id`, + `domain`.`uuid` AS `domain_uuid`, + `domain`.`name` AS `domain_name`, + `domain`.`path` AS `domain_path`, + `projects`.`id` AS `project_id`, + `projects`.`uuid` AS `project_uuid`, + `projects`.`name` AS `project_name`, + `instance_group`.`id` AS `instance_group_id`, + `instance_group`.`uuid` AS `instance_group_uuid`, + `instance_group`.`name` AS `instance_group_name`, + `vm_instance`.`uuid` AS `uuid`, + `vm_instance`.`user_id` AS `user_id`, + `vm_instance`.`last_host_id` AS `last_host_id`, + `vm_instance`.`vm_type` AS `type`, + `vm_instance`.`limit_cpu_use` AS `limit_cpu_use`, + `vm_instance`.`created` AS `created`, + `vm_instance`.`state` AS `state`, + `vm_instance`.`update_time` AS `update_time`, + `vm_instance`.`removed` AS `removed`, + `vm_instance`.`ha_enabled` AS `ha_enabled`, + `vm_instance`.`hypervisor_type` AS `hypervisor_type`, + `vm_instance`.`instance_name` AS `instance_name`, + `vm_instance`.`guest_os_id` AS `guest_os_id`, + `vm_instance`.`display_vm` AS `display_vm`, + `vm_instance`.`delete_protection` AS `delete_protection`, + `guest_os`.`uuid` AS `guest_os_uuid`, + `vm_instance`.`pod_id` AS `pod_id`, + `host_pod_ref`.`uuid` AS `pod_uuid`, + `vm_instance`.`private_ip_address` AS `private_ip_address`, + `vm_instance`.`private_mac_address` AS `private_mac_address`, + `vm_instance`.`vm_type` AS `vm_type`, + `data_center`.`id` AS `data_center_id`, + `data_center`.`uuid` AS `data_center_uuid`, + `data_center`.`name` AS `data_center_name`, + `data_center`.`is_security_group_enabled` AS `security_group_enabled`, + `data_center`.`networktype` AS `data_center_network_type`, + `host`.`id` AS `host_id`, + `host`.`uuid` AS `host_uuid`, + `host`.`name` AS `host_name`, + `host`.`cluster_id` AS `cluster_id`, + `host`.`status` AS `host_status`, + `host`.`resource_state` AS `host_resource_state`, + `vm_template`.`id` AS `template_id`, + `vm_template`.`uuid` AS `template_uuid`, + `vm_template`.`name` AS `template_name`, + `vm_template`.`type` AS `template_type`, + `vm_template`.`format` AS `template_format`, + `vm_template`.`display_text` AS `template_display_text`, + `vm_template`.`enable_password` AS `password_enabled`, + `iso`.`id` AS `iso_id`, + `iso`.`uuid` AS `iso_uuid`, + `iso`.`name` AS `iso_name`, + `iso`.`display_text` AS `iso_display_text`, + `service_offering`.`id` AS `service_offering_id`, + `service_offering`.`uuid` AS `service_offering_uuid`, + `disk_offering`.`uuid` AS `disk_offering_uuid`, + `disk_offering`.`id` AS `disk_offering_id`, + (CASE + WHEN ISNULL(`service_offering`.`cpu`) THEN `custom_cpu`.`value` + ELSE `service_offering`.`cpu` + END) AS `cpu`, + (CASE + WHEN ISNULL(`service_offering`.`speed`) THEN `custom_speed`.`value` + ELSE `service_offering`.`speed` + END) AS `speed`, + (CASE + WHEN ISNULL(`service_offering`.`ram_size`) THEN `custom_ram_size`.`value` + ELSE `service_offering`.`ram_size` + END) AS `ram_size`, + `backup_offering`.`uuid` AS `backup_offering_uuid`, + `backup_offering`.`id` AS `backup_offering_id`, + `service_offering`.`name` AS `service_offering_name`, + `disk_offering`.`name` AS `disk_offering_name`, + `backup_offering`.`name` AS `backup_offering_name`, + `storage_pool`.`id` AS `pool_id`, + `storage_pool`.`uuid` AS `pool_uuid`, + `storage_pool`.`pool_type` AS `pool_type`, + `volumes`.`id` AS `volume_id`, + `volumes`.`uuid` AS `volume_uuid`, + `volumes`.`device_id` AS `volume_device_id`, + `volumes`.`volume_type` AS `volume_type`, + `security_group`.`id` AS `security_group_id`, + `security_group`.`uuid` AS `security_group_uuid`, + `security_group`.`name` AS `security_group_name`, + `security_group`.`description` AS `security_group_description`, + `nics`.`id` AS `nic_id`, + `nics`.`uuid` AS `nic_uuid`, + `nics`.`device_id` AS `nic_device_id`, + `nics`.`network_id` AS `network_id`, + `nics`.`ip4_address` AS `ip_address`, + `nics`.`ip6_address` AS `ip6_address`, + `nics`.`ip6_gateway` AS `ip6_gateway`, + `nics`.`ip6_cidr` AS `ip6_cidr`, + `nics`.`default_nic` AS `is_default_nic`, + `nics`.`gateway` AS `gateway`, + `nics`.`netmask` AS `netmask`, + `nics`.`mac_address` AS `mac_address`, + `nics`.`broadcast_uri` AS `broadcast_uri`, + `nics`.`isolation_uri` AS `isolation_uri`, + `vpc`.`id` AS `vpc_id`, + `vpc`.`uuid` AS `vpc_uuid`, + `networks`.`uuid` AS `network_uuid`, + `networks`.`name` AS `network_name`, + `networks`.`traffic_type` AS `traffic_type`, + `networks`.`guest_type` AS `guest_type`, + `user_ip_address`.`id` AS `public_ip_id`, + `user_ip_address`.`uuid` AS `public_ip_uuid`, + `user_ip_address`.`public_ip_address` AS `public_ip_address`, + `ssh_details`.`value` AS `keypair_names`, + `resource_tags`.`id` AS `tag_id`, + `resource_tags`.`uuid` AS `tag_uuid`, + `resource_tags`.`key` AS `tag_key`, + `resource_tags`.`value` AS `tag_value`, + `resource_tags`.`domain_id` AS `tag_domain_id`, + `domain`.`uuid` AS `tag_domain_uuid`, + `domain`.`name` AS `tag_domain_name`, + `resource_tags`.`account_id` AS `tag_account_id`, + `account`.`account_name` AS `tag_account_name`, + `resource_tags`.`resource_id` AS `tag_resource_id`, + `resource_tags`.`resource_uuid` AS `tag_resource_uuid`, + `resource_tags`.`resource_type` AS `tag_resource_type`, + `resource_tags`.`customer` AS `tag_customer`, + `async_job`.`id` AS `job_id`, + `async_job`.`uuid` AS `job_uuid`, + `async_job`.`job_status` AS `job_status`, + `async_job`.`account_id` AS `job_account_id`, + `affinity_group`.`id` AS `affinity_group_id`, + `affinity_group`.`uuid` AS `affinity_group_uuid`, + `affinity_group`.`name` AS `affinity_group_name`, + `affinity_group`.`description` AS `affinity_group_description`, + `autoscale_vmgroups`.`id` AS `autoscale_vmgroup_id`, + `autoscale_vmgroups`.`uuid` AS `autoscale_vmgroup_uuid`, + `autoscale_vmgroups`.`name` AS `autoscale_vmgroup_name`, + `vm_instance`.`dynamically_scalable` AS `dynamically_scalable`, + `user_data`.`id` AS `user_data_id`, + `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`, + `lease_expiry_date`.`value` AS `lease_expiry_date`, + `lease_expiry_action`.`value` AS `lease_expiry_action` +FROM + (((((((((((((((((((((((((((((((((((`user_vm` + JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) + AND ISNULL(`vm_instance`.`removed`)))) + JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) + JOIN `domain` ON ((`vm_instance`.`domain_id` = `domain`.`id`))) + LEFT JOIN `guest_os` ON ((`vm_instance`.`guest_os_id` = `guest_os`.`id`))) + LEFT JOIN `host_pod_ref` ON ((`vm_instance`.`pod_id` = `host_pod_ref`.`id`))) + LEFT JOIN `projects` ON ((`projects`.`project_account_id` = `account`.`id`))) + LEFT JOIN `instance_group_vm_map` ON ((`vm_instance`.`id` = `instance_group_vm_map`.`instance_id`))) + LEFT JOIN `instance_group` ON ((`instance_group_vm_map`.`group_id` = `instance_group`.`id`))) + LEFT JOIN `data_center` ON ((`vm_instance`.`data_center_id` = `data_center`.`id`))) + LEFT JOIN `host` ON ((`vm_instance`.`host_id` = `host`.`id`))) + LEFT JOIN `vm_template` ON ((`vm_instance`.`vm_template_id` = `vm_template`.`id`))) + LEFT JOIN `vm_template` `iso` ON ((`iso`.`id` = `user_vm`.`iso_id`))) + LEFT JOIN `volumes` ON ((`vm_instance`.`id` = `volumes`.`instance_id`))) + LEFT JOIN `service_offering` ON ((`vm_instance`.`service_offering_id` = `service_offering`.`id`))) + LEFT JOIN `disk_offering` `svc_disk_offering` ON ((`volumes`.`disk_offering_id` = `svc_disk_offering`.`id`))) + LEFT JOIN `disk_offering` ON ((`volumes`.`disk_offering_id` = `disk_offering`.`id`))) + LEFT JOIN `backup_offering` ON ((`vm_instance`.`backup_offering_id` = `backup_offering`.`id`))) + LEFT JOIN `storage_pool` ON ((`volumes`.`pool_id` = `storage_pool`.`id`))) + LEFT JOIN `security_group_vm_map` ON ((`vm_instance`.`id` = `security_group_vm_map`.`instance_id`))) + LEFT JOIN `security_group` ON ((`security_group_vm_map`.`security_group_id` = `security_group`.`id`))) + LEFT JOIN `user_data` ON ((`user_data`.`id` = `user_vm`.`user_data_id`))) + LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`) + AND ISNULL(`nics`.`removed`)))) + LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`))) + LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) + AND ISNULL(`vpc`.`removed`)))) + LEFT JOIN `user_ip_address` FORCE INDEX(`fk_user_ip_address__vm_id`) ON ((`user_ip_address`.`vm_id` = `vm_instance`.`id`))) + LEFT JOIN `user_vm_details` `ssh_details` ON (((`ssh_details`.`vm_id` = `vm_instance`.`id`) + AND (`ssh_details`.`name` = 'SSH.KeyPairNames')))) + LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_instance`.`id`) + AND (`resource_tags`.`resource_type` = 'UserVm')))) + LEFT JOIN `async_job` ON (((`async_job`.`instance_id` = `vm_instance`.`id`) + AND (`async_job`.`instance_type` = 'VirtualMachine') + AND (`async_job`.`job_status` = 0)))) + LEFT JOIN `affinity_group_vm_map` ON ((`vm_instance`.`id` = `affinity_group_vm_map`.`instance_id`))) + LEFT JOIN `affinity_group` ON ((`affinity_group_vm_map`.`affinity_group_id` = `affinity_group`.`id`))) + LEFT JOIN `autoscale_vmgroup_vm_map` ON ((`autoscale_vmgroup_vm_map`.`instance_id` = `vm_instance`.`id`))) + LEFT JOIN `autoscale_vmgroups` ON ((`autoscale_vmgroup_vm_map`.`vmgroup_id` = `autoscale_vmgroups`.`id`))) + LEFT JOIN `user_vm_details` `custom_cpu` ON (((`custom_cpu`.`vm_id` = `vm_instance`.`id`) + AND (`custom_cpu`.`name` = 'CpuNumber')))) + 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'))) + 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_expiry_action` ON (((`lease_expiry_action`.`vm_id` = `vm_instance`.`id`) + AND (`lease_expiry_action`.`name` = 'leaseexpiryaction')))); 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..d7604b426cf5 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,9 @@ 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` FROM (((((((((((((((((((((((((((((((((((`user_vm` JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) @@ -215,4 +217,8 @@ 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_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 846fc1921992..16a081390ed7 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; @@ -1178,8 +1179,8 @@ private Pair, Integer> searchForVmGroupsInternal(ListV @Override public ListResponse searchForUserVMs(ListVMsCmd cmd) { - Pair, Integer> result = searchForUserVMsInternal(cmd); ListResponse response = new ListResponse<>(); + Pair, Integer> result = searchForUserVMsInternal(cmd); if (cmd.getRetrieveOnlyResourceCount()) { response.setResponses(new ArrayList<>(), result.second()); @@ -1477,6 +1478,16 @@ private Pair, Integer> searchForUserVMIdsAndCount(ListVMsCmd cmd) { userVmSearchBuilder.join("tags", resourceTagSearch, resourceTagSearch.entity().getResourceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); } + if (cmd.getOnlyLeasedInstances() != null && cmd.getOnlyLeasedInstances()) { + if (VMLeaseManagerImpl.InstanceLeaseEnabled.value()) { + SearchBuilder leasedInstancesSearch = userVmDetailsDao.createSearchBuilder(); + leasedInstancesSearch.and(leasedInstancesSearch.entity().getName(), SearchCriteria.Op.EQ).values(VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE); + userVmSearchBuilder.join("userVmToLeased", leasedInstancesSearch, leasedInstancesSearch.entity().getResourceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } else { + logger.warn("Lease feature is not enabled, onlyleasedinstances will be considered as false"); + } + } + 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..8a89aa4eb481 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() >= -1L) { + 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..f620c139a060 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 @@ -46,4 +46,8 @@ UserVmResponse newUserVmResponse(ResponseView view, String objectName, UserVmJoi List listByAccountServiceOfferingTemplateAndNotInState(long accountId, List states, List offeringIds, List templateIds); + + List listLeaseExpiredInstances(); + + 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..f66161ce0303 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 { @@ -111,6 +115,8 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; private final SearchBuilder activeVmByIsoSearch; + private final SearchBuilder leaseOverInstanceSearch; + private final SearchBuilder leaseExpiringInstanceSearch; protected UserVmJoinDaoImpl() { @@ -124,6 +130,20 @@ protected UserVmJoinDaoImpl() { activeVmByIsoSearch.and("isoId", activeVmByIsoSearch.entity().getIsoId(), SearchCriteria.Op.EQ); activeVmByIsoSearch.and("stateNotIn", activeVmByIsoSearch.entity().getState(), SearchCriteria.Op.NIN); activeVmByIsoSearch.done(); + + leaseOverInstanceSearch = createSearchBuilder(); + leaseOverInstanceSearch.selectFields(leaseOverInstanceSearch.entity().getId(), leaseOverInstanceSearch.entity().getState(), + leaseOverInstanceSearch.entity().isDeleteProtection(), leaseOverInstanceSearch.entity().getUuid()); + leaseOverInstanceSearch.and("leaseExpired", leaseOverInstanceSearch.entity().getLeaseExpiryDate(), Op.LT); + leaseOverInstanceSearch.done(); + + leaseExpiringInstanceSearch = createSearchBuilder(); + leaseExpiringInstanceSearch.selectFields(leaseExpiringInstanceSearch.entity().getId(), leaseExpiringInstanceSearch.entity().getUuid(), + leaseExpiringInstanceSearch.entity().getPodId(), leaseExpiringInstanceSearch.entity().getDataCenterId()); + leaseExpiringInstanceSearch.and("leaseCurrentDate", leaseExpiringInstanceSearch.entity().getLeaseExpiryDate(), Op.GTEQ); + leaseExpiringInstanceSearch.and("leaseExpiresOnDate", leaseExpiringInstanceSearch.entity().getLeaseExpiryDate(), Op.LT); + leaseExpiringInstanceSearch.done(); + } @Override @@ -426,10 +446,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 +466,13 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us userVmResponse.setUserDataPolicy(userVm.getUserDataPolicy()); } + if (VMLeaseManagerImpl.InstanceLeaseEnabled.value() && userVm.getLeaseExpiryDate() != null) { + userVmResponse.setLeaseExpiryAction(userVm.getLeaseExpiryAction()); + userVmResponse.setLeaseExpiryDate(userVm.getLeaseExpiryDate()); + long leaseDuration = getLeaseDuration(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 +482,13 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us return userVmResponse; } + + private long getLeaseDuration(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 +751,24 @@ public List listByAccountServiceOfferingTemplateAndNotInState(long sc.setParameters("displayVm", 1); return customSearch(sc, null); } + + @Override + public List listLeaseExpiredInstances() { + SearchCriteria sc = leaseOverInstanceSearch.create(); + sc.setParameters("leaseExpired", new Date()); + return listBy(sc); + } + + @Override + public List listLeaseInstancesExpiringInDays(int days) { + SearchCriteria sc = leaseExpiringInstanceSearch.create(); + Date currentDate = new Date(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(currentDate); + calendar.add(Calendar.DAY_OF_MONTH, days); + Date nextDate = calendar.getTime(); + sc.setParameters("leaseCurrentDate", currentDate); + sc.setParameters("leaseExpiresOnDate", 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..c59138e139d4 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 Long leaseDuration; + + @Column(name = "lease_expiry_action") + private String leaseExpiryAction; + public ServiceOfferingJoinVO() { } @@ -459,4 +463,12 @@ public String getDiskOfferingDisplayText() { } public boolean getEncryptRoot() { return encryptRoot; } + + public Long 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..7f60deb77a91 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,12 @@ 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; public UserVmJoinVO() { // Empty constructor @@ -949,7 +956,7 @@ public Boolean isDynamicallyScalable() { return isDynamicallyScalable; } - public Boolean getDeleteProtection() { + public Boolean isDeleteProtection() { return deleteProtection; } @@ -977,4 +984,16 @@ 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; + } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 56a86e65da02..719940611b61 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"; @@ -3319,6 +3319,10 @@ public ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) } } + // validate lease properties and set leaseExpiryAction + Long leaseDuration = cmd.getLeaseDuration(); + String leaseExpiryAction = validateAndGetLeaseExpiryAction(cmd.getLeaseDuration(), 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,7 +3331,7 @@ 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, @@ -3340,7 +3344,7 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole 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 boolean isCustomized, final boolean encryptRoot, final boolean purgeResources, Long leaseDuration, String leaseExpiryAction) { // Filter child domains when both parent and child domains are present List filteredDomainIds = filterChildSubDomains(domainIds); @@ -3447,6 +3451,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 +3480,42 @@ 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 > -1 and has valid leaseExpiryAction provided or configured + * @param leaseDuration + * @param cmdExpiryAction + * @return leaseExpiryAction + */ + public static String validateAndGetLeaseExpiryAction(Long leaseDuration, String cmdExpiryAction) { + String leaseExpiryAction = null; + if (!VMLeaseManagerImpl.InstanceLeaseEnabled.value() + || (leaseDuration == null && StringUtils.isEmpty(cmdExpiryAction))) { // both are null + return leaseExpiryAction; + } + + // 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"); + } + + leaseExpiryAction = StringUtils.isNotEmpty(cmdExpiryAction) ? cmdExpiryAction : VMLeaseManager.InstanceLeaseExpiryAction.valueIn(CallContext.current().getCallingAccountId()); + if (StringUtils.isNotEmpty(leaseExpiryAction)) { + try { + VMLeaseManager.ExpiryAction.valueOf(leaseExpiryAction); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException("Invalid value configured for leaseexpiryaction, valid values are: " + + com.cloud.utils.EnumUtils.listValues(VMLeaseManager.ExpiryAction.values())); + } + } + return leaseExpiryAction != null ? leaseExpiryAction.toUpperCase() : null; + } + @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 2eaf437af01a..b2f7bde2ee54 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -638,6 +638,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; @@ -4496,6 +4497,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..c54fdf2302a5 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,148 @@ 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.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.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +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 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 { @@ -2909,6 +2915,12 @@ public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableEx } } } + + Long 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 +6165,10 @@ public UserVm createVirtualMachine(DeployVMCmd cmd) throws InsufficientCapacityE } } + Long 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 +6267,115 @@ public UserVm createVirtualMachine(DeployVMCmd cmd) throws InsufficientCapacityE } } } + + applyLeaseOnCreateInstance(vm, leaseDuration, leaseExpiryAction, svcOffering); return vm; } + protected void validateLeaseProperties(Long leaseDuration, String leaseExpiryAction) { + if (!VMLeaseManagerImpl.InstanceLeaseEnabled.value() + || (leaseDuration == null && StringUtils.isEmpty(leaseExpiryAction))) { // if both are null + return; + } + + boolean bothValuesSet = true; + if (leaseDuration != null) { + if (leaseDuration == -1) { // special condition used to disable lease for instance + return; + } + // both params have value + if (leaseDuration < -1) { + throw new InvalidParameterValueException("Invalid value given for leaseduration, lesser than -1 is not supported "); + } + + 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 feature is enabled + * use leaseDuration and leaseExpiryAction passed in the cmd + * if passed leaseDuration is -1, get leaseDuration from service_offering + * @param vm + * @param leaseDuration + * @param leaseExpiryAction + * @param serviceOfferingJoinVO + */ + private void applyLeaseOnCreateInstance(UserVm vm, Long 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(); + if (StringUtils.isEmpty(leaseExpiryAction)) { + leaseExpiryAction = VMLeaseManager.InstanceLeaseExpiryAction.valueIn(vm.getAccountId()); + } + addLeaseDetailsForInstance(vm, leaseDuration, leaseExpiryAction); + } + + + protected void applyLeaseOnUpdateInstance(UserVm instance, Long leaseDuration, String leaseExpiryAction) { + if (leaseDuration == null) { + return; + } + + // find if lease feature is or was enabled for instance + boolean shouldApplyLease = VMLeaseManagerImpl.InstanceLeaseEnabled.value(); + if (!shouldApplyLease) { + UserVmDetailVO vmDetail = userVmDetailsDao.findDetail(instance.getId(), VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE); + if (vmDetail != null && StringUtils.isNotEmpty(vmDetail.getValue())) { + shouldApplyLease = true; + } + } + + // nothing to be done if lease feature is/was not enabled for vm + if (!shouldApplyLease) { + return; + } + + if (leaseDuration == -1L) { + userVmDetailsDao.removeDetail(instance.getId(), VmDetailConstants.INSTANCE_LEASE_EXPIRY_DATE); + userVmDetailsDao.removeDetail(instance.getId(), VmDetailConstants.INSTANCE_LEASE_EXPIRY_ACTION); + return; + } + + leaseExpiryAction = Strings.isNotEmpty(leaseExpiryAction) ? leaseExpiryAction : VMLeaseManager.InstanceLeaseExpiryAction.valueIn(instance.getAccountId()); + addLeaseDetailsForInstance(instance, leaseDuration, leaseExpiryAction); + } + + private void addLeaseDetailsForInstance(UserVm vm, Long leaseDuration, String leaseExpiryAction) { + LocalDate today = LocalDate.now(); + Date leaseExpiryDate = Date.from(today.plusDays(leaseDuration).atTime(LocalTime.MAX).atZone(ZoneId.systemDefault()).toInstant()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + 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); + + 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..f9e128aad68a --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManager.java @@ -0,0 +1,54 @@ +/* + * 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 com.cloud.utils.concurrency.Scheduler; +import org.apache.cloudstack.framework.config.ConfigKey; + +import java.util.List; + +public interface VMLeaseManager extends Manager, Scheduler { + + enum ExpiryAction { + STOP, + DESTROY + } + + ConfigKey InstanceLeaseDuration = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Long.class, + "instance.lease.duration", "90", "The default lease duration in days for the instance", + true, List.of(ConfigKey.Scope.Account, ConfigKey.Scope.Domain)); + + ConfigKey InstanceLeaseExpiryAction = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, String.class, + "instance.lease.expiryaction", "STOP", "Default action to be taken at instance lease expiry", + true, List.of(ConfigKey.Scope.Account, ConfigKey.Scope.Domain)); + + ConfigKey InstanceLeaseSchedulerInterval = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Long.class, + "instance.lease.scheduler.interval", "86400", "VM Lease Scheduler interval in seconds", + true, 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", + true, List.of(ConfigKey.Scope.Global)); + + ConfigKey InstanceLeaseAlertStartsAt = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Long.class, + "instance.lease.alert.startat", "7", "Denotes remaining day at which alerting will start", + true, List.of(ConfigKey.Scope.Global)); + +} 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..03c240a3189a --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/lease/VMLeaseManagerImpl.java @@ -0,0 +1,302 @@ +/* + * 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.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.db.GlobalLock; +import com.cloud.vm.VirtualMachine; +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.ManagedContextTimerTask; +import org.apache.commons.lang3.time.DateUtils; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +import static com.cloud.vm.VirtualMachine.State.Destroyed; +import static com.cloud.vm.VirtualMachine.State.Expunging; +import static com.cloud.vm.VirtualMachine.State.Unknown; + +public class VMLeaseManagerImpl extends ManagerBase implements VMLeaseManager, Configurable { + + + public static ConfigKey InstanceLeaseEnabled = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Boolean.class, + "instance.lease.enabled", "false", "Indicates whether to enable the Instance Lease feature", + true, List.of(ConfigKey.Scope.Global)); + + private static final int ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION = 5; // 5 seconds + + @Inject + private UserVmJoinDao userVmJoinDao; + + @Inject + private AlertManager alertManager; + + @Inject + private AsyncJobManager asyncJobManager; + + private AsyncJobDispatcher asyncJobDispatcher; + + Timer vmLeaseTimer; + + Timer vmLeaseAlterTimer; + + @Override + public String getConfigComponentName() { + return VMLeaseManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + InstanceLeaseEnabled, + InstanceLeaseDuration, + InstanceLeaseExpiryAction, + InstanceLeaseSchedulerInterval, + InstanceLeaseAlertSchedule, + InstanceLeaseAlertStartsAt + }; + } + + public void setAsyncJobDispatcher(final AsyncJobDispatcher dispatcher) { + asyncJobDispatcher = dispatcher; + } + + @Override + public boolean start() { + final TimerTask schedulerPollTask = new ManagedContextTimerTask() { + @Override + protected void runInContext() { + try { + poll(new Date()); + } catch (final Throwable t) { + logger.warn("Catch throwable in VM lease scheduler ", t); + } + } + }; + + final TimerTask leaseAlterSchedulerTask = new ManagedContextTimerTask() { + @Override + protected void runInContext() { + try { + alert(); + } catch (final Throwable t) { + logger.warn("Catch throwable in VM lease scheduler ", t); + } + } + }; + + vmLeaseTimer = new Timer("VMLeasePollTask"); + vmLeaseTimer.scheduleAtFixedRate(schedulerPollTask, 5_000L, InstanceLeaseSchedulerInterval.value() * 1000L); + + vmLeaseAlterTimer = new Timer("VMLeaseAlertPollTask"); + vmLeaseAlterTimer.scheduleAtFixedRate(leaseAlterSchedulerTask, 5_000L, InstanceLeaseAlertSchedule.value() * 1000) + ; + return true; + } + + protected void alert() { + // as feature is disabled, no action is required + if (!InstanceLeaseEnabled.value()) { + return; + } + + GlobalLock scanLock = GlobalLock.getInternLock("VMLeaseAlertScheduler"); + try { + if (scanLock.lock(ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION)) { + try { + List leaseExpiringForInstances = userVmJoinDao.listLeaseInstancesExpiringInDays(InstanceLeaseAlertStartsAt.value().intValue()); + for (UserVmJoinVO instance : leaseExpiringForInstances) { + alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_USERVM, instance.getDataCenterId(), instance.getPodId(), + "Lease expiring for instance id: " + instance.getUuid(), "Lease expiring for instance"); + } + } finally { + scanLock.unlock(); + } + } + } finally { + scanLock.releaseRef(); + } + } + + @Override + public void poll(Date timestamp) { + Date currentTimestamp = DateUtils.round(timestamp, Calendar.MINUTE); + String displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, currentTimestamp); + logger.debug("VMLeaseScheduler.poll is being called at {}", displayTime); + + if (!InstanceLeaseEnabled.value()) { + logger.debug("Instance lease feature is disabled, no action is required"); + return; + } + + GlobalLock scanLock = GlobalLock.getInternLock("VMLeaseScheduler"); + try { + if (scanLock.lock(ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION)) { + try { + reallyRun(); + } finally { + scanLock.unlock(); + } + } + } finally { + scanLock.releaseRef(); + } + } + + protected void reallyRun() { + // fetch user_instances having leaseDuration configured and has expired + List leaseExpiredInstances = userVmJoinDao.listLeaseExpiredInstances(); + List actionableInstanceIds = new ArrayList<>(); + // iterate over and skip ones with delete protection + for (UserVmJoinVO userVmVO : leaseExpiredInstances) { + if (userVmVO.isDeleteProtection() != null && userVmVO.isDeleteProtection()) { + logger.debug("Ignoring instance with id: {} as deleteProtection is enabled", userVmVO.getUuid()); + continue; + } + // state check, include instances not yet stopped or destroyed + if (!Arrays.asList(Destroyed, Expunging, Unknown, VirtualMachine.State.Error).contains(userVmVO.getState())) { + 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, + expiryAction.name(), true, + String.format("Executing action (%s) for VM: %s", instance.getLeaseExpiryAction(), instance), + instance.getId(), ApiCommandResourceType.VirtualMachine.toString(), 0); + + Long jobId = executeExpiryAction(instance, expiryAction, eventId); + if (jobId != null) { + submittedJobIds.add(jobId); + } 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..09062aa1c597 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,9 @@ 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.UsageEventUtils; import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InsufficientServerCapacityException; @@ -89,12 +40,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 +101,64 @@ 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.util.ArrayList; +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 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; @RunWith(MockitoJUnitRunner.class) public class UserVmManagerImplTest { @@ -612,6 +612,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 @@ -3125,4 +3127,76 @@ public void executeStepsToChangeOwnershipOfVmTestResourceCountRunningVmsOnlyEnab Mockito.verify(userVmManagerImpl, Mockito.never()).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); } } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateLeasePropertiesInvalidDuration() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(-2L, "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(20L, 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(-1L, null); + } + + @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(20L, "RUN"); + } + + @Test + public void testValidateLeasePropertiesValidValues() { + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + userVmManagerImpl.validateLeaseProperties(20L, "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(20L, 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"); + } } 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..dd778192386e --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/lease/VMLeaseManagerImplTest.java @@ -0,0 +1,287 @@ +/* + * 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.vm.VirtualMachine; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.api.command.user.vm.StopVMCmd; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +import org.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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static com.cloud.vm.VirtualMachine.State.Destroyed; +import static com.cloud.vm.VirtualMachine.State.Expunging; +import static com.cloud.vm.VirtualMachine.State.Unknown; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +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 DESTORY = "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 AlertManager alertManager; + + @Mock + private AsyncJobManager asyncJobManager; + + @Mock + private AsyncJobDispatcher asyncJobDispatcher; + + @Before + public void setUp() { + vmLeaseManager.setAsyncJobDispatcher(asyncJobDispatcher); + when(asyncJobDispatcher.getName()).thenReturn("AsyncJobDispatcher"); + when(asyncJobManager.submitAsyncJob(any(AsyncJobVO.class))).thenReturn(1L); + + ConfigKey instanceLeaseFeature = Mockito.mock(ConfigKey.class); + VMLeaseManagerImpl.InstanceLeaseEnabled = instanceLeaseFeature; + Mockito.when(instanceLeaseFeature.value()).thenReturn(Boolean.TRUE); + } + + @Test + public void testStart() { + assertTrue(vmLeaseManager.start()); + assertNotNull(vmLeaseManager.vmLeaseTimer); + assertNotNull(vmLeaseManager.vmLeaseAlterTimer); + } + + @Test + public void testAlert() { + UserVmJoinVO vm = createMockVm(1L, VM_UUID, VM_NAME, VirtualMachine.State.Running, false); + List expiringVms = Arrays.asList(vm); + when(userVmJoinDao.listLeaseInstancesExpiringInDays(anyInt())).thenReturn(expiringVms); + vmLeaseManager.alert(); + verify(alertManager).sendAlert( + eq(AlertManager.AlertType.ALERT_TYPE_USERVM), + anyLong(), + anyLong(), + anyString(), + anyString() + ); + } + + @Test + public void testReallyRunNoExpiredInstances() { + when(userVmJoinDao.listLeaseExpiredInstances()).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); + List expiredVms = Arrays.asList(vm); + when(userVmJoinDao.listLeaseExpiredInstances()).thenReturn(expiredVms); + vmLeaseManager.reallyRun(); + // Verify no jobs were submitted because of delete protection + verify(asyncJobManager, never()).submitAsyncJob(any(AsyncJobVO.class)); + } + + @Test + public void testReallyRunIgnoredStates() { + // Create VMs in states that should be ignored + UserVmJoinVO vmDestroyed = createMockVm(1L, "vm-uuid1", "vm-name1", Destroyed, false); + UserVmJoinVO vmExpunging = createMockVm(2L, "vm-uuid2", "vm-name2", Expunging, false); + UserVmJoinVO vmUnknown = createMockVm(3L, "vm-uuid3", "vm-name3", Unknown, false); + UserVmJoinVO vmError = createMockVm(4L, "vm-uuid4", "vm-name4", VirtualMachine.State.Error, false); + + List expiredVms = Arrays.asList(vmDestroyed, vmExpunging, vmUnknown, vmError); + when(userVmJoinDao.listLeaseExpiredInstances()).thenReturn(expiredVms); + vmLeaseManager.reallyRun(); + // Verify no jobs were submitted because all VMs are in ignored states + 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.listLeaseExpiredInstances()).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, DESTORY); + List expiredVms = Arrays.asList(vm); + when(userVmJoinDao.listLeaseExpiredInstances()).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, DESTORY); + 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, DESTORY); + 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.getState()).thenReturn(state); + when(vm.isDeleteProtection()).thenReturn(deleteProtection); + when(vm.getAccountId()).thenReturn(1L); + when(vm.getDataCenterId()).thenReturn(1L); + when(vm.getPodId()).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..7a9b0f66d5ac --- /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..f5ce587a1cd7 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/tools/marvin/setup.py b/tools/marvin/setup.py index 11a63a96aced..803829736892 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -27,7 +27,7 @@ raise RuntimeError("python setuptools is required to build Marvin") -VERSION = "4.21.0.0-SNAPSHOT" +VERSION = "4.21.0.0" setup(name="Marvin", version=VERSION, diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 1bfc22ecb766..97bbb404a262 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1158,6 +1158,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", @@ -2645,6 +2646,13 @@ "label.bucket.policy": "Bucket Policy", "label.usersecretkey": "Secret Key", "label.create.bucket": "Create Bucket", +"label.isLeaseFeatureEnabled": "Enable Lease", +"label.instance.lease": "Instance lease", +"label.instance.lease.placeholder": "Lease duration in days ( > 0)", +"label.leaseduration": "Lease duration", +"label.leaseexpirydate": "Lease expiry date", +"label.leaseexpiryaction": "Lease expiry action", +"label.remainingdays": "Lease", "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/InfoCard.vue b/ui/src/components/view/InfoCard.vue index e19456ecd5c9..52d26e82e3cc 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) + 'd' }} + diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 55aeb19317b3..bf2e19bab3ee 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -18,6 +18,7 @@ import { shallowRef, defineAsyncComponent } from 'vue' import store from '@/store' import { isZoneCreated } from '@/utils/zone' +import { useRoute } from 'vue-router' export default { name: 'compute', @@ -76,6 +77,12 @@ export default { fields.push('project') } fields.push('zonename') + const route = useRoute() + if (route !== undefined && route.query !== undefined && route.query.onlyleasedinstances !== undefined && route.query.onlyleasedinstances) { + fields.push('leaseduration') + fields.push('leaseexpirydate') + } + return fields }, searchFilters: ['name', 'zoneid', 'domainid', 'account', 'groupid', 'tags'], @@ -83,7 +90,7 @@ export default { var fields = ['name', 'displayname', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy', - 'hostcontrolstate', 'deleteprotection'] + 'hostcontrolstate', 'deleteprotection', 'leaseexpirydate', 'leaseexpiryaction'] const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true) if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) { return fields diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index f83daaea7638..d81daf1a38da 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -40,7 +40,7 @@ export default { filters: ['active', 'inactive'], columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'domain', 'zone', 'order'], details: () => { - var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources'] + var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'leaseexpiryaction'] if (store.getters.apis.createServiceOffering && store.getters.apis.createServiceOffering.params.filter(x => x.name === 'storagepolicy').length > 0) { fields.splice(6, 0, 'vspherestoragepolicy') diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 57dbe8eeabcf..062b313ff68d 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -603,6 +603,34 @@ @change="val => { dynamicscalingenabled = val }"/> + + + + + + + + + + + + + + + + + + + +
@@ -1101,7 +1129,14 @@ export default { description: 'ARM 64 bits (aarch64)' } ] - } + }, + isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled, + showLeaseOptions: false, + leaseduration: -1, + leaseexpiryaction: undefined, + expiryActions: ['STOP', 'DESTROY'], + defaultLeaseDuration: 90, + defaultLeaseExpiryAction: 'STOP' } }, computed: { @@ -2193,6 +2228,12 @@ export default { if (values.group) { deployVmData.group = values.group } + if (values.leaseduration) { + deployVmData.leaseduration = values.leaseduration + } + if (values.leaseexpiryaction) { + deployVmData.leaseexpiryaction = values.leaseexpiryaction + } // step 8: enter setup if ('properties' in values) { const keys = Object.keys(values.properties) @@ -2831,6 +2872,16 @@ export default { this.rootDiskSizeFixed = offering.rootdisksize this.showRootDiskSizeChanger = false } + + if (this.isLeaseFeatureEnabled) { + if (offering && offering.leaseduration > -1) { + this.showLeaseOptions = true + } else { + this.showLeaseOptions = false + } + this.onToggleLeaseData() + } + this.form.rootdisksizeitem = this.showRootDiskSizeChanger && this.rootDiskSizeFixed > 0 this.formModel = toRaw(this.form) }, @@ -2874,6 +2925,17 @@ export default { parent.$message.success(parent.$t('label.copied.clipboard')) } }) + }, + onToggleLeaseData () { + if (this.showLeaseOptions === false) { + this.leaseduration = -1 + this.leaseexpiryaction = undefined + } else { + this.leaseduration = this.serviceOffering.leaseduration ? this.serviceOffering.leaseduration : this.defaultLeaseDuration + this.leaseexpiryaction = this.serviceOffering.leaseexpiryaction ? this.serviceOffering.leaseexpiryaction : this.defaultLeaseExpiryAction + } + this.form.leaseduration = this.leaseduration + this.form.leaseexpiryaction = this.leaseexpiryaction } } } @@ -2926,7 +2988,7 @@ export default { .vm-info-card { .ant-card-body { min-height: 250px; - max-height: calc(100vh - 150px); + max-height: calc(100vh - 140px); overflow-y: auto; scroll-behavior: smooth; } diff --git a/ui/src/views/compute/EditVM.vue b/ui/src/views/compute/EditVM.vue index f2d679ee4445..668bdfd0c738 100644 --- a/ui/src/views/compute/EditVM.vue +++ b/ui/src/views/compute/EditVM.vue @@ -117,6 +117,34 @@ + + + + + + + + + + + + + + + + + + + +
{{ $t('label.cancel') }} @@ -165,7 +193,12 @@ export default { groups: { loading: false, opts: [] - } + }, + isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled, + showLeaseOptions: this.isLeaseFeatureEnabled === false ? false : this.resource.leaseduration > -1, + leaseduration: this.resource.leaseduration === undefined ? 90 : this.resource.leaseduration, + leaseexpiryaction: this.resource.leaseexpiryaction === undefined ? 'STOP' : this.resource.leaseexpiryaction, + expiryActions: ['STOP', 'DESTROY'] } }, beforeCreate () { @@ -187,7 +220,9 @@ 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({}) }, @@ -328,7 +363,6 @@ export default { }) }) }, - handleSubmit () { this.formRef.value.validate().then(() => { const values = toRaw(this.form) @@ -357,6 +391,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 +416,15 @@ 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 + } } } } diff --git a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue index 4450ce1144cd..41576f3a8d77 100644 --- a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue +++ b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue @@ -36,6 +36,12 @@ +
@@ -119,7 +125,8 @@ export default { key: 'name', dataIndex: 'name', title: this.$t('label.serviceofferingid'), - width: '40%' + width: '40%', + slots: { customRender: 'displayText' } }, { key: 'cpu', @@ -186,12 +193,14 @@ export default { if (this.allowAllOfferings) { disabled = false } + // var computedName = (item.leaseduration !== undefined) ? item.name + ': ' + item.leaseduration : item.name return { key: item.id, 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 } }) }, diff --git a/ui/src/views/dashboard/CapacityDashboard.vue b/ui/src/views/dashboard/CapacityDashboard.vue index 53a3d87aa23c..f73651aaba98 100644 --- a/ui/src/views/dashboard/CapacityDashboard.vue +++ b/ui/src/views/dashboard/CapacityDashboard.vue @@ -166,6 +166,18 @@ + + + + + + + @@ -382,7 +394,8 @@ export default { VIRTUAL_NETWORK_PUBLIC_IP: 'label.public.ips', VLAN: 'label.vlan', VIRTUAL_NETWORK_IPV6_SUBNET: 'label.ipv6.subnets' - } + }, + isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled } }, computed: { @@ -558,6 +571,15 @@ export default { this.data.instances = 0 } }) + if (this.isLeaseFeatureEnabled) { + api('listVirtualMachines', { zoneid: zone.id, onlyleasedinstances: true, listall: true, projectid: '-1', details: 'min', page: 1, pagesize: 1 }).then(json => { + this.loading = false + this.data.leasedinstances = json?.listvirtualmachinesresponse?.count + if (!this.data.leasedinstances) { + this.data.leasedinstances = 0 + } + }) + } }, listAlerts () { const params = { diff --git a/ui/src/views/dashboard/UsageDashboard.vue b/ui/src/views/dashboard/UsageDashboard.vue index 18b90bb837a7..bddb7990bf41 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..39200a601d4c 100644 --- a/ui/src/views/offering/AddComputeOffering.vue +++ b/ui/src/views/offering/AddComputeOffering.vue @@ -349,6 +349,34 @@ + + + + + + + + + + + + + + + + + + + +