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..5a321a19a7b7 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; @@ -233,13 +234,24 @@ public StrategyPriority canHandle(DataObject srcData, DataObject destData) { if (srcData instanceof VolumeInfo && destData instanceof VolumeInfo) { VolumeInfo srcVolumeInfo = (VolumeInfo)srcData; + VolumeInfo destVolumeInfo = (VolumeInfo)destData; + + // OntapDataMotionStrategy also returns HIGHEST for these and a tie keeps the first strategy. + if (srcVolumeInfo.getState() == Volume.State.Migrating + && srcVolumeInfo.getDataStore().getRole() == DataStoreRole.Primary + && destVolumeInfo.getDataStore().getRole() == DataStoreRole.Primary) { + StoragePoolVO srcPool = _storagePoolDao.findById(srcVolumeInfo.getDataStore().getId()); + StoragePoolVO destPool = _storagePoolDao.findById(destVolumeInfo.getDataStore().getId()); + if ((srcPool != null && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(srcPool.getStorageProviderName())) + || (destPool != null && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(destPool.getStorageProviderName()))) { + return StrategyPriority.CANT_HANDLE; + } + } if (isVolumeOnManagedStorage(srcVolumeInfo)) { return StrategyPriority.HIGHEST; } - VolumeInfo destVolumeInfo = (VolumeInfo)destData; - if (isVolumeOnManagedStorage(destVolumeInfo)) { return StrategyPriority.HIGHEST; } @@ -320,6 +332,20 @@ public final StrategyPriority canHandle(Map volumeMap, Ho * Handles migrating volumes on managed Storage. */ protected StrategyPriority internalCanHandle(Map volumeMap, Host srcHost, Host destHost) { + // OntapDataMotionStrategy also returns HIGHEST for these and a tie keeps the first strategy. + for (VolumeInfo volumeInfo : volumeMap.keySet()) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(volumeInfo.getPoolId()); + if (storagePoolVO != null && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(storagePoolVO.getStorageProviderName())) { + return StrategyPriority.CANT_HANDLE; + } + } + for (DataStore dataStore : volumeMap.values()) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(dataStore.getId()); + if (storagePoolVO != null && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(storagePoolVO.getStorageProviderName())) { + return StrategyPriority.CANT_HANDLE; + } + } + Set volumeInfoSet = volumeMap.keySet(); for (VolumeInfo volumeInfo : volumeInfoSet) { 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..37ec25c90d05 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 @@ -30,6 +30,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +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.StrategyPriority; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; @@ -133,6 +134,48 @@ public void internalCanHandleTestNoStoragePoolsIsManaged() { configureAndTestInternalCanHandle(false, false, StrategyPriority.CANT_HANDLE); } + @Test + public void canHandleDeclinesMigratingVolumeOnOntapPrimary() { + PrimaryDataStore srcStore = mock(PrimaryDataStoreImpl.class); + PrimaryDataStore destStore = mock(PrimaryDataStoreImpl.class); + VolumeObject srcVolume = mock(VolumeObject.class); + VolumeObject destVolume = mock(VolumeObject.class); + doReturn(Volume.State.Migrating).when(srcVolume).getState(); + doReturn(srcStore).when(srcVolume).getDataStore(); + doReturn(destStore).when(destVolume).getDataStore(); + doReturn(DataStoreRole.Primary).when(srcStore).getRole(); + doReturn(DataStoreRole.Primary).when(destStore).getRole(); + doReturn(1L).when(srcStore).getId(); + doReturn(2L).when(destStore).getId(); + StoragePoolVO srcPool = Mockito.spy(new StoragePoolVO()); + StoragePoolVO destPool = Mockito.spy(new StoragePoolVO()); + lenient().doReturn(true).when(srcPool).isManaged(); + doReturn(DataStoreProvider.ONTAP_PLUGIN_NAME).when(destPool).getStorageProviderName(); + doReturn(srcPool).when(primaryDataStoreDao).findById(1L); + doReturn(destPool).when(primaryDataStoreDao).findById(2L); + + Assert.assertEquals(StrategyPriority.CANT_HANDLE, strategy.canHandle(srcVolume, destVolume)); + } + + @Test + public void internalCanHandleDeclinesMapWithOntapPool() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + Mockito.doReturn(1L).when(ds).getId(); + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + StoragePoolVO srcPool = Mockito.spy(new StoragePoolVO()); + lenient().doReturn(true).when(srcPool).isManaged(); + StoragePoolVO destPool = Mockito.spy(new StoragePoolVO()); + Mockito.doReturn(DataStoreProvider.ONTAP_PLUGIN_NAME).when(destPool).getStorageProviderName(); + Mockito.doReturn(srcPool).when(primaryDataStoreDao).findById(0L); + Mockito.doReturn(destPool).when(primaryDataStoreDao).findById(1L); + + Assert.assertEquals(StrategyPriority.CANT_HANDLE, + strategy.internalCanHandle(volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid"))); + } + private void configureAndTestInternalCanHandle(boolean sPool0IsManaged, boolean sPool1IsManaged, StrategyPriority expectedStrategyPriority) { VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); Mockito.doReturn(0l).when(volumeInfo).getPoolId(); @@ -555,6 +598,7 @@ public void testInternalCanHandle_ClvmSourceManagedDestination() { Mockito.doReturn(0L).when(volumeInfo).getPoolId(); DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + Mockito.lenient().doReturn(1L).when(ds).getId(); Map volumeMap = new HashMap<>(); volumeMap.put(volumeInfo, ds); @@ -580,6 +624,7 @@ public void testInternalCanHandle_ClvmNgSourceManagedDestination() { Mockito.doReturn(0L).when(volumeInfo).getPoolId(); DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + Mockito.lenient().doReturn(1L).when(ds).getId(); Map volumeMap = new HashMap<>(); volumeMap.put(volumeInfo, ds); @@ -605,6 +650,7 @@ public void testInternalCanHandle_ClvmToClvmNg() { Mockito.doReturn(0L).when(volumeInfo).getPoolId(); DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + Mockito.lenient().doReturn(1L).when(ds).getId(); Map volumeMap = new HashMap<>(); volumeMap.put(volumeInfo, ds); @@ -633,6 +679,7 @@ public void testInternalCanHandle_ClvmNgToClvm() { Mockito.doReturn(0L).when(volumeInfo).getPoolId(); DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + Mockito.lenient().doReturn(1L).when(ds).getId(); Map volumeMap = new HashMap<>(); volumeMap.put(volumeInfo, ds); 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