From 209e9fc24c97f4f48720aade74448648a84f2bb4 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Thu, 9 May 2024 09:55:04 +0300 Subject: [PATCH 01/15] Support of snapshot copy to different StorPool primary storage between zones Added support to copy a snapshot to another StorPool primary storage in different zones. --- .../com/cloud/storage/VolumeApiService.java | 4 +- .../apache/cloudstack/api/ApiConstants.java | 1 + .../user/snapshot/CopySnapshotCmd.java | 49 ++- .../user/snapshot/CreateSnapshotCmd.java | 18 +- .../snapshot/CreateSnapshotPolicyCmd.java | 32 +- .../api/response/SnapshotPolicyResponse.java | 18 +- .../command/test/CreateSnapshotCmdTest.java | 4 +- .../api/storage/DataStoreCapabilities.java | 11 +- .../api/storage/SnapshotService.java | 2 + .../api/storage/SnapshotStrategy.java | 7 +- .../cloud/vm/VmWorkTakeVolumeSnapshot.java | 9 +- .../vm/VmWorkTakeVolumeSnapshotTest.java | 3 +- .../orchestration/VolumeOrchestrator.java | 21 +- .../resourcedetail/ResourceDetailsDao.java | 7 + .../ResourceDetailsDaoBase.java | 6 + .../datastore/db/SnapshotDataStoreDao.java | 3 + .../db/SnapshotDataStoreDaoImpl.java | 53 ++-- .../snapshot/CephSnapshotStrategy.java | 4 + .../snapshot/DefaultSnapshotStrategy.java | 5 + .../snapshot/ScaleIOSnapshotStrategy.java | 4 + .../storage/snapshot/SnapshotServiceImpl.java | 32 ++ .../StorageSystemSnapshotStrategy.java | 5 +- .../StorPoolModifyStoragePoolAnswer.java | 12 +- .../StorPoolModifyStorageCommandWrapper.java | 30 +- .../StorPoolAbandonObjectsCollector.java | 132 ++++++-- .../StorPoolPrimaryDataStoreDriver.java | 80 +++-- .../provider/StorPoolHostListener.java | 1 + .../datastore/util/StorPoolHelper.java | 58 ++-- .../storage/datastore/util/StorPoolUtil.java | 45 +++ .../motion/StorPoolDataMotionStrategy.java | 113 ++++--- .../StorPoolConfigurationManager.java | 6 +- .../snapshot/StorPoolSnapshotStrategy.java | 246 ++++++++++----- .../main/java/com/cloud/api/ApiDBUtils.java | 13 + .../java/com/cloud/api/ApiResponseHelper.java | 9 + .../com/cloud/api/query/QueryManagerImpl.java | 10 +- .../cloud/api/query/dao/SnapshotJoinDao.java | 8 +- .../api/query/dao/SnapshotJoinDaoImpl.java | 19 ++ .../cloud/storage/CreateSnapshotPayload.java | 10 + .../cloud/storage/VolumeApiServiceImpl.java | 56 +++- .../storage/snapshot/SnapshotManagerImpl.java | 288 +++++++++++++++--- .../cloud/template/TemplateManagerImpl.java | 28 +- .../cloudstack/snapshot/SnapshotHelper.java | 86 ++++-- .../storage/VolumeApiServiceImplTest.java | 6 +- .../snapshot/SnapshotManagerImplTest.java | 89 +++--- .../storage/snapshot/SnapshotManagerTest.java | 113 ++++--- .../template/TemplateManagerImplTest.java | 8 + .../snapshot/SnapshotHelperTest.java | 59 ++-- .../storpool/TestSnapshotCopyOnPrimary.py | 255 ++++++++++++++++ test/integration/plugins/storpool/sp_util.py | 56 ++++ tools/marvin/marvin/lib/base.py | 37 ++- ui/src/views/storage/SnapshotZones.vue | 4 +- 51 files changed, 1668 insertions(+), 507 deletions(-) create mode 100644 test/integration/plugins/storpool/TestSnapshotCopyOnPrimary.py diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index dd7341da1b5e..02515a516a25 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -113,10 +113,10 @@ public interface VolumeApiService { Volume detachVolumeFromVM(DetachVolumeCmd cmd); - Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, Map tags, List zoneIds) + Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, Map tags, List zoneIds, List poolIds) throws ResourceAllocationException; - Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List zoneIds) throws ResourceAllocationException; + Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List zoneIds, List poolIds) throws ResourceAllocationException; Volume updateVolume(long volumeId, String path, String state, Long storageId, Boolean displayVolume, Boolean deleteProtection, 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 70d3763b0beb..cacf53f3b6f4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1156,6 +1156,7 @@ public class ApiConstants { public static final String ZONE_ID_LIST = "zoneids"; public static final String DESTINATION_ZONE_ID_LIST = "destzoneids"; + public static final String STORAGE_ID_LIST = "storageids"; public static final String ADMIN = "admin"; public static final String CHECKSUM_PARAMETER_PREFIX_DESCRIPTION = "The parameter containing the checksum will be considered a MD5sum if it is not prefixed\n" + " and just a plain ascii/utf8 representation of a hexadecimal string. If it is required to\n" diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java index 07973fcbfca5..b66714a09f1e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java @@ -17,9 +17,13 @@ package org.apache.cloudstack.api.command.user.snapshot; -import java.util.ArrayList; -import java.util.List; - +import com.cloud.dc.DataCenter; +import com.cloud.event.EventTypes; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.exception.StorageUnavailableException; +import com.cloud.storage.Snapshot; +import com.cloud.user.Account; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -31,26 +35,23 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.response.SnapshotResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; import org.apache.commons.collections.CollectionUtils; - -import com.cloud.dc.DataCenter; -import com.cloud.event.EventTypes; -import com.cloud.exception.ResourceAllocationException; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.exception.StorageUnavailableException; -import com.cloud.storage.Snapshot; -import com.cloud.user.Account; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.ArrayList; +import java.util.List; + @APICommand(name = "copySnapshot", description = "Copies a snapshot from one zone to another.", responseObject = SnapshotResponse.class, responseView = ResponseObject.ResponseView.Restricted, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class CopySnapshotCmd extends BaseAsyncCmd implements UserCmd { public static final Logger logger = LogManager.getLogger(CopySnapshotCmd.class.getName()); + private Snapshot snapshot; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -84,6 +85,15 @@ public class CopySnapshotCmd extends BaseAsyncCmd implements UserCmd { "Do not specify destzoneid and destzoneids together, however one of them is required.") protected List destZoneIds; + @Parameter(name = ApiConstants.STORAGE_ID_LIST, + type=CommandType.LIST, + collectionType = CommandType.UUID, + entityType = StoragePoolResponse.class, + required = false, + description = "A comma-separated list of IDs of the storage pools in other zones in which the snapshot will be made available. " + + "The snapshot will always be made available in the zone in which the volume is present.") + protected List storagePoolIds; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -106,7 +116,11 @@ public List getDestinationZoneIds() { destIds.add(destZoneId); return destIds; } - return null; + return new ArrayList<>(); + } + + public List getStoragePoolIds() { + return storagePoolIds; } @Override @@ -152,7 +166,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException { try { - if (destZoneId == null && CollectionUtils.isEmpty(destZoneIds)) + if (destZoneId == null && CollectionUtils.isEmpty(destZoneIds) && CollectionUtils.isEmpty(storagePoolIds)) throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Either destzoneid or destzoneids parameters have to be specified."); @@ -161,7 +175,7 @@ public void execute() throws ResourceUnavailableException { "Both destzoneid and destzoneids cannot be specified at the same time."); CallContext.current().setEventDetails(getEventDescription()); - Snapshot snapshot = _snapshotService.copySnapshot(this); + snapshot = _snapshotService.copySnapshot(this); if (snapshot != null) { SnapshotResponse response = _queryService.listSnapshot(this); @@ -177,6 +191,13 @@ public void execute() throws ResourceUnavailableException { logger.warn("Exception: ", ex); throw new ServerApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR, ex.getMessage()); } + } + + public Snapshot getSnapshot() { + return snapshot; + } + public void setSnapshot(Snapshot snapshot) { + this.snapshot = snapshot; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java index 3289ac2fe106..078f613d8b74 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java @@ -32,6 +32,7 @@ import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.SnapshotPolicyResponse; import org.apache.cloudstack.api.response.SnapshotResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.commons.collections.MapUtils; @@ -99,6 +100,15 @@ public class CreateSnapshotCmd extends BaseAsyncCreateCmd { since = "4.19.0") protected List zoneIds; + @Parameter(name = ApiConstants.STORAGE_ID_LIST, + type=CommandType.LIST, + collectionType = CommandType.UUID, + entityType = StoragePoolResponse.class, + description = "A comma-separated list of IDs of the storage pools in other zones in which the snapshot will be made available. " + + "The snapshot will always be made available in the zone in which the volume is present.", + since = "4.20.0") + protected List storagePoolIds; + private String syncObjectType = BaseAsyncCmd.snapshotHostSyncObject; // /////////////////////////////////////////////////// @@ -161,6 +171,10 @@ public List getZoneIds() { return zoneIds; } + public List getStoragePoolIds() { + return storagePoolIds; + } + // /////////////////////////////////////////////////// // ///////////// API Implementation/////////////////// // /////////////////////////////////////////////////// @@ -209,7 +223,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void create() throws ResourceAllocationException { - Snapshot snapshot = _volumeService.allocSnapshot(getVolumeId(), getPolicyId(), getSnapshotName(), getLocationType(), getZoneIds()); + Snapshot snapshot = _volumeService.allocSnapshot(getVolumeId(), getPolicyId(), getSnapshotName(), getLocationType(), getZoneIds(), getStoragePoolIds()); if (snapshot != null) { setEntityId(snapshot.getId()); setEntityUuid(snapshot.getUuid()); @@ -223,7 +237,7 @@ public void execute() { Snapshot snapshot; try { snapshot = - _volumeService.takeSnapshot(getVolumeId(), getPolicyId(), getEntityId(), _accountService.getAccount(getEntityOwnerId()), getQuiescevm(), getLocationType(), getAsyncBackup(), getTags(), getZoneIds()); + _volumeService.takeSnapshot(getVolumeId(), getPolicyId(), getEntityId(), _accountService.getAccount(getEntityOwnerId()), getQuiescevm(), getLocationType(), getAsyncBackup(), getTags(), getZoneIds(), getStoragePoolIds()); if (snapshot != null) { SnapshotResponse response = _responseGenerator.createSnapshotResponse(snapshot); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotPolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotPolicyCmd.java index e30b897db2ea..0afeed364de4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotPolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotPolicyCmd.java @@ -16,11 +16,12 @@ // under the License. package org.apache.cloudstack.api.command.user.snapshot; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.projects.Project; +import com.cloud.storage.Volume; +import com.cloud.storage.snapshot.SnapshotPolicy; +import com.cloud.user.Account; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -30,16 +31,15 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.SnapshotPolicyResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.commons.collections.MapUtils; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.exception.PermissionDeniedException; -import com.cloud.projects.Project; -import com.cloud.storage.Volume; -import com.cloud.storage.snapshot.SnapshotPolicy; -import com.cloud.user.Account; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @APICommand(name = "createSnapshotPolicy", description = "Creates a snapshot policy for the account.", responseObject = SnapshotPolicyResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -83,6 +83,14 @@ public class CreateSnapshotPolicyCmd extends BaseCmd { "The snapshots will always be made available in the zone in which the volume is present.") protected List zoneIds; + @Parameter(name = ApiConstants.STORAGE_ID_LIST, + type=CommandType.LIST, + collectionType = CommandType.UUID, + entityType = StoragePoolResponse.class, + description = "A comma-separated list of IDs of the storage pools in other zones in which the snapshot will be made available. " + + "The snapshot will always be made available in the zone in which the volume is present.", + since = "4.20.0") + protected List storagePoolIds; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -119,6 +127,8 @@ public List getZoneIds() { return zoneIds; } + public List getStoragePoolIds() { return storagePoolIds; } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java index bfa1cca1ca08..1425dcd160c1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java @@ -16,17 +16,16 @@ // under the License. package org.apache.cloudstack.api.response; -import java.util.LinkedHashSet; -import java.util.Set; - +import com.cloud.serializer.Param; +import com.cloud.storage.snapshot.SnapshotPolicy; +import com.google.gson.annotations.SerializedName; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponseWithTagInformation; import org.apache.cloudstack.api.EntityReference; -import com.cloud.serializer.Param; -import com.cloud.storage.snapshot.SnapshotPolicy; -import com.google.gson.annotations.SerializedName; +import java.util.LinkedHashSet; +import java.util.Set; @EntityReference(value = SnapshotPolicy.class) public class SnapshotPolicyResponse extends BaseResponseWithTagInformation { @@ -62,9 +61,14 @@ public class SnapshotPolicyResponse extends BaseResponseWithTagInformation { @Param(description = "The list of zones in which snapshot backup is scheduled", responseObject = ZoneResponse.class, since = "4.19.0") protected Set zones; + @SerializedName(ApiConstants.STORAGE) + @Param(description = "The list of pools in which snapshot backup is scheduled", responseObject = StoragePoolResponse.class, since = "4.20.0") + protected Set storagePools; + public SnapshotPolicyResponse() { tags = new LinkedHashSet(); zones = new LinkedHashSet<>(); + storagePools = new LinkedHashSet<>(); } public String getId() { @@ -130,4 +134,6 @@ public void setTags(Set tags) { public void setZones(Set zones) { this.zones = zones; } + + public void setStoragePools(Set pools) { this.storagePools = pools; } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java index 34baebe52574..699919741941 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java @@ -93,7 +93,7 @@ public void testCreateSuccess() { Snapshot snapshot = Mockito.mock(Snapshot.class); try { Mockito.when(volumeApiService.takeSnapshot(nullable(Long.class), nullable(Long.class), isNull(), - nullable(Account.class), nullable(Boolean.class), nullable(Snapshot.LocationType.class), nullable(Boolean.class), nullable(Map.class), nullable(List.class))).thenReturn(snapshot); + nullable(Account.class), nullable(Boolean.class), nullable(Snapshot.LocationType.class), nullable(Boolean.class), nullable(Map.class), nullable(List.class), nullable(List.class))).thenReturn(snapshot); } catch (Exception e) { Assert.fail("Received exception when success expected " + e.getMessage()); @@ -126,7 +126,7 @@ public void testCreateFailure() { try { Mockito.when(volumeApiService.takeSnapshot(nullable(Long.class), nullable(Long.class), nullable(Long.class), - nullable(Account.class), nullable(Boolean.class), nullable(Snapshot.LocationType.class), nullable(Boolean.class), any(), Mockito.anyList())).thenReturn(null); + nullable(Account.class), nullable(Boolean.class), nullable(Snapshot.LocationType.class), nullable(Boolean.class), any(), Mockito.anyList(), Mockito.anyList())).thenReturn(null); } catch (Exception e) { Assert.fail("Received exception when success expected " + e.getMessage()); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java index f537d8f52028..8016d4cdc42c 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java @@ -40,5 +40,14 @@ public enum DataStoreCapabilities { /** * indicates that this driver supports reverting a volume to a snapshot state */ - CAN_REVERT_VOLUME_TO_SNAPSHOT + CAN_REVERT_VOLUME_TO_SNAPSHOT, + /** + * indicates that the driver supports copying snapshot between zones on pools of the same type + */ + CAN_COPY_SNAPSHOT_BETWEEN_ZONES, + /** + * indicates that the storage does not need to delete the snapshot when creating a volume/template from it + * and the setting `snapshot.backup.to.secondary` is enabled + */ + KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/SnapshotService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/SnapshotService.java index d7b6b2ec75b7..18c924167e0b 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/SnapshotService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/SnapshotService.java @@ -46,4 +46,6 @@ public interface SnapshotService { AsyncCallFuture copySnapshot(SnapshotInfo snapshot, String copyUrl, DataStore dataStore) throws ResourceUnavailableException; AsyncCallFuture queryCopySnapshot(SnapshotInfo snapshot) throws ResourceUnavailableException; + + AsyncCallFuture copySnapshot(SnapshotInfo sourceSnapshot, SnapshotInfo destSnapshot, SnapshotStrategy strategy); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/SnapshotStrategy.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/SnapshotStrategy.java index f3aa8f52c932..43f411f75533 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/SnapshotStrategy.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/SnapshotStrategy.java @@ -16,12 +16,14 @@ // under the License. package org.apache.cloudstack.engine.subsystem.api.storage; + import com.cloud.storage.Snapshot; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; public interface SnapshotStrategy { enum SnapshotOperation { - TAKE, BACKUP, DELETE, REVERT + TAKE, BACKUP, DELETE, REVERT, COPY } SnapshotInfo takeSnapshot(SnapshotInfo snapshot); @@ -35,4 +37,7 @@ enum SnapshotOperation { StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op); void postSnapshotCreation(SnapshotInfo snapshot); + + default void copySnapshot(DataObject snapshotSource, DataObject snapshotDest, AsyncCompletionCallback caller) { + } } diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeVolumeSnapshot.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeVolumeSnapshot.java index 8474052be201..88d25441e0ac 100644 --- a/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeVolumeSnapshot.java +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeVolumeSnapshot.java @@ -30,12 +30,12 @@ public class VmWorkTakeVolumeSnapshot extends VmWork { private boolean quiesceVm; private Snapshot.LocationType locationType; private boolean asyncBackup; - + private List poolIds; private List zoneIds; public VmWorkTakeVolumeSnapshot(long userId, long accountId, long vmId, String handlerName, Long volumeId, Long policyId, Long snapshotId, boolean quiesceVm, Snapshot.LocationType locationType, - boolean asyncBackup, List zoneIds) { + boolean asyncBackup, List zoneIds, List poolIds) { super(userId, accountId, vmId, handlerName); this.volumeId = volumeId; this.policyId = policyId; @@ -44,6 +44,7 @@ public VmWorkTakeVolumeSnapshot(long userId, long accountId, long vmId, String h this.locationType = locationType; this.asyncBackup = asyncBackup; this.zoneIds = zoneIds; + this.poolIds = poolIds; } public Long getVolumeId() { @@ -71,4 +72,8 @@ public boolean isAsyncBackup() { public List getZoneIds() { return zoneIds; } + + public List getPoolIds() { + return poolIds; + } } diff --git a/engine/components-api/src/test/java/com/cloud/vm/VmWorkTakeVolumeSnapshotTest.java b/engine/components-api/src/test/java/com/cloud/vm/VmWorkTakeVolumeSnapshotTest.java index feb7ee46aec5..f80ba9580d5d 100644 --- a/engine/components-api/src/test/java/com/cloud/vm/VmWorkTakeVolumeSnapshotTest.java +++ b/engine/components-api/src/test/java/com/cloud/vm/VmWorkTakeVolumeSnapshotTest.java @@ -26,8 +26,9 @@ public class VmWorkTakeVolumeSnapshotTest { @Test public void testVmWorkTakeVolumeSnapshotZoneIds() { List zoneIds = List.of(10L, 20L); + List poolIds = List.of(10L, 20L); VmWorkTakeVolumeSnapshot work = new VmWorkTakeVolumeSnapshot(1L, 1L, 1L, "handler", - 1L, 1L, 1L, false, null, false, zoneIds); + 1L, 1L, 1L, false, null, false, zoneIds, poolIds); Assert.assertNotNull(work.getZoneIds()); Assert.assertEquals(zoneIds.size(), work.getZoneIds().size()); Assert.assertEquals(zoneIds.get(0), work.getZoneIds().get(0)); diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index d9a79f9885b4..1fb7c02681ff 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -55,6 +55,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; @@ -577,14 +578,22 @@ public VolumeInfo createVolumeFromSnapshot(Volume volume, Snapshot snapshot, Use } VolumeInfo vol = volFactory.getVolume(volume.getId()); + long zoneId = volume.getDataCenterId(); DataStore store = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); - DataStoreRole dataStoreRole = snapshotHelper.getDataStoreRole(snapshot); - SnapshotInfo snapInfo = snapshotFactory.getSnapshotWithRoleAndZone(snapshot.getId(), dataStoreRole, volume.getDataCenterId()); - - boolean kvmSnapshotOnlyInPrimaryStorage = snapshotHelper.isKvmSnapshotOnlyInPrimaryStorage(snapshot, dataStoreRole); + DataStoreRole dataStoreRole = snapshotHelper.getDataStoreRole(snapshot, zoneId); + SnapshotInfo snapInfo = snapshotFactory.getSnapshotWithRoleAndZone(snapshot.getId(), dataStoreRole, zoneId); + boolean kvmSnapshotOnlyInPrimaryStorage = snapshotHelper.isKvmSnapshotOnlyInPrimaryStorage(snapshot, dataStoreRole, volume.getDataCenterId()); + boolean skipCopyToSecondary = false; + Map capabilities = snapInfo.getDataStore().getDriver().getCapabilities(); + boolean keepOnPrimary = MapUtils.isNotEmpty(capabilities) && capabilities.containsKey(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString()); + if (kvmSnapshotOnlyInPrimaryStorage && keepOnPrimary) { + skipCopyToSecondary = true; + } try { - snapInfo = snapshotHelper.backupSnapshotToSecondaryStorageIfNotExists(snapInfo, dataStoreRole, snapshot, kvmSnapshotOnlyInPrimaryStorage); + if (!skipCopyToSecondary) { + snapInfo = snapshotHelper.backupSnapshotToSecondaryStorageIfNotExists(snapInfo, dataStoreRole, snapshot, kvmSnapshotOnlyInPrimaryStorage); + } } catch (CloudRuntimeException e) { snapshotHelper.expungeTemporarySnapshot(kvmSnapshotOnlyInPrimaryStorage, snapInfo); throw e; @@ -596,7 +605,7 @@ public VolumeInfo createVolumeFromSnapshot(Volume volume, Snapshot snapshot, Use } // don't try to perform a sync if the DataStoreRole of the snapshot is equal to DataStoreRole.Primary - if (!DataStoreRole.Primary.equals(dataStoreRole) || kvmSnapshotOnlyInPrimaryStorage) { + if (!DataStoreRole.Primary.equals(dataStoreRole) || (kvmSnapshotOnlyInPrimaryStorage && !skipCopyToSecondary)) { try { // sync snapshot to region store if necessary DataStore snapStore = snapInfo.getDataStore(); 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 7c113a10af45..1102de16e4ea 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 @@ -33,6 +33,13 @@ public interface ResourceDetailsDao extends GenericDao */ R findDetail(long resourceId, String name); + /** + * Find details by key + * @param key + * @return + */ + List findDetails(String key); + /** * Find details by resourceId and key * @param resourceId 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 58b60531e5a7..eafaed182abd 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 @@ -65,6 +65,12 @@ public R findDetail(long resourceId, String name) { return findOneBy(sc); } + public List findDetails(String key) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("name", key); + return listBy(sc); + } + public List findDetails(long resourceId, String key) { SearchCriteria sc = AllFieldsSearch.create(); sc.setParameters("resourceId", resourceId); 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 db4c64bd0ab8..902cb73dc055 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 @@ -61,8 +61,11 @@ public interface SnapshotDataStoreDao extends GenericDao listExtractedSnapshotsBeforeDate(Date beforeDate); + List listSnapshotsBySnapshotId(long snapshotId); + List listReadyBySnapshot(long snapshotId, DataStoreRole role); + List listReadyBySnapshotId(long snapshotId); SnapshotDataStoreVO findBySourceSnapshot(long snapshotId, DataStoreRole role); List findBySnapshotIdAndNotInDestroyedHiddenState(long snapshotId); 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 b5faa6caedf1..7fb4d8d1c705 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 @@ -16,24 +16,6 @@ // under the License. package org.apache.cloudstack.storage.datastore.db; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; - -import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore; -import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; -import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; -import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.State; -import org.apache.commons.collections.CollectionUtils; -import org.springframework.stereotype.Component; - import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.DataStoreRole; import com.cloud.storage.SnapshotVO; @@ -46,6 +28,22 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.UpdateBuilder; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.State; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; @Component public class SnapshotDataStoreDaoImpl extends GenericDaoBase implements SnapshotDataStoreDao { @@ -76,6 +74,7 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore; private SearchBuilder searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq; + private SearchBuilder searchBySnapshotId; protected static final List HYPERVISORS_SUPPORTING_SNAPSHOTS_CHAINING = List.of(Hypervisor.HypervisorType.XenServer); @@ -187,6 +186,10 @@ public boolean configure(String name, Map params) throws Configu searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(STORE_ROLE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getRole(), SearchCriteria.Op.EQ); searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(STORE_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getDataStoreId(), SearchCriteria.Op.IN); + searchBySnapshotId = createSearchBuilder(); + searchBySnapshotId.and(SNAPSHOT_ID, searchBySnapshotId.entity().getSnapshotId(), SearchCriteria.Op.EQ); + searchBySnapshotId.done(); + return true; } @@ -403,6 +406,13 @@ public List listBySnapshotAndDataStoreRole(long snapshotId, return listBy(sc); } + @Override + public List listSnapshotsBySnapshotId(long snapshotId) { + SearchCriteria sc = searchBySnapshotId.create(); + sc.setParameters(SNAPSHOT_ID, snapshotId); + return listBy(sc); + } + @Override public List listReadyBySnapshot(long snapshotId, DataStoreRole role) { SearchCriteria sc = createSearchCriteriaBySnapshotIdAndStoreRole(snapshotId, role); @@ -410,6 +420,13 @@ public List listReadyBySnapshot(long snapshotId, DataStoreR return listBy(sc); } + @Override + public List listReadyBySnapshotId(long snapshotId) { + SearchCriteria sc = searchBySnapshotId.create(); + sc.setParameters(STATE, State.Ready); + return listBy(sc); + } + @Override public SnapshotDataStoreVO findBySourceSnapshot(long snapshotId, DataStoreRole role) { SearchCriteria sc = createSearchCriteriaBySnapshotIdAndStoreRole(snapshotId, role); diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/CephSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/CephSnapshotStrategy.java index 04cca2e8f923..d9d028d4d085 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/CephSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/CephSnapshotStrategy.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; @@ -46,6 +47,9 @@ public class CephSnapshotStrategy extends StorageSystemSnapshotStrategy { @Override public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) { + if (SnapshotOperation.COPY.equals(op)) { + return StrategyPriority.CANT_HANDLE; + } long volumeId = snapshot.getVolumeId(); VolumeVO volumeVO = volumeDao.findByIdIncludingRemoved(volumeId); boolean baseVolumeExists = volumeVO.getRemoved() == null; diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java index aedc2a12d0f0..c1981941ac03 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java @@ -627,9 +627,14 @@ public void doInTransactionWithoutResult(TransactionStatus status) { @Override public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) { + if (SnapshotOperation.COPY.equals(op)) { + return StrategyPriority.CANT_HANDLE; + } + if (SnapshotOperation.TAKE.equals(op)) { return validateVmSnapshot(snapshot); } + if (SnapshotOperation.REVERT.equals(op)) { long volumeId = snapshot.getVolumeId(); VolumeVO volumeVO = volumeDao.findById(volumeId); diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/ScaleIOSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/ScaleIOSnapshotStrategy.java index 0d48cb944aee..c1e38fc92512 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/ScaleIOSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/ScaleIOSnapshotStrategy.java @@ -22,6 +22,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; @@ -44,6 +45,9 @@ public class ScaleIOSnapshotStrategy extends StorageSystemSnapshotStrategy { @Override public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) { + if (SnapshotOperation.COPY.equals(op)) { + return StrategyPriority.CANT_HANDLE; + } long volumeId = snapshot.getVolumeId(); VolumeVO volumeVO = volumeDao.findByIdIncludingRemoved(volumeId); boolean baseVolumeExists = volumeVO.getRemoved() == null; diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java index 567df1262f62..10740289c8f6 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java @@ -46,6 +46,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotResult; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy; import org.apache.cloudstack.engine.subsystem.api.storage.StorageAction; import org.apache.cloudstack.engine.subsystem.api.storage.StorageCacheManager; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; @@ -899,4 +900,35 @@ public AsyncCallFuture queryCopySnapshot(SnapshotInfo snapshot) ep.sendMessageAsync(cmd, caller); return future; } + + public AsyncCallFuture copySnapshot(SnapshotInfo sourceSnapshot, SnapshotInfo destSnapshot, SnapshotStrategy strategy) { + try { + if (destSnapshot.getStatus() == ObjectInDataStoreStateMachine.State.Allocated) { + destSnapshot.processEvent(Event.CreateOnlyRequested); + } else if (sourceSnapshot.getStatus() == ObjectInDataStoreStateMachine.State.Ready) { + destSnapshot.processEvent(Event.CopyRequested); + } else { + logger.info(String.format("Cannot copy snapshot to another storage in different zone. It's not in the right state %s", sourceSnapshot.getStatus())); + sourceSnapshot.processEvent(Event.OperationFailed); + throw new CloudRuntimeException(String.format("Cannot copy snapshot to another storage in different zone. It's not in the right state %s", sourceSnapshot.getStatus())); + } + } catch (Exception e) { + logger.debug("Failed to change snapshot state: " + e.toString()); + sourceSnapshot.processEvent(Event.OperationFailed); + throw new CloudRuntimeException(e); + } + + AsyncCallFuture future = new AsyncCallFuture(); + try { + CopySnapshotContext context = new CopySnapshotContext<>(null, sourceSnapshot, destSnapshot, future); + AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().copySnapshotZoneAsyncCallback(null, null)).setContext(context); + strategy.copySnapshot(sourceSnapshot, destSnapshot, caller); + } catch (Exception e) { + logger.debug("Failed to take snapshot: " + destSnapshot.getId(), e); + destSnapshot.processEvent(Event.OperationFailed); + throw new CloudRuntimeException("Failed to copy snapshot" + destSnapshot.getId()); + } + return future; + } } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/StorageSystemSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/StorageSystemSnapshotStrategy.java index 9838e41f8f6c..8b90e58124a3 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/StorageSystemSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/StorageSystemSnapshotStrategy.java @@ -38,6 +38,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation; import org.apache.cloudstack.storage.command.SnapshotAndCopyAnswer; import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -912,7 +913,9 @@ private boolean usingBackendSnapshotFor(long snapshotId) { @Override public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) { Snapshot.LocationType locationType = snapshot.getLocationType(); - + if (SnapshotOperation.COPY.equals(op)) { + return StrategyPriority.CANT_HANDLE; + } // If the snapshot exists on Secondary Storage, we can't delete it. if (SnapshotOperation.DELETE.equals(op)) { if (Snapshot.LocationType.SECONDARY.equals(locationType)) { diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolAnswer.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolAnswer.java index 437e786f0f61..80b87a49acbd 100644 --- a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolAnswer.java +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolAnswer.java @@ -36,14 +36,16 @@ public class StorPoolModifyStoragePoolAnswer extends Answer{ private List datastoreClusterChildren = new ArrayList<>(); private String clusterId; private String clientNodeId; + private String clusterLocation; - public StorPoolModifyStoragePoolAnswer(StorPoolModifyStoragePoolCommand cmd, long capacityBytes, long availableBytes, Map tInfo, String clusterId, String clientNodeId) { + public StorPoolModifyStoragePoolAnswer(StorPoolModifyStoragePoolCommand cmd, long capacityBytes, long availableBytes, Map tInfo, String clusterId, String clientNodeId, String clusterLocation) { super(cmd); result = true; poolInfo = new StoragePoolInfo(null, cmd.getPool().getHost(), cmd.getPool().getPath(), cmd.getLocalPath(), cmd.getPool().getType(), capacityBytes, availableBytes); templateInfo = tInfo; this.clusterId = clusterId; this.clientNodeId = clientNodeId; + this.clusterLocation = clusterLocation; } public StorPoolModifyStoragePoolAnswer(String errMsg) { @@ -101,4 +103,12 @@ public String getClientNodeId() { public void setClientNodeId(String clientNodeId) { this.clientNodeId = clientNodeId; } + + public String getClusterLocation() { + return clusterLocation; + } + + public void setClusterLocation(String clusterLocation) { + this.clusterLocation = clusterLocation; + } } diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolModifyStorageCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolModifyStorageCommandWrapper.java index a44ff5473ae5..8d6dcff8aed7 100644 --- a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolModifyStorageCommandWrapper.java +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolModifyStorageCommandWrapper.java @@ -24,6 +24,7 @@ import java.util.Map.Entry; import java.util.Set; +import org.apache.commons.lang3.StringUtils; import com.cloud.agent.api.Answer; import com.cloud.agent.api.storage.StorPoolModifyStoragePoolAnswer; @@ -38,7 +39,9 @@ import com.cloud.storage.template.TemplateProp; import com.cloud.utils.script.OutputInterpreter; import com.cloud.utils.script.Script; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; @ResourceWrapper(handles = StorPoolModifyStoragePoolCommand.class) @@ -51,6 +54,7 @@ public Answer execute(final StorPoolModifyStoragePoolCommand command, final Libv logger.debug(String.format("Could not get StorPool cluster id for a command [%s]", command.getClass())); return new Answer(command, false, "spNotFound"); } + String clusterLocation = getStorPoolClusterLocation(clusterId); try { String result = attachOrDetachVolume("attach", "volume", command.getVolumeName()); if (result != null) { @@ -66,7 +70,7 @@ public Answer execute(final StorPoolModifyStoragePoolCommand command, final Libv } final Map tInfo = new HashMap<>(); - return new StorPoolModifyStoragePoolAnswer(command, storagepool.getCapacity(), storagepool.getAvailable(), tInfo, clusterId, storagepool.getStorageNodeId()); + return new StorPoolModifyStoragePoolAnswer(command, storagepool.getCapacity(), storagepool.getAvailable(), tInfo, clusterId, storagepool.getStorageNodeId(), clusterLocation); } catch (Exception e) { logger.debug(String.format("Could not modify storage due to %s", e.getMessage())); return new Answer(command, e); @@ -118,4 +122,28 @@ public String attachOrDetachVolume(String command, String type, String volumeUui } return res; } + + private String getStorPoolClusterLocation(String clusterId) { + Script sc = new Script("storpool", 300000, logger); + sc.add("-j"); + sc.add("location"); + sc.add("list"); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + + String res = sc.execute(parser); + if (res == null) { + JsonObject jsonObj = new JsonParser().parse(parser.getLines()).getAsJsonObject(); + if (jsonObj.getAsJsonObject("data") != null) { + JsonArray arr = jsonObj.getAsJsonObject("data").getAsJsonArray("locations"); + for (JsonElement jsonElement : arr) { + JsonObject obj = jsonElement.getAsJsonObject(); + if (StringUtils.contains(clusterId, obj.get("id").getAsString())) { + return obj.get("name").getAsString(); + } + } + } + } + return null; + } } diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java index 6258767921d2..1310cde72222 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java @@ -19,28 +19,9 @@ package org.apache.cloudstack.storage.collector; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.Configurable; -import org.apache.cloudstack.managed.context.ManagedContextRunnable; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -//import org.apache.cloudstack.storage.datastore.util.StorPoolHelper; -import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; -import org.apache.commons.collections.CollectionUtils; - +import com.cloud.dc.dao.ClusterDao; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.SnapshotDetailsVO; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.db.DB; @@ -50,14 +31,43 @@ import com.cloud.utils.db.TransactionStatus; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.util.StorPoolHelper; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; +import org.apache.cloudstack.storage.snapshot.StorPoolSnapshotStrategy; +import org.apache.commons.collections.CollectionUtils; + +import javax.inject.Inject; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class StorPoolAbandonObjectsCollector extends ManagerBase implements Configurable { @Inject private PrimaryDataStoreDao storagePoolDao; @Inject private StoragePoolDetailsDao storagePoolDetailsDao; + @Inject + private SnapshotDetailsDao snapshotDetailsDao; + @Inject + private ClusterDao clusterDao; private ScheduledExecutorService _volumeTagsUpdateExecutor; + private ScheduledExecutorService snapshotRecoveryCheckExecutor; private static final String ABANDON_LOGGER = "/var/log/cloudstack/management/storpool-abandoned-objects"; @@ -69,6 +79,9 @@ public class StorPoolAbandonObjectsCollector extends ManagerBase implements Conf "storpool.snapshot.tags.checkup", "86400", "Minimal interval (in seconds) to check and report if StorPool snapshot exists in CloudStack snapshots database", false); + static final ConfigKey snapshotRecoveryFromRemoteCheck = new ConfigKey("Advanced", Integer.class, + "storpool.snapshot.recovery.from.remote.check", "300", + "Minimal interval (in seconds) to check and recover StorPool snapshot from remote", false); @Override public String getConfigComponentName() { @@ -77,7 +90,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] { volumeCheckupTagsInterval, snapshotCheckupTagsInterval }; + return new ConfigKey[] { volumeCheckupTagsInterval, snapshotCheckupTagsInterval, snapshotRecoveryFromRemoteCheck }; } @Override @@ -93,6 +106,8 @@ private void init() { } _volumeTagsUpdateExecutor = Executors.newScheduledThreadPool(2, new NamedThreadFactory("StorPoolAbandonObjectsCollector")); + snapshotRecoveryCheckExecutor = Executors.newScheduledThreadPool(1, + new NamedThreadFactory("StorPoolSnapshotRecoveryCheck")); if (volumeCheckupTagsInterval.value() > 0) { _volumeTagsUpdateExecutor.scheduleAtFixedRate(new StorPoolVolumesTagsUpdate(), @@ -102,6 +117,10 @@ private void init() { _volumeTagsUpdateExecutor.scheduleAtFixedRate(new StorPoolSnapshotsTagsUpdate(), snapshotCheckupTagsInterval.value(), snapshotCheckupTagsInterval.value(), TimeUnit.SECONDS); } + if (snapshotRecoveryFromRemoteCheck.value() > 0) { + snapshotRecoveryCheckExecutor.scheduleAtFixedRate(new StorPoolSnapshotRecoveryCheck(), + snapshotRecoveryFromRemoteCheck.value(), snapshotRecoveryFromRemoteCheck.value(), TimeUnit.SECONDS); + } } class StorPoolVolumesTagsUpdate extends ManagedContextRunnable { @@ -322,4 +341,71 @@ private Map getStorPoolNamesAndCsTag(JsonArray arr) { } return map; } + + class StorPoolSnapshotRecoveryCheck extends ManagedContextRunnable { + + @Override + protected void runInContext() { + List spPools = storagePoolDao.findPoolsByProvider(StorPoolUtil.SP_PROVIDER_NAME); + if (CollectionUtils.isEmpty(spPools)) { + return; + } + List snapshotDetails = snapshotDetailsDao.findDetails(StorPoolUtil.SP_RECOVERED_SNAPSHOT); + if (CollectionUtils.isEmpty(snapshotDetails)) { + return; + } + Map onePoolforZone = new HashMap<>(); + for (StoragePoolVO storagePoolVO : spPools) { + onePoolforZone.put(storagePoolVO.getDataCenterId(), storagePoolVO); + } + List recoveredSnapshots = new ArrayList<>(); + for (StoragePoolVO storagePool : onePoolforZone.values()) { + try { + logger.debug(String.format("Checking StorPool recovered snapshots for zone [%s]", + storagePool.getDataCenterId())); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(storagePool.getUuid(), + storagePool.getId(), storagePoolDetailsDao, storagePoolDao); + JsonArray arr = StorPoolUtil.snapshotsList(conn); + List snapshots = snapshotsForRcovery(arr); + if (snapshots.isEmpty()) { + continue; + } + for (SnapshotDetailsVO snapshot : snapshotDetails) { + String name = snapshot.getValue().split(";")[0]; + String location = snapshot.getValue().split(";")[1]; + if (name == null || location == null) { + StorPoolUtil.spLog("Could not find name or location for the snapshot %s", snapshot.getValue()); + continue; + } + if (snapshots.contains(name)) { + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId(name, conn), clusterDao); + conn = StorPoolSnapshotStrategy.getSpConnectionDesc(conn, clusterId); + SpApiResponse resp = StorPoolUtil.snapshotUnexport(name, location, conn); + if (resp.getError() == null) { + recoveredSnapshots.add(snapshot.getId()); + } else { + logger.debug(String.format("Could not recover StorPool snapshot %s", resp.getError())); + } + } + } + } catch (Exception e) { + logger.debug(String.format("Could not collect StorPool recovered snapshots %s", e.getMessage())); + } + } + for (Long recoveredSnapshot : recoveredSnapshots) { + snapshotDetailsDao.remove(recoveredSnapshot); + } + } + } + + private static List snapshotsForRcovery(JsonArray arr) { + List snapshots = new ArrayList<>(); + for (int i = 0; i < arr.size(); i++) { + boolean recoveringFromRemote = arr.get(i).getAsJsonObject().get("recoveringFromRemote").getAsBoolean(); + if (!recoveringFromRemote) { + snapshots.add(arr.get(i).getAsJsonObject().get("name").getAsString()); + } + } + return snapshots; + } } diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java index d93990ee0711..e98a924b9c1a 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java @@ -18,18 +18,19 @@ */ package org.apache.cloudstack.storage.datastore.driver; +import com.cloud.storage.dao.SnapshotDetailsVO; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; -import com.cloud.storage.dao.SnapshotDetailsVO; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; @@ -123,6 +124,7 @@ import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.VMInstanceDao; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -187,7 +189,10 @@ private SnapshotDataStoreVO getSnapshotImageStoreRef(long snapshotId, long zoneI @Override public Map getCapabilities() { - return null; + Map mapCapabilities = new HashMap<>(); + mapCapabilities.put(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES.toString(), Boolean.TRUE.toString()); + mapCapabilities.put(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString(), Boolean.TRUE.toString()); + return mapCapabilities; } @Override @@ -520,6 +525,15 @@ public void deleteAsync(DataStore dataStore, DataObject data, AsyncCompletionCal } catch (Exception e) { err = String.format("Could not delete volume due to %s", e.getMessage()); } + } else if (data.getType() == DataObjectType.SNAPSHOT) { + SnapshotInfo snapshot = (SnapshotInfo) data; + SpConnectionDesc conn = StorPoolUtil.getSpConnection(snapshot.getDataStore().getUuid(), snapshot.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + String name = StorPoolStorageAdaptor.getVolumeNameFromPath(snapshot.getPath(), true); + SpApiResponse resp = StorPoolUtil.snapshotDelete(name, conn); + if (resp.getError() != null) { + err = String.format("Failed to clean-up Storpool snapshot %s. Error: %s", name, resp.getError()); + StorPoolUtil.spLog(err); + } } else { err = String.format("Invalid DataObjectType \"%s\" passed to deleteAsync", data.getType()); } @@ -606,7 +620,22 @@ private void logDataObject(final String pref, DataObject data) { @Override public boolean canCopy(DataObject srcData, DataObject dstData) { - return true; + DataObjectType srcType = srcData.getType(); + DataObjectType dstType = dstData.getType(); + if (srcType == DataObjectType.SNAPSHOT && dstType == DataObjectType.VOLUME) { + return true; + } else if (srcType == DataObjectType.SNAPSHOT && dstType == DataObjectType.SNAPSHOT) { + return true; + } else if (srcType == DataObjectType.VOLUME && dstType == DataObjectType.TEMPLATE) { + return true; + } else if (srcType == DataObjectType.TEMPLATE && dstType == DataObjectType.TEMPLATE) { + return true; + } else if (srcType == DataObjectType.TEMPLATE && dstType == DataObjectType.VOLUME) { + return true; + } else if (srcType == DataObjectType.VOLUME && dstType == DataObjectType.VOLUME) { + return true; + } + return false; } @Override @@ -624,25 +653,18 @@ public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCal try { if (srcType == DataObjectType.SNAPSHOT && dstType == DataObjectType.VOLUME) { SnapshotInfo sinfo = (SnapshotInfo)srcData; - final String snapshotName = StorPoolHelper.getSnapshotName(srcData.getId(), srcData.getUuid(), snapshotDataStoreDao, snapshotDetailsDao); - VolumeInfo vinfo = (VolumeInfo)dstData; final String volumeName = vinfo.getUuid(); final Long size = vinfo.getSize(); SpConnectionDesc conn = StorPoolUtil.getSpConnection(vinfo.getDataStore().getUuid(), vinfo.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(((SnapshotInfo) srcData).getPath(), true); StorPoolVolumeDef spVolume = createVolumeWithTags(sinfo, snapshotName, vinfo, volumeName, size, conn); SpApiResponse resp = StorPoolUtil.volumeCreate(spVolume, conn); if (resp.getError() == null) { updateStoragePool(dstData.getDataStore().getId(), size); - - VolumeObjectTO to = (VolumeObjectTO)dstData.getTO(); - to.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); - to.setSize(size); - - answer = new CopyCmdAnswer(to); - StorPoolUtil.spLog("Created volume=%s with uuid=%s from snapshot=%s with uuid=%s", StorPoolUtil.getNameFromResponse(resp, false), to.getUuid(), snapshotName, sinfo.getUuid()); + StorPoolUtil.spLog("Created volume=%s with uuid=%s from snapshot=%s with uuid=%s", StorPoolUtil.getNameFromResponse(resp, false), volumeName, snapshotName, sinfo.getUuid()); } else if (resp.getError().getName().equals("objectDoesNotExist")) { //check if snapshot is on secondary storage StorPoolUtil.spLog("Snapshot %s does not exists on StorPool, will try to create a volume from a snapshot on secondary storage", snapshotName); @@ -658,8 +680,24 @@ public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCal } else { answer = new Answer(cmd, false, String.format("Could not create Storpool volume %s from snapshot %s. Error: %s", volumeName, snapshotName, emptyVolumeCreateResp.getError())); } + VolumeObjectTO to = (VolumeObjectTO) dstData.getTO(); + to.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + to.setSize(size); + + answer = new CopyCmdAnswer(to); + StorPoolUtil.spLog("Created volume=%s with uuid=%s from snapshot=%s with uuid=%s", StorPoolUtil.getNameFromResponse(resp, false), to.getUuid(), snapshotName, sinfo.getUuid()); } else { - answer = new Answer(cmd, false, String.format("The snapshot %s does not exists neither on primary, neither on secondary storage. Cannot create volume from snapshot", snapshotName)); + err = String.format("Could not create volume from a snapshot due to {}", resp.getError()); + } + } else if (sinfo.getDataStore().getRole().equals(DataStoreRole.Image)) { + //check if snapshot is on secondary storage + StorPoolUtil.spLog("Snapshot %s does not exists on StorPool, will try to create a volume from a snapshot on secondary storage", sinfo.getName()); + SnapshotDataStoreVO snap = getSnapshotImageStoreRef(sinfo.getId(), vinfo.getDataCenterId()); + SpApiResponse emptyVolumeCreateResp = StorPoolUtil.volumeCreate(volumeName, null, size, null, null, "volume", null, conn); + if (emptyVolumeCreateResp.getError() == null) { + answer = createVolumeFromSnapshot(srcData, dstData, size, emptyVolumeCreateResp); + } else { + answer = new Answer(cmd, false, String.format("Could not create Storpool volume %s from snapshot %s. Error: %s", volumeName, snapshotName, emptyVolumeCreateResp.getError())); } } else { answer = new Answer(cmd, false, String.format("Could not create Storpool volume %s from snapshot %s. Error: %s", volumeName, snapshotName, resp.getError())); @@ -668,7 +706,7 @@ public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCal SnapshotInfo sinfo = (SnapshotInfo)srcData; SnapshotDetailsVO snapshotDetail = snapshotDetailsDao.findDetail(sinfo.getId(), StorPoolUtil.SP_DELAY_DELETE); // bypass secondary storage - if (StorPoolConfigurationManager.BypassSecondaryStorage.value() || snapshotDetail != null) { + if (Boolean.FALSE.equals(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value())) { SnapshotObjectTO snapshot = (SnapshotObjectTO) srcData.getTO(); answer = new CopyCmdAnswer(snapshot); } else { @@ -678,9 +716,9 @@ public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCal final String snapName = StorPoolStorageAdaptor.getVolumeNameFromPath(((SnapshotInfo) srcData).getPath(), true); SpConnectionDesc conn = StorPoolUtil.getSpConnection(srcData.getDataStore().getUuid(), srcData.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); try { - Long clusterId = StorPoolHelper.findClusterIdByGlobalId(snapName, clusterDao); - EndPoint ep = clusterId != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(StorPoolHelper.findHostByCluster(clusterId, hostDao)) : selector.select(srcData, dstData); - if (ep == null) { + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId(snapName, conn), clusterDao); + HostVO host = clusterId != null ? StorPoolHelper.findHostByCluster(clusterId, hostDao) : null; + EndPoint ep = host != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(host) : selector.select(srcData, dstData); if (ep == null) { err = "No remote endpoint to send command, check if host or ssvm is down?"; } else { answer = ep.sendMessage(cmd); @@ -712,8 +750,7 @@ public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCal StorPoolHelper.getTimeout(StorPoolHelper.PrimaryStorageDownloadWait, configDao), VirtualMachineManager.ExecuteInSequence.value()); try { - Long clusterId = StorPoolHelper.findClusterIdByGlobalId(volumeName, clusterDao); - EndPoint ep2 = clusterId != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(StorPoolHelper.findHostByCluster(clusterId, hostDao)) : selector.select(srcData, dstData); + EndPoint ep2 = selector.select(srcData, dstData); if (ep2 == null) { err = "No remote endpoint to send command, check if host or ssvm is down?"; } else { @@ -937,8 +974,9 @@ public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCal StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.copyAsnc command=%s ", cmd); try { - Long clusterId = StorPoolHelper.findClusterIdByGlobalId(snapshotName, clusterDao); - EndPoint ep = clusterId != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(StorPoolHelper.findHostByCluster(clusterId, hostDao)) : selector.select(srcData, dstData); + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId(snapshotName, conn), clusterDao); + HostVO host = clusterId != null ? StorPoolHelper.findHostByCluster(clusterId, hostDao) : null; + EndPoint ep = host != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(host) : selector.select(srcData, dstData); StorPoolUtil.spLog("selector.select(srcData, dstData) ", ep); if (ep == null) { ep = selector.select(dstData); diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolHostListener.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolHostListener.java index 7e0986bc63b5..e27e15e04a82 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolHostListener.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolHostListener.java @@ -170,6 +170,7 @@ public boolean hostConnect(long hostId, long poolId) throws StorageConflictExcep } StorPoolHelper.setSpClusterIdIfNeeded(hostId, mspAnswer.getClusterId(), clusterDao, hostDao, clusterDetailsDao); + StorPoolHelper.setLocationIfNeeded(pool, storagePoolDetailsDao, mspAnswer.getClusterLocation()); StorPoolUtil.spLog("Connection established between storage pool [%s] and host [%s]", poolVO, host); return true; diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java index f13d296af3b0..53fafda02b72 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java @@ -19,26 +19,6 @@ package org.apache.cloudstack.storage.datastore.util; -import java.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.config.impl.ConfigurationVO; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; -import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; -import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; -import org.apache.cloudstack.storage.snapshot.StorPoolConfigurationManager; -import org.apache.cloudstack.storage.to.VolumeObjectTO; -import org.apache.commons.collections4.CollectionUtils; - import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.ClusterDetailsVO; import com.cloud.dc.ClusterVO; @@ -49,6 +29,7 @@ import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.storage.DataStoreRole; +import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateStoragePoolVO; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.SnapshotDetailsDao; @@ -64,6 +45,25 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.config.impl.ConfigurationVO; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.snapshot.StorPoolConfigurationManager; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections4.CollectionUtils; + +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; public class StorPoolHelper { @@ -218,6 +218,22 @@ public static void setSpClusterIdIfNeeded(long hostId, String clusterId, Cluster } } + public static void setLocationIfNeeded(StoragePool storagePool, StoragePoolDetailsDao storagePoolDetails, + String location) { + if (location == null) { + return; + } + StoragePoolDetailVO storagePoolDetailVO = storagePoolDetails.findDetail(storagePool.getId(), + StorPoolConfigurationManager.StorPoolClusterLocation.key()); + if (storagePoolDetailVO == null) { + storagePoolDetails.persist(new StoragePoolDetailVO(storagePool.getId(), + StorPoolConfigurationManager.StorPoolClusterLocation.key(), location, true)); + } else if (storagePoolDetailVO.getValue() == null || !storagePoolDetailVO.getValue().equals(location)) { + storagePoolDetailVO.setValue(location); + storagePoolDetails.update(storagePoolDetailVO.getId(), storagePoolDetailVO); + } + } + public static Long findClusterIdByGlobalId(String globalId, ClusterDao clusterDao) { List clusterIds = clusterDao.listAllIds(); if (clusterIds.size() == 1) { @@ -238,7 +254,7 @@ public static Long findClusterIdByGlobalId(String globalId, ClusterDao clusterDa public static HostVO findHostByCluster(Long clusterId, HostDao hostDao) { List host = hostDao.findByClusterId(clusterId); - return host != null ? host.get(0) : null; + return CollectionUtils.isNotEmpty(host) ? host.get(0) : null; } public static int getTimeout(String cfg, ConfigurationDao configDao) { diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java index fa9248033bfb..b337e8a1f7f1 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java @@ -137,6 +137,9 @@ public static void spLog(String fmt, Object... args) { public static final String DELAY_DELETE = "delayDelete"; public static final String SP_TIER = "SP_QOSCLASS"; + public static final String SP_RECOVERED_SNAPSHOT = "SP_RECOVERED_SNAPSHOT"; + + public static final String SP_REMOTE_LOCATION = "SP_REMOTE_LOCATION"; public static final String OBJECT_DOES_NOT_EXIST = "objectDoesNotExist"; @@ -429,6 +432,14 @@ public static boolean snapshotExists(final String name, SpConnectionDesc conn) { return resp.getError() == null ? true : objectExists(resp.getError()); } + public static boolean snapshotRecovered(final String name, SpConnectionDesc conn) { + SpApiResponse resp = GET("Snapshot/" + name, conn); + JsonObject obj = resp.fullJson.getAsJsonObject(); + JsonObject data = obj.getAsJsonArray("data").get(0).getAsJsonObject(); + boolean recoveringFromRemote = data.getAsJsonPrimitive("recoveringFromRemote").getAsBoolean(); + return recoveringFromRemote; + } + public static JsonArray snapshotsList(SpConnectionDesc conn) { SpApiResponse resp = GET("MultiCluster/SnapshotsList", conn); JsonObject obj = resp.fullJson.getAsJsonObject(); @@ -675,6 +686,40 @@ public static SpApiResponse snapshotDelete(final String name, SpConnectionDesc c return resp.getError() == null ? POST("MultiCluster/SnapshotDelete/" + name, null, conn) : resp; } + public static SpApiResponse snapshotExport(String name, String location, SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("snapshot", name); + json.put("location", location); + return POST("SnapshotExport", json, conn); + } + + public static SpApiResponse snapshotUnexport(String name, String location, SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("snapshot", name); + json.put("location", location); + return POST("SnapshotUnexport", json, conn); + } + + public static String getSnapshotClusterId(String snapshotName, SpConnectionDesc conn) { + SpApiResponse resp = POST("MultiCluster/SnapshotUpdate/" + snapshotName, new HashMap<>(), conn); + JsonObject json = resp.fullJson.getAsJsonObject(); + return json.get("clusterId").getAsString(); + } + + public static SpApiResponse snapshotFromRemote(String name, String remoteLocation, String template, + SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("remoteId", name); + json.put("remoteLocation", remoteLocation); + json.put("template", template); + json.put("name", ""); + return POST("SnapshotFromRemote", json, conn); + } + + public static SpApiResponse snapshotReconcile(String name, SpConnectionDesc conn) { + return POST("SnapshotReconcile/" + name, null, conn); + } + public static SpApiResponse detachAllForced(final String name, final boolean snapshot, SpConnectionDesc conn) { final String type = snapshot ? "snapshot" : "volume"; List> json = new ArrayList<>(); diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java index aa972d44343a..c69ff5cc0f62 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java @@ -19,13 +19,42 @@ package org.apache.cloudstack.storage.motion; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; - +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.MigrateAnswer; +import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; +import com.cloud.agent.api.ModifyTargetsAnswer; +import com.cloud.agent.api.ModifyTargetsCommand; +import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.storage.StorPoolBackupTemplateFromSnapshotCommand; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.StorageManager; +import com.cloud.storage.VMTemplateDetailVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.GuestOSCategoryDao; +import com.cloud.storage.dao.GuestOSDao; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.SnapshotDetailsVO; +import com.cloud.storage.dao.VMTemplateDetailsDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionStrategy; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; @@ -55,48 +84,18 @@ import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; -import org.apache.cloudstack.storage.snapshot.StorPoolConfigurationManager; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.commons.collections.MapUtils; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; -import com.cloud.agent.AgentManager; -import com.cloud.agent.api.Answer; -import com.cloud.agent.api.Command; -import com.cloud.agent.api.MigrateAnswer; -import com.cloud.agent.api.MigrateCommand; -import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; -import com.cloud.agent.api.ModifyTargetsAnswer; -import com.cloud.agent.api.ModifyTargetsCommand; -import com.cloud.agent.api.PrepareForMigrationCommand; -import com.cloud.agent.api.storage.StorPoolBackupTemplateFromSnapshotCommand; -import com.cloud.agent.api.to.DataObjectType; -import com.cloud.agent.api.to.VirtualMachineTO; -import com.cloud.dc.dao.ClusterDao; -import com.cloud.exception.AgentUnavailableException; -import com.cloud.exception.OperationTimedoutException; -import com.cloud.host.Host; -import com.cloud.host.dao.HostDao; -import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.storage.Storage.ImageFormat; -import com.cloud.storage.StorageManager; -import com.cloud.storage.VMTemplateDetailVO; -import com.cloud.storage.Volume; -import com.cloud.storage.VolumeVO; -import com.cloud.storage.dao.GuestOSCategoryDao; -import com.cloud.storage.dao.GuestOSDao; -import com.cloud.storage.dao.SnapshotDao; -import com.cloud.storage.dao.SnapshotDetailsDao; -import com.cloud.storage.dao.SnapshotDetailsVO; -import com.cloud.storage.dao.VMTemplateDetailsDao; -import com.cloud.storage.dao.VolumeDao; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.VirtualMachineManager; -import com.cloud.vm.dao.VMInstanceDao; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Component public class StorPoolDataMotionStrategy implements DataMotionStrategy { @@ -149,10 +148,13 @@ public class StorPoolDataMotionStrategy implements DataMotionStrategy { public StrategyPriority canHandle(DataObject srcData, DataObject destData) { DataObjectType srcType = srcData.getType(); DataObjectType dstType = destData.getType(); + if (srcType == DataObjectType.SNAPSHOT && dstType == DataObjectType.TEMPLATE) { SnapshotInfo sinfo = (SnapshotInfo) srcData; - VolumeInfo volume = sinfo.getBaseVolume(); - StoragePoolVO storagePool = _storagePool.findById(volume.getPoolId()); + if (!sinfo.getDataStore().getRole().equals(DataStoreRole.Primary)) { + return StrategyPriority.CANT_HANDLE; + } + StoragePoolVO storagePool = _storagePool.findById(sinfo.getDataStore().getId()); if (!storagePool.getStorageProviderName().equals(StorPoolUtil.SP_PROVIDER_NAME)) { return StrategyPriority.CANT_HANDLE; } @@ -163,7 +165,7 @@ public StrategyPriority canHandle(DataObject srcData, DataObject destData) { String snapshotName = StorPoolHelper.getSnapshotName(sinfo.getId(), sinfo.getUuid(), _snapshotStoreDao, _snapshotDetailsDao); StorPoolUtil.spLog("StorPoolDataMotionStrategy.canHandle snapshot name=%s", snapshotName); - if (snapshotName != null && StorPoolConfigurationManager.BypassSecondaryStorage.value()) { + if (snapshotName != null) { return StrategyPriority.HIGHEST; } } @@ -175,13 +177,12 @@ public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback callback) { SnapshotObjectTO snapshot = (SnapshotObjectTO) srcData.getTO(); TemplateObjectTO template = (TemplateObjectTO) destData.getTO(); - DataStore store = _dataStore.getDataStore(snapshot.getVolume().getDataStore().getUuid(), - snapshot.getVolume().getDataStore().getRole()); + DataStore store = _dataStore.getDataStore(snapshot.getDataStore().getUuid(), + snapshot.getDataStore().getRole()); SnapshotInfo sInfo = _snapshotDataFactory.getSnapshot(snapshot.getId(), store); - VolumeInfo vInfo = sInfo.getBaseVolume(); - SpConnectionDesc conn = StorPoolUtil.getSpConnection(vInfo.getDataStore().getUuid(), - vInfo.getDataStore().getId(), _storagePoolDetails, _storagePool); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(sInfo.getDataStore().getUuid(), + sInfo.getDataStore().getId(), _storagePoolDetails, _storagePool); String name = template.getUuid(); String volumeName = ""; @@ -209,11 +210,9 @@ public void copyAsync(DataObject srcData, DataObject destData, Host destHost, // final String snapName = // StorpoolStorageAdaptor.getVolumeNameFromPath(((SnapshotInfo) // srcData).getPath(), true); - Long clusterId = StorPoolHelper.findClusterIdByGlobalId(parentName, _clusterDao); - EndPoint ep2 = clusterId != null - ? RemoteHostEndPoint - .getHypervisorHostEndPoint(StorPoolHelper.findHostByCluster(clusterId, _hostDao)) - : _selector.select(sInfo, destData); + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId(parentName, conn), _clusterDao); + HostVO host = clusterId != null ? StorPoolHelper.findHostByCluster(clusterId, _hostDao) : null; + EndPoint ep2 = host != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(host) : _selector.select(srcData, destData); if (ep2 == null) { err = "No remote endpoint to send command, check if host or ssvm is down?"; } else { @@ -238,7 +237,7 @@ public void copyAsync(DataObject srcData, DataObject destData, Host destHost, StorPoolUtil.volumeDelete(volumeName, conn); } _vmTemplateDetailsDao.persist(new VMTemplateDetailVO(template.getId(), StorPoolUtil.SP_STORAGE_POOL_ID, - String.valueOf(vInfo.getDataStore().getId()), false)); + String.valueOf(sInfo.getDataStore().getId()), false)); StorPoolUtil.spLog("StorPoolDataMotionStrategy.copyAsync Creating snapshot=%s for StorPool template=%s", volumeName, conn.getTemplateName()); final CopyCommandResult cmd = new CopyCommandResult(null, answer); diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolConfigurationManager.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolConfigurationManager.java index e4e930c8deea..00cef88c4cfc 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolConfigurationManager.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolConfigurationManager.java @@ -53,6 +53,10 @@ public class StorPoolConfigurationManager implements Configurable { "storpool.list.snapshots.delete.after.interval", "360", "The interval (in seconds) to fetch the StorPool snapshots with deleteAfter flag", false); + public static final ConfigKey StorPoolClusterLocation = new ConfigKey(String.class, "sp.cluster.location", "Advanced", null, + "StorPool cluster location", true, ConfigKey.Scope.StoragePool, null); + public static final ConfigKey StorPoolSubclusterEndpoint = new ConfigKey<>(String.class, "sp.cluster.endpoint", "Advanced", null, + "StorPool sub-cluster endpoint", true, ConfigKey.Scope.Cluster, null); @Override public String getConfigComponentName() { @@ -61,6 +65,6 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] { BypassSecondaryStorage, StorPoolClusterId, AlternativeEndPointEnabled, AlternativeEndpoint, VolumesStatsInterval, StorageStatsInterval, DeleteAfterInterval, ListSnapshotsWithDeleteAfterInterval }; + return new ConfigKey[] { BypassSecondaryStorage, StorPoolClusterId, AlternativeEndPointEnabled, AlternativeEndpoint, VolumesStatsInterval, StorageStatsInterval, DeleteAfterInterval, ListSnapshotsWithDeleteAfterInterval, StorPoolClusterLocation, StorPoolSubclusterEndpoint }; } } diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java index 5ec86df91e17..5901e2718f3e 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java @@ -16,12 +16,15 @@ // under the License. package org.apache.cloudstack.storage.snapshot; +import com.cloud.api.query.dao.SnapshotJoinDao; +import com.cloud.api.query.vo.SnapshotJoinVO; +import com.cloud.dc.dao.ClusterDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotVO; -import com.cloud.storage.VolumeVO; +import com.cloud.storage.Storage; import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.SnapshotDetailsDao; import com.cloud.storage.dao.SnapshotDetailsVO; @@ -30,6 +33,8 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.fsm.NoTransitionException; +import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; @@ -39,16 +44,23 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.storage.command.CreateObjectAnswer; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.util.StorPoolHelper; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; + import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; @@ -82,6 +94,10 @@ public class StorPoolSnapshotStrategy implements SnapshotStrategy { DataStoreManager dataStoreMgr; @Inject SnapshotZoneDao snapshotZoneDao; + @Inject + SnapshotJoinDao snapshotJoinDao; + @Inject + private ClusterDao clusterDao; @Override public SnapshotInfo backupSnapshot(SnapshotInfo snapshotInfo) { @@ -104,49 +120,70 @@ public SnapshotInfo backupSnapshot(SnapshotInfo snapshotInfo) { public boolean deleteSnapshot(Long snapshotId, Long zoneId) { final SnapshotVO snapshotVO = _snapshotDao.findById(snapshotId); - VolumeVO volume = _volumeDao.findByIdIncludingRemoved(snapshotVO.getVolumeId()); String name = StorPoolHelper.getSnapshotName(snapshotId, snapshotVO.getUuid(), _snapshotStoreDao, _snapshotDetailsDao); boolean res = false; // clean-up snapshot from Storpool storage pools - StoragePoolVO storage = _primaryDataStoreDao.findById(volume.getPoolId()); - if (storage.getStorageProviderName().equals(StorPoolUtil.SP_PROVIDER_NAME)) { - try { - SpConnectionDesc conn = StorPoolUtil.getSpConnection(storage.getUuid(), storage.getId(), storagePoolDetailsDao, _primaryDataStoreDao); - SpApiResponse resp = StorPoolUtil.snapshotDelete(name, conn); - if (resp.getError() != null) { - final String err = String.format("Failed to clean-up Storpool snapshot %s. Error: %s", name, resp.getError()); - StorPoolUtil.spLog(err); - markSnapshotAsDestroyedIfAlreadyRemoved(snapshotId, resp.getError().getName().equals(StorPoolUtil.OBJECT_DOES_NOT_EXIST)); - throw new CloudRuntimeException(err); - } else { - res = deleteSnapshotFromDbIfNeeded(snapshotVO, zoneId); - markSnapshotAsDestroyedIfAlreadyRemoved(snapshotId,true); - StorPoolUtil.spLog("StorpoolSnapshotStrategy.deleteSnapshot: executed successfully=%s, snapshot %s, name=%s", res, snapshotVO, name); + List snapshotDataStoreVOS; + List snapshotJoinVOList = snapshotJoinDao.listBySnapshotIdAndZoneId(zoneId, snapshotId); + try { + for (SnapshotJoinVO snapshot: snapshotJoinVOList) { + if (State.Destroyed.equals(snapshot.getStatus())) { + continue; + } + if (snapshot.getStoreRole().isImageStore()) { + continue; } - } catch (Exception e) { - String errMsg = String.format("Cannot delete snapshot due to %s", e.getMessage()); - throw new CloudRuntimeException(errMsg); + StoragePoolVO storage = _primaryDataStoreDao.findById(snapshot.getStoreId()); + if (zoneId != null) { + if (!zoneId.equals(snapshot.getDataCenterId())) { + continue; + } + res = deleteSnapshot(snapshotId, zoneId, snapshotVO, name, storage); + break; + } + res = deleteSnapshot(snapshotId, zoneId, snapshotVO, name, storage); } + } catch (Exception e) { + String errMsg = String.format("Cannot delete snapshot due to %s", e.getMessage()); + throw new CloudRuntimeException(errMsg); } - - List snapshots = _snapshotStoreDao.listBySnapshotIdAndState(snapshotId, State.Ready); - if (res || CollectionUtils.isEmpty(snapshots)) { + snapshotDataStoreVOS = _snapshotStoreDao.listSnapshotsBySnapshotId(snapshotId); + boolean areAllSnapshotsDestroyed = snapshotDataStoreVOS.stream().allMatch(v -> v.getState().equals(State.Destroyed) || v.getState().equals(State.Destroying)); + if (areAllSnapshotsDestroyed) { updateSnapshotToDestroyed(snapshotVO); return true; } return res; } - private void markSnapshotAsDestroyedIfAlreadyRemoved(Long snapshotId, boolean isSnapshotDeleted) { - if (!isSnapshotDeleted) { - return; - } - List snapshotsOnStore = _snapshotStoreDao.listBySnapshotIdAndState(snapshotId, State.Ready); - for (SnapshotDataStoreVO snapshot : snapshotsOnStore) { - if (snapshot.getInstallPath() != null && snapshot.getInstallPath().contains(StorPoolUtil.SP_DEV_PATH)) { - snapshot.setState(State.Destroyed); - _snapshotStoreDao.update(snapshot.getId(), snapshot); + private boolean deleteSnapshot(Long snapshotId, Long zoneId, SnapshotVO snapshotVO, String name, StoragePoolVO storage) { + + boolean res; + SpConnectionDesc conn = StorPoolUtil.getSpConnection(storage.getUuid(), storage.getId(), storagePoolDetailsDao, _primaryDataStoreDao); + SpApiResponse resp = StorPoolUtil.snapshotDelete(name, conn); + if (resp.getError() != null) { + if (resp.getError().getDescr().contains("still exported")) { + throw new CloudRuntimeException(String.format("The snapshot [%s] was exported to another cluster. [%s]", name, resp.getError())); } + final String err = String.format("Failed to clean-up Storpool snapshot %s. Error: %s", name, resp.getError()); + StorPoolUtil.spLog(err); + if (resp.getError().getName().equals("objectDoesNotExist")) { + markSnapshotAsDestroyedIfAlreadyRemoved(snapshotId, storage.getId()); + } + res = false; + } else { + markSnapshotAsDestroyedIfAlreadyRemoved(snapshotId, storage.getId()); + res = deleteSnapshotFromDbIfNeeded(snapshotVO, zoneId); + StorPoolUtil.spLog("StorpoolSnapshotStrategy.deleteSnapshot: executed successfully=%s, snapshot uuid=%s, name=%s", res, snapshotVO.getUuid(), name); + } + return res; + } + + private void markSnapshotAsDestroyedIfAlreadyRemoved(Long snapshotId, Long storeId) { + SnapshotDataStoreVO snapshotOnPrimary = _snapshotStoreDao.findByStoreSnapshot(DataStoreRole.Primary, storeId, snapshotId); + if (snapshotOnPrimary != null) { + snapshotOnPrimary.setState(State.Destroyed); + _snapshotStoreDao.update(snapshotOnPrimary.getId(), snapshotOnPrimary); } } @@ -154,29 +191,20 @@ private void markSnapshotAsDestroyedIfAlreadyRemoved(Long snapshotId, boolean is public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) { logger.debug("StorpoolSnapshotStrategy.canHandle: snapshot {}, op={}", snapshot, op); - if (op != SnapshotOperation.DELETE) { + if (op != SnapshotOperation.DELETE && op != SnapshotOperation.COPY) { return StrategyPriority.CANT_HANDLE; } - SnapshotDataStoreVO snapshotOnPrimary = _snapshotStoreDao.findOneBySnapshotAndDatastoreRole(snapshot.getId(), DataStoreRole.Primary); - if (snapshotOnPrimary == null) { + List pools = _primaryDataStoreDao.findPoolsByStorageType(Storage.StoragePoolType.StorPool); + if (CollectionUtils.isEmpty(pools)) { return StrategyPriority.CANT_HANDLE; } - if (zoneId != null) { // If zoneId is present, then it should be same as the zoneId of primary store - StoragePoolVO storagePoolVO = _primaryDataStoreDao.findById(snapshotOnPrimary.getDataStoreId()); - if (!zoneId.equals(storagePoolVO.getDataCenterId())) { - return StrategyPriority.CANT_HANDLE; + for (StoragePoolVO pool : pools) { + SnapshotDataStoreVO snapshotOnPrimary = _snapshotStoreDao.findByStoreSnapshot(DataStoreRole.Primary, pool.getId(), snapshot.getId()); + if (snapshotOnPrimary != null && (snapshotOnPrimary.getState().equals(State.Ready) || snapshotOnPrimary.getState().equals(State.Created))) { + return StrategyPriority.HIGHEST; } } - String name = StorPoolHelper.getSnapshotName(snapshot.getId(), snapshot.getUuid(), _snapshotStoreDao, _snapshotDetailsDao); - if (name != null) { - StorPoolUtil.spLog("StorpoolSnapshotStrategy.canHandle: globalId=%s", name); - return StrategyPriority.HIGHEST; - } - SnapshotDetailsVO snapshotDetails = _snapshotDetailsDao.findDetail(snapshot.getId(), snapshot.getUuid()); - if (snapshotDetails != null) { - _snapshotDetailsDao.remove(snapshotDetails.getId()); - } return StrategyPriority.CANT_HANDLE; } @@ -250,48 +278,23 @@ protected boolean areLastSnapshotRef(long snapshotId) { protected boolean deleteSnapshotOnImageAndPrimary(long snapshotId, DataStore store) { SnapshotInfo snapshotOnImage = snapshotDataFactory.getSnapshot(snapshotId, store); SnapshotObject obj = (SnapshotObject)snapshotOnImage; - boolean areLastSnapshotRef = areLastSnapshotRef(snapshotId); - try { - if (areLastSnapshotRef) { - obj.processEvent(Snapshot.Event.DestroyRequested); - } - } catch (NoTransitionException e) { - logger.debug("Failed to set the state to destroying: ", e); - return false; - } + boolean result = false; try { - boolean result = deleteSnapshotChain(snapshotOnImage); + result = deleteSnapshotChain(snapshotOnImage); _snapshotStoreDao.updateDisplayForSnapshotStoreRole(snapshotId, store.getId(), store.getRole(), false); - if (areLastSnapshotRef) { - obj.processEvent(Snapshot.Event.OperationSucceeded); - } - if (result) { - SnapshotDataStoreVO snapshotOnPrimary = _snapshotStoreDao.findOneBySnapshotAndDatastoreRole(snapshotOnImage.getSnapshotId(), DataStoreRole.Primary); - if (snapshotOnPrimary != null) { - snapshotOnPrimary.setState(State.Destroyed); - _snapshotStoreDao.update(snapshotOnPrimary.getId(), snapshotOnPrimary); - } - } } catch (Exception e) { logger.debug("Failed to delete snapshot: ", e); - try { - if (areLastSnapshotRef) { - obj.processEvent(Snapshot.Event.OperationFailed); - } - } catch (NoTransitionException e1) { - logger.debug("Failed to change snapshot state: " + e.toString()); - } return false; } - return true; + return result; } private boolean deleteSnapshotFromDbIfNeeded(SnapshotVO snapshotVO, Long zoneId) { final long snapshotId = snapshotVO.getId(); SnapshotDetailsVO snapshotDetails = _snapshotDetailsDao.findDetail(snapshotId, snapshotVO.getUuid()); if (snapshotDetails != null) { - _snapshotDetailsDao.removeDetails(snapshotId); + _snapshotDetailsDao.remove(snapshotId); } if (zoneId != null && List.of(Snapshot.State.Allocated, Snapshot.State.CreatedOnPrimary).contains(snapshotVO.getState())) { @@ -327,12 +330,6 @@ private boolean deleteSnapshotFromDbIfNeeded(SnapshotVO snapshotVO, Long zoneId) return true; } - if (snapshotVO.getState() == Snapshot.State.CreatedOnPrimary) { - snapshotVO.setState(Snapshot.State.Destroyed); - _snapshotDao.update(snapshotId, snapshotVO); - return true; - } - if (!Snapshot.State.BackedUp.equals(snapshotVO.getState()) && !Snapshot.State.Error.equals(snapshotVO.getState()) && !Snapshot.State.Destroying.equals(snapshotVO.getState())) { throw new InvalidParameterValueException(String.format("Can't delete snapshot %s due to it is in %s Status", snapshotVO, snapshotVO.getState())); @@ -354,7 +351,6 @@ private boolean deleteSnapshotFromDbIfNeeded(SnapshotVO snapshotVO, Long zoneId) if (CollectionUtils.isNotEmpty(retrieveSnapshotEntries(snapshotId, null))) { return true; } - updateSnapshotToDestroyed(snapshotVO); return true; } @@ -380,4 +376,88 @@ public boolean revertSnapshot(SnapshotInfo snapshot) { @Override public void postSnapshotCreation(SnapshotInfo snapshot) { } + + @Override + public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncCompletionCallback callback) { + + // export snapshot on remote + StoragePoolVO storagePoolVO = _primaryDataStoreDao.findById(snapshotDest.getDataStore().getId()); + String location = StorPoolConfigurationManager.StorPoolClusterLocation.valueIn(snapshotDest.getDataStore().getId()); + StorPoolUtil.spLog("StorpoolSnapshotStrategy.copySnapshot: snapshot %s to pool=%s", snapshot.getUuid(), storagePoolVO.getName()); + CreateCmdResult res = null; + SnapshotInfo srcSnapshot = (SnapshotInfo) snapshot; + SnapshotInfo destSnapshot = (SnapshotInfo) snapshotDest; + String err = null; + if (location != null) { + SpConnectionDesc connectionLocal = StorPoolUtil.getSpConnection(snapshot.getDataStore().getUuid(), + snapshot.getDataStore().getId(), storagePoolDetailsDao, _primaryDataStoreDao); + String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(srcSnapshot.getPath(), false); + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId("~" + snapshotName, connectionLocal), clusterDao); + connectionLocal = getSpConnectionDesc(connectionLocal, clusterId); + SpApiResponse resp = StorPoolUtil.snapshotExport("~" + snapshotName, location, connectionLocal); + if (resp.getError() != null) { + StorPoolUtil.spLog("Failed to export snapshot %s from %s due to %s", snapshotName, location, resp.getError()); + err = String.format("Failed to export snapshot %s from %s due to %s", snapshotName, location, resp.getError()); + res = new CreateCmdResult(destSnapshot.getPath(), null); + res.setResult(err); + callback.complete(res); + return; + } + SpConnectionDesc connectionRemote = StorPoolUtil.getSpConnection(storagePoolVO.getUuid(), + storagePoolVO.getId(), storagePoolDetailsDao, _primaryDataStoreDao); + String localLocation = StorPoolConfigurationManager.StorPoolClusterLocation + .valueIn(snapshot.getDataStore().getId()); + StoragePoolDetailVO template = storagePoolDetailsDao.findDetail(storagePoolVO.getId(), + StorPoolUtil.SP_TEMPLATE); + SpApiResponse respFromRemote = StorPoolUtil.snapshotFromRemote(snapshotName, localLocation, + template.getValue(), connectionRemote); + if (respFromRemote.getError() != null) { + StorPoolUtil.spLog("Failed to copy snapshot %s to %s due to %s", snapshotName, location, respFromRemote.getError()); + err = String.format("Failed to copy snapshot %s to %s due to %s", snapshotName, location, respFromRemote.getError()); + res = new CreateCmdResult(destSnapshot.getPath(), null); + res.setResult(err); + callback.complete(res); + return; + } + StorPoolUtil.spLog("The snapshot [%s] was copied from remote", snapshotName); + String detail = "~" + snapshotName + ";" + location; + SnapshotDetailsVO snapshotForRecovery = new SnapshotDetailsVO(snapshot.getId(), StorPoolUtil.SP_RECOVERED_SNAPSHOT, detail, true); + _snapshotDetailsDao.persist(snapshotForRecovery); + respFromRemote = StorPoolUtil.snapshotReconcile("~" + snapshotName, connectionRemote); + if (respFromRemote.getError() != null) { + StorPoolUtil.spLog("Failed to reconcile snapshot %s from %s due to %s", snapshotName, location, respFromRemote.getError()); + err = String.format("Failed to reconcile snapshot %s from %s due to %s", snapshotName, location, respFromRemote.getError()); + res = new CreateCmdResult(destSnapshot.getPath(), null); + res.setResult(err); + callback.complete(res); + return; + } + SnapshotDataStoreVO snapshotStore = _snapshotStoreDao.findByStoreSnapshot(DataStoreRole.Primary, snapshotDest.getDataStore().getId(), destSnapshot.getSnapshotId()); + snapshotStore.setInstallPath(srcSnapshot.getPath()); + _snapshotStoreDao.update(snapshotStore.getId(), snapshotStore); + + } else { + res = new CreateCmdResult(destSnapshot.getPath(), null); + res.setResult("The snapshot is not in the right location"); + callback.complete(res); + return; + } + SnapshotObjectTO snap = (SnapshotObjectTO) snapshotDest.getTO(); + snap.setPath(srcSnapshot.getPath()); + CreateObjectAnswer answer = new CreateObjectAnswer(snap); + res = new CreateCmdResult(destSnapshot.getPath(), answer); + res.setResult(err); + callback.complete(res); + } + + public static SpConnectionDesc getSpConnectionDesc(SpConnectionDesc connectionLocal, Long clusterId) { + + String subClusterEndPoint = StorPoolConfigurationManager.StorPoolSubclusterEndpoint.valueIn(clusterId); + if (StringUtils.isNotEmpty(subClusterEndPoint)) { + String host = subClusterEndPoint.split(";")[0].split("=")[1]; + String token = subClusterEndPoint.split(";")[1].split("=")[1]; + connectionLocal = new SpConnectionDesc(host, token, connectionLocal.getTemplateName()); + } + return connectionLocal; + } } diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 019d7ef213df..db4b87ad53eb 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -1717,6 +1717,19 @@ public static List findSnapshotPolicyZones(SnapshotPolicy policy, return s_zoneDao.listByIds(zoneIds); } + public static List findSnapshotPolicyPools(SnapshotPolicy policy, Volume volume) { + List poolDetails = s_snapshotPolicyDetailsDao.findDetails(policy.getId(), ApiConstants.STORAGE_ID); + List poolIds = new ArrayList<>(); + for (SnapshotPolicyDetailVO detail : poolDetails) { + try { + poolIds.add(Long.valueOf(detail.getValue())); + } catch (NumberFormatException ignored) {} + } + if (volume != null && !poolIds.contains(volume.getPoolId())) { + poolIds.add(0, volume.getPoolId()); + } + return s_storagePoolDao.listByIds(poolIds); + } public static VpcOffering findVpcOfferingById(long offeringId) { return s_vpcOfferingDao.findById(offeringId); } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 41433cb3e6f8..e3265fbb0231 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -884,6 +884,15 @@ public SnapshotPolicyResponse createSnapshotPolicyResponse(SnapshotPolicy policy zoneResponses.add(zoneResponse); } policyResponse.setZones(new HashSet<>(zoneResponses)); + List poolResponses = new ArrayList<>(); + List pools = ApiDBUtils.findSnapshotPolicyPools(policy, vol); + for (StoragePoolVO pool : pools) { + StoragePoolResponse storagePoolResponse = new StoragePoolResponse(); + storagePoolResponse.setId(pool.getUuid()); + storagePoolResponse.setName(pool.getName()); + poolResponses.add(storagePoolResponse); + } + policyResponse.setStoragePools(new HashSet<>(poolResponses)); return policyResponse; } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 1b60bdcc9e1a..63c109343056 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -5892,9 +5892,17 @@ public ListResponse listSnapshots(ListSnapshotsCmd cmd) { public SnapshotResponse listSnapshot(CopySnapshotCmd cmd) { Account caller = CallContext.current().getCallingAccount(); List zoneIds = cmd.getDestinationZoneIds(); + Long zoneId = null; + String location = null; + if (CollectionUtils.isNotEmpty(zoneIds)) { + zoneId = zoneIds.get(0); + location = Snapshot.LocationType.SECONDARY.name(); + } else { + location = cmd.getSnapshot().getLocationType() != null ? cmd.getSnapshot().getLocationType().name() : null; + } Pair, Integer> result = searchForSnapshotsWithParams(cmd.getId(), null, null, null, null, null, - null, null, zoneIds.get(0), Snapshot.LocationType.SECONDARY.name(), + null, null, zoneId, location, false, null, null, null, null, null, null, null, true, false, caller); ResponseView respView = ResponseView.Restricted; diff --git a/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDao.java index 25dfbfe67146..9e19228dbe74 100644 --- a/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDao.java @@ -17,13 +17,13 @@ package com.cloud.api.query.dao; -import java.util.List; +import com.cloud.api.query.vo.SnapshotJoinVO; +import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.response.SnapshotResponse; -import com.cloud.api.query.vo.SnapshotJoinVO; -import com.cloud.utils.db.GenericDao; +import java.util.List; public interface SnapshotJoinDao extends GenericDao { @@ -34,4 +34,6 @@ public interface SnapshotJoinDao extends GenericDao { List searchBySnapshotStorePair(String... pairs); List findByDistinctIds(Long zoneId, Long... ids); + + List listBySnapshotIdAndZoneId(Long zoneId, Long id); } diff --git a/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDaoImpl.java index bca60383501f..f198d9d81667 100644 --- a/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDaoImpl.java @@ -69,6 +69,8 @@ public class SnapshotJoinDaoImpl extends GenericDaoBaseWithTagInformation snapshotIdsSearch; + private final SearchBuilder snapshotByZoneSearch; + SnapshotJoinDaoImpl() { snapshotStorePairSearch = createSearchBuilder(); snapshotStorePairSearch.and("snapshotStoreState", snapshotStorePairSearch.entity().getStoreState(), SearchCriteria.Op.IN); @@ -80,6 +82,11 @@ public class SnapshotJoinDaoImpl extends GenericDaoBaseWithTagInformation findByDistinctIds(Long zoneId, Long... ids) { sc.setParameters("idsIN", ids); return searchIncludingRemoved(sc, searchFilter, null, false); } + + public List listBySnapshotIdAndZoneId(Long zoneId, Long id) { + if (id == null) { + return new ArrayList<>(); + } + SearchCriteria sc = snapshotByZoneSearch.create(); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + sc.setParameters("id", id); + return listBy(sc); + } } diff --git a/server/src/main/java/com/cloud/storage/CreateSnapshotPayload.java b/server/src/main/java/com/cloud/storage/CreateSnapshotPayload.java index 7ade5e51ef59..39dafbbeb414 100644 --- a/server/src/main/java/com/cloud/storage/CreateSnapshotPayload.java +++ b/server/src/main/java/com/cloud/storage/CreateSnapshotPayload.java @@ -29,6 +29,7 @@ public class CreateSnapshotPayload { private boolean asyncBackup; private List zoneIds; private boolean kvmIncrementalSnapshot = false; + private List storagePoolIds; public Long getSnapshotPolicyId() { return snapshotPolicyId; @@ -85,6 +86,15 @@ public boolean isKvmIncrementalSnapshot() { } public void setKvmIncrementalSnapshot(boolean kvmIncrementalSnapshot) { + this.kvmIncrementalSnapshot = kvmIncrementalSnapshot; } + + public List getStoragePoolIds() { + return storagePoolIds; + } + + public void setStoragePoolIds(List storagePoolIds) { + this.storagePoolIds = storagePoolIds; + } } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 529ed3f1d7b8..57bad42bd0f6 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -65,6 +65,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; @@ -3808,9 +3809,9 @@ protected Volume liveMigrateVolume(Volume volume, StoragePool destPool) throws S @Override @ActionEvent(eventType = EventTypes.EVENT_SNAPSHOT_CREATE, eventDescription = "taking snapshot", async = true) public Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, - Snapshot.LocationType locationType, boolean asyncBackup, Map tags, List zoneIds) + Snapshot.LocationType locationType, boolean asyncBackup, Map tags, List zoneIds, List poolIds) throws ResourceAllocationException { - final Snapshot snapshot = takeSnapshotInternal(volumeId, policyId, snapshotId, account, quiescevm, locationType, asyncBackup, zoneIds); + final Snapshot snapshot = takeSnapshotInternal(volumeId, policyId, snapshotId, account, quiescevm, locationType, asyncBackup, zoneIds, poolIds); if (snapshot != null && MapUtils.isNotEmpty(tags)) { taggedResourceService.createTags(Collections.singletonList(snapshot.getUuid()), ResourceTag.ResourceObjectType.Snapshot, tags, null); } @@ -3818,7 +3819,7 @@ public Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Acco } private Snapshot takeSnapshotInternal(Long volumeId, Long policyId, Long snapshotId, Account account, - boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, List zoneIds) + boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, List zoneIds, List poolIds) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); VolumeInfo volume = volFactory.getVolume(volumeId); @@ -3834,6 +3835,11 @@ private Snapshot takeSnapshotInternal(Long volumeId, Long policyId, Long snapsho } List details = snapshotPolicyDetailsDao.findDetails(policyId, ApiConstants.ZONE_ID); zoneIds = details.stream().map(d -> Long.valueOf(d.getValue())).collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(poolIds)) { + throw new InvalidParameterValueException(String.format("%s can not be specified for snapshots linked with snapshot policy", ApiConstants.STORAGE_ID_LIST)); + } + List poolDetails = snapshotPolicyDetailsDao.findDetails(policyId, ApiConstants.STORAGE_ID); + poolIds = poolDetails.stream().map(d -> Long.valueOf(d.getValue())).collect(Collectors.toList()); } if (CollectionUtils.isNotEmpty(zoneIds)) { for (Long destZoneId : zoneIds) { @@ -3872,14 +3878,14 @@ private Snapshot takeSnapshotInternal(Long volumeId, Long policyId, Long snapsho placeHolder = createPlaceHolderWork(vm.getId()); try { return orchestrateTakeVolumeSnapshot(volumeId, policyId, snapshotId, account, quiescevm, - locationType, asyncBackup, zoneIds); + locationType, asyncBackup, zoneIds, poolIds); } finally { _workJobDao.expunge(placeHolder.getId()); } } else { Outcome outcome = takeVolumeSnapshotThroughJobQueue(vm.getId(), volumeId, policyId, - snapshotId, account.getId(), quiescevm, locationType, asyncBackup, zoneIds); + snapshotId, account.getId(), quiescevm, locationType, asyncBackup, zoneIds, poolIds); try { outcome.get(); @@ -3912,13 +3918,16 @@ private Snapshot takeSnapshotInternal(Long volumeId, Long policyId, Long snapsho if (CollectionUtils.isNotEmpty(zoneIds)) { payload.setZoneIds(zoneIds); } + if (CollectionUtils.isNotEmpty(poolIds)) { + payload.setStoragePoolIds(poolIds); + } volume.addPayload(payload); return volService.takeSnapshot(volume); } } private Snapshot orchestrateTakeVolumeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, - boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, List zoneIds) + boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, List zoneIds, List poolIds) throws ResourceAllocationException { VolumeInfo volume = volFactory.getVolume(volumeId); @@ -3931,7 +3940,7 @@ private Snapshot orchestrateTakeVolumeSnapshot(Long volumeId, Long policyId, Lon throw new InvalidParameterValueException(String.format("Volume: %s is not in %s state but %s. Cannot take snapshot.", volume.getVolume(), Volume.State.Ready, volume.getState())); } - boolean isSnapshotOnStorPoolOnly = volume.getStoragePoolType() == StoragePoolType.StorPool && BooleanUtils.toBoolean(_configDao.getValue("sp.bypass.secondary.storage")); + boolean isSnapshotOnStorPoolOnly = volume.getStoragePoolType() == StoragePoolType.StorPool && SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value(); if (volume.getEncryptFormat() != null && volume.getAttachedVM() != null && volume.getAttachedVM().getState() != State.Stopped && !isSnapshotOnStorPoolOnly) { logger.debug(String.format("Refusing to take snapshot of encrypted volume (%s) on running VM (%s)", volume, volume.getAttachedVM())); throw new UnsupportedOperationException("Volume snapshots for encrypted volumes are not supported if VM is running"); @@ -3948,6 +3957,10 @@ private Snapshot orchestrateTakeVolumeSnapshot(Long volumeId, Long policyId, Lon if (CollectionUtils.isNotEmpty(zoneIds)) { payload.setZoneIds(zoneIds); } + if (CollectionUtils.isNotEmpty(poolIds)) { + payload.setStoragePoolIds(poolIds); + } + volume.addPayload(payload); return volService.takeSnapshot(volume); @@ -3963,7 +3976,7 @@ private boolean isOperationSupported(VMTemplateVO template, UserVmVO userVm) { @Override @ActionEvent(eventType = EventTypes.EVENT_SNAPSHOT_CREATE, eventDescription = "allocating snapshot", create = true) - public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List zoneIds) throws ResourceAllocationException { + public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List zoneIds, List poolIds) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); VolumeInfo volume = volFactory.getVolume(volumeId); @@ -4012,6 +4025,7 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, if (storagePool == null) { throw new InvalidParameterValueException(String.format("Volume: %s please attach this volume to a VM before create snapshot for it", volume.getVolume())); } + boolean canCopyOnPrimary = canCopyOnPrimary(poolIds, volume, CollectionUtils.isEmpty(poolIds)); if (CollectionUtils.isNotEmpty(zoneIds)) { if (policyId != null && policyId > 0) { @@ -4020,7 +4034,7 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, if (Snapshot.LocationType.PRIMARY.equals(locationType)) { throw new InvalidParameterValueException(String.format("%s cannot be specified with snapshot %s as %s", ApiConstants.ZONE_ID_LIST, ApiConstants.LOCATION_TYPE, Snapshot.LocationType.PRIMARY)); } - if (Boolean.FALSE.equals(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value())) { + if (Boolean.FALSE.equals(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value()) && !canCopyOnPrimary) { throw new InvalidParameterValueException("Backing up of snapshot has been disabled. Snapshot can not be taken for multiple zones"); } if (DataCenter.Type.Edge.equals(zone.getType())) { @@ -4044,6 +4058,24 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, return snapshotMgr.allocSnapshot(volumeId, policyId, snapshotName, locationType, false, zoneIds); } + private boolean canCopyOnPrimary(List poolIds, VolumeInfo volume, boolean isPoolIdsEmpty) { + if (!isPoolIdsEmpty) { + for (Long poolId : poolIds){ + DataStore dataStore = dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); + StoragePoolVO sPool = _storagePoolDao.findById(poolId); + if (dataStore != null + && !dataStore.getDriver().getCapabilities().containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES.toString()) + && sPool.getPoolType() != volume.getStoragePoolType() + && volume.getPoolId() == poolId) { + throw new InvalidParameterValueException("The specified pool doesn't support copying snapshots between zones" + poolId); + } + } + } else { + return false; + } + return true; + } + @Override public Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName, Long vmSnapshotId) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); @@ -5173,7 +5205,7 @@ private Outcome migrateVolumeThroughJobQueue(VMInstanceVO vm, VolumeVO v } public Outcome takeVolumeSnapshotThroughJobQueue(final Long vmId, final Long volumeId, final Long policyId, final Long snapshotId, final Long accountId, final boolean quiesceVm, - final Snapshot.LocationType locationType, final boolean asyncBackup, final List zoneIds) { + final Snapshot.LocationType locationType, final boolean asyncBackup, final List zoneIds, List poolIds) { final CallContext context = CallContext.current(); final User callingUser = context.getCallingUser(); @@ -5195,7 +5227,7 @@ public Outcome takeVolumeSnapshotThroughJobQueue(final Long vmId, fina // save work context info (there are some duplications) VmWorkTakeVolumeSnapshot workInfo = new VmWorkTakeVolumeSnapshot(callingUser.getId(), accountId != null ? accountId : callingAccount.getId(), vm.getId(), - VolumeApiServiceImpl.VM_WORK_JOB_HANDLER, volumeId, policyId, snapshotId, quiesceVm, locationType, asyncBackup, zoneIds); + VolumeApiServiceImpl.VM_WORK_JOB_HANDLER, volumeId, policyId, snapshotId, quiesceVm, locationType, asyncBackup, zoneIds, poolIds); workJob.setCmdInfo(VmWorkSerializer.serialize(workInfo)); _jobMgr.submitAsyncJob(workJob, VmWorkConstants.VM_WORK_QUEUE, vm.getId()); @@ -5246,7 +5278,7 @@ private Pair orchestrateMigrateVolume(VmWorkMigrateVolum private Pair orchestrateTakeVolumeSnapshot(VmWorkTakeVolumeSnapshot work) throws Exception { Account account = _accountDao.findById(work.getAccountId()); orchestrateTakeVolumeSnapshot(work.getVolumeId(), work.getPolicyId(), work.getSnapshotId(), account, - work.isQuiesceVm(), work.getLocationType(), work.isAsyncBackup(), work.getZoneIds()); + work.isQuiesceVm(), work.getLocationType(), work.isAsyncBackup(), work.getZoneIds(), work.getPoolIds()); return new Pair(JobInfo.Status.SUCCEEDED, _jobMgr.marshallResultObject(work.getSnapshotId())); } diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 5a4d6a68402a..d5cbefcc4853 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -135,6 +135,7 @@ import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; +import com.cloud.storage.StoragePoolStatus; import com.cloud.storage.VMTemplateStorageResourceAssoc; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; @@ -180,6 +181,8 @@ import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; @Component public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implements SnapshotManager, SnapshotApiService, Configurable { @@ -900,7 +903,8 @@ public boolean deleteSnapshot(long snapshotId, Long zoneId) { boolean result = snapshotStrategy.deleteSnapshot(snapshotId, zoneId); if (result) { for (Long zId : zoneIds) { - if (snapshotCheck.getState() == Snapshot.State.BackedUp) { + List listActiveSnapshots = _snapshotStoreDao.listReadyBySnapshotId(snapshotId); + if (snapshotCheck.getState() == Snapshot.State.BackedUp && CollectionUtils.isEmpty(listActiveSnapshots)) { UsageEventUtils.publishUsageEvent(EventTypes.EVENT_SNAPSHOT_DELETE, snapshotCheck.getAccountId(), zId, snapshotId, snapshotCheck.getName(), null, null, 0L, snapshotCheck.getClass().getName(), snapshotCheck.getUuid()); } @@ -919,6 +923,19 @@ public boolean deleteSnapshot(long snapshotId, Long zoneId) { } } } + final SnapshotVO postDeleteSnapshotEntry = _snapshotDao.findById(snapshotId); + if (postDeleteSnapshotEntry == null || Snapshot.State.Destroyed.equals(postDeleteSnapshotEntry.getState())) { + annotationDao.removeByEntityType(AnnotationService.EntityType.SNAPSHOT.name(), snapshotCheck.getUuid()); + + if (snapshotCheck.getState() != Snapshot.State.Error && snapshotCheck.getState() != Snapshot.State.Destroyed) { + _resourceLimitMgr.decrementResourceCount(snapshotCheck.getAccountId(), ResourceType.snapshot); + } + } + for (SnapshotDataStoreVO snapshotStoreRef : snapshotStoreRefs) { + if (ObjectInDataStoreStateMachine.State.Ready.equals(snapshotStoreRef.getState()) && !DataStoreRole.Primary.equals(snapshotStoreRef.getRole())) { + _resourceLimitMgr.decrementResourceCount(snapshotCheck.getAccountId(), ResourceType.secondary_storage, new Long(snapshotStoreRef.getPhysicalSize())); + } + } return result; } @@ -1114,11 +1131,13 @@ public boolean deleteSnapshotDirsForAccount(Account account) { return success; } - protected void validatePolicyZones(List zoneIds, VolumeVO volume, Account caller) { - if (CollectionUtils.isEmpty(zoneIds)) { + protected void validatePolicyZones(List zoneIds, List poolIds, VolumeVO volume, Account caller) { + boolean hasPools = CollectionUtils.isNotEmpty(poolIds); + boolean hasZones = CollectionUtils.isNotEmpty(zoneIds); + if (!hasZones && !hasPools) { return; } - if (Boolean.FALSE.equals(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value())) { + if (Boolean.FALSE.equals(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value()) && hasZones) { throw new InvalidParameterValueException("Backing up of snapshot has been disabled. Snapshot can not be taken for multiple zones"); } final DataCenterVO zone = dataCenterDao.findById(volume.getDataCenterId()); @@ -1126,8 +1145,16 @@ protected void validatePolicyZones(List zoneIds, VolumeVO volume, Account throw new InvalidParameterValueException("Backing up of snapshot is not supported by the zone of the volume. Snapshots can not be taken for multiple zones"); } boolean isRootAdminCaller = _accountMgr.isRootAdmin(caller.getId()); - for (Long zoneId : zoneIds) { - getCheckedDestinationZoneForSnapshotCopy(zoneId, isRootAdminCaller); + + if (hasZones) { + for (Long zoneId : zoneIds) { + getCheckedDestinationZoneForSnapshotCopy(zoneId, isRootAdminCaller); + } + } + if (hasPools) { + for (Long poolId : poolIds) { + getCheckedDestinationStorageForSnapshotCopy(poolId, isRootAdminCaller); + } } } @@ -1230,15 +1257,16 @@ public SnapshotPolicyVO createPolicy(CreateSnapshotPolicyCmd cmd, Account policy } final List zoneIds = cmd.getZoneIds(); - validatePolicyZones(zoneIds, volume, caller); + final List poolIds = cmd.getStoragePoolIds(); + validatePolicyZones(zoneIds, poolIds, volume, caller); Map tags = cmd.getTags(); boolean active = true; - return persistSnapshotPolicy(volume, schedule, timezoneId, intvType, maxSnaps, display, active, tags, zoneIds); + return persistSnapshotPolicy(volume, schedule, timezoneId, intvType, maxSnaps, display, active, tags, zoneIds, poolIds); } - protected SnapshotPolicyVO persistSnapshotPolicy(VolumeVO volume, String schedule, String timezone, IntervalType intervalType, int maxSnaps, boolean display, boolean active, Map tags, List zoneIds) { + protected SnapshotPolicyVO persistSnapshotPolicy(VolumeVO volume, String schedule, String timezone, IntervalType intervalType, int maxSnaps, boolean display, boolean active, Map tags, List zoneIds, List poolIds) { long volumeId = volume.getId(); GlobalLock createSnapshotPolicyLock = GlobalLock.getInternLock("createSnapshotPolicy_" + volumeId); @@ -1250,13 +1278,14 @@ protected SnapshotPolicyVO persistSnapshotPolicy(VolumeVO volume, String schedul logger.debug("Acquired lock for creating snapshot policy [{}] for volume {}.", intervalType, volume); + try { SnapshotPolicyVO policy = _snapshotPolicyDao.findOneByVolumeInterval(volumeId, intervalType); if (policy == null) { - policy = createSnapshotPolicy(volumeId, schedule, timezone, intervalType, maxSnaps, display, zoneIds); + policy = createSnapshotPolicy(volumeId, schedule, timezone, intervalType, maxSnaps, display, zoneIds, poolIds); } else { - updateSnapshotPolicy(policy, schedule, timezone, intervalType, maxSnaps, active, display, zoneIds); + updateSnapshotPolicy(policy, schedule, timezone, intervalType, maxSnaps, active, display, zoneIds, poolIds); } createTagsForSnapshotPolicy(tags, policy); @@ -1268,7 +1297,7 @@ protected SnapshotPolicyVO persistSnapshotPolicy(VolumeVO volume, String schedul } } - protected SnapshotPolicyVO createSnapshotPolicy(long volumeId, String schedule, String timezone, IntervalType intervalType, int maxSnaps, boolean display, List zoneIds) { + protected SnapshotPolicyVO createSnapshotPolicy(long volumeId, String schedule, String timezone, IntervalType intervalType, int maxSnaps, boolean display, List zoneIds, List poolIds) { SnapshotPolicyVO policy = new SnapshotPolicyVO(volumeId, schedule, timezone, intervalType, maxSnaps, display); policy = _snapshotPolicyDao.persist(policy); if (CollectionUtils.isNotEmpty(zoneIds)) { @@ -1278,12 +1307,19 @@ protected SnapshotPolicyVO createSnapshotPolicy(long volumeId, String schedule, } snapshotPolicyDetailsDao.saveDetails(details); } + if (CollectionUtils.isNotEmpty(poolIds)) { + List details = new ArrayList<>(); + for (Long poolId : poolIds) { + details.add(new SnapshotPolicyDetailVO(policy.getId(), ApiConstants.STORAGE_ID, String.valueOf(poolId))); + } + snapshotPolicyDetailsDao.saveDetails(details); + } _snapSchedMgr.scheduleNextSnapshotJob(policy); logger.debug(String.format("Created snapshot policy %s.", new ReflectionToStringBuilder(policy, ToStringStyle.JSON_STYLE).setExcludeFieldNames("id", "uuid", "active"))); return policy; } - protected void updateSnapshotPolicy(SnapshotPolicyVO policy, String schedule, String timezone, IntervalType intervalType, int maxSnaps, boolean active, boolean display, List zoneIds) { + protected void updateSnapshotPolicy(SnapshotPolicyVO policy, String schedule, String timezone, IntervalType intervalType, int maxSnaps, boolean active, boolean display, List zoneIds, List poolIds) { String previousPolicy = new ReflectionToStringBuilder(policy, ToStringStyle.JSON_STYLE).setExcludeFieldNames("id", "uuid").toString(); boolean previousDisplay = policy.isDisplay(); policy.setSchedule(schedule); @@ -1301,7 +1337,14 @@ protected void updateSnapshotPolicy(SnapshotPolicyVO policy, String schedule, St } snapshotPolicyDetailsDao.saveDetails(details); } - + if (CollectionUtils.isNotEmpty(poolIds)) { + List details = snapshotPolicyDetailsDao.listDetails(policy.getId()); + details = details.stream().filter(d -> !ApiConstants.STORAGE_ID.equals(d.getName())).collect(Collectors.toList()); + for (Long poolId : poolIds) { + details.add(new SnapshotPolicyDetailVO(policy.getId(), ApiConstants.STORAGE_ID, String.valueOf(poolId))); + } + snapshotPolicyDetailsDao.saveDetails(details); + } _snapSchedMgr.scheduleOrCancelNextSnapshotJobOnDisplayChange(policy, previousDisplay); taggedResourceService.deleteTags(Collections.singletonList(policy.getUuid()), ResourceObjectType.SnapshotPolicy, null); logger.debug(String.format("Updated snapshot policy %s to %s.", previousPolicy, new ReflectionToStringBuilder(policy, ToStringStyle.JSON_STYLE) @@ -1325,8 +1368,10 @@ public void copySnapshotPoliciesBetweenVolumes(VolumeVO srcVolume, VolumeVO dest for (SnapshotPolicyVO policy : policies) { List details = snapshotPolicyDetailsDao.findDetails(policy.getId(), ApiConstants.ZONE_ID); List zoneIds = details.stream().map(d -> Long.valueOf(d.getValue())).collect(Collectors.toList()); + List poolDetails = snapshotPolicyDetailsDao.findDetails(policy.getId(), ApiConstants.STORAGE_ID); + List poolIds = poolDetails.stream().map(d -> Long.valueOf(d.getValue())).collect(Collectors.toList()); persistSnapshotPolicy(destVolume, policy.getSchedule(), policy.getTimezone(), intervalTypes[policy.getInterval()], policy.getMaxSnaps(), - policy.isDisplay(), policy.isActive(), taggedResourceService.getTagsFromResource(ResourceObjectType.SnapshotPolicy, policy.getId()), zoneIds); + policy.isDisplay(), policy.isActive(), taggedResourceService.getTagsFromResource(ResourceObjectType.SnapshotPolicy, policy.getId()), zoneIds, poolIds); } } @@ -1580,12 +1625,19 @@ public SnapshotInfo takeSnapshot(VolumeInfo volume) throws ResourceAllocationExc if (backupSnapToSecondary) { if (!isKvmAndFileBasedStorage) { - backupSnapshotToSecondary(payload.getAsyncBackup(), snapshotStrategy, snapshotOnPrimary, payload.getZoneIds()); + backupSnapshotToSecondary(payload.getAsyncBackup(), snapshotStrategy, snapshotOnPrimary, payload.getZoneIds(), payload.getStoragePoolIds()); } else { postSnapshotDirectlyToSecondary(snapshot, snapshotOnPrimary, snapshotId); } } else { logger.debug("Skipping backup of snapshot [{}] to secondary due to configuration [{}].", snapshotOnPrimary.getUuid(), SnapshotInfo.BackupSnapshotAfterTakingSnapshot.key()); + + if (CollectionUtils.isNotEmpty(payload.getStoragePoolIds()) && payload.getAsyncBackup()) { + snapshotStrategy = _storageStrategyFactory.getSnapshotStrategy(snapshot, SnapshotOperation.COPY); + if (snapshotStrategy != null) { + backupSnapshotExecutor.schedule(new BackupSnapshotTask(snapshotOnPrimary, snapshotBackupRetries - 1, snapshotStrategy, payload.getZoneIds(), payload.getStoragePoolIds()), 0, TimeUnit.SECONDS); + } + } snapshotOnPrimary.markBackedUp(); } @@ -1606,8 +1658,13 @@ public SnapshotInfo takeSnapshot(VolumeInfo volume) throws ResourceAllocationExc // Correct the resource count of snapshot in case of delta snapshots. _resourceLimitMgr.decrementResourceCount(snapshotOwner.getId(), ResourceType.secondary_storage, new Long(volume.getSize() - snapshotStoreRef.getPhysicalSize())); - if (!payload.getAsyncBackup() && backupSnapToSecondary) { - copyNewSnapshotToZones(snapshotId, snapshot.getDataCenterId(), payload.getZoneIds()); + if (!payload.getAsyncBackup()) { + if (backupSnapToSecondary) { + copyNewSnapshotToZones(snapshotId, snapshot.getDataCenterId(), payload.getZoneIds()); + } + if (CollectionUtils.isNotEmpty(payload.getStoragePoolIds())) { + copyNewSnapshotToZonesOnPrimary(payload, snapshot); + } } } catch (Exception e) { logger.debug("post process snapshot failed", e); @@ -1652,10 +1709,59 @@ public boolean isHypervisorKvmAndFileBasedStorage(VolumeInfo volumeInfo, Storage return volumeInfo.getHypervisorType() == HypervisorType.KVM && fileBasedStores.contains(storagePool.getPoolType()); } + private void copyNewSnapshotToZonesOnPrimary(CreateSnapshotPayload payload, SnapshotInfo snapshot) { + SnapshotStrategy snapshotStrategy; + snapshotStrategy = _storageStrategyFactory.getSnapshotStrategy(snapshot, SnapshotOperation.COPY); + if (snapshotStrategy != null) { + for (Long storagePoolId : payload.getStoragePoolIds()) { + copySnapshotOnPool(snapshot, snapshotStrategy, storagePoolId); + } + } else { + logger.info("Unable to find snapshot strategy to handle the copy of a snapshot with id " + snapshot.getUuid()); + } + } + + private boolean copySnapshotOnPool(SnapshotInfo snapshot, SnapshotStrategy snapshotStrategy, Long storagePoolId) { + DataStore store = dataStoreMgr.getDataStore(storagePoolId, DataStoreRole.Primary); + SnapshotInfo snapshotOnStore = (SnapshotInfo) store.create(snapshot); +// if (snapshotOnStore.getStatus() == ObjectInDataStoreStateMachine.State.Allocated) { +// snapshotOnStore.processEvent(Event.CreateOnlyRequested); +// } else if (snapshotOnStore.getStatus() == ObjectInDataStoreStateMachine.State.Ready) { +// snapshotOnStore.processEvent(Event.CopyRequested); +// } else { +// logger.info(String.format("Cannot copy snapshot to another storage in different zone. It's not in the right state %s", snapshotOnStore.getStatus())); +// return false; +// } + + try { + AsyncCallFuture future = snapshotSrv.copySnapshot(snapshot, snapshotOnStore, snapshotStrategy); + SnapshotResult result = future.get(); + if (result.isFailed()) { + logger.debug(String.format("Copy snapshot ID: %d failed for primary storage %s: %s", snapshot.getSnapshotId(), storagePoolId, result.getResult())); + return false; + } + snapshotZoneDao.addSnapshotToZone(snapshot.getId(), snapshotOnStore.getDataCenterId()); + _resourceLimitMgr.incrementResourceCount(CallContext.current().getCallingUserId(), ResourceType.primary_storage, snapshot.getSize()); + if (CallContext.current().getCallingUserId() != Account.ACCOUNT_ID_SYSTEM) { + SnapshotVO snapshotVO = _snapshotDao.findByIdIncludingRemoved(snapshot.getSnapshotId()); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_SNAPSHOT_COPY, CallContext.current().getCallingUserId(), snapshotOnStore.getDataCenterId(), snapshotVO.getId(), null, null, null, snapshotVO.getSize(), + snapshotVO.getSize(), snapshotVO.getClass().getName(), snapshotVO.getUuid()); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } +// boolean copySuccessful = snapshotStrategy.copySnapshot(snapshot, snapshotOnStore, null); +// if (!copySuccessful) { +// snapshotOnStore.processEvent(Event.OperationFailed); +// } + return true; + } - protected void backupSnapshotToSecondary(boolean asyncBackup, SnapshotStrategy snapshotStrategy, SnapshotInfo snapshotOnPrimary, List zoneIds) { + protected void backupSnapshotToSecondary(boolean asyncBackup, SnapshotStrategy snapshotStrategy, SnapshotInfo snapshotOnPrimary, List zoneIds, List poolIds) { if (asyncBackup) { - backupSnapshotExecutor.schedule(new BackupSnapshotTask(snapshotOnPrimary, snapshotBackupRetries - 1, snapshotStrategy, zoneIds), 0, TimeUnit.SECONDS); + backupSnapshotExecutor.schedule(new BackupSnapshotTask(snapshotOnPrimary, snapshotBackupRetries - 1, snapshotStrategy, zoneIds, poolIds), 0, TimeUnit.SECONDS); } else { SnapshotInfo backupedSnapshot = snapshotStrategy.backupSnapshot(snapshotOnPrimary); if (backupedSnapshot != null) { @@ -1670,33 +1776,46 @@ protected class BackupSnapshotTask extends ManagedContextRunnable { SnapshotStrategy snapshotStrategy; List zoneIds; + List poolIds; - public BackupSnapshotTask(SnapshotInfo snap, int maxRetries, SnapshotStrategy strategy, List zoneIds) { + public BackupSnapshotTask(SnapshotInfo snap, int maxRetries, SnapshotStrategy strategy, List zoneIds, List poolIds) { snapshot = snap; attempts = maxRetries; snapshotStrategy = strategy; this.zoneIds = zoneIds; + this.poolIds = poolIds; } @Override protected void runInContext() { try { logger.debug("Value of attempts is " + (snapshotBackupRetries - attempts)); + if (Boolean.TRUE.equals(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value()) && CollectionUtils.isEmpty(zoneIds)) { + SnapshotInfo backupedSnapshot = snapshotStrategy.backupSnapshot(snapshot); - SnapshotInfo backupedSnapshot = snapshotStrategy.backupSnapshot(snapshot); + if (backupedSnapshot != null) { + snapshotStrategy.postSnapshotCreation(snapshot); + copyNewSnapshotToZones(snapshot.getId(), snapshot.getDataCenterId(), zoneIds); + } + } - if (backupedSnapshot != null) { - snapshotStrategy.postSnapshotCreation(snapshot); - copyNewSnapshotToZones(snapshot.getId(), snapshot.getDataCenterId(), zoneIds); + if (CollectionUtils.isNotEmpty(poolIds)) { + for (Long poolId: poolIds) { + copySnapshotOnPool(snapshot, snapshotStrategy, poolId); + } } } catch (final Exception e) { - if (attempts >= 0) { - logger.debug("Backing up of snapshot failed, for snapshot {}, left with {} more attempts", snapshot, attempts); - backupSnapshotExecutor.schedule(new BackupSnapshotTask(snapshot, --attempts, snapshotStrategy, zoneIds), snapshotBackupRetryInterval, TimeUnit.SECONDS); - } else { - logger.debug("Done with {} attempts in backing up of snapshot {}", snapshotBackupRetries, snapshot.getSnapshotVO()); - snapshotSrv.cleanupOnSnapshotBackupFailure(snapshot); - } + decriseBackupSnapshotAttempts(); + } + } + + private void decriseBackupSnapshotAttempts() { + if (attempts >= 0) { + logger.debug("Backing up of snapshot failed, for snapshot {}, left with {} more attempts", snapshot, attempts); + backupSnapshotExecutor.schedule(new BackupSnapshotTask(snapshot, --attempts, snapshotStrategy, zoneIds, poolIds), snapshotBackupRetryInterval, TimeUnit.SECONDS); + } else { + logger.debug("Done with {} attempts in backing up of snapshot {}", snapshotBackupRetries, snapshot.getSnapshotVO()); + snapshotSrv.cleanupOnSnapshotBackupFailure(snapshot); } } } @@ -2080,26 +2199,23 @@ private List copySnapshotToZones(SnapshotVO snapshotVO, DataStore srcSec return failedZones; } - protected Pair getCheckedSnapshotForCopy(final long snapshotId, final List destZoneIds, Long sourceZoneId) { - SnapshotVO snapshot = _snapshotDao.findById(snapshotId); - if (snapshot == null) { - throw new InvalidParameterValueException("Unable to find snapshot with id"); - } + protected Pair getCheckedSnapshotForCopy(final SnapshotVO snapshot, final List destZoneIds, Long sourceZoneId, boolean canCopyBeteenStoragePools) { // Verify snapshot is BackedUp and is on secondary store - if (!Snapshot.State.BackedUp.equals(snapshot.getState())) { + if (!Snapshot.State.BackedUp.equals(snapshot.getState()) && !canCopyBeteenStoragePools) { throw new InvalidParameterValueException("Snapshot is not backed up"); } - if (snapshot.getLocationType() != null && !Snapshot.LocationType.SECONDARY.equals(snapshot.getLocationType())) { + if (snapshot.getLocationType() != null && !Snapshot.LocationType.SECONDARY.equals(snapshot.getLocationType()) && !canCopyBeteenStoragePools) { throw new InvalidParameterValueException("Snapshot is not backed up"); } - if (CollectionUtils.isEmpty(destZoneIds)) { - throw new InvalidParameterValueException("Please specify valid destination zone(s)."); - } Volume volume = _volsDao.findById(snapshot.getVolumeId()); if (sourceZoneId == null) { sourceZoneId = volume.getDataCenterId(); } - if (destZoneIds.contains(sourceZoneId)) { + if (CollectionUtils.isEmpty(destZoneIds)) { + if (!canCopyBeteenStoragePools) { + throw new InvalidParameterValueException("Please specify valid destination zone(s)."); + } + } else if (destZoneIds.contains(sourceZoneId)) { throw new InvalidParameterValueException("Please specify different source and destination zones."); } DataCenterVO sourceZone = dataCenterDao.findById(sourceZoneId); @@ -2124,14 +2240,36 @@ protected DataCenterVO getCheckedDestinationZoneForSnapshotCopy(long zoneId, boo return dstZone; } + protected StoragePoolVO getCheckedDestinationStorageForSnapshotCopy(long poolId, boolean isRootAdmin) { + StoragePoolVO destPool = _storagePoolDao.findById(poolId); + if (destPool == null) { + throw new InvalidParameterValueException("Please specify a valid destination zone."); + } + if (!StoragePoolStatus.Up.equals(destPool.getStatus()) && !isRootAdmin) { + throw new PermissionDeniedException("Cannot perform this operation, the storage pool is not in Up state: " + destPool.getName()); + } + DataCenterVO destZone = dataCenterDao.findById(destPool.getDataCenterId()); + if (DataCenter.Type.Edge.equals(destZone.getType())) { + logger.error(String.format("Edge zone %s specified for snapshot copy", destZone)); + throw new InvalidParameterValueException(String.format("Snapshot copy is not supported by zone %s", destZone.getName())); + } + return destPool; + } + @Override @ActionEvent(eventType = EventTypes.EVENT_SNAPSHOT_COPY, eventDescription = "copying snapshot", create = false) public Snapshot copySnapshot(CopySnapshotCmd cmd) throws StorageUnavailableException, ResourceAllocationException { final Long snapshotId = cmd.getId(); Long sourceZoneId = cmd.getSourceZoneId(); List destZoneIds = cmd.getDestinationZoneIds(); + List storagePoolIds = cmd.getStoragePoolIds(); Account caller = CallContext.current().getCallingAccount(); - Pair snapshotZonePair = getCheckedSnapshotForCopy(snapshotId, destZoneIds, sourceZoneId); + SnapshotVO snapshotVO = _snapshotDao.findById(snapshotId); + if (snapshotVO == null) { + throw new InvalidParameterValueException("Unable to find snapshot with id"); + } + boolean canCopyBetweenStoragePools = CollectionUtils.isNotEmpty(storagePoolIds) && canCopyOnPrimary(storagePoolIds, snapshotVO); + Pair snapshotZonePair = getCheckedSnapshotForCopy(snapshotVO, destZoneIds, sourceZoneId, canCopyBetweenStoragePools); SnapshotVO snapshot = snapshotZonePair.first(); sourceZoneId = snapshotZonePair.second(); Map dataCenterVOs = new HashMap<>(); @@ -2142,11 +2280,14 @@ public Snapshot copySnapshot(CopySnapshotCmd cmd) throws StorageUnavailableExcep } _accountMgr.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, true, snapshot); DataStore srcSecStore = getSnapshotZoneImageStore(snapshotId, sourceZoneId); - if (srcSecStore == null) { + if (srcSecStore == null && !canCopyBetweenStoragePools) { throw new InvalidParameterValueException(String.format("There is no snapshot ID: %s ready on image store", snapshot.getUuid())); } + if (canCopyBetweenStoragePools) { + copySnapshotToPrimaryDifferentZone(storagePoolIds, snapshot); + } List failedZones = copySnapshotToZones(snapshot, srcSecStore, new ArrayList<>(dataCenterVOs.values())); - if (destZoneIds.size() > failedZones.size()){ + if (destZoneIds.size() > failedZones.size() || canCopyBetweenStoragePools){ if (!failedZones.isEmpty()) { logger.error(String.format("There were failures when copying snapshot to zones: %s", StringUtils.joinWith(", ", failedZones.toArray()))); @@ -2157,6 +2298,61 @@ public Snapshot copySnapshot(CopySnapshotCmd cmd) throws StorageUnavailableExcep } } + private boolean canCopyOnPrimary(List poolIds, Snapshot snapshot) { + List poolsToBeRemoved = new ArrayList<>(); + for (Long poolId : poolIds) { + PrimaryDataStore dataStore = (PrimaryDataStore) dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); + if (dataStore == null) { + poolsToBeRemoved.add(poolId); + continue; + } + SnapshotInfo snapshotInfo = snapshotFactory.getSnapshot(snapshot.getId(), poolId, DataStoreRole.Primary); + if (snapshotInfo != null) { + logger.debug(String.format("Snapshot [%s] already exist on pool [%s]", snapshot.getUuid(), dataStore.getName())); + continue; + } + + VolumeVO volume = _volsDao.findById(snapshot.getVolumeId()); + if (volume == null) { + poolsToBeRemoved.add(poolId); + continue; + } + if (!dataStore.getDriver().getCapabilities() + .containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES.toString()) + && dataStore.getPoolType() != volume.getPoolType()) { + poolsToBeRemoved.add(poolId); + logger.debug(String.format("The %s does not support copy to %s between zones", dataStore.getPoolType(), volume.getPoolType())); + } + } + poolIds.removeAll(poolsToBeRemoved); + if (CollectionUtils.isEmpty(poolIds)) { + return false; + } + return true; + } + + private void copySnapshotToPrimaryDifferentZone(List poolIds, SnapshotVO snapshot) { + VolumeInfo volume = volFactory.getVolume(snapshot.getVolumeId()); + if (volume == null) { + throw new CloudRuntimeException("Failed to find volume with id: " + snapshot.getVolumeId()); + } + CreateSnapshotPayload payload = setPayload(poolIds, volume, snapshot); + SnapshotInfo snapshotInfo = snapshotFactory.getSnapshotOnPrimaryStore(snapshot.getId()); + copyNewSnapshotToZonesOnPrimary(payload, snapshotInfo); + } + + private CreateSnapshotPayload setPayload(List poolIds, VolumeInfo vol, SnapshotVO snapshotCreate) { + CreateSnapshotPayload payload = new CreateSnapshotPayload(); + payload.setSnapshotId(snapshotCreate.getId()); + payload.setSnapshotPolicyId(SnapshotVO.MANUAL_POLICY_ID); + payload.setLocationType(snapshotCreate.getLocationType()); + payload.setAccount(_accountMgr.getAccount(vol.getAccountId())); + payload.setAsyncBackup(false); + payload.setQuiescevm(false); + payload.setStoragePoolIds(poolIds); + return payload; + } + protected void copyNewSnapshotToZones(long snapshotId, long zoneId, List destZoneIds) { if (CollectionUtils.isEmpty(destZoneIds)) { return; diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 81b75c23eba1..59f33c5e614c 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -63,6 +63,7 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; @@ -322,6 +323,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, @Inject private HeuristicRuleHelper heuristicRuleHelper; + protected boolean backupSnapshotAfterTakingSnapshot = SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value(); + private TemplateAdapter getAdapter(HypervisorType type) { TemplateAdapter adapter = null; if (type == HypervisorType.BareMetal) { @@ -1693,13 +1696,20 @@ public VirtualMachineTemplate createPrivateTemplate(CreateTemplateCmd command) t AsyncCallFuture future = null; if (snapshotId != null) { - DataStoreRole dataStoreRole = snapshotHelper.getDataStoreRole(snapshot); - kvmSnapshotOnlyInPrimaryStorage = snapshotHelper.isKvmSnapshotOnlyInPrimaryStorage(snapshot, dataStoreRole); - snapInfo = _snapshotFactory.getSnapshotWithRoleAndZone(snapshotId, dataStoreRole, zoneId); + DataStoreRole dataStoreRole = snapshotHelper.getDataStoreRole(snapshot, zoneId); + kvmSnapshotOnlyInPrimaryStorage = snapshotHelper.isKvmSnapshotOnlyInPrimaryStorage(snapshot, dataStoreRole, zoneId); + snapInfo = _snapshotFactory.getSnapshotWithRoleAndZone(snapshotId, dataStoreRole, zoneId); boolean kvmIncrementalSnapshot = SnapshotManager.kvmIncrementalSnapshot.valueIn(_hostDao.findClusterIdByVolumeInfo(snapInfo.getBaseVolume())); - if (dataStoreRole == DataStoreRole.Image || kvmSnapshotOnlyInPrimaryStorage) { + boolean skipCopyToSecondary = false; + + Map capabilities = snapInfo.getDataStore().getDriver().getCapabilities(); + boolean keepOnPrimary = MapUtils.isNotEmpty(capabilities) && capabilities.containsKey(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString()); + if (kvmSnapshotOnlyInPrimaryStorage && keepOnPrimary) { + skipCopyToSecondary = true; + } + if (dataStoreRole == DataStoreRole.Image || !skipCopyToSecondary) { snapInfo = snapshotHelper.backupSnapshotToSecondaryStorageIfNotExists(snapInfo, dataStoreRole, snapshot, kvmSnapshotOnlyInPrimaryStorage); _accountMgr.checkAccess(caller, null, true, snapInfo); DataStore snapStore = snapInfo.getDataStore(); @@ -1707,6 +1717,16 @@ public VirtualMachineTemplate createPrivateTemplate(CreateTemplateCmd command) t if (snapStore != null) { store = snapStore; // pick snapshot image store to create template } + } else if (skipCopyToSecondary) { + ImageStoreVO imageStore = _imgStoreDao.findOneByZoneAndProtocol(zoneId, "nfs"); + if (imageStore == null) { + throw new CloudRuntimeException(String.format("Could not find an NFS secondary storage pool on zone %s to use as a temporary location " + + "for instance conversion", zoneId)); + } + DataStore dataStore = _dataStoreMgr.getDataStore(imageStore.getId(), DataStoreRole.Image); + if (dataStore != null) { + store = dataStore; + } } if (kvmIncrementalSnapshot && DataStoreRole.Image.equals(dataStoreRole)) { snapInfo = snapshotHelper.convertSnapshotIfNeeded(snapInfo); diff --git a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java index d67180d3eb2b..910d7d2a514c 100644 --- a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java +++ b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java @@ -19,15 +19,17 @@ package org.apache.cloudstack.snapshot; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import javax.inject.Inject; - +import com.cloud.api.query.dao.SnapshotJoinDao; +import com.cloud.api.query.vo.SnapshotJoinVO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Snapshot; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; @@ -45,18 +47,16 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; -import com.cloud.hypervisor.Hypervisor; -import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.storage.DataStoreRole; -import com.cloud.storage.Snapshot; -import com.cloud.storage.SnapshotVO; -import com.cloud.storage.Storage.StoragePoolType; -import com.cloud.storage.VolumeVO; -import com.cloud.storage.dao.SnapshotDao; -import com.cloud.utils.exception.CloudRuntimeException; +import javax.inject.Inject; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; public class SnapshotHelper { protected Logger logger = LogManager.getLogger(getClass()); @@ -82,6 +82,9 @@ public class SnapshotHelper { @Inject protected PrimaryDataStoreDao primaryDataStoreDao; + @Inject + protected SnapshotJoinDao snapshotJoinDao; + protected boolean backupSnapshotAfterTakingSnapshot = SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value(); protected final Set storagePoolTypesToValidateWithBackupSnapshotAfterTakingSnapshot = new HashSet<>(Arrays.asList(StoragePoolType.RBD, @@ -92,6 +95,23 @@ public class SnapshotHelper { * @param snapInfo the snapshot info to delete. */ public void expungeTemporarySnapshot(boolean kvmSnapshotOnlyInPrimaryStorage, SnapshotInfo snapInfo) { + long storeId = snapInfo.getDataStore().getId(); + long zoneId = dataStorageManager.getStoreZoneId(storeId, snapInfo.getDataStore().getRole()); + + Map capabilities = snapInfo.getDataStore().getDriver().getCapabilities(); + boolean keepOnPrimary = MapUtils.isNotEmpty(capabilities) && capabilities.containsKey(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString()); + if (keepOnPrimary) { + logger.debug("The primary storage does not delete the snapshots even if there is a backup on secondary"); + return; + } + List snapshots = snapshotJoinDao.listBySnapshotIdAndZoneId(zoneId, snapInfo.getSnapshotId()); + if (kvmSnapshotOnlyInPrimaryStorage || snapshots.size() <= 1) { + if (snapInfo != null) { + logger.trace(String.format("Snapshot [{}] is not a temporary backup to create a volume from snapshot. Not expunging it.", snapInfo.getId())); + } + return; + } + if (snapInfo == null) { logger.warn("Unable to expunge snapshot due to its info is null."); return; @@ -118,9 +138,7 @@ public void expungeTemporarySnapshot(boolean kvmSnapshotOnlyInPrimaryStorage, Sn } } - long storeId = snapInfo.getDataStore().getId(); if (!DataStoreRole.Image.equals(snapInfo.getDataStore().getRole())) { - long zoneId = dataStorageManager.getStoreZoneId(storeId, snapInfo.getDataStore().getRole()); SnapshotInfo imageStoreSnapInfo = snapshotFactory.getSnapshotWithRoleAndZone(snapInfo.getId(), DataStoreRole.Image, zoneId); storeId = imageStoreSnapInfo.getDataStore().getId(); } @@ -181,8 +199,11 @@ protected boolean isSnapshotBackupable(SnapshotInfo snapInfo, DataStoreRole data * @return true if hypervisor is {@link HypervisorType#KVM} and data store role is {@link DataStoreRole#Primary} and global setting "snapshot.backup.to.secondary" is false, * else false. */ - public boolean isKvmSnapshotOnlyInPrimaryStorage(Snapshot snapshot, DataStoreRole dataStoreRole){ - return snapshot.getHypervisorType() == Hypervisor.HypervisorType.KVM && dataStoreRole == DataStoreRole.Primary && !backupSnapshotAfterTakingSnapshot; + public boolean isKvmSnapshotOnlyInPrimaryStorage(Snapshot snapshot, DataStoreRole dataStoreRole, Long zoneId){ + List snapshots = snapshotJoinDao.listBySnapshotIdAndZoneId(zoneId, snapshot.getSnapshotId()); + boolean isKvmSnapshotOnlyInPrimaryStorage = snapshots.stream().filter(s -> s.getStoreRole().equals(DataStoreRole.Image)).count() == 0; + + return snapshot.getHypervisorType() == Hypervisor.HypervisorType.KVM && dataStoreRole == DataStoreRole.Primary && isKvmSnapshotOnlyInPrimaryStorage; } public DataStoreRole getDataStoreRole(Snapshot snapshot) { @@ -215,10 +236,21 @@ public DataStoreRole getDataStoreRole(Snapshot snapshot) { return DataStoreRole.Image; } - /** - * Verifies if it is a KVM volume that has snapshots only in primary storage. - * @throws CloudRuntimeException If it is a KVM volume and has at least one snapshot only in primary storage. - */ + public DataStoreRole getDataStoreRole(Snapshot snapshot, Long zoneId) { + if (zoneId == null) { + getDataStoreRole(snapshot); + } + List snapshots = snapshotJoinDao.listBySnapshotIdAndZoneId(zoneId, snapshot.getId()); + boolean snapshotOnPrimary = snapshots.stream().anyMatch(s -> s.getStoreRole().equals(DataStoreRole.Primary)); + if (snapshotOnPrimary) { + return DataStoreRole.Primary; + } + return DataStoreRole.Image; + } + /** + * Verifies if it is a KVM volume that has snapshots only in primary storage. + * @throws CloudRuntimeException If it is a KVM volume and has at least one snapshot only in primary storage. + */ public void checkKvmVolumeSnapshotsOnlyInPrimaryStorage(VolumeVO volumeVo, HypervisorType hypervisorType) throws CloudRuntimeException { if (HypervisorType.KVM != hypervisorType) { logger.trace(String.format("The %s hypervisor [%s] is not KVM, therefore we will not check if the snapshots are only in primary storage.", volumeVo, hypervisorType)); diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 7f1030992f94..2814a9c52930 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -579,7 +579,7 @@ public void testTakeSnapshotF1() throws ResourceAllocationException { when(volumeDataFactoryMock.getVolume(anyLong())).thenReturn(volumeInfoMock); when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); lenient().when(volumeInfoMock.getPoolId()).thenReturn(1L); - volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null, null); + volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null, null, null); } @Test @@ -592,7 +592,7 @@ public void testTakeSnapshotF2() throws ResourceAllocationException { final TaggedResourceService taggedResourceService = Mockito.mock(TaggedResourceService.class); Mockito.lenient().when(taggedResourceService.createTags(any(), any(), any(), any())).thenReturn(null); ReflectionTestUtils.setField(volumeApiServiceImpl, "taggedResourceService", taggedResourceService); - volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null, null); + volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null, null, null); } @Test @@ -640,7 +640,7 @@ public void testUpdateMissingRootDiskControllerWithValidChainInfo() { @Test public void testAllocSnapshotNonManagedStorageArchive() { try { - volumeApiServiceImpl.allocSnapshot(6L, 1L, "test", Snapshot.LocationType.SECONDARY, null); + volumeApiServiceImpl.allocSnapshot(6L, 1L, "test", Snapshot.LocationType.SECONDARY, null, null); } catch (InvalidParameterValueException e) { Assert.assertEquals(e.getMessage(), "VolumeId: 6 LocationType is supported only for managed storage"); return; diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java index e6c2a0d0f3cf..a74c41b4257f 100644 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java @@ -16,30 +16,6 @@ // under the License. package com.cloud.storage.snapshot; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotResult; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; -import org.apache.cloudstack.framework.async.AsyncCallFuture; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; - import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.DataCenterDao; @@ -62,6 +38,29 @@ import com.cloud.user.ResourceLimitService; import com.cloud.user.dao.AccountDao; import com.cloud.utils.Pair; +import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotResult; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; +import org.apache.cloudstack.framework.async.AsyncCallFuture; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; @RunWith(MockitoJUnitRunner.class) public class SnapshotManagerImplTest { @@ -176,7 +175,7 @@ public void testGetStoreRefsAndZonesForSnapshotDeleteSingle() { } @Test public void testValidatePolicyZonesNoZones() { - snapshotManager.validatePolicyZones(null, Mockito.mock(VolumeVO.class), Mockito.mock(Account.class)); + snapshotManager.validatePolicyZones(null, null, Mockito.mock(VolumeVO.class), Mockito.mock(Account.class)); } @Test(expected = InvalidParameterValueException.class) @@ -186,7 +185,7 @@ public void testValidatePolicyZonesVolumeEdgeZone() { DataCenterVO zone = Mockito.mock(DataCenterVO.class); Mockito.when(zone.getType()).thenReturn(DataCenter.Type.Edge); Mockito.when(dataCenterDao.findById(1L)).thenReturn(zone); - snapshotManager.validatePolicyZones(List.of(1L), volumeVO, Mockito.mock(Account.class)); + snapshotManager.validatePolicyZones(List.of(1L), null, volumeVO, Mockito.mock(Account.class)); } @Test(expected = InvalidParameterValueException.class) @@ -197,7 +196,7 @@ public void testValidatePolicyZonesNullZone() { Mockito.when(zone.getType()).thenReturn(DataCenter.Type.Core); Mockito.when(dataCenterDao.findById(1L)).thenReturn(zone); Mockito.when(dataCenterDao.findById(2L)).thenReturn(null); - snapshotManager.validatePolicyZones(List.of(2L), volumeVO, Mockito.mock(Account.class)); + snapshotManager.validatePolicyZones(List.of(2L), null, volumeVO, Mockito.mock(Account.class)); } @Test(expected = PermissionDeniedException.class) @@ -211,7 +210,7 @@ public void testValidatePolicyZonesDisabledZone() { Mockito.when(zone1.getAllocationState()).thenReturn(Grouping.AllocationState.Disabled); Mockito.when(dataCenterDao.findById(2L)).thenReturn(zone1); Mockito.when(accountManager.isRootAdmin(Mockito.any())).thenReturn(false); - snapshotManager.validatePolicyZones(List.of(2L), volumeVO, Mockito.mock(Account.class)); + snapshotManager.validatePolicyZones(List.of(2L), null, volumeVO, Mockito.mock(Account.class)); } @Test(expected = InvalidParameterValueException.class) @@ -225,7 +224,7 @@ public void testValidatePolicyZonesEdgeZone() { Mockito.when(zone1.getType()).thenReturn(DataCenter.Type.Edge); Mockito.when(zone1.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); Mockito.when(dataCenterDao.findById(2L)).thenReturn(zone1); - snapshotManager.validatePolicyZones(List.of(2L), volumeVO, Mockito.mock(Account.class)); + snapshotManager.validatePolicyZones(List.of(2L), null, volumeVO, Mockito.mock(Account.class)); } @Test @@ -239,7 +238,7 @@ public void testValidatePolicyZonesValidZone() { Mockito.when(zone1.getType()).thenReturn(DataCenter.Type.Core); Mockito.when(zone1.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); Mockito.when(dataCenterDao.findById(2L)).thenReturn(zone1); - snapshotManager.validatePolicyZones(List.of(2L), volumeVO, Mockito.mock(Account.class)); + snapshotManager.validatePolicyZones(List.of(2L), null, volumeVO, Mockito.mock(Account.class)); } @Test @@ -308,15 +307,14 @@ public void testCopyNewSnapshotToZones() { @Test(expected = InvalidParameterValueException.class) public void testGetCheckedSnapshotForCopyNoSnapshot() { - snapshotManager.getCheckedSnapshotForCopy(1L, List.of(100L), null); + SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); + snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L), null, false); } @Test(expected = InvalidParameterValueException.class) public void testGetCheckedSnapshotForCopyNoSnapshotBackup() { - final long snapshotId = 1L; SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); - Mockito.when(snapshotDao.findById(snapshotId)).thenReturn(snapshotVO); - snapshotManager.getCheckedSnapshotForCopy(snapshotId, List.of(100L), null); + snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L), null, false); } @Test(expected = InvalidParameterValueException.class) @@ -325,73 +323,62 @@ public void testGetCheckedSnapshotForCopyNotOnSecondary() { SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); Mockito.when(snapshotVO.getState()).thenReturn(Snapshot.State.BackedUp); Mockito.when(snapshotVO.getLocationType()).thenReturn(Snapshot.LocationType.PRIMARY); - Mockito.when(snapshotDao.findById(snapshotId)).thenReturn(snapshotVO); - snapshotManager.getCheckedSnapshotForCopy(snapshotId, List.of(100L), null); + snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L), null, false); } @Test(expected = InvalidParameterValueException.class) public void testGetCheckedSnapshotForCopyDestNotSpecified() { - final long snapshotId = 1L; SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); Mockito.when(snapshotVO.getState()).thenReturn(Snapshot.State.BackedUp); - Mockito.when(snapshotDao.findById(snapshotId)).thenReturn(snapshotVO); - snapshotManager.getCheckedSnapshotForCopy(snapshotId, new ArrayList<>(), null); + snapshotManager.getCheckedSnapshotForCopy(snapshotVO, new ArrayList<>(), 1L, false); } @Test(expected = InvalidParameterValueException.class) public void testGetCheckedSnapshotForCopyDestContainsSource() { - final long snapshotId = 1L; SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); Mockito.when(snapshotVO.getState()).thenReturn(Snapshot.State.BackedUp); Mockito.when(snapshotVO.getVolumeId()).thenReturn(1L); - Mockito.when(snapshotDao.findById(snapshotId)).thenReturn(snapshotVO); Mockito.when(volumeDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(VolumeVO.class)); - snapshotManager.getCheckedSnapshotForCopy(snapshotId, List.of(100L, 1L), 1L); + snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L, 1L), 1L, false); } @Test(expected = InvalidParameterValueException.class) public void testGetCheckedSnapshotForCopyNullSourceZone() { - final long snapshotId = 1L; SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); Mockito.when(snapshotVO.getState()).thenReturn(Snapshot.State.BackedUp); Mockito.when(snapshotVO.getVolumeId()).thenReturn(1L); - Mockito.when(snapshotDao.findById(snapshotId)).thenReturn(snapshotVO); VolumeVO volumeVO = Mockito.mock(VolumeVO.class); Mockito.when(volumeVO.getDataCenterId()).thenReturn(1L); Mockito.when(volumeDao.findById(Mockito.anyLong())).thenReturn(volumeVO); - snapshotManager.getCheckedSnapshotForCopy(snapshotId, List.of(100L, 101L), null); + snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L, 101L), null, false); } @Test public void testGetCheckedSnapshotForCopyValid() { - final long snapshotId = 1L; final Long zoneId = 1L; SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); Mockito.when(snapshotVO.getState()).thenReturn(Snapshot.State.BackedUp); Mockito.when(snapshotVO.getVolumeId()).thenReturn(1L); - Mockito.when(snapshotDao.findById(snapshotId)).thenReturn(snapshotVO); VolumeVO volumeVO = Mockito.mock(VolumeVO.class); Mockito.when(volumeVO.getDataCenterId()).thenReturn(zoneId); Mockito.when(volumeDao.findById(Mockito.anyLong())).thenReturn(volumeVO); Mockito.when(dataCenterDao.findById(zoneId)).thenReturn(Mockito.mock(DataCenterVO.class)); - Pair result = snapshotManager.getCheckedSnapshotForCopy(snapshotId, List.of(100L, 101L), null); + Pair result = snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L, 101L), null, false); Assert.assertNotNull(result.first()); Assert.assertEquals(zoneId, result.second()); } @Test public void testGetCheckedSnapshotForCopyNullDest() { - final long snapshotId = 1L; final Long zoneId = 1L; SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class); Mockito.when(snapshotVO.getState()).thenReturn(Snapshot.State.BackedUp); Mockito.when(snapshotVO.getVolumeId()).thenReturn(1L); - Mockito.when(snapshotDao.findById(snapshotId)).thenReturn(snapshotVO); VolumeVO volumeVO = Mockito.mock(VolumeVO.class); Mockito.when(volumeVO.getDataCenterId()).thenReturn(zoneId); Mockito.when(volumeDao.findById(Mockito.anyLong())).thenReturn(volumeVO); Mockito.when(dataCenterDao.findById(zoneId)).thenReturn(Mockito.mock(DataCenterVO.class)); - Pair result = snapshotManager.getCheckedSnapshotForCopy(snapshotId, List.of(100L, 101L), null); + Pair result = snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L, 101L), null, false); Assert.assertNotNull(result.first()); Assert.assertEquals(zoneId, result.second()); } diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java index 4ccc6e999618..952e77e10dc3 100755 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java @@ -16,65 +16,13 @@ // under the License. package com.cloud.storage.snapshot; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - import com.cloud.api.ApiDBUtils; -import com.cloud.exception.PermissionDeniedException; -import com.cloud.storage.Storage; -import org.apache.cloudstack.api.command.user.snapshot.ExtractSnapshotCmd; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation; -import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.snapshot.SnapshotHelper; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.BDDMockito; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; - import com.cloud.configuration.Resource.ResourceType; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.DataCenterDao; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceAllocationException; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; @@ -86,6 +34,7 @@ import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotPolicyVO; import com.cloud.storage.SnapshotVO; +import com.cloud.storage.Storage; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.SnapshotDao; @@ -107,6 +56,56 @@ import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.api.command.user.snapshot.ExtractSnapshotCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation; +import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.snapshot.SnapshotHelper; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.BDDMockito; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class SnapshotManagerTest { @@ -428,7 +427,7 @@ public void validateCreateSnapshotPolicy(){ Mockito.doReturn(null).when(snapshotSchedulerMock).scheduleNextSnapshotJob(any()); SnapshotPolicyVO result = _snapshotMgr.createSnapshotPolicy(TEST_VOLUME_ID, TEST_SNAPSHOT_POLICY_SCHEDULE, TEST_SNAPSHOT_POLICY_TIMEZONE, TEST_SNAPSHOT_POLICY_INTERVAL, - TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY, null); + TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY, null, null); assertSnapshotPolicyResultAgainstPreBuiltInstance(result, null); } @@ -443,7 +442,7 @@ public void validateUpdateSnapshotPolicy(){ TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY); _snapshotMgr.updateSnapshotPolicy(snapshotPolicyVo, TEST_SNAPSHOT_POLICY_SCHEDULE, TEST_SNAPSHOT_POLICY_TIMEZONE, - TEST_SNAPSHOT_POLICY_INTERVAL, TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY, TEST_SNAPSHOT_POLICY_ACTIVE, null); + TEST_SNAPSHOT_POLICY_INTERVAL, TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY, TEST_SNAPSHOT_POLICY_ACTIVE, null, null); assertSnapshotPolicyResultAgainstPreBuiltInstance(snapshotPolicyVo, null); } @@ -478,7 +477,7 @@ public void validatePersistSnapshotPolicyLockIsNotAquiredMustThrowException() { Mockito.doReturn(false).when(globalLockMock).lock(Mockito.anyInt()); _snapshotMgr.persistSnapshotPolicy(volumeMock, TEST_SNAPSHOT_POLICY_SCHEDULE, TEST_SNAPSHOT_POLICY_TIMEZONE, TEST_SNAPSHOT_POLICY_INTERVAL, TEST_SNAPSHOT_POLICY_MAX_SNAPS, - TEST_SNAPSHOT_POLICY_DISPLAY, TEST_SNAPSHOT_POLICY_ACTIVE, mapStringStringMock, null); + TEST_SNAPSHOT_POLICY_DISPLAY, TEST_SNAPSHOT_POLICY_ACTIVE, mapStringStringMock, null, null); } } @@ -503,7 +502,7 @@ private void testPersistSnapshotPolicyLockAcquired(boolean forUpdate) { for (IntervalType intervalType : listIntervalTypes) { Mockito.doReturn(forUpdate ? snapshotPolicyVoInstance : null).when(snapshotPolicyDaoMock).findOneByVolumeInterval(Mockito.anyLong(), Mockito.eq(intervalType)); SnapshotPolicyVO result = _snapshotMgr.persistSnapshotPolicy(volumeMock, TEST_SNAPSHOT_POLICY_SCHEDULE, TEST_SNAPSHOT_POLICY_TIMEZONE, intervalType, - TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY, TEST_SNAPSHOT_POLICY_ACTIVE, null, null); + TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY, TEST_SNAPSHOT_POLICY_ACTIVE, null, null, null); assertSnapshotPolicyResultAgainstPreBuiltInstance(result, (short)intervalType.ordinal()); } diff --git a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java index bb510e2aaa1b..819694a226b3 100755 --- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java +++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java @@ -20,6 +20,7 @@ import com.cloud.agent.AgentManager; +import com.cloud.api.query.dao.SnapshotJoinDao; import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.configuration.Resource; import com.cloud.dc.dao.DataCenterDao; @@ -204,6 +205,8 @@ public class TemplateManagerImplTest { AccountManager _accountMgr; @Inject VnfTemplateManager vnfTemplateManager; + @Inject + SnapshotJoinDao snapshotJoinDao; @Inject HeuristicRuleHelper heuristicRuleHelperMock; @@ -975,6 +978,11 @@ public SecondaryStorageHeuristicDao secondaryStorageHeuristicDao() { public HeuristicRuleHelper heuristicRuleHelper() { return Mockito.mock(HeuristicRuleHelper.class); } + @Bean + public SnapshotJoinDao snapshotJoinDao() { + return Mockito.mock(SnapshotJoinDao.class); + } + public static class Library implements TypeFilter { @Override diff --git a/server/src/test/java/org/apache/cloudstack/snapshot/SnapshotHelperTest.java b/server/src/test/java/org/apache/cloudstack/snapshot/SnapshotHelperTest.java index 1b0a8486e350..721e6755870c 100644 --- a/server/src/test/java/org/apache/cloudstack/snapshot/SnapshotHelperTest.java +++ b/server/src/test/java/org/apache/cloudstack/snapshot/SnapshotHelperTest.java @@ -19,13 +19,15 @@ package org.apache.cloudstack.snapshot; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - +import com.cloud.api.query.dao.SnapshotJoinDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; @@ -42,12 +44,12 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import com.cloud.hypervisor.Hypervisor; -import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.storage.DataStoreRole; -import com.cloud.storage.VolumeVO; -import com.cloud.storage.dao.SnapshotDao; -import com.cloud.utils.exception.CloudRuntimeException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; @RunWith(MockitoJUnitRunner.class) public class SnapshotHelperTest { @@ -83,6 +85,8 @@ public class SnapshotHelperTest { @Mock VolumeVO volumeVoMock; + @Mock + SnapshotJoinDao snapshotJoinDao; List dataStoreRoles = Arrays.asList(DataStoreRole.values()); @@ -94,10 +98,18 @@ public void init() { snapshotHelperSpy.storageStrategyFactory = storageStrategyFactoryMock; snapshotHelperSpy.snapshotDao = snapshotDaoMock; snapshotHelperSpy.dataStorageManager = dataStoreManager; + snapshotHelperSpy.snapshotJoinDao = snapshotJoinDao; } @Test public void validateExpungeTemporarySnapshotNotAKvmSnapshotOnPrimaryStorageDoNothing() { + DataStore store = Mockito.mock(DataStore.class); + DataStoreDriver storeDriver = Mockito.mock(DataStoreDriver.class); + Mockito.when(snapshotInfoMock.getDataStore()).thenReturn(store); + Mockito.when(snapshotInfoMock.getDataStore().getId()).thenReturn(1L); + Mockito.when(snapshotInfoMock.getSnapshotId()).thenReturn(1L); + Mockito.when(snapshotInfoMock.getDataStore().getDriver()).thenReturn(storeDriver); + Mockito.when(snapshotInfoMock.getDataStore().getDriver().getCapabilities()).thenReturn(new HashMap<>()); snapshotHelperSpy.expungeTemporarySnapshot(false, snapshotInfoMock); Mockito.verifyNoInteractions(snapshotServiceMock, snapshotDataStoreDaoMock); } @@ -105,27 +117,28 @@ public void validateExpungeTemporarySnapshotNotAKvmSnapshotOnPrimaryStorageDoNot @Test public void validateExpungeTemporarySnapshotKvmSnapshotOnPrimaryStorageExpungesSnapshot() { DataStore store = Mockito.mock(DataStore.class); + DataStoreDriver storeDriver = Mockito.mock(DataStoreDriver.class); + Mockito.when(store.getRole()).thenReturn(DataStoreRole.Image); Mockito.when(store.getId()).thenReturn(1L); Mockito.when(snapshotInfoMock.getDataStore()).thenReturn(store); - Mockito.doReturn(true).when(snapshotServiceMock).deleteSnapshot(Mockito.any()); - Mockito.doReturn(true).when(snapshotDataStoreDaoMock).expungeReferenceBySnapshotIdAndDataStoreRole(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); - + Mockito.when(snapshotInfoMock.getDataStore().getDriver()).thenReturn(storeDriver); + Mockito.when(snapshotInfoMock.getDataStore().getDriver().getCapabilities()).thenReturn(new HashMap<>()); snapshotHelperSpy.expungeTemporarySnapshot(true, snapshotInfoMock); - - Mockito.verify(snapshotServiceMock).deleteSnapshot(Mockito.any()); - Mockito.verify(snapshotDataStoreDaoMock).expungeReferenceBySnapshotIdAndDataStoreRole(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); } @Test public void validateIsKvmSnapshotOnlyInPrimaryStorageBackupToSecondaryTrue() { List hypervisorTypes = Arrays.asList(Hypervisor.HypervisorType.values()); - snapshotHelperSpy.backupSnapshotAfterTakingSnapshot = true; - hypervisorTypes.forEach(type -> { Mockito.doReturn(type).when(snapshotInfoMock).getHypervisorType(); dataStoreRoles.forEach(role -> { - Assert.assertFalse(snapshotHelperSpy.isKvmSnapshotOnlyInPrimaryStorage(snapshotInfoMock, role)); + if (!role.equals(DataStoreRole.Primary)) { + Assert.assertFalse(snapshotHelperSpy.isKvmSnapshotOnlyInPrimaryStorage(snapshotInfoMock, role, 1l)); + } else { + if (type.equals(HypervisorType.KVM)) + Assert.assertTrue(snapshotHelperSpy.isKvmSnapshotOnlyInPrimaryStorage(snapshotInfoMock, role, 1l)); + } }); }); } @@ -139,9 +152,9 @@ public void validateIsKvmSnapshotOnlyInPrimaryStorageBackupToSecondaryFalse() { Mockito.doReturn(type).when(snapshotInfoMock).getHypervisorType(); dataStoreRoles.forEach(role -> { if (type == Hypervisor.HypervisorType.KVM && role == DataStoreRole.Primary) { - Assert.assertTrue(snapshotHelperSpy.isKvmSnapshotOnlyInPrimaryStorage(snapshotInfoMock, role)); + Assert.assertTrue(snapshotHelperSpy.isKvmSnapshotOnlyInPrimaryStorage(snapshotInfoMock, role, null)); } else { - Assert.assertFalse(snapshotHelperSpy.isKvmSnapshotOnlyInPrimaryStorage(snapshotInfoMock, role)); + Assert.assertFalse(snapshotHelperSpy.isKvmSnapshotOnlyInPrimaryStorage(snapshotInfoMock, role, null)); } }); }); diff --git a/test/integration/plugins/storpool/TestSnapshotCopyOnPrimary.py b/test/integration/plugins/storpool/TestSnapshotCopyOnPrimary.py new file mode 100644 index 000000000000..b01ea2661efa --- /dev/null +++ b/test/integration/plugins/storpool/TestSnapshotCopyOnPrimary.py @@ -0,0 +1,255 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import time + +# Import Local Modules +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.cloudstackAPI import (createSnapshot, + deleteSnapshot, + copySnapshot, + createVolume, + createTemplate, + listOsTypes) +from marvin.lib.utils import (cleanup_resources, + random_gen) +from marvin.lib.base import (Account, + Zone, + ServiceOffering, + DiskOffering, + VirtualMachine, + Volume, + Snapshot, + Template, + StoragePool) +from marvin.lib.common import (get_domain, + get_zone, + get_template) +from marvin.lib.decoratorGenerators import skipTestIf +from marvin.codes import FAILED, PASS +from nose.plugins.attrib import attr +import logging +from sp_util import (TestData, StorPoolHelper) +import math + +class TestSnapshotCopy(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + testClient = super(TestSnapshotCopy, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.services = testClient.getParsedTestDataConfig() + + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests()) + cls.services['mode'] = cls.zone.networktype + + cls._cleanup = [] + cls.logger = logging.getLogger('TestSnapshotCopy') + cls.testsNotSupported = False + cls.zones = Zone.list(cls.apiclient) + cls.pools = StoragePool.list(cls.apiclient, status="Up") + enabled_core_zones = [] + if not isinstance(cls.zones, list): + cls.testsNotSupported = True + elif len(cls.zones) < 2: + cls.testsNotSupported = True + else: + for z in cls.zones: + if z.type == 'Core' and z.allocationstate == 'Enabled': + enabled_core_zones.append(z) + if len(enabled_core_zones) < 2: + cls.testsNotSupported = True + + if cls.testsNotSupported == True: + cls.logger.info("Unsupported") + return + + cls.additional_zone = None + for z in enabled_core_zones: + if z.id != cls.zone.id: + cls.additional_zone = z + + cls.storpool_pool = None + for pool in cls.pools: + if pool.provider == "StorPool" and pool.zoneid != cls.zone.id: + cls.storpool_pool = pool + break + + 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.services["iso"]["zoneid"] = cls.zone.id + + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain.id) + cls._cleanup.append(cls.account) + + cls.helper = StorPoolHelper() + + compute_offering_service = cls.services["service_offerings"]["tiny"].copy() + cls.service_offering = ServiceOffering.create( + cls.apiclient, + compute_offering_service) + cls._cleanup.append(cls.service_offering) + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + cls.services["virtual_machine"]["template"] = template.id + cls.virtual_machine = VirtualMachine.create( + cls.apiclient, + cls.services["virtual_machine"], + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.service_offering.id, + mode=cls.services["mode"] + ) + cls._cleanup.append(cls.virtual_machine) + cls.volume = Volume.list( + cls.apiclient, + virtualmachineid=cls.virtual_machine.id, + type='ROOT', + listall=True + )[0] + + @classmethod + def tearDownClass(cls): + super(TestSnapshotCopy, cls).tearDownClass() + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.userapiclient = self.testClient.getUserApiClient( + UserName=self.account.name, + DomainName=self.account.domain + ) + self.dbclient = self.testClient.getDbConnection() + self.snapshot_id = None + self.cleanup = [] + + def tearDown(self): + super(TestSnapshotCopy, self).tearDown() + + + @skipTestIf("testsNotSupported") + @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_01_take_snapshot_multi_zone(self): + """Test to take volume snapshot in multiple StorPool primary storage pools + """ + + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, pool_ids =[str(self.storpool_pool.id)]) + self.snapshot_id = snapshot.id + self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) + time.sleep(420) + Snapshot.delete(snapshot, self.userapiclient) + return + + @skipTestIf("testsNotSupported") + @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_02_copy_snapshot_multi_pools(self): + """Test to take volume snapshot on StorPool primary storage and then copy on StorPool primary storage in another pool + """ + + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id) + self.snapshot_id = snapshot.id + Snapshot.copy(self.userapiclient, self.snapshot_id, pool_ids=[str(self.storpool_pool.id)], source_zone_id=self.zone.id) + self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) + time.sleep(420) + Snapshot.delete(snapshot, self.userapiclient) + return + + @skipTestIf("testsNotSupported") + @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_03_take_snapshot_multi_pools_delete_single_zone(self): + """Test to take volume snapshot in multiple StorPool storages in diff zones and delete from one zone + """ + + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, pool_ids=[str(self.storpool_pool.id)]) + self.snapshot_id = snapshot.id + self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) + time.sleep(420) + Snapshot.delete(snapshot, self.userapiclient, self.zone.id) + self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.additional_zone.id]) + self.cleanup.append(snapshot) + return + + @skipTestIf("testsNotSupported") + @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_04_copy_snapshot_multi_zone_delete_all(self): + """Test to take volume snapshot on StorPool, copy in another StorPool primary storage in another zone and delete for all + """ + + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id) + self.snapshot_id = snapshot.id + Snapshot.copy(self.userapiclient, self.snapshot_id, pool_ids=[str(self.storpool_pool.id)], source_zone_id=self.zone.id) + self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) + time.sleep(420) + Snapshot.delete(snapshot, self.userapiclient) + snapshot_entries = Snapshot.list(self.userapiclient, id=snapshot.id) + if snapshot_entries and isinstance(snapshot_entries, list) and len(snapshot_entries) > 0: + self.fail("Snapshot delete for all zones failed") + return + + @skipTestIf("testsNotSupported") + @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_05_take_snapshot_multi_zone_create_volume_additional_zone(self): + """Test to take volume snapshot on StorPool in multiple zones and create a volume in one of the additional zones + """ + + snapshot = Snapshot.create(self.userapiclient,volume_id=self.volume.id, pool_ids=[str(self.storpool_pool.id)]) + self.snapshot_id = snapshot.id + self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) + disk_offering_id = None + if snapshot.volumetype == 'ROOT': + service = self.services["disk_offering"] + service["disksize"] = math.ceil(snapshot.virtualsize/(1024*1024*1024)) + self.disk_offering = DiskOffering.create( + self.apiclient, + service + ) + self.cleanup.append(self.disk_offering) + disk_offering_id = self.disk_offering.id + + self.volume = Volume.create(self.userapiclient, {"diskname":"StorPoolDisk-1" }, snapshotid=self.snapshot_id, zoneid=self.zone.id, diskofferingid=disk_offering_id) + self.cleanup.append(self.volume) + time.sleep(420) + Snapshot.delete(snapshot, self.userapiclient) + if self.zone.id != self.volume.zoneid: + self.fail("Volume from snapshot not created in the additional zone") + return + + @skipTestIf("testsNotSupported") + @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_06_take_snapshot_multi_zone_create_template_additional_zone(self): + """Test to take volume snapshot in multiple StorPool primary storages in diff zones and create a volume in one of the additional zones + """ + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, pool_ids=[str(self.storpool_pool.id)]) + self.snapshot_id = snapshot.id + self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) + self.template = self.helper.create_snapshot_template(self.userapiclient, self.services, self.snapshot_id, self.additional_zone.id) + if self.additional_zone.id != self.template.zoneid: + self.fail("Template from snapshot not created in the additional zone") + time.sleep(420) + Snapshot.delete(snapshot, self.userapiclient) + self.cleanup.append(self.template) + return diff --git a/test/integration/plugins/storpool/sp_util.py b/test/integration/plugins/storpool/sp_util.py index 633cba7915b0..084a57ee954e 100644 --- a/test/integration/plugins/storpool/sp_util.py +++ b/test/integration/plugins/storpool/sp_util.py @@ -925,3 +925,59 @@ def get_pool(cls, zone): cls.debug("Cannot perform the tests because there aren't the required count of StorPool storage pools %s" % sp_pools) return return sp_pools + + @classmethod + def create_snapshot_template(cls, apiclient, services, snapshot_id, zone_id): + cmd = createTemplate.createTemplateCmd() + cmd.displaytext = "TemplateFromSnap" + name = "-".join([cmd.displaytext, random_gen()]) + cmd.name = name + if "ostypeid" in services: + cmd.ostypeid = services["ostypeid"] + elif "ostype" in services: + sub_cmd = listOsTypes.listOsTypesCmd() + sub_cmd.description = services["ostype"] + ostypes = apiclient.listOsTypes(sub_cmd) + + if not isinstance(ostypes, list): + cls.fail("Unable to find Ostype id with desc: %s" % + services["ostype"]) + cmd.ostypeid = ostypes[0].id + else: + cls.fail("Unable to find Ostype is required for creating template") + + cmd.isfeatured = True + cmd.ispublic = True + cmd.isextractable = False + + cmd.snapshotid = snapshot_id + cmd.zoneid = zone_id + apiclient.createTemplate(cmd) + templates = Template.list(apiclient, name=name, templatefilter="self") + if not isinstance(templates, list) and len(templates) < 0: + cls.fail("Unable to find created template with name %s" % name) + template = Template(templates[0].__dict__) + return template + + @classmethod + def verify_snapshot_copies(cls, userapiclient, snapshot_id, zone_ids): + snapshot_entries = Snapshot.list(userapiclient, id=snapshot_id, showunique=False) + if not isinstance(snapshot_entries, list): + cls.fail("Unable to list snapshot for multiple zones") + snapshots = set() + new_list = [] + for obj in snapshot_entries: + if obj.zoneid not in snapshots: + new_list.append(obj) + snapshots.add(obj.zoneid) + + if len(new_list) != len(zone_ids): + cls.fail("Undesired list snapshot size for multiple zones") + for zone_id in zone_ids: + zone_found = False + for entry in new_list: + if entry.zoneid == zone_id: + zone_found = True + break + if zone_found == False: + cls.fail("Unable to find snapshot entry for the zone ID: %s" % zone_id) diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index ebfc4816015d..a053f46638e2 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -1149,7 +1149,7 @@ def __init__(self, items): @classmethod def create(cls, apiclient, services, zoneid=None, account=None, - domainid=None, diskofferingid=None, projectid=None, size=None): + domainid=None, diskofferingid=None, projectid=None, size=None, snapshotid=None): """Create Volume""" cmd = createVolume.createVolumeCmd() cmd.name = "-".join([services["diskname"], random_gen()]) @@ -1180,6 +1180,9 @@ def create(cls, apiclient, services, zoneid=None, account=None, if size: cmd.size = size + if snapshotid: + cmd.snapshotid = snapshotid + return Volume(apiclient.createVolume(cmd).__dict__) def update(self, apiclient, **kwargs): @@ -1395,7 +1398,7 @@ def __init__(self, items): @classmethod def create(cls, apiclient, volume_id, account=None, - domainid=None, projectid=None, locationtype=None, asyncbackup=None): + domainid=None, projectid=None, locationtype=None, asyncbackup=None, zoneids=None, pool_ids=None): """Create Snapshot""" cmd = createSnapshot.createSnapshotCmd() cmd.volumeid = volume_id @@ -1409,12 +1412,18 @@ def create(cls, apiclient, volume_id, account=None, cmd.locationtype = locationtype if asyncbackup: cmd.asyncbackup = asyncbackup + if zoneids: + cmd.zoneids = zoneids + if pool_ids: + cmd.storageids = pool_ids return Snapshot(apiclient.createSnapshot(cmd).__dict__) - def delete(self, apiclient): + def delete(self, apiclient, zone_id=None): """Delete Snapshot""" cmd = deleteSnapshot.deleteSnapshotCmd() cmd.id = self.id + if zone_id: + cmd.zoneid = zone_id apiclient.deleteSnapshot(cmd) @classmethod @@ -1427,6 +1436,20 @@ def list(cls, apiclient, **kwargs): cmd.listall = True return (apiclient.listSnapshots(cmd)) + @classmethod + def copy(cls, apiclient, snapshotid, zone_ids=None, source_zone_id=None, pool_ids=None): + """ Copy snapshot to another zone or a primary storage in another zone""" + cmd = copySnapshot.copySnapshotCmd() + cmd.id = snapshotid + if source_zone_id: + cmd.sourcezoneid = source_zone_id + if zone_ids: + cmd.zoneids = zone_ids + if pool_ids: + cmd.storageids = pool_ids + return Snapshot(apiclient.copySnapshot(cmd).__dict__) + + def validateState(self, apiclient, snapshotstate, timeout=600): """Check if snapshot is in required state returnValue: List[Result, Reason] @@ -1462,7 +1485,7 @@ def __init__(self, items): @classmethod def create(cls, apiclient, services, volumeid=None, - account=None, domainid=None, projectid=None, randomise=True): + account=None, domainid=None, projectid=None, randomise=True, snapshotid=None, zoneid=None): """Create template from Volume""" # Create template from Virtual machine and Volume ID cmd = createTemplate.createTemplateCmd() @@ -1508,6 +1531,12 @@ def create(cls, apiclient, services, volumeid=None, if projectid: cmd.projectid = projectid + + if snapshotid: + cmd.snapshotid = snapshotid + + if zoneid: + cmd.zoneid = zoneid return Template(apiclient.createTemplate(cmd).__dict__) @classmethod diff --git a/ui/src/views/storage/SnapshotZones.vue b/ui/src/views/storage/SnapshotZones.vue index d386229377ed..140c0d2b4335 100644 --- a/ui/src/views/storage/SnapshotZones.vue +++ b/ui/src/views/storage/SnapshotZones.vue @@ -309,7 +309,6 @@ export default { const params = {} params.id = this.resource.id params.showunique = false - params.locationtype = 'Secondary' params.listall = true params.page = this.page params.pagesize = this.pageSize @@ -320,6 +319,9 @@ export default { getAPI('listSnapshots', params).then(json => { this.dataSource = json.listsnapshotsresponse.snapshot || [] this.itemCount = json.listsnapshotsresponse.count || 0 + if (this.itemCount > 0) { + this.dataSource = this.dataSource.filter((obj, index) => this.dataSource.findIndex((item) => item.zoneid === obj.zoneid) === index) + } }).catch(error => { this.$notifyError(error) }).finally(() => { From 6c1f2cca02efcfd3448c9a7d6c160404e9e899a9 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Wed, 14 Aug 2024 11:09:15 +0300 Subject: [PATCH 02/15] added option in the UI for the storage pools Added drop down to choose the primary storage pools to copy a snapshot Small fixes --- .../StorPoolAbandonObjectsCollector.java | 6 +- .../datastore/util/StorPoolHelper.java | 12 ++++ .../storage/datastore/util/StorPoolUtil.java | 3 +- .../snapshot/StorPoolSnapshotStrategy.java | 27 ++++---- .../cloud/storage/VolumeApiServiceImpl.java | 1 + .../storage/snapshot/SnapshotManagerImpl.java | 2 + .../cloud/template/TemplateManagerImpl.java | 4 +- .../cloudstack/snapshot/SnapshotHelper.java | 17 ++++++ ui/public/locales/en.json | 4 +- ui/src/views/storage/FormSchedule.vue | 47 +++++++++++++- ui/src/views/storage/SnapshotZones.vue | 61 +++++++++++++++++-- ui/src/views/storage/TakeSnapshot.vue | 46 +++++++++++++- 12 files changed, 201 insertions(+), 29 deletions(-) diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java index 1310cde72222..4a7f56a8d251 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java @@ -41,7 +41,6 @@ import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; -import org.apache.cloudstack.storage.snapshot.StorPoolSnapshotStrategy; import org.apache.commons.collections.CollectionUtils; import javax.inject.Inject; @@ -379,12 +378,13 @@ protected void runInContext() { } if (snapshots.contains(name)) { Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId(name, conn), clusterDao); - conn = StorPoolSnapshotStrategy.getSpConnectionDesc(conn, clusterId); + conn = StorPoolHelper.getSpConnectionDesc(conn, clusterId); SpApiResponse resp = StorPoolUtil.snapshotUnexport(name, location, conn); if (resp.getError() == null) { + StorPoolUtil.spLog("Unexport of snapshot %s was successful", name); recoveredSnapshots.add(snapshot.getId()); } else { - logger.debug(String.format("Could not recover StorPool snapshot %s", resp.getError())); + StorPoolUtil.spLog("Could not recover StorPool snapshot %s", resp.getError()); } } } diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java index 53fafda02b72..e101df148a37 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java @@ -64,6 +64,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; public class StorPoolHelper { @@ -305,4 +306,15 @@ public static boolean isPoolSupportsAllFunctionalityFromPreviousVersion(StorageP } return true; } + + public static StorPoolUtil.SpConnectionDesc getSpConnectionDesc(StorPoolUtil.SpConnectionDesc connectionLocal, Long clusterId) { + + String subClusterEndPoint = StorPoolConfigurationManager.StorPoolSubclusterEndpoint.valueIn(clusterId); + if (StringUtils.isNotEmpty(subClusterEndPoint)) { + String host = subClusterEndPoint.split(";")[0].split("=")[1]; + String token = subClusterEndPoint.split(";")[1].split("=")[1]; + connectionLocal = new StorPoolUtil.SpConnectionDesc(host, token, connectionLocal.getTemplateName()); + } + return connectionLocal; + } } diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java index b337e8a1f7f1..23c480ec90a2 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java @@ -696,7 +696,8 @@ public static SpApiResponse snapshotExport(String name, String location, SpConne public static SpApiResponse snapshotUnexport(String name, String location, SpConnectionDesc conn) { Map json = new HashMap<>(); json.put("snapshot", name); - json.put("location", location); + json.put("force", true); + json.put("all", true); return POST("SnapshotUnexport", json, conn); } diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java index 5901e2718f3e..1d3c0711253e 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java @@ -59,7 +59,6 @@ import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -198,6 +197,12 @@ public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperat if (CollectionUtils.isEmpty(pools)) { return StrategyPriority.CANT_HANDLE; } + List snapshots = snapshotJoinDao.listBySnapshotIdAndZoneId(zoneId, snapshot.getId()); + boolean snapshotNotOnStorPool = snapshots.stream().filter(s -> s.getStoreRole().equals(DataStoreRole.Primary)).count() == 0; + + if (snapshotNotOnStorPool) { + return StrategyPriority.CANT_HANDLE; + } for (StoragePoolVO pool : pools) { SnapshotDataStoreVO snapshotOnPrimary = _snapshotStoreDao.findByStoreSnapshot(DataStoreRole.Primary, pool.getId(), snapshot.getId()); if (snapshotOnPrimary != null && (snapshotOnPrimary.getState().equals(State.Ready) || snapshotOnPrimary.getState().equals(State.Created))) { @@ -393,7 +398,7 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp snapshot.getDataStore().getId(), storagePoolDetailsDao, _primaryDataStoreDao); String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(srcSnapshot.getPath(), false); Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId("~" + snapshotName, connectionLocal), clusterDao); - connectionLocal = getSpConnectionDesc(connectionLocal, clusterId); + connectionLocal = StorPoolHelper.getSpConnectionDesc(connectionLocal, clusterId); SpApiResponse resp = StorPoolUtil.snapshotExport("~" + snapshotName, location, connectionLocal); if (resp.getError() != null) { StorPoolUtil.spLog("Failed to export snapshot %s from %s due to %s", snapshotName, location, resp.getError()); @@ -403,6 +408,9 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp callback.complete(res); return; } + String detail = "~" + snapshotName + ";" + location; + SnapshotDetailsVO snapshotForRecovery = new SnapshotDetailsVO(snapshot.getId(), StorPoolUtil.SP_RECOVERED_SNAPSHOT, detail, true); + _snapshotDetailsDao.persist(snapshotForRecovery); SpConnectionDesc connectionRemote = StorPoolUtil.getSpConnection(storagePoolVO.getUuid(), storagePoolVO.getId(), storagePoolDetailsDao, _primaryDataStoreDao); String localLocation = StorPoolConfigurationManager.StorPoolClusterLocation @@ -420,9 +428,7 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp return; } StorPoolUtil.spLog("The snapshot [%s] was copied from remote", snapshotName); - String detail = "~" + snapshotName + ";" + location; - SnapshotDetailsVO snapshotForRecovery = new SnapshotDetailsVO(snapshot.getId(), StorPoolUtil.SP_RECOVERED_SNAPSHOT, detail, true); - _snapshotDetailsDao.persist(snapshotForRecovery); + respFromRemote = StorPoolUtil.snapshotReconcile("~" + snapshotName, connectionRemote); if (respFromRemote.getError() != null) { StorPoolUtil.spLog("Failed to reconcile snapshot %s from %s due to %s", snapshotName, location, respFromRemote.getError()); @@ -449,15 +455,4 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp res.setResult(err); callback.complete(res); } - - public static SpConnectionDesc getSpConnectionDesc(SpConnectionDesc connectionLocal, Long clusterId) { - - String subClusterEndPoint = StorPoolConfigurationManager.StorPoolSubclusterEndpoint.valueIn(clusterId); - if (StringUtils.isNotEmpty(subClusterEndPoint)) { - String host = subClusterEndPoint.split(";")[0].split("=")[1]; - String token = subClusterEndPoint.split(";")[1].split("=")[1]; - connectionLocal = new SpConnectionDesc(host, token, connectionLocal.getTemplateName()); - } - return connectionLocal; - } } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 57bad42bd0f6..040e6a4cd0f9 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -4073,6 +4073,7 @@ private boolean canCopyOnPrimary(List poolIds, VolumeInfo volume, boolean } else { return false; } + snapshotHelper.checkIfThereAreMoreThanOnePoolInTheZone(poolIds); return true; } diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index d5cbefcc4853..c56193e57b62 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -1152,6 +1152,7 @@ protected void validatePolicyZones(List zoneIds, List poolIds, Volum } } if (hasPools) { + snapshotHelper.checkIfThereAreMoreThanOnePoolInTheZone(poolIds); for (Long poolId : poolIds) { getCheckedDestinationStorageForSnapshotCopy(poolId, isRootAdminCaller); } @@ -2284,6 +2285,7 @@ public Snapshot copySnapshot(CopySnapshotCmd cmd) throws StorageUnavailableExcep throw new InvalidParameterValueException(String.format("There is no snapshot ID: %s ready on image store", snapshot.getUuid())); } if (canCopyBetweenStoragePools) { + snapshotHelper.checkIfThereAreMoreThanOnePoolInTheZone(storagePoolIds); copySnapshotToPrimaryDifferentZone(storagePoolIds, snapshot); } List failedZones = copySnapshotToZones(snapshot, srcSecStore, new ArrayList<>(dataCenterVOs.values())); diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 59f33c5e614c..3a2c6b183918 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -1709,7 +1709,7 @@ public VirtualMachineTemplate createPrivateTemplate(CreateTemplateCmd command) t if (kvmSnapshotOnlyInPrimaryStorage && keepOnPrimary) { skipCopyToSecondary = true; } - if (dataStoreRole == DataStoreRole.Image || !skipCopyToSecondary) { + if (dataStoreRole == DataStoreRole.Image) { snapInfo = snapshotHelper.backupSnapshotToSecondaryStorageIfNotExists(snapInfo, dataStoreRole, snapshot, kvmSnapshotOnlyInPrimaryStorage); _accountMgr.checkAccess(caller, null, true, snapInfo); DataStore snapStore = snapInfo.getDataStore(); @@ -1717,7 +1717,7 @@ public VirtualMachineTemplate createPrivateTemplate(CreateTemplateCmd command) t if (snapStore != null) { store = snapStore; // pick snapshot image store to create template } - } else if (skipCopyToSecondary) { + } else if (keepOnPrimary) { ImageStoreVO imageStore = _imgStoreDao.findOneByZoneAndProtocol(zoneId, "nfs"); if (imageStore == null) { throw new CloudRuntimeException(String.format("Could not find an NFS secondary storage pool on zone %s to use as a temporary location " + diff --git a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java index 910d7d2a514c..2083167e9ccb 100644 --- a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java +++ b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java @@ -51,7 +51,9 @@ import org.apache.logging.log4j.Logger; import javax.inject.Inject; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -303,10 +305,25 @@ protected Set getSnapshotIdsOnlyInPrimaryStorage(long volumeId) { } public SnapshotInfo convertSnapshotIfNeeded(SnapshotInfo snapshotInfo) { + if (snapshotInfo.getParent() == null || !HypervisorType.KVM.equals(snapshotInfo.getHypervisorType())) { return snapshotInfo; } return snapshotService.convertSnapshot(snapshotInfo); } + + public void checkIfThereAreMoreThanOnePoolInTheZone(List poolIds) { + List poolsInOneZone = new ArrayList<>(); + for (Long poolId : poolIds) { + StoragePoolVO pool = primaryDataStoreDao.findById(poolId); + if (pool != null) { + poolsInOneZone.add(pool.getDataCenterId()); + } + } + boolean moreThanOnePoolForZone = poolsInOneZone.stream().filter(itr -> Collections.frequency(poolsInOneZone, itr) > 1).count() > 1; + if (moreThanOnePoolForZone) { + throw new CloudRuntimeException("Cannot copy the snapshot on multiple storage pools in one zone"); + } + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 4eace1c810ec..65ba517a94be 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2198,7 +2198,8 @@ "label.select.root.disk": "Select the ROOT disk", "label.select.source.vcenter.datacenter": "Select the source VMware vCenter Datacenter", "label.select.tier": "Select Network Tier", -"label.select.zones": "Select Zones", +"label.select.zones": "Select zones", +"label.select.storagepools": "Select storage pools", "label.select.2fa.provider": "Select the provider", "label.selected.storage": "Selected storage", "label.self": "Mine", @@ -2381,6 +2382,7 @@ "label.storagemotionenabled": "Storage motion enabled", "label.storagepolicy": "Storage policy", "label.storagepool": "Storage pool", +"label.storagepools": "Storage pools", "label.storagepool.tooltip": "Destination Storage Pool. Volume should be located in this Storage Pool", "label.storagetags": "Storage tags", "label.storagetype": "Storage type", diff --git a/ui/src/views/storage/FormSchedule.vue b/ui/src/views/storage/FormSchedule.vue index ea8559016b09..d8d14cb231e0 100644 --- a/ui/src/views/storage/FormSchedule.vue +++ b/ui/src/views/storage/FormSchedule.vue @@ -169,6 +169,32 @@ + + + + + + + + + {{ opt.name || opt.description }} + + + + +
{{ $t('label.tags') }}
@@ -272,7 +298,8 @@ export default { timeZoneMap: [], fetching: false, listDayOfWeek: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'], - zones: [] + zones: [], + storagePools: [] } }, created () { @@ -307,6 +334,7 @@ export default { }) if (this.resourceType === 'Volume') { this.fetchZoneData() + this.fetchStoragePoolData() } }, fetchZoneData () { @@ -323,6 +351,20 @@ export default { this.zoneLoading = false }) }, + fetchStoragePoolData () { + const params = {} + params.showicon = true + this.storagePoolsLoading = true + api('listStoragePools', params).then(json => { + const listStoragePools = json.liststoragepoolsresponse.storagepool + if (listStoragePools) { + this.storagePools = listStoragePools + this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES && pool.zoneid !== this.resource.zoneid) + } + }).finally(() => { + this.storagePoolsLoading = false + }) + }, fetchTimeZone (value) { this.timeZoneMap = [] this.fetching = true @@ -422,6 +464,9 @@ export default { if (values.zoneids && values.zoneids.length > 0) { params.zoneids = values.zoneids.join() } + if (values.storageids && values.storageids.length > 0) { + params.storageids = values.storageids.join() + } switch (values.intervaltype) { case 'hourly': params.schedule = values.time diff --git a/ui/src/views/storage/SnapshotZones.vue b/ui/src/views/storage/SnapshotZones.vue index 140c0d2b4335..aa869d4176f5 100644 --- a/ui/src/views/storage/SnapshotZones.vue +++ b/ui/src/views/storage/SnapshotZones.vue @@ -137,10 +137,39 @@ - + + + +
+ + + + + {{ opt.name || opt.description }} +
+
+
+
{{ $t('label.cancel') }} - {{ $t('label.ok') }} + + {{ $t('label.ok') }} +
@@ -238,6 +267,8 @@ export default { currentRecord: {}, zones: [], zoneLoading: false, + storagePools: [], + storagePoolLoading: false, copyLoading: false, deleteLoading: false, showDeleteSnapshot: false, @@ -297,12 +328,17 @@ export default { } } }, + computed: { + isCopySnapshotSubmitDisabled () { + return this.form.storageid.length === 0 && this.form.zoneid.length === 0 + } + }, methods: { initForm () { this.formRef = ref() this.form = reactive({}) this.rules = reactive({ - zoneid: [{ type: 'array', required: true, message: this.$t('message.error.select') }] + zoneid: [{ type: 'array', required: false }] }) }, fetchData () { @@ -488,10 +524,26 @@ export default { this.zoneLoading = false }) }, + fetchStoragePoolData () { + const params = {} + params.showicon = true + this.storagePoolsLoading = true + api('listStoragePools', params).then(json => { + const listStoragePools = json.liststoragepoolsresponse.storagepool + if (listStoragePools) { + this.storagePools = listStoragePools + this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES && pool.zoneid !== this.resource.zoneid) + } + }).finally(() => { + this.storagePoolsLoading = false + }) + }, showCopySnapshot (record) { this.currentRecord = record this.form.zoneid = [] + this.form.storageid = [] this.fetchZoneData() + this.fetchStoragePoolData() this.showCopyActionForm = true }, onShowDeleteModal (record) { @@ -520,7 +572,8 @@ export default { const params = { id: this.currentRecord.id, sourcezoneid: this.currentRecord.zoneid, - destzoneids: values.zoneid.join() + destzoneids: values.zoneid.join(), + storageids: values.storageid.join() } this.copyLoading = true postAPI(this.copyApi, params).then(json => { diff --git a/ui/src/views/storage/TakeSnapshot.vue b/ui/src/views/storage/TakeSnapshot.vue index 69847f946196..dd4fa482e64b 100644 --- a/ui/src/views/storage/TakeSnapshot.vue +++ b/ui/src/views/storage/TakeSnapshot.vue @@ -66,7 +66,31 @@ - + + + + + + + + {{ opt.name || opt.description }} + + + + + @@ -159,6 +183,8 @@ export default { inputVisible: '', zones: [], zoneLoading: false, + storagePools: [], + storagePoolLoading: false, tags: [], dataSource: [] } @@ -175,6 +201,7 @@ export default { this.supportsStorageSnapshot = this.resource.supportsstoragesnapshot this.fetchZoneData() + this.fetchStoragePoolData() }, computed: { formattedAdditionalZoneMessage () { @@ -205,6 +232,20 @@ export default { this.zoneLoading = false }) }, + fetchStoragePoolData () { + const params = {} + params.showicon = true + this.storagePoolsLoading = true + api('listStoragePools', params).then(json => { + const listStoragePools = json.liststoragepoolsresponse.storagepool + if (listStoragePools) { + this.storagePools = listStoragePools + this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES && pool.zoneid !== this.resource.zoneid) + } + }).finally(() => { + this.storagePoolsLoading = false + }) + }, handleSubmit (e) { e.preventDefault() if (this.actionLoading) return @@ -228,6 +269,9 @@ export default { if (values.zoneids && values.zoneids.length > 0) { params.zoneids = values.zoneids.join() } + if (values.storageids && values.storageids.length > 0) { + params.storageids = values.storageids.join() + } for (let i = 0; i < this.tags.length; i++) { const formattedTagData = {} const tag = this.tags[i] From baa2f93c1f785843c98a95b2165fe799a6f79960 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Fri, 30 Aug 2024 17:10:44 +0300 Subject: [PATCH 03/15] Addressed comments --- .../cloudstack/api/command/user/snapshot/CopySnapshotCmd.java | 2 +- .../engine/subsystem/api/storage/DataStoreCapabilities.java | 2 +- .../datastore/driver/StorPoolPrimaryDataStoreDriver.java | 2 +- .../src/main/java/com/cloud/storage/VolumeApiServiceImpl.java | 2 +- .../java/com/cloud/storage/snapshot/SnapshotManagerImpl.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java index b66714a09f1e..b84fe9067203 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java @@ -91,7 +91,7 @@ public class CopySnapshotCmd extends BaseAsyncCmd implements UserCmd { entityType = StoragePoolResponse.class, required = false, description = "A comma-separated list of IDs of the storage pools in other zones in which the snapshot will be made available. " + - "The snapshot will always be made available in the zone in which the volume is present.") + "The snapshot will always be made available in the zone in which the volume is present. Currently supported for StorPool only") protected List storagePoolIds; ///////////////////////////////////////////////////// diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java index 8016d4cdc42c..0b97190d1261 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java @@ -44,7 +44,7 @@ public enum DataStoreCapabilities { /** * indicates that the driver supports copying snapshot between zones on pools of the same type */ - CAN_COPY_SNAPSHOT_BETWEEN_ZONES, + CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE, /** * indicates that the storage does not need to delete the snapshot when creating a volume/template from it * and the setting `snapshot.backup.to.secondary` is enabled diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java index e98a924b9c1a..dac22e82d1bc 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java @@ -190,7 +190,7 @@ private SnapshotDataStoreVO getSnapshotImageStoreRef(long snapshotId, long zoneI @Override public Map getCapabilities() { Map mapCapabilities = new HashMap<>(); - mapCapabilities.put(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES.toString(), Boolean.TRUE.toString()); + mapCapabilities.put(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE.toString(), Boolean.TRUE.toString()); mapCapabilities.put(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString(), Boolean.TRUE.toString()); return mapCapabilities; } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 040e6a4cd0f9..4843890d62f2 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -4064,7 +4064,7 @@ private boolean canCopyOnPrimary(List poolIds, VolumeInfo volume, boolean DataStore dataStore = dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); StoragePoolVO sPool = _storagePoolDao.findById(poolId); if (dataStore != null - && !dataStore.getDriver().getCapabilities().containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES.toString()) + && !dataStore.getDriver().getCapabilities().containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE.toString()) && sPool.getPoolType() != volume.getStoragePoolType() && volume.getPoolId() == poolId) { throw new InvalidParameterValueException("The specified pool doesn't support copying snapshots between zones" + poolId); diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index c56193e57b62..49de80bbd4a2 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -2320,7 +2320,7 @@ private boolean canCopyOnPrimary(List poolIds, Snapshot snapshot) { continue; } if (!dataStore.getDriver().getCapabilities() - .containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES.toString()) + .containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE.toString()) && dataStore.getPoolType() != volume.getPoolType()) { poolsToBeRemoved.add(poolId); logger.debug(String.format("The %s does not support copy to %s between zones", dataStore.getPoolType(), volume.getPoolType())); From 39923f43fa78990e59ec51d8218de061ef8cb989 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Mon, 9 Sep 2024 13:41:13 +0300 Subject: [PATCH 04/15] Address reviews --- .../api/storage/DataStoreCapabilities.java | 7 +------ .../orchestration/VolumeOrchestrator.java | 5 ++--- .../StorPoolAbandonObjectsCollector.java | 8 ++++++-- .../driver/StorPoolPrimaryDataStoreDriver.java | 1 - .../storage/snapshot/SnapshotManagerImpl.java | 18 +++--------------- .../cloud/template/TemplateManagerImpl.java | 7 ++----- .../cloudstack/snapshot/SnapshotHelper.java | 12 +++++++++--- .../snapshot/SnapshotHelperTest.java | 5 ----- ui/src/views/storage/FormSchedule.vue | 2 +- ui/src/views/storage/SnapshotZones.vue | 2 +- ui/src/views/storage/TakeSnapshot.vue | 2 +- 11 files changed, 26 insertions(+), 43 deletions(-) diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java index 0b97190d1261..b221a90c0b32 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java @@ -44,10 +44,5 @@ public enum DataStoreCapabilities { /** * indicates that the driver supports copying snapshot between zones on pools of the same type */ - CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE, - /** - * indicates that the storage does not need to delete the snapshot when creating a volume/template from it - * and the setting `snapshot.backup.to.secondary` is enabled - */ - KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP + CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 1fb7c02681ff..d5bfb279b792 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -55,7 +55,6 @@ import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; @@ -585,8 +584,8 @@ public VolumeInfo createVolumeFromSnapshot(Volume volume, Snapshot snapshot, Use boolean kvmSnapshotOnlyInPrimaryStorage = snapshotHelper.isKvmSnapshotOnlyInPrimaryStorage(snapshot, dataStoreRole, volume.getDataCenterId()); boolean skipCopyToSecondary = false; - Map capabilities = snapInfo.getDataStore().getDriver().getCapabilities(); - boolean keepOnPrimary = MapUtils.isNotEmpty(capabilities) && capabilities.containsKey(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString()); + boolean keepOnPrimary = snapshotHelper.isStorPoolStorage(snapInfo); + if (kvmSnapshotOnlyInPrimaryStorage && keepOnPrimary) { skipCopyToSecondary = true; } diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java index 4a7f56a8d251..ca9e3c01a368 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java @@ -365,11 +365,15 @@ protected void runInContext() { SpConnectionDesc conn = StorPoolUtil.getSpConnection(storagePool.getUuid(), storagePool.getId(), storagePoolDetailsDao, storagePoolDao); JsonArray arr = StorPoolUtil.snapshotsList(conn); - List snapshots = snapshotsForRcovery(arr); + List snapshots = snapshotsForRecovery(arr); if (snapshots.isEmpty()) { continue; } for (SnapshotDetailsVO snapshot : snapshotDetails) { + String[] snapshotOnRemote = snapshot.getValue().split(";"); + if (snapshotOnRemote.length != 2) { + continue; + } String name = snapshot.getValue().split(";")[0]; String location = snapshot.getValue().split(";")[1]; if (name == null || location == null) { @@ -398,7 +402,7 @@ protected void runInContext() { } } - private static List snapshotsForRcovery(JsonArray arr) { + private static List snapshotsForRecovery(JsonArray arr) { List snapshots = new ArrayList<>(); for (int i = 0; i < arr.size(); i++) { boolean recoveringFromRemote = arr.get(i).getAsJsonObject().get("recoveringFromRemote").getAsBoolean(); diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java index dac22e82d1bc..233ed0d5193a 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java @@ -191,7 +191,6 @@ private SnapshotDataStoreVO getSnapshotImageStoreRef(long snapshotId, long zoneI public Map getCapabilities() { Map mapCapabilities = new HashMap<>(); mapCapabilities.put(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE.toString(), Boolean.TRUE.toString()); - mapCapabilities.put(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString(), Boolean.TRUE.toString()); return mapCapabilities; } diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 49de80bbd4a2..298ac9516d0d 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.storage.snapshot; +import com.cloud.storage.StoragePoolStatus; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -53,10 +54,12 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotResult; @@ -135,7 +138,6 @@ import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; -import com.cloud.storage.StoragePoolStatus; import com.cloud.storage.VMTemplateStorageResourceAssoc; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; @@ -181,8 +183,6 @@ import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; @Component public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implements SnapshotManager, SnapshotApiService, Configurable { @@ -1725,14 +1725,6 @@ private void copyNewSnapshotToZonesOnPrimary(CreateSnapshotPayload payload, Snap private boolean copySnapshotOnPool(SnapshotInfo snapshot, SnapshotStrategy snapshotStrategy, Long storagePoolId) { DataStore store = dataStoreMgr.getDataStore(storagePoolId, DataStoreRole.Primary); SnapshotInfo snapshotOnStore = (SnapshotInfo) store.create(snapshot); -// if (snapshotOnStore.getStatus() == ObjectInDataStoreStateMachine.State.Allocated) { -// snapshotOnStore.processEvent(Event.CreateOnlyRequested); -// } else if (snapshotOnStore.getStatus() == ObjectInDataStoreStateMachine.State.Ready) { -// snapshotOnStore.processEvent(Event.CopyRequested); -// } else { -// logger.info(String.format("Cannot copy snapshot to another storage in different zone. It's not in the right state %s", snapshotOnStore.getStatus())); -// return false; -// } try { AsyncCallFuture future = snapshotSrv.copySnapshot(snapshot, snapshotOnStore, snapshotStrategy); @@ -1753,10 +1745,6 @@ private boolean copySnapshotOnPool(SnapshotInfo snapshot, SnapshotStrategy snaps } catch (ExecutionException e) { throw new RuntimeException(e); } -// boolean copySuccessful = snapshotStrategy.copySnapshot(snapshot, snapshotOnStore, null); -// if (!copySuccessful) { -// snapshotOnStore.processEvent(Event.OperationFailed); -// } return true; } diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 3a2c6b183918..25fad638124c 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -63,7 +63,6 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; @@ -1703,13 +1702,11 @@ public VirtualMachineTemplate createPrivateTemplate(CreateTemplateCmd command) t boolean kvmIncrementalSnapshot = SnapshotManager.kvmIncrementalSnapshot.valueIn(_hostDao.findClusterIdByVolumeInfo(snapInfo.getBaseVolume())); boolean skipCopyToSecondary = false; - - Map capabilities = snapInfo.getDataStore().getDriver().getCapabilities(); - boolean keepOnPrimary = MapUtils.isNotEmpty(capabilities) && capabilities.containsKey(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString()); + boolean keepOnPrimary = snapshotHelper.isStorPoolStorage(snapInfo); if (kvmSnapshotOnlyInPrimaryStorage && keepOnPrimary) { skipCopyToSecondary = true; } - if (dataStoreRole == DataStoreRole.Image) { + if (dataStoreRole == DataStoreRole.Image || !skipCopyToSecondary) { snapInfo = snapshotHelper.backupSnapshotToSecondaryStorageIfNotExists(snapInfo, dataStoreRole, snapshot, kvmSnapshotOnlyInPrimaryStorage); _accountMgr.checkAccess(caller, null, true, snapInfo); DataStore snapStore = snapInfo.getDataStore(); diff --git a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java index 2083167e9ccb..ca6bbbd1dddb 100644 --- a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java +++ b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java @@ -100,12 +100,11 @@ public void expungeTemporarySnapshot(boolean kvmSnapshotOnlyInPrimaryStorage, Sn long storeId = snapInfo.getDataStore().getId(); long zoneId = dataStorageManager.getStoreZoneId(storeId, snapInfo.getDataStore().getRole()); - Map capabilities = snapInfo.getDataStore().getDriver().getCapabilities(); - boolean keepOnPrimary = MapUtils.isNotEmpty(capabilities) && capabilities.containsKey(DataStoreCapabilities.KEEP_SNAPSHOT_ON_PRIMARY_AND_BACKUP.toString()); - if (keepOnPrimary) { + if (isStorPoolStorage(snapInfo)) { logger.debug("The primary storage does not delete the snapshots even if there is a backup on secondary"); return; } + List snapshots = snapshotJoinDao.listBySnapshotIdAndZoneId(zoneId, snapInfo.getSnapshotId()); if (kvmSnapshotOnlyInPrimaryStorage || snapshots.size() <= 1) { if (snapInfo != null) { @@ -147,6 +146,13 @@ public void expungeTemporarySnapshot(boolean kvmSnapshotOnlyInPrimaryStorage, Sn snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(snapInfo.getId(), storeId, DataStoreRole.Image); } + public boolean isStorPoolStorage(SnapshotInfo snapInfo) { + if (DataStoreRole.Primary.equals(snapInfo.getDataStore().getRole())) { + StoragePoolVO storagePoolVO = primaryDataStoreDao.findById(snapInfo.getDataStore().getId()); + return storagePoolVO != null && StoragePoolType.StorPool.equals(storagePoolVO.getPoolType()); + } + return false; + } /** * Backup the snapshot to secondary storage if it should be backed up and was not yet or it is a temporary backup to create a volume. * @return The parameter snapInfo if the snapshot is not backupable, else backs up the snapshot to secondary storage and returns its info. diff --git a/server/src/test/java/org/apache/cloudstack/snapshot/SnapshotHelperTest.java b/server/src/test/java/org/apache/cloudstack/snapshot/SnapshotHelperTest.java index 721e6755870c..ac254ed1c5ec 100644 --- a/server/src/test/java/org/apache/cloudstack/snapshot/SnapshotHelperTest.java +++ b/server/src/test/java/org/apache/cloudstack/snapshot/SnapshotHelperTest.java @@ -46,7 +46,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -108,8 +107,6 @@ public void validateExpungeTemporarySnapshotNotAKvmSnapshotOnPrimaryStorageDoNot Mockito.when(snapshotInfoMock.getDataStore()).thenReturn(store); Mockito.when(snapshotInfoMock.getDataStore().getId()).thenReturn(1L); Mockito.when(snapshotInfoMock.getSnapshotId()).thenReturn(1L); - Mockito.when(snapshotInfoMock.getDataStore().getDriver()).thenReturn(storeDriver); - Mockito.when(snapshotInfoMock.getDataStore().getDriver().getCapabilities()).thenReturn(new HashMap<>()); snapshotHelperSpy.expungeTemporarySnapshot(false, snapshotInfoMock); Mockito.verifyNoInteractions(snapshotServiceMock, snapshotDataStoreDaoMock); } @@ -122,8 +119,6 @@ public void validateExpungeTemporarySnapshotKvmSnapshotOnPrimaryStorageExpungesS Mockito.when(store.getRole()).thenReturn(DataStoreRole.Image); Mockito.when(store.getId()).thenReturn(1L); Mockito.when(snapshotInfoMock.getDataStore()).thenReturn(store); - Mockito.when(snapshotInfoMock.getDataStore().getDriver()).thenReturn(storeDriver); - Mockito.when(snapshotInfoMock.getDataStore().getDriver().getCapabilities()).thenReturn(new HashMap<>()); snapshotHelperSpy.expungeTemporarySnapshot(true, snapshotInfoMock); } diff --git a/ui/src/views/storage/FormSchedule.vue b/ui/src/views/storage/FormSchedule.vue index d8d14cb231e0..e5c99e3b98c3 100644 --- a/ui/src/views/storage/FormSchedule.vue +++ b/ui/src/views/storage/FormSchedule.vue @@ -359,7 +359,7 @@ export default { const listStoragePools = json.liststoragepoolsresponse.storagepool if (listStoragePools) { this.storagePools = listStoragePools - this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES && pool.zoneid !== this.resource.zoneid) + this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE && pool.zoneid !== this.resource.zoneid) } }).finally(() => { this.storagePoolsLoading = false diff --git a/ui/src/views/storage/SnapshotZones.vue b/ui/src/views/storage/SnapshotZones.vue index aa869d4176f5..d0fd56517554 100644 --- a/ui/src/views/storage/SnapshotZones.vue +++ b/ui/src/views/storage/SnapshotZones.vue @@ -532,7 +532,7 @@ export default { const listStoragePools = json.liststoragepoolsresponse.storagepool if (listStoragePools) { this.storagePools = listStoragePools - this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES && pool.zoneid !== this.resource.zoneid) + this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE && pool.zoneid !== this.resource.zoneid) } }).finally(() => { this.storagePoolsLoading = false diff --git a/ui/src/views/storage/TakeSnapshot.vue b/ui/src/views/storage/TakeSnapshot.vue index dd4fa482e64b..63fa0c418bc5 100644 --- a/ui/src/views/storage/TakeSnapshot.vue +++ b/ui/src/views/storage/TakeSnapshot.vue @@ -240,7 +240,7 @@ export default { const listStoragePools = json.liststoragepoolsresponse.storagepool if (listStoragePools) { this.storagePools = listStoragePools - this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES && pool.zoneid !== this.resource.zoneid) + this.storagePools = this.storagePools.filter(pool => pool.storagecapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE && pool.zoneid !== this.resource.zoneid) } }).finally(() => { this.storagePoolsLoading = false From 8496965fdccdcaa453e7dd111334ee534b5fd314 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Mon, 16 Sep 2024 17:23:52 +0300 Subject: [PATCH 05/15] fix snapshot deletion --- .../snapshot/StorPoolSnapshotStrategy.java | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java index 1d3c0711253e..82301255c1f2 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java @@ -37,6 +37,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.State; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; @@ -157,35 +158,49 @@ public boolean deleteSnapshot(Long snapshotId, Long zoneId) { private boolean deleteSnapshot(Long snapshotId, Long zoneId, SnapshotVO snapshotVO, String name, StoragePoolVO storage) { - boolean res; + boolean res = false; SpConnectionDesc conn = StorPoolUtil.getSpConnection(storage.getUuid(), storage.getId(), storagePoolDetailsDao, _primaryDataStoreDao); SpApiResponse resp = StorPoolUtil.snapshotDelete(name, conn); + List snapshotInfos = snapshotDataFactory.getSnapshots(snapshotId, zoneId); + processResult(snapshotInfos, ObjectInDataStoreStateMachine.Event.DestroyRequested); if (resp.getError() != null) { if (resp.getError().getDescr().contains("still exported")) { + processResult(snapshotInfos, Event.OperationFailed); throw new CloudRuntimeException(String.format("The snapshot [%s] was exported to another cluster. [%s]", name, resp.getError())); } final String err = String.format("Failed to clean-up Storpool snapshot %s. Error: %s", name, resp.getError()); StorPoolUtil.spLog(err); if (resp.getError().getName().equals("objectDoesNotExist")) { - markSnapshotAsDestroyedIfAlreadyRemoved(snapshotId, storage.getId()); + return true; } - res = false; } else { - markSnapshotAsDestroyedIfAlreadyRemoved(snapshotId, storage.getId()); res = deleteSnapshotFromDbIfNeeded(snapshotVO, zoneId); StorPoolUtil.spLog("StorpoolSnapshotStrategy.deleteSnapshot: executed successfully=%s, snapshot uuid=%s, name=%s", res, snapshotVO.getUuid(), name); } + if (res) { + processResult(snapshotInfos, Event.OperationSuccessed); + cleanUpDestroyedRecords(snapshotId); + } else { + processResult(snapshotInfos, Event.OperationFailed); + } return res; } - private void markSnapshotAsDestroyedIfAlreadyRemoved(Long snapshotId, Long storeId) { - SnapshotDataStoreVO snapshotOnPrimary = _snapshotStoreDao.findByStoreSnapshot(DataStoreRole.Primary, storeId, snapshotId); - if (snapshotOnPrimary != null) { - snapshotOnPrimary.setState(State.Destroyed); - _snapshotStoreDao.update(snapshotOnPrimary.getId(), snapshotOnPrimary); + private void cleanUpDestroyedRecords(Long snapshotId) { + List snapshots = _snapshotStoreDao.listBySnapshotId(snapshotId); + for (SnapshotDataStoreVO snapshot : snapshots) { + if (snapshot.getInstallPath().contains("/dev/storpool-byid") && State.Destroyed.equals(snapshot.getState())) { + _snapshotStoreDao.remove(snapshot.getId()); + } } } + private void processResult(List snapshotInfos, ObjectInDataStoreStateMachine.Event event) { + for (SnapshotInfo snapshot : snapshotInfos) { + SnapshotObject snapshotObject = (SnapshotObject) snapshot; + snapshotObject.processEvent(event); + } + } @Override public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) { logger.debug("StorpoolSnapshotStrategy.canHandle: snapshot {}, op={}", snapshot, op); @@ -198,9 +213,15 @@ public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperat return StrategyPriority.CANT_HANDLE; } List snapshots = snapshotJoinDao.listBySnapshotIdAndZoneId(zoneId, snapshot.getId()); - boolean snapshotNotOnStorPool = snapshots.stream().filter(s -> s.getStoreRole().equals(DataStoreRole.Primary)).count() == 0; + boolean snapshotNotOnStorPool = snapshots.stream().filter(s -> DataStoreRole.Primary.equals(s.getStoreRole())).count() == 0; if (snapshotNotOnStorPool) { + for (SnapshotJoinVO snapshotOnStore : snapshots) { + SnapshotDataStoreVO snap = _snapshotStoreDao.findOneBySnapshotAndDatastoreRole(snapshot.getId(), DataStoreRole.Image); + if (snap != null && snap.getInstallPath() != null && snap.getInstallPath().startsWith(StorPoolUtil.SP_DEV_PATH)) { + return StrategyPriority.HIGHEST; + } + } return StrategyPriority.CANT_HANDLE; } for (StoragePoolVO pool : pools) { From f720b474d4d1f8bd2d37b5f7f0b425a35af1e0eb Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Mon, 9 Dec 2024 14:09:30 +0200 Subject: [PATCH 06/15] Address comments and fix conflicts --- .../api/storage/DataStoreCapabilities.java | 6 +- .../orchestration/VolumeOrchestrator.java | 10 +-- .../db/SnapshotDataStoreDaoImpl.java | 3 + .../kvm/storage/StorPoolStorageAdaptor.java | 3 + .../StorPoolAbandonObjectsCollector.java | 5 ++ .../StorPoolPrimaryDataStoreDriver.java | 47 ++++++---- .../datastore/util/StorPoolHelper.java | 2 + .../motion/StorPoolDataMotionStrategy.java | 4 + .../snapshot/StorPoolSnapshotStrategy.java | 89 ++++++++++++------- .../cloud/api/query/dao/SnapshotJoinDao.java | 2 +- .../api/query/dao/SnapshotJoinDaoImpl.java | 30 +++---- .../cloud/storage/VolumeApiServiceImpl.java | 16 ++-- .../storage/snapshot/SnapshotManagerImpl.java | 49 ++++++---- .../cloud/template/TemplateManagerImpl.java | 23 +++-- .../cloudstack/snapshot/SnapshotHelper.java | 12 ++- .../snapshot/SnapshotManagerImplTest.java | 3 + .../storage/snapshot/SnapshotManagerTest.java | 3 + 17 files changed, 191 insertions(+), 116 deletions(-) diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java index b221a90c0b32..2494cc7c5fcb 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreCapabilities.java @@ -44,5 +44,9 @@ public enum DataStoreCapabilities { /** * indicates that the driver supports copying snapshot between zones on pools of the same type */ - CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE + CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE, + /** + * indicates that this driver supports the option to create a template from the back-end snapshot + */ + CAN_CREATE_TEMPLATE_FROM_SNAPSHOT } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index d5bfb279b792..1ce46448dc7d 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -583,14 +583,10 @@ public VolumeInfo createVolumeFromSnapshot(Volume volume, Snapshot snapshot, Use SnapshotInfo snapInfo = snapshotFactory.getSnapshotWithRoleAndZone(snapshot.getId(), dataStoreRole, zoneId); boolean kvmSnapshotOnlyInPrimaryStorage = snapshotHelper.isKvmSnapshotOnlyInPrimaryStorage(snapshot, dataStoreRole, volume.getDataCenterId()); - boolean skipCopyToSecondary = false; - boolean keepOnPrimary = snapshotHelper.isStorPoolStorage(snapInfo); + boolean keepOnPrimary = snapshotHelper.isStorageSupportSnapshotToTemplate(snapInfo); - if (kvmSnapshotOnlyInPrimaryStorage && keepOnPrimary) { - skipCopyToSecondary = true; - } try { - if (!skipCopyToSecondary) { + if (!keepOnPrimary) { snapInfo = snapshotHelper.backupSnapshotToSecondaryStorageIfNotExists(snapInfo, dataStoreRole, snapshot, kvmSnapshotOnlyInPrimaryStorage); } } catch (CloudRuntimeException e) { @@ -604,7 +600,7 @@ public VolumeInfo createVolumeFromSnapshot(Volume volume, Snapshot snapshot, Use } // don't try to perform a sync if the DataStoreRole of the snapshot is equal to DataStoreRole.Primary - if (!DataStoreRole.Primary.equals(dataStoreRole) || (kvmSnapshotOnlyInPrimaryStorage && !skipCopyToSecondary)) { + if (!DataStoreRole.Primary.equals(dataStoreRole) || !keepOnPrimary) { try { // sync snapshot to region store if necessary DataStore snapStore = snapInfo.getDataStore(); 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 7fb4d8d1c705..2b440f348b9b 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 @@ -28,11 +28,14 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.UpdateBuilder; + import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.State; + import org.apache.commons.collections.CollectionUtils; + import org.springframework.stereotype.Component; import javax.inject.Inject; diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java index fdccd76b709c..4dcc67f3f06b 100644 --- a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java @@ -25,10 +25,12 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.OutputInterpreter; import com.cloud.utils.script.Script; + import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; + import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; @@ -49,6 +51,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; + import java.util.UUID; public class StorPoolStorageAdaptor implements StorageAdaptor { diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java index ca9e3c01a368..84abf57ca946 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java @@ -20,8 +20,10 @@ package org.apache.cloudstack.storage.collector; import com.cloud.dc.dao.ClusterDao; + import com.cloud.storage.dao.SnapshotDetailsDao; import com.cloud.storage.dao.SnapshotDetailsVO; + import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.db.DB; @@ -29,8 +31,10 @@ import com.cloud.utils.db.TransactionCallbackNoReturn; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.TransactionStatus; + import com.google.gson.JsonArray; import com.google.gson.JsonObject; + import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.managed.context.ManagedContextRunnable; @@ -41,6 +45,7 @@ import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; + import org.apache.commons.collections.CollectionUtils; import javax.inject.Inject; diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java index 233ed0d5193a..df719ccff26e 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java @@ -19,12 +19,25 @@ package org.apache.cloudstack.storage.datastore.driver; import com.cloud.storage.dao.SnapshotDetailsVO; + import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; +import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.storage.dao.VMTemplateDetailsDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.tags.dao.ResourceTagDao; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine.State; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.VMInstanceDao; + import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; @@ -69,6 +82,7 @@ import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.storage.volume.VolumeObject; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; @@ -113,17 +127,6 @@ import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.SnapshotDetailsDao; -import com.cloud.storage.dao.StoragePoolHostDao; -import com.cloud.storage.dao.VMTemplateDetailsDao; -import com.cloud.storage.dao.VolumeDao; -import com.cloud.storage.dao.VolumeDetailsDao; -import com.cloud.tags.dao.ResourceTagDao; -import com.cloud.utils.Pair; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.VirtualMachine.State; -import com.cloud.vm.VirtualMachineManager; -import com.cloud.vm.dao.VMInstanceDao; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -191,6 +194,7 @@ private SnapshotDataStoreVO getSnapshotImageStoreRef(long snapshotId, long zoneI public Map getCapabilities() { Map mapCapabilities = new HashMap<>(); mapCapabilities.put(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE.toString(), Boolean.TRUE.toString()); + mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_TEMPLATE_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString()); return mapCapabilities; } @@ -525,14 +529,7 @@ public void deleteAsync(DataStore dataStore, DataObject data, AsyncCompletionCal err = String.format("Could not delete volume due to %s", e.getMessage()); } } else if (data.getType() == DataObjectType.SNAPSHOT) { - SnapshotInfo snapshot = (SnapshotInfo) data; - SpConnectionDesc conn = StorPoolUtil.getSpConnection(snapshot.getDataStore().getUuid(), snapshot.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); - String name = StorPoolStorageAdaptor.getVolumeNameFromPath(snapshot.getPath(), true); - SpApiResponse resp = StorPoolUtil.snapshotDelete(name, conn); - if (resp.getError() != null) { - err = String.format("Failed to clean-up Storpool snapshot %s. Error: %s", name, resp.getError()); - StorPoolUtil.spLog(err); - } + err = deleteSnapshot((SnapshotInfo) data, err); } else { err = String.format("Invalid DataObjectType \"%s\" passed to deleteAsync", data.getType()); } @@ -547,6 +544,18 @@ public void deleteAsync(DataStore dataStore, DataObject data, AsyncCompletionCal callback.complete(res); } + private String deleteSnapshot(SnapshotInfo data, String err) { + SnapshotInfo snapshot = data; + SpConnectionDesc conn = StorPoolUtil.getSpConnection(snapshot.getDataStore().getUuid(), snapshot.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + String name = StorPoolStorageAdaptor.getVolumeNameFromPath(snapshot.getPath(), true); + SpApiResponse resp = StorPoolUtil.snapshotDelete(name, conn); + if (resp.getError() != null) { + err = String.format("Failed to clean-up Storpool snapshot %s. Error: %s", name, resp.getError()); + StorPoolUtil.spLog(err); + } + return err; + } + private void tryToSnapshotVolumeBeforeDelete(VolumeInfo vinfo, DataStore dataStore, String name, SpConnectionDesc conn) { Integer deleteAfter = StorPoolConfigurationManager.DeleteAfterInterval.valueIn(dataStore.getId()); if (deleteAfter != null && deleteAfter > 0 && vinfo.getPassphraseId() == null) { diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java index e101df148a37..685b99e12d59 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java @@ -45,6 +45,7 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; + import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.impl.ConfigurationVO; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; @@ -56,6 +57,7 @@ import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; import org.apache.cloudstack.storage.snapshot.StorPoolConfigurationManager; import org.apache.cloudstack.storage.to.VolumeObjectTO; + import org.apache.commons.collections4.CollectionUtils; import java.sql.PreparedStatement; diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java index c69ff5cc0f62..f260c5669869 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java @@ -55,6 +55,7 @@ import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.VMInstanceDao; + import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionStrategy; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; @@ -86,9 +87,12 @@ import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; + import org.apache.commons.collections.MapUtils; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; + import org.springframework.stereotype.Component; import javax.inject.Inject; diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java index 82301255c1f2..650b73d5b080 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java @@ -63,6 +63,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; + +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; import javax.inject.Inject; @@ -201,6 +203,7 @@ private void processResult(List snapshotInfos, ObjectInDataStoreSt snapshotObject.processEvent(event); } } + @Override public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) { logger.debug("StorpoolSnapshotStrategy.canHandle: snapshot {}, op={}", snapshot, op); @@ -414,38 +417,25 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp SnapshotInfo srcSnapshot = (SnapshotInfo) snapshot; SnapshotInfo destSnapshot = (SnapshotInfo) snapshotDest; String err = null; + String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(srcSnapshot.getPath(), false); if (location != null) { - SpConnectionDesc connectionLocal = StorPoolUtil.getSpConnection(snapshot.getDataStore().getUuid(), - snapshot.getDataStore().getId(), storagePoolDetailsDao, _primaryDataStoreDao); - String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(srcSnapshot.getPath(), false); - Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId("~" + snapshotName, connectionLocal), clusterDao); - connectionLocal = StorPoolHelper.getSpConnectionDesc(connectionLocal, clusterId); - SpApiResponse resp = StorPoolUtil.snapshotExport("~" + snapshotName, location, connectionLocal); + SpApiResponse resp = exportSnapshot(snapshot, location, snapshotName); if (resp.getError() != null) { StorPoolUtil.spLog("Failed to export snapshot %s from %s due to %s", snapshotName, location, resp.getError()); err = String.format("Failed to export snapshot %s from %s due to %s", snapshotName, location, resp.getError()); - res = new CreateCmdResult(destSnapshot.getPath(), null); - res.setResult(err); - callback.complete(res); + completeCallback(callback, res, destSnapshot.getPath(), err); return; } - String detail = "~" + snapshotName + ";" + location; - SnapshotDetailsVO snapshotForRecovery = new SnapshotDetailsVO(snapshot.getId(), StorPoolUtil.SP_RECOVERED_SNAPSHOT, detail, true); - _snapshotDetailsDao.persist(snapshotForRecovery); + keepExportedSnapshot(snapshot, location, snapshotName); + SpConnectionDesc connectionRemote = StorPoolUtil.getSpConnection(storagePoolVO.getUuid(), storagePoolVO.getId(), storagePoolDetailsDao, _primaryDataStoreDao); - String localLocation = StorPoolConfigurationManager.StorPoolClusterLocation - .valueIn(snapshot.getDataStore().getId()); - StoragePoolDetailVO template = storagePoolDetailsDao.findDetail(storagePoolVO.getId(), - StorPoolUtil.SP_TEMPLATE); - SpApiResponse respFromRemote = StorPoolUtil.snapshotFromRemote(snapshotName, localLocation, - template.getValue(), connectionRemote); + SpApiResponse respFromRemote = copySnapshotFromRemote(snapshot, storagePoolVO, snapshotName, connectionRemote); + if (respFromRemote.getError() != null) { StorPoolUtil.spLog("Failed to copy snapshot %s to %s due to %s", snapshotName, location, respFromRemote.getError()); err = String.format("Failed to copy snapshot %s to %s due to %s", snapshotName, location, respFromRemote.getError()); - res = new CreateCmdResult(destSnapshot.getPath(), null); - res.setResult(err); - callback.complete(res); + completeCallback(callback, res, destSnapshot.getPath(), err); return; } StorPoolUtil.spLog("The snapshot [%s] was copied from remote", snapshotName); @@ -454,26 +444,59 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp if (respFromRemote.getError() != null) { StorPoolUtil.spLog("Failed to reconcile snapshot %s from %s due to %s", snapshotName, location, respFromRemote.getError()); err = String.format("Failed to reconcile snapshot %s from %s due to %s", snapshotName, location, respFromRemote.getError()); - res = new CreateCmdResult(destSnapshot.getPath(), null); - res.setResult(err); - callback.complete(res); + completeCallback(callback, res, destSnapshot.getPath(), err); return; } - SnapshotDataStoreVO snapshotStore = _snapshotStoreDao.findByStoreSnapshot(DataStoreRole.Primary, snapshotDest.getDataStore().getId(), destSnapshot.getSnapshotId()); - snapshotStore.setInstallPath(srcSnapshot.getPath()); - _snapshotStoreDao.update(snapshotStore.getId(), snapshotStore); - + updateSnapshotPath(snapshotDest, srcSnapshot, destSnapshot); } else { - res = new CreateCmdResult(destSnapshot.getPath(), null); - res.setResult("The snapshot is not in the right location"); - callback.complete(res); - return; + completeCallback(callback, res, destSnapshot.getPath(), "The snapshot is not in the right location"); } SnapshotObjectTO snap = (SnapshotObjectTO) snapshotDest.getTO(); snap.setPath(srcSnapshot.getPath()); CreateObjectAnswer answer = new CreateObjectAnswer(snap); - res = new CreateCmdResult(destSnapshot.getPath(), answer); + completeCallback(callback, res, destSnapshot.getPath(), err); + } + + private void completeCallback(AsyncCompletionCallback callback, CreateCmdResult res, String snapshotPath, String err) { + res = new CreateCmdResult(snapshotPath, null); res.setResult(err); callback.complete(res); } + + private void updateSnapshotPath(DataObject snapshotDest, SnapshotInfo srcSnapshot, SnapshotInfo destSnapshot) { + + SnapshotDataStoreVO snapshotStore = _snapshotStoreDao.findByStoreSnapshot(DataStoreRole.Primary, snapshotDest.getDataStore().getId(), destSnapshot.getSnapshotId()); + snapshotStore.setInstallPath(srcSnapshot.getPath()); + _snapshotStoreDao.update(snapshotStore.getId(), snapshotStore); + } + + @NotNull + private SpApiResponse copySnapshotFromRemote(DataObject snapshot, StoragePoolVO storagePoolVO, String snapshotName, SpConnectionDesc connectionRemote) { + + String localLocation = StorPoolConfigurationManager.StorPoolClusterLocation + .valueIn(snapshot.getDataStore().getId()); + StoragePoolDetailVO template = storagePoolDetailsDao.findDetail(storagePoolVO.getId(), + StorPoolUtil.SP_TEMPLATE); + SpApiResponse respFromRemote = StorPoolUtil.snapshotFromRemote(snapshotName, localLocation, + template.getValue(), connectionRemote); + return respFromRemote; + } + + private void keepExportedSnapshot(DataObject snapshot, String location, String snapshotName) { + + String detail = "~" + snapshotName + ";" + location; + SnapshotDetailsVO snapshotForRecovery = new SnapshotDetailsVO(snapshot.getId(), StorPoolUtil.SP_RECOVERED_SNAPSHOT, detail, true); + _snapshotDetailsDao.persist(snapshotForRecovery); + } + + @NotNull + private SpApiResponse exportSnapshot(DataObject snapshot, String location, String snapshotName) { + + SpConnectionDesc connectionLocal = StorPoolUtil.getSpConnection(snapshot.getDataStore().getUuid(), + snapshot.getDataStore().getId(), storagePoolDetailsDao, _primaryDataStoreDao); + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(StorPoolUtil.getSnapshotClusterId("~" + snapshotName, connectionLocal), clusterDao); + connectionLocal = StorPoolHelper.getSpConnectionDesc(connectionLocal, clusterId); + SpApiResponse resp = StorPoolUtil.snapshotExport("~" + snapshotName, location, connectionLocal); + return resp; + } } diff --git a/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDao.java index 9e19228dbe74..7ca1d7f72f73 100644 --- a/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDao.java @@ -35,5 +35,5 @@ public interface SnapshotJoinDao extends GenericDao { List findByDistinctIds(Long zoneId, Long... ids); - List listBySnapshotIdAndZoneId(Long zoneId, Long id); + List listBySnapshotIdAndZoneId(Long zoneId, Long snapshotId); } diff --git a/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDaoImpl.java index f198d9d81667..9ea14edf2b73 100644 --- a/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/SnapshotJoinDaoImpl.java @@ -26,18 +26,6 @@ import javax.inject.Inject; -import org.apache.cloudstack.annotation.AnnotationService; -import org.apache.cloudstack.annotation.dao.AnnotationDao; -import org.apache.cloudstack.api.ResponseObject; -import org.apache.cloudstack.api.response.SnapshotResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.query.QueryService; - -import org.apache.commons.collections.CollectionUtils; - import com.cloud.api.ApiDBUtils; import com.cloud.api.ApiResponseHelper; import com.cloud.api.query.vo.SnapshotJoinVO; @@ -54,6 +42,18 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.vm.VMInstanceVO; +import org.apache.cloudstack.annotation.AnnotationService; +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.response.SnapshotResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.query.QueryService; + +import org.apache.commons.collections.CollectionUtils; + public class SnapshotJoinDaoImpl extends GenericDaoBaseWithTagInformation implements SnapshotJoinDao { @Inject @@ -300,15 +300,15 @@ public List findByDistinctIds(Long zoneId, Long... ids) { return searchIncludingRemoved(sc, searchFilter, null, false); } - public List listBySnapshotIdAndZoneId(Long zoneId, Long id) { - if (id == null) { + public List listBySnapshotIdAndZoneId(Long zoneId, Long snapshotId) { + if (snapshotId == null) { return new ArrayList<>(); } SearchCriteria sc = snapshotByZoneSearch.create(); if (zoneId != null) { sc.setParameters("zoneId", zoneId); } - sc.setParameters("id", id); + sc.setParameters("id", snapshotId); return listBy(sc); } } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 4843890d62f2..6e47cdf92e17 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -3835,11 +3835,7 @@ private Snapshot takeSnapshotInternal(Long volumeId, Long policyId, Long snapsho } List details = snapshotPolicyDetailsDao.findDetails(policyId, ApiConstants.ZONE_ID); zoneIds = details.stream().map(d -> Long.valueOf(d.getValue())).collect(Collectors.toList()); - if (CollectionUtils.isNotEmpty(poolIds)) { - throw new InvalidParameterValueException(String.format("%s can not be specified for snapshots linked with snapshot policy", ApiConstants.STORAGE_ID_LIST)); - } - List poolDetails = snapshotPolicyDetailsDao.findDetails(policyId, ApiConstants.STORAGE_ID); - poolIds = poolDetails.stream().map(d -> Long.valueOf(d.getValue())).collect(Collectors.toList()); + poolIds = getPoolIdsByPolicy(policyId, poolIds); } if (CollectionUtils.isNotEmpty(zoneIds)) { for (Long destZoneId : zoneIds) { @@ -3926,6 +3922,16 @@ private Snapshot takeSnapshotInternal(Long volumeId, Long policyId, Long snapsho } } + @NotNull + private List getPoolIdsByPolicy(Long policyId, List poolIds) { + if (CollectionUtils.isNotEmpty(poolIds)) { + throw new InvalidParameterValueException(String.format("%s can not be specified for snapshots linked with snapshot policy", ApiConstants.STORAGE_ID_LIST)); + } + List poolDetails = snapshotPolicyDetailsDao.findDetails(policyId, ApiConstants.STORAGE_ID); + poolIds = poolDetails.stream().map(d -> Long.valueOf(d.getValue())).collect(Collectors.toList()); + return poolIds; + } + private Snapshot orchestrateTakeVolumeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, List zoneIds, List poolIds) throws ResourceAllocationException { diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 298ac9516d0d..f5b1d2fddcf8 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.storage.snapshot; + import com.cloud.storage.StoragePoolStatus; import java.util.ArrayList; import java.util.Collections; @@ -2292,27 +2293,14 @@ private boolean canCopyOnPrimary(List poolIds, Snapshot snapshot) { List poolsToBeRemoved = new ArrayList<>(); for (Long poolId : poolIds) { PrimaryDataStore dataStore = (PrimaryDataStore) dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); - if (dataStore == null) { - poolsToBeRemoved.add(poolId); - continue; - } + if (isObjectNull(dataStore == null, poolsToBeRemoved, poolId)) continue; + SnapshotInfo snapshotInfo = snapshotFactory.getSnapshot(snapshot.getId(), poolId, DataStoreRole.Primary); - if (snapshotInfo != null) { - logger.debug(String.format("Snapshot [%s] already exist on pool [%s]", snapshot.getUuid(), dataStore.getName())); - continue; - } + if (isSnapshotExistsOnPool(snapshot, dataStore, snapshotInfo)) continue; VolumeVO volume = _volsDao.findById(snapshot.getVolumeId()); - if (volume == null) { - poolsToBeRemoved.add(poolId); - continue; - } - if (!dataStore.getDriver().getCapabilities() - .containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE.toString()) - && dataStore.getPoolType() != volume.getPoolType()) { - poolsToBeRemoved.add(poolId); - logger.debug(String.format("The %s does not support copy to %s between zones", dataStore.getPoolType(), volume.getPoolType())); - } + if (isObjectNull(volume == null, poolsToBeRemoved, poolId)) continue; + doesStorageSupportCopySnapshot(poolsToBeRemoved, poolId, dataStore, volume); } poolIds.removeAll(poolsToBeRemoved); if (CollectionUtils.isEmpty(poolIds)) { @@ -2321,6 +2309,31 @@ private boolean canCopyOnPrimary(List poolIds, Snapshot snapshot) { return true; } + private void doesStorageSupportCopySnapshot(List poolsToBeRemoved, Long poolId, PrimaryDataStore dataStore, VolumeVO volume) { + if (!dataStore.getDriver().getCapabilities() + .containsKey(DataStoreCapabilities.CAN_COPY_SNAPSHOT_BETWEEN_ZONES_AND_SAME_POOL_TYPE.toString()) + && dataStore.getPoolType() != volume.getPoolType()) { + poolsToBeRemoved.add(poolId); + logger.debug(String.format("The %s does not support copy to %s between zones", dataStore.getPoolType(), volume.getPoolType())); + } + } + + private boolean isSnapshotExistsOnPool(Snapshot snapshot, PrimaryDataStore dataStore, SnapshotInfo snapshotInfo) { + if (snapshotInfo != null) { + logger.debug(String.format("Snapshot [%s] already exist on pool [%s]", snapshot.getUuid(), dataStore.getName())); + return true; + } + return false; + } + + private static boolean isObjectNull(boolean object, List poolsToBeRemoved, Long poolId) { + if (object) { + poolsToBeRemoved.add(poolId); + return true; + } + return false; + } + private void copySnapshotToPrimaryDifferentZone(List poolIds, SnapshotVO snapshot) { VolumeInfo volume = volFactory.getVolume(snapshot.getVolumeId()); if (volume == null) { diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 25fad638124c..9d9ec4a7c5c3 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -1702,19 +1702,8 @@ public VirtualMachineTemplate createPrivateTemplate(CreateTemplateCmd command) t boolean kvmIncrementalSnapshot = SnapshotManager.kvmIncrementalSnapshot.valueIn(_hostDao.findClusterIdByVolumeInfo(snapInfo.getBaseVolume())); boolean skipCopyToSecondary = false; - boolean keepOnPrimary = snapshotHelper.isStorPoolStorage(snapInfo); - if (kvmSnapshotOnlyInPrimaryStorage && keepOnPrimary) { - skipCopyToSecondary = true; - } - if (dataStoreRole == DataStoreRole.Image || !skipCopyToSecondary) { - snapInfo = snapshotHelper.backupSnapshotToSecondaryStorageIfNotExists(snapInfo, dataStoreRole, snapshot, kvmSnapshotOnlyInPrimaryStorage); - _accountMgr.checkAccess(caller, null, true, snapInfo); - DataStore snapStore = snapInfo.getDataStore(); - - if (snapStore != null) { - store = snapStore; // pick snapshot image store to create template - } - } else if (keepOnPrimary) { + boolean keepOnPrimary = snapshotHelper.isStorageSupportSnapshotToTemplate(snapInfo); + if (keepOnPrimary) { ImageStoreVO imageStore = _imgStoreDao.findOneByZoneAndProtocol(zoneId, "nfs"); if (imageStore == null) { throw new CloudRuntimeException(String.format("Could not find an NFS secondary storage pool on zone %s to use as a temporary location " + @@ -1724,6 +1713,14 @@ public VirtualMachineTemplate createPrivateTemplate(CreateTemplateCmd command) t if (dataStore != null) { store = dataStore; } + } else if (dataStoreRole == DataStoreRole.Image) { + snapInfo = snapshotHelper.backupSnapshotToSecondaryStorageIfNotExists(snapInfo, dataStoreRole, snapshot, kvmSnapshotOnlyInPrimaryStorage); + _accountMgr.checkAccess(caller, null, true, snapInfo); + DataStore snapStore = snapInfo.getDataStore(); + + if (snapStore != null) { + store = snapStore; // pick snapshot image store to create template + } } if (kvmIncrementalSnapshot && DataStoreRole.Image.equals(dataStoreRole)) { snapInfo = snapshotHelper.convertSnapshotIfNeeded(snapInfo); diff --git a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java index ca6bbbd1dddb..b1a5edf24337 100644 --- a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java +++ b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java @@ -30,6 +30,7 @@ import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.SnapshotDao; import com.cloud.utils.exception.CloudRuntimeException; + import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; @@ -42,11 +43,14 @@ import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; + import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; + import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -100,7 +104,7 @@ public void expungeTemporarySnapshot(boolean kvmSnapshotOnlyInPrimaryStorage, Sn long storeId = snapInfo.getDataStore().getId(); long zoneId = dataStorageManager.getStoreZoneId(storeId, snapInfo.getDataStore().getRole()); - if (isStorPoolStorage(snapInfo)) { + if (isStorageSupportSnapshotToTemplate(snapInfo)) { logger.debug("The primary storage does not delete the snapshots even if there is a backup on secondary"); return; } @@ -146,10 +150,10 @@ public void expungeTemporarySnapshot(boolean kvmSnapshotOnlyInPrimaryStorage, Sn snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(snapInfo.getId(), storeId, DataStoreRole.Image); } - public boolean isStorPoolStorage(SnapshotInfo snapInfo) { + public boolean isStorageSupportSnapshotToTemplate(SnapshotInfo snapInfo) { if (DataStoreRole.Primary.equals(snapInfo.getDataStore().getRole())) { - StoragePoolVO storagePoolVO = primaryDataStoreDao.findById(snapInfo.getDataStore().getId()); - return storagePoolVO != null && StoragePoolType.StorPool.equals(storagePoolVO.getPoolType()); + Map capabilities = snapInfo.getDataStore().getDriver().getCapabilities(); + return org.apache.commons.collections4.MapUtils.isNotEmpty(capabilities) && capabilities.containsKey(DataStoreCapabilities.CAN_CREATE_TEMPLATE_FROM_SNAPSHOT.toString()); } return false; } diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java index a74c41b4257f..f178c6b8912f 100644 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java @@ -38,6 +38,7 @@ import com.cloud.user.ResourceLimitService; import com.cloud.user.dao.AccountDao; import com.cloud.utils.Pair; + import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; @@ -48,9 +49,11 @@ import org.apache.cloudstack.framework.async.AsyncCallFuture; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; + import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; + import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java index 952e77e10dc3..4d8023199351 100755 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java @@ -56,6 +56,7 @@ import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; + import org.apache.cloudstack.api.command.user.snapshot.ExtractSnapshotCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -76,11 +77,13 @@ import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; + import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; + import org.mockito.BDDMockito; import org.mockito.InjectMocks; import org.mockito.Mock; From e06de9394b9eec8c20418f26a70d3ab7572f3442 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Fri, 17 Jan 2025 14:54:05 +0200 Subject: [PATCH 07/15] small rearranges --- .../snapshot/StorPoolSnapshotStrategy.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java index 650b73d5b080..7328793d9a10 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java @@ -46,7 +46,6 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; -import org.apache.cloudstack.storage.command.CreateObjectAnswer; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; @@ -199,8 +198,10 @@ private void cleanUpDestroyedRecords(Long snapshotId) { private void processResult(List snapshotInfos, ObjectInDataStoreStateMachine.Event event) { for (SnapshotInfo snapshot : snapshotInfos) { - SnapshotObject snapshotObject = (SnapshotObject) snapshot; + SnapshotObject snapshotObject = (SnapshotObject) snapshot; + if (DataStoreRole.Primary.equals(snapshotObject.getDataStore().getRole())) { snapshotObject.processEvent(event); + } } } @@ -363,7 +364,7 @@ private boolean deleteSnapshotFromDbIfNeeded(SnapshotVO snapshotVO, Long zoneId) !Snapshot.State.Destroying.equals(snapshotVO.getState())) { throw new InvalidParameterValueException(String.format("Can't delete snapshot %s due to it is in %s Status", snapshotVO, snapshotVO.getState())); } - List storeRefs = _snapshotStoreDao.listReadyBySnapshot(snapshotId, DataStoreRole.Image); + List storeRefs = _snapshotStoreDao.listBySnapshot(snapshotId, DataStoreRole.Image); if (zoneId != null) { storeRefs.removeIf(ref -> !zoneId.equals(dataStoreMgr.getStoreZoneId(ref.getDataStoreId(), ref.getRole()))); } @@ -413,7 +414,6 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp StoragePoolVO storagePoolVO = _primaryDataStoreDao.findById(snapshotDest.getDataStore().getId()); String location = StorPoolConfigurationManager.StorPoolClusterLocation.valueIn(snapshotDest.getDataStore().getId()); StorPoolUtil.spLog("StorpoolSnapshotStrategy.copySnapshot: snapshot %s to pool=%s", snapshot.getUuid(), storagePoolVO.getName()); - CreateCmdResult res = null; SnapshotInfo srcSnapshot = (SnapshotInfo) snapshot; SnapshotInfo destSnapshot = (SnapshotInfo) snapshotDest; String err = null; @@ -421,9 +421,9 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp if (location != null) { SpApiResponse resp = exportSnapshot(snapshot, location, snapshotName); if (resp.getError() != null) { - StorPoolUtil.spLog("Failed to export snapshot %s from %s due to %s", snapshotName, location, resp.getError()); - err = String.format("Failed to export snapshot %s from %s due to %s", snapshotName, location, resp.getError()); - completeCallback(callback, res, destSnapshot.getPath(), err); + err = String.format("Failed to export snapshot [{}] from [{}] due to [{}]", snapshotName, location, resp.getError()); + StorPoolUtil.spLog(err); + completeCallback(callback, destSnapshot.getPath(), err); return; } keepExportedSnapshot(snapshot, location, snapshotName); @@ -433,32 +433,31 @@ public void copySnapshot(DataObject snapshot, DataObject snapshotDest, AsyncComp SpApiResponse respFromRemote = copySnapshotFromRemote(snapshot, storagePoolVO, snapshotName, connectionRemote); if (respFromRemote.getError() != null) { - StorPoolUtil.spLog("Failed to copy snapshot %s to %s due to %s", snapshotName, location, respFromRemote.getError()); - err = String.format("Failed to copy snapshot %s to %s due to %s", snapshotName, location, respFromRemote.getError()); - completeCallback(callback, res, destSnapshot.getPath(), err); + err = String.format("Failed to copy snapshot [{}] to [{}] due to [{}]", snapshotName, location, respFromRemote.getError()); + StorPoolUtil.spLog(err); + completeCallback(callback, destSnapshot.getPath(), err); return; } StorPoolUtil.spLog("The snapshot [%s] was copied from remote", snapshotName); respFromRemote = StorPoolUtil.snapshotReconcile("~" + snapshotName, connectionRemote); if (respFromRemote.getError() != null) { - StorPoolUtil.spLog("Failed to reconcile snapshot %s from %s due to %s", snapshotName, location, respFromRemote.getError()); - err = String.format("Failed to reconcile snapshot %s from %s due to %s", snapshotName, location, respFromRemote.getError()); - completeCallback(callback, res, destSnapshot.getPath(), err); + err = String.format("Failed to reconcile snapshot [{}] from [{}] due to [{}]", snapshotName, location, respFromRemote.getError()); + StorPoolUtil.spLog(err); + completeCallback(callback, destSnapshot.getPath(), err); return; } updateSnapshotPath(snapshotDest, srcSnapshot, destSnapshot); } else { - completeCallback(callback, res, destSnapshot.getPath(), "The snapshot is not in the right location"); + completeCallback(callback, destSnapshot.getPath(), "The snapshot is not in the right location"); } SnapshotObjectTO snap = (SnapshotObjectTO) snapshotDest.getTO(); snap.setPath(srcSnapshot.getPath()); - CreateObjectAnswer answer = new CreateObjectAnswer(snap); - completeCallback(callback, res, destSnapshot.getPath(), err); + completeCallback(callback, destSnapshot.getPath(), err); } - private void completeCallback(AsyncCompletionCallback callback, CreateCmdResult res, String snapshotPath, String err) { - res = new CreateCmdResult(snapshotPath, null); + private void completeCallback(AsyncCompletionCallback callback, String snapshotPath, String err) { + CreateCmdResult res = new CreateCmdResult(snapshotPath, null); res.setResult(err); callback.complete(res); } From bc274204aeb797e464ff8a593c1e900634f4d2ef Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Wed, 5 Feb 2025 08:48:58 +0200 Subject: [PATCH 08/15] removed unused imports --- server/src/main/java/com/cloud/api/query/QueryManagerImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 63c109343056..079c3d45ba2d 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -227,7 +227,6 @@ import com.cloud.api.query.vo.UserAccountJoinVO; import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.api.query.vo.VolumeJoinVO; -import com.cloud.cluster.ManagementServerHostPeerJoinVO; import com.cloud.cluster.ManagementServerHostVO; import com.cloud.cluster.dao.ManagementServerHostDao; import com.cloud.cluster.dao.ManagementServerHostPeerJoinDao; From 2e416afc56197277ef057060add9facb1df1617e Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Thu, 3 Apr 2025 11:30:50 +0300 Subject: [PATCH 09/15] hide the primary storage when a user copies the snapshots --- .../com/cloud/storage/VolumeApiService.java | 4 +-- .../apache/cloudstack/api/ApiConstants.java | 3 ++ .../user/snapshot/CopySnapshotCmd.java | 13 ++++++++- .../user/snapshot/CreateSnapshotCmd.java | 18 ++++++++++-- .../command/test/CreateSnapshotCmdTest.java | 4 +-- .../user/snapshot/CopySnapshotCmdTest.java | 5 ++++ .../datastore/db/PrimaryDataStoreDao.java | 3 ++ .../datastore/db/PrimaryDataStoreDaoImpl.java | 8 +++++ .../snapshot/StorPoolSnapshotStrategy.java | 2 +- .../com/cloud/api/query/QueryManagerImpl.java | 2 ++ .../cloud/storage/VolumeApiServiceImpl.java | 18 ++++++++---- .../storage/snapshot/SnapshotManager.java | 2 ++ .../storage/snapshot/SnapshotManagerImpl.java | 18 +++++++----- .../cloudstack/snapshot/SnapshotHelper.java | 29 +++++++++++++++++++ .../storage/VolumeApiServiceImplTest.java | 4 +-- ... test_snapshot_copy_on_primary_storage.py} | 12 ++++---- tools/marvin/marvin/lib/base.py | 8 +++-- 17 files changed, 120 insertions(+), 33 deletions(-) rename test/integration/plugins/storpool/{TestSnapshotCopyOnPrimary.py => test_snapshot_copy_on_primary_storage.py} (95%) diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index 02515a516a25..9cca5b3223cb 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -113,10 +113,10 @@ public interface VolumeApiService { Volume detachVolumeFromVM(DetachVolumeCmd cmd); - Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, Map tags, List zoneIds, List poolIds) + Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, Map tags, List zoneIds, List poolIds, Boolean useStorageReplication) throws ResourceAllocationException; - Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List zoneIds, List poolIds) throws ResourceAllocationException; + Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List zoneIds, Boolean useStorageReplication) throws ResourceAllocationException; Volume updateVolume(long volumeId, String path, String state, Long storageId, Boolean displayVolume, Boolean deleteProtection, 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 cacf53f3b6f4..f5431845d575 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -536,6 +536,9 @@ public class ApiConstants { public static final String SNAPSHOT_POLICY_ID = "snapshotpolicyid"; public static final String SNAPSHOT_TYPE = "snapshottype"; public static final String SNAPSHOT_QUIESCEVM = "quiescevm"; + + public static final String USE_STORAGE_REPLICATION = "usestoragereplication"; + public static final String SOURCE_CIDR_LIST = "sourcecidrlist"; public static final String SOURCE_ZONE_ID = "sourcezoneid"; public static final String SSL_VERIFICATION = "sslverification"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java index b84fe9067203..033af6be063d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java @@ -90,10 +90,14 @@ public class CopySnapshotCmd extends BaseAsyncCmd implements UserCmd { collectionType = CommandType.UUID, entityType = StoragePoolResponse.class, required = false, + authorized = RoleType.Admin, description = "A comma-separated list of IDs of the storage pools in other zones in which the snapshot will be made available. " + "The snapshot will always be made available in the zone in which the volume is present. Currently supported for StorPool only") protected List storagePoolIds; + @Parameter (name = ApiConstants.USE_STORAGE_REPLICATION, type=CommandType.BOOLEAN, required = false, description = "This parameter enables the option the snapshot to be copied to supported primary storage") + protected Boolean useStorageReplication; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -123,6 +127,13 @@ public List getStoragePoolIds() { return storagePoolIds; } + public Boolean useStorageReplication() { + if (useStorageReplication == null) { + return false; + } + return useStorageReplication; + } + @Override public String getEventType() { return EventTypes.EVENT_SNAPSHOT_COPY; @@ -166,7 +177,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException { try { - if (destZoneId == null && CollectionUtils.isEmpty(destZoneIds) && CollectionUtils.isEmpty(storagePoolIds)) + if (destZoneId == null && CollectionUtils.isEmpty(destZoneIds) && useStorageReplication()) throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Either destzoneid or destzoneids parameters have to be specified."); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java index 078f613d8b74..8cf157649366 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; @@ -104,11 +105,15 @@ public class CreateSnapshotCmd extends BaseAsyncCreateCmd { type=CommandType.LIST, collectionType = CommandType.UUID, entityType = StoragePoolResponse.class, + authorized = RoleType.Admin, description = "A comma-separated list of IDs of the storage pools in other zones in which the snapshot will be made available. " + "The snapshot will always be made available in the zone in which the volume is present.", since = "4.20.0") protected List storagePoolIds; + @Parameter (name = ApiConstants.USE_STORAGE_REPLICATION, type=CommandType.BOOLEAN, required = false, description = "This parameter enables the option the snapshot to be copied to supported primary storage") + protected Boolean useStorageReplication; + private String syncObjectType = BaseAsyncCmd.snapshotHostSyncObject; // /////////////////////////////////////////////////// @@ -175,6 +180,13 @@ public List getStoragePoolIds() { return storagePoolIds; } + public Boolean useStorageReplication() { + if (useStorageReplication == null) { + return false; + } + return useStorageReplication; + } + // /////////////////////////////////////////////////// // ///////////// API Implementation/////////////////// // /////////////////////////////////////////////////// @@ -223,7 +235,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void create() throws ResourceAllocationException { - Snapshot snapshot = _volumeService.allocSnapshot(getVolumeId(), getPolicyId(), getSnapshotName(), getLocationType(), getZoneIds(), getStoragePoolIds()); + Snapshot snapshot = _volumeService.allocSnapshot(getVolumeId(), getPolicyId(), getSnapshotName(), getLocationType(), getZoneIds(), useStorageReplication()); if (snapshot != null) { setEntityId(snapshot.getId()); setEntityUuid(snapshot.getUuid()); @@ -237,7 +249,7 @@ public void execute() { Snapshot snapshot; try { snapshot = - _volumeService.takeSnapshot(getVolumeId(), getPolicyId(), getEntityId(), _accountService.getAccount(getEntityOwnerId()), getQuiescevm(), getLocationType(), getAsyncBackup(), getTags(), getZoneIds(), getStoragePoolIds()); + _volumeService.takeSnapshot(getVolumeId(), getPolicyId(), getEntityId(), _accountService.getAccount(getEntityOwnerId()), getQuiescevm(), getLocationType(), getAsyncBackup(), getTags(), getZoneIds(), getStoragePoolIds(), useStorageReplication()); if (snapshot != null) { SnapshotResponse response = _responseGenerator.createSnapshotResponse(snapshot); @@ -257,7 +269,7 @@ public void execute() { } } - private Snapshot.LocationType getLocationType() { + public Snapshot.LocationType getLocationType() { if (Snapshot.LocationType.values() == null || Snapshot.LocationType.values().length == 0 || locationType == null) { return null; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java index 699919741941..5fa46ec97e52 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java @@ -93,7 +93,7 @@ public void testCreateSuccess() { Snapshot snapshot = Mockito.mock(Snapshot.class); try { Mockito.when(volumeApiService.takeSnapshot(nullable(Long.class), nullable(Long.class), isNull(), - nullable(Account.class), nullable(Boolean.class), nullable(Snapshot.LocationType.class), nullable(Boolean.class), nullable(Map.class), nullable(List.class), nullable(List.class))).thenReturn(snapshot); + nullable(Account.class), nullable(Boolean.class), nullable(Snapshot.LocationType.class), nullable(Boolean.class), nullable(Map.class), nullable(List.class), nullable(List.class), Mockito.anyBoolean())).thenReturn(snapshot); } catch (Exception e) { Assert.fail("Received exception when success expected " + e.getMessage()); @@ -126,7 +126,7 @@ public void testCreateFailure() { try { Mockito.when(volumeApiService.takeSnapshot(nullable(Long.class), nullable(Long.class), nullable(Long.class), - nullable(Account.class), nullable(Boolean.class), nullable(Snapshot.LocationType.class), nullable(Boolean.class), any(), Mockito.anyList(), Mockito.anyList())).thenReturn(null); + nullable(Account.class), nullable(Boolean.class), nullable(Snapshot.LocationType.class), nullable(Boolean.class), any(), Mockito.anyList(), Mockito.anyList(), Mockito.anyBoolean())).thenReturn(null); } catch (Exception e) { Assert.fail("Received exception when success expected " + e.getMessage()); } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmdTest.java index 632496ad2151..db27cc76ec9f 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmdTest.java @@ -87,7 +87,12 @@ public void testGetDestZoneIdWithBothParams() { @Test (expected = ServerApiException.class) public void testExecuteWrongNoParams() { + UUIDManager uuidManager = Mockito.mock(UUIDManager.class); + SnapshotApiService snapshotApiService = Mockito.mock(SnapshotApiService.class); final CopySnapshotCmd cmd = new CopySnapshotCmd(); + cmd._uuidMgr = uuidManager; + cmd._snapshotService = snapshotApiService; + try { cmd.execute(); } catch (ResourceUnavailableException e) { diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDao.java index 7600cdb9b811..37aa70abb6ed 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDao.java @@ -168,4 +168,7 @@ Pair, Integer> searchForIdsAndCount(Long storagePoolId, String storag List listByIds(List ids); List findStoragePoolsByEmptyStorageAccessGroups(Long dcId, Long podId, Long clusterId, ScopeType scope, HypervisorType hypervisorType); + + List findPoolsByStorageTypeAndZone(Storage.StoragePoolType storageType, Long zoneId); + } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDaoImpl.java index 71d5c93f0273..8b230d03154e 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDaoImpl.java @@ -916,6 +916,14 @@ public List listByIds(List ids) { return listBy(sc); } + @Override + public List findPoolsByStorageTypeAndZone(Storage.StoragePoolType storageType, Long zoneId) { + SearchCriteria sc = AllFieldSearch.create(); + sc.setParameters("poolType", storageType); + sc.addAnd("dataCenterId", Op.EQ, zoneId); + return listBy(sc); + } + private SearchCriteria createStoragePoolSearchCriteria(Long storagePoolId, String storagePoolName, Long zoneId, String path, Long podId, Long clusterId, Long hostId, String address, ScopeType scopeType, StoragePoolStatus status, String keyword, String storageAccessGroup) { diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java index 7328793d9a10..1a04f1156d05 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java @@ -364,7 +364,7 @@ private boolean deleteSnapshotFromDbIfNeeded(SnapshotVO snapshotVO, Long zoneId) !Snapshot.State.Destroying.equals(snapshotVO.getState())) { throw new InvalidParameterValueException(String.format("Can't delete snapshot %s due to it is in %s Status", snapshotVO, snapshotVO.getState())); } - List storeRefs = _snapshotStoreDao.listBySnapshot(snapshotId, DataStoreRole.Image); + List storeRefs = _snapshotStoreDao.listBySnapshotAndDataStoreRole(snapshotId, DataStoreRole.Image); if (zoneId != null) { storeRefs.removeIf(ref -> !zoneId.equals(dataStoreMgr.getStoreZoneId(ref.getDataStoreId(), ref.getRole()))); } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 079c3d45ba2d..21fba4766108 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -18,6 +18,8 @@ import static com.cloud.vm.VmDetailConstants.SSH_PUBLIC_KEY; + +import com.cloud.cluster.ManagementServerHostPeerJoinVO; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 6e47cdf92e17..ef56af915764 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -188,6 +188,7 @@ import com.cloud.template.TemplateManager; import com.cloud.user.Account; import com.cloud.user.AccountManager; +import com.cloud.user.AccountService; import com.cloud.user.ResourceLimitService; import com.cloud.user.User; import com.cloud.user.VmDiskStatisticsVO; @@ -371,6 +372,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic public static final String KVM_FILE_BASED_STORAGE_SNAPSHOT = "kvmFileBasedStorageSnapshot"; + public AccountService _accountService; + protected Gson _gson; private static final List SupportedHypervisorsForVolResize = Arrays.asList(HypervisorType.KVM, HypervisorType.XenServer, @@ -3809,9 +3812,10 @@ protected Volume liveMigrateVolume(Volume volume, StoragePool destPool) throws S @Override @ActionEvent(eventType = EventTypes.EVENT_SNAPSHOT_CREATE, eventDescription = "taking snapshot", async = true) public Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, - Snapshot.LocationType locationType, boolean asyncBackup, Map tags, List zoneIds, List poolIds) - throws ResourceAllocationException { - final Snapshot snapshot = takeSnapshotInternal(volumeId, policyId, snapshotId, account, quiescevm, locationType, asyncBackup, zoneIds, poolIds); + Snapshot.LocationType locationType, boolean asyncBackup, Map tags, List zoneIds, List poolIds, Boolean useStorageReplication) + + throws ResourceAllocationException { + final Snapshot snapshot = takeSnapshotInternal(volumeId, policyId, snapshotId, account, quiescevm, locationType, asyncBackup, zoneIds, poolIds, useStorageReplication); if (snapshot != null && MapUtils.isNotEmpty(tags)) { taggedResourceService.createTags(Collections.singletonList(snapshot.getUuid()), ResourceTag.ResourceObjectType.Snapshot, tags, null); } @@ -3819,10 +3823,12 @@ public Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Acco } private Snapshot takeSnapshotInternal(Long volumeId, Long policyId, Long snapshotId, Account account, - boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, List zoneIds, List poolIds) + boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, List zoneIds, List poolIds, Boolean useStorageReplication) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); VolumeInfo volume = volFactory.getVolume(volumeId); + poolIds = snapshotHelper.addStoragePoolsForCopyToPrimary(volume, zoneIds, poolIds, useStorageReplication); + canCopyOnPrimary(poolIds, volume,CollectionUtils.isEmpty(poolIds)); if (volume == null) { throw new InvalidParameterValueException("Creating snapshot failed due to volume:" + volumeId + " doesn't exist"); } @@ -3982,7 +3988,7 @@ private boolean isOperationSupported(VMTemplateVO template, UserVmVO userVm) { @Override @ActionEvent(eventType = EventTypes.EVENT_SNAPSHOT_CREATE, eventDescription = "allocating snapshot", create = true) - public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List zoneIds, List poolIds) throws ResourceAllocationException { + public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List zoneIds, Boolean useStorageReplication) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); VolumeInfo volume = volFactory.getVolume(volumeId); @@ -4031,7 +4037,7 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, if (storagePool == null) { throw new InvalidParameterValueException(String.format("Volume: %s please attach this volume to a VM before create snapshot for it", volume.getVolume())); } - boolean canCopyOnPrimary = canCopyOnPrimary(poolIds, volume, CollectionUtils.isEmpty(poolIds)); + boolean canCopyOnPrimary = useStorageReplication; if (CollectionUtils.isNotEmpty(zoneIds)) { if (policyId != null && policyId > 0) { diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManager.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManager.java index b8ae367bbabd..6e2059e57761 100644 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManager.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManager.java @@ -65,6 +65,8 @@ public interface SnapshotManager extends Configurable { "Whether to show chain size (sum of physical size of snapshot and all its parents) for incremental snapshots in the snapshot response", true, ConfigKey.Scope.Global, null); + public static final ConfigKey UseStorageReplication = new ConfigKey(Boolean.class, "use.storage.replication", "Snapshots", "false", "For snapshot copy to another primary storage in a different zone. Supports only StorPool storage for now", true, ConfigKey.Scope.StoragePool, null); + void deletePoliciesForVolume(Long volumeId); /** diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index f5b1d2fddcf8..10af4fd163e9 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -298,7 +298,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[] {BackupRetryAttempts, BackupRetryInterval, SnapshotHourlyMax, SnapshotDailyMax, SnapshotMonthlyMax, SnapshotWeeklyMax, usageSnapshotSelection, - SnapshotInfo.BackupSnapshotAfterTakingSnapshot, VmStorageSnapshotKvm, kvmIncrementalSnapshot, snapshotDeltaMax, snapshotShowChainSize}; + SnapshotInfo.BackupSnapshotAfterTakingSnapshot, VmStorageSnapshotKvm, kvmIncrementalSnapshot, snapshotDeltaMax, snapshotShowChainSize, UseStorageReplication}; } @Override @@ -2189,12 +2189,12 @@ private List copySnapshotToZones(SnapshotVO snapshotVO, DataStore srcSec return failedZones; } - protected Pair getCheckedSnapshotForCopy(final SnapshotVO snapshot, final List destZoneIds, Long sourceZoneId, boolean canCopyBeteenStoragePools) { + protected Pair getCheckedSnapshotForCopy(final SnapshotVO snapshot, final List destZoneIds, Long sourceZoneId, boolean useStorageReplication) { // Verify snapshot is BackedUp and is on secondary store - if (!Snapshot.State.BackedUp.equals(snapshot.getState()) && !canCopyBeteenStoragePools) { + if (!Snapshot.State.BackedUp.equals(snapshot.getState()) && !useStorageReplication) { throw new InvalidParameterValueException("Snapshot is not backed up"); } - if (snapshot.getLocationType() != null && !Snapshot.LocationType.SECONDARY.equals(snapshot.getLocationType()) && !canCopyBeteenStoragePools) { + if (snapshot.getLocationType() != null && !Snapshot.LocationType.SECONDARY.equals(snapshot.getLocationType()) && !useStorageReplication) { throw new InvalidParameterValueException("Snapshot is not backed up"); } Volume volume = _volsDao.findById(snapshot.getVolumeId()); @@ -2202,9 +2202,7 @@ protected Pair getCheckedSnapshotForCopy(final SnapshotVO snap sourceZoneId = volume.getDataCenterId(); } if (CollectionUtils.isEmpty(destZoneIds)) { - if (!canCopyBeteenStoragePools) { throw new InvalidParameterValueException("Please specify valid destination zone(s)."); - } } else if (destZoneIds.contains(sourceZoneId)) { throw new InvalidParameterValueException("Please specify different source and destination zones."); } @@ -2253,15 +2251,19 @@ public Snapshot copySnapshot(CopySnapshotCmd cmd) throws StorageUnavailableExcep Long sourceZoneId = cmd.getSourceZoneId(); List destZoneIds = cmd.getDestinationZoneIds(); List storagePoolIds = cmd.getStoragePoolIds(); + Boolean useStorageReplication = cmd.useStorageReplication(); Account caller = CallContext.current().getCallingAccount(); SnapshotVO snapshotVO = _snapshotDao.findById(snapshotId); if (snapshotVO == null) { throw new InvalidParameterValueException("Unable to find snapshot with id"); } - boolean canCopyBetweenStoragePools = CollectionUtils.isNotEmpty(storagePoolIds) && canCopyOnPrimary(storagePoolIds, snapshotVO); - Pair snapshotZonePair = getCheckedSnapshotForCopy(snapshotVO, destZoneIds, sourceZoneId, canCopyBetweenStoragePools); + + Pair snapshotZonePair = getCheckedSnapshotForCopy(snapshotVO, destZoneIds, sourceZoneId, useStorageReplication); SnapshotVO snapshot = snapshotZonePair.first(); sourceZoneId = snapshotZonePair.second(); + VolumeInfo volume = volFactory.getVolume(snapshot.getVolumeId()); + storagePoolIds = snapshotHelper.addStoragePoolsForCopyToPrimary(volume, destZoneIds, storagePoolIds, useStorageReplication); + boolean canCopyBetweenStoragePools = CollectionUtils.isNotEmpty(storagePoolIds) && canCopyOnPrimary(storagePoolIds, snapshotVO); Map dataCenterVOs = new HashMap<>(); boolean isRootAdminCaller = _accountMgr.isRootAdmin(caller.getId()); for (Long destZoneId: destZoneIds) { diff --git a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java index b1a5edf24337..17f70d820342 100644 --- a/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java +++ b/server/src/main/java/org/apache/cloudstack/snapshot/SnapshotHelper.java @@ -21,6 +21,7 @@ import com.cloud.api.query.dao.SnapshotJoinDao; import com.cloud.api.query.vo.SnapshotJoinVO; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.storage.DataStoreRole; @@ -29,6 +30,7 @@ import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.snapshot.SnapshotManager; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -39,6 +41,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy; import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; @@ -336,4 +339,30 @@ public void checkIfThereAreMoreThanOnePoolInTheZone(List poolIds) { throw new CloudRuntimeException("Cannot copy the snapshot on multiple storage pools in one zone"); } } + + public List addStoragePoolsForCopyToPrimary(VolumeInfo volume, List destZoneIds, List storagePoolIds, Boolean useStorageReplication) { + if (useStorageReplication) { + if (volume == null) { + throw new InvalidParameterValueException("Could not find volume of a snapshot"); + } + if (CollectionUtils.isEmpty(destZoneIds) && CollectionUtils.isEmpty(storagePoolIds)) { + throw new InvalidParameterValueException("There is no destination zone provided"); + } + if (CollectionUtils.isEmpty(storagePoolIds)) { + storagePoolIds = new ArrayList<>(); + for (Long destZone : destZoneIds) { + List pools = primaryDataStoreDao.findPoolsByStorageTypeAndZone(volume.getStoragePoolType(), destZone); + if (CollectionUtils.isNotEmpty(pools)) { + StoragePoolVO storagePoolVO = pools.stream().filter(pool -> SnapshotManager.UseStorageReplication.valueIn(pool.getId()) == true).findFirst().get(); + storagePoolIds.add(storagePoolVO.getId()); + } + } + if (CollectionUtils.isEmpty(storagePoolIds)) { + throw new InvalidParameterValueException("Cannot copy snapshot to primary storage. There aren't storage pools that support this operation"); + } + } + destZoneIds.clear(); + } + return storagePoolIds; + } } diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 2814a9c52930..91ef1b0b0a16 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -579,7 +579,7 @@ public void testTakeSnapshotF1() throws ResourceAllocationException { when(volumeDataFactoryMock.getVolume(anyLong())).thenReturn(volumeInfoMock); when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); lenient().when(volumeInfoMock.getPoolId()).thenReturn(1L); - volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null, null, null); + volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null, null, null, false); } @Test @@ -592,7 +592,7 @@ public void testTakeSnapshotF2() throws ResourceAllocationException { final TaggedResourceService taggedResourceService = Mockito.mock(TaggedResourceService.class); Mockito.lenient().when(taggedResourceService.createTags(any(), any(), any(), any())).thenReturn(null); ReflectionTestUtils.setField(volumeApiServiceImpl, "taggedResourceService", taggedResourceService); - volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null, null, null); + volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null, null, null, false); } @Test diff --git a/test/integration/plugins/storpool/TestSnapshotCopyOnPrimary.py b/test/integration/plugins/storpool/test_snapshot_copy_on_primary_storage.py similarity index 95% rename from test/integration/plugins/storpool/TestSnapshotCopyOnPrimary.py rename to test/integration/plugins/storpool/test_snapshot_copy_on_primary_storage.py index b01ea2661efa..9d7ded1b4360 100644 --- a/test/integration/plugins/storpool/TestSnapshotCopyOnPrimary.py +++ b/test/integration/plugins/storpool/test_snapshot_copy_on_primary_storage.py @@ -157,7 +157,7 @@ def test_01_take_snapshot_multi_zone(self): """Test to take volume snapshot in multiple StorPool primary storage pools """ - snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, pool_ids =[str(self.storpool_pool.id)]) + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, usestoragereplication=True) self.snapshot_id = snapshot.id self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) time.sleep(420) @@ -172,7 +172,7 @@ def test_02_copy_snapshot_multi_pools(self): snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id) self.snapshot_id = snapshot.id - Snapshot.copy(self.userapiclient, self.snapshot_id, pool_ids=[str(self.storpool_pool.id)], source_zone_id=self.zone.id) + Snapshot.copy(self.userapiclient, self.snapshot_id, source_zone_id=self.zone.id, usestoragereplication=True) self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) time.sleep(420) Snapshot.delete(snapshot, self.userapiclient) @@ -184,7 +184,7 @@ def test_03_take_snapshot_multi_pools_delete_single_zone(self): """Test to take volume snapshot in multiple StorPool storages in diff zones and delete from one zone """ - snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, pool_ids=[str(self.storpool_pool.id)]) + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, usestoragereplication=True) self.snapshot_id = snapshot.id self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) time.sleep(420) @@ -201,7 +201,7 @@ def test_04_copy_snapshot_multi_zone_delete_all(self): snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id) self.snapshot_id = snapshot.id - Snapshot.copy(self.userapiclient, self.snapshot_id, pool_ids=[str(self.storpool_pool.id)], source_zone_id=self.zone.id) + Snapshot.copy(self.userapiclient, self.snapshot_id, source_zone_id=self.zone.id, usestoragereplication=True) self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) time.sleep(420) Snapshot.delete(snapshot, self.userapiclient) @@ -216,7 +216,7 @@ def test_05_take_snapshot_multi_zone_create_volume_additional_zone(self): """Test to take volume snapshot on StorPool in multiple zones and create a volume in one of the additional zones """ - snapshot = Snapshot.create(self.userapiclient,volume_id=self.volume.id, pool_ids=[str(self.storpool_pool.id)]) + snapshot = Snapshot.create(self.userapiclient,volume_id=self.volume.id, usestoragereplication=True) self.snapshot_id = snapshot.id self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) disk_offering_id = None @@ -243,7 +243,7 @@ def test_05_take_snapshot_multi_zone_create_volume_additional_zone(self): def test_06_take_snapshot_multi_zone_create_template_additional_zone(self): """Test to take volume snapshot in multiple StorPool primary storages in diff zones and create a volume in one of the additional zones """ - snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, pool_ids=[str(self.storpool_pool.id)]) + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, usestoragereplication=True) self.snapshot_id = snapshot.id self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) self.template = self.helper.create_snapshot_template(self.userapiclient, self.services, self.snapshot_id, self.additional_zone.id) diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index a053f46638e2..e1424c6821e2 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -1398,7 +1398,7 @@ def __init__(self, items): @classmethod def create(cls, apiclient, volume_id, account=None, - domainid=None, projectid=None, locationtype=None, asyncbackup=None, zoneids=None, pool_ids=None): + domainid=None, projectid=None, locationtype=None, asyncbackup=None, zoneids=None, pool_ids=None, usestoragereplication=None): """Create Snapshot""" cmd = createSnapshot.createSnapshotCmd() cmd.volumeid = volume_id @@ -1416,6 +1416,8 @@ def create(cls, apiclient, volume_id, account=None, cmd.zoneids = zoneids if pool_ids: cmd.storageids = pool_ids + if usestoragereplication: + cmd.usestoragereplication = usestoragereplication return Snapshot(apiclient.createSnapshot(cmd).__dict__) def delete(self, apiclient, zone_id=None): @@ -1437,7 +1439,7 @@ def list(cls, apiclient, **kwargs): return (apiclient.listSnapshots(cmd)) @classmethod - def copy(cls, apiclient, snapshotid, zone_ids=None, source_zone_id=None, pool_ids=None): + def copy(cls, apiclient, snapshotid, zone_ids=None, source_zone_id=None, pool_ids=None, usestoragereplication=None): """ Copy snapshot to another zone or a primary storage in another zone""" cmd = copySnapshot.copySnapshotCmd() cmd.id = snapshotid @@ -1447,6 +1449,8 @@ def copy(cls, apiclient, snapshotid, zone_ids=None, source_zone_id=None, pool_id cmd.zoneids = zone_ids if pool_ids: cmd.storageids = pool_ids + if usestoragereplication: + cmd.usestoragereplication = usestoragereplication return Snapshot(apiclient.copySnapshot(cmd).__dict__) From f33b6150683d733da34d5235209c5c34f96679d0 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Wed, 16 Apr 2025 13:22:05 +0300 Subject: [PATCH 10/15] hide the primary storage from users in UI hide the primary storage from the users in the UI refactor smoke test --- .../test_snapshot_copy_on_primary_storage.py | 12 ++++----- tools/marvin/marvin/lib/base.py | 2 +- ui/src/views/storage/FormSchedule.vue | 15 ++++++++--- ui/src/views/storage/SnapshotZones.vue | 18 ++++++++++--- ui/src/views/storage/TakeSnapshot.vue | 25 ++++++++++++++++--- 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/test/integration/plugins/storpool/test_snapshot_copy_on_primary_storage.py b/test/integration/plugins/storpool/test_snapshot_copy_on_primary_storage.py index 9d7ded1b4360..4e627a58f3b7 100644 --- a/test/integration/plugins/storpool/test_snapshot_copy_on_primary_storage.py +++ b/test/integration/plugins/storpool/test_snapshot_copy_on_primary_storage.py @@ -157,7 +157,7 @@ def test_01_take_snapshot_multi_zone(self): """Test to take volume snapshot in multiple StorPool primary storage pools """ - snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, usestoragereplication=True) + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, zoneids=[str(self.additional_zone.id)], usestoragereplication=True) self.snapshot_id = snapshot.id self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) time.sleep(420) @@ -172,7 +172,7 @@ def test_02_copy_snapshot_multi_pools(self): snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id) self.snapshot_id = snapshot.id - Snapshot.copy(self.userapiclient, self.snapshot_id, source_zone_id=self.zone.id, usestoragereplication=True) + Snapshot.copy(self.userapiclient, self.snapshot_id, zone_ids=[str(self.additional_zone.id)], source_zone_id=self.zone.id, usestoragereplication=True) self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) time.sleep(420) Snapshot.delete(snapshot, self.userapiclient) @@ -184,7 +184,7 @@ def test_03_take_snapshot_multi_pools_delete_single_zone(self): """Test to take volume snapshot in multiple StorPool storages in diff zones and delete from one zone """ - snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, usestoragereplication=True) + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, zoneids=[str(self.additional_zone.id)], usestoragereplication=True) self.snapshot_id = snapshot.id self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) time.sleep(420) @@ -201,7 +201,7 @@ def test_04_copy_snapshot_multi_zone_delete_all(self): snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id) self.snapshot_id = snapshot.id - Snapshot.copy(self.userapiclient, self.snapshot_id, source_zone_id=self.zone.id, usestoragereplication=True) + Snapshot.copy(self.userapiclient, self.snapshot_id, zone_ids=[str(self.additional_zone.id)], source_zone_id=self.zone.id, usestoragereplication=True) self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) time.sleep(420) Snapshot.delete(snapshot, self.userapiclient) @@ -216,7 +216,7 @@ def test_05_take_snapshot_multi_zone_create_volume_additional_zone(self): """Test to take volume snapshot on StorPool in multiple zones and create a volume in one of the additional zones """ - snapshot = Snapshot.create(self.userapiclient,volume_id=self.volume.id, usestoragereplication=True) + snapshot = Snapshot.create(self.userapiclient,volume_id=self.volume.id, zoneids=[str(self.additional_zone.id)], usestoragereplication=True) self.snapshot_id = snapshot.id self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) disk_offering_id = None @@ -243,7 +243,7 @@ def test_05_take_snapshot_multi_zone_create_volume_additional_zone(self): def test_06_take_snapshot_multi_zone_create_template_additional_zone(self): """Test to take volume snapshot in multiple StorPool primary storages in diff zones and create a volume in one of the additional zones """ - snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, usestoragereplication=True) + snapshot = Snapshot.create(self.userapiclient, volume_id=self.volume.id, zoneids=[str(self.additional_zone.id)], usestoragereplication=True) self.snapshot_id = snapshot.id self.helper.verify_snapshot_copies(self.userapiclient, self.snapshot_id, [self.zone.id, self.additional_zone.id]) self.template = self.helper.create_snapshot_template(self.userapiclient, self.services, self.snapshot_id, self.additional_zone.id) diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index e1424c6821e2..16b2467b63df 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -1446,7 +1446,7 @@ def copy(cls, apiclient, snapshotid, zone_ids=None, source_zone_id=None, pool_id if source_zone_id: cmd.sourcezoneid = source_zone_id if zone_ids: - cmd.zoneids = zone_ids + cmd.destzoneids = zone_ids if pool_ids: cmd.storageids = pool_ids if usestoragereplication: diff --git a/ui/src/views/storage/FormSchedule.vue b/ui/src/views/storage/FormSchedule.vue index e5c99e3b98c3..c831b32af5c6 100644 --- a/ui/src/views/storage/FormSchedule.vue +++ b/ui/src/views/storage/FormSchedule.vue @@ -139,7 +139,7 @@ - + @@ -170,7 +170,10 @@ - + + + + @@ -250,6 +253,7 @@