Skip to content

Commit 76a4bc8

Browse files
committed
Merge remote-tracking branch 'apache/4.20' into 4.22
2 parents 63c142b + e8df87e commit 76a4bc8

3 files changed

Lines changed: 1512 additions & 79 deletions

File tree

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;
@@ -589,6 +596,91 @@ private String getAccessToken() {
589596
return accessToken;
590597
}
591598

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

665757
apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION);
666-
if (apiVersion == null) {
758+
boolean apiVersionExplicit = apiVersion != null;
759+
if (!apiVersionExplicit) {
667760
apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION);
668-
if (apiVersion == null) {
761+
apiVersionExplicit = apiVersion != null;
762+
if (!apiVersionExplicit) {
669763
apiVersion = API_VERSION_DEFAULT;
670764
}
671765
}
@@ -732,72 +826,66 @@ private void login() {
732826
skipTlsValidation = true;
733827
}
734828

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

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

770-
statusCode = response.getStatusLine().getStatusCode();
771-
if (statusCode == 200 | statusCode == 201) {
855+
// Exchange the long-lived api-token for a short-lived x-auth-token (REST 2.x).
856+
HttpPost request = new HttpPost(url + "/" + apiVersion + "/login");
857+
request.addHeader("api-token", apiToken);
858+
response = client.execute(request);
859+
int statusCode = response.getStatusLine().getStatusCode();
860+
if (statusCode == 200 || statusCode == 201) {
772861
Header[] headers = response.getHeaders("x-auth-token");
773862
if (headers == null || headers.length == 0) {
774863
throw new CloudRuntimeException(
775-
"Getting access token responded successfully but access token was not available");
864+
"FlashArray /login responded successfully but no x-auth-token header was returned");
776865
}
777866
accessToken = headers[0].getValue();
778867
} else if (statusCode == 401 || statusCode == 403) {
779868
throw new CloudRuntimeException(
780-
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
781-
+ "] failed, unable to retrieve session token");
869+
"FlashArray [" + url + "] rejected the api-token at /" + apiVersion + "/login");
782870
} else {
783871
throw new CloudRuntimeException(
784-
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
785-
+ "] - " + response.getStatusLine().getReasonPhrase());
872+
"Unexpected HTTP response code from FlashArray [" + url + "] /" + apiVersion
873+
+ "/login - [" + statusCode + "] - "
874+
+ response.getStatusLine().getReasonPhrase());
786875
}
787-
788876
} catch (UnsupportedEncodingException e) {
789-
throw new CloudRuntimeException("Error creating input for login, check username/password encoding");
877+
throw new CloudRuntimeException("Error encoding login form for FlashArray [" + url + "]", e);
790878
} catch (UnsupportedOperationException e) {
791879
throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e);
792880
} catch (IOException e) {
793881
throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e);
794882
} finally {
795-
try {
796-
if (response != null) {
883+
if (response != null) {
884+
try {
797885
response.close();
886+
} catch (IOException e) {
887+
logger.debug("Error closing response from login attempt to FlashArray", e);
798888
}
799-
} catch (IOException e) {
800-
logger.debug("Error closing response from login attempt to FlashArray", e);
801889
}
802890
}
803891
}
@@ -965,7 +1053,7 @@ private <T> T PATCH(String path, Object input, final TypeReference<T> type) {
9651053
request.setEntity(new StringEntity(data));
9661054

9671055
CloseableHttpClient client = getClient();
968-
response = (CloseableHttpResponse) client.execute(request);
1056+
response = client.execute(request);
9691057

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

10221110
CloseableHttpClient client = getClient();
1023-
response = (CloseableHttpResponse) client.execute(request);
1111+
response = client.execute(request);
10241112
final int statusCode = response.getStatusLine().getStatusCode();
10251113
if (statusCode == 200) {
10261114
try {
@@ -1062,7 +1150,7 @@ private void DELETE(String path) {
10621150
request.addHeader("X-auth-token", getAccessToken());
10631151

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

0 commit comments

Comments
 (0)