From b9ca974d98a7bcb6f2ee85574f7ce12992668410 Mon Sep 17 00:00:00 2001 From: sandeeplocharla Date: Mon, 28 Sep 2026 15:16:22 +0530 Subject: [PATCH 1/6] CSTACKEX-262: Support NetApp ONTAP Compute + storage and storage migration --- .../cloud/vm/VirtualMachineManagerImpl.java | 40 ++ .../vm/VirtualMachineManagerImplTest.java | 54 +++ .../StorageSystemDataMotionStrategy.java | 199 ++++++++-- .../StorageSystemDataMotionStrategyTest.java | 369 ++++++++++++++++++ .../wrapper/LibvirtMigrateCommandWrapper.java | 23 ++ .../kvm/storage/KVMStorageProcessor.java | 26 +- .../LibvirtMigrateCommandWrapperTest.java | 28 ++ .../kvm/storage/KVMStorageProcessorTest.java | 177 +++++++++ .../driver/OntapPrimaryDatastoreDriver.java | 79 +++- .../storage/feign/model/LunMap.java | 1 + .../storage/service/UnifiedNASStrategy.java | 8 +- .../storage/service/UnifiedSANStrategy.java | 12 +- .../OntapPrimaryDatastoreDriverTest.java | 225 ++++++++++- .../service/UnifiedNASStrategyTest.java | 10 +- .../service/UnifiedSANStrategyTest.java | 5 +- 15 files changed, 1193 insertions(+), 63 deletions(-) diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 364db685c9de..d7f2f47b7e8b 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -19,6 +19,7 @@ import static com.cloud.configuration.ConfigurationManagerImpl.EXPOSE_ERRORS_TO_USER; import static com.cloud.configuration.ConfigurationManagerImpl.MIGRATE_VM_ACROSS_CLUSTERS; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.lang.reflect.Field; import java.net.URI; @@ -319,6 +320,10 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac public static final String VM_WORK_JOB_HANDLER = VirtualMachineManagerImpl.class.getSimpleName(); private static final String VM_SYNC_ALERT_SUBJECT = "VM state sync alert"; + private static final String ONTAP_SVM_NAME_DETAIL = "svmName"; + private static final String ONTAP_SVM_UUID_DETAIL = "svmUUID"; + private static final String ONTAP_STORAGE_IP_DETAIL = "storageIP"; + private static final String ONTAP_PROTOCOL_DETAIL = "protocol"; @Inject private UserVmManager _userVmMgr; @@ -3512,6 +3517,9 @@ protected void executeManagedStorageChecksWhenTargetStoragePoolProvided(StorageP if (currentPool.getId() == targetPool.getId()) { return; } + if (isSupportedOntapLiveStorageMigration(currentPool, targetPool)) { + return; + } Map details = _storagePoolDao.getDetails(currentPool.getId()); if (details != null && Boolean.parseBoolean(details.get(Storage.Capability.ALLOW_MIGRATE_OTHER_POOLS.toString()))) { @@ -3521,6 +3529,38 @@ protected void executeManagedStorageChecksWhenTargetStoragePoolProvided(StorageP volume.getUuid(), currentPool.getUuid(), targetPool.getUuid())); } + protected boolean isSupportedOntapLiveStorageMigration(StoragePoolVO currentPool, StoragePoolVO targetPool) { + if (!DataStoreProvider.ONTAP_PLUGIN_NAME.equals(currentPool.getStorageProviderName()) + || !DataStoreProvider.ONTAP_PLUGIN_NAME.equals(targetPool.getStorageProviderName())) { + return false; + } + + Map currentDetails = _storagePoolDao.getDetails(currentPool.getId()); + Map targetDetails = _storagePoolDao.getDetails(targetPool.getId()); + if (currentDetails == null || targetDetails == null) { + return false; + } + + String currentSvmName = currentDetails.get(ONTAP_SVM_NAME_DETAIL); + String targetSvmName = targetDetails.get(ONTAP_SVM_NAME_DETAIL); + String currentSvmUuid = currentDetails.get(ONTAP_SVM_UUID_DETAIL); + String targetSvmUuid = targetDetails.get(ONTAP_SVM_UUID_DETAIL); + String currentStorageIp = currentDetails.get(ONTAP_STORAGE_IP_DETAIL); + String targetStorageIp = targetDetails.get(ONTAP_STORAGE_IP_DETAIL); + String currentProtocol = currentDetails.get(ONTAP_PROTOCOL_DETAIL); + String targetProtocol = targetDetails.get(ONTAP_PROTOCOL_DETAIL); + boolean isSameSvm = isNotBlank(currentSvmUuid) || isNotBlank(targetSvmUuid) + ? isNotBlank(currentSvmUuid) && currentSvmUuid.equals(targetSvmUuid) + : isNotBlank(currentSvmName) && currentSvmName.equals(targetSvmName); + return isNotBlank(currentStorageIp) + && currentStorageIp.equals(targetStorageIp) + && isSameSvm + && isNotBlank(currentProtocol) + && currentProtocol.equalsIgnoreCase(targetProtocol) + && currentPool.getPoolType() != null + && currentPool.getPoolType() == targetPool.getPoolType(); + } + /** * For each one of the volumes we will map it to a storage pool that is available via the target host. * An exception is thrown if we cannot find a storage pool that is accessible in the target host to migrate the volume to. diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java index c656e1a282af..91b4bb3111b9 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -552,6 +552,60 @@ public void allowVolumeMigrationsForPowerFlexStorage() { verify(storagePoolVoMock, Mockito.times(0)).getId(); } + @Test + public void allowVolumeMigrationBetweenOntapPoolsOnSameSvmAndProtocol() { + StoragePoolVO targetPool = Mockito.mock(StoragePoolVO.class); + Mockito.doReturn(true).when(storagePoolVoMock).isManaged(); + Mockito.doReturn(Storage.StoragePoolType.OntapiSCSI).when(storagePoolVoMock).getPoolType(); + Mockito.doReturn(org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider.ONTAP_PLUGIN_NAME) + .when(storagePoolVoMock).getStorageProviderName(); + Mockito.doReturn(12L).when(targetPool).getId(); + Mockito.doReturn(Storage.StoragePoolType.OntapiSCSI).when(targetPool).getPoolType(); + Mockito.doReturn(org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider.ONTAP_PLUGIN_NAME) + .when(targetPool).getStorageProviderName(); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "ISCSI")) + .when(storagePoolDaoMock).getDetails(storagePoolVoMockId); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "ISCSI")) + .when(storagePoolDaoMock).getDetails(12L); + + virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolProvided(storagePoolVoMock, volumeVoMock, targetPool); + } + + @Test + public void rejectVolumeMigrationBetweenOntapPoolsUsingDifferentProtocols() { + StoragePoolVO targetPool = Mockito.mock(StoragePoolVO.class); + Mockito.doReturn(true).when(storagePoolVoMock).isManaged(); + Mockito.doReturn(Storage.StoragePoolType.OntapiSCSI).when(storagePoolVoMock).getPoolType(); + Mockito.doReturn(org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider.ONTAP_PLUGIN_NAME) + .when(storagePoolVoMock).getStorageProviderName(); + Mockito.doReturn(12L).when(targetPool).getId(); + Mockito.doReturn(org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider.ONTAP_PLUGIN_NAME) + .when(targetPool).getStorageProviderName(); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "ISCSI")) + .when(storagePoolDaoMock).getDetails(storagePoolVoMockId); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "NFS3")) + .when(storagePoolDaoMock).getDetails(12L); + + assertThrows(CloudRuntimeException.class, + () -> virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolProvided(storagePoolVoMock, volumeVoMock, targetPool)); + } + + @Test + public void rejectOntapPoolsWithSameSvmNameOnDifferentStorageSystems() { + StoragePoolVO targetPool = Mockito.mock(StoragePoolVO.class); + Mockito.doReturn(org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider.ONTAP_PLUGIN_NAME) + .when(storagePoolVoMock).getStorageProviderName(); + Mockito.doReturn(12L).when(targetPool).getId(); + Mockito.doReturn(org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider.ONTAP_PLUGIN_NAME) + .when(targetPool).getStorageProviderName(); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "NFS3")) + .when(storagePoolDaoMock).getDetails(storagePoolVoMockId); + Mockito.doReturn(Map.of("storageIP", "10.0.0.2", "svmName", "svm1", "protocol", "NFS3")) + .when(storagePoolDaoMock).getDetails(12L); + + assertFalse(virtualMachineManagerImpl.isSupportedOntapLiveStorageMigration(storagePoolVoMock, targetPool)); + } + @Test public void executeManagedStorageChecksWhenTargetStoragePoolProvidedTestCurrentStoragePoolEqualsTargetPool() { Mockito.doReturn(true).when(storagePoolVoMock).isManaged(); diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java index 76bbc9cac908..db20444eb7c8 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java @@ -48,6 +48,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; @@ -85,6 +86,7 @@ import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; import com.cloud.agent.api.MigrateAnswer; import com.cloud.agent.api.MigrateCommand; import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; @@ -160,6 +162,10 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { private static final Random RANDOM = new Random(System.nanoTime()); private static final int LOCK_TIME_IN_SECONDS = 300; private static final String OPERATION_NOT_SUPPORTED = "This operation is not supported."; + private static final String ONTAP_SVM_NAME_DETAIL = "svmName"; + private static final String ONTAP_SVM_UUID_DETAIL = "svmUUID"; + private static final String ONTAP_STORAGE_IP_DETAIL = "storageIP"; + private static final String ONTAP_PROTOCOL_DETAIL = "protocol"; @Inject @@ -849,11 +855,10 @@ private void handleFailedVolumeMigration(VolumeInfo srcVolumeInfo, VolumeInfo de private void handleVolumeMigrationForKVM(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo, AsyncCompletionCallback callback) { VirtualMachine vm = srcVolumeInfo.getAttachedVM(); - checkAvailableForMigration(vm); - String errMsg = null; HostVO hostVO = null; try { + checkAvailableForMigration(vm); destVolumeInfo.getDataStore().getDriver().createAsync(destVolumeInfo.getDataStore(), destVolumeInfo, null); VolumeVO volumeVO = _volumeDao.findById(destVolumeInfo.getId()); updatePathFromScsiName(volumeVO); @@ -872,8 +877,12 @@ private void handleVolumeMigrationForKVM(VolumeInfo srcVolumeInfo, VolumeInfo de // re-retrieve volume to get any updated information from grant destVolumeInfo = _volumeDataFactory.getVolume(destVolumeInfo.getId(), destVolumeInfo.getDataStore()); - // migrate the volume via the hypervisor - String path = migrateVolumeForKVM(srcVolumeInfo, destVolumeInfo, hostVO, "Unable to migrate the volume from non-managed storage to managed storage"); + // migrate the volume via the hypervisor, unless the storage system can copy it between the pools itself + boolean useStorageSystemCopy = isSupportedOntapOfflineVolumeCopy(srcVolumeInfo, destVolumeInfo); + String path = migrateVolumeForKVM(srcVolumeInfo, destVolumeInfo, hostVO, + useStorageSystemCopy ? "Unable to copy the volume between storage pools" + : "Unable to migrate the volume from non-managed storage to managed storage", + useStorageSystemCopy); updateVolumePath(destVolumeInfo.getId(), path); volumeVO = _volumeDao.findById(destVolumeInfo.getId()); @@ -2057,8 +2066,10 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach StoragePoolVO destStoragePool = _storagePoolDao.findById(destDataStore.getId()); StoragePoolVO sourceStoragePool = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); - // do not initiate migration for the same PowerFlex/ScaleIO pool - if (sourceStoragePool.getId() == destStoragePool.getId() && sourceStoragePool.getPoolType() == Storage.StoragePoolType.PowerFlex) { + // do not initiate migration for the same PowerFlex/ScaleIO or ONTAP pool + if (sourceStoragePool.getId() == destStoragePool.getId() + && (sourceStoragePool.getPoolType() == Storage.StoragePoolType.PowerFlex + || isOntapPool(sourceStoragePool))) { continue; } @@ -2099,7 +2110,10 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach destDataStore.getDriver().createAsync(destDataStore, destVolumeInfo, null); managedStorageDestination = destStoragePool.isManaged(); - String volumeIdentifier = managedStorageDestination ? destVolumeInfo.get_iScsiName() : destVolumeInfo.getUuid(); + String volumeIdentifier = destVolumeInfo.getUuid(); + if (destStoragePool.getPoolType() != StoragePoolType.NetworkFilesystem && managedStorageDestination) { + volumeIdentifier = destVolumeInfo.get_iScsiName(); + } destVolume = _volumeDao.findById(destVolume.getId()); destVolume.setPath(volumeIdentifier); @@ -2115,21 +2129,36 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach handleQualityOfServiceForVolumeMigration(destVolumeInfo, PrimaryDataStoreDriver.QualityOfServiceState.MIGRATION); _volumeService.grantAccess(destVolumeInfo, destHost, destDataStore); + destVolumeInfo = _volumeDataFactory.getVolume(destVolume.getId(), destDataStore); String destPath = generateDestPath(destHost, destStoragePool, destVolumeInfo); MigrateCommand.MigrateDiskInfo migrateDiskInfo; - boolean isNonManagedToNfs = supportStoragePoolType(sourceStoragePool.getPoolType(), StoragePoolType.Filesystem, StoragePoolType.CLVM, StoragePoolType.CLVM_NG) && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem && !managedStorageDestination; - if (isNonManagedToNfs) { + boolean isNonManagedToNfs = supportStoragePoolType(sourceStoragePool.getPoolType(), + StoragePoolType.Filesystem, StoragePoolType.CLVM, StoragePoolType.CLVM_NG) + && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem + && !managedStorageDestination; + // ONTAP NFS is managed, but the dest is still a file on an NFS export, not a block device. + boolean isOntapNfsDestination = isOntapPool(destStoragePool) + && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem; + if (isNonManagedToNfs || isOntapNfsDestination) { migrateDiskInfo = new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), MigrateCommand.MigrateDiskInfo.DiskType.FILE, MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, MigrateCommand.MigrateDiskInfo.Source.FILE, - connectHostToVolume(destHost, destVolumeInfo.getPoolId(), volumeIdentifier)); + destPath); } else { String backingPath = generateBackingPath(destStoragePool, destVolumeInfo); - migrateDiskInfo = configureMigrateDiskInfo(srcVolumeInfo, destPath, backingPath); + String sourcePath = srcVolumeInfo.getPath(); + if (sourceStoragePool.isManaged()) { + if (sourceStoragePool.getPoolType() == StoragePoolType.OntapiSCSI) { + _volumeService.grantAccess(srcVolumeInfo, srcHost, srcVolumeInfo.getDataStore()); + srcVolumeInfo = _volumeDataFactory.getVolume(srcVolumeInfo.getId(), srcVolumeInfo.getDataStore()); + } + sourcePath = connectHostToVolume(srcHost, srcVolumeInfo.getPoolId(), srcVolumeInfo.get_iScsiName()); + } + migrateDiskInfo = configureMigrateDiskInfo(srcVolumeInfo, destPath, backingPath, sourcePath); migrateDiskInfo = updateMigrateDiskInfoForBlockDevice(migrateDiskInfo, destStoragePool); migrateDiskInfo.setSourceDiskOnStorageFileSystem(isStoragePoolTypeOfFile(sourceStoragePool)); migrateDiskInfoList.add(migrateDiskInfo); @@ -2137,8 +2166,14 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach migrateDiskInfo.setSourcePoolType(sourceStoragePool.getPoolType()); migrateDiskInfo.setDestPoolType(destVolumeInfo.getStoragePoolType()); prepareDiskWithSecretConsumerDetail(vmTO, srcVolumeInfo, destVolumeInfo.getPath()); + if (isOntapPool(destStoragePool)) { + prepareDiskWithDestinationVolume(vmTO, srcVolumeInfo, destVolumeInfo); + } migrateStorage.put(srcVolumeInfo.getPath(), migrateDiskInfo); + if (!srcVolumeInfo.getPath().equals(migrateDiskInfo.getSerialNumber())) { + migrateStorage.put(migrateDiskInfo.getSerialNumber(), migrateDiskInfo); + } srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo); } @@ -2360,6 +2395,9 @@ protected boolean isStoragePoolTypeOfFile(StoragePoolVO sourceStoragePool) { * Returns the iScsi connection path. */ protected String generateDestPath(Host destHost, StoragePoolVO destStoragePool, VolumeInfo destVolumeInfo) { + if (destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem) { + return connectHostToVolume(destHost, destVolumeInfo.getPoolId(), destVolumeInfo.getUuid()); + } return connectHostToVolume(destHost, destVolumeInfo.getPoolId(), destVolumeInfo.get_iScsiName()); } @@ -2371,7 +2409,11 @@ protected String generateBackingPath(StoragePoolVO destStoragePool, VolumeInfo d * Configures a {@link MigrateDiskInfo} object with disk type of BLOCK, Driver type RAW and Source DEV */ protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo srcVolumeInfo, String destPath, String backingPath) { - return new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), + return configureMigrateDiskInfo(srcVolumeInfo, destPath, backingPath, srcVolumeInfo.getPath()); + } + + protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo srcVolumeInfo, String destPath, String backingPath, String sourcePath) { + return new MigrateCommand.MigrateDiskInfo(sourcePath, MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, MigrateCommand.MigrateDiskInfo.DriverType.RAW, MigrateCommand.MigrateDiskInfo.Source.DEV, destPath, backingPath); @@ -2414,7 +2456,9 @@ protected MigrateCommand.MigrateDiskInfo updateMigrateDiskInfoForBlockDevice(Mig * Sets the volume path as the iScsi name in case of a configured iScsi. */ protected void setVolumePath(VolumeVO volume) { - volume.setPath(volume.get_iScsiName()); + if (StringUtils.isNotBlank(volume.get_iScsiName())) { + volume.setPath(volume.get_iScsiName()); + } } /** @@ -2502,8 +2546,8 @@ private void handlePostMigration(boolean success, Map sr VolumeVO volumeVO = _volumeDao.findById(destVolumeInfo.getId()); StoragePoolVO srcPoolVO = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); StoragePoolVO destPoolVO = _storagePoolDao.findById(destVolumeInfo.getPoolId()); - volumeVO.setFormat(destPoolVO != null && destPoolVO.getPoolType() == StoragePoolType.CLVM - ? ImageFormat.RAW : ImageFormat.QCOW2); + volumeVO.setFormat(destPoolVO != null && supportStoragePoolType(destPoolVO.getPoolType(), + StoragePoolType.CLVM, StoragePoolType.OntapiSCSI) ? ImageFormat.RAW : ImageFormat.QCOW2); volumeVO.setLastId(srcVolumeInfo.getId()); if (Objects.equals(srcVolumeInfo.getDiskOfferingId(), destVolumeInfo.getDiskOfferingId())) { @@ -2524,6 +2568,10 @@ private void handlePostMigration(boolean success, Map sr clvmPoolManager.setClvmLockHostId(destVolumeInfo.getId(), destHost.getId()); } + if (srcPoolVO != null && srcPoolVO.getPoolType() == StoragePoolType.OntapiSCSI) { + disconnectSourceVolumeAfterMigration(srcVolumeInfo, srcHost); + } + _volumeService.copyPoliciesBetweenVolumesAndDestroySourceVolumeAfterMigration(Event.OperationSucceeded, null, srcVolumeInfo, destVolumeInfo, false); // Update the volume ID for snapshots on secondary storage @@ -2535,7 +2583,12 @@ private void handlePostMigration(boolean success, Map sr } else { try { - disconnectHostFromVolume(destHost, destVolumeInfo.getPoolId(), destVolumeInfo.get_iScsiName()); + StoragePoolVO destPool = _storagePoolDao.findById(destVolumeInfo.getPoolId()); + String volumeIdentifier = destVolumeInfo.get_iScsiName(); + if (destPool != null && destPool.getPoolType() == StoragePoolType.NetworkFilesystem) { + volumeIdentifier = destVolumeInfo.getUuid(); + } + disconnectHostFromVolume(destHost, destVolumeInfo.getPoolId(), volumeIdentifier); } catch (Exception e) { logger.debug("Failed to disconnect (new) dest volume", e); @@ -2568,6 +2621,17 @@ private void handlePostMigration(boolean success, Map sr } } + protected void disconnectSourceVolumeAfterMigration(VolumeInfo srcVolumeInfo, Host srcHost) { + if (srcHost != null) { + try { + disconnectHostFromVolume(srcHost, srcVolumeInfo.getPoolId(), srcVolumeInfo.get_iScsiName()); + } catch (Exception e) { + logger.warn("Failed to disconnect source volume [{}] from source host [{}] after migration", + srcVolumeInfo.getId(), srcHost.getId(), e); + } + } + } + private Long getSuitableDiskOfferingForVolumeOnPool(VolumeVO volume, StoragePoolVO pool) { List diskOfferings = _diskOfferingDao.listAllActiveAndNonComputeDiskOfferings(); for (DiskOfferingVO diskOffering : diskOfferings) { @@ -2590,6 +2654,7 @@ private VolumeVO duplicateVolumeOnAnotherStorage(Volume volume, StoragePoolVO st newVol.setInstanceId(null); newVol.setChainInfo(null); newVol.setPath(null); + newVol.set_iScsiName(null); newVol.setFolder(null); newVol.setPodId(storagePoolVO.getPodId()); newVol.setPoolId(storagePoolVO.getId()); @@ -2647,18 +2712,21 @@ private ModifyTargetsCommand getModifyTargetsCommand(long storagePoolId, String } private List sendModifyTargetsCommand(ModifyTargetsCommand cmd, long hostId) { - ModifyTargetsAnswer modifyTargetsAnswer = (ModifyTargetsAnswer)agentManager.easySend(hostId, cmd); + Answer answer = agentManager.easySend(hostId, cmd); - if (modifyTargetsAnswer == null) { + if (answer == null) { throw new CloudRuntimeException("Unable to get an answer to the modify targets command"); } - if (!modifyTargetsAnswer.getResult()) { - String msg = "Unable to modify targets on the following host: " + hostId; - - throw new CloudRuntimeException(msg); + if (!answer.getResult()) { + throw new CloudRuntimeException(String.format("Unable to modify targets on host [%s]: %s", hostId, answer.getDetails())); + } + if (!(answer instanceof ModifyTargetsAnswer)) { + throw new CloudRuntimeException(String.format("Unexpected answer type [%s] while modifying targets on host [%s]", + answer.getClass().getSimpleName(), hostId)); } + ModifyTargetsAnswer modifyTargetsAnswer = (ModifyTargetsAnswer)answer; return modifyTargetsAnswer.getConnectedPaths(); } @@ -2705,6 +2773,13 @@ protected void prepareDiskWithSecretConsumerDetail(VirtualMachineTO vmTO, Volume } } + protected void prepareDiskWithDestinationVolume(VirtualMachineTO vmTO, VolumeInfo srcVolume, VolumeInfo destVolume) { + if (vmTO.getDisks() != null) { + Arrays.stream(vmTO.getDisks()).filter(diskTO -> diskTO.getData().getId() == srcVolume.getId()) + .forEach(diskTO -> diskTO.setData(destVolume.getTO())); + } + } + /** * At a high level: The source storage cannot be managed and * the destination storages can be all managed or all not managed, not mixed. @@ -2728,7 +2803,8 @@ protected void verifyLiveMigrationForKVM(Map volumeDataSt throw new CloudRuntimeException("Destination storage pool with ID " + dataStore.getId() + " was not located."); } - if (srcStoragePoolVO.isManaged() && srcStoragePoolVO.getId() != destStoragePoolVO.getId()) { + if (srcStoragePoolVO.isManaged() && srcStoragePoolVO.getId() != destStoragePoolVO.getId() + && !isSupportedOntapMigrationPoolPair(srcStoragePoolVO, destStoragePoolVO)) { throw new CloudRuntimeException("Migrating a volume online with KVM from managed storage is not currently supported."); } @@ -2740,6 +2816,53 @@ protected void verifyLiveMigrationForKVM(Map volumeDataSt } } + protected boolean isSupportedOntapMigrationPoolPair(StoragePoolVO srcStoragePoolVO, StoragePoolVO destStoragePoolVO) { + if (!isOntapPool(srcStoragePoolVO) || !isOntapPool(destStoragePoolVO)) { + return false; + } + + Map srcDetails = _storagePoolDao.getDetails(srcStoragePoolVO.getId()); + Map destDetails = _storagePoolDao.getDetails(destStoragePoolVO.getId()); + if (srcDetails == null || destDetails == null) { + return false; + } + + String srcSvmName = srcDetails.get(ONTAP_SVM_NAME_DETAIL); + String destSvmName = destDetails.get(ONTAP_SVM_NAME_DETAIL); + String srcSvmUuid = srcDetails.get(ONTAP_SVM_UUID_DETAIL); + String destSvmUuid = destDetails.get(ONTAP_SVM_UUID_DETAIL); + String srcStorageIp = srcDetails.get(ONTAP_STORAGE_IP_DETAIL); + String destStorageIp = destDetails.get(ONTAP_STORAGE_IP_DETAIL); + String srcProtocol = srcDetails.get(ONTAP_PROTOCOL_DETAIL); + String destProtocol = destDetails.get(ONTAP_PROTOCOL_DETAIL); + boolean isSameSvm = StringUtils.isNotBlank(srcSvmUuid) || StringUtils.isNotBlank(destSvmUuid) + ? StringUtils.isNotBlank(srcSvmUuid) && srcSvmUuid.equals(destSvmUuid) + : StringUtils.isNotBlank(srcSvmName) && srcSvmName.equals(destSvmName); + return StringUtils.isNotBlank(srcStorageIp) + && srcStorageIp.equals(destStorageIp) + && isSameSvm + && StringUtils.isNotBlank(srcProtocol) + && srcProtocol.equalsIgnoreCase(destProtocol) + && srcStoragePoolVO.getPoolType() != null + && srcStoragePoolVO.getPoolType() == destStoragePoolVO.getPoolType(); + } + + protected boolean isOntapPool(StoragePoolVO storagePoolVO) { + return storagePoolVO != null && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(storagePoolVO.getStorageProviderName()); + } + + protected boolean isSupportedOntapOfflineVolumeCopy(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo) { + StoragePoolVO srcStoragePoolVO = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); + StoragePoolVO destStoragePoolVO = _storagePoolDao.findById(destVolumeInfo.getPoolId()); + if (srcStoragePoolVO == null || destStoragePoolVO == null) { + return false; + } + StoragePoolType poolType = srcStoragePoolVO.getPoolType(); + return (poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.OntapiSCSI) + && poolType == destStoragePoolVO.getPoolType() + && isSupportedOntapMigrationPoolPair(srcStoragePoolVO, destStoragePoolVO); + } + private boolean canStorageSystemCreateVolumeFromVolume(long storagePoolId) { return storageSystemSupportsCapability(storagePoolId, DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString()); } @@ -3177,29 +3300,45 @@ private DataObject cacheSnapshotChain(SnapshotInfo snapshot, Scope scope) { } private String migrateVolumeForKVM(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo, HostVO hostVO, String errMsg) { + return migrateVolumeForKVM(srcVolumeInfo, destVolumeInfo, hostVO, errMsg, false); + } + + private String migrateVolumeForKVM(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo, HostVO hostVO, String errMsg, + boolean useStorageSystemCopy) { try { Map srcDetails = getVolumeDetails(srcVolumeInfo); Map destDetails = getVolumeDetails(destVolumeInfo); _volumeService.grantAccess(srcVolumeInfo, hostVO, srcVolumeInfo.getDataStore()); - MigrateVolumeCommand migrateVolumeCommand = new MigrateVolumeCommand(srcVolumeInfo.getTO(), destVolumeInfo.getTO(), - srcDetails, destDetails, StorageManager.KvmStorageOfflineMigrationWait.value()); + Command command; + if (useStorageSystemCopy) { + CopyCommand copyCommand = new CopyCommand(srcVolumeInfo.getTO(), destVolumeInfo.getTO(), + StorageManager.KvmStorageOfflineMigrationWait.value(), VirtualMachineManager.ExecuteInSequence.value()); + copyCommand.setOptions(srcDetails); + copyCommand.setOptions2(destDetails); + command = copyCommand; + } else { + command = new MigrateVolumeCommand(srcVolumeInfo.getTO(), destVolumeInfo.getTO(), + srcDetails, destDetails, StorageManager.KvmStorageOfflineMigrationWait.value()); + } _volumeService.grantAccess(srcVolumeInfo, hostVO, srcVolumeInfo.getDataStore()); handleQualityOfServiceForVolumeMigration(destVolumeInfo, PrimaryDataStoreDriver.QualityOfServiceState.MIGRATION); _volumeService.grantAccess(destVolumeInfo, hostVO, destVolumeInfo.getDataStore()); - MigrateVolumeAnswer migrateVolumeAnswer = (MigrateVolumeAnswer)agentManager.send(hostVO.getId(), migrateVolumeCommand); - if (migrateVolumeAnswer == null || !migrateVolumeAnswer.getResult()) { - if (migrateVolumeAnswer != null && StringUtils.isNotEmpty(migrateVolumeAnswer.getDetails())) { - throw new CloudRuntimeException(migrateVolumeAnswer.getDetails()); + Answer answer = agentManager.send(hostVO.getId(), command); + if (answer == null || !answer.getResult()) { + if (answer != null && StringUtils.isNotEmpty(answer.getDetails())) { + throw new CloudRuntimeException(answer.getDetails()); } else { throw new CloudRuntimeException(errMsg); } } - return migrateVolumeAnswer.getVolumePath(); + return useStorageSystemCopy + ? ((VolumeObjectTO)((CopyCmdAnswer)answer).getNewData()).getPath() + : ((MigrateVolumeAnswer)answer).getVolumePath(); } catch (CloudRuntimeException ex) { throw ex; } catch (Exception ex) { diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java index fc51eeba7b6e..bab2ff908058 100644 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java @@ -29,14 +29,25 @@ import java.util.Map; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; +import org.apache.cloudstack.engine.subsystem.api.storage.Scope; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.datastore.PrimaryDataStoreImpl; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.image.store.ImageStoreImpl; +import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.storage.volume.VolumeObject; import org.junit.Assert; import org.junit.Before; @@ -48,8 +59,18 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.agent.AgentManager; import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.ModifyTargetsAnswer; +import com.cloud.agent.api.ModifyTargetsCommand; +import com.cloud.agent.api.storage.MigrateVolumeAnswer; +import com.cloud.agent.api.storage.MigrateVolumeCommand; +import com.cloud.agent.api.to.DiskTO; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.storage.DataStoreRole; import com.cloud.storage.ImageStore; import com.cloud.storage.ScopeType; @@ -57,6 +78,10 @@ import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; import java.util.AbstractMap; import java.util.Arrays; import java.util.HashSet; @@ -80,6 +105,21 @@ public class StorageSystemDataMotionStrategyTest { private ImageStore destinationStore; @Mock private PrimaryDataStoreDao primaryDataStoreDao; + @Mock + private VolumeDao volumeDao; + @Mock + private VolumeDetailsDao volumeDetailsDao; + @Mock + // The strategy holds two VolumeDataFactory fields, so the mock has to carry the name of the one under test. + private VolumeDataFactory _volumeDataFactory; + @Mock + private VolumeService volumeService; + @Mock + private DataStoreManager dataStoreManager; + @Mock + private AgentManager agentManager; + @Mock + private HostDao hostDao; @Mock StoragePoolVO sourceStoragePoolVoMock, destinationStoragePoolVoMock; @@ -156,6 +196,256 @@ private void configureAndTestInternalCanHandle(boolean sPool0IsManaged, boolean Assert.assertEquals(expectedStrategyPriority, strategyPriority); } + @Test + public void supportedOntapMigrationPoolPairRequiresSameSvmAndProtocol() { + StoragePoolVO srcPool = Mockito.mock(StoragePoolVO.class); + StoragePoolVO destPool = Mockito.mock(StoragePoolVO.class); + Mockito.doReturn(1L).when(srcPool).getId(); + Mockito.doReturn(2L).when(destPool).getId(); + Mockito.doReturn(StoragePoolType.NetworkFilesystem).when(srcPool).getPoolType(); + Mockito.doReturn(StoragePoolType.NetworkFilesystem).when(destPool).getPoolType(); + Mockito.doReturn(DataStoreProvider.ONTAP_PLUGIN_NAME).when(srcPool).getStorageProviderName(); + Mockito.doReturn(DataStoreProvider.ONTAP_PLUGIN_NAME).when(destPool).getStorageProviderName(); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "NFS3")).when(primaryDataStoreDao).getDetails(1L); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "NFS3")).when(primaryDataStoreDao).getDetails(2L); + + Assert.assertTrue(strategy.isSupportedOntapMigrationPoolPair(srcPool, destPool)); + + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm2", "protocol", "NFS3")).when(primaryDataStoreDao).getDetails(2L); + Assert.assertFalse(strategy.isSupportedOntapMigrationPoolPair(srcPool, destPool)); + + Mockito.doReturn(Map.of("storageIP", "10.0.0.2", "svmName", "svm1", "protocol", "NFS3")).when(primaryDataStoreDao).getDetails(2L); + Assert.assertFalse(strategy.isSupportedOntapMigrationPoolPair(srcPool, destPool)); + + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmUUID", "svm-uuid", "svmName", "old-name", "protocol", "NFS3")) + .when(primaryDataStoreDao).getDetails(1L); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmUUID", "svm-uuid", "svmName", "new-name", "protocol", "NFS3")) + .when(primaryDataStoreDao).getDetails(2L); + Assert.assertTrue(strategy.isSupportedOntapMigrationPoolPair(srcPool, destPool)); + } + + @Test + public void verifyLiveMigrationAllowsOntapPoolsOnSameSvmAndProtocol() { + VolumeInfo srcVolume = Mockito.mock(VolumeInfo.class); + DataStore destStore = Mockito.mock(DataStore.class); + StoragePoolVO srcPool = Mockito.mock(StoragePoolVO.class); + StoragePoolVO destPool = Mockito.mock(StoragePoolVO.class); + Mockito.doReturn(1L).when(srcVolume).getPoolId(); + Mockito.doReturn(2L).when(destStore).getId(); + Mockito.doReturn(srcPool).when(primaryDataStoreDao).findById(1L); + Mockito.doReturn(destPool).when(primaryDataStoreDao).findById(2L); + Mockito.doReturn(1L).when(srcPool).getId(); + Mockito.doReturn(2L).when(destPool).getId(); + Mockito.doReturn(StoragePoolType.OntapiSCSI).when(srcPool).getPoolType(); + Mockito.doReturn(StoragePoolType.OntapiSCSI).when(destPool).getPoolType(); + Mockito.doReturn(true).when(srcPool).isManaged(); + Mockito.doReturn(true).when(destPool).isManaged(); + Mockito.doReturn(DataStoreProvider.ONTAP_PLUGIN_NAME).when(srcPool).getStorageProviderName(); + Mockito.doReturn(DataStoreProvider.ONTAP_PLUGIN_NAME).when(destPool).getStorageProviderName(); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "ISCSI")).when(primaryDataStoreDao).getDetails(1L); + Mockito.doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", "ISCSI")).when(primaryDataStoreDao).getDetails(2L); + + strategy.verifyLiveMigrationForKVM(Map.of(srcVolume, destStore)); + } + + @Test + public void offlineMigrationBetweenSupportedOntapPoolsUsesCopyCommand() throws Exception { + OfflineMigrationTestContext context = configureOfflineMigration(true, "NFS3", "svm1", null); + VolumeObjectTO copiedVolume = new VolumeObjectTO(); + copiedVolume.setPath("copied-volume-path"); + Mockito.when(agentManager.send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class))) + .thenReturn(new CopyCmdAnswer(copiedVolume)); + + strategy.copyAsync(context.srcVolume, context.destVolume, (Host) null, context.callback); + + Mockito.verify(agentManager).send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class)); + Mockito.verify(agentManager, Mockito.never()).send(Mockito.eq(context.host.getId()), Mockito.any(MigrateVolumeCommand.class)); + } + + @Test + public void offlineMigrationFromNonOntapPoolUsesMigrateVolumeCommand() throws Exception { + OfflineMigrationTestContext context = configureOfflineMigration(false, "NFS3", "svm1", null); + Mockito.when(agentManager.send(Mockito.eq(context.host.getId()), Mockito.any(MigrateVolumeCommand.class))) + .thenReturn(new MigrateVolumeAnswer(null, true, null, "migrated-volume-path")); + + strategy.copyAsync(context.srcVolume, context.destVolume, (Host) null, context.callback); + + Mockito.verify(agentManager).send(Mockito.eq(context.host.getId()), Mockito.any(MigrateVolumeCommand.class)); + Mockito.verify(agentManager, Mockito.never()).send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class)); + } + + @Test + public void offlineMigrationBetweenSupportedOntapIscsiPoolsUsesCopyCommand() throws Exception { + OfflineMigrationTestContext context = configureOfflineMigration(true, "ISCSI", "ISCSI", "svm1", null, + StoragePoolType.OntapiSCSI, StoragePoolType.OntapiSCSI); + VolumeObjectTO copiedVolume = new VolumeObjectTO(); + copiedVolume.setPath("copied-volume-path"); + Mockito.when(agentManager.send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class))) + .thenReturn(new CopyCmdAnswer(copiedVolume)); + + strategy.copyAsync(context.srcVolume, context.destVolume, (Host) null, context.callback); + + Mockito.verify(agentManager).send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class)); + Mockito.verify(agentManager, Mockito.never()).send(Mockito.eq(context.host.getId()), Mockito.any(MigrateVolumeCommand.class)); + } + + @Test + public void offlineMigrationBetweenOntapPoolsWithDifferentProtocolUsesMigrateVolumeCommand() throws Exception { + OfflineMigrationTestContext context = configureOfflineMigration(true, "NFS3", "ISCSI", "svm1", null, + StoragePoolType.NetworkFilesystem, StoragePoolType.OntapiSCSI); + Mockito.when(agentManager.send(Mockito.eq(context.host.getId()), Mockito.any(MigrateVolumeCommand.class))) + .thenReturn(new MigrateVolumeAnswer(null, true, null, "migrated-volume-path")); + + strategy.copyAsync(context.srcVolume, context.destVolume, (Host) null, context.callback); + + Mockito.verify(agentManager).send(Mockito.eq(context.host.getId()), Mockito.any(MigrateVolumeCommand.class)); + Mockito.verify(agentManager, Mockito.never()).send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class)); + } + + @Test + public void offlineMigrationBetweenOntapPoolsWithDifferentSvmUsesMigrateVolumeCommand() throws Exception { + OfflineMigrationTestContext context = configureOfflineMigration(true, "NFS3", "svm2", null); + Mockito.when(agentManager.send(Mockito.eq(context.host.getId()), Mockito.any(MigrateVolumeCommand.class))) + .thenReturn(new MigrateVolumeAnswer(null, true, null, "migrated-volume-path")); + + strategy.copyAsync(context.srcVolume, context.destVolume, (Host) null, context.callback); + + Mockito.verify(agentManager).send(Mockito.eq(context.host.getId()), Mockito.any(MigrateVolumeCommand.class)); + Mockito.verify(agentManager, Mockito.never()).send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class)); + } + + @Test + public void offlineMigrationAttachedToStoppedVmUsesCopyCommand() throws Exception { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + Mockito.doReturn(VirtualMachine.State.Stopped).when(vm).getState(); + OfflineMigrationTestContext context = configureOfflineMigration(true, "NFS3", "svm1", vm); + VolumeObjectTO copiedVolume = new VolumeObjectTO(); + copiedVolume.setPath("copied-volume-path"); + Mockito.when(agentManager.send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class))) + .thenReturn(new CopyCmdAnswer(copiedVolume)); + + strategy.copyAsync(context.srcVolume, context.destVolume, (Host) null, context.callback); + + Mockito.verify(agentManager).send(Mockito.eq(context.host.getId()), Mockito.any(CopyCommand.class)); + } + + @Test(expected = CloudRuntimeException.class) + public void offlineMigrationRejectsRunningVm() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + VolumeInfo srcVolume = Mockito.mock(VolumeInfo.class); + VolumeInfo destVolume = Mockito.mock(VolumeInfo.class); + PrimaryDataStore srcStore = Mockito.mock(PrimaryDataStore.class); + PrimaryDataStore destStore = Mockito.mock(PrimaryDataStore.class); + StoragePoolVO srcPool = Mockito.mock(StoragePoolVO.class); + StoragePoolVO destPool = Mockito.mock(StoragePoolVO.class); + Mockito.doReturn(VirtualMachine.State.Running).when(vm).getState(); + Mockito.doReturn(Volume.State.Migrating).when(srcVolume).getState(); + Mockito.doReturn(HypervisorType.KVM).when(srcVolume).getHypervisorType(); + Mockito.doReturn(vm).when(srcVolume).getAttachedVM(); + Mockito.doReturn(srcStore).when(srcVolume).getDataStore(); + Mockito.doReturn(destStore).when(destVolume).getDataStore(); + Mockito.doReturn(DataStoreRole.Primary).when(srcStore).getRole(); + Mockito.doReturn(DataStoreRole.Primary).when(destStore).getRole(); + Mockito.doReturn(1L).when(srcStore).getId(); + Mockito.doReturn(2L).when(destStore).getId(); + Mockito.doReturn(true).when(srcPool).isManaged(); + Mockito.doReturn(true).when(destPool).isManaged(); + Mockito.doReturn(srcPool).when(primaryDataStoreDao).findById(1L); + Mockito.doReturn(destPool).when(primaryDataStoreDao).findById(2L); + + strategy.copyAsync(srcVolume, destVolume, (Host) null, Mockito.mock(AsyncCompletionCallback.class)); + } + + private OfflineMigrationTestContext configureOfflineMigration(boolean isSourceOntap, String destProtocol, + String destSvmName, VirtualMachine vm) { + return configureOfflineMigration(isSourceOntap, "NFS3", destProtocol, destSvmName, vm, + StoragePoolType.NetworkFilesystem, StoragePoolType.NetworkFilesystem); + } + + private OfflineMigrationTestContext configureOfflineMigration(boolean isSourceOntap, String srcProtocol, + String destProtocol, String destSvmName, VirtualMachine vm, StoragePoolType srcPoolType, + StoragePoolType destPoolType) { + VolumeInfo srcVolume = Mockito.mock(VolumeInfo.class); + VolumeInfo destVolume = Mockito.mock(VolumeInfo.class); + PrimaryDataStore srcStore = Mockito.mock(PrimaryDataStore.class); + PrimaryDataStore destStore = Mockito.mock(PrimaryDataStore.class); + PrimaryDataStoreDriver destDriver = Mockito.mock(PrimaryDataStoreDriver.class); + Scope srcScope = Mockito.mock(Scope.class); + HostVO host = Mockito.mock(HostVO.class); + StoragePoolVO srcPool = Mockito.mock(StoragePoolVO.class); + StoragePoolVO destPool = Mockito.mock(StoragePoolVO.class); + VolumeVO srcVolumeVO = Mockito.mock(VolumeVO.class); + VolumeVO destVolumeVO = Mockito.mock(VolumeVO.class); + AsyncCompletionCallback callback = Mockito.mock(AsyncCompletionCallback.class); + + Mockito.doReturn(3L).when(host).getId(); + Mockito.doReturn(Volume.State.Migrating).when(srcVolume).getState(); + Mockito.doReturn(HypervisorType.KVM).when(srcVolume).getHypervisorType(); + Mockito.doReturn(vm).when(srcVolume).getAttachedVM(); + Mockito.doReturn(10L).when(srcVolume).getId(); + Mockito.doReturn(20L).when(destVolume).getId(); + Mockito.doReturn(1L).when(srcVolume).getPoolId(); + Mockito.doReturn(2L).when(destVolume).getPoolId(); + Mockito.doReturn(srcStore).when(srcVolume).getDataStore(); + Mockito.doReturn(destStore).when(destVolume).getDataStore(); + Mockito.doReturn(new VolumeObjectTO()).when(srcVolume).getTO(); + Mockito.doReturn(new VolumeObjectTO()).when(destVolume).getTO(); + + Mockito.doReturn(DataStoreRole.Primary).when(srcStore).getRole(); + Mockito.doReturn(DataStoreRole.Primary).when(destStore).getRole(); + Mockito.doReturn(1L).when(srcStore).getId(); + Mockito.doReturn(2L).when(destStore).getId(); + Mockito.doReturn("dest-store-uuid").when(destStore).getUuid(); + Mockito.doReturn(destDriver).when(destStore).getDriver(); + Mockito.doReturn(srcScope).when(srcStore).getScope(); + Mockito.doReturn(ScopeType.HOST).when(srcScope).getScopeType(); + Mockito.doReturn(host.getId()).when(srcScope).getScopeId(); + + Mockito.doReturn(isSourceOntap).when(srcPool).isManaged(); + Mockito.doReturn(true).when(destPool).isManaged(); + Mockito.doReturn(1L).when(srcPool).getId(); + Mockito.doReturn(2L).when(destPool).getId(); + Mockito.lenient().doReturn(srcPoolType).when(srcPool).getPoolType(); + Mockito.lenient().doReturn(destPoolType).when(destPool).getPoolType(); + Mockito.lenient().doReturn(isSourceOntap ? DataStoreProvider.ONTAP_PLUGIN_NAME : DataStoreProvider.DEFAULT_PRIMARY) + .when(srcPool).getStorageProviderName(); + Mockito.lenient().doReturn(DataStoreProvider.ONTAP_PLUGIN_NAME).when(destPool).getStorageProviderName(); + Mockito.lenient().doReturn(Map.of("storageIP", "10.0.0.1", "svmName", "svm1", "protocol", srcProtocol)) + .when(primaryDataStoreDao).getDetails(1L); + Mockito.lenient().doReturn(Map.of("storageIP", "10.0.0.1", "svmName", destSvmName, "protocol", destProtocol)) + .when(primaryDataStoreDao).getDetails(2L); + Mockito.doReturn(srcPool).when(primaryDataStoreDao).findById(1L); + Mockito.doReturn(destPool).when(primaryDataStoreDao).findById(2L); + + if (isSourceOntap) { + Mockito.doReturn(srcPoolType).when(srcVolumeVO).getPoolType(); + Mockito.doReturn(srcVolumeVO).when(volumeDao).findById(10L); + } + Mockito.doReturn(destPoolType).when(destVolumeVO).getPoolType(); + Mockito.doReturn(Storage.ImageFormat.QCOW2).when(destVolumeVO).getFormat(); + Mockito.doReturn(destVolumeVO).when(volumeDao).findById(20L); + Mockito.doReturn(destVolume).when(_volumeDataFactory).getVolume(20L, destStore); + Mockito.doReturn(destStore).when(dataStoreManager).getPrimaryDataStore("dest-store-uuid"); + Mockito.doReturn(host).when(hostDao).findById(3L); + + return new OfflineMigrationTestContext(srcVolume, destVolume, host, callback); + } + + private static class OfflineMigrationTestContext { + private final VolumeInfo srcVolume; + private final VolumeInfo destVolume; + private final HostVO host; + private final AsyncCompletionCallback callback; + + OfflineMigrationTestContext(VolumeInfo srcVolume, VolumeInfo destVolume, HostVO host, + AsyncCompletionCallback callback) { + this.srcVolume = srcVolume; + this.destVolume = destVolume; + this.host = host; + this.callback = callback; + } + } + @Test public void isStoragePoolTypeOfFileTest() { StoragePoolVO sourceStoragePool = Mockito.spy(new StoragePoolVO()); @@ -185,6 +475,65 @@ public void generateDestPathTest() { Mockito.verify(strategy).connectHostToVolume(destHost, 0l, "iScsiName"); } + @Test + public void generateDestPathForNfsUsesVolumeUuid() { + VolumeObject destVolumeInfo = Mockito.spy(new VolumeObject()); + StoragePoolVO destStoragePool = Mockito.mock(StoragePoolVO.class); + HostVO destHost = new HostVO("guid"); + Mockito.doReturn(StoragePoolType.NetworkFilesystem).when(destStoragePool).getPoolType(); + Mockito.doReturn("volume-uuid").when(destVolumeInfo).getUuid(); + Mockito.doReturn(0L).when(destVolumeInfo).getPoolId(); + Mockito.doReturn("expected").when(strategy).connectHostToVolume(destHost, 0L, "volume-uuid"); + + String result = strategy.generateDestPath(destHost, destStoragePool, destVolumeInfo); + + Assert.assertEquals("expected", result); + Mockito.verify(strategy).connectHostToVolume(destHost, 0L, "volume-uuid"); + } + + @Test + public void disconnectSourceVolumeAfterMigrationDisconnectsSourceHost() { + VolumeInfo srcVolumeInfo = Mockito.mock(VolumeInfo.class); + HostVO srcHost = Mockito.mock(HostVO.class); + StoragePoolVO storagePool = Mockito.mock(StoragePoolVO.class); + ModifyTargetsAnswer answer = new ModifyTargetsAnswer(); + answer.setConnectedPaths(List.of()); + + Mockito.doReturn(10L).when(srcVolumeInfo).getPoolId(); + Mockito.doReturn("/iqn/0").when(srcVolumeInfo).get_iScsiName(); + Mockito.doReturn(20L).when(srcHost).getId(); + Mockito.doReturn(storagePool).when(primaryDataStoreDao).findById(10L); + Mockito.doReturn(StoragePoolType.OntapiSCSI).when(storagePool).getPoolType(); + Mockito.doReturn("pool-uuid").when(storagePool).getUuid(); + Mockito.doReturn("10.0.0.1").when(storagePool).getHostAddress(); + Mockito.doReturn(3260).when(storagePool).getPort(); + Mockito.doReturn(answer).when(agentManager).easySend(Mockito.anyLong(), Mockito.any(ModifyTargetsCommand.class)); + + strategy.disconnectSourceVolumeAfterMigration(srcVolumeInfo, srcHost); + + Mockito.verify(agentManager).easySend(Mockito.eq(20L), Mockito.any(ModifyTargetsCommand.class)); + } + + @Test + public void prepareDiskWithDestinationVolumeUsesDestinationData() { + VirtualMachineTO vmTO = Mockito.mock(VirtualMachineTO.class); + DiskTO diskTO = Mockito.mock(DiskTO.class); + VolumeObjectTO srcVolumeTO = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO destVolumeTO = Mockito.mock(VolumeObjectTO.class); + VolumeInfo srcVolumeInfo = Mockito.mock(VolumeInfo.class); + VolumeInfo destVolumeInfo = Mockito.mock(VolumeInfo.class); + + Mockito.doReturn(new DiskTO[] {diskTO}).when(vmTO).getDisks(); + Mockito.doReturn(srcVolumeTO).when(diskTO).getData(); + Mockito.doReturn(10L).when(srcVolumeTO).getId(); + Mockito.doReturn(10L).when(srcVolumeInfo).getId(); + Mockito.doReturn(destVolumeTO).when(destVolumeInfo).getTO(); + + strategy.prepareDiskWithDestinationVolume(vmTO, srcVolumeInfo, destVolumeInfo); + + Mockito.verify(diskTO).setData(destVolumeTO); + } + @Test public void configureMigrateDiskInfoTest() { VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); @@ -210,6 +559,16 @@ public void configureMigrateDiskInfoWithBackingTest() { Assert.assertEquals("backingPath", migrateDiskInfo.getBackingStoreText()); } + @Test + public void configureMigrateDiskInfoUsesConnectedSourcePath() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + MigrateCommand.MigrateDiskInfo migrateDiskInfo = strategy.configureMigrateDiskInfo( + srcVolumeInfo, "destPath", null, "/dev/disk/by-path/source-lun-2"); + + Assert.assertEquals("/dev/disk/by-path/source-lun-2", migrateDiskInfo.getSerialNumber()); + Assert.assertEquals("destPath", migrateDiskInfo.getSourceText()); + } + @Test public void setVolumePathTest() { VolumeVO volume = new VolumeVO("name", 0l, 0l, 0l, 0l, 0l, "folder", "path", Storage.ProvisioningType.THIN, 0l, Volume.Type.ROOT); @@ -221,6 +580,16 @@ public void setVolumePathTest() { Assert.assertEquals(volumePath, volume.getPath()); } + @Test + public void setVolumePathPreservesNfsPathWithoutIscsiName() { + VolumeVO volume = new VolumeVO("name", 0L, 0L, 0L, 0L, 0L, "folder", "volume-uuid", + Storage.ProvisioningType.THIN, 0L, Volume.Type.ROOT); + + strategy.setVolumePath(volume); + + Assert.assertEquals("volume-uuid", volume.getPath()); + } + @Test public void shouldMigrateVolumeTest() { StoragePoolVO sourceStoragePool = Mockito.spy(new StoragePoolVO()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java index ed02ae6da38d..520a28c5aa05 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java @@ -347,6 +347,7 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. } deleteOrDisconnectDisksOnSourcePool(libvirtComputingResource, migrateDiskInfoList, disks); + disconnectDestinationOntapIscsiDisksOnSourceHost(libvirtComputingResource, mapMigrateStorage); libvirtComputingResource.cleanOldSecretsByDiskDef(conn, disks); } @@ -704,6 +705,28 @@ protected void deleteOrDisconnectDisksOnSourcePool(final LibvirtComputingResourc } } + protected void disconnectDestinationOntapIscsiDisksOnSourceHost(final LibvirtComputingResource libvirtComputingResource, + final Map migrateStorage) { + if (MapUtils.isEmpty(migrateStorage)) { + return; + } + + Set disconnectedPaths = new HashSet<>(); + for (MigrateDiskInfo migrateDiskInfo : migrateStorage.values()) { + String destinationPath = migrateDiskInfo.getSourceText(); + if (migrateDiskInfo.getDestPoolType() != Storage.StoragePoolType.OntapiSCSI + || StringUtils.isBlank(destinationPath) || !disconnectedPaths.add(destinationPath)) { + continue; + } + + DiskDef destinationDisk = new DiskDef(); + destinationDisk.setDiskPath(destinationPath); + if (!libvirtComputingResource.cleanupDisk(destinationDisk)) { + logger.warn("Failed to disconnect destination ONTAP iSCSI disk [{}] from the source host after migration", destinationPath); + } + } + } + /** * Deletes the local volume from the storage pool. */ diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 883570e24e4b..ce524391caa8 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -36,6 +36,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -3337,8 +3338,13 @@ public Answer copyVolumeFromPrimaryToPrimary(CopyCommand cmd) { destFormat, destVolumePath, destPrimaryStore.getId(), destPrimaryStore.getUuid(), destPrimaryStore.getName(), destPrimaryStore.getPoolType())); + Map srcDetails = ObjectUtils.defaultIfNull( + ObjectUtils.firstNonNull(cmd.getOptions(), srcPrimaryStore.getDetails()), Collections.emptyMap()); + Map destDetails = ObjectUtils.defaultIfNull( + ObjectUtils.firstNonNull(cmd.getOptions2(), destPrimaryStore.getDetails()), Collections.emptyMap()); + if (srcPrimaryStore.isManaged()) { - if (!storagePoolMgr.connectPhysicalDisk(srcPrimaryStore.getPoolType(), srcPrimaryStore.getUuid(), srcVolumePath, srcPrimaryStore.getDetails())) { + if (!storagePoolMgr.connectPhysicalDisk(srcPrimaryStore.getPoolType(), srcPrimaryStore.getUuid(), srcVolumePath, srcDetails)) { logger.warn(String.format("Failed to connect src volume %s, in storage pool %s", srcVol, srcPrimaryStore)); } } @@ -3354,12 +3360,24 @@ public Answer copyVolumeFromPrimaryToPrimary(CopyCommand cmd) { volume.setVmName(srcVol.getVmName()); KVMPhysicalDisk newVolume; String destVolumeName; - destPool = storagePoolMgr.getStoragePool(destPrimaryStore.getPoolType(), destPrimaryStore.getUuid()); + try { + destPool = storagePoolMgr.getStoragePool(destPrimaryStore.getPoolType(), destPrimaryStore.getUuid()); + } catch (CloudRuntimeException e) { + if (!destPrimaryStore.isManaged() || !e.getMessage().contains("not found")) { + throw e; + } + destPool = storagePoolMgr.createStoragePool(destPrimaryStore.getUuid(), destPrimaryStore.getHost(), + destPrimaryStore.getPort(), destPrimaryStore.getPath(), null, destPrimaryStore.getPoolType(), + destDetails); + } if (destPrimaryStore.isManaged()) { - if (!storagePoolMgr.connectPhysicalDisk(destPrimaryStore.getPoolType(), destPrimaryStore.getUuid(), destVolumePath, destPrimaryStore.getDetails())) { + if (!storagePoolMgr.connectPhysicalDisk(destPrimaryStore.getPoolType(), destPrimaryStore.getUuid(), destVolumePath, destDetails)) { logger.warn("Failed to connect dest volume {}, in storage pool {}", destVol, destPrimaryStore); } - destVolumeName = derivePath(destPrimaryStore, destData, destPrimaryStore.getDetails()); + } + if (destPrimaryStore.isManaged()) { + destVolumeName = StringUtils.defaultIfBlank( + derivePath(destPrimaryStore, destData, destDetails), destVolumePath); } else { PhysicalDiskFormat destPoolDefaultFormat = destPool.getDefaultFormat(); destFormat = getFormat(destPoolDefaultFormat); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java index aa36fb89765c..8e77f2580b9c 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java @@ -70,6 +70,7 @@ import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtConnection; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; +import com.cloud.storage.Storage; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.gpu.GpuDevice; @@ -805,6 +806,33 @@ public void deleteOrDisconnectDisksOnSourcePoolTest() { inOrderVerifyDeleteOrDisconnect(inOrder, spyLibvirtMigrateCmdWrapper, libvirtComputingResource, migrateDiskInfoList, diskDef2, 0, 1); } + @Test + public void disconnectDestinationOntapIscsiDisksOnSourceHostTest() { + String ontapIscsiPath = "/dev/disk/by-path/ip-10.0.0.1:3260-iscsi-iqn.test-lun-1"; + MigrateDiskInfo duplicateOntapIscsiDisk = new MigrateDiskInfo( + "serial-2", DiskType.BLOCK, DriverType.RAW, Source.DEV, ontapIscsiPath); + duplicateOntapIscsiDisk.setDestPoolType(Storage.StoragePoolType.OntapiSCSI); + MigrateDiskInfo nfsDisk = new MigrateDiskInfo( + "nfsSerial", DiskType.FILE, DriverType.QCOW2, Source.FILE, "/mnt/pool/volume"); + nfsDisk.setDestPoolType(Storage.StoragePoolType.NetworkFilesystem); + + Map migrateStorage = new HashMap<>(); + migrateStorage.put("source-1", new MigrateDiskInfo( + "serial-1", DiskType.BLOCK, DriverType.RAW, Source.DEV, ontapIscsiPath)); + migrateStorage.get("source-1").setDestPoolType(Storage.StoragePoolType.OntapiSCSI); + migrateStorage.put("source-2", duplicateOntapIscsiDisk); + migrateStorage.put("source-3", nfsDisk); + + LibvirtComputingResource libvirtComputingResource = Mockito.mock(LibvirtComputingResource.class); + Mockito.when(libvirtComputingResource.cleanupDisk(Mockito.any(DiskDef.class))).thenReturn(true); + + libvirtMigrateCmdWrapper.disconnectDestinationOntapIscsiDisksOnSourceHost(libvirtComputingResource, migrateStorage); + + Mockito.verify(libvirtComputingResource).cleanupDisk(Mockito.argThat( + disk -> ontapIscsiPath.equals(disk.getDiskPath()))); + Mockito.verifyNoMoreInteractions(libvirtComputingResource); + } + private MigrateDiskInfo createMigrateDiskInfo(boolean isSourceDiskOnStorageFileSystem) { MigrateDiskInfo migrateDiskInfo = new MigrateDiskInfo("serialNumber", DiskType.FILE, DriverType.QCOW2, Source.FILE, "sourceText"); migrateDiskInfo.setSourceDiskOnStorageFileSystem(isSourceDiskOnStorageFileSystem); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java index 0a58dfc79b0d..3f69f130dce6 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java @@ -23,10 +23,14 @@ import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; import com.cloud.storage.Storage; +import com.cloud.storage.Volume; import com.cloud.storage.template.TemplateConstants; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.command.CopyCommand; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.qemu.QemuImageOptions; @@ -43,6 +47,7 @@ import org.libvirt.Domain; import org.libvirt.LibvirtException; import org.mockito.InjectMocks; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; @@ -151,6 +156,178 @@ public void testIsEnoughSpaceForDownloadTemplateOnTemporaryLocationEnoughSpace() } } + @Test + public void copyManagedVolumeWithoutTargetReturnsCopiedDiskPath() { + CopyCommand command = Mockito.mock(CopyCommand.class); + VolumeObjectTO sourceVolume = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO destinationVolume = Mockito.mock(VolumeObjectTO.class); + PrimaryDataStoreTO sourceStore = Mockito.mock(PrimaryDataStoreTO.class); + PrimaryDataStoreTO destinationStore = Mockito.mock(PrimaryDataStoreTO.class); + KVMStoragePool destinationPool = Mockito.mock(KVMStoragePool.class); + KVMPhysicalDisk sourceDisk = Mockito.mock(KVMPhysicalDisk.class); + KVMPhysicalDisk copiedDisk = Mockito.mock(KVMPhysicalDisk.class); + Map destinationDetails = Map.of(); + + Mockito.when(command.getSrcTO()).thenReturn(sourceVolume); + Mockito.when(command.getDestTO()).thenReturn(destinationVolume); + Mockito.when(command.getWaitInMillSeconds()).thenReturn(60); + Mockito.when(sourceVolume.getDataStore()).thenReturn(sourceStore); + Mockito.when(destinationVolume.getDataStore()).thenReturn(destinationStore); + Mockito.when(sourceVolume.getPath()).thenReturn("source-path"); + Mockito.when(destinationVolume.getPath()).thenReturn("destination-path"); + Mockito.when(sourceVolume.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + Mockito.when(destinationVolume.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + Mockito.when(sourceVolume.getVolumeType()).thenReturn(Volume.Type.DATADISK); + Mockito.when(sourceStore.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(destinationStore.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(sourceStore.getUuid()).thenReturn("source-pool"); + Mockito.when(destinationStore.getUuid()).thenReturn("destination-pool"); + Mockito.when(destinationStore.getHost()).thenReturn("destination-host"); + Mockito.when(destinationStore.getPort()).thenReturn(2049); + Mockito.when(destinationStore.getPath()).thenReturn("/destination-export"); + Mockito.when(sourceStore.isManaged()).thenReturn(true); + Mockito.when(destinationStore.isManaged()).thenReturn(true); + Mockito.when(destinationStore.getDetails()).thenReturn(destinationDetails); + Mockito.when(storagePoolManager.getStoragePool( + Storage.StoragePoolType.NetworkFilesystem, + "destination-pool")).thenThrow(new CloudRuntimeException("not found")); + Mockito.when(storagePoolManager.createStoragePool( + "destination-pool", "destination-host", 2049, "/destination-export", null, + Storage.StoragePoolType.NetworkFilesystem, destinationDetails)).thenReturn(destinationPool); + Mockito.when(storagePoolManager.getPhysicalDisk( + Storage.StoragePoolType.NetworkFilesystem, + "source-pool", "source-path")).thenReturn(sourceDisk); + Mockito.when(storagePoolManager.copyPhysicalDisk( + Mockito.eq(sourceDisk), Mockito.eq("destination-path"), + Mockito.eq(destinationPool), Mockito.eq(60))).thenReturn(copiedDisk); + Mockito.when(copiedDisk.getFormat()).thenReturn(QemuImg.PhysicalDiskFormat.QCOW2); + + CopyCmdAnswer answer = (CopyCmdAnswer)storageProcessor.copyVolumeFromPrimaryToPrimary(command); + + InOrder destinationPoolSetup = Mockito.inOrder(storagePoolManager); + destinationPoolSetup.verify(storagePoolManager).getStoragePool( + Storage.StoragePoolType.NetworkFilesystem, "destination-pool"); + destinationPoolSetup.verify(storagePoolManager).createStoragePool( + "destination-pool", "destination-host", 2049, "/destination-export", null, + Storage.StoragePoolType.NetworkFilesystem, destinationDetails); + destinationPoolSetup.verify(storagePoolManager).connectPhysicalDisk( + Storage.StoragePoolType.NetworkFilesystem, + "destination-pool", "destination-path", destinationStore.getDetails()); + Assert.assertTrue(answer.getResult()); + Assert.assertEquals("destination-path", answer.getNewData().getPath()); + } + + @Test + public void copyVolumeFromPrimaryToPrimaryUsesCommandOptionsWhenStoreDetailsAreNull() { + CopyCommand command = Mockito.mock(CopyCommand.class); + VolumeObjectTO sourceVolume = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO destinationVolume = Mockito.mock(VolumeObjectTO.class); + PrimaryDataStoreTO sourceStore = Mockito.mock(PrimaryDataStoreTO.class); + PrimaryDataStoreTO destinationStore = Mockito.mock(PrimaryDataStoreTO.class); + KVMStoragePool destinationPool = Mockito.mock(KVMStoragePool.class); + KVMPhysicalDisk sourceDisk = Mockito.mock(KVMPhysicalDisk.class); + KVMPhysicalDisk copiedDisk = Mockito.mock(KVMPhysicalDisk.class); + Map sourceDetails = Map.of("iqn", "/iqn.source/7"); + Map destinationDetails = Map.of(); + + Mockito.when(command.getSrcTO()).thenReturn(sourceVolume); + Mockito.when(command.getDestTO()).thenReturn(destinationVolume); + Mockito.when(command.getWaitInMillSeconds()).thenReturn(60); + Mockito.when(command.getOptions()).thenReturn(sourceDetails); + Mockito.when(sourceVolume.getDataStore()).thenReturn(sourceStore); + Mockito.when(destinationVolume.getDataStore()).thenReturn(destinationStore); + Mockito.when(sourceVolume.getPath()).thenReturn("source-path"); + Mockito.when(destinationVolume.getPath()).thenReturn("destination-path"); + Mockito.when(sourceVolume.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + Mockito.when(destinationVolume.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + Mockito.when(sourceVolume.getVolumeType()).thenReturn(Volume.Type.DATADISK); + Mockito.when(sourceStore.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(destinationStore.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(sourceStore.getUuid()).thenReturn("source-pool"); + Mockito.when(destinationStore.getUuid()).thenReturn("destination-pool"); + Mockito.when(sourceStore.isManaged()).thenReturn(true); + Mockito.when(destinationStore.isManaged()).thenReturn(true); + Mockito.when(sourceStore.getDetails()).thenReturn(null); + Mockito.when(destinationStore.getDetails()).thenReturn(destinationDetails); + Mockito.when(storagePoolManager.getStoragePool( + Storage.StoragePoolType.NetworkFilesystem, + "destination-pool")).thenReturn(destinationPool); + Mockito.when(storagePoolManager.getPhysicalDisk( + Storage.StoragePoolType.NetworkFilesystem, + "source-pool", "source-path")).thenReturn(sourceDisk); + Mockito.when(storagePoolManager.copyPhysicalDisk( + Mockito.eq(sourceDisk), Mockito.eq("destination-path"), + Mockito.eq(destinationPool), Mockito.eq(60))).thenReturn(copiedDisk); + Mockito.when(copiedDisk.getFormat()).thenReturn(QemuImg.PhysicalDiskFormat.QCOW2); + + CopyCmdAnswer answer = (CopyCmdAnswer)storageProcessor.copyVolumeFromPrimaryToPrimary(command); + + Mockito.verify(storagePoolManager).connectPhysicalDisk( + Storage.StoragePoolType.NetworkFilesystem, + "source-pool", "source-path", sourceDetails); + Assert.assertTrue(answer.getResult()); + Assert.assertEquals("destination-path", answer.getNewData().getPath()); + } + + @Test + public void copyVolumeFromPrimaryToPrimaryUsesCommandOptions2WhenStoreDetailsAreNull() { + CopyCommand command = Mockito.mock(CopyCommand.class); + VolumeObjectTO sourceVolume = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO destinationVolume = Mockito.mock(VolumeObjectTO.class); + PrimaryDataStoreTO sourceStore = Mockito.mock(PrimaryDataStoreTO.class); + PrimaryDataStoreTO destinationStore = Mockito.mock(PrimaryDataStoreTO.class); + KVMStoragePool destinationPool = Mockito.mock(KVMStoragePool.class); + KVMPhysicalDisk sourceDisk = Mockito.mock(KVMPhysicalDisk.class); + KVMPhysicalDisk copiedDisk = Mockito.mock(KVMPhysicalDisk.class); + Map destinationDetails = Map.of("managedStoreTarget", "destination-iqn-path"); + + Mockito.when(command.getSrcTO()).thenReturn(sourceVolume); + Mockito.when(command.getDestTO()).thenReturn(destinationVolume); + Mockito.when(command.getWaitInMillSeconds()).thenReturn(60); + Mockito.when(command.getOptions2()).thenReturn(destinationDetails); + Mockito.when(sourceVolume.getDataStore()).thenReturn(sourceStore); + Mockito.when(destinationVolume.getDataStore()).thenReturn(destinationStore); + Mockito.when(sourceVolume.getPath()).thenReturn("source-path"); + Mockito.when(destinationVolume.getPath()).thenReturn("destination-path"); + Mockito.when(sourceVolume.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + Mockito.when(destinationVolume.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + Mockito.when(sourceVolume.getVolumeType()).thenReturn(Volume.Type.DATADISK); + Mockito.when(sourceStore.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(destinationStore.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(sourceStore.getUuid()).thenReturn("source-pool"); + Mockito.when(destinationStore.getUuid()).thenReturn("destination-pool"); + Mockito.when(destinationStore.getHost()).thenReturn("destination-host"); + Mockito.when(destinationStore.getPort()).thenReturn(2049); + Mockito.when(destinationStore.getPath()).thenReturn("/destination-export"); + Mockito.when(sourceStore.isManaged()).thenReturn(true); + Mockito.when(destinationStore.isManaged()).thenReturn(true); + Mockito.when(destinationStore.getDetails()).thenReturn(null); + Mockito.when(storagePoolManager.getStoragePool( + Storage.StoragePoolType.NetworkFilesystem, + "destination-pool")).thenThrow(new CloudRuntimeException("not found")); + Mockito.when(storagePoolManager.createStoragePool( + "destination-pool", "destination-host", 2049, "/destination-export", null, + Storage.StoragePoolType.NetworkFilesystem, destinationDetails)).thenReturn(destinationPool); + Mockito.when(storagePoolManager.getPhysicalDisk( + Storage.StoragePoolType.NetworkFilesystem, + "source-pool", "source-path")).thenReturn(sourceDisk); + Mockito.when(storagePoolManager.copyPhysicalDisk( + Mockito.eq(sourceDisk), Mockito.eq("destination-iqn-path"), + Mockito.eq(destinationPool), Mockito.eq(60))).thenReturn(copiedDisk); + Mockito.when(copiedDisk.getFormat()).thenReturn(QemuImg.PhysicalDiskFormat.QCOW2); + + CopyCmdAnswer answer = (CopyCmdAnswer)storageProcessor.copyVolumeFromPrimaryToPrimary(command); + + Mockito.verify(storagePoolManager).createStoragePool( + "destination-pool", "destination-host", 2049, "/destination-export", null, + Storage.StoragePoolType.NetworkFilesystem, destinationDetails); + Mockito.verify(storagePoolManager).connectPhysicalDisk( + Storage.StoragePoolType.NetworkFilesystem, + "destination-pool", "destination-path", destinationDetails); + Assert.assertTrue(answer.getResult()); + Assert.assertEquals("destination-iqn-path", answer.getNewData().getPath()); + } + @Test public void testIsEnoughSpaceForDownloadTemplateOnTemporaryLocationNotExistingLocation() { try (MockedStatic