Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,34 @@ public boolean migrateToObjectStore(DataStore store) {

@Override
public void updateStoragePool(StoragePool storagePool, Map<String, String> 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();
Comment thread
sathvikaragi marked this conversation as resolved.
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));
Comment thread
sathvikaragi marked this conversation as resolved.
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should handle other exception also, bcz job polling can get time out and throw cloudRuntime Exception

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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> 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<OntapStorageUtils> 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<String, String> 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<String, String> details = new HashMap<>();
details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(3221225472L));
details.put("protocol", "NFS3");
// No VOLUME_UUID — cannot resize without it

try (MockedStatic<OntapStorageUtils> 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<String, String> 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<OntapStorageUtils> 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));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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));
}

Expand Down Expand Up @@ -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, () -> {
Expand Down Expand Up @@ -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));

Expand Down
Loading