Skip to content

Commit 36a732b

Browse files
genegrEugenio Grosso
authored andcommitted
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 <eugenio.grosso@gmail.com>
1 parent 9f96c9d commit 36a732b

1 file changed

Lines changed: 88 additions & 40 deletions

File tree

  • plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray

plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java

Lines changed: 88 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import com.cloud.utils.exception.CloudRuntimeException;
6262
import com.fasterxml.jackson.core.JsonProcessingException;
6363
import com.fasterxml.jackson.core.type.TypeReference;
64+
import com.fasterxml.jackson.databind.JsonNode;
6465
import com.fasterxml.jackson.databind.ObjectMapper;
6566
import org.apache.logging.log4j.LogManager;
6667
import org.apache.logging.log4j.Logger;
@@ -591,8 +592,10 @@ private void login() {
591592
}
592593

593594
apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION);
595+
boolean apiVersionExplicit = apiVersion != null;
594596
if (apiVersion == null) {
595597
apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION);
598+
apiVersionExplicit = apiVersion != null;
596599
if (apiVersion == null) {
597600
apiVersion = API_VERSION_DEFAULT;
598601
}
@@ -660,72 +663,117 @@ private void login() {
660663
skipTlsValidation = true;
661664
}
662665

666+
// Resolve the long-lived API token. Prefer a pre-minted api_token (Purity REST 2.x flow);
667+
// fall back to legacy username/password auth via Purity REST 1.x for backward compatibility.
668+
String apiToken = connectionDetails.get(ProviderAdapter.API_TOKEN_KEY);
669+
if (apiToken != null && apiToken.isEmpty()) {
670+
apiToken = null;
671+
}
672+
boolean usingLegacyUserPass = apiToken == null;
673+
if (usingLegacyUserPass && (username == null || password == null)) {
674+
throw new CloudRuntimeException("FlashArray adapter requires either " + ProviderAdapter.API_TOKEN_KEY
675+
+ " (preferred) or both " + ProviderAdapter.API_USERNAME_KEY + " and "
676+
+ ProviderAdapter.API_PASSWORD_KEY + " in the connection details");
677+
}
678+
679+
CloseableHttpClient client = getClient();
663680
CloseableHttpResponse response = null;
664681
try {
665-
HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
666-
// request.addHeader("Content-Type", "application/json");
667-
// request.addHeader("Accept", "application/json");
668-
ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
669-
postParms.add(new BasicNameValuePair("username", username));
670-
postParms.add(new BasicNameValuePair("password", password));
671-
request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
672-
CloseableHttpClient client = getClient();
673-
response = (CloseableHttpResponse) client.execute(request);
682+
// Discover the latest supported API version from the array unless one was explicitly configured.
683+
// GET /api/api_version is unauthenticated and returns {"version":["1.0",...,"2.36"]}.
684+
if (!apiVersionExplicit) {
685+
HttpGet vReq = new HttpGet(url + "/api_version");
686+
CloseableHttpResponse vResp = null;
687+
try {
688+
vResp = (CloseableHttpResponse) client.execute(vReq);
689+
if (vResp.getStatusLine().getStatusCode() == 200) {
690+
JsonNode root = mapper.readTree(vResp.getEntity().getContent());
691+
JsonNode versions = root.get("version");
692+
if (versions != null && versions.isArray() && versions.size() > 0) {
693+
apiVersion = versions.get(versions.size() - 1).asText();
694+
}
695+
} else {
696+
logger.warn("Unexpected HTTP " + vResp.getStatusLine().getStatusCode()
697+
+ " from FlashArray [" + url + "] /api_version, falling back to default "
698+
+ API_VERSION_DEFAULT);
699+
}
700+
} catch (Exception e) {
701+
logger.warn("Failed to discover Purity REST API version from " + url
702+
+ "/api_version, falling back to default " + API_VERSION_DEFAULT, e);
703+
} finally {
704+
if (vResp != null) {
705+
vResp.close();
706+
}
707+
}
708+
}
674709

675-
int statusCode = response.getStatusLine().getStatusCode();
676-
FlashArrayApiToken apitoken = null;
677-
if (statusCode == 200 | statusCode == 201) {
678-
apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class);
679-
if (apitoken == null) {
710+
if (usingLegacyUserPass) {
711+
logger.warn("FlashArray adapter at [" + url + "] is using deprecated username/password "
712+
+ "login against Purity REST 1.x. Replace with a pre-minted "
713+
+ ProviderAdapter.API_TOKEN_KEY + " detail; the username/password code path will be "
714+
+ "removed in a future release.");
715+
HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
716+
ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
717+
postParms.add(new BasicNameValuePair("username", username));
718+
postParms.add(new BasicNameValuePair("password", password));
719+
request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
720+
response = (CloseableHttpResponse) client.execute(request);
721+
int statusCode = response.getStatusLine().getStatusCode();
722+
if (statusCode == 200 || statusCode == 201) {
723+
FlashArrayApiToken legacyToken = mapper.readValue(response.getEntity().getContent(),
724+
FlashArrayApiToken.class);
725+
if (legacyToken == null || legacyToken.getApiToken() == null) {
726+
throw new CloudRuntimeException(
727+
"Authentication responded successfully but no api token was returned");
728+
}
729+
apiToken = legacyToken.getApiToken();
730+
} else if (statusCode == 401 || statusCode == 403) {
731+
throw new CloudRuntimeException(
732+
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
733+
+ "] failed, unable to retrieve session token");
734+
} else {
680735
throw new CloudRuntimeException(
681-
"Authentication responded successfully but no api token was returned");
736+
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
737+
+ "] - " + response.getStatusLine().getReasonPhrase());
682738
}
683-
} else if (statusCode == 401 || statusCode == 403) {
684-
throw new CloudRuntimeException(
685-
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
686-
+ "] failed, unable to retrieve session token");
687-
} else {
688-
throw new CloudRuntimeException(
689-
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
690-
+ "] - " + response.getStatusLine().getReasonPhrase());
739+
response.close();
740+
response = null;
691741
}
692742

693-
// now we need to get the access token
694-
request = new HttpPost(url + "/" + apiVersion + "/login");
695-
request.addHeader("api-token", apitoken.getApiToken());
743+
// Exchange the long-lived api-token for a short-lived x-auth-token (REST 2.x).
744+
HttpPost request = new HttpPost(url + "/" + apiVersion + "/login");
745+
request.addHeader("api-token", apiToken);
696746
response = (CloseableHttpResponse) client.execute(request);
697-
698-
statusCode = response.getStatusLine().getStatusCode();
699-
if (statusCode == 200 | statusCode == 201) {
747+
int statusCode = response.getStatusLine().getStatusCode();
748+
if (statusCode == 200 || statusCode == 201) {
700749
Header[] headers = response.getHeaders("x-auth-token");
701750
if (headers == null || headers.length == 0) {
702751
throw new CloudRuntimeException(
703-
"Getting access token responded successfully but access token was not available");
752+
"FlashArray /login responded successfully but no x-auth-token header was returned");
704753
}
705754
accessToken = headers[0].getValue();
706755
} else if (statusCode == 401 || statusCode == 403) {
707756
throw new CloudRuntimeException(
708-
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
709-
+ "] failed, unable to retrieve session token");
757+
"FlashArray [" + url + "] rejected the api-token at /" + apiVersion + "/login");
710758
} else {
711759
throw new CloudRuntimeException(
712-
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
713-
+ "] - " + response.getStatusLine().getReasonPhrase());
760+
"Unexpected HTTP response code from FlashArray [" + url + "] /" + apiVersion
761+
+ "/login - [" + statusCode + "] - "
762+
+ response.getStatusLine().getReasonPhrase());
714763
}
715-
716764
} catch (UnsupportedEncodingException e) {
717-
throw new CloudRuntimeException("Error creating input for login, check username/password encoding");
765+
throw new CloudRuntimeException("Error encoding login form for FlashArray [" + url + "]", e);
718766
} catch (UnsupportedOperationException e) {
719767
throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e);
720768
} catch (IOException e) {
721769
throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e);
722770
} finally {
723-
try {
724-
if (response != null) {
771+
if (response != null) {
772+
try {
725773
response.close();
774+
} catch (IOException e) {
775+
logger.debug("Error closing response from login attempt to FlashArray", e);
726776
}
727-
} catch (IOException e) {
728-
logger.debug("Error closing response from login attempt to FlashArray", e);
729777
}
730778
}
731779
}

0 commit comments

Comments
 (0)