package org.ovirt.engine.core.bll.snapshots; import static org.ovirt.engine.core.bll.storage.disk.image.DisksFilter.ONLY_ACTIVE; import static org.ovirt.engine.core.bll.storage.disk.image.DisksFilter.ONLY_NOT_SHAREABLE; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import javax.inject.Inject; import org.apache.commons.lang.StringUtils; import org.ovirt.engine.core.bll.ConcurrentChildCommandsExecutionCallback; import org.ovirt.engine.core.bll.DisableInPrepareMode; import org.ovirt.engine.core.bll.LockMessagesMatchUtil; import org.ovirt.engine.core.bll.VmCommand; import org.ovirt.engine.core.bll.context.CommandContext; import org.ovirt.engine.core.bll.quota.QuotaConsumptionParameter; import org.ovirt.engine.core.bll.quota.QuotaStorageDependent; import org.ovirt.engine.core.bll.storage.disk.image.DisksFilter; import org.ovirt.engine.core.bll.storage.disk.image.ImagesHandler; import org.ovirt.engine.core.bll.tasks.CommandCoordinatorUtil; import org.ovirt.engine.core.bll.tasks.interfaces.CommandCallback; import org.ovirt.engine.core.bll.validator.VmValidator; import org.ovirt.engine.core.bll.validator.storage.DiskImagesValidator; import org.ovirt.engine.core.bll.validator.storage.MultipleStorageDomainsValidator; import org.ovirt.engine.core.bll.validator.storage.StoragePoolValidator; import org.ovirt.engine.core.common.AuditLogType; import org.ovirt.engine.core.common.FeatureSupported; import org.ovirt.engine.core.common.VdcObjectType; import org.ovirt.engine.core.common.action.ImagesContainterParametersBase; import org.ovirt.engine.core.common.action.LockProperties; import org.ovirt.engine.core.common.action.LockProperties.Scope; import org.ovirt.engine.core.common.action.RemoveMemoryVolumesParameters; import org.ovirt.engine.core.common.action.RemoveSnapshotParameters; import org.ovirt.engine.core.common.action.RemoveSnapshotSingleDiskParameters; import org.ovirt.engine.core.common.action.VdcActionParametersBase; import org.ovirt.engine.core.common.action.VdcActionParametersBase.EndProcedure; import org.ovirt.engine.core.common.action.VdcActionType; import org.ovirt.engine.core.common.action.VdcReturnValueBase; import org.ovirt.engine.core.common.asynctasks.EntityInfo; import org.ovirt.engine.core.common.businessentities.Snapshot; import org.ovirt.engine.core.common.businessentities.Snapshot.SnapshotStatus; import org.ovirt.engine.core.common.businessentities.SubchainInfo; import org.ovirt.engine.core.common.businessentities.VM; import org.ovirt.engine.core.common.businessentities.storage.CinderDisk; import org.ovirt.engine.core.common.businessentities.storage.Disk; import org.ovirt.engine.core.common.businessentities.storage.DiskImage; import org.ovirt.engine.core.common.businessentities.storage.DiskStorageType; import org.ovirt.engine.core.common.errors.EngineError; import org.ovirt.engine.core.common.errors.EngineException; import org.ovirt.engine.core.common.errors.EngineMessage; import org.ovirt.engine.core.common.locks.LockingGroup; import org.ovirt.engine.core.common.utils.Pair; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.dao.DiskDao; import org.ovirt.engine.core.dao.DiskImageDao; import org.ovirt.engine.core.dao.SnapshotDao; import org.ovirt.engine.core.dao.VmTemplateDao; import org.ovirt.engine.core.utils.ovf.OvfManager; import org.ovirt.engine.core.utils.transaction.TransactionSupport; /** * Merges snapshots either live or non-live based on VM status */ @DisableInPrepareMode public class RemoveSnapshotCommand<T extends RemoveSnapshotParameters> extends VmCommand<T> implements QuotaStorageDependent { private List<DiskImage> _sourceImages = null; @Inject private OvfManager ovfManager; @Inject private DiskImageDao diskImageDao; @Inject private DiskDao diskDao; @Inject private VmTemplateDao vmTemplateDao; @Inject private SnapshotDao snapshotDao; public RemoveSnapshotCommand(T parameters, CommandContext context) { super(parameters, context); } public RemoveSnapshotCommand(Guid commandId) { super(commandId); } @Override protected LockProperties applyLockProperties(LockProperties lockProperties) { return lockProperties.withScope(Scope.Execution); } private void initializeObjectState() { if (StringUtils.isEmpty(getSnapshotName())) { Snapshot snapshot = snapshotDao.get(getParameters().getSnapshotId()); if (snapshot != null) { setSnapshotName(snapshot.getDescription()); getParameters().setUseCinderCommandCallback( !DisksFilter.filterCinderDisks(getSourceImages()).isEmpty()); } } setStoragePoolId(getVm().getStoragePoolId()); } @Override public Map<String, String> getJobMessageProperties() { if (jobProperties == null) { jobProperties = super.getJobMessageProperties(); initializeObjectState(); jobProperties.put(VdcObjectType.Snapshot.name().toLowerCase(), getSnapshotName()); } return jobProperties; } /** * @return The image snapshots associated with the VM snapshot. * Note that the first time this method is run it issues Dao call. */ protected List<DiskImage> getSourceImages() { if (_sourceImages == null) { _sourceImages = diskImageDao.getAllSnapshotsForVmSnapshot(getParameters().getSnapshotId()); } return _sourceImages; } @Override protected void executeCommand() { if (!getVm().isDown() && !getVm().isQualifiedForSnapshotMerge()) { log.error("Cannot remove VM snapshot. Vm is not Down, Up or Paused"); throw new EngineException(EngineError.VM_NOT_QUALIFIED_FOR_SNAPSHOT_MERGE); } final Snapshot snapshot = snapshotDao.get(getParameters().getSnapshotId()); boolean snapshotHasImages = hasImages(); boolean removeSnapshotMemory = isMemoryVolumeRemoveable(snapshot.getMemoryVolume()); // If the VM hasn't got any images and memory - simply remove the snapshot. // No need for locking, VDSM tasks, and all that jazz. if (!snapshotHasImages && !removeSnapshotMemory) { snapshotDao.remove(getParameters().getSnapshotId()); setSucceeded(true); return; } lockSnapshot(snapshot); freeLock(); getParameters().setEntityInfo(new EntityInfo(VdcObjectType.VM, getVmId())); boolean useTaskManagerToRemoveMemory = false; if (snapshotHasImages) { removeImages(); if (getSnapshotActionType() == VdcActionType.RemoveSnapshotSingleDiskLive) { persistCommand(getParameters().getParentCommand(), true); useTaskManagerToRemoveMemory = true; } } if (removeSnapshotMemory) { removeMemory(snapshot, useTaskManagerToRemoveMemory); if (!snapshotHasImages) { // no async tasks - ending command manually endVmCommand(); } } setSucceeded(true); } /** * There is a one to many relation between memory volumes and snapshots, so memory * volumes should be removed only if the only snapshot that points to them is removed */ protected boolean isMemoryVolumeRemoveable(String memoryVolume) { return !memoryVolume.isEmpty() && snapshotDao.getNumOfSnapshotsByMemory(memoryVolume) == 1; } private void removeMemory(final Snapshot snapshot, boolean useTaskManager) { RemoveMemoryVolumesParameters parameters = new RemoveMemoryVolumesParameters(snapshot.getMemoryVolume(), getVmId()); if (useTaskManager) { CommandCoordinatorUtil.executeAsyncCommand(VdcActionType.RemoveMemoryVolumes, parameters, cloneContextAndDetachFromParent()); } else { VdcReturnValueBase ret = runInternalAction(VdcActionType.RemoveMemoryVolumes, parameters); if (!ret.getSucceeded()) { log.error("Cannot remove memory volumes for snapshot '{}'", snapshot.getId()); } } } private void removeImages() { List<CinderDisk> cinderDisks = new ArrayList<>(); for (final DiskImage source : getSourceImages()) { if (source.getDiskStorageType() == DiskStorageType.CINDER) { cinderDisks.add((CinderDisk) source); continue; } // The following is ok because we have tested in the validate that the vm // is not a template and the vm is not in preview mode and that // this is not the active snapshot. List<DiskImage> images = diskImageDao.getAllSnapshotsForParent(source.getImageId()); DiskImage dest = null; if (!images.isEmpty()) { dest = images.get(0); } if (getSnapshotActionType() == VdcActionType.RemoveSnapshotSingleDiskLive) { CommandCoordinatorUtil.executeAsyncCommand( getSnapshotActionType(), buildRemoveSnapshotSingleDiskParameters(source, dest, getSnapshotActionType()), cloneContextAndDetachFromParent()); } else { RemoveSnapshotSingleDiskParameters parameters = buildRemoveSnapshotSingleDiskParameters( source, dest, getSnapshotActionType()); VdcReturnValueBase vdcReturnValue = runInternalActionWithTasksContext( getSnapshotActionType(), parameters); getTaskIdList().addAll(vdcReturnValue.getInternalVdsmTaskIdList()); } List<Guid> quotasToRemoveFromCache = new ArrayList<>(); quotasToRemoveFromCache.add(source.getQuotaId()); if (dest != null) { quotasToRemoveFromCache.add(dest.getQuotaId()); } getQuotaManager().removeQuotaFromCache(getStoragePoolId(), quotasToRemoveFromCache); } if (!cinderDisks.isEmpty()) { handleCinderSnapshotDisks(cinderDisks); } } private void handleCinderSnapshotDisks(List<CinderDisk> cinderDisks) { for (CinderDisk cinderDisk : cinderDisks) { VdcReturnValueBase vdcReturnValueBase = runInternalAction( VdcActionType.RemoveCinderSnapshotDisk, buildRemoveCinderSnapshotDiskParameters(cinderDisk), cloneContextAndDetachFromParent()); if (!vdcReturnValueBase.getSucceeded()) { log.error("Error removing snapshots for Cinder disk"); } } } private void lockSnapshot(final Snapshot snapshot) { TransactionSupport.executeInNewTransaction(() -> { getCompensationContext().snapshotEntityStatus(snapshot); snapshotDao.updateStatus(getParameters().getSnapshotId(), SnapshotStatus.LOCKED); getCompensationContext().stateChanged(); return null; }); } private RemoveSnapshotSingleDiskParameters buildRemoveSnapshotSingleDiskParameters(final DiskImage source, DiskImage dest, VdcActionType snapshotActionType) { RemoveSnapshotSingleDiskParameters parameters = new RemoveSnapshotSingleDiskParameters(source.getImageId(), getVmId()); parameters.setStorageDomainId(source.getStorageIds().get(0)); parameters.setDestinationImageId(dest != null ? dest.getImageId() : null); parameters.setEntityInfo(getParameters().getEntityInfo()); parameters.setParentParameters(getParameters()); parameters.setParentCommand(getActionType()); parameters.setCommandType(snapshotActionType); parameters.setVdsId(getVm().getRunOnVds()); parameters.setEndProcedure(EndProcedure.COMMAND_MANAGED); return parameters; } private ImagesContainterParametersBase buildRemoveCinderSnapshotDiskParameters(CinderDisk cinderDisk) { ImagesContainterParametersBase removeCinderSnapshotParams = new ImagesContainterParametersBase(cinderDisk.getImageId()); removeCinderSnapshotParams.setDestinationImageId(cinderDisk.getImageId()); removeCinderSnapshotParams.setStorageDomainId(cinderDisk.getStorageIds().get(0)); removeCinderSnapshotParams.setParentCommand(getActionType()); removeCinderSnapshotParams.setParentParameters(getParameters()); return removeCinderSnapshotParams; } @Override protected void endVmCommand() { initializeObjectState(); if (getParameters().getTaskGroupSuccess()) { snapshotDao.remove(getParameters().getSnapshotId()); } else { List<String> failedToRemoveDisks = new ArrayList<>(); Snapshot snapshot = snapshotDao.get(getParameters().getSnapshotId()); for (VdcActionParametersBase parameters : getParameters().getImagesParameters()) { ImagesContainterParametersBase imagesParams = parameters instanceof ImagesContainterParametersBase ? (ImagesContainterParametersBase) parameters : null; if (imagesParams == null) { // Shouldn't happen as for now ImagesParameters list contains only // instances of ImagesContainterParametersBase objects. continue; } if (imagesParams.getTaskGroupSuccess()) { snapshot = ImagesHandler.prepareSnapshotConfigWithoutImageSingleImage( snapshot, imagesParams.getImageId(), ovfManager); } else { log.error("Could not delete image '{}' from snapshot '{}'", imagesParams.getImageId(), getParameters().getSnapshotId()); DiskImage diskImage = diskImageDao.getSnapshotById(imagesParams.getImageId()); failedToRemoveDisks.add(diskImage.getDiskAlias()); } } // Remove memory volume and update the dao. // Note: on failure, we can treat memory volume deletion as deleting an image // and remove it from the snapshot entity (rollback isn't applicable). snapshot.setMemoryVolume(""); snapshotDao.update(snapshot); if (!failedToRemoveDisks.isEmpty()) { addCustomValue("DiskAliases", StringUtils.join(failedToRemoveDisks, ", ")); auditLogDirector.log(this, AuditLogType.USER_REMOVE_SNAPSHOT_FINISHED_FAILURE_PARTIAL_SNAPSHOT); } snapshotDao.updateStatus(getParameters().getSnapshotId(), SnapshotStatus.OK); } super.endVmCommand(); } /** * @return Don't override the child success, we want merged image chains to be so also in the DB, or else we will be * out of sync with the storage and this is not a good situation. */ @Override protected boolean overrideChildCommandSuccess() { return false; } @Override protected boolean validate() { initializeObjectState(); if (getVm() == null) { return failValidation(EngineMessage.ACTION_TYPE_FAILED_VM_NOT_FOUND); } if (!canRunActionOnNonManagedVm()) { return false; } VmValidator vmValidator = createVmValidator(getVm()); if (!validate(new StoragePoolValidator(getStoragePool()).isUp()) || !validateVmNotDuringSnapshot() || !validateVmNotInPreview() || !validateSnapshotExists() || !validateSnapshotType() || !validate(vmValidator.vmQualifiedForSnapshotMerge()) || !validate(vmValidator.vmNotHavingDeviceSnapshotsAttachedToOtherVms(false))) { return false; } if (hasImages()) { // Check the VM's images if (!validateImages()) { return false; } // check that we are not deleting the template if (!validateImageNotInTemplate()) { return failValidation(EngineMessage.ACTION_TYPE_FAILED_CANNOT_REMOVE_IMAGE_TEMPLATE); } if (!validateStorageDomains()) { return false; } } return true; } /** * Validates the storage domains. * * Each domain is validated for status, threshold and for enough free space to perform removeSnapshot. * The remove snapshot logic in VDSM includes creating a new temporary volume which might be as large as the disk's * actual size. * Hence, as part of the validation, we sum up all the disks virtual sizes, for each storage domain. * * @return True if there is enough space in all relevant storage domains. False otherwise. */ protected boolean validateStorageDomains() { MultipleStorageDomainsValidator storageDomainsValidator = getStorageDomainsValidator(getStoragePoolId(), getStorageDomainsIds()); return validate(storageDomainsValidator.allDomainsExistAndActive()) && validate(storageDomainsValidator.allDomainsWithinThresholds()) && validate(storageDomainsValidator.allDomainsHaveSpaceForMerge(getAllDisksSnapshot(getSourceImages()), getSnapshotActionType())); } /** * The base snapshot is the parent of the top snapshot. This is reversed the if old cold merge * is performed (pre-4.1). * * @param snapshots list of the parent snapshot disks * @return list of subchains which contain the base and top snapshots. */ protected List<SubchainInfo> getAllDisksSnapshot(List<DiskImage> snapshots) { Set<DiskImage> topSnapshots = diskImageDao.getAllSnapshotsForParents( snapshots .stream() .map(DiskImage::getImageId) .collect(Collectors.toList()) ); Map<Guid, DiskImage> baseSnapshotMap = snapshots .stream() .collect(Collectors.toMap(DiskImage::getImageId, Function.identity())); return topSnapshots .stream() .map(topSnapshot -> { if (!isQemuimgCommitSupported() && getSnapshotActionType() == VdcActionType.RemoveSnapshotSingleDisk) { return new SubchainInfo(topSnapshot, baseSnapshotMap.get(topSnapshot.getParentId())); } else { return new SubchainInfo(baseSnapshotMap.get(topSnapshot.getParentId()), topSnapshot); } }) .collect(Collectors.toList()); } protected Collection<Guid> getStorageDomainsIds() { return ImagesHandler.getAllStorageIdsForImageIds(getSourceImages()); } protected MultipleStorageDomainsValidator getStorageDomainsValidator(Guid spId, Collection<Guid> sdIds) { return new MultipleStorageDomainsValidator(spId, sdIds); } @Override protected void setActionMessageParameters() { addValidationMessage(EngineMessage.VAR__TYPE__SNAPSHOT); addValidationMessage(EngineMessage.VAR__ACTION__REMOVE); } protected boolean validateVmNotDuringSnapshot() { return validate(snapshotsValidator.vmNotDuringSnapshot(getVmId())); } protected boolean validateVmNotInPreview() { return validate(snapshotsValidator.vmNotInPreview(getVmId())); } protected boolean validateSnapshotExists() { return validate(snapshotsValidator.snapshotExists(getVmId(), getParameters().getSnapshotId())); } protected boolean validateSnapshotType() { Snapshot snapshot = snapshotDao.get(getParameters().getSnapshotId()); return validate(snapshotsValidator.isRegularSnapshot(snapshot)); } protected boolean validateImages() { List<DiskImage> imagesToValidate = getDiskImagesToValidate(); DiskImagesValidator diskImagesValidator = new DiskImagesValidator(imagesToValidate); return validateImagesNotLocked(diskImagesValidator) && (getVm().isQualifiedForLiveSnapshotMerge() || validate(diskImagesValidator.diskImagesNotIllegal())); } private boolean validateImagesNotLocked(DiskImagesValidator diskImagesValidator) { return !getParameters().isNeedsLocking() || validate(diskImagesValidator.diskImagesNotLocked()); } private List<DiskImage> getDiskImagesToValidate() { List<Disk> disks = diskDao.getAllForVm(getVmId()); List<DiskImage> allDisks = DisksFilter.filterImageDisks(disks, ONLY_NOT_SHAREABLE, ONLY_ACTIVE); List<CinderDisk> cinderDisks = DisksFilter.filterCinderDisks(disks); allDisks.addAll(cinderDisks); return allDisks; } protected boolean validateImageNotInTemplate() { return vmTemplateDao.get(getRepresentativeSourceImageId()) == null; } private boolean hasImages() { return !getSourceImages().isEmpty(); } private Guid getRepresentativeSourceImageId() { return getSourceImages().get(0).getImageId(); } protected VmValidator createVmValidator(VM vm) { return new VmValidator(vm); } @Override public AuditLogType getAuditLogTypeValue() { switch (getActionState()) { case EXECUTE: return getSucceeded() ? AuditLogType.USER_REMOVE_SNAPSHOT : AuditLogType.USER_FAILED_REMOVE_SNAPSHOT; case END_SUCCESS: return getSucceeded() ? AuditLogType.USER_REMOVE_SNAPSHOT_FINISHED_SUCCESS : AuditLogType.USER_REMOVE_SNAPSHOT_FINISHED_FAILURE; default: return AuditLogType.USER_REMOVE_SNAPSHOT_FINISHED_FAILURE; } } private VdcActionType getSnapshotActionType() { if (getVm().isQualifiedForLiveSnapshotMerge()) { return VdcActionType.RemoveSnapshotSingleDiskLive; } if (isQemuimgCommitSupported()) { return VdcActionType.ColdMergeSnapshotSingleDisk; } return VdcActionType.RemoveSnapshotSingleDisk; } @Override protected Map<String, Pair<String, String>> getExclusiveLocks() { return getParameters().isNeedsLocking() ? Collections.singletonMap(getVmId().toString(), LockMessagesMatchUtil.makeLockingPair(LockingGroup.VM, EngineMessage.ACTION_TYPE_FAILED_OBJECT_LOCKED)) : null; } @Override public List<QuotaConsumptionParameter> getQuotaStorageConsumptionParameters() { //return empty list - the command only release quota so it could never fail the quota check return new ArrayList<>(); } @Override public CommandCallback getCallback() { if (getVm().isQualifiedForLiveSnapshotMerge() || getParameters().isUseCinderCommandCallback() || isQemuimgCommitSupported()) { return new ConcurrentChildCommandsExecutionCallback(); } return null; } private boolean isQemuimgCommitSupported() { return FeatureSupported.isQemuimgCommitSupported(getStoragePool().getCompatibilityVersion()); } }