From 36a732b870b2a8e36214909728d0418f30ce13a2 Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Wed, 22 Apr 2026 17:47:34 +0000 Subject: [PATCH 1/3] flasharray: authenticate via REST 2.x api-token and discover API version The FlashArray adapter previously always made an initial call to the deprecated Purity REST 1.x session endpoint using a username and password to obtain a long-lived api_token, then exchanged that token for the REST 2.x x-auth-token session key. Purity 1.x is being removed from the array, so this path has an expiration date, and storing the username and password as pool details is not what the Purity documentation recommends. Accept a pre-minted api_token in the pool details (ProviderAdapter.API_TOKEN_KEY, already reserved in the base interface) and go straight to the REST 2.x /login endpoint. The api_token is long-lived and is created on the array via the Purity GUI (Users -> API Tokens) or CLI (pureadmin create --api-token). If api_token is not set, fall back to the legacy username/password flow and emit a deprecation warning so existing deployments keep working during the transition. The fallback path will be removed in a later release. While here, resolve the API version dynamically by calling the unauthenticated GET /api/api_version endpoint the first time a login happens on a pool, unless the operator pinned a specific version via API_VERSION. This makes the adapter pick up newer Purity releases automatically instead of being stuck on the hard-coded 2.23 default. Signed-off-by: Eugenio Grosso --- .../adapter/flasharray/FlashArrayAdapter.java | 128 ++++++++++++------ 1 file changed, 88 insertions(+), 40 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..2bd7d593a043 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 @@ -61,6 +61,7 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -591,8 +592,10 @@ private void login() { } apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION); + boolean apiVersionExplicit = apiVersion != null; if (apiVersion == null) { apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION); + apiVersionExplicit = apiVersion != null; if (apiVersion == null) { apiVersion = API_VERSION_DEFAULT; } @@ -660,72 +663,117 @@ private void login() { skipTlsValidation = true; } + // Resolve the long-lived API token. Prefer a pre-minted api_token (Purity REST 2.x flow); + // fall back to legacy username/password auth via Purity REST 1.x for backward compatibility. + String apiToken = connectionDetails.get(ProviderAdapter.API_TOKEN_KEY); + if (apiToken != null && apiToken.isEmpty()) { + apiToken = null; + } + boolean usingLegacyUserPass = apiToken == null; + if (usingLegacyUserPass && (username == null || password == null)) { + throw new CloudRuntimeException("FlashArray adapter requires either " + ProviderAdapter.API_TOKEN_KEY + + " (preferred) or both " + ProviderAdapter.API_USERNAME_KEY + " and " + + ProviderAdapter.API_PASSWORD_KEY + " in the connection details"); + } + + CloseableHttpClient client = getClient(); CloseableHttpResponse response = null; try { - HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken"); - // request.addHeader("Content-Type", "application/json"); - // request.addHeader("Accept", "application/json"); - ArrayList postParms = new ArrayList(); - postParms.add(new BasicNameValuePair("username", username)); - postParms.add(new BasicNameValuePair("password", password)); - request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8")); - CloseableHttpClient client = getClient(); - response = (CloseableHttpResponse) client.execute(request); + // Discover the latest supported API version from the array unless one was explicitly configured. + // GET /api/api_version is unauthenticated and returns {"version":["1.0",...,"2.36"]}. + if (!apiVersionExplicit) { + HttpGet vReq = new HttpGet(url + "/api_version"); + CloseableHttpResponse vResp = null; + try { + vResp = (CloseableHttpResponse) client.execute(vReq); + if (vResp.getStatusLine().getStatusCode() == 200) { + JsonNode root = mapper.readTree(vResp.getEntity().getContent()); + JsonNode versions = root.get("version"); + if (versions != null && versions.isArray() && versions.size() > 0) { + apiVersion = versions.get(versions.size() - 1).asText(); + } + } else { + logger.warn("Unexpected HTTP " + vResp.getStatusLine().getStatusCode() + + " from FlashArray [" + url + "] /api_version, falling back to default " + + API_VERSION_DEFAULT); + } + } catch (Exception e) { + logger.warn("Failed to discover Purity REST API version from " + url + + "/api_version, falling back to default " + API_VERSION_DEFAULT, e); + } finally { + if (vResp != null) { + vResp.close(); + } + } + } - int statusCode = response.getStatusLine().getStatusCode(); - FlashArrayApiToken apitoken = null; - if (statusCode == 200 | statusCode == 201) { - apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class); - if (apitoken == null) { + if (usingLegacyUserPass) { + logger.warn("FlashArray adapter at [" + url + "] is using deprecated username/password " + + "login against Purity REST 1.x. Replace with a pre-minted " + + ProviderAdapter.API_TOKEN_KEY + " detail; the username/password code path will be " + + "removed in a future release."); + HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken"); + ArrayList postParms = new ArrayList(); + postParms.add(new BasicNameValuePair("username", username)); + postParms.add(new BasicNameValuePair("password", password)); + request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8")); + response = (CloseableHttpResponse) client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200 || statusCode == 201) { + FlashArrayApiToken legacyToken = mapper.readValue(response.getEntity().getContent(), + FlashArrayApiToken.class); + if (legacyToken == null || legacyToken.getApiToken() == null) { + throw new CloudRuntimeException( + "Authentication responded successfully but no api token was returned"); + } + apiToken = legacyToken.getApiToken(); + } else if (statusCode == 401 || statusCode == 403) { + throw new CloudRuntimeException( + "Authentication or Authorization to FlashArray [" + url + "] with user [" + username + + "] failed, unable to retrieve session token"); + } else { throw new CloudRuntimeException( - "Authentication responded successfully but no api token was returned"); + "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode + + "] - " + response.getStatusLine().getReasonPhrase()); } - } else if (statusCode == 401 || statusCode == 403) { - throw new CloudRuntimeException( - "Authentication or Authorization to FlashArray [" + url + "] with user [" + username - + "] failed, unable to retrieve session token"); - } else { - throw new CloudRuntimeException( - "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode - + "] - " + response.getStatusLine().getReasonPhrase()); + response.close(); + response = null; } - // now we need to get the access token - request = new HttpPost(url + "/" + apiVersion + "/login"); - request.addHeader("api-token", apitoken.getApiToken()); + // Exchange the long-lived api-token for a short-lived x-auth-token (REST 2.x). + HttpPost request = new HttpPost(url + "/" + apiVersion + "/login"); + request.addHeader("api-token", apiToken); response = (CloseableHttpResponse) client.execute(request); - - statusCode = response.getStatusLine().getStatusCode(); - if (statusCode == 200 | statusCode == 201) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200 || statusCode == 201) { Header[] headers = response.getHeaders("x-auth-token"); if (headers == null || headers.length == 0) { throw new CloudRuntimeException( - "Getting access token responded successfully but access token was not available"); + "FlashArray /login responded successfully but no x-auth-token header was returned"); } accessToken = headers[0].getValue(); } else if (statusCode == 401 || statusCode == 403) { throw new CloudRuntimeException( - "Authentication or Authorization to FlashArray [" + url + "] with user [" + username - + "] failed, unable to retrieve session token"); + "FlashArray [" + url + "] rejected the api-token at /" + apiVersion + "/login"); } else { throw new CloudRuntimeException( - "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode - + "] - " + response.getStatusLine().getReasonPhrase()); + "Unexpected HTTP response code from FlashArray [" + url + "] /" + apiVersion + + "/login - [" + statusCode + "] - " + + response.getStatusLine().getReasonPhrase()); } - } catch (UnsupportedEncodingException e) { - throw new CloudRuntimeException("Error creating input for login, check username/password encoding"); + throw new CloudRuntimeException("Error encoding login form for FlashArray [" + url + "]", e); } catch (UnsupportedOperationException e) { throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e); } catch (IOException e) { throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e); } finally { - try { - if (response != null) { + if (response != null) { + try { response.close(); + } catch (IOException e) { + logger.debug("Error closing response from login attempt to FlashArray", e); } - } catch (IOException e) { - logger.debug("Error closing response from login attempt to FlashArray", e); } } } From 50b560a4e9bb12a7f2734a6c55833bf46cf1ba58 Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Thu, 23 Apr 2026 11:32:28 +0000 Subject: [PATCH 2/3] flasharray: address review on login() - Replace nested null-check of apiVersion with !apiVersionExplicit for clarity (sureshanaparti). - Wrap vResp.close() in the api_version-discovery finally with its own try/catch; log any IOException at debug so a failed close does not mask a successful discovery. - Wrap the explicit response.close() after the legacy username/password auth with try/catch for the same reason. Signed-off-by: Eugenio Grosso --- .../adapter/flasharray/FlashArrayAdapter.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 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 2bd7d593a043..494959b7623c 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 @@ -593,10 +593,10 @@ private void login() { apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION); boolean apiVersionExplicit = apiVersion != null; - if (apiVersion == null) { + if (!apiVersionExplicit) { apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION); apiVersionExplicit = apiVersion != null; - if (apiVersion == null) { + if (!apiVersionExplicit) { apiVersion = API_VERSION_DEFAULT; } } @@ -702,7 +702,11 @@ private void login() { + "/api_version, falling back to default " + API_VERSION_DEFAULT, e); } finally { if (vResp != null) { - vResp.close(); + try { + vResp.close(); + } catch (IOException e) { + logger.debug("Error closing /api/api_version response from FlashArray [" + url + "]", e); + } } } } @@ -736,7 +740,11 @@ private void login() { "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode + "] - " + response.getStatusLine().getReasonPhrase()); } - response.close(); + try { + response.close(); + } catch (IOException e) { + logger.debug("Error closing legacy auth/apitoken response from FlashArray [" + url + "]", e); + } response = null; } From 29385709cb6feebf5332bcfe9d2df92f5b884b6d Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Tue, 28 Apr 2026 12:50:46 +0000 Subject: [PATCH 3/3] flasharray: align api_version close-error log text with actual path The Copilot review pointed out the debug message said /api/api_version but the path passed to the helper is /api_version (the helper itself prepends url + apiVersion). Update the log text to match what the code actually invokes. Signed-off-by: Eugenio Grosso --- .../storage/datastore/adapter/flasharray/FlashArrayAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 494959b7623c..aa5bffef8692 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 @@ -705,7 +705,7 @@ private void login() { try { vResp.close(); } catch (IOException e) { - logger.debug("Error closing /api/api_version response from FlashArray [" + url + "]", e); + logger.debug("Error closing /api_version response from FlashArray [" + url + "]", e); } } }