Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
eeb7885
CSTACKEX-262: Support NetApp ONTAP Compute + storage and storage migr…
sandeeplocharla Sep 28, 2026
d821166
CSTACKEX-262: Keep offline CopyCommand on compatible ONTAP NFS and re…
sandeeplocharla Sep 28, 2026
9095897
CSTACKEX-262: Use the remapped iSCSI LUN and register the pool on the…
sandeeplocharla Sep 30, 2026
aeaf1fd
Got back the lost changes for iSCSI Offline volume case
sandeeplocharla Oct 4, 2026
7326b27
Renamed a method in driver class
sandeeplocharla Oct 5, 2026
9a744a4
CSTACKEX-262: Move ONTAP migration into the plugin and set live CPU s…
sandeeplocharla Oct 6, 2026
93b9686
Addressed PR and claude review comments
sandeeplocharla Oct 26, 2026
aa3d8eb
Changes to support migration of volumes with QoS
sandeeplocharla Oct 26, 2026
41ac927
Wait for ONTAP to report LUN copy progress before treating the copy a…
sandeeplocharla Oct 26, 2026
6853195
Report the ONTAP values that rejected a storage migration
sandeeplocharla Oct 27, 2026
1198c7d
Addressed review comments
sandeeplocharla Nov 4, 2026
c2550c2
Addressed build failure issues
sandeeplocharla Nov 4, 2026
7dae4ba
CSTACKEX-262: Address PR 112 review feedback for ONTAP KVM migration
sandeeplocharla Nov 6, 2026
b45e89c
CSTACKEX-262: Throw on failed null-callback create and retry post-mig…
sandeeplocharla Nov 7, 2026
8d65422
Addressing review comments
sandeeplocharla Nov 11, 2026
4c1e400
Removed an unnecessary provider check in driver class
sandeeplocharla Nov 13, 2026
4497817
CSTACKEX-262: Tag offline migration destination volumes explicitly
sandeeplocharla Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Comment thread
sandeeplocharla marked this conversation as resolved.
Resolved
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;
Expand Down Expand Up @@ -3528,15 +3533,63 @@ protected void executeManagedStorageChecksWhenTargetStoragePoolProvided(StorageP
if (currentPool.getId() == targetPool.getId()) {
return;
}
if (isSupportedOntapLiveStorageMigration(currentPool, targetPool)) {
Comment thread
sandeeplocharla marked this conversation as resolved.
Resolved
return;
}

Map<String, String> 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<String, String> 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) {
Comment thread
sandeeplocharla marked this conversation as resolved.
Resolved
if (!DataStoreProvider.ONTAP_PLUGIN_NAME.equals(currentPool.getStorageProviderName())
|| !DataStoreProvider.ONTAP_PLUGIN_NAME.equals(targetPool.getStorageProviderName())) {
return false;
}

Map<String, String> currentDetails = _storagePoolDao.getDetails(currentPool.getId());
Map<String, String> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
sandeeplocharla marked this conversation as resolved.
Resolved
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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -320,6 +330,20 @@ public final StrategyPriority canHandle(Map<VolumeInfo, DataStore> volumeMap, Ho
* Handles migrating volumes on managed Storage.
*/
protected StrategyPriority internalCanHandle(Map<VolumeInfo, DataStore> volumeMap, Host srcHost, Host destHost) {
// OntapDataMotionStrategy also returns HIGHEST for these and a tie keeps the first strategy.
for (VolumeInfo volumeInfo : volumeMap.keySet()) {
Comment thread
sandeeplocharla marked this conversation as resolved.
Resolved
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<VolumeInfo> volumeInfoSet = volumeMap.keySet();

for (VolumeInfo volumeInfo : volumeInfoSet) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<VolumeInfo, DataStore> 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();
Expand Down Expand Up @@ -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<VolumeInfo, DataStore> volumeMap = new HashMap<>();
volumeMap.put(volumeInfo, ds);
Expand All @@ -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<VolumeInfo, DataStore> volumeMap = new HashMap<>();
volumeMap.put(volumeInfo, ds);
Expand All @@ -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<VolumeInfo, DataStore> volumeMap = new HashMap<>();
volumeMap.put(volumeInfo, ds);
Expand Down Expand Up @@ -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<VolumeInfo, DataStore> volumeMap = new HashMap<>();
volumeMap.put(volumeInfo, ds);
Expand Down
Loading
Loading