package org.ovirt.engine.core.bll.snapshots; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; 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.LockMessagesMatchUtil; import org.ovirt.engine.core.bll.NonTransactiveCommandAttribute; import org.ovirt.engine.core.bll.SerialChildCommandsExecutionCallback; import org.ovirt.engine.core.bll.SerialChildExecutingCommand; import org.ovirt.engine.core.bll.ValidationResult; import org.ovirt.engine.core.bll.VmHandler; import org.ovirt.engine.core.bll.context.CommandContext; import org.ovirt.engine.core.bll.storage.disk.image.BaseImagesCommand; 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.utils.PermissionSubject; 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.DiskSnapshotsValidator; import org.ovirt.engine.core.bll.validator.storage.StorageDomainValidator; import org.ovirt.engine.core.bll.validator.storage.StoragePoolValidator; import org.ovirt.engine.core.common.AuditLogType; 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.RemoveAllVmCinderDisksParameters; import org.ovirt.engine.core.common.action.RemoveDiskSnapshotsParameters; import org.ovirt.engine.core.common.action.RemoveSnapshotSingleDiskParameters; 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.businessentities.Snapshot; import org.ovirt.engine.core.common.businessentities.VM; import org.ovirt.engine.core.common.businessentities.VmDevice; import org.ovirt.engine.core.common.businessentities.storage.CinderDisk; import org.ovirt.engine.core.common.businessentities.storage.DiskImage; import org.ovirt.engine.core.common.businessentities.storage.DiskStorageType; import org.ovirt.engine.core.common.businessentities.storage.ImageStatus; 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.DiskImageDao; import org.ovirt.engine.core.dao.ImageDao; import org.ovirt.engine.core.dao.SnapshotDao; import org.ovirt.engine.core.dao.VmDao; import org.ovirt.engine.core.dao.VmDeviceDao; import org.ovirt.engine.core.utils.ovf.OvfManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @NonTransactiveCommandAttribute(forceCompensation = true) public class RemoveDiskSnapshotsCommand<T extends RemoveDiskSnapshotsParameters> extends BaseImagesCommand<T> implements SerialChildExecutingCommand { private static final Logger log = LoggerFactory.getLogger(RemoveDiskSnapshotsCommand.class); private List<DiskImage> images; private StorageDomainValidator storageDomainValidator; @Inject private OvfManager ovfManager; @Inject private ImageDao imageDao; @Inject private SnapshotDao snapshotDao; @Inject private VmDeviceDao vmDeviceDao; @Inject private DiskImageDao diskImageDao; @Inject private SnapshotsValidator snapshotsValidator; @Inject private VmDao vmDao; public RemoveDiskSnapshotsCommand(T parameters, CommandContext cmdContext) { super(parameters, cmdContext); } public RemoveDiskSnapshotsCommand(Guid commandId) { super(commandId); } @Override protected LockProperties applyLockProperties(LockProperties lockProperties) { return lockProperties.withScope(Scope.Execution); } @Override public void init() { super.init(); sortImages(); // Images must be specified in parameters and belong to a single Disk; // Otherwise, we'll fail on validate. if (getRepresentativeImage().isPresent()) { DiskImage representativeImage = getRepresentativeImage().get(); setImage(representativeImage); getParameters().setStorageDomainId(representativeImage.getStorageIds().get(0)); getParameters().setDiskAlias(representativeImage.getDiskAlias()); getParameters().setUseCinderCommandCallback(!DisksFilter.filterCinderDisks(getImages()).isEmpty()); if (Guid.isNullOrEmpty(getParameters().getContainerId())) { List<VM> listVms = vmDao.getVmsListForDisk(representativeImage.getId(), false); if (!listVms.isEmpty()) { VM vm = listVms.get(0); setVm(vm); getParameters().setContainerId(vm.getId()); } } } setVmId(getParameters().getContainerId()); setStorageDomainId(getParameters().getStorageDomainId()); } protected List<DiskImage> getImages() { if (images == null) { images = new ArrayList<>(); for (Guid imageId : getParameters().getImageIds()) { if (imageId == null) { // Disks existence is validated in validate continue; } DiskImage image = diskImageDao.getSnapshotById(imageId); if (image != null) { images.add(image); } } } return images; } /** * Returns the images chain of the disk. */ protected List<DiskImage> getAllImagesForDisk() { return diskImageDao.getAllSnapshotsForImageGroup(getImageGroupId()); } protected StorageDomainValidator getStorageDomainValidator() { if (storageDomainValidator == null) { storageDomainValidator = new StorageDomainValidator(getStorageDomain()); } return storageDomainValidator; } @Override protected boolean validate() { if (getVm() == null) { return failValidation(EngineMessage.ACTION_TYPE_FAILED_VM_NOT_FOUND); } DiskSnapshotsValidator diskSnapshotsValidator = createDiskSnapshotsValidator(getImages()); if (!validate(diskSnapshotsValidator.diskSnapshotsNotExist(getParameters().getImageIds())) || !validate(diskSnapshotsValidator.diskImagesBelongToSameImageGroup()) || !validate(diskSnapshotsValidator.imagesAreSnapshots())) { return false; } // Validate all chain of images in the disk if (!validateAllDiskImages()) { return false; } DiskImagesValidator diskImagesValidator = createDiskImageValidator(getImages()); if (!validate(diskImagesValidator.diskImagesSnapshotsNotAttachedToOtherVms(false))) { return false; } if (!canRunActionOnNonManagedVm()) { return false; } if (isDiskPlugged()) { VmValidator vmValidator = createVmValidator(getVm()); if (!validate(vmValidator.vmQualifiedForSnapshotMerge())) { return false; } } if (!validate(new StoragePoolValidator(getStoragePool()).isUp()) || !validateVmNotDuringSnapshot() || !validateVmNotInPreview() || !validateSnapshotExists() || !validateStorageDomainActive()) { return false; } if (!validateStorageDomainAvailableSpace()) { return false; } return true; } @Override protected void setActionMessageParameters() { addValidationMessage(EngineMessage.VAR__ACTION__REMOVE); addValidationMessage(EngineMessage.VAR__TYPE__DISK__SNAPSHOT); } private void sortImages() { // Sort images from parent to leaf (active) - needed only once as the sorted list is // being saved in the parameters. The conditions to check vary between cold and live // merge (and we can't yet run isLiveMerge()), so we just use an explicit flag. if (!getParameters().isImageIdsSorted()) { // Retrieve and sort the entire chain of images List<DiskImage> images = getAllImagesForDisk(); ImagesHandler.sortImageList(images); // Get a sorted list of the selected images List<DiskImage> sortedImages = images.stream() .filter(image -> image.getDiskStorageType() == DiskStorageType.IMAGE) .filter(image -> getImages().contains(image)) .collect(Collectors.toList()); getParameters().setImageIds(new ArrayList<>(ImagesHandler.getDiskImageIds(sortedImages))); getParameters().setImageIdsSorted(true); getParameters().setImageGroupID(getImageGroupId()); } } @Override public CommandCallback getCallback() { return getParameters().isUseCinderCommandCallback() ? new ConcurrentChildCommandsExecutionCallback() : new SerialChildCommandsExecutionCallback(); } private boolean isLiveMerge() { return (getParameters().isLiveMerge() || (getVm() != null && getVm().isQualifiedForLiveSnapshotMerge())) && !DisksFilter.filterImageDisks(getImages()).isEmpty(); } @Override protected void executeCommand() { if (isLiveMerge()) { getParameters().setLiveMerge(true); } persistCommand(getParameters().getParentCommand(), true); removeCinderSnapshotDisks(); setSucceeded(true); } @Override public boolean performNextOperation(int completedChildren) { if (completedChildren == getParameters().getImageIds().size()) { return false; } if (completedChildren == 0) { // Lock all disk images in advance imageDao.updateStatusOfImagesByImageGroupId(getImageGroupId(), ImageStatus.LOCKED); } return isLiveMerge() ? performNextOperationLiveMerge(completedChildren) : performNextOperationColdMerge(completedChildren); } private boolean performNextOperationLiveMerge(int completedChildren) { if (completedChildren != 0) { checkImageIdConsistency(completedChildren - 1); } Guid nextImageId = getParameters().getImageIds().get(completedChildren); log.info("Starting child command {} of {}, image '{}'", completedChildren + 1, getParameters().getImageIds().size(), nextImageId); ImagesContainterParametersBase parameters = buildRemoveSnapshotSingleDiskLiveParameters(nextImageId, completedChildren); updateParameters(completedChildren, parameters.getDestinationImageId()); persistCommandIfNeeded(); CommandCoordinatorUtil.executeAsyncCommand(VdcActionType.RemoveSnapshotSingleDiskLive, parameters, cloneContextAndDetachFromParent()); return true; } private void updateParameters(int completedChildren, Guid destinationImageId) { if (getParameters().getChildImageIds() == null) { getParameters().setChildImageIds(Arrays.asList(new Guid[getParameters().getImageIds().size()])); } getParameters().getChildImageIds().set(completedChildren, destinationImageId); } private boolean performNextOperationColdMerge(int completedChildren) { Guid nextImageId = getParameters().getImageIds().get(completedChildren); log.info("Starting child command {} of {}, image '{}'", completedChildren + 1, getParameters().getImageIds().size(), nextImageId); ImagesContainterParametersBase parameters = buildRemoveSnapshotSingleDiskParameters(nextImageId); CommandCoordinatorUtil.executeAsyncCommand(VdcActionType.RemoveSnapshotSingleDisk, parameters, cloneContextAndDetachFromParent()); return true; } /** * Ensures that after a backwards merge (in which the current snapshot's image takes the * place of the next snapshot's image), subsequent iterations will refer to the correct * image id and not the one that has been removed. */ private void checkImageIdConsistency(int completedImageIndex) { Guid imageId = getParameters().getImageIds().get(completedImageIndex); Guid childImageId = getParameters().getChildImageIds().get(completedImageIndex); if (diskImageDao.get(childImageId) == null) { // Swap instances of the removed id with our id for (int i = completedImageIndex + 1; i < getParameters().getImageIds().size(); i++) { if (getParameters().getImageIds().get(i).equals(childImageId)) { getParameters().getImageIds().set(i, imageId); log.info("Switched child command {} image id from '{}' to '{}' due to backwards merge", i + 1, childImageId, imageId); persistCommand(getParameters().getParentCommand(), true); } } } } private RemoveSnapshotSingleDiskParameters buildRemoveSnapshotSingleDiskLiveParameters(Guid imageId, int completedChildren) { DiskImage dest = diskImageDao.getAllSnapshotsForParent(imageId).get(0); RemoveSnapshotSingleDiskParameters parameters = new RemoveSnapshotSingleDiskParameters(imageId, getVmId()); parameters.setDestinationImageId(dest.getImageId()); parameters.setEntityInfo(getParameters().getEntityInfo()); parameters.setParentParameters(getParameters()); parameters.setParentCommand(getActionType()); parameters.setCommandType(VdcActionType.RemoveSnapshotSingleDiskLive); parameters.setVdsId(getVm().getRunOnVds()); parameters.setSessionId(getParameters().getSessionId()); parameters.setEndProcedure(EndProcedure.COMMAND_MANAGED); return parameters; } private ImagesContainterParametersBase buildRemoveSnapshotSingleDiskParameters(Guid imageId) { ImagesContainterParametersBase parameters = new ImagesContainterParametersBase( imageId, getVmId()); DiskImage dest = diskImageDao.getAllSnapshotsForParent(imageId).get(0); parameters.setDestinationImageId(dest.getImageId()); parameters.setEntityInfo(getParameters().getEntityInfo()); parameters.setParentParameters(getParameters()); parameters.setParentCommand(getActionType()); parameters.setWipeAfterDelete(dest.isWipeAfterDelete()); parameters.setSessionId(getParameters().getSessionId()); parameters.setVmSnapshotId(diskImageDao.getSnapshotById(imageId).getVmSnapshotId()); parameters.setEndProcedure(EndProcedure.COMMAND_MANAGED); return parameters; } @Override protected void endSuccessfully() { unlockImages(); setSucceeded(true); } @Override protected void endWithFailure() { unlockImages(); setSucceeded(true); } private void removeCinderSnapshotDisks() { List<CinderDisk> cinderDisks = DisksFilter.filterCinderDisks(getImages()); if (cinderDisks.isEmpty()) { return; } Future<VdcReturnValueBase> future = CommandCoordinatorUtil.executeAsyncCommand( VdcActionType.RemoveAllCinderSnapshotDisks, buildRemoveCinderSnapshotDiskParameters(cinderDisks), cloneContextAndDetachFromParent()); try { VdcReturnValueBase vdcReturnValueBase = future.get(); if (!vdcReturnValueBase.getSucceeded()) { log.error("Error removing snapshots for Cinder disks"); endWithFailure(); getParameters().setTaskGroupSuccess(false); } else { Snapshot snapshotWithoutImage = null; Snapshot snapshot = snapshotDao.get(cinderDisks.get(0).getSnapshotId()); lockVmSnapshotsWithWait(getVm()); for (CinderDisk cinderDisk : cinderDisks) { snapshotWithoutImage = ImagesHandler.prepareSnapshotConfigWithoutImageSingleImage( snapshot, cinderDisk.getImageId(), ovfManager); } snapshotDao.update(snapshotWithoutImage); if (getSnapshotsEngineLock() != null) { lockManager.releaseLock(getSnapshotsEngineLock()); } endSuccessfully(); getParameters().setTaskGroupSuccess(true); } } catch (InterruptedException | ExecutionException e) { log.error("Error removing snapshots for Cinder disks"); endWithFailure(); getParameters().setTaskGroupSuccess(false); } } private RemoveAllVmCinderDisksParameters buildRemoveCinderSnapshotDiskParameters(List<CinderDisk> cinderDisks) { RemoveAllVmCinderDisksParameters params = new RemoveAllVmCinderDisksParameters(); params.setCinderDisks(cinderDisks); params.setParentCommand(getActionType()); params.setParentParameters(getParameters()); params.setSessionId(getParameters().getSessionId()); params.setInvokeEndActionOnParent(false); params.setEndProcedure(EndProcedure.COMMAND_MANAGED); return params; } private void unlockImages() { if (isLiveMerge()) { // Some Live Merge failure cases leave a subset of images illegal; // they should remain illegal while the others are unlocked. List<DiskImage> images = getAllImagesForDisk(); for (DiskImage image : images) { if (image.getImageStatus() == ImageStatus.LOCKED) { imageDao.updateStatus(image.getImageId(), ImageStatus.OK); } } } else { imageDao.updateStatusOfImagesByImageGroupId(getParameters().getImageGroupID(), ImageStatus.OK); } } @Override public Map<String, String> getJobMessageProperties() { if (jobProperties == null) { jobProperties = super.getJobMessageProperties(); jobProperties.put("snapshots", StringUtils.join(getSnapshotsNames(), ", ")); } return jobProperties; } private void addAuditLogCustomValues() { this.addCustomValue("DiskAlias", getParameters().getDiskAlias()); this.addCustomValue("Snapshots", StringUtils.join(getSnapshotsNames(), ", ")); } private List<String> getSnapshotsNames() { // The caching is done during initial command execution (called by addAuditLogCustomValues) // which will prevent audit logging of '<UNKNOWN>' when the command completes. if (getParameters().getSnapshotNames() == null) { getParameters().setSnapshotNames(new LinkedList<>()); for (DiskImage image : getImages()) { Snapshot snapshot = snapshotDao.get(image.getSnapshotId()); if (snapshot != null) { getParameters().getSnapshotNames().add(snapshot.getDescription()); } } } return getParameters().getSnapshotNames(); } protected boolean canRunActionOnNonManagedVm() { ValidationResult nonManagedVmValidationResult = VmHandler.canRunActionOnNonManagedVm(getVm(), this.getActionType()); if (!nonManagedVmValidationResult.isValid()) { return failValidation(nonManagedVmValidationResult.getMessages()); } return true; } protected boolean validateVmNotDuringSnapshot() { return validate(snapshotsValidator.vmNotDuringSnapshot(getVmId())); } protected boolean validateVmNotInPreview() { return validate(snapshotsValidator.vmNotInPreview(getVmId())); } protected boolean validateSnapshotExists() { return validate(snapshotsValidator.snapshotExists(getVmId(), getSnapshotId())); } protected boolean validateAllDiskImages() { List<DiskImage> images = diskImageDao.getAllSnapshotsForImageGroup(getDiskImage().getId()); DiskImagesValidator diskImagesValidator = new DiskImagesValidator(images); return validate(diskImagesValidator.diskImagesNotLocked()) && validate(diskImagesValidator.diskImagesNotIllegal()); } protected boolean validateStorageDomainActive() { return validate(getStorageDomainValidator().isDomainExistAndActive()); } protected boolean validateStorageDomainAvailableSpace() { // What should be checked here is that there's enough space for removing a set of disk snapshots consecutively. // Worst-case scenario when merging a snapshot in terms of space, is the outcome volume, along with the not-yet-deleted volumes. // The following implementation does just that. In this case only snapshots are passed to the validation // (as opposed to the whole chain). List<DiskImage> disksList = ImagesHandler.getSnapshotsDummiesForStorageAllocations(getImages()); return validate(getStorageDomainValidator().hasSpaceForClonedDisks(disksList)); } protected DiskImagesValidator createDiskImageValidator(List<DiskImage> disksList) { return new DiskImagesValidator(disksList); } protected DiskSnapshotsValidator createDiskSnapshotsValidator(List<DiskImage> images) { return new DiskSnapshotsValidator(images); } protected VmValidator createVmValidator(VM vm) { return new VmValidator(vm); } private Optional<DiskImage> getRepresentativeImage() { return getImages().stream().findFirst(); } @Override protected Guid getImageGroupId() { return getRepresentativeImage().map(DiskImage::getId).orElse(Guid.Empty); } protected boolean isDiskPlugged() { List<VmDevice> devices = vmDeviceDao.getVmDevicesByDeviceId(getImageGroupId(), getVmId()); return !devices.isEmpty() && devices.get(0).isPlugged(); } @Override public List<PermissionSubject> getPermissionCheckSubjects() { List<PermissionSubject> permissionList = new ArrayList<>(); permissionList.add(new PermissionSubject(getVmId(), VdcObjectType.VM, getActionType().getActionGroup())); return permissionList; } @Override public AuditLogType getAuditLogTypeValue() { addAuditLogCustomValues(); switch (getActionState()) { case EXECUTE: return AuditLogType.USER_REMOVE_DISK_SNAPSHOT; case END_SUCCESS: return AuditLogType.USER_REMOVE_DISK_SNAPSHOT_FINISHED_SUCCESS; case END_FAILURE: return AuditLogType.USER_REMOVE_DISK_SNAPSHOT_FINISHED_FAILURE; } return AuditLogType.UNASSIGNED; } @Override protected Map<String, Pair<String, String>> getExclusiveLocks() { return Collections.singletonMap(getImageGroupId().toString(), LockMessagesMatchUtil.makeLockingPair(LockingGroup.DISK, EngineMessage.ACTION_TYPE_FAILED_DISKS_LOCKED)); } protected Guid getSnapshotId() { return getImage() != null ? getImage().getSnapshotId() : null; } }