/*
* 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.solr.handler.admin;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.fs.Path;
import org.apache.solr.api.Api;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.cloud.OverseerSolrResponse;
import org.apache.solr.cloud.OverseerTaskQueue.QueueEvent;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkConfigManager;
import org.apache.solr.common.cloud.ZkNodeProps;
import org.apache.solr.common.params.ConfigSetParams;
import org.apache.solr.common.params.ConfigSetParams.ConfigSetAction;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthenticationPlugin;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import static org.apache.solr.cloud.OverseerConfigSetMessageHandler.BASE_CONFIGSET;
import static org.apache.solr.cloud.OverseerConfigSetMessageHandler.CONFIGSETS_ACTION_PREFIX;
import static org.apache.solr.cloud.OverseerConfigSetMessageHandler.PROPERTY_PREFIX;
import static org.apache.solr.common.params.CommonParams.NAME;
import static org.apache.solr.common.params.ConfigSetParams.ConfigSetAction.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
/**
* A {@link org.apache.solr.request.SolrRequestHandler} for ConfigSets API requests.
*/
public class ConfigSetsHandler extends RequestHandlerBase implements PermissionNameProvider {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
protected final CoreContainer coreContainer;
public static long DEFAULT_ZK_TIMEOUT = 300*1000;
private final ConfigSetsHandlerApi configSetsHandlerApi = new ConfigSetsHandlerApi(this);
/**
* Overloaded ctor to inject CoreContainer into the handler.
*
* @param coreContainer Core Container of the solr webapp installed.
*/
public ConfigSetsHandler(final CoreContainer coreContainer) {
this.coreContainer = coreContainer;
}
@Override
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
if (coreContainer == null) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Core container instance missing");
}
// Make sure that the core is ZKAware
if(!coreContainer.isZooKeeperAware()) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"Solr instance is not running in SolrCloud mode.");
}
// Pick the action
SolrParams params = req.getParams();
String a = params.get(ConfigSetParams.ACTION);
if (a != null) {
ConfigSetAction action = ConfigSetAction.get(a);
if (action == null)
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unknown action: " + a);
if (action == ConfigSetAction.UPLOAD) {
handleConfigUploadRequest(req, rsp);
return;
}
invokeAction(req, rsp, action);
} else {
throw new SolrException(ErrorCode.BAD_REQUEST, "action is a required param");
}
rsp.setHttpCaching(false);
}
void invokeAction(SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetAction action) throws Exception {
ConfigSetOperation operation = ConfigSetOperation.get(action);
log.info("Invoked ConfigSet Action :{} with params {} ", action.toLower(), req.getParamString());
Map<String, Object> result = operation.call(req, rsp, this);
sendToZk(rsp, operation, result);
}
protected void sendToZk(SolrQueryResponse rsp, ConfigSetOperation operation, Map<String, Object> result)
throws KeeperException, InterruptedException {
if (result != null) {
// We need to differentiate between collection and configsets actions since they currently
// use the same underlying queue.
result.put(QUEUE_OPERATION, CONFIGSETS_ACTION_PREFIX + operation.action.toLower());
ZkNodeProps props = new ZkNodeProps(result);
handleResponse(operation.action.toLower(), props, rsp, DEFAULT_ZK_TIMEOUT);
}
}
private void handleConfigUploadRequest(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
String configSetName = req.getParams().get(NAME);
if (StringUtils.isBlank(configSetName)) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"The configuration name should be provided in the \"name\" parameter");
}
SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
String configPathInZk = ZkConfigManager.CONFIGS_ZKNODE + Path.SEPARATOR + configSetName;
if (zkClient.exists(configPathInZk, true)) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"The configuration " + configSetName + " already exists in zookeeper");
}
Iterator<ContentStream> contentStreamsIterator = req.getContentStreams().iterator();
if (!contentStreamsIterator.hasNext()) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"No stream found for the config data to be uploaded");
}
InputStream inputStream = contentStreamsIterator.next().getStream();
// Create a node for the configuration in zookeeper
boolean trusted = getTrusted(req);
zkClient.makePath(configPathInZk, ("{\"trusted\": " + Boolean.toString(trusted) + "}").
getBytes(StandardCharsets.UTF_8), true);
ZipInputStream zis = new ZipInputStream(inputStream, StandardCharsets.UTF_8);
ZipEntry zipEntry = null;
while ((zipEntry = zis.getNextEntry()) != null) {
String filePathInZk = configPathInZk + "/" + zipEntry.getName();
if (zipEntry.isDirectory()) {
zkClient.makePath(filePathInZk, true);
} else {
createZkNodeIfNotExistsAndSetData(zkClient, filePathInZk,
IOUtils.toByteArray(zis));
}
}
zis.close();
}
boolean getTrusted(SolrQueryRequest req) {
AuthenticationPlugin authcPlugin = coreContainer.getAuthenticationPlugin();
log.info("Trying to upload a configset. authcPlugin: {}, user principal: {}",
authcPlugin, req.getUserPrincipal());
if (authcPlugin != null && req.getUserPrincipal() != null) {
return true;
}
return false;
}
private void createZkNodeIfNotExistsAndSetData(SolrZkClient zkClient,
String filePathInZk, byte[] data) throws Exception {
if (!zkClient.exists(filePathInZk, true)) {
zkClient.create(filePathInZk, data, CreateMode.PERSISTENT, true);
} else {
zkClient.setData(filePathInZk, data, true);
}
}
private void handleResponse(String operation, ZkNodeProps m,
SolrQueryResponse rsp, long timeout) throws KeeperException, InterruptedException {
long time = System.nanoTime();
QueueEvent event = coreContainer.getZkController()
.getOverseerConfigSetQueue()
.offer(Utils.toJSON(m), timeout);
if (event.getBytes() != null) {
SolrResponse response = SolrResponse.deserialize(event.getBytes());
rsp.getValues().addAll(response.getResponse());
SimpleOrderedMap exp = (SimpleOrderedMap) response.getResponse().get("exception");
if (exp != null) {
Integer code = (Integer) exp.get("rspCode");
rsp.setException(new SolrException(code != null && code != -1 ? ErrorCode.getErrorCode(code) : ErrorCode.SERVER_ERROR, (String)exp.get("msg")));
}
} else {
if (System.nanoTime() - time >= TimeUnit.NANOSECONDS.convert(timeout, TimeUnit.MILLISECONDS)) {
throw new SolrException(ErrorCode.SERVER_ERROR, operation
+ " the configset time out:" + timeout / 1000 + "s");
} else if (event.getWatchedEvent() != null) {
throw new SolrException(ErrorCode.SERVER_ERROR, operation
+ " the configset error [Watcher fired on path: "
+ event.getWatchedEvent().getPath() + " state: "
+ event.getWatchedEvent().getState() + " type "
+ event.getWatchedEvent().getType() + "]");
} else {
throw new SolrException(ErrorCode.SERVER_ERROR, operation
+ " the configset unknown case");
}
}
}
private static Map<String, Object> copyPropertiesWithPrefix(SolrParams params, Map<String, Object> props, String prefix) {
Iterator<String> iter = params.getParameterNamesIterator();
while (iter.hasNext()) {
String param = iter.next();
if (param.startsWith(prefix)) {
props.put(param, params.get(param));
}
}
return props;
}
@Override
public String getDescription() {
return "Manage SolrCloud ConfigSets";
}
@Override
public Category getCategory() {
return Category.ADMIN;
}
enum ConfigSetOperation {
CREATE_OP(CREATE) {
@Override
Map<String, Object> call(SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception {
Map<String, Object> props = req.getParams().required().getAll(null, NAME, BASE_CONFIGSET);
return copyPropertiesWithPrefix(req.getParams(), props, PROPERTY_PREFIX + ".");
}
},
DELETE_OP(DELETE) {
@Override
Map<String, Object> call(SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception {
return req.getParams().required().getAll(null, NAME);
}
},
LIST_OP(LIST) {
@Override
Map<String, Object> call(SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception {
NamedList<Object> results = new NamedList<>();
SolrZkClient zk = h.coreContainer.getZkController().getZkStateReader().getZkClient();
ZkConfigManager zkConfigManager = new ZkConfigManager(zk);
List<String> configSetsList = zkConfigManager.listConfigs();
results.add("configSets", configSetsList);
SolrResponse response = new OverseerSolrResponse(results);
rsp.getValues().addAll(response.getResponse());
return null;
}
};
ConfigSetAction action;
ConfigSetOperation(ConfigSetAction action) {
this.action = action;
}
abstract Map<String, Object> call(SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception;
public static ConfigSetOperation get(ConfigSetAction action) {
for (ConfigSetOperation op : values()) {
if (op.action == action) return op;
}
throw new SolrException(ErrorCode.SERVER_ERROR, "No such action" + action);
}
}
@Override
public Collection<Api> getApis() {
return configSetsHandlerApi.getApis();
}
@Override
public Boolean registerV2() {
return Boolean.TRUE;
}
@Override
public Name getPermissionName(AuthorizationContext ctx) {
String a = ctx.getParams().get(ConfigSetParams.ACTION);
if (a != null) {
ConfigSetAction action = ConfigSetAction.get(a);
if (action == ConfigSetAction.CREATE || action == ConfigSetAction.DELETE || action == ConfigSetAction.UPLOAD) {
return Name.CONFIG_EDIT_PERM;
} else if (action == ConfigSetAction.LIST) {
return Name.CONFIG_READ_PERM;
}
}
return null;
}
}