From 92a10fc9b93bf1c64138b1147805877d7a660a55 Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Wed, 22 Apr 2026 17:47:34 +0000 Subject: [PATCH 1/2] 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 41125f3e1135..0417d94a024b 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; @@ -592,8 +593,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; } @@ -661,72 +664,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 929f88a88dfff0e1abe5cf49b8992df91c0e465a Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Thu, 23 Apr 2026 11:32:28 +0000 Subject: [PATCH 2/2] 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 0417d94a024b..c664e0d34989 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 @@ -594,10 +594,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; } } @@ -703,7 +703,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); + } } } } @@ -737,7 +741,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; }