From 85076cb0f8fd6a033d0f6570f5149ddd13a145de Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 9 Oct 2024 13:06:33 +0200 Subject: [PATCH] Resize volume: add pool capacity disablethreshold for resize and allow volume auto migration (#492) * server: add global settings for volume resize * resizeVolume: support automigrate * server: fix build errors as it is backported from 4.20/main * Address Suresh's comments * Update api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java Co-authored-by: Suresh Kumar Anaparti * Apple issue-299: address Suresh's comments * Update api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java * UI: add autoMigrate to resizeVolume --------- Co-authored-by: Suresh Kumar Anaparti --- .../command/user/volume/ResizeVolumeCmd.java | 8 +++ .../com/cloud/capacity/CapacityManager.java | 12 ++++ .../com/cloud/storage/StorageManager.java | 5 ++ .../cloud/capacity/CapacityManagerImpl.java | 3 +- .../ConfigurationManagerImpl.java | 1 + .../com/cloud/storage/StorageManagerImpl.java | 25 +++++-- .../cloud/storage/VolumeApiServiceImpl.java | 68 ++++++++++++++++--- ui/src/views/storage/ResizeVolume.vue | 13 ++++ 8 files changed, 122 insertions(+), 13 deletions(-) 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 0daf141ba4a..12562d7e8d0 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 @@ -75,6 +75,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.18.1.2") + private Boolean autoMigrate; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -123,6 +127,10 @@ public class ResizeVolumeCmd extends BaseAsyncCmd implements UserCmd { 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 1c3edad886b..e1bb10f5d26 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 b81dcda6a59..82e883839f0 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 @@ -206,6 +206,11 @@ public interface StorageManager extends StorageService { "Whether HTTP redirect is followed during store downloads for objects such as template, volume etc.", true, ConfigKey.Scope.Global); + 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 044e28a2fb3..c7e84033723 100644 --- a/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java +++ b/server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java @@ -1260,6 +1260,7 @@ public class CapacityManagerImpl extends ManagerBase implements CapacityManager, @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 45632713af6..8bbd92cd934 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -565,6 +565,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati 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 592c5084077..8bdd57cea0c 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -2605,7 +2605,7 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C } 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); } } @@ -2677,6 +2677,10 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C } protected boolean checkPoolforSpace(StoragePool pool, long allocatedSizeWithTemplate, long totalAskingSize) { + return checkPoolforSpace(pool, allocatedSizeWithTemplate, totalAskingSize, false); + } + + protected boolean checkPoolforSpace(StoragePool pool, long allocatedSizeWithTemplate, long totalAskingSize, boolean forVolumeResize) { // allocated space includes templates StoragePoolVO poolVO = _storagePoolDao.findById(pool.getId()); @@ -2709,10 +2713,22 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C if (usedPercentage > storageAllocatedThreshold) { if (s_logger.isDebugEnabled()) { s_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())) { + s_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) { + s_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)) { @@ -3525,7 +3541,8 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C 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 176e91f6e9c..d8bc9e6b9ff 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -31,6 +31,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -1064,6 +1065,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic Long newMaxIops = cmd.getMaxIops(); Integer newHypervisorSnapshotReserve = null; boolean shrinkOk = cmd.isShrinkOk(); + boolean autoMigrateVolume = cmd.getAutoMigrate(); VolumeVO volume = _volsDao.findById(cmd.getEntityId()); if (volume == null) { @@ -1125,8 +1127,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic 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."); @@ -1136,8 +1136,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic 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."); @@ -1259,6 +1257,53 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic 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()); + + 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) { @@ -1938,6 +1983,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic 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(); @@ -2016,7 +2062,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic 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()))) { @@ -2036,10 +2082,16 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic 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/ui/src/views/storage/ResizeVolume.vue b/ui/src/views/storage/ResizeVolume.vue index 38a7ea5cb23..8d5bd28917a 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: {