From 9bd4f1e5ae6c9dbdbe1360e1640d6fd3ac2595ca Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 8 Apr 2024 14:05:45 +0200 Subject: [PATCH 01/16] upgrade: move 4.19.0->4.20.0 to 4.19.1->4.20.0 --- .../java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 6 ++++-- ...{Upgrade41900to42000.java => Upgrade41910to42000.java} | 8 ++++---- ...o42000-cleanup.sql => schema-41910to42000-cleanup.sql} | 0 .../{schema-41900to42000.sql => schema-41910to42000.sql} | 0 4 files changed, 8 insertions(+), 6 deletions(-) rename engine/schema/src/main/java/com/cloud/upgrade/dao/{Upgrade41900to42000.java => Upgrade41910to42000.java} (91%) rename engine/schema/src/main/resources/META-INF/db/{schema-41900to42000-cleanup.sql => schema-41910to42000-cleanup.sql} (100%) rename engine/schema/src/main/resources/META-INF/db/{schema-41900to42000.sql => schema-41910to42000.sql} (100%) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index ea8ce47611a2..d390a480e414 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -33,7 +33,6 @@ import javax.inject.Inject; -import com.cloud.upgrade.dao.Upgrade41900to42000; import com.cloud.utils.FileUtil; import org.apache.cloudstack.utils.CloudStackVersion; import org.apache.commons.lang3.StringUtils; @@ -87,6 +86,8 @@ import com.cloud.upgrade.dao.Upgrade41720to41800; import com.cloud.upgrade.dao.Upgrade41800to41810; import com.cloud.upgrade.dao.Upgrade41810to41900; +import com.cloud.upgrade.dao.Upgrade41900to41910; +import com.cloud.upgrade.dao.Upgrade41910to42000; import com.cloud.upgrade.dao.Upgrade420to421; import com.cloud.upgrade.dao.Upgrade421to430; import com.cloud.upgrade.dao.Upgrade430to440; @@ -226,7 +227,8 @@ public DatabaseUpgradeChecker() { .next("4.17.2.0", new Upgrade41720to41800()) .next("4.18.0.0", new Upgrade41800to41810()) .next("4.18.1.0", new Upgrade41810to41900()) - .next("4.19.0.0", new Upgrade41900to42000()) + .next("4.19.0.0", new Upgrade41900to41910()) + .next("4.19.1.0", new Upgrade41910to42000()) .build(); } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41900to42000.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java similarity index 91% rename from engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41900to42000.java rename to engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java index 200c5fda2326..d8c7684f8e3a 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41900to42000.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to42000.java @@ -22,12 +22,12 @@ import com.cloud.upgrade.SystemVmTemplateRegistration; import com.cloud.utils.exception.CloudRuntimeException; -public class Upgrade41900to42000 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { +public class Upgrade41910to42000 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { private SystemVmTemplateRegistration systemVmTemplateRegistration; @Override public String[] getUpgradableVersionRange() { - return new String[] {"4.19.0.0", "4.20.0.0"}; + return new String[] {"4.19.1.0", "4.20.0.0"}; } @Override @@ -42,7 +42,7 @@ public boolean supportsRollingUpgrade() { @Override public InputStream[] getPrepareScripts() { - final String scriptFile = "META-INF/db/schema-41900to42000.sql"; + final String scriptFile = "META-INF/db/schema-41910to42000.sql"; final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); if (script == null) { throw new CloudRuntimeException("Unable to find " + scriptFile); @@ -57,7 +57,7 @@ public void performDataMigration(Connection conn) { @Override public InputStream[] getCleanupScripts() { - final String scriptFile = "META-INF/db/schema-41900to42000-cleanup.sql"; + final String scriptFile = "META-INF/db/schema-41910to42000-cleanup.sql"; final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); if (script == null) { throw new CloudRuntimeException("Unable to find " + scriptFile); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000-cleanup.sql similarity index 100% rename from engine/schema/src/main/resources/META-INF/db/schema-41900to42000-cleanup.sql rename to engine/schema/src/main/resources/META-INF/db/schema-41910to42000-cleanup.sql diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql similarity index 100% rename from engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql rename to engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql From af9d68c3e697991805f33bde5f2f99d2ddfd3d92 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 8 Mar 2024 12:03:38 +0530 Subject: [PATCH 02/16] api,server: purge expunged resources 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 admin-only API purgeExpungedResources. It will allow passing the following parameters - resourcetype, batchsize, startdate, enddate. Currently, API is not supported in the UI. 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 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 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 expunged resources expunged.resources.purge.delay: Default: 300. Initial delay (in seconds) to start the background task to purge the expunged resources task. expunged.resources.purge.batch.size: Default: 50. Batch size to be used during expunged resources purging. expunged.resources.purge.start.time: Default: (empty). Start time to be used by the background task to purge 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 expunged resources for which the expunged resources must not be purged. To enable purging 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 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. Documentation PR: apache/cloudstack-documentation#397 Signed-off-by: Abhishek Kumar --- .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 | 12 +- .../offering/UpdateServiceOfferingCmd.java | 9 + .../resource/PurgeExpungedResourcesCmd.java | 131 +++ .../PurgeExpungedResourcesResponse.java | 39 + .../api/response/ServiceOfferingResponse.java | 8 + .../resource/ResourceCleanupService.java | 72 ++ .../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 | 14 +- .../com/cloud/network/dao/IPAddressDao.java | 2 + .../cloud/network/dao/IPAddressDaoImpl.java | 9 + .../dao/InlineLoadBalancerNicMapDao.java | 4 + .../dao/InlineLoadBalancerNicMapDaoImpl.java | 11 + .../network/dao/LoadBalancerVMMapDao.java | 1 + .../network/dao/LoadBalancerVMMapDaoImpl.java | 11 +- .../dao/OpRouterMonitorServiceDao.java | 4 + .../dao/OpRouterMonitorServiceDaoImpl.java | 15 +- .../rules/dao/PortForwardingRulesDao.java | 1 + .../rules/dao/PortForwardingRulesDaoImpl.java | 9 + .../cloud/secstorage/CommandExecLogDao.java | 2 + .../secstorage/CommandExecLogDaoImpl.java | 9 + .../com/cloud/storage/dao/SnapshotDao.java | 1 + .../cloud/storage/dao/SnapshotDaoImpl.java | 9 + .../java/com/cloud/storage/dao/VolumeDao.java | 1 + .../com/cloud/storage/dao/VolumeDaoImpl.java | 18 +- .../src/main/java/com/cloud/vm/ItWorkDao.java | 1 + .../main/java/com/cloud/vm/ItWorkDaoImpl.java | 10 +- .../main/java/com/cloud/vm/dao/NicDao.java | 1 + .../java/com/cloud/vm/dao/NicDaoImpl.java | 11 + .../cloud/vm/dao/NicExtraDhcpOptionDao.java | 1 + .../vm/dao/NicExtraDhcpOptionDaoImpl.java | 13 +- .../com/cloud/vm/dao/NicSecondaryIpDao.java | 1 + .../cloud/vm/dao/NicSecondaryIpDaoImpl.java | 9 + .../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 | 21 +- .../resourcedetail/ResourceDetailsDao.java | 2 + .../ResourceDetailsDaoBase.java | 26 +- .../datastore/db/SnapshotDataStoreDao.java | 2 + .../db/SnapshotDataStoreDaoImpl.java | 12 + .../datastore/db/VolumeDataStoreDao.java | 2 + .../image/db/VolumeDataStoreDaoImpl.java | 9 + .../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 | 10 +- .../element/ElasticLoadBalancerElement.java | 4 + .../lb/ElasticLoadBalancerManager.java | 2 + .../lb/ElasticLoadBalancerManagerImpl.java | 4 + .../network/lb/dao/ElasticLbVmMapDao.java | 2 + .../network/lb/dao/ElasticLbVmMapDaoImpl.java | 9 + .../query/dao/ServiceOfferingJoinDaoImpl.java | 22 +- .../ConfigurationManagerImpl.java | 45 +- .../cloud/ha/HighAvailabilityManagerImpl.java | 5 + .../com/cloud/ha/dao/HighAvailabilityDao.java | 1 + .../cloud/ha/dao/HighAvailabilityDaoImpl.java | 10 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 12 +- .../resource/ResourceCleanupServiceImpl.java | 820 ++++++++++++++++++ .../spring-server-core-managers-context.xml | 2 + .../com/cloud/user/MockUsageEventDao.java | 15 + .../com/cloud/vpc/MockNetworkManagerImpl.java | 4 + .../ResourceCleanupServiceImplTest.java | 640 ++++++++++++++ .../smoke/test_purge_expunged_vms.py | 368 ++++++++ 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 +- 77 files changed, 2691 insertions(+), 52 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/resource/PurgeExpungedResourcesCmdTest.java create mode 100644 server/src/main/java/org/apache/cloudstack/resource/ResourceCleanupServiceImpl.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 fac2d6266fa5..6ee3856f9c05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,8 @@ jobs: smoke/test_migration smoke/test_multipleips_per_nic smoke/test_nested_virtualization - smoke/test_set_sourcenat", + smoke/test_set_sourcenat + smoke/test_purge_expunged_resources.py", "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 01ad12a71e08..45e39654b476 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -721,6 +721,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"; // OBJECT STORE public static final String EVENT_OBJECT_STORE_CREATE = "OBJECT.STORE.CREATE"; 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 1bb37be970d3..dc0f60f45196 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 58c7b0dbaf96..acb7a9f1cf91 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 6dfcf6561244..c0314ca234ed 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -381,6 +381,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"; @@ -898,6 +899,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 4562aa7da19e..ac3f5fe7af4b 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 @@ -242,6 +242,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 /////////////////////// @@ -269,7 +275,7 @@ public Integer getMemory() { 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; } @@ -477,6 +483,10 @@ public boolean getEncryptRoot() { 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 7d6bae860834..e580f0d9f41a 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 @@ -89,6 +89,11 @@ public class UpdateServiceOfferingCmd extends BaseCmd { description = "state of the service offering") private String serviceOfferingState; + @Parameter(name = ApiConstants.PURGE_RESOURCES, type = CommandType.BOOLEAN, + description = "Whether to cleanup VM and its associated resource upon expunge", + since="4.20") + private Boolean purgeResources; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -185,6 +190,10 @@ public State getState() { return state; } + 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 000000000000..c8eac24ffb57 --- /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 000000000000..3807d0d5b168 --- /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 c7740c19214b..c2861cc4e9ef 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 @@ -234,6 +234,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() { } @@ -555,4 +559,8 @@ public String getDiskOfferingDisplayText() { } 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 000000000000..4e7e3616ec8e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/resource/ResourceCleanupService.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 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 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 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 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 expunged resources task", + false); + ConfigKey ExpungedResourcesPurgeBatchSize = new ConfigKey<>("Advanced", Integer.class, + "expunged.resources.purge.batch.size", "50", + "Batch size to be used during purging 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 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 expunged resources for which the expunged resources must not be purged. To enable purging " + + "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 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/resource/PurgeExpungedResourcesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/resource/PurgeExpungedResourcesCmdTest.java new file mode 100644 index 000000000000..a628f13275c5 --- /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 110592161f96..41bd74f11924 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 @@ -348,4 +348,6 @@ void implementNetworkElementsAndResources(DeployDestination dest, ReservationCon Pair importNic(final String macAddress, int deviceId, final Network network, final Boolean isDefaultNic, final VirtualMachine vm, final Network.IpAddresses ipAddresses, final DataCenter datacenter, 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 72737d0b04d2..ae47b1d76ed8 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 @@ -156,4 +156,5 @@ enum Step { 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 824b9f5f45d6..c28e40252318 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -89,6 +89,7 @@ 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; @@ -401,6 +402,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac private VpcDao vpcDao; @Inject private DomainDao domainDao; + @Inject + ResourceCleanupService resourceCleanupService; VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); @@ -690,6 +693,7 @@ protected void advanceExpunge(VMInstanceVO vm) throws ResourceUnavailableExcepti if (logger.isDebugEnabled()) { logger.debug("Expunged " + vm); } + resourceCleanupService.purgeExpungedVmResourcesLaterIfNeeded(vm); } private void handleUnsuccessfulExpungeOperation(List finalizeExpungeCommands, List nicExpungeCommands, 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 d07fee322765..005ffce60546 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 @@ -4722,6 +4722,19 @@ public void unmanageNics(VirtualMachineProfile vm) { } } + @Override + public void expungeLbVmRefs(List vmIds, Long batchSize) { + if (CollectionUtils.isEmpty(networkElements)) { + 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 4b25c63403e2..718511746c2f 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 8fca4c26f9a7..9cbe2fbb99c6 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,6 +18,8 @@ import java.util.List; +import javax.annotation.PostConstruct; +import javax.inject.Inject; import org.springframework.stereotype.Component; @@ -31,9 +33,6 @@ 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 +114,13 @@ public boolean removeByGroup(long vmGroupId) { sc.setParameters("vmGroupId", vmGroupId); return remove(sc) >= 0; } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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 b1b1e1cf7571..3f8c36ac94ed 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 @@ -105,4 +105,6 @@ public interface IPAddressDao extends GenericDao { void buildQuarantineSearchCriteria(SearchCriteria sc); IPAddressVO findBySourceNetworkIdAndDatacenterIdAndState(long sourceNetworkId, long dataCenterId, State state); + + 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 ca779f7e9cee..f3670d1c7315 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 @@ -561,4 +561,13 @@ public IPAddressVO findBySourceNetworkIdAndDatacenterIdAndState(long sourceNetwo sc.setParameters("state", State.Free); return findOneBy(sc); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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 ac3845beffe4..b1831b407a41 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 1c3f231f9c1d..9fddafdbdb83 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,12 @@ package com.cloud.network.dao; +import java.util.List; + 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 +44,12 @@ public InlineLoadBalancerNicMapVO findByNicId(long nicId) { return findOneBy(sc); } + @Override + public int expungeByNicList(List nicIds, Long batchSize) { + 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 a25534b7010f..be2941d5cb2f 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 b32320a84cb0..7cf6b87cefce 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,11 @@ import java.util.List; - 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 +135,13 @@ public List listByLoadBalancerIdAndVmId(long loadBalancerId sc.addAnd("instanceId", SearchCriteria.Op.EQ, instanceId); return listBy(sc); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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 ebc0f1af2271..0516e26e13a7 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 451320ac9b6c..25bb0ef03e43 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,23 @@ package com.cloud.network.dao; -import com.cloud.utils.db.GenericDaoBase; +import java.util.List; + 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) { + 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 b89d04ad15a0..8cd114b7fc4f 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 29cba516d720..f19dfd4f987c 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 @@ -170,4 +170,13 @@ public PortForwardingRuleVO findByIdAndIp(long id, String secondaryIp) { sc.setParameters("dstIp", secondaryIp); return findOneBy(sc); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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 98fc8c8687b8..5023aaa3794c 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 f89a1bbf4ccb..c206506ca5d8 100644 --- a/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/secstorage/CommandExecLogDaoImpl.java @@ -57,4 +57,13 @@ public Integer getCopyCmdCountForSSVM(Long id) { List copyCmds = customSearch(sc, null); return copyCmds.size(); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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 998d0bbd724c..171634fb1044 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 030d10d66827..ec916168bcd4 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 @@ -285,4 +285,13 @@ public List listByStatusNotIn(long volumeId, Snapshot.State... statu sc.setParameters("status", (Object[]) status); return listBy(sc, null); } + + @Override + public List searchByVolumes(List volumeIds) { + 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 4e9c63699ca9..a5977fbf9b99 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 @@ -158,4 +158,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 31d64daf147c..8c80d2bd81b3 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 @@ -27,14 +27,12 @@ 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.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.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; @@ -896,4 +897,15 @@ public VolumeVO persist(VolumeVO entity) { return volume; }); } + + @Override + public List searchRemovedByVms(List vmIds, Long batchSize) { + 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, null, 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 2d4a5e138fea..ab07d6989fae 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 ff727904dcb7..09c2778d0e52 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,6 @@ import java.util.List; - import org.springframework.stereotype.Component; import com.cloud.utils.db.GenericDaoBase; @@ -103,4 +102,13 @@ public List listWorkInProgressFor(long nodeId) { return search(sc, null); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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 23c26ea07180..d34b03c4cb08 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 @@ -100,4 +100,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 3eee1d4e749d..3007dae54aa9 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 @@ -428,4 +428,15 @@ public List listByNetworkIdAndType(long networkId, VirtualMachine.Type vm sc.setParameters("vmType", vmType); return listBy(sc); } + + @Override + public List searchRemovedByVms(List vmIds, Long batchSize) { + 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, null, 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 69d9c00e1e0b..7bae64a6acbd 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 3056c73938e7..0f26ac1ba720 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,12 @@ // under the License. package com.cloud.vm.dao; -import org.springframework.stereotype.Component; - import java.util.List; +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 +73,12 @@ public void removeByNicId(long nicId) { expunge(sc); } + @Override + public int expungeByNicList(List nicIds, Long batchSize) { + 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 cbb52e57282b..ff7089ca4276 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 a56d35d5a63d..68c36aa5f907 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 @@ -192,4 +192,13 @@ public int moveSecondaryIps(long fromNicId, long toNicId) { return update(update, sc); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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 42c00231aac1..ac25eac34800 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(List hostIds, 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 b7b787b00451..09dce2d98135 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 javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.host.HostVO; @@ -39,6 +40,7 @@ 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; @@ -1016,4 +1018,26 @@ public List listByHostOrLastHostOrHostPod(List hostIds, long 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, null, 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 31999ef15d66..0143aaa1e735 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 062960130aca..194779b18989 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 @@ -20,9 +20,9 @@ import java.util.Date; import java.util.List; - 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; @@ -180,4 +180,23 @@ public boolean updateState(State currentState, Event event, State nextState, VMS return rows > 0; } + @Override + public List searchByVms(List vmIds) { + 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) { + 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, null, 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 5a173191be1f..49ba947d4673 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(final List ids, final 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 37ebfebf5ddd..e64c1c82b56a 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,16 @@ import java.util.Map; import org.apache.cloudstack.api.ResourceDetail; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +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.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 +204,25 @@ public List findResourceIdsByNameAndValueIn(String name, Object[] values) return customSearch(sc, null); } + + @Override + public long batchExpungeForResources(List ids, 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()); + int removed = 0; + long totalRemoved = 0; + Filter filter = new Filter(_entityBeanType, "id", true, null, batchSize); + final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); + do { + removed = expunge(sc, filter); + totalRemoved += removed; + } while (batchSizeFinal > 0 && removed >= batchSizeFinal); + return totalRemoved; + } } 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 344ff8b2a699..0641aecfaba2 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 @@ -106,4 +106,6 @@ public interface SnapshotDataStoreDao extends GenericDao snapshotIds, final 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 c095f4222e76..7ec2327d6388 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 @@ -559,4 +559,16 @@ public void updateDisplayForSnapshotStoreRole(long snapshotId, long storeId, Dat ref.setDisplay(display); update(ref.getId(), ref); } + + @Override + public int expungeBySnapshotList(List snapshotIds, 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 c3a4b58fbd50..3381391e70e4 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 @@ -59,4 +59,6 @@ public interface VolumeDataStoreDao extends GenericDao, List listByVolume(long volumeId, long storeId); List listByStoreIdAndInstallPaths(Long storeId, List paths); + + int expungeByVolumeList(List volumeIds, Long 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 2c3d5ccfdde8..d08c78bcf56d 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 @@ -407,4 +407,13 @@ public boolean updateVolumeId(long srcVolId, long destVolId) { } return true; } + + @Override + public int expungeByVolumeList(List volumeIds, Long batchSize) { + 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/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java index 2fc02301cb7a..4855d83e81f5 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(final 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 a09f323905e4..22e2874994db 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.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,9 +61,12 @@ 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 com.amazonaws.util.CollectionUtils; import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; @@ -74,8 +79,6 @@ 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; @@ -1234,7 +1237,7 @@ public boolean expunge(final ID id) { // 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"); } @@ -1246,6 +1249,7 @@ public int expunge(final SearchCriteria sc) { if (sc != null && sc.getWhereClause().length() > 0) { str.append(sc.getWhereClause()); } + addFilter(str, filter); final String sql = str.toString(); @@ -1264,6 +1268,47 @@ public int expunge(final SearchCriteria sc) { 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 && expunged >= batchSizeFinal); + return expunged; + } + + @Override + public int expungeList(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) { + 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 89601e6b5d20..b3bfda0334cf 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 e66221cc8fe0..d7285b30857e 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,7 +24,6 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; - import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO.Step; import org.apache.cloudstack.jobs.JobInfo; @@ -212,4 +211,13 @@ public void doInTransactionWithoutResult(TransactionStatus status) { } }); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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/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 6c0ac160ceb2..c1ea7823811b 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 @@ -224,4 +224,8 @@ public boolean handlesOnlyRulesInTransitionState() { 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 f885f7e1012d..a687c8113165 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 LoadBalancer handleCreateLoadBalancerRule(CreateLoadBalancerRuleCmd lb, A 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 b47b7aad28e1..c7f4b8bf2443 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 @@ -599,4 +599,8 @@ public void prepareStop(VirtualMachineProfile profile) { 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 83aa6118a63d..eec7eaa4f5f1 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 bbe79ef31030..73611b6b2aeb 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 @@ -136,4 +136,13 @@ public List listLbsForElbVm(long elbVmId) { return _loadbalancerDao.search(sc, null); } + @Override + public int expungeByLbVmList(List vmIds, Long batchSize) { + 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 bf6167e7f3eb..48ccc356f1a6 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 @@ -19,35 +19,35 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import com.cloud.dc.VsphereStoragePolicyVO; -import com.cloud.dc.dao.VsphereStoragePolicyDao; -import com.cloud.user.AccountManager; -import com.cloud.utils.db.TransactionLegacy; +import javax.inject.Inject; + import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; -import com.cloud.storage.DiskOfferingVO; 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.springframework.stereotype.Component; import com.cloud.api.ApiDBUtils; import com.cloud.api.query.vo.ServiceOfferingJoinVO; +import com.cloud.dc.VsphereStoragePolicyVO; +import com.cloud.dc.dao.VsphereStoragePolicyDao; import com.cloud.offering.ServiceOffering; import com.cloud.server.ResourceTag.ResourceObjectType; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.user.AccountManager; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; - -import javax.inject.Inject; +import com.cloud.utils.db.TransactionLegacy; @Component public class ServiceOfferingJoinDaoImpl extends GenericDaoBase implements ServiceOfferingJoinDao { @@ -167,6 +167,10 @@ public ServiceOfferingResponse newServiceOfferingResponse(ServiceOfferingJoinVO if (vsphereStoragePolicyVO != null) offeringResponse.setVsphereStoragePolicy(vsphereStoragePolicyVO.getName()); } + String purgeResource = offeringDetails.get(ServiceOffering.PURGE_DB_ENTITIES_KEY); + if (StringUtils.isNotBlank(purgeResource)) { + offeringResponse.setPurgeResources(Boolean.parseBoolean(purgeResource)); + } } long rootDiskSizeInGb = (long) offering.getRootDiskSize() / GB_TO_BYTES; diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index a5f119000809..014635dd96f1 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -3187,7 +3187,7 @@ public ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) cmd.getIopsReadRate(), cmd.getIopsReadRateMax(), cmd.getIopsReadRateMaxLength(), cmd.getIopsWriteRate(), cmd.getIopsWriteRateMax(), cmd.getIopsWriteRateMaxLength(), cmd.getHypervisorSnapshotReserve(), cmd.getCacheMode(), storagePolicyId, cmd.getDynamicScalingEnabled(), diskOfferingId, - cmd.getDiskOfferingStrictness(), cmd.isCustomized(), cmd.getEncryptRoot()); + cmd.getDiskOfferingStrictness(), cmd.isCustomized(), cmd.getEncryptRoot(), cmd.isPurgeResources()); } protected ServiceOfferingVO createServiceOffering(final long userId, final boolean isSystem, final VirtualMachine.Type vmType, @@ -3198,8 +3198,9 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole Long bytesWriteRate, Long bytesWriteRateMax, Long bytesWriteRateMaxLength, Long iopsReadRate, Long iopsReadRateMax, Long iopsReadRateMaxLength, Long iopsWriteRate, Long iopsWriteRateMax, Long iopsWriteRateMaxLength, - final Integer hypervisorSnapshotReserve, String cacheMode, final Long storagePolicyID, final boolean dynamicScalingEnabled, final Long diskOfferingId, - final boolean diskOfferingStrictness, final boolean isCustomized, final boolean encryptRoot) { + final Integer hypervisorSnapshotReserve, String cacheMode, final Long storagePolicyID, + final boolean dynamicScalingEnabled, final Long diskOfferingId, final boolean diskOfferingStrictness, + final boolean isCustomized, final boolean encryptRoot, final boolean purgeResources) { // Filter child domains when both parent and child domains are present List filteredDomainIds = filterChildSubDomains(domainIds); @@ -3234,7 +3235,7 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole 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. @@ -3268,12 +3269,16 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole // 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); @@ -3300,18 +3305,18 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole 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()); @@ -3468,6 +3473,7 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) String storageTags = cmd.getStorageTags(); String hostTags = cmd.getHostTags(); ServiceOffering.State state = cmd.getState(); + boolean purgeResources = cmd.isPurgeResources(); if (userId == null) { userId = Long.valueOf(User.UID_SYSTEM); @@ -3485,6 +3491,12 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) 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) { @@ -3553,7 +3565,8 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) } final boolean updateNeeded = name != null || displayText != null || sortKey != null || storageTags != null || hostTags != null || state != 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); } @@ -3612,6 +3625,14 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) 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 b815f21e2064..00e25284f428 100644 --- a/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java +++ b/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java @@ -1092,4 +1092,9 @@ public ConfigKey[] getConfigKeys() { StopRetryInterval, RestartRetryInterval, MigrateRetryInterval, InvestigateRetryInterval, HAWorkers, ForceHA, KvmHAFenceHostIfHeartbeatFailsOnStorage}; } + + @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 e8a3e17f8052..f6539105d78f 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 357796a6a70b..5755e6901c3d 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,6 @@ import java.util.Date; import java.util.List; - import org.springframework.stereotype.Component; import com.cloud.ha.HaWorkVO; @@ -258,4 +257,13 @@ public int releaseWorkItems(long nodeId) { return update(vo, sc); } + + @Override + public int expungeByVmList(List vmIds, Long batchSize) { + 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 a9d9ad090955..b1116662a4dc 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -54,11 +54,6 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; -import com.cloud.kubernetes.cluster.KubernetesClusterHelper; -import com.cloud.network.dao.NsxProviderDao; -import com.cloud.network.element.NsxProviderVO; -import com.cloud.user.AccountVO; -import com.cloud.utils.exception.ExceptionProxyObject; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -136,8 +131,8 @@ import org.apache.cloudstack.userdata.UserDataManager; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.security.ParserUtils; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; import org.apache.cloudstack.vm.UnmanagedVMsManager; +import org.apache.cloudstack.vm.schedule.VMScheduleManager; import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -246,6 +241,7 @@ import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; import com.cloud.hypervisor.kvm.dpdk.DpdkHelper; +import com.cloud.kubernetes.cluster.KubernetesClusterHelper; import com.cloud.network.IpAddressManager; import com.cloud.network.Network; import com.cloud.network.Network.GuestType; @@ -264,7 +260,9 @@ import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkServiceMapDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.NsxProviderDao; import com.cloud.network.dao.PhysicalNetworkDao; +import com.cloud.network.element.NsxProviderVO; import com.cloud.network.element.UserDataServiceProvider; import com.cloud.network.guru.NetworkGuru; import com.cloud.network.lb.LoadBalancingRulesManager; @@ -333,6 +331,7 @@ import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountService; +import com.cloud.user.AccountVO; import com.cloud.user.ResourceLimitService; import com.cloud.user.SSHKeyPairVO; import com.cloud.user.User; @@ -366,6 +365,7 @@ import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.db.UUIDManager; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.exception.ExceptionProxyObject; import com.cloud.utils.exception.ExecutionException; import com.cloud.utils.fsm.NoTransitionException; import com.cloud.utils.net.Ip; 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 000000000000..99b4b5e764b9 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/resource/ResourceCleanupServiceImpl.java @@ -0,0 +1,820 @@ +// 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 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.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 { + + @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 + 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, null, 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) { + 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); + } + + 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 b5f72a1763c4..b33a1f2deb82 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/user/MockUsageEventDao.java b/server/src/test/java/com/cloud/user/MockUsageEventDao.java index 52e1b1a02b49..7e15a4f0aaf3 100644 --- a/server/src/test/java/com/cloud/user/MockUsageEventDao.java +++ b/server/src/test/java/com/cloud/user/MockUsageEventDao.java @@ -332,4 +332,19 @@ public void saveDetails(long eventId, Map details) { 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 106fc7fa5439..3453223034e9 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -1108,4 +1108,8 @@ public InternalLoadBalancerElementService getInternalLoadBalancerElementById(lon public List getInternalLoadBalancerElements() { return null; } + + @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 000000000000..79691c754b0e --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/resource/ResourceCleanupServiceImplTest.java @@ -0,0 +1,640 @@ +// 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.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 + 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 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); + + 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); + } + + @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 000000000000..c760afa05e4d --- /dev/null +++ b/test/integration/smoke/test_purge_expunged_vms.py @@ -0,0 +1,368 @@ +# 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): + testClient = super(TestPurgeExpungedVms, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.services = testClient.getParsedTestDataConfig() + cls.dbConnection = testClient.getDbConnection() + + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests()) + cls.services['mode'] = cls.zone.networktype + + cls.mgtSvrDetails = cls.config.__dict__["mgtSvr"][0].__dict__ + + + cls.hypervisor = 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, + mode=self.services["mode"] + ) + 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): + print(self.mgtSvrDetails) + 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) + print(active_server_ips) + return active_server_ip + + 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, + mode=self.services["mode"] + ) + 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 9df6bf9efc5c..04f307134b10 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -274,7 +274,8 @@ 'deleteBucket': 'Object Store', 'listBuckets': 'Object Store', 'listVmsForImport': 'Virtual Machine', - 'importVm': 'Virtual Machine' + 'importVm': 'Virtual Machine', + 'purgeExpungedResources': 'Resource' } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 064b35338820..6f0a3e6b589f 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1656,6 +1656,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 9250842ff4e2..800661b2029e 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -39,7 +39,7 @@ export default { filters: ['active', 'inactive'], columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'domain', 'zone', 'order'], details: () => { - var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot'] + 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 80d200f1f25d..1fd600ae566e 100644 --- a/ui/src/views/offering/AddComputeOffering.vue +++ b/ui/src/views/offering/AddComputeOffering.vue @@ -343,6 +343,12 @@ + + + +