Skip to content

Commit 5e2f3fe

Browse files
committed
CSTACKEX-234: Enabling storage pool resize (grow and shrink) (#87)
storage pool resize (Grow and shrink) This PR... <!--- Describe your changes in DETAIL - And how has behaviour functionally changed. --> [updateStoragePool] API now resizes the ONTAP FlexVolume backing the pool. When called with a new [capacityBytes], StorageManagerImpl (previously never called the lifecycle hook) now invokes [OntapPrimaryDatastoreLifecycle.updateStoragePool()], which calls the ONTAP REST API (PATCH /api/storage/volumes/{uuid}) and polls the async job to completion. No validation is applied — the new size is passed directly to ONTAP, which enforces all constraints and returns any errors as-is. This also includes UT's. <!-- For new features, provide link to FS, dev ML discussion etc. --> <!-- In case of bug fix, the expected and actual behaviours, steps to reproduce. --> <!-- When "Fixes: #<id>" is specified, the issue/PR will automatically be closed when this PR gets merged --> <!-- For addressing multiple issues/PRs, use multiple "Fixes: #<id>" --> <!-- Fixes: # --> <!--- ******************************************************************************* --> <!--- NOTE: AUTOMATION USES THE DESCRIPTIONS TO SET LABELS AND PRODUCE DOCUMENTATION. --> <!--- PLEASE PUT AN 'X' in only **ONE** box --> <!--- ******************************************************************************* --> - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] New feature (non-breaking change which adds functionality) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Enhancement (improves an existing feature and functionality) - [ ] Cleanup (Code refactoring and cleanup, that may add test cases) - [ ] Build/CI - [ ] Test (unit or integration test code) - [x] Major - [] Minor - [ ] BLOCKER - [ ] Critical - [ ] Major - [ ] Minor - [ ] Trivial the flex volume is created with size 20GiB: <img width="1550" height="870" alt="Screenshot 2026-08-07 at 2 53 44 PM" src="https://github.com/user-attachments/assets/b312f4ec-b4b7-420a-9b1e-81e392a882c4" /> case 1: when A valid input for resize is filled by user: <img width="1458" height="681" alt="Screenshot 2026-08-07 at 2 54 17 PM" src="https://github.com/user-attachments/assets/bb0caf98-df9b-4d6f-ae76-0ca4f7659a0f" /> <img width="1255" height="736" alt="Screenshot 2026-08-07 at 2 54 44 PM" src="https://github.com/user-attachments/assets/7c0b8f49-213f-4163-971a-3a39c1d49936" /> after successful resize: <img width="1555" height="836" alt="Screenshot 2026-08-07 at 3 23 15 PM" src="https://github.com/user-attachments/assets/b7de8bfb-d8ca-4983-ae58-6c0c2503d943" /> case 2: capacity bytes given is smaller than ontap volume minimum size <img width="1476" height="846" alt="Screenshot 2026-08-07 at 2 49 58 PM" src="https://github.com/user-attachments/assets/a670be26-ab86-4e79-be50-86dd68bf39e9" /> case 3: capacity bytes given is smaller than ontap volume maximum size <img width="1469" height="874" alt="Screenshot 2026-08-07 at 2 52 11 PM" src="https://github.com/user-attachments/assets/da10b5c8-f9a3-4651-a1c2-404a908f4832" /> UT's for it : Ran just the update storage pool tests in storagestrategytest and primarydatastorelifecycletest <img width="856" height="773" alt="Screenshot 2026-08-14 at 4 07 37 PM" src="https://github.com/user-attachments/assets/b3d23c36-b635-48da-9403-aad5cd5685a6" /> Ran all tests in both the files: <img width="868" height="788" alt="Screenshot 2026-08-14 at 4 09 12 PM" src="https://github.com/user-attachments/assets/956e7c0c-190c-478f-be79-5a77623cb0bc" /> <!-- Please describe in detail how you tested your changes. --> <!-- Include details of your testing environment, and the tests you ran to --> change? <!-- see how your change affects other areas of the code, etc. --> <!-- Please read the [CONTRIBUTING](https://github.com/apache/cloudstack/blob/main/CONTRIBUTING.md) document -->
1 parent 602d9ec commit 5e2f3fe

8 files changed

Lines changed: 441 additions & 7 deletions

File tree

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@ public interface VolumeFeignClient {
5252

5353
@RequestLine("PATCH /api/storage/volumes/{uuid}")
5454
@Headers({ "Authorization: {authHeader}"})
55-
JobResponse updateVolumeRebalancing(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Volume volumeRequest);
55+
JobResponse updateVolume(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Volume volumeRequest);
5656
}

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,34 @@ public boolean migrateToObjectStore(DataStore store) {
523523

524524
@Override
525525
public void updateStoragePool(StoragePool storagePool, Map<String, String> details) {
526+
String newCapacityStr = details.get(PrimaryDataStoreLifeCycle.CAPACITY_BYTES);
527+
if (newCapacityStr == null) {
528+
logger.debug("No capacity change requested for pool: {}, skipping FlexVolume resize", storagePool.getName());
529+
return;
530+
}
531+
532+
long currentCapacityBytes = storagePool.getCapacityBytes();
533+
long newCapacityBytes = Long.parseLong(newCapacityStr);
534+
StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
535+
536+
String volumeUuid = details.get(OntapStorageConstants.VOLUME_UUID);
537+
if (volumeUuid == null || volumeUuid.isEmpty()) {
538+
logger.error("Volume UUID or name not found in details for pool: {}, cannot resize", storagePool.getName());
539+
throw new CloudRuntimeException("Volume UUID or name not found in details, cannot resize ONTAP FlexVolume");
540+
}
526541

542+
Volume volume = new Volume();
543+
volume.setUuid(volumeUuid);
544+
volume.setName(details.get(OntapStorageConstants.VOLUME_NAME));
545+
volume.setSize(newCapacityBytes);
546+
try {
547+
storageStrategy.updateStorageVolume(volume);
548+
logger.info("Successfully resized ONTAP FlexVolume '{}' (UUID: {}) for pool '{}' from {} bytes to {} bytes",
549+
volume.getName(), volume.getUuid(), storagePool.getName(), currentCapacityBytes, newCapacityBytes);
550+
} catch (Exception e) {
551+
logger.error("Exception while resizing FlexVolume for pool: {}. Error: {}", storagePool.getName(), e.getMessage(), e);
552+
throw new CloudRuntimeException("Failed to resize ONTAP FlexVolume for pool: " + storagePool.getName() + ". " + e.getMessage(), e);
553+
}
527554
}
528555

529556
@Override

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,25 @@ public Volume createStorageVolume(String volumeName, Long size) {
339339
* @return the updated Volume object
340340
*/
341341
public Volume updateStorageVolume(Volume volume) {
342-
return null;
342+
logger.info("Resizing ONTAP FlexVolume '{}' (UUID: {}) to {} bytes", volume.getName(), volume.getUuid(), volume.getSize());
343+
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
344+
try {
345+
Volume resizeRequest = new Volume();
346+
resizeRequest.setSize(volume.getSize());
347+
JobResponse jobResponse = volumeFeignClient.updateVolume(authHeader, volume.getUuid(), resizeRequest);
348+
pollJobIfPresent(jobResponse, "resize FlexVolume [" + volume.getUuid() + "]",
349+
OntapStorageConstants.ONTAP_VOLUME_JOB_MAX_RETRIES, OntapStorageConstants.ONTAP_VOLUME_JOB_POLL_INTERVAL_MS);
350+
logger.info("FlexVolume '{}' (UUID: {}) resized successfully to {} bytes", volume.getName(), volume.getUuid(), volume.getSize());
351+
} catch (FeignException e) {
352+
if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
353+
String msg = String.format("Cannot resize FlexVolume '%s' (UUID: %s): volume not found on ONTAP (404). ", volume.getName(), volume.getUuid());
354+
logger.error(msg);
355+
throw new CloudRuntimeException(msg, e);
356+
}
357+
logger.error("Exception while resizing FlexVolume '{}' (UUID: {}): {}", volume.getName(), volume.getUuid(), e.getMessage(), e);
358+
throw new CloudRuntimeException("Failed to resize ONTAP FlexVolume: " + e.getMessage(), e);
359+
}
360+
return volume;
343361
}
344362

345363
/**

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ private void assignExportPolicyToVolume(String volumeUuid, String policyName) {
256256
volumeUpdate.setNas(nas);
257257

258258
try {
259-
JobResponse jobResponse = volumeFeignClient.updateVolumeRebalancing(authHeader, volumeUuid, volumeUpdate);
259+
JobResponse jobResponse = volumeFeignClient.updateVolume(authHeader, volumeUuid, volumeUpdate);
260260
if (jobResponse == null || jobResponse.getJob() == null) {
261261
throw new CloudRuntimeException("Failed to attach policy " + policyName + "to volume " + volumeUuid);
262262
}

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,22 @@ public class OntapStorageConstants {
106106
public static final String ONTAP_SNAP_SIZE = "ontap_snap_size";
107107
public static final String FILE_PATH = "file_path";
108108
public static final int MAX_SNAPSHOT_NAME_LENGTH = 255;
109+
public static final String ONTAP_TEMP_CG_PREFIX = "cs-temp-cg-";
110+
/** ONTAP CG API: action required when referencing existing FlexVols in a consistency group. */
111+
public static final String CG_VOLUME_PROVISIONING_ACTION_ADD = "add";
112+
public static final int ONTAP_CG_JOB_MAX_RETRIES = 60;
113+
public static final int ONTAP_CG_JOB_POLL_INTERVAL_MS = 2000;
114+
public static final int ONTAP_CG_SNAPSHOT_RESOLVE_MAX_RETRIES = 30;
115+
public static final int ONTAP_CG_SNAPSHOT_RESOLVE_POLL_INTERVAL_MS = 1000;
116+
public static final int ONTAP_SFSR_JOB_MAX_RETRIES = 60;
117+
public static final int ONTAP_SFSR_JOB_POLL_INTERVAL_MS = 2000;
118+
public static final int ONTAP_SNAPSHOT_DELETE_JOB_MAX_RETRIES = 30;
119+
public static final int ONTAP_SNAPSHOT_DELETE_JOB_POLL_INTERVAL_MS = 2000;
120+
/** Retry settings for FlexVolume create/resize/delete job polling. */
121+
public static final int ONTAP_VOLUME_JOB_MAX_RETRIES = 10;
122+
public static final int ONTAP_VOLUME_JOB_POLL_INTERVAL_MS = 1000;
123+
public static final int ONTAP_FLEXVOL_JOB_POLL_INTERVAL_MS = 2000;
124+
public static final int ONTAP_FLEXVOL_RESOLVE_MAX_RETRIES = 30;
109125

110126
/** vm_snapshot_details key for ONTAP FlexVolume-level VM snapshots. */
111127
public static final String ONTAP_FLEXVOL_SNAPSHOT = "ontapFlexVolSnapshot";

plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,15 @@
5454
import static org.mockito.Mockito.verify;
5555
import static org.mockito.Mockito.times;
5656
import static org.mockito.Mockito.withSettings;
57+
import static org.mockito.Mockito.mock;
58+
import static org.mockito.Mockito.never;
59+
import static org.mockito.ArgumentMatchers.contains;
5760
import static org.junit.jupiter.api.Assertions.assertThrows;
5861
import static org.junit.jupiter.api.Assertions.assertTrue;
5962
import static org.junit.jupiter.api.Assertions.assertFalse;
6063
import java.util.HashMap;
64+
import com.cloud.storage.StoragePool;
65+
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreLifeCycle;
6166
import org.apache.cloudstack.storage.provider.StorageProviderFactory;
6267
import org.apache.cloudstack.storage.service.StorageStrategy;
6368
import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper;
@@ -854,4 +859,104 @@ public void testAttachZone_kvmHypervisorSetsAndUpdatesPool() throws Exception {
854859
}
855860
}
856861

862+
// ========== updateStoragePool() Tests ==========
863+
864+
@Test
865+
public void testUpdateStoragePool_positive_resizesFlexVolume() {
866+
// Setup
867+
StoragePool storagePool = mock(StoragePool.class);
868+
when(storagePool.getName()).thenReturn("test-pool");
869+
when(storagePool.getCapacityBytes()).thenReturn(2147483648L); // 2 GB current
870+
871+
Map<String, String> details = new HashMap<>();
872+
details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(5368709120L)); // 5 GB new
873+
details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-123");
874+
details.put(OntapStorageConstants.VOLUME_NAME, "flexvol-name");
875+
details.put("protocol", "NFS3");
876+
877+
Volume updatedVolume = new Volume();
878+
updatedVolume.setUuid("flex-vol-uuid-123");
879+
updatedVolume.setSize(5368709120L);
880+
when(storageStrategy.updateStorageVolume(any(Volume.class))).thenReturn(updatedVolume);
881+
882+
try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
883+
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
884+
.thenReturn(storageStrategy);
885+
886+
// Execute
887+
ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details);
888+
889+
// Verify
890+
verify(storageStrategy, times(1)).updateStorageVolume(any(Volume.class));
891+
}
892+
}
893+
894+
@Test
895+
public void testUpdateStoragePool_noCapacityBytesInDetails_skipsResize() {
896+
// Setup
897+
StoragePool storagePool = mock(StoragePool.class);
898+
when(storagePool.getName()).thenReturn("test-pool");
899+
900+
Map<String, String> details = new HashMap<>();
901+
details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-123");
902+
details.put("protocol", "NFS3");
903+
// No CAPACITY_BYTES key — resize should be skipped
904+
905+
// Execute
906+
ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details);
907+
908+
// Verify — storageStrategy should never be called
909+
verify(storageStrategy, never()).updateStorageVolume(any());
910+
}
911+
912+
@Test
913+
public void testUpdateStoragePool_missingVolumeUuid_throwsCloudRuntimeException() {
914+
// Setup
915+
StoragePool storagePool = mock(StoragePool.class);
916+
when(storagePool.getName()).thenReturn("test-pool");
917+
when(storagePool.getCapacityBytes()).thenReturn(1073741824L);
918+
919+
Map<String, String> details = new HashMap<>();
920+
details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(3221225472L));
921+
details.put("protocol", "NFS3");
922+
// No VOLUME_UUID — cannot resize without it
923+
924+
try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
925+
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
926+
.thenReturn(storageStrategy);
927+
928+
// Execute & Verify
929+
assertThrows(CloudRuntimeException.class,
930+
() -> ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details));
931+
verify(storageStrategy, never()).updateStorageVolume(any());
932+
}
933+
}
934+
935+
@Test
936+
public void testUpdateStoragePool_updateStorageVolumeThrows_propagatesCloudRuntimeException() {
937+
// Setup
938+
StoragePool storagePool = mock(StoragePool.class);
939+
when(storagePool.getName()).thenReturn("test-pool");
940+
when(storagePool.getCapacityBytes()).thenReturn(1073741824L);
941+
942+
Map<String, String> details = new HashMap<>();
943+
details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(3221225472L));
944+
details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-err");
945+
details.put(OntapStorageConstants.VOLUME_NAME, "flexvol-err");
946+
details.put("protocol", "NFS3");
947+
948+
when(storageStrategy.updateStorageVolume(any(Volume.class)))
949+
.thenThrow(new CloudRuntimeException("ONTAP resize failed"));
950+
951+
try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
952+
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
953+
.thenReturn(storageStrategy);
954+
955+
// Execute & Verify
956+
assertThrows(CloudRuntimeException.class,
957+
() -> ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details));
958+
verify(storageStrategy, times(1)).updateStorageVolume(any(Volume.class));
959+
}
960+
}
961+
857962
}

0 commit comments

Comments
 (0)