/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.cloudstack.storage.motion; import com.cloud.agent.AgentManager; import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.NfsTO; import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.configuration.Config; 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.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.server.ManagementService; import com.cloud.storage.DataStoreRole; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotVO; import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.VolumeDetailVO; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.SnapshotDetailsDao; import com.cloud.storage.dao.SnapshotDetailsVO; import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.utils.NumbersUtil; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachineManager; import com.google.common.base.Preconditions; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; 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; 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.HostScope; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; import org.apache.cloudstack.engine.subsystem.api.storage.Scope; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StorageCacheManager; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; 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.VolumeService.VolumeApiResult; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.framework.async.AsyncCallFuture; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.command.CopyCmdAnswer; import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.command.ResignatureAnswer; import org.apache.cloudstack.storage.command.ResignatureCommand; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @Component public class StorageSystemDataMotionStrategy implements DataMotionStrategy { private static final Logger LOGGER = Logger.getLogger(StorageSystemDataMotionStrategy.class); private static final Random RANDOM = new Random(System.nanoTime()); @Inject private AgentManager _agentMgr; @Inject private ConfigurationDao _configDao; @Inject private DataStoreManager dataStoreMgr; @Inject private DiskOfferingDao _diskOfferingDao; @Inject private ClusterDao clusterDao; @Inject private HostDao _hostDao; @Inject private HostDetailsDao hostDetailsDao; @Inject private ManagementService _mgr; @Inject private PrimaryDataStoreDao _storagePoolDao; @Inject private SnapshotDao _snapshotDao; @Inject private SnapshotDetailsDao _snapshotDetailsDao; @Inject private VolumeDao _volumeDao; @Inject private VolumeDataFactory _volumeDataFactory; @Inject private VolumeDetailsDao volumeDetailsDao; @Inject private VolumeService _volumeService; @Inject private StorageCacheManager cacheMgr; @Inject private EndPointSelector selector; @Override public StrategyPriority canHandle(DataObject srcData, DataObject destData) { if (srcData instanceof SnapshotInfo) { if (canHandle(srcData) || canHandle(destData)) { return StrategyPriority.HIGHEST; } } if (srcData instanceof TemplateInfo && destData instanceof VolumeInfo && (srcData.getDataStore().getId() == destData.getDataStore().getId()) && (canHandle(srcData) || canHandle(destData))) { // Both source and dest are on the same storage, so just clone them. return StrategyPriority.HIGHEST; } return StrategyPriority.CANT_HANDLE; } private boolean canHandle(DataObject dataObject) { Preconditions.checkArgument(dataObject != null, "Passing 'null' to dataObject of canHandle(DataObject) is not supported."); DataStore dataStore = dataObject.getDataStore(); if (dataStore.getRole() == DataStoreRole.Primary) { Map<String, String> mapCapabilities = dataStore.getDriver().getCapabilities(); if (mapCapabilities == null) { return false; } if (dataObject instanceof VolumeInfo || dataObject instanceof SnapshotInfo) { String value = mapCapabilities.get(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString()); Boolean supportsStorageSystemSnapshots = Boolean.valueOf(value); if (supportsStorageSystemSnapshots) { LOGGER.info("Using 'StorageSystemDataMotionStrategy' (dataObject is a volume or snapshot and the storage system supports snapshots)"); return true; } } else if (dataObject instanceof TemplateInfo) { // If the storage system can clone volumes, we can cache templates on it. String value = mapCapabilities.get(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString()); Boolean canCloneVolume = Boolean.valueOf(value); if (canCloneVolume) { LOGGER.info("Using 'StorageSystemDataMotionStrategy' (dataObject is a template and the storage system can create a volume from a volume)"); return true; } } } return false; } @Override public StrategyPriority canHandle(Map<VolumeInfo, DataStore> volumeMap, Host srcHost, Host destHost) { return StrategyPriority.CANT_HANDLE; } @Override public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback<CopyCommandResult> callback) { if (srcData instanceof SnapshotInfo) { SnapshotInfo snapshotInfo = (SnapshotInfo)srcData; validate(snapshotInfo); boolean canHandleSrc = canHandle(srcData); if (canHandleSrc && (destData instanceof TemplateInfo || destData instanceof SnapshotInfo) && (destData.getDataStore().getRole() == DataStoreRole.Image || destData.getDataStore().getRole() == DataStoreRole.ImageCache)) { handleCopyDataToSecondaryStorage(snapshotInfo, destData, callback); return; } if (destData instanceof VolumeInfo) { VolumeInfo volumeInfo = (VolumeInfo)destData; boolean canHandleDest = canHandle(destData); if (canHandleSrc && canHandleDest) { if (snapshotInfo.getDataStore().getId() == volumeInfo.getDataStore().getId()) { handleCreateVolumeFromSnapshotBothOnStorageSystem(snapshotInfo, volumeInfo, callback); return; } else { String errMsg = "This operation is not supported (DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT " + "not supported by source or destination storage plug-in). " + getSrcDestDataStoreMsg(srcData, destData); LOGGER.warn(errMsg); throw new UnsupportedOperationException(errMsg); } } if (canHandleDest) { handleCreateVolumeFromSnapshotOnSecondaryStorage(snapshotInfo, volumeInfo, callback); return; } if (canHandleSrc) { String errMsg = "This operation is not supported (DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT " + "not supported by source storage plug-in). " + getSrcDataStoreMsg(srcData); LOGGER.warn(errMsg); throw new UnsupportedOperationException(errMsg); } } } else if (srcData instanceof TemplateInfo && destData instanceof VolumeInfo) { boolean canHandleSrc = canHandle(srcData); if (!canHandleSrc) { String errMsg = "This operation is not supported (DataStoreCapabilities.STORAGE_CAN_CREATE_VOLUME_FROM_VOLUME " + "not supported by destination storage plug-in). " + getDestDataStoreMsg(destData); LOGGER.warn(errMsg); throw new UnsupportedOperationException(errMsg); } handleCreateVolumeFromTemplateBothOnStorageSystem((TemplateInfo)srcData, (VolumeInfo)destData, callback); return; } throw new UnsupportedOperationException("This operation is not supported."); } private String getSrcDestDataStoreMsg(DataObject srcData, DataObject destData) { Preconditions.checkArgument(srcData != null, "Passing 'null' to srcData of getSrcDestDataStoreMsg(DataObject, DataObject) is not supported."); Preconditions.checkArgument(destData != null, "Passing 'null' to destData of getSrcDestDataStoreMsg(DataObject, DataObject) is not supported."); return "Source data store = " + srcData.getDataStore().getName() + "; " + "Destination data store = " + destData.getDataStore().getName() + "."; } private String getSrcDataStoreMsg(DataObject srcData) { Preconditions.checkArgument(srcData != null, "Passing 'null' to srcData of getSrcDataStoreMsg(DataObject) is not supported."); return "Source data store = " + srcData.getDataStore().getName() + "."; } private String getDestDataStoreMsg(DataObject destData) { Preconditions.checkArgument(destData != null, "Passing 'null' to destData of getDestDataStoreMsg(DataObject) is not supported."); return "Destination data store = " + destData.getDataStore().getName() + "."; } private void validate(SnapshotInfo snapshotInfo) { long volumeId = snapshotInfo.getVolumeId(); VolumeVO volumeVO = _volumeDao.findByIdIncludingRemoved(volumeId); if (volumeVO.getFormat() != ImageFormat.VHD) { throw new CloudRuntimeException("Only the " + ImageFormat.VHD.toString() + " image type is currently supported."); } } private boolean usingBackendSnapshotFor(SnapshotInfo snapshotInfo) { String property = getProperty(snapshotInfo.getId(), "takeSnapshot"); return Boolean.parseBoolean(property); } protected boolean needCacheStorage(DataObject srcData, DataObject destData) { DataTO srcTO = srcData.getTO(); DataStoreTO srcStoreTO = srcTO.getDataStore(); DataTO destTO = destData.getTO(); DataStoreTO destStoreTO = destTO.getDataStore(); // both snapshot and volume are on primary datastore. No need for a cache storage as // hypervisor will copy directly if (srcStoreTO instanceof PrimaryDataStoreTO && destStoreTO instanceof PrimaryDataStoreTO) { return false; } if (srcStoreTO instanceof NfsTO || srcStoreTO.getRole() == DataStoreRole.ImageCache) { return false; } if (destStoreTO instanceof NfsTO || destStoreTO.getRole() == DataStoreRole.ImageCache) { return false; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("needCacheStorage true, dest at " + destTO.getPath() + " dest role " + destStoreTO.getRole().toString() + srcTO.getPath() + " src role " + srcStoreTO.getRole().toString()); } return true; } private Scope pickCacheScopeForCopy(DataObject srcData, DataObject destData) { Scope srcScope = srcData.getDataStore().getScope(); Scope destScope = destData.getDataStore().getScope(); Scope selectedScope = null; if (srcScope.getScopeId() != null) { selectedScope = getZoneScope(srcScope); } else if (destScope.getScopeId() != null) { selectedScope = getZoneScope(destScope); } else { LOGGER.warn("Cannot find a zone-wide scope for movement that needs a cache storage"); } return selectedScope; } private Scope getZoneScope(Scope scope) { ZoneScope zoneScope; if (scope instanceof ClusterScope) { ClusterScope clusterScope = (ClusterScope)scope; zoneScope = new ZoneScope(clusterScope.getZoneId()); } else if (scope instanceof HostScope) { HostScope hostScope = (HostScope)scope; zoneScope = new ZoneScope(hostScope.getZoneId()); } else { zoneScope = (ZoneScope)scope; } return zoneScope; } /** * This function is responsible for copying a volume from the managed store to a secondary store. This is used in two cases * 1) When creating a template from a snapshot * 2) When createSnapshot is called with location=SECONDARY * * @param snapshotInfo Source snapshot * @param destData destination (can be template or snapshot) * @param callback callback for async */ private void handleCopyDataToSecondaryStorage(SnapshotInfo snapshotInfo, DataObject destData, AsyncCompletionCallback<CopyCommandResult> callback) { try { snapshotInfo.processEvent(Event.CopyingRequested); } catch (Exception ex) { throw new CloudRuntimeException("This snapshot is not currently in a state where it can be used to create a template."); } HostVO hostVO = getHost(snapshotInfo); boolean usingBackendSnapshot = usingBackendSnapshotFor(snapshotInfo); boolean computeClusterSupportsResign = clusterDao.getSupportsResigning(hostVO.getClusterId()); boolean needCache = needCacheStorage(snapshotInfo, destData); DataObject destOnStore = destData; if (needCache) { // creates an object in the DB for data to be cached Scope selectedScope = pickCacheScopeForCopy(snapshotInfo, destData); destOnStore = cacheMgr.getCacheObject(snapshotInfo, selectedScope); destOnStore.processEvent(Event.CreateOnlyRequested); } if (usingBackendSnapshot && !computeClusterSupportsResign) { String noSupportForResignErrMsg = "Unable to locate an applicable host with which to perform a resignature operation : Cluster ID = " + hostVO.getClusterId(); LOGGER.warn(noSupportForResignErrMsg); throw new CloudRuntimeException(noSupportForResignErrMsg); } try { if (usingBackendSnapshot) { createVolumeFromSnapshot(hostVO, snapshotInfo, true); } DataStore srcDataStore = snapshotInfo.getDataStore(); String value = _configDao.getValue(Config.PrimaryStorageDownloadWait.toString()); int primaryStorageDownloadWait = NumbersUtil.parseInt(value, Integer.parseInt(Config.PrimaryStorageDownloadWait.getDefaultValue())); CopyCommand copyCommand = new CopyCommand(snapshotInfo.getTO(), destOnStore.getTO(), primaryStorageDownloadWait, VirtualMachineManager.ExecuteInSequence.value()); String errMsg = null; CopyCmdAnswer copyCmdAnswer = null; try { // If we are using a back-end snapshot, then we should still have access to it from the hosts in the cluster that hostVO is in // (because we passed in true as the third parameter to createVolumeFromSnapshot above). if (!usingBackendSnapshot) { _volumeService.grantAccess(snapshotInfo, hostVO, srcDataStore); } Map<String, String> srcDetails = getSnapshotDetails(snapshotInfo); copyCommand.setOptions(srcDetails); copyCmdAnswer = (CopyCmdAnswer)_agentMgr.send(hostVO.getId(), copyCommand); if (!copyCmdAnswer.getResult()) { // We were not able to copy. Handle it. errMsg = copyCmdAnswer.getDetails(); throw new CloudRuntimeException(errMsg); } if (needCache) { // If cached storage was needed (in case of object store as secondary // storage), at this point, the data has been copied from the primary // to the NFS cache by the hypervisor. We now invoke another copy // command to copy this data from cache to secondary storage. We // then cleanup the cache destOnStore.processEvent(Event.OperationSuccessed, copyCmdAnswer); CopyCommand cmd = new CopyCommand(destOnStore.getTO(), destData.getTO(), primaryStorageDownloadWait, VirtualMachineManager.ExecuteInSequence.value()); EndPoint ep = selector.select(destOnStore, destData); if (ep == null) { errMsg = "No remote endpoint to send command, check if host or ssvm is down?"; LOGGER.error(errMsg); copyCmdAnswer = new CopyCmdAnswer(errMsg); } else { copyCmdAnswer = (CopyCmdAnswer)ep.sendMessage(cmd); } // clean up snapshot copied to staging cacheMgr.deleteCacheObject(destOnStore); } } catch (CloudRuntimeException | AgentUnavailableException | OperationTimedoutException ex) { String msg = "Failed to create template from snapshot (Snapshot ID = " + snapshotInfo.getId() + ") : "; LOGGER.warn(msg, ex); throw new CloudRuntimeException(msg + ex.getMessage()); } finally { _volumeService.revokeAccess(snapshotInfo, hostVO, srcDataStore); if (copyCmdAnswer == null || !copyCmdAnswer.getResult()) { if (copyCmdAnswer != null && !StringUtils.isEmpty(copyCmdAnswer.getDetails())) { errMsg = copyCmdAnswer.getDetails(); if (needCache) { cacheMgr.deleteCacheObject(destOnStore); } } else { errMsg = "Unable to create template from snapshot"; } } try { if (StringUtils.isEmpty(errMsg)) { snapshotInfo.processEvent(Event.OperationSuccessed); } else { snapshotInfo.processEvent(Event.OperationFailed); } } catch (Exception ex) { LOGGER.warn("Error processing snapshot event: " + ex.getMessage(), ex); } } CopyCommandResult result = new CopyCommandResult(null, copyCmdAnswer); result.setResult(errMsg); callback.complete(result); } finally { if (usingBackendSnapshot) { deleteVolumeFromSnapshot(snapshotInfo); } } } /** * Creates a volume on the storage from a snapshot that resides on the secondary storage (archived snapshot). * @param snapshotInfo snapshot on secondary * @param volumeInfo volume to be created on the storage * @param callback for async */ private void handleCreateVolumeFromSnapshotOnSecondaryStorage(SnapshotInfo snapshotInfo, VolumeInfo volumeInfo, AsyncCompletionCallback<CopyCommandResult> callback) { // at this point, the snapshotInfo and volumeInfo should have the same disk offering ID (so either one should be OK to get a DiskOfferingVO instance) DiskOfferingVO diskOffering = _diskOfferingDao.findByIdIncludingRemoved(volumeInfo.getDiskOfferingId()); SnapshotVO snapshot = _snapshotDao.findById(snapshotInfo.getId()); // update the volume's hv_ss_reserve (hypervisor snapshot reserve) from a disk offering (used for managed storage) _volumeService.updateHypervisorSnapshotReserveForVolume(diskOffering, volumeInfo.getId(), snapshot.getHypervisorType()); CopyCmdAnswer copyCmdAnswer = null; String errMsg = null; HostVO hostVO = null; try { // create a volume on the storage AsyncCallFuture<VolumeApiResult> future = _volumeService.createVolumeAsync(volumeInfo, volumeInfo.getDataStore()); VolumeApiResult result = future.get(); if (result.isFailed()) { LOGGER.error("Failed to create a volume: " + result.getResult()); throw new CloudRuntimeException(result.getResult()); } volumeInfo = _volumeDataFactory.getVolume(volumeInfo.getId(), volumeInfo.getDataStore()); volumeInfo.processEvent(Event.MigrationRequested); volumeInfo = _volumeDataFactory.getVolume(volumeInfo.getId(), volumeInfo.getDataStore()); hostVO = getHost(snapshotInfo.getDataCenterId(), false); // copy the volume from secondary via the hypervisor copyCmdAnswer = performCopyOfVdi(volumeInfo, snapshotInfo, hostVO); if (copyCmdAnswer == null || !copyCmdAnswer.getResult()) { if (copyCmdAnswer != null && !StringUtils.isEmpty(copyCmdAnswer.getDetails())) { errMsg = copyCmdAnswer.getDetails(); } else { errMsg = "Unable to create volume from snapshot"; } } } catch (Exception ex) { errMsg = ex.getMessage() != null ? ex.getMessage() : "Copy operation failed in 'StorageSystemDataMotionStrategy.handleCreateVolumeFromSnapshotBothOnStorageSystem'"; } CopyCommandResult result = new CopyCommandResult(null, copyCmdAnswer); result.setResult(errMsg); callback.complete(result); } /** * Clones a template present on the storage to a new volume and resignatures it. * * @param templateInfo source template * @param volumeInfo destination ROOT volume * @param callback for async */ private void handleCreateVolumeFromTemplateBothOnStorageSystem(TemplateInfo templateInfo, VolumeInfo volumeInfo, AsyncCompletionCallback<CopyCommandResult> callback) { Preconditions.checkArgument(templateInfo != null, "Passing 'null' to templateInfo of handleCreateVolumeFromTemplateBothOnStorageSystem is not supported."); Preconditions.checkArgument(volumeInfo != null, "Passing 'null' to volumeInfo of handleCreateVolumeFromTemplateBothOnStorageSystem is not supported."); CopyCmdAnswer copyCmdAnswer = null; String errMsg = null; HostVO hostVO = getHost(volumeInfo.getDataCenterId(), true); if (hostVO == null) { throw new CloudRuntimeException("Unable to locate a host capable of resigning in the zone with the following ID: " + volumeInfo.getDataCenterId()); } boolean computeClusterSupportsResign = clusterDao.getSupportsResigning(hostVO.getClusterId()); if (!computeClusterSupportsResign) { String noSupportForResignErrMsg = "Unable to locate an applicable host with which to perform a resignature operation : Cluster ID = " + hostVO.getClusterId(); LOGGER.warn(noSupportForResignErrMsg); throw new CloudRuntimeException(noSupportForResignErrMsg); } try { VolumeDetailVO volumeDetail = new VolumeDetailVO(volumeInfo.getId(), "cloneOfTemplate", String.valueOf(templateInfo.getId()), false); volumeDetail = volumeDetailsDao.persist(volumeDetail); AsyncCallFuture<VolumeApiResult> future = _volumeService.createVolumeAsync(volumeInfo, volumeInfo.getDataStore()); int storagePoolMaxWaitSeconds = NumbersUtil.parseInt(_configDao.getValue(Config.StoragePoolMaxWaitSeconds.key()), 3600); VolumeApiResult result = future.get(storagePoolMaxWaitSeconds, TimeUnit.SECONDS); if (volumeDetail != null) { volumeDetailsDao.remove(volumeDetail.getId()); } if (result.isFailed()) { LOGGER.warn("Failed to create a volume: " + result.getResult()); throw new CloudRuntimeException(result.getResult()); } volumeInfo = _volumeDataFactory.getVolume(volumeInfo.getId(), volumeInfo.getDataStore()); volumeInfo.processEvent(Event.MigrationRequested); volumeInfo = _volumeDataFactory.getVolume(volumeInfo.getId(), volumeInfo.getDataStore()); copyCmdAnswer = performResignature(volumeInfo, hostVO); if (copyCmdAnswer == null || !copyCmdAnswer.getResult()) { if (copyCmdAnswer != null && !StringUtils.isEmpty(copyCmdAnswer.getDetails())) { throw new CloudRuntimeException(copyCmdAnswer.getDetails()); } else { throw new CloudRuntimeException("Unable to create a volume from a template"); } } } catch (InterruptedException | ExecutionException | TimeoutException ex ) { volumeInfo.getDataStore().getDriver().deleteAsync(volumeInfo.getDataStore(), volumeInfo, null); throw new CloudRuntimeException("Create volume from template (ID = " + templateInfo.getId() + ") failed " + ex.getMessage()); } CopyCommandResult result = new CopyCommandResult(null, copyCmdAnswer); result.setResult(errMsg); callback.complete(result); } private void handleCreateVolumeFromSnapshotBothOnStorageSystem(SnapshotInfo snapshotInfo, VolumeInfo volumeInfo, AsyncCompletionCallback<CopyCommandResult> callback) { CopyCmdAnswer copyCmdAnswer = null; String errMsg = null; try { HostVO hostVO = getHost(snapshotInfo); boolean usingBackendSnapshot = usingBackendSnapshotFor(snapshotInfo); boolean computeClusterSupportsResign = clusterDao.getSupportsResigning(hostVO.getClusterId()); if (usingBackendSnapshot && !computeClusterSupportsResign) { String noSupportForResignErrMsg = "Unable to locate an applicable host with which to perform a resignature operation : Cluster ID = " + hostVO.getClusterId(); LOGGER.warn(noSupportForResignErrMsg); throw new CloudRuntimeException(noSupportForResignErrMsg); } boolean canStorageSystemCreateVolumeFromVolume = canStorageSystemCreateVolumeFromVolume(snapshotInfo); boolean useCloning = usingBackendSnapshot || (canStorageSystemCreateVolumeFromVolume && computeClusterSupportsResign); VolumeDetailVO volumeDetail = null; if (useCloning) { volumeDetail = new VolumeDetailVO(volumeInfo.getId(), "cloneOfSnapshot", String.valueOf(snapshotInfo.getId()), false); volumeDetail = volumeDetailsDao.persist(volumeDetail); } // at this point, the snapshotInfo and volumeInfo should have the same disk offering ID (so either one should be OK to get a DiskOfferingVO instance) DiskOfferingVO diskOffering = _diskOfferingDao.findByIdIncludingRemoved(volumeInfo.getDiskOfferingId()); SnapshotVO snapshot = _snapshotDao.findById(snapshotInfo.getId()); // update the volume's hv_ss_reserve (hypervisor snapshot reserve) from a disk offering (used for managed storage) _volumeService.updateHypervisorSnapshotReserveForVolume(diskOffering, volumeInfo.getId(), snapshot.getHypervisorType()); AsyncCallFuture<VolumeApiResult> future = _volumeService.createVolumeAsync(volumeInfo, volumeInfo.getDataStore()); VolumeApiResult result = future.get(); if (volumeDetail != null) { volumeDetailsDao.remove(volumeDetail.getId()); } if (result.isFailed()) { LOGGER.warn("Failed to create a volume: " + result.getResult()); throw new CloudRuntimeException(result.getResult()); } volumeInfo = _volumeDataFactory.getVolume(volumeInfo.getId(), volumeInfo.getDataStore()); volumeInfo.processEvent(Event.MigrationRequested); volumeInfo = _volumeDataFactory.getVolume(volumeInfo.getId(), volumeInfo.getDataStore()); if (useCloning) { copyCmdAnswer = performResignature(volumeInfo, hostVO); } else { // asking for a XenServer host here so we don't always prefer to use XenServer hosts that support resigning // even when we don't need those hosts to do this kind of copy work hostVO = getHost(snapshotInfo.getDataCenterId(), false); copyCmdAnswer = performCopyOfVdi(volumeInfo, snapshotInfo, hostVO); } if (copyCmdAnswer == null || !copyCmdAnswer.getResult()) { if (copyCmdAnswer != null && !StringUtils.isEmpty(copyCmdAnswer.getDetails())) { errMsg = copyCmdAnswer.getDetails(); } else { errMsg = "Unable to create volume from snapshot"; } } } catch (Exception ex) { errMsg = ex.getMessage() != null ? ex.getMessage() : "Copy operation failed in 'StorageSystemDataMotionStrategy.handleCreateVolumeFromSnapshotBothOnStorageSystem'"; } CopyCommandResult result = new CopyCommandResult(null, copyCmdAnswer); result.setResult(errMsg); callback.complete(result); } /** * If the underlying storage system is making use of read-only snapshots, this gives the storage system the opportunity to * create a volume from the snapshot so that we can copy the VHD file that should be inside of the snapshot to secondary storage. * * The resultant volume must be writable because we need to resign the SR and the VDI that should be inside of it before we copy * the VHD file to secondary storage. * * If the storage system is using writable snapshots, then nothing need be done by that storage system here because we can just * resign the SR and the VDI that should be inside of the snapshot before copying the VHD file to secondary storage. */ private void createVolumeFromSnapshot(HostVO hostVO, SnapshotInfo snapshotInfo, boolean keepGrantedAccess) { SnapshotDetailsVO snapshotDetails = handleSnapshotDetails(snapshotInfo.getId(), "tempVolume", "create"); try { snapshotInfo.getDataStore().getDriver().createAsync(snapshotInfo.getDataStore(), snapshotInfo, null); } finally { _snapshotDetailsDao.remove(snapshotDetails.getId()); } CopyCmdAnswer copyCmdAnswer = performResignature(snapshotInfo, hostVO, keepGrantedAccess); if (copyCmdAnswer == null || !copyCmdAnswer.getResult()) { if (copyCmdAnswer != null && !StringUtils.isEmpty(copyCmdAnswer.getDetails())) { throw new CloudRuntimeException(copyCmdAnswer.getDetails()); } else { throw new CloudRuntimeException("Unable to create volume from snapshot"); } } } /** * If the underlying storage system needed to create a volume from a snapshot for createVolumeFromSnapshot(HostVO, SnapshotInfo), then * this is its opportunity to delete that temporary volume and restore properties in snapshot_details to the way they were before the * invocation of createVolumeFromSnapshot(HostVO, SnapshotInfo). */ private void deleteVolumeFromSnapshot(SnapshotInfo snapshotInfo) { SnapshotDetailsVO snapshotDetails = handleSnapshotDetails(snapshotInfo.getId(), "tempVolume", "delete"); try { snapshotInfo.getDataStore().getDriver().createAsync(snapshotInfo.getDataStore(), snapshotInfo, null); } finally { _snapshotDetailsDao.remove(snapshotDetails.getId()); } } private SnapshotDetailsVO handleSnapshotDetails(long csSnapshotId, String name, String value) { _snapshotDetailsDao.removeDetail(csSnapshotId, name); SnapshotDetailsVO snapshotDetails = new SnapshotDetailsVO(csSnapshotId, name, value, false); return _snapshotDetailsDao.persist(snapshotDetails); } private boolean canStorageSystemCreateVolumeFromVolume(SnapshotInfo snapshotInfo) { boolean supportsCloningVolumeFromVolume = false; DataStore dataStore = dataStoreMgr.getDataStore(snapshotInfo.getDataStore().getId(), DataStoreRole.Primary); Map<String, String> mapCapabilities = dataStore.getDriver().getCapabilities(); if (mapCapabilities != null) { String value = mapCapabilities.get(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString()); supportsCloningVolumeFromVolume = Boolean.valueOf(value); } return supportsCloningVolumeFromVolume; } private String getProperty(long snapshotId, String property) { SnapshotDetailsVO snapshotDetails = _snapshotDetailsDao.findDetail(snapshotId, property); if (snapshotDetails != null) { return snapshotDetails.getValue(); } return null; } private Map<String, String> getVolumeDetails(VolumeInfo volumeInfo) { Map<String, String> volumeDetails = new HashMap<String, String>(); VolumeVO volumeVO = _volumeDao.findById(volumeInfo.getId()); long storagePoolId = volumeVO.getPoolId(); StoragePoolVO storagePoolVO = _storagePoolDao.findById(storagePoolId); volumeDetails.put(DiskTO.STORAGE_HOST, storagePoolVO.getHostAddress()); volumeDetails.put(DiskTO.STORAGE_PORT, String.valueOf(storagePoolVO.getPort())); volumeDetails.put(DiskTO.IQN, volumeVO.get_iScsiName()); ChapInfo chapInfo = _volumeService.getChapInfo(volumeInfo, volumeInfo.getDataStore()); if (chapInfo != null) { volumeDetails.put(DiskTO.CHAP_INITIATOR_USERNAME, chapInfo.getInitiatorUsername()); volumeDetails.put(DiskTO.CHAP_INITIATOR_SECRET, chapInfo.getInitiatorSecret()); volumeDetails.put(DiskTO.CHAP_TARGET_USERNAME, chapInfo.getTargetUsername()); volumeDetails.put(DiskTO.CHAP_TARGET_SECRET, chapInfo.getTargetSecret()); } return volumeDetails; } private Map<String, String> getSnapshotDetails(SnapshotInfo snapshotInfo) { Map<String, String> snapshotDetails = new HashMap<>(); long storagePoolId = snapshotInfo.getDataStore().getId(); StoragePoolVO storagePoolVO = _storagePoolDao.findById(storagePoolId); snapshotDetails.put(DiskTO.STORAGE_HOST, storagePoolVO.getHostAddress()); snapshotDetails.put(DiskTO.STORAGE_PORT, String.valueOf(storagePoolVO.getPort())); long snapshotId = snapshotInfo.getId(); snapshotDetails.put(DiskTO.IQN, getProperty(snapshotId, DiskTO.IQN)); snapshotDetails.put(DiskTO.CHAP_INITIATOR_USERNAME, getProperty(snapshotId, DiskTO.CHAP_INITIATOR_USERNAME)); snapshotDetails.put(DiskTO.CHAP_INITIATOR_SECRET, getProperty(snapshotId, DiskTO.CHAP_INITIATOR_SECRET)); snapshotDetails.put(DiskTO.CHAP_TARGET_USERNAME, getProperty(snapshotId, DiskTO.CHAP_TARGET_USERNAME)); snapshotDetails.put(DiskTO.CHAP_TARGET_SECRET, getProperty(snapshotId, DiskTO.CHAP_TARGET_SECRET)); return snapshotDetails; } private HostVO getHost(SnapshotInfo snapshotInfo) { HostVO hostVO = getHost(snapshotInfo.getDataCenterId(), true); if (hostVO == null) { hostVO = getHost(snapshotInfo.getDataCenterId(), false); if (hostVO == null) { throw new CloudRuntimeException("Unable to locate an applicable host in data center with ID = " + snapshotInfo.getDataCenterId()); } } return hostVO; } private HostVO getHost(Long zoneId, boolean computeClusterMustSupportResign) { Preconditions.checkArgument(zoneId != null, "Zone ID cannot be null."); List<HostVO> hosts = _hostDao.listByDataCenterIdAndHypervisorType(zoneId, HypervisorType.XenServer); if (hosts == null) { return null; } List<Long> clustersToSkip = new ArrayList<>(); Collections.shuffle(hosts, RANDOM); for (HostVO host : hosts) { if (computeClusterMustSupportResign) { long clusterId = host.getClusterId(); if (clustersToSkip.contains(clusterId)) { continue; } if (clusterDao.getSupportsResigning(clusterId)) { return host; } else { clustersToSkip.add(clusterId); } } else { return host; } } return null; } @Override public void copyAsync(Map<VolumeInfo, DataStore> volumeMap, VirtualMachineTO vmTo, Host srcHost, Host destHost, AsyncCompletionCallback<CopyCommandResult> callback) { CopyCommandResult result = new CopyCommandResult(null, null); result.setResult("Unsupported operation requested for copying data."); callback.complete(result); } private Map<String, String> getDetails(DataObject dataObj) { if (dataObj instanceof VolumeInfo) { return getVolumeDetails((VolumeInfo)dataObj); } else if (dataObj instanceof SnapshotInfo) { return getSnapshotDetails((SnapshotInfo)dataObj); } throw new CloudRuntimeException("'dataObj' must be of type 'VolumeInfo' or 'SnapshotInfo'."); } private CopyCmdAnswer performResignature(DataObject dataObj, HostVO hostVO) { return performResignature(dataObj, hostVO, false); } private CopyCmdAnswer performResignature(DataObject dataObj, HostVO hostVO, boolean keepGrantedAccess) { long storagePoolId = dataObj.getDataStore().getId(); DataStore dataStore = dataStoreMgr.getDataStore(storagePoolId, DataStoreRole.Primary); Map<String, String> details = getDetails(dataObj); ResignatureCommand command = new ResignatureCommand(details); ResignatureAnswer answer = null; try { _volumeService.grantAccess(dataObj, hostVO, dataStore); answer = (ResignatureAnswer)_agentMgr.send(hostVO.getId(), command); } catch (CloudRuntimeException | AgentUnavailableException | OperationTimedoutException ex) { keepGrantedAccess = false; String msg = "Failed to resign the DataObject with the following ID: " + dataObj.getId(); LOGGER.warn(msg, ex); throw new CloudRuntimeException(msg + ex.getMessage()); } finally { if (keepGrantedAccess == false) { _volumeService.revokeAccess(dataObj, hostVO, dataStore); } } if (answer == null || !answer.getResult()) { final String errMsg; if (answer != null && answer.getDetails() != null && !answer.getDetails().isEmpty()) { errMsg = answer.getDetails(); } else { errMsg = "Unable to perform resignature operation in 'StorageSystemDataMotionStrategy.performResignature'"; } throw new CloudRuntimeException(errMsg); } VolumeObjectTO newVolume = new VolumeObjectTO(); newVolume.setSize(answer.getSize()); newVolume.setPath(answer.getPath()); newVolume.setFormat(answer.getFormat()); return new CopyCmdAnswer(newVolume); } protected DataObject cacheSnapshotChain(SnapshotInfo snapshot, Scope scope) { DataObject leafData = null; DataStore store = cacheMgr.getCacheStorage(snapshot, scope); while (snapshot != null) { DataObject cacheData = cacheMgr.createCacheObject(snapshot, store); if (leafData == null) { leafData = cacheData; } snapshot = snapshot.getParent(); } return leafData; } /** * Copies data from secondary storage to a primary volume * @param volumeInfo The primary volume * @param snapshotInfo destination of the copy * @param hostVO the host used to copy the data * @return result of the copy */ private CopyCmdAnswer performCopyOfVdi(VolumeInfo volumeInfo, SnapshotInfo snapshotInfo, HostVO hostVO) { Snapshot.LocationType locationType = snapshotInfo.getLocationType(); String value = _configDao.getValue(Config.PrimaryStorageDownloadWait.toString()); int primaryStorageDownloadWait = NumbersUtil.parseInt(value, Integer.parseInt(Config.PrimaryStorageDownloadWait.getDefaultValue())); DataObject srcData = snapshotInfo; CopyCmdAnswer copyCmdAnswer = null; DataObject cacheData = null; boolean needCacheStorage = needCacheStorage(snapshotInfo, volumeInfo); if (needCacheStorage) { cacheData = cacheSnapshotChain(snapshotInfo, new ZoneScope(volumeInfo.getDataCenterId())); srcData = cacheData; } CopyCommand copyCommand = new CopyCommand(srcData.getTO(), volumeInfo.getTO(), primaryStorageDownloadWait, VirtualMachineManager.ExecuteInSequence.value()); try { if (Snapshot.LocationType.PRIMARY.equals(locationType)) { _volumeService.grantAccess(snapshotInfo, hostVO, snapshotInfo.getDataStore()); Map<String, String> srcDetails = getSnapshotDetails(snapshotInfo); copyCommand.setOptions(srcDetails); } _volumeService.grantAccess(volumeInfo, hostVO, volumeInfo.getDataStore()); Map<String, String> destDetails = getVolumeDetails(volumeInfo); copyCommand.setOptions2(destDetails); copyCmdAnswer = (CopyCmdAnswer)_agentMgr.send(hostVO.getId(), copyCommand); } catch (CloudRuntimeException | AgentUnavailableException | OperationTimedoutException ex) { String msg = "Failed to perform VDI copy : "; LOGGER.warn(msg, ex); throw new CloudRuntimeException(msg + ex.getMessage()); } finally { if (Snapshot.LocationType.PRIMARY.equals(locationType)) { _volumeService.revokeAccess(snapshotInfo, hostVO, snapshotInfo.getDataStore()); } _volumeService.revokeAccess(volumeInfo, hostVO, volumeInfo.getDataStore()); if (needCacheStorage && copyCmdAnswer != null && copyCmdAnswer.getResult()) { cacheMgr.deleteCacheObject(cacheData); } } return copyCmdAnswer; } }