package org.ovirt.engine.core.bll.storage.disk.image; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import javax.inject.Inject; import org.ovirt.engine.core.bll.InternalCommandAttribute; import org.ovirt.engine.core.bll.NonTransactiveCommandAttribute; import org.ovirt.engine.core.bll.context.CommandContext; 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.ExtendImageSizeParameters; import org.ovirt.engine.core.common.action.RefreshVolumeParameters; import org.ovirt.engine.core.common.action.VdcActionType; import org.ovirt.engine.core.common.action.VdcReturnValueBase; import org.ovirt.engine.core.common.asynctasks.AsyncTaskType; import org.ovirt.engine.core.common.businessentities.ActionGroup; import org.ovirt.engine.core.common.businessentities.VM; import org.ovirt.engine.core.common.businessentities.VmDevice; import org.ovirt.engine.core.common.businessentities.storage.DiskImage; import org.ovirt.engine.core.common.errors.EngineException; import org.ovirt.engine.core.common.errors.EngineMessage; import org.ovirt.engine.core.common.utils.Pair; import org.ovirt.engine.core.common.vdscommands.ExtendImageSizeVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.ExtendVmDiskSizeVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.GetImageInfoVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.VDSCommandType; import org.ovirt.engine.core.common.vdscommands.VDSReturnValue; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.dao.ImageDao; import org.ovirt.engine.core.dao.VmDao; @InternalCommandAttribute @NonTransactiveCommandAttribute public class ExtendImageSizeCommand<T extends ExtendImageSizeParameters> extends BaseImagesCommand<T> { @Inject private ImageDao imageDao; @Inject private VmDao vmDao; private List<PermissionSubject> permissionsList; private List<VM> vmsDiskPluggedTo; public ExtendImageSizeCommand(T parameters, CommandContext cmdContext) { super(parameters, cmdContext); } @Override protected void executeCommand() { VDSReturnValue vdsReturnValue = extendUnderlyingVolumeSize(getImage()); setSucceeded(vdsReturnValue.getSucceeded()); if (vdsReturnValue.getSucceeded()) { Guid taskId = createTask(getAsyncTaskId(), vdsReturnValue.getCreationInfo(), getParameters().getParentCommand()); getReturnValue().getInternalVdsmTaskIdList().add(taskId); if (getParameters().getParentNotifiesCallback()) { getParameters().setVdsmTaskIds(new ArrayList<>(Collections.singletonList(taskId))); getReturnValue().getVdsmTaskIdList().add(taskId); persistCommand(getParameters().getParentCommand(), true); } } else { updateAuditLog(AuditLogType.USER_EXTEND_DISK_SIZE_FAILURE, getParameters().getNewSizeInGB()); } } private VDSReturnValue extendUnderlyingVolumeSize(DiskImage diskImage) { ExtendImageSizeVDSCommandParameters params = new ExtendImageSizeVDSCommandParameters( diskImage.getStoragePoolId(), diskImage.getStorageIds().get(0), diskImage.getId(), diskImage.getImageId(), getParameters().getNewSize() ); return runVdsCommand(VDSCommandType.ExtendImageSize, params); } @Override protected void endSuccessfully() { if (getImage().getActive()) { updateRelevantVms(); } else if (getImage().hasRawBlock()) { refreshVolume(); } DiskImage diskImage = getImageInfo(); if (diskImage != null && getImage().getSize() != diskImage.getSize()) { getReturnValue().setActionReturnValue(diskImage.getSize()); imageDao.updateImageSize(diskImage.getImageId(), diskImage.getSize()); updateAuditLog(AuditLogType.USER_EXTEND_DISK_SIZE_SUCCESS, diskImage.getSizeInGigabytes()); } setSucceeded(true); } private void updateRelevantVms() { List<VM> vms = getVmsDiskPluggedTo(); for (VM vm : vms) { try { VDSReturnValue ret = extendVmDiskSize(vm, getParameters().getNewSize()); if (!ret.getSucceeded()) { updateAuditLogFailedToUpdateVM(vm.getName()); } } catch (EngineException e) { log.warn("Failed to update VM '{}' with the new volume size due to error, " + "VM should be restarted to detect the new size: {}", vm.getName(), e.getMessage()); log.debug("Exception", e); updateAuditLogFailedToUpdateVM(vm.getName()); } } } private VDSReturnValue extendVmDiskSize(VM vm, Long newSize) { Guid vdsId; Guid vmId; if (vm.getStatus().isDownOrSuspended()) { vdsId = getStoragePool().getSpmVdsId(); vmId = Guid.Empty; } else { vdsId = vm.getRunOnVds(); vmId = vm.getId(); } ExtendVmDiskSizeVDSCommandParameters params = new ExtendVmDiskSizeVDSCommandParameters(vdsId, vmId, getParameters().getStoragePoolId(), getParameters().getStorageDomainId(), getParameters().getImageId(), getParameters().getImageGroupID(), newSize); return runVdsCommand(VDSCommandType.ExtendVmDiskSize, params); } /** * Refreshes the size of an extended volume for the VM having the image to extend attached. * Note that because this is not an active layer image, it can only be in use by a single VM. */ public void refreshVolume() { List<VM> vms = getVmsDiskPluggedTo(); if (vms.isEmpty() || vms.get(0).getStatus().isDownOrSuspended()) { return; } VM vm = vms.get(0); log.info("Refreshing size of extended volume '{}' for VM '{}' on host '{}'", getParameters().getImageId(), vm.getName(), vm.getRunOnVdsName()); RefreshVolumeParameters parameters = new RefreshVolumeParameters( vm.getRunOnVds(), getParameters().getStoragePoolId(), getParameters().getStorageDomainId(), getParameters().getImageGroupID(), getParameters().getImageId()); parameters.setParentCommand(getActionType()); parameters.setParentParameters(getParameters()); VdcReturnValueBase returnValue = runInternalAction(VdcActionType.RefreshVolume, parameters, cloneContextAndDetachFromParent()); if (returnValue == null || !returnValue.getSucceeded()) { // TODO this might be better in MergeExtendCommand: there is a race due to the refresh // being called from endSuccessfully(), and it would also give more context upon error. // The flow to refresh an internal volume is called only from Live Merge, thus the // solution to retry the operation. This should be updated if ever used elsewhere. log.warn("Failed to update host '{}' with the new size of volume '{}' due to error. " + "Please try the operation again.", vm.getRunOnVdsName(), getParameters().getImageId()); updateAuditLogFailedToUpdateHost(vm.getRunOnVdsName()); } } private DiskImage getImageInfo() { DiskImage diskImage = null; GetImageInfoVDSCommandParameters params = new GetImageInfoVDSCommandParameters( getParameters().getStoragePoolId(), getParameters().getStorageDomainId(), getParameters().getImageGroupID(), getParameters().getImageId() ); try { diskImage = (DiskImage) runVdsCommand(VDSCommandType.GetImageInfo, params).getReturnValue(); } catch (EngineException e) { log.error("Failed to retrieve image '{}' info: {}", params.getImageId(), e.getMessage()); log.debug("Exception", e); } return diskImage; } @Override protected void endWithFailure() { getReturnValue().setEndActionTryAgain(false); DiskImage diskImage = getImageInfo(); if (diskImage != null && getImage().getSize() != diskImage.getSize()) { getReturnValue().setActionReturnValue(diskImage.getSize()); imageDao.updateImageSize(diskImage.getImageId(), diskImage.getSize()); } updateAuditLog(AuditLogType.USER_EXTEND_DISK_SIZE_FAILURE, getParameters().getNewSizeInGB()); } private void updateAuditLog(AuditLogType auditLogType, Long imageSizeInGigabytes) { addCustomValue("DiskAlias", getImage().getDiskAlias()); addCustomValue("NewSize", String.valueOf(imageSizeInGigabytes)); auditLogDirector.log(this, auditLogType); } private void updateAuditLogFailedToUpdateVM(String vmName) { addCustomValue("VmName", vmName); auditLogDirector.log(this, AuditLogType.USER_EXTEND_DISK_SIZE_UPDATE_VM_FAILURE); } private void updateAuditLogFailedToUpdateHost(String vdsName) { addCustomValue("VdsName", vdsName); auditLogDirector.log(this, AuditLogType.USER_EXTEND_DISK_SIZE_UPDATE_HOST_FAILURE); } @Override public List<PermissionSubject> getPermissionCheckSubjects() { if (permissionsList == null) { permissionsList = new ArrayList<>(); permissionsList.add(new PermissionSubject(getImage().getId(), VdcObjectType.Disk, ActionGroup.EDIT_DISK_PROPERTIES)); } return permissionsList; } @Override protected void setActionMessageParameters() { addValidationMessage(EngineMessage.VAR__ACTION__EXTEND_IMAGE_SIZE); addValidationMessage(EngineMessage.VAR__TYPE__DISK); } @Override protected AsyncTaskType getTaskType() { return AsyncTaskType.extendImageSize; } private List<VM> getVmsDiskPluggedTo() { if (vmsDiskPluggedTo == null) { List<Pair<VM, VmDevice>> attachedVmsInfo = vmDao.getVmsWithPlugInfo(getImage().getId()); vmsDiskPluggedTo = new LinkedList<>(); for (Pair<VM, VmDevice> pair : attachedVmsInfo) { if (Boolean.TRUE.equals(pair.getSecond().isPlugged()) && pair.getSecond().getSnapshotId() == null) { vmsDiskPluggedTo.add(pair.getFirst()); } } } return vmsDiskPluggedTo; } }