diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java index 65a3d6a7063a..eb89115cf464 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java @@ -73,6 +73,10 @@ public class ResizeVolumeCmd extends BaseAsyncCmd implements UserCmd { description = "new disk offering id") private Long newDiskOfferingId; + @Parameter(name = ApiConstants.AUTO_MIGRATE, type = CommandType.BOOLEAN, required = false, + description = "Flag to allow automatic migration of the volume to another suitable storage pool that accommodates the new size", since = "4.20.1") + private Boolean autoMigrate; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -129,6 +133,10 @@ public Long getNewDiskOfferingId() { return newDiskOfferingId; } + public boolean getAutoMigrate() { + return autoMigrate == null ? false : autoMigrate; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java b/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java index 1c3edad886bb..e1bb10f5d268 100644 --- a/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java +++ b/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java @@ -40,6 +40,7 @@ public interface CapacityManager { static final String StorageCapacityDisableThresholdCK = "pool.storage.capacity.disablethreshold"; static final String StorageOverprovisioningFactorCK = "storage.overprovisioning.factor"; static final String StorageAllocatedCapacityDisableThresholdCK = "pool.storage.allocated.capacity.disablethreshold"; + static final String StorageAllocatedCapacityDisableThresholdForVolumeResizeCK = "pool.storage.allocated.resize.capacity.disablethreshold"; static final ConfigKey CpuOverprovisioningFactor = new ConfigKey<>( @@ -118,6 +119,17 @@ public interface CapacityManager { "Percentage (as a value between 0 and 1) of secondary storage capacity threshold.", true); + static final ConfigKey StorageAllocatedCapacityDisableThresholdForVolumeSize = + new ConfigKey<>( + ConfigKey.CATEGORY_ALERT, + Double.class, + StorageAllocatedCapacityDisableThresholdForVolumeResizeCK, + "0.90", + "Percentage (as a value between 0 and 1) of allocated storage utilization above which allocators will disable using the pool for volume resize. " + + "This is applicable only when volume.resize.allowed.beyond.allocation is set to true.", + true, + ConfigKey.Scope.Zone); + public boolean releaseVmCapacity(VirtualMachine vm, boolean moveFromReserved, boolean moveToReservered, Long hostId); void allocateVmCapacity(VirtualMachine vm, boolean fromLastHost); diff --git a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java index c3909bc56b0d..b51536688990 100644 --- a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java +++ b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java @@ -209,6 +209,11 @@ public interface StorageManager extends StorageService { ConfigKey HEURISTICS_SCRIPT_TIMEOUT = new ConfigKey<>("Advanced", Long.class, "heuristics.script.timeout", "3000", "The maximum runtime, in milliseconds, to execute the heuristic rule; if it is reached, a timeout will happen.", true); + ConfigKey AllowVolumeReSizeBeyondAllocation = new ConfigKey("Advanced", Boolean.class, "volume.resize.allowed.beyond.allocation", "false", + "Determines whether volume size can exceed the pool capacity allocation disable threshold (pool.storage.allocated.capacity.disablethreshold) " + + "when resize a volume upto resize capacity disable threshold (pool.storage.allocated.resize.capacity.disablethreshold)", + true, ConfigKey.Scope.Zone); + /** * should we execute in sequence not involving any storages? * @return tru if commands should execute in sequence diff --git a/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java b/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java index 421c980b2096..08f055ca3a35 100644 --- a/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java +++ b/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java @@ -1254,6 +1254,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[] {CpuOverprovisioningFactor, MemOverprovisioningFactor, StorageCapacityDisableThreshold, StorageOverprovisioningFactor, - StorageAllocatedCapacityDisableThreshold, StorageOperationsExcludeCluster, ImageStoreNFSVersion, SecondaryStorageCapacityThreshold}; + StorageAllocatedCapacityDisableThreshold, StorageOperationsExcludeCluster, ImageStoreNFSVersion, SecondaryStorageCapacityThreshold, + StorageAllocatedCapacityDisableThresholdForVolumeSize }; } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 25cbd10de0a4..14855e17ce1a 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -590,6 +590,7 @@ protected void weightBasedParametersForValidation() { weightBasedParametersForValidation.add(Config.LocalStorageCapacityThreshold.key()); weightBasedParametersForValidation.add(CapacityManager.StorageAllocatedCapacityDisableThreshold.key()); weightBasedParametersForValidation.add(CapacityManager.StorageCapacityDisableThreshold.key()); + weightBasedParametersForValidation.add(CapacityManager.StorageAllocatedCapacityDisableThresholdForVolumeSize.key()); weightBasedParametersForValidation.add(DeploymentClusterPlanner.ClusterCPUCapacityDisableThreshold.key()); weightBasedParametersForValidation.add(DeploymentClusterPlanner.ClusterMemoryCapacityDisableThreshold.key()); weightBasedParametersForValidation.add(Config.AgentLoadThreshold.key()); diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index 2ed6be39b543..f2e03cddb7cd 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -3101,7 +3101,7 @@ public boolean storagePoolHasEnoughSpaceForResize(StoragePool pool, long current } else { final StoragePoolVO poolVO = _storagePoolDao.findById(pool.getId()); final long allocatedSizeWithTemplate = _capacityMgr.getAllocatedPoolCapacity(poolVO, null); - return checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize); + return checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, true); } } @@ -3164,6 +3164,10 @@ public boolean isStoragePoolCompliantWithStoragePolicy(List storageAllocatedThreshold) { if (logger.isDebugEnabled()) { logger.debug("Insufficient un-allocated capacity on: " + pool.getId() + " for storage allocation since its allocated percentage: " + usedPercentage - + " has crossed the allocated pool.storage.allocated.capacity.disablethreshold: " + storageAllocatedThreshold + ", skipping this pool"); + + " has crossed the allocated pool.storage.allocated.capacity.disablethreshold: " + storageAllocatedThreshold); + } + if (!forVolumeResize) { + return false; + } + if (!AllowVolumeReSizeBeyondAllocation.valueIn(pool.getDataCenterId())) { + logger.debug(String.format("Skipping the pool %s as %s is false", pool, AllowVolumeReSizeBeyondAllocation.key())); + return false; } - return false; + double storageAllocatedThresholdForResize = CapacityManager.StorageAllocatedCapacityDisableThresholdForVolumeSize.valueIn(pool.getDataCenterId()); + if (usedPercentage > storageAllocatedThresholdForResize) { + logger.debug(String.format("Skipping the pool %s since its allocated percentage: %s has crossed the allocated %s: %s", + pool, usedPercentage, CapacityManager.StorageAllocatedCapacityDisableThresholdForVolumeSize.key(), storageAllocatedThresholdForResize)); + return false; + } } if (totalOverProvCapacity < (allocatedSizeWithTemplate + totalAskingSize)) { @@ -4050,7 +4066,8 @@ public ConfigKey[] getConfigKeys() { MountDisabledStoragePool, VmwareCreateCloneFull, VmwareAllowParallelExecution, - DataStoreDownloadFollowRedirects + DataStoreDownloadFollowRedirects, + AllowVolumeReSizeBeyondAllocation }; } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index cb859f2dde91..b12575a8a088 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -1093,6 +1093,7 @@ public VolumeVO resizeVolume(ResizeVolumeCmd cmd) throws ResourceAllocationExcep Long newMaxIops = cmd.getMaxIops(); Integer newHypervisorSnapshotReserve = null; boolean shrinkOk = cmd.isShrinkOk(); + boolean autoMigrateVolume = cmd.getAutoMigrate(); VolumeVO volume = _volsDao.findById(cmd.getEntityId()); if (volume == null) { @@ -1154,8 +1155,6 @@ public VolumeVO resizeVolume(ResizeVolumeCmd cmd) throws ResourceAllocationExcep newSize = volume.getSize(); } - newMinIops = cmd.getMinIops(); - if (newMinIops != null) { if (!volume.getVolumeType().equals(Volume.Type.ROOT) && (diskOffering.isCustomizedIops() == null || !diskOffering.isCustomizedIops())) { throw new InvalidParameterValueException("The current disk offering does not support customization of the 'Min IOPS' parameter."); @@ -1165,8 +1164,6 @@ public VolumeVO resizeVolume(ResizeVolumeCmd cmd) throws ResourceAllocationExcep newMinIops = volume.getMinIops(); } - newMaxIops = cmd.getMaxIops(); - if (newMaxIops != null) { if (!volume.getVolumeType().equals(Volume.Type.ROOT) && (diskOffering.isCustomizedIops() == null || !diskOffering.isCustomizedIops())) { throw new InvalidParameterValueException("The current disk offering does not support customization of the 'Max IOPS' parameter."); @@ -1288,6 +1285,54 @@ public VolumeVO resizeVolume(ResizeVolumeCmd cmd) throws ResourceAllocationExcep return volume; } + Long newDiskOfferingId = newDiskOffering != null ? newDiskOffering.getId() : diskOffering.getId(); + + boolean volumeMigrateRequired = false; + List suitableStoragePoolsWithEnoughSpace = null; + StoragePoolVO storagePool = _storagePoolDao.findById(volume.getPoolId()); + if (!storageMgr.storagePoolHasEnoughSpaceForResize(storagePool, currentSize, newSize)) { + if (!autoMigrateVolume) { + throw new CloudRuntimeException(String.format("Failed to resize volume %s since the storage pool does not have enough space to accommodate new size for the volume %s, try with automigrate set to true in order to check in the other suitable pools for the new size and then migrate & resize volume there.", volume.getUuid(), volume.getName())); + } + Pair, List> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOfferingId, currentSize, newMinIops, newMaxIops, true, false); + List suitableStoragePools = poolsPair.second(); + if (CollectionUtils.isEmpty(poolsPair.first()) && CollectionUtils.isEmpty(poolsPair.second())) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resize failed for volume ID: %s as no suitable pool(s) found for migrating to support new disk offering or new size", volume.getUuid())); + } + final Long newSizeFinal = newSize; + suitableStoragePoolsWithEnoughSpace = suitableStoragePools.stream().filter(pool -> storageMgr.storagePoolHasEnoughSpaceForResize(pool, 0L, newSizeFinal)).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(suitableStoragePoolsWithEnoughSpace)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume resize failed for volume ID: %s as no suitable pool(s) with enough space found.", volume.getUuid())); + } + Collections.shuffle(suitableStoragePoolsWithEnoughSpace); + volumeMigrateRequired = true; + } + + boolean volumeResizeRequired = false; + if (currentSize != newSize || !compareEqualsIncludingNullOrZero(newMaxIops, volume.getMaxIops()) || !compareEqualsIncludingNullOrZero(newMinIops, volume.getMinIops())) { + volumeResizeRequired = true; + } + if (!volumeMigrateRequired && !volumeResizeRequired && newDiskOffering != null) { + _volsDao.updateDiskOffering(volume.getId(), newDiskOffering.getId()); + volume = _volsDao.findById(volume.getId()); + updateStorageWithTheNewDiskOffering(volume, newDiskOffering); + + return volume; + } + + if (volumeMigrateRequired) { + MigrateVolumeCmd migrateVolumeCmd = new MigrateVolumeCmd(volume.getId(), suitableStoragePoolsWithEnoughSpace.get(0).getId(), newDiskOfferingId, true); + try { + Volume result = migrateVolume(migrateVolumeCmd); + volume = (result != null) ? _volsDao.findById(result.getId()) : null; + if (volume == null) { + throw new CloudRuntimeException(String.format("Volume resize operation failed for volume ID: %s as migration failed to storage pool %s accommodating new size", volume.getUuid(), suitableStoragePoolsWithEnoughSpace.get(0).getId())); + } + } catch (Exception e) { + throw new CloudRuntimeException(String.format("Volume resize operation failed for volume ID: %s as migration failed to storage pool %s accommodating new size", volume.getUuid(), suitableStoragePoolsWithEnoughSpace.get(0).getId())); + } + } + UserVmVO userVm = _userVmDao.findById(volume.getInstanceId()); if (userVm != null) { @@ -1973,6 +2018,7 @@ public Outcome checkAndRepairVolumeThroughJobQueue(final Long vmId, final public Volume changeDiskOfferingForVolume(ChangeOfferingForVolumeCmd cmd) throws ResourceAllocationException { Long newSize = cmd.getSize(); Long newMinIops = cmd.getMinIops(); + Long newMaxIops = cmd.getMaxIops(); Long newDiskOfferingId = cmd.getNewDiskOfferingId(); boolean shrinkOk = cmd.isShrinkOk(); @@ -2055,7 +2101,7 @@ public Volume changeDiskOfferingForVolumeInternal(Long volumeId, Long newDiskOff StoragePoolVO existingStoragePool = _storagePoolDao.findById(volume.getPoolId()); - Pair, List> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOffering.getId(), newSize, newMinIops, newMaxIops, true, false); + Pair, List> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOffering.getId(), currentSize, newMinIops, newMaxIops, true, false); List suitableStoragePools = poolsPair.second(); if (!suitableStoragePools.stream().anyMatch(p -> (p.getId() == existingStoragePool.getId()))) { @@ -2077,10 +2123,16 @@ public Volume changeDiskOfferingForVolumeInternal(Long volumeId, Long newDiskOff if (CollectionUtils.isEmpty(poolsPair.first()) && CollectionUtils.isEmpty(poolsPair.second())) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume change offering operation failed for volume ID: %s as no suitable pool(s) found for migrating to support new disk offering", volume.getUuid())); } - Collections.shuffle(suitableStoragePools); - MigrateVolumeCmd migrateVolumeCmd = new MigrateVolumeCmd(volume.getId(), suitableStoragePools.get(0).getId(), newDiskOffering.getId(), true); + final Long newSizeFinal = newSize; + List suitableStoragePoolsWithEnoughSpace = suitableStoragePools.stream().filter(pool -> storageMgr.storagePoolHasEnoughSpaceForResize(pool, 0L, newSizeFinal)).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(suitableStoragePoolsWithEnoughSpace)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Volume change offering operation failed for volume ID: %s as no suitable pool(s) with enough space found for volume migration.", volume.getUuid())); + } + Collections.shuffle(suitableStoragePoolsWithEnoughSpace); + MigrateVolumeCmd migrateVolumeCmd = new MigrateVolumeCmd(volume.getId(), suitableStoragePoolsWithEnoughSpace.get(0).getId(), newDiskOffering.getId(), true); try { - volume = (VolumeVO) migrateVolume(migrateVolumeCmd); + Volume result = migrateVolume(migrateVolumeCmd); + volume = (result != null) ? _volsDao.findById(result.getId()) : null; if (volume == null) { throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume ID: %s migration failed to storage pool %s", volume.getUuid(), suitableStoragePools.get(0).getId())); } diff --git a/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java b/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java index fcbae4f339c7..98a6e203ed78 100644 --- a/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.storage; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -42,6 +43,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.framework.config.ConfigDepot; +import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.apache.cloudstack.storage.command.CheckDataStoreStoragePolicyComplainceCommand; @@ -756,4 +758,81 @@ public void testGetStoragePoolMountFailureReason() { String failureReason = storageManagerImpl.getStoragePoolMountFailureReason(error); Assert.assertEquals(failureReason, "An incorrect mount option was specified"); } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + private Long testCheckPoolforSpaceForResizeSetup(StoragePoolVO pool, Long allocatedSizeWithTemplate) { + Long poolId = 10L; + Long zoneId = 2L; + + Long capacityBytes = (long) (allocatedSizeWithTemplate / Double.valueOf(CapacityManager.StorageAllocatedCapacityDisableThreshold.defaultValue()) + / Double.valueOf(CapacityManager.StorageOverprovisioningFactor.defaultValue())); + Long maxAllocatedSizeForResize = (long) (capacityBytes * Double.valueOf(CapacityManager.StorageOverprovisioningFactor.defaultValue()) + * Double.valueOf(CapacityManager.StorageAllocatedCapacityDisableThresholdForVolumeSize.defaultValue())); + + System.out.println("maxAllocatedSizeForResize = " + maxAllocatedSizeForResize); + System.out.println("allocatedSizeWithTemplate = " + allocatedSizeWithTemplate); + + Mockito.when(pool.getId()).thenReturn(poolId); + Mockito.when(pool.getCapacityBytes()).thenReturn(capacityBytes); + Mockito.when(pool.getDataCenterId()).thenReturn(zoneId); + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(pool); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + return maxAllocatedSizeForResize - allocatedSizeWithTemplate; + } + + @Test + public void testCheckPoolforSpaceForResize1() { + StoragePoolVO pool = Mockito.mock(StoragePoolVO.class); + Long allocatedSizeWithTemplate = 100L * 1024 * 1024 * 1024; + + Long maxAskingSize = testCheckPoolforSpaceForResizeSetup(pool, allocatedSizeWithTemplate); + Long totalAskingSize = maxAskingSize / 2; + + boolean result = storageManagerImpl.checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, false); + Assert.assertFalse(result); + } + + @Test + public void testCheckPoolforSpaceForResize2() { + StoragePoolVO pool = Mockito.mock(StoragePoolVO.class); + Long allocatedSizeWithTemplate = 100L * 1024 * 1024 * 1024; + + Long maxAskingSize = testCheckPoolforSpaceForResizeSetup(pool, allocatedSizeWithTemplate); + Long totalAskingSize = maxAskingSize / 2; + + boolean result = storageManagerImpl.checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, true); + Assert.assertFalse(result); + } + + @Test + public void testCheckPoolforSpaceForResize3() throws NoSuchFieldException, IllegalAccessException { + StoragePoolVO pool = Mockito.mock(StoragePoolVO.class); + Long allocatedSizeWithTemplate = 100L * 1024 * 1024 * 1024; + + Long maxAskingSize = testCheckPoolforSpaceForResizeSetup(pool, allocatedSizeWithTemplate); + Long totalAskingSize = maxAskingSize + 1; + overrideDefaultConfigValue(StorageManagerImpl.AllowVolumeReSizeBeyondAllocation, "_defaultValue", "true"); + + boolean result = storageManagerImpl.checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, true); + Assert.assertFalse(result); + } + + @Test + public void testCheckPoolforSpaceForResize4() throws NoSuchFieldException, IllegalAccessException { + StoragePoolVO pool = Mockito.mock(StoragePoolVO.class); + Long allocatedSizeWithTemplate = 100L * 1024 * 1024 * 1024; + + Long maxAskingSize = testCheckPoolforSpaceForResizeSetup(pool, allocatedSizeWithTemplate); + Long totalAskingSize = maxAskingSize / 2; + overrideDefaultConfigValue(StorageManagerImpl.AllowVolumeReSizeBeyondAllocation, "_defaultValue", "true"); + + boolean result = storageManagerImpl.checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, true); + Assert.assertTrue(result); + } } diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 80835891327f..666324d4ed27 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -19,10 +19,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -38,14 +41,17 @@ import java.util.UUID; import java.util.concurrent.ExecutionException; +import com.cloud.server.ManagementService; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd; import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd; import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; @@ -85,6 +91,7 @@ import org.springframework.test.util.ReflectionTestUtils; import com.cloud.api.query.dao.ServiceOfferingJoinDao; +import com.cloud.configuration.ConfigurationManager; import com.cloud.configuration.Resource; import com.cloud.configuration.Resource.ResourceType; import com.cloud.dc.DataCenterVO; @@ -210,6 +217,8 @@ public class VolumeApiServiceImplTest { private StoragePool storagePoolMock; private long storagePoolMockId = 1; @Mock + private DiskOfferingVO diskOfferingMock; + @Mock private DiskOfferingVO newDiskOfferingMock; @Mock @@ -238,10 +247,20 @@ public class VolumeApiServiceImplTest { @Mock private StorageManager storageMgr; + @Mock + private ConfigurationManager _configMgr; + + @Mock + private VolumeOrchestrationService _volumeMgr; + + @Mock + private ManagementService managementService; + private long accountMockId = 456l; private long volumeMockId = 12313l; private long vmInstanceMockId = 1123l; private long volumeSizeMock = 456789921939l; + private long newVolumeSizeMock = 456789930000l; private static long imageStoreId = 10L; private String projectMockUuid = "projectUuid"; @@ -250,6 +269,7 @@ public class VolumeApiServiceImplTest { private long projectMockAccountId = 132329390L; private long diskOfferingMockId = 100203L; + private long newDiskOfferingMockId = 100204L; private long offeringMockId = 31902L; @@ -1820,4 +1840,92 @@ public void testValidationsForCheckVolumeAPIWithInvalidVolumeFormat() { volumeApiServiceImpl.validationsForCheckVolumeOperation(volume); } + + private void testResizeVolumeSetup() throws ExecutionException, InterruptedException { + Long poolId = 11L; + + when(volumeDaoMock.findById(volumeMockId)).thenReturn(volumeVoMock); + when(volumeVoMock.getId()).thenReturn(volumeMockId); + when(volumeDaoMock.getHypervisorType(volumeMockId)).thenReturn(HypervisorType.KVM); + when(volumeVoMock.getState()).thenReturn(Volume.State.Ready); + when(volumeVoMock.getDiskOfferingId()).thenReturn(diskOfferingMockId); + when(_diskOfferingDao.findById(diskOfferingMockId)).thenReturn(diskOfferingMock); + when(_diskOfferingDao.findById(newDiskOfferingMockId)).thenReturn(newDiskOfferingMock); + when(newDiskOfferingMock.getRemoved()).thenReturn(null); + when(diskOfferingMock.getDiskSizeStrictness()).thenReturn(false); + when(newDiskOfferingMock.getDiskSizeStrictness()).thenReturn(false); + when(volumeVoMock.getInstanceId()).thenReturn(null); + when(volumeVoMock.getVolumeType()).thenReturn(Type.DATADISK); + when(newDiskOfferingMock.getDiskSize()).thenReturn(newVolumeSizeMock); + + VolumeInfo volInfo = Mockito.mock(VolumeInfo.class); + when(volumeDataFactoryMock.getVolume(volumeMockId)).thenReturn(volInfo); + DataStore dataStore = Mockito.mock(DataStore.class); + when((volInfo.getDataStore())).thenReturn(dataStore); + + when(volumeVoMock.getPoolId()).thenReturn(poolId); + StoragePoolVO storagePool = Mockito.mock(StoragePoolVO.class); + when(primaryDataStoreDaoMock.findById(poolId)).thenReturn(storagePool); + + Mockito.lenient().doReturn(asyncCallFutureVolumeapiResultMock).when(volumeServiceMock).resize(any(VolumeInfo.class)); + Mockito.doReturn(Mockito.mock(VolumeApiResult.class)).when(asyncCallFutureVolumeapiResultMock).get(); + } + + @Test + public void testResizeVolumeWithEnoughCapacity() throws ResourceAllocationException, ExecutionException, InterruptedException { + ResizeVolumeCmd cmd = new ResizeVolumeCmd(); + ReflectionTestUtils.setField(cmd, "id", volumeMockId); + ReflectionTestUtils.setField(cmd, "newDiskOfferingId", newDiskOfferingMockId); + + testResizeVolumeSetup(); + + when(storageMgr.storagePoolHasEnoughSpaceForResize(any(), nullable(Long.class), nullable(Long.class))).thenReturn(true); + + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + volumeApiServiceImpl.resizeVolume(cmd); + + verify(volumeServiceMock).resize(any(VolumeInfo.class)); + } + } + + @Test + public void testResizeVolumeWithoutEnoughCapacity() throws ResourceAllocationException, ExecutionException, InterruptedException { + ResizeVolumeCmd cmd = new ResizeVolumeCmd(); + ReflectionTestUtils.setField(cmd, "id", volumeMockId); + ReflectionTestUtils.setField(cmd, "newDiskOfferingId", newDiskOfferingMockId); + ReflectionTestUtils.setField(cmd, "autoMigrate", true); + + testResizeVolumeSetup(); + + when(storageMgr.storagePoolHasEnoughSpaceForResize(any(), nullable(Long.class), nullable(Long.class))).thenReturn(false).thenReturn(true); + StoragePoolVO suitableStoragePool = Mockito.mock(StoragePoolVO.class); + Pair, List> poolsPair = new Pair<>(Arrays.asList(suitableStoragePool), Arrays.asList(suitableStoragePool)); + when(managementService.listStoragePoolsForSystemMigrationOfVolume(anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyBoolean(), anyBoolean())).thenReturn(poolsPair); + doReturn(volumeInfoMock).when(volumeApiServiceImpl).migrateVolume(any()); + when(volumeInfoMock.getId()).thenReturn(volumeMockId); + + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + volumeApiServiceImpl.resizeVolume(cmd); + + verify(volumeApiServiceImpl).migrateVolume(any()); + verify(volumeServiceMock).resize(any(VolumeInfo.class)); + } + } + + @Test + public void testResizeVolumeInAllocateState() throws ResourceAllocationException, ExecutionException, InterruptedException { + ResizeVolumeCmd cmd = new ResizeVolumeCmd(); + ReflectionTestUtils.setField(cmd, "id", volumeMockId); + ReflectionTestUtils.setField(cmd, "newDiskOfferingId", newDiskOfferingMockId); + + testResizeVolumeSetup(); + + when(volumeVoMock.getState()).thenReturn(Volume.State.Allocated); + + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + volumeApiServiceImpl.resizeVolume(cmd); + + verify(volumeServiceMock, times(0)).resize(any(VolumeInfo.class)); + } + } } diff --git a/ui/src/views/storage/ResizeVolume.vue b/ui/src/views/storage/ResizeVolume.vue index 38a7ea5cb23b..8d5bd28917a6 100644 --- a/ui/src/views/storage/ResizeVolume.vue +++ b/ui/src/views/storage/ResizeVolume.vue @@ -47,6 +47,15 @@ :checked="shrinkOk" @change="val => { shrinkOk = val }"/> + + + +
{{ $t('label.cancel') }} {{ $t('label.ok') }} @@ -58,9 +67,13 @@ import { ref, reactive, toRaw } from 'vue' import { api } from '@/api' import { mixinForm } from '@/utils/mixin' +import TooltipLabel from '@/components/widgets/TooltipLabel' export default { name: 'ResizeVolume', + components: { + TooltipLabel + }, mixins: [mixinForm], props: { resource: {