From 531c80acab5cc3b8a4292bd7e612ad41fd2a3b87 Mon Sep 17 00:00:00 2001
From: sr73318
Date: Tue, 15 Sep 2026 10:48:11 +0530
Subject: [PATCH 1/6] CSTACKEX-261: enabling cloudstack volume resize (GROW)
for iscsi and nfs
---
.../command/user/volume/ResizeVolumeCmd.java | 6 +-
.../driver/OntapPrimaryDatastoreDriver.java | 163 ++++++--
.../storage/service/StorageStrategy.java | 19 +-
.../storage/service/UnifiedNASStrategy.java | 12 +-
.../OntapPrimaryDatastoreDriverTest.java | 394 ++++++++++++++++--
.../service/UnifiedNASStrategyTest.java | 114 ++++-
.../service/UnifiedSANStrategyTest.java | 33 +-
7 files changed, 657 insertions(+), 84 deletions(-)
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java
index f8f744285c04..60f4f308503d 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java
@@ -15,14 +15,14 @@
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.volume;
-import org.apache.cloudstack.api.BaseAsyncCmd;
-
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiArgValidator;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.ServerApiException;
@@ -60,7 +60,7 @@ public class ResizeVolumeCmd extends BaseAsyncCmd implements UserCmd {
@Parameter(name = ApiConstants.MAX_IOPS, type = CommandType.LONG, required = false, description = "New maximum number of IOPS")
private Long maxIops;
- @Parameter(name = ApiConstants.SIZE, type = CommandType.LONG, required = false, description = "New volume size in GB")
+ @Parameter(name = ApiConstants.SIZE, type = CommandType.LONG, required = false, description = "New volume size in GB",validations = {ApiArgValidator.PositiveNumber})
private Long size;
@Parameter(name = ApiConstants.SHRINK_OK, type = CommandType.BOOLEAN, required = false, description = "Verify OK to Shrink")
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
index ed942b438e16..5e4c0559ac30 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
@@ -18,31 +18,14 @@
*/
package org.apache.cloudstack.storage.driver;
-import org.apache.cloudstack.storage.utils.OntapStorageConstants;
-import com.cloud.agent.api.Answer;
-import com.cloud.agent.api.to.DataObjectType;
-import com.cloud.agent.api.to.DataStoreTO;
-import com.cloud.agent.api.to.DataTO;
-import com.cloud.exception.InvalidParameterValueException;
-import com.cloud.host.Host;
-import com.cloud.host.HostVO;
-import com.cloud.hypervisor.Hypervisor.HypervisorType;
-import com.cloud.storage.Storage;
-import com.cloud.storage.StoragePool;
-import com.cloud.storage.Volume;
-import com.cloud.storage.VolumeDetailVO;
-import com.cloud.storage.VolumeVO;
-import com.cloud.storage.ScopeType;
-import com.cloud.storage.SnapshotVO;
-import com.cloud.storage.VMTemplateStoragePoolVO;
-import com.cloud.storage.dao.SnapshotDao;
-import com.cloud.storage.dao.SnapshotDetailsDao;
-import com.cloud.storage.dao.SnapshotDetailsVO;
-import com.cloud.storage.dao.VMTemplatePoolDao;
-import com.cloud.storage.dao.VolumeDao;
-import com.cloud.storage.dao.VolumeDetailsDao;
-import com.cloud.utils.Pair;
-import com.cloud.utils.exception.CloudRuntimeException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
@@ -55,7 +38,6 @@
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
-import org.apache.commons.lang3.StringUtils;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.storage.command.CommandResult;
import org.apache.cloudstack.storage.command.CreateObjectAnswer;
@@ -78,17 +60,38 @@
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
+import org.apache.cloudstack.storage.utils.OntapStorageConstants;
import org.apache.cloudstack.storage.utils.OntapStorageUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
-import javax.inject.Inject;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import com.cloud.agent.api.Answer;
+import com.cloud.agent.api.to.DataObjectType;
+import com.cloud.agent.api.to.DataStoreTO;
+import com.cloud.agent.api.to.DataTO;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.host.Host;
+import com.cloud.host.HostVO;
+import com.cloud.hypervisor.Hypervisor.HypervisorType;
+import com.cloud.storage.ResizeVolumePayload;
+import com.cloud.storage.ScopeType;
+import com.cloud.storage.SnapshotVO;
+import com.cloud.storage.Storage;
+import com.cloud.storage.StoragePool;
+import com.cloud.storage.VMTemplateStoragePoolVO;
+import com.cloud.storage.Volume;
+import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.VolumeVO;
+import com.cloud.storage.dao.SnapshotDao;
+import com.cloud.storage.dao.SnapshotDetailsDao;
+import com.cloud.storage.dao.SnapshotDetailsVO;
+import com.cloud.storage.dao.VMTemplatePoolDao;
+import com.cloud.storage.dao.VolumeDao;
+import com.cloud.storage.dao.VolumeDetailsDao;
+import com.cloud.utils.Pair;
+import com.cloud.utils.exception.CloudRuntimeException;
/**
* Primary datastore driver for NetApp ONTAP storage systems.
@@ -596,7 +599,62 @@ public boolean canCopy(DataObject srcData, DataObject destData) {
}
@Override
- public void resize(DataObject data, AsyncCompletionCallback callback) {}
+ public void resize(DataObject data, AsyncCompletionCallback callback) {
+ CreateCmdResult result = null;
+ try {
+ VolumeInfo volumeInfo = (VolumeInfo) data;
+ ResizeVolumePayload payload = (ResizeVolumePayload) volumeInfo.getpayload();
+ if (payload == null || payload.newSize == null) {
+ throw new CloudRuntimeException("Invalid resize payload for volume " + volumeInfo.getId());
+ }
+
+ StoragePoolVO storagePool = storagePoolDao.findById(volumeInfo.getDataStore().getId());
+ if (storagePool == null) {
+ throw new CloudRuntimeException("Storage pool not found for volume " + volumeInfo.getId());
+ }
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+ VolumeVO volumeVO = volumeDao.findById(volumeInfo.getId());
+ if (volumeVO == null) {
+ throw new CloudRuntimeException("Volume not found for id " + volumeInfo.getId());
+ }
+ if (payload.newSize < volumeVO.getSize()) {
+ throw new CloudRuntimeException(String.format(
+ "Storage pool %s does not support shrinking a volume.", storagePool.getName()));
+ }
+
+ CloudStackVolume cloudStackVolume = new CloudStackVolume();
+ cloudStackVolume.setVolumeInfo(volumeInfo);
+
+ if (ProtocolType.ISCSI.name().equals(details.get(OntapStorageConstants.PROTOCOL))) {
+ VolumeDetailVO lunUuidDetail = volumeDetailsDao.findDetail(volumeInfo.getId(), OntapStorageConstants.LUN_DOT_UUID);
+ if (lunUuidDetail == null || lunUuidDetail.getValue() == null) {
+ throw new CloudRuntimeException("LUN UUID not found in volume details for volume " + volumeInfo.getId());
+ }
+ Lun lun = new Lun();
+ lun.setUuid(lunUuidDetail.getValue());
+ cloudStackVolume.setLun(lun);
+ }
+
+ // delegates to UnifiedSANStrategy (PATCH /api/storage/luns/{uuid}) for iSCSI
+ // or to UnifiedNASStrategy (ResizeVolumeCommand to KVM agent) for NFS3
+ storageStrategy.resizeCloudStackVolume(cloudStackVolume, payload.newSize);
+
+ volumeVO.setSize(payload.newSize);
+ volumeDao.update(volumeVO.getId(), volumeVO);
+
+ result = new CreateCmdResult(null, new Answer(null, true, null));
+ logger.info("resize: Successfully resized volume [{}] to [{}] bytes", volumeInfo.getId(), payload.newSize);
+ } catch (Exception e) {
+ String errMsg = e.getMessage();
+ logger.error("resize: Failed for volume [{}]: {}", data.getId(), errMsg, e);
+ result = new CreateCmdResult(null, new Answer(null, false, errMsg));
+ result.setResult(errMsg);
+ } finally {
+ callback.complete(result);
+ }
+ }
@Override
public ChapInfo getChapInfo(DataObject dataObject) {
@@ -1018,9 +1076,46 @@ private boolean isTemplateCachedOnPool(VMTemplateStoragePoolVO templatePoolRef,
return StringUtils.isNotBlank(templatePoolRef.getInstallPath());
}
+ /**
+ * Returns the bytes available on the FlexVolume backing this pool, read directly from ONTAP
+ * ({@code space.available}).
+ *
+ * Returns {@code 0} if the FlexVolume UUID is not yet recorded in pool details, or if the
+ * ONTAP REST call fails for any reason (array unreachable, auth error, etc.).
+ */
@Override
public long getUsedBytes(StoragePool storagePool) {
- return 0;
+ if (storagePool == null) {
+ return 0;
+ }
+
+ try {
+ Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+ String flexVolUuid = poolDetails != null ? poolDetails.get(OntapStorageConstants.VOLUME_UUID) : null;
+
+ if (StringUtils.isBlank(flexVolUuid)) {
+ logger.warn("getUsedBytes: No FlexVolume UUID recorded for pool [{}]; returning 0",
+ storagePool.getId());
+ return 0;
+ }
+
+ StorageStrategy strategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails);
+ var flexVol = strategy.getStorageVolume(flexVolUuid);
+
+ if (flexVol == null || flexVol.getSpace() == null) {
+ logger.warn("getUsedBytes: FlexVolume [{}] not found or has no space info for pool [{}]; returning 0",
+ flexVolUuid, storagePool.getId());
+ return 0;
+ }
+
+ logger.debug("getUsedBytes: FlexVolume [{}] backing pool [{}] reports {} bytes used",
+ flexVolUuid, storagePool.getId(), flexVol.getSpace().getUsed());
+ return flexVol.getSpace().getUsed();
+ } catch (Exception e) {
+ logger.warn("getUsedBytes: Could not read used space from ONTAP for pool [{}]; returning 0",
+ storagePool.getId(), e);
+ return 0;
+ }
}
@Override
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
index 6f79c4061419..a15cf9d52135 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
@@ -446,7 +446,24 @@ public void deleteStorageVolume(Volume volume) {
* @return the retrieved Volume object
*/
public Volume getStorageVolume(Volume volume) {
- return null;
+ return getStorageVolume(volume.getUuid());
+ }
+
+ public Volume getStorageVolume(String uuid) {
+ logger.info("getStorageVolume: Fetching ONTAP volume by UUID: {}", uuid);
+ String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ try {
+ Volume fetchedVolume = volumeFeignClient.getVolumeByUUID(authHeader, uuid);
+ logger.info("getStorageVolume: Volume [{}] fetched successfully", uuid);
+ return fetchedVolume;
+ } catch (FeignException e) {
+ if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
+ logger.warn("getStorageVolume: Volume [{}] not found in ONTAP", uuid);
+ return null;
+ }
+ logger.error("getStorageVolume: Exception while fetching volume [{}]: ", uuid, e);
+ throw new CloudRuntimeException("Failed to fetch volume: " + e.getMessage());
+ }
}
/**
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java
index 4a9f45f7301e..f9b409afa650 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java
@@ -60,6 +60,7 @@
import com.cloud.agent.api.storage.ResizeVolumeCommand;
import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.host.HostVO;
+import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.Storage;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
@@ -223,8 +224,15 @@ private Answer resizeVolumeOnKVMHost(DataObject volumeInfo, long sizeInBytes) {
throw new CloudRuntimeException("Storage Pool not found for id: " + volume.getPoolId());
}
- ResizeVolumeCommand cmd = new ResizeVolumeCommand(volume.getPath(), new StorageFilerTO(storagePool),
- volume.getSize(), sizeInBytes, false, null);
+ // instanceName is set by VolumeApiServiceImpl.orchestrateResizeVolume() before calling the
+ // driver — it is the VM instance name when attached, or "none" when the volume is detached.
+ ResizeVolumePayload resizePayload = volumeObject.getpayload() instanceof ResizeVolumePayload
+ ? (ResizeVolumePayload) volumeObject.getpayload()
+ : null;
+ String instanceName = resizePayload != null ? resizePayload.instanceName : "none";
+ ResizeVolumeCommand cmd = new ResizeVolumeCommand(volume.getPath(), new StorageFilerTO(storagePool),
+ volume.getSize(), sizeInBytes, false, instanceName);
+
EndPoint ep = epSelector.select(volumeInfo);
if (ep == null) {
String errMsg = "No remote endpoint to send ResizeVolumeCommand, check if host is up";
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
index db1806c8473e..49026361d321 100644
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
@@ -18,19 +18,9 @@
*/
package org.apache.cloudstack.storage.driver;
-import com.cloud.exception.InvalidParameterValueException;
-import com.cloud.host.Host;
-import com.cloud.host.HostVO;
-import com.cloud.hypervisor.Hypervisor;
-import com.cloud.storage.ScopeType;
-import com.cloud.storage.Storage;
-import com.cloud.storage.VMTemplateStoragePoolVO;
-import com.cloud.storage.VolumeVO;
-import com.cloud.storage.VolumeDetailVO;
-import com.cloud.storage.dao.VMTemplatePoolDao;
-import com.cloud.storage.dao.VolumeDao;
-import com.cloud.storage.dao.VolumeDetailsDao;
-import com.cloud.utils.exception.CloudRuntimeException;
+import java.util.HashMap;
+import java.util.Map;
+
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
@@ -44,6 +34,8 @@
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.model.Igroup;
import org.apache.cloudstack.storage.feign.model.Lun;
+import org.apache.cloudstack.storage.feign.model.Volume;
+import org.apache.cloudstack.storage.feign.model.VolumeSpace;
import org.apache.cloudstack.storage.service.UnifiedNASStrategy;
import org.apache.cloudstack.storage.service.UnifiedSANStrategy;
import org.apache.cloudstack.storage.service.model.AccessGroup;
@@ -51,31 +43,24 @@
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.cloudstack.storage.utils.OntapStorageConstants;
import org.apache.cloudstack.storage.utils.OntapStorageUtils;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.MockedStatic;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import static com.cloud.agent.api.to.DataObjectType.TEMPLATE;
-import static com.cloud.agent.api.to.DataObjectType.VOLUME;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
@@ -85,6 +70,24 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static com.cloud.agent.api.to.DataObjectType.TEMPLATE;
+import static com.cloud.agent.api.to.DataObjectType.VOLUME;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.host.Host;
+import com.cloud.host.HostVO;
+import com.cloud.hypervisor.Hypervisor;
+import com.cloud.storage.ScopeType;
+import com.cloud.storage.Storage;
+import com.cloud.storage.ResizeVolumePayload;
+import com.cloud.storage.VMTemplateStoragePoolVO;
+import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.VolumeVO;
+import com.cloud.storage.dao.VMTemplatePoolDao;
+import com.cloud.storage.dao.VolumeDao;
+import com.cloud.storage.dao.VolumeDetailsDao;
+import com.cloud.utils.exception.CloudRuntimeException;
@ExtendWith(MockitoExtension.class)
class OntapPrimaryDatastoreDriverTest {
@@ -1415,4 +1418,341 @@ void testCreateAsync_VolumeClonedFromTemplate_MissingSpoolRef_Fails() {
verify(sanStrategy, never()).cloneCloudStackVolume(any());
}
}
+
+ // =========================================================================
+ // resize() tests
+ // =========================================================================
+
+ private void stubResizeCommon(long currentSize, long newSize) {
+ ResizeVolumePayload payload = new ResizeVolumePayload(newSize, null, null, null, false, "i-2-VM", null, false);
+ when(volumeInfo.getDataStore()).thenReturn(dataStore);
+ when(dataStore.getId()).thenReturn(1L);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(payload);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ // storagePool.getName() is only needed for the shrink-error message path
+ lenient().when(storagePool.getName()).thenReturn("test-pool");
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getSize()).thenReturn(currentSize);
+ // volumeVO.getId() is only needed in the success path (volumeDao.update call)
+ lenient().when(volumeVO.getId()).thenReturn(100L);
+ }
+
+ @Test
+ void testResize_iSCSI_Success() {
+ long currentSize = 10737418240L; // 10 GB
+ long newSize = 21474836480L; // 20 GB
+ stubResizeCommon(currentSize, newSize);
+
+ VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+ doNothing().when(sanStrategy).resizeCloudStackVolume(any(), eq(newSize));
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+
+ // verify LUN UUID was wired into the request and ONTAP was called
+ ArgumentCaptor volumeCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(sanStrategy).resizeCloudStackVolume(volumeCaptor.capture(), eq(newSize));
+ assertEquals("lun-uuid-123", volumeCaptor.getValue().getLun().getUuid());
+
+ verify(volumeVO).setSize(newSize);
+ verify(volumeDao).update(eq(100L), any(VolumeVO.class));
+ }
+ }
+
+ @Test
+ void testResize_NFS_Success() {
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+ long currentSize = 10737418240L;
+ long newSize = 21474836480L;
+ stubResizeCommon(currentSize, newSize);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(nasStrategy);
+ doNothing().when(nasStrategy).resizeCloudStackVolume(any(), eq(newSize));
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+
+ verify(nasStrategy).resizeCloudStackVolume(any(CloudStackVolume.class), eq(newSize));
+ // NFS has no LUN – volumeDetailsDao must never be queried for a LUN UUID
+ verify(volumeDetailsDao, never()).findDetail(anyLong(), eq(OntapStorageConstants.LUN_DOT_UUID));
+ verify(volumeVO).setSize(newSize);
+ verify(volumeDao).update(eq(100L), any(VolumeVO.class));
+ }
+ }
+
+ @Test
+ void testResize_NullPayload_Fails() {
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(null);
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("Invalid resize payload"));
+ }
+
+ @Test
+ void testResize_NullNewSize_Fails() {
+ // payload.newSize is null
+ ResizeVolumePayload payload = new ResizeVolumePayload(null, null, null, null, false, "i-2-VM", null, false);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(payload);
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("Invalid resize payload"));
+ }
+
+ @Test
+ void testResize_StoragePoolNotFound_Fails() {
+ ResizeVolumePayload payload = new ResizeVolumePayload(21474836480L, null, null, null, false, "i-2-VM", null, false);
+ when(volumeInfo.getDataStore()).thenReturn(dataStore);
+ when(dataStore.getId()).thenReturn(1L);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(payload);
+ when(storagePoolDao.findById(1L)).thenReturn(null);
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("Storage pool not found"));
+ }
+
+ @Test
+ void testResize_VolumeVONotFound_Fails() {
+ long newSize = 21474836480L;
+ ResizeVolumePayload payload = new ResizeVolumePayload(newSize, null, null, null, false, "i-2-VM", null, false);
+ when(volumeInfo.getDataStore()).thenReturn(dataStore);
+ when(dataStore.getId()).thenReturn(1L);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(payload);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(null);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("Volume not found"));
+ }
+ }
+
+ @Test
+ void testResize_ShrinkAttempt_Fails() {
+ long currentSize = 21474836480L; // 20 GB
+ long newSize = 10737418240L; // 10 GB – smaller than current
+ stubResizeCommon(currentSize, newSize);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("does not support shrinking"));
+ verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ }
+ }
+
+ @Test
+ void testResize_iSCSI_LunUuidNotFound_Fails() {
+ long currentSize = 10737418240L;
+ long newSize = 21474836480L;
+ stubResizeCommon(currentSize, newSize);
+
+ // LUN UUID detail is missing
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(null);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("LUN UUID not found"));
+ verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ }
+ }
+
+ @Test
+ void testResize_iSCSI_LunUuidValueNull_Fails() {
+ long currentSize = 10737418240L;
+ long newSize = 21474836480L;
+ stubResizeCommon(currentSize, newSize);
+
+ // Detail exists but value is null
+ VolumeDetailVO blankDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, null, false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(blankDetail);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("LUN UUID not found"));
+ }
+ }
+
+ @Test
+ void testResize_StrategyThrows_Fails() {
+ long currentSize = 10737418240L;
+ long newSize = 21474836480L;
+ stubResizeCommon(currentSize, newSize);
+
+ VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+ doThrow(new com.cloud.utils.exception.CloudRuntimeException("ONTAP resize failed"))
+ .when(sanStrategy).resizeCloudStackVolume(any(), eq(newSize));
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("ONTAP resize failed"));
+ // volumeVO size must NOT be updated on failure
+ verify(volumeVO, never()).setSize(anyLong());
+ }
+ }
+
+ // =========================================================================
+ // getUsedBytes() tests
+ // =========================================================================
+
+ @Test
+ void testGetUsedBytes_NullPool_ReturnsZero() {
+ assertEquals(0L, driver.getUsedBytes(null));
+ }
+
+ @Test
+ void testGetUsedBytes_NoFlexVolUuid_ReturnsZero() {
+ // VOLUME_UUID key is absent from pool details
+ storagePoolDetails.remove(OntapStorageConstants.VOLUME_UUID);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ assertEquals(0L, driver.getUsedBytes(storagePool));
+ }
+
+ @Test
+ void testGetUsedBytes_BlankFlexVolUuid_ReturnsZero() {
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, " ");
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ assertEquals(0L, driver.getUsedBytes(storagePool));
+ }
+
+ @Test
+ void testGetUsedBytes_Success_ReturnsUsedBytes() {
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flexvol-uuid-123");
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ Volume flexVol = new Volume();
+ VolumeSpace space = new VolumeSpace();
+ space.setUsed(10737418240L); // 10 GB
+ flexVol.setSpace(space);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.getStorageVolume("flexvol-uuid-123")).thenReturn(flexVol);
+
+ assertEquals(10737418240L, driver.getUsedBytes(storagePool));
+ }
+ }
+
+ @Test
+ void testGetUsedBytes_FlexVolNotFound_ReturnsZero() {
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flexvol-uuid-123");
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.getStorageVolume("flexvol-uuid-123")).thenReturn(null);
+
+ assertEquals(0L, driver.getUsedBytes(storagePool));
+ }
+ }
+
+ @Test
+ void testGetUsedBytes_FlexVolNullSpace_ReturnsZero() {
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flexvol-uuid-123");
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ Volume flexVol = new Volume();
+ // space is intentionally left null
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.getStorageVolume("flexvol-uuid-123")).thenReturn(flexVol);
+
+ assertEquals(0L, driver.getUsedBytes(storagePool));
+ }
+ }
+
+ @Test
+ void testGetUsedBytes_OntapException_ReturnsZero() {
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flexvol-uuid-123");
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.getStorageVolume("flexvol-uuid-123"))
+ .thenThrow(new com.cloud.utils.exception.CloudRuntimeException("ONTAP unreachable"));
+
+ assertEquals(0L, driver.getUsedBytes(storagePool));
+ }
+ }
}
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
index dd90363af045..c79f3640e71c 100755
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
@@ -22,6 +22,7 @@
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.storage.ResizeVolumeCommand;
import com.cloud.host.HostVO;
+import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.utils.exception.CloudRuntimeException;
@@ -1028,9 +1029,12 @@ public void testResizeCloudStackVolume_SendsResizeCommand() {
VolumeVO volumeVO = mock(VolumeVO.class);
StoragePoolVO storagePool = mock(StoragePoolVO.class);
EndPoint endPoint = mock(EndPoint.class);
+ ResizeVolumePayload payload = new ResizeVolumePayload(
+ 21474836480L, null, null, null, false, "i-2-VM", null, false);
when(volumeObject.getId()).thenReturn(100L);
when(volumeObject.getUuid()).thenReturn("volume-uuid");
+ when(volumeObject.getpayload()).thenReturn(payload);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
when(volumeVO.getPath()).thenReturn("volume-uuid");
when(volumeVO.getSize()).thenReturn(5368709120L);
@@ -1044,7 +1048,74 @@ public void testResizeCloudStackVolume_SendsResizeCommand() {
strategy.resizeCloudStackVolume(request, 21474836480L);
- verify(endPoint).sendMessage(any(ResizeVolumeCommand.class));
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(ResizeVolumeCommand.class);
+ verify(endPoint).sendMessage(commandCaptor.capture());
+ ResizeVolumeCommand command = commandCaptor.getValue();
+ assertEquals("volume-uuid", command.getPath());
+ assertEquals(5368709120L, command.getCurrentSize());
+ assertEquals(21474836480L, command.getNewSize());
+ assertEquals("i-2-VM", command.getInstanceName());
+ }
+
+ @Test
+ public void testResizeCloudStackVolume_WithoutPayloadUsesDetachedInstanceName() {
+ VolumeObject volumeObject = mock(VolumeObject.class);
+ VolumeVO volumeVO = mock(VolumeVO.class);
+ StoragePoolVO storagePool = mock(StoragePoolVO.class);
+ EndPoint endPoint = mock(EndPoint.class);
+
+ when(volumeObject.getId()).thenReturn(100L);
+ when(volumeObject.getUuid()).thenReturn("volume-uuid");
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getPath()).thenReturn("volume-uuid");
+ when(volumeVO.getSize()).thenReturn(5368709120L);
+ when(volumeVO.getPoolId()).thenReturn(1L);
+ when(primaryDataStoreDao.findById(1L)).thenReturn(storagePool);
+ when(epSelector.select(volumeObject)).thenReturn(endPoint);
+ when(endPoint.sendMessage(any(ResizeVolumeCommand.class))).thenReturn(new Answer(null, true, "Success"));
+
+ CloudStackVolume request = new CloudStackVolume();
+ request.setVolumeInfo(volumeObject);
+
+ strategy.resizeCloudStackVolume(request, 21474836480L);
+
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(ResizeVolumeCommand.class);
+ verify(endPoint).sendMessage(commandCaptor.capture());
+ assertEquals("none", commandCaptor.getValue().getInstanceName());
+ }
+
+ @Test
+ public void testResizeCloudStackVolume_AgentFailureThrowsException() {
+ VolumeObject volumeObject = mock(VolumeObject.class);
+ VolumeVO volumeVO = mock(VolumeVO.class);
+ StoragePoolVO storagePool = mock(StoragePoolVO.class);
+ EndPoint endPoint = mock(EndPoint.class);
+
+ when(volumeObject.getId()).thenReturn(100L);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getPoolId()).thenReturn(1L);
+ when(primaryDataStoreDao.findById(1L)).thenReturn(storagePool);
+ when(epSelector.select(volumeObject)).thenReturn(endPoint);
+ when(endPoint.sendMessage(any(ResizeVolumeCommand.class)))
+ .thenReturn(new Answer(null, false, "qemu-img resize failed"));
+
+ CloudStackVolume request = new CloudStackVolume();
+ request.setVolumeInfo(volumeObject);
+
+ CloudRuntimeException exception = assertThrows(CloudRuntimeException.class,
+ () -> strategy.resizeCloudStackVolume(request, 21474836480L));
+ assertEquals("qemu-img resize failed", exception.getMessage());
+ }
+
+ @Test
+ public void testResizeCloudStackVolume_InvalidSizeDoesNotSendCommand() {
+ VolumeObject volumeObject = mock(VolumeObject.class);
+ CloudStackVolume request = new CloudStackVolume();
+ request.setVolumeInfo(volumeObject);
+
+ assertThrows(CloudRuntimeException.class, () -> strategy.resizeCloudStackVolume(request, 0L));
+
+ verify(epSelector, never()).select(any(org.apache.cloudstack.engine.subsystem.api.storage.DataObject.class));
}
@Test
@@ -1133,6 +1204,47 @@ public void testResizeCloudStackVolume_InvalidRequest_Throws() {
assertThrows(CloudRuntimeException.class, () -> strategy.resizeCloudStackVolume(withVol, 0L));
}
+ // =========================================================================
+ // getStorageVolume(String uuid) tests – exercising the new overload added
+ // to StorageStrategy that fetches a FlexVolume by UUID from ONTAP REST API
+ // =========================================================================
+
+ @Test
+ public void testGetStorageVolume_ByUuid_Success() {
+ org.apache.cloudstack.storage.feign.model.Volume expected =
+ new org.apache.cloudstack.storage.feign.model.Volume();
+ expected.setUuid("flexvol-uuid-123");
+
+ when(volumeFeignClient.getVolumeByUUID(anyString(), eq("flexvol-uuid-123"))).thenReturn(expected);
+
+ org.apache.cloudstack.storage.feign.model.Volume result = strategy.getStorageVolume("flexvol-uuid-123");
+
+ assertNotNull(result);
+ assertEquals("flexvol-uuid-123", result.getUuid());
+ verify(volumeFeignClient).getVolumeByUUID(anyString(), eq("flexvol-uuid-123"));
+ }
+
+ @Test
+ public void testGetStorageVolume_ByUuid_NotFound_ReturnsNull() {
+ FeignException notFound = mock(FeignException.class);
+ when(notFound.status()).thenReturn(404);
+ doThrow(notFound).when(volumeFeignClient).getVolumeByUUID(anyString(), eq("missing-uuid"));
+
+ org.apache.cloudstack.storage.feign.model.Volume result = strategy.getStorageVolume("missing-uuid");
+
+ assertNull(result);
+ }
+
+ @Test
+ public void testGetStorageVolume_ByUuid_ServerError_Throws() {
+ FeignException serverError = mock(FeignException.class);
+ when(serverError.status()).thenReturn(500);
+ when(serverError.getMessage()).thenReturn("Internal Server Error");
+ doThrow(serverError).when(volumeFeignClient).getVolumeByUUID(anyString(), eq("flexvol-uuid-999"));
+
+ assertThrows(CloudRuntimeException.class, () -> strategy.getStorageVolume("flexvol-uuid-999"));
+ }
+
@Test
public void testResizeCloudStackVolume_KvmHostFails_Throws() {
VolumeObject volumeObject = mock(VolumeObject.class);
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java
index 700b63d15575..43043cdad0e1 100644
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java
@@ -18,9 +18,11 @@
*/
package org.apache.cloudstack.storage.service;
-import com.cloud.host.HostVO;
-import com.cloud.utils.exception.CloudRuntimeException;
-import feign.FeignException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.Scope;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
@@ -36,19 +38,6 @@
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.cloudstack.storage.utils.OntapStorageConstants;
import org.apache.cloudstack.storage.utils.OntapStorageUtils;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockedStatic;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -56,10 +45,16 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.lenient;
@@ -68,6 +63,12 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.cloud.host.HostVO;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+import feign.FeignException;
@ExtendWith(MockitoExtension.class)
class UnifiedSANStrategyTest {
From 91847be70dc8fc37b8fe1067a7cbefab931e12d7 Mon Sep 17 00:00:00 2001
From: sr73318
Date: Wed, 23 Sep 2026 11:11:17 +0530
Subject: [PATCH 2/6] CSTACKEX-261: RESOLVING COMMENTS
---
.../driver/OntapPrimaryDatastoreDriver.java | 22 +++++++++----------
.../storage/service/StorageStrategy.java | 3 +++
.../storage/service/UnifiedSANStrategy.java | 19 +++++++++++++++-
3 files changed, 31 insertions(+), 13 deletions(-)
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
index 5e4c0559ac30..8ee1f681a518 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
@@ -602,8 +602,14 @@ public boolean canCopy(DataObject srcData, DataObject destData) {
public void resize(DataObject data, AsyncCompletionCallback callback) {
CreateCmdResult result = null;
try {
+ if (!(data instanceof VolumeInfo)) {
+ throw new CloudRuntimeException("resize: Expected VolumeInfo but received " +
+ (data != null ? data.getClass().getSimpleName() : "null"));
+ }
VolumeInfo volumeInfo = (VolumeInfo) data;
- ResizeVolumePayload payload = (ResizeVolumePayload) volumeInfo.getpayload();
+ Object rawPayload = volumeInfo.getpayload();
+ ResizeVolumePayload payload = (rawPayload instanceof ResizeVolumePayload)
+ ? (ResizeVolumePayload) rawPayload : null;
if (payload == null || payload.newSize == null) {
throw new CloudRuntimeException("Invalid resize payload for volume " + volumeInfo.getId());
}
@@ -626,19 +632,11 @@ public void resize(DataObject data, AsyncCompletionCallback cal
CloudStackVolume cloudStackVolume = new CloudStackVolume();
cloudStackVolume.setVolumeInfo(volumeInfo);
-
- if (ProtocolType.ISCSI.name().equals(details.get(OntapStorageConstants.PROTOCOL))) {
- VolumeDetailVO lunUuidDetail = volumeDetailsDao.findDetail(volumeInfo.getId(), OntapStorageConstants.LUN_DOT_UUID);
- if (lunUuidDetail == null || lunUuidDetail.getValue() == null) {
- throw new CloudRuntimeException("LUN UUID not found in volume details for volume " + volumeInfo.getId());
- }
- Lun lun = new Lun();
- lun.setUuid(lunUuidDetail.getValue());
- cloudStackVolume.setLun(lun);
- }
+
// delegates to UnifiedSANStrategy (PATCH /api/storage/luns/{uuid}) for iSCSI
- // or to UnifiedNASStrategy (ResizeVolumeCommand to KVM agent) for NFS3
+ // or to UnifiedNASStrategy (ResizeVolumeCommand to KVM agent) for NFS3;
+ // protocol-specific setup (e.g. LUN UUID lookup) is handled inside each strategy
storageStrategy.resizeCloudStackVolume(cloudStackVolume, payload.newSize);
volumeVO.setSize(payload.newSize);
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
index a15cf9d52135..bfa812065824 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
@@ -450,6 +450,9 @@ public Volume getStorageVolume(Volume volume) {
}
public Volume getStorageVolume(String uuid) {
+ if (uuid == null || uuid.isBlank()) {
+ throw new CloudRuntimeException("Cannot fetch ONTAP volume: UUID is null or empty");
+ }
logger.info("getStorageVolume: Fetching ONTAP volume by UUID: {}", uuid);
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
try {
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
index b9e32b081e4d..2ff7a2edaa4a 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
@@ -20,6 +20,8 @@
package org.apache.cloudstack.storage.service;
import com.cloud.host.HostVO;
+import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.dao.VolumeDetailsDao;
import com.cloud.utils.exception.CloudRuntimeException;
import feign.FeignException;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
@@ -53,6 +55,8 @@ public class UnifiedSANStrategy extends SANStrategy {
private static final Logger logger = LogManager.getLogger(UnifiedSANStrategy.class);
@Inject
private StoragePoolDetailsDao storagePoolDetailsDao;
+ @Inject
+ private VolumeDetailsDao volumeDetailsDao;
public UnifiedSANStrategy(OntapStorage ontapStorage) {
super(ontapStorage);
@@ -280,13 +284,26 @@ private void validateCreatedLun(Lun lun, String requestName, String operation) {
*/
@Override
public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes) {
- if (cloudstackVolume == null || cloudstackVolume.getLun() == null || cloudstackVolume.getLun().getUuid() == null) {
+ if (cloudstackVolume == null || cloudstackVolume.getVolumeInfo() == null) {
logger.error("resizeCloudStackVolume: Lun resize failed. Invalid request: {}", cloudstackVolume);
throw new CloudRuntimeException("Failed to resize Lun, invalid request");
}
if (sizeInBytes <= 0) {
throw new CloudRuntimeException("Failed to resize Lun, invalid size " + sizeInBytes);
}
+
+ // Resolve LUN UUID from volume details when not pre-populated on the cloudstackVolume
+ if (cloudstackVolume.getLun() == null || cloudstackVolume.getLun().getUuid() == null) {
+ long volumeId = cloudstackVolume.getVolumeInfo().getId();
+ VolumeDetailVO lunUuidDetail = volumeDetailsDao.findDetail(volumeId, OntapStorageConstants.LUN_DOT_UUID);
+ if (lunUuidDetail == null || lunUuidDetail.getValue() == null) {
+ throw new CloudRuntimeException("LUN UUID not found in volume details for volume " + volumeId);
+ }
+ Lun resolvedLun = new Lun();
+ resolvedLun.setUuid(lunUuidDetail.getValue());
+ cloudstackVolume.setLun(resolvedLun);
+ }
+
String lunUuid = cloudstackVolume.getLun().getUuid();
logger.trace("resizeCloudStackVolume: Resizing Lun {} to {} bytes", lunUuid, sizeInBytes);
try {
From 3b2be0c24cb5d654c587b32692e726b5ae5464b5 Mon Sep 17 00:00:00 2001
From: sr73318
Date: Wed, 23 Sep 2026 11:33:05 +0530
Subject: [PATCH 3/6] CSTACKEX-261: RESOLVING COMMENTS - PT2
---
.../storage/driver/OntapPrimaryDatastoreDriver.java | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
index 8ee1f681a518..04d375e3e20c 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
@@ -68,9 +68,11 @@
import org.jetbrains.annotations.Nullable;
import com.cloud.agent.api.Answer;
+import com.cloud.agent.api.storage.ResizeVolumeCommand;
import com.cloud.agent.api.to.DataObjectType;
import com.cloud.agent.api.to.DataStoreTO;
import com.cloud.agent.api.to.DataTO;
+import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
@@ -633,7 +635,6 @@ public void resize(DataObject data, AsyncCompletionCallback cal
CloudStackVolume cloudStackVolume = new CloudStackVolume();
cloudStackVolume.setVolumeInfo(volumeInfo);
-
// delegates to UnifiedSANStrategy (PATCH /api/storage/luns/{uuid}) for iSCSI
// or to UnifiedNASStrategy (ResizeVolumeCommand to KVM agent) for NFS3;
// protocol-specific setup (e.g. LUN UUID lookup) is handled inside each strategy
@@ -641,13 +642,16 @@ public void resize(DataObject data, AsyncCompletionCallback cal
volumeVO.setSize(payload.newSize);
volumeDao.update(volumeVO.getId(), volumeVO);
+ String instanceName = payload.instanceName != null ? payload.instanceName : "none";
- result = new CreateCmdResult(null, new Answer(null, true, null));
+ ResizeVolumeCommand resizeCmd = new ResizeVolumeCommand(volumeVO.getPath(),
+ new StorageFilerTO(storagePool), volumeVO.getSize(), payload.newSize,
+ false, instanceName);
+ result = new CreateCmdResult(volumeVO.getPath(), new Answer(resizeCmd, true, null));
logger.info("resize: Successfully resized volume [{}] to [{}] bytes", volumeInfo.getId(), payload.newSize);
} catch (Exception e) {
String errMsg = e.getMessage();
logger.error("resize: Failed for volume [{}]: {}", data.getId(), errMsg, e);
- result = new CreateCmdResult(null, new Answer(null, false, errMsg));
result.setResult(errMsg);
} finally {
callback.complete(result);
From 067268d488cf80c7c70cd5d639694ca8317002f4 Mon Sep 17 00:00:00 2001
From: sr73318
Date: Wed, 23 Sep 2026 12:16:23 +0530
Subject: [PATCH 4/6] CSTACKEX-261: resolving comments and build issues
---
.../driver/OntapPrimaryDatastoreDriver.java | 24 ++---
.../OntapPrimaryDatastoreDriverTest.java | 92 ++++++-------------
2 files changed, 42 insertions(+), 74 deletions(-)
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
index 04d375e3e20c..a9a0b74931c4 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
@@ -634,7 +634,7 @@ public void resize(DataObject data, AsyncCompletionCallback cal
CloudStackVolume cloudStackVolume = new CloudStackVolume();
cloudStackVolume.setVolumeInfo(volumeInfo);
-
+
// delegates to UnifiedSANStrategy (PATCH /api/storage/luns/{uuid}) for iSCSI
// or to UnifiedNASStrategy (ResizeVolumeCommand to KVM agent) for NFS3;
// protocol-specific setup (e.g. LUN UUID lookup) is handled inside each strategy
@@ -652,6 +652,7 @@ public void resize(DataObject data, AsyncCompletionCallback cal
} catch (Exception e) {
String errMsg = e.getMessage();
logger.error("resize: Failed for volume [{}]: {}", data.getId(), errMsg, e);
+ result = new CreateCmdResult(null, new Answer(null, false, errMsg));
result.setResult(errMsg);
} finally {
callback.complete(result);
@@ -1082,8 +1083,11 @@ private boolean isTemplateCachedOnPool(VMTemplateStoragePoolVO templatePoolRef,
* Returns the bytes available on the FlexVolume backing this pool, read directly from ONTAP
* ({@code space.available}).
*
- * Returns {@code 0} if the FlexVolume UUID is not yet recorded in pool details, or if the
- * ONTAP REST call fails for any reason (array unreachable, auth error, etc.).
+ * Returns {@code 0} if the ONTAP REST call fails for any reason (array unreachable, auth
+ * error, etc.). Throws if the FlexVolume UUID is not recorded in pool details, since that
+ * indicates the pool was never fully provisioned.
+ *
+ * @throws CloudRuntimeException if the pool has no FlexVolume UUID in its details
*/
@Override
public long getUsedBytes(StoragePool storagePool) {
@@ -1091,16 +1095,14 @@ public long getUsedBytes(StoragePool storagePool) {
return 0;
}
- try {
- Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
- String flexVolUuid = poolDetails != null ? poolDetails.get(OntapStorageConstants.VOLUME_UUID) : null;
+ Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+ String flexVolUuid = poolDetails != null ? poolDetails.get(OntapStorageConstants.VOLUME_UUID) : null;
- if (StringUtils.isBlank(flexVolUuid)) {
- logger.warn("getUsedBytes: No FlexVolume UUID recorded for pool [{}]; returning 0",
- storagePool.getId());
- return 0;
- }
+ if (StringUtils.isBlank(flexVolUuid)) {
+ throw new CloudRuntimeException("FlexVolume UUID not found in pool details for pool " + storagePool.getId());
+ }
+ try {
StorageStrategy strategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails);
var flexVol = strategy.getStorageVolume(flexVolUuid);
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
index 49026361d321..f1bb8eb7679a 100644
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
@@ -78,9 +78,9 @@
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor;
+import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.ScopeType;
import com.cloud.storage.Storage;
-import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.VolumeDetailVO;
import com.cloud.storage.VolumeVO;
@@ -1438,6 +1438,7 @@ private void stubResizeCommon(long currentSize, long newSize) {
when(volumeVO.getSize()).thenReturn(currentSize);
// volumeVO.getId() is only needed in the success path (volumeDao.update call)
lenient().when(volumeVO.getId()).thenReturn(100L);
+ lenient().when(volumeVO.getPath()).thenReturn("/vol/vol1/lun1");
}
@Test
@@ -1446,9 +1447,6 @@ void testResize_iSCSI_Success() {
long newSize = 21474836480L; // 20 GB
stubResizeCommon(currentSize, newSize);
- VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
-
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
.thenReturn(sanStrategy);
@@ -1460,10 +1458,12 @@ void testResize_iSCSI_Success() {
verify(createCallback).complete(resultCaptor.capture());
assertTrue(resultCaptor.getValue().isSuccess());
- // verify LUN UUID was wired into the request and ONTAP was called
+ // Driver only wires VolumeInfo; LUN UUID lookup is inside UnifiedSANStrategy
ArgumentCaptor volumeCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
verify(sanStrategy).resizeCloudStackVolume(volumeCaptor.capture(), eq(newSize));
- assertEquals("lun-uuid-123", volumeCaptor.getValue().getLun().getUuid());
+ assertEquals(volumeInfo, volumeCaptor.getValue().getVolumeInfo());
+ assertNull(volumeCaptor.getValue().getLun());
+ verify(volumeDetailsDao, never()).findDetail(anyLong(), eq(OntapStorageConstants.LUN_DOT_UUID));
verify(volumeVO).setSize(newSize);
verify(volumeDao).update(eq(100L), any(VolumeVO.class));
@@ -1488,8 +1488,10 @@ void testResize_NFS_Success() {
verify(createCallback).complete(resultCaptor.capture());
assertTrue(resultCaptor.getValue().isSuccess());
- verify(nasStrategy).resizeCloudStackVolume(any(CloudStackVolume.class), eq(newSize));
- // NFS has no LUN – volumeDetailsDao must never be queried for a LUN UUID
+ ArgumentCaptor volumeCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(nasStrategy).resizeCloudStackVolume(volumeCaptor.capture(), eq(newSize));
+ assertEquals(volumeInfo, volumeCaptor.getValue().getVolumeInfo());
+ // NFS has no LUN – driver must not look up a LUN UUID
verify(volumeDetailsDao, never()).findDetail(anyLong(), eq(OntapStorageConstants.LUN_DOT_UUID));
verify(volumeVO).setSize(newSize);
verify(volumeDao).update(eq(100L), any(VolumeVO.class));
@@ -1587,61 +1589,12 @@ void testResize_ShrinkAttempt_Fails() {
}
}
- @Test
- void testResize_iSCSI_LunUuidNotFound_Fails() {
- long currentSize = 10737418240L;
- long newSize = 21474836480L;
- stubResizeCommon(currentSize, newSize);
-
- // LUN UUID detail is missing
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(null);
-
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
- .thenReturn(sanStrategy);
-
- driver.resize(volumeInfo, createCallback);
-
- ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
- verify(createCallback).complete(resultCaptor.capture());
- assertFalse(resultCaptor.getValue().isSuccess());
- assertTrue(resultCaptor.getValue().getResult().contains("LUN UUID not found"));
- verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
- }
- }
-
- @Test
- void testResize_iSCSI_LunUuidValueNull_Fails() {
- long currentSize = 10737418240L;
- long newSize = 21474836480L;
- stubResizeCommon(currentSize, newSize);
-
- // Detail exists but value is null
- VolumeDetailVO blankDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, null, false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(blankDetail);
-
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
- .thenReturn(sanStrategy);
-
- driver.resize(volumeInfo, createCallback);
-
- ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
- verify(createCallback).complete(resultCaptor.capture());
- assertFalse(resultCaptor.getValue().isSuccess());
- assertTrue(resultCaptor.getValue().getResult().contains("LUN UUID not found"));
- }
- }
-
@Test
void testResize_StrategyThrows_Fails() {
long currentSize = 10737418240L;
long newSize = 21474836480L;
stubResizeCommon(currentSize, newSize);
- VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
-
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
.thenReturn(sanStrategy);
@@ -1664,27 +1617,40 @@ void testResize_StrategyThrows_Fails() {
// =========================================================================
@Test
- void testGetUsedBytes_NullPool_ReturnsZero() {
- assertEquals(0L, driver.getUsedBytes(null));
+ void testGetUsedBytes_NullPool_ThrowsException() {
+ InvalidParameterValueException ex = assertThrows(InvalidParameterValueException.class,
+ () -> driver.getUsedBytes(null));
+ assertTrue(ex.getMessage().contains("storagePool should not be null"));
}
@Test
- void testGetUsedBytes_NoFlexVolUuid_ReturnsZero() {
+ void testGetUsedBytes_NoFlexVolUuid_ThrowsException() {
// VOLUME_UUID key is absent from pool details
storagePoolDetails.remove(OntapStorageConstants.VOLUME_UUID);
when(storagePool.getId()).thenReturn(1L);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- assertEquals(0L, driver.getUsedBytes(storagePool));
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> driver.getUsedBytes(storagePool));
+ assertTrue(ex.getMessage().contains("FlexVolume UUID not found in pool details for pool 1"));
}
@Test
- void testGetUsedBytes_BlankFlexVolUuid_ReturnsZero() {
+ void testGetUsedBytes_BlankFlexVolUuid_ThrowsException() {
storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, " ");
when(storagePool.getId()).thenReturn(1L);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- assertEquals(0L, driver.getUsedBytes(storagePool));
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> driver.getUsedBytes(storagePool));
+ assertTrue(ex.getMessage().contains("FlexVolume UUID not found in pool details for pool 1"));
+ }
+
+ @Test
+ void testGetUsedBytes_NullPoolDetails_ThrowsException() {
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(null);
+
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> driver.getUsedBytes(storagePool));
+ assertTrue(ex.getMessage().contains("FlexVolume UUID not found in pool details for pool 1"));
}
@Test
From c9f59ea85ee4fa63164984a58499d9abc6c65e82 Mon Sep 17 00:00:00 2001
From: sr73318
Date: Wed, 23 Sep 2026 14:15:12 +0530
Subject: [PATCH 5/6] CSTACKEX-261: Adding more UT's
---
.../driver/OntapPrimaryDatastoreDriver.java | 3 +-
.../OntapPrimaryDatastoreDriverTest.java | 2 +-
.../service/UnifiedSANStrategyTest.java | 48 +++++++++++++++++++
3 files changed, 51 insertions(+), 2 deletions(-)
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
index a9a0b74931c4..61d2d1f7893c 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java
@@ -1087,12 +1087,13 @@ private boolean isTemplateCachedOnPool(VMTemplateStoragePoolVO templatePoolRef,
* error, etc.). Throws if the FlexVolume UUID is not recorded in pool details, since that
* indicates the pool was never fully provisioned.
*
+ * @throws InvalidParameterValueException if {@code storagePool} is null
* @throws CloudRuntimeException if the pool has no FlexVolume UUID in its details
*/
@Override
public long getUsedBytes(StoragePool storagePool) {
if (storagePool == null) {
- return 0;
+ throw new InvalidParameterValueException("storagePool is null, ensure the pool exists and is fully initialised before querying used bytes");
}
Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
index f1bb8eb7679a..e17eaa947260 100644
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java
@@ -1620,7 +1620,7 @@ void testResize_StrategyThrows_Fails() {
void testGetUsedBytes_NullPool_ThrowsException() {
InvalidParameterValueException ex = assertThrows(InvalidParameterValueException.class,
() -> driver.getUsedBytes(null));
- assertTrue(ex.getMessage().contains("storagePool should not be null"));
+ assertTrue(ex.getMessage().contains("storagePool is null, ensure the pool exists and is fully initialised before querying used bytes"));
}
@Test
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java
index 43043cdad0e1..fb518d926507 100644
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java
@@ -25,6 +25,7 @@
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.Scope;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.feign.client.SANFeignClient;
import org.apache.cloudstack.storage.feign.model.Igroup;
@@ -66,6 +67,8 @@
import org.mockito.junit.jupiter.MockitoExtension;
import com.cloud.host.HostVO;
+import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.dao.VolumeDetailsDao;
import com.cloud.utils.exception.CloudRuntimeException;
import feign.FeignException;
@@ -88,6 +91,9 @@ class UnifiedSANStrategyTest {
@Mock
private StoragePoolDetailsDao storagePoolDetailsDao;
+ @Mock
+ private VolumeDetailsDao volumeDetailsDao;
+
private UnifiedSANStrategy unifiedSANStrategy;
private String authHeader;
@@ -115,6 +121,11 @@ void setUp() {
java.lang.reflect.Field storagePoolDetailsDaoField = UnifiedSANStrategy.class.getDeclaredField("storagePoolDetailsDao");
storagePoolDetailsDaoField.setAccessible(true);
storagePoolDetailsDaoField.set(unifiedSANStrategy, storagePoolDetailsDao);
+
+ // Inject volumeDetailsDao, used to resolve the LUN UUID during resize
+ java.lang.reflect.Field volumeDetailsDaoField = UnifiedSANStrategy.class.getDeclaredField("volumeDetailsDao");
+ volumeDetailsDaoField.setAccessible(true);
+ volumeDetailsDaoField.set(unifiedSANStrategy, volumeDetailsDao);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -1013,6 +1024,7 @@ void testResizeCloudStackVolume_ValidRequest_PatchesSize() {
lun.setUuid("lun-uuid-123");
CloudStackVolume request = new CloudStackVolume();
request.setLun(lun);
+ request.setVolumeInfo(mock(VolumeInfo.class));
unifiedSANStrategy.resizeCloudStackVolume(request, 21474836480L);
@@ -1023,19 +1035,54 @@ void testResizeCloudStackVolume_ValidRequest_PatchesSize() {
@Test
void testResizeCloudStackVolume_NoUuid_Throws() {
+ VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ when(volumeInfo.getId()).thenReturn(100L);
CloudStackVolume request = new CloudStackVolume();
request.setLun(new Lun());
+ request.setVolumeInfo(volumeInfo);
+
+ // LUN UUID is absent on the request and cannot be resolved from volume details
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(null);
assertThrows(CloudRuntimeException.class, () -> unifiedSANStrategy.resizeCloudStackVolume(request, 100L));
verify(sanFeignClient, never()).updateLun(any(), any(), any());
}
+ @Test
+ void testResizeCloudStackVolume_NoVolumeInfo_Throws() {
+ Lun lun = new Lun();
+ lun.setUuid("lun-uuid-123");
+ CloudStackVolume request = new CloudStackVolume();
+ request.setLun(lun);
+
+ assertThrows(CloudRuntimeException.class, () -> unifiedSANStrategy.resizeCloudStackVolume(request, 100L));
+ verify(sanFeignClient, never()).updateLun(any(), any(), any());
+ }
+
+ @Test
+ void testResizeCloudStackVolume_UuidResolvedFromVolumeDetails() {
+ VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ when(volumeInfo.getId()).thenReturn(100L);
+ CloudStackVolume request = new CloudStackVolume();
+ request.setVolumeInfo(volumeInfo);
+
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false));
+
+ unifiedSANStrategy.resizeCloudStackVolume(request, 21474836480L);
+
+ ArgumentCaptor lunCaptor = ArgumentCaptor.forClass(Lun.class);
+ verify(sanFeignClient).updateLun(any(), eq("lun-uuid-123"), lunCaptor.capture());
+ assertEquals(21474836480L, lunCaptor.getValue().getSpace().getSize());
+ }
+
@Test
void testResizeCloudStackVolume_InvalidSize_Throws() {
Lun lun = new Lun();
lun.setUuid("lun-uuid-123");
CloudStackVolume request = new CloudStackVolume();
request.setLun(lun);
+ request.setVolumeInfo(mock(VolumeInfo.class));
assertThrows(CloudRuntimeException.class, () -> unifiedSANStrategy.resizeCloudStackVolume(request, 0L));
assertThrows(CloudRuntimeException.class, () -> unifiedSANStrategy.resizeCloudStackVolume(null, 100L));
@@ -1048,6 +1095,7 @@ void testResizeCloudStackVolume_FeignException_Throws() {
lun.setUuid("lun-uuid-123");
CloudStackVolume request = new CloudStackVolume();
request.setLun(lun);
+ request.setVolumeInfo(mock(VolumeInfo.class));
FeignException feignException = mock(FeignException.class);
when(feignException.status()).thenReturn(500);
From 82c7d156a61ee18c1184c915717aaf5be2d9b156 Mon Sep 17 00:00:00 2001
From: sr73318
Date: Wed, 23 Sep 2026 15:31:31 +0530
Subject: [PATCH 6/6] CSTACKEX-261: handling casee where lun is deleted on
ontap and trying to resize
---
.../storage/service/UnifiedSANStrategy.java | 43 ++++++++++++-------
1 file changed, 27 insertions(+), 16 deletions(-)
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
index 2ff7a2edaa4a..f4b3ce7e6a2a 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
@@ -19,22 +19,23 @@
package org.apache.cloudstack.storage.service;
-import com.cloud.host.HostVO;
-import com.cloud.storage.VolumeDetailVO;
-import com.cloud.storage.dao.VolumeDetailsDao;
-import com.cloud.utils.exception.CloudRuntimeException;
-import feign.FeignException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
import org.apache.cloudstack.storage.feign.model.Igroup;
import org.apache.cloudstack.storage.feign.model.Initiator;
-import org.apache.cloudstack.storage.feign.model.Svm;
-import org.apache.cloudstack.storage.feign.model.OntapStorage;
import org.apache.cloudstack.storage.feign.model.Lun;
import org.apache.cloudstack.storage.feign.model.LunMap;
import org.apache.cloudstack.storage.feign.model.LunSpace;
-import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
+import org.apache.cloudstack.storage.feign.model.OntapStorage;
+import org.apache.cloudstack.storage.feign.model.Svm;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.service.model.AccessGroup;
@@ -45,10 +46,13 @@
import org.apache.commons.collections.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import javax.inject.Inject;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+
+import com.cloud.host.HostVO;
+import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.dao.VolumeDetailsDao;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+import feign.FeignException;
public class UnifiedSANStrategy extends SANStrategy {
@@ -315,12 +319,19 @@ public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeI
sanFeignClient.updateLun(authHeader, lunUuid, patch);
logger.debug("resizeCloudStackVolume: Lun {} resized to {} bytes", lunUuid, sizeInBytes);
} catch (FeignException e) {
- logger.error("FeignException occurred while resizing LUN: {}, Status: {}, Exception: {}",
+ logger.error("FeignException occurred while resizing LUN [{}], Status: {}, Exception: {}",
lunUuid, e.status(), e.getMessage());
- throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage());
+ if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
+ throw new CloudRuntimeException(String.format(
+ "LUN [%s] no longer exists on ONTAP; it may have been deleted externally. " +
+ "Verify the LUN is present before retrying the resize.", lunUuid));
+ }
+ throw new CloudRuntimeException(String.format(
+ "Failed to resize LUN [%s]: %s",lunUuid, e.getMessage()));
} catch (Exception e) {
- logger.error("Exception occurred while resizing LUN: {}, Exception: {}", lunUuid, e.getMessage());
- throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage());
+ logger.error("Exception occurred while resizing LUN [{}]: {}", lunUuid, e.getMessage());
+ throw new CloudRuntimeException(String.format(
+ "Unexpected error while resizing LUN [%s]: %s", lunUuid, e.getMessage()));
}
}