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