diff --git a/Makefile b/Makefile index 87371c15636..4fed0bc9cc9 100644 --- a/Makefile +++ b/Makefile @@ -433,6 +433,7 @@ install_artifacts: install -dm 0755 "$(DESTDIR)$(PKG_JBOSS_MODULES)/$${category}"; \ find "$(MAVEN_OUTPUT_DIR)" -name '*'"-$${category}-modules.zip" | grep -v tmp.repos | xargs -r -n 1 unzip -q -o -d "$(DESTDIR)$(PKG_JBOSS_MODULES)/$${category}"; \ done + rm -rf "$(DESTDIR)$(PKG_EAR_DIR)" install -dm 0755 "$(DESTDIR)$(PKG_EAR_DIR)" find "$(MAVEN_OUTPUT_DIR)" -name '*.ear' -type f | grep -v tmp.repos | xargs -n 1 unzip -q -o -d "$(DESTDIR)$(PKG_EAR_DIR)" install -dm 0755 "$(DESTDIR)$(DATA_DIR)/restapi.war" diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/profiles/DiskProfileHelper.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/profiles/DiskProfileHelper.java index 9ca0f419b40..57337d43d09 100644 --- a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/profiles/DiskProfileHelper.java +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/profiles/DiskProfileHelper.java @@ -73,6 +73,10 @@ public ValidationResult setAndValidateDiskProfiles(Map map, DbU diskProfilesList = diskProfileDao.getAllForStorageDomain(storageDomainId); storageDiskProfilesMap.put(storageDomainId, diskProfilesList); } + // No disk profiles for this domain (e.g. managed block storage) - leave diskProfileId null. + if (diskProfilesList.isEmpty()) { + continue; + } // Set Disk Profile according to permissions if (!updateDiskProfileForBackwardCompatibility(diskImage, diskProfilesList, diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/AddDiskCommand.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/AddDiskCommand.java index a005910a421..e18334a7b1e 100644 --- a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/AddDiskCommand.java +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/AddDiskCommand.java @@ -458,7 +458,11 @@ protected void executeVmCommand() { switch (getParameters().getDiskInfo().getDiskStorageType()) { case IMAGE: case KUBERNETES: - createDiskBasedOnImage(); + if (getStorageDomain() != null && StorageType.MANAGED_BLOCK_STORAGE.equals(getStorageDomain().getStorageType())) { + createManagedBlockStorageDisk(); + } else { + createDiskBasedOnImage(); + } break; case LUN: createDiskBasedOnLun(); diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/AddDiskCommandCallback.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/AddDiskCommandCallback.java index 95547f81e1f..27243684a8d 100644 --- a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/AddDiskCommandCallback.java +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/AddDiskCommandCallback.java @@ -39,6 +39,13 @@ protected void childCommandsExecutionEnded(CommandBase command, } DiskImage diskImage = (DiskImage) addDiskCommand.getParameters().getDiskInfo(); + // Skip VDSM volume-info path when storageIds is not set (e.g. MBS upload: disk created via + // AddManagedBlockStorageDisk while getChildActionType() is AddImageFromScratch due to IMAGE disk type). + if (diskImage.getStorageIds() == null || diskImage.getStorageIds().isEmpty()) { + super.childCommandsExecutionEnded(command, anyFailed, childCmdIds, status, completedChildren); + return; + } + log.info("Getting volume info for image '{}/{}'", diskImage.getId(), diskImage.getImageId()); try { DiskImage fromVdsm = imagesHandler.getVolumeInfoFromVdsm(diskImage.getStoragePoolId(), diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/image/TransferDiskImageCommand.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/image/TransferDiskImageCommand.java index 803888750ad..7a0ef7defc8 100644 --- a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/image/TransferDiskImageCommand.java +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/image/TransferDiskImageCommand.java @@ -27,7 +27,6 @@ import org.ovirt.engine.core.bll.utils.PermissionSubject; import org.ovirt.engine.core.bll.validator.storage.DiskImagesValidator; import org.ovirt.engine.core.bll.validator.storage.DiskValidator; -import org.ovirt.engine.core.bll.validator.storage.ManagedBlockStorageDomainValidator; import org.ovirt.engine.core.bll.validator.storage.StorageDomainValidator; import org.ovirt.engine.core.common.AuditLogType; import org.ovirt.engine.core.common.VdcObjectType; @@ -37,9 +36,12 @@ import org.ovirt.engine.core.common.action.AddDiskParameters; import org.ovirt.engine.core.common.action.LockProperties; import org.ovirt.engine.core.common.action.RemoveDiskParameters; +import org.ovirt.engine.core.common.action.RemoveImageParameters; import org.ovirt.engine.core.common.action.TransferDiskImageParameters; import org.ovirt.engine.core.common.action.TransferImageStatusParameters; import org.ovirt.engine.core.common.businessentities.ActionGroup; +import org.ovirt.engine.core.common.businessentities.AsyncTaskStatus; +import org.ovirt.engine.core.common.businessentities.AsyncTaskStatusEnum; import org.ovirt.engine.core.common.businessentities.StorageDomain; import org.ovirt.engine.core.common.businessentities.VDS; import org.ovirt.engine.core.common.businessentities.VM; @@ -47,6 +49,7 @@ import org.ovirt.engine.core.common.businessentities.VmBackupPhase; import org.ovirt.engine.core.common.businessentities.storage.DiskBackupMode; import org.ovirt.engine.core.common.businessentities.storage.DiskImage; +import org.ovirt.engine.core.common.businessentities.storage.DiskImageDynamic; import org.ovirt.engine.core.common.businessentities.storage.DiskStorageType; import org.ovirt.engine.core.common.businessentities.storage.ImageStatus; import org.ovirt.engine.core.common.businessentities.storage.ImageTicket; @@ -71,6 +74,7 @@ import org.ovirt.engine.core.common.vdscommands.AddImageTicketVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.ExtendImageTicketVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.GetImageTicketVDSCommandParameters; +import org.ovirt.engine.core.common.vdscommands.HSMTaskGuidBaseVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.ImageActionsVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.NbdServerVDSParameters; import org.ovirt.engine.core.common.vdscommands.PrepareImageVDSCommandParameters; @@ -81,8 +85,11 @@ import org.ovirt.engine.core.compat.CommandStatus; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.dal.dbbroker.auditloghandling.AuditLogDirector; +import org.ovirt.engine.core.dao.BaseDiskDao; import org.ovirt.engine.core.dao.DiskDao; +import org.ovirt.engine.core.dao.DiskImageDynamicDao; import org.ovirt.engine.core.dao.ImageDao; +import org.ovirt.engine.core.dao.ImageStorageDomainMapDao; import org.ovirt.engine.core.dao.ImageTransferDao; import org.ovirt.engine.core.dao.SnapshotDao; import org.ovirt.engine.core.dao.StorageDomainDao; @@ -102,29 +109,29 @@ public class TransferDiskImageCommand ext private static final int PROXY_CONTROL_PORT = 54324; private static final String HTTPS_SCHEME = "https://"; private static final String IMAGES_PATH = "/images"; - private static final String FILE_URL_SCHEME = "file://"; + protected static final String FILE_URL_SCHEME = "file://"; private static final String IMAGE_TYPE = "disk"; @Inject - private ImageTransferDao imageTransferDao; + protected ImageTransferDao imageTransferDao; @Inject private AuditLogDirector auditLogDirector; @Inject - private DiskDao diskDao; + protected DiskDao diskDao; @Inject - private StorageDomainDao storageDomainDao; + protected StorageDomainDao storageDomainDao; @Inject private VmDao vmDao; @Inject private ImageTransferUpdater imageTransferUpdater; @Inject - private ImageDao imageDao; + protected ImageDao imageDao; @Inject - private VdsDao vdsDao; + protected VdsDao vdsDao; @Inject private VmBackupDao vmBackupDao; @Inject - private SnapshotDao snapshotDao; + protected SnapshotDao snapshotDao; @Inject private CommandCoordinatorUtil commandCoordinatorUtil; @Inject @@ -134,6 +141,12 @@ public class TransferDiskImageCommand ext private VdsCommandsHelper vdsCommandsHelper; @Inject private ResourceManager resourceManager; + @Inject + protected BaseDiskDao baseDiskDao; + @Inject + protected DiskImageDynamicDao diskImageDynamicDao; + @Inject + protected ImageStorageDomainMapDao imageStorageDomainMapDao; private ImageioClient proxyClient; private VmBackup backup; @@ -186,7 +199,8 @@ protected String prepareImage(Guid vdsId) { VDSReturnValue vdsRetVal = runVdsCommand(VDSCommandType.PrepareImage, getPrepareParameters(vdsId)); - return FILE_URL_SCHEME + ((PrepareImageReturn) vdsRetVal.getReturnValue()).getImagePath(); + String path = ((PrepareImageReturn) vdsRetVal.getReturnValue()).getImagePath(); + return path != null ? FILE_URL_SCHEME + path : null; } protected boolean validateImageTransfer() { @@ -201,7 +215,7 @@ && validate(diskImagesValidator.diskImagesNotIllegal()) && validate(storageDomainValidator.isDomainExistAndActive()); if (diskImage.getDiskStorageType() == DiskStorageType.MANAGED_BLOCK_STORAGE) { - return validate(ManagedBlockStorageDomainValidator.isOperationSupportedByManagedBlockStorage(getActionType())); + return failValidation(EngineMessage.ACTION_TYPE_FAILED_UNSUPPORTED_ACTION_FOR_MANAGED_BLOCK_STORAGE_TYPE); } if (isBackup()) { @@ -217,11 +231,11 @@ && validateActiveDiskPluggedToAnyNonDownVm(diskImage, diskValidator) && validate(diskImagesValidator.diskImagesNotLocked()); } - private boolean validateActiveDiskPluggedToAnyNonDownVm(DiskImage diskImage, DiskValidator diskValidator) { + protected boolean validateActiveDiskPluggedToAnyNonDownVm(DiskImage diskImage, DiskValidator diskValidator) { return diskImage.isDiskSnapshot() || validate(diskValidator.isDiskPluggedToAnyNonDownVm(false)); } - private ValidationResult isVmBackupReady() { + protected ValidationResult isVmBackupReady() { if (getBackup() == null) { return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_VM_BACKUP_NOT_EXIST); } @@ -232,7 +246,7 @@ private ValidationResult isVmBackupReady() { return ValidationResult.VALID; } - private ValidationResult isFormatApplicableForBackup() { + protected ValidationResult isFormatApplicableForBackup() { if (getParameters().getVolumeFormat() == VolumeFormat.COW) { return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_FORMAT_NOT_APPLICABLE_FOR_BACKUP); } @@ -369,6 +383,13 @@ private AddDiskParameters getAddDiskParameters() { diskParameters.setShouldRemainIllegalOnFailedExecution(true); diskParameters.setSkipDomainCheck(true); diskParameters.setEndProcedure(ActionParametersBase.EndProcedure.COMMAND_MANAGED); + // When conversion will run after upload: first volume must be in source (upload) format so VDSM + // receives the file as-is; we then create a second volume in destination format and convert. + if (needsConversionAfterUpload() && diskParameters.getDiskInfo() instanceof DiskImage) { + DiskImage diskInfo = (DiskImage) diskParameters.getDiskInfo(); + diskInfo.setVolumeFormat(getParameters().getSourceVolumeFormat()); + diskInfo.setActualSizeInBytes(getParameters().getTransferSize()); + } return diskParameters; } @@ -402,7 +423,7 @@ protected DiskImage getDiskImage() { return diskImage; } - private VmBackup getBackup() { + protected VmBackup getBackup() { if (backup == null) { backup = vmBackupDao.get(getParameters().getBackupId()); } @@ -537,6 +558,10 @@ private boolean isImageProvided() { } private VolumeFormat getTransferImageFormat() { + // When browser upload with format conversion: transfer uses source format (upload volume). + if (getParameters().getSourceVolumeFormat() != null) { + return getParameters().getSourceVolumeFormat(); + } if (getParameters().getVolumeFormat() != null) { return getParameters().getVolumeFormat(); } @@ -551,10 +576,33 @@ protected ImageTransferBackend getTransferBackend() { // Incremental backup uses NBD transfer backend return ImageTransferBackend.NBD; } - return getParameters().getVolumeFormat() == VolumeFormat.RAW ? + VolumeFormat formatForBackend = getParameters().getSourceVolumeFormat() != null + ? getParameters().getSourceVolumeFormat() + : getParameters().getVolumeFormat(); + return formatForBackend == VolumeFormat.RAW ? ImageTransferBackend.NBD : ImageTransferBackend.FILE; } + /** + * True when upload needs format conversion: we upload to a volume in source format, + * then convert to destination format and replace. + */ + protected boolean needsConversionAfterUpload() { + if (getParameters().getTransferType() != TransferType.Upload) { + log.debug("needsConversionAfterUpload: false (not upload)"); + return false; + } + VolumeFormat srcFmt = getParameters().getSourceVolumeFormat(); + VolumeFormat dstFmt = getParameters().getVolumeFormat(); + log.debug("needsConversionAfterUpload: srcFmt={} dstFmt={}", srcFmt, dstFmt); + if (srcFmt != null && dstFmt != null && !srcFmt.equals(dstFmt)) { + log.debug("needsConversionAfterUpload: true (src != dst)"); + return true; + } + log.debug("needsConversionAfterUpload: false"); + return false; + } + public void proceedCommandExecution(Guid childCmdId) { ImageTransfer entity = imageTransferDao.get(getCommandId()); if (entity == null || entity.getPhase() == null) { @@ -615,6 +663,12 @@ public void executeStateHandler(ImageTransfer entity, long timestamp, Guid child case FINISHED_CLEANUP: handleFinishedCleanup(); break; + case CONVERTING: + handleConverting(context); + break; + default: + // UNKNOWN, FINISHED_SUCCESS, FINISHED_FAILURE - no action + break; } } @@ -893,6 +947,29 @@ private void handleFinalizingSuccess(final StateContext context) { if (stopImageTransferSession(context.entity)) { Guid transferingVdsId = context.entity.getVdsId(); + // When browser upload and source format != destination: convert via qemu-img then replace volume. + if (getParameters().getTransferType() == TransferType.Upload) { + log.info("Upload transfer '{}': srcFmt={} destFmt={} browser={} needsConversion={}", + getCommandId(), getParameters().getSourceVolumeFormat(), getParameters().getVolumeFormat(), + getParameters().getTransferClientType().isBrowserTransfer(), needsConversionAfterUpload()); + } + if (needsConversionAfterUpload()) { + log.info("Upload transfer '{}' requires format conversion: {} -> {}", + getCommandId(), getParameters().getSourceVolumeFormat(), getParameters().getVolumeFormat()); + ManagedBlockUploadConversionResult mbsResult = startManagedBlockUploadConversion(context); + if (mbsResult != ManagedBlockUploadConversionResult.NOT_APPLICABLE) { + if (mbsResult == ManagedBlockUploadConversionResult.STARTED) { + updateEntityPhase(ImageTransferPhase.CONVERTING); + } else { + nextImageStatus = ImageStatus.ILLEGAL; + updateEntityPhase(ImageTransferPhase.FINALIZING_FAILURE); + tearDownImage(context.entity.getVdsId(), context.entity.getBackupId()); + setImageStatus(nextImageStatus); + } + return; + } + } + // Verify image is relevant only on upload if (getParameters().getTransferType() == TransferType.Download) { setAuditLogTypeFromPhase(ImageTransferPhase.FINISHED_SUCCESS); @@ -924,7 +1001,126 @@ private void handleFinalizingSuccess(final StateContext context) { } } - private boolean verifyImage(Guid transferingVdsId) { + protected enum ManagedBlockUploadConversionResult { + NOT_APPLICABLE, + STARTED, + FAILED + } + + protected ManagedBlockUploadConversionResult startManagedBlockUploadConversion(StateContext context) { + return ManagedBlockUploadConversionResult.NOT_APPLICABLE; + } + + protected boolean handleManagedBlockConverting(StateContext context) { + return false; + } + + protected boolean hostSelectionIgnoresDomainCache() { + return false; + } + + protected boolean connectManagedBlockVolumeBeforeNbd() { + return true; + } + + protected void detachManagedBlockVolumeWhenSessionStops(ImageTransfer entity) { + } + + private void handleConverting(StateContext context) { + if (handleManagedBlockConverting(context)) { + return; + } + + Guid copyTaskId = getParameters().getCopyTaskId(); + if (copyTaskId == null) { + log.error("Conversion phase but no copy task id for transfer '{}'", getCommandId()); + updateEntityPhase(ImageTransferPhase.FINALIZING_FAILURE); + setCommandStatus(CommandStatus.FAILED); + return; + } + Guid spmId = getStoragePool().getSpmVdsId(); + if (spmId == null || Guid.Empty.equals(spmId)) { + log.debug("Waiting for SPM for transfer '{}'", getCommandId()); + return; + } + try { + VDSReturnValue vdsReturnValue = runVdsCommand(VDSCommandType.HSMGetTaskStatus, + new HSMTaskGuidBaseVDSCommandParameters(spmId, copyTaskId)); + if (!vdsReturnValue.getSucceeded()) { + log.debug("Could not get copy task status for transfer '{}'", getCommandId()); + return; + } + AsyncTaskStatus taskStatus = (AsyncTaskStatus) vdsReturnValue.getReturnValue(); + if (taskStatus.getStatus() == AsyncTaskStatusEnum.finished) { + log.info("Upload conversion copy task finished for transfer '{}', completing conversion", getCommandId()); + finishUploadConversion(context); + } else if (taskStatus.getStatus() == AsyncTaskStatusEnum.unknown + || taskStatus.getStatus() == AsyncTaskStatusEnum.aborting) { + log.error("Copy task failed for transfer '{}': {}", getCommandId(), taskStatus); + updateEntityPhase(ImageTransferPhase.FINALIZING_FAILURE); + setCommandStatus(CommandStatus.FAILED); + } + } catch (Exception e) { + log.debug("Polling copy task for transfer '{}': {}", getCommandId(), e.getMessage()); + } + } + + private void finishUploadConversion(StateContext context) { + Guid oldImageId = getDiskImage().getImageId(); + Guid diskId = getParameters().getImageGroupID(); + Guid newVolId = getParameters().getConvertedVolumeId(); + DiskImage currentImage = getDiskImage(); + + log.info("Finishing upload conversion for transfer '{}': replacing oldVol={} with newVol={} (disk={})", + getCommandId(), oldImageId, newVolId, diskId); + + DiskImage newImage = new DiskImage(); + newImage.setId(diskId); + newImage.setImageId(newVolId); + newImage.setVolumeFormat(getParameters().getVolumeFormat()); + newImage.setVolumeType(currentImage.getVolumeType()); + newImage.setSize(currentImage.getSize()); + newImage.setDiskAlias(currentImage.getDiskAlias()); + newImage.setDiskDescription(currentImage.getDiskDescription()); + newImage.setStorageIds(currentImage.getStorageIds()); + newImage.setStoragePoolId(currentImage.getStoragePoolId()); + newImage.setActive(true); + newImage.setParentId(Guid.Empty); + newImage.setImageTemplateId(Guid.Empty); + newImage.setQuotaId(currentImage.getQuotaId()); + newImage.setDiskProfileId(currentImage.getDiskProfileId()); + newImage.setWipeAfterDelete(currentImage.isWipeAfterDelete()); + if (VolumeFormat.COW.equals(getParameters().getVolumeFormat())) { + newImage.setBackup(currentImage.getBackup()); + } + + imagesHandler.saveImage(newImage); + baseDiskDao.update(newImage); // Disk already exists from AddDisk; update, don't insert + log.info("Upload conversion for transfer '{}': saved new image disk={} vol={}", getCommandId(), diskId, newVolId); + + DiskImageDynamic diskDynamic = new DiskImageDynamic(); + diskDynamic.setId(newVolId); + diskDynamic.setActualSize(currentImage.getActualSizeInBytes()); + diskImageDynamicDao.save(diskDynamic); + + log.info("Upload conversion for transfer '{}': removing old volume {}", getCommandId(), oldImageId); + runInternalAction(ActionType.RemoveImage, new RemoveImageParameters(oldImageId)); + + setImageId(newVolId); + + setVolumeLegalityInStorage(LEGAL_IMAGE); + if (VolumeFormat.COW.equals(getParameters().getVolumeFormat())) { + setQcowCompat(getDiskImage().getImage(), getStoragePool().getId(), getDiskImage().getId(), + getDiskImage().getImageId(), getStorageDomainId(), context.entity.getVdsId()); + imageDao.update(getDiskImage().getImage()); + } + setImageStatus(ImageStatus.OK); + tearDownImage(context.entity.getVdsId(), context.entity.getBackupId()); + setAuditLogTypeFromPhase(ImageTransferPhase.FINISHED_SUCCESS); + setCommandStatus(CommandStatus.SUCCEEDED); + } + + protected boolean verifyImage(Guid transferingVdsId) { ImageActionsVDSCommandParameters parameters = new ImageActionsVDSCommandParameters(transferingVdsId, getStoragePool().getId(), getStorageDomainId(), @@ -1069,6 +1265,17 @@ protected boolean startImageTransferSession() { return false; } + // When host does not return a path (e.g. managed block), NBD path will be set below + if (imagePath == null && !usingNbdServer()) { + log.error("Image prepare did not return a path for image transfer '{}' (storage may not support file path)", getCommandId()); + setImageStatus(ImageStatus.OK); + setCommandStatus(CommandStatus.FAILED); + return false; + } + if (imagePath == null) { + imagePath = ""; + } + // From this point if an operation fails we have to perform cleanup Guid imagedTicketId = Guid.newGuid(); ImageTransfer updates = new ImageTransfer(); @@ -1079,6 +1286,9 @@ protected boolean startImageTransferSession() { updateEntity(updates); if (usingNbdServer()) { + if (!connectManagedBlockVolumeBeforeNbd()) { + return false; + } try { VDSReturnValue vdsReturnValue = runVdsCommand(VDSCommandType.StartNbdServer, getStartNbdServerParameters(getVdsId())); @@ -1136,6 +1346,9 @@ private static boolean proxyEnabled() { @Override protected VDS checkForActiveVds() { Guid hostForExecution = vdsCommandsHelper.getHostForExecution(getStoragePoolId(), host -> { + if (hostSelectionIgnoresDomainCache()) { + return true; + } var domainsData = resourceManager.getVdsManager(host.getId()).getDomains(); if (domainsData == null) { return false; @@ -1235,15 +1448,15 @@ private boolean isSparseImage() { getStorageDomain().getStorageType().isFileDomain(); } - private boolean isBackup() { + protected boolean isBackup() { return getParameters().getBackupId() != null; } - private boolean isLiveBackup() { + protected boolean isLiveBackup() { return isBackup() && getBackup().getBackupType() == VmBackupType.Live; } - private boolean isHybridBackup() { + protected boolean isHybridBackup() { return isBackup() && getBackup().getBackupType() == VmBackupType.Hybrid; } @@ -1295,7 +1508,7 @@ private ImageTicket buildImageTicket(Guid ticketId, String ticketUrl) { return ticket; } - private boolean setVolumeLegalityInStorage(boolean legal) { + protected boolean setVolumeLegalityInStorage(boolean legal) { SetVolumeLegalityVDSCommandParameters parameters = new SetVolumeLegalityVDSCommandParameters(getStoragePool().getId(), getStorageDomainId(), @@ -1402,6 +1615,7 @@ private boolean stopImageTransferSession(ImageTransfer entity) { if (usingNbdServer()) { stopNbdServer(entity.getVdsId()); } + detachManagedBlockVolumeWhenSessionStops(entity); ImageTransfer updates = new ImageTransfer(); updateEntity(updates, true); @@ -1448,13 +1662,13 @@ private void updateEntityFinalPhase(ImageTransferPhase phase) { setAuditLogTypeFromPhase(phase); } - private void updateEntityPhase(ImageTransferPhase phase) { + protected void updateEntityPhase(ImageTransferPhase phase) { ImageTransfer updates = new ImageTransfer(getCommandId()); updates.setPhase(phase); updateEntity(updates); } - private void updateEntityPhaseToStoppedBySystem(AuditLogType stoppedBySystemReason) { + protected void updateEntityPhaseToStoppedBySystem(AuditLogType stoppedBySystemReason) { auditLog(this, stoppedBySystemReason); if (getParameters().getTransferType() == TransferType.Upload) { updateEntityPhase(ImageTransferPhase.PAUSED_SYSTEM); @@ -1505,7 +1719,7 @@ private static String getImageDaemonUri(String daemonHostname) { return HTTPS_SCHEME + daemonHostname + ":" + port; } - private void setAuditLogTypeFromPhase(ImageTransferPhase phase) { + protected void setAuditLogTypeFromPhase(ImageTransferPhase phase) { if (getParameters().getAuditLogType() != null) { // Some flows, e.g. cancellation, may set the log type more than once. // In this case, the first type is the most accurate. @@ -1593,9 +1807,9 @@ protected LockProperties applyLockProperties(LockProperties lockProperties) { } // Container for context needed by state machine handlers - class StateContext { - ImageTransfer entity; - long iterationTimestamp; - Guid childCmdId; + protected static class StateContext { + public ImageTransfer entity; + public long iterationTimestamp; + public Guid childCmdId; } } diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/managedblock/AddManagedBlockStorageDiskCommand.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/managedblock/AddManagedBlockStorageDiskCommand.java index 1669c5c1ca0..eca8a0d0ba1 100644 --- a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/managedblock/AddManagedBlockStorageDiskCommand.java +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/managedblock/AddManagedBlockStorageDiskCommand.java @@ -18,6 +18,8 @@ import org.ovirt.engine.core.bll.utils.VmDeviceUtils; import org.ovirt.engine.core.common.VdcObjectType; import org.ovirt.engine.core.common.action.AddManagedBlockStorageDiskParameters; +import org.ovirt.engine.core.common.businessentities.StorageDomain; +import org.ovirt.engine.core.common.businessentities.StoragePoolIsoMap; import org.ovirt.engine.core.common.businessentities.SubjectEntity; import org.ovirt.engine.core.common.businessentities.VmDevice; import org.ovirt.engine.core.common.businessentities.storage.DiskVmElement; @@ -37,6 +39,8 @@ import org.ovirt.engine.core.dao.DiskVmElementDao; import org.ovirt.engine.core.dao.ImageDao; import org.ovirt.engine.core.dao.ManagedBlockStorageDao; +import org.ovirt.engine.core.dao.StorageDomainDao; +import org.ovirt.engine.core.dao.StoragePoolIsoMapDao; import org.ovirt.engine.core.utils.JsonHelper; import org.ovirt.engine.core.utils.transaction.TransactionSupport; @@ -66,6 +70,12 @@ public class AddManagedBlockStorageDiskCommand(Arrays.asList(getParameters().getStorageDomainId()))); + + Guid storagePoolId = null; + StorageDomain storageDomain = + storageDomainDao.get(getParameters().getStorageDomainId()); + if (storageDomain != null) { + storagePoolId = storageDomain.getStoragePoolId(); + } + if (storagePoolId == null) { + List maps = + storagePoolIsoMapDao.getAllForStorage(getParameters().getStorageDomainId()); + if (!maps.isEmpty() && maps.get(0).getStoragePoolId() != null) { + storagePoolId = maps.get(0).getStoragePoolId(); + } + } + if (storagePoolId != null) { + disk.setStoragePoolId(storagePoolId); + } disk.setVolumeType(VolumeType.Unassigned); disk.setVolumeFormat(VolumeFormat.RAW); disk.setImageStatus(ImageStatus.OK); diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/managedblock/MbsTransferDiskImageCommand.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/managedblock/MbsTransferDiskImageCommand.java new file mode 100644 index 00000000000..0729f4593fb --- /dev/null +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/managedblock/MbsTransferDiskImageCommand.java @@ -0,0 +1,657 @@ +package org.ovirt.engine.core.bll.storage.disk.managedblock; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.ovirt.engine.core.bll.NonTransactiveCommandAttribute; +import org.ovirt.engine.core.bll.context.CommandContext; +import org.ovirt.engine.core.bll.storage.disk.image.TransferDiskImageCommand; +import org.ovirt.engine.core.bll.validator.storage.DiskImagesValidator; +import org.ovirt.engine.core.bll.validator.storage.DiskValidator; +import org.ovirt.engine.core.bll.validator.storage.ManagedBlockStorageDomainValidator; +import org.ovirt.engine.core.bll.validator.storage.StorageDomainValidator; +import org.ovirt.engine.core.common.action.ActionReturnValue; +import org.ovirt.engine.core.common.action.ActionType; +import org.ovirt.engine.core.common.action.ConnectManagedBlockStorageDeviceCommandParameters; +import org.ovirt.engine.core.common.action.DisconnectManagedBlockStorageDeviceParameters; +import org.ovirt.engine.core.common.action.TransferDiskImageParameters; +import org.ovirt.engine.core.common.businessentities.VDS; +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.DiskImageDynamic; +import org.ovirt.engine.core.common.businessentities.storage.ImageStatus; +import org.ovirt.engine.core.common.businessentities.storage.ImageTransfer; +import org.ovirt.engine.core.common.businessentities.storage.ImageTransferPhase; +import org.ovirt.engine.core.common.businessentities.storage.ManagedBlockStorage; +import org.ovirt.engine.core.common.businessentities.storage.ManagedBlockStorageDisk; +import org.ovirt.engine.core.common.businessentities.storage.VolumeFormat; +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.utils.SizeConverter; +import org.ovirt.engine.core.common.utils.managedblock.ManagedBlockCommandParameters; +import org.ovirt.engine.core.common.utils.managedblock.ManagedBlockExecutor; +import org.ovirt.engine.core.common.utils.managedblock.ManagedBlockExecutor.ManagedBlockCommand; +import org.ovirt.engine.core.common.vdscommands.AttachManagedBlockStorageVolumeVDSCommandParameters; +import org.ovirt.engine.core.common.vdscommands.ConvertManagedBlockVolumeVDSCommandParameters; +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.ManagedBlockStorageDao; +import org.ovirt.engine.core.utils.JsonHelper; +import org.ovirt.engine.core.utils.transaction.TransactionSupport; + +@NonTransactiveCommandAttribute +public class MbsTransferDiskImageCommand + extends TransferDiskImageCommand { + + @Inject + private ManagedBlockStorageDao managedBlockStorageDao; + @Inject + private ManagedBlockExecutor managedBlockExecutor; + + public MbsTransferDiskImageCommand(T parameters, CommandContext cmdContext) { + super(parameters, cmdContext); + } + + @Override + protected boolean hostSelectionIgnoresDomainCache() { + return true; + } + + @Override + protected boolean validateImageTransfer() { + DiskImage diskImage = getDiskImage(); + if (!validate(ManagedBlockStorageDomainValidator.isOperationSupportedByManagedBlockStorage(getActionType()))) { + return false; + } + DiskValidator diskValidator = getDiskValidator(diskImage); + DiskImagesValidator diskImagesValidator = getDiskImagesValidator(diskImage); + StorageDomainValidator storageDomainValidator = getStorageDomainValidator( + storageDomainDao.getForStoragePool(diskImage.getStorageIds().get(0), diskImage.getStoragePoolId())); + boolean isValid = + validate(diskValidator.isDiskExists()) + && validate(diskImagesValidator.diskImagesNotIllegal()) + && validate(storageDomainValidator.isDomainExistAndActive()); + + if (isBackup()) { + if (isHybridBackup()) { + if (!snapshotDao.exists(getBackup().getVmId(), getBackup().getSnapshotId())) { + return failValidation(EngineMessage.ACTION_TYPE_FAILED_VM_SNAPSHOT_DOES_NOT_EXIST); + } + } + return isValid && validate(isVmBackupReady()) && validate(isFormatApplicableForBackup()); + } + return isValid + && validateActiveDiskPluggedToAnyNonDownVm(diskImage, diskValidator) + && validate(diskImagesValidator.diskImagesNotLocked()); + } + + @Override + protected String prepareImage(Guid vdsId) { + if (isLiveBackup()) { + return super.prepareImage(vdsId); + } + validateHostConnectorForMbs(); + DiskImage disk = getDiskImage(); + if (disk instanceof ManagedBlockStorageDisk) { + String path = connectAttachAndGetMbsVolumePath((ManagedBlockStorageDisk) disk); + if (path != null) { + return path; + } + } + return null; + } + + @Override + protected void tearDownImage(Guid vdsId, Guid backupId) { + if (backupId != null) { + return; + } + + DiskImage image = getDiskImage(); + if (image.isDiskSnapshot() && !isDiskSnapshotPluggedToDownVmsOnly(image)) { + return; + } + + ImageTransfer entity = imageTransferDao.get(getCommandId()); + if (entity != null && entity.getDiskId() != null) { + detachManagedBlockVolumeFromHost(entity); + disconnectManagedBlockVolumeForTransfer(entity); + } + } + + @Override + protected boolean needsConversionAfterUpload() { + if (super.needsConversionAfterUpload()) { + return true; + } + if (getParameters().getSourceVolumeFormat() == null + && VolumeFormat.COW.equals(getParameters().getVolumeFormat())) { + log.debug("needsConversionAfterUpload: true (MBS qcow2 upload)"); + return true; + } + return false; + } + + @Override + protected boolean connectManagedBlockVolumeBeforeNbd() { + return connectAndAttachManagedBlockVolumeForTransfer(); + } + + @Override + protected void detachManagedBlockVolumeWhenSessionStops(ImageTransfer entity) { + if (!needsConversionAfterUpload() && entity.getDiskId() != null) { + detachManagedBlockVolumeFromHost(entity); + disconnectManagedBlockVolumeForTransfer(entity); + } + } + + @Override + protected ManagedBlockUploadConversionResult startManagedBlockUploadConversion(StateContext context) { + return startMbsUploadConversion(context) ? ManagedBlockUploadConversionResult.STARTED + : ManagedBlockUploadConversionResult.FAILED; + } + + @Override + protected boolean handleManagedBlockConverting(StateContext context) { + if (getParameters().getConvertedVolumeId() == null) { + return false; + } + Guid sdId = getStorageDomainId(); + Guid srcVolId = getDiskImage().getImageId(); + Guid dstVolId = getParameters().getConvertedVolumeId(); + VolumeFormat srcFmt = getParameters().getSourceVolumeFormat() != null + ? getParameters().getSourceVolumeFormat() + : getParameters().getVolumeFormat(); + VolumeFormat dstFmt = getParameters().getSourceVolumeFormat() != null + && getParameters().getVolumeFormat() != null + ? getParameters().getVolumeFormat() + : VolumeFormat.RAW; + String srcFormat = srcFmt == VolumeFormat.COW ? "qcow2" : "raw"; + String dstFormat = dstFmt == VolumeFormat.COW ? "qcow2" : "raw"; + + VDS vds = vdsDao.get(context.entity.getVdsId()); + if (vds == null) { + log.error("Host not found for MBS conversion"); + updateEntityPhase(ImageTransferPhase.FINALIZING_FAILURE); + setCommandStatus(CommandStatus.FAILED); + return true; + } + try { + ConvertManagedBlockVolumeVDSCommandParameters convertParams = + new ConvertManagedBlockVolumeVDSCommandParameters(vds, sdId, srcVolId, dstVolId, srcFormat, dstFormat); + VDSReturnValue vdsReturnValue = runVdsCommand(VDSCommandType.ConvertManagedBlockVolume, convertParams); + if (!vdsReturnValue.getSucceeded()) { + log.error("ConvertManagedBlockVolume failed for transfer '{}': {}", getCommandId(), vdsReturnValue.getVdsError()); + updateEntityPhase(ImageTransferPhase.FINALIZING_FAILURE); + setCommandStatus(CommandStatus.FAILED); + return true; + } + log.info("MBS upload conversion completed for transfer '{}', finishing", getCommandId()); + finishMbsUploadConversion(context); + } catch (Exception e) { + log.error("Failed MBS conversion for transfer '{}': {}", getCommandId(), e); + updateEntityPhase(ImageTransferPhase.FINALIZING_FAILURE); + setCommandStatus(CommandStatus.FAILED); + } + return true; + } + + @Override + protected boolean setVolumeLegalityInStorage(boolean legal) { + return true; + } + + @Override + protected boolean verifyImage(Guid transferingVdsId) { + return true; + } + + private boolean connectAndAttachManagedBlockVolumeForTransfer() { + DiskImage disk = getDiskImage(); + if (!(disk instanceof ManagedBlockStorageDisk)) { + return true; + } + ManagedBlockStorageDisk mbsDisk = (ManagedBlockStorageDisk) disk; + Guid storageDomainId = mbsDisk.getStorageIds().isEmpty() ? getStorageDomainId() : mbsDisk.getStorageIds().get(0); + + ActionReturnValue connectResult = connectManagedBlockStorageDeviceForTransfer(mbsDisk, storageDomainId); + if (!connectResult.getSucceeded()) { + log.error("Failed to connect managed block volume for image transfer '{}': {}", + getCommandId(), connectResult.getFault()); + updateEntityPhaseToStoppedBySystem( + org.ovirt.engine.core.common.AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); + return false; + } + + @SuppressWarnings("unchecked") + Map connectionInfo = (Map) connectResult.getActionReturnValue(); + if (connectionInfo != null && !attachManagedBlockVolumeToHostForTransfer(mbsDisk, storageDomainId, connectionInfo)) { + return false; + } + return true; + } + + private ActionReturnValue connectManagedBlockStorageDeviceForTransfer(ManagedBlockStorageDisk mbsDisk, + Guid storageDomainId) { + ConnectManagedBlockStorageDeviceCommandParameters connectParams = + new ConnectManagedBlockStorageDeviceCommandParameters(storageDomainId, + getVds().getConnectorInfo(), + mbsDisk.getImageId()); + return runInternalAction(ActionType.ConnectManagedBlockStorageDevice, connectParams); + } + + private boolean attachManagedBlockVolumeToHostForTransfer(ManagedBlockStorageDisk mbsDisk, + Guid storageDomainId, + Map connectionInfo) { + AttachManagedBlockStorageVolumeVDSCommandParameters attachParams = + new AttachManagedBlockStorageVolumeVDSCommandParameters(getVds(), + connectionInfo, + storageDomainId); + attachParams.setVolumeId(mbsDisk.getImageId()); + VDSReturnValue attachResult = runVdsCommand(VDSCommandType.AttachManagedBlockStorageVolume, attachParams); + if (!attachResult.getSucceeded()) { + log.error("Failed to attach managed block volume to host for image transfer '{}': {}", + getCommandId(), attachResult.getVdsError()); + updateEntityPhaseToStoppedBySystem( + org.ovirt.engine.core.common.AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); + return false; + } + return true; + } + + private enum MbsConversionStartProgress { + VOLUME_CREATED, + CONNECTED_ON_HOST, + ATTACHED_ON_HOST + } + + private boolean startMbsUploadConversion(StateContext context) { + Guid sdId = getStorageDomainId(); + Guid dstVolId = Guid.newGuid(); + ManagedBlockStorage managedBlockStorage = managedBlockStorageDao.get(sdId); + if (managedBlockStorage == null) { + log.error("Managed block storage domain '{}' not found for conversion", sdId); + return false; + } + + log.debug("MBS upload conversion for transfer '{}': disk={} srcVol={} -> dstVol={}, srcFmt={} destFmt={}", + getCommandId(), getParameters().getImageGroupID(), getDiskImage().getImageId(), dstVolId, + getParameters().getSourceVolumeFormat(), getParameters().getVolumeFormat()); + + if (!mbsConversionCreateVolume(managedBlockStorage, dstVolId)) { + return false; + } + + VDS vds = vdsDao.get(context.entity.getVdsId()); + if (vds == null || vds.getConnectorInfo() == null) { + log.error("Host or connector info missing for MBS conversion"); + cleanupOrphanMbsConversionVolume(managedBlockStorage, sdId, vds, dstVolId, null, + MbsConversionStartProgress.VOLUME_CREATED); + return false; + } + + ActionReturnValue connectResult = mbsConversionConnectVolume(vds, sdId, dstVolId); + if (!connectResult.getSucceeded()) { + log.error("Connect failed for MBS conversion: {}", connectResult.getFault()); + cleanupOrphanMbsConversionVolume(managedBlockStorage, sdId, vds, dstVolId, null, + MbsConversionStartProgress.VOLUME_CREATED); + return false; + } + Map connectionInfo = connectResult.getActionReturnValue(); + if (connectionInfo == null) { + log.error("No connection info returned for MBS conversion"); + cleanupOrphanMbsConversionVolume(managedBlockStorage, sdId, vds, dstVolId, null, + MbsConversionStartProgress.VOLUME_CREATED); + return false; + } + + if (!mbsConversionAttachVolume(vds, connectionInfo, sdId, dstVolId)) { + log.error("Attach failed for MBS conversion"); + cleanupOrphanMbsConversionVolume(managedBlockStorage, sdId, vds, dstVolId, connectionInfo, + MbsConversionStartProgress.CONNECTED_ON_HOST); + return false; + } + + try { + getParameters().setConvertedVolumeId(dstVolId); + persistCommand(getParameters().getParentCommand(), true); + log.info("Started MBS upload conversion for transfer '{}': convertedVolumeId={}", + getCommandId(), dstVolId); + return true; + } catch (Exception e) { + log.error("Failed to persist MBS upload conversion for transfer '{}': {}", getCommandId(), e); + cleanupOrphanMbsConversionVolume(managedBlockStorage, sdId, vds, dstVolId, connectionInfo, + MbsConversionStartProgress.ATTACHED_ON_HOST); + return false; + } + } + + private boolean mbsConversionCreateVolume(ManagedBlockStorage managedBlockStorage, Guid dstVolId) { + try { + long sizeGiB = SizeConverter.convert(getDiskImage().getSize(), + SizeConverter.SizeUnit.BYTES, SizeConverter.SizeUnit.GiB).longValue(); + List extraParams = new ArrayList<>(); + extraParams.add(dstVolId.toString()); + extraParams.add(Long.toString(sizeGiB)); + ManagedBlockCommandParameters params = new ManagedBlockCommandParameters( + JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), + extraParams, getCorrelationId()); + if (!managedBlockExecutor.runCommand(ManagedBlockCommand.CREATE_VOLUME, params).getSucceed()) { + log.error("CREATE_VOLUME failed for transfer '{}'", getCommandId()); + return false; + } + return true; + } catch (Exception e) { + log.error("CREATE_VOLUME raised for transfer '{}': {}", getCommandId(), e); + return false; + } + } + + private ActionReturnValue mbsConversionConnectVolume(VDS vds, Guid sdId, Guid dstVolId) { + ConnectManagedBlockStorageDeviceCommandParameters connectParams = + new ConnectManagedBlockStorageDeviceCommandParameters(sdId, vds.getConnectorInfo(), dstVolId); + return runInternalAction(ActionType.ConnectManagedBlockStorageDevice, connectParams); + } + + private boolean mbsConversionAttachVolume(VDS vds, Map connectionInfo, Guid sdId, + Guid dstVolId) { + AttachManagedBlockStorageVolumeVDSCommandParameters attachParams = + new AttachManagedBlockStorageVolumeVDSCommandParameters(vds, connectionInfo, sdId); + attachParams.setVolumeId(dstVolId); + try { + VDSReturnValue attachResult = runVdsCommand(VDSCommandType.AttachManagedBlockStorageVolume, attachParams); + if (!attachResult.getSucceeded()) { + log.error("Attach failed for MBS conversion: {}", attachResult.getVdsError()); + return false; + } + return true; + } catch (Exception e) { + log.error("Attach raised for MBS conversion: {}", e); + return false; + } + } + + private void cleanupOrphanMbsConversionVolume(ManagedBlockStorage mbs, Guid sdId, VDS vds, + Guid volId, Map connectionInfo, MbsConversionStartProgress progress) { + if (mbs == null) { + return; + } + log.warn("Cleaning up orphan MBS conversion volume '{}' for transfer '{}' (progress={})", + volId, getCommandId(), progress); + if (progress == MbsConversionStartProgress.ATTACHED_ON_HOST && vds != null) { + mbsTryDetach(vds, sdId, volId); + } + if (progress == MbsConversionStartProgress.CONNECTED_ON_HOST + || progress == MbsConversionStartProgress.ATTACHED_ON_HOST) { + if (vds != null && connectionInfo != null) { + mbsConversionTryDisconnectDevice(sdId, vds, volId, connectionInfo); + } + mbsTryDisconnect(mbs, volId); + } + mbsTryDelete(mbs, volId); + } + + private void mbsConversionTryDisconnectDevice(Guid sdId, VDS vds, Guid volId, + Map connectionInfo) { + try { + DisconnectManagedBlockStorageDeviceParameters disconnectParams = + new DisconnectManagedBlockStorageDeviceParameters(sdId, connectionInfo, volId, vds.getId()); + runInternalAction(ActionType.DisconnectManagedBlockStorageDevice, disconnectParams); + } catch (Exception e) { + log.warn("Orphan cleanup: disconnect device for volume {} failed: {}", volId, e); + } + } + + /** + * Finish MBS conversion keeping base_disks.disk_id stable. + * + * The upload creates a volume whose UUID becomes disk_id. Conversion creates a + * second volume (newVolId). Instead of creating a new base_disks row and + * repointing every referencing table, we keep disk_id unchanged and only swap + * the images row — exactly what finishUploadConversion does for non-MBS. + * + * DB invariant before: images.image_group_id = diskId, images.image_guid = oldImageId + * DB invariant after: images.image_group_id = diskId, images.image_guid = newVolId + * base_disks.disk_id = diskId throughout — no repoint needed. + */ + private void finishMbsUploadConversion(StateContext context) { + final Guid diskId = getParameters().getImageGroupID(); // stable — does NOT change + final Guid oldImageId = getDiskImage().getImageId(); // upload volume UUID + final Guid newVolId = getParameters().getConvertedVolumeId(); // converted volume UUID + final Guid sdId = getStorageDomainId(); + final DiskImage currentImage = getDiskImage(); + + VDS vds = vdsDao.get(context.entity.getVdsId()); + if (vds == null) { + log.error("Host not found for MBS conversion finish"); + setCommandStatus(CommandStatus.FAILED); + return; + } + log.info("Finishing MBS upload conversion for transfer '{}': vol {} -> {} (disk={} stable)", + getCommandId(), oldImageId, newVolId, diskId); + + ManagedBlockStorage mbs = managedBlockStorageDao.get(sdId); + + mbsTryDetach(vds, sdId, oldImageId); + mbsTryDisconnect(mbs, oldImageId); + mbsTryDelete(mbs, oldImageId); + + TransactionSupport.executeInNewTransaction(() -> { + imageStorageDomainMapDao.remove(oldImageId); + diskImageDynamicDao.remove(oldImageId); + imageDao.remove(oldImageId); + + DiskImage finishedImage = buildConversionFinishImage(diskId, newVolId, currentImage); + imagesHandler.saveImage(finishedImage); // images: image_group_id=diskId, image_guid=newVolId + baseDiskDao.update(new ManagedBlockStorageDisk(finishedImage)); // update format in-place + + DiskImageDynamic dynamic = new DiskImageDynamic(); + dynamic.setId(newVolId); + dynamic.setActualSize(currentImage.getActualSizeInBytes()); + diskImageDynamicDao.save(dynamic); + return null; + }); + + setImageId(newVolId); // update images-layer ref; diskId is unchanged + + // Storage: detach/disconnect the converted volume (still attached from conversion). + detachAndDisconnectMbsVolume(vds, newVolId, sdId, mbs); + + completeMbsConversionSuccess(context); + } + + /** + * Build the DiskImage for the conversion result. + * diskId = stable disk entity UUID (base_disks.disk_id = images.image_group_id) — unchanged. + * imageId = new converted volume UUID (images.image_guid). + */ + private DiskImage buildConversionFinishImage(Guid diskId, Guid imageId, DiskImage src) { + VolumeFormat destFormat = getParameters().getVolumeFormat() != null + && getParameters().getSourceVolumeFormat() != null + ? getParameters().getVolumeFormat() + : VolumeFormat.RAW; + DiskImage img = new DiskImage(); + img.setId(diskId); // stable — images.image_group_id + img.setImageId(imageId); // new volume — images.image_guid + img.setVolumeFormat(destFormat); + img.setVolumeType(src.getVolumeType()); + img.setSize(src.getSize()); + img.setDiskAlias(src.getDiskAlias()); + img.setDiskDescription(src.getDiskDescription()); + img.setStorageIds(src.getStorageIds()); + img.setStoragePoolId(src.getStoragePoolId()); + img.setActive(true); + img.setParentId(Guid.Empty); + img.setImageTemplateId(Guid.Empty); + img.setQuotaId(src.getQuotaId()); + img.setDiskProfileId(src.getDiskProfileId()); + img.setWipeAfterDelete(src.isWipeAfterDelete()); + if (VolumeFormat.COW.equals(getParameters().getVolumeFormat())) { + img.setBackup(src.getBackup()); + } + return img; + } + + private ManagedBlockCommandParameters buildMbsParams(ManagedBlockStorage mbs, Guid volId) + throws IOException { + return new ManagedBlockCommandParameters( + JsonHelper.mapToJson(mbs.getAllDriverOptions(), false), + Collections.singletonList(volId.toString()), + getCorrelationId()); + } + + private void mbsTryDetach(VDS vds, Guid sdId, Guid volId) { + try { + AttachManagedBlockStorageVolumeVDSCommandParameters params = + new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); + params.setVolumeId(volId); + params.setStorageDomainId(sdId); + runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, params); + } catch (Exception e) { + log.warn("Detach volume '{}' failed: {}", volId, e); + } + } + + private void mbsTryDisconnect(ManagedBlockStorage mbs, Guid volId) { + if (mbs == null) { + return; + } + try { + managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, + buildMbsParams(mbs, volId)); + } catch (Exception e) { + log.warn("Managed block DISCONNECT_VOLUME for '{}' failed: {}", volId, e); + } + } + + private void mbsTryDelete(ManagedBlockStorage mbs, Guid volId) { + if (mbs == null) { + return; + } + try { + managedBlockExecutor.runCommand(ManagedBlockCommand.DELETE_VOLUME, + buildMbsParams(mbs, volId)); + } catch (Exception e) { + log.warn("Managed block DELETE_VOLUME for '{}' failed: {}", volId, e); + } + } + + private void detachAndDisconnectMbsVolume(VDS vds, Guid volId, Guid sdId, ManagedBlockStorage mbs) { + mbsTryDetach(vds, sdId, volId); + mbsTryDisconnect(mbs, volId); + } + + private void completeMbsConversionSuccess(StateContext context) { + setVolumeLegalityInStorage(true); + if (VolumeFormat.COW.equals(getParameters().getVolumeFormat()) && getDiskImage() != null) { + setQcowCompat(getDiskImage().getImage(), getStoragePool().getId(), getDiskImage().getId(), + getDiskImage().getImageId(), getStorageDomainId(), context.entity.getVdsId()); + imageDao.update(getDiskImage().getImage()); + } + setImageStatus(ImageStatus.OK); + setAuditLogTypeFromPhase(ImageTransferPhase.FINISHED_SUCCESS); + setCommandStatus(CommandStatus.SUCCEEDED); + } + + private void validateHostConnectorForMbs() { + if (getVds() == null || getVds().getConnectorInfo() == null) { + throw new EngineException(EngineError.StorageException, + "Host has no connector info; cannot connect managed block volume for image transfer"); + } + } + + private String connectAttachAndGetMbsVolumePath(ManagedBlockStorageDisk mbsDisk) { + Guid storageDomainId = mbsDisk.getStorageIds().isEmpty() + ? getStorageDomainId() + : mbsDisk.getStorageIds().get(0); + Map connectionInfo = connectMbsVolume(storageDomainId, mbsDisk.getImageId()); + if (connectionInfo == null) { + return null; + } + return attachMbsVolumeAndGetPath(storageDomainId, mbsDisk.getImageId(), connectionInfo); + } + + private Map connectMbsVolume(Guid storageDomainId, Guid volumeId) { + ConnectManagedBlockStorageDeviceCommandParameters connectParams = + new ConnectManagedBlockStorageDeviceCommandParameters(storageDomainId, + getVds().getConnectorInfo(), + volumeId); + ActionReturnValue connectResult = + runInternalAction(ActionType.ConnectManagedBlockStorageDevice, connectParams); + if (!connectResult.getSucceeded()) { + throw new EngineException(EngineError.StorageException, + connectResult.getFault() != null ? connectResult.getFault().getMessage() : "Failed to connect managed block volume"); + } + return connectResult.getActionReturnValue(); + } + + private String attachMbsVolumeAndGetPath(Guid storageDomainId, Guid volumeId, Map connectionInfo) { + AttachManagedBlockStorageVolumeVDSCommandParameters attachParams = + new AttachManagedBlockStorageVolumeVDSCommandParameters(getVds(), connectionInfo, storageDomainId); + attachParams.setVolumeId(volumeId); + VDSReturnValue attachResult = runVdsCommand(VDSCommandType.AttachManagedBlockStorageVolume, attachParams); + if (!attachResult.getSucceeded()) { + throw new EngineException(EngineError.StorageException, + attachResult.getVdsError() != null ? attachResult.getVdsError().getMessage() : "Failed to attach managed block volume"); + } + String path = extractPathFromAttachResult(attachResult.getReturnValue()); + if (path == null) { + throw new EngineException(EngineError.StorageException, + "Attach managed block volume succeeded but did not return a path"); + } + return FILE_URL_SCHEME + path; + } + + @SuppressWarnings("unchecked") + private static String extractPathFromAttachResult(Object returnValue) { + if (!(returnValue instanceof Map)) { + return null; + } + Map resultMap = (Map) returnValue; + String path = (String) resultMap.get("path"); + if (path == null) { + path = (String) resultMap.get("managed_path"); + } + return path; + } + + private void detachManagedBlockVolumeFromHost(ImageTransfer imageTransfer) { + Disk disk = diskDao.get(imageTransfer.getDiskId()); + if (!(disk instanceof ManagedBlockStorageDisk)) { + return; + } + ManagedBlockStorageDisk mbsDisk = (ManagedBlockStorageDisk) disk; + if (mbsDisk.getStorageIds() == null || mbsDisk.getStorageIds().isEmpty()) { + return; + } + VDS vds = vdsDao.get(imageTransfer.getVdsId()); + if (vds == null) { + log.warn("Host '{}' not found for managed block volume detach", imageTransfer.getVdsId()); + return; + } + mbsTryDetach(vds, mbsDisk.getStorageIds().get(0), mbsDisk.getImageId()); + } + + private void disconnectManagedBlockVolumeForTransfer(ImageTransfer imageTransfer) { + Disk disk = diskDao.get(imageTransfer.getDiskId()); + if (!(disk instanceof ManagedBlockStorageDisk)) { + return; + } + ManagedBlockStorageDisk mbsDisk = (ManagedBlockStorageDisk) disk; + if (mbsDisk.getStorageIds() == null || mbsDisk.getStorageIds().isEmpty()) { + log.warn("Managed block disk '{}' has no storage domain for disconnect", imageTransfer.getDiskId()); + return; + } + Guid storageDomainId = mbsDisk.getStorageIds().get(0); + ManagedBlockStorage managedBlockStorage = managedBlockStorageDao.get(storageDomainId); + mbsTryDisconnect(managedBlockStorage, mbsDisk.getImageId()); + } +} diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/utils/VdsCommandsHelper.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/utils/VdsCommandsHelper.java index bf3daf6a15c..0ce214b2b02 100644 --- a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/utils/VdsCommandsHelper.java +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/utils/VdsCommandsHelper.java @@ -151,12 +151,21 @@ public Guid getHostForExecution(Guid poolId, Collection hostsToFilter) { } public Guid getHostForExecution(Guid poolId, Predicate predicate) { - List hostsForExecution = vdsDao - .getAllForStoragePoolAndStatus(poolId, VDSStatus.Up).stream() + List allHosts = vdsDao.getAllForStoragePoolAndStatus(poolId, VDSStatus.Up); + log.debug("getHostForExecution: Found {} hosts from database for poolId: {}", allHosts.size(), poolId); + if (allHosts.isEmpty()) { + log.warn("getHostForExecution: No hosts found in database for storage pool: {}", poolId); + return null; + } + + List hostsForExecution = allHosts.stream() .filter(predicate) .map(VDS::getId) .collect(Collectors.toList()); + log.debug("getHostForExecution found {} hosts after filtering", hostsForExecution.size()); if (hostsForExecution.isEmpty()) { + log.warn("getHostForExecution: No hosts passed the predicate filter for storage pool: {}. " + + "All {} hosts were filtered out.", poolId, allHosts.size()); return null; } diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/validator/storage/ManagedBlockStorageDomainValidator.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/validator/storage/ManagedBlockStorageDomainValidator.java index f019a081b86..d46bce53cca 100644 --- a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/validator/storage/ManagedBlockStorageDomainValidator.java +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/validator/storage/ManagedBlockStorageDomainValidator.java @@ -46,7 +46,8 @@ public class ManagedBlockStorageDomainValidator { ActionType.CopyImageGroup, ActionType.CopyImageGroupWithData, ActionType.CopyManagedBlockDisk, - ActionType.MoveOrCopyDisk + ActionType.MoveOrCopyDisk, + ActionType.MbsTransferDiskImage )); EngineLocalConfig config = EngineLocalConfig.getInstance(); isDataBaseInitialized = Boolean.parseBoolean(config.getProperty(MANAGEDBLOCK_DB_ENABLE)); diff --git a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/ActionType.java b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/ActionType.java index dc2bc859569..d58120b5118 100644 --- a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/ActionType.java +++ b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/ActionType.java @@ -349,6 +349,7 @@ public enum ActionType { AddStorageServerConnectionExtension(1021, ActionGroup.CREATE_STORAGE_DOMAIN, QuotaDependency.NONE), RefreshVolume(1022, QuotaDependency.NONE), TransferDiskImage(1024, ActionGroup.EDIT_DISK_PROPERTIES, false, QuotaDependency.STORAGE), + MbsTransferDiskImage(1054, ActionGroup.EDIT_DISK_PROPERTIES, false, QuotaDependency.STORAGE), TransferImageStatus(1025, ActionGroup.EDIT_DISK_PROPERTIES, false, QuotaDependency.NONE), ScanStorageForUnregisteredDisks(1026, ActionGroup.MANIPULATE_STORAGE_DOMAIN, QuotaDependency.NONE), CreateImagePlaceholder(1028, QuotaDependency.NONE), diff --git a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/TransferDiskImageParameters.java b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/TransferDiskImageParameters.java index 6665e268c3f..f45372a8119 100644 --- a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/TransferDiskImageParameters.java +++ b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/TransferDiskImageParameters.java @@ -22,12 +22,18 @@ public class TransferDiskImageParameters extends ImagesActionsParametersBase { private Integer clientInactivityTimeout; private TimeoutPolicyType timeoutPolicyType; private VolumeFormat volumeFormat; + private VolumeFormat sourceVolumeFormat; private Guid backupId; private TransferClientType transferClientType = TransferClientType.UNKNOWN; // Transfer only specified image data instead of entire image chain. private boolean shallow; + // For browser upload when source format != destination: copy task and new image/volume ids. + private Guid copyTaskId; + private Guid convertedImageId; + private Guid convertedVolumeId; + public TransferDiskImageParameters() { } public TransferDiskImageParameters(Guid storageDomainId, AddDiskParameters addDiskParameters) { @@ -123,6 +129,14 @@ public void setVolumeFormat(VolumeFormat volumeFormat) { this.volumeFormat = volumeFormat; } + public VolumeFormat getSourceVolumeFormat() { + return sourceVolumeFormat; + } + + public void setSourceVolumeFormat(VolumeFormat sourceVolumeFormat) { + this.sourceVolumeFormat = sourceVolumeFormat; + } + public Guid getBackupId() { return backupId; } @@ -146,4 +160,28 @@ public boolean isShallow() { public void setShallow(boolean shallow) { this.shallow = shallow; } + + public Guid getCopyTaskId() { + return copyTaskId; + } + + public void setCopyTaskId(Guid copyTaskId) { + this.copyTaskId = copyTaskId; + } + + public Guid getConvertedImageId() { + return convertedImageId; + } + + public void setConvertedImageId(Guid convertedImageId) { + this.convertedImageId = convertedImageId; + } + + public Guid getConvertedVolumeId() { + return convertedVolumeId; + } + + public void setConvertedVolumeId(Guid convertedVolumeId) { + this.convertedVolumeId = convertedVolumeId; + } } diff --git a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/businessentities/storage/ImageTransferPhase.java b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/businessentities/storage/ImageTransferPhase.java index 0b377bd1801..cba19a0644f 100644 --- a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/businessentities/storage/ImageTransferPhase.java +++ b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/businessentities/storage/ImageTransferPhase.java @@ -20,7 +20,9 @@ public enum ImageTransferPhase implements Identifiable { FINISHED_FAILURE(10, "Finished Failure"), CANCELLED_USER(11, "Cancelled by user"), FINALIZING_CLEANUP(12, "Finalizing Cleanup"), - FINISHED_CLEANUP(13, "Finished Cleanup"); + FINISHED_CLEANUP(13, "Finished Cleanup"), + /** Volume format conversion performed on the host (VDSM) via CopyImage / qemu-img convert. */ + CONVERTING(14, "Converting"); private int value; private String description; diff --git a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/vdscommands/ConvertManagedBlockVolumeVDSCommandParameters.java b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/vdscommands/ConvertManagedBlockVolumeVDSCommandParameters.java new file mode 100644 index 00000000000..476383a7380 --- /dev/null +++ b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/vdscommands/ConvertManagedBlockVolumeVDSCommandParameters.java @@ -0,0 +1,70 @@ +package org.ovirt.engine.core.common.vdscommands; + +import org.ovirt.engine.core.common.businessentities.VDS; +import org.ovirt.engine.core.compat.Guid; + +public class ConvertManagedBlockVolumeVDSCommandParameters extends VdsIdAndVdsVDSCommandParametersBase { + + private Guid storageDomainId; + private Guid srcVolId; + private Guid dstVolId; + private String srcFormat; + private String dstFormat; + + public ConvertManagedBlockVolumeVDSCommandParameters() { + } + + public ConvertManagedBlockVolumeVDSCommandParameters(VDS vds) { + super(vds); + } + + public ConvertManagedBlockVolumeVDSCommandParameters(VDS vds, Guid storageDomainId, + Guid srcVolId, Guid dstVolId, String srcFormat, String dstFormat) { + super(vds); + this.storageDomainId = storageDomainId; + this.srcVolId = srcVolId; + this.dstVolId = dstVolId; + this.srcFormat = srcFormat; + this.dstFormat = dstFormat; + } + + public Guid getStorageDomainId() { + return storageDomainId; + } + + public void setStorageDomainId(Guid storageDomainId) { + this.storageDomainId = storageDomainId; + } + + public Guid getSrcVolId() { + return srcVolId; + } + + public void setSrcVolId(Guid srcVolId) { + this.srcVolId = srcVolId; + } + + public Guid getDstVolId() { + return dstVolId; + } + + public void setDstVolId(Guid dstVolId) { + this.dstVolId = dstVolId; + } + + public String getSrcFormat() { + return srcFormat; + } + + public void setSrcFormat(String srcFormat) { + this.srcFormat = srcFormat; + } + + public String getDstFormat() { + return dstFormat; + } + + public void setDstFormat(String dstFormat) { + this.dstFormat = dstFormat; + } +} diff --git a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/vdscommands/VDSCommandType.java b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/vdscommands/VDSCommandType.java index 81a19572d62..92d84e30f6b 100644 --- a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/vdscommands/VDSCommandType.java +++ b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/vdscommands/VDSCommandType.java @@ -274,7 +274,8 @@ public enum VDSCommandType { // Managed block storage AttachManagedBlockStorageVolume("org.ovirt.engine.core.vdsbroker.vdsbroker"), - DetachManagedBlockStorageVolume("org.ovirt.engine.core.vdsbroker.vdsbroker"); + DetachManagedBlockStorageVolume("org.ovirt.engine.core.vdsbroker.vdsbroker"), + ConvertManagedBlockVolume("org.ovirt.engine.core.vdsbroker.vdsbroker"); String packageName; diff --git a/backend/manager/modules/dal/src/main/java/org/ovirt/engine/core/dal/dbbroker/SimpleJdbcCallsHandler.java b/backend/manager/modules/dal/src/main/java/org/ovirt/engine/core/dal/dbbroker/SimpleJdbcCallsHandler.java index cfbbd2299bd..a6d832d5b73 100644 --- a/backend/manager/modules/dal/src/main/java/org/ovirt/engine/core/dal/dbbroker/SimpleJdbcCallsHandler.java +++ b/backend/manager/modules/dal/src/main/java/org/ovirt/engine/core/dal/dbbroker/SimpleJdbcCallsHandler.java @@ -12,6 +12,8 @@ import javax.inject.Singleton; import org.ovirt.engine.core.common.utils.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -24,6 +26,7 @@ public class SimpleJdbcCallsHandler { private static final String RETURN_VALUE_PARAMETER = "RETURN_VALUE"; + private static final Logger log = LoggerFactory.getLogger(SimpleJdbcCallsHandler.class); private final ConcurrentMap callsMap = new ConcurrentHashMap<>(); private final ConcurrentMap> outParamsMap = new ConcurrentHashMap<>(); @@ -154,6 +157,8 @@ private Map executeImpl(String procedureName, private Map executeImpl(String procedureName, MapSqlParameterSource paramsSource, CallCreator callCreator, RowMapper mapper) { SimpleJdbcCall call = getCall(procedureName, callCreator, mapper); + log.debug("Executing stored procedure '{}'", procedureName); + log.debug("Parameters: {}", paramsSource); return call.execute(paramsSource); } diff --git a/backend/manager/modules/restapi/jaxrs/src/main/java/org/ovirt/engine/api/restapi/resource/BackendImageTransfersResource.java b/backend/manager/modules/restapi/jaxrs/src/main/java/org/ovirt/engine/api/restapi/resource/BackendImageTransfersResource.java index 6380c4efd8b..a60832422b9 100644 --- a/backend/manager/modules/restapi/jaxrs/src/main/java/org/ovirt/engine/api/restapi/resource/BackendImageTransfersResource.java +++ b/backend/manager/modules/restapi/jaxrs/src/main/java/org/ovirt/engine/api/restapi/resource/BackendImageTransfersResource.java @@ -14,6 +14,10 @@ import org.ovirt.engine.api.restapi.utils.GuidUtils; import org.ovirt.engine.core.common.action.ActionType; import org.ovirt.engine.core.common.action.TransferDiskImageParameters; +import org.ovirt.engine.core.common.businessentities.StorageDomain; +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.StorageType; import org.ovirt.engine.core.common.businessentities.storage.TimeoutPolicyType; import org.ovirt.engine.core.common.businessentities.storage.TransferClientType; import org.ovirt.engine.core.common.businessentities.storage.TransferType; @@ -79,8 +83,30 @@ private Response performCreate(ImageTransfer imageTransfer, TransferDiskImagePar params.setShallow(imageTransfer.isShallow()); } params.setTransferClientType(TransferClientType.TRANSFER_VIA_API); - return performCreate(ActionType.TransferDiskImage, params, new QueryIdResolver(QueryType.GetImageTransferById, - IdQueryParameters.class)); + ActionType actionType = resolveTransferDiskImageActionType(params); + return performCreate(actionType, params, + new QueryIdResolver(QueryType.GetImageTransferById, IdQueryParameters.class)); + } + + private ActionType resolveTransferDiskImageActionType(TransferDiskImageParameters params) { + if (!Guid.isNullOrEmpty(params.getImageGroupID())) { + QueryReturnValue diskQuery = runQuery(QueryType.GetDiskByDiskId, + new IdQueryParameters(params.getImageGroupID())); + if (diskQuery.getReturnValue() instanceof DiskImage + && ((DiskImage) diskQuery.getReturnValue()).getDiskStorageType() == DiskStorageType.MANAGED_BLOCK_STORAGE) { + return ActionType.MbsTransferDiskImage; + } + } + if (params.getAddDiskParameters() != null + && params.getAddDiskParameters().getStorageDomainId() != null) { + QueryReturnValue sdQuery = runQuery(QueryType.GetStorageDomainById, + new IdQueryParameters(params.getAddDiskParameters().getStorageDomainId())); + if (sdQuery.getReturnValue() instanceof StorageDomain + && ((StorageDomain) sdQuery.getReturnValue()).getStorageType() == StorageType.MANAGED_BLOCK_STORAGE) { + return ActionType.MbsTransferDiskImage; + } + } + return ActionType.TransferDiskImage; } private void updateTransferType(ImageTransfer imageTransfer, TransferDiskImageParameters params) { diff --git a/backend/manager/modules/restapi/types/src/main/java/org/ovirt/engine/api/restapi/types/ImageTransferMapper.java b/backend/manager/modules/restapi/types/src/main/java/org/ovirt/engine/api/restapi/types/ImageTransferMapper.java index ea4b2abd0c2..449f06ffb00 100644 --- a/backend/manager/modules/restapi/types/src/main/java/org/ovirt/engine/api/restapi/types/ImageTransferMapper.java +++ b/backend/manager/modules/restapi/types/src/main/java/org/ovirt/engine/api/restapi/types/ImageTransferMapper.java @@ -98,6 +98,8 @@ private static ImageTransferPhase mapPhase(org.ovirt.engine.core.common.business return ImageTransferPhase.INITIALIZING; case TRANSFERRING: return ImageTransferPhase.TRANSFERRING; + case CONVERTING: + return ImageTransferPhase.TRANSFERRING; case RESUMING: return ImageTransferPhase.RESUMING; case PAUSED_SYSTEM: diff --git a/backend/manager/modules/restapi/types/src/test/java/org/ovirt/engine/api/restapi/types/ImageTransferMapperTest.java b/backend/manager/modules/restapi/types/src/test/java/org/ovirt/engine/api/restapi/types/ImageTransferMapperTest.java index 7507dd60822..0b0dece6eec 100644 --- a/backend/manager/modules/restapi/types/src/test/java/org/ovirt/engine/api/restapi/types/ImageTransferMapperTest.java +++ b/backend/manager/modules/restapi/types/src/test/java/org/ovirt/engine/api/restapi/types/ImageTransferMapperTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.ovirt.engine.api.model.ImageTransfer; -import org.ovirt.engine.api.model.ImageTransferPhase; public class ImageTransferMapperTest extends AbstractInvertibleMappingTest response = new FutureMap(this.client, request); + return new StatusOnlyReturn(response); + } + @Override public VDSInfoReturn getLeaseStatus(String leaseUUID, String sdUUID) { Map leaseDict = new HashMap<>(); diff --git a/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/ConvertManagedBlockVolumeVDSCommand.java b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/ConvertManagedBlockVolumeVDSCommand.java new file mode 100644 index 00000000000..96bf5e37637 --- /dev/null +++ b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/ConvertManagedBlockVolumeVDSCommand.java @@ -0,0 +1,22 @@ +package org.ovirt.engine.core.vdsbroker.vdsbroker; + +import org.ovirt.engine.core.common.vdscommands.ConvertManagedBlockVolumeVDSCommandParameters; + +public class ConvertManagedBlockVolumeVDSCommand extends VdsBrokerCommand { + + public ConvertManagedBlockVolumeVDSCommand(ConvertManagedBlockVolumeVDSCommandParameters parameters) { + super(parameters, parameters.getVds()); + } + + @Override + protected void executeVdsBrokerCommand() { + status = getBroker().convertManagedBlockVolume( + getParameters().getStorageDomainId(), + getParameters().getSrcVolId(), + getParameters().getDstVolId(), + getParameters().getSrcFormat(), + getParameters().getDstFormat()); + proceedProxyReturnValue(); + setReturnValue(status); + } +} diff --git a/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/IVdsServer.java b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/IVdsServer.java index 05fc2073886..9057b1fe3fb 100644 --- a/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/IVdsServer.java +++ b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/IVdsServer.java @@ -604,6 +604,9 @@ StatusOnlyReturn glusterVolumeResetBrickCommitForce(String volumeName, StatusOnlyReturn detachManagedBlockStorageVolume(Guid volumeId, Guid sdUUID); + StatusOnlyReturn convertManagedBlockVolume(Guid sdId, Guid srcVolId, Guid dstVolId, + String srcFormat, String dstFormat); + VDSInfoReturn getLeaseStatus(String leaseUUID, String sdUUID); StatusOnlyReturn fenceLeaseJob(String leaseUUID, String sdUUID, Map leaseMetadata); diff --git a/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/NullVdsServer.java b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/NullVdsServer.java index f157ce32875..40c280223ee 100644 --- a/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/NullVdsServer.java +++ b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/NullVdsServer.java @@ -1091,6 +1091,11 @@ public DeviceInfoReturn attachManagedBlockStorageVolume(Guid volumeId, Guid sdUU return null; } + @Override public StatusOnlyReturn convertManagedBlockVolume(Guid sdId, Guid srcVolId, Guid dstVolId, + String srcFormat, String dstFormat) { + return null; + } + @Override public VDSInfoReturn getLeaseStatus(String leaseUUID, String sdUUID) { return null; } diff --git a/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/PrepareImageReturn.java b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/PrepareImageReturn.java index df249448c63..6a8ba226864 100644 --- a/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/PrepareImageReturn.java +++ b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/PrepareImageReturn.java @@ -12,7 +12,14 @@ public class PrepareImageReturn extends StatusReturn { @SuppressWarnings("unchecked") public PrepareImageReturn(Map innerMap) { super(innerMap); - imagePath = (String) ((Map) innerMap.get(INFO)).get(PATH); + Map infoMap = (Map) innerMap.get(INFO); + if (infoMap != null) { + imagePath = (String) infoMap.get(PATH); + } + if (imagePath == null) { + // Some backends (e.g. managed block) may return path at top level or omit it + imagePath = (String) innerMap.get(PATH); + } } public String getImagePath() { diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/disks/DisksViewColumns.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/disks/DisksViewColumns.java index a57e42fdc1f..5de73f888ea 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/disks/DisksViewColumns.java +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/disks/DisksViewColumns.java @@ -514,7 +514,7 @@ public static final AbstractTextColumn getDiskSnapshotIDColumn(String sort AbstractTextColumn column = new AbstractTextColumn() { @Override public String getValue(Disk disk) { - return diskImagePredicate.test(disk) ? ((DiskImage) disk).getImageId().toString() : null; + return diskImagePredicate.test(disk) ? disk.getId().toString() : null; } }; diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/vm/VmDiskPopupWidget.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/vm/VmDiskPopupWidget.java index 2b131c0cd02..890a30f23c3 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/vm/VmDiskPopupWidget.java +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/vm/VmDiskPopupWidget.java @@ -280,6 +280,14 @@ public void edit(final AbstractDiskModel disk) { } }); + // Show/hide disk profile row (e.g. for upload to IMAGE or managed block storage). + diskProfileEditor.setVisible(disk.getDiskProfile().getIsAvailable()); + disk.getDiskProfile().getPropertyChangedEvent().addListener((ev, sender, args) -> { + if ("IsAvailable".equals(args.propertyName)) { //$NON-NLS-1$ + diskProfileEditor.setVisible(disk.getDiskProfile().getIsAvailable()); + } + }); + disk.getIsVirtioScsiEnabled().getEntityChangedEvent().addListener((ev, sender, args) -> { if (disk.getVm() == null) { // not relevant for floating disks diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java index 1ccea13ab94..871f3882999 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java @@ -12,6 +12,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.ovirt.engine.core.common.action.ActionType; import org.ovirt.engine.core.common.businessentities.BusinessEntity; import org.ovirt.engine.core.common.businessentities.BusinessEntityWithStatus; import org.ovirt.engine.core.common.businessentities.Identifiable; @@ -29,6 +30,7 @@ import org.ovirt.engine.core.common.businessentities.network.Network; import org.ovirt.engine.core.common.businessentities.network.VnicProfileView; 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.businessentities.storage.StorageType; import org.ovirt.engine.core.compat.Guid; @@ -69,6 +71,18 @@ public static boolean isDataActiveStorageDomain(StorageDomain storageDomain) { return isData && isActive; } + public static ActionType transferDiskImageActionType(StorageDomain storageDomain) { + return isManagedBlockActiveStorageDomain(storageDomain) + ? ActionType.MbsTransferDiskImage + : ActionType.TransferDiskImage; + } + + public static ActionType transferDiskImageActionType(DiskImage diskImage) { + return diskImage.getDiskStorageType() == DiskStorageType.MANAGED_BLOCK_STORAGE + ? ActionType.MbsTransferDiskImage + : ActionType.TransferDiskImage; + } + public static boolean isManagedBlockActiveStorageDomain(StorageDomain storageDomain) { boolean isManagedBlock = storageDomain.getStorageDomainType() == StorageDomainType.ManagedBlockStorage; boolean isActive = storageDomain.getStatus() == StorageDomainStatus.Active; diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/DownloadImageManager.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/DownloadImageManager.java index 4adfe53e2be..9ab904088e3 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/DownloadImageManager.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/DownloadImageManager.java @@ -1,8 +1,8 @@ package org.ovirt.engine.ui.uicommonweb.models.storage; +import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; -import java.util.stream.Collectors; import org.ovirt.engine.core.common.action.ActionParametersBase; import org.ovirt.engine.core.common.action.ActionType; @@ -27,14 +27,17 @@ public static DownloadImageManager getInstance() { public void startDownload(List disks) { log.info("Start download for disks: " + Linq.getDiskAliases(disks)); //$NON-NLS-1$ - List transferDiskImageParameters = disks.stream() - .map(DownloadImageHandler::createInitParams) - .collect(Collectors.toList()); - Frontend.getInstance().runMultipleAction(ActionType.TransferDiskImage, - transferDiskImageParameters, callback(), true, true); + List transferDiskImageParameters = new ArrayList<>(disks.size()); + for (DiskImage disk : disks) { + transferDiskImageParameters.add(DownloadImageHandler.createInitParams(disk)); + } + + ActionType actionType = Linq.transferDiskImageActionType(disks.get(0)); + Frontend.getInstance().runMultipleAction(actionType, transferDiskImageParameters, + getDownloadCallback(), true, true); } - private IFrontendMultipleActionAsyncCallback callback() { + private IFrontendMultipleActionAsyncCallback getDownloadCallback() { return result -> result.getReturnValue() .forEach(actionReturnValue -> { if (actionReturnValue.getSucceeded()) { @@ -43,4 +46,3 @@ private IFrontendMultipleActionAsyncCallback callback() { }); } } - diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/ImageInfoModel.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/ImageInfoModel.java index 6bc33ef1041..45eca438116 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/ImageInfoModel.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/ImageInfoModel.java @@ -191,6 +191,15 @@ public void clearModel() { } public boolean validate(StorageFormatType storageFormatType, long imageSize) { + return validate(storageFormatType, imageSize, false); + } + + /** + * @param skipQcowCompatCheckForManagedBlock when true, do not reject qcow2 compat 1.1 for V1/V2/V3 + * (managed block storage uses V1 but backend handles compat separately). + */ + public boolean validate(StorageFormatType storageFormatType, long imageSize, + boolean skipQcowCompatCheckForManagedBlock) { if (!fileLoaded) { getInvalidityReasons().add(constants.uploadImageCannotBeOpened()); return false; @@ -200,7 +209,7 @@ public boolean validate(StorageFormatType storageFormatType, long imageSize) { return false; } - if (qcowCompat != null && qcowCompat != ImageInfoModel.QemuCompat.V2) { + if (!skipQcowCompatCheckForManagedBlock && qcowCompat != null && qcowCompat != ImageInfoModel.QemuCompat.V2) { switch (storageFormatType) { case V1: case V2: diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageHandler.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageHandler.java index f0882e446cc..72c24b8e039 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageHandler.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageHandler.java @@ -234,8 +234,8 @@ public void resetUploadState() { * end offset */ public void start(TransferDiskImageParameters transferDiskImageParameters, - long startByte, long endByte) { - Frontend.getInstance().runAction(ActionType.TransferDiskImage, transferDiskImageParameters, + long startByte, long endByte, ActionType actionType) { + Frontend.getInstance().runAction(actionType, transferDiskImageParameters, result -> { if (result.getReturnValue().getSucceeded()) { setCommandId(result.getReturnValue().getActionReturnValue()); @@ -291,6 +291,8 @@ private void respondToPollStatus(FrontendActionAsyncResult result) { case INITIALIZING: case RESUMING: + case CONVERTING: + // CONVERTING: MBS format conversion in progress; keep polling until FINISHED_SUCCESS/FAILURE break; case TRANSFERRING: diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageManager.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageManager.java index 08bb5148782..271b620d5e7 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageManager.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageManager.java @@ -5,6 +5,7 @@ import java.util.Set; import java.util.logging.Logger; +import org.ovirt.engine.core.common.action.ActionType; import org.ovirt.engine.core.common.action.TransferDiskImageParameters; import org.ovirt.engine.core.common.action.TransferImageStatusParameters; import org.ovirt.engine.core.compat.Guid; @@ -44,33 +45,35 @@ public UploadImageManager() { * @param transferDiskImageParameters * transfer parameters */ + public void startUpload(Element fileUploadElement, TransferDiskImageParameters transferDiskImageParameters, + String proxyLocation, ActionType actionType) { + startUpload(fileUploadElement, transferDiskImageParameters, proxyLocation, actionType, + 0, transferDiskImageParameters.getTransferSize()); + } + public void startUpload(Element fileUploadElement, TransferDiskImageParameters transferDiskImageParameters, String proxyLocation) { - startUpload(fileUploadElement, - transferDiskImageParameters, - proxyLocation, - 0, - transferDiskImageParameters.getTransferSize()); + startUpload(fileUploadElement, transferDiskImageParameters, proxyLocation, ActionType.TransferDiskImage); } /** * Start a new upload by a specified range. - * - * @param fileUploadElement - * the file upload html element - * @param transferDiskImageParameters - * transfer parameters - * @param startByte - * start offset - * @param endByte - * end offset */ public void startUpload(Element fileUploadElement, TransferDiskImageParameters transferDiskImageParameters, - String proxyLocation, long startByte, long endByte) { + String proxyLocation, ActionType actionType, long startByte, long endByte) { UploadImageHandler uploadImageHandler = createUploadImageHandler(fileUploadElement, proxyLocation, transferDiskImageParameters.getStorageDomainId()); uploadImageHandlers.add(uploadImageHandler); - uploadImageHandler.start(transferDiskImageParameters, startByte, endByte); + uploadImageHandler.start(transferDiskImageParameters, startByte, endByte, actionType); + } + + /** + * @deprecated use {@link #startUpload(Element, TransferDiskImageParameters, String, ActionType, long, long)} + */ + public void startUpload(Element fileUploadElement, TransferDiskImageParameters transferDiskImageParameters, + String proxyLocation, long startByte, long endByte) { + startUpload(fileUploadElement, transferDiskImageParameters, proxyLocation, + ActionType.TransferDiskImage, startByte, endByte); } /** diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageModel.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageModel.java index e752de0a026..b4d57d8d37e 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageModel.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageModel.java @@ -13,12 +13,14 @@ import org.ovirt.engine.core.common.businessentities.StorageDomain; import org.ovirt.engine.core.common.businessentities.StorageFormatType; import org.ovirt.engine.core.common.businessentities.StoragePool; +import org.ovirt.engine.core.common.businessentities.profiles.DiskProfile; import org.ovirt.engine.core.common.businessentities.storage.Disk; import org.ovirt.engine.core.common.businessentities.storage.DiskContentType; 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.ImageTransfer; import org.ovirt.engine.core.common.businessentities.storage.ImageTransferPhase; +import org.ovirt.engine.core.common.businessentities.storage.StorageType; import org.ovirt.engine.core.common.businessentities.storage.TransferClientType; import org.ovirt.engine.core.common.businessentities.storage.TransferType; import org.ovirt.engine.core.common.businessentities.storage.VolumeFormat; @@ -28,8 +30,10 @@ import org.ovirt.engine.core.common.queries.QueryType; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.compat.StringHelper; +import org.ovirt.engine.ui.frontend.AsyncQuery; import org.ovirt.engine.ui.frontend.Frontend; import org.ovirt.engine.ui.uicommonweb.ICommandTarget; +import org.ovirt.engine.ui.uicommonweb.Linq; import org.ovirt.engine.ui.uicommonweb.UICommand; import org.ovirt.engine.ui.uicommonweb.dataprovider.AsyncDataProvider; import org.ovirt.engine.ui.uicommonweb.help.HelpTag; @@ -173,6 +177,9 @@ public UploadImageModel(final Guid limitToStorageDomainId, final DiskImage resum public void initialize() { super.initialize(); + // Show disk profile for upload (IMAGE and managed block storage domains). + getDiskProfile().setIsAvailable(true); + getDataCenter().setIsChangeable(isChangeable); if (!isChangeable) { // Set the selected item on the storage domains and data centers lists. @@ -187,6 +194,8 @@ public void initialize() { List availableDomains = new ArrayList<>(); availableDomains.add(storageDomain); getStorageDomain().setItems(availableDomains); + getStorageDomain().setSelectedItem(storageDomain); + loadDiskProfilesForStorageDomain(storageDomain.getId()); Guid dcId = storageDomain.getStoragePoolId(); if (!Guid.isNullOrEmpty(dcId)) { // Set selected data center if the list of data centers is populated. @@ -238,6 +247,22 @@ private boolean selectDataCenter(Guid dcId) { // data center is still not there. So, need to wait... return false; } + + private void loadDiskProfilesForStorageDomain(Guid storageDomainId) { + Frontend.getInstance().runQuery( + QueryType.GetDiskProfilesByStorageDomainId, + new IdQueryParameters(storageDomainId), + new AsyncQuery(returnValue -> { + List profiles = returnValue.getReturnValue(); + if (profiles != null && !profiles.isEmpty()) { + getDiskProfile().setItems(profiles); + getDiskProfile().setSelectedItem(profiles.get(0)); + } else { + getDiskProfile().setItems(new ArrayList<>()); + getDiskProfile().setSelectedItem(null); + } + })); + } }); } else { setDiskModel(new ReadOnlyDiskModel() { @@ -410,8 +435,14 @@ public boolean validate() { return result; } }); - StorageFormatType storageFormatType = getDiskModel().getStorageDomain().getSelectedItem().getStorageFormat(); - uploadImageIsValid = getImagePath().getIsValid() && getImageInfoModel().validate(storageFormatType, imageInfoModel.getActualSize()); + StorageDomain selectedSd = getDiskModel().getStorageDomain().getSelectedItem(); + StorageFormatType storageFormatType = selectedSd.getStorageFormat(); + boolean isManagedBlockDomain = selectedSd.getStorageType() == StorageType.MANAGED_BLOCK_STORAGE; + uploadImageIsValid = getImagePath().getIsValid() + && getImageInfoModel().validate(storageFormatType, imageInfoModel.getActualSize(), isManagedBlockDomain); + + // Browser sends file bytes as-is; no client-side conversion. Both raw and qcow2 are supported for MBS: + // the file is written as-is to the volume (qcow2 stays qcow2, visible as such with qemu-img info). getInvalidityReasons().addAll(getImagePath().getInvalidityReasons()); getInvalidityReasons().addAll(getImageInfoModel().getInvalidityReasons()); @@ -424,8 +455,9 @@ public boolean validate() { } private void initiateNewUpload() { + ActionType actionType = Linq.transferDiskImageActionType(getDiskModel().getStorageDomain().getSelectedItem()); UploadImageManager.getInstance().startUpload(getImageFileUploadElement(), createInitParams(), - getProxyLocation()); + getProxyLocation(), actionType); // Close dialog getCancelCommand().execute(); @@ -465,14 +497,56 @@ private TransferDiskImageParameters createInitParams() { AddDiskParameters diskParameters = new AddDiskParameters(newDisk); if (diskModel.getDiskStorageType().getEntity() == DiskStorageType.IMAGE) { - diskParameters.setStorageDomainId(getDiskModel().getStorageDomain().getSelectedItem().getId()); + StorageDomain selectedSd = getDiskModel().getStorageDomain().getSelectedItem(); + diskParameters.setStorageDomainId(selectedSd.getId()); + if (selectedSd.getStorageType() == StorageType.MANAGED_BLOCK_STORAGE) { + log.info("UploadImageModel.createInitParams: upload target is Managed Block Storage domain id=" + selectedSd.getId() + " name=" + selectedSd.getName()); //$NON-NLS-1$ //$NON-NLS-2$ + if (newDisk instanceof DiskImage) { + DiskImage diskImage = (DiskImage) newDisk; + diskImage.setDiskProfileId(null); + // Same as Ansible (format: cow): send COW + Sparse so checkImageConfiguration() passes; + // format is then ignored for MBS (AddManagedBlockStorageDiskCommand overwrites to RAW when saving). + diskImage.setVolumeFormat(VolumeFormat.COW); + diskImage.setVolumeType(VolumeType.Sparse); + } + } } TransferDiskImageParameters parameters = new TransferDiskImageParameters( diskParameters.getStorageDomainId(), diskParameters); parameters.setTransferSize(imageInfoModel.getActualSize()); - parameters.setVolumeFormat(imageInfoModel.getFormat()); + VolumeFormat sourceFormat = imageInfoModel.getFormat(); + VolumeFormat destFormat; + if (diskModel.getDiskStorageType().getEntity() == DiskStorageType.IMAGE) { + StorageDomain selectedSd = getDiskModel().getStorageDomain().getSelectedItem(); + if (selectedSd != null && selectedSd.getStorageType() == StorageType.MANAGED_BLOCK_STORAGE) { + // MBS: disk is always RAW (AddManagedBlockStorageDiskCommand overwrites). Use RAW so + // needsConversionAfterUpload() is correct: raw upload -> no conversion; qcow2 upload -> convert to raw. + destFormat = VolumeFormat.RAW; + } else { + destFormat = newDisk instanceof DiskImage + ? ((DiskImage) newDisk).getVolumeFormat() + : sourceFormat; + } + } else { + destFormat = newDisk instanceof DiskImage + ? ((DiskImage) newDisk).getVolumeFormat() + : sourceFormat; + } + parameters.setSourceVolumeFormat(sourceFormat); + parameters.setVolumeFormat(destFormat); + // When browser upload and source format != destination: create disk in source format (upload volume), + // then backend converts to destination format and replaces the volume. + if (diskModel.getDiskStorageType().getEntity() == DiskStorageType.IMAGE + && sourceFormat != null && destFormat != null && !sourceFormat.equals(destFormat)) { + StorageDomain sd = getDiskModel().getStorageDomain().getSelectedItem(); + if (sd == null || sd.getStorageType() != StorageType.MANAGED_BLOCK_STORAGE) { + if (newDisk instanceof DiskImage) { + ((DiskImage) newDisk).setVolumeFormat(sourceFormat); + } + } + } parameters.setVdsId(getDiskModel().getHost().getSelectedItem().getId()); parameters.setTransferClientType(TransferClientType.TRANSFER_VIA_BROWSER); diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/AbstractDiskModel.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/AbstractDiskModel.java index ecf8d6e5b89..656cd9d4ca4 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/AbstractDiskModel.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/AbstractDiskModel.java @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.List; import java.util.function.Predicate; +import java.util.logging.Logger; import java.util.stream.Collectors; import org.ovirt.engine.core.common.businessentities.ActionGroup; @@ -61,6 +62,7 @@ import org.ovirt.engine.ui.uicompat.UIConstants; public abstract class AbstractDiskModel extends DiskModel { + private static final Logger log = Logger.getLogger(AbstractDiskModel.class.getName()); protected static final UIConstants constants = ConstantsManager.getInstance().getConstants(); private EntityModel isWipeAfterDelete; @@ -373,13 +375,17 @@ protected void updateStorageDomains(final StoragePool datacenter) { switch (getDiskStorageType().getEntity()) { case IMAGE: domainByDiskType = d -> d.getStorageDomainType().isDataDomain() - || d.getStorageDomainType().isKubevirtDomain(); + || d.getStorageDomainType().isKubevirtDomain() + || d.getStorageType().isManagedBlockStorage(); + log.fine("AbstractDiskModel.updateStorageDomains: IMAGE disk type, including Data/KubeVirt/ManagedBlock domains"); //$NON-NLS-1$ break; case MANAGED_BLOCK_STORAGE: domainByDiskType = d -> d.getStorageType().isManagedBlockStorage(); + log.fine("AbstractDiskModel.updateStorageDomains: MANAGED_BLOCK_STORAGE disk type, MBS domains only"); //$NON-NLS-1$ break; default: domainByDiskType = s -> true; + break; } List filteredStorageDomains = @@ -389,6 +395,8 @@ protected void updateStorageDomains(final StoragePool datacenter) { .sorted(new NameableComparator()) .collect(Collectors.toList()); + long mbsCount = filteredStorageDomains.stream().filter(d -> d.getStorageType().isManagedBlockStorage()).count(); + log.info("AbstractDiskModel.updateStorageDomains: dc=" + datacenter.getName() + ", total=" + filteredStorageDomains.size() + ", managedBlock=" + mbsCount); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ StorageDomain storage = Linq.firstOrNull(filteredStorageDomains); getStorageDomain().setItems(filteredStorageDomains, storage); @@ -576,16 +584,17 @@ private void updateDiskProfiles(StoragePool selectedItem) { } private void setDiskProfilesList(List diskProfiles) { - // set disk profiles - if (diskProfiles != null && !diskProfiles.isEmpty()) { - getDiskProfile().setItems(diskProfiles); - } + List safeList = diskProfiles != null ? diskProfiles : new ArrayList<>(); + getDiskProfile().setItems(safeList); + // handle disk profile selected item Guid defaultProfileId = - (getDisk() != null && !getIsNew() && getDisk().getDiskStorageType() == DiskStorageType.IMAGE) + (getDisk() != null && !getIsNew() + && (getDisk().getDiskStorageType() == DiskStorageType.IMAGE + || getDisk().getDiskStorageType() == DiskStorageType.MANAGED_BLOCK_STORAGE)) ? ((DiskImage) getDisk()).getDiskProfileId() : null; - if (defaultProfileId != null) { - for (DiskProfile profile : diskProfiles) { + if (defaultProfileId != null && !safeList.isEmpty()) { + for (DiskProfile profile : safeList) { if (profile.getId().equals(defaultProfileId)) { getDiskProfile().setSelectedItem(profile); return; @@ -597,9 +606,15 @@ private void setDiskProfilesList(List diskProfiles) { if (getDisk() != null) { diskProfile.setName(getDiskImage().getDiskProfileName()); } - diskProfiles.add(diskProfile); - getDiskProfile().setItems(diskProfiles); + safeList = new ArrayList<>(safeList); + safeList.add(diskProfile); + getDiskProfile().setItems(safeList); getDiskProfile().setSelectedItem(diskProfile); + } else if (safeList.isEmpty()) { + getDiskProfile().setSelectedItem(null); + } else { + // New storage domain selected with profiles; select first so selection is valid. + getDiskProfile().setSelectedItem(safeList.get(0)); } } @@ -667,7 +682,9 @@ protected void diskStorageType_EntityChanged() { getHost().setIsAvailable(isLunDisk); getStorageType().setIsAvailable(isLunDisk); getDataCenter().setIsAvailable(!isInVm); - getDiskProfile().setIsAvailable(isDiskImage); + StorageDomain sd = getStorageDomain().getSelectedItem(); + boolean isManagedBlockDomain = sd != null && sd.getStorageType() == StorageType.MANAGED_BLOCK_STORAGE; + getDiskProfile().setIsAvailable((isDiskImage || isManagedBlockDisk) && !isManagedBlockDomain); getIsIncrementalBackup().setIsAvailable(isDiskImage); if (!isDiskImage) { @@ -945,6 +962,11 @@ private void storageDomain_SelectedItemChanged() { } updateQuota(getDataCenter().getSelectedItem()); updateDiskProfiles(getDataCenter().getSelectedItem()); + boolean isManagedBlockDomain = selectedStorage != null && selectedStorage.getStorageType() == StorageType.MANAGED_BLOCK_STORAGE; + boolean showDiskProfile = !isManagedBlockDomain + && (getDiskStorageType().getEntity() == DiskStorageType.IMAGE + || getDiskStorageType().getEntity() == DiskStorageType.MANAGED_BLOCK_STORAGE); + getDiskProfile().setIsAvailable(showDiskProfile); updatePassDiscardAvailability(); updatePassDiscardChangeability(); updateWipeAfterDeleteChangeability(); @@ -1065,6 +1087,9 @@ public void flush() { break; case MANAGED_BLOCK_STORAGE: DiskImage managedBlockDisk = getManagedBlockDisk(); + if (getDiskProfile().getSelectedItem() != null) { + managedBlockDisk.setDiskProfileId(getDiskProfile().getSelectedItem().getId()); + } updateQuota(managedBlockDisk); updateDiskSize(managedBlockDisk); setDisk(managedBlockDisk);