/*
* Copyright 2015 floragunn UG (haftungsbeschränkt)
*
* Licensed 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 com.floragunn.searchguard.action.configupdate;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReferenceArray;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.nodes.BaseNodeRequest;
import org.elasticsearch.action.support.nodes.TransportNodesAction;
import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.ClusterService;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.component.LifecycleListener;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.Provider;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import com.floragunn.searchguard.action.configupdate.ConfigUpdateResponse.Node;
import com.floragunn.searchguard.auth.BackendRegistry;
import com.floragunn.searchguard.configuration.ConfigChangeListener;
import com.floragunn.searchguard.configuration.ConfigurationLoader;
import com.floragunn.searchguard.support.ConfigConstants;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps;
public class TransportConfigUpdateAction
extends
TransportNodesAction<ConfigUpdateRequest, ConfigUpdateResponse, TransportConfigUpdateAction.NodeConfigUpdateRequest, ConfigUpdateResponse.Node> {
private final ClusterService clusterService;
private final ConfigurationLoader cl;
private final Provider<BackendRegistry> backendRegistry;
private final ListMultimap<String, ConfigChangeListener> multimap = Multimaps.synchronizedListMultimap(ArrayListMultimap
.<String, ConfigChangeListener> create());
private final String searchguardIndex;
@Inject
public TransportConfigUpdateAction(final Provider<Client> clientProvider, final Settings settings, final ClusterName clusterName,
final ThreadPool threadPool, final ClusterService clusterService, final TransportService transportService,
final ConfigurationLoader cl, final ActionFilters actionFilters, final IndexNameExpressionResolver indexNameExpressionResolver,
Provider<BackendRegistry> backendRegistry) {
super(settings, ConfigUpdateAction.NAME, clusterName, threadPool, clusterService, transportService, actionFilters,
indexNameExpressionResolver, ConfigUpdateRequest.class, TransportConfigUpdateAction.NodeConfigUpdateRequest.class,
ThreadPool.Names.MANAGEMENT);
this.cl = cl;
this.clusterService = clusterService;
this.backendRegistry = backendRegistry;
this.searchguardIndex = settings.get(ConfigConstants.SG_CONFIG_INDEX, ConfigConstants.SG_DEFAULT_CONFIG_INDEX);
clusterService.addLifecycleListener(new LifecycleListener() {
@Override
public void afterStart() {
final Thread ct = new Thread(new Runnable() {
@Override
public void run() {
try {
Client client = clientProvider.get();
logger.debug("Node started, try to initialize it. Wait for at least yellow cluster state....");
ClusterHealthResponse response = null;
try {
response = client.admin().cluster().health(new ClusterHealthRequest(searchguardIndex).waitForYellowStatus()).actionGet();
} catch (Exception e1) {
logger.debug("Catched a {} but we just try again ...", e1.toString());
}
while(response == null || response.isTimedOut() || response.getStatus() == ClusterHealthStatus.RED) {
logger.warn("index '{}' not healthy yet, we try again ... (Reason: {})", searchguardIndex, response==null?"no response":(response.isTimedOut()?"timeout":"other, maybe red cluster"));
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
//ignore
}
try {
response = client.admin().cluster().health(new ClusterHealthRequest(searchguardIndex).waitForYellowStatus()).actionGet();
} catch (Exception e1) {
logger.debug("Catched again a {} but we just try again ...", e1.toString());
}
continue;
}
Map<String, Settings> setn = null;
while(setn == null || !setn.keySet().containsAll(Lists.newArrayList("config", "roles", "rolesmapping"))) {
if (setn != null) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.debug("Thread was interrupted so we cancle initialization");
return;
}
}
logger.debug("Try to load config ...");
try {
setn = cl.load(new String[] { "config", "roles", "rolesmapping", "internalusers",
"actiongroups" }, 1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.debug("Thread was interrupted so we cancle initialization");
return;
} catch (TimeoutException e) {
logger.warn("Timeout, we just try again in a few seconds ... ");
}
}
logger.debug("Retrieved {} configs", setn.keySet());
logger.debug("Retrieved config on node startup and will now update config change listeners");
for (final String evt : setn.keySet()) {
for (final ConfigChangeListener cl : new ArrayList<ConfigChangeListener>(multimap.get(evt))) {
Settings settings = setn.get(evt);
if(settings != null) {
cl.onChange(evt, settings);
logger.debug("Updated {} for {} due to initial configuration on node '{}'", evt, cl.getClass().getSimpleName(), clusterService.localNode().getName());
}
}
}
logger.info("Node '{}' initialized", clusterService.localNode().getName());
} catch (Exception e) {
logger.error("Unexpected exception while initializing node "+e, e);
}
}
});
logger.info("Check if "+searchguardIndex+" index exists ...");
try {
IndicesExistsRequest ier = new IndicesExistsRequest(searchguardIndex)
.masterNodeTimeout(TimeValue.timeValueMinutes(1));
ier.putHeader(ConfigConstants.SG_CONF_REQUEST_HEADER, "true");
clientProvider.get().admin().indices().exists(ier, new ActionListener<IndicesExistsResponse>() {
@Override
public void onResponse(IndicesExistsResponse response) {
if(response != null && response.isExists()) {
ct.start();
} else {
if(settings.getAsBoolean("action.master.force_local", false) && settings.getByPrefix("tribe").getAsMap().size() > 0) {
logger.info("{} index does not exist yet, but we are a tribe node. So we will load the config anyhow until we got it ...", searchguardIndex);
ct.start();
} else {
logger.info("{} index does not exist yet, so no need to load config on node startup. Use sgadmin to initialize cluster", searchguardIndex);
}
}
}
@Override
public void onFailure(Throwable e) {
logger.error("Failure while checking {} index {}",e, searchguardIndex, e);
ct.start();
}
});
} catch (Throwable e2) {
logger.error("Failure while executing IndicesExistsRequest {}",e2, e2);
ct.start();
}
}
});
}
public static class NodeConfigUpdateRequest extends BaseNodeRequest {
ConfigUpdateRequest request;
public NodeConfigUpdateRequest() {
}
public NodeConfigUpdateRequest(final String nodeId, final ConfigUpdateRequest request) {
super(request, nodeId);
this.request = request;
}
@Override
public void readFrom(final StreamInput in) throws IOException {
super.readFrom(in);
request = new ConfigUpdateRequest();
request.readFrom(in);
}
@Override
public void writeTo(final StreamOutput out) throws IOException {
super.writeTo(out);
request.writeTo(out);
}
}
@Override
protected ConfigUpdateResponse newResponse(final ConfigUpdateRequest request, final AtomicReferenceArray nodesResponses) {
final List<ConfigUpdateResponse.Node> nodes = Lists.<ConfigUpdateResponse.Node> newArrayList();
for (int i = 0; i < nodesResponses.length(); i++) {
final Object resp = nodesResponses.get(i);
if (resp instanceof ConfigUpdateResponse.Node) {
nodes.add((ConfigUpdateResponse.Node) resp);
}
}
return new ConfigUpdateResponse(this.clusterName, nodes.toArray(new ConfigUpdateResponse.Node[nodes.size()]));
}
@Override
protected NodeConfigUpdateRequest newNodeRequest(final String nodeId, final ConfigUpdateRequest request) {
return new NodeConfigUpdateRequest(nodeId, request);
}
@Override
protected Node newNodeResponse() {
return new ConfigUpdateResponse.Node(clusterService.localNode(), new String[0], null);
}
@Override
protected Node nodeOperation(final NodeConfigUpdateRequest request) {
try {
final Map<String, Settings> setn = cl.load(request.request.getConfigTypes(), 30, TimeUnit.SECONDS);
logger.debug("Retrieved config ({}) due to config update request and will now update config change listeners", Arrays.toString(request.request.getConfigTypes()));
backendRegistry.get().invalidateCache();
for (final String evt : setn.keySet()) {
for (final ConfigChangeListener cl : new ArrayList<ConfigChangeListener>(multimap.get(evt))) {
Settings settings = setn.get(evt);
if (settings != null) {
cl.onChange(evt, settings);
logger.debug("Updated {} for {} due to node operation on node {}", evt, cl.getClass().getSimpleName(),
clusterService.localNode().getName());
}
}
}
return new ConfigUpdateResponse.Node(clusterService.localNode(), setn.keySet().toArray(new String[0]), null);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
logger.debug("Thread was interrupted, we return just a empty response");
return new ConfigUpdateResponse.Node(clusterService.localNode(), new String[0], "Interrupted");
} catch (TimeoutException e1) {
logger.error("Timeout {}",e1,e1);
return new ConfigUpdateResponse.Node(clusterService.localNode(), new String[0], "Timeout ("+e1+")");
}
}
public void addConfigChangeListener(final String event, final ConfigChangeListener listener) {
logger.debug("Add config listener {}",listener.getClass());
multimap.put(event, listener);
}
@Override
protected boolean accumulateExceptions() {
return false;
}
}