package org.ovirt.engine.core.bll.snapshots;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import org.ovirt.engine.core.bll.InternalCommandAttribute;
import org.ovirt.engine.core.bll.SerialChildExecutingCommand;
import org.ovirt.engine.core.bll.context.CommandContext;
import org.ovirt.engine.core.bll.tasks.CommandCoordinatorUtil;
import org.ovirt.engine.core.bll.tasks.interfaces.CommandCallback;
import org.ovirt.engine.core.common.action.MergeParameters;
import org.ovirt.engine.core.common.action.MergeStatusReturnValue;
import org.ovirt.engine.core.common.action.RemoveSnapshotSingleDiskParameters;
import org.ovirt.engine.core.common.action.RemoveSnapshotSingleDiskStep;
import org.ovirt.engine.core.common.action.VdcActionParametersBase;
import org.ovirt.engine.core.common.action.VdcActionType;
import org.ovirt.engine.core.common.action.VdcReturnValueBase;
import org.ovirt.engine.core.common.businessentities.VmBlockJobType;
import org.ovirt.engine.core.common.businessentities.storage.DiskImage;
import org.ovirt.engine.core.common.businessentities.storage.Image;
import org.ovirt.engine.core.common.businessentities.storage.ImageStatus;
import org.ovirt.engine.core.common.utils.Pair;
import org.ovirt.engine.core.compat.CommandStatus;
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.utils.transaction.TransactionSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@InternalCommandAttribute
public class RemoveSnapshotSingleDiskLiveCommand<T extends RemoveSnapshotSingleDiskParameters>
extends RemoveSnapshotSingleDiskCommandBase<T> implements SerialChildExecutingCommand {
private static final Logger log = LoggerFactory.getLogger(RemoveSnapshotSingleDiskLiveCommand.class);
@Inject
private ImageDao imageDao;
@Inject
private DiskImageDao diskImageDao;
public RemoveSnapshotSingleDiskLiveCommand(T parameters, CommandContext cmdContext) {
super(parameters, cmdContext);
}
@Override
protected void executeCommand() {
// Let doPolling() drive the execution; we don't have any guarantee that
// executeCommand() will finish before doPolling() is called, and we don't
// want to possibly run the first command twice.
setSucceeded(true); // Allow runAction to succeed
}
@Override
public void handleFailure() {
log.error("Command id: '{} failed child command status for step '{}'",
getCommandId(),
getParameters().getCommandStep());
}
@Override
public boolean ignoreChildCommandFailure() {
if (getParameters().getCommandStep() == RemoveSnapshotSingleDiskStep.DESTROY_IMAGE) {
// It's possible that the image was destroyed already if this is retry of Live
// Merge for the given volume. Proceed to check if the image is present.
log.warn("Child command '{}' failed, proceeding to verify", getParameters().getCommandStep());
return true;
}
return false;
}
@Override
public boolean performNextOperation(int completedChildCount) {
// Steps are executed such that:
// a) all logic before the command runs is idempotent
// b) the command is the last action in the step
// This allows for recovery after a crash at any point during command execution.
log.debug("Proceeding with execution of RemoveSnapshotSingleDiskLiveCommand");
if (getParameters().getCommandStep() == null) {
getParameters().setCommandStep(getInitialMergeStepForImage(getParameters().getImageId()));
getParameters().setChildCommands(new HashMap<>());
}
// Upon recovery or after invoking a new child command, our map may be missing an entry
syncChildCommandList(getParameters());
Guid currentChildId = getCurrentChildId(getParameters());
VdcReturnValueBase vdcReturnValue = null;
if (currentChildId != null) {
vdcReturnValue = CommandCoordinatorUtil.getCommandReturnValue(currentChildId);
getParameters().setCommandStep(getParameters().getNextCommandStep());
}
log.info("Executing Live Merge command step '{}'", getParameters().getCommandStep());
Pair<VdcActionType, ? extends VdcActionParametersBase> nextCommand = null;
switch (getParameters().getCommandStep()) {
case EXTEND:
nextCommand = new Pair<>(VdcActionType.MergeExtend, buildMergeParameters());
getParameters().setNextCommandStep(RemoveSnapshotSingleDiskStep.MERGE);
break;
case MERGE:
nextCommand = new Pair<>(VdcActionType.Merge, buildMergeParameters());
getParameters().setNextCommandStep(RemoveSnapshotSingleDiskStep.MERGE_STATUS);
break;
case MERGE_STATUS:
getParameters().setMergeCommandComplete(true);
nextCommand = new Pair<>(VdcActionType.MergeStatus, buildMergeParameters());
getParameters().setNextCommandStep(RemoveSnapshotSingleDiskStep.DESTROY_IMAGE);
break;
case DESTROY_IMAGE:
if (vdcReturnValue != null) {
getParameters().setMergeStatusReturnValue(vdcReturnValue.getActionReturnValue());
} else if (getParameters().getMergeStatusReturnValue() == null) {
// If the images were already merged, just add the orphaned image
getParameters().setMergeStatusReturnValue(synthesizeMergeStatusReturnValue());
}
nextCommand = buildDestroyCommand(VdcActionType.DestroyImage, getActionType(),
new ArrayList<>(getParameters().getMergeStatusReturnValue().getImagesToRemove()));
getParameters().setNextCommandStep(RemoveSnapshotSingleDiskStep.DESTROY_IMAGE_CHECK);
break;
case DESTROY_IMAGE_CHECK:
nextCommand = buildDestroyCommand(VdcActionType.DestroyImageCheck, getActionType(),
new ArrayList<>(getParameters().getMergeStatusReturnValue().getImagesToRemove()));
getParameters().setNextCommandStep(RemoveSnapshotSingleDiskStep.COMPLETE);
break;
case COMPLETE:
getParameters().setDestroyImageCommandComplete(true);
setCommandStatus(CommandStatus.SUCCEEDED);
break;
}
persistCommand(getParameters().getParentCommand(), true);
if (nextCommand != null) {
CommandCoordinatorUtil.executeAsyncCommand(nextCommand.getFirst(), nextCommand.getSecond(), cloneContextAndDetachFromParent());
// Add the child, but wait, it's a race! child will start, task may spawn, get polled, and we won't have the child id
return true;
} else {
return false;
}
}
private RemoveSnapshotSingleDiskStep getInitialMergeStepForImage(Guid imageId) {
Image image = imageDao.get(imageId);
if (image.getStatus() == ImageStatus.ILLEGAL
&& image.getParentId().equals(Guid.Empty)) {
List<DiskImage> children = diskImageDao.getAllSnapshotsForParent(imageId);
if (children.isEmpty()) {
// An illegal, orphaned image means its contents have been merged
log.info("Image has been previously merged, proceeding with deletion");
return RemoveSnapshotSingleDiskStep.DESTROY_IMAGE;
}
}
return RemoveSnapshotSingleDiskStep.EXTEND;
}
private boolean completedMerge() {
return getParameters().getCommandStep() == RemoveSnapshotSingleDiskStep.DESTROY_IMAGE
|| getParameters().getCommandStep() == RemoveSnapshotSingleDiskStep.DESTROY_IMAGE_CHECK
|| getParameters().getCommandStep() == RemoveSnapshotSingleDiskStep.COMPLETE;
}
private MergeParameters buildMergeParameters() {
MergeParameters parameters = new MergeParameters(
getVdsId(),
getVmId(),
getActiveDiskImage(),
getDiskImage(),
getDestinationDiskImage(),
0);
parameters.setParentCommand(VdcActionType.RemoveSnapshotSingleDiskLive);
parameters.setParentParameters(getParameters());
return parameters;
}
/**
* Add orphaned, already-merged images from this snapshot to a MergeStatusReturnValue that
* can be used by the DESTROY_IMAGE command step to tell what needs to be deleted.
*
* @return A suitable MergeStatusReturnValue object
*/
private MergeStatusReturnValue synthesizeMergeStatusReturnValue() {
Set<Guid> images = new HashSet<>();
images.add(getDiskImage().getImageId());
return new MergeStatusReturnValue(VmBlockJobType.UNKNOWN, images);
}
public void onSucceeded() {
syncDbRecords(getParameters().getMergeStatusReturnValue().getBlockJobType(),
getTargetImageInfoFromVdsm(),
getParameters().getMergeStatusReturnValue().getImagesToRemove(),
true);
log.info("Successfully merged snapshot '{}' images '{}'..'{}'",
getDiskImage().getImage().getSnapshotId(),
getDiskImage().getImageId(),
getDestinationDiskImage() != null ? getDestinationDiskImage().getImageId() : "(n/a)");
}
private void syncDbRecordsMergeFailure() {
DiskImage curr = getDestinationDiskImage();
while (!curr.getImageId().equals(getDiskImage().getImageId())) {
curr = diskImageDao.getSnapshotById(curr.getParentId());
imageDao.updateStatus(curr.getImageId(), ImageStatus.ILLEGAL);
}
}
private DiskImage getTargetImageInfoFromVdsm() {
VmBlockJobType blockJobType = getParameters().getMergeStatusReturnValue().getBlockJobType();
return getImageInfoFromVdsm(blockJobType == VmBlockJobType.PULL ? getDestinationDiskImage() : getDiskImage());
}
@Override
protected void endSuccessfully() {
setSucceeded(true);
}
@Override
protected void endWithFailure() {
setSucceeded(true);
}
public void onFailed() {
if (!completedMerge()) {
TransactionSupport.executeInNewTransaction(() -> {
syncDbRecordsMergeFailure();
return null;
});
log.error("Merging of snapshot '{}' images '{}'..'{}' failed. Images have been" +
" marked illegal and can no longer be previewed or reverted to." +
" Please retry Live Merge on the snapshot to complete the operation.",
getDiskImage().getImage().getSnapshotId(),
getDiskImage().getImageId(),
getDestinationDiskImage() != null ? getDestinationDiskImage().getImageId() : "(n/a)"
);
} else {
syncDbRecords(getParameters().getMergeStatusReturnValue().getBlockJobType(),
getTargetImageInfoFromVdsm(),
getParameters().getMergeStatusReturnValue().getImagesToRemove(),
false);
log.error("Snapshot '{}' images '{}'..'{}' merged, but volume removal failed." +
" Some or all of the following volumes may be orphaned: {}." +
" Please retry Live Merge on the snapshot to complete the operation.",
getDiskImage().getImage().getSnapshotId(),
getDiskImage().getImageId(),
getDestinationDiskImage() != null ? getDestinationDiskImage().getImageId() : "(n/a)",
getParameters().getMergeStatusReturnValue().getImagesToRemove());
}
}
@Override
public CommandCallback getCallback() {
return new RemoveSnapshotSingleDiskLiveCommandCallback();
}
}