Download Volume Snapshots (#8878)

Co-authored-by: Rodrigo D. Lopez <19981369+RodrigoDLopez@users.noreply.github.com>
This commit is contained in:
Gabriel Pordeus Santos 2024-08-21 04:38:56 -03:00 committed by GitHub
parent f5c7729871
commit 9b22cd590d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 418 additions and 61 deletions

View File

@ -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);

View File

@ -40,7 +40,7 @@ public interface Upload extends InternalIdentity, Identity {
}
public static enum Type {
VOLUME, TEMPLATE, ISO
VOLUME, SNAPSHOT, TEMPLATE, ISO
}
public static enum Mode {

View File

@ -21,6 +21,7 @@ import java.util.List;
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 @@ public interface SnapshotApiService {
*/
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

View File

@ -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);

View File

@ -120,7 +120,7 @@ public class ExtractIsoCmd extends BaseAsyncCmd {
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);

View File

@ -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");
}
}
}

View File

@ -120,8 +120,9 @@ public class ExtractTemplateCmd extends BaseAsyncCmd {
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");

View File

@ -31,9 +31,7 @@ import org.apache.cloudstack.api.response.VolumeResponse;
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 class ExtractVolumeCmd extends BaseAsyncCmd {
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");

View File

@ -86,6 +86,13 @@ public class SnapshotDataStoreVO implements StateObject<ObjectInDataStoreStateMa
@Column(name = "install_path")
private String installPath;
@Column(name = "download_url", length = 2048)
private String extractUrl;
@Column(name = "download_url_created")
@Temporal(value = TemporalType.TIMESTAMP)
private Date extractUrlCreated = null;
@Column(name = "update_count", updatable = true, nullable = false)
protected long updatedCount;
@ -310,6 +317,22 @@ public class SnapshotDataStoreVO implements StateObject<ObjectInDataStoreStateMa
this.volumeId = volumeId;
}
public String getExtractUrl() {
return extractUrl;
}
public void setExtractUrl(String extractUrl) {
this.extractUrl = extractUrl;
}
public Date getExtractUrlCreated() {
return extractUrlCreated;
}
public void setExtractUrlCreated(Date extractUrlCreated) {
this.extractUrlCreated = extractUrlCreated;
}
public void setCreated(Date created) {
this.created = created;
}

View File

@ -93,6 +93,10 @@ CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_email_configuration`(
-- Add `is_implicit` column to `host_tags` table
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host_tags', 'is_implicit', 'int(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT "If host tag is implicit or explicit" ');
-- Fields related to Snapshot Extraction
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_store_ref', 'download_url', 'varchar(2048) DEFAULT NULL');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_store_ref', 'download_url_created', 'datetime DEFAULT NULL');
-- Webhooks feature
DROP TABLE IF EXISTS `cloud`.`webhook`;
CREATE TABLE `cloud`.`webhook` (

View File

@ -372,7 +372,6 @@ import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.StoragePool;
import com.cloud.storage.Upload;
import com.cloud.storage.UploadVO;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
@ -1914,58 +1913,49 @@ public class ApiResponseHelper implements ResponseGenerator {
return listSgs.get(0);
}
//TODO: we need to deprecate uploadVO, since extract is done in a synchronous fashion
@Override
public ExtractResponse createExtractResponse(Long id, Long zoneId, Long accountId, String mode, String url) {
private ExtractResponse createExtractResponse (Long zoneId, Long accountId, String url) {
ExtractResponse response = new ExtractResponse();
response.setObjectName("template");
VMTemplateVO template = ApiDBUtils.findTemplateById(id);
response.setId(template.getUuid());
response.setName(template.getName());
if (zoneId != null) {
DataCenter zone = ApiDBUtils.findZoneById(zoneId);
response.setZoneId(zone.getUuid());
response.setZoneName(zone.getName());
}
response.setMode(mode);
response.setUrl(url);
response.setState(Upload.Status.DOWNLOAD_URL_CREATED.toString());
Account account = ApiDBUtils.findAccountById(accountId);
response.setAccountId(account.getUuid());
return response;
}
@Override
public ExtractResponse createExtractResponse(Long uploadId, Long id, Long zoneId, Long accountId, String mode, String url) {
public ExtractResponse createVolumeExtractResponse(Long id, Long zoneId, Long accountId, String mode, String url) {
ExtractResponse response = createExtractResponse(zoneId, accountId, url);
response.setObjectName("volume");
response.setMode(mode);
Volume volume = ApiDBUtils.findVolumeById(id);
response.setId(volume.getUuid());
response.setName(volume.getName());
return response;
}
ExtractResponse response = new ExtractResponse();
response.setObjectName("template");
@Override
public ExtractResponse createSnapshotExtractResponse(Long id, Long zoneId, Long accountId, String url) {
ExtractResponse response = createExtractResponse(zoneId, accountId, url);
response.setObjectName("snapshot");
Snapshot snapshot = ApiDBUtils.findSnapshotById(id);
response.setId(snapshot.getUuid());
response.setName(snapshot.getName());
return response;
}
@Override
public ExtractResponse createImageExtractResponse(Long id, Long zoneId, Long accountId, String mode, String url) {
ExtractResponse response = createExtractResponse(zoneId, accountId, url);
response.setMode(mode);
VMTemplateVO template = ApiDBUtils.findTemplateById(id);
response.setId(template.getUuid());
response.setName(template.getName());
if (zoneId != null) {
DataCenter zone = ApiDBUtils.findZoneById(zoneId);
response.setZoneId(zone.getUuid());
response.setZoneName(zone.getName());
}
response.setMode(mode);
if (uploadId == null) {
// region-wide image store
response.setUrl(url);
response.setState(Upload.Status.DOWNLOAD_URL_CREATED.toString());
} else {
UploadVO uploadInfo = ApiDBUtils.findUploadById(uploadId);
response.setUploadId(uploadInfo.getUuid());
response.setState(uploadInfo.getUploadState().toString());
response.setUrl(uploadInfo.getUploadUrl());
}
Account account = ApiDBUtils.findAccountById(accountId);
response.setAccountId(account.getUuid());
return response;
}
@Override

View File

@ -557,7 +557,7 @@ public enum Config {
Boolean.class,
"disable.extraction",
"false",
"Flag for disabling extraction of template, isos and volumes",
"Flag for disabling extraction of templates, isos, snapshots and volumes",
null),
ExtractURLExpirationInterval(
"Advanced",

View File

@ -495,6 +495,7 @@ import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotFromVMSnaps
import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotPolicyCmd;
import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotCmd;
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.RevertSnapshotCmd;
@ -3755,6 +3756,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
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);

View File

@ -33,6 +33,7 @@ import javax.inject.Inject;
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.ApiConstants;
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.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.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 class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
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<DataStore> 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);

View File

@ -298,7 +298,6 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
@Inject
private HypervisorGuruManager _hvGuruMgr;
private boolean _disableExtraction = false;
private List<TemplateAdapter> _adapters;
ExecutorService _preloadExecutor;
@ -539,7 +538,7 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
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 class TemplateManagerImpl extends ManagerBase implements TemplateManager,
@Override
public boolean configure(String name, Map<String, Object> 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;

View File

@ -27,13 +27,20 @@ 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;
@ -49,6 +56,7 @@ 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;
@ -176,6 +184,16 @@ public class SnapshotManagerTest {
@Mock
DataCenterDao dataCenterDao;
MockedStatic<ApiDBUtils> apiDBUtilsMock;
@Mock
ExtractSnapshotCmd extractSnapshotCmdMock;
@Mock
DataCenterVO dataCenterVOMock;
@Mock
ImageStoreEntity imageStoreEntityMock;
@Mock
DataStoreManager dataStoreManagerMock;
SnapshotPolicyVO snapshotPolicyVoInstance;
List<DateUtil.IntervalType> 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 class SnapshotManagerTest {
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 class SnapshotManagerTest {
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<DataStore> 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);
}
}

View File

@ -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.",

View File

@ -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.",

View File

@ -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 <a href="${result.snapshot.url}" target="_blank">${result.snapshot.url}</a> to download.` }
},
{
api: 'deleteSnapshot',
icon: 'delete-outlined',