From dca606b4be4908d35486c8106609577bb6d0c07c Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Mon, 20 Apr 2026 21:35:25 +0000 Subject: [PATCH] flasharray: fall back to array capacity when pod has no quota FlashArrayAdapter.getManagedStorageStats() returns null whenever the backing pod has no volumes (footprint == 0) and never reports anything other than the pod quota otherwise. A freshly-registered pool that sits on a pod without an explicit quota therefore shows disksizetotal=0, disksizeused=0 and the ClusterScopeStoragePoolAllocator refuses to allocate any volume against it (zero-capacity pool is skipped). The plugin is unusable until a pod quota is set manually on the array - which is not documented anywhere and not discoverable from the CloudStack side. Fix: fall back to the arrays total physical capacity (retrieved via GET /arrays?space=true) when the pod has no quota, or when the quota is zero. The used value falls back to the pod footprint, defaulting to 0 when absent. Only return null when no capacity value is obtainable at all, which now only happens if the array itself is unreachable. The math for usedBytes was also simplified: the previous form pod.getQuotaLimit() - (pod.getQuotaLimit() - pod.getFootprint()) is just pod.getFootprint() with an extra NPE risk when getQuotaLimit() is null. --- .../adapter/flasharray/FlashArrayAdapter.java | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java b/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java index 715379daf86d..8915dc47ad46 100644 --- a/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java +++ b/plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java @@ -26,11 +26,14 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.Map; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; +import org.apache.commons.collections4.CollectionUtils; import org.apache.http.Header; import org.apache.http.NameValuePair; import org.apache.cloudstack.storage.datastore.adapter.ProviderAdapter; @@ -452,18 +455,79 @@ public void disconnect() { @Override public ProviderVolumeStorageStats getManagedStorageStats() { FlashArrayPod pod = getVolumeNamespace(this.pod); - // just in case - if (pod == null || pod.getFootprint() == 0) { + if (pod == null) { return null; } Long capacityBytes = pod.getQuotaLimit(); - Long usedBytes = pod.getQuotaLimit() - (pod.getQuotaLimit() - pod.getFootprint()); + if (capacityBytes == null || capacityBytes == 0) { + // Pod has no explicit quota set; report the array total physical + // capacity so the CloudStack allocator has a real ceiling to plan + // against rather than bailing out with a zero-capacity pool. + capacityBytes = getArrayTotalCapacity(); + } + if (capacityBytes == null || capacityBytes == 0) { + return null; + } + Long usedBytes = pod.getFootprint(); + if (usedBytes == null) { + usedBytes = 0L; + } ProviderVolumeStorageStats stats = new ProviderVolumeStorageStats(); stats.setCapacityInBytes(capacityBytes); stats.setActualUsedInBytes(usedBytes); return stats; } + /** + * Cache of array total capacity keyed by FlashArray URL. The capacity of a + * physical FlashArray changes only when hardware is added or removed, so a + * several-minute TTL is safe and avoids an extra REST call on every + * storage stats refresh for every pool that has no pod quota set. + */ + private static final ConcurrentMap ARRAY_CAPACITY_CACHE = new ConcurrentHashMap<>(); + private static final long ARRAY_CAPACITY_CACHE_TTL_MS = 5L * 60L * 1000L; + + private static final class CachedCapacity { + final long capacityBytes; + final long expiresAtMs; + + CachedCapacity(long capacityBytes, long ttlMs) { + this.capacityBytes = capacityBytes; + this.expiresAtMs = System.currentTimeMillis() + ttlMs; + } + + boolean isExpired() { + return System.currentTimeMillis() > expiresAtMs; + } + } + + private Long getArrayTotalCapacity() { + CachedCapacity cached = ARRAY_CAPACITY_CACHE.get(this.url); + if (cached != null && !cached.isExpired()) { + return cached.capacityBytes; + } + try { + FlashArrayList> list = GET("/arrays?space=true", + new TypeReference>>() { + }); + if (list != null && CollectionUtils.isNotEmpty(list.getItems())) { + Object cap = list.getItems().get(0).get("capacity"); + if (cap instanceof Number) { + long capacityBytes = ((Number) cap).longValue(); + ARRAY_CAPACITY_CACHE.put(this.url, + new CachedCapacity(capacityBytes, ARRAY_CAPACITY_CACHE_TTL_MS)); + return capacityBytes; + } + } + } catch (Exception e) { + logger.warn("Could not retrieve total capacity for FlashArray [{}] (pod [{}]): {}", + this.url, this.pod, e.getMessage()); + logger.debug("Stack trace for array total capacity lookup failure on FlashArray [{}] (pod [{}])", + this.url, this.pod, e); + } + return null; + } + @Override public ProviderVolumeStats getVolumeStats(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) { ProviderVolume vol = getVolume(dataObject.getExternalName());