From bdd9162c9e1140f5ca1de6a7fe64783d28ebc43a Mon Sep 17 00:00:00 2001 From: "Gupta, Surya" Date: Tue, 13 Oct 2026 14:28:05 +0530 Subject: [PATCH 1/7] [CSTACKEX-328] Change Disk Offering, allow min as 0 during volume create for min IOPS and delete QoS --- .../driver/OntapPrimaryDatastoreDriver.java | 155 +++++++++--- .../storage/service/UnifiedSANStrategy.java | 19 +- .../OntapPrimaryDatastoreDriverTest.java | 226 ++++++++++++++++++ .../cloud/storage/VolumeApiServiceImpl.java | 6 +- .../views/storage/ChangeOfferingForVolume.vue | 2 +- ui/src/views/storage/CreateVolume.vue | 2 +- 6 files changed, 375 insertions(+), 35 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 8a5cbb6a753c..bd35fc4f9112 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 @@ -205,7 +205,8 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet * via this path; bootable recovery is createTemplate(snapshotid) → deploy). * - Backend: iSCSI → POST /api/storage/luns (clone.source in .snapshot/); * NFS → POST /api/storage/file/clone with snapshot.name. - * - IOPS: MIN_IOPS/MAX_IOPS may be on snapshot_details; apply is TODO below. + * - IOPS: the new volume's min/max (snapshot values, or the root offering when one + * is supplied). Applied here; the temporary template clone does not get a policy. * * 2) cloneOfTemplate — deploy / create from cached template on this pool. * @@ -219,9 +220,7 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet CloudStackVolume clonedCloudStackVolume; if (cloneOfSnapshotId != null) { clonedCloudStackVolume = cloneCloudStackVolumeFromSnapshot( - storagePool, volInfo, details, cloneOfSnapshotId); - // TODO(CSTACKEX-306): apply persisted MIN_IOPS / MAX_IOPS from snapshot_details - // onto this CloudStack volume (and ONTAP QoS if applicable) after successful clone. + storagePool, volInfo, details, cloneOfSnapshotId, true); } else if (cloneOfTemplateId != null) { clonedCloudStackVolume = cloneCloudStackVolumeFromTemplate( storagePool, volInfo, details, cloneOfTemplateId); @@ -525,9 +524,15 @@ private Long getSnapshotIdForCloning(long volumeId) { * *

Optional grow when the disk offering is larger than the snapshot size (same pattern as * clone-from-template).

+ * + *

When {@code applyQos} is true, the new volume's min and max IOPS are applied the same way + * as a template clone: iSCSI carries the policy on the clone request, NFS attaches it after + * the file exists. The temporary volume used for {@code createTemplate(snapshotid)} passes + * false and is left without a policy.

*/ private CloudStackVolume cloneCloudStackVolumeFromSnapshot(StoragePoolVO storagePool, VolumeInfo volumeInfo, - Map details, long csSnapshotId) { + Map details, long csSnapshotId, + boolean applyQos) { String snapshotName = requireSnapshotDetail(csSnapshotId, OntapStorageConstants.ONTAP_SNAP_NAME); String volumePath = requireSnapshotDetail(csSnapshotId, OntapStorageConstants.VOLUME_PATH); String primaryPoolId = requireSnapshotDetail(csSnapshotId, OntapStorageConstants.PRIMARY_POOL_ID); @@ -546,30 +551,56 @@ private CloudStackVolume cloneCloudStackVolumeFromSnapshot(StoragePoolVO storage } StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details); + boolean iscsi = isIscsi(details); + VolumeQosPolicy qosPolicy = null; logger.info("cloneCloudStackVolumeFromSnapshot: Cloning from CS snapshot [{}] (ONTAP snap [{}], path [{}]) " + "for volume [{}] on pool [{}] protocol [{}]", csSnapshotId, snapshotName, volumePath, volumeInfo.getId(), storagePool.getId(), poolProtocol); - CloudStackVolume request = isIscsi(details) - ? createCloneLunFromSnapshotRequest(storagePool, details, volumeInfo, volumePath, snapshotName) - : createCloneFileFromSnapshotRequest(storagePool, details, volumeInfo, volumePath, snapshotName); - CloudStackVolume cloned = storageStrategy.cloneCloudStackVolume(request); - if (cloned == null) { - throw new CloudRuntimeException("ONTAP returned nothing when cloning snapshot [" + csSnapshotId - + "] for volume [" + volumeInfo.getId() + "]"); - } + try { + if (applyQos) { + verifySufficientIopsForStoragePool(storagePool, volumeInfo.getMinIops(), volumeInfo.getId()); + Volume.Type volumeType = volumeInfo.getVolumeType(); + if (volumeType == Volume.Type.DATADISK || volumeType == Volume.Type.ROOT) { + qosPolicy = createQosPolicyIfNeeded(storageStrategy, details, + volumeInfo.getMinIops(), volumeInfo.getMaxIops(), storagePool.getId()); + } + } - long requestedSize = getDataObjectSizeIncludingHypervisorSnapshotReserve(volumeInfo, storagePool); - long snapshotSize = resolveSnapshotSizeBytes(csSnapshotId); - if (snapshotSize > 0 && requestedSize > snapshotSize) { - logger.info("cloneCloudStackVolumeFromSnapshot: Growing clone of snapshot [{}] from {} to {} bytes for volume [{}]", - csSnapshotId, snapshotSize, requestedSize, volumeInfo.getId()); - resizeClonedVolumeOrCleanup(storageStrategy, cloned, requestedSize, - "cloneCloudStackVolumeFromSnapshot snapshot [" + csSnapshotId + "] volume [" + volumeInfo.getId() + "]"); - } + CloudStackVolume request = iscsi + ? createCloneLunFromSnapshotRequest(storagePool, details, volumeInfo, volumePath, snapshotName, qosPolicy) + : createCloneFileFromSnapshotRequest(storagePool, details, volumeInfo, volumePath, snapshotName); + CloudStackVolume cloned = storageStrategy.cloneCloudStackVolume(request); + if (cloned == null) { + throw new CloudRuntimeException("ONTAP returned nothing when cloning snapshot [" + csSnapshotId + + "] for volume [" + volumeInfo.getId() + "]"); + } + + long requestedSize = getDataObjectSizeIncludingHypervisorSnapshotReserve(volumeInfo, storagePool); + long snapshotSize = resolveSnapshotSizeBytes(csSnapshotId); + if (snapshotSize > 0 && requestedSize > snapshotSize) { + logger.info("cloneCloudStackVolumeFromSnapshot: Growing clone of snapshot [{}] from {} to {} bytes for volume [{}]", + csSnapshotId, snapshotSize, requestedSize, volumeInfo.getId()); + resizeClonedVolumeOrCleanup(storageStrategy, cloned, requestedSize, + "cloneCloudStackVolumeFromSnapshot snapshot [" + csSnapshotId + "] volume [" + volumeInfo.getId() + "]"); + } - return cloned; + if (!iscsi && qosPolicy != null) { + CloudStackVolume cloudStackVolume = createCloudStackVolumeRequestByProtocol( + storagePool, details, volumeInfo, qosPolicy); + storageStrategy.updateCloudStackVolume(cloudStackVolume); + } + if (applyQos) { + persistQosPolicyDetails(volumeInfo.getId(), qosPolicy); + } + return cloned; + } catch (Exception e) { + if (qosPolicy != null) { + storageStrategy.deleteVolumeQosPolicy(qosPolicy.getUuid()); + } + throw e; + } } /** @@ -779,10 +810,24 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details); logger.info("createCloudStackVolumeForTypeVolume: Connection to Ontap SVM [{}] successful, preparing CloudStackVolumeRequest", details.get(OntapStorageConstants.SVM_NAME)); VolumeInfo volumeInfo = (VolumeInfo) data; - VolumeDetailVO qosPolicyDetail = volumeDetailsDao.findDetail( - volumeInfo.getId(), OntapStorageConstants.QOS_POLICY_UUID); - CloudStackVolume cloudStackVolumeRequest = createDeleteCloudStackVolumeRequest(storagePool, details, volumeInfo); - storageStrategy.deleteCloudStackVolume(cloudStackVolumeRequest); + VolumeDetailVO qosPolicyDetail = volumeDetailsDao.findDetail(volumeInfo.getId(), OntapStorageConstants.QOS_POLICY_UUID); + + // NFS file delete leaves the QoS assignment in place unless the file is set to none first. + // iSCSI LUN delete already drops object_count. + // TODO: revisit this NFS none-policy step when the file is deleted from ONTAP instead of KVM. + boolean qosCleared = false; + if (!isIscsi(details)) { + qosCleared = clearQosPolicyBeforeDelete(storageStrategy, storagePool, details, volumeInfo, qosPolicyDetail); + } + try { + CloudStackVolume cloudStackVolumeRequest = createDeleteCloudStackVolumeRequest(storagePool, details, volumeInfo); + storageStrategy.deleteCloudStackVolume(cloudStackVolumeRequest); + } catch (Exception e) { + if (qosCleared) { + restoreQosPolicyAfterFailedDelete(storageStrategy, storagePool, details, volumeInfo, qosPolicyDetail); + } + throw e; + } if (qosPolicyDetail != null) { deleteUnusedQosPolicy(storageStrategy, qosPolicyDetail.getValue()); } @@ -813,6 +858,51 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac } } + /** + * Sets the NFS file QoS policy to {@code none} before the file is deleted. + * Failure must not block volume delete. + * + * @return true when the file was moved off its policy + */ + private boolean clearQosPolicyBeforeDelete(StorageStrategy storageStrategy, StoragePoolVO storagePool, + Map details, VolumeInfo volumeInfo, + VolumeDetailVO qosPolicyDetail) { + if (qosPolicyDetail == null || qosPolicyDetail.getValue() == null) { + return false; + } + VolumeQosPolicy nonePolicy = new VolumeQosPolicy(); + nonePolicy.setName(OntapStorageConstants.QOS_POLICY_NONE); + try { + CloudStackVolume request = createCloudStackVolumeRequestByProtocol(storagePool, details, volumeInfo, nonePolicy); + storageStrategy.updateCloudStackVolume(request); + return true; + } catch (Exception e) { + logger.warn("Could not clear QoS policy [{}] on volume [{}] before delete: {}", + qosPolicyDetail.getValue(), volumeInfo.getId(), e.getMessage()); + return false; + } + } + + /** + * Puts the previous QoS policy back on an NFS file when delete fails after the policy was cleared. + */ + private void restoreQosPolicyAfterFailedDelete(StorageStrategy storageStrategy, StoragePoolVO storagePool, + Map details, VolumeInfo volumeInfo, + VolumeDetailVO qosPolicyDetail) { + if (qosPolicyDetail == null || qosPolicyDetail.getValue() == null) { + return; + } + VolumeQosPolicy previousPolicy = new VolumeQosPolicy(); + previousPolicy.setUuid(qosPolicyDetail.getValue()); + try { + CloudStackVolume request = createCloudStackVolumeRequestByProtocol(storagePool, details, volumeInfo, previousPolicy); + storageStrategy.updateCloudStackVolume(request); + } catch (Exception e) { + logger.warn("Could not restore QoS policy [{}] on volume [{}] after delete failed: {}", + qosPolicyDetail.getValue(), volumeInfo.getId(), e.getMessage()); + } + } + /** * Orchestrates CloudStack volume-snapshot delete on ONTAP. * @@ -984,7 +1074,7 @@ private CopyCommandResult copySnapshotToTemporaryVolume(DataObject srcData, Volu renameTemporarySnapshotCopy(volInfo, volumeVO, srcData.getId()); logger.info("copySnapshotToTemporaryVolume: Cloning CS snapshot [{}] into temporary volume [{}] on pool [{}] for template creation", srcData.getId(), volInfo.getId(), storagePool.getId()); - CloudStackVolume cloned = cloneCloudStackVolumeFromSnapshot(storagePool, volInfo, details, srcData.getId()); + CloudStackVolume cloned = cloneCloudStackVolumeFromSnapshot(storagePool, volInfo, details, srcData.getId(), false); if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) { volumeVO.setPath(volInfo.getUuid()); } @@ -1825,7 +1915,8 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback details, VolumeInfo volumeInfo, String sourceVolumePath, - String snapshotName) { + String snapshotName, VolumeQosPolicy qosPolicy) { String lunName = volumeInfo.getName().replace(OntapStorageConstants.HYPHEN, OntapStorageConstants.UNDERSCORE); if (!OntapStorageUtils.isValidName(lunName)) { throw new CloudRuntimeException("Invalid dataObject name [" + lunName @@ -2276,6 +2367,12 @@ private CloudStackVolume createCloneLunFromSnapshotRequest(StoragePoolVO storage lunRequest.setSvm(svm); lunRequest.setName(OntapStorageUtils.getLunName(storagePool.getName(), lunName)); lunRequest.setClone(clone); + if (qosPolicy != null) { + VolumeQosPolicy qosPolicyReference = new VolumeQosPolicy(); + qosPolicyReference.setName(qosPolicy.getName()); + qosPolicyReference.setUuid(qosPolicy.getUuid()); + lunRequest.setQosPolicy(qosPolicyReference); + } CloudStackVolume request = new CloudStackVolume(); request.setLun(lunRequest); 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 e0166cbf2829..397e64b782e1 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 @@ -196,7 +196,24 @@ private void bestEffortDeleteTemplateCacheLun(String svmName, String lunName, St @Override public CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) { - return null; + if (cloudstackVolume == null || cloudstackVolume.getLun() == null + || cloudstackVolume.getLun().getUuid() == null) { + throw new CloudRuntimeException("Invalid iSCSI volume QoS update request"); + } + Lun lunUpdate = new Lun(); + lunUpdate.setQosPolicy(cloudstackVolume.getLun().getQosPolicy()); + try { + JobResponse response = sanFeignClient.updateLun( + getAuthHeader(), cloudstackVolume.getLun().getUuid(), lunUpdate); + pollJobIfPresent(response, "update QoS policy on LUN [" + cloudstackVolume.getLun().getUuid() + "]"); + } catch (FeignException e) { + throw new CloudRuntimeException("Failed to apply QoS policy to LUN: " + e.getMessage(), e); + } + logger.info("Applied QoS policy [{}] to LUN [{}]", + cloudstackVolume.getLun().getQosPolicy() != null + ? cloudstackVolume.getLun().getQosPolicy().getName() : null, + cloudstackVolume.getLun().getUuid()); + return cloudstackVolume; } @Override 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 12e5ff3faced..67820a86d70a 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 @@ -1034,6 +1034,131 @@ void testCreateAsync_VolumeClonedFromSnapshot_IscsiSuccessWithoutGrow() { } } + @Test + void testCreateAsync_VolumeClonedFromSnapshot_IscsiWithIops_SetsQosOnLunCreate() { + stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name()); + when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK); + when(volumeInfo.getMinIops()).thenReturn(100L); + when(volumeInfo.getMaxIops()).thenReturn(200L); + lenient().when(storagePool.getCapacityIops()).thenReturn(null); + + Lun clonedLun = new Lun(); + clonedLun.setName("/vol/vol1/test_volume"); + clonedLun.setUuid("snap-cloned-lun-uuid"); + CloudStackVolume cloned = new CloudStackVolume(); + cloned.setLun(clonedLun); + VolumeQosPolicy qosPolicy = qosPolicy("qos-snap-uuid", "cs_100_to_200_iops_svm1"); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + stubQosCreateMocks(utilityMock, sanStrategy, cloned, qosPolicy); + when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(cloned); + + driver.createAsync(dataStore, volumeInfo, createCallback); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class); + verify(createCallback).complete(resultCaptor.capture()); + assertTrue(resultCaptor.getValue().isSuccess()); + verify(sanStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L)); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class); + verify(sanStrategy).cloneCloudStackVolume(requestCaptor.capture()); + assertTrue(isQosPolicyReference(requestCaptor.getValue().getLun().getQosPolicy(), + "qos-snap-uuid", "cs_100_to_200_iops_svm1")); + verify(sanStrategy, never()).updateCloudStackVolume(any()); + verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.QOS_POLICY_UUID), + eq("qos-snap-uuid"), eq(false)); + } + } + + @Test + void testCreateAsync_VolumeClonedFromSnapshot_NfsWithIops_AttachesQosAfterClone() { + stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.NFS3.name()); + storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flex-uuid"); + when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK); + when(volumeInfo.getMinIops()).thenReturn(100L); + when(volumeInfo.getMaxIops()).thenReturn(200L); + lenient().when(storagePool.getCapacityIops()).thenReturn(null); + + CloudStackVolume cloned = new CloudStackVolume(); + VolumeQosPolicy qosPolicy = lookedUpQosPolicy("qos-nfs-snap-uuid", "cs_100_to_200_iops_svm1"); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + stubQosCreateMocks(utilityMock, nasStrategy, cloned, qosPolicy); + when(nasStrategy.cloneCloudStackVolume(any())).thenReturn(cloned); + when(nasStrategy.updateCloudStackVolume(any())).thenReturn(cloned); + + driver.createAsync(dataStore, volumeInfo, createCallback); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class); + verify(createCallback).complete(resultCaptor.capture()); + assertTrue(resultCaptor.getValue().isSuccess()); + verify(nasStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L)); + ArgumentCaptor cloneCaptor = ArgumentCaptor.forClass(CloudStackVolume.class); + verify(nasStrategy).cloneCloudStackVolume(cloneCaptor.capture()); + assertNull(cloneCaptor.getValue().getFile().getQosPolicy()); + verify(nasStrategy).updateCloudStackVolume(argThat(request -> + "1".equals(request.getDatastoreId()) + && "flex-uuid".equals(request.getFlexVolumeUuid()) + && request.getVolumeInfo() == volumeInfo + && request.getFile() != null + && isQosPolicyReference(request.getFile().getQosPolicy(), + "qos-nfs-snap-uuid", "cs_100_to_200_iops_svm1"))); + verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.QOS_POLICY_UUID), + eq("qos-nfs-snap-uuid"), eq(false)); + } + } + + @Test + void testCreateAsync_VolumeClonedFromSnapshot_NoIops_DoesNotCreatePolicy() { + stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name()); + when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK); + when(volumeInfo.getMinIops()).thenReturn(null); + when(volumeInfo.getMaxIops()).thenReturn(null); + + Lun clonedLun = new Lun(); + clonedLun.setName("/vol/vol1/test_volume"); + clonedLun.setUuid("snap-cloned-lun-uuid"); + CloudStackVolume cloned = new CloudStackVolume(); + cloned.setLun(clonedLun); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy); + when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(cloned); + + driver.createAsync(dataStore, volumeInfo, createCallback); + + verify(sanStrategy, never()).createVolumeQosPolicy(nullable(String.class), nullable(Long.class), nullable(Long.class)); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class); + verify(sanStrategy).cloneCloudStackVolume(requestCaptor.capture()); + assertNull(requestCaptor.getValue().getLun().getQosPolicy()); + } + } + + @Test + void testCreateAsync_VolumeClonedFromSnapshot_CloneFails_DeletesNewPolicy() { + stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name()); + when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.ROOT); + when(volumeInfo.getMinIops()).thenReturn(100L); + when(volumeInfo.getMaxIops()).thenReturn(200L); + lenient().when(storagePool.getCapacityIops()).thenReturn(null); + + VolumeQosPolicy qosPolicy = qosPolicy("qos-snap-uuid", "cs_100_to_200_iops_svm1"); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + stubQosCreateMocks(utilityMock, sanStrategy, new CloudStackVolume(), qosPolicy); + when(sanStrategy.cloneCloudStackVolume(any())) + .thenThrow(new CloudRuntimeException("clone failed")); + + driver.createAsync(dataStore, volumeInfo, createCallback); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class); + verify(createCallback).complete(resultCaptor.capture()); + assertFalse(resultCaptor.getValue().isSuccess()); + verify(sanStrategy).deleteVolumeQosPolicy("qos-snap-uuid"); + verify(volumeDetailsDao, never()).addDetail(eq(100L), eq(OntapStorageConstants.QOS_POLICY_UUID), + anyString(), eq(false)); + } + } + @Test void testCreateAsync_VolumeClonedFromSnapshot_GrowsWhenOfferingIsLarger() { stubVolumeCloneFromSnapshot(5368709120L, 21474836480L, ProtocolType.ISCSI.name()); @@ -2360,6 +2485,76 @@ void testDeleteAsync_DeletesUnusedQosPolicy() { } } + @Test + void testDeleteAsync_NfsClearsQosBeforeFileDelete() { + storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name()); + storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flex-uuid"); + when(dataStore.getId()).thenReturn(1L); + when(volumeInfo.getType()).thenReturn(VOLUME); + when(volumeInfo.getId()).thenReturn(100L); + when(storagePoolDao.findById(1L)).thenReturn(storagePool); + when(storagePool.getId()).thenReturn(1L); + when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails); + when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)) + .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false)); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(nasStrategy); + when(nasStrategy.updateCloudStackVolume(any())).thenAnswer(invocation -> invocation.getArgument(0)); + doNothing().when(nasStrategy).deleteCloudStackVolume(any()); + + driver.deleteAsync(dataStore, volumeInfo, commandCallback); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class); + verify(commandCallback).complete(resultCaptor.capture()); + assertTrue(resultCaptor.getValue().isSuccess()); + verify(nasStrategy).updateCloudStackVolume(argThat(request -> + "flex-uuid".equals(request.getFlexVolumeUuid()) + && request.getFile() != null + && isQosPolicyReference(request.getFile().getQosPolicy(), null, OntapStorageConstants.QOS_POLICY_NONE))); + verify(nasStrategy).deleteCloudStackVolume(any()); + verify(nasStrategy).deleteVolumeQosPolicy("qos-uuid"); + } + } + + @Test + void testDeleteAsync_NfsRestoresQosWhenFileDeleteFails() { + storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name()); + storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flex-uuid"); + when(dataStore.getId()).thenReturn(1L); + when(volumeInfo.getType()).thenReturn(VOLUME); + when(volumeInfo.getId()).thenReturn(100L); + when(storagePoolDao.findById(1L)).thenReturn(storagePool); + when(storagePool.getId()).thenReturn(1L); + when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails); + when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)) + .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false)); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(nasStrategy); + when(nasStrategy.updateCloudStackVolume(any())).thenAnswer(invocation -> invocation.getArgument(0)); + doThrow(new CloudRuntimeException("file delete failed")).when(nasStrategy).deleteCloudStackVolume(any()); + + driver.deleteAsync(dataStore, volumeInfo, commandCallback); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class); + verify(commandCallback).complete(resultCaptor.capture()); + assertFalse(resultCaptor.getValue().isSuccess()); + ArgumentCaptor qosCaptor = ArgumentCaptor.forClass(CloudStackVolume.class); + verify(nasStrategy, times(2)).updateCloudStackVolume(qosCaptor.capture()); + assertTrue(isQosPolicyReference(qosCaptor.getAllValues().get(0).getFile().getQosPolicy(), + null, OntapStorageConstants.QOS_POLICY_NONE)); + VolumeQosPolicy restored = qosCaptor.getAllValues().get(1).getFile().getQosPolicy(); + assertEquals("qos-uuid", restored.getUuid()); + assertNull(restored.getName()); + assertNull(restored.getSvm()); + assertNull(restored.getFixed()); + verify(nasStrategy, never()).deleteVolumeQosPolicy(any()); + } + } + @Test void testDeleteAsync_QosPolicyDeleteFails_VolumeDeleteStillSucceeds() { when(dataStore.getId()).thenReturn(1L); @@ -3128,6 +3323,37 @@ void testCopyAsync_IscsiTemporarySnapshotCopy_ClonesLunAndRecordsIdentity() { verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.LUN_DOT_NAME), eq("/vol/vol1/cs_tmp_snap_200_100"), eq(false)); verify(volumeDao).update(anyLong(), eq(dbVolume)); verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong()); + verify(sanStrategy, never()).createVolumeQosPolicy(anyString(), any(), any()); + } + } + + @Test + void testCopyAsync_IscsiTemporarySnapshotCopy_IgnoresVolumeIops() { + VolumeVO dbVolume = temporarySnapshotCopyVolume(); + SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(dbVolume); + when(volumeInfo.getVolume()).thenReturn(temporarySnapshotCopyVolume()); + stubSnapshotCloneSource(ProtocolType.ISCSI.name()); + when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.OntapiSCSI); + lenient().when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK); + lenient().when(volumeInfo.getMinIops()).thenReturn(100L); + lenient().when(volumeInfo.getMaxIops()).thenReturn(200L); + + Lun clonedLun = new Lun(); + clonedLun.setName("/vol/vol1/cs_tmp_snap_200_100"); + clonedLun.setUuid("tmp-lun-uuid"); + CloudStackVolume cloned = new CloudStackVolume(); + cloned.setLun(clonedLun); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy); + when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(cloned); + + driver.copyAsync(snapshotInfo, volumeInfo, null, null); + + verify(sanStrategy, never()).createVolumeQosPolicy(nullable(String.class), nullable(Long.class), nullable(Long.class)); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class); + verify(sanStrategy).cloneCloudStackVolume(requestCaptor.capture()); + assertNull(requestCaptor.getValue().getLun().getQosPolicy()); } } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 445bad769a06..f3a49aad23db 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -884,7 +884,7 @@ public VolumeVO allocVolume(long ownerId, Long zoneId, Long diskOfferingId, Long minIops = 0L; maxIops = 0L; } else { - if (minIops == null || minIops <= 0) { + if (minIops == null || minIops < 0) { throw new InvalidParameterValueException("The min IOPS must be greater than 0."); } @@ -2328,7 +2328,7 @@ public Volume changeDiskOfferingForVolumeInternal(Long volumeId, Long newDiskOff if (volumeMigrateRequired) { logger.warn(String.format("Volume change offering operation succeeded for volume ID: %s but volume resize operation failed, so please try resize volume operation separately", volume.getUuid())); } else { - throw new CloudRuntimeException(String.format("Volume disk offering change operation failed for volume ID [%s] because the volume resize operation failed.", volume.getUuid())); + throw new CloudRuntimeException(String.format("Volume disk offering change operation failed for volume ID [%s] because the volume resize operation failed: %s", volume.getUuid(), e.getMessage())); } } } @@ -2372,7 +2372,7 @@ public Volume changeDiskOfferingForVolumeInternal(Long volumeId, Long newDiskOff if (volumeMigrateRequired) { logger.warn(String.format("Volume change offering operation succeeded for volume ID: %s but volume resize operation failed, so please try resize volume operation separately", volume.getUuid())); } else { - throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume ID: %s due to resize volume operation failed", volume.getUuid())); + throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume ID: %s due to resize volume operation failed: %s", volume.getUuid(), e.getMessage()), e); } } } diff --git a/ui/src/views/storage/ChangeOfferingForVolume.vue b/ui/src/views/storage/ChangeOfferingForVolume.vue index 66a3cf1442e4..7a7109f5ae1d 100644 --- a/ui/src/views/storage/ChangeOfferingForVolume.vue +++ b/ui/src/views/storage/ChangeOfferingForVolume.vue @@ -158,7 +158,7 @@ export default { miniops: [{ type: 'number', validator: async (rule, value) => { - if (value && (isNaN(value) || value <= 0)) { + if (value && (isNaN(value) || Number(value) < 0)) { return Promise.reject(this.$t('message.error.number')) } return Promise.resolve() diff --git a/ui/src/views/storage/CreateVolume.vue b/ui/src/views/storage/CreateVolume.vue index ae203bdfa9b8..67431f739d25 100644 --- a/ui/src/views/storage/CreateVolume.vue +++ b/ui/src/views/storage/CreateVolume.vue @@ -311,7 +311,7 @@ export default { size: [{ required: true, message: this.$t('message.error.custom.disk.size') }], miniops: [{ validator: async (rule, value) => { - if (value && (isNaN(value) || value <= 0)) { + if (value && (isNaN(value) || Number(value) < 0)) { return Promise.reject(this.$t('message.error.number')) } return Promise.resolve() From 8f2175f08d9438117c7f271096504175fadaae47 Mon Sep 17 00:00:00 2001 From: "Gupta, Surya" Date: Tue, 13 Oct 2026 19:37:44 +0530 Subject: [PATCH 2/7] [CSTACKEX-328] Fix Issues --- .../driver/OntapPrimaryDatastoreDriver.java | 4 +- .../storage/utils/OntapStorageConstants.java | 3 +- .../OntapPrimaryDatastoreDriverTest.java | 49 ++++++++++++++++++- 3 files changed, 51 insertions(+), 5 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 bd35fc4f9112..60c5ba169a38 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 @@ -327,8 +327,8 @@ private boolean validateIops(StorageStrategy storageStrategy, Map 0 && max > 0 && min > max) { - throw new CloudRuntimeException("Minimum IOPS cannot be greater than maximum IOPS"); + if (min > 0 && max > 0 && min >= max) { + throw new CloudRuntimeException("Minimum IOPS cannot be greater than or equal to maximum IOPS"); } if (min > 0) { String isAff = details.get(OntapStorageConstants.IS_AFF); diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java index 6fb66d20c215..c12fe7325761 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java @@ -129,8 +129,7 @@ public class OntapStorageConstants { public static final String LUN_DOT_SERIAL_NUMBER = "lun.serial_number"; public static final String QOS_POLICY_UUID = "qosPolicyUuid"; public static final String IS_AFF = "isAFF"; - public static final String MIN_IOPS_NOT_SUPPORTED_ON_FAS = - "Minimum IOPS is not supported on FAS/non-AFF ONTAP platforms; only maximum IOPS is supported"; + public static final String MIN_IOPS_NOT_SUPPORTED_ON_FAS = "Minimum IOPS must be 0 for FAS. Only a Maximum IOPS limit is supported"; public static final String QOS_POLICY_NONE = "none"; public static final String QOS_POLICY_NAME_PREFIX = "cs_"; public static final String QOS_POLICY_NAME_TO = "to_"; 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 67820a86d70a..bd9efe87680f 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 @@ -2036,6 +2036,30 @@ void testResize_MinIopsBeyondPoolCapacity_Fails() { verify(sanStrategy, never()).updateCloudStackVolume(any()); } + @Test + void testResize_MinIopsEqualToMaxIops_Fails() { + long currentSize = 4L * 1024 * 1024 * 1024; + stubResizeVolumeRecord(currentSize); + when(volumeInfo.getpayload()).thenReturn( + new ResizeVolumePayload(currentSize, 100L, 100L, null, false, null, null, true)); + when(storagePool.getCapacityIops()).thenReturn(null); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())) + .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( + "Minimum IOPS cannot be greater than or equal to maximum IOPS")); + verify(sanStrategy, never()).createVolumeQosPolicy(any(), any(), any()); + verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong()); + } + } + @Test void testCreateAsync_MinIopsBeyondPoolCapacity_FailsWithCapacityDetails() { VolumeVO allocatedVolume = mock(VolumeVO.class); @@ -2385,7 +2409,30 @@ void testCreateAsync_MinIopsGreaterThanMaxIops_Fails() { verify(createCallback).complete(resultCaptor.capture()); assertFalse(resultCaptor.getValue().isSuccess()); assertTrue(resultCaptor.getValue().getResult().contains( - "Minimum IOPS cannot be greater than maximum IOPS")); + "Minimum IOPS cannot be greater than or equal to maximum IOPS")); + verify(sanStrategy, never()).createVolumeQosPolicy(any(), any(), any()); + verify(sanStrategy, never()).createCloudStackVolume(any()); + } + } + + @Test + void testCreateAsync_MinIopsEqualToMaxIops_Fails() { + stubIscsiVolumeCreate(); + when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK); + when(volumeInfo.getMinIops()).thenReturn(100L); + when(volumeInfo.getMaxIops()).thenReturn(100L); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())) + .thenReturn(sanStrategy); + + driver.createAsync(dataStore, volumeInfo, createCallback); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class); + verify(createCallback).complete(resultCaptor.capture()); + assertFalse(resultCaptor.getValue().isSuccess()); + assertTrue(resultCaptor.getValue().getResult().contains( + "Minimum IOPS cannot be greater than or equal to maximum IOPS")); verify(sanStrategy, never()).createVolumeQosPolicy(any(), any(), any()); verify(sanStrategy, never()).createCloudStackVolume(any()); } From b12dc903f0e582216aeb94921b1c406365fee8b9 Mon Sep 17 00:00:00 2001 From: "Gupta, Surya" Date: Wed, 14 Oct 2026 20:17:21 +0530 Subject: [PATCH 3/7] [CSTACKEX-328] Fix Build Issue --- .../service/UnifiedSANStrategyTest.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) 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 0ffb0166d960..2ffd435bfd17 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 @@ -1344,17 +1344,37 @@ void testSetOntapStorage() { } @Test - void testUpdateCloudStackVolume_ReturnsNull() { + void testUpdateCloudStackVolume_AppliesQosPolicy() { Lun lun = new Lun(); lun.setUuid("lun-uuid-123"); VolumeQosPolicy qosPolicy = new VolumeQosPolicy(); qosPolicy.setName("cs_0_to_5000_iops_svm1"); + qosPolicy.setUuid("qos-uuid"); lun.setQosPolicy(qosPolicy); CloudStackVolume request = new CloudStackVolume(); request.setLun(lun); - assertNull(unifiedSANStrategy.updateCloudStackVolume(request)); - assertNull(unifiedSANStrategy.updateCloudStackVolume(new CloudStackVolume())); + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password")) + .thenReturn(authHeader); + when(sanFeignClient.updateLun(eq(authHeader), eq("lun-uuid-123"), any(Lun.class))) + .thenReturn(null); + + CloudStackVolume result = unifiedSANStrategy.updateCloudStackVolume(request); + + assertSame(request, result); + ArgumentCaptor lunCaptor = ArgumentCaptor.forClass(Lun.class); + verify(sanFeignClient).updateLun(eq(authHeader), eq("lun-uuid-123"), lunCaptor.capture()); + assertEquals("cs_0_to_5000_iops_svm1", lunCaptor.getValue().getQosPolicy().getName()); + assertEquals("qos-uuid", lunCaptor.getValue().getQosPolicy().getUuid()); + } + } + + @Test + void testUpdateCloudStackVolume_InvalidRequest_ThrowsException() { + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, + () -> unifiedSANStrategy.updateCloudStackVolume(new CloudStackVolume())); + assertTrue(ex.getMessage().contains("Invalid iSCSI volume QoS update request")); verify(sanFeignClient, never()).updateLun(any(), any(), any()); } From 09bf05ee22245762d2d2d87e903b08770c6f71b5 Mon Sep 17 00:00:00 2001 From: "Gupta, Surya" Date: Wed, 14 Oct 2026 21:51:56 +0530 Subject: [PATCH 4/7] [CSTACKEX-328] Fix Review Comments --- ui/src/views/storage/ChangeOfferingForVolume.vue | 2 +- ui/src/views/storage/CreateVolume.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/views/storage/ChangeOfferingForVolume.vue b/ui/src/views/storage/ChangeOfferingForVolume.vue index 7a7109f5ae1d..29d00dfe512e 100644 --- a/ui/src/views/storage/ChangeOfferingForVolume.vue +++ b/ui/src/views/storage/ChangeOfferingForVolume.vue @@ -158,7 +158,7 @@ export default { miniops: [{ type: 'number', validator: async (rule, value) => { - if (value && (isNaN(value) || Number(value) < 0)) { + if (value && (isNaN(value) || value < 0)) { return Promise.reject(this.$t('message.error.number')) } return Promise.resolve() diff --git a/ui/src/views/storage/CreateVolume.vue b/ui/src/views/storage/CreateVolume.vue index 67431f739d25..073ff204bba1 100644 --- a/ui/src/views/storage/CreateVolume.vue +++ b/ui/src/views/storage/CreateVolume.vue @@ -311,7 +311,7 @@ export default { size: [{ required: true, message: this.$t('message.error.custom.disk.size') }], miniops: [{ validator: async (rule, value) => { - if (value && (isNaN(value) || Number(value) < 0)) { + if (value && (isNaN(value) || value < 0)) { return Promise.reject(this.$t('message.error.number')) } return Promise.resolve() From 27e863e75c21044417837b301bb3724d702cefd3 Mon Sep 17 00:00:00 2001 From: "Gupta, Surya" Date: Wed, 14 Oct 2026 22:43:52 +0530 Subject: [PATCH 5/7] [CSTACKEX-328] Fix Build Issue --- .../storage/driver/OntapPrimaryDatastoreDriverTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 bd9efe87680f..3df349458056 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 @@ -1072,7 +1072,10 @@ void testCreateAsync_VolumeClonedFromSnapshot_IscsiWithIops_SetsQosOnLunCreate() @Test void testCreateAsync_VolumeClonedFromSnapshot_NfsWithIops_AttachesQosAfterClone() { stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.NFS3.name()); + storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name()); storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flex-uuid"); + when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + when(volumeInfo.getUuid()).thenReturn("new-volume-uuid"); when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK); when(volumeInfo.getMinIops()).thenReturn(100L); when(volumeInfo.getMaxIops()).thenReturn(200L); @@ -2916,7 +2919,7 @@ private void stubResizeVolumeRecord(long currentSize) { when(volumeVO.getId()).thenReturn(100L); when(volumeVO.getSize()).thenReturn(currentSize); lenient().when(volumeVO.getPath()).thenReturn("/vol/vol1/lun1"); - when(volumeDao.update(100L, volumeVO)).thenReturn(true); + lenient().when(volumeDao.update(100L, volumeVO)).thenReturn(true); when(storagePoolDao.findById(1L)).thenReturn(storagePool); when(storagePool.getId()).thenReturn(1L); when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails); From 8a24278bd472173fa00817161220f7946c7ea2c8 Mon Sep 17 00:00:00 2001 From: "Gupta, Surya" Date: Thu, 15 Oct 2026 23:01:42 +0530 Subject: [PATCH 6/7] [CSTACKEX-328] Resolve review comments --- .../apache/cloudstack/storage/service/UnifiedNASStrategy.java | 2 +- .../apache/cloudstack/storage/service/UnifiedSANStrategy.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 1c8fae171b67..e7ca26cbe7a9 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 @@ -139,7 +139,7 @@ public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateI public CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) { if (cloudstackVolume == null || cloudstackVolume.getVolumeInfo() == null || cloudstackVolume.getFlexVolumeUuid() == null || cloudstackVolume.getFile() == null) { - throw new CloudRuntimeException("Invalid NFS volume QoS update request"); + throw new CloudRuntimeException("Invalid NFS volume request"); } FileInfo fileInfo = new FileInfo(); fileInfo.setQosPolicy(cloudstackVolume.getFile().getQosPolicy()); 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 397e64b782e1..9f9bbe1789ff 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 @@ -198,7 +198,7 @@ private void bestEffortDeleteTemplateCacheLun(String svmName, String lunName, St public CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) { if (cloudstackVolume == null || cloudstackVolume.getLun() == null || cloudstackVolume.getLun().getUuid() == null) { - throw new CloudRuntimeException("Invalid iSCSI volume QoS update request"); + throw new CloudRuntimeException("Invalid iSCSI volume request"); } Lun lunUpdate = new Lun(); lunUpdate.setQosPolicy(cloudstackVolume.getLun().getQosPolicy()); From 90e193a4734f2da14c31050c9269823c1f02d366 Mon Sep 17 00:00:00 2001 From: "Gupta, Surya" Date: Fri, 16 Oct 2026 15:16:33 +0530 Subject: [PATCH 7/7] [CSTACKEX-328] Fix build failure and resolve new comments --- .../storage/service/UnifiedNASStrategyTest.java | 15 ++++++++++++++- .../storage/service/UnifiedSANStrategyTest.java | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) 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 199a847e2136..86fdf1ff4a44 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 @@ -330,10 +330,23 @@ public void testUpdateCloudStackVolume_AppliesQosPolicy() { @Test public void testUpdateCloudStackVolume_InvalidRequest_ThrowsException() { - assertThrows(CloudRuntimeException.class, () -> strategy.updateCloudStackVolume(new CloudStackVolume())); + CloudStackVolume missingFile = new CloudStackVolume(); + missingFile.setVolumeInfo(mock(VolumeInfo.class)); + missingFile.setFlexVolumeUuid("flex-uuid"); + + assertInvalidNfsVolumeUpdate(null); + assertInvalidNfsVolumeUpdate(new CloudStackVolume()); + assertInvalidNfsVolumeUpdate(missingFile); + verify(nasFeignClient, never()).updateFile(anyString(), anyString(), anyString(), any(FileInfo.class)); } + private void assertInvalidNfsVolumeUpdate(CloudStackVolume request) { + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, + () -> strategy.updateCloudStackVolume(request)); + assertEquals("Invalid NFS volume request", ex.getMessage()); + } + // Test createCloudStackVolume - Volume Not Found @Test public void testCreateCloudStackVolume_VolumeNotFound() { 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 2ffd435bfd17..fabdf321e020 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 @@ -1372,12 +1372,22 @@ void testUpdateCloudStackVolume_AppliesQosPolicy() { @Test void testUpdateCloudStackVolume_InvalidRequest_ThrowsException() { - CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, - () -> unifiedSANStrategy.updateCloudStackVolume(new CloudStackVolume())); - assertTrue(ex.getMessage().contains("Invalid iSCSI volume QoS update request")); + CloudStackVolume lunWithoutUuid = new CloudStackVolume(); + lunWithoutUuid.setLun(new Lun()); + + assertInvalidIscsiVolumeUpdate(null); + assertInvalidIscsiVolumeUpdate(new CloudStackVolume()); + assertInvalidIscsiVolumeUpdate(lunWithoutUuid); + verify(sanFeignClient, never()).updateLun(any(), any(), any()); } + private void assertInvalidIscsiVolumeUpdate(CloudStackVolume request) { + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, + () -> unifiedSANStrategy.updateCloudStackVolume(request)); + assertEquals("Invalid iSCSI volume request", ex.getMessage()); + } + @Test void testUpdateAccessGroup_ReturnsNull() { AccessGroup accessGroup = new AccessGroup();