flasharray: authenticate via REST 2.x api-token and discover API version (#13060)
Some checks failed
Build / build (push) Has been cancelled
Simulator CI / build (component/find_hosts_for_migration component/test_acl_isolatednetwork component/test_acl_isolatednetwork_delete component/test_acl_listsnapshot) (push) Has been cancelled
Simulator CI / build (component/test_acl_listvm component/test_acl_listvolume) (push) Has been cancelled
Simulator CI / build (component/test_acl_sharednetwork component/test_acl_sharednetwork_deployVM-impersonation component/test_user_private_gateway component/test_user_shared_network) (push) Has been cancelled
Simulator CI / build (component/test_affinity_groups_projects component/test_allocation_states component/test_assign_vm) (push) Has been cancelled
Simulator CI / build (component/test_concurrent_snapshots_limit component/test_cpu_domain_limits component/test_cpu_limits component/test_cpu_max_limits component/test_cpu_project_limits component/test_deploy_vm_userdata_multi_nic) (push) Has been cancelled
Simulator CI / build (component/test_egress_fw_rules component/test_invalid_gw_nm component/test_ip_reservation) (push) Has been cancelled
Simulator CI / build (component/test_lb_secondary_ip component/test_list_nics component/test_list_pod component/test_memory_limits) (push) Has been cancelled
Simulator CI / build (component/test_mm_domain_limits component/test_mm_max_limits component/test_mm_project_limits component/test_network_offering component/test_non_contiguous_vlan) (push) Has been cancelled
Simulator CI / build (component/test_persistent_networks component/test_project_configs component/test_project_limits component/test_project_resources) (push) Has been cancelled
Simulator CI / build (component/test_project_usage component/test_protocol_number_security_group component/test_public_ip component/test_resource_limits component/test_resource_limit_tags) (push) Has been cancelled
Simulator CI / build (component/test_regions_accounts component/test_routers component/test_snapshots component/test_stopped_vm component/test_tags component/test_templates component/test_updateResourceCount component/test_update_vm) (push) Has been cancelled
Simulator CI / build (component/test_volumes component/test_vpc component/test_vpc_distributed_routing_offering component/test_vpc_network component/test_vpc_offerings component/test_vpc_routers component/test_vpn_users component/test_vpc_network_lbrules) (push) Has been cancelled
Simulator CI / build (smoke/test_accounts smoke/test_account_access smoke/test_affinity_groups smoke/test_affinity_groups_projects smoke/test_annotations smoke/test_async_job smoke/test_attach_multiple_volumes smoke/test_backup_recovery_dummy smoke/test_certauthority… (push) Has been cancelled
Simulator CI / build (smoke/test_cluster_drs smoke/test_dynamicroles smoke/test_enable_account_settings_for_domain smoke/test_enable_role_based_users_in_projects smoke/test_events_resource smoke/test_global_settings smoke/test_guest_vlan_range smoke/test_host_mainten… (push) Has been cancelled
Simulator CI / build (smoke/test_list_accounts smoke/test_list_disk_offerings smoke/test_list_domains smoke/test_list_hosts smoke/test_list_service_offerings smoke/test_list_storage_pools smoke/test_list_volumes) (push) Has been cancelled
Simulator CI / build (smoke/test_network smoke/test_network_acl smoke/test_network_ipv6 smoke/test_network_permissions smoke/test_nic smoke/test_nic_adapter_type smoke/test_non_contigiousvlan smoke/test_object_stores smoke/test_outofbandmanagement smoke/test_outofban… (push) Has been cancelled
Simulator CI / build (smoke/test_router_dhcphosts smoke/test_router_dns smoke/test_router_dnsservice smoke/test_routers smoke/test_routers_iptables_default_policy smoke/test_routers_network_ops smoke/test_scale_vm smoke/test_secondary_storage smoke/test_service_offer… (push) Has been cancelled
Coverage Check / codecov (push) Has been cancelled
PR Merge Conflict Check / triage (push) Has been cancelled
License Check / build (push) Has been cancelled
UI Build / build (push) Has been cancelled

Signed-off-by: Eugenio Grosso <eugenio.grosso@gmail.com>
Co-authored-by: Eugenio Grosso <egrosso@purestorage.com>
This commit is contained in:
Eugenio Grosso 2026-07-10 21:02:07 +02:00 committed by GitHub
parent ec2d3ea1e6
commit e8df87e89b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -28,6 +28,8 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
@ -63,6 +65,7 @@ import org.apache.http.ssl.SSLContextBuilder;
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;
@ -87,6 +90,10 @@ public class FlashArrayAdapter implements ProviderAdapter {
private static final String API_LOGIN_VERSION_DEFAULT = "1.19";
private static final String API_VERSION_DEFAULT = "2.23";
// URLs for which the legacy-auth deprecation WARN has already been emitted,
// so we don't spam the logs once per refresh per pool while it's still configured.
private static final Set<String> WARNED_LEGACY_URLS = ConcurrentHashMap.newKeySet();
static final ObjectMapper mapper = new ObjectMapper();
public String pod = null;
public String hostgroup = null;
@ -588,6 +595,91 @@ public class FlashArrayAdapter implements ProviderAdapter {
return accessToken;
}
/**
* Discover the latest supported Purity REST API version by hitting the unauthenticated
* {@code /api/api_version} endpoint (returns {@code {"version":["1.0",...,"2.36"]}}).
* The discovered version is stored on {@link #apiVersion}; on failure the caller-configured
* default remains in place.
*/
private void fetchApiVersionFromPurity(CloseableHttpClient client) {
HttpGet vReq = new HttpGet(url + "/api_version");
CloseableHttpResponse vResp = null;
try {
vResp = 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_version response from FlashArray [" + url + "]", e);
}
}
}
}
/**
* Exchange the operator-configured username/password for a long-lived Purity api-token
* via REST 1.x {@code /auth/apitoken}. Emits the once-per-URL deprecation WARN.
* @return the api-token to feed into the REST 2.x /login exchange.
*/
private String getApiTokenUsingUserPass(CloseableHttpClient client) throws IOException {
if (WARNED_LEGACY_URLS.add(url)) {
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.");
}
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"));
CloseableHttpResponse response = null;
try {
response = 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");
}
return legacyToken.getApiToken();
} 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());
}
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
logger.debug("Error closing legacy auth/apitoken response from FlashArray [" + url + "]", e);
}
}
}
}
private synchronized void refreshSession(boolean force) {
try {
if (force || keyExpiration < System.currentTimeMillis()) {
@ -662,9 +754,11 @@ public class FlashArrayAdapter implements ProviderAdapter {
}
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;
}
}
@ -731,72 +825,66 @@ public class FlashArrayAdapter implements ProviderAdapter {
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);
int statusCode = response.getStatusLine().getStatusCode();
FlashArrayApiToken apitoken = null;
if (statusCode == 200 | statusCode == 201) {
apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class);
if (apitoken == null) {
throw new CloudRuntimeException(
"Authentication responded successfully but no api token was returned");
}
} 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());
// 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) {
fetchApiVersionFromPurity(client);
}
// now we need to get the access token
request = new HttpPost(url + "/" + apiVersion + "/login");
request.addHeader("api-token", apitoken.getApiToken());
response = (CloseableHttpResponse) client.execute(request);
if (usingLegacyUserPass) {
apiToken = getApiTokenUsingUserPass(client);
}
statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 | statusCode == 201) {
// 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 = client.execute(request);
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);
}
}
}
@ -964,7 +1052,7 @@ public class FlashArrayAdapter implements ProviderAdapter {
request.setEntity(new StringEntity(data));
CloseableHttpClient client = getClient();
response = (CloseableHttpResponse) client.execute(request);
response = client.execute(request);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 || statusCode == 201) {
@ -1019,7 +1107,7 @@ public class FlashArrayAdapter implements ProviderAdapter {
request.addHeader("X-auth-token", getAccessToken());
CloseableHttpClient client = getClient();
response = (CloseableHttpResponse) client.execute(request);
response = client.execute(request);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
try {
@ -1061,7 +1149,7 @@ public class FlashArrayAdapter implements ProviderAdapter {
request.addHeader("X-auth-token", getAccessToken());
CloseableHttpClient client = getClient();
response = (CloseableHttpResponse) client.execute(request);
response = client.execute(request);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 || statusCode == 404 || statusCode == 400) {
// this means the volume was deleted successfully, or doesn't exist (effective