/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.atlas;
import static org.apache.atlas.security.SecurityProperties.TLS_ENABLED;
import java.io.IOException;
import java.net.ConnectException;
import java.util.List;
import java.util.Map;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.apache.atlas.model.metrics.AtlasMetrics;
import org.apache.atlas.security.SecureClientUtils;
import org.apache.atlas.utils.AuthenticationUtil;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.security.UserGroupInformation;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.GenericType;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
import com.sun.jersey.api.json.JSONConfiguration;
import com.sun.jersey.client.urlconnection.URLConnectionClientHandler;
public abstract class AtlasBaseClient {
public static final String BASE_URI = "api/atlas/";
public static final String TYPES = "types";
public static final String ADMIN_VERSION = "admin/version";
public static final String ADMIN_STATUS = "admin/status";
public static final String ADMIN_METRICS = "admin/metrics";
public static final String HTTP_AUTHENTICATION_ENABLED = "atlas.http.authentication.enabled";
//Admin operations
public static final APIInfo VERSION = new APIInfo(BASE_URI + ADMIN_VERSION, HttpMethod.GET, Response.Status.OK);
public static final APIInfo STATUS = new APIInfo(BASE_URI + ADMIN_STATUS, HttpMethod.GET, Response.Status.OK);
public static final APIInfo METRICS = new APIInfo(BASE_URI + ADMIN_METRICS, HttpMethod.GET, Response.Status.OK);
static final String JSON_MEDIA_TYPE = MediaType.APPLICATION_JSON + "; charset=UTF-8";
static final String UNKNOWN_STATUS = "Unknown status";
static final String ATLAS_CLIENT_HA_RETRIES_KEY = "atlas.client.ha.retries";
// Setting the default value based on testing failovers while client code like quickstart is running.
static final int DEFAULT_NUM_RETRIES = 4;
static final String ATLAS_CLIENT_HA_SLEEP_INTERVAL_MS_KEY = "atlas.client.ha.sleep.interval.ms";
// Setting the default value based on testing failovers while client code like quickstart is running.
// With number of retries, this gives a total time of about 20s for the server to start.
static final int DEFAULT_SLEEP_BETWEEN_RETRIES_MS = 5000;
private static final Logger LOG = LoggerFactory.getLogger(AtlasBaseClient.class);
protected WebResource service;
protected Configuration configuration;
private String basicAuthUser;
private String basicAuthPassword;
private AtlasClientContext atlasClientContext;
private boolean retryEnabled = false;
protected AtlasBaseClient() {
}
protected AtlasBaseClient(String[] baseUrl, String[] basicAuthUserNamePassword) {
if (basicAuthUserNamePassword != null) {
if (basicAuthUserNamePassword.length > 0) {
this.basicAuthUser = basicAuthUserNamePassword[0];
}
if (basicAuthUserNamePassword.length > 1) {
this.basicAuthPassword = basicAuthUserNamePassword[1];
}
}
initializeState(baseUrl, null, null);
}
protected AtlasBaseClient(String... baseUrls) throws AtlasException {
this(getCurrentUGI(), baseUrls);
}
protected AtlasBaseClient(UserGroupInformation ugi, String[] baseUrls) {
this(ugi, ugi.getShortUserName(), baseUrls);
}
protected AtlasBaseClient(UserGroupInformation ugi, String doAsUser, String[] baseUrls) {
initializeState(baseUrls, ugi, doAsUser);
}
@VisibleForTesting
protected AtlasBaseClient(WebResource service, Configuration configuration) {
this.service = service;
this.configuration = configuration;
}
@VisibleForTesting
protected AtlasBaseClient(Configuration configuration, String[] baseUrl, String[] basicAuthUserNamePassword) {
if (basicAuthUserNamePassword != null) {
if (basicAuthUserNamePassword.length > 0) {
this.basicAuthUser = basicAuthUserNamePassword[0];
}
if (basicAuthUserNamePassword.length > 1) {
this.basicAuthPassword = basicAuthUserNamePassword[1];
}
}
initializeState(configuration, baseUrl, null, null);
}
protected static UserGroupInformation getCurrentUGI() throws AtlasException {
try {
return UserGroupInformation.getCurrentUser();
} catch (IOException e) {
throw new AtlasException(e);
}
}
void initializeState(String[] baseUrls, UserGroupInformation ugi, String doAsUser) {
initializeState(getClientProperties(), baseUrls, ugi, doAsUser);
}
void initializeState(Configuration configuration, String[] baseUrls, UserGroupInformation ugi, String doAsUser) {
this.configuration = configuration;
Client client = getClient(configuration, ugi, doAsUser);
if ((!AuthenticationUtil.isKerberosAuthenticationEnabled()) && basicAuthUser != null && basicAuthPassword != null) {
final HTTPBasicAuthFilter authFilter = new HTTPBasicAuthFilter(basicAuthUser, basicAuthPassword);
client.addFilter(authFilter);
}
String activeServiceUrl = determineActiveServiceURL(baseUrls, client);
atlasClientContext = new AtlasClientContext(baseUrls, client, ugi, doAsUser);
service = client.resource(UriBuilder.fromUri(activeServiceUrl).build());
}
@VisibleForTesting
protected Client getClient(Configuration configuration, UserGroupInformation ugi, String doAsUser) {
DefaultClientConfig config = new DefaultClientConfig();
// Enable POJO mapping feature
config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
int readTimeout = configuration.getInt("atlas.client.readTimeoutMSecs", 60000);
int connectTimeout = configuration.getInt("atlas.client.connectTimeoutMSecs", 60000);
if (configuration.getBoolean(TLS_ENABLED, false)) {
// create an SSL properties configuration if one doesn't exist. SSLFactory expects a file, so forced
// to create a
// configuration object, persist it, then subsequently pass in an empty configuration to SSLFactory
try {
SecureClientUtils.persistSSLClientConfiguration(configuration);
} catch (Exception e) {
LOG.info("Error processing client configuration.", e);
}
}
final URLConnectionClientHandler handler;
if ((!AuthenticationUtil.isKerberosAuthenticationEnabled()) && basicAuthUser != null && basicAuthPassword != null) {
if (configuration.getBoolean(TLS_ENABLED, false)) {
handler = SecureClientUtils.getUrlConnectionClientHandler();
} else {
handler = new URLConnectionClientHandler();
}
} else {
handler = SecureClientUtils.getClientConnectionHandler(config, configuration, doAsUser, ugi);
}
Client client = new Client(handler, config);
client.setReadTimeout(readTimeout);
client.setConnectTimeout(connectTimeout);
return client;
}
@VisibleForTesting
protected String determineActiveServiceURL(String[] baseUrls, Client client) {
if (baseUrls.length == 0) {
throw new IllegalArgumentException("Base URLs cannot be null or empty");
}
final String baseUrl;
AtlasServerEnsemble atlasServerEnsemble = new AtlasServerEnsemble(baseUrls);
if (atlasServerEnsemble.hasSingleInstance()) {
baseUrl = atlasServerEnsemble.firstURL();
LOG.info("Client has only one service URL, will use that for all actions: {}", baseUrl);
} else {
try {
baseUrl = selectActiveServerAddress(client, atlasServerEnsemble);
} catch (AtlasServiceException e) {
LOG.error("None of the passed URLs are active: {}", atlasServerEnsemble, e);
throw new IllegalArgumentException("None of the passed URLs are active " + atlasServerEnsemble, e);
}
}
return baseUrl;
}
private String selectActiveServerAddress(Client client, AtlasServerEnsemble serverEnsemble)
throws AtlasServiceException {
List<String> serverInstances = serverEnsemble.getMembers();
String activeServerAddress = null;
for (String serverInstance : serverInstances) {
LOG.info("Trying with address {}", serverInstance);
activeServerAddress = getAddressIfActive(client, serverInstance);
if (activeServerAddress != null) {
LOG.info("Found service {} as active service.", serverInstance);
break;
}
}
if (activeServerAddress != null)
return activeServerAddress;
else
throw new AtlasServiceException(STATUS, new RuntimeException("Could not find any active instance"));
}
private String getAddressIfActive(Client client, String serverInstance) {
String activeServerAddress = null;
for (int i = 0; i < getNumberOfRetries(); i++) {
try {
service = client.resource(UriBuilder.fromUri(serverInstance).build());
String adminStatus = getAdminStatus();
if (StringUtils.equals(adminStatus, "ACTIVE")) {
activeServerAddress = serverInstance;
break;
} else {
LOG.info("attempt #{}: Service {} - is not active. status={}", (i + 1), serverInstance, adminStatus);
}
} catch (Exception e) {
LOG.error("attempt #{}: Service {} - could not get status", (i + 1), serverInstance, e);
}
sleepBetweenRetries();
}
return activeServerAddress;
}
protected Configuration getClientProperties() {
try {
if (configuration == null) {
configuration = ApplicationProperties.get();
}
} catch (AtlasException e) {
LOG.error("Exception while loading configuration.", e);
}
return configuration;
}
public boolean isServerReady() throws AtlasServiceException {
WebResource resource = getResource(VERSION.getPath());
try {
callAPIWithResource(VERSION, resource, null, JSONObject.class);
return true;
} catch (ClientHandlerException che) {
return false;
} catch (AtlasServiceException ase) {
if (ase.getStatus() != null && ase.getStatus().equals(ClientResponse.Status.SERVICE_UNAVAILABLE)) {
LOG.warn("Received SERVICE_UNAVAILABLE, server is not yet ready");
return false;
}
throw ase;
}
}
protected WebResource getResource(String path, String... pathParams) {
return getResource(service, path, pathParams);
}
protected <T> T callAPIWithResource(APIInfo api, WebResource resource, Object requestObject, Class<T> responseType) throws AtlasServiceException {
GenericType<T> genericType = null;
if (responseType != null) {
genericType = new GenericType<>(responseType);
}
return callAPIWithResource(api, resource, requestObject, genericType);
}
protected <T> T callAPIWithResource(APIInfo api, WebResource resource, Object requestObject, GenericType<T> responseType) throws AtlasServiceException {
ClientResponse clientResponse = null;
int i = 0;
do {
if (LOG.isDebugEnabled()) {
LOG.debug("Calling API [ {} : {} ] {}", api.getMethod(), api.getPath(), requestObject != null ? "<== " + requestObject : "");
}
clientResponse = resource
.accept(JSON_MEDIA_TYPE)
.type(JSON_MEDIA_TYPE)
.method(api.getMethod(), ClientResponse.class, requestObject);
if (LOG.isDebugEnabled()) {
LOG.debug("API {} returned status {}", resource.getURI(), clientResponse.getStatus());
}
if (clientResponse.getStatus() == api.getExpectedStatus().getStatusCode()) {
if (null == responseType) {
return null;
}
try {
if (responseType.getRawClass() == JSONObject.class) {
String stringEntity = clientResponse.getEntity(String.class);
try {
JSONObject jsonObject = new JSONObject(stringEntity);
LOG.info("Response = {}", jsonObject);
return (T) jsonObject;
} catch (JSONException e) {
throw new AtlasServiceException(api, e);
}
} else {
T entity = clientResponse.getEntity(responseType);
return entity;
}
} catch (ClientHandlerException e) {
throw new AtlasServiceException(api, e);
}
} else if (clientResponse.getStatus() != ClientResponse.Status.SERVICE_UNAVAILABLE.getStatusCode()) {
break;
} else {
LOG.error("Got a service unavailable when calling: {}, will retry..", resource);
sleepBetweenRetries();
}
i++;
} while (i < getNumberOfRetries());
throw new AtlasServiceException(api, clientResponse);
}
private WebResource getResource(WebResource service, String path, String... pathParams) {
WebResource resource = service.path(path);
resource = appendPathParams(resource, pathParams);
return resource;
}
void sleepBetweenRetries() {
try {
Thread.sleep(getSleepBetweenRetriesMs());
} catch (InterruptedException e) {
LOG.error("Interrupted from sleeping between retries.", e);
}
}
int getNumberOfRetries() {
return configuration.getInt(AtlasBaseClient.ATLAS_CLIENT_HA_RETRIES_KEY, AtlasBaseClient.DEFAULT_NUM_RETRIES);
}
private int getSleepBetweenRetriesMs() {
return configuration.getInt(AtlasBaseClient.ATLAS_CLIENT_HA_SLEEP_INTERVAL_MS_KEY, AtlasBaseClient.DEFAULT_SLEEP_BETWEEN_RETRIES_MS);
}
/**
* Return status of the service instance the client is pointing to.
*
* @return One of the values in ServiceState.ServiceStateValue or {@link #UNKNOWN_STATUS} if
* there is a JSON parse exception
* @throws AtlasServiceException if there is a HTTP error.
*/
public String getAdminStatus() throws AtlasServiceException {
String result = AtlasBaseClient.UNKNOWN_STATUS;
WebResource resource = getResource(service, STATUS.getPath());
JSONObject response = callAPIWithResource(STATUS, resource, null, JSONObject.class);
try {
result = response.getString("Status");
} catch (JSONException e) {
LOG.error("Exception while parsing admin status response. Returned response {}", response.toString(), e);
}
return result;
}
/**
* @return Return metrics of the service instance the client is pointing to
* @throws AtlasServiceException
*/
public AtlasMetrics getAtlasMetrics() throws AtlasServiceException {
return callAPI(METRICS, AtlasMetrics.class, null);
}
boolean isRetryableException(ClientHandlerException che) {
return che.getCause().getClass().equals(IOException.class)
|| che.getCause().getClass().equals(ConnectException.class);
}
void handleClientHandlerException(ClientHandlerException che) {
if (isRetryableException(che)) {
atlasClientContext.getClient().destroy();
LOG.warn("Destroyed current context while handling ClientHandlerEception.");
LOG.warn("Will retry and create new context.");
sleepBetweenRetries();
initializeState(atlasClientContext.getBaseUrls(), atlasClientContext.getUgi(),
atlasClientContext.getDoAsUser());
return;
}
throw che;
}
@VisibleForTesting
JSONObject callAPIWithRetries(APIInfo api, Object requestObject, ResourceCreator resourceCreator)
throws AtlasServiceException {
for (int i = 0; i < getNumberOfRetries(); i++) {
WebResource resource = resourceCreator.createResource();
try {
LOG.debug("Using resource {} for {} times", resource.getURI(), i);
return callAPIWithResource(api, resource, requestObject, JSONObject.class);
} catch (ClientHandlerException che) {
if (i == (getNumberOfRetries() - 1)) {
throw che;
}
LOG.warn("Handled exception in calling api {}", api.getPath(), che);
LOG.warn("Exception's cause: {}", che.getCause().getClass());
handleClientHandlerException(che);
}
}
throw new AtlasServiceException(api, new RuntimeException("Could not get response after retries."));
}
public <T> T callAPI(APIInfo api, Class<T> responseType, Object requestObject, String... params)
throws AtlasServiceException {
return callAPIWithResource(api, getResource(api, params), requestObject, responseType);
}
public <T> T callAPI(APIInfo api, GenericType<T> responseType, Object requestObject, String... params)
throws AtlasServiceException {
return callAPIWithResource(api, getResource(api, params), requestObject, responseType);
}
public <T> T callAPI(APIInfo api, Class<T> responseType, Object requestBody,
MultivaluedMap<String, String> queryParams, String... params) throws AtlasServiceException {
WebResource resource = getResource(api, queryParams, params);
return callAPIWithResource(api, resource, requestBody, responseType);
}
public <T> T callAPI(APIInfo api, Class<T> responseType, MultivaluedMap<String, String> queryParams, String... params)
throws AtlasServiceException {
WebResource resource = getResource(api, queryParams, params);
return callAPIWithResource(api, resource, null, responseType);
}
public <T> T callAPI(APIInfo api, GenericType<T> responseType, MultivaluedMap<String, String> queryParams, String... params)
throws AtlasServiceException {
WebResource resource = getResource(api, queryParams, params);
return callAPIWithResource(api, resource, null, responseType);
}
protected WebResource getResource(APIInfo api, String... pathParams) {
return getResource(service, api, pathParams);
}
// Modify URL to include the path params
private WebResource getResource(WebResource service, APIInfo api, String... pathParams) {
WebResource resource = service.path(api.getPath());
resource = appendPathParams(resource, pathParams);
return resource;
}
public <T> T callAPI(APIInfo api, Class<T> responseType, MultivaluedMap<String, String> queryParams)
throws AtlasServiceException {
return callAPIWithResource(api, getResource(api, queryParams), null, responseType);
}
public <T> T callAPI(APIInfo api, Class<T> responseType, String queryParamKey, List<String> queryParamValues)
throws AtlasServiceException {
return callAPIWithResource(api, getResource(api, queryParamKey, queryParamValues), null, responseType);
}
private WebResource getResource(APIInfo api, String queryParamKey, List<String> queryParamValues) {
WebResource resource = service.path(api.getPath());
for (String queryParamValue : queryParamValues) {
if (StringUtils.isNotBlank(queryParamKey) && StringUtils.isNotBlank(queryParamValue)) {
resource = resource.queryParam(queryParamKey, queryParamValue);
}
}
return resource;
}
protected WebResource getResource(APIInfo api, MultivaluedMap<String, String> queryParams, String... pathParams) {
WebResource resource = service.path(api.getPath());
resource = appendPathParams(resource, pathParams);
resource = appendQueryParams(queryParams, resource);
return resource;
}
private WebResource appendPathParams(WebResource resource, String[] pathParams) {
if (pathParams != null) {
for (String pathParam : pathParams) {
resource = resource.path(pathParam);
}
}
return resource;
}
protected WebResource getResource(APIInfo api, MultivaluedMap<String, String> queryParams) {
return getResource(service, api, queryParams);
}
// Modify URL to include the query params
private WebResource getResource(WebResource service, APIInfo api, MultivaluedMap<String, String> queryParams) {
WebResource resource = service.path(api.getPath());
resource = appendQueryParams(queryParams, resource);
return resource;
}
private WebResource appendQueryParams(MultivaluedMap<String, String> queryParams, WebResource resource) {
if (null != queryParams && !queryParams.isEmpty()) {
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
for (String value : entry.getValue()) {
if (StringUtils.isNotBlank(value)) {
resource = resource.queryParam(entry.getKey(), value);
}
}
}
}
return resource;
}
protected APIInfo updatePathParameters(APIInfo apiInfo, String... params) {
return new APIInfo(String.format(apiInfo.getPath(), params), apiInfo.getMethod(), apiInfo.getExpectedStatus());
}
@VisibleForTesting
void setConfiguration(Configuration configuration) {
this.configuration = configuration;
}
@VisibleForTesting
void setService(WebResource resource) {
this.service = resource;
}
public static class APIInfo {
private final String method;
private final String path;
private final Response.Status status;
public APIInfo(String path, String method, Response.Status status) {
this.path = path;
this.method = method;
this.status = status;
}
public String getMethod() {
return method;
}
public String getPath() {
return path;
}
public Response.Status getExpectedStatus() {
return status;
}
}
/**
* A class to capture input state while creating the client.
*
* The information here will be reused when the client is re-initialized on switch-over
* in case of High Availability.
*/
private class AtlasClientContext {
private String[] baseUrls;
private Client client;
private String doAsUser;
private UserGroupInformation ugi;
public AtlasClientContext(String[] baseUrls, Client client, UserGroupInformation ugi, String doAsUser) {
this.baseUrls = baseUrls;
this.client = client;
this.ugi = ugi;
this.doAsUser = doAsUser;
}
public Client getClient() {
return client;
}
public String[] getBaseUrls() {
return baseUrls;
}
public String getDoAsUser() {
return doAsUser;
}
public UserGroupInformation getUgi() {
return ugi;
}
}
}