diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java index 031d9315cd08..a0d49010097f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java @@ -117,6 +117,10 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co @Param(description = "Max IOPS of the disk volume") private Long maxIops; + @SerializedName("iscustomizediops") + @Param(description = "True if the volume's disk offering uses custom IOPS, false otherwise") + private Boolean customizedIops; + @SerializedName(ApiConstants.CREATED) @Param(description = "The date the disk volume was created") private Date created; @@ -410,6 +414,10 @@ public void setMaxIops(Long maxIops) { this.maxIops = maxIops; } + public void setCustomizedIops(Boolean customizedIops) { + this.customizedIops = customizedIops; + } + public void setCreated(Date created) { this.created = created; } @@ -712,6 +720,10 @@ public Long getMaxIops() { return maxIops; } + public Boolean isCustomizedIops() { + return customizedIops; + } + public Date getCreated() { return created; } diff --git a/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDaoImpl.java index e82ec525f01b..42b12475bd9d 100644 --- a/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/VolumeJoinDaoImpl.java @@ -220,6 +220,12 @@ public VolumeResponse newVolumeResponse(ResponseView view, VolumeJoinVO volume) volResponse.setDiskOfferingName(volume.getDiskOfferingName()); volResponse.setDiskOfferingDisplayText(volume.getDiskOfferingDisplayText()); } + DiskOffering diskOffering = computeOnlyDiskOffering != null + ? computeOnlyDiskOffering + : ApiDBUtils.findDiskOfferingById(volume.getDiskOfferingId()); + if (diskOffering != null) { + volResponse.setCustomizedIops(diskOffering.isCustomizedIops()); + } if (view == ResponseView.Full) { volResponse.setStorageType(volume.isUseLocalStorage() ? ServiceOffering.StorageType.local.toString() : ServiceOffering.StorageType.shared.toString()); diff --git a/server/src/test/java/com/cloud/api/query/dao/VolumeJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/VolumeJoinDaoImplTest.java index 8ac60f4a64c8..b84c9d453aad 100755 --- a/server/src/test/java/com/cloud/api/query/dao/VolumeJoinDaoImplTest.java +++ b/server/src/test/java/com/cloud/api/query/dao/VolumeJoinDaoImplTest.java @@ -16,31 +16,161 @@ // under the License. package com.cloud.api.query.dao; -import com.cloud.api.query.vo.VolumeJoinVO; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.function.Consumer; + +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.context.CallContext; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.api.ApiDBUtils; +import com.cloud.api.ApiResponseHelper; +import com.cloud.api.query.vo.VolumeJoinVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Volume; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; + @RunWith(MockitoJUnitRunner.class) public class VolumeJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseTest { + private static final long DISK_OFFERING_ID = 10L; + @InjectMocks private VolumeJoinDaoImpl _volumeJoinDaoImpl; + @Mock + private AnnotationDao annotationDao; + + @Mock + private AccountManager _accountMgr; + private VolumeJoinVO volume = new VolumeJoinVO(); private VolumeResponse volumeResponse = new VolumeResponse(); @Before public void setup() { - prepareSetup(); + lenient().when(annotationDao.hasAnnotations(anyString(), anyString(), anyBoolean())).thenReturn(false); + lenient().when(_accountMgr.isRootAdmin(eq(1L))).thenReturn(true); } @Test public void testUpdateVolumeTagInfo(){ + prepareSetup(); testUpdateTagInformation(_volumeJoinDaoImpl, volume, volumeResponse); } + @Test + public void testNewVolumeResponseSetsCustomizedIopsForComputeOfferingRoot() { + VolumeJoinVO volumeJoin = mockVolumeWithDiskOffering(); + DiskOfferingVO computeOnlyOffering = mock(DiskOfferingVO.class); + when(computeOnlyOffering.isCustomizedIops()).thenReturn(true); + ServiceOfferingVO serviceOffering = mock(ServiceOfferingVO.class); + when(serviceOffering.getId()).thenReturn(5L); + when(serviceOffering.getName()).thenReturn("FullCustom_Compute"); + when(serviceOffering.getDisplayText()).thenReturn("FullCustom_Compute"); + + VolumeResponse response = newVolumeResponse(volumeJoin, apiDBUtils -> { + apiDBUtils.when(() -> ApiDBUtils.findComputeOnlyDiskOfferingById(DISK_OFFERING_ID)) + .thenReturn(computeOnlyOffering); + apiDBUtils.when(() -> ApiDBUtils.findServiceOfferingByComputeOnlyDiskOffering(DISK_OFFERING_ID, false)) + .thenReturn(serviceOffering); + }); + + assertTrue(response.isCustomizedIops()); + assertEquals("5", response.getServiceOfferingId()); + assertEquals("FullCustom_Compute", response.getServiceOfferingName()); + assertNull(response.getDiskOfferingId()); + } + + @Test + public void testNewVolumeResponseSetsCustomizedIopsForDiskOffering() { + VolumeJoinVO volumeJoin = mockVolumeWithDiskOffering(); + when(volumeJoin.getDiskOfferingUuid()).thenReturn("disk-offering-uuid"); + when(volumeJoin.getDiskOfferingName()).thenReturn("NFS_Disk_Custom75"); + when(volumeJoin.getDiskOfferingDisplayText()).thenReturn("NFS_Disk_Custom75"); + DiskOfferingVO diskOffering = mock(DiskOfferingVO.class); + when(diskOffering.isCustomizedIops()).thenReturn(true); + + VolumeResponse response = newVolumeResponse(volumeJoin, apiDBUtils -> { + apiDBUtils.when(() -> ApiDBUtils.findComputeOnlyDiskOfferingById(DISK_OFFERING_ID)).thenReturn(null); + apiDBUtils.when(() -> ApiDBUtils.findDiskOfferingById(DISK_OFFERING_ID)).thenReturn(diskOffering); + }); + + assertTrue(response.isCustomizedIops()); + assertEquals("disk-offering-uuid", response.getDiskOfferingId()); + assertNull(response.getServiceOfferingId()); + } + + @Test + public void testNewVolumeResponseSetsCustomizedIopsFalseWhenOfferingIsFixed() { + VolumeJoinVO volumeJoin = mockVolumeWithDiskOffering(); + DiskOfferingVO diskOffering = mock(DiskOfferingVO.class); + when(diskOffering.isCustomizedIops()).thenReturn(false); + + VolumeResponse response = newVolumeResponse(volumeJoin, apiDBUtils -> { + apiDBUtils.when(() -> ApiDBUtils.findComputeOnlyDiskOfferingById(DISK_OFFERING_ID)).thenReturn(null); + apiDBUtils.when(() -> ApiDBUtils.findDiskOfferingById(DISK_OFFERING_ID)).thenReturn(diskOffering); + }); + + assertFalse(response.isCustomizedIops()); + } + + private VolumeJoinVO mockVolumeWithDiskOffering() { + VolumeJoinVO volumeJoin = mock(VolumeJoinVO.class); + when(volumeJoin.getUuid()).thenReturn("volume-uuid"); + when(volumeJoin.getName()).thenReturn("ROOT-67"); + when(volumeJoin.getDiskOfferingId()).thenReturn(DISK_OFFERING_ID); + when(volumeJoin.getVmId()).thenReturn(0L); + when(volumeJoin.getState()).thenReturn(Volume.State.Ready); + when(volumeJoin.getVolumeType()).thenReturn(Volume.Type.ROOT); + when(volumeJoin.getTemplateId()).thenReturn(0L); + when(volumeJoin.getTagId()).thenReturn(0L); + when(volumeJoin.getMinIops()).thenReturn(0L); + when(volumeJoin.getMaxIops()).thenReturn(1000L); + when(volumeJoin.getDeleteProtection()).thenReturn(false); + when(volumeJoin.isDisplayVolume()).thenReturn(true); + when(volumeJoin.getKmsWrappedKeyId()).thenReturn(null); + when(volumeJoin.getJobId()).thenReturn(null); + return volumeJoin; + } + + private VolumeResponse newVolumeResponse(VolumeJoinVO volumeJoin, Consumer> apiStubs) { + Account callingAccount = mock(Account.class); + when(callingAccount.getId()).thenReturn(1L); + CallContext callContext = mock(CallContext.class); + when(callContext.getCallingAccount()).thenReturn(callingAccount); + + try (MockedStatic apiDBUtils = mockStatic(ApiDBUtils.class); + MockedStatic callContextMocked = mockStatic(CallContext.class); + MockedStatic apiResponseHelperMocked = mockStatic(ApiResponseHelper.class)) { + apiStubs.accept(apiDBUtils); + callContextMocked.when(CallContext::current).thenReturn(callContext); + apiResponseHelperMocked.when(() -> ApiResponseHelper.populateOwner(any(), any())) + .thenAnswer(invocation -> null); + return _volumeJoinDaoImpl.newVolumeResponse(ResponseView.Restricted, volumeJoin); + } + } + } diff --git a/ui/src/views/storage/ResizeVolume.vue b/ui/src/views/storage/ResizeVolume.vue index 6698bdb72411..372b8d462d73 100644 --- a/ui/src/views/storage/ResizeVolume.vue +++ b/ui/src/views/storage/ResizeVolume.vue @@ -65,7 +65,7 @@