mirror of https://github.com/apache/cloudstack.git
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 <suresh.anaparti@shapeblue.com> * 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 <suresh.anaparti@shapeblue.com>
This commit is contained in:
parent
7068a6ebeb
commit
85076cb0f8
|
|
@ -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///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -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<Float> 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<Double> 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);
|
||||
|
|
|
|||
|
|
@ -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<Boolean> AllowVolumeReSizeBeyondAllocation = new ConfigKey<Boolean>("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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<? extends StoragePool> 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<? extends StoragePool>, List<? extends StoragePool>> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOfferingId, currentSize, newMinIops, newMaxIops, true, false);
|
||||
List<? extends StoragePool> 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<? extends StoragePool>, List<? extends StoragePool>> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOffering.getId(), newSize, newMinIops, newMaxIops, true, false);
|
||||
Pair<List<? extends StoragePool>, List<? extends StoragePool>> poolsPair = managementService.listStoragePoolsForSystemMigrationOfVolume(volume.getId(), newDiskOffering.getId(), currentSize, newMinIops, newMaxIops, true, false);
|
||||
List<? extends StoragePool> 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<? extends StoragePool> 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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@
|
|||
:checked="shrinkOk"
|
||||
@change="val => { shrinkOk = val }"/>
|
||||
</a-form-item>
|
||||
<a-form-item name="autoMigrate" ref="autoMigrate" :label="$t('label.automigrate.volume')">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.automigrate.volume')" :tooltip="apiParams.automigrate.description"/>
|
||||
</template>
|
||||
<a-switch
|
||||
v-model:checked="form.autoMigrate"
|
||||
:checked="autoMigrate"
|
||||
@change="val => { autoMigrate = val }"/>
|
||||
</a-form-item>
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button :loading="loading" type="primary" ref="submit" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue