diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java index 6384566487d4..8427f5ba7f67 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/VolumeFeignClient.java @@ -52,5 +52,5 @@ public interface VolumeFeignClient { @RequestLine("PATCH /api/storage/volumes/{uuid}") @Headers({ "Authorization: {authHeader}"}) - JobResponse updateVolumeRebalancing(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Volume volumeRequest); + JobResponse updateVolume(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Volume volumeRequest); } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java index c002db728dd1..7133c24f9a6f 100755 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java @@ -553,7 +553,34 @@ public boolean migrateToObjectStore(DataStore store) { @Override public void updateStoragePool(StoragePool storagePool, Map details) { + String newCapacityStr = details.get(PrimaryDataStoreLifeCycle.CAPACITY_BYTES); + if (newCapacityStr == null) { + logger.debug("No capacity change requested for pool: {}, skipping FlexVolume resize", storagePool.getName()); + return; + } + + long currentCapacityBytes = storagePool.getCapacityBytes(); + long newCapacityBytes = Long.parseLong(newCapacityStr); + StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details); + + String volumeUuid = details.get(OntapStorageConstants.VOLUME_UUID); + if (volumeUuid == null || volumeUuid.isEmpty()) { + logger.error("Volume UUID or name not found in details for pool: {}, cannot resize", storagePool.getName()); + throw new CloudRuntimeException("Volume UUID or name not found in details, cannot resize ONTAP FlexVolume"); + } + Volume volume = new Volume(); + volume.setUuid(volumeUuid); + volume.setName(details.get(OntapStorageConstants.VOLUME_NAME)); + volume.setSize(newCapacityBytes); + try { + storageStrategy.updateStorageVolume(volume); + logger.info("Successfully resized ONTAP FlexVolume '{}' (UUID: {}) for pool '{}' from {} bytes to {} bytes", + volume.getName(), volume.getUuid(), storagePool.getName(), currentCapacityBytes, newCapacityBytes); + } catch (Exception e) { + logger.error("Exception while resizing FlexVolume for pool: {}. Error: {}", storagePool.getName(), e.getMessage(), e); + throw new CloudRuntimeException("Failed to resize ONTAP FlexVolume for pool: " + storagePool.getName() + ". " + e.getMessage(), e); + } } @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 ac142edf57ae..99d6e04c3018 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 @@ -384,7 +384,24 @@ public Volume createStorageVolume(String volumeName, Long size) { * @return the updated Volume object */ public Volume updateStorageVolume(Volume volume) { - return null; + logger.info("Resizing ONTAP FlexVolume '{}' (UUID: {}) to {} bytes", volume.getName(), volume.getUuid(), volume.getSize()); + String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); + try { + Volume resizeRequest = new Volume(); + resizeRequest.setSize(volume.getSize()); + JobResponse jobResponse = volumeFeignClient.updateVolume(authHeader, volume.getUuid(), resizeRequest); + pollJobIfPresent(jobResponse, "resize FlexVolume [" + volume.getUuid() + "]", 10, 1000); + logger.info("FlexVolume '{}' (UUID: {}) resized successfully to {} bytes", volume.getName(), volume.getUuid(), volume.getSize()); + } catch (FeignException e) { + if (OntapStorageUtils.isOntapObjectNotFoundError(e)) { + String msg = String.format("Cannot resize FlexVolume '%s' (UUID: %s): volume not found on ONTAP (404). ", volume.getName(), volume.getUuid()); + logger.error(msg); + throw new CloudRuntimeException(msg, e); + } + logger.error("Exception while resizing FlexVolume '{}' (UUID: {}): {}", volume.getName(), volume.getUuid(), e.getMessage(), e); + throw new CloudRuntimeException("Failed to resize ONTAP FlexVolume: " + e.getMessage(), e); + } + return volume; } /** 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 131d15bc6a38..0f55a742952d 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 @@ -391,7 +391,7 @@ private void assignExportPolicyToVolume(String volumeUuid, String policyName) { volumeUpdate.setNas(nas); try { - JobResponse jobResponse = volumeFeignClient.updateVolumeRebalancing(authHeader, volumeUuid, volumeUpdate); + JobResponse jobResponse = volumeFeignClient.updateVolume(authHeader, volumeUuid, volumeUpdate); if (jobResponse == null || jobResponse.getJob() == null) { throw new CloudRuntimeException("Failed to attach policy " + policyName + "to volume " + volumeUuid); } diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java index ed538de4a49c..04632433a7ef 100644 --- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java +++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java @@ -56,11 +56,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.times; import static org.mockito.Mockito.withSettings; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.ArgumentMatchers.contains; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; import java.util.HashMap; +import com.cloud.storage.StoragePool; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreLifeCycle; import org.apache.cloudstack.storage.provider.StorageProviderFactory; import org.apache.cloudstack.storage.service.StorageStrategy; import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; @@ -1090,4 +1094,104 @@ public void testAttachZone_kvmHypervisorSetsAndUpdatesPool() throws Exception { } } + // ========== updateStoragePool() Tests ========== + + @Test + public void testUpdateStoragePool_positive_resizesFlexVolume() { + // Setup + StoragePool storagePool = mock(StoragePool.class); + when(storagePool.getName()).thenReturn("test-pool"); + when(storagePool.getCapacityBytes()).thenReturn(2147483648L); // 2 GB current + + Map details = new HashMap<>(); + details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(5368709120L)); // 5 GB new + details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-123"); + details.put(OntapStorageConstants.VOLUME_NAME, "flexvol-name"); + details.put("protocol", "NFS3"); + + Volume updatedVolume = new Volume(); + updatedVolume.setUuid("flex-vol-uuid-123"); + updatedVolume.setSize(5368709120L); + when(storageStrategy.updateStorageVolume(any(Volume.class))).thenReturn(updatedVolume); + + try (MockedStatic utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())) + .thenReturn(storageStrategy); + + // Execute + ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details); + + // Verify + verify(storageStrategy, times(1)).updateStorageVolume(any(Volume.class)); + } + } + + @Test + public void testUpdateStoragePool_noCapacityBytesInDetails_skipsResize() { + // Setup + StoragePool storagePool = mock(StoragePool.class); + when(storagePool.getName()).thenReturn("test-pool"); + + Map details = new HashMap<>(); + details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-123"); + details.put("protocol", "NFS3"); + // No CAPACITY_BYTES key — resize should be skipped + + // Execute + ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details); + + // Verify — storageStrategy should never be called + verify(storageStrategy, never()).updateStorageVolume(any()); + } + + @Test + public void testUpdateStoragePool_missingVolumeUuid_throwsCloudRuntimeException() { + // Setup + StoragePool storagePool = mock(StoragePool.class); + when(storagePool.getName()).thenReturn("test-pool"); + when(storagePool.getCapacityBytes()).thenReturn(1073741824L); + + Map details = new HashMap<>(); + details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(3221225472L)); + details.put("protocol", "NFS3"); + // No VOLUME_UUID — cannot resize without it + + try (MockedStatic utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())) + .thenReturn(storageStrategy); + + // Execute & Verify + assertThrows(CloudRuntimeException.class, + () -> ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details)); + verify(storageStrategy, never()).updateStorageVolume(any()); + } + } + + @Test + public void testUpdateStoragePool_updateStorageVolumeThrows_propagatesCloudRuntimeException() { + // Setup + StoragePool storagePool = mock(StoragePool.class); + when(storagePool.getName()).thenReturn("test-pool"); + when(storagePool.getCapacityBytes()).thenReturn(1073741824L); + + Map details = new HashMap<>(); + details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(3221225472L)); + details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-err"); + details.put(OntapStorageConstants.VOLUME_NAME, "flexvol-err"); + details.put("protocol", "NFS3"); + + when(storageStrategy.updateStorageVolume(any(Volume.class))) + .thenThrow(new CloudRuntimeException("ONTAP resize failed")); + + try (MockedStatic utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())) + .thenReturn(storageStrategy); + + // Execute & Verify + assertThrows(CloudRuntimeException.class, + () -> ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details)); + verify(storageStrategy, times(1)).updateStorageVolume(any(Volume.class)); + } + } + } diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java index d8a249a4447a..f5d21412a512 100644 --- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java +++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java @@ -1259,4 +1259,105 @@ void testDeleteFlexVolSnapshotForCloudStackVolume_Feign404_TreatedAsSuccess() { verify(snapshotFeignClient).deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1")); verify(jobFeignClient, never()).getJobByUUID(anyString(), anyString()); } + + // ========== updateStorageVolume() Tests ========== + + @Test + public void testUpdateStorageVolume_positive() { + // Setup + Volume volume = new Volume(); + volume.setUuid("vol-uuid-resize"); + volume.setName("flexvol-resize"); + volume.setSize(5368709120L); // 5 GB + + Job job = new Job(); + job.setUuid("resize-job-uuid"); + JobResponse jobResponse = new JobResponse(); + jobResponse.setJob(job); + + when(volumeFeignClient.updateVolume(anyString(), eq("vol-uuid-resize"), any())) + .thenReturn(jobResponse); + + Job completedJob = new Job(); + completedJob.setUuid("resize-job-uuid"); + completedJob.setState(OntapStorageConstants.JOB_SUCCESS); + when(jobFeignClient.getJobByUUID(anyString(), eq("resize-job-uuid"))) + .thenReturn(completedJob); + + // Execute + Volume result = storageStrategy.updateStorageVolume(volume); + + // Verify + assertNotNull(result); + assertEquals(5368709120L, result.getSize()); + verify(volumeFeignClient, times(1)).updateVolume(anyString(), eq("vol-uuid-resize"), any()); + verify(jobFeignClient, atLeastOnce()).getJobByUUID(anyString(), eq("resize-job-uuid")); + } + + @Test + public void testUpdateStorageVolume_jobFailed() { + // Setup + Volume volume = new Volume(); + volume.setUuid("vol-uuid-resize"); + volume.setName("flexvol-resize"); + volume.setSize(5368709120L); + + Job job = new Job(); + job.setUuid("resize-job-uuid"); + JobResponse jobResponse = new JobResponse(); + jobResponse.setJob(job); + + when(volumeFeignClient.updateVolume(anyString(), eq("vol-uuid-resize"), any())) + .thenReturn(jobResponse); + + Job failedJob = new Job(); + failedJob.setUuid("resize-job-uuid"); + failedJob.setState(OntapStorageConstants.JOB_FAILURE); + failedJob.setMessage("Resize failed"); + when(jobFeignClient.getJobByUUID(anyString(), eq("resize-job-uuid"))) + .thenReturn(failedJob); + + // Execute & Verify + Exception ex = assertThrows(CloudRuntimeException.class, + () -> storageStrategy.updateStorageVolume(volume)); + assertTrue(ex.getMessage().contains("Job failed")); + } + + @Test + public void testUpdateStorageVolume_feignException() { + // Setup + Volume volume = new Volume(); + volume.setUuid("vol-uuid-fail"); + volume.setName("flexvol-fail"); + volume.setSize(3221225472L); + + FeignException feignException = mock(FeignException.class); + when(feignException.status()).thenReturn(500); + when(volumeFeignClient.updateVolume(anyString(), eq("vol-uuid-fail"), any())) + .thenThrow(feignException); + + // Execute & Verify + Exception ex = assertThrows(CloudRuntimeException.class, + () -> storageStrategy.updateStorageVolume(volume)); + assertTrue(ex.getMessage().contains("Failed to resize ONTAP FlexVolume")); + } + + @Test + public void testUpdateStorageVolume_notFound_404_throwsCloudRuntimeException() { + // Setup + Volume volume = new Volume(); + volume.setUuid("vol-uuid-notfound"); + volume.setName("flexvol-notfound"); + volume.setSize(1073741824L); + + FeignException feignEx = mock(FeignException.class); + when(feignEx.status()).thenReturn(404); + when(volumeFeignClient.updateVolume(anyString(), eq("vol-uuid-notfound"), any())) + .thenThrow(feignEx); + + // Execute & Verify — 404 means volume not found on ONTAP, should throw + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, + () -> storageStrategy.updateStorageVolume(volume)); + assertTrue(ex.getMessage().contains("not found on ONTAP")); + } } 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 f0eb5f0ccced..60d20df8aae6 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 @@ -296,7 +296,7 @@ public void testCreateAccessGroup_Success() throws Exception { when(accessGroup.getHostsToConnect()).thenReturn(hosts); doNothing().when(nasFeignClient).createExportPolicy(anyString(), any(ExportPolicy.class)); when(nasFeignClient.getExportPolicyResponse(anyString(), anyMap())).thenReturn(policyResponse); - when(volumeFeignClient.updateVolumeRebalancing(anyString(), anyString(), any())).thenReturn(jobResponse); + when(volumeFeignClient.updateVolume(anyString(), anyString(), any())).thenReturn(jobResponse); when(jobFeignClient.getJobByUUID(anyString(), anyString())).thenReturn(job); doNothing().when(storagePoolDetailsDao).addDetail(anyLong(), anyString(), anyString(), eq(true)); @@ -307,7 +307,7 @@ public void testCreateAccessGroup_Success() throws Exception { assertNotNull(result); verify(nasFeignClient).createExportPolicy(anyString(), any(ExportPolicy.class)); verify(nasFeignClient).getExportPolicyResponse(anyString(), anyMap()); - verify(volumeFeignClient).updateVolumeRebalancing(anyString(), eq("vol-uuid-123"), any()); + verify(volumeFeignClient).updateVolume(anyString(), eq("vol-uuid-123"), any()); verify(storagePoolDetailsDao, times(2)).addDetail(anyLong(), anyString(), anyString(), eq(true)); } @@ -402,7 +402,7 @@ public void testCreateAccessGroup_JobFailure() throws Exception { when(accessGroup.getHostsToConnect()).thenReturn(hosts); doNothing().when(nasFeignClient).createExportPolicy(anyString(), any(ExportPolicy.class)); when(nasFeignClient.getExportPolicyResponse(anyString(), anyMap())).thenReturn(policyResponse); - when(volumeFeignClient.updateVolumeRebalancing(anyString(), anyString(), any())).thenReturn(jobResponse); + when(volumeFeignClient.updateVolume(anyString(), anyString(), any())).thenReturn(jobResponse); when(jobFeignClient.getJobByUUID(anyString(), anyString())).thenReturn(job); assertThrows(CloudRuntimeException.class, () -> { @@ -446,7 +446,7 @@ public void testCreateAccessGroup_HostWithPrivateIP() throws Exception { when(accessGroup.getHostsToConnect()).thenReturn(hosts); doNothing().when(nasFeignClient).createExportPolicy(anyString(), any(ExportPolicy.class)); when(nasFeignClient.getExportPolicyResponse(anyString(), anyMap())).thenReturn(policyResponse); - when(volumeFeignClient.updateVolumeRebalancing(anyString(), anyString(), any())).thenReturn(jobResponse); + when(volumeFeignClient.updateVolume(anyString(), anyString(), any())).thenReturn(jobResponse); when(jobFeignClient.getJobByUUID(anyString(), anyString())).thenReturn(job); doNothing().when(storagePoolDetailsDao).addDetail(anyLong(), anyString(), anyString(), eq(true));