package org.ovirt.engine.core.bll; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import org.ovirt.engine.core.bll.context.CommandContext; import org.ovirt.engine.core.bll.tasks.interfaces.CommandCallback; import org.ovirt.engine.core.bll.utils.PermissionSubject; import org.ovirt.engine.core.common.AuditLogType; import org.ovirt.engine.core.common.VdcObjectType; import org.ovirt.engine.core.common.action.MergeParameters; import org.ovirt.engine.core.common.action.MergeStatusReturnValue; import org.ovirt.engine.core.common.businessentities.StoragePool; import org.ovirt.engine.core.common.businessentities.StoragePoolStatus; import org.ovirt.engine.core.common.businessentities.VmBlockJobType; import org.ovirt.engine.core.common.businessentities.storage.DiskImage; import org.ovirt.engine.core.common.vdscommands.FullListVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.ReconcileVolumeChainVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.VDSCommandType; import org.ovirt.engine.core.common.vdscommands.VDSReturnValue; 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.SnapshotDao; import org.ovirt.engine.core.dao.StoragePoolDao; import org.ovirt.engine.core.dao.VmDao; import org.ovirt.engine.core.vdsbroker.vdsbroker.VdsProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @InternalCommandAttribute @NonTransactiveCommandAttribute public class MergeStatusCommand<T extends MergeParameters> extends CommandBase<T> { private static final Logger log = LoggerFactory.getLogger(MergeStatusCommand.class); @Inject private StoragePoolDao storagePoolDao; @Inject private SnapshotDao snapshotDao; @Inject private DiskImageDao diskImageDao; @Inject private VmDao vmDao; public MergeStatusCommand(T parameters, CommandContext cmdContext) { super(parameters, cmdContext); } @Override protected void executeCommand() { attemptResolution(); } public void attemptResolution() { Set<Guid> images; if (vmDao.get(getParameters().getVmId()).isDown()) { StoragePool pool = storagePoolDao.get(getParameters().getStoragePoolId()); if (pool.getSpmVdsId() == null || pool.getStatus() != StoragePoolStatus.Up) { log.info("VM down, waiting on SPM election to resolve Live Merge"); setSucceeded(true); return; } else { log.error("VM is not running, proceeding with Live Merge recovery"); images = getVolumeChainFromRecovery(); } } else { images = getVolumeChain(); } if (images == null) { setCommandStatus(CommandStatus.FAILED); return; } Set<Guid> imagesToRemove = getImagesToRemove(); images.retainAll(imagesToRemove); if (images.size() != 1) { log.error("Failed to live merge, still in volume chain: {}", images); setCommandStatus(CommandStatus.FAILED); return; } if (!images.contains(getParameters().getBaseImage().getImageId())) { // If the base image isn't found in qemu chain, it means that the image was already deleted. // In this case, we will not allow PULL merge but rather ask the user to check if the parent // snapshot contains illegal volume(s). If so, that snapshot must be deleted before deleting // other snapshots addCustomValue("SnapshotName", snapshotDao.get(getParameters().getBaseImage().getSnapshotId()).getDescription()); addCustomValue("BaseVolumeId", getParameters().getBaseImage().getImageId().toString()); auditLog(this, AuditLogType.USER_REMOVE_SNAPSHOT_FINISHED_FAILURE_BASE_IMAGE_NOT_FOUND); setCommandStatus(CommandStatus.FAILED); return; } imagesToRemove.removeAll(images); log.info("Successfully removed volume(s): {}", imagesToRemove); // For now, only COMMIT type is supported log.info("Volume merge type '{}'", VmBlockJobType.COMMIT.name()); MergeStatusReturnValue returnValue = new MergeStatusReturnValue(VmBlockJobType.COMMIT, imagesToRemove); getReturnValue().setActionReturnValue(returnValue); setSucceeded(true); persistCommand(getParameters().getParentCommand(), true); setCommandStatus(CommandStatus.SUCCEEDED); } private Set<Guid> getVolumeChain() { List<String> vmIds = new ArrayList<>(); vmIds.add(getParameters().getVmId().toString()); Map[] vms = (Map[]) runVdsCommand( VDSCommandType.FullList, new FullListVDSCommandParameters(getParameters().getVdsId(), vmIds)) .getReturnValue(); if (vms == null || vms.length == 0) { log.error("Failed to retrieve VM information"); return null; } Map vm = vms[0]; if (vm == null || vm.get(VdsProperties.vm_guid) == null) { log.error("Received incomplete VM information"); return null; } Guid vmId = new Guid((String) vm.get(VdsProperties.vm_guid)); if (!vmId.equals(getParameters().getVmId())) { log.error("Invalid VM returned when querying status: expected '{}', got '{}'", getParameters().getVmId(), vmId); return null; } Set<Guid> images = new HashSet<>(); DiskImage activeDiskImage = getParameters().getActiveImage(); for (Object o : (Object[]) vm.get(VdsProperties.Devices)) { Map device = (Map<String, Object>) o; if (VdsProperties.Disk.equals(device.get(VdsProperties.Type)) && activeDiskImage.getId().equals(Guid.createGuidFromString( (String) device.get(VdsProperties.ImageId)))) { Object[] volumeChain = (Object[]) device.get("volumeChain"); for (Object v : volumeChain) { Map<String, Object> volume = (Map<String, Object>) v; images.add(Guid.createGuidFromString((String) volume.get(VdsProperties.VolumeId))); } break; } } return images; } private Set<Guid> getVolumeChainFromRecovery() { ReconcileVolumeChainVDSCommandParameters parameters = new ReconcileVolumeChainVDSCommandParameters( getParameters().getStoragePoolId(), getParameters().getStorageDomainId(), getParameters().getImageGroupId(), getParameters().getImageId() ); VDSReturnValue vdsReturnValue = runVdsCommand(VDSCommandType.ReconcileVolumeChain, parameters); if (!vdsReturnValue.getSucceeded()) { log.error("Unable to retrieve volume list during Live Merge recovery"); return null; } return new HashSet<>((List<Guid>) vdsReturnValue.getReturnValue()); } /** * Returns the set of images which may be merged/removed in the live merge operation * on this disk. We don't know whether VDSM will choose a forward or backward merge * so we return both the top and bottom images here; the caller will need to figure * out the merge direction and preserve the appropriate image. * * @return Set of Guids representing the images which may be removed */ public Set<Guid> getImagesToRemove() { Set<Guid> imagesToRemove = new HashSet<>(); DiskImage curr = getParameters().getTopImage(); imagesToRemove.add(curr.getImageId()); while (!curr.getParentId().equals(getParameters().getBaseImage().getParentId())) { curr = diskImageDao.getSnapshotById(curr.getParentId()); imagesToRemove.add(curr.getImageId()); } return imagesToRemove; } @Override public List<PermissionSubject> getPermissionCheckSubjects() { return Collections.singletonList(new PermissionSubject(getParameters().getStorageDomainId(), VdcObjectType.Storage, getActionType().getActionGroup())); } @Override public CommandCallback getCallback() { return new MergeStatusCommandCallback(); } }