This is an automated email from the ASF dual-hosted git repository.

winterhazel pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.20 by this push:
     new e8df87e89be flasharray: authenticate via REST 2.x api-token and 
discover API version (#13060)
e8df87e89be is described below

commit e8df87e89be3a1834e163c01ef395fe220a72027
Author: Eugenio Grosso <[email protected]>
AuthorDate: Fri Jul 10 21:02:07 2026 +0200

    flasharray: authenticate via REST 2.x api-token and discover API version 
(#13060)
    
    Signed-off-by: Eugenio Grosso <[email protected]>
    Co-authored-by: Eugenio Grosso <[email protected]>
---
 .../adapter/flasharray/FlashArrayAdapter.java      | 184 +++++++++++++++------
 1 file changed, 136 insertions(+), 48 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 dd554af36fe..3ffbeb1f9a3 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
@@ -28,6 +28,8 @@ import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
@@ -63,6 +65,7 @@ import org.apache.http.ssl.SSLContextBuilder;
 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;
@@ -87,6 +90,10 @@ public class FlashArrayAdapter implements ProviderAdapter {
     private static final String API_LOGIN_VERSION_DEFAULT = "1.19";
     private static final String API_VERSION_DEFAULT = "2.23";
 
+    // URLs for which the legacy-auth deprecation WARN has already been 
emitted,
+    // so we don't spam the logs once per refresh per pool while it's still 
configured.
+    private static final Set<String> WARNED_LEGACY_URLS = 
ConcurrentHashMap.newKeySet();
+
     static final ObjectMapper mapper = new ObjectMapper();
     public String pod = null;
     public String hostgroup = null;
@@ -588,6 +595,91 @@ public class FlashArrayAdapter implements ProviderAdapter {
         return accessToken;
     }
 
+    /**
+     * Discover the latest supported Purity REST API version by hitting the 
unauthenticated
+     * {@code /api/api_version} endpoint (returns {@code 
{"version":["1.0",...,"2.36"]}}).
+     * The discovered version is stored on {@link #apiVersion}; on failure the 
caller-configured
+     * default remains in place.
+     */
+    private void fetchApiVersionFromPurity(CloseableHttpClient client) {
+        HttpGet vReq = new HttpGet(url + "/api_version");
+        CloseableHttpResponse vResp = null;
+        try {
+            vResp = 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) {
+                try {
+                    vResp.close();
+                } catch (IOException e) {
+                    logger.debug("Error closing /api_version response from 
FlashArray [" + url + "]", e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Exchange the operator-configured username/password for a long-lived 
Purity api-token
+     * via REST 1.x {@code /auth/apitoken}. Emits the once-per-URL deprecation 
WARN.
+     * @return the api-token to feed into the REST 2.x /login exchange.
+     */
+    private String getApiTokenUsingUserPass(CloseableHttpClient client) throws 
IOException {
+        if (WARNED_LEGACY_URLS.add(url)) {
+            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<NameValuePair> postParms = new ArrayList<NameValuePair>();
+        postParms.add(new BasicNameValuePair("username", username));
+        postParms.add(new BasicNameValuePair("password", password));
+        request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
+        CloseableHttpResponse response = null;
+        try {
+            response = 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");
+                }
+                return 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(
+                        "Unexpected HTTP response code from FlashArray [" + 
url + "] - [" + statusCode
+                                + "] - " + 
response.getStatusLine().getReasonPhrase());
+            }
+        } finally {
+            if (response != null) {
+                try {
+                    response.close();
+                } catch (IOException e) {
+                    logger.debug("Error closing legacy auth/apitoken response 
from FlashArray [" + url + "]", e);
+                }
+            }
+        }
+    }
+
     private synchronized void refreshSession(boolean force) {
         try {
             if (force || keyExpiration < System.currentTimeMillis()) {
@@ -662,9 +754,11 @@ public class FlashArrayAdapter implements ProviderAdapter {
         }
 
         apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION);
-        if (apiVersion == null) {
+        boolean apiVersionExplicit = apiVersion != null;
+        if (!apiVersionExplicit) {
             apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION);
-            if (apiVersion == null) {
+            apiVersionExplicit = apiVersion != null;
+            if (!apiVersionExplicit) {
                 apiVersion = API_VERSION_DEFAULT;
             }
         }
@@ -731,72 +825,66 @@ public class FlashArrayAdapter implements ProviderAdapter 
{
             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<NameValuePair> postParms = new 
ArrayList<NameValuePair>();
-            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);
-
-            int statusCode = response.getStatusLine().getStatusCode();
-            FlashArrayApiToken apitoken = null;
-            if (statusCode == 200 | statusCode == 201) {
-                apitoken = mapper.readValue(response.getEntity().getContent(), 
FlashArrayApiToken.class);
-                if (apitoken == null) {
-                    throw new CloudRuntimeException(
-                            "Authentication responded successfully but no api 
token was returned");
-                }
-            } 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());
+            // 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) {
+                fetchApiVersionFromPurity(client);
             }
 
-            // now we need to get the access token
-            request = new HttpPost(url + "/" + apiVersion + "/login");
-            request.addHeader("api-token", apitoken.getApiToken());
-            response = (CloseableHttpResponse) client.execute(request);
+            if (usingLegacyUserPass) {
+                apiToken = getApiTokenUsingUserPass(client);
+            }
 
-            statusCode = response.getStatusLine().getStatusCode();
-            if (statusCode == 200 | statusCode == 201) {
+            // 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 = client.execute(request);
+            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);
             }
         }
     }
@@ -964,7 +1052,7 @@ public class FlashArrayAdapter implements ProviderAdapter {
             request.setEntity(new StringEntity(data));
 
             CloseableHttpClient client = getClient();
-            response = (CloseableHttpResponse) client.execute(request);
+            response = client.execute(request);
 
             final int statusCode = response.getStatusLine().getStatusCode();
             if (statusCode == 200 || statusCode == 201) {
@@ -1019,7 +1107,7 @@ public class FlashArrayAdapter implements ProviderAdapter 
{
             request.addHeader("X-auth-token", getAccessToken());
 
             CloseableHttpClient client = getClient();
-            response = (CloseableHttpResponse) client.execute(request);
+            response = client.execute(request);
             final int statusCode = response.getStatusLine().getStatusCode();
             if (statusCode == 200) {
                 try {
@@ -1061,7 +1149,7 @@ public class FlashArrayAdapter implements ProviderAdapter 
{
             request.addHeader("X-auth-token", getAccessToken());
 
             CloseableHttpClient client = getClient();
-            response = (CloseableHttpResponse) client.execute(request);
+            response = client.execute(request);
             final int statusCode = response.getStatusLine().getStatusCode();
             if (statusCode == 200 || statusCode == 404 || statusCode == 400) {
                 // this means the volume was deleted successfully, or doesn't 
exist (effective

Reply via email to