Skip to content

Commit 7bf536c

Browse files
committed
Linstor: implement volume and storage stats (apache#10850)
1 parent b2f420b commit 7bf536c

4 files changed

Lines changed: 111 additions & 13 deletions

File tree

plugins/storage/volume/linstor/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to Linstor CloudStack plugin will be documented in this file
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2025-05-07]
9+
10+
### Added
11+
- Implemented storage/volume stats
12+
813
## [2025-03-13]
914

1015
### Fixed

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@
2828
import com.linbit.linstor.api.model.ResourceDefinitionCloneRequest;
2929
import com.linbit.linstor.api.model.ResourceDefinitionCloneStarted;
3030
import com.linbit.linstor.api.model.ResourceDefinitionCreate;
31-
3231
import com.linbit.linstor.api.model.ResourceDefinitionModify;
3332
import com.linbit.linstor.api.model.ResourceGroup;
3433
import com.linbit.linstor.api.model.ResourceGroupSpawn;
3534
import com.linbit.linstor.api.model.ResourceMakeAvailable;
35+
import com.linbit.linstor.api.model.ResourceWithVolumes;
3636
import com.linbit.linstor.api.model.Snapshot;
3737
import com.linbit.linstor.api.model.SnapshotRestore;
3838
import com.linbit.linstor.api.model.VolumeDefinition;
@@ -131,6 +131,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
131131
@Inject
132132
private HostDao _hostDao;
133133

134+
private long volumeStatsLastUpdate = 0L;
135+
private final Map<String, Pair<Long, Long>> volumeStats = new HashMap<>();
136+
134137
public LinstorPrimaryDataStoreDriverImpl()
135138
{
136139
}
@@ -400,9 +403,9 @@ private void applyQoSSettings(StoragePoolVO storagePool, DevelopersApi api, Stri
400403
}
401404
}
402405

403-
private String getRscGrp(StoragePoolVO storagePoolVO) {
404-
return storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ?
405-
storagePoolVO.getUserInfo() : "DfltRscGrp";
406+
private String getRscGrp(StoragePool storagePool) {
407+
return storagePool.getUserInfo() != null && !storagePool.getUserInfo().isEmpty() ?
408+
storagePool.getUserInfo() : "DfltRscGrp";
406409
}
407410

408411
/**
@@ -615,7 +618,7 @@ private void resizeResource(DevelopersApi api, String resourceName, long sizeByt
615618
*/
616619
private void updateRscGrpIfNecessary(DevelopersApi api, String rscName, String tgtRscGrp) throws ApiException {
617620
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(
618-
Collections.singletonList(rscName), null, null, null);
621+
Collections.singletonList(rscName), false, null, null, null);
619622
if (rscDfns != null && !rscDfns.isEmpty()) {
620623
ResourceDefinition rscDfn = rscDfns.get(0);
621624

@@ -645,7 +648,7 @@ private void updateRscGrpIfNecessary(DevelopersApi api, String rscName, String t
645648
private void deleteTemplateForProps(
646649
DevelopersApi api, String rscName) throws ApiException {
647650
List<ResourceDefinition> rdList = api.resourceDefinitionList(
648-
Collections.singletonList(rscName), null, null, null);
651+
Collections.singletonList(rscName), false, null, null, null);
649652

650653
if (CollectionUtils.isNotEmpty(rdList)) {
651654
ResourceDefinitionModify rdm = new ResourceDefinitionModify();
@@ -1503,22 +1506,77 @@ public void takeSnapshot(SnapshotInfo snapshotInfo, AsyncCompletionCallback<Crea
15031506

15041507
@Override
15051508
public boolean canProvideStorageStats() {
1506-
return false;
1509+
return true;
15071510
}
15081511

15091512
@Override
15101513
public Pair<Long, Long> getStorageStats(StoragePool storagePool) {
1511-
return null;
1514+
s_logger.debug(String.format("Requesting storage stats: %s", storagePool));
1515+
return LinstorUtil.getStorageStats(storagePool.getHostAddress(), getRscGrp(storagePool));
15121516
}
15131517

15141518
@Override
15151519
public boolean canProvideVolumeStats() {
1516-
return false;
1520+
return LinstorConfigurationManager.VolumeStatsCacheTime.value() > 0;
1521+
}
1522+
1523+
/**
1524+
* Updates the cache map containing current allocated size data.
1525+
* @param api Linstor Developers api object
1526+
*/
1527+
private void fillVolumeStatsCache(DevelopersApi api) {
1528+
try {
1529+
s_logger.trace("Start volume stats cache update");
1530+
List<ResourceWithVolumes> resources = api.viewResources(
1531+
Collections.emptyList(),
1532+
Collections.emptyList(),
1533+
Collections.emptyList(),
1534+
null,
1535+
null,
1536+
null);
1537+
1538+
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(
1539+
Collections.emptyList(), true, null, null, null);
1540+
1541+
HashMap<String, Long> resSizeMap = new HashMap<>();
1542+
for (ResourceDefinition rscDfn : rscDfns) {
1543+
if (CollectionUtils.isNotEmpty(rscDfn.getVolumeDefinitions())) {
1544+
resSizeMap.put(rscDfn.getName(), rscDfn.getVolumeDefinitions().get(0).getSizeKib() * 1024);
1545+
}
1546+
}
1547+
1548+
HashMap<String, Long> allocSizeMap = new HashMap<>();
1549+
for (ResourceWithVolumes rsc : resources) {
1550+
if (!LinstorUtil.isRscDiskless(rsc) && !rsc.getVolumes().isEmpty()) {
1551+
long allocatedBytes = allocSizeMap.getOrDefault(rsc.getName(), 0L);
1552+
allocSizeMap.put(rsc.getName(), Math.max(allocatedBytes, rsc.getVolumes().get(0).getAllocatedSizeKib() * 1024));
1553+
}
1554+
}
1555+
1556+
volumeStats.clear();
1557+
for (Map.Entry<String, Long> entry : allocSizeMap.entrySet()) {
1558+
Long reserved = resSizeMap.getOrDefault(entry.getKey(), 0L);
1559+
Pair<Long, Long> volStat = new Pair<>(entry.getValue(), reserved);
1560+
volumeStats.put(entry.getKey(), volStat);
1561+
}
1562+
volumeStatsLastUpdate = System.currentTimeMillis();
1563+
s_logger.trace("Done volume stats cache update: " + volumeStats.size());
1564+
} catch (ApiException e) {
1565+
s_logger.error("Unable to fetch Linstor resources: " + e.getBestMessage());
1566+
}
15171567
}
15181568

15191569
@Override
15201570
public Pair<Long, Long> getVolumeStats(StoragePool storagePool, String volumeId) {
1521-
return null;
1571+
final DevelopersApi api = LinstorUtil.getLinstorAPI(storagePool.getHostAddress());
1572+
synchronized (volumeStats) {
1573+
long invalidateCacheTime = volumeStatsLastUpdate +
1574+
LinstorConfigurationManager.VolumeStatsCacheTime.value() * 1000;
1575+
if (invalidateCacheTime < System.currentTimeMillis()) {
1576+
fillVolumeStatsCache(api);
1577+
}
1578+
return volumeStats.get(LinstorUtil.RSC_PREFIX + volumeId);
1579+
}
15221580
}
15231581

15241582
@Override

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ public class LinstorConfigurationManager implements Configurable
2424
public static final ConfigKey<Boolean> BackupSnapshots = new ConfigKey<>(Boolean.class, "lin.backup.snapshots", "Advanced", "true",
2525
"Backup Linstor primary storage snapshots to secondary storage (deleting ps snapshot)", true, ConfigKey.Scope.Global, null);
2626

27-
public static final ConfigKey<?>[] CONFIG_KEYS = new ConfigKey<?>[] { BackupSnapshots };
27+
public static final ConfigKey<Integer> VolumeStatsCacheTime = new ConfigKey<>("Advanced", Integer.class,
28+
"lin.volumes.stats.cachetime", "300",
29+
"Cache time of volume stats for Linstor volumes. 0 to disable volume stats",
30+
false);
31+
32+
public static final ConfigKey<?>[] CONFIG_KEYS = new ConfigKey<?>[] {
33+
BackupSnapshots, VolumeStatsCacheTime
34+
};
2835

2936
@Override
3037
public String getConfigComponentName()

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,30 @@ public static long getCapacityBytes(String linstorUrl, String rscGroupName) {
195195
}
196196
}
197197

198+
public static Pair<Long, Long> getStorageStats(String linstorUrl, String rscGroupName) {
199+
DevelopersApi linstorApi = getLinstorAPI(linstorUrl);
200+
try {
201+
List<StoragePool> storagePools = LinstorUtil.getRscGroupStoragePools(linstorApi, rscGroupName);
202+
203+
long capacity = storagePools.stream()
204+
.filter(sp -> sp.getProviderKind() != ProviderKind.DISKLESS)
205+
.mapToLong(sp -> sp.getTotalCapacity() != null ? sp.getTotalCapacity() : 0L)
206+
.sum() * 1024; // linstor uses kiB
207+
208+
long used = storagePools.stream()
209+
.filter(sp -> sp.getProviderKind() != ProviderKind.DISKLESS)
210+
.mapToLong(sp -> sp.getTotalCapacity() != null && sp.getFreeCapacity() != null ?
211+
sp.getTotalCapacity() - sp.getFreeCapacity() : 0L)
212+
.sum() * 1024; // linstor uses Kib
213+
s_logger.debug(
214+
String.format("Linstor(%s;%s): storageStats -> %d/%d", linstorUrl, rscGroupName, capacity, used));
215+
return new Pair<>(capacity, used);
216+
} catch (ApiException apiEx) {
217+
s_logger.error(apiEx.getMessage());
218+
throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx);
219+
}
220+
}
221+
198222
/**
199223
* Check if any resource of the given name is InUse on any host.
200224
*
@@ -303,7 +327,7 @@ public static ApiCallRcList applyAuxProps(DevelopersApi api, String rscName, Str
303327
public static List<ResourceDefinition> getRDListStartingWith(DevelopersApi api, String startWith)
304328
throws ApiException
305329
{
306-
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, null, null, null);
330+
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, false, null, null, null);
307331

308332
return rscDfns.stream()
309333
.filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(startWith.toLowerCase()))
@@ -386,7 +410,7 @@ public static void setAuxTemplateForProperty(DevelopersApi api, String rscName,
386410
*/
387411
public static ResourceDefinition findResourceDefinition(DevelopersApi api, String rscName, String rscGrpName)
388412
throws ApiException {
389-
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, null, null, null);
413+
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, false, null, null, null);
390414

391415
List<ResourceDefinition> rdsStartingWith = rscDfns.stream()
392416
.filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(rscName.toLowerCase()))
@@ -402,4 +426,8 @@ public static ResourceDefinition findResourceDefinition(DevelopersApi api, Strin
402426

403427
return rd.orElseGet(() -> rdsStartingWith.get(0));
404428
}
429+
430+
public static boolean isRscDiskless(ResourceWithVolumes rsc) {
431+
return rsc.getFlags() != null && rsc.getFlags().contains(ApiConsts.FLAG_DISKLESS);
432+
}
405433
}

0 commit comments

Comments
 (0)