From 88ce639255dac458c12d8f56547c63d259265f08 Mon Sep 17 00:00:00 2001 From: Rene Peinthor Date: Tue, 13 May 2025 10:06:35 +0200 Subject: [PATCH] Linstor: implement volume and storage stats (#10850) --- plugins/storage/volume/linstor/CHANGELOG.md | 5 ++ .../LinstorPrimaryDataStoreDriverImpl.java | 78 ++++++++++++++++--- .../util/LinstorConfigurationManager.java | 9 ++- .../storage/datastore/util/LinstorUtil.java | 32 +++++++- pom.xml | 2 +- 5 files changed, 112 insertions(+), 14 deletions(-) diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md index 7e9d754b9f6..2abda3ebc50 100644 --- a/plugins/storage/volume/linstor/CHANGELOG.md +++ b/plugins/storage/volume/linstor/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to Linstor CloudStack plugin will be documented in this file The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2025-05-07] + +### Added +- Implemented storage/volume stats + ## [2025-03-13] ### Fixed diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java index a0cb5d17444..3b384831518 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java @@ -28,11 +28,11 @@ import com.linbit.linstor.api.model.ResourceDefinition; import com.linbit.linstor.api.model.ResourceDefinitionCloneRequest; import com.linbit.linstor.api.model.ResourceDefinitionCloneStarted; import com.linbit.linstor.api.model.ResourceDefinitionCreate; - import com.linbit.linstor.api.model.ResourceDefinitionModify; import com.linbit.linstor.api.model.ResourceGroup; import com.linbit.linstor.api.model.ResourceGroupSpawn; import com.linbit.linstor.api.model.ResourceMakeAvailable; +import com.linbit.linstor.api.model.ResourceWithVolumes; import com.linbit.linstor.api.model.Snapshot; import com.linbit.linstor.api.model.SnapshotRestore; import com.linbit.linstor.api.model.VolumeDefinition; @@ -132,6 +132,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver @Inject private HostDao _hostDao; + private long volumeStatsLastUpdate = 0L; + private final Map> volumeStats = new HashMap<>(); + public LinstorPrimaryDataStoreDriverImpl() { } @@ -401,9 +404,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver } } - private String getRscGrp(StoragePoolVO storagePoolVO) { - return storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ? - storagePoolVO.getUserInfo() : "DfltRscGrp"; + private String getRscGrp(StoragePool storagePool) { + return storagePool.getUserInfo() != null && !storagePool.getUserInfo().isEmpty() ? + storagePool.getUserInfo() : "DfltRscGrp"; } /** @@ -616,7 +619,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver */ private void updateRscGrpIfNecessary(DevelopersApi api, String rscName, String tgtRscGrp) throws ApiException { List rscDfns = api.resourceDefinitionList( - Collections.singletonList(rscName), null, null, null); + Collections.singletonList(rscName), false, null, null, null); if (rscDfns != null && !rscDfns.isEmpty()) { ResourceDefinition rscDfn = rscDfns.get(0); @@ -646,7 +649,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver private void deleteTemplateForProps( DevelopersApi api, String rscName) throws ApiException { List rdList = api.resourceDefinitionList( - Collections.singletonList(rscName), null, null, null); + Collections.singletonList(rscName), false, null, null, null); if (CollectionUtils.isNotEmpty(rdList)) { ResourceDefinitionModify rdm = new ResourceDefinitionModify(); @@ -1504,22 +1507,77 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver @Override public boolean canProvideStorageStats() { - return false; + return true; } @Override public Pair getStorageStats(StoragePool storagePool) { - return null; + s_logger.debug(String.format("Requesting storage stats: %s", storagePool)); + return LinstorUtil.getStorageStats(storagePool.getHostAddress(), getRscGrp(storagePool)); } @Override public boolean canProvideVolumeStats() { - return false; + return LinstorConfigurationManager.VolumeStatsCacheTime.value() > 0; + } + + /** + * Updates the cache map containing current allocated size data. + * @param api Linstor Developers api object + */ + private void fillVolumeStatsCache(DevelopersApi api) { + try { + s_logger.trace("Start volume stats cache update"); + List resources = api.viewResources( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + null, + null, + null); + + List rscDfns = api.resourceDefinitionList( + Collections.emptyList(), true, null, null, null); + + HashMap resSizeMap = new HashMap<>(); + for (ResourceDefinition rscDfn : rscDfns) { + if (CollectionUtils.isNotEmpty(rscDfn.getVolumeDefinitions())) { + resSizeMap.put(rscDfn.getName(), rscDfn.getVolumeDefinitions().get(0).getSizeKib() * 1024); + } + } + + HashMap allocSizeMap = new HashMap<>(); + for (ResourceWithVolumes rsc : resources) { + if (!LinstorUtil.isRscDiskless(rsc) && !rsc.getVolumes().isEmpty()) { + long allocatedBytes = allocSizeMap.getOrDefault(rsc.getName(), 0L); + allocSizeMap.put(rsc.getName(), Math.max(allocatedBytes, rsc.getVolumes().get(0).getAllocatedSizeKib() * 1024)); + } + } + + volumeStats.clear(); + for (Map.Entry entry : allocSizeMap.entrySet()) { + Long reserved = resSizeMap.getOrDefault(entry.getKey(), 0L); + Pair volStat = new Pair<>(entry.getValue(), reserved); + volumeStats.put(entry.getKey(), volStat); + } + volumeStatsLastUpdate = System.currentTimeMillis(); + s_logger.trace("Done volume stats cache update: " + volumeStats.size()); + } catch (ApiException e) { + s_logger.error("Unable to fetch Linstor resources: " + e.getBestMessage()); + } } @Override public Pair getVolumeStats(StoragePool storagePool, String volumeId) { - return null; + final DevelopersApi api = LinstorUtil.getLinstorAPI(storagePool.getHostAddress()); + synchronized (volumeStats) { + long invalidateCacheTime = volumeStatsLastUpdate + + LinstorConfigurationManager.VolumeStatsCacheTime.value() * 1000; + if (invalidateCacheTime < System.currentTimeMillis()) { + fillVolumeStatsCache(api); + } + return volumeStats.get(LinstorUtil.RSC_PREFIX + volumeId); + } } @Override diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java index 90ebf30f7cd..85a0804dbab 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java @@ -24,7 +24,14 @@ public class LinstorConfigurationManager implements Configurable public static final ConfigKey BackupSnapshots = new ConfigKey<>(Boolean.class, "lin.backup.snapshots", "Advanced", "true", "Backup Linstor primary storage snapshots to secondary storage (deleting ps snapshot)", true, ConfigKey.Scope.Global, null); - public static final ConfigKey[] CONFIG_KEYS = new ConfigKey[] { BackupSnapshots }; + public static final ConfigKey VolumeStatsCacheTime = new ConfigKey<>("Advanced", Integer.class, + "lin.volumes.stats.cachetime", "300", + "Cache time of volume stats for Linstor volumes. 0 to disable volume stats", + false); + + public static final ConfigKey[] CONFIG_KEYS = new ConfigKey[] { + BackupSnapshots, VolumeStatsCacheTime + }; @Override public String getConfigComponentName() diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java index e252753502c..60d06590006 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java @@ -195,6 +195,30 @@ public class LinstorUtil { } } + public static Pair getStorageStats(String linstorUrl, String rscGroupName) { + DevelopersApi linstorApi = getLinstorAPI(linstorUrl); + try { + List storagePools = LinstorUtil.getRscGroupStoragePools(linstorApi, rscGroupName); + + long capacity = storagePools.stream() + .filter(sp -> sp.getProviderKind() != ProviderKind.DISKLESS) + .mapToLong(sp -> sp.getTotalCapacity() != null ? sp.getTotalCapacity() : 0L) + .sum() * 1024; // linstor uses kiB + + long used = storagePools.stream() + .filter(sp -> sp.getProviderKind() != ProviderKind.DISKLESS) + .mapToLong(sp -> sp.getTotalCapacity() != null && sp.getFreeCapacity() != null ? + sp.getTotalCapacity() - sp.getFreeCapacity() : 0L) + .sum() * 1024; // linstor uses Kib + s_logger.debug( + String.format("Linstor(%s;%s): storageStats -> %d/%d", linstorUrl, rscGroupName, capacity, used)); + return new Pair<>(capacity, used); + } catch (ApiException apiEx) { + s_logger.error(apiEx.getMessage()); + throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx); + } + } + /** * Check if any resource of the given name is InUse on any host. * @@ -303,7 +327,7 @@ public class LinstorUtil { public static List getRDListStartingWith(DevelopersApi api, String startWith) throws ApiException { - List rscDfns = api.resourceDefinitionList(null, null, null, null); + List rscDfns = api.resourceDefinitionList(null, false, null, null, null); return rscDfns.stream() .filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(startWith.toLowerCase())) @@ -386,7 +410,7 @@ public class LinstorUtil { */ public static ResourceDefinition findResourceDefinition(DevelopersApi api, String rscName, String rscGrpName) throws ApiException { - List rscDfns = api.resourceDefinitionList(null, null, null, null); + List rscDfns = api.resourceDefinitionList(null, false, null, null, null); List rdsStartingWith = rscDfns.stream() .filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(rscName.toLowerCase())) @@ -402,4 +426,8 @@ public class LinstorUtil { return rd.orElseGet(() -> rdsStartingWith.get(0)); } + + public static boolean isRscDiskless(ResourceWithVolumes rsc) { + return rsc.getFlags() != null && rsc.getFlags().contains(ApiConsts.FLAG_DISKLESS); + } } diff --git a/pom.xml b/pom.xml index 483d380cb9c..4662356f203 100644 --- a/pom.xml +++ b/pom.xml @@ -169,7 +169,7 @@ 10.1 2.6.6 0.6.0 - 0.6.0 + 0.6.1 0.10.2 3.4.4_1 4.0.1