diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index d4235cbc9bcb..71de3505e0d9 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -333,6 +333,7 @@ public class EventTypes { public static final String EVENT_SNAPSHOT_OFF_PRIMARY = "SNAPSHOT.OFF_PRIMARY"; public static final String EVENT_SNAPSHOT_DELETE = "SNAPSHOT.DELETE"; public static final String EVENT_SNAPSHOT_REVERT = "SNAPSHOT.REVERT"; + public static final String EVENT_SNAPSHOT_EXTRACT = "SNAPSHOT.EXTRACT"; public static final String EVENT_SNAPSHOT_POLICY_CREATE = "SNAPSHOTPOLICY.CREATE"; public static final String EVENT_SNAPSHOT_POLICY_UPDATE = "SNAPSHOTPOLICY.UPDATE"; public static final String EVENT_SNAPSHOT_POLICY_DELETE = "SNAPSHOTPOLICY.DELETE"; @@ -897,6 +898,7 @@ public class EventTypes { // Snapshots entityEventDetails.put(EVENT_SNAPSHOT_CREATE, Snapshot.class); entityEventDetails.put(EVENT_SNAPSHOT_DELETE, Snapshot.class); + entityEventDetails.put(EVENT_SNAPSHOT_EXTRACT, Snapshot.class); entityEventDetails.put(EVENT_SNAPSHOT_ON_PRIMARY, Snapshot.class); entityEventDetails.put(EVENT_SNAPSHOT_OFF_PRIMARY, Snapshot.class); entityEventDetails.put(EVENT_SNAPSHOT_POLICY_CREATE, SnapshotPolicy.class); diff --git a/api/src/main/java/com/cloud/storage/Upload.java b/api/src/main/java/com/cloud/storage/Upload.java index 59d203ac73ae..4e696e877cc8 100644 --- a/api/src/main/java/com/cloud/storage/Upload.java +++ b/api/src/main/java/com/cloud/storage/Upload.java @@ -40,7 +40,7 @@ public static enum Status { } public static enum Type { - VOLUME, TEMPLATE, ISO + VOLUME, SNAPSHOT, TEMPLATE, ISO } public static enum Mode { diff --git a/api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java b/api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java index 0893f337ce2f..67afd6aa4e24 100644 --- a/api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java +++ b/api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java @@ -21,6 +21,7 @@ import org.apache.cloudstack.api.command.user.snapshot.CopySnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotPolicyCmd; import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotPoliciesCmd; +import org.apache.cloudstack.api.command.user.snapshot.ExtractSnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotPoliciesCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd; import org.apache.cloudstack.api.command.user.snapshot.UpdateSnapshotPolicyCmd; @@ -106,6 +107,16 @@ Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapsh */ Snapshot createSnapshot(Long volumeId, Long policyId, Long snapshotId, Account snapshotOwner); + /** + * Extracts the snapshot to a particular location. + * + * @param cmd + * the command specifying url (where the snapshot needs to be extracted to), zoneId (zone where the snapshot exists) and + * id (the id of the snapshot) + * + */ + String extractSnapshot(ExtractSnapshotCmd cmd); + /** * Archives a snapshot from primary storage to secondary storage. * @param id Snapshot ID diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index ef759aaf9c3e..a4d52384df37 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -345,9 +345,11 @@ public interface ResponseGenerator { SecurityGroupResponse createSecurityGroupResponse(SecurityGroup group); - ExtractResponse createExtractResponse(Long uploadId, Long id, Long zoneId, Long accountId, String mode, String url); + ExtractResponse createImageExtractResponse(Long id, Long zoneId, Long accountId, String mode, String url); - ExtractResponse createExtractResponse(Long id, Long zoneId, Long accountId, String mode, String url); + ExtractResponse createVolumeExtractResponse(Long id, Long zoneId, Long accountId, String mode, String url); + + ExtractResponse createSnapshotExtractResponse(Long id, Long zoneId, Long accountId, String url); String toSerializedString(CreateCmdResponse response, String responseType); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/ExtractIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/ExtractIsoCmd.java index 5db680066a6f..7861c1e5d412 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/ExtractIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/ExtractIsoCmd.java @@ -120,7 +120,7 @@ public void execute() { CallContext.current().setEventDetails(getEventDescription()); String uploadUrl = _templateService.extract(this); if (uploadUrl != null) { - ExtractResponse response = _responseGenerator.createExtractResponse(id, zoneId, getEntityOwnerId(), mode, uploadUrl); + ExtractResponse response = _responseGenerator.createImageExtractResponse(id, zoneId, getEntityOwnerId(), mode, uploadUrl); response.setResponseName(getCommandName()); response.setObjectName("iso"); this.setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ExtractSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ExtractSnapshotCmd.java new file mode 100644 index 000000000000..3f0f82ea4e3b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ExtractSnapshotCmd.java @@ -0,0 +1,115 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.snapshot; + +import com.cloud.event.EventTypes; +import com.cloud.storage.Snapshot; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ExtractResponse; +import org.apache.cloudstack.api.response.SnapshotResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "extractSnapshot", description = "Returns a download URL for extracting a snapshot. It must be in the Backed Up state.", since = "4.20.0", + responseObject = ExtractResponse.class, entityType = {Snapshot.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class ExtractSnapshotCmd extends BaseAsyncCmd { + + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = AccessType.OperateEntry) + @Parameter(name=ApiConstants.ID, type=CommandType.UUID, entityType=SnapshotResponse.class, required=true, since="4.20.0", description="the ID of the snapshot") + private Long id; + + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, required = true, since="4.20.0", + description = "the ID of the zone where the snapshot is located") + private Long zoneId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public Long getZoneId() { + return zoneId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.Snapshot; + } + + @Override + public Long getApiResourceId() { + return getId(); + } + + /** + * @return ID of the snapshot to extract, if any. Otherwise returns the ACCOUNT_ID_SYSTEM, so ERROR events will be traceable. + */ + @Override + public long getEntityOwnerId() { + Snapshot snapshot = _entityMgr.findById(Snapshot.class, getId()); + if (snapshot != null) { + return snapshot.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_SNAPSHOT_EXTRACT; + } + + @Override + public String getEventDescription() { + return "Snapshot extraction job"; + } + + @Override + public void execute() { + CallContext.current().setEventDetails("Snapshot ID: " + this._uuidMgr.getUuid(Snapshot.class, getId())); + String uploadUrl = _snapshotService.extractSnapshot(this); + logger.info("Extract URL [{}] of snapshot [{}].", uploadUrl, id); + if (uploadUrl != null) { + ExtractResponse response = _responseGenerator.createSnapshotExtractResponse(id, zoneId, getEntityOwnerId(), uploadUrl); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to extract snapshot"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ExtractTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ExtractTemplateCmd.java index ce6ba5e300c1..0fa0679bfd9e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ExtractTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ExtractTemplateCmd.java @@ -120,8 +120,9 @@ public void execute() { CallContext.current().setEventDetails(getEventDescription()); String uploadUrl = _templateService.extract(this); if (uploadUrl != null) { - ExtractResponse response = _responseGenerator.createExtractResponse(id, zoneId, getEntityOwnerId(), mode, uploadUrl); + ExtractResponse response = _responseGenerator.createImageExtractResponse(id, zoneId, getEntityOwnerId(), mode, uploadUrl); response.setResponseName(getCommandName()); + response.setObjectName("template"); this.setResponseObject(response); } else { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to extract template"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ExtractVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ExtractVolumeCmd.java index 1146f80f0e2c..9445aba23c06 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ExtractVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ExtractVolumeCmd.java @@ -31,9 +31,7 @@ import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; -import com.cloud.dc.DataCenter; import com.cloud.event.EventTypes; -import com.cloud.storage.Upload; import com.cloud.storage.Volume; import com.cloud.user.Account; @@ -124,20 +122,8 @@ public void execute() { CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId())); String uploadUrl = _volumeService.extractVolume(this); if (uploadUrl != null) { - ExtractResponse response = new ExtractResponse(); + ExtractResponse response = _responseGenerator.createVolumeExtractResponse(id, zoneId, getEntityOwnerId(), mode, uploadUrl); response.setResponseName(getCommandName()); - response.setObjectName("volume"); - Volume vol = _entityMgr.findById(Volume.class, id); - response.setId(vol.getUuid()); - response.setName(vol.getName()); - DataCenter zone = _entityMgr.findById(DataCenter.class, zoneId); - response.setZoneId(zone.getUuid()); - response.setZoneName(zone.getName()); - response.setMode(mode); - response.setState(Upload.Status.DOWNLOAD_URL_CREATED.toString()); - Account account = _entityMgr.findById(Account.class, getEntityOwnerId()); - response.setAccountId(account.getUuid()); - response.setUrl(uploadUrl); setResponseObject(response); } else { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to extract volume"); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreVO.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreVO.java index a1dc05fce58b..7a466c1f5055 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreVO.java @@ -86,6 +86,13 @@ public class SnapshotDataStoreVO implements StateObject> getCommands() { cmdList.add(CreateSnapshotFromVMSnapshotCmd.class); cmdList.add(CopySnapshotCmd.class); cmdList.add(DeleteSnapshotCmd.class); + cmdList.add(ExtractSnapshotCmd.class); cmdList.add(ArchiveSnapshotCmd.class); cmdList.add(CreateSnapshotPolicyCmd.class); cmdList.add(UpdateSnapshotPolicyCmd.class); 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 56981cfe55c3..51634adfa4c1 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -33,6 +33,7 @@ import javax.naming.ConfigurationException; import org.apache.cloudstack.acl.SecurityChecker; +import com.cloud.api.ApiDBUtils; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -40,6 +41,7 @@ import org.apache.cloudstack.api.command.user.snapshot.CopySnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotPolicyCmd; import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotPoliciesCmd; +import org.apache.cloudstack.api.command.user.snapshot.ExtractSnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotPoliciesCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd; import org.apache.cloudstack.api.command.user.snapshot.UpdateSnapshotPolicyCmd; @@ -72,10 +74,12 @@ 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.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.springframework.stereotype.Component; @@ -466,6 +470,74 @@ public Snapshot createSnapshot(Long volumeId, Long policyId, Long snapshotId, Ac return snapshot; } + @Override + @ActionEvent(eventType = EventTypes.EVENT_SNAPSHOT_EXTRACT, eventDescription = "extracting snapshot", async = true) + public String extractSnapshot(ExtractSnapshotCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + Long snapshotId = cmd.getId(); + Long zoneId = cmd.getZoneId(); + + if (!_accountMgr.isRootAdmin(caller.getId()) && ApiDBUtils.isExtractionDisabled()) { + logger.error("Extraction is disabled through [{}].", Config.DisableExtraction); + throw new PermissionDeniedException("Extraction could not be completed."); + } + + SnapshotVO snapshot = _snapshotDao.findById(snapshotId); + if (snapshot == null || snapshot.getRemoved() != null) { + logger.error("Unable to find active [{}].", snapshot); + throw new InvalidParameterValueException("Unable to find active snapshot."); + } + + if (zoneId != null && dataCenterDao.findById(zoneId) == null) { + logger.error("Invalid zone id [{}].", zoneId); + throw new IllegalArgumentException("Please specify a valid zone."); + } + + _accountMgr.checkAccess(caller, null, true, snapshot); + + List imageStores = dataStoreMgr.getImageStoresByScope(new ZoneScope(zoneId)); + + if (CollectionUtils.isEmpty(imageStores)) { + logger.error("Could not find any zone storages."); + throw new InvalidParameterValueException("Extraction could not be completed"); + } + + SnapshotDataStoreVO snapshotDataStoreReference = null; + ImageStoreEntity chosenStore = null; + + for (DataStore store : imageStores) { + snapshotDataStoreReference = _snapshotStoreDao.findByStoreSnapshot(DataStoreRole.Image, store.getId(), snapshotId); + if (snapshotDataStoreReference == null) { + logger.trace("Snapshot [{}] not in store [{}].", snapshotId, store.getId()); + continue; + } + String existingExtractUrl = snapshotDataStoreReference.getExtractUrl(); + if (existingExtractUrl != null) { + logger.debug("Extract URL already exists: [{}].", existingExtractUrl); + return existingExtractUrl; + } + chosenStore = (ImageStoreEntity) store; + logger.debug("Snapshot [{}] found in store [{}].", snapshotId, chosenStore.getId()); + break; + } + + if (ObjectUtils.anyNull(chosenStore, snapshotDataStoreReference)) { + logger.error("Snapshot [{}] not found in any secondary storage.", snapshotId); + throw new InvalidParameterValueException("Snapshot not found."); + } + + snapshotSrv.syncVolumeSnapshotsToRegionStore(snapshot.getVolumeId(), chosenStore); + + SnapshotInfo snapshotObject = snapshotFactory.getSnapshot(snapshotId, chosenStore); + String extractUrl = chosenStore.createEntityExtractUrl(snapshotObject.getPath(), snapshotObject.getBaseVolume().getFormat(), snapshotObject); + logger.debug("Extract URL [{}] created for snapshot [{}].", extractUrl, snapshot); + snapshotDataStoreReference.setExtractUrl(extractUrl); + snapshotDataStoreReference.setExtractUrlCreated(DateUtil.now()); + _snapshotStoreDao.update(snapshotDataStoreReference.getId(), snapshotDataStoreReference); + + return extractUrl; + } + @Override public Snapshot archiveSnapshot(Long snapshotId) { SnapshotInfo snapshotOnPrimary = snapshotFactory.getSnapshotOnPrimaryStore(snapshotId); diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 11254afbaadc..4d095d09cc66 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -298,7 +298,6 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, @Inject private HypervisorGuruManager _hvGuruMgr; - private boolean _disableExtraction = false; private List _adapters; ExecutorService _preloadExecutor; @@ -539,7 +538,7 @@ private String extract(Account caller, Long templateId, String url, Long zoneId, if (isISO) { desc = Upload.Type.ISO.toString(); } - if (!_accountMgr.isRootAdmin(caller.getId()) && _disableExtraction) { + if (!_accountMgr.isRootAdmin(caller.getId()) && ApiDBUtils.isExtractionDisabled()) { throw new PermissionDeniedException("Extraction has been disabled by admin"); } @@ -1112,10 +1111,6 @@ public boolean stop() { @Override public boolean configure(String name, Map params) throws ConfigurationException { - - String disableExtraction = _configDao.getValue(Config.DisableExtraction.toString()); - _disableExtraction = (disableExtraction == null) ? false : Boolean.parseBoolean(disableExtraction); - _preloadExecutor = Executors.newFixedThreadPool(TemplatePreloaderPoolSize.value(), new NamedThreadFactory("Template-Preloader")); return true; 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 74b31283d9dd..28903c72cc3c 100755 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java @@ -27,13 +27,20 @@ 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; @@ -49,6 +56,7 @@ 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; @@ -176,6 +184,16 @@ public class SnapshotManagerTest { @Mock DataCenterDao dataCenterDao; + MockedStatic apiDBUtilsMock; + @Mock + ExtractSnapshotCmd extractSnapshotCmdMock; + @Mock + DataCenterVO dataCenterVOMock; + @Mock + ImageStoreEntity imageStoreEntityMock; + @Mock + DataStoreManager dataStoreManagerMock; + SnapshotPolicyVO snapshotPolicyVoInstance; List listIntervalTypes = Arrays.asList(DateUtil.IntervalType.values()); @@ -191,6 +209,11 @@ public class SnapshotManagerTest { private static final int TEST_SNAPSHOT_POLICY_MAX_SNAPS = 1; private static final boolean TEST_SNAPSHOT_POLICY_DISPLAY = true; private static final boolean TEST_SNAPSHOT_POLICY_ACTIVE = true; + private static final long TEST_ZONE_ID = 7L; + private static final long TEST_SNAPSHOTDATASTORE_ID = 7L; + private static final String TEST_EXTRACT_URL = "extractUrl"; + private static final String TEST_SNAPSHOT_PATH = "path"; + private static final Storage.ImageFormat TEST_VOLUME_FORMAT = Storage.ImageFormat.RAW; @Before public void setup() throws ResourceAllocationException { @@ -228,10 +251,13 @@ public void setup() throws ResourceAllocationException { snapshotPolicyVoInstance = new SnapshotPolicyVO(TEST_VOLUME_ID, TEST_SNAPSHOT_POLICY_SCHEDULE, TEST_SNAPSHOT_POLICY_TIMEZONE, TEST_SNAPSHOT_POLICY_INTERVAL, TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY); + + apiDBUtilsMock = Mockito.mockStatic(ApiDBUtils.class); } @After public void tearDown() throws Exception { + apiDBUtilsMock.close(); CallContext.unregister(); } @@ -533,4 +559,108 @@ public void testIsBackupSnapshotToSecondaryForEdgeZone() { mockForBackupSnapshotToSecondaryZoneTest(true, DataCenter.Type.Edge); Assert.assertFalse(_snapshotMgr.isBackupSnapshotToSecondaryForZone(1L)); } + + private void mockForExtractSnapshotTests() { + Mockito.doReturn(TEST_SNAPSHOT_ID).when(extractSnapshotCmdMock).getId(); + Mockito.doReturn(TEST_ZONE_ID).when(extractSnapshotCmdMock).getZoneId(); + Mockito.doReturn(false).when(_accountMgr).isRootAdmin(Mockito.anyLong()); + Mockito.when(ApiDBUtils.isExtractionDisabled()).thenReturn(false); + + Mockito.doReturn(dataCenterVOMock).when(dataCenterDao).findById(TEST_ZONE_ID); + + List dataStores = new ArrayList<>(); + dataStores.add(imageStoreEntityMock); + Mockito.doReturn(dataStores).when(dataStoreManagerMock).getImageStoresByScope(Mockito.any()); + Mockito.doReturn(TEST_STORAGE_POOL_ID).when(imageStoreEntityMock).getId(); + + Mockito.doReturn(snapshotStoreMock).when(snapshotStoreDao).findByStoreSnapshot(DataStoreRole.Image, TEST_STORAGE_POOL_ID, TEST_SNAPSHOT_ID); + + Mockito.doReturn(snapshotInfoMock).when(snapshotFactory).getSnapshot(TEST_SNAPSHOT_ID, imageStoreEntityMock); + Mockito.doReturn(TEST_SNAPSHOT_PATH).when(snapshotInfoMock).getPath(); + Mockito.doReturn(volumeInfoMock).when(snapshotInfoMock).getBaseVolume(); + Mockito.doReturn(TEST_VOLUME_FORMAT).when(volumeInfoMock).getFormat(); + + Mockito.doReturn(TEST_SNAPSHOTDATASTORE_ID).when(snapshotStoreMock).getId(); + Mockito.doReturn(TEST_EXTRACT_URL).when(imageStoreEntityMock).createEntityExtractUrl(TEST_SNAPSHOT_PATH, TEST_VOLUME_FORMAT, snapshotInfoMock); + } + + @Test(expected = PermissionDeniedException.class) + public void extractSnapshotTestNotRootAdminDisabledExtractionReturnException() { + mockForExtractSnapshotTests(); + Mockito.when(ApiDBUtils.isExtractionDisabled()).thenReturn(true); + + _snapshotMgr.extractSnapshot(extractSnapshotCmdMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void extractSnapshotTestNullSnapshotReturnException() { + mockForExtractSnapshotTests(); + Mockito.doReturn(null).when(_snapshotDao).findById(TEST_SNAPSHOT_ID); + + _snapshotMgr.extractSnapshot(extractSnapshotCmdMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void extractSnapshotTestRemovedSnapshotReturnException() { + mockForExtractSnapshotTests(); + Mockito.doReturn(Mockito.mock(Date.class)).when(snapshotMock).getRemoved(); + Mockito.doReturn(snapshotMock).when(_snapshotDao).findById(TEST_SNAPSHOT_ID); + + _snapshotMgr.extractSnapshot(extractSnapshotCmdMock); + } + + @Test(expected = IllegalArgumentException.class) + public void extractSnapshotTestNullDataCenterReturnException() { + mockForExtractSnapshotTests(); + Mockito.doReturn(null).when(dataCenterDao).findById(TEST_ZONE_ID); + + _snapshotMgr.extractSnapshot(extractSnapshotCmdMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void extractSnapshotTestNoZoneStoragesReturnException() { + mockForExtractSnapshotTests(); + Mockito.doReturn(Collections.emptyList()).when(dataStoreManagerMock).getImageStoresByScope(Mockito.any()); + + _snapshotMgr.extractSnapshot(extractSnapshotCmdMock); + } + + @Test() + public void extractSnapshotTestExistingExtractUrlReturnUrl() { + mockForExtractSnapshotTests(); + String extractUrl = "extractUrl"; + Mockito.doReturn(extractUrl).when(snapshotStoreMock).getExtractUrl(); + + Assert.assertEquals(extractUrl, _snapshotMgr.extractSnapshot(extractSnapshotCmdMock)); + Mockito.verify(snapshotSrv, Mockito.never()).syncVolumeSnapshotsToRegionStore(Mockito.anyLong(), Mockito.any()); + Mockito.verify(snapshotStoreDao, Mockito.never()).update(Mockito.anyLong(), Mockito.any()); + } + + @Test(expected = InvalidParameterValueException.class) + public void extractSnapshotTestNullSnapshotStoreReturnException() { + mockForExtractSnapshotTests(); + Mockito.doReturn(null).when(snapshotStoreDao).findByStoreSnapshot(DataStoreRole.Image, TEST_STORAGE_POOL_ID, TEST_SNAPSHOT_ID); + + _snapshotMgr.extractSnapshot(extractSnapshotCmdMock); + } + + @Test() + public void extractSnapshotTestCreateExtractUrlReturnUrl() { + mockForExtractSnapshotTests(); + + Assert.assertEquals(TEST_EXTRACT_URL, _snapshotMgr.extractSnapshot(extractSnapshotCmdMock)); + Mockito.verify(snapshotSrv).syncVolumeSnapshotsToRegionStore(TEST_VOLUME_ID, imageStoreEntityMock); + Mockito.verify(snapshotStoreDao).update(TEST_SNAPSHOTDATASTORE_ID, snapshotStoreMock); + } + + @Test() + public void extractSnapshotTestRootAdminDisabledExtractionCreateExtractUrlReturnUrl() { + mockForExtractSnapshotTests(); + Mockito.doReturn(true).when(_accountMgr).isRootAdmin(Mockito.anyLong()); + Mockito.when(ApiDBUtils.isExtractionDisabled()).thenReturn(true); + + Assert.assertEquals(TEST_EXTRACT_URL, _snapshotMgr.extractSnapshot(extractSnapshotCmdMock)); + Mockito.verify(snapshotSrv).syncVolumeSnapshotsToRegionStore(TEST_VOLUME_ID, imageStoreEntityMock); + Mockito.verify(snapshotStoreDao).update(TEST_SNAPSHOTDATASTORE_ID, snapshotStoreMock); + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index cda4e34618aa..3e02e2810426 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -115,6 +115,7 @@ "label.action.disable.user": "Disable User", "label.action.disable.zone": "Disable zone", "label.action.download.iso": "Download ISO", +"label.action.download.snapshot": "Download Snapshot", "label.action.download.template": "Download Template", "label.action.download.volume": "Download volume", "label.action.edit.account": "Edit Account", @@ -2575,6 +2576,7 @@ "message.action.disable.static.nat": "Please confirm that you want to disable static NAT.", "message.action.disable.zone": "Please confirm that you want to disable this zone.", "message.action.download.iso": "Please confirm that you want to download this ISO.", +"message.action.download.snapshot": "Please confirm that you want to download this Snapshot.", "message.action.download.template": "Please confirm that you want to download this Template.", "message.action.edit.nfs.mount.options": "Changes to NFS mount options will only take affect on cancelling maintenance mode which will cause the storage pool to be remounted on all KVM hosts with the new mount options.", "message.action.enable.cluster": "Please confirm that you want to enable this cluster.", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index 82d527ae4c10..79333c100d31 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -95,6 +95,7 @@ "label.action.disable.user": "Desativar usu\u00e1rio", "label.action.disable.zone": "Desativar zona", "label.action.download.iso": "Baixar ISO", +"label.action.download.snapshot": "Baixar snapshot", "label.action.download.template": "Baixar template", "label.action.download.volume": "Baixar disco", "label.action.edit.account": "Editar conta", @@ -1856,6 +1857,7 @@ "message.action.disable.static.nat": "Confirme que voc\u00ea deseja desativar o NAT est\u00e1tico.", "message.action.disable.zone": "Confirma a desativa\u00e7\u00e3o da zona.", "message.action.download.iso": "Por favor confirme que voc\u00ea deseja baixar esta ISO.", +"message.action.download.snapshot": "Por favor confirme que voc\u00ea deseja baixar esta snapshot.", "message.action.download.template": "Por favor confirme que voc\u00ea deseja baixar este template.", "message.action.enable.cluster": "Confirma a ativa\u00e7\u00e3o do cluster.", "message.action.enable.physical.network": "Por favor confirme que voc\u00ea deseja habilitar esta rede f\u00edsica.", diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 4c76ebf8db3f..b4debc83a851 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -394,6 +394,26 @@ export default { dataView: true, show: (record) => { return record.state === 'BackedUp' && record.revertable } }, + { + api: 'extractSnapshot', + icon: 'cloud-download-outlined', + label: 'label.action.download.snapshot', + message: 'message.action.download.snapshot', + dataView: true, + show: (record, store) => { + return (['Admin'].includes(store.userInfo.roletype) || // If admin or owner or belongs to current project + ((record.domainid === store.userInfo.domainid && record.account === store.userInfo.account) || + (record.domainid === store.userInfo.domainid && record.projectid && store.project && store.project.id && record.projectid === store.project.id))) && + record.state === 'BackedUp' + }, + args: ['zoneid'], + mapping: { + zoneid: { + value: (record) => { return record.zoneid } + } + }, + response: (result) => { return `Please click ${result.snapshot.url} to download.` } + }, { api: 'deleteSnapshot', icon: 'delete-outlined',