// 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.snapshot; import com.cloud.agent.AgentManager; import com.cloud.agent.api.to.DiskTO; import com.cloud.dc.dao.ClusterDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.org.Cluster; import com.cloud.org.Grouping.AllocationState; import com.cloud.resource.ResourceState; import com.cloud.server.ManagementService; import com.cloud.storage.CreateSnapshotPayload; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotVO; import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; 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.utils.db.DB; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.fsm.NoTransitionException; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; 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.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotResult; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; import org.apache.cloudstack.storage.command.SnapshotAndCopyAnswer; import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; 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; @Component public class StorageSystemSnapshotStrategy extends SnapshotStrategyBase { private static final Logger s_logger = Logger.getLogger(StorageSystemSnapshotStrategy.class); @Inject private AgentManager agentMgr; @Inject private ClusterDao clusterDao; @Inject private DataStoreManager dataStoreMgr; @Inject private HostDao hostDao; @Inject private ManagementService mgr; @Inject private PrimaryDataStoreDao storagePoolDao; @Inject private SnapshotDao snapshotDao; @Inject private SnapshotDataFactory snapshotDataFactory; @Inject private SnapshotDetailsDao snapshotDetailsDao; @Inject private VMInstanceDao vmInstanceDao; @Inject private VolumeDao volumeDao; @Inject private VolumeService volService; @Override public SnapshotInfo backupSnapshot(SnapshotInfo snapshotInfo) { Preconditions.checkArgument(snapshotInfo != null, "backupSnapshot expects a valid snapshot"); if (snapshotInfo.getLocationType() != Snapshot.LocationType.SECONDARY) { markAsBackedUp((SnapshotObject)snapshotInfo); return snapshotInfo; } // At this point, the snapshot is either taken as a native // snapshot on the storage or exists as a volume on the storage (clone). // If archive flag is passed in, we should copy this snapshot to secondary // storage and delete it from the primary storage. HostVO host = getHost(snapshotInfo.getVolumeId()); boolean canStorageSystemCreateVolumeFromSnapshot = canStorageSystemCreateVolumeFromSnapshot(snapshotInfo.getBaseVolume().getPoolId()); boolean computeClusterSupportsResign = clusterDao.getSupportsResigning(host.getClusterId()); if (!canStorageSystemCreateVolumeFromSnapshot || !computeClusterSupportsResign) { String msg = "Cannot archive snapshot: canStorageSystemCreateVolumeFromSnapshot and/or computeClusterSupportsResign were false."; s_logger.warn(msg); throw new CloudRuntimeException(msg); } return snapshotSvr.backupSnapshot(snapshotInfo); } @Override public boolean deleteSnapshot(Long snapshotId) { SnapshotVO snapshotVO = snapshotDao.findById(snapshotId); if (Snapshot.State.Destroyed.equals(snapshotVO.getState())) { return true; } if (Snapshot.State.Error.equals(snapshotVO.getState())) { snapshotDao.remove(snapshotId); return true; } if (!Snapshot.State.BackedUp.equals(snapshotVO.getState())) { throw new InvalidParameterValueException("Unable to delete snapshotshot " + snapshotId + " because it is in the following state: " + snapshotVO.getState()); } return cleanupSnapshotOnPrimaryStore(snapshotId); } /** * Cleans up a snapshot which was taken on a primary store. This function * removes * * @param snapshotId: ID of snapshot that needs to be removed * @return true if snapshot is removed, false otherwise */ private boolean cleanupSnapshotOnPrimaryStore(long snapshotId) { SnapshotObject snapshotObj = (SnapshotObject)snapshotDataFactory.getSnapshot(snapshotId, DataStoreRole.Primary); if (snapshotObj == null) { s_logger.debug("Can't find snapshot; deleting it in DB"); snapshotDao.remove(snapshotId); return true; } if (ObjectInDataStoreStateMachine.State.Copying.equals(snapshotObj.getStatus())) { throw new InvalidParameterValueException("Unable to delete snapshotshot " + snapshotId + " because it is in the copying state."); } try { snapshotObj.processEvent(Snapshot.Event.DestroyRequested); } catch (NoTransitionException e) { s_logger.debug("Failed to set the state to destroying: ", e); return false; } try { snapshotSvr.deleteSnapshot(snapshotObj); snapshotObj.processEvent(Snapshot.Event.OperationSucceeded); } catch (Exception e) { s_logger.debug("Failed to delete snapshot: ", e); try { snapshotObj.processEvent(Snapshot.Event.OperationFailed); } catch (NoTransitionException e1) { s_logger.debug("Failed to change snapshot state: " + e.toString()); } return false; } return true; } @Override public boolean revertSnapshot(SnapshotInfo snapshot) { throw new UnsupportedOperationException("Reverting not supported. Create a template or volume based on the snapshot instead."); } @Override @DB public SnapshotInfo takeSnapshot(SnapshotInfo snapshotInfo) { VolumeInfo volumeInfo = snapshotInfo.getBaseVolume(); if (volumeInfo.getFormat() != ImageFormat.VHD) { throw new CloudRuntimeException("Only the " + ImageFormat.VHD.toString() + " image type is currently supported."); } SnapshotVO snapshotVO = snapshotDao.acquireInLockTable(snapshotInfo.getId()); if (snapshotVO == null) { throw new CloudRuntimeException("Failed to acquire lock on the following snapshot: " + snapshotInfo.getId()); } SnapshotResult result = null; SnapshotInfo snapshotOnPrimary = null; SnapshotInfo backedUpSnapshot = null; try { volumeInfo.stateTransit(Volume.Event.SnapshotRequested); // only XenServer is currently supported HostVO hostVO = getHost(volumeInfo.getId()); boolean canStorageSystemCreateVolumeFromSnapshot = canStorageSystemCreateVolumeFromSnapshot(volumeInfo.getPoolId()); boolean computeClusterSupportsResign = clusterDao.getSupportsResigning(hostVO.getClusterId()); // if canStorageSystemCreateVolumeFromSnapshot && computeClusterSupportsResign, then take a back-end snapshot or create a back-end clone; // else, just create a new back-end volume (eventually used to create a new SR on and to copy a VDI to) if (canStorageSystemCreateVolumeFromSnapshot && computeClusterSupportsResign) { SnapshotDetailsVO snapshotDetail = new SnapshotDetailsVO(snapshotInfo.getId(), "takeSnapshot", Boolean.TRUE.toString(), false); snapshotDetailsDao.persist(snapshotDetail); } result = snapshotSvr.takeSnapshot(snapshotInfo); if (result.isFailed()) { s_logger.debug("Failed to take a snapshot: " + result.getResult()); throw new CloudRuntimeException(result.getResult()); } if (!canStorageSystemCreateVolumeFromSnapshot || !computeClusterSupportsResign) { performSnapshotAndCopyOnHostSide(volumeInfo, snapshotInfo); } snapshotOnPrimary = result.getSnapshot(); backedUpSnapshot = backupSnapshot(snapshotOnPrimary); updateLocationTypeInDb(backedUpSnapshot); } finally { if (result != null && result.isSuccess()) { volumeInfo.stateTransit(Volume.Event.OperationSucceeded); if (snapshotOnPrimary != null && snapshotInfo.getLocationType() == Snapshot.LocationType.SECONDARY) { // remove the snapshot on primary storage try { snapshotSvr.deleteSnapshot(snapshotOnPrimary); } catch (Exception e) { s_logger.warn("Failed to clean up snapshot on primary Id:" + snapshotOnPrimary.getId() + " " + e.getMessage()); } } } else { volumeInfo.stateTransit(Volume.Event.OperationFailed); } } snapshotDao.releaseFromLockTable(snapshotInfo.getId()); return backedUpSnapshot; } private void updateLocationTypeInDb(SnapshotInfo snapshotInfo) { Object objPayload = snapshotInfo.getPayload(); if (objPayload instanceof CreateSnapshotPayload) { CreateSnapshotPayload payload = (CreateSnapshotPayload)objPayload; SnapshotVO snapshot = snapshotDao.findById(snapshotInfo.getId()); snapshot.setLocationType(payload.getLocationType()); snapshotDao.update(snapshotInfo.getId(), snapshot); } } private boolean canStorageSystemCreateVolumeFromSnapshot(long storagePoolId) { boolean supportsCloningVolumeFromSnapshot = false; DataStore dataStore = dataStoreMgr.getDataStore(storagePoolId, DataStoreRole.Primary); Map<String, String> mapCapabilities = dataStore.getDriver().getCapabilities(); if (mapCapabilities != null) { String value = mapCapabilities.get(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString()); supportsCloningVolumeFromSnapshot = Boolean.valueOf(value); } return supportsCloningVolumeFromSnapshot; } private void performSnapshotAndCopyOnHostSide(VolumeInfo volumeInfo, SnapshotInfo snapshotInfo) { Map<String, String> sourceDetails = null; VolumeVO volumeVO = volumeDao.findById(volumeInfo.getId()); Long vmInstanceId = volumeVO.getInstanceId(); VMInstanceVO vmInstanceVO = vmInstanceDao.findById(vmInstanceId); Long hostId = null; // if the volume to snapshot is associated with a VM if (vmInstanceVO != null) { hostId = vmInstanceVO.getHostId(); // if the VM is not associated with a host if (hostId == null) { hostId = vmInstanceVO.getLastHostId(); if (hostId == null) { sourceDetails = getSourceDetails(volumeInfo); } } } // volume to snapshot is not associated with a VM (could be a data disk in the detached state) else { sourceDetails = getSourceDetails(volumeInfo); } HostVO hostVO = null; if (hostId != null) { hostVO = hostDao.findById(hostId); } else { Optional<HostVO> optHostVO = getHost(volumeInfo.getDataCenterId(), false); if (optHostVO.isPresent()) { hostVO = optHostVO.get(); } } if (hostVO == null) { final String errMsg = "Unable to locate an applicable host"; s_logger.error("performSnapshotAndCopyOnHostSide: " + errMsg); throw new CloudRuntimeException(errMsg); } long storagePoolId = volumeVO.getPoolId(); StoragePoolVO storagePoolVO = storagePoolDao.findById(storagePoolId); DataStore dataStore = dataStoreMgr.getDataStore(storagePoolId, DataStoreRole.Primary); Map<String, String> destDetails = getDestDetails(storagePoolVO, snapshotInfo); SnapshotAndCopyCommand snapshotAndCopyCommand = new SnapshotAndCopyCommand(volumeInfo.getPath(), sourceDetails, destDetails); SnapshotAndCopyAnswer snapshotAndCopyAnswer = null; try { // if sourceDetails != null, we need to connect the host(s) to the volume if (sourceDetails != null) { volService.grantAccess(volumeInfo, hostVO, dataStore); } volService.grantAccess(snapshotInfo, hostVO, dataStore); snapshotAndCopyAnswer = (SnapshotAndCopyAnswer)agentMgr.send(hostVO.getId(), snapshotAndCopyCommand); } catch (Exception ex) { throw new CloudRuntimeException(ex.getMessage()); } finally { try { volService.revokeAccess(snapshotInfo, hostVO, dataStore); // if sourceDetails != null, we need to disconnect the host(s) from the volume if (sourceDetails != null) { volService.revokeAccess(volumeInfo, hostVO, dataStore); } } catch (Exception ex) { s_logger.debug(ex.getMessage(), ex); } } if (snapshotAndCopyAnswer == null || !snapshotAndCopyAnswer.getResult()) { final String errMsg; if (snapshotAndCopyAnswer != null && snapshotAndCopyAnswer.getDetails() != null && !snapshotAndCopyAnswer.getDetails().isEmpty()) { errMsg = snapshotAndCopyAnswer.getDetails(); } else { errMsg = "Unable to perform host-side operation"; } throw new CloudRuntimeException(errMsg); } String path = snapshotAndCopyAnswer.getPath(); // for XenServer, this is the VDI's UUID SnapshotDetailsVO snapshotDetail = new SnapshotDetailsVO(snapshotInfo.getId(), DiskTO.PATH, path, false); snapshotDetailsDao.persist(snapshotDetail); } private Map<String, String> getSourceDetails(VolumeInfo volumeInfo) { Map<String, String> sourceDetails = new HashMap<>(); VolumeVO volumeVO = volumeDao.findById(volumeInfo.getId()); long storagePoolId = volumeVO.getPoolId(); StoragePoolVO storagePoolVO = storagePoolDao.findById(storagePoolId); sourceDetails.put(DiskTO.STORAGE_HOST, storagePoolVO.getHostAddress()); sourceDetails.put(DiskTO.STORAGE_PORT, String.valueOf(storagePoolVO.getPort())); sourceDetails.put(DiskTO.IQN, volumeVO.get_iScsiName()); ChapInfo chapInfo = volService.getChapInfo(volumeInfo, volumeInfo.getDataStore()); if (chapInfo != null) { sourceDetails.put(DiskTO.CHAP_INITIATOR_USERNAME, chapInfo.getInitiatorUsername()); sourceDetails.put(DiskTO.CHAP_INITIATOR_SECRET, chapInfo.getInitiatorSecret()); sourceDetails.put(DiskTO.CHAP_TARGET_USERNAME, chapInfo.getTargetUsername()); sourceDetails.put(DiskTO.CHAP_TARGET_SECRET, chapInfo.getTargetSecret()); } return sourceDetails; } private Map<String, String> getDestDetails(StoragePoolVO storagePoolVO, SnapshotInfo snapshotInfo) { Map<String, String> destDetails = new HashMap<>(); destDetails.put(DiskTO.STORAGE_HOST, storagePoolVO.getHostAddress()); destDetails.put(DiskTO.STORAGE_PORT, String.valueOf(storagePoolVO.getPort())); long snapshotId = snapshotInfo.getId(); destDetails.put(DiskTO.IQN, getProperty(snapshotId, DiskTO.IQN)); destDetails.put(DiskTO.CHAP_INITIATOR_USERNAME, getProperty(snapshotId, DiskTO.CHAP_INITIATOR_USERNAME)); destDetails.put(DiskTO.CHAP_INITIATOR_SECRET, getProperty(snapshotId, DiskTO.CHAP_INITIATOR_SECRET)); destDetails.put(DiskTO.CHAP_TARGET_USERNAME, getProperty(snapshotId, DiskTO.CHAP_TARGET_USERNAME)); destDetails.put(DiskTO.CHAP_TARGET_SECRET, getProperty(snapshotId, DiskTO.CHAP_TARGET_SECRET)); return destDetails; } private String getProperty(long snapshotId, String property) { SnapshotDetailsVO snapshotDetails = snapshotDetailsDao.findDetail(snapshotId, property); if (snapshotDetails != null) { return snapshotDetails.getValue(); } return null; } private HostVO getHost(long volumeId) { VolumeVO volumeVO = volumeDao.findById(volumeId); Long vmInstanceId = volumeVO.getInstanceId(); VMInstanceVO vmInstanceVO = vmInstanceDao.findById(vmInstanceId); Long hostId = null; // if the volume to snapshot is associated with a VM if (vmInstanceVO != null) { hostId = vmInstanceVO.getHostId(); // if the VM is not associated with a host if (hostId == null) { hostId = vmInstanceVO.getLastHostId(); } } return getHost(volumeVO.getDataCenterId(), hostId); } private HostVO getHost(long zoneId, Long hostId) { Optional<HostVO> optHostVO = getHost(zoneId, true); if (optHostVO.isPresent()) { return optHostVO.get(); } HostVO hostVO = hostDao.findById(hostId); if (hostVO != null) { return hostVO; } optHostVO = getHost(zoneId, false); if (optHostVO.isPresent()) { return optHostVO.get(); } throw new CloudRuntimeException("Unable to locate an applicable host"); } private Optional<HostVO> getHost(long zoneId, boolean computeClusterMustSupportResign) { List<? extends Cluster> clusters = mgr.searchForClusters(zoneId, 0L, Long.MAX_VALUE, HypervisorType.XenServer.toString()); if (clusters == null) { clusters = new ArrayList<>(); } Collections.shuffle(clusters, new Random(System.nanoTime())); clusters: for (Cluster cluster : clusters) { if (cluster.getAllocationState() == AllocationState.Enabled) { List<HostVO> hosts = hostDao.findByClusterId(cluster.getId()); if (hosts != null) { Collections.shuffle(hosts, new Random(System.nanoTime())); for (HostVO host : hosts) { if (host.getResourceState() == ResourceState.Enabled) { if (computeClusterMustSupportResign) { if (clusterDao.getSupportsResigning(cluster.getId())) { return Optional.of(host); } else { // no other host in the cluster in question should be able to satisfy our requirements here, so move on to the next cluster continue clusters; } } else { return Optional.of(host); } } } } } } return Optional.absent(); } private void markAsBackedUp(SnapshotObject snapshotObj) { try { snapshotObj.processEvent(Snapshot.Event.BackupToSecondary); snapshotObj.processEvent(Snapshot.Event.OperationSucceeded); } catch (NoTransitionException ex) { s_logger.debug("Failed to change state: " + ex.toString()); try { snapshotObj.processEvent(Snapshot.Event.OperationFailed); } catch (NoTransitionException ex2) { s_logger.debug("Failed to change state: " + ex2.toString()); } } } @Override public StrategyPriority canHandle(Snapshot snapshot, SnapshotOperation op) { if (SnapshotOperation.REVERT.equals(op)) { return StrategyPriority.CANT_HANDLE; } long volumeId = snapshot.getVolumeId(); VolumeVO volumeVO = volumeDao.findByIdIncludingRemoved(volumeId); long storagePoolId = volumeVO.getPoolId(); DataStore dataStore = dataStoreMgr.getDataStore(storagePoolId, DataStoreRole.Primary); Snapshot.LocationType locationType = snapshot.getLocationType(); // If the snapshot exists on Secondary Storage, we can't delete it. if (SnapshotOperation.DELETE.equals(op) && Snapshot.LocationType.SECONDARY.equals(locationType)) { return StrategyPriority.CANT_HANDLE; } if (dataStore != null) { Map<String, String> mapCapabilities = dataStore.getDriver().getCapabilities(); if (mapCapabilities != null) { String value = mapCapabilities.get(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString()); Boolean supportsStorageSystemSnapshots = Boolean.valueOf(value); if (supportsStorageSystemSnapshots) { return StrategyPriority.HIGHEST; } } } return StrategyPriority.CANT_HANDLE; } }