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 8c9a1653a260..043bfbb22a15 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; @@ -3528,15 +3533,63 @@ 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()))) { return; } + if (DataStoreProvider.ONTAP_PLUGIN_NAME.equals(currentPool.getStorageProviderName()) + && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(targetPool.getStorageProviderName())) { + Map targetDetails = _storagePoolDao.getDetails(targetPool.getId()); + throw new CloudRuntimeException(String.format("ONTAP migration requires matching storageIP, SVM, protocol, and pool type " + + "[source: storageIP=%s, svmUUID=%s, svmName=%s, protocol=%s, type=%s; destination: storageIP=%s, svmUUID=%s, svmName=%s, protocol=%s, type=%s].", + details != null ? details.get(ONTAP_STORAGE_IP_DETAIL) : null, + details != null ? details.get(ONTAP_SVM_UUID_DETAIL) : null, + details != null ? details.get(ONTAP_SVM_NAME_DETAIL) : null, details != null ? details.get(ONTAP_PROTOCOL_DETAIL) : null, + currentPool.getPoolType(), targetDetails != null ? targetDetails.get(ONTAP_STORAGE_IP_DETAIL) : null, + targetDetails != null ? targetDetails.get(ONTAP_SVM_UUID_DETAIL) : null, + targetDetails != null ? targetDetails.get(ONTAP_SVM_NAME_DETAIL) : null, + targetDetails != null ? targetDetails.get(ONTAP_PROTOCOL_DETAIL) : null, targetPool.getPoolType())); + } throw new CloudRuntimeException(String.format("Currently, a volume on managed storage can only be 'migrated' to itself " + "[volumeId=%s, currentStoragePoolId=%s, targetStoragePoolId=%s].", 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 3b680f0bc74a..afaf1ed1d7aa 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -556,6 +556,62 @@ 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); + + CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, + () -> virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolProvided(storagePoolVoMock, volumeVoMock, targetPool)); + assertTrue(exception.getMessage().contains("protocol=ISCSI")); + assertTrue(exception.getMessage().contains("protocol=NFS3")); + } + + @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..778e63b5a4ef 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,22 @@ 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) { + 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 +330,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..2680b7c25faf 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,46 @@ 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(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 +596,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 +622,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 +648,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 +677,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/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 630d02d3c480..a0354a783e36 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; @@ -3419,8 +3420,13 @@ public Answer copyVolumeFromPrimaryToPrimary(CopyCommand cmd) { destFormat, destVolumePath, destPrimaryStore.getId(), destPrimaryStore.getUuid(), destPrimaryStore.getName(), destPrimaryStore.getPoolType())); + Map srcDetails = ObjectUtils.defaultIfNull( + ObjectUtils.firstNonNull(srcPrimaryStore.getDetails(), cmd.getOptions()), Collections.emptyMap()); + Map destDetails = ObjectUtils.defaultIfNull( + ObjectUtils.firstNonNull(destPrimaryStore.getDetails(), cmd.getOptions2()), 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)); } } @@ -3436,12 +3442,25 @@ 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")) { + destPool = storagePoolMgr.createStoragePool(destPrimaryStore.getUuid(), destPrimaryStore.getHost(), + destPrimaryStore.getPort(), destPrimaryStore.getPath(), null, destPrimaryStore.getPoolType(), + destDetails); + } else { + throw e; + } + } 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/storage/KVMStorageProcessorTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java index a85fc31224e0..1a03e19cc8f0 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 @@ -29,10 +29,14 @@ import com.cloud.storage.Storage.StoragePoolType; 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; @@ -49,6 +53,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; @@ -162,6 +167,237 @@ 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 copyVolumeFromPrimaryToPrimaryPrefersStoreDetailsOverCommandOptions() { + 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 sourceStoreDetails = Map.of("iqn", "/iqn.store/1"); + Map destinationStoreDetails = Map.of("nfsMountOpts", "vers=3"); + + Mockito.when(command.getSrcTO()).thenReturn(sourceVolume); + Mockito.when(command.getDestTO()).thenReturn(destinationVolume); + Mockito.when(command.getWaitInMillSeconds()).thenReturn(60); + Mockito.when(command.getOptions()).thenReturn(Map.of("iqn", "/iqn.command/9")); + Mockito.when(command.getOptions2()).thenReturn(Map.of("managedStoreTarget", "command-target")); + 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(sourceStoreDetails); + Mockito.when(destinationStore.getDetails()).thenReturn(destinationStoreDetails); + 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", sourceStoreDetails); + Mockito.verify(storagePoolManager).connectPhysicalDisk( + Storage.StoragePoolType.NetworkFilesystem, + "destination-pool", "destination-path", destinationStoreDetails); + Mockito.verify(storagePoolManager, Mockito.never()).createStoragePool( + Mockito.anyString(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyString(), + Mockito.nullable(String.class), Mockito.any(Storage.StoragePoolType.class), Mockito.any()); + Assert.assertTrue(answer.getResult()); + Assert.assertEquals("destination-path", answer.getNewData().getPath()); + } + @Test public void testIsEnoughSpaceForDownloadTemplateOnTemporaryLocationNotExistingLocation() { try (MockedStatic