Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import com.cloud.utils.exception.CloudRuntimeException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -592,9 +593,11 @@ private void login() {
}

apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION);
if (apiVersion == null) {
boolean apiVersionExplicit = apiVersion != null;
if (!apiVersionExplicit) {
apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION);
if (apiVersion == null) {
apiVersionExplicit = apiVersion != null;
if (!apiVersionExplicit) {
apiVersion = API_VERSION_DEFAULT;
}
}
Expand Down Expand Up @@ -661,72 +664,125 @@ private void login() {
skipTlsValidation = true;
}

// Resolve the long-lived API token. Prefer a pre-minted api_token (Purity REST 2.x flow);
// fall back to legacy username/password auth via Purity REST 1.x for backward compatibility.
String apiToken = connectionDetails.get(ProviderAdapter.API_TOKEN_KEY);
if (apiToken != null && apiToken.isEmpty()) {
apiToken = null;
}
boolean usingLegacyUserPass = apiToken == null;
if (usingLegacyUserPass && (username == null || password == null)) {
throw new CloudRuntimeException("FlashArray adapter requires either " + ProviderAdapter.API_TOKEN_KEY
+ " (preferred) or both " + ProviderAdapter.API_USERNAME_KEY + " and "
+ ProviderAdapter.API_PASSWORD_KEY + " in the connection details");
}

CloseableHttpClient client = getClient();
CloseableHttpResponse response = null;
try {
HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
// request.addHeader("Content-Type", "application/json");
// request.addHeader("Accept", "application/json");
ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
postParms.add(new BasicNameValuePair("username", username));
postParms.add(new BasicNameValuePair("password", password));
request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
CloseableHttpClient client = getClient();
response = (CloseableHttpResponse) client.execute(request);
// Discover the latest supported API version from the array unless one was explicitly configured.
// GET /api/api_version is unauthenticated and returns {"version":["1.0",...,"2.36"]}.
if (!apiVersionExplicit) {
HttpGet vReq = new HttpGet(url + "/api_version");
CloseableHttpResponse vResp = null;
try {
vResp = (CloseableHttpResponse) client.execute(vReq);
if (vResp.getStatusLine().getStatusCode() == 200) {
JsonNode root = mapper.readTree(vResp.getEntity().getContent());
JsonNode versions = root.get("version");
if (versions != null && versions.isArray() && versions.size() > 0) {
apiVersion = versions.get(versions.size() - 1).asText();
}
} else {
logger.warn("Unexpected HTTP " + vResp.getStatusLine().getStatusCode()
+ " from FlashArray [" + url + "] /api_version, falling back to default "
+ API_VERSION_DEFAULT);
}
} catch (Exception e) {
logger.warn("Failed to discover Purity REST API version from " + url
+ "/api_version, falling back to default " + API_VERSION_DEFAULT, e);
} finally {
if (vResp != null) {
try {
vResp.close();
} catch (IOException e) {
logger.debug("Error closing /api/api_version response from FlashArray [" + url + "]", e);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request is sent to .../api_version (relative to url), but the debug message says "Error closing /api/api_version response...". Align the log text with the actual endpoint/path to avoid confusion during troubleshooting.

Suggested change
logger.debug("Error closing /api/api_version response from FlashArray [" + url + "]", e);
logger.debug("Error closing /api_version response from FlashArray [" + url + "]", e);

Copilot uses AI. Check for mistakes.
}
}
}
}

int statusCode = response.getStatusLine().getStatusCode();
FlashArrayApiToken apitoken = null;
if (statusCode == 200 | statusCode == 201) {
apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class);
if (apitoken == null) {
if (usingLegacyUserPass) {
logger.warn("FlashArray adapter at [" + url + "] is using deprecated username/password "
+ "login against Purity REST 1.x. Replace with a pre-minted "
+ ProviderAdapter.API_TOKEN_KEY + " detail; the username/password code path will be "
+ "removed in a future release.");
Comment on lines +715 to +719
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deprecation warning for legacy username/password auth is emitted on every login/refresh. With the default key TTL (~14 minutes), this can spam logs for each configured FlashArray pool. Consider rate-limiting (e.g., log once per adapter instance/endpoint) or downgrading subsequent messages to DEBUG after the first WARN.

Copilot uses AI. Check for mistakes.
HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
postParms.add(new BasicNameValuePair("username", username));
postParms.add(new BasicNameValuePair("password", password));
request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
response = (CloseableHttpResponse) client.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 || statusCode == 201) {
FlashArrayApiToken legacyToken = mapper.readValue(response.getEntity().getContent(),
FlashArrayApiToken.class);
if (legacyToken == null || legacyToken.getApiToken() == null) {
throw new CloudRuntimeException(
"Authentication responded successfully but no api token was returned");
}
apiToken = legacyToken.getApiToken();
} else if (statusCode == 401 || statusCode == 403) {
throw new CloudRuntimeException(
"Authentication responded successfully but no api token was returned");
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
+ "] failed, unable to retrieve session token");
} else {
throw new CloudRuntimeException(
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
+ "] - " + response.getStatusLine().getReasonPhrase());
}
} else if (statusCode == 401 || statusCode == 403) {
throw new CloudRuntimeException(
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
+ "] failed, unable to retrieve session token");
} else {
throw new CloudRuntimeException(
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
+ "] - " + response.getStatusLine().getReasonPhrase());
try {
response.close();
} catch (IOException e) {
logger.debug("Error closing legacy auth/apitoken response from FlashArray [" + url + "]", e);
}
response = null;
}

// now we need to get the access token
request = new HttpPost(url + "/" + apiVersion + "/login");
request.addHeader("api-token", apitoken.getApiToken());
// Exchange the long-lived api-token for a short-lived x-auth-token (REST 2.x).
HttpPost request = new HttpPost(url + "/" + apiVersion + "/login");
request.addHeader("api-token", apiToken);
response = (CloseableHttpResponse) client.execute(request);

statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 | statusCode == 201) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 || statusCode == 201) {
Header[] headers = response.getHeaders("x-auth-token");
if (headers == null || headers.length == 0) {
throw new CloudRuntimeException(
"Getting access token responded successfully but access token was not available");
"FlashArray /login responded successfully but no x-auth-token header was returned");
}
accessToken = headers[0].getValue();
} else if (statusCode == 401 || statusCode == 403) {
throw new CloudRuntimeException(
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
+ "] failed, unable to retrieve session token");
"FlashArray [" + url + "] rejected the api-token at /" + apiVersion + "/login");
} else {
throw new CloudRuntimeException(
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
+ "] - " + response.getStatusLine().getReasonPhrase());
"Unexpected HTTP response code from FlashArray [" + url + "] /" + apiVersion
+ "/login - [" + statusCode + "] - "
+ response.getStatusLine().getReasonPhrase());
}

} catch (UnsupportedEncodingException e) {
throw new CloudRuntimeException("Error creating input for login, check username/password encoding");
throw new CloudRuntimeException("Error encoding login form for FlashArray [" + url + "]", e);
} catch (UnsupportedOperationException e) {
throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e);
} catch (IOException e) {
throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e);
} finally {
try {
if (response != null) {
if (response != null) {
try {
response.close();
} catch (IOException e) {
logger.debug("Error closing response from login attempt to FlashArray", e);
}
} catch (IOException e) {
logger.debug("Error closing response from login attempt to FlashArray", e);
}
}
}
Expand Down
Loading