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 ;
@@ -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