From e065c93c3fc3a4d8aef5e2f750eec871469b19b0 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 30 May 2024 13:50:37 +0200 Subject: [PATCH 01/16] Apple FR76: Implicit host tags (#427) * Merge two HostTagVO and HostTagDaoImpl * Apple FR76: dynamic host tags * Revert "Apple FR76: dynamic host tags" This reverts commit 01b93a873f167018c4fafd0744c0de07ae4de4ed. * Apple FR76: Implicit host tags * Apple FR76: address Abhishek's comments * Apple FR76: move updateImplicitTags * Apple FR76: add since to other two responses * Update 8929: add unit test in LibvirtComputingResourceTest * Update variable names * Update FR76: add explicithosttags in response * Update FR76 UI: Update explicit host tags * Update 8929: remove host tags and change labels on UI * Update: ui polish for host tags * fix since in responses * Update 8929: fix UI error if no host tags --- agent/conf/agent.properties | 3 + .../agent/properties/AgentProperties.java | 7 + .../apache/cloudstack/api/ApiConstants.java | 1 + .../response/HostForMigrationResponse.java | 16 ++ .../cloudstack/api/response/HostResponse.java | 24 +++ .../api/response/HostTagResponse.java | 13 ++ .../agent/api/StartupRoutingCommand.java | 4 + .../main/java/com/cloud/host/HostTagVO.java | 11 ++ .../java/com/cloud/host/dao/HostTagsDao.java | 9 + .../com/cloud/host/dao/HostTagsDaoImpl.java | 124 ++++++++++++- ...spring-engine-schema-core-daos-context.xml | 1 - .../META-INF/db/schema-41800to41810.sql | 102 +++++++++++ .../resource/LibvirtComputingResource.java | 14 ++ .../LibvirtComputingResourceTest.java | 33 ++++ .../main/java/com/cloud/api/ApiDBUtils.java | 8 +- .../com/cloud/api/query/QueryManagerImpl.java | 12 +- .../cloud/api/query/ViewResponseHelper.java | 2 +- .../cloud/api/query/dao/HostJoinDaoImpl.java | 3 + .../com/cloud/api/query/dao/HostTagDao.java | 30 --- .../cloud/api/query/dao/HostTagDaoImpl.java | 124 ------------- .../com/cloud/api/query/vo/HostJoinVO.java | 14 ++ .../com/cloud/api/query/vo/HostTagVO.java | 61 ------ .../cloud/resource/ResourceManagerImpl.java | 17 +- test/integration/smoke/test_host_tags.py | 160 ++++++++++++++++ ui/public/locales/en.json | 7 + ui/src/config/section/infra/hosts.js | 8 +- ui/src/views/infra/HostInfo.vue | 26 ++- ui/src/views/infra/HostUpdate.vue | 173 ++++++++++++++++++ 28 files changed, 755 insertions(+), 252 deletions(-) delete mode 100644 server/src/main/java/com/cloud/api/query/dao/HostTagDao.java delete mode 100644 server/src/main/java/com/cloud/api/query/dao/HostTagDaoImpl.java delete mode 100644 server/src/main/java/com/cloud/api/query/vo/HostTagVO.java create mode 100644 test/integration/smoke/test_host_tags.py create mode 100644 ui/src/views/infra/HostUpdate.vue diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties index d5728b807b4..188007bfef2 100644 --- a/agent/conf/agent.properties +++ b/agent/conf/agent.properties @@ -415,3 +415,6 @@ iscsi.session.cleanup.enabled=false # If set to "true", the agent will register for libvirt domain events, allowing for immediate updates on crashed or # unexpectedly stopped. Experimental, requires agent restart. # libvirt.events.enabled=false + +# Implicit host tags managed by agent.properties +# host.tags= diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index 835ee65157a..9217aaa2078 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -755,6 +755,13 @@ public class AgentProperties{ */ public static final Property CONTROL_CIDR = new Property<>("control.cidr", "169.254.0.0/16"); + /** + * Implicit host tags + * Data type: String.
+ * Default value: null + */ + public static final Property HOST_TAGS = new Property<>("host.tags", null, String.class); + public static class Property { private String name; private T defaultValue; 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 626621798c2..e6db3fc8da0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -252,6 +252,7 @@ public class ApiConstants { public static final String IS_EDGE = "isedge"; public static final String IS_EXTRACTABLE = "isextractable"; public static final String IS_FEATURED = "isfeatured"; + public static final String IS_IMPLICIT = "isimplicit"; public static final String IS_PORTABLE = "isportable"; public static final String IS_PUBLIC = "ispublic"; public static final String IS_PERSISTENT = "ispersistent"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java index 41a0fdc4567..3fcc8b00f6b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java @@ -208,6 +208,14 @@ public class HostForMigrationResponse extends BaseResponse { @Param(description = "comma-separated list of tags for the host") private String hostTags; + @SerializedName("explicithosttags") + @Param(description = "comma-separated list of explicit host tags for the host", since = "4.18.1") + private String explicitHostTags; + + @SerializedName("implicithosttags") + @Param(description = "comma-separated list of implicit host tags for the host", since = "4.18.1") + private String implicitHostTags; + @SerializedName("hasenoughcapacity") @Param(description = "true if this host has enough CPU and RAM capacity to migrate a VM to it, false otherwise") private Boolean hasEnoughCapacity; @@ -414,6 +422,14 @@ public class HostForMigrationResponse extends BaseResponse { this.hostTags = hostTags; } + public void setExplicitHostTags(String explicitHostTags) { + this.explicitHostTags = explicitHostTags; + } + + public void setImplicitHostTags(String implicitHostTags) { + this.implicitHostTags = implicitHostTags; + } + public void setHasEnoughCapacity(Boolean hasEnoughCapacity) { this.hasEnoughCapacity = hasEnoughCapacity; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java index e1f1e5ee241..10610433e1e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java @@ -221,6 +221,14 @@ public class HostResponse extends BaseResponseWithAnnotations { @Param(description = "comma-separated list of tags for the host") private String hostTags; + @SerializedName("explicithosttags") + @Param(description = "comma-separated list of explicit host tags for the host", since = "4.18.1") + private String explicitHostTags; + + @SerializedName("implicithosttags") + @Param(description = "comma-separated list of implicit host tags for the host", since = "4.18.1") + private String implicitHostTags; + @SerializedName("hasenoughcapacity") @Param(description = "true if this host has enough CPU and RAM capacity to migrate a VM to it, false otherwise") private Boolean hasEnoughCapacity; @@ -454,6 +462,22 @@ public class HostResponse extends BaseResponseWithAnnotations { this.hostTags = hostTags; } + public String getExplicitHostTags() { + return explicitHostTags; + } + + public void setExplicitHostTags(String explicitHostTags) { + this.explicitHostTags = explicitHostTags; + } + + public String getImplicitHostTags() { + return implicitHostTags; + } + + public void setImplicitHostTags(String implicitHostTags) { + this.implicitHostTags = implicitHostTags; + } + public void setHasEnoughCapacity(Boolean hasEnoughCapacity) { this.hasEnoughCapacity = hasEnoughCapacity; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostTagResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostTagResponse.java index 4a924ea78a0..9ade2f4ce48 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostTagResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostTagResponse.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.api.response; import com.google.gson.annotations.SerializedName; import com.cloud.serializer.Param; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; public class HostTagResponse extends BaseResponse { @@ -34,6 +35,10 @@ public class HostTagResponse extends BaseResponse { @Param(description = "the name of the host tag") private String name; + @SerializedName(ApiConstants.IS_IMPLICIT) + @Param(description = "true if the host tag is implicit", since = "4.18.1") + private boolean isImplicit; + public String getId() { return id; } @@ -57,4 +62,12 @@ public class HostTagResponse extends BaseResponse { public void setName(String name) { this.name = name; } + + public boolean isImplicit() { + return isImplicit; + } + + public void setImplicit(boolean implicit) { + isImplicit = implicit; + } } diff --git a/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java b/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java index b4f9d20df5e..2d4ed8c9cc4 100644 --- a/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java +++ b/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java @@ -174,6 +174,10 @@ public class StartupRoutingCommand extends StartupCommand { this.hostTags.add(hostTag); } + public void setHostTags(List hostTags) { + this.hostTags = hostTags; + } + public HashMap> getGpuGroupDetails() { return groupDetails; } diff --git a/engine/schema/src/main/java/com/cloud/host/HostTagVO.java b/engine/schema/src/main/java/com/cloud/host/HostTagVO.java index 6b8b754849c..ad835a18867 100644 --- a/engine/schema/src/main/java/com/cloud/host/HostTagVO.java +++ b/engine/schema/src/main/java/com/cloud/host/HostTagVO.java @@ -39,6 +39,9 @@ public class HostTagVO implements InternalIdentity { @Column(name = "tag") private String tag; + @Column(name = "is_implicit") + private boolean isImplicit = false; + protected HostTagVO() { } @@ -59,6 +62,14 @@ public class HostTagVO implements InternalIdentity { this.tag = tag; } + public void setIsImplicit(boolean isImplicit) { + this.isImplicit = isImplicit; + } + + public boolean getIsImplicit() { + return isImplicit; + } + @Override public long getId() { return id; diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java index 0fb5370d81a..145f5c839af 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java @@ -21,6 +21,8 @@ import java.util.List; import com.cloud.host.HostTagVO; import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.response.HostTagResponse; + public interface HostTagsDao extends GenericDao { void persist(long hostId, List hostTags); @@ -31,4 +33,11 @@ public interface HostTagsDao extends GenericDao { void deleteTags(long hostId); + boolean updateImplicitTags(long hostId, List hostTags); + + List getExplicitHostTags(long hostId); + + HostTagResponse newHostTagResponse(HostTagVO hostTag); + + List searchByIds(Long... hostTagIds); } diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java index a73899b7b33..b2fc0882a37 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java @@ -19,7 +19,9 @@ package com.cloud.host.dao; import java.util.ArrayList; import java.util.List; - +import org.apache.cloudstack.api.response.HostTagResponse; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import com.cloud.host.HostTagVO; @@ -30,14 +32,23 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.SearchCriteria.Func; +import javax.inject.Inject; + @Component public class HostTagsDaoImpl extends GenericDaoBase implements HostTagsDao { protected final SearchBuilder HostSearch; protected final GenericSearchBuilder DistinctImplictTagsSearch; + private final SearchBuilder stSearch; + private final SearchBuilder tagIdsearch; + private final SearchBuilder ImplicitTagsSearch; + + @Inject + private ConfigurationDao _configDao; public HostTagsDaoImpl() { HostSearch = createSearchBuilder(); HostSearch.and("hostId", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.and("isImplicit", HostSearch.entity().getIsImplicit(), SearchCriteria.Op.EQ); HostSearch.done(); DistinctImplictTagsSearch = createSearchBuilder(String.class); @@ -45,6 +56,19 @@ public class HostTagsDaoImpl extends GenericDaoBase implements DistinctImplictTagsSearch.and("hostIds", DistinctImplictTagsSearch.entity().getHostId(), SearchCriteria.Op.IN); DistinctImplictTagsSearch.and("implicitTags", DistinctImplictTagsSearch.entity().getTag(), SearchCriteria.Op.IN); DistinctImplictTagsSearch.done(); + + stSearch = createSearchBuilder(); + stSearch.and("idIN", stSearch.entity().getId(), SearchCriteria.Op.IN); + stSearch.done(); + + tagIdsearch = createSearchBuilder(); + tagIdsearch.and("id", tagIdsearch.entity().getId(), SearchCriteria.Op.EQ); + tagIdsearch.done(); + + ImplicitTagsSearch = createSearchBuilder(); + ImplicitTagsSearch.and("hostId", ImplicitTagsSearch.entity().getHostId(), SearchCriteria.Op.EQ); + ImplicitTagsSearch.and("isImplicit", ImplicitTagsSearch.entity().getIsImplicit(), SearchCriteria.Op.EQ); + ImplicitTagsSearch.done(); } @Override @@ -79,6 +103,36 @@ public class HostTagsDaoImpl extends GenericDaoBase implements txn.commit(); } + @Override + public boolean updateImplicitTags(long hostId, List hostTags) { + TransactionLegacy txn = TransactionLegacy.currentTxn(); + txn.start(); + SearchCriteria sc = ImplicitTagsSearch.create(); + sc.setParameters("hostId", hostId); + sc.setParameters("isImplicit", true); + boolean expunged = expunge(sc) > 0; + boolean persisted = false; + for (String tag : hostTags) { + if (StringUtils.isNotBlank(tag)) { + HostTagVO vo = new HostTagVO(hostId, tag.trim()); + vo.setIsImplicit(true); + persist(vo); + persisted = true; + } + } + txn.commit(); + return expunged || persisted; + } + + @Override + public List getExplicitHostTags(long hostId) { + SearchCriteria sc = ImplicitTagsSearch.create(); + sc.setParameters("hostId", hostId); + sc.setParameters("isImplicit", false); + + return search(sc, null); + } + @Override public void persist(long hostId, List hostTags) { TransactionLegacy txn = TransactionLegacy.currentTxn(); @@ -86,6 +140,7 @@ public class HostTagsDaoImpl extends GenericDaoBase implements txn.start(); SearchCriteria sc = HostSearch.create(); sc.setParameters("hostId", hostId); + sc.setParameters("isImplicit", false); expunge(sc); for (String tag : hostTags) { @@ -97,4 +152,71 @@ public class HostTagsDaoImpl extends GenericDaoBase implements } txn.commit(); } + + @Override + public HostTagResponse newHostTagResponse(HostTagVO tag) { + HostTagResponse tagResponse = new HostTagResponse(); + + tagResponse.setName(tag.getTag()); + tagResponse.setHostId(tag.getHostId()); + + tagResponse.setObjectName("hosttag"); + + return tagResponse; + } + + @Override + public List searchByIds(Long... tagIds) { + String batchCfg = _configDao.getValue("detail.batch.query.size"); + + final int detailsBatchSize = batchCfg != null ? Integer.parseInt(batchCfg) : 2000; + + // query details by batches + List tagList = new ArrayList<>(); + int curr_index = 0; + + if (tagIds.length > detailsBatchSize) { + while ((curr_index + detailsBatchSize) <= tagIds.length) { + Long[] ids = new Long[detailsBatchSize]; + + for (int k = 0, j = curr_index; j < curr_index + detailsBatchSize; j++, k++) { + ids[k] = tagIds[j]; + } + + SearchCriteria sc = stSearch.create(); + + sc.setParameters("idIN", (Object[])ids); + + List vms = searchIncludingRemoved(sc, null, null, false); + + if (vms != null) { + tagList.addAll(vms); + } + + curr_index += detailsBatchSize; + } + } + + if (curr_index < tagIds.length) { + int batch_size = (tagIds.length - curr_index); + // set the ids value + Long[] ids = new Long[batch_size]; + + for (int k = 0, j = curr_index; j < curr_index + batch_size; j++, k++) { + ids[k] = tagIds[j]; + } + + SearchCriteria sc = stSearch.create(); + + sc.setParameters("idIN", (Object[])ids); + + List tags = searchIncludingRemoved(sc, null, null, false); + + if (tags != null) { + tagList.addAll(tags); + } + } + + return tagList; + } } diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 51e557f0c52..22cf8f71130 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -186,7 +186,6 @@ - diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql b/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql index 7c5e2ac1c8e..4cda23b79e0 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql @@ -431,3 +431,105 @@ ALTER TABLE `cloud`.`resource_reservation` ALTER TABLE `cloud`.`resource_reservation` MODIFY COLUMN `amount` bigint NOT NULL; + +-- Add `is_implicit` column to `host_tags` table +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host_tags', 'is_implicit', 'int(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT "If host tag is implicit or explicit" '); + +-- Update host_view for implicit host tags +DROP VIEW IF EXISTS `cloud`.`host_view`; + +CREATE VIEW `cloud`.`host_view` AS + SELECT + host.id, + host.uuid, + host.name, + host.status, + host.disconnected, + host.type, + host.private_ip_address, + host.version, + host.hypervisor_type, + host.hypervisor_version, + host.capabilities, + host.last_ping, + host.created, + host.removed, + host.resource_state, + host.mgmt_server_id, + host.cpu_sockets, + host.cpus, + host.speed, + host.ram, + cluster.id cluster_id, + cluster.uuid cluster_uuid, + cluster.name cluster_name, + cluster.cluster_type, + data_center.id data_center_id, + data_center.uuid data_center_uuid, + data_center.name data_center_name, + data_center.networktype data_center_type, + host_pod_ref.id pod_id, + host_pod_ref.uuid pod_uuid, + host_pod_ref.name pod_name, + GROUP_CONCAT(DISTINCT(host_tags.tag)) AS tag, + GROUP_CONCAT(DISTINCT(explicit_host_tags.tag)) AS explicit_tag, + GROUP_CONCAT(DISTINCT(implicit_host_tags.tag)) AS implicit_tag, + guest_os_category.id guest_os_category_id, + guest_os_category.uuid guest_os_category_uuid, + guest_os_category.name guest_os_category_name, + mem_caps.used_capacity memory_used_capacity, + mem_caps.reserved_capacity memory_reserved_capacity, + cpu_caps.used_capacity cpu_used_capacity, + cpu_caps.reserved_capacity cpu_reserved_capacity, + async_job.id job_id, + async_job.uuid job_uuid, + async_job.job_status job_status, + async_job.account_id job_account_id, + oobm.enabled AS `oobm_enabled`, + oobm.power_state AS `oobm_power_state`, + ha_config.enabled AS `ha_enabled`, + ha_config.ha_state AS `ha_state`, + ha_config.provider AS `ha_provider`, + `last_annotation_view`.`annotation` AS `annotation`, + `last_annotation_view`.`created` AS `last_annotated`, + `user`.`username` AS `username` + FROM + `cloud`.`host` + LEFT JOIN + `cloud`.`cluster` ON host.cluster_id = cluster.id + LEFT JOIN + `cloud`.`data_center` ON host.data_center_id = data_center.id + LEFT JOIN + `cloud`.`host_pod_ref` ON host.pod_id = host_pod_ref.id + LEFT JOIN + `cloud`.`host_details` ON host.id = host_details.host_id + AND host_details.name = 'guest.os.category.id' + LEFT JOIN + `cloud`.`guest_os_category` ON guest_os_category.id = CONVERT ( host_details.value, UNSIGNED ) + LEFT JOIN + `cloud`.`host_tags` ON host_tags.host_id = host.id + LEFT JOIN + `cloud`.`host_tags` AS explicit_host_tags ON explicit_host_tags.host_id = host.id AND explicit_host_tags.is_implicit = 0 + LEFT JOIN + `cloud`.`host_tags` AS implicit_host_tags ON implicit_host_tags.host_id = host.id AND implicit_host_tags.is_implicit = 1 + LEFT JOIN + `cloud`.`op_host_capacity` mem_caps ON host.id = mem_caps.host_id + AND mem_caps.capacity_type = 0 + LEFT JOIN + `cloud`.`op_host_capacity` cpu_caps ON host.id = cpu_caps.host_id + AND cpu_caps.capacity_type = 1 + LEFT JOIN + `cloud`.`async_job` ON async_job.instance_id = host.id + AND async_job.instance_type = 'Host' + AND async_job.job_status = 0 + LEFT JOIN + `cloud`.`oobm` ON oobm.host_id = host.id + left join + `cloud`.`ha_config` ON ha_config.resource_id=host.id + and ha_config.resource_type='Host' + LEFT JOIN + `cloud`.`last_annotation_view` ON `last_annotation_view`.`entity_uuid` = `host`.`uuid` + LEFT JOIN + `cloud`.`user` ON `user`.`uuid` = `last_annotation_view`.`user_uuid` + GROUP BY + `host`.`id`; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 4ce11287029..fadc51b8885 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -3586,6 +3586,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv cmd.setGatewayIpAddress(_localGateway); cmd.setIqn(getIqn()); cmd.getHostDetails().put(HOST_VOLUME_ENCRYPTION, String.valueOf(hostSupportsVolumeEncryption())); + cmd.setHostTags(getHostTags()); HealthCheckResult healthCheckResult = getHostHealthCheckResult(); if (healthCheckResult != HealthCheckResult.IGNORE) { cmd.setHostHealthCheckResult(healthCheckResult == HealthCheckResult.SUCCESS); @@ -3614,6 +3615,19 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv return startupCommandsArray; } + protected List getHostTags() { + List hostTagsList = new ArrayList<>(); + String hostTags = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_TAGS); + if (StringUtils.isNotBlank(hostTags)) { + for (String hostTag : hostTags.split(",")) { + if (!hostTagsList.contains(hostTag.trim())) { + hostTagsList.add(hostTag.trim()); + } + } + } + return hostTagsList; + } + private StartupStorageCommand createLocalStoragePool(String localStoragePath, String localStorageUUID, StartupRoutingCommand cmd) { StartupStorageCommand sscmd = null; try { diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index cc658ff8455..5b011f92c01 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -6270,4 +6270,37 @@ public class LibvirtComputingResourceTest { Mockito.verify(loggerMock).debug("Skipping the memory balloon stats period setting for the VM (Libvirt Domain) with ID [1] and name [fake-VM-name] because this" + " VM has no memory balloon."); } + + @Test + public void testGetHostTags() throws ConfigurationException { + PowerMockito.mockStatic(AgentPropertiesFileHandler.class); + PowerMockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS))) + .thenReturn("aa,bb,cc,dd"); + + List hostTagsList = libvirtComputingResourceSpy.getHostTags(); + Assert.assertEquals(4, hostTagsList.size()); + Assert.assertEquals("aa,bb,cc,dd", StringUtils.join(hostTagsList, ",")); + } + + @Test + public void testGetHostTagsWithSpace() throws ConfigurationException { + PowerMockito.mockStatic(AgentPropertiesFileHandler.class); + PowerMockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS))) + .thenReturn(" aa, bb , cc , dd "); + + List hostTagsList = libvirtComputingResourceSpy.getHostTags(); + Assert.assertEquals(4, hostTagsList.size()); + Assert.assertEquals("aa,bb,cc,dd", StringUtils.join(hostTagsList, ",")); + } + + @Test + public void testGetHostTagsWithEmptyPropertyValue() throws ConfigurationException { + PowerMockito.mockStatic(AgentPropertiesFileHandler.class); + PowerMockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS))) + .thenReturn(" "); + + List hostTagsList = libvirtComputingResourceSpy.getHostTags(); + Assert.assertEquals(0, hostTagsList.size()); + Assert.assertEquals("", StringUtils.join(hostTagsList, ",")); + } } diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 4f64043c35e..fa36a86330b 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -25,7 +25,6 @@ import com.cloud.api.query.dao.DiskOfferingJoinDao; import com.cloud.api.query.dao.DomainJoinDao; import com.cloud.api.query.dao.DomainRouterJoinDao; import com.cloud.api.query.dao.HostJoinDao; -import com.cloud.api.query.dao.HostTagDao; import com.cloud.api.query.dao.ImageStoreJoinDao; import com.cloud.api.query.dao.InstanceGroupJoinDao; import com.cloud.api.query.dao.NetworkOfferingJoinDao; @@ -50,7 +49,6 @@ import com.cloud.api.query.vo.DomainJoinVO; import com.cloud.api.query.vo.DomainRouterJoinVO; import com.cloud.api.query.vo.EventJoinVO; import com.cloud.api.query.vo.HostJoinVO; -import com.cloud.api.query.vo.HostTagVO; import com.cloud.api.query.vo.ImageStoreJoinVO; import com.cloud.api.query.vo.InstanceGroupJoinVO; import com.cloud.api.query.vo.NetworkOfferingJoinVO; @@ -103,9 +101,11 @@ import com.cloud.gpu.dao.VGPUTypesDao; import com.cloud.ha.HighAvailabilityManager; import com.cloud.host.Host; import com.cloud.host.HostStats; +import com.cloud.host.HostTagVO; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDetailsDao; +import com.cloud.host.dao.HostTagsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.IpAddress; import com.cloud.network.Network; @@ -440,7 +440,7 @@ public class ApiDBUtils { static VolumeJoinDao s_volJoinDao; static StoragePoolJoinDao s_poolJoinDao; static StoragePoolTagsDao s_tagDao; - static HostTagDao s_hostTagDao; + static HostTagsDao s_hostTagDao; static ImageStoreJoinDao s_imageStoreJoinDao; static AccountJoinDao s_accountJoinDao; static AsyncJobJoinDao s_jobJoinDao; @@ -658,7 +658,7 @@ public class ApiDBUtils { @Inject private StoragePoolTagsDao tagDao; @Inject - private HostTagDao hosttagDao; + private HostTagsDao hosttagDao; @Inject private ImageStoreJoinDao imageStoreJoinDao; @Inject 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 e4bd7f99de5..4ba19cb77f6 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -38,8 +38,10 @@ import javax.inject.Inject; import com.cloud.event.EventVO; import com.cloud.event.dao.EventDao; import com.cloud.host.Host; +import com.cloud.host.HostTagVO; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostTagsDao; import com.cloud.network.as.AutoScaleVmGroupVmMapVO; import com.cloud.network.as.dao.AutoScaleVmGroupDao; import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao; @@ -173,7 +175,6 @@ import com.cloud.api.query.dao.DiskOfferingJoinDao; import com.cloud.api.query.dao.DomainJoinDao; import com.cloud.api.query.dao.DomainRouterJoinDao; import com.cloud.api.query.dao.HostJoinDao; -import com.cloud.api.query.dao.HostTagDao; import com.cloud.api.query.dao.ImageStoreJoinDao; import com.cloud.api.query.dao.InstanceGroupJoinDao; import com.cloud.api.query.dao.ManagementServerJoinDao; @@ -197,7 +198,6 @@ import com.cloud.api.query.vo.DomainJoinVO; import com.cloud.api.query.vo.DomainRouterJoinVO; import com.cloud.api.query.vo.EventJoinVO; import com.cloud.api.query.vo.HostJoinVO; -import com.cloud.api.query.vo.HostTagVO; import com.cloud.api.query.vo.ImageStoreJoinVO; import com.cloud.api.query.vo.InstanceGroupJoinVO; import com.cloud.api.query.vo.ManagementServerJoinVO; @@ -391,7 +391,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private StoragePoolTagsDao _storageTagDao; @Inject - private HostTagDao _hostTagDao; + private HostTagsDao _hostTagDao; @Inject private ImageStoreJoinDao _imageStoreJoinDao; @@ -2173,10 +2173,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (haHosts != null && haTag != null && !haTag.isEmpty()) { SearchBuilder hostTagSearchBuilder = _hostTagDao.createSearchBuilder(); if ((Boolean)haHosts) { - hostTagSearchBuilder.and("tag", hostTagSearchBuilder.entity().getName(), SearchCriteria.Op.EQ); + hostTagSearchBuilder.and("tag", hostTagSearchBuilder.entity().getTag(), SearchCriteria.Op.EQ); } else { - hostTagSearchBuilder.and().op("tag", hostTagSearchBuilder.entity().getName(), Op.NEQ); - hostTagSearchBuilder.or("tagNull", hostTagSearchBuilder.entity().getName(), Op.NULL); + hostTagSearchBuilder.and().op("tag", hostTagSearchBuilder.entity().getTag(), Op.NEQ); + hostTagSearchBuilder.or("tagNull", hostTagSearchBuilder.entity().getTag(), Op.NULL); hostTagSearchBuilder.cp(); } hostSearchBuilder.join("hostTagSearch", hostTagSearchBuilder, hostSearchBuilder.entity().getId(), hostTagSearchBuilder.entity().getHostId(), JoinBuilder.JoinType.LEFT); diff --git a/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java b/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java index e30d21ab8ec..0f1daa3e7cb 100644 --- a/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java +++ b/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java @@ -69,7 +69,6 @@ import com.cloud.api.query.vo.DomainJoinVO; import com.cloud.api.query.vo.DomainRouterJoinVO; import com.cloud.api.query.vo.EventJoinVO; import com.cloud.api.query.vo.HostJoinVO; -import com.cloud.api.query.vo.HostTagVO; import com.cloud.api.query.vo.ImageStoreJoinVO; import com.cloud.api.query.vo.InstanceGroupJoinVO; import com.cloud.api.query.vo.ProjectAccountJoinVO; @@ -85,6 +84,7 @@ import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.api.query.vo.VolumeJoinVO; import com.cloud.configuration.Resource; import com.cloud.domain.Domain; +import com.cloud.host.HostTagVO; import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.StoragePoolTagVO; import com.cloud.storage.VolumeStats; diff --git a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java index bf3ded1fef0..a857c16fb3a 100644 --- a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java @@ -206,6 +206,8 @@ public class HostJoinDaoImpl extends GenericDaoBase implements String hostTags = host.getTag(); hostResponse.setHostTags(hostTags); hostResponse.setHaHost(containsHostHATag(hostTags)); + hostResponse.setExplicitHostTags(host.getExplicitTag()); + hostResponse.setImplicitHostTags(host.getImplicitTag()); hostResponse.setHypervisorVersion(host.getHypervisorVersion()); @@ -350,6 +352,7 @@ public class HostJoinDaoImpl extends GenericDaoBase implements String hostTags = host.getTag(); hostResponse.setHostTags(hostTags); hostResponse.setHaHost(containsHostHATag(hostTags)); + hostResponse.setImplicitHostTags(host.getImplicitTag()); hostResponse.setHypervisorVersion(host.getHypervisorVersion()); diff --git a/server/src/main/java/com/cloud/api/query/dao/HostTagDao.java b/server/src/main/java/com/cloud/api/query/dao/HostTagDao.java deleted file mode 100644 index ab43e71221c..00000000000 --- a/server/src/main/java/com/cloud/api/query/dao/HostTagDao.java +++ /dev/null @@ -1,30 +0,0 @@ -// 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 com.cloud.api.query.dao; - -import java.util.List; - -import org.apache.cloudstack.api.response.HostTagResponse; - -import com.cloud.api.query.vo.HostTagVO; -import com.cloud.utils.db.GenericDao; - -public interface HostTagDao extends GenericDao { - HostTagResponse newHostTagResponse(HostTagVO hostTag); - - List searchByIds(Long... hostTagIds); -} diff --git a/server/src/main/java/com/cloud/api/query/dao/HostTagDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/HostTagDaoImpl.java deleted file mode 100644 index 5395fd4a33b..00000000000 --- a/server/src/main/java/com/cloud/api/query/dao/HostTagDaoImpl.java +++ /dev/null @@ -1,124 +0,0 @@ -// 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 com.cloud.api.query.dao; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -import org.apache.cloudstack.api.response.HostTagResponse; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.log4j.Logger; -import org.springframework.stereotype.Component; - -import com.cloud.api.query.vo.HostTagVO; -import com.cloud.utils.db.GenericDaoBase; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; - -@Component -public class HostTagDaoImpl extends GenericDaoBase implements HostTagDao { - public static final Logger s_logger = Logger.getLogger(HostTagDaoImpl.class); - - @Inject - private ConfigurationDao _configDao; - - private final SearchBuilder stSearch; - private final SearchBuilder stIdSearch; - - protected HostTagDaoImpl() { - stSearch = createSearchBuilder(); - - stSearch.and("idIN", stSearch.entity().getId(), SearchCriteria.Op.IN); - stSearch.done(); - - stIdSearch = createSearchBuilder(); - - stIdSearch.and("id", stIdSearch.entity().getId(), SearchCriteria.Op.EQ); - stIdSearch.done(); - - _count = "select count(distinct id) from host_tags WHERE "; - } - - @Override - public HostTagResponse newHostTagResponse(HostTagVO tag) { - HostTagResponse tagResponse = new HostTagResponse(); - - tagResponse.setName(tag.getName()); - tagResponse.setHostId(tag.getHostId()); - - tagResponse.setObjectName("hosttag"); - - return tagResponse; - } - - @Override - public List searchByIds(Long... stIds) { - String batchCfg = _configDao.getValue("detail.batch.query.size"); - - final int detailsBatchSize = batchCfg != null ? Integer.parseInt(batchCfg) : 2000; - - // query details by batches - List uvList = new ArrayList(); - int curr_index = 0; - - if (stIds.length > detailsBatchSize) { - while ((curr_index + detailsBatchSize) <= stIds.length) { - Long[] ids = new Long[detailsBatchSize]; - - for (int k = 0, j = curr_index; j < curr_index + detailsBatchSize; j++, k++) { - ids[k] = stIds[j]; - } - - SearchCriteria sc = stSearch.create(); - - sc.setParameters("idIN", (Object[])ids); - - List vms = searchIncludingRemoved(sc, null, null, false); - - if (vms != null) { - uvList.addAll(vms); - } - - curr_index += detailsBatchSize; - } - } - - if (curr_index < stIds.length) { - int batch_size = (stIds.length - curr_index); - // set the ids value - Long[] ids = new Long[batch_size]; - - for (int k = 0, j = curr_index; j < curr_index + batch_size; j++, k++) { - ids[k] = stIds[j]; - } - - SearchCriteria sc = stSearch.create(); - - sc.setParameters("idIN", (Object[])ids); - - List vms = searchIncludingRemoved(sc, null, null, false); - - if (vms != null) { - uvList.addAll(vms); - } - } - - return uvList; - } -} diff --git a/server/src/main/java/com/cloud/api/query/vo/HostJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/HostJoinVO.java index f5ff7c3a41e..703650202a6 100644 --- a/server/src/main/java/com/cloud/api/query/vo/HostJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/HostJoinVO.java @@ -174,6 +174,12 @@ public class HostJoinVO extends BaseViewVO implements InternalIdentity, Identity @Column(name = "tag") private String tag; + @Column(name = "explicit_tag") + private String explicitTag; + + @Column(name = "implicit_tag") + private String implicitTag; + @Column(name = "memory_used_capacity") private long memUsedCapacity; @@ -390,6 +396,14 @@ public class HostJoinVO extends BaseViewVO implements InternalIdentity, Identity return tag; } + public String getExplicitTag() { + return explicitTag; + } + + public String getImplicitTag() { + return implicitTag; + } + public String getAnnotation() { return annotation; } diff --git a/server/src/main/java/com/cloud/api/query/vo/HostTagVO.java b/server/src/main/java/com/cloud/api/query/vo/HostTagVO.java deleted file mode 100644 index 0a279e5c490..00000000000 --- a/server/src/main/java/com/cloud/api/query/vo/HostTagVO.java +++ /dev/null @@ -1,61 +0,0 @@ -// 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 com.cloud.api.query.vo; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; - -import org.apache.cloudstack.api.InternalIdentity; - -/** - * Storage Tags DB view. - * - */ -@Entity -@Table(name = "host_tags") -public class HostTagVO extends BaseViewVO implements InternalIdentity { - private static final long serialVersionUID = 1L; - - @Id - @Column(name = "id") - private long id; - - @Column(name = "tag") - private String name; - - @Column(name = "host_id") - long hostId; - - @Override - public long getId() { - return id; - } - - public String getName() { - return name; - } - - public long getHostId() { - return hostId; - } - - public void setHostId(long hostId) { - this.hostId = hostId; - } -} diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 69b80d10c01..d7f6288d48c 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -2334,22 +2334,6 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, } } - if (startup instanceof StartupRoutingCommand) { - final StartupRoutingCommand ssCmd = (StartupRoutingCommand)startup; - final List implicitHostTags = ssCmd.getHostTags(); - if (!implicitHostTags.isEmpty()) { - if (hostTags == null) { - hostTags = _hostTagsDao.getHostTags(host.getId()); - } - if (hostTags != null) { - implicitHostTags.removeAll(hostTags); - hostTags.addAll(implicitHostTags); - } else { - hostTags = implicitHostTags; - } - } - } - host.setDataCenterId(dc.getId()); host.setPodId(podId); host.setClusterId(clusterId); @@ -2392,6 +2376,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, if (startup instanceof StartupRoutingCommand) { final StartupRoutingCommand ssCmd = (StartupRoutingCommand)startup; + _hostTagsDao.updateImplicitTags(host.getId(), ssCmd.getHostTags()); updateSupportsClonedVolumes(host, ssCmd.getSupportsClonedVolumes()); } diff --git a/test/integration/smoke/test_host_tags.py b/test/integration/smoke/test_host_tags.py new file mode 100644 index 00000000000..b6bfe79148d --- /dev/null +++ b/test/integration/smoke/test_host_tags.py @@ -0,0 +1,160 @@ +# 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. +""" Tests for importVolume and unmanageVolume APIs +""" +# Import Local Modules +from marvin.cloudstackAPI import updateHost +from marvin.cloudstackTestCase import cloudstackTestCase, unittest +from marvin.lib.base import Host +from marvin.lib.utils import is_server_ssh_ready, wait_until + +# Import System modules +from nose.plugins.attrib import attr + +import logging + +class TestHostTags(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + testClient = super(TestHostTags, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.hypervisor = testClient.getHypervisorInfo() + if cls.testClient.getHypervisorInfo().lower() != "kvm": + raise unittest.SkipTest("This is only available for KVM") + + cls.hostConfig = cls.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0].__dict__["hosts"][0].__dict__ + + hosts = Host.list( + cls.apiclient, + type = "Routing", + hypervisor = cls.hypervisor + ) + if isinstance(hosts, list) and len(hosts) > 0: + cls.host = hosts[0] + else: + raise unittest.SkipTest("No available host for this test") + + cls.logger = logging.getLogger("TestHostTags") + cls.stream_handler = logging.StreamHandler() + cls.logger.setLevel(logging.DEBUG) + cls.logger.addHandler(cls.stream_handler) + + @classmethod + def tearDownClass(cls): + cls.update_host_tags_via_api(cls.host.hosttags) + cls.update_implicit_host_tags_via_agent_properties(cls.host.implicithosttags) + + @classmethod + def update_host_tags_via_api(cls, hosttags): + cmd = updateHost.updateHostCmd() + cmd.id = cls.host.id + cmd.hosttags = hosttags + cls.apiclient.updateHost(cmd) + + @classmethod + def update_implicit_host_tags_via_agent_properties(cls, implicithosttags): + ssh_client = is_server_ssh_ready( + cls.host.ipaddress, + 22, + cls.hostConfig["username"], + cls.hostConfig["password"], + ) + if implicithosttags: + command = "sed -i '/host.tags=/d' /etc/cloudstack/agent/agent.properties \ + && echo 'host.tags=%s' >> /etc/cloudstack/agent/agent.properties \ + && systemctl restart cloudstack-agent" % implicithosttags + else: + command = "sed -i '/host.tags=/d' /etc/cloudstack/agent/agent.properties \ + && systemctl restart cloudstack-agent" + + ssh_client.execute(command) + + def wait_until_host_is_up_and_verify_hosttags(self, explicithosttags, implicithosttags, interval=3, retries=20): + def check_host_state(): + hosts = Host.list( + self.apiclient, + id=self.host.id + ) + if isinstance(hosts, list) and len(hosts) > 0: + host = hosts[0] + if host.state == "Up": + self.logger.debug("Host %s is in Up state" % host.name) + self.logger.debug("Host explicithosttags is %s, implicit hosttags is %s" % (host.explicithosttags, host.implicithosttags)) + if explicithosttags: + self.assertEquals(explicithosttags, host.explicithosttags) + else: + self.assertIsNone(host.explicithosttags) + if implicithosttags: + self.assertEquals(implicithosttags, host.implicithosttags) + else: + self.assertIsNone(host.implicithosttags) + return True, None + else: + self.logger.debug("Waiting for host %s to be Up state, current state is %s" % (host.name, host.state)) + return False, None + + done, _ = wait_until(interval, retries, check_host_state) + if not done: + raise Exception("Failed to wait for host %s to be Up" % self.host.name) + return True + + @attr(tags=['advanced', 'basic', 'sg'], required_hardware=False) + def test_01_host_tags(self): + """Test implicit/explicit host tags + """ + + # update explicit host tags to "s1,s2" + explicithosttags="s1,s2" + implicithosttags=self.host.implicithosttags + self.update_host_tags_via_api(explicithosttags) + self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, implicithosttags) + + # update implicit host tags to "d1,d2" + implicithosttags="d1,d2" + self.update_implicit_host_tags_via_agent_properties(implicithosttags) + self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, implicithosttags) + + # update explicit host tags to "s3,s4" + explicithosttags="s3,s4" + self.update_host_tags_via_api(explicithosttags) + self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, implicithosttags) + + # update implicit host tags to "d3,d4" + implicithosttags="d3,d4" + self.update_implicit_host_tags_via_agent_properties(implicithosttags) + self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, implicithosttags) + + # update hosttags to "" + explicithosttags="" + self.update_host_tags_via_api(explicithosttags) + self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, implicithosttags) + + # update implicit host tags to "" + implicithosttags="" + self.update_implicit_host_tags_via_agent_properties(implicithosttags) + self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, implicithosttags) + + # update explicit host tags to "s1,s2" + explicithosttags="s1,s2" + self.update_host_tags_via_api(explicithosttags) + self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, implicithosttags) + + # update implicit host tags to "d1,d2" + implicithosttags="d1,d2" + self.update_implicit_host_tags_via_agent_properties(implicithosttags) + self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, implicithosttags) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index a5cc98a0565..90d8619168c 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -180,6 +180,7 @@ "label.action.unmanage.instance": "Unmanage instance", "label.action.unmanage.instances": "Unmanage instances", "label.action.unmanage.virtualmachine": "Unmanage VM", +"label.action.update.host": "Update host", "label.action.update.offering.access": "Update offering access", "label.action.update.resource.count": "Update resource count", "label.action.value": "Action/Value", @@ -930,6 +931,12 @@ "label.hostnamelabel": "Host name", "label.hosts": "Hosts", "label.hosttags": "Host tags", +"label.hosttags.explicit": "API-defined Host tags", +"label.hosttags.explicit.abbr": "api-defined", +"label.hosttags.explicit.description": "The host tags defined by CloudStack APIs", +"label.hosttags.implicit": "Agent-defined Host tags", +"label.hosttags.implicit.abbr": "agent-defined", +"label.hosttags.implicit.description": "The host tags defined by CloudStack Agent", "label.hourly": "Hourly", "label.hypervisor": "Hypervisor", "label.hypervisor.capabilities": "Hypervisor capabilities", diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js index 376bd973a84..d91724b32a4 100644 --- a/ui/src/config/section/infra/hosts.js +++ b/ui/src/config/section/infra/hosts.js @@ -67,12 +67,8 @@ export default { icon: 'edit-outlined', label: 'label.edit', dataView: true, - args: ['name', 'hosttags', 'oscategoryid'], - mapping: { - oscategoryid: { - api: 'listOsCategories' - } - } + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/infra/HostUpdate'))) }, { api: 'provisionCertificate', diff --git a/ui/src/views/infra/HostInfo.vue b/ui/src/views/infra/HostInfo.vue index 1d0b47eba95..0ad6b86b740 100644 --- a/ui/src/views/infra/HostInfo.vue +++ b/ui/src/views/infra/HostInfo.vue @@ -51,8 +51,14 @@
{{ $t('label.hosttags') }} -
- {{ host.hosttags }} +
+ {{ hosttag.tag }} + + {{ $t('label.hosttags.explicit.abbr') }} + + + {{ $t('label.hosttags.implicit.abbr') }} +
@@ -158,6 +164,22 @@ export default { this.fetchLoading = true api('listHosts', { id: this.resource.id }).then(json => { this.host = json.listhostsresponse.host[0] + const hosttags = this.host.hosttags?.split(',') || [] + const explicithosttags = this.host.explicithosttags?.split(',') || [] + const implicithosttags = this.host.implicithosttags?.split(',') || [] + const allHostTags = [] + for (const hosttag of hosttags) { + var isexplicit = false + var isimplicit = false + if (explicithosttags.includes(hosttag)) { + isexplicit = true + } + if (implicithosttags.includes(hosttag)) { + isimplicit = true + } + allHostTags.push({ tag: hosttag, isexplicit: isexplicit, isimplicit: isimplicit }) + } + this.host.allhosttags = allHostTags }).catch(error => { this.$notifyError(error) }).finally(() => { diff --git a/ui/src/views/infra/HostUpdate.vue b/ui/src/views/infra/HostUpdate.vue new file mode 100644 index 00000000000..d3026899c44 --- /dev/null +++ b/ui/src/views/infra/HostUpdate.vue @@ -0,0 +1,173 @@ +// 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. + + + + + + From 256051af1d1016a4d2077349b02119bdb34e932b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 10 Jun 2024 12:29:45 +0530 Subject: [PATCH 02/16] server: fix resource reservation leakage (#456) * server: fix resource reservation leakage Fixes #453 Signed-off-by: Abhishek Kumar * refactor Signed-off-by: Abhishek Kumar * Fix resource reservation leftover entries (#455) * Resolve comments * Address comments --------- Signed-off-by: Abhishek Kumar Co-authored-by: Vishesh --- .../com/cloud/user/ResourceLimitService.java | 5 +- .../user/resource/UpdateResourceCountCmd.java | 5 +- .../cloudstack/user/ResourceReservation.java | 4 + .../cloudstack/reservation/ReservationVO.java | 23 ++++++ .../reservation/dao/ReservationDao.java | 6 +- .../reservation/dao/ReservationDaoImpl.java | 79 +++++++++++++------ .../META-INF/db/schema-41800to41810.sql | 5 ++ .../resourcelimit/CheckedReservation.java | 33 +++++--- .../ResourceLimitManagerImpl.java | 28 +++++++ .../resourcelimit/CheckedReservationTest.java | 39 +++++++++ 10 files changed, 188 insertions(+), 39 deletions(-) diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index ba19719ea8d..3b30b8fc4a5 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -38,7 +38,10 @@ public interface ResourceLimitService { static final ConfigKey MaxProjectSecondaryStorage = new ConfigKey<>("Project Defaults", Long.class, "max.project.secondary.storage", "400", "The default maximum secondary storage space (in GiB) that can be used for a project", false); static final ConfigKey ResourceCountCheckInterval = new ConfigKey<>("Advanced", Long.class, "resourcecount.check.interval", "300", - "Time (in seconds) to wait before running resource recalculation and fixing task. Default is 300 seconds, Setting this to 0 disables execution of the task", true); + "Time (in seconds) to wait before running resource recalculation and fixing tasks like stale resource reservation cleanup" + + ". Default is 300 seconds, Setting this to 0 disables execution of the task", true); + static final ConfigKey ResourceReservationCleanupDelay = new ConfigKey<>("Advanced", Long.class, "resource.reservation.cleanup.delay", "3600", + "Time (in seconds) after which a resource reservation gets deleted. Default is 3600 seconds, Setting this to 0 disables execution of the task", true); static final ConfigKey ResourceLimitHostTags = new ConfigKey<>("Advanced", String.class, "resource.limit.host.tags", "", "A comma-separated list of tags for host resource limits", true); static final ConfigKey ResourceLimitStorageTags = new ConfigKey<>("Advanced", String.class, "resource.limit.storage.tags", "", diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/resource/UpdateResourceCountCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/resource/UpdateResourceCountCmd.java index 76b998edb92..d59c25083cb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/resource/UpdateResourceCountCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/resource/UpdateResourceCountCmd.java @@ -35,7 +35,10 @@ import org.apache.log4j.Logger; import com.cloud.configuration.ResourceCount; import com.cloud.user.Account; -@APICommand(name = "updateResourceCount", description = "Recalculate and update resource count for an account or domain.", responseObject = ResourceCountResponse.class, +@APICommand(name = "updateResourceCount", + description = "Recalculate and update resource count for an account or domain. This also executes some " + + "cleanup tasks before calculating resource counts.", + responseObject = ResourceCountResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class UpdateResourceCountCmd extends BaseCmd { public static final Logger s_logger = Logger.getLogger(UpdateResourceCountCmd.class.getName()); diff --git a/api/src/main/java/org/apache/cloudstack/user/ResourceReservation.java b/api/src/main/java/org/apache/cloudstack/user/ResourceReservation.java index fb4fe121cc7..d49120d4491 100644 --- a/api/src/main/java/org/apache/cloudstack/user/ResourceReservation.java +++ b/api/src/main/java/org/apache/cloudstack/user/ResourceReservation.java @@ -22,6 +22,8 @@ import org.apache.cloudstack.api.InternalIdentity; import com.cloud.configuration.Resource; +import java.util.Date; + /** * an interface defining an {code}AutoClosable{code} reservation object */ @@ -39,4 +41,6 @@ ResourceReservation extends InternalIdentity { String getTag(); Long getReservedAmount(); + + Date getCreated(); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/reservation/ReservationVO.java b/engine/schema/src/main/java/org/apache/cloudstack/reservation/ReservationVO.java index df888312a92..df0ede6821a 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/reservation/ReservationVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/reservation/ReservationVO.java @@ -25,10 +25,14 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; +import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.user.ResourceReservation; import com.cloud.configuration.Resource; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.utils.identity.ManagementServerNode; + +import java.util.Date; @Entity @Table(name = "resource_reservation") @@ -57,6 +61,12 @@ public class ReservationVO implements ResourceReservation { @Column(name = "amount") long amount; + @Column(name = "mgmt_server_id") + Long managementServerId; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + protected ReservationVO() { } @@ -69,6 +79,7 @@ public class ReservationVO implements ResourceReservation { this.resourceType = resourceType; this.tag = tag; this.amount = delta; + this.managementServerId = ManagementServerNode.getManagementServerId(); } public ReservationVO(Long accountId, Long domainId, Resource.ResourceType resourceType, Long delta) { @@ -114,4 +125,16 @@ public class ReservationVO implements ResourceReservation { this.resourceId = resourceId; } + @Override + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Long getManagementServerId() { + return managementServerId; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/reservation/dao/ReservationDao.java b/engine/schema/src/main/java/org/apache/cloudstack/reservation/dao/ReservationDao.java index 0433dc8c57d..d6d494f61f9 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/reservation/dao/ReservationDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/reservation/dao/ReservationDao.java @@ -23,13 +23,17 @@ import org.apache.cloudstack.reservation.ReservationVO; import com.cloud.configuration.Resource; import com.cloud.utils.db.GenericDao; +import java.util.Date; import java.util.List; public interface ReservationDao extends GenericDao { long getAccountReservation(Long account, Resource.ResourceType resourceType, String tag); long getDomainReservation(Long domain, Resource.ResourceType resourceType, String tag); void setResourceId(Resource.ResourceType type, Long resourceId); - List getResourceIds(long accountId, Resource.ResourceType type); List getReservationsForAccount(long accountId, Resource.ResourceType type, String tag); void removeByIds(List reservationIds); + + int removeByMsId(long managementServerId); + + int removeStaleReservations(Long accountId, Resource.ResourceType resourceType, String tag, Date createdBefore); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/reservation/dao/ReservationDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/reservation/dao/ReservationDaoImpl.java index fc4d6dfd5e3..ba3f6d60981 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/reservation/dao/ReservationDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/reservation/dao/ReservationDaoImpl.java @@ -18,8 +18,8 @@ // package org.apache.cloudstack.reservation.dao; +import java.util.Date; import java.util.List; -import java.util.stream.Collectors; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.reservation.ReservationVO; @@ -41,6 +41,8 @@ public class ReservationDaoImpl extends GenericDaoBase impl private static final String ACCOUNT_ID = "accountId"; private static final String DOMAIN_ID = "domainId"; private static final String IDS = "ids"; + private static final String MS_ID = "managementServerId"; + private static final String CREATED = "created"; private final SearchBuilder listResourceByAccountAndTypeSearch; private final SearchBuilder listAccountAndTypeSearch; private final SearchBuilder listAccountAndTypeAndNoTagSearch; @@ -49,6 +51,7 @@ public class ReservationDaoImpl extends GenericDaoBase impl private final SearchBuilder listDomainAndTypeAndNoTagSearch; private final SearchBuilder listResourceByAccountAndTypeAndNoTagSearch; private final SearchBuilder listIdsSearch; + private final SearchBuilder listMsIdSearch; public ReservationDaoImpl() { @@ -70,12 +73,14 @@ public class ReservationDaoImpl extends GenericDaoBase impl listAccountAndTypeSearch.and(ACCOUNT_ID, listAccountAndTypeSearch.entity().getAccountId(), SearchCriteria.Op.EQ); listAccountAndTypeSearch.and(RESOURCE_TYPE, listAccountAndTypeSearch.entity().getResourceType(), SearchCriteria.Op.EQ); listAccountAndTypeSearch.and(RESOURCE_TAG, listAccountAndTypeSearch.entity().getTag(), SearchCriteria.Op.EQ); + listAccountAndTypeSearch.and(CREATED, listAccountAndTypeSearch.entity().getCreated(), SearchCriteria.Op.LT); listAccountAndTypeSearch.done(); listAccountAndTypeAndNoTagSearch = createSearchBuilder(); listAccountAndTypeAndNoTagSearch.and(ACCOUNT_ID, listAccountAndTypeAndNoTagSearch.entity().getAccountId(), SearchCriteria.Op.EQ); listAccountAndTypeAndNoTagSearch.and(RESOURCE_TYPE, listAccountAndTypeAndNoTagSearch.entity().getResourceType(), SearchCriteria.Op.EQ); listAccountAndTypeAndNoTagSearch.and(RESOURCE_TAG, listAccountAndTypeAndNoTagSearch.entity().getTag(), SearchCriteria.Op.NULL); + listAccountAndTypeAndNoTagSearch.and(CREATED, listAccountAndTypeAndNoTagSearch.entity().getCreated(), SearchCriteria.Op.LT); listAccountAndTypeAndNoTagSearch.done(); listDomainAndTypeSearch = createSearchBuilder(); @@ -93,18 +98,24 @@ public class ReservationDaoImpl extends GenericDaoBase impl listIdsSearch = createSearchBuilder(); listIdsSearch.and(IDS, listIdsSearch.entity().getId(), SearchCriteria.Op.IN); listIdsSearch.done(); + + listMsIdSearch = createSearchBuilder(); + listMsIdSearch.and(MS_ID, listMsIdSearch.entity().getManagementServerId(), SearchCriteria.Op.EQ); + listMsIdSearch.done(); } @Override public long getAccountReservation(Long accountId, Resource.ResourceType resourceType, String tag) { long total = 0; - SearchCriteria sc = tag == null ? - listAccountAndTypeAndNoTagSearch.create() : listAccountAndTypeSearch.create(); - sc.setParameters(ACCOUNT_ID, accountId); - sc.setParameters(RESOURCE_TYPE, resourceType); - if (tag != null) { + SearchCriteria sc; + if (tag == null) { + sc = listAccountAndTypeAndNoTagSearch.create(); + } else { + sc = listAccountAndTypeSearch.create(); sc.setParameters(RESOURCE_TAG, tag); } + sc.setParameters(ACCOUNT_ID, accountId); + sc.setParameters(RESOURCE_TYPE, resourceType); List reservations = listBy(sc); for (ReservationVO reservation : reservations) { total += reservation.getReservedAmount(); @@ -115,13 +126,15 @@ public class ReservationDaoImpl extends GenericDaoBase impl @Override public long getDomainReservation(Long domainId, Resource.ResourceType resourceType, String tag) { long total = 0; - SearchCriteria sc = tag == null ? - listDomainAndTypeAndNoTagSearch.create() : listDomainAndTypeSearch.create(); - sc.setParameters(DOMAIN_ID, domainId); - sc.setParameters(RESOURCE_TYPE, resourceType); - if (tag != null) { + SearchCriteria sc; + if (tag == null) { + sc = listDomainAndTypeAndNoTagSearch.create(); + } else { + sc = listDomainAndTypeSearch.create(); sc.setParameters(RESOURCE_TAG, tag); } + sc.setParameters(DOMAIN_ID, domainId); + sc.setParameters(RESOURCE_TYPE, resourceType); List reservations = listBy(sc); for (ReservationVO reservation : reservations) { total += reservation.getReservedAmount(); @@ -148,23 +161,17 @@ public class ReservationDaoImpl extends GenericDaoBase impl } } - @Override - public List getResourceIds(long accountId, Resource.ResourceType type) { - SearchCriteria sc = listResourceByAccountAndTypeSearch.create(); - sc.setParameters(ACCOUNT_ID, accountId); - sc.setParameters(RESOURCE_TYPE, type); - return listBy(sc).stream().map(ReservationVO::getResourceId).collect(Collectors.toList()); - } - @Override public List getReservationsForAccount(long accountId, Resource.ResourceType type, String tag) { - SearchCriteria sc = tag == null ? - listResourceByAccountAndTypeAndNoTagSearch.create() : listResourceByAccountAndTypeSearch.create(); - sc.setParameters(ACCOUNT_ID, accountId); - sc.setParameters(RESOURCE_TYPE, type); - if (tag != null) { + SearchCriteria sc; + if (tag == null) { + sc = listResourceByAccountAndTypeAndNoTagSearch.create(); + } else { + sc = listResourceByAccountAndTypeSearch.create(); sc.setParameters(RESOURCE_TAG, tag); } + sc.setParameters(ACCOUNT_ID, accountId); + sc.setParameters(RESOURCE_TYPE, type); return listBy(sc); } @@ -176,4 +183,28 @@ public class ReservationDaoImpl extends GenericDaoBase impl remove(sc); } } + + @Override + public int removeByMsId(long managementServerId) { + SearchCriteria sc = listMsIdSearch.create(); + sc.setParameters(MS_ID, managementServerId); + return remove(sc); + } + + @Override + public int removeStaleReservations(Long accountId, Resource.ResourceType resourceType, String tag, + Date createdBefore) { + SearchCriteria sc; + if (tag == null) { + sc = listAccountAndTypeAndNoTagSearch.create(); + } else { + sc = listAccountAndTypeSearch.create(); + sc.setParameters(RESOURCE_TAG, tag); + } + sc.setParameters(ACCOUNT_ID, accountId); + sc.setParameters(RESOURCE_TYPE, resourceType); + sc.setParameters(CREATED, createdBefore); + return remove(sc); + } + } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql b/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql index 4cda23b79e0..15ec83c665a 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41800to41810.sql @@ -533,3 +533,8 @@ CREATE VIEW `cloud`.`host_view` AS `cloud`.`user` ON `user`.`uuid` = `last_annotation_view`.`user_uuid` GROUP BY `host`.`id`; + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.resource_reservation', 'mgmt_server_id', 'bigint unsigned NULL COMMENT "management server id" '); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.resource_reservation', 'created', 'datetime DEFAULT NULL COMMENT "date when the reservation was created" '); + +UPDATE `cloud`.`resource_reservation` SET `created` = now() WHERE created IS NULL; diff --git a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java index 494b204c02a..6ba53d30fef 100644 --- a/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java +++ b/server/src/main/java/com/cloud/resourcelimit/CheckedReservation.java @@ -61,12 +61,28 @@ public class CheckedReservation implements AutoCloseable { return String.format("%s-%s", ResourceReservation.class.getSimpleName(), type.getName()); } + private void removeAllReservations() { + if (CollectionUtils.isEmpty(reservations)) { + return; + } + CallContext.current().removeContextParameter(getContextParameterKey()); + for (ResourceReservation reservation : reservations) { + reservationDao.remove(reservation.getId()); + } + this.reservations = null; + } + protected void checkLimitAndPersistReservations(Account account, ResourceType resourceType, Long resourceId, List resourceLimitTags, Long amount) throws ResourceAllocationException { - checkLimitAndPersistReservation(account, resourceType, resourceId, null, amount); - if (CollectionUtils.isNotEmpty(resourceLimitTags)) { - for (String tag : resourceLimitTags) { - checkLimitAndPersistReservation(account, resourceType, resourceId, tag, amount); + try { + checkLimitAndPersistReservation(account, resourceType, resourceId, null, amount); + if (CollectionUtils.isNotEmpty(resourceLimitTags)) { + for (String tag : resourceLimitTags) { + checkLimitAndPersistReservation(account, resourceType, resourceId, tag, amount); + } } + } catch (ResourceAllocationException rae) { + removeAllReservations(); + throw rae; } } @@ -147,14 +163,7 @@ public class CheckedReservation implements AutoCloseable { @Override public void close() throws Exception { - if (CollectionUtils.isEmpty(reservations)) { - return; - } - CallContext.current().removeContextParameter(getContextParameterKey()); - for (ResourceReservation reservation : reservations) { - reservationDao.remove(reservation.getId()); - } - reservations = null; + removeAllReservations(); } public Account getAccount() { diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index cdb9664a518..c6e2bf90d75 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -20,6 +20,7 @@ import static com.cloud.utils.NumbersUtil.toHumanReadableSize; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -221,8 +222,16 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim }); } + private void cleanupResourceReservationsForMs() { + int reservationsRemoved = reservationDao.removeByMsId(ManagementServerNode.getManagementServerId()); + if (reservationsRemoved > 0) { + s_logger.warn(String.format("Removed %d resource reservations for management server id %d", reservationsRemoved, ManagementServerNode.getManagementServerId())); + } + } + @Override public boolean start() { + cleanupResourceReservationsForMs(); if (ResourceCountCheckInterval.value() >= 0) { ConfigKeyScheduledExecutionWrapper runner = new ConfigKeyScheduledExecutionWrapper(_rcExecutor, new ResourceCountCheckTask(), ResourceCountCheckInterval, TimeUnit.SECONDS); runner.start(); @@ -232,6 +241,10 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim @Override public boolean stop() { + if (_rcExecutor != null) { + _rcExecutor.shutdown(); + } + cleanupResourceReservationsForMs(); return true; } @@ -1205,8 +1218,22 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim }); } + protected void cleanupStaleResourceReservations(final long accountId, final ResourceType type, String tag) { + Long delay = ResourceReservationCleanupDelay.value(); + if (delay == null || delay <= 0) { + return; + } + Date cleanupBefore = new Date(System.currentTimeMillis() - delay * 1000); + int rowsRemoved = reservationDao.removeStaleReservations(accountId, type, tag, cleanupBefore); + if (rowsRemoved > 0) { + s_logger.warn(String.format("Removed %d stale resource reservations for account %d of type %s and tag %s", + rowsRemoved, accountId, type, tag)); + } + } + @DB protected long recalculateAccountResourceCount(final long accountId, final ResourceType type, String tag) { + cleanupStaleResourceReservations(accountId, type, tag); final Long newCount; if (type == Resource.ResourceType.user_vm) { newCount = calculateVmCountForAccount(accountId, tag); @@ -2111,6 +2138,7 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim public ConfigKey[] getConfigKeys() { return new ConfigKey[] { ResourceCountCheckInterval, + ResourceReservationCleanupDelay, MaxAccountSecondaryStorage, MaxProjectSecondaryStorage, ResourceLimitHostTags, diff --git a/server/src/test/java/com/cloud/resourcelimit/CheckedReservationTest.java b/server/src/test/java/com/cloud/resourcelimit/CheckedReservationTest.java index bf4a45ccb21..27379e9ab01 100644 --- a/server/src/test/java/com/cloud/resourcelimit/CheckedReservationTest.java +++ b/server/src/test/java/com/cloud/resourcelimit/CheckedReservationTest.java @@ -26,7 +26,9 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.reservation.ReservationVO; @@ -144,4 +146,41 @@ public class CheckedReservationTest { Assert.fail("Exception faced: " + e.getMessage()); } } + + @Test + public void testMultipleReservationsWithOneFailing() { + List tags = List.of("abc", "xyz"); + when(account.getAccountId()).thenReturn(1L); + when(account.getDomainId()).thenReturn(4L); + Map persistedReservations = new HashMap<>(); + Mockito.when(reservationDao.persist(Mockito.any(ReservationVO.class))).thenAnswer((Answer) invocation -> { + ReservationVO reservationVO = (ReservationVO) invocation.getArguments()[0]; + Long id = (long) (persistedReservations.size() + 1); + ReflectionTestUtils.setField(reservationVO, "id", id); + persistedReservations.put(id, reservationVO); + return reservationVO; + }); + Mockito.when(reservationDao.remove(Mockito.anyLong())).thenAnswer((Answer) invocation -> { + Long id = (Long) invocation.getArguments()[0]; + persistedReservations.remove(id); + return true; + }); + try { + Mockito.doThrow(ResourceAllocationException.class).when(resourceLimitService).checkResourceLimitWithTag(account, Resource.ResourceType.cpu, "xyz", 1L); + try (CheckedReservation vmReservation = new CheckedReservation(account, Resource.ResourceType.user_vm, tags, 1L, reservationDao, resourceLimitService); + CheckedReservation cpuReservation = new CheckedReservation(account, Resource.ResourceType.cpu, tags, 1L, reservationDao, resourceLimitService); + CheckedReservation memReservation = new CheckedReservation(account, Resource.ResourceType.memory, tags, 256L, reservationDao, resourceLimitService); + ) { + Assert.fail("Exception should have occurred but all reservations successful!"); + } catch (Exception ex) { + if (!(ex instanceof ResourceAllocationException)) { + Assert.fail(String.format("Expected ResourceAllocationException but %s occurred!", ex.getClass().getSimpleName())); + } + throw ex; + } + } catch (Exception rae) { + // Check if all persisted reservations are removed + Assert.assertTrue("All persisted reservations are not removed", persistedReservations.isEmpty()); + } + } } From bda0543dd0d4cca3e2175cda5842a35883db44c0 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 10 Jun 2024 14:12:06 +0530 Subject: [PATCH 03/16] ScaleIO volume live migration - use usable bytes from source disk to format the destination disk (#452) * ScaleIO volume live migration - use usable bytes from source disk to format the destination disk * Don't abort block copy job when cur,end = 0 * code improvements --- .../cloud/agent/api/GetVolumeStatAnswer.java | 85 +++++++++++++++++++ .../cloud/agent/api/GetVolumeStatCommand.java | 72 ++++++++++++++++ .../cloudstack/storage/to/VolumeObjectTO.java | 9 ++ .../LibvirtGetVolumeStatCommandWrapper.java | 66 ++++++++++++++ .../LibvirtMigrateVolumeCommandWrapper.java | 23 +++-- .../kvm/storage/KVMStoragePool.java | 4 + .../kvm/storage/KVMStorageProcessor.java | 2 +- .../kvm/storage/ScaleIOStorageAdaptor.java | 15 +++- .../kvm/storage/ScaleIOStoragePool.java | 5 ++ .../kvm/storage/StorageAdaptor.java | 5 ++ .../driver/ScaleIOPrimaryDataStoreDriver.java | 23 ++++- .../ScaleIOPrimaryDataStoreDriverTest.java | 21 ++++- 12 files changed, 313 insertions(+), 17 deletions(-) create mode 100644 core/src/main/java/com/cloud/agent/api/GetVolumeStatAnswer.java create mode 100644 core/src/main/java/com/cloud/agent/api/GetVolumeStatCommand.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumeStatCommandWrapper.java diff --git a/core/src/main/java/com/cloud/agent/api/GetVolumeStatAnswer.java b/core/src/main/java/com/cloud/agent/api/GetVolumeStatAnswer.java new file mode 100644 index 00000000000..8352c97c108 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetVolumeStatAnswer.java @@ -0,0 +1,85 @@ +// +// 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 com.cloud.agent.api; + +import com.cloud.agent.api.LogLevel.Log4jLevel; +import com.cloud.storage.Storage.StoragePoolType; + +@LogLevel(Log4jLevel.Trace) +public class GetVolumeStatAnswer extends Answer { + String poolUuid; + StoragePoolType poolType; + String volumePath; + long size = 0; + long virtualSize = 0; + + public GetVolumeStatAnswer(GetVolumeStatCommand cmd, long size, long virtualSize) { + super(cmd, true, ""); + this.poolUuid = cmd.getPoolUuid(); + this.poolType = cmd.getPoolType(); + this.volumePath = cmd.getVolumePath(); + this.size = size; + this.virtualSize = virtualSize; + } + + public GetVolumeStatAnswer(GetVolumeStatCommand cmd, boolean success, String details) { + super(cmd, success, details); + } + + protected GetVolumeStatAnswer() { + //no-args constructor for json serialization-deserialization + } + + public String getPoolUuid() { + return poolUuid; + } + + public void setPoolUuid(String poolUuid) { + this.poolUuid = poolUuid; + } + + public StoragePoolType getPoolType() { + return poolType; + } + + public void setPoolType(StoragePoolType poolType) { + this.poolType = poolType; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getVirtualSize() { + return virtualSize; + } + + public void setVirtualSize(long virtualSize) { + this.virtualSize = virtualSize; + } + + public String getString() { + return "GetVolumeStatAnswer [poolUuid=" + poolUuid + ", poolType=" + poolType + ", volumePath=" + volumePath + ", size=" + size + ", virtualSize=" + virtualSize + "]"; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/GetVolumeStatCommand.java b/core/src/main/java/com/cloud/agent/api/GetVolumeStatCommand.java new file mode 100644 index 00000000000..1be3d6a0419 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetVolumeStatCommand.java @@ -0,0 +1,72 @@ +// +// 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 com.cloud.agent.api; + +import com.cloud.agent.api.LogLevel.Log4jLevel; +import com.cloud.storage.Storage.StoragePoolType; + +@LogLevel(Log4jLevel.Trace) +public class GetVolumeStatCommand extends Command { + String volumePath; + StoragePoolType poolType; + String poolUuid; + + protected GetVolumeStatCommand() { + } + + public GetVolumeStatCommand(String volumePath, StoragePoolType poolType, String poolUuid) { + this.volumePath = volumePath; + this.poolType = poolType; + this.poolUuid = poolUuid; + } + + public String getVolumePath() { + return volumePath; + } + + public void setVolumePath(String volumePath) { + this.volumePath = volumePath; + } + + public StoragePoolType getPoolType() { + return poolType; + } + + public void setPoolType(StoragePoolType poolType) { + this.poolType = poolType; + } + + public String getPoolUuid() { + return poolUuid; + } + + public void setPoolUuid(String storeUuid) { + this.poolUuid = storeUuid; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getString() { + return "GetVolumeStatCommand [volumePath=" + volumePath + ", poolType=" + poolType + ", poolUuid=" + poolUuid + "]"; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java index 2bb67c80ce4..6514038ac62 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java +++ b/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java @@ -39,6 +39,7 @@ public class VolumeObjectTO extends DownloadableObjectTO implements DataTO { private DataStoreTO dataStore; private String name; private Long size; + private Long usableSize; private String path; private Long volumeId; private String vmName; @@ -161,6 +162,10 @@ public class VolumeObjectTO extends DownloadableObjectTO implements DataTO { return size; } + public Long getUsableSize() { + return usableSize; + } + @Override public DataObjectType getObjectType() { return DataObjectType.VOLUME; @@ -178,6 +183,10 @@ public class VolumeObjectTO extends DownloadableObjectTO implements DataTO { this.size = size; } + public void setUsableSize(Long usableSize) { + this.usableSize = usableSize; + } + public void setPath(String path) { this.path = path; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumeStatCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumeStatCommandWrapper.java new file mode 100644 index 00000000000..2a9c886f449 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumeStatCommandWrapper.java @@ -0,0 +1,66 @@ +// +//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 com.cloud.hypervisor.kvm.resource.wrapper; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.GetVolumeStatAnswer; +import com.cloud.agent.api.GetVolumeStatCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.utils.exception.CloudRuntimeException; + +@ResourceWrapper(handles = GetVolumeStatCommand.class) +public final class LibvirtGetVolumeStatCommandWrapper extends CommandWrapper { + private static final Logger s_logger = Logger.getLogger(LibvirtGetVolumeStatCommandWrapper.class); + + @Override + public Answer execute(final GetVolumeStatCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + try { + String volumePath = cmd.getVolumePath(); + StoragePoolType poolType = cmd.getPoolType(); + String poolUuid = cmd.getPoolUuid(); + + KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + KVMStoragePool primaryPool = storagePoolMgr.getStoragePool(poolType, poolUuid); + if (primaryPool == null) { + String msg = "Can't get volume stats as pool details unavailable for volume: " + volumePath + " on the storage pool: " + poolUuid; + return new GetVolumeStatAnswer(cmd, false, msg); + } + + KVMPhysicalDisk disk = primaryPool.getPhysicalDisk(volumePath); + if (disk == null) { + String msg = "Can't get volume stats as disk details unavailable for volume: " + volumePath + " on the storage pool: " + poolUuid; + return new GetVolumeStatAnswer(cmd, false, msg); + } + + return new GetVolumeStatAnswer(cmd, disk.getSize(), disk.getVirtualSize()); + } catch (CloudRuntimeException e) { + s_logger.error("Can't get volume stats, due to: " + e.getMessage(), e); + return new GetVolumeStatAnswer(cmd, false, "Can't get volume stats, due to: " + e.getMessage()); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateVolumeCommandWrapper.java index 5c893e5d12f..11a94e19238 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateVolumeCommandWrapper.java @@ -142,7 +142,6 @@ public class LibvirtMigrateVolumeCommandWrapper extends CommandWrapper 0) { DomainBlockJobInfo blockJobInfo = dm.getBlockJobInfo(diskLabel, 0); if (blockJobInfo != null) { - LOGGER.debug(String.format("Volume %s : %s block copy progress: %s%% current value:%s end value:%s", diskLabel, srcPath, (blockJobInfo.end == 0)? 0 : 100*(blockJobInfo.cur / (double) blockJobInfo.end), blockJobInfo.cur, blockJobInfo.end)); + blockCopyProgress = (blockJobInfo.end == 0)? blockCopyProgress : 100 * (blockJobInfo.cur / (double) blockJobInfo.end); + LOGGER.debug(String.format("Volume %s : %s, block copy progress: %s%%, current value: %s end value: %s, job info - type: %s, bandwidth: %s", + diskLabel, srcPath, blockCopyProgress, blockJobInfo.cur, blockJobInfo.end, blockJobInfo.type, blockJobInfo.bandwidth)); if (blockJobInfo.cur == blockJobInfo.end) { - LOGGER.info(String.format("Block copy completed for the volume %s : %s", diskLabel, srcPath)); - dm.blockJobAbort(diskLabel, Domain.BlockJobAbortFlags.PIVOT); - if (StringUtils.isNotEmpty(srcSecretUUID)) { - libvirtComputingResource.removeLibvirtVolumeSecret(conn, srcSecretUUID); + if (blockJobInfo.end > 0) { + LOGGER.info(String.format("Block copy completed for the volume %s : %s", diskLabel, srcPath)); + dm.blockJobAbort(diskLabel, Domain.BlockJobAbortFlags.PIVOT); + if (StringUtils.isNotEmpty(srcSecretUUID)) { + libvirtComputingResource.removeLibvirtVolumeSecret(conn, srcSecretUUID); + } + break; + } else { + // cur = 0, end = 0 - at this point, disk does not have an active block job (so, no need to abort job) + String msg = String.format("No active block copy job for the volume %s : %s - job stopped at %s progress", diskLabel, srcPath, blockCopyProgress); + LOGGER.warn(msg); + return new MigrateVolumeAnswer(command, false, msg, null); } - break; } } else { LOGGER.info("Failed to get the block copy status, trying to abort the job"); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java index bce3b402dbb..9920d235f30 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java @@ -26,6 +26,10 @@ import com.cloud.storage.Storage; import com.cloud.storage.Storage.StoragePoolType; public interface KVMStoragePool { + public default KVMPhysicalDisk createPhysicalDisk(String volumeUuid, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, Long usableSize, byte[] passphrase) { + return createPhysicalDisk(volumeUuid, format, provisioningType, size, passphrase); + } + public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase); public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, Storage.ProvisioningType provisioningType, long size, byte[] passphrase); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 184415871e3..50c6c0c9b0c 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -1613,7 +1613,7 @@ public class KVMStorageProcessor implements StorageProcessor { } } else { vol = primaryPool.createPhysicalDisk(volume.getUuid(), format, - volume.getProvisioningType(), disksize, volume.getPassphrase()); + volume.getProvisioningType(), disksize, volume.getUsableSize(), volume.getPassphrase()); } final VolumeObjectTO newVol = new VolumeObjectTO(); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java index 2cd68753e01..64144ecab87 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java @@ -153,6 +153,11 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { return MapStorageUuidToStoragePool.remove(uuid) != null; } + @Override + public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, QemuImg.PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { + return createPhysicalDisk(name, pool, format, provisioningType, size, null, passphrase); + } + /** * ScaleIO doesn't need to communicate with the hypervisor normally to create a volume. This is used only to prepare a ScaleIO data disk for encryption. * Thin encrypted volumes are provisioned in QCOW2 format, which insulates the guest from zeroes/unallocated blocks in the block device that would @@ -162,11 +167,12 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { * @param format disk format * @param provisioningType provisioning type * @param size disk size + * @param usableSize usage disk size * @param passphrase passphrase * @return the disk object */ @Override - public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, QemuImg.PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { + public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, QemuImg.PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, Long usableSize, byte[] passphrase) { if (passphrase == null || passphrase.length == 0) { return null; } @@ -184,7 +190,12 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { QemuImg qemuImg = new QemuImg(0, true, false); Map options = new HashMap<>(); List qemuObjects = new ArrayList<>(); - long formattedSize = getUsableBytesFromRawBytes(disk.getSize()); + long formattedSize; + if (usableSize != null && usableSize > 0) { + formattedSize = usableSize; + } else { + formattedSize = getUsableBytesFromRawBytes(disk.getSize()); + } options.put("preallocation", QemuImg.PreallocationType.Metadata.toString()); qemuObjects.add(QemuObject.prepareSecretForQemuImg(disk.getFormat(), disk.getQemuEncryptFormat(), keyFile.toString(), "sec0", options)); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePool.java index cf977f5467b..6a53710f808 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePool.java @@ -69,6 +69,11 @@ public class ScaleIOStoragePool implements KVMStoragePool { } } + @Override + public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, QemuImg.PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, Long usableSize, byte[] passphrase) { + return this.storageAdaptor.createPhysicalDisk(volumeUuid, this, format, provisioningType, size, usableSize, passphrase); + } + @Override public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, QemuImg.PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { return this.storageAdaptor.createPhysicalDisk(volumeUuid, this, format, provisioningType, size, passphrase); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java index 34bf08f4496..bb838282948 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java @@ -41,6 +41,11 @@ public interface StorageAdaptor { public boolean deleteStoragePool(String uuid); + public default KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, + PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, Long usableSize, byte[] passphrase) { + return createPhysicalDisk(name, pool, format, provisioningType, size, passphrase); + } + public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase); diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java index 2ac11da9f65..e42a74fb402 100644 --- a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java @@ -22,6 +22,8 @@ import java.util.Map; import javax.inject.Inject; +import com.cloud.agent.api.GetVolumeStatAnswer; +import com.cloud.agent.api.GetVolumeStatCommand; import com.cloud.agent.api.storage.MigrateVolumeCommand; import com.cloud.agent.api.storage.ResizeVolumeCommand; import com.cloud.agent.api.to.StorageFilerTO; @@ -489,10 +491,10 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { } public CreateObjectAnswer createVolume(VolumeInfo volumeInfo, long storagePoolId) { - return createVolume(volumeInfo, storagePoolId, false); + return createVolume(volumeInfo, storagePoolId, false, null); } - public CreateObjectAnswer createVolume(VolumeInfo volumeInfo, long storagePoolId, boolean migrationInvolved) { + public CreateObjectAnswer createVolume(VolumeInfo volumeInfo, long storagePoolId, boolean migrationInvolved, Long usageSize) { LOGGER.debug("Creating PowerFlex volume"); StoragePoolVO storagePool = storagePoolDao.findById(storagePoolId); @@ -542,6 +544,9 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { VolumeObjectTO prepVolume = (VolumeObjectTO) createdObject.getTO(); prepVolume.setPath(volumePath); prepVolume.setUuid(volumePath); + if (usageSize != null) { + prepVolume.setUsableSize(usageSize); + } CreateObjectCommand cmd = new CreateObjectCommand(prepVolume); EndPoint ep = selector.select(volumeInfo, true); if (ep == null) { @@ -844,7 +849,8 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { // Volume migration across different PowerFlex/ScaleIO clusters final long srcVolumeId = srcData.getId(); DataStore srcStore = srcData.getDataStore(); - Map srcDetails = getVolumeDetails((VolumeInfo) srcData, srcStore); + VolumeInfo srcVolumeInfo = (VolumeInfo) srcData; + Map srcDetails = getVolumeDetails(srcVolumeInfo, srcStore); DataStore destStore = destData.getDataStore(); final long destPoolId = destStore.getId(); @@ -856,8 +862,17 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { EndPoint ep = RemoteHostEndPoint.getHypervisorHostEndPoint(host); Answer answer = null; + Long srcVolumeUsableSize = null; try { - CreateObjectAnswer createAnswer = createVolume((VolumeInfo) destData, destStore.getId(), true); + GetVolumeStatCommand statCmd = new GetVolumeStatCommand(srcVolumeInfo.getPath(), srcVolumeInfo.getStoragePoolType(), srcStore.getUuid()); + GetVolumeStatAnswer statAnswer = (GetVolumeStatAnswer) ep.sendMessage(statCmd); + if (!statAnswer.getResult() ) { + LOGGER.warn(String.format("Unable to get volume %s stats", srcVolumeInfo.getId())); + } else if (statAnswer.getVirtualSize() > 0) { + srcVolumeUsableSize = statAnswer.getVirtualSize(); + } + + CreateObjectAnswer createAnswer = createVolume((VolumeInfo) destData, destStore.getId(), true, srcVolumeUsableSize); destVolumePath = createAnswer.getData().getPath(); destVolTO.setPath(destVolumePath); diff --git a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriverTest.java b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriverTest.java index acbfaa822f1..f9cd2cda4f1 100644 --- a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriverTest.java +++ b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriverTest.java @@ -53,7 +53,10 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.GetVolumeStatAnswer; +import com.cloud.agent.api.GetVolumeStatCommand; import com.cloud.agent.api.storage.MigrateVolumeAnswer; +import com.cloud.agent.api.storage.MigrateVolumeCommand; import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.DiskTO; import com.cloud.configuration.Config; @@ -185,16 +188,23 @@ public class ScaleIOPrimaryDataStoreDriverTest { RemoteHostEndPoint ep = Mockito.mock(RemoteHostEndPoint.class); when(RemoteHostEndPoint.getHypervisorHostEndPoint(host)).thenReturn(ep); + long volumeVirtualSize = 68673077248L; DataTO dataTO = Mockito.mock(DataTO.class); CreateObjectAnswer createAnswer = new CreateObjectAnswer(dataTO); - doReturn(createAnswer).when(scaleIOPrimaryDataStoreDriver).createVolume(destData, 2L, true); + doReturn(createAnswer).when(scaleIOPrimaryDataStoreDriver).createVolume(destData, 2L, true, volumeVirtualSize); when(dataTO.getPath()).thenReturn("bec0ba7700000007:vol-11-6aef-10ee"); doReturn(true).when(scaleIOPrimaryDataStoreDriver) .grantAccess(any(), any(), any()); when(configDao.getValue(Config.MigrateWait.key())).thenReturn("3600"); + + GetVolumeStatAnswer getVolumeStatAnswer = Mockito.mock(GetVolumeStatAnswer.class); + when(ep.sendMessage(any(GetVolumeStatCommand.class))).thenReturn(getVolumeStatAnswer); + when(getVolumeStatAnswer.getResult()).thenReturn(true); + when(getVolumeStatAnswer.getVirtualSize()).thenReturn(volumeVirtualSize); + MigrateVolumeAnswer migrateVolumeAnswer = Mockito.mock(MigrateVolumeAnswer.class); - when(ep.sendMessage(any())).thenReturn(migrateVolumeAnswer); + when(ep.sendMessage(any(MigrateVolumeCommand.class))).thenReturn(migrateVolumeAnswer); when(migrateVolumeAnswer.getResult()).thenReturn(true); Mockito.doNothing().when(scaleIOPrimaryDataStoreDriver) @@ -232,12 +242,17 @@ public class ScaleIOPrimaryDataStoreDriverTest { DataTO dataTO = Mockito.mock(DataTO.class); CreateObjectAnswer createAnswer = new CreateObjectAnswer(dataTO); - doReturn(createAnswer).when(scaleIOPrimaryDataStoreDriver).createVolume(destData, 2L, true); + doReturn(createAnswer).when(scaleIOPrimaryDataStoreDriver).createVolume(destData, 2L, true, null); when(dataTO.getPath()).thenReturn("bec0ba7700000007:vol-11-6aef-10ee"); doReturn(true).when(scaleIOPrimaryDataStoreDriver) .grantAccess(any(), any(), any()); when(configDao.getValue(Config.MigrateWait.key())).thenReturn("3600"); + + GetVolumeStatAnswer getVolumeStatAnswer = Mockito.mock(GetVolumeStatAnswer.class); + when(ep.sendMessage(any(GetVolumeStatCommand.class))).thenReturn(getVolumeStatAnswer); + when(getVolumeStatAnswer.getResult()).thenReturn(false); + MigrateVolumeAnswer migrateVolumeAnswer = Mockito.mock(MigrateVolumeAnswer.class); when(ep.sendMessage(any())).thenReturn(migrateVolumeAnswer); when(migrateVolumeAnswer.getResult()).thenReturn(false); From 04091abc0d5b9d46fc14fbeb0057a733bc3677ac Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Wed, 19 Jun 2024 12:54:32 +0530 Subject: [PATCH 04/16] User data content size validation, register managed user data using POST call from UI, and related code improvements (#361) * Validate user data with actual length, and some code improvements * Ignore if user data is not set (don't fail) * Validate user data after finalizing it * Updated registerUserData API using POST call from UI, to support user data upto 1048576 bytes * Apply suggestions from code review * Added logs for user data * Addressed review comments * Check user data length with base64 encoded data, and some code improvements --- .../CreateAutoScaleVmProfileCmd.java | 2 +- .../UpdateAutoScaleVmProfileCmd.java | 2 +- .../user/userdata/RegisterUserDataCmd.java | 9 +- .../api/command/user/vm/DeployVMCmd.java | 6 +- .../command/user/vm/ResetVMUserDataCmd.java | 2 +- .../api/command/user/vm/UpdateVMCmd.java | 2 +- .../response/AutoScaleVmProfileResponse.java | 2 +- .../api/response/VMUserDataResponse.java | 2 +- .../cloudstack/userdata/UserDataManager.java | 5 + .../configuration/ConfigurationManager.java | 4 - .../userdata/UserDataManagerImpl.java | 91 ++++++++++--------- .../ConfigurationManagerImpl.java | 9 +- .../network/as/AutoScaleManagerImpl.java | 7 +- .../main/java/com/cloud/vm/UserVmManager.java | 2 - .../java/com/cloud/vm/UserVmManagerImpl.java | 55 +---------- .../network/as/AutoScaleManagerImplTest.java | 10 +- ui/src/views/compute/RegisterUserData.vue | 2 +- 17 files changed, 93 insertions(+), 119 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java index db6ccd9ce53..8c1d38c1375 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmProfileCmd.java @@ -102,7 +102,7 @@ public class CreateAutoScaleVmProfileCmd extends BaseAsyncCreateCmd { description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. " + "This binary data must be base64 encoded before adding it to the request. " + "Using HTTP GET (via querystring), you can send up to 4KB of data after base64 encoding. " + - "Using HTTP POST(via POST body), you can send up to 1MB of data after base64 encoding." + + "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " + "You also need to change vm.userdata.max.length value", length = 1048576, since = "4.18.0") diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmProfileCmd.java index 3e65d38e520..34c81bfc483 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmProfileCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmProfileCmd.java @@ -97,7 +97,7 @@ public class UpdateAutoScaleVmProfileCmd extends BaseAsyncCustomIdCmd { description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. " + "This binary data must be base64 encoded before adding it to the request. " + "Using HTTP GET (via querystring), you can send up to 4KB of data after base64 encoding. " + - "Using HTTP POST(via POST body), you can send up to 1MB of data after base64 encoding." + + "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " + "You also need to change vm.userdata.max.length value", length = 1048576, since = "4.18.0") diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java index a8a87c40725..21141fc595d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java @@ -74,7 +74,14 @@ public class RegisterUserDataCmd extends BaseCmd { @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "an optional project for the userdata") private Long projectId; - @Parameter(name = ApiConstants.USER_DATA, type = CommandType.STRING, required = true, description = "Userdata content", length = 1048576) + @Parameter(name = ApiConstants.USER_DATA, + type = CommandType.STRING, + required = true, + description = "Base64 encoded userdata content. " + + "Using HTTP GET (via querystring), you can send up to 4KB of data after base64 encoding. " + + "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " + + "You also need to change vm.userdata.max.length value", + length = 1048576) private String userData; @Parameter(name = ApiConstants.PARAMS, type = CommandType.STRING, description = "comma separated list of variables declared in userdata content") 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 107bd817516..99663dd75d5 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 @@ -152,7 +152,11 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG private String hypervisor; @Parameter(name = ApiConstants.USER_DATA, type = CommandType.STRING, - description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. This binary data must be base64 encoded before adding it to the request. Using HTTP GET (via querystring), you can send up to 4KB of data after base64 encoding. Using HTTP POST(via POST body), you can send up to 1MB of data after base64 encoding.", + description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. " + + "This binary data must be base64 encoded before adding it to the request. " + + "Using HTTP GET (via querystring), you can send up to 4KB of data after base64 encoding. " + + "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " + + "You also need to change vm.userdata.max.length value", length = 1048576) private String userData; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java index 3ead67e2106..7e0aab98760 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java @@ -62,7 +62,7 @@ public class ResetVMUserDataCmd extends BaseCmd implements UserCmd { description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. " + "This binary data must be base64 encoded before adding it to the request. " + "Using HTTP GET (via querystring), you can send up to 4KB of data after base64 encoding. " + - "Using HTTP POST(via POST body), you can send up to 1MB of data after base64 encoding." + + "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " + "You also need to change vm.userdata.max.length value", length = 1048576) private String userData; 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 32ce1f6db52..4527e152f18 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 @@ -86,7 +86,7 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction, description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. " + "This binary data must be base64 encoded before adding it to the request. " + "Using HTTP GET (via querystring), you can send up to 4KB of data after base64 encoding. " + - "Using HTTP POST(via POST body), you can send up to 1MB of data after base64 encoding." + + "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " + "You also need to change vm.userdata.max.length value", length = 1048576, since = "4.16.0") diff --git a/api/src/main/java/org/apache/cloudstack/api/response/AutoScaleVmProfileResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/AutoScaleVmProfileResponse.java index 9f238344730..678c4fcaaca 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/AutoScaleVmProfileResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/AutoScaleVmProfileResponse.java @@ -69,7 +69,7 @@ public class AutoScaleVmProfileResponse extends BaseResponse implements Controll private Map counterParams; @SerializedName(ApiConstants.USER_DATA) - @Param(description = "Base 64 encoded VM user data") + @Param(description = "Base64 encoded VM user data") private String userData; @SerializedName(ApiConstants.USER_DATA_ID) @Param(description="the id of userdata used for the VM", since = "4.18.1") diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VMUserDataResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VMUserDataResponse.java index 1b739e56442..cf819491c2c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VMUserDataResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VMUserDataResponse.java @@ -30,7 +30,7 @@ public class VMUserDataResponse extends BaseResponse { private String vmId; @SerializedName(ApiConstants.USER_DATA) - @Param(description = "Base 64 encoded VM user data") + @Param(description = "Base64 encoded VM user data") private String userData; public void setUserData(String userData) { diff --git a/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java b/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java index 4dfcd0a7de1..b4bede24890 100644 --- a/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java +++ b/api/src/main/java/org/apache/cloudstack/userdata/UserDataManager.java @@ -17,11 +17,16 @@ package org.apache.cloudstack.userdata; import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import com.cloud.utils.component.Manager; public interface UserDataManager extends Manager, Configurable { + String VM_USERDATA_MAX_LENGTH_STRING = "vm.userdata.max.length"; + ConfigKey VM_USERDATA_MAX_LENGTH = new ConfigKey<>("Advanced", Integer.class, VM_USERDATA_MAX_LENGTH_STRING, "32768", + "Max length of vm userdata after base64 encoding. Default is 32768 and maximum is 1048576", true); + String concatenateUserData(String userdata1, String userdata2, String userdataProvider); String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod); } diff --git a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java index 728511ba8d5..3341f6e6bb0 100644 --- a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java +++ b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java @@ -21,7 +21,6 @@ import java.util.Map; import java.util.Set; import com.cloud.dc.VlanVO; -import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.impl.ConfigurationSubGroupVO; import com.cloud.dc.ClusterVO; @@ -61,9 +60,6 @@ public interface ConfigurationManager { public static final String MESSAGE_CREATE_VLAN_IP_RANGE_EVENT = "Message.CreateVlanIpRange.Event"; public static final String MESSAGE_DELETE_VLAN_IP_RANGE_EVENT = "Message.DeleteVlanIpRange.Event"; - static final String VM_USERDATA_MAX_LENGTH_STRING = "vm.userdata.max.length"; - static final ConfigKey VM_USERDATA_MAX_LENGTH = new ConfigKey<>("Advanced", Integer.class, VM_USERDATA_MAX_LENGTH_STRING, "32768", - "Max length of vm userdata after base64 decoding. Default is 32768 and maximum is 1048576", true); /** * @param offering diff --git a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java index 91f24fe7045..81a77fa5c0b 100644 --- a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java +++ b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java @@ -26,19 +26,18 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; -import com.cloud.configuration.ConfigurationManager; import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.exception.CloudRuntimeException; public class UserDataManagerImpl extends ManagerBase implements UserDataManager { - - + private static final Logger s_logger = Logger.getLogger(UserDataManagerImpl.class); private static final int MAX_USER_DATA_LENGTH_BYTES = 2048; - private static final int MAX_HTTP_GET_LENGTH = 2 * MAX_USER_DATA_LENGTH_BYTES; + private static final int MAX_HTTP_GET_LENGTH = 2 * MAX_USER_DATA_LENGTH_BYTES; // 4KB private static final int NUM_OF_2K_BLOCKS = 512; - private static final int MAX_HTTP_POST_LENGTH = NUM_OF_2K_BLOCKS * MAX_USER_DATA_LENGTH_BYTES; + private static final int MAX_HTTP_POST_LENGTH = NUM_OF_2K_BLOCKS * MAX_USER_DATA_LENGTH_BYTES; // 1MB private List userDataProviders; private static Map userDataProvidersMap = new HashMap<>(); @@ -67,7 +66,7 @@ public class UserDataManagerImpl extends ManagerBase implements UserDataManager @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] {}; + return new ConfigKey[] {VM_USERDATA_MAX_LENGTH}; } protected UserDataProvider getUserdataProvider(String name) { @@ -90,49 +89,57 @@ public class UserDataManagerImpl extends ManagerBase implements UserDataManager @Override public String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) { - byte[] decodedUserData = null; - if (userData != null) { - - if (userData.contains("%")) { - try { - userData = URLDecoder.decode(userData, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new InvalidParameterValueException("Url decoding of userdata failed."); - } - } - - if (!Base64.isBase64(userData)) { - throw new InvalidParameterValueException("User data is not base64 encoded"); - } - // If GET, use 4K. If POST, support up to 1M. - if (httpmethod.equals(BaseCmd.HTTPMethod.GET)) { - decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_GET_LENGTH, BaseCmd.HTTPMethod.GET); - } else if (httpmethod.equals(BaseCmd.HTTPMethod.POST)) { - decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_POST_LENGTH, BaseCmd.HTTPMethod.POST); - } - - if (decodedUserData == null || decodedUserData.length < 1) { - throw new InvalidParameterValueException("User data is too short"); - } - // Re-encode so that the '=' paddings are added if necessary since 'isBase64' does not require it, but python does on the VR. - return Base64.encodeBase64String(decodedUserData); + s_logger.trace(String.format("Validating base64 encoded user data: [%s].", userData)); + if (StringUtils.isBlank(userData)) { + s_logger.debug("Null/empty base64 encoded user data set"); + return null; } - return null; + + if (userData.contains("%")) { + try { + userData = URLDecoder.decode(userData, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new InvalidParameterValueException("Url decoding of user data failed."); + } + } + + if (!Base64.isBase64(userData)) { + throw new InvalidParameterValueException("User data is not base64 encoded."); + } + + byte[] decodedUserData = null; + + // If GET, use 4K. If POST, support up to 1M. + if (httpmethod.equals(BaseCmd.HTTPMethod.GET)) { + decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_GET_LENGTH, BaseCmd.HTTPMethod.GET); + } else if (httpmethod.equals(BaseCmd.HTTPMethod.POST)) { + decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_POST_LENGTH, BaseCmd.HTTPMethod.POST); + } + + // Re-encode so that the '=' paddings are added if necessary since 'isBase64' does not require it, but python does on the VR. + return Base64.encodeBase64String(decodedUserData); } private byte[] validateAndDecodeByHTTPMethod(String userData, int maxHTTPLength, BaseCmd.HTTPMethod httpMethod) { - byte[] decodedUserData = null; + byte[] decodedUserData = Base64.decodeBase64(userData.getBytes()); + if (decodedUserData == null || decodedUserData.length < 1) { + throw new InvalidParameterValueException("User data is too short."); + } - if (userData.length() >= maxHTTPLength) { - throw new InvalidParameterValueException(String.format("User data is too long for an http %s request", httpMethod.toString())); - } - if (userData.length() > ConfigurationManager.VM_USERDATA_MAX_LENGTH.value()) { - throw new InvalidParameterValueException("User data has exceeded configurable max length : " + ConfigurationManager.VM_USERDATA_MAX_LENGTH.value()); - } - decodedUserData = Base64.decodeBase64(userData.getBytes()); - if (decodedUserData.length > maxHTTPLength) { + s_logger.trace(String.format("Decoded user data: [%s].", decodedUserData)); + int userDataLength = userData.length(); + int decodedUserDataLength = decodedUserData.length; + s_logger.info(String.format("Configured base64 encoded user data size: %d bytes, actual user data size: %d bytes", userDataLength, decodedUserDataLength)); + + if (userDataLength > maxHTTPLength) { + s_logger.warn(String.format("Base64 encoded user data (size: %d bytes) too long for http %s request (accepted size: %d bytes)", userDataLength, httpMethod.toString(), maxHTTPLength)); throw new InvalidParameterValueException(String.format("User data is too long for http %s request", httpMethod.toString())); } + if (userDataLength > VM_USERDATA_MAX_LENGTH.value()) { + s_logger.warn(String.format("Base64 encoded user data (size: %d bytes) has exceeded configurable max length of %d bytes", userDataLength, VM_USERDATA_MAX_LENGTH.value())); + throw new InvalidParameterValueException("User data has exceeded configurable max length: " + VM_USERDATA_MAX_LENGTH.value()); + } + return decodedUserData; } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 9771c289afe..7ce49c739b0 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -126,6 +126,7 @@ 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.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -547,7 +548,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati configValuesForValidation.add(StorageManager.STORAGE_POOL_DISK_WAIT.key()); configValuesForValidation.add(StorageManager.STORAGE_POOL_CLIENT_TIMEOUT.key()); configValuesForValidation.add(StorageManager.STORAGE_POOL_CLIENT_MAX_CONNECTIONS.key()); - configValuesForValidation.add(VM_USERDATA_MAX_LENGTH_STRING); + configValuesForValidation.add(UserDataManager.VM_USERDATA_MAX_LENGTH_STRING); } private void weightBasedParametersForValidation() { @@ -1256,7 +1257,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati throw new InvalidParameterValueException("Please enter a value less than 257 for the configuration parameter:" + name); } } - if (VM_USERDATA_MAX_LENGTH_STRING.equalsIgnoreCase(name)) { + if (UserDataManager.VM_USERDATA_MAX_LENGTH_STRING.equalsIgnoreCase(name)) { if (val > 1048576) { throw new InvalidParameterValueException("Please enter a value less than 1048576 for the configuration parameter:" + name); } @@ -7661,8 +7662,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[] {SystemVMUseLocalStorage, IOPS_MAX_READ_LENGTH, IOPS_MAX_WRITE_LENGTH, - BYTES_MAX_READ_LENGTH, BYTES_MAX_WRITE_LENGTH, ADD_HOST_ON_SERVICE_RESTART_KVM, SET_HOST_DOWN_TO_MAINTENANCE, VM_SERVICE_OFFERING_MAX_CPU_CORES, - VM_SERVICE_OFFERING_MAX_RAM_SIZE, VM_USERDATA_MAX_LENGTH, MIGRATE_VM_ACROSS_CLUSTERS, + BYTES_MAX_READ_LENGTH, BYTES_MAX_WRITE_LENGTH, ADD_HOST_ON_SERVICE_RESTART_KVM, SET_HOST_DOWN_TO_MAINTENANCE, + VM_SERVICE_OFFERING_MAX_CPU_CORES, VM_SERVICE_OFFERING_MAX_RAM_SIZE, MIGRATE_VM_ACROSS_CLUSTERS, ENABLE_ACCOUNT_SETTINGS_FOR_DOMAIN, ENABLE_DOMAIN_SETTINGS_FOR_CHILD_DOMAIN, ALLOW_DOMAIN_ADMINS_TO_CREATE_TAGGED_OFFERINGS }; } diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java index de8a3ff3c83..9beff6555ca 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -70,6 +70,7 @@ 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.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.userdata.UserDataManager; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.RandomStringUtils; @@ -254,6 +255,8 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage @Inject private UserVmManager userVmMgr; @Inject + private UserDataManager userDataMgr; + @Inject private UserVmDao userVmDao; @Inject private HostDao hostDao; @@ -573,7 +576,7 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage userDataDetails = cmd.getUserDataDetails().toString(); } userData = userVmMgr.finalizeUserData(userData, userDataId, template); - userData = userVmMgr.validateUserData(userData, cmd.getHttpMethod()); + userData = userDataMgr.validateUserData(userData, cmd.getHttpMethod()); if (userData != null) { profileVO.setUserData(userData); } @@ -652,7 +655,7 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage } VirtualMachineTemplate template = entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, templateId); userData = userVmMgr.finalizeUserData(userData, userDataId, template); - userData = userVmMgr.validateUserData(userData, cmd.getHttpMethod()); + userData = userDataMgr.validateUserData(userData, cmd.getHttpMethod()); vmProfile.setUserDataId(userDataId); vmProfile.setUserData(userData); vmProfile.setUserDataDetails(userDataDetails); diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index e73a572d1d7..c38a1052c37 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -93,8 +93,6 @@ public interface UserVmManager extends UserVmService { String finalizeUserData(String userData, Long userDataId, VirtualMachineTemplate template); - String validateUserData(String userData, HTTPMethod httpmethod); - void validateExtraConfig(long accountId, HypervisorType hypervisorType, String extraConfig); boolean isVMUsingLocalStorage(VMInstanceVO vm); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index cb9a3911273..7f9635c0a8a 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -16,7 +16,6 @@ // under the License. package com.cloud.vm; -import static com.cloud.configuration.ConfigurationManagerImpl.VM_USERDATA_MAX_LENGTH; import static com.cloud.storage.Volume.IOPS_LIMIT; import static com.cloud.utils.NumbersUtil.toHumanReadableSize; import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS; @@ -130,7 +129,6 @@ import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.userdata.UserDataManager; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.security.ParserUtils; -import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.math.NumberUtils; @@ -2744,6 +2742,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir userDataDetails = cmd.getUserdataDetails().toString(); } userData = finalizeUserData(userData, userDataId, template); + userData = userDataManager.validateUserData(userData, cmd.getHttpMethod()); long accountId = vmInstance.getAccountId(); @@ -4852,56 +4851,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } - @Override - public String validateUserData(String userData, HTTPMethod httpmethod) { - byte[] decodedUserData = null; - if (userData != null) { - - if (userData.contains("%")) { - try { - userData = URLDecoder.decode(userData, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new InvalidParameterValueException("Url decoding of userdata failed."); - } - } - - if (!Base64.isBase64(userData)) { - throw new InvalidParameterValueException("User data is not base64 encoded"); - } - // If GET, use 4K. If POST, support up to 1M. - if (httpmethod.equals(HTTPMethod.GET)) { - if (userData.length() >= MAX_HTTP_GET_LENGTH) { - throw new InvalidParameterValueException("User data is too long for an http GET request"); - } - if (userData.length() > VM_USERDATA_MAX_LENGTH.value()) { - throw new InvalidParameterValueException("User data has exceeded configurable max length : " + VM_USERDATA_MAX_LENGTH.value()); - } - decodedUserData = Base64.decodeBase64(userData.getBytes()); - if (decodedUserData.length > MAX_HTTP_GET_LENGTH) { - throw new InvalidParameterValueException("User data is too long for GET request"); - } - } else if (httpmethod.equals(HTTPMethod.POST)) { - if (userData.length() >= MAX_HTTP_POST_LENGTH) { - throw new InvalidParameterValueException("User data is too long for an http POST request"); - } - if (userData.length() > VM_USERDATA_MAX_LENGTH.value()) { - throw new InvalidParameterValueException("User data has exceeded configurable max length : " + VM_USERDATA_MAX_LENGTH.value()); - } - decodedUserData = Base64.decodeBase64(userData.getBytes()); - if (decodedUserData.length > MAX_HTTP_POST_LENGTH) { - throw new InvalidParameterValueException("User data is too long for POST request"); - } - } - - if (decodedUserData == null || decodedUserData.length < 1) { - throw new InvalidParameterValueException("User data is too short"); - } - // Re-encode so that the '=' paddings are added if necessary since 'isBase64' does not require it, but python does on the VR. - return Base64.encodeBase64String(decodedUserData); - } - return null; - } - @Override @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "deploying Vm", async = true) public UserVm startVirtualMachine(DeployVMCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException, ConcurrentOperationException, ResourceAllocationException { @@ -5961,13 +5910,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } String userData = cmd.getUserData(); - userData = userDataManager.validateUserData(userData, cmd.getHttpMethod()); Long userDataId = cmd.getUserdataId(); String userDataDetails = null; if (MapUtils.isNotEmpty(cmd.getUserdataDetails())) { userDataDetails = cmd.getUserdataDetails().toString(); } userData = finalizeUserData(userData, userDataId, template); + userData = userDataManager.validateUserData(userData, cmd.getHttpMethod()); Account caller = CallContext.current().getCallingAccount(); Long callerId = caller.getId(); diff --git a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java index 08070faf92c..780880f2c01 100644 --- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java @@ -61,6 +61,7 @@ import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.userdata.UserDataManager; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -195,6 +196,9 @@ public class AutoScaleManagerImplTest { @Mock UserVmManager userVmMgr; + @Mock + UserDataManager userDataMgr; + @Mock EntityManager entityManager; @@ -412,7 +416,7 @@ public class AutoScaleManagerImplTest { userDataDetails.put("0", new HashMap<>() {{ put("key1", "value1"); put("key2", "value2"); }}); Mockito.doReturn(userDataFinal).when(userVmMgr).finalizeUserData(any(), any(), any()); - Mockito.doReturn(userDataFinal).when(userVmMgr).validateUserData(eq(userDataFinal), nullable(BaseCmd.HTTPMethod.class)); + Mockito.doReturn(userDataFinal).when(userDataMgr).validateUserData(eq(userDataFinal), nullable(BaseCmd.HTTPMethod.class)); } @After @@ -767,7 +771,7 @@ public class AutoScaleManagerImplTest { Mockito.verify(autoScaleVmProfileDao).persist(Mockito.any()); Mockito.verify(userVmMgr).finalizeUserData(any(), any(), any()); - Mockito.verify(userVmMgr).validateUserData(eq(userDataFinal), nullable(BaseCmd.HTTPMethod.class)); + Mockito.verify(userDataMgr).validateUserData(eq(userDataFinal), nullable(BaseCmd.HTTPMethod.class)); } @Test(expected = InvalidParameterValueException.class) @@ -831,7 +835,7 @@ public class AutoScaleManagerImplTest { Mockito.verify(autoScaleVmProfileDao).persist(Mockito.any()); Mockito.verify(userVmMgr).finalizeUserData(any(), any(), any()); - Mockito.verify(userVmMgr).validateUserData(eq(userDataFinal), nullable(BaseCmd.HTTPMethod.class)); + Mockito.verify(userDataMgr).validateUserData(eq(userDataFinal), nullable(BaseCmd.HTTPMethod.class)); } @Test diff --git a/ui/src/views/compute/RegisterUserData.vue b/ui/src/views/compute/RegisterUserData.vue index a8e5577d005..9c677b00499 100644 --- a/ui/src/views/compute/RegisterUserData.vue +++ b/ui/src/views/compute/RegisterUserData.vue @@ -208,7 +208,7 @@ export default { params.params = userdataparams } - api('registerUserData', params).then(json => { + api('registerUserData', {}, 'POST', params).then(json => { this.$message.success(this.$t('message.success.register.user.data') + ' ' + values.name) }).catch(error => { this.$notifyError(error) From 8f88103a293ef4072b84769f608118b7048bbb82 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 19 Jun 2024 12:59:50 +0530 Subject: [PATCH 05/16] FR72 - api,server: purge expunged resources (#405) This PR introduces the functionality of purging removed DB entries for CloudStack entities (currently only for VirtualMachine). There would be three mechanisms for purging removed resources: - Background task - CloudStack will run a background task which runs at a defined interval. Other parameters for this task can be controlled with new global settings. - API - New API `purgeExpungedResources`. It will allow passing the following parameters - resourcetype, batchsize, startdate, enddate - Config for service offering. Service offerings can be created with purgeresources parameter which would allow purging resources immediately on expunge. Following new global settings have been added: - `expunged.resources.purge.enabled`: Default: false. Whether to run a background task to purge the DB records of the expunged resources. - `expunged.resources.purge.resources`: Default: (empty). A comma-separated list of resource types that will be considered by the background task to purge the DB records of the expunged resources. Currently only VirtualMachine is supported. An empty value will result in considering all resource types for purging. - `expunged.resources.purge.interval`: Default: 86400. Interval (in seconds) for the background task to purge the DB records of the expunged resources. - `expunged.resources.purge.delay`: Default: 300. Initial delay (in seconds) to start the background task to purge the DB records of the expunged resources task. - `expunged.resources.purge.batch.size`: Default: 50. Batch size to be used during purging of the DB records of the expunged resources. - `expunged.resources.purge.start.time`: Default: (empty). Start time to be used by the background task to purge the DB records of the expunged resources. Use format `yyyy-MM-dd` or `yyyy-MM-dd HH:mm:ss`. - `expunged.resources.purge.keep.past.days`: Default: 30. The number of days in the past from the execution time of the background task to purge the DB records of the expunged resources for which the expunged resources must not be purged. To enable purging DB records of the expunged resource till the execution of the background task, set the value to zero. - `expunged.resource.purge.job.delay`: Default: 180. Delay (in seconds) to execute the purging of the DB records of an expunged resource initiated by the configuration in the offering. Minimum value should be 180 seconds and if a lower value is set then the minimum value will be used. Upstream PRs: https://github.com/apache/cloudstack/pull/8999 https://github.com/apache/cloudstack-documentation/pull/397 Signed-off-by: Abhishek Kumar Co-authored-by: Suresh Kumar Anaparti --- .github/workflows/ci.yml | 3 +- .../main/java/com/cloud/event/EventTypes.java | 2 + .../element/LoadBalancingServiceProvider.java | 3 + .../com/cloud/offering/ServiceOffering.java | 3 + .../apache/cloudstack/api/ApiConstants.java | 2 + .../offering/CreateServiceOfferingCmd.java | 14 +- .../offering/UpdateServiceOfferingCmd.java | 9 + .../resource/PurgeExpungedResourcesCmd.java | 131 +++ .../PurgeExpungedResourcesResponse.java | 39 + .../api/response/ServiceOfferingResponse.java | 8 + .../resource/ResourceCleanupService.java | 74 ++ .../CreateServiceOfferingCmdTest.java | 51 ++ .../UpdateServiceOfferingCmdTest.java | 51 ++ .../PurgeExpungedResourcesCmdTest.java | 104 +++ .../service/NetworkOrchestrationService.java | 2 + .../com/cloud/ha/HighAvailabilityManager.java | 1 + .../cloud/vm/VirtualMachineManagerImpl.java | 4 + .../orchestration/NetworkOrchestrator.java | 13 + .../as/dao/AutoScaleVmGroupVmMapDao.java | 2 + .../as/dao/AutoScaleVmGroupVmMapDaoImpl.java | 18 +- .../com/cloud/network/dao/IPAddressDao.java | 1 + .../cloud/network/dao/IPAddressDaoImpl.java | 13 + .../dao/InlineLoadBalancerNicMapDao.java | 4 + .../dao/InlineLoadBalancerNicMapDaoImpl.java | 15 + .../network/dao/LoadBalancerVMMapDao.java | 1 + .../network/dao/LoadBalancerVMMapDaoImpl.java | 15 +- .../dao/OpRouterMonitorServiceDao.java | 4 + .../dao/OpRouterMonitorServiceDaoImpl.java | 19 +- .../rules/dao/PortForwardingRulesDao.java | 1 + .../rules/dao/PortForwardingRulesDaoImpl.java | 13 + .../cloud/secstorage/CommandExecLogDao.java | 2 + .../secstorage/CommandExecLogDaoImpl.java | 13 + .../com/cloud/storage/dao/SnapshotDao.java | 1 + .../cloud/storage/dao/SnapshotDaoImpl.java | 14 + .../java/com/cloud/storage/dao/VolumeDao.java | 1 + .../com/cloud/storage/dao/VolumeDaoImpl.java | 21 +- .../src/main/java/com/cloud/vm/ItWorkDao.java | 1 + .../main/java/com/cloud/vm/ItWorkDaoImpl.java | 14 +- .../com/cloud/vm/dao/ConsoleSessionDao.java | 3 + .../cloud/vm/dao/ConsoleSessionDaoImpl.java | 21 +- .../main/java/com/cloud/vm/dao/NicDao.java | 1 + .../java/com/cloud/vm/dao/NicDaoImpl.java | 16 + .../cloud/vm/dao/NicExtraDhcpOptionDao.java | 1 + .../vm/dao/NicExtraDhcpOptionDaoImpl.java | 17 +- .../com/cloud/vm/dao/NicSecondaryIpDao.java | 1 + .../cloud/vm/dao/NicSecondaryIpDaoImpl.java | 13 + .../java/com/cloud/vm/dao/VMInstanceDao.java | 3 + .../com/cloud/vm/dao/VMInstanceDaoImpl.java | 24 + .../cloud/vm/snapshot/dao/VMSnapshotDao.java | 2 + .../vm/snapshot/dao/VMSnapshotDaoImpl.java | 29 +- .../resourcedetail/ResourceDetailsDao.java | 2 + .../ResourceDetailsDaoBase.java | 16 +- .../datastore/db/SnapshotDataStoreDao.java | 2 + .../db/SnapshotDataStoreDaoImpl.java | 12 + .../datastore/db/VolumeDataStoreDao.java | 1 + .../as/dao/AutoScaleVmGroupDaoImplTest.java | 10 +- .../dao/AutoScaleVmGroupVmMapDaoImplTest.java | 45 +- .../network/dao/IPAddressDaoImplTest.java | 67 ++ .../InlineLoadBalancerNicMapDaoImplTest.java | 67 ++ .../dao/LoadBalancerVMMapDaoImplTest.java | 67 ++ .../OpRouterMonitorServiceDaoImplTest.java | 67 ++ .../dao/PortForwardingRulesDaoImplTest.java | 68 ++ .../secstorage/CommandExecLogDaoImplTest.java | 67 ++ .../dao/StoragePoolTagsDaoImplTest.java | 18 +- .../cloud/storage/dao/VolumeDaoImplTest.java | 40 + .../java/com/cloud/vm/ItWorkDaoImplTest.java | 67 ++ .../vm/dao/ConsoleSessionDaoImplTest.java | 68 ++ .../java/com/cloud/vm/dao/NicDaoImplTest.java | 69 ++ .../vm/dao/NicExtraDhcpOptionDaoImplTest.java | 68 ++ .../vm/dao/NicSecondaryIpDaoImplTest.java | 67 ++ .../cloud/vm/dao/VMInstanceDaoImplTest.java | 31 + .../snapshot/dao/VMSnapshotDaoImplTest.java | 69 ++ .../db/SnapshotDataStoreDaoImplTest.java | 67 ++ .../image/db/VolumeDataStoreDaoImpl.java | 21 +- .../image/db/VolumeDataStoreDaoImplTest.java | 68 ++ .../java/com/cloud/utils/db/GenericDao.java | 18 + .../com/cloud/utils/db/GenericDaoBase.java | 53 +- .../framework/jobs/dao/VmWorkJobDao.java | 1 + .../framework/jobs/dao/VmWorkJobDaoImpl.java | 16 +- .../jobs/dao/VmWorkJobDaoImplTest.java | 68 ++ .../element/ElasticLoadBalancerElement.java | 4 + .../lb/ElasticLoadBalancerManager.java | 2 + .../lb/ElasticLoadBalancerManagerImpl.java | 4 + .../network/lb/dao/ElasticLbVmMapDao.java | 2 + .../network/lb/dao/ElasticLbVmMapDaoImpl.java | 13 + .../query/dao/ServiceOfferingJoinDaoImpl.java | 5 + .../ConfigurationManagerImpl.java | 45 +- .../cloud/ha/HighAvailabilityManagerImpl.java | 5 + .../com/cloud/ha/dao/HighAvailabilityDao.java | 1 + .../cloud/ha/dao/HighAvailabilityDaoImpl.java | 14 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 2 +- .../resource/ResourceCleanupServiceImpl.java | 829 ++++++++++++++++++ .../spring-server-core-managers-context.xml | 2 + .../ha/dao/HighAvailabilityDaoImplTest.java | 68 ++ .../com/cloud/user/MockUsageEventDao.java | 15 + .../com/cloud/vpc/MockNetworkManagerImpl.java | 4 + .../ResourceCleanupServiceImplTest.java | 656 ++++++++++++++ .../smoke/test_purge_expunged_vms.py | 364 ++++++++ tools/apidoc/gen_toc.py | 3 +- ui/public/locales/en.json | 1 + ui/src/config/section/offering.js | 2 +- ui/src/views/offering/AddComputeOffering.vue | 9 +- 102 files changed, 4099 insertions(+), 69 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/PurgeExpungedResourcesResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/resource/ResourceCleanupService.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmdTest.java create mode 100644 api/src/test/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmdTest.java create mode 100644 engine/schema/src/test/java/com/cloud/network/dao/IPAddressDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/network/dao/InlineLoadBalancerNicMapDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/network/dao/LoadBalancerVMMapDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/network/dao/OpRouterMonitorServiceDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/network/rules/dao/PortForwardingRulesDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/secstorage/CommandExecLogDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/vm/ItWorkDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/vm/dao/ConsoleSessionDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/vm/dao/NicDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/vm/dao/NicExtraDhcpOptionDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/vm/dao/NicSecondaryIpDaoImplTest.java create mode 100644 engine/schema/src/test/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImplTest.java create mode 100644 engine/schema/src/test/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImplTest.java create mode 100644 engine/storage/src/test/java/org/apache/cloudstack/storage/image/db/VolumeDataStoreDaoImplTest.java create mode 100644 framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDaoImplTest.java create mode 100644 server/src/main/java/org/apache/cloudstack/resource/ResourceCleanupServiceImpl.java create mode 100644 server/src/test/java/com/cloud/ha/dao/HighAvailabilityDaoImplTest.java create mode 100644 server/src/test/java/org/apache/cloudstack/resource/ResourceCleanupServiceImplTest.java create mode 100644 test/integration/smoke/test_purge_expunged_vms.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43baf5b1631..7196c4aed21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,8 @@ jobs: smoke/test_metrics_api smoke/test_migration smoke/test_multipleips_per_nic - smoke/test_nested_virtualization", + smoke/test_nested_virtualization + smoke/test_purge_expunged_vms", "smoke/test_network smoke/test_network_acl smoke/test_network_ipv6 diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 2d945575b37..c8642a9e94e 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -693,6 +693,8 @@ public class EventTypes { // SystemVM public static final String EVENT_LIVE_PATCH_SYSTEMVM = "LIVE.PATCH.SYSTEM.VM"; + //Purge resources + public static final String EVENT_PURGE_EXPUNGED_RESOURCES = "PURGE.EXPUNGED.RESOURCES"; static { diff --git a/api/src/main/java/com/cloud/network/element/LoadBalancingServiceProvider.java b/api/src/main/java/com/cloud/network/element/LoadBalancingServiceProvider.java index 1bb37be970d..dc0f60f4519 100644 --- a/api/src/main/java/com/cloud/network/element/LoadBalancingServiceProvider.java +++ b/api/src/main/java/com/cloud/network/element/LoadBalancingServiceProvider.java @@ -48,4 +48,7 @@ public interface LoadBalancingServiceProvider extends NetworkElement, IpDeployin List updateHealthChecks(Network network, List lbrules); boolean handlesOnlyRulesInTransitionState(); + + default void expungeLbVmRefs(List vmIds, Long batchSize) { + } } diff --git a/api/src/main/java/com/cloud/offering/ServiceOffering.java b/api/src/main/java/com/cloud/offering/ServiceOffering.java index 58c7b0dbaf9..acb7a9f1cf9 100644 --- a/api/src/main/java/com/cloud/offering/ServiceOffering.java +++ b/api/src/main/java/com/cloud/offering/ServiceOffering.java @@ -33,6 +33,9 @@ public interface ServiceOffering extends InfrastructureEntity, InternalIdentity, static final String internalLbVmDefaultOffUniqueName = "Cloud.Com-InternalLBVm"; // leaving cloud.com references as these are identifyers and no real world addresses (check against DB) + + static final String PURGE_DB_ENTITIES_KEY = "purge.db.entities"; + enum State { Inactive, Active, } 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 e6db3fc8da0..ff57e76548e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -359,6 +359,7 @@ public class ApiConstants { public static final String PUBLIC_START_PORT = "publicport"; public static final String PUBLIC_END_PORT = "publicendport"; public static final String PUBLIC_ZONE = "publiczone"; + public static final String PURGE_RESOURCES = "purgeresources"; public static final String RECEIVED_BYTES = "receivedbytes"; public static final String RECONNECT = "reconnect"; public static final String RECOVER = "recover"; @@ -849,6 +850,7 @@ public class ApiConstants { public static final String AUTOSCALE_VMGROUP_NAME = "autoscalevmgroupname"; public static final String BAREMETAL_DISCOVER_NAME = "baremetaldiscovername"; public static final String BAREMETAL_RCT_URL = "baremetalrcturl"; + public static final String BATCH_SIZE = "batchsize"; public static final String UCS_DN = "ucsdn"; public static final String GSLB_PROVIDER = "gslbprovider"; public static final String EXCLUSIVE_GSLB_PROVIDER = "isexclusivegslbprovider"; 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 24f5682a218..641ecd04a4d 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 @@ -244,6 +244,12 @@ public class CreateServiceOfferingCmd extends BaseCmd { @Parameter(name = ApiConstants.ENCRYPT_ROOT, type = CommandType.BOOLEAN, description = "VMs using this offering require root volume encryption", since="4.18") private Boolean encryptRoot; + @Parameter(name = ApiConstants.PURGE_RESOURCES, type = CommandType.BOOLEAN, + description = "Whether to cleanup instance and its associated resource from database upon expunge of the instance", + since="4.18.1") + private Boolean purgeResources; + + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -259,7 +265,7 @@ public class CreateServiceOfferingCmd extends BaseCmd { public String getDisplayText() { if (StringUtils.isEmpty(displayText)) { - throw new InvalidParameterValueException("Failed to create service offering because the offering display text has not been spified."); + throw new InvalidParameterValueException("Failed to create service offering because the offering display text has not been specified."); } return displayText; } @@ -274,7 +280,7 @@ public class CreateServiceOfferingCmd extends BaseCmd { public String getServiceOfferingName() { if (StringUtils.isEmpty(serviceOfferingName)) { - throw new InvalidParameterValueException("Failed to create service offering because offering name has not been spified."); + throw new InvalidParameterValueException("Failed to create service offering because offering name has not been specified."); } return serviceOfferingName; } @@ -482,6 +488,10 @@ public class CreateServiceOfferingCmd extends BaseCmd { return false; } + public boolean isPurgeResources() { + return Boolean.TRUE.equals(purgeResources); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java index d86564a60c6..01b060837c9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java @@ -84,6 +84,11 @@ public class UpdateServiceOfferingCmd extends BaseCmd { since = "4.16") private String hostTags; + @Parameter(name = ApiConstants.PURGE_RESOURCES, type = CommandType.BOOLEAN, + description = "Whether to cleanup VM and its associated resource upon expunge", + since="4.18.1") + private Boolean purgeResources; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -172,6 +177,10 @@ public class UpdateServiceOfferingCmd extends BaseCmd { return hostTags; } + public boolean isPurgeResources() { + return Boolean.TRUE.equals(purgeResources); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmd.java new file mode 100644 index 00000000000..c8eac24ffb5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmd.java @@ -0,0 +1,131 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.resource; + + +import java.util.Date; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.PurgeExpungedResourcesResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.resource.ResourceCleanupService; + +import com.cloud.event.EventTypes; + +@APICommand(name = "purgeExpungedResources", + description = "Purge expunged resources", + responseObject = SuccessResponse.class, + responseView = ResponseObject.ResponseView.Full, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin}, + since = "4.18.1") +public class PurgeExpungedResourcesCmd extends BaseAsyncCmd { + + @Inject + ResourceCleanupService resourceCleanupService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = BaseCmd.CommandType.STRING, + description = "The type of the resource which need to be purged. Supported types: " + + "VirtualMachine") + private String resourceType; + + @Parameter(name = ApiConstants.BATCH_SIZE, type = CommandType.LONG, + description = "The size of batch used during purging") + private Long batchSize; + + @Parameter(name = ApiConstants.START_DATE, + type = CommandType.DATE, + description = "The start date range of the expunged resources used for purging " + + "(use format \"yyyy-MM-dd\" or \"yyyy-MM-dd HH:mm:ss\")") + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, + type = CommandType.DATE, + description = "The end date range of the expunged resources used for purging " + + "(use format \"yyyy-MM-dd\" or \"yyyy-MM-dd HH:mm:ss\")") + private Date endDate; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + + public String getResourceType() { + return resourceType; + } + + public Long getBatchSize() { + return batchSize; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_PURGE_EXPUNGED_RESOURCES; + } + + @Override + public String getEventDescription() { + return "Purging expunged resources"; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + long result = resourceCleanupService.purgeExpungedResources(this); + PurgeExpungedResourcesResponse response = new PurgeExpungedResourcesResponse(); + response.setResourceCount(result); + response.setObjectName(getCommandName().toLowerCase()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getLocalizedMessage()); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/PurgeExpungedResourcesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/PurgeExpungedResourcesResponse.java new file mode 100644 index 00000000000..3807d0d5b16 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/PurgeExpungedResourcesResponse.java @@ -0,0 +1,39 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class PurgeExpungedResourcesResponse extends BaseResponse { + + @SerializedName(ApiConstants.RESOURCE_COUNT) + @Param(description = "The count of the purged expunged resources") + private Long resourceCount; + + public Long getResourceCount() { + return resourceCount; + } + + public void setResourceCount(Long resourceCount) { + this.resourceCount = resourceCount; + } +} 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 53767adf17d..7e4150bbe7a 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 @@ -230,6 +230,10 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations { @Param(description = "true if virtual machine root disk will be encrypted on storage", since = "4.18") private Boolean encryptRoot; + @SerializedName(ApiConstants.PURGE_RESOURCES) + @Param(description = "Whether to cleanup VM and its associated resource upon expunge", since = "4.18.1") + private Boolean purgeResources; + public ServiceOfferingResponse() { } @@ -543,4 +547,8 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations { } public void setEncryptRoot(Boolean encrypt) { this.encryptRoot = encrypt; } + + public void setPurgeResources(Boolean purgeResources) { + this.purgeResources = purgeResources; + } } diff --git a/api/src/main/java/org/apache/cloudstack/resource/ResourceCleanupService.java b/api/src/main/java/org/apache/cloudstack/resource/ResourceCleanupService.java new file mode 100644 index 00000000000..0d72edb0748 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/resource/ResourceCleanupService.java @@ -0,0 +1,74 @@ +// 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.resource; + +import org.apache.cloudstack.api.command.admin.resource.PurgeExpungedResourcesCmd; +import org.apache.cloudstack.framework.config.ConfigKey; + +import com.cloud.vm.VirtualMachine; + +public interface ResourceCleanupService { + int MINIMUM_EXPUNGED_RESOURCE_PURGE_JOB_DELAY_IN_SECONDS = 3 * 60; + ConfigKey ExpungedResourcePurgeEnabled = new ConfigKey<>("Advanced", Boolean.class, + "expunged.resources.purge.enabled", "false", + "Whether to run a background task to purge the DB records of the expunged resources", + false, ConfigKey.Scope.Global); + ConfigKey ExpungedResourcePurgeResources = new ConfigKey<>("Advanced", String.class, + "expunged.resources.purge.resources", "", + "A comma-separated list of resource types that will be considered by the background task " + + "to purge the DB records of the expunged resources. Currently only VirtualMachine is supported. " + + "An empty value will result in considering all resource types for purging", + false, ConfigKey.Scope.Global); + ConfigKey ExpungedResourcesPurgeInterval = new ConfigKey<>("Advanced", Integer.class, + "expunged.resources.purge.interval", "86400", + "Interval (in seconds) for the background task to purge the DB records of the expunged resources", + false); + ConfigKey ExpungedResourcesPurgeDelay = new ConfigKey<>("Advanced", Integer.class, + "expunged.resources.purge.delay", "300", + "Initial delay (in seconds) to start the background task to purge the DB records of the " + + "expunged resources task", false); + ConfigKey ExpungedResourcesPurgeBatchSize = new ConfigKey<>("Advanced", Integer.class, + "expunged.resources.purge.batch.size", "50", + "Batch size to be used during purging of the DB records of the expunged resources", + true); + ConfigKey ExpungedResourcesPurgeStartTime = new ConfigKey<>("Advanced", String.class, + "expunged.resources.purge.start.time", "", + "Start time to be used by the background task to purge the DB records of the expunged " + + "resources. Use format \"yyyy-MM-dd\" or \"yyyy-MM-dd HH:mm:ss\"", true); + ConfigKey ExpungedResourcesPurgeKeepPastDays = new ConfigKey<>("Advanced", Integer.class, + "expunged.resources.purge.keep.past.days", "30", + "The number of days in the past from the execution time of the background task to purge " + + "the DB records of the expunged resources for which the expunged resources must not be purged. " + + "To enable purging DB records of the expunged resource till the execution of the background " + + "task, set the value to zero.", true); + ConfigKey ExpungedResourcePurgeJobDelay = new ConfigKey<>("Advanced", Integer.class, + "expunged.resource.purge.job.delay", + String.valueOf(MINIMUM_EXPUNGED_RESOURCE_PURGE_JOB_DELAY_IN_SECONDS), + String.format("Delay (in seconds) to execute the purging of the DB records of an expunged resource " + + "initiated by the configuration in the offering. Minimum value should be %d seconds " + + "and if a lower value is set then the minimum value will be used", + MINIMUM_EXPUNGED_RESOURCE_PURGE_JOB_DELAY_IN_SECONDS), + true); + + enum ResourceType { + VirtualMachine + } + + long purgeExpungedResources(PurgeExpungedResourcesCmd cmd); + void purgeExpungedVmResourcesLaterIfNeeded(VirtualMachine vm); +} 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 new file mode 100644 index 00000000000..f9e42e97228 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java @@ -0,0 +1,51 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.offering; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(MockitoJUnitRunner.class) +public class CreateServiceOfferingCmdTest { + + @InjectMocks + private CreateServiceOfferingCmd createServiceOfferingCmd; + + @Test + public void testIsPurgeResourcesNoOrNullValue() { + Assert.assertFalse(createServiceOfferingCmd.isPurgeResources()); + ReflectionTestUtils.setField(createServiceOfferingCmd, "purgeResources", false); + Assert.assertFalse(createServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testIsPurgeResourcesFalse() { + ReflectionTestUtils.setField(createServiceOfferingCmd, "purgeResources", false); + Assert.assertFalse(createServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testIsPurgeResourcesTrue() { + ReflectionTestUtils.setField(createServiceOfferingCmd, "purgeResources", true); + Assert.assertTrue(createServiceOfferingCmd.isPurgeResources()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmdTest.java new file mode 100644 index 00000000000..1bb2be041e1 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmdTest.java @@ -0,0 +1,51 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.offering; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(MockitoJUnitRunner.class) +public class UpdateServiceOfferingCmdTest { + + @InjectMocks + private UpdateServiceOfferingCmd updateServiceOfferingCmd; + + @Test + public void testIsPurgeResourcesNoOrNullValue() { + Assert.assertFalse(updateServiceOfferingCmd.isPurgeResources()); + ReflectionTestUtils.setField(updateServiceOfferingCmd, "purgeResources", false); + Assert.assertFalse(updateServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testIsPurgeResourcesFalse() { + ReflectionTestUtils.setField(updateServiceOfferingCmd, "purgeResources", false); + Assert.assertFalse(updateServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testIsPurgeResourcesTrue() { + ReflectionTestUtils.setField(updateServiceOfferingCmd, "purgeResources", true); + Assert.assertTrue(updateServiceOfferingCmd.isPurgeResources()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmdTest.java new file mode 100644 index 00000000000..a628f13275c --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmdTest.java @@ -0,0 +1,104 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.resource; + +import static org.junit.Assert.assertNull; + +import java.util.Date; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.PurgeExpungedResourcesResponse; +import org.apache.cloudstack.resource.ResourceCleanupService; +import org.junit.Assert; +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.mockito.stubbing.Answer; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.utils.exception.CloudRuntimeException; + +@RunWith(MockitoJUnitRunner.class) +public class PurgeExpungedResourcesCmdTest { + @Mock + ResourceCleanupService resourceCleanupService; + + @Spy + @InjectMocks + PurgeExpungedResourcesCmd spyCmd = Mockito.spy(new PurgeExpungedResourcesCmd()); + + @Test + public void testGetResourceType() { + PurgeExpungedResourcesCmd cmd = new PurgeExpungedResourcesCmd(); + assertNull(cmd.getResourceType()); + ReflectionTestUtils.setField(cmd, "resourceType", ResourceCleanupService.ResourceType.VirtualMachine.toString()); + Assert.assertEquals(ResourceCleanupService.ResourceType.VirtualMachine.toString(), cmd.getResourceType()); + } + + @Test + public void testGetBatchSize() { + PurgeExpungedResourcesCmd cmd = new PurgeExpungedResourcesCmd(); + assertNull(cmd.getBatchSize()); + Long batchSize = 100L; + ReflectionTestUtils.setField(cmd, "batchSize", batchSize); + Assert.assertEquals(batchSize, cmd.getBatchSize()); + } + + @Test + public void testGetStartDate() { + PurgeExpungedResourcesCmd cmd = new PurgeExpungedResourcesCmd(); + assertNull(cmd.getStartDate()); + Date date = new Date(); + ReflectionTestUtils.setField(cmd, "startDate", date); + Assert.assertEquals(date, cmd.getStartDate()); + } + + @Test + public void testGetEndDate() { + PurgeExpungedResourcesCmd cmd = new PurgeExpungedResourcesCmd(); + assertNull(cmd.getEndDate()); + Date date = new Date(); + ReflectionTestUtils.setField(cmd, "endDate", date); + Assert.assertEquals(date, cmd.getEndDate()); + } + + @Test + public void testExecute() { + final PurgeExpungedResourcesResponse[] executeResponse = new PurgeExpungedResourcesResponse[1]; + Long result = 100L; + Mockito.when(resourceCleanupService.purgeExpungedResources(Mockito.any())).thenReturn(result); + Mockito.doAnswer((Answer) invocation -> { + executeResponse[0] = (PurgeExpungedResourcesResponse)invocation.getArguments()[0]; + return null; + }).when(spyCmd).setResponseObject(Mockito.any()); + spyCmd.execute(); + PurgeExpungedResourcesResponse response = executeResponse[0]; + Assert.assertNotNull(response); + Assert.assertEquals(result, response.getResourceCount()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteException() { + Mockito.doThrow(CloudRuntimeException.class).when(resourceCleanupService).purgeExpungedResources(Mockito.any()); + spyCmd.execute(); + } +} diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 08c2546e6d5..903a0d3e80d 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -344,4 +344,6 @@ public interface NetworkOrchestrationService { Pair importNic(final String macAddress, int deviceId, final Network network, final Boolean isDefaultNic, final VirtualMachine vm, final Network.IpAddresses ipAddresses, boolean forced) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException; void unmanageNics(VirtualMachineProfile vm); + + void expungeLbVmRefs(List vmIds, Long batchSize); } diff --git a/engine/components-api/src/main/java/com/cloud/ha/HighAvailabilityManager.java b/engine/components-api/src/main/java/com/cloud/ha/HighAvailabilityManager.java index 1dd999dad97..da36980d09c 100644 --- a/engine/components-api/src/main/java/com/cloud/ha/HighAvailabilityManager.java +++ b/engine/components-api/src/main/java/com/cloud/ha/HighAvailabilityManager.java @@ -153,4 +153,5 @@ public interface HighAvailabilityManager extends Manager { String getHaTag(); DeploymentPlanner getHAPlanner(); + int expungeWorkItemsByVmList(List vmIds, Long batchSize); } diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 62eb1dfca47..e51a1d7fa97 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -83,6 +83,7 @@ import org.apache.cloudstack.framework.messagebus.MessageHandler; import org.apache.cloudstack.jobs.JobInfo; import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.resource.ResourceCleanupService; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -389,6 +390,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac private DomainRouterJoinDao domainRouterJoinDao; @Inject private AnnotationDao annotationDao; + @Inject + ResourceCleanupService resourceCleanupService; VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); @@ -698,6 +701,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac if (s_logger.isDebugEnabled()) { s_logger.debug("Expunged " + vm); } + resourceCleanupService.purgeExpungedVmResourcesLaterIfNeeded(vm); } protected void handleUnsuccessfulCommands(Commands cmds, VMInstanceVO vm) throws CloudRuntimeException { diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 18672c7620f..cc68b3a9e1c 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -4666,6 +4666,19 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra } } + @Override + public void expungeLbVmRefs(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(networkElements) || CollectionUtils.isEmpty(vmIds)) { + return; + } + for (NetworkElement element : networkElements) { + if (element instanceof LoadBalancingServiceProvider) { + LoadBalancingServiceProvider lbProvider = (LoadBalancingServiceProvider)element; + lbProvider.expungeLbVmRefs(vmIds, batchSize); + } + } + } + @Override public String getConfigComponentName() { return NetworkOrchestrationService.class.getSimpleName(); diff --git a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDao.java b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDao.java index 4b25c63403e..718511746c2 100644 --- a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDao.java +++ b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDao.java @@ -35,4 +35,6 @@ public interface AutoScaleVmGroupVmMapDao extends GenericDao vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java index 8fca4c26f9a..1ae55d97da2 100644 --- a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java @@ -18,7 +18,10 @@ package com.cloud.network.as.dao; import java.util.List; +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.network.as.AutoScaleVmGroupVmMapVO; @@ -31,9 +34,6 @@ import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.VMInstanceDao; -import javax.annotation.PostConstruct; -import javax.inject.Inject; - @Component public class AutoScaleVmGroupVmMapDaoImpl extends GenericDaoBase implements AutoScaleVmGroupVmMapDao { @@ -115,4 +115,16 @@ public class AutoScaleVmGroupVmMapDaoImpl extends GenericDaoBase= 0; } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getInstanceId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDao.java b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDao.java index 51dfa91a404..bb2a0c1f67a 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDao.java @@ -100,4 +100,5 @@ public interface IPAddressDao extends GenericDao { List listByDcIdAndAssociatedNetwork(long dcId); List listByNetworkId(long networkId); + int expungeByVmList(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java index b995959f01e..412e87ea956 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java @@ -25,6 +25,7 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; import org.apache.cloudstack.resourcedetail.dao.UserIpAddressDetailsDao; +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -534,4 +535,16 @@ public class IPAddressDaoImpl extends GenericDaoBase implemen sc.setParameters("state", State.Allocated); return listBy(sc); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getAssociatedWithVmId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/InlineLoadBalancerNicMapDao.java b/engine/schema/src/main/java/com/cloud/network/dao/InlineLoadBalancerNicMapDao.java index ac3845beffe..b1831b407a4 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/InlineLoadBalancerNicMapDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/InlineLoadBalancerNicMapDao.java @@ -16,10 +16,14 @@ // under the License. package com.cloud.network.dao; +import java.util.List; + import com.cloud.utils.db.GenericDao; public interface InlineLoadBalancerNicMapDao extends GenericDao { InlineLoadBalancerNicMapVO findByPublicIpAddress(String publicIpAddress); InlineLoadBalancerNicMapVO findByNicId(long nicId); + int expungeByNicList(List nicIds, Long batchSize); + } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/InlineLoadBalancerNicMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/InlineLoadBalancerNicMapDaoImpl.java index 1c3f231f9c1..d64ba8b4155 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/InlineLoadBalancerNicMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/InlineLoadBalancerNicMapDaoImpl.java @@ -17,9 +17,13 @@ package com.cloud.network.dao; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @Component @@ -41,4 +45,15 @@ public class InlineLoadBalancerNicMapDaoImpl extends GenericDaoBase nicIds, Long batchSize) { + if (CollectionUtils.isEmpty(nicIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("nicIds", sb.entity().getNicId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("nicIds", nicIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDao.java b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDao.java index a25534b7010..be2941d5cb2 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDao.java @@ -42,4 +42,5 @@ public interface LoadBalancerVMMapDao extends GenericDao vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java index b32320a84cb..dc37cdeefe3 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java @@ -18,11 +18,12 @@ package com.cloud.network.dao; import java.util.List; - +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Func; @@ -135,4 +136,16 @@ public class LoadBalancerVMMapDaoImpl extends GenericDaoBase vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getInstanceId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/OpRouterMonitorServiceDao.java b/engine/schema/src/main/java/com/cloud/network/dao/OpRouterMonitorServiceDao.java index ebc0f1af227..0516e26e13a 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/OpRouterMonitorServiceDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/OpRouterMonitorServiceDao.java @@ -18,8 +18,12 @@ package com.cloud.network.dao; +import java.util.List; + import com.cloud.utils.db.GenericDao; public interface OpRouterMonitorServiceDao extends GenericDao { + int expungeByVmList(List vmIds, Long batchSize); + } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/OpRouterMonitorServiceDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/OpRouterMonitorServiceDaoImpl.java index 451320ac9b6..a8e818cfb18 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/OpRouterMonitorServiceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/OpRouterMonitorServiceDaoImpl.java @@ -17,10 +17,27 @@ package com.cloud.network.dao; -import com.cloud.utils.db.GenericDaoBase; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + @Component public class OpRouterMonitorServiceDaoImpl extends GenericDaoBase implements OpRouterMonitorServiceDao { + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/rules/dao/PortForwardingRulesDao.java b/engine/schema/src/main/java/com/cloud/network/rules/dao/PortForwardingRulesDao.java index b89d04ad15a..8cd114b7fc4 100644 --- a/engine/schema/src/main/java/com/cloud/network/rules/dao/PortForwardingRulesDao.java +++ b/engine/schema/src/main/java/com/cloud/network/rules/dao/PortForwardingRulesDao.java @@ -47,4 +47,5 @@ public interface PortForwardingRulesDao extends GenericDao listByNetworkAndDestIpAddr(String ip4Address, long networkId); + int expungeByVmList(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/network/rules/dao/PortForwardingRulesDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/rules/dao/PortForwardingRulesDaoImpl.java index 29cba516d72..3a404b3f2df 100644 --- a/engine/schema/src/main/java/com/cloud/network/rules/dao/PortForwardingRulesDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/rules/dao/PortForwardingRulesDaoImpl.java @@ -20,6 +20,7 @@ import java.util.List; import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.network.dao.FirewallRulesCidrsDao; @@ -170,4 +171,16 @@ public class PortForwardingRulesDaoImpl extends GenericDaoBase vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getVirtualMachineId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDao.java b/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDao.java index 98fc8c8687b..5023aaa3794 100644 --- a/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDao.java +++ b/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDao.java @@ -17,10 +17,12 @@ package com.cloud.secstorage; import java.util.Date; +import java.util.List; import com.cloud.utils.db.GenericDao; public interface CommandExecLogDao extends GenericDao { public void expungeExpiredRecords(Date cutTime); public Integer getCopyCmdCountForSSVM(Long id); + int expungeByVmList(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDaoImpl.java b/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDaoImpl.java index f89a1bbf4cc..a37acdf6029 100644 --- a/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDaoImpl.java @@ -19,6 +19,7 @@ package com.cloud.secstorage; import java.util.Date; import java.util.List; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.utils.db.GenericDaoBase; @@ -57,4 +58,16 @@ public class CommandExecLogDaoImpl extends GenericDaoBase copyCmds = customSearch(sc, null); return copyCmds.size(); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getInstanceId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java index 998d0bbd724..171634fb104 100755 --- a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java @@ -57,4 +57,5 @@ public interface SnapshotDao extends GenericDao, StateDao listByIds(Object... ids); + List searchByVolumes(List volumeIds); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java index f5ebf4bcf3d..26368e2a3bb 100755 --- a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java @@ -18,11 +18,13 @@ package com.cloud.storage.dao; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.util.ArrayList; import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -287,4 +289,16 @@ public class SnapshotDaoImpl extends GenericDaoBase implements sc.setParameters("status", (Object[]) status); return listBy(sc, null); } + + @Override + public List searchByVolumes(List volumeIds) { + if (CollectionUtils.isEmpty(volumeIds)) { + return new ArrayList<>(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("volumeIds", sb.entity().getVolumeId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("volumeIds", volumeIds.toArray()); + return search(sc, null); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index 64a5c8e07ba..e95209ee8b0 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -151,4 +151,5 @@ public interface VolumeDao extends GenericDao, StateDao listAllocatedVolumesForAccountDiskOfferingIdsAndNotForVms(long accountId, List diskOfferingIds, List vmIds); + List searchRemovedByVms(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 4e671cbdbd9..07359935a88 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -26,15 +26,13 @@ import java.util.stream.Collectors; import javax.inject.Inject; -import com.cloud.configuration.Resource; -import com.cloud.utils.db.Transaction; -import com.cloud.utils.db.TransactionCallback; import org.apache.cloudstack.reservation.ReservationVO; import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; +import com.cloud.configuration.Resource; import com.cloud.exception.InvalidParameterValueException; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.server.ResourceTag.ResourceObjectType; @@ -48,12 +46,15 @@ import com.cloud.storage.VolumeVO; import com.cloud.tags.dao.ResourceTagDao; import com.cloud.utils.Pair; import com.cloud.utils.db.DB; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.GenericSearchBuilder; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Func; import com.cloud.utils.db.SearchCriteria.Op; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.UpdateBuilder; import com.cloud.utils.exception.CloudRuntimeException; @@ -849,4 +850,18 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol return volume; }); } + + @Override + public List searchRemovedByVms(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return new ArrayList<>(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getInstanceId(), SearchCriteria.Op.IN); + sb.and("removed", sb.entity().getRemoved(), SearchCriteria.Op.NNULL); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + Filter filter = new Filter(VolumeVO.class, "id", true, 0L, batchSize); + return searchIncludingRemoved(sc, filter, null, false); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/ItWorkDao.java b/engine/schema/src/main/java/com/cloud/vm/ItWorkDao.java index 2d4a5e138fe..ab07d6989fa 100644 --- a/engine/schema/src/main/java/com/cloud/vm/ItWorkDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/ItWorkDao.java @@ -41,5 +41,6 @@ public interface ItWorkDao extends GenericDao { boolean updateStep(ItWorkVO work, Step step); List listWorkInProgressFor(long nodeId); + int expungeByVmList(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/vm/ItWorkDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/ItWorkDaoImpl.java index ff727904dcb..0cc0a084443 100644 --- a/engine/schema/src/main/java/com/cloud/vm/ItWorkDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/ItWorkDaoImpl.java @@ -18,7 +18,7 @@ package com.cloud.vm; import java.util.List; - +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.utils.db.GenericDaoBase; @@ -103,4 +103,16 @@ public class ItWorkDaoImpl extends GenericDaoBase implements I return search(sc, null); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getInstanceId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleSessionDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleSessionDao.java index 71b1aed1938..79158dd13b2 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleSessionDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleSessionDao.java @@ -23,6 +23,7 @@ import com.cloud.vm.ConsoleSessionVO; import com.cloud.utils.db.GenericDao; import java.util.Date; +import java.util.List; public interface ConsoleSessionDao extends GenericDao { @@ -33,4 +34,6 @@ public interface ConsoleSessionDao extends GenericDao { int expungeSessionsOlderThanDate(Date date); void acquireSession(String sessionUuid); + + int expungeByVmList(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleSessionDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleSessionDaoImpl.java index f2f4703a2a2..98428db9a86 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleSessionDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleSessionDaoImpl.java @@ -19,12 +19,15 @@ package com.cloud.vm.dao; +import java.util.Date; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; + +import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.vm.ConsoleSessionVO; -import com.cloud.utils.db.GenericDaoBase; - -import java.util.Date; public class ConsoleSessionDaoImpl extends GenericDaoBase implements ConsoleSessionDao { @@ -65,5 +68,15 @@ public class ConsoleSessionDaoImpl extends GenericDaoBase vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getInstanceId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java index c52c690d8b5..35948276ff2 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java @@ -94,4 +94,5 @@ public interface NicDao extends GenericDao { NicVO findByIpAddressAndVmType(String ip, VirtualMachine.Type vmType); List listByNetworkIdAndType(long networkId, VirtualMachine.Type vmType); + List searchRemovedByVms(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java index c8efc074a10..0652cf0694e 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java @@ -17,11 +17,13 @@ package com.cloud.vm.dao; import java.net.URI; +import java.util.ArrayList; import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.utils.db.Filter; @@ -398,4 +400,18 @@ public class NicDaoImpl extends GenericDaoBase implements NicDao { sc.setParameters("vmType", vmType); return listBy(sc); } + + @Override + public List searchRemovedByVms(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return new ArrayList<>(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getInstanceId(), SearchCriteria.Op.IN); + sb.and("removed", sb.entity().getRemoved(), SearchCriteria.Op.NNULL); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + Filter filter = new Filter(NicVO.class, "id", true, 0L, batchSize); + return searchIncludingRemoved(sc, filter, null, false); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicExtraDhcpOptionDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicExtraDhcpOptionDao.java index 69d9c00e1e0..7bae64a6acb 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicExtraDhcpOptionDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicExtraDhcpOptionDao.java @@ -29,4 +29,5 @@ public interface NicExtraDhcpOptionDao extends GenericDao extraDhcpOptions); + int expungeByNicList(List nicIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicExtraDhcpOptionDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicExtraDhcpOptionDaoImpl.java index 3056c73938e..0f3679d66a3 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicExtraDhcpOptionDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicExtraDhcpOptionDaoImpl.java @@ -16,13 +16,13 @@ // under the License. package com.cloud.vm.dao; -import org.springframework.stereotype.Component; - import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + import com.cloud.utils.db.DB; import com.cloud.utils.db.GenericDaoBase; - import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.vm.NicExtraDhcpOption; @@ -74,4 +74,15 @@ public class NicExtraDhcpOptionDaoImpl extends GenericDaoBase nicIds, Long batchSize) { + if (CollectionUtils.isEmpty(nicIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("nicIds", sb.entity().getNicId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("nicIds", nicIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpDao.java index cbb52e57282..ff7089ca427 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpDao.java @@ -55,4 +55,5 @@ public interface NicSecondaryIpDao extends GenericDao { List listSecondaryIpUsingKeyword(long nicId, String keyword); int moveSecondaryIps(long fromNicId, long toNicId); + int expungeByVmList(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpDaoImpl.java index a56d35d5a63..563b3279520 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpDaoImpl.java @@ -19,6 +19,7 @@ package com.cloud.vm.dao; import java.util.ArrayList; import java.util.List; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -192,4 +193,16 @@ public class NicSecondaryIpDaoImpl extends GenericDaoBase vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getVmId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java index bdb2534b62d..aaa6642567d 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java @@ -165,4 +165,7 @@ public interface VMInstanceDao extends GenericDao, StateDao< void updateSystemVmTemplateId(long templateId, Hypervisor.HypervisorType hypervisorType); List listByHostOrLastHostOrHostPod(long hostId, long podId); + List searchRemovedByRemoveDate(final Date startDate, final Date endDate, final Long batchSize, + List skippedVmIds); + } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index 44c36c0910d..3049f7b0602 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -28,6 +28,7 @@ import java.util.Map; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -40,6 +41,7 @@ import com.cloud.utils.DateUtil; import com.cloud.utils.Pair; import com.cloud.utils.db.Attribute; import com.cloud.utils.db.DB; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.GenericSearchBuilder; import com.cloud.utils.db.JoinBuilder; @@ -1018,4 +1020,26 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem sc.setParameters("podId", String.valueOf(podId)); return listBy(sc); } + + @Override + public List searchRemovedByRemoveDate(Date startDate, Date endDate, Long batchSize, + List skippedVmIds) { + SearchBuilder sb = createSearchBuilder(); + sb.and("removed", sb.entity().getRemoved(), SearchCriteria.Op.NNULL); + sb.and("startDate", sb.entity().getRemoved(), SearchCriteria.Op.GTEQ); + sb.and("endDate", sb.entity().getRemoved(), SearchCriteria.Op.LTEQ); + sb.and("skippedVmIds", sb.entity().getId(), Op.NOTIN); + SearchCriteria sc = sb.create(); + if (startDate != null) { + sc.setParameters("startDate", startDate); + } + if (endDate != null) { + sc.setParameters("endDate", endDate); + } + if (CollectionUtils.isNotEmpty(skippedVmIds)) { + sc.setParameters("skippedVmIds", skippedVmIds.toArray()); + } + Filter filter = new Filter(VMInstanceVO.class, "id", true, 0L, batchSize); + return searchIncludingRemoved(sc, filter, null, false); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java index 31999ef15d6..0143aaa1e73 100644 --- a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java @@ -38,4 +38,6 @@ public interface VMSnapshotDao extends GenericDao, StateDao< VMSnapshotVO findByName(Long vmId, String name); List listByAccountId(Long accountId); + List searchByVms(List vmIds); + List searchRemovedByVms(List vmIds, Long batchSize); } diff --git a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java index 1b1842dfd89..4be5a4bd4a7 100644 --- a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java @@ -17,13 +17,15 @@ package com.cloud.vm.snapshot.dao; +import java.util.ArrayList; import java.util.Date; import java.util.List; - +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @@ -182,4 +184,29 @@ public class VMSnapshotDaoImpl extends GenericDaoBase implem return rows > 0; } + @Override + public List searchByVms(List vmIds) { + if (CollectionUtils.isEmpty(vmIds)) { + return new ArrayList<>(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getVmId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return search(sc, null); + } + + @Override + public List searchRemovedByVms(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return new ArrayList<>(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getVmId(), SearchCriteria.Op.IN); + sb.and("removed", sb.entity().getRemoved(), SearchCriteria.Op.NNULL); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + Filter filter = new Filter(VMSnapshotVO.class, "id", true, 0L, batchSize); + return searchIncludingRemoved(sc, filter, null, false); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java index 5a173191be1..8f3d264da98 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java @@ -97,4 +97,6 @@ public interface ResourceDetailsDao extends GenericDao public void addDetail(long resourceId, String key, String value, boolean display); public List findResourceIdsByNameAndValueIn(String name, Object[] values); + + public long batchExpungeForResources(List ids, Long batchSize); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java index 37ebfebf5dd..4205a7823e4 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java @@ -21,13 +21,14 @@ import java.util.List; import java.util.Map; import org.apache.cloudstack.api.ResourceDetail; +import org.apache.commons.collections.CollectionUtils; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.GenericSearchBuilder; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; -import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.SearchCriteria.Op; +import com.cloud.utils.db.TransactionLegacy; public abstract class ResourceDetailsDaoBase extends GenericDaoBase implements ResourceDetailsDao { private SearchBuilder AllFieldsSearch; @@ -201,4 +202,17 @@ public abstract class ResourceDetailsDaoBase extends G return customSearch(sc, null); } + + @Override + public long batchExpungeForResources(final List ids, final Long batchSize) { + if (CollectionUtils.isEmpty(ids)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("ids", sb.entity().getResourceId(), Op.IN); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("ids", ids.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java index 2ce15894228..fd2fdf341b5 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java @@ -91,4 +91,6 @@ StateDao listReadyByVolumeId(long volumeId); + + int expungeBySnapshotList(List snapshotIds, Long batchSize); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java index 066a36ddff4..b029c626b8d 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java @@ -463,4 +463,16 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase snapshotIds, final Long batchSize) { + if (CollectionUtils.isEmpty(snapshotIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("snapshotIds", sb.entity().getSnapshotId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("snapshotIds", snapshotIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/VolumeDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/VolumeDataStoreDao.java index b3b2ece9043..f9a96c69ff0 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/VolumeDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/VolumeDataStoreDao.java @@ -57,4 +57,5 @@ public interface VolumeDataStoreDao extends GenericDao, List listVolumeDownloadUrlsByZoneId(long zoneId); List listByVolume(long volumeId, long storeId); + int expungeByVolumeList(List volumeIds, Long batchSize); } diff --git a/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupDaoImplTest.java b/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupDaoImplTest.java index bcfcdf9868c..8f512d2882d 100644 --- a/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupDaoImplTest.java @@ -19,10 +19,7 @@ package com.cloud.network.as.dao; -import com.cloud.network.as.AutoScaleVmGroup; -import com.cloud.network.as.AutoScaleVmGroupVO; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; +import java.util.List; import org.junit.Assert; import org.junit.Before; @@ -34,7 +31,10 @@ import org.mockito.Spy; import org.powermock.api.mockito.PowerMockito; import org.powermock.modules.junit4.PowerMockRunner; -import java.util.List; +import com.cloud.network.as.AutoScaleVmGroup; +import com.cloud.network.as.AutoScaleVmGroupVO; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; @RunWith(PowerMockRunner.class) public class AutoScaleVmGroupDaoImplTest { diff --git a/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImplTest.java b/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImplTest.java index 2dde1008e6b..90544c716d6 100644 --- a/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImplTest.java @@ -19,11 +19,9 @@ package com.cloud.network.as.dao; -import com.cloud.network.as.AutoScaleVmGroupVmMapVO; -import com.cloud.utils.db.GenericSearchBuilder; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; -import com.cloud.vm.VirtualMachine; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.junit.Assert; import org.junit.Before; @@ -32,11 +30,15 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; +import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.modules.junit4.PowerMockRunner; -import java.util.Arrays; -import java.util.List; +import com.cloud.network.as.AutoScaleVmGroupVmMapVO; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VirtualMachine; @RunWith(PowerMockRunner.class) public class AutoScaleVmGroupVmMapDaoImplTest { @@ -201,4 +203,33 @@ public class AutoScaleVmGroupVmMapDaoImplTest { Mockito.verify(searchCriteriaAutoScaleVmGroupVmMapVOMock).setParameters("vmGroupId", groupId); Mockito.verify(AutoScaleVmGroupVmMapDaoImplSpy).remove(searchCriteriaAutoScaleVmGroupVmMapVOMock); } + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, AutoScaleVmGroupVmMapDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, AutoScaleVmGroupVmMapDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(AutoScaleVmGroupVmMapDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(AutoScaleVmGroupVmMapDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final AutoScaleVmGroupVmMapVO mockedVO = Mockito.mock(AutoScaleVmGroupVmMapVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), AutoScaleVmGroupVmMapDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(AutoScaleVmGroupVmMapDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } } diff --git a/engine/schema/src/test/java/com/cloud/network/dao/IPAddressDaoImplTest.java b/engine/schema/src/test/java/com/cloud/network/dao/IPAddressDaoImplTest.java new file mode 100644 index 00000000000..d8f6a08d8d3 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/network/dao/IPAddressDaoImplTest.java @@ -0,0 +1,67 @@ +// 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 com.cloud.network.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class IPAddressDaoImplTest { + + @Spy + IPAddressDaoImpl ipAddressDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, ipAddressDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, ipAddressDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(ipAddressDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(ipAddressDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final IPAddressVO mockedVO = Mockito.mock(IPAddressVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), ipAddressDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(ipAddressDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/network/dao/InlineLoadBalancerNicMapDaoImplTest.java b/engine/schema/src/test/java/com/cloud/network/dao/InlineLoadBalancerNicMapDaoImplTest.java new file mode 100644 index 00000000000..8e06c7618f6 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/network/dao/InlineLoadBalancerNicMapDaoImplTest.java @@ -0,0 +1,67 @@ +// 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 com.cloud.network.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class InlineLoadBalancerNicMapDaoImplTest { + + @Spy + InlineLoadBalancerNicMapDaoImpl inlineLoadBalancerNicMapDaoImplSpy; + + @Test + public void testExpungeByNicListNoVms() { + Assert.assertEquals(0, inlineLoadBalancerNicMapDaoImplSpy.expungeByNicList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, inlineLoadBalancerNicMapDaoImplSpy.expungeByNicList( + null, 100L)); + } + + @Test + public void testExpungeByNicList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(inlineLoadBalancerNicMapDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(inlineLoadBalancerNicMapDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final InlineLoadBalancerNicMapVO mockedVO = Mockito.mock(InlineLoadBalancerNicMapVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), inlineLoadBalancerNicMapDaoImplSpy.expungeByNicList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("nicIds", array); + Mockito.verify(inlineLoadBalancerNicMapDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/network/dao/LoadBalancerVMMapDaoImplTest.java b/engine/schema/src/test/java/com/cloud/network/dao/LoadBalancerVMMapDaoImplTest.java new file mode 100644 index 00000000000..fa957194903 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/network/dao/LoadBalancerVMMapDaoImplTest.java @@ -0,0 +1,67 @@ +// 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 com.cloud.network.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class LoadBalancerVMMapDaoImplTest { + + @Spy + LoadBalancerVMMapDaoImpl loadBalancerVMMapDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, loadBalancerVMMapDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, loadBalancerVMMapDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(loadBalancerVMMapDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(loadBalancerVMMapDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final LoadBalancerVMMapVO mockedVO = Mockito.mock(LoadBalancerVMMapVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), loadBalancerVMMapDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(loadBalancerVMMapDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/network/dao/OpRouterMonitorServiceDaoImplTest.java b/engine/schema/src/test/java/com/cloud/network/dao/OpRouterMonitorServiceDaoImplTest.java new file mode 100644 index 00000000000..7d0b1b069ba --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/network/dao/OpRouterMonitorServiceDaoImplTest.java @@ -0,0 +1,67 @@ +// 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 com.cloud.network.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class OpRouterMonitorServiceDaoImplTest { + + @Spy + OpRouterMonitorServiceDaoImpl opRouterMonitorServiceDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, opRouterMonitorServiceDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, opRouterMonitorServiceDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(opRouterMonitorServiceDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(opRouterMonitorServiceDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final OpRouterMonitorServiceVO mockedVO = Mockito.mock(OpRouterMonitorServiceVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), opRouterMonitorServiceDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(opRouterMonitorServiceDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/network/rules/dao/PortForwardingRulesDaoImplTest.java b/engine/schema/src/test/java/com/cloud/network/rules/dao/PortForwardingRulesDaoImplTest.java new file mode 100644 index 00000000000..c60e9b1f1bf --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/network/rules/dao/PortForwardingRulesDaoImplTest.java @@ -0,0 +1,68 @@ +// 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 com.cloud.network.rules.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.network.rules.PortForwardingRuleVO; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class PortForwardingRulesDaoImplTest { + + @Spy + PortForwardingRulesDaoImpl portForwardingRulesDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, portForwardingRulesDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, portForwardingRulesDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(portForwardingRulesDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(portForwardingRulesDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final PortForwardingRuleVO mockedVO = Mockito.mock(PortForwardingRuleVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), portForwardingRulesDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(portForwardingRulesDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/secstorage/CommandExecLogDaoImplTest.java b/engine/schema/src/test/java/com/cloud/secstorage/CommandExecLogDaoImplTest.java new file mode 100644 index 00000000000..f86df6bdd36 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/secstorage/CommandExecLogDaoImplTest.java @@ -0,0 +1,67 @@ +// 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 com.cloud.secstorage; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class CommandExecLogDaoImplTest { + + @Spy + CommandExecLogDaoImpl commandExecLogDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, commandExecLogDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, commandExecLogDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(commandExecLogDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(commandExecLogDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final CommandExecLogVO mockedVO = Mockito.mock(CommandExecLogVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), commandExecLogDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(commandExecLogDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/StoragePoolTagsDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/StoragePoolTagsDaoImplTest.java index dcbd665a57d..e00a87f6896 100755 --- a/engine/schema/src/test/java/com/cloud/storage/dao/StoragePoolTagsDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/storage/dao/StoragePoolTagsDaoImplTest.java @@ -16,6 +16,15 @@ // under the License. package com.cloud.storage.dao; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.junit.Before; import org.junit.Test; @@ -32,15 +41,6 @@ import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.doNothing; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import junit.framework.TestCase; @RunWith(PowerMockRunner.class) diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java index 5a4a3453ae1..7624f84d12f 100644 --- a/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java @@ -26,16 +26,25 @@ import static org.powermock.api.mockito.PowerMockito.when; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.Spy; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import com.cloud.storage.VolumeVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.TransactionLegacy; @RunWith(PowerMockRunner.class) @@ -48,6 +57,7 @@ public class VolumeDaoImplTest { @Mock private TransactionLegacy transactionMock; + @Spy private final VolumeDaoImpl volumeDao = new VolumeDaoImpl(); @Test @@ -94,4 +104,34 @@ public class VolumeDaoImplTest { verify(preparedStatementMock, times(2)).setLong(anyInt(), anyLong()); verify(preparedStatementMock, times(1)).executeQuery(); } + + @Test + public void testSearchRemovedByVmsNoVms() { + Assert.assertTrue(CollectionUtils.isEmpty(volumeDao.searchRemovedByVms( + new ArrayList<>(), 100L))); + Assert.assertTrue(CollectionUtils.isEmpty(volumeDao.searchRemovedByVms( + null, 100L))); + } + + @Test + public void testSearchRemovedByVms() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(volumeDao).searchIncludingRemoved( + Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), + Mockito.eq(false)); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + final VolumeVO mockedVO = Mockito.mock(VolumeVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + volumeDao.searchRemovedByVms(List.of(1L, 2L), batchSize); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(volumeDao, Mockito.times(1)).searchIncludingRemoved( + Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), + Mockito.eq(false)); + } + } diff --git a/engine/schema/src/test/java/com/cloud/vm/ItWorkDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/ItWorkDaoImplTest.java new file mode 100644 index 00000000000..04bc125e05f --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/ItWorkDaoImplTest.java @@ -0,0 +1,67 @@ +// 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 com.cloud.vm; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class ItWorkDaoImplTest { + + @Spy + ItWorkDaoImpl itWorkDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, itWorkDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, itWorkDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(itWorkDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(itWorkDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final ItWorkVO mockedVO = Mockito.mock(ItWorkVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), itWorkDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(itWorkDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/ConsoleSessionDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/ConsoleSessionDaoImplTest.java new file mode 100644 index 00000000000..c9919e26af6 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/ConsoleSessionDaoImplTest.java @@ -0,0 +1,68 @@ +// 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 com.cloud.vm.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.ConsoleSessionVO; + +@RunWith(MockitoJUnitRunner.class) +public class ConsoleSessionDaoImplTest { + + @Spy + ConsoleSessionDaoImpl consoleSessionDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, consoleSessionDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, consoleSessionDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(consoleSessionDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(consoleSessionDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final ConsoleSessionVO mockedVO = Mockito.mock(ConsoleSessionVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), consoleSessionDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(consoleSessionDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/NicDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/NicDaoImplTest.java new file mode 100644 index 00000000000..506fdb7fc92 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/NicDaoImplTest.java @@ -0,0 +1,69 @@ +// 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 com.cloud.vm.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.NicVO; + +@RunWith(MockitoJUnitRunner.class) +public class NicDaoImplTest { + + @Spy + NicDaoImpl nicDaoImplSpy; + + @Test + public void testSearchRemovedByVmsNoVms() { + Assert.assertTrue(CollectionUtils.isEmpty(nicDaoImplSpy.searchRemovedByVms( + new ArrayList<>(), 100L))); + Assert.assertTrue(CollectionUtils.isEmpty(nicDaoImplSpy.searchRemovedByVms( + null, 100L))); + } + + @Test + public void testSearchRemovedByVms() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(nicDaoImplSpy).searchIncludingRemoved( + Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), + Mockito.eq(false)); + Mockito.when(nicDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final NicVO mockedVO = Mockito.mock(NicVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + nicDaoImplSpy.searchRemovedByVms(List.of(1L, 2L), batchSize); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(nicDaoImplSpy, Mockito.times(1)).searchIncludingRemoved( + Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), + Mockito.eq(false)); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/NicExtraDhcpOptionDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/NicExtraDhcpOptionDaoImplTest.java new file mode 100644 index 00000000000..7a1e32e95ca --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/NicExtraDhcpOptionDaoImplTest.java @@ -0,0 +1,68 @@ +// 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 com.cloud.vm.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.NicExtraDhcpOptionVO; + +@RunWith(MockitoJUnitRunner.class) +public class NicExtraDhcpOptionDaoImplTest { + + @Spy + NicExtraDhcpOptionDaoImpl nicExtraDhcpOptionDaoImplSpy; + + @Test + public void testExpungeByNicListNoVms() { + Assert.assertEquals(0, nicExtraDhcpOptionDaoImplSpy.expungeByNicList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, nicExtraDhcpOptionDaoImplSpy.expungeByNicList( + null, 100L)); + } + + @Test + public void testExpungeByNicList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(nicExtraDhcpOptionDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(nicExtraDhcpOptionDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final NicExtraDhcpOptionVO mockedVO = Mockito.mock(NicExtraDhcpOptionVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), nicExtraDhcpOptionDaoImplSpy.expungeByNicList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("nicIds", array); + Mockito.verify(nicExtraDhcpOptionDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/NicSecondaryIpDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/NicSecondaryIpDaoImplTest.java new file mode 100644 index 00000000000..a9f798dbc01 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/NicSecondaryIpDaoImplTest.java @@ -0,0 +1,67 @@ +// 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 com.cloud.vm.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class NicSecondaryIpDaoImplTest { + + @Spy + NicSecondaryIpDaoImpl nicSecondaryIpDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, nicSecondaryIpDaoImplSpy.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, nicSecondaryIpDaoImplSpy.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(nicSecondaryIpDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(nicSecondaryIpDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final NicSecondaryIpVO mockedVO = Mockito.mock(NicSecondaryIpVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), nicSecondaryIpDaoImplSpy.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(nicSecondaryIpDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java index 9dc773cd7d6..442d40aaf51 100644 --- a/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java @@ -30,16 +30,22 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import org.joda.time.DateTime; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; @@ -191,4 +197,29 @@ public class VMInstanceDaoImplTest { assertTrue(result); } + + @Test + public void testSearchRemovedByRemoveDate() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.when(vmInstanceDao.createSearchBuilder()).thenReturn(sb); + final VMInstanceVO mockedVO = Mockito.mock(VMInstanceVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + Mockito.doReturn(new ArrayList<>()).when(vmInstanceDao).searchIncludingRemoved( + Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), + Mockito.eq(false)); + Calendar cal = Calendar.getInstance(); + Date endDate = new Date(); + cal.setTime(endDate); + cal.add(Calendar.DATE, -1 * 10); + Date startDate = cal.getTime(); + vmInstanceDao.searchRemovedByRemoveDate(startDate, endDate, 50L, new ArrayList<>()); + Mockito.verify(sc).setParameters("startDate", startDate); + Mockito.verify(sc).setParameters("endDate", endDate); + Mockito.verify(sc, Mockito.never()).setParameters(Mockito.eq("skippedVmIds"), Mockito.any()); + Mockito.verify(vmInstanceDao, Mockito.times(1)).searchIncludingRemoved( + Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), + Mockito.eq(false)); + } } diff --git a/engine/schema/src/test/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImplTest.java new file mode 100644 index 00000000000..e71518080d2 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImplTest.java @@ -0,0 +1,69 @@ +// 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 com.cloud.vm.snapshot.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.snapshot.VMSnapshotVO; + +@RunWith(MockitoJUnitRunner.class) +public class VMSnapshotDaoImplTest { + + @Spy + VMSnapshotDaoImpl vmSnapshotDaoImplSpy; + + @Test + public void testSearchRemovedByVmsNoVms() { + Assert.assertTrue(CollectionUtils.isEmpty(vmSnapshotDaoImplSpy.searchRemovedByVms( + new ArrayList<>(), 100L))); + Assert.assertTrue(CollectionUtils.isEmpty(vmSnapshotDaoImplSpy.searchRemovedByVms( + null, 100L))); + } + + @Test + public void testSearchRemovedByVms() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(vmSnapshotDaoImplSpy).searchIncludingRemoved( + Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), + Mockito.eq(false)); + Mockito.when(vmSnapshotDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final VMSnapshotVO mockedVO = Mockito.mock(VMSnapshotVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + vmSnapshotDaoImplSpy.searchRemovedByVms(List.of(1L, 2L), batchSize); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(vmSnapshotDaoImplSpy, Mockito.times(1)).searchIncludingRemoved( + Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), + Mockito.eq(false)); + } +} diff --git a/engine/schema/src/test/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImplTest.java b/engine/schema/src/test/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImplTest.java new file mode 100644 index 00000000000..85240ab4a05 --- /dev/null +++ b/engine/schema/src/test/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImplTest.java @@ -0,0 +1,67 @@ +// 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.storage.datastore.db; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class SnapshotDataStoreDaoImplTest { + + @Spy + SnapshotDataStoreDaoImpl snapshotDataStoreDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, snapshotDataStoreDaoImplSpy.expungeBySnapshotList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, snapshotDataStoreDaoImplSpy.expungeBySnapshotList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(snapshotDataStoreDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(snapshotDataStoreDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final SnapshotDataStoreVO mockedVO = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), snapshotDataStoreDaoImplSpy.expungeBySnapshotList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("snapshotIds", array); + Mockito.verify(snapshotDataStoreDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/db/VolumeDataStoreDaoImpl.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/db/VolumeDataStoreDaoImpl.java index dca2e9a862e..fc87ddcda0d 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/db/VolumeDataStoreDaoImpl.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/db/VolumeDataStoreDaoImpl.java @@ -25,10 +25,6 @@ import java.util.Map; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.utils.db.Filter; -import com.cloud.utils.exception.CloudRuntimeException; -import org.apache.log4j.Logger; -import org.springframework.stereotype.Component; import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; @@ -36,10 +32,14 @@ import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreState import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.State; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; +import org.apache.commons.collections.CollectionUtils; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.JoinBuilder.JoinType; import com.cloud.utils.db.SearchBuilder; @@ -47,6 +47,7 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.UpdateBuilder; +import com.cloud.utils.exception.CloudRuntimeException; @Component public class VolumeDataStoreDaoImpl extends GenericDaoBase implements VolumeDataStoreDao { @@ -389,4 +390,16 @@ public class VolumeDataStoreDaoImpl extends GenericDaoBase volumeIds, Long batchSize) { + if (CollectionUtils.isEmpty(volumeIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("volumeIds", sb.entity().getVolumeId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("volumeIds", volumeIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/engine/storage/src/test/java/org/apache/cloudstack/storage/image/db/VolumeDataStoreDaoImplTest.java b/engine/storage/src/test/java/org/apache/cloudstack/storage/image/db/VolumeDataStoreDaoImplTest.java new file mode 100644 index 00000000000..0cd88699956 --- /dev/null +++ b/engine/storage/src/test/java/org/apache/cloudstack/storage/image/db/VolumeDataStoreDaoImplTest.java @@ -0,0 +1,68 @@ +// 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.storage.image.db; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class VolumeDataStoreDaoImplTest { + + @Spy + VolumeDataStoreDaoImpl volumeDataStoreDaoImplSpy; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, volumeDataStoreDaoImplSpy.expungeByVolumeList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, volumeDataStoreDaoImplSpy.expungeByVolumeList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(volumeDataStoreDaoImplSpy).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(volumeDataStoreDaoImplSpy.createSearchBuilder()).thenReturn(sb); + final VolumeDataStoreVO mockedVO = Mockito.mock(VolumeDataStoreVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), volumeDataStoreDaoImplSpy.expungeByVolumeList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("volumeIds", array); + Mockito.verify(volumeDataStoreDaoImplSpy, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java index 65288f4a577..9e9cc68dc8e 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java @@ -229,6 +229,24 @@ public interface GenericDao { */ int expunge(final SearchCriteria sc); + /** + * remove the entity bean specified by the search criteria and filter + * @param sc + * @param filter + * @return number of rows deleted + */ + int expunge(final SearchCriteria sc, final Filter filter); + + /** + * remove the entity bean specified by the search criteria and batchSize + * @param sc + * @param batchSize + * @return number of rows deleted + */ + int batchExpunge(final SearchCriteria sc, final Long batchSize); + + int expungeList(List ids); + /** * expunge the removed rows. */ diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java index 675d911113e..8bfd8f2cee0 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java @@ -20,6 +20,8 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.math.BigInteger; @@ -59,10 +61,13 @@ import javax.persistence.Enumerated; import javax.persistence.Table; import javax.persistence.TableGenerator; -import com.amazonaws.util.CollectionUtils; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.log4j.Logger; +import com.amazonaws.util.CollectionUtils; import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; @@ -75,8 +80,6 @@ import com.cloud.utils.db.SearchCriteria.SelectType; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.Ip; import com.cloud.utils.net.NetUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import net.sf.cglib.proxy.Callback; import net.sf.cglib.proxy.CallbackFilter; @@ -1226,7 +1229,7 @@ public abstract class GenericDaoBase extends Compone // FIXME: Does not work for joins. @Override - public int expunge(final SearchCriteria sc) { + public int expunge(final SearchCriteria sc, final Filter filter) { if (sc == null) { throw new CloudRuntimeException("Call to throw new expunge with null search Criteria"); } @@ -1238,6 +1241,7 @@ public abstract class GenericDaoBase extends Compone if (sc != null && sc.getWhereClause().length() > 0) { str.append(sc.getWhereClause()); } + addFilter(str, filter); final String sql = str.toString(); @@ -1256,6 +1260,47 @@ public abstract class GenericDaoBase extends Compone throw new CloudRuntimeException("Caught: " + pstmt, e); } } + @Override + public int expunge(final SearchCriteria sc) { + return expunge(sc, null); + } + + @Override + public int batchExpunge(final SearchCriteria sc, final Long batchSize) { + Filter filter = null; + final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); + if (batchSizeFinal > 0) { + filter = new Filter(batchSizeFinal); + } + int expunged = 0; + int currentExpunged = 0; + do { + currentExpunged = expunge(sc, filter); + expunged += currentExpunged; + } while (batchSizeFinal > 0 && currentExpunged >= batchSizeFinal); + return expunged; + } + + @Override + public int expungeList(final List ids) { + if (org.apache.commons.collections.CollectionUtils.isEmpty(ids)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + Object obj = null; + try { + Method m = sb.entity().getClass().getMethod("getId"); + obj = m.invoke(sb.entity()); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ignored) {} + if (obj == null) { + s_logger.warn(String.format("Unable to get ID object for entity: %s", _entityBeanType.getSimpleName())); + return 0; + } + sb.and("id", obj, SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("id", ids.toArray()); + return expunge(sc); + } @DB() protected StringBuilder createPartialSelectSql(SearchCriteria sc, final boolean whereClause, final boolean enableQueryCache) { diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDao.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDao.java index 89601e6b5d2..b3bfda0334c 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDao.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDao.java @@ -39,4 +39,5 @@ public interface VmWorkJobDao extends GenericDao { void expungeCompletedWorkJobs(Date cutDate); void expungeLeftoverWorkJobs(long msid); + int expungeByVmList(List vmIds, Long batchSize); } diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDaoImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDaoImpl.java index 4a10727546e..f78241fff49 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDaoImpl.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDaoImpl.java @@ -24,11 +24,11 @@ import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Inject; -import org.apache.log4j.Logger; - import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO.Step; import org.apache.cloudstack.jobs.JobInfo; +import org.apache.commons.collections.CollectionUtils; +import org.apache.log4j.Logger; import com.cloud.utils.DateUtil; import com.cloud.utils.db.Filter; @@ -214,4 +214,16 @@ public class VmWorkJobDaoImpl extends GenericDaoBase implemen } }); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getVmInstanceId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDaoImplTest.java b/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDaoImplTest.java new file mode 100644 index 00000000000..3e2bc15b1e0 --- /dev/null +++ b/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/dao/VmWorkJobDaoImplTest.java @@ -0,0 +1,68 @@ +// 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.framework.jobs.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class VmWorkJobDaoImplTest { + + @Spy + VmWorkJobDaoImpl vmWorkJobDaoImpl; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, vmWorkJobDaoImpl.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, vmWorkJobDaoImpl.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(vmWorkJobDaoImpl).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(vmWorkJobDaoImpl.createSearchBuilder()).thenReturn(sb); + final VmWorkJobVO mockedVO = Mockito.mock(VmWorkJobVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), vmWorkJobDaoImpl.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(vmWorkJobDaoImpl, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/element/ElasticLoadBalancerElement.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/element/ElasticLoadBalancerElement.java index 87ecf0071f1..8851e6a91f0 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/element/ElasticLoadBalancerElement.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/element/ElasticLoadBalancerElement.java @@ -226,4 +226,8 @@ public class ElasticLoadBalancerElement extends AdapterBase implements LoadBalan return true; } + @Override + public void expungeLbVmRefs(List vmIds, Long batchSize) { + _lbMgr.expungeLbVmRefs(vmIds, batchSize); + } } diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManager.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManager.java index f885f7e1012..a687c811316 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManager.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManager.java @@ -37,4 +37,6 @@ public interface ElasticLoadBalancerManager { NetworkRuleConflictException; public void handleDeleteLoadBalancerRule(LoadBalancer lb, long callerUserId, Account caller); + + void expungeLbVmRefs(List vmIds, Long batchSize); } diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java index 6975f76e968..07a9e17264e 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/ElasticLoadBalancerManagerImpl.java @@ -601,4 +601,8 @@ public class ElasticLoadBalancerManagerImpl extends ManagerBase implements Elast public void finalizeUnmanage(VirtualMachine vm) { } + @Override + public void expungeLbVmRefs(List vmIds, Long batchSize) { + _elbVmMapDao.expungeByLbVmList(vmIds, batchSize); + } } diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/dao/ElasticLbVmMapDao.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/dao/ElasticLbVmMapDao.java index 83aa6118a63..eec7eaa4f5f 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/dao/ElasticLbVmMapDao.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/dao/ElasticLbVmMapDao.java @@ -40,4 +40,6 @@ public interface ElasticLbVmMapDao extends GenericDao { List listLbsForElbVm(long elbVmId); + int expungeByLbVmList(List vmIds, Long batchSize); + } diff --git a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/dao/ElasticLbVmMapDaoImpl.java b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/dao/ElasticLbVmMapDaoImpl.java index bbe79ef3103..ea1673967a5 100644 --- a/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/dao/ElasticLbVmMapDaoImpl.java +++ b/plugins/network-elements/elastic-loadbalancer/src/main/java/com/cloud/network/lb/dao/ElasticLbVmMapDaoImpl.java @@ -21,6 +21,7 @@ import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.network.ElasticLbVmMapVO; @@ -136,4 +137,16 @@ public class ElasticLbVmMapDaoImpl extends GenericDaoBase vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getElbVmId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } + } 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 bac7ff8b934..9da68205a41 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 @@ -30,6 +30,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -149,6 +150,10 @@ public class ServiceOfferingJoinDaoImpl extends GenericDaoBase filteredDomainIds = filterChildSubDomains(domainIds); @@ -3196,7 +3197,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati limitResourceUse, volatileVm, displayText, isSystem, vmType, hostTag, deploymentPlanner, dynamicScalingEnabled, isCustomized); - List detailsVO = new ArrayList(); + List detailsVOList = new ArrayList(); if (details != null) { // To have correct input, either both gpu card name and VGPU type should be passed or nothing should be passed. // Use XOR condition to verify that. @@ -3230,12 +3231,16 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati // Add in disk offering details continue; } - detailsVO.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), detailEntry.getKey(), detailEntryValue, true)); + detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), detailEntry.getKey(), detailEntryValue, true)); } } if (storagePolicyID != null) { - detailsVO.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.STORAGE_POLICY, String.valueOf(storagePolicyID), false)); + detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.STORAGE_POLICY, String.valueOf(storagePolicyID), false)); + } + if (purgeResources) { + detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), + ServiceOffering.PURGE_DB_ENTITIES_KEY, Boolean.TRUE.toString(), false)); } serviceOffering.setDiskOfferingStrictness(diskOfferingStrictness); @@ -3262,18 +3267,18 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati if ((serviceOffering = _serviceOfferingDao.persist(serviceOffering)) != null) { for (Long domainId : filteredDomainIds) { - detailsVO.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); + detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); } if (CollectionUtils.isNotEmpty(zoneIds)) { for (Long zoneId : zoneIds) { - detailsVO.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.ZONE_ID, String.valueOf(zoneId), false)); + detailsVOList.add(new ServiceOfferingDetailsVO(serviceOffering.getId(), ApiConstants.ZONE_ID, String.valueOf(zoneId), false)); } } - if (CollectionUtils.isNotEmpty(detailsVO)) { - for (ServiceOfferingDetailsVO detail : detailsVO) { + if (CollectionUtils.isNotEmpty(detailsVOList)) { + for (ServiceOfferingDetailsVO detail : detailsVOList) { detail.setResourceId(serviceOffering.getId()); } - _serviceOfferingDetailsDao.saveDetails(detailsVO); + _serviceOfferingDetailsDao.saveDetails(detailsVOList); } CallContext.current().setEventDetails("Service offering id=" + serviceOffering.getId()); @@ -3429,6 +3434,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati final List zoneIds = cmd.getZoneIds(); String storageTags = cmd.getStorageTags(); String hostTags = cmd.getHostTags(); + boolean purgeResources = cmd.isPurgeResources(); if (userId == null) { userId = Long.valueOf(User.UID_SYSTEM); @@ -3446,6 +3452,12 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati List existingZoneIds = _serviceOfferingDetailsDao.findZoneIds(id); Collections.sort(existingZoneIds); + String purgeResourceStr = _serviceOfferingDetailsDao.getDetail(id, ServiceOffering.PURGE_DB_ENTITIES_KEY); + boolean existingPurgeResources = false; + if (StringUtils.isNotBlank(purgeResourceStr)) { + existingPurgeResources = Boolean.parseBoolean(purgeResourceStr); + } + // check if valid domain if (CollectionUtils.isNotEmpty(domainIds)) { for (final Long domainId: domainIds) { @@ -3514,7 +3526,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati } final boolean updateNeeded = name != null || displayText != null || sortKey != null || storageTags != null || hostTags != null; - final boolean detailsUpdateNeeded = !filteredDomainIds.equals(existingDomainIds) || !filteredZoneIds.equals(existingZoneIds); + final boolean detailsUpdateNeeded = !filteredDomainIds.equals(existingDomainIds) || + !filteredZoneIds.equals(existingZoneIds) || purgeResources != existingPurgeResources; if (!updateNeeded && !detailsUpdateNeeded) { return _serviceOfferingDao.findById(id); } @@ -3564,6 +3577,14 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati detailsVO.add(new ServiceOfferingDetailsVO(id, ApiConstants.ZONE_ID, String.valueOf(zoneId), false)); } } + if (purgeResources != existingPurgeResources) { + sc.setParameters("detailName", ServiceOffering.PURGE_DB_ENTITIES_KEY); + _serviceOfferingDetailsDao.remove(sc); + if (purgeResources) { + detailsVO.add(new ServiceOfferingDetailsVO(id, ServiceOffering.PURGE_DB_ENTITIES_KEY, + "true", false)); + } + } } if (!detailsVO.isEmpty()) { for (ServiceOfferingDetailsVO detailVO : detailsVO) { diff --git a/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java b/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java index 1b9b512118d..7a27172d79b 100644 --- a/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java +++ b/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java @@ -1066,4 +1066,9 @@ public class HighAvailabilityManagerImpl extends ManagerBase implements Configur StopRetryInterval, RestartRetryInterval, MigrateRetryInterval, InvestigateRetryInterval, HAWorkers, ForceHA}; } + + @Override + public int expungeWorkItemsByVmList(List vmIds, Long batchSize) { + return _haDao.expungeByVmList(vmIds, batchSize); + } } diff --git a/server/src/main/java/com/cloud/ha/dao/HighAvailabilityDao.java b/server/src/main/java/com/cloud/ha/dao/HighAvailabilityDao.java index e8a3e17f805..f6539105d78 100644 --- a/server/src/main/java/com/cloud/ha/dao/HighAvailabilityDao.java +++ b/server/src/main/java/com/cloud/ha/dao/HighAvailabilityDao.java @@ -85,4 +85,5 @@ public interface HighAvailabilityDao extends GenericDao { List listPendingHaWorkForVm(long vmId); List listPendingMigrationsForVm(long vmId); + int expungeByVmList(List vmIds, Long batchSize); } diff --git a/server/src/main/java/com/cloud/ha/dao/HighAvailabilityDaoImpl.java b/server/src/main/java/com/cloud/ha/dao/HighAvailabilityDaoImpl.java index c7284053fb2..743a07607dd 100644 --- a/server/src/main/java/com/cloud/ha/dao/HighAvailabilityDaoImpl.java +++ b/server/src/main/java/com/cloud/ha/dao/HighAvailabilityDaoImpl.java @@ -19,7 +19,7 @@ package com.cloud.ha.dao; import java.util.Date; import java.util.List; - +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -260,4 +260,16 @@ public class HighAvailabilityDaoImpl extends GenericDaoBase impl return update(vo, sc); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("vmIds", sb.entity().getInstanceId(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + sc.setParameters("vmIds", vmIds.toArray()); + return batchExpunge(sc, batchSize); + } } diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 7f9635c0a8a..f254210b38a 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -357,8 +357,8 @@ import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.db.UUIDManager; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.exception.ExecutionException; import com.cloud.utils.exception.ExceptionProxyObject; +import com.cloud.utils.exception.ExecutionException; import com.cloud.utils.fsm.NoTransitionException; import com.cloud.utils.net.Ip; import com.cloud.utils.net.NetUtils; diff --git a/server/src/main/java/org/apache/cloudstack/resource/ResourceCleanupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/resource/ResourceCleanupServiceImpl.java new file mode 100644 index 00000000000..c88f045e47b --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/resource/ResourceCleanupServiceImpl.java @@ -0,0 +1,829 @@ +// 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.resource; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.admin.resource.PurgeExpungedResourcesCmd; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.framework.async.AsyncCallFuture; +import org.apache.cloudstack.framework.async.AsyncCallbackDispatcher; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.framework.async.AsyncRpcContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.jobs.dao.VmWorkJobDao; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.storage.command.CommandResult; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.ha.HighAvailabilityManager; +import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.InlineLoadBalancerNicMapDao; +import com.cloud.network.dao.LoadBalancerVMMapDao; +import com.cloud.network.dao.OpRouterMonitorServiceDao; +import com.cloud.network.rules.dao.PortForwardingRulesDao; +import com.cloud.offering.ServiceOffering; +import com.cloud.secstorage.CommandExecLogDao; +import com.cloud.service.dao.ServiceOfferingDetailsDao; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.utils.Pair; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackWithException; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.ItWorkDao; +import com.cloud.vm.NicVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.ConsoleSessionDao; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicDetailsDao; +import com.cloud.vm.dao.NicExtraDhcpOptionDao; +import com.cloud.vm.dao.NicSecondaryIpDao; +import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; + +public class ResourceCleanupServiceImpl extends ManagerBase implements ResourceCleanupService, PluggableService, + Configurable { + private static final Logger logger = Logger.getLogger(ResourceCleanupServiceImpl.class); + + @Inject + VMInstanceDao vmInstanceDao; + @Inject + VolumeDao volumeDao; + @Inject + VolumeDetailsDao volumeDetailsDao; + @Inject + VolumeDataStoreDao volumeDataStoreDao; + @Inject + SnapshotDao snapshotDao; + @Inject + SnapshotDetailsDao snapshotDetailsDao; + @Inject + SnapshotDataStoreDao snapshotDataStoreDao; + @Inject + NicDao nicDao; + @Inject + NicDetailsDao nicDetailsDao; + @Inject + NicExtraDhcpOptionDao nicExtraDhcpOptionDao; + @Inject + InlineLoadBalancerNicMapDao inlineLoadBalancerNicMapDao; + @Inject + UserVmDetailsDao userVmDetailsDao; + @Inject + VMSnapshotDao vmSnapshotDao; + @Inject + VMSnapshotDetailsDao vmSnapshotDetailsDao; + @Inject + AutoScaleVmGroupVmMapDao autoScaleVmGroupVmMapDao; + @Inject + CommandExecLogDao commandExecLogDao; + @Inject + NetworkOrchestrationService networkOrchestrationService; + @Inject + LoadBalancerVMMapDao loadBalancerVMMapDao; + @Inject + NicSecondaryIpDao nicSecondaryIpDao; + @Inject + HighAvailabilityManager highAvailabilityManager; + @Inject + ItWorkDao itWorkDao; + @Inject + OpRouterMonitorServiceDao opRouterMonitorServiceDao; + @Inject + PortForwardingRulesDao portForwardingRulesDao; + @Inject + IPAddressDao ipAddressDao; + @Inject + VmWorkJobDao vmWorkJobDao; + @Inject + ConsoleSessionDao consoleSessionDao; + @Inject + ManagementServerHostDao managementServerHostDao; + @Inject + ServiceOfferingDetailsDao serviceOfferingDetailsDao; + + private ScheduledExecutorService expungedResourcesCleanupExecutor; + private ExecutorService purgeExpungedResourcesJobExecutor; + + protected void purgeLinkedSnapshotEntities(final List snapshotIds, final Long batchSize) { + if (CollectionUtils.isEmpty(snapshotIds)) { + return; + } + snapshotDetailsDao.batchExpungeForResources(snapshotIds, batchSize); + snapshotDataStoreDao.expungeBySnapshotList(snapshotIds, batchSize); + // Snapshot policies are using ON DELETE CASCADE + } + + protected long purgeVolumeSnapshots(final List volumeIds, final Long batchSize) { + if (CollectionUtils.isEmpty(volumeIds)) { + return 0; + } + SearchBuilder sb = snapshotDao.createSearchBuilder(); + sb.and("volumeIds", sb.entity().getVolumeId(), SearchCriteria.Op.IN); + sb.and("removed", sb.entity().getRemoved(), SearchCriteria.Op.NNULL); + SearchCriteria sc = sb.create(); + sc.setParameters("volumeIds", volumeIds.toArray()); + int removed = 0; + long totalRemoved = 0; + Filter filter = new Filter(SnapshotVO.class, "id", true, 0L, batchSize); + final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); + do { + List snapshots = snapshotDao.searchIncludingRemoved(sc, filter, null, false); + List snapshotIds = snapshots.stream().map(SnapshotVO::getId).collect(Collectors.toList()); + purgeLinkedSnapshotEntities(snapshotIds, batchSize); + removed = snapshotDao.expungeList(snapshotIds); + totalRemoved += removed; + } while (batchSizeFinal > 0 && removed >= batchSizeFinal); + return totalRemoved; + } + + protected void purgeLinkedVolumeEntities(final List volumeIds, final Long batchSize) { + if (CollectionUtils.isEmpty(volumeIds)) { + return; + } + volumeDetailsDao.batchExpungeForResources(volumeIds, batchSize); + volumeDataStoreDao.expungeByVolumeList(volumeIds, batchSize); + purgeVolumeSnapshots(volumeIds, batchSize); + } + + protected long purgeVMVolumes(final List vmIds, final Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + int removed = 0; + long totalRemoved = 0; + final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); + do { + List volumes = volumeDao.searchRemovedByVms(vmIds, batchSize); + List volumeIds = volumes.stream().map(VolumeVO::getId).collect(Collectors.toList()); + purgeLinkedVolumeEntities(volumeIds, batchSize); + removed = volumeDao.expungeList(volumeIds); + totalRemoved += removed; + } while (batchSizeFinal > 0 && removed >= batchSizeFinal); + return totalRemoved; + } + + protected void purgeLinkedNicEntities(final List nicIds, final Long batchSize) { + if (CollectionUtils.isEmpty(nicIds)) { + return; + } + nicDetailsDao.batchExpungeForResources(nicIds, batchSize); + nicExtraDhcpOptionDao.expungeByNicList(nicIds, batchSize); + inlineLoadBalancerNicMapDao.expungeByNicList(nicIds, batchSize); + } + + protected long purgeVMNics(final List vmIds, final Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + int removed = 0; + long totalRemoved = 0; + final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); + do { + List nics = nicDao.searchRemovedByVms(vmIds, batchSize); + List nicIds = nics.stream().map(NicVO::getId).collect(Collectors.toList()); + purgeLinkedNicEntities(nicIds, batchSize); + removed = nicDao.expungeList(nicIds); + totalRemoved += removed; + } while (batchSizeFinal > 0 && removed >= batchSizeFinal); + return totalRemoved; + } + + protected long purgeVMSnapshots(final List vmIds, final Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return 0; + } + int removed = 0; + long totalRemoved = 0; + final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); + do { + List vmSnapshots = vmSnapshotDao.searchRemovedByVms(vmIds, batchSize); + List ids = vmSnapshots.stream().map(VMSnapshotVO::getId).collect(Collectors.toList()); + vmSnapshotDetailsDao.batchExpungeForResources(ids, batchSize); + removed = vmSnapshotDao.expungeList(ids); + totalRemoved += removed; + } while (batchSizeFinal > 0 && removed >= batchSizeFinal); + return totalRemoved; + } + + protected void purgeLinkedVMEntities(final List vmIds, final Long batchSize) { + if (CollectionUtils.isEmpty(vmIds)) { + return; + } + purgeVMVolumes(vmIds, batchSize); + purgeVMNics(vmIds, batchSize); + userVmDetailsDao.batchExpungeForResources(vmIds, batchSize); + purgeVMSnapshots(vmIds, batchSize); + autoScaleVmGroupVmMapDao.expungeByVmList(vmIds, batchSize); + commandExecLogDao.expungeByVmList(vmIds, batchSize); + networkOrchestrationService.expungeLbVmRefs(vmIds, batchSize); + loadBalancerVMMapDao.expungeByVmList(vmIds, batchSize); + nicSecondaryIpDao.expungeByVmList(vmIds, batchSize); + highAvailabilityManager.expungeWorkItemsByVmList(vmIds, batchSize); + itWorkDao.expungeByVmList(vmIds, batchSize); + opRouterMonitorServiceDao.expungeByVmList(vmIds, batchSize); + portForwardingRulesDao.expungeByVmList(vmIds, batchSize); + ipAddressDao.expungeByVmList(vmIds, batchSize); + vmWorkJobDao.expungeByVmList(vmIds, batchSize); + consoleSessionDao.expungeByVmList(vmIds, batchSize); + } + + protected HashSet getVmIdsWithActiveVolumeSnapshots(List vmIds) { + if (CollectionUtils.isEmpty(vmIds)) { + return new HashSet<>(); + } + List volumes = volumeDao.searchRemovedByVms(vmIds, null); + List volumeIds = volumes.stream().map(VolumeVO::getId).collect(Collectors.toList()); + List activeSnapshots = snapshotDao.searchByVolumes(volumeIds); + HashSet activeSnapshotVolumeIds = + activeSnapshots.stream().map(SnapshotVO::getVolumeId).collect(Collectors.toCollection(HashSet::new)); + List volumesWithActiveSnapshots = + volumes.stream().filter(v -> activeSnapshotVolumeIds.contains(v.getId())).collect(Collectors.toList()); + return volumesWithActiveSnapshots.stream().map(VolumeVO::getInstanceId) + .collect(Collectors.toCollection(HashSet::new)); + } + + protected Pair, List> getFilteredVmIdsForSnapshots(List vmIds) { + HashSet currentSkippedVmIds = new HashSet<>(); + List activeSnapshots = vmSnapshotDao.searchByVms(vmIds); + if (CollectionUtils.isNotEmpty(activeSnapshots)) { + HashSet vmIdsWithActiveSnapshots = activeSnapshots.stream().map(VMSnapshotVO::getVmId) + .collect(Collectors.toCollection(HashSet::new)); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Skipping purging VMs with IDs %s as they have active " + + "VM snapshots", StringUtils.join(vmIdsWithActiveSnapshots))); + } + currentSkippedVmIds.addAll(vmIdsWithActiveSnapshots); + } + HashSet vmIdsWithActiveVolumeSnapshots = getVmIdsWithActiveVolumeSnapshots(vmIds); + if (CollectionUtils.isNotEmpty(vmIdsWithActiveVolumeSnapshots)) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Skipping purging VMs with IDs %s as they have volumes with active " + + "snapshots", StringUtils.join(vmIdsWithActiveVolumeSnapshots))); + } + currentSkippedVmIds.addAll(vmIdsWithActiveVolumeSnapshots); + } + if (CollectionUtils.isNotEmpty(currentSkippedVmIds)) { + vmIds.removeAll(currentSkippedVmIds); + } + return new Pair<>(vmIds, new ArrayList<>(currentSkippedVmIds)); + } + + protected Pair, List> getVmIdsWithNoActiveSnapshots(final Date startDate, final Date endDate, + final Long batchSize, final List skippedVmIds) { + List vms = vmInstanceDao.searchRemovedByRemoveDate(startDate, endDate, batchSize, skippedVmIds); + if (CollectionUtils.isEmpty(vms)) { + return new Pair<>(new ArrayList<>(), new ArrayList<>()); + } + List vmIds = vms.stream().map(VMInstanceVO::getId).collect(Collectors.toList()); + return getFilteredVmIdsForSnapshots(vmIds); + } + + protected long purgeVMEntities(final Long batchSize, final Date startDate, final Date endDate) { + return Transaction.execute((TransactionCallbackWithException) status -> { + int count; + long totalRemoved = 0; + final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); + List skippedVmIds = new ArrayList<>(); + do { + Pair, List> allVmIds = + getVmIdsWithNoActiveSnapshots(startDate, endDate, batchSize, skippedVmIds); + List vmIds = allVmIds.first(); + List currentSkippedVmIds = allVmIds.second(); + count = vmIds.size() + currentSkippedVmIds.size(); + skippedVmIds.addAll(currentSkippedVmIds); + purgeLinkedVMEntities(vmIds, batchSize); + totalRemoved += vmInstanceDao.expungeList(vmIds); + } while (batchSizeFinal > 0 && count >= batchSizeFinal); + if (logger.isTraceEnabled()) { + logger.trace(String.format("Purged total %d VM records", totalRemoved)); + } + return totalRemoved; + }); + } + + protected boolean purgeVMEntity(final long vmId) { + return Transaction.execute((TransactionCallbackWithException) status -> { + final Long batchSize = ExpungedResourcesPurgeBatchSize.value().longValue(); + List vmIds = new ArrayList<>(); + vmIds.add(vmId); + Pair, List> allVmIds = getFilteredVmIdsForSnapshots(vmIds); + if (CollectionUtils.isEmpty(allVmIds.first())) { + return false; + } + purgeLinkedVMEntities(vmIds, batchSize); + return vmInstanceDao.expunge(vmId); + }); + } + + protected long purgeEntities(final List resourceTypes, final Long batchSize, + final Date startDate, final Date endDate) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Expunging entities with parameters - resourceType: %s, batchSize: %d, " + + "startDate: %s, endDate: %s", StringUtils.join(resourceTypes), batchSize, startDate, endDate)); + } + long totalPurged = 0; + if (CollectionUtils.isEmpty(resourceTypes) || resourceTypes.contains(ResourceType.VirtualMachine)) { + totalPurged += purgeVMEntities(batchSize, startDate, endDate); + } + return totalPurged; + } + + protected Void purgeExpungedResourcesCallback( + AsyncCallbackDispatcher callback, + PurgeExpungedResourcesContext context) { + PurgeExpungedResourcesResult result = callback.getResult(); + context.future.complete(result); + return null; + } + + protected ResourceType getResourceTypeAndValidatePurgeExpungedResourcesCmdParams(final String resourceTypeStr, + final Date startDate, final Date endDate, final Long batchSize) { + ResourceType resourceType = null; + if (StringUtils.isNotBlank(resourceTypeStr)) { + resourceType = EnumUtils.getEnumIgnoreCase(ResourceType.class, resourceTypeStr, null); + if (resourceType == null) { + throw new InvalidParameterValueException("Invalid resource type specified"); + } + } + if (batchSize != null && batchSize <= 0) { + throw new InvalidParameterValueException(String.format("Invalid %s specified", ApiConstants.BATCH_SIZE)); + } + if (endDate != null && startDate != null && endDate.before(startDate)) { + throw new InvalidParameterValueException(String.format("Invalid %s specified", ApiConstants.END_DATE)); + } + return resourceType; + } + + protected long purgeExpungedResourceUsingJob(final ResourceType resourceType, final Long batchSize, + final Date startDate, final Date endDate) { + AsyncCallFuture future = new AsyncCallFuture<>(); + PurgeExpungedResourcesContext context = + new PurgeExpungedResourcesContext<>(null, future); + AsyncCallbackDispatcher caller = + AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().purgeExpungedResourcesCallback(null, null)) + .setContext(context); + PurgeExpungedResourceThread job = new PurgeExpungedResourceThread(resourceType, batchSize, startDate, endDate, + caller); + purgeExpungedResourcesJobExecutor.submit(job); + long expungedCount; + try { + PurgeExpungedResourcesResult result = future.get(); + if (result.isFailed()) { + throw new CloudRuntimeException(String.format("Failed to purge expunged resources due to: %s", result.getResult())); + } + expungedCount = result.getPurgedCount(); + } catch (InterruptedException | ExecutionException e) { + logger.error(String.format("Failed to purge expunged resources due to: %s", e.getMessage()), e); + throw new CloudRuntimeException("Failed to purge expunged resources"); + } + return expungedCount; + } + + protected boolean isVmOfferingPurgeResourcesEnabled(long vmServiceOfferingId) { + String detail = + serviceOfferingDetailsDao.getDetail(vmServiceOfferingId, ServiceOffering.PURGE_DB_ENTITIES_KEY); + return StringUtils.isNotBlank(detail) && Boolean.parseBoolean(detail); + } + + protected boolean purgeExpungedResource(long resourceId, ResourceType resourceType) { + if (!ResourceType.VirtualMachine.equals(resourceType)) { + return false; + } + return purgeVMEntity(resourceId); + } + + protected void purgeExpungedResourceLater(long resourceId, ResourceType resourceType) { + AsyncCallFuture future = new AsyncCallFuture<>(); + PurgeExpungedResourcesContext context = + new PurgeExpungedResourcesContext<>(null, future); + AsyncCallbackDispatcher caller = + AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().purgeExpungedResourcesCallback(null, null)) + .setContext(context); + PurgeExpungedResourceThread job = new PurgeExpungedResourceThread(resourceId, resourceType, caller); + purgeExpungedResourcesJobExecutor.submit(job); + } + + protected Date parseDateFromConfig(String configKey, String configValue) { + if (StringUtils.isBlank(configValue)) { + return null; + } + final List dateFormats = List.of("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd"); + Date date = null; + for (String format : dateFormats) { + final SimpleDateFormat dateFormat = new SimpleDateFormat(format); + try { + date = dateFormat.parse(configValue); + break; + } catch (ParseException e) { + logger.trace(String.format("Unable to parse value for config %s: %s with date " + + "format: %s due to %s", configKey, configValue, format, e.getMessage())); + } + } + if (date == null) { + throw new CloudRuntimeException(String.format("Unable to parse value for config %s: %s with date " + + "formats: %s", configKey, configValue, StringUtils.join(dateFormats))); + } + return date; + } + + protected Date getStartDateFromConfig() { + return parseDateFromConfig(ExpungedResourcesPurgeStartTime.key(), ExpungedResourcesPurgeStartTime.value()); + } + + protected Date calculatePastDateFromConfig(String configKey, Integer configValue) { + if (configValue == null || configValue == 0) { + return null; + } + if (configValue < 0) { + throw new CloudRuntimeException(String.format("Unable to retrieve a valid value for config %s: %s", + configKey, configValue)); + } + Calendar cal = Calendar.getInstance(); + Date endDate = new Date(); + cal.setTime(endDate); + cal.add(Calendar.DATE, -1 * configValue); + return cal.getTime(); + } + + protected Date getEndDateFromConfig() { + return calculatePastDateFromConfig(ExpungedResourcesPurgeKeepPastDays.key(), + ExpungedResourcesPurgeKeepPastDays.value()); + } + + protected List getResourceTypesFromConfig() { + String resourceTypesConfig = ExpungedResourcePurgeResources.value(); + if (StringUtils.isBlank(resourceTypesConfig)) { + return null; + } + List resourceTypes = new ArrayList<>(); + for (String type : resourceTypesConfig.split(",")) { + ResourceType resourceType = EnumUtils.getEnum(ResourceType.class, type.trim(), null); + if (resourceType == null) { + throw new CloudRuntimeException(String.format("Invalid resource type: '%s' specified in " + + "the config: %s", type, ExpungedResourcePurgeResources.key())); + } + resourceTypes.add(resourceType); + } + return resourceTypes; + } + + protected long getBatchSizeFromConfig() { + Integer batchSize = ExpungedResourcesPurgeBatchSize.value(); + if (batchSize == null || batchSize <= 0) { + throw new CloudRuntimeException(String.format("Unable to retrieve a valid value for config %s: %s", + ExpungedResourcesPurgeBatchSize.key(), batchSize)); + } + return batchSize.longValue(); + } + + @Override + public long purgeExpungedResources(PurgeExpungedResourcesCmd cmd) { + final String resourceTypeStr = cmd.getResourceType(); + final Date startDate = cmd.getStartDate(); + final Date endDate = cmd.getEndDate(); + Long batchSize = cmd.getBatchSize(); + ResourceType resourceType = getResourceTypeAndValidatePurgeExpungedResourcesCmdParams(resourceTypeStr, + startDate, endDate, batchSize); + Integer globalBatchSize = ExpungedResourcesPurgeBatchSize.value(); + if (batchSize == null && globalBatchSize > 0) { + batchSize = globalBatchSize.longValue(); + } + long expungedCount = purgeExpungedResourceUsingJob(resourceType, batchSize, startDate, endDate); + if (expungedCount <= 0) { + logger.debug("No resource expunged during purgeExpungedResources execution"); + } + return expungedCount; + } + + @Override + public void purgeExpungedVmResourcesLaterIfNeeded(VirtualMachine vm) { + if (!isVmOfferingPurgeResourcesEnabled(vm.getServiceOfferingId())) { + return; + } + purgeExpungedResourceLater(vm.getId(), ResourceType.VirtualMachine); + } + + @Override + public boolean start() { + if (Boolean.TRUE.equals(ExpungedResourcePurgeEnabled.value())) { + expungedResourcesCleanupExecutor = new ScheduledThreadPoolExecutor(1, + new NamedThreadFactory("ExpungedResourceCleanupWorker")); + expungedResourcesCleanupExecutor.scheduleWithFixedDelay(new ExpungedResourceCleanupWorker(), + ExpungedResourcesPurgeDelay.value(), ExpungedResourcesPurgeInterval.value(), TimeUnit.SECONDS); + } + purgeExpungedResourcesJobExecutor = Executors.newFixedThreadPool(3, + new NamedThreadFactory("Purge-Expunged-Resources-Job-Executor")); + return true; + } + + @Override + public boolean stop() { + purgeExpungedResourcesJobExecutor.shutdown(); + expungedResourcesCleanupExecutor.shutdownNow(); + return true; + } + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList<>(); + cmdList.add(PurgeExpungedResourcesCmd.class); + return cmdList; + } + + @Override + public String getConfigComponentName() { + return ResourceCleanupService.class.getName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + ExpungedResourcePurgeEnabled, + ExpungedResourcePurgeResources, + ExpungedResourcesPurgeInterval, + ExpungedResourcesPurgeDelay, + ExpungedResourcesPurgeBatchSize, + ExpungedResourcesPurgeStartTime, + ExpungedResourcesPurgeKeepPastDays, + ExpungedResourcePurgeJobDelay + }; + } + + public class ExpungedResourceCleanupWorker extends ManagedContextRunnable { + @Override + protected void runInContext() { + GlobalLock gcLock = GlobalLock.getInternLock("Expunged.Resource.Cleanup.Lock"); + try { + if (gcLock.lock(3)) { + try { + runCleanupForLongestRunningManagementServer(); + } finally { + gcLock.unlock(); + } + } + } finally { + gcLock.releaseRef(); + } + } + + protected void runCleanupForLongestRunningManagementServer() { + ManagementServerHostVO msHost = managementServerHostDao.findOneByLongestRuntime(); + if (msHost == null || (msHost.getMsid() != ManagementServerNode.getManagementServerId())) { + logger.debug("Skipping the expunged resource cleanup task on this management server"); + return; + } + reallyRun(); + } + + public void reallyRun() { + try { + Date startDate = getStartDateFromConfig(); + Date endDate = getEndDateFromConfig(); + List resourceTypes = getResourceTypesFromConfig(); + long batchSize = getBatchSizeFromConfig(); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Purging resources: %s as part of cleanup with start date: %s, " + + "end date: %s and batch size: %d", StringUtils.join(resourceTypes), startDate, endDate, batchSize)); + } + purgeEntities(resourceTypes, batchSize, startDate, endDate); + } catch (Exception e) { + logger.warn("Caught exception while running expunged resources cleanup task: ", e); + } + } + } + + protected class PurgeExpungedResourceThread extends ManagedContextRunnable { + ResourceType resourceType; + Long resourceId; + Long batchSize; + Date startDate; + Date endDate; + AsyncCompletionCallback callback; + long taskTimestamp; + + public PurgeExpungedResourceThread(final ResourceType resourceType, final Long batchSize, + final Date startDate, final Date endDate, + AsyncCompletionCallback callback) { + this.resourceType = resourceType; + this.batchSize = batchSize; + this.startDate = startDate; + this.endDate = endDate; + this.callback = callback; + } + + public PurgeExpungedResourceThread(final Long resourceId, final ResourceType resourceType, + AsyncCompletionCallback callback) { + this.resourceType = resourceType; + this.resourceId = resourceId; + this.callback = callback; + this.taskTimestamp = System.currentTimeMillis(); + } + + @Override + protected void runInContext() { + logger.trace(String.format("Executing purge for resource type: %s with batch size: %d start: %s, end: %s", + resourceType, batchSize, startDate, endDate)); + reallyRun(); + } + + protected void waitForPurgeSingleResourceDelay(String resourceAsString) throws InterruptedException { + long jobDelayConfig = ExpungedResourcePurgeJobDelay.value(); + if (jobDelayConfig < MINIMUM_EXPUNGED_RESOURCE_PURGE_JOB_DELAY_IN_SECONDS) { + logger.debug(String.format("Value: %d for config: %s is lesser than the minimum value: %d, " + + "using minimum value", + jobDelayConfig, + ExpungedResourcePurgeJobDelay.key(), + MINIMUM_EXPUNGED_RESOURCE_PURGE_JOB_DELAY_IN_SECONDS)); + jobDelayConfig = MINIMUM_EXPUNGED_RESOURCE_PURGE_JOB_DELAY_IN_SECONDS; + } + long delay = (jobDelayConfig * 1000) - + (System.currentTimeMillis() - taskTimestamp); + + if (delay > 0) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Waiting for %d before purging %s", delay, resourceAsString)); + } + Thread.sleep(delay); + } + } + + protected void purgeSingleResource() { + String resourceAsString = String.format("resource [type: %s, ID: %d]", resourceType, resourceId); + try { + waitForPurgeSingleResourceDelay(resourceAsString); + if (!purgeExpungedResource(resourceId, resourceType)) { + throw new CloudRuntimeException(String.format("Failed to purge %s", resourceAsString)); + } + if (logger.isDebugEnabled()) { + logger.info(String.format("Purged %s", resourceAsString)); + } + callback.complete(new PurgeExpungedResourcesResult(resourceId, resourceType, null)); + } catch (CloudRuntimeException e) { + logger.error(String.format("Caught exception while purging %s: ", resourceAsString), e); + callback.complete(new PurgeExpungedResourcesResult(resourceId, resourceType, e.getMessage())); + } catch (InterruptedException e) { + logger.error(String.format("Caught exception while waiting for purging %s: ", resourceAsString), e); + callback.complete(new PurgeExpungedResourcesResult(resourceId, resourceType, e.getMessage())); + } + } + + protected void purgeMultipleResources() { + try { + long purged = purgeEntities(resourceType == null ? null : List.of(resourceType), + batchSize, startDate, endDate); + callback.complete(new PurgeExpungedResourcesResult(resourceType, batchSize, startDate, endDate, purged)); + } catch (CloudRuntimeException e) { + logger.error("Caught exception while expunging resources: ", e); + callback.complete(new PurgeExpungedResourcesResult(resourceType, batchSize, startDate, endDate, e.getMessage())); + } + } + + public void reallyRun() { + if (resourceId != null) { + purgeSingleResource(); + return; + } + purgeMultipleResources(); + } + } + + public static class PurgeExpungedResourcesResult extends CommandResult { + ResourceType resourceType; + Long resourceId; + Long batchSize; + Date startDate; + Date endDate; + Long purgedCount; + + public PurgeExpungedResourcesResult(final ResourceType resourceType, final Long batchSize, + final Date startDate, final Date endDate, final long purgedCount) { + super(); + this.resourceType = resourceType; + this.batchSize = batchSize; + this.startDate = startDate; + this.endDate = endDate; + this.purgedCount = purgedCount; + this.setSuccess(true); + } + + public PurgeExpungedResourcesResult(final ResourceType resourceType, final Long batchSize, + final Date startDate, final Date endDate, final String error) { + super(); + this.resourceType = resourceType; + this.batchSize = batchSize; + this.startDate = startDate; + this.endDate = endDate; + this.setResult(error); + } + + public PurgeExpungedResourcesResult(final Long resourceId, final ResourceType resourceType, + final String error) { + super(); + this.resourceId = resourceId; + this.resourceType = resourceType; + if (error != null) { + this.setResult(error); + } else { + this.purgedCount = 1L; + this.setSuccess(true); + } + } + + public ResourceType getResourceType() { + return resourceType; + } + + public Long getResourceId() { + return resourceId; + } + + public Long getBatchSize() { + return batchSize; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Long getPurgedCount() { + return purgedCount; + } + } + + public static class PurgeExpungedResourcesContext extends AsyncRpcContext { + final AsyncCallFuture future; + + public PurgeExpungedResourcesContext(AsyncCompletionCallback callback, + AsyncCallFuture future) { + super(callback); + this.future = future; + } + + } +} 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 a9db15979c9..8e1871125e0 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 @@ -81,6 +81,8 @@ value="#{resourceDiscoverersRegistry.registered}" /> + + diff --git a/server/src/test/java/com/cloud/ha/dao/HighAvailabilityDaoImplTest.java b/server/src/test/java/com/cloud/ha/dao/HighAvailabilityDaoImplTest.java new file mode 100644 index 00000000000..783497740fd --- /dev/null +++ b/server/src/test/java/com/cloud/ha/dao/HighAvailabilityDaoImplTest.java @@ -0,0 +1,68 @@ +// 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 com.cloud.ha.dao; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.ha.HaWorkVO; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class HighAvailabilityDaoImplTest { + + @Spy + HighAvailabilityDaoImpl highAvailabilityDaoImpl; + + @Test + public void testExpungeByVmListNoVms() { + Assert.assertEquals(0, highAvailabilityDaoImpl.expungeByVmList( + new ArrayList<>(), 100L)); + Assert.assertEquals(0, highAvailabilityDaoImpl.expungeByVmList( + null, 100L)); + } + + @Test + public void testExpungeByVmList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doAnswer((Answer) invocationOnMock -> { + Long batchSize = (Long)invocationOnMock.getArguments()[1]; + return batchSize == null ? 0 : batchSize.intValue(); + }).when(highAvailabilityDaoImpl).batchExpunge(Mockito.any(SearchCriteria.class), Mockito.anyLong()); + Mockito.when(highAvailabilityDaoImpl.createSearchBuilder()).thenReturn(sb); + final HaWorkVO mockedVO = Mockito.mock(HaWorkVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + List vmIds = List.of(1L, 2L); + Object[] array = vmIds.toArray(); + Long batchSize = 50L; + Assert.assertEquals(batchSize.intValue(), highAvailabilityDaoImpl.expungeByVmList(List.of(1L, 2L), batchSize)); + Mockito.verify(sc).setParameters("vmIds", array); + Mockito.verify(highAvailabilityDaoImpl, Mockito.times(1)) + .batchExpunge(sc, batchSize); + } +} diff --git a/server/src/test/java/com/cloud/user/MockUsageEventDao.java b/server/src/test/java/com/cloud/user/MockUsageEventDao.java index c393291476f..1e777855a4a 100644 --- a/server/src/test/java/com/cloud/user/MockUsageEventDao.java +++ b/server/src/test/java/com/cloud/user/MockUsageEventDao.java @@ -328,4 +328,19 @@ public class MockUsageEventDao implements UsageEventDao{ public Pair, Integer> searchAndCount(SearchCriteria sc, Filter filter, boolean includeRemoved) { return null; } + + @Override + public int expunge(SearchCriteria sc, Filter filter) { + return 0; + } + + @Override + public int batchExpunge(SearchCriteria sc, Long batchSize) { + return 0; + } + + @Override + public int expungeList(List longs) { + return 0; + } } diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index 0c7af09d69e..9c04e051be5 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -1060,4 +1060,8 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches @Override public void validateIfServiceOfferingIsActiveAndSystemVmTypeIsDomainRouter(final Long serviceOfferingId) { } + + @Override + public void expungeLbVmRefs(List vmIds, Long batchSize) { + } } diff --git a/server/src/test/java/org/apache/cloudstack/resource/ResourceCleanupServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/resource/ResourceCleanupServiceImplTest.java new file mode 100644 index 00000000000..7446e290488 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/resource/ResourceCleanupServiceImplTest.java @@ -0,0 +1,656 @@ +// 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.resource; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.cloudstack.api.command.admin.resource.PurgeExpungedResourcesCmd; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.jobs.dao.VmWorkJobDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; +import org.apache.commons.collections.CollectionUtils; +import org.junit.Assert; +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 com.cloud.exception.InvalidParameterValueException; +import com.cloud.ha.HighAvailabilityManager; +import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.InlineLoadBalancerNicMapDao; +import com.cloud.network.dao.LoadBalancerVMMapDao; +import com.cloud.network.dao.OpRouterMonitorServiceDao; +import com.cloud.network.rules.dao.PortForwardingRulesDao; +import com.cloud.offering.ServiceOffering; +import com.cloud.secstorage.CommandExecLogDao; +import com.cloud.service.dao.ServiceOfferingDetailsDao; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.utils.Pair; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.ItWorkDao; +import com.cloud.vm.NicVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.ConsoleSessionDao; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicDetailsDao; +import com.cloud.vm.dao.NicExtraDhcpOptionDao; +import com.cloud.vm.dao.NicSecondaryIpDao; +import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; + +@RunWith(MockitoJUnitRunner.class) +public class ResourceCleanupServiceImplTest { + + @Mock + VMInstanceDao vmInstanceDao; + @Mock + VolumeDao volumeDao; + @Mock + VolumeDetailsDao volumeDetailsDao; + @Mock + VolumeDataStoreDao volumeDataStoreDao; + @Mock + SnapshotDao snapshotDao; + @Mock + SnapshotDetailsDao snapshotDetailsDao; + @Mock + SnapshotDataStoreDao snapshotDataStoreDao; + @Mock + NicDao nicDao; + @Mock + NicDetailsDao nicDetailsDao; + @Mock + NicExtraDhcpOptionDao nicExtraDhcpOptionDao; + @Mock + InlineLoadBalancerNicMapDao inlineLoadBalancerNicMapDao; + @Mock + VMSnapshotDao vmSnapshotDao; + @Mock + VMSnapshotDetailsDao vmSnapshotDetailsDao; + @Mock + UserVmDetailsDao userVmDetailsDao; + @Mock + AutoScaleVmGroupVmMapDao autoScaleVmGroupVmMapDao; + @Mock + CommandExecLogDao commandExecLogDao; + @Mock + NetworkOrchestrationService networkOrchestrationService; + @Mock + LoadBalancerVMMapDao loadBalancerVMMapDao; + @Mock + NicSecondaryIpDao nicSecondaryIpDao; + @Mock + HighAvailabilityManager highAvailabilityManager; + @Mock + ItWorkDao itWorkDao; + @Mock + OpRouterMonitorServiceDao opRouterMonitorServiceDao; + @Mock + PortForwardingRulesDao portForwardingRulesDao; + @Mock + IPAddressDao ipAddressDao; + @Mock + VmWorkJobDao vmWorkJobDao; + @Mock + ConsoleSessionDao consoleSessionDao; + @Mock + ServiceOfferingDetailsDao serviceOfferingDetailsDao; + + @Spy + @InjectMocks + ResourceCleanupServiceImpl resourceCleanupService = Mockito.spy(new ResourceCleanupServiceImpl()); + + List ids = List.of(1L, 2L); + Long batchSize = 100L; + + private void overrideConfigValue(final ConfigKey configKey, final Object value) { + try { + Field f = ConfigKey.class.getDeclaredField("_value"); + f.setAccessible(true); + f.set(configKey, value); + } catch (IllegalAccessException | NoSuchFieldException e) { + Assert.fail(e.getMessage()); + } + } + + + @Test + public void testPurgeLinkedSnapshotEntitiesNoSnapshots() { + resourceCleanupService.purgeLinkedSnapshotEntities(new ArrayList<>(), batchSize); + Mockito.verify(snapshotDetailsDao, Mockito.never()) + .batchExpungeForResources(Mockito.anyList(), Mockito.anyLong()); + Mockito.verify(snapshotDataStoreDao, Mockito.never()) + .expungeBySnapshotList(Mockito.anyList(), Mockito.anyLong()); + } + + + @Test + public void testPurgeLinkedSnapshotEntities() { + Mockito.when(snapshotDetailsDao.batchExpungeForResources(ids, batchSize)).thenReturn(2L); + Mockito.when(snapshotDataStoreDao.expungeBySnapshotList(ids, batchSize)).thenReturn(2); + resourceCleanupService.purgeLinkedSnapshotEntities(ids, batchSize); + Mockito.verify(snapshotDetailsDao, Mockito.times(1)) + .batchExpungeForResources(ids, batchSize); + Mockito.verify(snapshotDataStoreDao, Mockito.times(1)) + .expungeBySnapshotList(ids, batchSize); + } + + @Test + public void testPurgeVolumeSnapshotsNoVolumes() { + Assert.assertEquals(0, resourceCleanupService.purgeVolumeSnapshots(new ArrayList<>(), 50L)); + Mockito.verify(snapshotDao, Mockito.never()).createSearchBuilder(); + } + + @Test + public void testPurgeVolumeSnapshots() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + Mockito.when(sb.entity()).thenReturn(Mockito.mock(SnapshotVO.class)); + Mockito.when(sb.create()).thenReturn(Mockito.mock(SearchCriteria.class)); + Mockito.when(snapshotDao.createSearchBuilder()).thenReturn(sb); + Assert.assertEquals(0, resourceCleanupService.purgeVolumeSnapshots(new ArrayList<>(), 50L)); + Mockito.when(snapshotDao.searchIncludingRemoved(Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyBoolean())) + .thenReturn(List.of(Mockito.mock(SnapshotVO.class), Mockito.mock(SnapshotVO.class))); + Mockito.when(snapshotDao.expungeList(Mockito.anyList())).thenReturn(2); + Assert.assertEquals(2, resourceCleanupService.purgeVolumeSnapshots(ids, batchSize)); + } + + @Test + public void testPurgeLinkedVolumeEntitiesNoVolumes() { + resourceCleanupService.purgeLinkedVolumeEntities(new ArrayList<>(), 50L); + Mockito.verify(volumeDetailsDao, Mockito.never()).batchExpungeForResources(Mockito.anyList(), + Mockito.anyLong()); + } + + @Test + public void testPurgeLinkedVolumeEntities() { + Mockito.when(volumeDetailsDao.batchExpungeForResources(ids, batchSize)).thenReturn(2L); + Mockito.when(volumeDataStoreDao.expungeByVolumeList(ids, batchSize)).thenReturn(2); + Mockito.doReturn(2L).when(resourceCleanupService).purgeVolumeSnapshots(ids, batchSize); + resourceCleanupService.purgeLinkedVolumeEntities(ids, batchSize); + Mockito.verify(volumeDetailsDao, Mockito.times(1)) + .batchExpungeForResources(ids, batchSize); + Mockito.verify(volumeDataStoreDao, Mockito.times(1)) + .expungeByVolumeList(ids, batchSize); + Mockito.verify(resourceCleanupService, Mockito.times(1)) + .purgeVolumeSnapshots(ids, batchSize); + } + + @Test + public void testPurgeVMVolumesNoVms() { + Assert.assertEquals(0, resourceCleanupService.purgeVMVolumes(new ArrayList<>(), 50L)); + Mockito.verify(volumeDao, Mockito.never()).searchRemovedByVms(Mockito.anyList(), Mockito.anyLong()); + } + + @Test + public void testPurgeVMVolumes() { + Mockito.when(volumeDao.searchRemovedByVms(ids, batchSize)) + .thenReturn(List.of(Mockito.mock(VolumeVO.class), Mockito.mock(VolumeVO.class))); + Mockito.when(volumeDao.expungeList(Mockito.anyList())).thenReturn(2); + Mockito.doNothing().when(resourceCleanupService).purgeLinkedVolumeEntities(Mockito.anyList(), + Mockito.eq(batchSize)); + Assert.assertEquals(2, resourceCleanupService.purgeVMVolumes(ids, batchSize)); + } + + @Test + public void testPurgeLinkedNicEntitiesNoNics() { + resourceCleanupService.purgeLinkedNicEntities(new ArrayList<>(), batchSize); + Mockito.verify(nicDetailsDao, Mockito.never()) + .batchExpungeForResources(ids, batchSize); + Mockito.verify(nicExtraDhcpOptionDao, Mockito.never()) + .expungeByNicList(ids, batchSize); + Mockito.verify(inlineLoadBalancerNicMapDao, Mockito.never()) + .expungeByNicList(ids, batchSize); + } + + @Test + public void testPurgeLinkedNicEntities() { + Mockito.when(nicDetailsDao.batchExpungeForResources(ids, batchSize)).thenReturn(2L); + Mockito.when(nicExtraDhcpOptionDao.expungeByNicList(ids, batchSize)).thenReturn(2); + Mockito.when(inlineLoadBalancerNicMapDao.expungeByNicList(ids, batchSize)).thenReturn(2); + resourceCleanupService.purgeLinkedNicEntities(ids, batchSize); + Mockito.verify(nicDetailsDao, Mockito.times(1)) + .batchExpungeForResources(ids, batchSize); + Mockito.verify(nicExtraDhcpOptionDao, Mockito.times(1)) + .expungeByNicList(ids, batchSize); + Mockito.verify(inlineLoadBalancerNicMapDao, Mockito.times(1)) + .expungeByNicList(ids, batchSize); + } + + @Test + public void testPurgeVMNicsNoVms() { + Assert.assertEquals(0, resourceCleanupService.purgeVMNics(new ArrayList<>(), 50L)); + Mockito.verify(nicDao, Mockito.never()).searchRemovedByVms(Mockito.anyList(), Mockito.anyLong()); + } + + @Test + public void testPurgeVMNics() { + Mockito.when(nicDao.searchRemovedByVms(ids, batchSize)) + .thenReturn(List.of(Mockito.mock(NicVO.class), Mockito.mock(NicVO.class))); + Mockito.when(nicDao.expungeList(Mockito.anyList())).thenReturn(2); + Mockito.doNothing().when(resourceCleanupService).purgeLinkedNicEntities(Mockito.anyList(), + Mockito.eq(batchSize)); + Assert.assertEquals(2, resourceCleanupService.purgeVMNics(ids, batchSize)); + } + + @Test + public void testPurgeVMSnapshotsNoVms() { + Assert.assertEquals(0, resourceCleanupService.purgeVMSnapshots(new ArrayList<>(), 50L)); + Mockito.verify(vmSnapshotDao, Mockito.never()).searchRemovedByVms(Mockito.anyList(), Mockito.anyLong()); + } + + @Test + public void testPurgeVMSnapshots() { + Mockito.when(vmSnapshotDao.searchRemovedByVms(ids, batchSize)) + .thenReturn(List.of(Mockito.mock(VMSnapshotVO.class), Mockito.mock(VMSnapshotVO.class))); + Mockito.when(vmSnapshotDao.expungeList(Mockito.anyList())).thenReturn(2); + Mockito.when(vmSnapshotDetailsDao.batchExpungeForResources(Mockito.anyList(), + Mockito.eq(batchSize))).thenReturn(2L); + Assert.assertEquals(2, resourceCleanupService.purgeVMSnapshots(ids, batchSize)); + } + + @Test + public void testPurgeLinkedVMEntitiesNoVms() { + resourceCleanupService.purgeLinkedVMEntities(new ArrayList<>(), 50L); + Mockito.verify(resourceCleanupService, Mockito.never()).purgeVMVolumes(Mockito.anyList(), + Mockito.anyLong()); + Mockito.verify(userVmDetailsDao, Mockito.never()) + .batchExpungeForResources(Mockito.anyList(), Mockito.anyLong()); + } + + @Test + public void testPurgeLinkedVMEntities() { + Mockito.doReturn(2L).when(resourceCleanupService).purgeVMVolumes(Mockito.anyList(), + Mockito.eq(batchSize)); + Mockito.doReturn(2L).when(resourceCleanupService).purgeVMNics(Mockito.anyList(), + Mockito.eq(batchSize)); + Mockito.when(userVmDetailsDao.batchExpungeForResources(Mockito.anyList(), Mockito.anyLong())).thenReturn(2L); + Mockito.doReturn(2L).when(resourceCleanupService).purgeVMSnapshots(Mockito.anyList(), + Mockito.eq(batchSize)); + Mockito.when(autoScaleVmGroupVmMapDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(commandExecLogDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(loadBalancerVMMapDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(nicSecondaryIpDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(highAvailabilityManager.expungeWorkItemsByVmList(Mockito.anyList(), Mockito.anyLong())) + .thenReturn(2); + Mockito.when(itWorkDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(opRouterMonitorServiceDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(portForwardingRulesDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(ipAddressDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(vmWorkJobDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + Mockito.when(consoleSessionDao.expungeByVmList(Mockito.anyList(), Mockito.anyLong())).thenReturn(2); + + resourceCleanupService.purgeLinkedVMEntities(ids, batchSize); + + Mockito.verify(resourceCleanupService, Mockito.times(1)).purgeVMVolumes(ids, batchSize); + Mockito.verify(resourceCleanupService, Mockito.times(1)).purgeVMNics(ids, batchSize); + Mockito.verify(userVmDetailsDao, Mockito.times(1)) + .batchExpungeForResources(ids, batchSize); + Mockito.verify(resourceCleanupService, Mockito.times(1)) + .purgeVMSnapshots(ids, batchSize); + Mockito.verify(autoScaleVmGroupVmMapDao, Mockito.times(1)) + .expungeByVmList(ids, batchSize); + Mockito.verify(commandExecLogDao, Mockito.times(1)).expungeByVmList(ids, batchSize); + Mockito.verify(loadBalancerVMMapDao, Mockito.times(1)).expungeByVmList(ids, batchSize); + Mockito.verify(nicSecondaryIpDao, Mockito.times(1)).expungeByVmList(ids, batchSize); + Mockito.verify(highAvailabilityManager, Mockito.times(1)). + expungeWorkItemsByVmList(ids, batchSize); + Mockito.verify(itWorkDao, Mockito.times(1)).expungeByVmList(ids, batchSize); + Mockito.verify(opRouterMonitorServiceDao, Mockito.times(1)) + .expungeByVmList(ids, batchSize); + Mockito.verify(portForwardingRulesDao, Mockito.times(1)).expungeByVmList(ids, batchSize); + Mockito.verify(ipAddressDao, Mockito.times(1)).expungeByVmList(ids, batchSize); + Mockito.verify(vmWorkJobDao, Mockito.times(1)).expungeByVmList(ids, batchSize); + Mockito.verify(consoleSessionDao, Mockito.times(1)).expungeByVmList(ids, batchSize); + } + + @Test + public void testGetVmIdsWithActiveVolumeSnapshotsNoVms() { + Assert.assertTrue(CollectionUtils.isEmpty( + resourceCleanupService.getVmIdsWithActiveVolumeSnapshots(new ArrayList<>()))); + } + + @Test + public void testGetVmIdsWithActiveVolumeSnapshots() { + VolumeVO vol1 = Mockito.mock(VolumeVO.class); + Mockito.when(vol1.getId()).thenReturn(1L); + Mockito.when(vol1.getInstanceId()).thenReturn(1L); + VolumeVO vol2 = Mockito.mock(VolumeVO.class); + Mockito.when(vol2.getId()).thenReturn(2L); + Mockito.when(volumeDao.searchRemovedByVms(ids, null)).thenReturn(List.of(vol1, vol2)); + SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); + Mockito.when(snapshotVO.getVolumeId()).thenReturn(1L); + Mockito.when(snapshotDao.searchByVolumes(Mockito.anyList())).thenReturn(List.of(snapshotVO)); + HashSet vmIds = resourceCleanupService.getVmIdsWithActiveVolumeSnapshots(ids); + Assert.assertTrue(CollectionUtils.isNotEmpty(vmIds)); + Assert.assertEquals(1, vmIds.size()); + Assert.assertEquals(1L, vmIds.toArray()[0]); + } + + @Test + public void testGetFilteredVmIdsForSnapshots() { + Long skippedVmIds = ids.get(0); + Long notSkippedVmIds = ids.get(1); + VMSnapshotVO vmSnapshotVO = Mockito.mock(VMSnapshotVO.class); + Mockito.when(vmSnapshotVO.getVmId()).thenReturn(1L); + Mockito.when(vmSnapshotDao.searchByVms(Mockito.anyList())).thenReturn(List.of(vmSnapshotVO)); + HashSet set = new HashSet<>(); + set.add(1L); + Mockito.doReturn(set).when(resourceCleanupService).getVmIdsWithActiveVolumeSnapshots(ids); + Pair, List> result = resourceCleanupService.getFilteredVmIdsForSnapshots(new ArrayList<>(ids)); + Assert.assertEquals(1, result.first().size()); + Assert.assertEquals(1, result.second().size()); + Assert.assertEquals(notSkippedVmIds, result.first().get(0)); + Assert.assertEquals(skippedVmIds, result.second().get(0)); + } + + @Test + public void testGetVmIdsWithNoActiveSnapshots() { + VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm1.getId()).thenReturn(ids.get(0)); + VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm2.getId()).thenReturn(ids.get(1)); + Mockito.when(vmInstanceDao.searchRemovedByRemoveDate(Mockito.any(), Mockito.any(), + Mockito.anyLong(), Mockito.anyList())).thenReturn(List.of(vm1, vm2)); + Long skippedVmIds = ids.get(0); + Long notSkippedVmIds = ids.get(1); + VMSnapshotVO vmSnapshotVO = Mockito.mock(VMSnapshotVO.class); + Mockito.when(vmSnapshotVO.getVmId()).thenReturn(1L); + Mockito.when(vmSnapshotDao.searchByVms(Mockito.anyList())).thenReturn(List.of(vmSnapshotVO)); + HashSet set = new HashSet<>(); + set.add(1L); + Mockito.doReturn(set).when(resourceCleanupService).getVmIdsWithActiveVolumeSnapshots(Mockito.anyList()); + Pair, List> result = + resourceCleanupService.getVmIdsWithNoActiveSnapshots(new Date(), new Date(), batchSize, + new ArrayList<>()); + Assert.assertEquals(1, result.first().size()); + Assert.assertEquals(1, result.second().size()); + Assert.assertEquals(notSkippedVmIds, result.first().get(0)); + Assert.assertEquals(skippedVmIds, result.second().get(0)); + } + + @Test + public void testPurgeVMEntitiesNoVms() { + Mockito.when(vmInstanceDao.searchRemovedByRemoveDate(Mockito.any(), Mockito.any(), + Mockito.anyLong(), Mockito.anyList())).thenReturn(new ArrayList<>()); + Assert.assertEquals(0, resourceCleanupService.purgeVMEntities(batchSize, new Date(), new Date())); + } + + @Test + public void testPurgeVMEntities() { + Mockito.doReturn(new Pair<>(ids, new ArrayList<>())).when(resourceCleanupService) + .getVmIdsWithNoActiveSnapshots(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyList()); + Mockito.when(vmInstanceDao.expungeList(ids)).thenReturn(ids.size()); + Assert.assertEquals(ids.size(), resourceCleanupService.purgeVMEntities(batchSize, new Date(), new Date())); + } + + @Test + public void testExpungeVMEntityFiltered() { + Mockito.doReturn(new Pair<>(new ArrayList<>(), List.of(ids.get(0)))).when(resourceCleanupService) + .getFilteredVmIdsForSnapshots(Mockito.anyList()); + Assert.assertFalse(resourceCleanupService.purgeVMEntity(ids.get(0))); + } + + @Test + public void testPurgeVMEntityFiltered() { + Mockito.doReturn(new Pair<>(List.of(ids.get(0)), new ArrayList<>())).when(resourceCleanupService) + .getFilteredVmIdsForSnapshots(Mockito.anyList()); + Mockito.doNothing().when(resourceCleanupService) + .purgeLinkedVMEntities(Mockito.anyList(), Mockito.anyLong()); + Mockito.when(vmInstanceDao.expunge(ids.get(0))).thenReturn(true); + Assert.assertTrue(resourceCleanupService.purgeVMEntity(ids.get(0))); + } + + @Test + public void testPurgeVMEntity() { + Mockito.doReturn(new Pair<>(List.of(ids.get(0)), new ArrayList<>())).when(resourceCleanupService) + .getFilteredVmIdsForSnapshots(Mockito.anyList()); + Mockito.doNothing().when(resourceCleanupService) + .purgeLinkedVMEntities(Mockito.anyList(), Mockito.anyLong()); + Mockito.when(vmInstanceDao.expunge(ids.get(0))).thenReturn(true); + Assert.assertTrue(resourceCleanupService.purgeVMEntity(ids.get(0))); + } + + @Test + public void testPurgeEntities() { + Mockito.doReturn((long)ids.size()).when(resourceCleanupService) + .purgeVMEntities(Mockito.anyLong(), Mockito.any(), Mockito.any()); + long result = resourceCleanupService.purgeEntities( + List.of(ResourceCleanupService.ResourceType.VirtualMachine), batchSize, new Date(), new Date()); + Assert.assertEquals(ids.size(), result); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetResourceTypeAndValidatePurgeExpungedResourcesCmdParamsInvalidResourceType() { + resourceCleanupService.getResourceTypeAndValidatePurgeExpungedResourcesCmdParams("Volume", + new Date(), new Date(), batchSize); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetResourceTypeAndValidatePurgeExpungedResourcesCmdParamsInvalidBatchSize() { + resourceCleanupService.getResourceTypeAndValidatePurgeExpungedResourcesCmdParams( + ResourceCleanupService.ResourceType.VirtualMachine.toString(), + new Date(), new Date(), -1L); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetResourceTypeAndValidatePurgeExpungedResourcesCmdParamsInvalidDates() { + Calendar cal = Calendar.getInstance(); + Date startDate = new Date(); + cal.setTime(startDate); + cal.add(Calendar.DATE, -1); + Date endDate = cal.getTime(); + resourceCleanupService.getResourceTypeAndValidatePurgeExpungedResourcesCmdParams( + ResourceCleanupService.ResourceType.VirtualMachine.toString(), + startDate, endDate, 100L); + } + + @Test + public void testGetResourceTypeAndValidatePurgeExpungedResourcesCmdParams() { + Calendar cal = Calendar.getInstance(); + Date endDate = new Date(); + cal.setTime(endDate); + cal.add(Calendar.DATE, -1); + Date startDate = cal.getTime(); + ResourceCleanupService.ResourceType type = + resourceCleanupService.getResourceTypeAndValidatePurgeExpungedResourcesCmdParams( + ResourceCleanupService.ResourceType.VirtualMachine.toString(), + startDate, endDate, 100L); + Assert.assertEquals(ResourceCleanupService.ResourceType.VirtualMachine, type); + } + + @Test + public void testGetResourceTypeAndValidatePurgeExpungedResourcesCmdParamsNoValues() { + ResourceCleanupService.ResourceType type = + resourceCleanupService.getResourceTypeAndValidatePurgeExpungedResourcesCmdParams( + null, null, null, null); + Assert.assertNull(type); + } + + @Test + public void testIsVmOfferingPurgeResourcesEnabled() { + Mockito.when(serviceOfferingDetailsDao.getDetail(1L, + ServiceOffering.PURGE_DB_ENTITIES_KEY)).thenReturn(null); + Assert.assertFalse(resourceCleanupService.isVmOfferingPurgeResourcesEnabled(1L)); + Mockito.when(serviceOfferingDetailsDao.getDetail(2L, + ServiceOffering.PURGE_DB_ENTITIES_KEY)).thenReturn("false"); + Assert.assertFalse(resourceCleanupService.isVmOfferingPurgeResourcesEnabled(2L)); + Mockito.when(serviceOfferingDetailsDao.getDetail(3L, + ServiceOffering.PURGE_DB_ENTITIES_KEY)).thenReturn("true"); + Assert.assertTrue(resourceCleanupService.isVmOfferingPurgeResourcesEnabled(3L)); + } + + @Test + public void testPurgeExpungedResource() { + Assert.assertFalse(resourceCleanupService.purgeExpungedResource(1L, null)); + + Mockito.doReturn(true).when(resourceCleanupService) + .purgeExpungedResource(Mockito.anyLong(), Mockito.any()); + Assert.assertTrue(resourceCleanupService.purgeExpungedResource(1L, + ResourceCleanupService.ResourceType.VirtualMachine)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testPurgeExpungedResourcesInvalidResourceType() { + PurgeExpungedResourcesCmd cmd = Mockito.mock(PurgeExpungedResourcesCmd.class); + Mockito.when(cmd.getResourceType()).thenReturn("Volume"); + resourceCleanupService.purgeExpungedResources(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testPurgeExpungedResourcesInvalidBatchSize() { + PurgeExpungedResourcesCmd cmd = Mockito.mock(PurgeExpungedResourcesCmd.class); + Mockito.when(cmd.getBatchSize()).thenReturn(-1L); + resourceCleanupService.purgeExpungedResources(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testPurgeExpungedResourcesInvalidDates() { + Calendar cal = Calendar.getInstance(); + Date startDate = new Date(); + cal.setTime(startDate); + cal.add(Calendar.DATE, -1); + Date endDate = cal.getTime(); + PurgeExpungedResourcesCmd cmd = Mockito.mock(PurgeExpungedResourcesCmd.class); + Mockito.when(cmd.getStartDate()).thenReturn(startDate); + Mockito.when(cmd.getEndDate()).thenReturn(endDate); + resourceCleanupService.purgeExpungedResources(cmd); + } + + @Test + public void testPurgeExpungedResources() { + Mockito.doReturn((long)ids.size()).when(resourceCleanupService).purgeExpungedResourceUsingJob( + ResourceCleanupService.ResourceType.VirtualMachine, batchSize, null, null); + PurgeExpungedResourcesCmd cmd = Mockito.mock(PurgeExpungedResourcesCmd.class); + Mockito.when(cmd.getResourceType()).thenReturn(ResourceCleanupService.ResourceType.VirtualMachine.toString()); + Mockito.when(cmd.getBatchSize()).thenReturn(batchSize); + long result = resourceCleanupService.purgeExpungedResources(cmd); + Assert.assertEquals(ids.size(), result); + } + + @Test + public void testExpungedVmResourcesLaterIfNeededFalse() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + Mockito.when(vm.getServiceOfferingId()).thenReturn(1L); + Mockito.doReturn(false).when(resourceCleanupService).isVmOfferingPurgeResourcesEnabled(1L); + resourceCleanupService.purgeExpungedVmResourcesLaterIfNeeded(vm); + Mockito.verify(resourceCleanupService, Mockito.never()).purgeExpungedResourceLater(Mockito.anyLong(), Mockito.any()); + } + + @Test + public void testExpungedVmResourcesLaterIfNeeded() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + Mockito.when(vm.getServiceOfferingId()).thenReturn(1L); + Mockito.doReturn(true).when(resourceCleanupService).isVmOfferingPurgeResourcesEnabled(1L); + Mockito.doNothing().when(resourceCleanupService).purgeExpungedResourceLater(Mockito.anyLong(), Mockito.any()); + resourceCleanupService.purgeExpungedVmResourcesLaterIfNeeded(vm); + Mockito.verify(resourceCleanupService, Mockito.times(1)) + .purgeExpungedResourceLater(Mockito.anyLong(), Mockito.any()); + } + + @Test + public void testGetBatchSizeFromConfig() { + int value = 50; + overrideConfigValue(ResourceCleanupService.ExpungedResourcesPurgeBatchSize, String.valueOf(value)); + Assert.assertEquals(value, resourceCleanupService.getBatchSizeFromConfig()); + } + + @Test + public void testGetResourceTypesFromConfigEmpty() { + overrideConfigValue(ResourceCleanupService.ExpungedResourcePurgeResources, ""); + Assert.assertNull(resourceCleanupService.getResourceTypesFromConfig()); + } + + @Test + public void testGetResourceTypesFromConfig() { + overrideConfigValue(ResourceCleanupService.ExpungedResourcePurgeResources, "VirtualMachine"); + List types = resourceCleanupService.getResourceTypesFromConfig(); + Assert.assertEquals(1, types.size()); + } + + @Test + public void testCalculatePastDateFromConfigNull() { + Assert.assertNull(resourceCleanupService.calculatePastDateFromConfig( + ResourceCleanupService.ExpungedResourcesPurgeKeepPastDays.key(), + null)); + Assert.assertNull(resourceCleanupService.calculatePastDateFromConfig( + ResourceCleanupService.ExpungedResourcesPurgeKeepPastDays.key(), + 0)); + } + + @Test(expected = CloudRuntimeException.class) + public void testCalculatePastDateFromConfigFail() { + Assert.assertNull(resourceCleanupService.calculatePastDateFromConfig( + ResourceCleanupService.ExpungedResourcesPurgeKeepPastDays.key(), + -1)); + } + + @Test + public void testCalculatePastDateFromConfig() { + int days = 10; + Date result = resourceCleanupService.calculatePastDateFromConfig( + ResourceCleanupService.ExpungedResourcesPurgeKeepPastDays.key(), + days); + Date today = new Date(); + long diff = today.getTime() - result.getTime(); + Assert.assertEquals(days, TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)); + } + + @Test + public void testParseDateFromConfig() { + Assert.assertNull(resourceCleanupService.parseDateFromConfig( + ResourceCleanupService.ExpungedResourcesPurgeStartTime.key(), "")); + Date date = resourceCleanupService.parseDateFromConfig( + ResourceCleanupService.ExpungedResourcesPurgeStartTime.key(), "2020-01-01"); + Assert.assertNotNull(date); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + Assert.assertEquals(2020, calendar.get(Calendar.YEAR)); + Assert.assertEquals(0, calendar.get(Calendar.MONTH)); + Assert.assertEquals(1, calendar.get(Calendar.DATE)); + } + + @Test(expected = CloudRuntimeException.class) + public void testParseDateFromConfigFail() { + resourceCleanupService.parseDateFromConfig( + ResourceCleanupService.ExpungedResourcesPurgeStartTime.key(), "ABC"); + } +} diff --git a/test/integration/smoke/test_purge_expunged_vms.py b/test/integration/smoke/test_purge_expunged_vms.py new file mode 100644 index 00000000000..4d885dc68e1 --- /dev/null +++ b/test/integration/smoke/test_purge_expunged_vms.py @@ -0,0 +1,364 @@ +# 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. +""" BVT tests for purging expunged VMs and their resources +""" +# Import Local Modules +from marvin.codes import FAILED +from marvin.cloudstackAPI import (purgeExpungedResources, + listInfrastructure, + listManagementServers) +from marvin.cloudstackException import CloudstackAPIException +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (Account, + Domain, + ServiceOffering, + DiskOffering, + NetworkOffering, + Network, + VirtualMachine, + Configurations) +from marvin.lib.common import (get_domain, + get_zone, + get_template) +from marvin.lib.utils import (random_gen) +from marvin.lib.decoratorGenerators import skipTestIf +from marvin.sshClient import SshClient +from nose.plugins.attrib import attr +import logging +# Import System modules +import time +from datetime import datetime, timedelta +import pytz +import threading + + +_multiprocess_shared_ = True +DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + +class TestPurgeExpungedVms(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.testClient = super(TestPurgeExpungedVms, cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + cls.dbConnection = cls.testClient.getDbConnection() + + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.services['mode'] = cls.zone.networktype + + cls.mgtSvrDetails = cls.config.__dict__["mgtSvr"][0].__dict__ + + + cls.hypervisor = cls.testClient.getHypervisorInfo().lower() + cls.hypervisorIsSimulator = False + if cls.hypervisor == 'simulator': + cls.hypervisorIsSimulator = True + + cls._cleanup = [] + cls.logger = logging.getLogger('TestPurgeExpungedVms') + cls.logger.setLevel(logging.DEBUG) + + template = get_template( + cls.apiclient, + cls.zone.id, + cls.services["ostype"]) + if template == FAILED: + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + + # Set Zones and disk offerings + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["small"]["template"] = template.id + + cls.compute_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["tiny"]) + cls._cleanup.append(cls.compute_offering) + + cls.purge_resource_compute_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["tiny"], + purgeresources=True) + cls._cleanup.append(cls.purge_resource_compute_offering) + + cls.disk_offering = DiskOffering.create( + cls.apiclient, + cls.services["disk_offering"] + ) + cls._cleanup.append(cls.disk_offering) + + cls.network_offering = NetworkOffering.create( + cls.apiclient, + cls.services["l2-network_offering"], + ) + cls._cleanup.append(cls.network_offering) + cls.network_offering.update(cls.apiclient, state='Enabled') + cls.services["network"]["networkoffering"] = cls.network_offering.id + + cls.domain1 = Domain.create( + cls.apiclient, + cls.services["domain"]) + cls._cleanup.append(cls.domain1) + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain1.id) + cls._cleanup.append(cls.account) + cls.userapiclient = cls.testClient.getUserApiClient( + UserName=cls.account.name, + DomainName=cls.account.domain + ) + cls.l2_network = Network.create( + cls.userapiclient, + cls.services["l2-network"], + zoneid=cls.zone.id, + networkofferingid=cls.network_offering.id + ) + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + cls.services["virtual_machine"]["template"] = template.id + + @classmethod + def tearDownClass(cls): + super(TestPurgeExpungedVms, cls).tearDownClass() + + def updateVmCreatedRemovedInDb(self, vm_id, timestamp): + # Assuming DB is UTC + utc_timestamp = datetime.strptime(timestamp, DATETIME_FORMAT).astimezone(pytz.utc).strftime(DATETIME_FORMAT) + logging.info("Updating VM: %s created and removed in DB with timestamp: %s" % (vm_id, timestamp)) + query = "UPDATE cloud.vm_instance SET created='%s', removed='%s' WHERE uuid='%s'" % (utc_timestamp, utc_timestamp, vm_id) + self.dbConnection.execute(query) + + def setupExpungedVm(self, timestamp): + logging.info("Setting up expunged VM with timestamp: %s" % timestamp) + vm = VirtualMachine.create( + self.userapiclient, + self.services["virtual_machine"], + serviceofferingid=self.compute_offering.id, + networkids=self.l2_network.id + ) + self.cleanup.append(vm) + vm_id = vm.id + self.vm_ids[timestamp] = vm_id + vm.delete(self.apiclient, expunge=True) + self.cleanup.remove(vm) + self.updateVmCreatedRemovedInDb(vm_id, timestamp) + + def setupExpungedVms(self): + logging.info("Setup VMs") + self.vm_ids = {} + self.threads = [] + days = 3 + for i in range(days): + logging.info("Setting up expunged VMs for day: %d" % (i + 1)) + thread = threading.Thread(target=self.setupExpungedVm, args=(self.timestamps[i],)) + self.threads.append(thread) + thread.start() + + for index, thread in enumerate(self.threads): + logging.info("Before joining thread %d." % index) + thread.join() + logging.info("Thread %d done" % index) + + def setUp(self): + self.cleanup = [] + self.changedConfigurations = {} + self.staticConfigurations = [] + if 'service_offering' in self._testMethodName: + return + if 'background_task' in self._testMethodName and self.hypervisorIsSimulator: + return + self.days = 3 + self.timestamps = [] + for i in range(self.days): + days_ago = (self.days - i) * 2 + now = (datetime.now() - timedelta(days = days_ago)) + timestamp = now.strftime(DATETIME_FORMAT) + self.timestamps.append(timestamp) + self.setupExpungedVms() + + def tearDown(self): + restartServer = False + for config in self.changedConfigurations: + value = self.changedConfigurations[config] + logging.info("Reverting value of config: %s to %s" % (config, value)) + Configurations.update(self.apiclient, + config, + value=value) + if config in self.staticConfigurations: + restartServer = True + if restartServer: + self.restartAllManagementServers() + super(TestPurgeExpungedVms, self).tearDown() + + def executePurgeExpungedResources(self, start_date, end_date): + cmd = purgeExpungedResources.purgeExpungedResourcesCmd() + if start_date is not None: + cmd.startdate = start_date + if end_date is not None: + cmd.enddate = end_date + self.apiclient.purgeExpungedResources(cmd) + + def getVmsInDb(self, vm_ids): + vm_id_str = "','".join(vm_ids) + vm_id_str = "'" + vm_id_str + "'" + query = "SELECT * FROM cloud.vm_instance WHERE uuid IN (%s)" % vm_id_str + response = self.dbConnection.execute(query) + logging.info("DB response from VM: %s:: %s" % (vm_id_str, response)) + return response + + def validatePurgedVmEntriesInDb(self, purged, not_purged): + if purged is not None: + response = self.getVmsInDb(purged) + self.assertTrue(response is None or len(response) == 0, + "Purged VMs still present in DB") + if not_purged is not None: + response = self.getVmsInDb(not_purged) + self.assertTrue(response is not None or len(response) == len(not_purged), + "Not purged VM not present in DB") + + def changeConfiguration(self, name, value): + current_config = Configurations.list(self.apiclient, name=name)[0] + if current_config.value == value: + return + logging.info("Current value for config: %s is %s, changing it to %s" % (name, current_config.value, value)) + self.changedConfigurations[name] = current_config.value + if current_config.isdynamic == False: + self.staticConfigurations.append(name) + Configurations.update(self.apiclient, + name, + value=value) + + def isManagementUp(self): + try: + self.apiclient.listInfrastructure(listInfrastructure.listInfrastructureCmd()) + return True + except Exception: + return False + + def getManagementServerIps(self): + if self.mgtSvrDetails["mgtSvrIp"] == 'localhost': + return None + cmd = listManagementServers.listManagementServersCmd() + servers = self.apiclient.listManagementServers(cmd) + active_server_ips = [] + active_server_ips.append(self.mgtSvrDetails["mgtSvrIp"]) + for idx, server in enumerate(servers): + if server.state == 'Up' and server.serviceip != self.mgtSvrDetails["mgtSvrIp"]: + active_server_ips.append(server.serviceip) + return active_server_ips + + def restartAllManagementServers(self): + """Restart all management servers + Assumes all servers have same username and password""" + server_ips = self.getManagementServerIps() + if server_ips is None: + self.staticConfigurations.clear() + self.fail("MS restarts cannot be done on %s" % self.mgtSvrDetails["mgtSvrIp"]) + return False + self.debug("Restarting all management server") + for idx, server_ip in enumerate(server_ips): + sshClient = SshClient( + server_ip, + 22, + self.mgtSvrDetails["user"], + self.mgtSvrDetails["passwd"] + ) + command = "service cloudstack-management stop" + sshClient.execute(command) + command = "service cloudstack-management start" + sshClient.execute(command) + + # Waits for management to come up in 10 mins, when it's up it will continue + timeout = time.time() + (10 * 60) + while time.time() < timeout: + if self.isManagementUp() is True: return True + time.sleep(5) + self.debug("Management server did not come up, failing") + return False + + @attr(tags=["advanced"], required_hardware="true") + def test_01_purge_expunged_api_vm_start_date(self): + self.executePurgeExpungedResources(self.timestamps[1], None) + self.validatePurgedVmEntriesInDb( + [self.vm_ids[self.timestamps[1]], self.vm_ids[self.timestamps[2]]], + [self.vm_ids[self.timestamps[0]]] + ) + + @attr(tags=["advanced"], required_hardware="true") + def test_02_purge_expunged_api_vm_end_date(self): + self.executePurgeExpungedResources(None, self.timestamps[1]) + self.validatePurgedVmEntriesInDb( + [self.vm_ids[self.timestamps[0]], self.vm_ids[self.timestamps[1]]], + [self.vm_ids[self.timestamps[2]]] + ) + + @attr(tags=["advanced"], required_hardware="true") + def test_03_purge_expunged_api_vm_start_end_date(self): + self.executePurgeExpungedResources(self.timestamps[0], self.timestamps[2]) + self.validatePurgedVmEntriesInDb( + [self.vm_ids[self.timestamps[0]], self.vm_ids[self.timestamps[1]], self.vm_ids[self.timestamps[2]]], + None + ) + + @attr(tags=["advanced"], required_hardware="true") + def test_04_purge_expunged_api_vm_no_date(self): + self.executePurgeExpungedResources(None, None) + self.validatePurgedVmEntriesInDb( + [self.vm_ids[self.timestamps[0]], self.vm_ids[self.timestamps[1]], self.vm_ids[self.timestamps[2]]], + None + ) + + @attr(tags=["advanced", "skip_setup_vms"], required_hardware="true") + def test_05_purge_expunged_vm_service_offering(self): + purge_delay = 181 + self.changeConfiguration('expunged.resource.purge.job.delay', purge_delay) + vm = VirtualMachine.create( + self.userapiclient, + self.services["virtual_machine"], + serviceofferingid=self.purge_resource_compute_offering.id, + networkids=self.l2_network.id + ) + self.cleanup.append(vm) + vm_id = vm.id + vm.delete(self.apiclient, expunge=True) + self.cleanup.remove(vm) + wait = 1.25 * purge_delay + logging.info("Waiting for 1.25x%d = %d seconds for VM to get purged" % (purge_delay, wait)) + time.sleep(wait) + self.validatePurgedVmEntriesInDb( + [vm_id], + None + ) + + @skipTestIf("hypervisorIsSimulator") + @attr(tags=["advanced"], required_hardware="true") + def test_06_purge_expunged_vm_background_task(self): + purge_task_delay = 60 + self.changeConfiguration('expunged.resources.purge.enabled', 'true') + self.changeConfiguration('expunged.resources.purge.delay', purge_task_delay) + self.changeConfiguration('expunged.resources.purge.keep.past.days', 1) + if len(self.staticConfigurations) > 0: + self.restartAllManagementServers() + wait = 2 * purge_task_delay + logging.info("Waiting for 2x%d = %d seconds for background task to execute" % (purge_task_delay, wait)) + time.sleep(wait) + self.validatePurgedVmEntriesInDb( + [self.vm_ids[self.timestamps[0]], self.vm_ids[self.timestamps[1]], self.vm_ids[self.timestamps[2]]], + None + ) diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 1352800c2bf..c5136a7ffb7 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -248,7 +248,8 @@ known_categories = { 'Rolling': 'Rolling Maintenance', 'importVsphereStoragePolicies' : 'vSphere storage policies', 'listVsphereStoragePolicies' : 'vSphere storage policies', - 'ConsoleEndpoint': 'Console Endpoint' + 'ConsoleEndpoint': 'Console Endpoint', + 'purgeExpungedResources': 'Resource' } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 90d8619168c..37d9c7bd4d8 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1525,6 +1525,7 @@ "label.publickey": "Public key", "label.publicnetwork": "Public network", "label.publicport": "Public port", +"label.purgeresources": "Purge Resources", "label.purpose": "Purpose", "label.qostype": "QoS type", "label.quickview": "Quick view", diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index 35acfbb674d..f36fd18a33e 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -38,7 +38,7 @@ export default { }, columns: ['name', 'displaytext', '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'] + var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources'] 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/offering/AddComputeOffering.vue b/ui/src/views/offering/AddComputeOffering.vue index ae8e07e652b..d75d0c25d23 100644 --- a/ui/src/views/offering/AddComputeOffering.vue +++ b/ui/src/views/offering/AddComputeOffering.vue @@ -338,6 +338,12 @@ + + + +