Linstor: implement volume and storage stats (#10850)

This commit is contained in:
Rene Peinthor 2025-05-13 10:06:35 +02:00 committed by GitHub
parent 47a268202b
commit 88ce639255
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 112 additions and 14 deletions

View File

@ -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/), 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). 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] ## [2025-03-13]
### Fixed ### Fixed

View File

@ -28,11 +28,11 @@ import com.linbit.linstor.api.model.ResourceDefinition;
import com.linbit.linstor.api.model.ResourceDefinitionCloneRequest; import com.linbit.linstor.api.model.ResourceDefinitionCloneRequest;
import com.linbit.linstor.api.model.ResourceDefinitionCloneStarted; import com.linbit.linstor.api.model.ResourceDefinitionCloneStarted;
import com.linbit.linstor.api.model.ResourceDefinitionCreate; import com.linbit.linstor.api.model.ResourceDefinitionCreate;
import com.linbit.linstor.api.model.ResourceDefinitionModify; import com.linbit.linstor.api.model.ResourceDefinitionModify;
import com.linbit.linstor.api.model.ResourceGroup; import com.linbit.linstor.api.model.ResourceGroup;
import com.linbit.linstor.api.model.ResourceGroupSpawn; import com.linbit.linstor.api.model.ResourceGroupSpawn;
import com.linbit.linstor.api.model.ResourceMakeAvailable; 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.Snapshot;
import com.linbit.linstor.api.model.SnapshotRestore; import com.linbit.linstor.api.model.SnapshotRestore;
import com.linbit.linstor.api.model.VolumeDefinition; import com.linbit.linstor.api.model.VolumeDefinition;
@ -132,6 +132,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
@Inject @Inject
private HostDao _hostDao; private HostDao _hostDao;
private long volumeStatsLastUpdate = 0L;
private final Map<String, Pair<Long, Long>> volumeStats = new HashMap<>();
public LinstorPrimaryDataStoreDriverImpl() public LinstorPrimaryDataStoreDriverImpl()
{ {
} }
@ -401,9 +404,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
} }
} }
private String getRscGrp(StoragePoolVO storagePoolVO) { private String getRscGrp(StoragePool storagePool) {
return storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ? return storagePool.getUserInfo() != null && !storagePool.getUserInfo().isEmpty() ?
storagePoolVO.getUserInfo() : "DfltRscGrp"; storagePool.getUserInfo() : "DfltRscGrp";
} }
/** /**
@ -616,7 +619,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
*/ */
private void updateRscGrpIfNecessary(DevelopersApi api, String rscName, String tgtRscGrp) throws ApiException { private void updateRscGrpIfNecessary(DevelopersApi api, String rscName, String tgtRscGrp) throws ApiException {
List<ResourceDefinition> rscDfns = api.resourceDefinitionList( List<ResourceDefinition> rscDfns = api.resourceDefinitionList(
Collections.singletonList(rscName), null, null, null); Collections.singletonList(rscName), false, null, null, null);
if (rscDfns != null && !rscDfns.isEmpty()) { if (rscDfns != null && !rscDfns.isEmpty()) {
ResourceDefinition rscDfn = rscDfns.get(0); ResourceDefinition rscDfn = rscDfns.get(0);
@ -646,7 +649,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
private void deleteTemplateForProps( private void deleteTemplateForProps(
DevelopersApi api, String rscName) throws ApiException { DevelopersApi api, String rscName) throws ApiException {
List<ResourceDefinition> rdList = api.resourceDefinitionList( List<ResourceDefinition> rdList = api.resourceDefinitionList(
Collections.singletonList(rscName), null, null, null); Collections.singletonList(rscName), false, null, null, null);
if (CollectionUtils.isNotEmpty(rdList)) { if (CollectionUtils.isNotEmpty(rdList)) {
ResourceDefinitionModify rdm = new ResourceDefinitionModify(); ResourceDefinitionModify rdm = new ResourceDefinitionModify();
@ -1504,22 +1507,77 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
@Override @Override
public boolean canProvideStorageStats() { public boolean canProvideStorageStats() {
return false; return true;
} }
@Override @Override
public Pair<Long, Long> getStorageStats(StoragePool storagePool) { public Pair<Long, Long> getStorageStats(StoragePool storagePool) {
return null; s_logger.debug(String.format("Requesting storage stats: %s", storagePool));
return LinstorUtil.getStorageStats(storagePool.getHostAddress(), getRscGrp(storagePool));
} }
@Override @Override
public boolean canProvideVolumeStats() { 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<ResourceWithVolumes> resources = api.viewResources(
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
null,
null,
null);
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(
Collections.emptyList(), true, null, null, null);
HashMap<String, Long> resSizeMap = new HashMap<>();
for (ResourceDefinition rscDfn : rscDfns) {
if (CollectionUtils.isNotEmpty(rscDfn.getVolumeDefinitions())) {
resSizeMap.put(rscDfn.getName(), rscDfn.getVolumeDefinitions().get(0).getSizeKib() * 1024);
}
}
HashMap<String, Long> 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<String, Long> entry : allocSizeMap.entrySet()) {
Long reserved = resSizeMap.getOrDefault(entry.getKey(), 0L);
Pair<Long, Long> 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 @Override
public Pair<Long, Long> getVolumeStats(StoragePool storagePool, String volumeId) { public Pair<Long, Long> 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 @Override

View File

@ -24,7 +24,14 @@ public class LinstorConfigurationManager implements Configurable
public static final ConfigKey<Boolean> BackupSnapshots = new ConfigKey<>(Boolean.class, "lin.backup.snapshots", "Advanced", "true", public static final ConfigKey<Boolean> 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); "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<Integer> 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 @Override
public String getConfigComponentName() public String getConfigComponentName()

View File

@ -195,6 +195,30 @@ public class LinstorUtil {
} }
} }
public static Pair<Long, Long> getStorageStats(String linstorUrl, String rscGroupName) {
DevelopersApi linstorApi = getLinstorAPI(linstorUrl);
try {
List<StoragePool> 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. * Check if any resource of the given name is InUse on any host.
* *
@ -303,7 +327,7 @@ public class LinstorUtil {
public static List<ResourceDefinition> getRDListStartingWith(DevelopersApi api, String startWith) public static List<ResourceDefinition> getRDListStartingWith(DevelopersApi api, String startWith)
throws ApiException throws ApiException
{ {
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, null, null, null); List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, false, null, null, null);
return rscDfns.stream() return rscDfns.stream()
.filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(startWith.toLowerCase())) .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) public static ResourceDefinition findResourceDefinition(DevelopersApi api, String rscName, String rscGrpName)
throws ApiException { throws ApiException {
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, null, null, null); List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, false, null, null, null);
List<ResourceDefinition> rdsStartingWith = rscDfns.stream() List<ResourceDefinition> rdsStartingWith = rscDfns.stream()
.filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(rscName.toLowerCase())) .filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(rscName.toLowerCase()))
@ -402,4 +426,8 @@ public class LinstorUtil {
return rd.orElseGet(() -> rdsStartingWith.get(0)); return rd.orElseGet(() -> rdsStartingWith.get(0));
} }
public static boolean isRscDiskless(ResourceWithVolumes rsc) {
return rsc.getFlags() != null && rsc.getFlags().contains(ApiConsts.FLAG_DISKLESS);
}
} }

View File

@ -169,7 +169,7 @@
<cs.nitro.version>10.1</cs.nitro.version> <cs.nitro.version>10.1</cs.nitro.version>
<cs.opensaml.version>2.6.6</cs.opensaml.version> <cs.opensaml.version>2.6.6</cs.opensaml.version>
<cs.rados-java.version>0.6.0</cs.rados-java.version> <cs.rados-java.version>0.6.0</cs.rados-java.version>
<cs.java-linstor.version>0.6.0</cs.java-linstor.version> <cs.java-linstor.version>0.6.1</cs.java-linstor.version>
<cs.reflections.version>0.10.2</cs.reflections.version> <cs.reflections.version>0.10.2</cs.reflections.version>
<cs.servicemix.version>3.4.4_1</cs.servicemix.version> <cs.servicemix.version>3.4.4_1</cs.servicemix.version>
<cs.servlet.version>4.0.1</cs.servlet.version> <cs.servlet.version>4.0.1</cs.servlet.version>