Skip to content

Commit e8df87e

Browse files
genegrEugenio Grosso
andauthored
flasharray: authenticate via REST 2.x api-token and discover API version (#13060)
Signed-off-by: Eugenio Grosso <eugenio.grosso@gmail.com> Co-authored-by: Eugenio Grosso <egrosso@purestorage.com>
1 parent ec2d3ea commit e8df87e

1 file changed

Lines changed: 136 additions & 48 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: 136 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.ArrayList;
2929
import java.util.HashMap;
3030
import java.util.Map;
31+
import java.util.Set;
32+
import java.util.concurrent.ConcurrentHashMap;
3133

3234
import javax.net.ssl.HostnameVerifier;
3335
import javax.net.ssl.SSLContext;
@@ -63,6 +65,7 @@
6365
import com.cloud.utils.exception.CloudRuntimeException;
6466
import com.fasterxml.jackson.core.JsonProcessingException;
6567
import com.fasterxml.jackson.core.type.TypeReference;
68+
import com.fasterxml.jackson.databind.JsonNode;
6669
import com.fasterxml.jackson.databind.ObjectMapper;
6770
import org.apache.logging.log4j.LogManager;
6871
import org.apache.logging.log4j.Logger;
@@ -87,6 +90,10 @@ public class FlashArrayAdapter implements ProviderAdapter {
8790
private static final String API_LOGIN_VERSION_DEFAULT = "1.19";
8891
private static final String API_VERSION_DEFAULT = "2.23";
8992

93+
// URLs for which the legacy-auth deprecation WARN has already been emitted,
94+
// so we don't spam the logs once per refresh per pool while it's still configured.
95+
private static final Set<String> WARNED_LEGACY_URLS = ConcurrentHashMap.newKeySet();
96+
9097
static final ObjectMapper mapper = new ObjectMapper();
9198
public String pod = null;
9299
public String hostgroup = null;
@@ -588,6 +595,91 @@ private String getAccessToken() {
588595
return accessToken;
589596
}
590597

598+
/**
599+
* Discover the latest supported Purity REST API version by hitting the unauthenticated
600+
* {@code /api/api_version} endpoint (returns {@code {"version":["1.0",...,"2.36"]}}).
601+
* The discovered version is stored on {@link #apiVersion}; on failure the caller-configured
602+
* default remains in place.
603+
*/
604+
private void fetchApiVersionFromPurity(CloseableHttpClient client) {
605+
HttpGet vReq = new HttpGet(url + "/api_version");
606+
CloseableHttpResponse vResp = null;
607+
try {
608+
vResp = client.execute(vReq);
609+
if (vResp.getStatusLine().getStatusCode() == 200) {
610+
JsonNode root = mapper.readTree(vResp.getEntity().getContent());
611+
JsonNode versions = root.get("version");
612+
if (versions != null && versions.isArray() && versions.size() > 0) {
613+
apiVersion = versions.get(versions.size() - 1).asText();
614+
}
615+
} else {
616+
logger.warn("Unexpected HTTP " + vResp.getStatusLine().getStatusCode()
617+
+ " from FlashArray [" + url + "] /api_version, falling back to default "
618+
+ API_VERSION_DEFAULT);
619+
}
620+
} catch (Exception e) {
621+
logger.warn("Failed to discover Purity REST API version from " + url
622+
+ "/api_version, falling back to default " + API_VERSION_DEFAULT, e);
623+
} finally {
624+
if (vResp != null) {
625+
try {
626+
vResp.close();
627+
} catch (IOException e) {
628+
logger.debug("Error closing /api_version response from FlashArray [" + url + "]", e);
629+
}
630+
}
631+
}
632+
}
633+
634+
/**
635+
* Exchange the operator-configured username/password for a long-lived Purity api-token
636+
* via REST 1.x {@code /auth/apitoken}. Emits the once-per-URL deprecation WARN.
637+
* @return the api-token to feed into the REST 2.x /login exchange.
638+
*/
639+
private String getApiTokenUsingUserPass(CloseableHttpClient client) throws IOException {
640+
if (WARNED_LEGACY_URLS.add(url)) {
641+
logger.warn("FlashArray adapter at [" + url + "] is using deprecated username/password "
642+
+ "login against Purity REST 1.x. Replace with a pre-minted "
643+
+ ProviderAdapter.API_TOKEN_KEY + " detail; the username/password code path will be "
644+
+ "removed in a future release.");
645+
}
646+
HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
647+
ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
648+
postParms.add(new BasicNameValuePair("username", username));
649+
postParms.add(new BasicNameValuePair("password", password));
650+
request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
651+
CloseableHttpResponse response = null;
652+
try {
653+
response = client.execute(request);
654+
int statusCode = response.getStatusLine().getStatusCode();
655+
if (statusCode == 200 || statusCode == 201) {
656+
FlashArrayApiToken legacyToken = mapper.readValue(response.getEntity().getContent(),
657+
FlashArrayApiToken.class);
658+
if (legacyToken == null || legacyToken.getApiToken() == null) {
659+
throw new CloudRuntimeException(
660+
"Authentication responded successfully but no api token was returned");
661+
}
662+
return legacyToken.getApiToken();
663+
} else if (statusCode == 401 || statusCode == 403) {
664+
throw new CloudRuntimeException(
665+
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
666+
+ "] failed, unable to retrieve session token");
667+
} else {
668+
throw new CloudRuntimeException(
669+
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
670+
+ "] - " + response.getStatusLine().getReasonPhrase());
671+
}
672+
} finally {
673+
if (response != null) {
674+
try {
675+
response.close();
676+
} catch (IOException e) {
677+
logger.debug("Error closing legacy auth/apitoken response from FlashArray [" + url + "]", e);
678+
}
679+
}
680+
}
681+
}
682+
591683
private synchronized void refreshSession(boolean force) {
592684
try {
593685
if (force || keyExpiration < System.currentTimeMillis()) {
@@ -662,9 +754,11 @@ private void login() {
662754
}
663755

664756
apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION);
665-
if (apiVersion == null) {
757+
boolean apiVersionExplicit = apiVersion != null;
758+
if (!apiVersionExplicit) {
666759
apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION);
667-
if (apiVersion == null) {
760+
apiVersionExplicit = apiVersion != null;
761+
if (!apiVersionExplicit) {
668762
apiVersion = API_VERSION_DEFAULT;
669763
}
670764
}
@@ -731,72 +825,66 @@ private void login() {
731825
skipTlsValidation = true;
732826
}
733827

828+
// Resolve the long-lived API token. Prefer a pre-minted api_token (Purity REST 2.x flow);
829+
// fall back to legacy username/password auth via Purity REST 1.x for backward compatibility.
830+
String apiToken = connectionDetails.get(ProviderAdapter.API_TOKEN_KEY);
831+
if (apiToken != null && apiToken.isEmpty()) {
832+
apiToken = null;
833+
}
834+
boolean usingLegacyUserPass = apiToken == null;
835+
if (usingLegacyUserPass && (username == null || password == null)) {
836+
throw new CloudRuntimeException("FlashArray adapter requires either " + ProviderAdapter.API_TOKEN_KEY
837+
+ " (preferred) or both " + ProviderAdapter.API_USERNAME_KEY + " and "
838+
+ ProviderAdapter.API_PASSWORD_KEY + " in the connection details");
839+
}
840+
841+
CloseableHttpClient client = getClient();
734842
CloseableHttpResponse response = null;
735843
try {
736-
HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
737-
// request.addHeader("Content-Type", "application/json");
738-
// request.addHeader("Accept", "application/json");
739-
ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
740-
postParms.add(new BasicNameValuePair("username", username));
741-
postParms.add(new BasicNameValuePair("password", password));
742-
request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
743-
CloseableHttpClient client = getClient();
744-
response = (CloseableHttpResponse) client.execute(request);
745-
746-
int statusCode = response.getStatusLine().getStatusCode();
747-
FlashArrayApiToken apitoken = null;
748-
if (statusCode == 200 | statusCode == 201) {
749-
apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class);
750-
if (apitoken == null) {
751-
throw new CloudRuntimeException(
752-
"Authentication responded successfully but no api token was returned");
753-
}
754-
} else if (statusCode == 401 || statusCode == 403) {
755-
throw new CloudRuntimeException(
756-
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
757-
+ "] failed, unable to retrieve session token");
758-
} else {
759-
throw new CloudRuntimeException(
760-
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
761-
+ "] - " + response.getStatusLine().getReasonPhrase());
844+
// Discover the latest supported API version from the array unless one was explicitly configured.
845+
// GET /api/api_version is unauthenticated and returns {"version":["1.0",...,"2.36"]}.
846+
if (!apiVersionExplicit) {
847+
fetchApiVersionFromPurity(client);
762848
}
763849

764-
// now we need to get the access token
765-
request = new HttpPost(url + "/" + apiVersion + "/login");
766-
request.addHeader("api-token", apitoken.getApiToken());
767-
response = (CloseableHttpResponse) client.execute(request);
850+
if (usingLegacyUserPass) {
851+
apiToken = getApiTokenUsingUserPass(client);
852+
}
768853

769-
statusCode = response.getStatusLine().getStatusCode();
770-
if (statusCode == 200 | statusCode == 201) {
854+
// Exchange the long-lived api-token for a short-lived x-auth-token (REST 2.x).
855+
HttpPost request = new HttpPost(url + "/" + apiVersion + "/login");
856+
request.addHeader("api-token", apiToken);
857+
response = client.execute(request);
858+
int statusCode = response.getStatusLine().getStatusCode();
859+
if (statusCode == 200 || statusCode == 201) {
771860
Header[] headers = response.getHeaders("x-auth-token");
772861
if (headers == null || headers.length == 0) {
773862
throw new CloudRuntimeException(
774-
"Getting access token responded successfully but access token was not available");
863+
"FlashArray /login responded successfully but no x-auth-token header was returned");
775864
}
776865
accessToken = headers[0].getValue();
777866
} else if (statusCode == 401 || statusCode == 403) {
778867
throw new CloudRuntimeException(
779-
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
780-
+ "] failed, unable to retrieve session token");
868+
"FlashArray [" + url + "] rejected the api-token at /" + apiVersion + "/login");
781869
} else {
782870
throw new CloudRuntimeException(
783-
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
784-
+ "] - " + response.getStatusLine().getReasonPhrase());
871+
"Unexpected HTTP response code from FlashArray [" + url + "] /" + apiVersion
872+
+ "/login - [" + statusCode + "] - "
873+
+ response.getStatusLine().getReasonPhrase());
785874
}
786-
787875
} catch (UnsupportedEncodingException e) {
788-
throw new CloudRuntimeException("Error creating input for login, check username/password encoding");
876+
throw new CloudRuntimeException("Error encoding login form for FlashArray [" + url + "]", e);
789877
} catch (UnsupportedOperationException e) {
790878
throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e);
791879
} catch (IOException e) {
792880
throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e);
793881
} finally {
794-
try {
795-
if (response != null) {
882+
if (response != null) {
883+
try {
796884
response.close();
885+
} catch (IOException e) {
886+
logger.debug("Error closing response from login attempt to FlashArray", e);
797887
}
798-
} catch (IOException e) {
799-
logger.debug("Error closing response from login attempt to FlashArray", e);
800888
}
801889
}
802890
}
@@ -964,7 +1052,7 @@ private <T> T PATCH(String path, Object input, final TypeReference<T> type) {
9641052
request.setEntity(new StringEntity(data));
9651053

9661054
CloseableHttpClient client = getClient();
967-
response = (CloseableHttpResponse) client.execute(request);
1055+
response = client.execute(request);
9681056

9691057
final int statusCode = response.getStatusLine().getStatusCode();
9701058
if (statusCode == 200 || statusCode == 201) {
@@ -1019,7 +1107,7 @@ private <T> T GET(String path, final TypeReference<T> type) {
10191107
request.addHeader("X-auth-token", getAccessToken());
10201108

10211109
CloseableHttpClient client = getClient();
1022-
response = (CloseableHttpResponse) client.execute(request);
1110+
response = client.execute(request);
10231111
final int statusCode = response.getStatusLine().getStatusCode();
10241112
if (statusCode == 200) {
10251113
try {
@@ -1061,7 +1149,7 @@ private void DELETE(String path) {
10611149
request.addHeader("X-auth-token", getAccessToken());
10621150

10631151
CloseableHttpClient client = getClient();
1064-
response = (CloseableHttpResponse) client.execute(request);
1152+
response = client.execute(request);
10651153
final int statusCode = response.getStatusLine().getStatusCode();
10661154
if (statusCode == 200 || statusCode == 404 || statusCode == 400) {
10671155
// this means the volume was deleted successfully, or doesn't exist (effective

0 commit comments

Comments
 (0)