From dfbd23b7c940008bae471e1a51e13e34cd61e7fc Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Mon, 2 Mar 2026 10:44:58 +0200 Subject: [PATCH 1/4] core: enable disk upload to managed block storage domains Add the initial engine and webadmin support for uploading disks to Managed Block Storage (MBS) domains via the existing NBD image-transfer path. Extend TransferDiskImageCommand to connect and attach MBS volumes on the transfer host, validate MBS domains for TransferDiskImage, and tear down volumes when the session ends. Update AddDiskCommand and AddManagedBlockStorageDiskCommand for the MBS upload flow, and allow MBS domains in the upload UI (UploadImageModel, AbstractDiskModel). Depends on vdsm support for MBS upload-image. Signed-off-by: Slavka Peleva --- Makefile | 1 + .../core/bll/profiles/DiskProfileHelper.java | 4 + .../core/bll/storage/disk/AddDiskCommand.java | 6 +- .../disk/image/TransferDiskImageCommand.java | 174 +++++++++++++++++- .../AddManagedBlockStorageDiskCommand.java | 27 +++ .../bll/storage/utils/VdsCommandsHelper.java | 13 +- .../ManagedBlockStorageDomainValidator.java | 3 +- .../dal/dbbroker/SimpleJdbcCallsHandler.java | 5 + .../vdsbroker/PrepareImageReturn.java | 9 +- .../uicommon/popup/vm/VmDiskPopupWidget.java | 8 + .../models/storage/ImageInfoModel.java | 11 +- .../models/storage/UploadImageModel.java | 40 +++- .../models/vms/AbstractDiskModel.java | 47 +++-- 13 files changed, 327 insertions(+), 21 deletions(-) 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/image/TransferDiskImageCommand.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/image/TransferDiskImageCommand.java index 803888750ad..e8c30ee0681 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 @@ -35,6 +35,7 @@ import org.ovirt.engine.core.common.action.ActionReturnValue; import org.ovirt.engine.core.common.action.ActionType; import org.ovirt.engine.core.common.action.AddDiskParameters; +import org.ovirt.engine.core.common.action.ConnectManagedBlockStorageDeviceCommandParameters; import org.ovirt.engine.core.common.action.LockProperties; import org.ovirt.engine.core.common.action.RemoveDiskParameters; import org.ovirt.engine.core.common.action.TransferDiskImageParameters; @@ -45,15 +46,19 @@ import org.ovirt.engine.core.common.businessentities.VM; import org.ovirt.engine.core.common.businessentities.VmBackup; import org.ovirt.engine.core.common.businessentities.VmBackupPhase; +import org.ovirt.engine.core.common.businessentities.storage.Disk; 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.DiskStorageType; +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.ImageStatus; import org.ovirt.engine.core.common.businessentities.storage.ImageTicket; import org.ovirt.engine.core.common.businessentities.storage.ImageTicketInformation; import org.ovirt.engine.core.common.businessentities.storage.ImageTransfer; import org.ovirt.engine.core.common.businessentities.storage.ImageTransferBackend; 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.TimeoutPolicyType; import org.ovirt.engine.core.common.businessentities.storage.TransferType; import org.ovirt.engine.core.common.businessentities.storage.VmBackupType; @@ -68,6 +73,9 @@ import org.ovirt.engine.core.common.locks.LockingGroup; import org.ovirt.engine.core.common.utils.Pair; import org.ovirt.engine.core.common.utils.SizeConverter; +import org.ovirt.engine.core.common.utils.cinderlib.CinderlibCommandParameters; +import org.ovirt.engine.core.common.utils.cinderlib.CinderlibExecutor; +import org.ovirt.engine.core.common.vdscommands.AttachManagedBlockStorageVolumeVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.AddImageTicketVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.ExtendImageTicketVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.GetImageTicketVDSCommandParameters; @@ -83,6 +91,7 @@ import org.ovirt.engine.core.dal.dbbroker.auditloghandling.AuditLogDirector; import org.ovirt.engine.core.dao.DiskDao; import org.ovirt.engine.core.dao.ImageDao; +import org.ovirt.engine.core.dao.CinderStorageDao; import org.ovirt.engine.core.dao.ImageTransferDao; import org.ovirt.engine.core.dao.SnapshotDao; import org.ovirt.engine.core.dao.StorageDomainDao; @@ -90,6 +99,7 @@ import org.ovirt.engine.core.dao.VmBackupDao; import org.ovirt.engine.core.dao.VmDao; import org.ovirt.engine.core.utils.EngineLocalConfig; +import org.ovirt.engine.core.utils.JsonHelper; import org.ovirt.engine.core.utils.ReplacementUtils; import org.ovirt.engine.core.vdsbroker.ResourceManager; import org.ovirt.engine.core.vdsbroker.vdsbroker.PrepareImageReturn; @@ -134,6 +144,10 @@ public class TransferDiskImageCommand ext private VdsCommandsHelper vdsCommandsHelper; @Inject private ResourceManager resourceManager; + @Inject + private CinderlibExecutor cinderlibExecutor; + @Inject + private CinderStorageDao cinderStorageDao; private ImageioClient proxyClient; private VmBackup backup; @@ -184,9 +198,17 @@ protected String prepareImage(Guid vdsId) { getParameters().getBackupId(), getDiskImage().getId()); } + if (usingNbdServer()) { + StorageDomain sd = getStorageDomain(); + if (sd != null && StorageType.MANAGED_BLOCK_STORAGE.equals(sd.getStorageType())) { + return null; + } + } + 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() { @@ -925,6 +947,9 @@ private void handleFinalizingSuccess(final StateContext context) { } private boolean verifyImage(Guid transferingVdsId) { + if (isManagedBlockStorageForTransfer()) { + return true; + } ImageActionsVDSCommandParameters parameters = new ImageActionsVDSCommandParameters(transferingVdsId, getStoragePool().getId(), getStorageDomainId(), @@ -1069,6 +1094,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 +1115,50 @@ protected boolean startImageTransferSession() { updateEntity(updates); if (usingNbdServer()) { + if (isManagedBlockStorageForTransfer()) { + if (getVds().getConnectorInfo() == null) { + log.error("Host '{}' has no connector info; cannot connect managed block volume for image transfer '{}'", + getVds().getName(), getCommandId()); + updateEntityPhaseToStoppedBySystem( + AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); + return false; + } + DiskImage disk = getDiskImage(); + if (disk instanceof ManagedBlockStorageDisk) { + ManagedBlockStorageDisk mbsDisk = (ManagedBlockStorageDisk) disk; + Guid storageDomainId = mbsDisk.getStorageIds().isEmpty() ? getStorageDomainId() : mbsDisk.getStorageIds().get(0); + ConnectManagedBlockStorageDeviceCommandParameters connectParams = + new ConnectManagedBlockStorageDeviceCommandParameters(storageDomainId, + getVds().getConnectorInfo(), + mbsDisk.getImageId()); + ActionReturnValue connectResult = runInternalAction(ActionType.ConnectManagedBlockStorageDevice, connectParams); + if (!connectResult.getSucceeded()) { + log.error("Failed to connect managed block volume for image transfer '{}': {}", + getCommandId(), connectResult.getFault()); + updateEntityPhaseToStoppedBySystem( + AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); + return false; + } + // Attach the volume to the host so VDSM can expose it via NBD (StartNbdServer looks up + // the storage domain on the host; without attach the domain is not present on the host). + Map connectionInfo = connectResult.getActionReturnValue(); + if (connectionInfo != null) { + 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( + AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); + return false; + } + } + } + } try { VDSReturnValue vdsReturnValue = runVdsCommand(VDSCommandType.StartNbdServer, getStartNbdServerParameters(getVdsId())); @@ -1135,7 +1215,14 @@ private static boolean proxyEnabled() { @Override protected VDS checkForActiveVds() { + StorageDomain storageDomain = getStorageDomain(); + boolean isManagedBlockStorage = storageDomain != null + && StorageType.MANAGED_BLOCK_STORAGE.equals(storageDomain.getStorageType()); + Guid hostForExecution = vdsCommandsHelper.getHostForExecution(getStoragePoolId(), host -> { + if (isManagedBlockStorage) { + return true; + } var domainsData = resourceManager.getVdsManager(host.getId()).getDomains(); if (domainsData == null) { return false; @@ -1261,6 +1348,11 @@ private boolean usingNbdServer() { return !isLiveBackup() && getTransferBackend() == ImageTransferBackend.NBD; } + private boolean isManagedBlockStorageForTransfer() { + StorageDomain sd = getStorageDomain(); + return sd != null && StorageType.MANAGED_BLOCK_STORAGE.equals(sd.getStorageType()); + } + private boolean addImageTicketToProxy(Guid imagedTicketId, String hostUri) { // ToDo: move formatting to an helper for reuse in ImageTransfer String url = String.format("%s%s/%s", hostUri, IMAGES_PATH, imagedTicketId); @@ -1296,6 +1388,9 @@ private ImageTicket buildImageTicket(Guid ticketId, String ticketUrl) { } private boolean setVolumeLegalityInStorage(boolean legal) { + if (isManagedBlockStorageForTransfer()) { + return true; + } SetVolumeLegalityVDSCommandParameters parameters = new SetVolumeLegalityVDSCommandParameters(getStoragePool().getId(), getStorageDomainId(), @@ -1401,6 +1496,11 @@ private boolean stopImageTransferSession(ImageTransfer entity) { // Stopping NBD server if necessary if (usingNbdServer()) { stopNbdServer(entity.getVdsId()); + // For managed block: detach volume from host, then disconnect from storage adapter + if (isManagedBlockStorageForTransfer() && entity.getDiskId() != null) { + detachManagedBlockVolumeFromHost(entity); + disconnectManagedBlockVolumeForTransfer(entity); + } } ImageTransfer updates = new ImageTransfer(); @@ -1408,6 +1508,78 @@ private boolean stopImageTransferSession(ImageTransfer entity) { return true; } + /** + * Detaches the managed block volume from the host (VDS DetachManagedBlockStorageVolume). + * Called when transfer ends, before disconnecting from the storage adapter. + */ + 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; + } + AttachManagedBlockStorageVolumeVDSCommandParameters params = + new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); + params.setVolumeId(mbsDisk.getImageId()); + params.setStorageDomainId(mbsDisk.getStorageIds().get(0)); + try { + VDSReturnValue result = runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, params); + if (!result.getSucceeded()) { + log.warn("Host detach of managed block volume '{}' failed for transfer '{}': {}", + mbsDisk.getImageId(), getCommandId(), result.getVdsError()); + } + } catch (Exception e) { + log.error("Failed to detach managed block volume from host '{}' for transfer '{}': {}", + imageTransfer.getVdsId(), getCommandId(), e); + } + } + + /** + * Disconnects the managed block volume via the storage adapter (DISCONNECT_VOLUME). + * Called when transfer ends, after detaching from the host. + */ + 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 = cinderStorageDao.get(storageDomainId); + if (managedBlockStorage == null) { + log.warn("Managed block storage domain '{}' not found for disconnect", storageDomainId); + return; + } + List extraParams = new ArrayList<>(); + extraParams.add(mbsDisk.getImageId().toString()); + try { + CinderlibCommandParameters params = new CinderlibCommandParameters( + JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), + extraParams, + getCorrelationId()); + if (!cinderlibExecutor.runCommand(CinderlibExecutor.CinderlibCommand.DISCONNECT_VOLUME, params) + .getSucceed()) { + log.warn("Storage adapter DISCONNECT_VOLUME failed for disk '{}' volume '{}'", + imageTransfer.getDiskId(), mbsDisk.getImageId()); + } + } catch (Exception e) { + log.error("Failed to disconnect managed block volume '{}' for transfer '{}': {}", + mbsDisk.getImageId(), getCommandId(), e); + } + } + private boolean removeImageTicketFromDaemon(Guid imagedTicketId, Guid vdsId) { RemoveImageTicketVDSCommandParameters parameters = new RemoveImageTicketVDSCommandParameters( vdsId, imagedTicketId); 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/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..d697fd7a33e 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.TransferDiskImage )); EngineLocalConfig config = EngineLocalConfig.getInstance(); isDataBaseInitialized = Boolean.parseBoolean(config.getProperty(MANAGEDBLOCK_DB_ENABLE)); 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/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/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..a852355d2af 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)) { + 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/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/UploadImageModel.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageModel.java index e752de0a026..ccfd064a01e 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 @@ -10,6 +10,7 @@ import org.ovirt.engine.core.common.action.AddDiskParameters; import org.ovirt.engine.core.common.action.TransferDiskImageParameters; import org.ovirt.engine.core.common.action.TransferImageStatusParameters; +import org.ovirt.engine.core.common.businessentities.profiles.DiskProfile; import org.ovirt.engine.core.common.businessentities.StorageDomain; import org.ovirt.engine.core.common.businessentities.StorageFormatType; import org.ovirt.engine.core.common.businessentities.StoragePool; @@ -17,6 +18,7 @@ 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.StorageType; 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.TransferClientType; @@ -28,6 +30,7 @@ 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.UICommand; @@ -173,6 +176,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 +193,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 +246,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 +434,11 @@ 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); getInvalidityReasons().addAll(getImagePath().getInvalidityReasons()); getInvalidityReasons().addAll(getImageInfoModel().getInvalidityReasons()); @@ -465,7 +492,14 @@ 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()); + if (newDisk instanceof DiskImage) { + ((DiskImage) newDisk).setDiskProfileId(null); + } + } } TransferDiskImageParameters parameters = new TransferDiskImageParameters( 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..803d7d5f452 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 @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.logging.Logger; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -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"); break; case MANAGED_BLOCK_STORAGE: domainByDiskType = d -> d.getStorageType().isManagedBlockStorage(); + log.fine("AbstractDiskModel.updateStorageDomains: MANAGED_BLOCK_STORAGE disk type, MBS domains only"); 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); 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); From 2f84649a507b32ec876a88b5a1af83c4e9714275 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Tue, 10 Mar 2026 11:00:19 +0200 Subject: [PATCH 2/4] core: convert QCOW2 uploads to RAW on managed block storage Add support for converting QCOW2 images to RAW format when uploading disks via the Web UI to Managed Block Storage (MBS). Previously, the Web UI lacked an option to convert these images, which caused issues since most MBS domains require volumes in RAW format. Now, if a user uploads a QCOW2 disk to MBS, the system automatically uses qemu-img convert to handle the conversion. Both the source and converted volumes are created directly on the MBS. Signed-off-by: Slavka Peleva --- .../storage/disk/AddDiskCommandCallback.java | 11 + .../disk/image/TransferDiskImageCommand.java | 867 ++++++++++++++++-- .../action/TransferDiskImageParameters.java | 38 + .../storage/ImageTransferPhase.java | 4 +- ...anagedBlockVolumeVDSCommandParameters.java | 70 ++ .../common/vdscommands/VDSCommandType.java | 3 +- .../vdsbroker/jsonrpc/JsonRpcVdsServer.java | 15 + .../ConvertManagedBlockVolumeVDSCommand.java | 22 + .../core/vdsbroker/vdsbroker/IVdsServer.java | 3 + .../vdsbroker/vdsbroker/NullVdsServer.java | 5 + .../uicommon/disks/DisksViewColumns.java | 2 +- .../uicommon/popup/vm/VmDiskPopupWidget.java | 2 +- .../models/storage/UploadImageHandler.java | 2 + .../models/storage/UploadImageModel.java | 48 +- .../models/vms/AbstractDiskModel.java | 8 +- 15 files changed, 1031 insertions(+), 69 deletions(-) create mode 100644 backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/vdscommands/ConvertManagedBlockVolumeVDSCommandParameters.java create mode 100644 backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/vdsbroker/ConvertManagedBlockVolumeVDSCommand.java 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..68906aad5e3 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 @@ -14,6 +14,7 @@ import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.compat.backendcompat.CommandExecutionStatus; import org.ovirt.engine.core.dao.ImageDao; +import org.ovirt.engine.core.dao.StorageDomainDao; import org.ovirt.engine.core.utils.transaction.TransactionSupport; @Typed(AddDiskCommandCallback.class) @@ -25,6 +26,9 @@ public class AddDiskCommandCallback extends ConcurrentChildCommandsExecutionCall @Inject private ImageDao imageDao; + @Inject + private StorageDomainDao storageDomainDao; + @Override protected void childCommandsExecutionEnded(CommandBase command, boolean anyFailed, @@ -39,6 +43,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 e8c30ee0681..4f9bb1488e8 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 @@ -1,6 +1,7 @@ package org.ovirt.engine.core.bll.storage.disk.image; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -36,28 +37,39 @@ import org.ovirt.engine.core.common.action.ActionType; import org.ovirt.engine.core.common.action.AddDiskParameters; import org.ovirt.engine.core.common.action.ConnectManagedBlockStorageDeviceCommandParameters; +import org.ovirt.engine.core.common.action.DisconnectManagedBlockStorageDeviceParameters; 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.Permission; +import org.ovirt.engine.core.common.businessentities.Snapshot; import org.ovirt.engine.core.common.businessentities.StorageDomain; import org.ovirt.engine.core.common.businessentities.VDS; import org.ovirt.engine.core.common.businessentities.VM; import org.ovirt.engine.core.common.businessentities.VmBackup; import org.ovirt.engine.core.common.businessentities.VmBackupPhase; +import org.ovirt.engine.core.common.businessentities.VmDevice; +import org.ovirt.engine.core.common.businessentities.VmDeviceId; import org.ovirt.engine.core.common.businessentities.storage.Disk; 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.DiskLunMap; import org.ovirt.engine.core.common.businessentities.storage.DiskStorageType; -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.DiskVmElement; import org.ovirt.engine.core.common.businessentities.storage.ImageStatus; import org.ovirt.engine.core.common.businessentities.storage.ImageTicket; import org.ovirt.engine.core.common.businessentities.storage.ImageTicketInformation; import org.ovirt.engine.core.common.businessentities.storage.ImageTransfer; import org.ovirt.engine.core.common.businessentities.storage.ImageTransferBackend; 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.StorageType; import org.ovirt.engine.core.common.businessentities.storage.TimeoutPolicyType; import org.ovirt.engine.core.common.businessentities.storage.TransferType; @@ -67,6 +79,7 @@ import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.common.constants.StorageConstants; +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.locks.LockInfo; @@ -75,10 +88,13 @@ import org.ovirt.engine.core.common.utils.SizeConverter; import org.ovirt.engine.core.common.utils.cinderlib.CinderlibCommandParameters; import org.ovirt.engine.core.common.utils.cinderlib.CinderlibExecutor; -import org.ovirt.engine.core.common.vdscommands.AttachManagedBlockStorageVolumeVDSCommandParameters; +import org.ovirt.engine.core.common.utils.cinderlib.CinderlibExecutor.CinderlibCommand; import org.ovirt.engine.core.common.vdscommands.AddImageTicketVDSCommandParameters; +import org.ovirt.engine.core.common.vdscommands.AttachManagedBlockStorageVolumeVDSCommandParameters; +import org.ovirt.engine.core.common.vdscommands.ConvertManagedBlockVolumeVDSCommandParameters; 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; @@ -89,15 +105,22 @@ 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.CinderStorageDao; import org.ovirt.engine.core.dao.DiskDao; +import org.ovirt.engine.core.dao.DiskImageDynamicDao; +import org.ovirt.engine.core.dao.DiskLunMapDao; +import org.ovirt.engine.core.dao.DiskVmElementDao; import org.ovirt.engine.core.dao.ImageDao; -import org.ovirt.engine.core.dao.CinderStorageDao; +import org.ovirt.engine.core.dao.ImageStorageDomainMapDao; import org.ovirt.engine.core.dao.ImageTransferDao; +import org.ovirt.engine.core.dao.PermissionDao; import org.ovirt.engine.core.dao.SnapshotDao; import org.ovirt.engine.core.dao.StorageDomainDao; import org.ovirt.engine.core.dao.VdsDao; import org.ovirt.engine.core.dao.VmBackupDao; import org.ovirt.engine.core.dao.VmDao; +import org.ovirt.engine.core.dao.VmDeviceDao; import org.ovirt.engine.core.utils.EngineLocalConfig; import org.ovirt.engine.core.utils.JsonHelper; import org.ovirt.engine.core.utils.ReplacementUtils; @@ -145,9 +168,23 @@ public class TransferDiskImageCommand ext @Inject private ResourceManager resourceManager; @Inject + private CinderStorageDao cinderStorageDao; + @Inject private CinderlibExecutor cinderlibExecutor; @Inject - private CinderStorageDao cinderStorageDao; + private BaseDiskDao baseDiskDao; + @Inject + private DiskImageDynamicDao diskImageDynamicDao; + @Inject + private ImageStorageDomainMapDao imageStorageDomainMapDao; + @Inject + private VmDeviceDao vmDeviceDao; + @Inject + private DiskVmElementDao diskVmElementDao; + @Inject + private PermissionDao permissionDao; + @Inject + private DiskLunMapDao diskLunMapDao; private ImageioClient proxyClient; private VmBackup backup; @@ -197,13 +234,24 @@ protected String prepareImage(Guid vdsId) { return vmBackupDao.getBackupUrlForDisk( getParameters().getBackupId(), getDiskImage().getId()); } - if (usingNbdServer()) { StorageDomain sd = getStorageDomain(); if (sd != null && StorageType.MANAGED_BLOCK_STORAGE.equals(sd.getStorageType())) { return null; } } + // For MBS, VDSM does not have the domain in sdCache; connect and attach the volume on the host + // and use the path from the attach result instead of calling PrepareImageVDS. + if (isManagedBlockStorageForTransfer()) { + validateHostConnectorForMbs(); + DiskImage disk = getDiskImage(); + if (disk instanceof ManagedBlockStorageDisk) { + String path = connectAttachAndGetMbsVolumePath((ManagedBlockStorageDisk) disk); + if (path != null) { + return path; + } + } + } VDSReturnValue vdsRetVal = runVdsCommand(VDSCommandType.PrepareImage, getPrepareParameters(vdsId)); @@ -317,6 +365,68 @@ private NbdServerVDSParameters getStartNbdServerParameters(Guid vdsId) { return nbdServerVDSParameters; } + /** + * For NBD-based transfer of an MBS disk: connect the volume and attach it to the host so VDSM can + * expose it via NBD. Returns false if the disk is MBS and connect/attach fails (caller should return false). + * Returns true if not MBS, or connect+attach succeeded. + */ + 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( + 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); + } + + /** + * Attach the MBS volume to the host so VDSM can expose it via NBD. On failure logs and updates + * transfer phase; returns false. + */ + 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( + AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); + return false; + } + return true; + } + protected void tearDownImage(Guid vdsId, Guid backupId) { if (backupId != null) { // shouldn't teardown as prepare wasn't invoked @@ -331,6 +441,16 @@ protected void tearDownImage(Guid vdsId, Guid backupId) { boolean tearDownFailed = false; + // MBS: detach volume from host and disconnect (no TeardownImage; volume was attached for NBD transfer). + if (isManagedBlockStorageForTransfer()) { + ImageTransfer entity = imageTransferDao.get(getCommandId()); + if (entity != null && entity.getDiskId() != null) { + detachManagedBlockVolumeFromHost(entity); + disconnectManagedBlockVolumeForTransfer(entity); + } + return; + } + if (getTransferBackend() == ImageTransferBackend.FILE) { if (isTemplateBeingUsed(image)) { log.info("Transfer '{}': The template image is being used, skipping teardown", getCommandId()); @@ -391,6 +511,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; } @@ -559,6 +686,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(); } @@ -573,10 +704,41 @@ 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. + * For MBS: also infer when sourceVolumeFormat is null (e.g. REST API sets only format= + * source; MBS destination is always RAW). + */ + private boolean needsConversionAfterUpload() { + if (getParameters().getTransferType() != TransferType.Upload) { + log.debug("needsConversionAfterUpload: false (not upload)"); + return false; + } + VolumeFormat srcFmt = getParameters().getSourceVolumeFormat(); + VolumeFormat dstFmt = getParameters().getVolumeFormat(); + boolean mbs = isManagedBlockStorageForTransfer(); + log.debug("needsConversionAfterUpload: srcFmt={} dstFmt={} mbs={}", srcFmt, dstFmt, mbs); + if (srcFmt != null && dstFmt != null && !srcFmt.equals(dstFmt)) { + log.debug("needsConversionAfterUpload: true (src != dst)"); + return true; + } + // MBS: REST API sets only volumeFormat (source format); dest is always RAW + if (mbs && srcFmt == null && VolumeFormat.COW.equals(dstFmt)) { + log.debug("needsConversionAfterUpload: true (MBS qcow2 upload)"); + return true; // qcow2 upload to MBS needs convert to raw + } + log.debug("needsConversionAfterUpload: false"); + return false; + } + public void proceedCommandExecution(Guid childCmdId) { ImageTransfer entity = imageTransferDao.get(getCommandId()); if (entity == null || entity.getPhase() == null) { @@ -637,6 +799,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; } } @@ -915,6 +1083,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()); + if (isManagedBlockStorageForTransfer()) { + boolean started = startMbsUploadConversion(context); + if (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); @@ -946,6 +1137,544 @@ private void handleFinalizingSuccess(final StateContext context) { } } + /** + * How far MBS upload conversion got before failure; drives orphan cleanup order. + */ + private enum MbsConversionStartProgress { + VOLUME_CREATED, + CONNECTED_ON_HOST, + ATTACHED_ON_HOST + } + + /** + * Start MBS conversion: create destination volume, connect and attach on host. + * Persists convertedVolumeId for handleConverting. + */ + private boolean startMbsUploadConversion(StateContext context) { + Guid sdId = getStorageDomainId(); + Guid dstVolId = Guid.newGuid(); + ManagedBlockStorage managedBlockStorage = cinderStorageDao.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)); + CinderlibCommandParameters params = new CinderlibCommandParameters( + JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), + extraParams, getCorrelationId()); + if (!cinderlibExecutor.runCommand(CinderlibCommand.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; + } + } + + /** + * Best-effort removal of a partially created conversion volume after startMbsUploadConversion fails. + */ + private void cleanupOrphanMbsConversionVolume(ManagedBlockStorage managedBlockStorage, Guid sdId, VDS vds, + Guid volId, Map connectionInfo, MbsConversionStartProgress progress) { + if (managedBlockStorage == null) { + return; + } + log.warn("Cleaning up orphan MBS conversion volume '{}' for transfer '{}' (progress={})", + volId, getCommandId(), progress); + if (progress == MbsConversionStartProgress.ATTACHED_ON_HOST && vds != null) { + mbsConversionTryDetachVolume(vds, sdId, volId); + } + if (progress == MbsConversionStartProgress.CONNECTED_ON_HOST + || progress == MbsConversionStartProgress.ATTACHED_ON_HOST) { + if (vds != null && connectionInfo != null) { + mbsConversionTryDisconnectDevice(sdId, vds, volId, connectionInfo); + } + mbsConversionTryCinderDisconnect(managedBlockStorage, volId); + } + mbsConversionTryCinderDeleteVolume(managedBlockStorage, volId); + } + + private void mbsConversionTryDetachVolume(VDS vds, Guid sdId, Guid volId) { + try { + AttachManagedBlockStorageVolumeVDSCommandParameters detachParams = + new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); + detachParams.setVolumeId(volId); + detachParams.setStorageDomainId(sdId); + runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, detachParams); + } catch (Exception e) { + log.warn("Orphan cleanup: detach volume {} failed: {}", volId, e); + } + } + + 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); + } + } + + private void mbsConversionTryCinderDisconnect(ManagedBlockStorage managedBlockStorage, Guid volId) { + List extraParams = Collections.singletonList(volId.toString()); + try { + cinderlibExecutor.runCommand(CinderlibCommand.DISCONNECT_VOLUME, + new CinderlibCommandParameters( + JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), + extraParams, getCorrelationId())); + } catch (Exception e) { + log.warn("Orphan cleanup: cinderlib DISCONNECT_VOLUME for {} failed: {}", volId, e); + } + } + + private void mbsConversionTryCinderDeleteVolume(ManagedBlockStorage managedBlockStorage, Guid volId) { + List extraParams = Collections.singletonList(volId.toString()); + try { + cinderlibExecutor.runCommand(CinderlibCommand.DELETE_VOLUME, + new CinderlibCommandParameters( + JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), + extraParams, getCorrelationId())); + } catch (Exception e) { + log.warn("Orphan cleanup: cinderlib DELETE_VOLUME for {} failed: {}", volId, e); + } + } + + private void handleConverting(StateContext context) { + // MBS: synchronous convert (no copy task) + if (isManagedBlockStorageForTransfer() && getParameters().getConvertedVolumeId() != null) { + // Run convert (synchronous) on host + Guid sdId = getStorageDomainId(); + Guid srcVolId = getDiskImage().getImageId(); + Guid dstVolId = getParameters().getConvertedVolumeId(); + VolumeFormat srcFmt = getParameters().getSourceVolumeFormat() != null + ? getParameters().getSourceVolumeFormat() + : getParameters().getVolumeFormat(); // REST API: volumeFormat = source + VolumeFormat dstFmt = getParameters().getSourceVolumeFormat() != null && getParameters().getVolumeFormat() != null + ? getParameters().getVolumeFormat() + : VolumeFormat.RAW; // MBS dest is always RAW when inferred + 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; + } + 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; + } + 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; + } + + 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); + } + + /** + * Finish MBS conversion: detach/delete source volume, save new image, detach new volume. + */ + private void finishMbsUploadConversion(StateContext context) { + Guid oldImageId = getDiskImage().getImageId(); + Guid diskId = getParameters().getImageGroupID(); + Guid newVolId = getParameters().getConvertedVolumeId(); + Guid sdId = getStorageDomainId(); + 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 '{}': replacing oldVol={} with newVol={} (disk={})", + getCommandId(), oldImageId, newVolId, diskId); + + ManagedBlockStorage managedBlockStorage = cinderStorageDao.get(sdId); + detachAndDeleteMbsSourceVolume(vds, oldImageId, sdId, managedBlockStorage); + replaceSourceDiskWithDestinationMbsDisk(oldImageId, diskId, newVolId, currentImage); + getParameters().setImageGroupID(newVolId); + setImageId(newVolId); + setDiskImage(null); + setImage(null); + detachAndDisconnectMbsVolume(vds, newVolId, sdId, managedBlockStorage); + completeMbsConversionSuccess(context); + } + + private void detachAndDeleteMbsSourceVolume(VDS vds, Guid oldImageId, Guid sdId, + ManagedBlockStorage managedBlockStorage) { + AttachManagedBlockStorageVolumeVDSCommandParameters detachParams = + new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); + detachParams.setVolumeId(oldImageId); + detachParams.setStorageDomainId(sdId); + try { + runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, detachParams); + } catch (Exception e) { + log.warn("Detach source volume failed for MBS conversion: {}", e); + } + if (managedBlockStorage != null) { + List extraParams = new ArrayList<>(); + extraParams.add(oldImageId.toString()); + try { + cinderlibExecutor.runCommand(CinderlibCommand.DISCONNECT_VOLUME, + new CinderlibCommandParameters( + JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), + extraParams, getCorrelationId())); + } catch (Exception e) { + log.warn("Disconnect source volume failed for MBS conversion: {}", e); + } + try { + cinderlibExecutor.runCommand(CinderlibCommand.DELETE_VOLUME, + new CinderlibCommandParameters( + JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), + extraParams, getCorrelationId())); + } catch (Exception e) { + log.warn("Delete source volume failed for MBS conversion: {}", e); + } + } + } + + /** + * Two-disk flow (like copy): create destination disk for new volume, repoint all references + * from source disk to destination, then remove source disk. No new DB procedures. + */ + private void replaceSourceDiskWithDestinationMbsDisk(Guid oldImageId, Guid sourceDiskId, Guid newVolId, + DiskImage currentImage) { + imageStorageDomainMapDao.remove(oldImageId); + diskImageDynamicDao.remove(oldImageId); + imageDao.remove(oldImageId); + + DiskImage destImage = buildNewMbsDiskImage(newVolId, currentImage); + imagesHandler.saveImage(destImage); + baseDiskDao.save(new ManagedBlockStorageDisk(destImage)); + + DiskImageDynamic diskDynamic = new DiskImageDynamic(); + diskDynamic.setId(newVolId); + diskDynamic.setActualSize(currentImage.getActualSizeInBytes()); + diskImageDynamicDao.save(diskDynamic); + + repointReferencesFromSourceToDestination(sourceDiskId, newVolId); + baseDiskDao.remove(sourceDiskId); + } + + /** + * Repoint all references from source disk id to destination disk id using existing DAOs + * (no new DB procedures). Order preserves FKs where applicable. + */ + private void repointReferencesFromSourceToDestination(Guid sourceDiskId, Guid destDiskId) { + List vmDevices = vmDeviceDao.getVmDevicesByDeviceId(sourceDiskId, null); + for (VmDevice device : vmDevices) { + vmDeviceDao.remove(device.getId()); + VmDevice newDevice = new VmDevice(); + newDevice.setId(new VmDeviceId(destDiskId, device.getVmId())); + newDevice.setDevice(device.getDevice()); + newDevice.setType(device.getType()); + newDevice.setAddress(device.getAddress()); + newDevice.setSpecParams(device.getSpecParams()); + newDevice.setManaged(device.isManaged()); + newDevice.setPlugged(device.isPlugged()); + newDevice.setReadOnly(device.getReadOnly()); + newDevice.setSnapshotId(device.getSnapshotId()); + newDevice.setAlias(device.getAlias()); + newDevice.setCustomProperties(device.getCustomProperties()); + newDevice.setLogicalName(device.getLogicalName()); + newDevice.setHostDevice(device.getHostDevice()); + vmDeviceDao.save(newDevice); + } + + List diskVmElements = diskVmElementDao.getAllDiskVmElementsByDiskId(sourceDiskId); + for (DiskVmElement dve : diskVmElements) { + diskVmElementDao.remove(dve.getId()); + DiskVmElement newDve = new DiskVmElement(destDiskId, dve.getVmId()); + newDve.setBoot(dve.isBoot()); + newDve.setPassDiscard(dve.isPassDiscard()); + newDve.setDiskInterface(dve.getDiskInterface()); + newDve.setUsingScsiReservation(dve.isUsingScsiReservation()); + diskVmElementDao.save(newDve); + } + + ImageTransfer transfer = imageTransferDao.getByDiskId(sourceDiskId); + if (transfer != null) { + transfer.setDiskId(destDiskId); + imageTransferDao.update(transfer); + } + + List permissions = permissionDao.getAllForEntity(sourceDiskId); + for (Permission perm : permissions) { + perm.setObjectId(destDiskId); + permissionDao.update(perm); + } + + DiskLunMap lunMap = diskLunMapDao.getDiskLunMapByDiskId(sourceDiskId); + if (lunMap != null) { + diskLunMapDao.remove(lunMap.getId()); + DiskLunMap newMap = new DiskLunMap(destDiskId, lunMap.getLunId()); + diskLunMapDao.save(newMap); + } + + List snapshotsWithMemory = snapshotDao.getSnapshotsByMemoryDiskId(sourceDiskId); + for (Snapshot snapshot : snapshotsWithMemory) { + if (sourceDiskId.equals(snapshot.getMemoryDiskId())) { + snapshot.setMemoryDiskId(destDiskId); + } + if (sourceDiskId.equals(snapshot.getMetadataDiskId())) { + snapshot.setMetadataDiskId(destDiskId); + } + snapshotDao.update(snapshot); + } + } + + private DiskImage buildNewMbsDiskImage(Guid newVolId, DiskImage currentImage) { + VolumeFormat destFormat = getParameters().getVolumeFormat() != null && getParameters().getSourceVolumeFormat() != null + ? getParameters().getVolumeFormat() + : VolumeFormat.RAW; + DiskImage newImage = new DiskImage(); + newImage.setId(newVolId); + newImage.setImageId(newVolId); + newImage.setVolumeFormat(destFormat); + 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()); + } + return newImage; + } + + private void detachAndDisconnectMbsVolume(VDS vds, Guid volId, Guid sdId, + ManagedBlockStorage managedBlockStorage) { + AttachManagedBlockStorageVolumeVDSCommandParameters detachParams = + new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); + detachParams.setVolumeId(volId); + detachParams.setStorageDomainId(sdId); + try { + runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, detachParams); + } catch (Exception e) { + log.warn("Detach volume failed for MBS conversion: {}", e); + } + if (managedBlockStorage != null) { + List disconnectParams = new ArrayList<>(); + disconnectParams.add(volId.toString()); + try { + cinderlibExecutor.runCommand(CinderlibCommand.DISCONNECT_VOLUME, + new CinderlibCommandParameters( + JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), + disconnectParams, getCorrelationId())); + } catch (Exception e) { + log.warn("Disconnect volume failed for MBS conversion: {}", e); + } + } + } + + private void completeMbsConversionSuccess(StateContext context) { + setVolumeLegalityInStorage(LEGAL_IMAGE); + 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 boolean verifyImage(Guid transferingVdsId) { if (isManagedBlockStorageForTransfer()) { return true; @@ -1115,49 +1844,8 @@ protected boolean startImageTransferSession() { updateEntity(updates); if (usingNbdServer()) { - if (isManagedBlockStorageForTransfer()) { - if (getVds().getConnectorInfo() == null) { - log.error("Host '{}' has no connector info; cannot connect managed block volume for image transfer '{}'", - getVds().getName(), getCommandId()); - updateEntityPhaseToStoppedBySystem( - AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); - return false; - } - DiskImage disk = getDiskImage(); - if (disk instanceof ManagedBlockStorageDisk) { - ManagedBlockStorageDisk mbsDisk = (ManagedBlockStorageDisk) disk; - Guid storageDomainId = mbsDisk.getStorageIds().isEmpty() ? getStorageDomainId() : mbsDisk.getStorageIds().get(0); - ConnectManagedBlockStorageDeviceCommandParameters connectParams = - new ConnectManagedBlockStorageDeviceCommandParameters(storageDomainId, - getVds().getConnectorInfo(), - mbsDisk.getImageId()); - ActionReturnValue connectResult = runInternalAction(ActionType.ConnectManagedBlockStorageDevice, connectParams); - if (!connectResult.getSucceeded()) { - log.error("Failed to connect managed block volume for image transfer '{}': {}", - getCommandId(), connectResult.getFault()); - updateEntityPhaseToStoppedBySystem( - AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); - return false; - } - // Attach the volume to the host so VDSM can expose it via NBD (StartNbdServer looks up - // the storage domain on the host; without attach the domain is not present on the host). - Map connectionInfo = connectResult.getActionReturnValue(); - if (connectionInfo != null) { - 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( - AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); - return false; - } - } - } + if (isManagedBlockStorageForTransfer() && !connectAndAttachManagedBlockVolumeForTransfer()) { + return false; } try { VDSReturnValue vdsReturnValue = runVdsCommand(VDSCommandType.StartNbdServer, @@ -1353,6 +2041,72 @@ private boolean isManagedBlockStorageForTransfer() { return sd != null && StorageType.MANAGED_BLOCK_STORAGE.equals(sd.getStorageType()); } + 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"); + } + } + + /** + * Connect and attach the MBS volume on the host, then return the file path for the transfer. + * @return file URL (e.g. file:///path) or null if connection info was missing + */ + 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 boolean addImageTicketToProxy(Guid imagedTicketId, String hostUri) { // ToDo: move formatting to an helper for reuse in ImageTransfer String url = String.format("%s%s/%s", hostUri, IMAGES_PATH, imagedTicketId); @@ -1496,11 +2250,11 @@ private boolean stopImageTransferSession(ImageTransfer entity) { // Stopping NBD server if necessary if (usingNbdServer()) { stopNbdServer(entity.getVdsId()); - // For managed block: detach volume from host, then disconnect from storage adapter - if (isManagedBlockStorageForTransfer() && entity.getDiskId() != null) { - detachManagedBlockVolumeFromHost(entity); - disconnectManagedBlockVolumeForTransfer(entity); - } + } + // For managed block: detach volume from host when not doing conversion (conversion keeps it attached). + if (isManagedBlockStorageForTransfer() && !needsConversionAfterUpload() && entity.getDiskId() != null) { + detachManagedBlockVolumeFromHost(entity); + disconnectManagedBlockVolumeForTransfer(entity); } ImageTransfer updates = new ImageTransfer(); @@ -1569,7 +2323,7 @@ private void disconnectManagedBlockVolumeForTransfer(ImageTransfer imageTransfer JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), extraParams, getCorrelationId()); - if (!cinderlibExecutor.runCommand(CinderlibExecutor.CinderlibCommand.DISCONNECT_VOLUME, params) + if (!cinderlibExecutor.runCommand(CinderlibCommand.DISCONNECT_VOLUME, params) .getSucceed()) { log.warn("Storage adapter DISCONNECT_VOLUME failed for disk '{}' volume '{}'", imageTransfer.getDiskId(), mbsDisk.getImageId()); @@ -1771,3 +2525,4 @@ class StateContext { Guid childCmdId; } } +// DE72501108006231412815 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/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/jsonrpc/JsonRpcVdsServer.java b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/jsonrpc/JsonRpcVdsServer.java index bd771342b79..af353cac84b 100644 --- a/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/jsonrpc/JsonRpcVdsServer.java +++ b/backend/manager/modules/vdsbroker/src/main/java/org/ovirt/engine/core/vdsbroker/jsonrpc/JsonRpcVdsServer.java @@ -2657,6 +2657,21 @@ public StatusOnlyReturn detachManagedBlockStorageVolume(Guid volumeId, Guid sdUU return new StatusOnlyReturn(response); } + @Override + public StatusOnlyReturn convertManagedBlockVolume(Guid sdId, Guid srcVolId, Guid dstVolId, + String srcFormat, String dstFormat) { + JsonRpcRequest request = + new RequestBuilder("ManagedVolume.convert_volume") + .withParameter("sd_id", sdId.toString()) + .withParameter("src_vol_id", srcVolId.toString()) + .withParameter("dst_vol_id", dstVolId.toString()) + .withParameter("src_format", srcFormat) + .withParameter("dst_format", dstFormat) + .build(); + Map 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/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 a852355d2af..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 @@ -283,7 +283,7 @@ 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)) { + if ("IsAvailable".equals(args.propertyName)) { //$NON-NLS-1$ diskProfileEditor.setVisible(disk.getDiskProfile().getIsAvailable()); } }); 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..cc253f827d5 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 @@ -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/UploadImageModel.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageModel.java index ccfd064a01e..eba5dad0a15 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 @@ -10,17 +10,17 @@ import org.ovirt.engine.core.common.action.AddDiskParameters; import org.ovirt.engine.core.common.action.TransferDiskImageParameters; import org.ovirt.engine.core.common.action.TransferImageStatusParameters; -import org.ovirt.engine.core.common.businessentities.profiles.DiskProfile; 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.StorageType; 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; @@ -440,6 +440,9 @@ public boolean validate() { 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()); } else { @@ -495,9 +498,14 @@ private TransferDiskImageParameters createInitParams() { 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()); + 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) newDisk).setDiskProfileId(null); + 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); } } } @@ -506,7 +514,37 @@ private TransferDiskImageParameters createInitParams() { 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 803d7d5f452..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 @@ -5,8 +5,8 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.logging.Logger; import java.util.function.Predicate; +import java.util.logging.Logger; import java.util.stream.Collectors; import org.ovirt.engine.core.common.businessentities.ActionGroup; @@ -377,11 +377,11 @@ protected void updateStorageDomains(final StoragePool datacenter) { domainByDiskType = d -> d.getStorageDomainType().isDataDomain() || d.getStorageDomainType().isKubevirtDomain() || d.getStorageType().isManagedBlockStorage(); - log.fine("AbstractDiskModel.updateStorageDomains: IMAGE disk type, including Data/KubeVirt/ManagedBlock domains"); + 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"); + log.fine("AbstractDiskModel.updateStorageDomains: MANAGED_BLOCK_STORAGE disk type, MBS domains only"); //$NON-NLS-1$ break; default: domainByDiskType = s -> true; @@ -396,7 +396,7 @@ protected void updateStorageDomains(final StoragePool datacenter) { .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); + 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); From 3618c3852a38efc43695594903b8088c5e458a79 Mon Sep 17 00:00:00 2001 From: Slavka Peleva Date: Fri, 24 Apr 2026 13:24:39 +0300 Subject: [PATCH 3/4] core: use ManagedBlockExecutor in MBS upload conversion Replace leftover cinderlib types with the managedblock executor and DAO in TransferDiskImageCommand's upload conversion path. Map the CONVERTING ImageTransferPhase in ImageTransferMapper so REST API clients see a valid phase while conversion is in progress. Signed-off-by: Slavka Peleva --- .../disk/image/TransferDiskImageCommand.java | 46 +++++++++---------- .../restapi/types/ImageTransferMapper.java | 2 + .../types/ImageTransferMapperTest.java | 9 ++-- 3 files changed, 30 insertions(+), 27 deletions(-) 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 4f9bb1488e8..8161bd602f3 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 @@ -86,9 +86,9 @@ import org.ovirt.engine.core.common.locks.LockingGroup; import org.ovirt.engine.core.common.utils.Pair; import org.ovirt.engine.core.common.utils.SizeConverter; -import org.ovirt.engine.core.common.utils.cinderlib.CinderlibCommandParameters; -import org.ovirt.engine.core.common.utils.cinderlib.CinderlibExecutor; -import org.ovirt.engine.core.common.utils.cinderlib.CinderlibExecutor.CinderlibCommand; +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.AddImageTicketVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.AttachManagedBlockStorageVolumeVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.ConvertManagedBlockVolumeVDSCommandParameters; @@ -106,7 +106,6 @@ 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.CinderStorageDao; import org.ovirt.engine.core.dao.DiskDao; import org.ovirt.engine.core.dao.DiskImageDynamicDao; import org.ovirt.engine.core.dao.DiskLunMapDao; @@ -114,6 +113,7 @@ 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.ManagedBlockStorageDao; import org.ovirt.engine.core.dao.PermissionDao; import org.ovirt.engine.core.dao.SnapshotDao; import org.ovirt.engine.core.dao.StorageDomainDao; @@ -168,9 +168,9 @@ public class TransferDiskImageCommand ext @Inject private ResourceManager resourceManager; @Inject - private CinderStorageDao cinderStorageDao; + private ManagedBlockStorageDao managedBlockStorageDao; @Inject - private CinderlibExecutor cinderlibExecutor; + private ManagedBlockExecutor managedBlockExecutor; @Inject private BaseDiskDao baseDiskDao; @Inject @@ -1153,7 +1153,7 @@ private enum MbsConversionStartProgress { private boolean startMbsUploadConversion(StateContext context) { Guid sdId = getStorageDomainId(); Guid dstVolId = Guid.newGuid(); - ManagedBlockStorage managedBlockStorage = cinderStorageDao.get(sdId); + ManagedBlockStorage managedBlockStorage = managedBlockStorageDao.get(sdId); if (managedBlockStorage == null) { log.error("Managed block storage domain '{}' not found for conversion", sdId); return false; @@ -1218,10 +1218,10 @@ private boolean mbsConversionCreateVolume(ManagedBlockStorage managedBlockStorag List extraParams = new ArrayList<>(); extraParams.add(dstVolId.toString()); extraParams.add(Long.toString(sizeGiB)); - CinderlibCommandParameters params = new CinderlibCommandParameters( + ManagedBlockCommandParameters params = new ManagedBlockCommandParameters( JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), extraParams, getCorrelationId()); - if (!cinderlibExecutor.runCommand(CinderlibCommand.CREATE_VOLUME, params).getSucceed()) { + if (!managedBlockExecutor.runCommand(ManagedBlockCommand.CREATE_VOLUME, params).getSucceed()) { log.error("CREATE_VOLUME failed for transfer '{}'", getCommandId()); return false; } @@ -1305,8 +1305,8 @@ private void mbsConversionTryDisconnectDevice(Guid sdId, VDS vds, Guid volId, private void mbsConversionTryCinderDisconnect(ManagedBlockStorage managedBlockStorage, Guid volId) { List extraParams = Collections.singletonList(volId.toString()); try { - cinderlibExecutor.runCommand(CinderlibCommand.DISCONNECT_VOLUME, - new CinderlibCommandParameters( + managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, + new ManagedBlockCommandParameters( JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), extraParams, getCorrelationId())); } catch (Exception e) { @@ -1317,8 +1317,8 @@ private void mbsConversionTryCinderDisconnect(ManagedBlockStorage managedBlockSt private void mbsConversionTryCinderDeleteVolume(ManagedBlockStorage managedBlockStorage, Guid volId) { List extraParams = Collections.singletonList(volId.toString()); try { - cinderlibExecutor.runCommand(CinderlibCommand.DELETE_VOLUME, - new CinderlibCommandParameters( + managedBlockExecutor.runCommand(ManagedBlockCommand.DELETE_VOLUME, + new ManagedBlockCommandParameters( JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), extraParams, getCorrelationId())); } catch (Exception e) { @@ -1478,7 +1478,7 @@ private void finishMbsUploadConversion(StateContext context) { log.info("Finishing MBS upload conversion for transfer '{}': replacing oldVol={} with newVol={} (disk={})", getCommandId(), oldImageId, newVolId, diskId); - ManagedBlockStorage managedBlockStorage = cinderStorageDao.get(sdId); + ManagedBlockStorage managedBlockStorage = managedBlockStorageDao.get(sdId); detachAndDeleteMbsSourceVolume(vds, oldImageId, sdId, managedBlockStorage); replaceSourceDiskWithDestinationMbsDisk(oldImageId, diskId, newVolId, currentImage); getParameters().setImageGroupID(newVolId); @@ -1504,16 +1504,16 @@ private void detachAndDeleteMbsSourceVolume(VDS vds, Guid oldImageId, Guid sdId, List extraParams = new ArrayList<>(); extraParams.add(oldImageId.toString()); try { - cinderlibExecutor.runCommand(CinderlibCommand.DISCONNECT_VOLUME, - new CinderlibCommandParameters( + managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, + new ManagedBlockCommandParameters( JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), extraParams, getCorrelationId())); } catch (Exception e) { log.warn("Disconnect source volume failed for MBS conversion: {}", e); } try { - cinderlibExecutor.runCommand(CinderlibCommand.DELETE_VOLUME, - new CinderlibCommandParameters( + managedBlockExecutor.runCommand(ManagedBlockCommand.DELETE_VOLUME, + new ManagedBlockCommandParameters( JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), extraParams, getCorrelationId())); } catch (Exception e) { @@ -1653,8 +1653,8 @@ private void detachAndDisconnectMbsVolume(VDS vds, Guid volId, Guid sdId, List disconnectParams = new ArrayList<>(); disconnectParams.add(volId.toString()); try { - cinderlibExecutor.runCommand(CinderlibCommand.DISCONNECT_VOLUME, - new CinderlibCommandParameters( + managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, + new ManagedBlockCommandParameters( JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), disconnectParams, getCorrelationId())); } catch (Exception e) { @@ -2311,7 +2311,7 @@ private void disconnectManagedBlockVolumeForTransfer(ImageTransfer imageTransfer return; } Guid storageDomainId = mbsDisk.getStorageIds().get(0); - ManagedBlockStorage managedBlockStorage = cinderStorageDao.get(storageDomainId); + ManagedBlockStorage managedBlockStorage = managedBlockStorageDao.get(storageDomainId); if (managedBlockStorage == null) { log.warn("Managed block storage domain '{}' not found for disconnect", storageDomainId); return; @@ -2319,11 +2319,11 @@ private void disconnectManagedBlockVolumeForTransfer(ImageTransfer imageTransfer List extraParams = new ArrayList<>(); extraParams.add(mbsDisk.getImageId().toString()); try { - CinderlibCommandParameters params = new CinderlibCommandParameters( + ManagedBlockCommandParameters params = new ManagedBlockCommandParameters( JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), extraParams, getCorrelationId()); - if (!cinderlibExecutor.runCommand(CinderlibCommand.DISCONNECT_VOLUME, params) + if (!managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, params) .getSucceed()) { log.warn("Storage adapter DISCONNECT_VOLUME failed for disk '{}' volume '{}'", imageTransfer.getDiskId(), mbsDisk.getImageId()); 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 Date: Thu, 23 Jul 2026 13:21:54 +0300 Subject: [PATCH 4/4] core: split MBS disk image transfer into dedicated command Introduce MbsTransferDiskImageCommand and ActionType.MbsTransferDiskImage, and move MBS-specific upload/transfer logic out of TransferDiskImageCommand. Finish MBS upload conversion with a stable disk_id: update the images row in place instead of repointing base_disks and related references. Dispatch MbsTransferDiskImage from the web UI, REST API, and download flow for managed block storage domains. Signed-off-by: Slavka Peleva --- .../storage/disk/AddDiskCommandCallback.java | 4 - .../disk/image/TransferDiskImageCommand.java | 811 ++---------------- .../MbsTransferDiskImageCommand.java | 657 ++++++++++++++ .../ManagedBlockStorageDomainValidator.java | 2 +- .../engine/core/common/action/ActionType.java | 1 + .../BackendImageTransfersResource.java | 30 +- .../org/ovirt/engine/ui/uicommonweb/Linq.java | 14 + .../models/storage/DownloadImageManager.java | 18 +- .../models/storage/UploadImageHandler.java | 4 +- .../models/storage/UploadImageManager.java | 35 +- .../models/storage/UploadImageModel.java | 4 +- 11 files changed, 784 insertions(+), 796 deletions(-) create mode 100644 backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/storage/disk/managedblock/MbsTransferDiskImageCommand.java 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 68906aad5e3..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 @@ -14,7 +14,6 @@ import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.compat.backendcompat.CommandExecutionStatus; import org.ovirt.engine.core.dao.ImageDao; -import org.ovirt.engine.core.dao.StorageDomainDao; import org.ovirt.engine.core.utils.transaction.TransactionSupport; @Typed(AddDiskCommandCallback.class) @@ -26,9 +25,6 @@ public class AddDiskCommandCallback extends ConcurrentChildCommandsExecutionCall @Inject private ImageDao imageDao; - @Inject - private StorageDomainDao storageDomainDao; - @Override protected void childCommandsExecutionEnded(CommandBase command, boolean anyFailed, 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 8161bd602f3..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 @@ -1,7 +1,6 @@ package org.ovirt.engine.core.bll.storage.disk.image; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -28,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; @@ -36,8 +34,6 @@ import org.ovirt.engine.core.common.action.ActionReturnValue; import org.ovirt.engine.core.common.action.ActionType; import org.ovirt.engine.core.common.action.AddDiskParameters; -import org.ovirt.engine.core.common.action.ConnectManagedBlockStorageDeviceCommandParameters; -import org.ovirt.engine.core.common.action.DisconnectManagedBlockStorageDeviceParameters; import org.ovirt.engine.core.common.action.LockProperties; import org.ovirt.engine.core.common.action.RemoveDiskParameters; import org.ovirt.engine.core.common.action.RemoveImageParameters; @@ -46,31 +42,21 @@ 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.Permission; -import org.ovirt.engine.core.common.businessentities.Snapshot; import org.ovirt.engine.core.common.businessentities.StorageDomain; import org.ovirt.engine.core.common.businessentities.VDS; import org.ovirt.engine.core.common.businessentities.VM; import org.ovirt.engine.core.common.businessentities.VmBackup; import org.ovirt.engine.core.common.businessentities.VmBackupPhase; -import org.ovirt.engine.core.common.businessentities.VmDevice; -import org.ovirt.engine.core.common.businessentities.VmDeviceId; -import org.ovirt.engine.core.common.businessentities.storage.Disk; 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.DiskLunMap; import org.ovirt.engine.core.common.businessentities.storage.DiskStorageType; -import org.ovirt.engine.core.common.businessentities.storage.DiskVmElement; import org.ovirt.engine.core.common.businessentities.storage.ImageStatus; import org.ovirt.engine.core.common.businessentities.storage.ImageTicket; import org.ovirt.engine.core.common.businessentities.storage.ImageTicketInformation; import org.ovirt.engine.core.common.businessentities.storage.ImageTransfer; import org.ovirt.engine.core.common.businessentities.storage.ImageTransferBackend; 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.StorageType; import org.ovirt.engine.core.common.businessentities.storage.TimeoutPolicyType; import org.ovirt.engine.core.common.businessentities.storage.TransferType; import org.ovirt.engine.core.common.businessentities.storage.VmBackupType; @@ -79,19 +65,13 @@ import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.common.constants.StorageConstants; -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.locks.LockInfo; import org.ovirt.engine.core.common.locks.LockingGroup; import org.ovirt.engine.core.common.utils.Pair; 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.AddImageTicketVDSCommandParameters; -import org.ovirt.engine.core.common.vdscommands.AttachManagedBlockStorageVolumeVDSCommandParameters; -import org.ovirt.engine.core.common.vdscommands.ConvertManagedBlockVolumeVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.ExtendImageTicketVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.GetImageTicketVDSCommandParameters; import org.ovirt.engine.core.common.vdscommands.HSMTaskGuidBaseVDSCommandParameters; @@ -108,21 +88,15 @@ 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.DiskLunMapDao; -import org.ovirt.engine.core.dao.DiskVmElementDao; 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.ManagedBlockStorageDao; -import org.ovirt.engine.core.dao.PermissionDao; import org.ovirt.engine.core.dao.SnapshotDao; import org.ovirt.engine.core.dao.StorageDomainDao; import org.ovirt.engine.core.dao.VdsDao; import org.ovirt.engine.core.dao.VmBackupDao; import org.ovirt.engine.core.dao.VmDao; -import org.ovirt.engine.core.dao.VmDeviceDao; import org.ovirt.engine.core.utils.EngineLocalConfig; -import org.ovirt.engine.core.utils.JsonHelper; import org.ovirt.engine.core.utils.ReplacementUtils; import org.ovirt.engine.core.vdsbroker.ResourceManager; import org.ovirt.engine.core.vdsbroker.vdsbroker.PrepareImageReturn; @@ -135,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 @@ -168,23 +142,11 @@ public class TransferDiskImageCommand ext @Inject private ResourceManager resourceManager; @Inject - private ManagedBlockStorageDao managedBlockStorageDao; + protected BaseDiskDao baseDiskDao; @Inject - private ManagedBlockExecutor managedBlockExecutor; + protected DiskImageDynamicDao diskImageDynamicDao; @Inject - private BaseDiskDao baseDiskDao; - @Inject - private DiskImageDynamicDao diskImageDynamicDao; - @Inject - private ImageStorageDomainMapDao imageStorageDomainMapDao; - @Inject - private VmDeviceDao vmDeviceDao; - @Inject - private DiskVmElementDao diskVmElementDao; - @Inject - private PermissionDao permissionDao; - @Inject - private DiskLunMapDao diskLunMapDao; + protected ImageStorageDomainMapDao imageStorageDomainMapDao; private ImageioClient proxyClient; private VmBackup backup; @@ -234,24 +196,6 @@ protected String prepareImage(Guid vdsId) { return vmBackupDao.getBackupUrlForDisk( getParameters().getBackupId(), getDiskImage().getId()); } - if (usingNbdServer()) { - StorageDomain sd = getStorageDomain(); - if (sd != null && StorageType.MANAGED_BLOCK_STORAGE.equals(sd.getStorageType())) { - return null; - } - } - // For MBS, VDSM does not have the domain in sdCache; connect and attach the volume on the host - // and use the path from the attach result instead of calling PrepareImageVDS. - if (isManagedBlockStorageForTransfer()) { - validateHostConnectorForMbs(); - DiskImage disk = getDiskImage(); - if (disk instanceof ManagedBlockStorageDisk) { - String path = connectAttachAndGetMbsVolumePath((ManagedBlockStorageDisk) disk); - if (path != null) { - return path; - } - } - } VDSReturnValue vdsRetVal = runVdsCommand(VDSCommandType.PrepareImage, getPrepareParameters(vdsId)); @@ -271,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()) { @@ -287,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); } @@ -302,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); } @@ -365,68 +309,6 @@ private NbdServerVDSParameters getStartNbdServerParameters(Guid vdsId) { return nbdServerVDSParameters; } - /** - * For NBD-based transfer of an MBS disk: connect the volume and attach it to the host so VDSM can - * expose it via NBD. Returns false if the disk is MBS and connect/attach fails (caller should return false). - * Returns true if not MBS, or connect+attach succeeded. - */ - 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( - 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); - } - - /** - * Attach the MBS volume to the host so VDSM can expose it via NBD. On failure logs and updates - * transfer phase; returns false. - */ - 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( - AuditLogType.TRANSFER_IMAGE_STOPPED_BY_SYSTEM_FAILED_TO_CREATE_TICKET); - return false; - } - return true; - } - protected void tearDownImage(Guid vdsId, Guid backupId) { if (backupId != null) { // shouldn't teardown as prepare wasn't invoked @@ -441,16 +323,6 @@ protected void tearDownImage(Guid vdsId, Guid backupId) { boolean tearDownFailed = false; - // MBS: detach volume from host and disconnect (no TeardownImage; volume was attached for NBD transfer). - if (isManagedBlockStorageForTransfer()) { - ImageTransfer entity = imageTransferDao.get(getCommandId()); - if (entity != null && entity.getDiskId() != null) { - detachManagedBlockVolumeFromHost(entity); - disconnectManagedBlockVolumeForTransfer(entity); - } - return; - } - if (getTransferBackend() == ImageTransferBackend.FILE) { if (isTemplateBeingUsed(image)) { log.info("Transfer '{}': The template image is being used, skipping teardown", getCommandId()); @@ -551,7 +423,7 @@ protected DiskImage getDiskImage() { return diskImage; } - private VmBackup getBackup() { + protected VmBackup getBackup() { if (backup == null) { backup = vmBackupDao.get(getParameters().getBackupId()); } @@ -714,27 +586,19 @@ protected ImageTransferBackend getTransferBackend() { /** * True when upload needs format conversion: we upload to a volume in source format, * then convert to destination format and replace. - * For MBS: also infer when sourceVolumeFormat is null (e.g. REST API sets only format= - * source; MBS destination is always RAW). */ - private boolean needsConversionAfterUpload() { + protected boolean needsConversionAfterUpload() { if (getParameters().getTransferType() != TransferType.Upload) { log.debug("needsConversionAfterUpload: false (not upload)"); return false; } VolumeFormat srcFmt = getParameters().getSourceVolumeFormat(); VolumeFormat dstFmt = getParameters().getVolumeFormat(); - boolean mbs = isManagedBlockStorageForTransfer(); - log.debug("needsConversionAfterUpload: srcFmt={} dstFmt={} mbs={}", srcFmt, dstFmt, mbs); + log.debug("needsConversionAfterUpload: srcFmt={} dstFmt={}", srcFmt, dstFmt); if (srcFmt != null && dstFmt != null && !srcFmt.equals(dstFmt)) { log.debug("needsConversionAfterUpload: true (src != dst)"); return true; } - // MBS: REST API sets only volumeFormat (source format); dest is always RAW - if (mbs && srcFmt == null && VolumeFormat.COW.equals(dstFmt)) { - log.debug("needsConversionAfterUpload: true (MBS qcow2 upload)"); - return true; // qcow2 upload to MBS needs convert to raw - } log.debug("needsConversionAfterUpload: false"); return false; } @@ -1092,9 +956,9 @@ private void handleFinalizingSuccess(final StateContext context) { if (needsConversionAfterUpload()) { log.info("Upload transfer '{}' requires format conversion: {} -> {}", getCommandId(), getParameters().getSourceVolumeFormat(), getParameters().getVolumeFormat()); - if (isManagedBlockStorageForTransfer()) { - boolean started = startMbsUploadConversion(context); - if (started) { + ManagedBlockUploadConversionResult mbsResult = startManagedBlockUploadConversion(context); + if (mbsResult != ManagedBlockUploadConversionResult.NOT_APPLICABLE) { + if (mbsResult == ManagedBlockUploadConversionResult.STARTED) { updateEntityPhase(ImageTransferPhase.CONVERTING); } else { nextImageStatus = ImageStatus.ILLEGAL; @@ -1137,235 +1001,33 @@ private void handleFinalizingSuccess(final StateContext context) { } } - /** - * How far MBS upload conversion got before failure; drives orphan cleanup order. - */ - private enum MbsConversionStartProgress { - VOLUME_CREATED, - CONNECTED_ON_HOST, - ATTACHED_ON_HOST - } - - /** - * Start MBS conversion: create destination volume, connect and attach on host. - * Persists convertedVolumeId for handleConverting. - */ - 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; - } + protected enum ManagedBlockUploadConversionResult { + NOT_APPLICABLE, + STARTED, + FAILED } - private ActionReturnValue mbsConversionConnectVolume(VDS vds, Guid sdId, Guid dstVolId) { - ConnectManagedBlockStorageDeviceCommandParameters connectParams = - new ConnectManagedBlockStorageDeviceCommandParameters(sdId, vds.getConnectorInfo(), dstVolId); - return runInternalAction(ActionType.ConnectManagedBlockStorageDevice, connectParams); + protected ManagedBlockUploadConversionResult startManagedBlockUploadConversion(StateContext context) { + return ManagedBlockUploadConversionResult.NOT_APPLICABLE; } - 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; - } - } - - /** - * Best-effort removal of a partially created conversion volume after startMbsUploadConversion fails. - */ - private void cleanupOrphanMbsConversionVolume(ManagedBlockStorage managedBlockStorage, Guid sdId, VDS vds, - Guid volId, Map connectionInfo, MbsConversionStartProgress progress) { - if (managedBlockStorage == null) { - return; - } - log.warn("Cleaning up orphan MBS conversion volume '{}' for transfer '{}' (progress={})", - volId, getCommandId(), progress); - if (progress == MbsConversionStartProgress.ATTACHED_ON_HOST && vds != null) { - mbsConversionTryDetachVolume(vds, sdId, volId); - } - if (progress == MbsConversionStartProgress.CONNECTED_ON_HOST - || progress == MbsConversionStartProgress.ATTACHED_ON_HOST) { - if (vds != null && connectionInfo != null) { - mbsConversionTryDisconnectDevice(sdId, vds, volId, connectionInfo); - } - mbsConversionTryCinderDisconnect(managedBlockStorage, volId); - } - mbsConversionTryCinderDeleteVolume(managedBlockStorage, volId); - } - - private void mbsConversionTryDetachVolume(VDS vds, Guid sdId, Guid volId) { - try { - AttachManagedBlockStorageVolumeVDSCommandParameters detachParams = - new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); - detachParams.setVolumeId(volId); - detachParams.setStorageDomainId(sdId); - runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, detachParams); - } catch (Exception e) { - log.warn("Orphan cleanup: detach volume {} failed: {}", volId, e); - } + protected boolean handleManagedBlockConverting(StateContext context) { + return false; } - 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); - } + protected boolean hostSelectionIgnoresDomainCache() { + return false; } - private void mbsConversionTryCinderDisconnect(ManagedBlockStorage managedBlockStorage, Guid volId) { - List extraParams = Collections.singletonList(volId.toString()); - try { - managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, - new ManagedBlockCommandParameters( - JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), - extraParams, getCorrelationId())); - } catch (Exception e) { - log.warn("Orphan cleanup: cinderlib DISCONNECT_VOLUME for {} failed: {}", volId, e); - } + protected boolean connectManagedBlockVolumeBeforeNbd() { + return true; } - private void mbsConversionTryCinderDeleteVolume(ManagedBlockStorage managedBlockStorage, Guid volId) { - List extraParams = Collections.singletonList(volId.toString()); - try { - managedBlockExecutor.runCommand(ManagedBlockCommand.DELETE_VOLUME, - new ManagedBlockCommandParameters( - JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), - extraParams, getCorrelationId())); - } catch (Exception e) { - log.warn("Orphan cleanup: cinderlib DELETE_VOLUME for {} failed: {}", volId, e); - } + protected void detachManagedBlockVolumeWhenSessionStops(ImageTransfer entity) { } private void handleConverting(StateContext context) { - // MBS: synchronous convert (no copy task) - if (isManagedBlockStorageForTransfer() && getParameters().getConvertedVolumeId() != null) { - // Run convert (synchronous) on host - Guid sdId = getStorageDomainId(); - Guid srcVolId = getDiskImage().getImageId(); - Guid dstVolId = getParameters().getConvertedVolumeId(); - VolumeFormat srcFmt = getParameters().getSourceVolumeFormat() != null - ? getParameters().getSourceVolumeFormat() - : getParameters().getVolumeFormat(); // REST API: volumeFormat = source - VolumeFormat dstFmt = getParameters().getSourceVolumeFormat() != null && getParameters().getVolumeFormat() != null - ? getParameters().getVolumeFormat() - : VolumeFormat.RAW; // MBS dest is always RAW when inferred - 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; - } - 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; - } - 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); - } + if (handleManagedBlockConverting(context)) { return; } @@ -1458,227 +1120,7 @@ private void finishUploadConversion(StateContext context) { setCommandStatus(CommandStatus.SUCCEEDED); } - /** - * Finish MBS conversion: detach/delete source volume, save new image, detach new volume. - */ - private void finishMbsUploadConversion(StateContext context) { - Guid oldImageId = getDiskImage().getImageId(); - Guid diskId = getParameters().getImageGroupID(); - Guid newVolId = getParameters().getConvertedVolumeId(); - Guid sdId = getStorageDomainId(); - 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 '{}': replacing oldVol={} with newVol={} (disk={})", - getCommandId(), oldImageId, newVolId, diskId); - - ManagedBlockStorage managedBlockStorage = managedBlockStorageDao.get(sdId); - detachAndDeleteMbsSourceVolume(vds, oldImageId, sdId, managedBlockStorage); - replaceSourceDiskWithDestinationMbsDisk(oldImageId, diskId, newVolId, currentImage); - getParameters().setImageGroupID(newVolId); - setImageId(newVolId); - setDiskImage(null); - setImage(null); - detachAndDisconnectMbsVolume(vds, newVolId, sdId, managedBlockStorage); - completeMbsConversionSuccess(context); - } - - private void detachAndDeleteMbsSourceVolume(VDS vds, Guid oldImageId, Guid sdId, - ManagedBlockStorage managedBlockStorage) { - AttachManagedBlockStorageVolumeVDSCommandParameters detachParams = - new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); - detachParams.setVolumeId(oldImageId); - detachParams.setStorageDomainId(sdId); - try { - runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, detachParams); - } catch (Exception e) { - log.warn("Detach source volume failed for MBS conversion: {}", e); - } - if (managedBlockStorage != null) { - List extraParams = new ArrayList<>(); - extraParams.add(oldImageId.toString()); - try { - managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, - new ManagedBlockCommandParameters( - JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), - extraParams, getCorrelationId())); - } catch (Exception e) { - log.warn("Disconnect source volume failed for MBS conversion: {}", e); - } - try { - managedBlockExecutor.runCommand(ManagedBlockCommand.DELETE_VOLUME, - new ManagedBlockCommandParameters( - JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), - extraParams, getCorrelationId())); - } catch (Exception e) { - log.warn("Delete source volume failed for MBS conversion: {}", e); - } - } - } - - /** - * Two-disk flow (like copy): create destination disk for new volume, repoint all references - * from source disk to destination, then remove source disk. No new DB procedures. - */ - private void replaceSourceDiskWithDestinationMbsDisk(Guid oldImageId, Guid sourceDiskId, Guid newVolId, - DiskImage currentImage) { - imageStorageDomainMapDao.remove(oldImageId); - diskImageDynamicDao.remove(oldImageId); - imageDao.remove(oldImageId); - - DiskImage destImage = buildNewMbsDiskImage(newVolId, currentImage); - imagesHandler.saveImage(destImage); - baseDiskDao.save(new ManagedBlockStorageDisk(destImage)); - - DiskImageDynamic diskDynamic = new DiskImageDynamic(); - diskDynamic.setId(newVolId); - diskDynamic.setActualSize(currentImage.getActualSizeInBytes()); - diskImageDynamicDao.save(diskDynamic); - - repointReferencesFromSourceToDestination(sourceDiskId, newVolId); - baseDiskDao.remove(sourceDiskId); - } - - /** - * Repoint all references from source disk id to destination disk id using existing DAOs - * (no new DB procedures). Order preserves FKs where applicable. - */ - private void repointReferencesFromSourceToDestination(Guid sourceDiskId, Guid destDiskId) { - List vmDevices = vmDeviceDao.getVmDevicesByDeviceId(sourceDiskId, null); - for (VmDevice device : vmDevices) { - vmDeviceDao.remove(device.getId()); - VmDevice newDevice = new VmDevice(); - newDevice.setId(new VmDeviceId(destDiskId, device.getVmId())); - newDevice.setDevice(device.getDevice()); - newDevice.setType(device.getType()); - newDevice.setAddress(device.getAddress()); - newDevice.setSpecParams(device.getSpecParams()); - newDevice.setManaged(device.isManaged()); - newDevice.setPlugged(device.isPlugged()); - newDevice.setReadOnly(device.getReadOnly()); - newDevice.setSnapshotId(device.getSnapshotId()); - newDevice.setAlias(device.getAlias()); - newDevice.setCustomProperties(device.getCustomProperties()); - newDevice.setLogicalName(device.getLogicalName()); - newDevice.setHostDevice(device.getHostDevice()); - vmDeviceDao.save(newDevice); - } - - List diskVmElements = diskVmElementDao.getAllDiskVmElementsByDiskId(sourceDiskId); - for (DiskVmElement dve : diskVmElements) { - diskVmElementDao.remove(dve.getId()); - DiskVmElement newDve = new DiskVmElement(destDiskId, dve.getVmId()); - newDve.setBoot(dve.isBoot()); - newDve.setPassDiscard(dve.isPassDiscard()); - newDve.setDiskInterface(dve.getDiskInterface()); - newDve.setUsingScsiReservation(dve.isUsingScsiReservation()); - diskVmElementDao.save(newDve); - } - - ImageTransfer transfer = imageTransferDao.getByDiskId(sourceDiskId); - if (transfer != null) { - transfer.setDiskId(destDiskId); - imageTransferDao.update(transfer); - } - - List permissions = permissionDao.getAllForEntity(sourceDiskId); - for (Permission perm : permissions) { - perm.setObjectId(destDiskId); - permissionDao.update(perm); - } - - DiskLunMap lunMap = diskLunMapDao.getDiskLunMapByDiskId(sourceDiskId); - if (lunMap != null) { - diskLunMapDao.remove(lunMap.getId()); - DiskLunMap newMap = new DiskLunMap(destDiskId, lunMap.getLunId()); - diskLunMapDao.save(newMap); - } - - List snapshotsWithMemory = snapshotDao.getSnapshotsByMemoryDiskId(sourceDiskId); - for (Snapshot snapshot : snapshotsWithMemory) { - if (sourceDiskId.equals(snapshot.getMemoryDiskId())) { - snapshot.setMemoryDiskId(destDiskId); - } - if (sourceDiskId.equals(snapshot.getMetadataDiskId())) { - snapshot.setMetadataDiskId(destDiskId); - } - snapshotDao.update(snapshot); - } - } - - private DiskImage buildNewMbsDiskImage(Guid newVolId, DiskImage currentImage) { - VolumeFormat destFormat = getParameters().getVolumeFormat() != null && getParameters().getSourceVolumeFormat() != null - ? getParameters().getVolumeFormat() - : VolumeFormat.RAW; - DiskImage newImage = new DiskImage(); - newImage.setId(newVolId); - newImage.setImageId(newVolId); - newImage.setVolumeFormat(destFormat); - 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()); - } - return newImage; - } - - private void detachAndDisconnectMbsVolume(VDS vds, Guid volId, Guid sdId, - ManagedBlockStorage managedBlockStorage) { - AttachManagedBlockStorageVolumeVDSCommandParameters detachParams = - new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); - detachParams.setVolumeId(volId); - detachParams.setStorageDomainId(sdId); - try { - runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, detachParams); - } catch (Exception e) { - log.warn("Detach volume failed for MBS conversion: {}", e); - } - if (managedBlockStorage != null) { - List disconnectParams = new ArrayList<>(); - disconnectParams.add(volId.toString()); - try { - managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, - new ManagedBlockCommandParameters( - JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), - disconnectParams, getCorrelationId())); - } catch (Exception e) { - log.warn("Disconnect volume failed for MBS conversion: {}", e); - } - } - } - - private void completeMbsConversionSuccess(StateContext context) { - setVolumeLegalityInStorage(LEGAL_IMAGE); - 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 boolean verifyImage(Guid transferingVdsId) { - if (isManagedBlockStorageForTransfer()) { - return true; - } + protected boolean verifyImage(Guid transferingVdsId) { ImageActionsVDSCommandParameters parameters = new ImageActionsVDSCommandParameters(transferingVdsId, getStoragePool().getId(), getStorageDomainId(), @@ -1844,7 +1286,7 @@ protected boolean startImageTransferSession() { updateEntity(updates); if (usingNbdServer()) { - if (isManagedBlockStorageForTransfer() && !connectAndAttachManagedBlockVolumeForTransfer()) { + if (!connectManagedBlockVolumeBeforeNbd()) { return false; } try { @@ -1903,12 +1345,8 @@ private static boolean proxyEnabled() { @Override protected VDS checkForActiveVds() { - StorageDomain storageDomain = getStorageDomain(); - boolean isManagedBlockStorage = storageDomain != null - && StorageType.MANAGED_BLOCK_STORAGE.equals(storageDomain.getStorageType()); - Guid hostForExecution = vdsCommandsHelper.getHostForExecution(getStoragePoolId(), host -> { - if (isManagedBlockStorage) { + if (hostSelectionIgnoresDomainCache()) { return true; } var domainsData = resourceManager.getVdsManager(host.getId()).getDomains(); @@ -2010,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; } @@ -2036,77 +1474,6 @@ private boolean usingNbdServer() { return !isLiveBackup() && getTransferBackend() == ImageTransferBackend.NBD; } - private boolean isManagedBlockStorageForTransfer() { - StorageDomain sd = getStorageDomain(); - return sd != null && StorageType.MANAGED_BLOCK_STORAGE.equals(sd.getStorageType()); - } - - 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"); - } - } - - /** - * Connect and attach the MBS volume on the host, then return the file path for the transfer. - * @return file URL (e.g. file:///path) or null if connection info was missing - */ - 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 boolean addImageTicketToProxy(Guid imagedTicketId, String hostUri) { // ToDo: move formatting to an helper for reuse in ImageTransfer String url = String.format("%s%s/%s", hostUri, IMAGES_PATH, imagedTicketId); @@ -2141,10 +1508,7 @@ private ImageTicket buildImageTicket(Guid ticketId, String ticketUrl) { return ticket; } - private boolean setVolumeLegalityInStorage(boolean legal) { - if (isManagedBlockStorageForTransfer()) { - return true; - } + protected boolean setVolumeLegalityInStorage(boolean legal) { SetVolumeLegalityVDSCommandParameters parameters = new SetVolumeLegalityVDSCommandParameters(getStoragePool().getId(), getStorageDomainId(), @@ -2251,89 +1615,13 @@ private boolean stopImageTransferSession(ImageTransfer entity) { if (usingNbdServer()) { stopNbdServer(entity.getVdsId()); } - // For managed block: detach volume from host when not doing conversion (conversion keeps it attached). - if (isManagedBlockStorageForTransfer() && !needsConversionAfterUpload() && entity.getDiskId() != null) { - detachManagedBlockVolumeFromHost(entity); - disconnectManagedBlockVolumeForTransfer(entity); - } + detachManagedBlockVolumeWhenSessionStops(entity); ImageTransfer updates = new ImageTransfer(); updateEntity(updates, true); return true; } - /** - * Detaches the managed block volume from the host (VDS DetachManagedBlockStorageVolume). - * Called when transfer ends, before disconnecting from the storage adapter. - */ - 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; - } - AttachManagedBlockStorageVolumeVDSCommandParameters params = - new AttachManagedBlockStorageVolumeVDSCommandParameters(vds); - params.setVolumeId(mbsDisk.getImageId()); - params.setStorageDomainId(mbsDisk.getStorageIds().get(0)); - try { - VDSReturnValue result = runVdsCommand(VDSCommandType.DetachManagedBlockStorageVolume, params); - if (!result.getSucceeded()) { - log.warn("Host detach of managed block volume '{}' failed for transfer '{}': {}", - mbsDisk.getImageId(), getCommandId(), result.getVdsError()); - } - } catch (Exception e) { - log.error("Failed to detach managed block volume from host '{}' for transfer '{}': {}", - imageTransfer.getVdsId(), getCommandId(), e); - } - } - - /** - * Disconnects the managed block volume via the storage adapter (DISCONNECT_VOLUME). - * Called when transfer ends, after detaching from the host. - */ - 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); - if (managedBlockStorage == null) { - log.warn("Managed block storage domain '{}' not found for disconnect", storageDomainId); - return; - } - List extraParams = new ArrayList<>(); - extraParams.add(mbsDisk.getImageId().toString()); - try { - ManagedBlockCommandParameters params = new ManagedBlockCommandParameters( - JsonHelper.mapToJson(managedBlockStorage.getAllDriverOptions(), false), - extraParams, - getCorrelationId()); - if (!managedBlockExecutor.runCommand(ManagedBlockCommand.DISCONNECT_VOLUME, params) - .getSucceed()) { - log.warn("Storage adapter DISCONNECT_VOLUME failed for disk '{}' volume '{}'", - imageTransfer.getDiskId(), mbsDisk.getImageId()); - } - } catch (Exception e) { - log.error("Failed to disconnect managed block volume '{}' for transfer '{}': {}", - mbsDisk.getImageId(), getCommandId(), e); - } - } - private boolean removeImageTicketFromDaemon(Guid imagedTicketId, Guid vdsId) { RemoveImageTicketVDSCommandParameters parameters = new RemoveImageTicketVDSCommandParameters( vdsId, imagedTicketId); @@ -2374,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); @@ -2431,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. @@ -2519,10 +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; } } -// DE72501108006231412815 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/validator/storage/ManagedBlockStorageDomainValidator.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/validator/storage/ManagedBlockStorageDomainValidator.java index d697fd7a33e..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 @@ -47,7 +47,7 @@ public class ManagedBlockStorageDomainValidator { ActionType.CopyImageGroupWithData, ActionType.CopyManagedBlockDisk, ActionType.MoveOrCopyDisk, - ActionType.TransferDiskImage + 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/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/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/UploadImageHandler.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/storage/UploadImageHandler.java index cc253f827d5..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()); 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 eba5dad0a15..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 @@ -33,6 +33,7 @@ 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; @@ -454,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();