/*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.nifi.processors.elasticsearch;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.ssl.SSLContextService;
import org.apache.nifi.util.StringUtils;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
abstract class AbstractElasticsearch5TransportClientProcessor extends AbstractElasticsearch5Processor {
protected static final PropertyDescriptor CLUSTER_NAME = new PropertyDescriptor.Builder()
.name("el5-cluster-name")
.displayName("Cluster Name")
.description("Name of the ES cluster (for example, elasticsearch_brew). Defaults to 'elasticsearch'")
.required(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.defaultValue("elasticsearch")
.expressionLanguageSupported(true)
.build();
protected static final PropertyDescriptor HOSTS = new PropertyDescriptor.Builder()
.name("el5-hosts")
.displayName("ElasticSearch Hosts")
.description("ElasticSearch Hosts, which should be comma separated and colon for hostname/port "
+ "host1:port,host2:port,.... For example testcluster:9300. This processor uses the Transport Client to "
+ "connect to hosts. The default transport client port is 9300.")
.required(true)
.expressionLanguageSupported(true)
.addValidator(StandardValidators.HOSTNAME_PORT_LIST_VALIDATOR)
.build();
public static final PropertyDescriptor PROP_XPACK_LOCATION = new PropertyDescriptor.Builder()
.name("el5-xpack-location")
.displayName("X-Pack Transport Location")
.description("Specifies the path to the JAR(s) for the Elasticsearch X-Pack Transport feature. "
+ "If the Elasticsearch cluster has been secured with the X-Pack plugin, then the X-Pack Transport "
+ "JARs must also be available to this processor. Note: Do NOT place the X-Pack JARs into NiFi's "
+ "lib/ directory, doing so will prevent the X-Pack Transport JARs from being loaded.")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.dynamicallyModifiesClasspath(true)
.expressionLanguageSupported(true)
.build();
protected static final PropertyDescriptor PING_TIMEOUT = new PropertyDescriptor.Builder()
.name("el5-ping-timeout")
.displayName("ElasticSearch Ping Timeout")
.description("The ping timeout used to determine when a node is unreachable. " +
"For example, 5s (5 seconds). If non-local recommended is 30s")
.required(true)
.defaultValue("5s")
.addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
.expressionLanguageSupported(true)
.build();
protected static final PropertyDescriptor SAMPLER_INTERVAL = new PropertyDescriptor.Builder()
.name("el5-sampler-interval")
.displayName("Sampler Interval")
.description("How often to sample / ping the nodes listed and connected. For example, 5s (5 seconds). "
+ "If non-local recommended is 30s.")
.required(true)
.defaultValue("5s")
.addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
.expressionLanguageSupported(true)
.build();
protected final AtomicReference<Client> esClient = new AtomicReference<>();
protected List<InetSocketAddress> esHosts;
/**
* Instantiate ElasticSearch Client. This should be called by subclasses' @OnScheduled method to create a client
* if one does not yet exist. If called when scheduled, closeClient() should be called by the subclasses' @OnStopped
* method so the client will be destroyed when the processor is stopped.
*
* @param context The context for this processor
* @throws ProcessException if an error occurs while creating an Elasticsearch client
*/
@Override
protected void createElasticsearchClient(ProcessContext context) throws ProcessException {
ComponentLog log = getLogger();
if (esClient.get() != null) {
return;
}
log.debug("Creating ElasticSearch Client");
try {
final String clusterName = context.getProperty(CLUSTER_NAME).evaluateAttributeExpressions().getValue();
final String pingTimeout = context.getProperty(PING_TIMEOUT).evaluateAttributeExpressions().getValue();
final String samplerInterval = context.getProperty(SAMPLER_INTERVAL).evaluateAttributeExpressions().getValue();
final String username = context.getProperty(USERNAME).evaluateAttributeExpressions().getValue();
final String password = context.getProperty(PASSWORD).getValue();
final SSLContextService sslService =
context.getProperty(PROP_SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class);
Settings.Builder settingsBuilder = Settings.builder()
.put("cluster.name", clusterName)
.put("client.transport.ping_timeout", pingTimeout)
.put("client.transport.nodes_sampler_interval", samplerInterval);
String xPackUrl = context.getProperty(PROP_XPACK_LOCATION).evaluateAttributeExpressions().getValue();
if (sslService != null) {
settingsBuilder.put("xpack.security.transport.ssl.enabled", "true");
if (!StringUtils.isEmpty(sslService.getKeyStoreFile())) {
settingsBuilder.put("xpack.ssl.keystore.path", sslService.getKeyStoreFile());
}
if (!StringUtils.isEmpty(sslService.getKeyStorePassword())) {
settingsBuilder.put("xpack.ssl.keystore.password", sslService.getKeyStorePassword());
}
if (!StringUtils.isEmpty(sslService.getKeyPassword())) {
settingsBuilder.put("xpack.ssl.keystore.key_password", sslService.getKeyPassword());
}
if (!StringUtils.isEmpty(sslService.getTrustStoreFile())) {
settingsBuilder.put("xpack.ssl.truststore.path", sslService.getTrustStoreFile());
}
if (!StringUtils.isEmpty(sslService.getTrustStorePassword())) {
settingsBuilder.put("xpack.ssl.truststore.password", sslService.getTrustStorePassword());
}
}
// Set username and password for X-Pack
if (!StringUtils.isEmpty(username)) {
StringBuffer secureUser = new StringBuffer(username);
if (!StringUtils.isEmpty(password)) {
secureUser.append(":");
secureUser.append(password);
}
settingsBuilder.put("xpack.security.user", secureUser);
}
final String hosts = context.getProperty(HOSTS).evaluateAttributeExpressions().getValue();
esHosts = getEsHosts(hosts);
Client transportClient = getTransportClient(settingsBuilder, xPackUrl, username, password, esHosts, log);
esClient.set(transportClient);
} catch (Exception e) {
log.error("Failed to create Elasticsearch client due to {}", new Object[]{e}, e);
throw new ProcessException(e);
}
}
protected Client getTransportClient(Settings.Builder settingsBuilder, String xPackPath,
String username, String password,
List<InetSocketAddress> esHosts, ComponentLog log)
throws MalformedURLException {
// Map of headers
Map<String, String> headers = new HashMap<>();
TransportClient transportClient = null;
// See if the Elasticsearch X-Pack JAR locations were specified, and create the
// authorization token if username and password are supplied.
if (!StringUtils.isBlank(xPackPath)) {
ClassLoader xPackClassloader = Thread.currentThread().getContextClassLoader();
try {
// Get the plugin class
Class xPackTransportClientClass = Class.forName("org.elasticsearch.xpack.client.PreBuiltXPackTransportClient", true, xPackClassloader);
Constructor<?> ctor = xPackTransportClientClass.getConstructor(Settings.class, Class[].class);
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
// Need a couple of classes from the X-Path Transport JAR to build the token
Class usernamePasswordTokenClass =
Class.forName("org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken", true, xPackClassloader);
Class securedStringClass =
Class.forName("org.elasticsearch.xpack.security.authc.support.SecuredString", true, xPackClassloader);
Constructor<?> securedStringCtor = securedStringClass.getConstructor(char[].class);
Object securePasswordString = securedStringCtor.newInstance(password.toCharArray());
Method basicAuthHeaderValue = usernamePasswordTokenClass.getMethod("basicAuthHeaderValue", String.class, securedStringClass);
String authToken = (String) basicAuthHeaderValue.invoke(null, username, securePasswordString);
if (authToken != null) {
headers.put("Authorization", authToken);
}
transportClient = (TransportClient) ctor.newInstance(settingsBuilder.build(), new Class[0]);
}
} catch (ClassNotFoundException
| NoSuchMethodException
| InstantiationException
| IllegalAccessException
| InvocationTargetException xPackLoadException) {
throw new ProcessException("X-Pack plugin could not be loaded and/or configured", xPackLoadException);
}
} else {
getLogger().debug("No X-Pack Transport location specified, secure connections and/or authorization will not be available");
}
// If transportClient is null, either the processor is not configured for secure connections or there is a problem with config
// (which is logged), so continue with a non-secure client
if (transportClient == null) {
transportClient = new PreBuiltTransportClient(settingsBuilder.build());
}
if (esHosts != null) {
for (final InetSocketAddress host : esHosts) {
try {
transportClient.addTransportAddress(new InetSocketTransportAddress(host));
} catch (IllegalArgumentException iae) {
log.error("Could not add transport address {}", new Object[]{host});
}
}
}
Client client = transportClient.filterWithHeader(headers);
return client;
}
/**
* Dispose of ElasticSearch client
*/
public void closeClient() {
Client client = esClient.get();
if (client != null) {
getLogger().info("Closing ElasticSearch Client");
esClient.set(null);
client.close();
}
}
/**
* Get the ElasticSearch hosts from a NiFi attribute, e.g.
*
* @param hosts A comma-separated list of ElasticSearch hosts (host:port,host2:port2, etc.)
* @return List of InetSocketAddresses for the ES hosts
*/
private List<InetSocketAddress> getEsHosts(String hosts) {
if (hosts == null) {
return null;
}
final List<String> esList = Arrays.asList(hosts.split(","));
List<InetSocketAddress> esHosts = new ArrayList<>();
for (String item : esList) {
String[] addresses = item.split(":");
final String hostName = addresses[0].trim();
final int port = Integer.parseInt(addresses[1].trim());
esHosts.add(new InetSocketAddress(hostName, port));
}
return esHosts;
}
}