2828import java .util .ArrayList ;
2929import java .util .HashMap ;
3030import java .util .Map ;
31+ import java .util .Set ;
32+ import java .util .concurrent .ConcurrentHashMap ;
3133
3234import javax .net .ssl .HostnameVerifier ;
3335import javax .net .ssl .SSLContext ;
6365import com .cloud .utils .exception .CloudRuntimeException ;
6466import com .fasterxml .jackson .core .JsonProcessingException ;
6567import com .fasterxml .jackson .core .type .TypeReference ;
68+ import com .fasterxml .jackson .databind .JsonNode ;
6669import com .fasterxml .jackson .databind .ObjectMapper ;
6770import org .apache .logging .log4j .LogManager ;
6871import 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