From d2499c050298d0bd2c3be879aa70f4513d55b2ec Mon Sep 17 00:00:00 2001 From: Rene Peinthor Date: Mon, 12 May 2025 12:11:26 +0200 Subject: [PATCH 1/2] linstor: update java-linstor to 0.6.1 --- .../datastore/driver/LinstorPrimaryDataStoreDriverImpl.java | 4 ++-- .../apache/cloudstack/storage/datastore/util/LinstorUtil.java | 4 ++-- pom.xml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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 a0cb5d17444b..3dca968f9519 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 @@ -616,7 +616,7 @@ private void resizeResource(DevelopersApi api, String resourceName, long sizeByt */ 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 +646,7 @@ private void updateRscGrpIfNecessary(DevelopersApi api, String rscName, String t 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(); 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 e252753502c8..dc88c7b52df6 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 @@ -303,7 +303,7 @@ public static ApiCallRcList applyAuxProps(DevelopersApi api, String rscName, Str 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 +386,7 @@ public static void setAuxTemplateForProperty(DevelopersApi api, String rscName, */ 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())) diff --git a/pom.xml b/pom.xml index 483d380cb9c3..4662356f203f 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 From e7ac6132d7f77d00d11c3641542baf326ed98d46 Mon Sep 17 00:00:00 2001 From: Rene Peinthor Date: Mon, 12 May 2025 12:13:38 +0200 Subject: [PATCH 2/2] linstor: implement storage and volume stats methods Using storage stats not every agent has to query the controller for current used and capacity storage. Volume stats will show physical used size for thin volume in Cloudstack. This volume stats are cached to not query the Linstor controller each time a volume is asked for. --- plugins/storage/volume/linstor/CHANGELOG.md | 5 ++ .../LinstorPrimaryDataStoreDriverImpl.java | 74 +++++++++++++++++-- .../util/LinstorConfigurationManager.java | 9 ++- .../storage/datastore/util/LinstorUtil.java | 28 +++++++ 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md index 7e9d754b9f60..2abda3ebc50f 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 3dca968f9519..3b384831518c 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.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 @@ private void applyQoSSettings(StoragePoolVO storagePool, DevelopersApi api, Stri } } - 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"; } /** @@ -1504,22 +1507,77 @@ public void takeSnapshot(SnapshotInfo snapshotInfo, AsyncCompletionCallback 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 90ebf30f7cdc..85a0804dbab6 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 dc88c7b52df6..60d065900065 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 static long getCapacityBytes(String linstorUrl, String rscGroupName) { } } + 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. * @@ -402,4 +426,8 @@ public static ResourceDefinition findResourceDefinition(DevelopersApi api, Strin return rd.orElseGet(() -> rdsStartingWith.get(0)); } + + public static boolean isRscDiskless(ResourceWithVolumes rsc) { + return rsc.getFlags() != null && rsc.getFlags().contains(ApiConsts.FLAG_DISKLESS); + } }