| /* |
| * 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.cloud; |
| |
| import static org.apache.solr.common.params.CommonParams.NAME; |
| import static org.apache.solr.common.params.CommonParams.VALUE_LONG; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.lang.reflect.InvocationTargetException; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.List; |
| import java.util.Properties; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.regex.Pattern; |
| import javax.xml.parsers.ParserConfigurationException; |
| import org.apache.commons.cli.CommandLine; |
| import org.apache.commons.cli.CommandLineParser; |
| import org.apache.commons.cli.HelpFormatter; |
| import org.apache.commons.cli.Option; |
| import org.apache.commons.cli.Options; |
| import org.apache.commons.cli.ParseException; |
| import org.apache.commons.cli.PosixParser; |
| import org.apache.solr.cli.CLIO; |
| import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.cloud.ClusterProperties; |
| import org.apache.solr.common.cloud.SolrZkClient; |
| import org.apache.solr.common.cloud.ZkMaintenanceUtils; |
| import org.apache.solr.common.util.Compressor; |
| import org.apache.solr.common.util.StrUtils; |
| import org.apache.solr.common.util.ZLibCompressor; |
| import org.apache.solr.core.ConfigSetService; |
| import org.apache.solr.core.CoreContainer; |
| import org.apache.solr.core.NodeConfig; |
| import org.apache.solr.core.SolrXmlConfig; |
| import org.apache.zookeeper.CreateMode; |
| import org.apache.zookeeper.KeeperException; |
| import org.xml.sax.SAXException; |
| |
| public class ZkCLI implements CLIO { |
| |
| private static final String MAKEPATH = "makepath"; |
| private static final String PUT = "put"; |
| private static final String PUT_FILE = "putfile"; |
| private static final String GET = "get"; |
| private static final String GET_FILE = "getfile"; |
| private static final String DOWNCONFIG = "downconfig"; |
| private static final String ZK_CLI_NAME = "ZkCLI"; |
| private static final String HELP = "help"; |
| private static final String LINKCONFIG = "linkconfig"; |
| private static final String CONFDIR = "confdir"; |
| private static final String CONFNAME = "confname"; |
| private static final String ZKHOST = "zkhost"; |
| private static final String RUNZK = "runzk"; |
| private static final String SOLRHOME = "solrhome"; |
| private static final String BOOTSTRAP = "bootstrap"; |
| static final String UPCONFIG = "upconfig"; |
| static final String EXCLUDE_REGEX_SHORT = "x"; |
| static final String EXCLUDE_REGEX = "excluderegex"; |
| static final String EXCLUDE_REGEX_DEFAULT = ConfigSetService.UPLOAD_FILENAME_EXCLUDE_REGEX; |
| private static final String COLLECTION = "collection"; |
| private static final String CLEAR = "clear"; |
| private static final String LIST = "list"; |
| private static final String LS = "ls"; |
| private static final String CMD = "cmd"; |
| private static final String CLUSTERPROP = "clusterprop"; |
| private static final String UPDATEACLS = "updateacls"; |
| private static final String VERBOSE = "verbose"; |
| |
| @VisibleForTesting |
| public static void setStdout(PrintStream stdout) { |
| ZkCLI.stdout = stdout; |
| } |
| |
| private static PrintStream stdout = CLIO.getOutStream(); |
| |
| /** |
| * Allows you to perform a variety of zookeeper related tasks, such as: |
| * |
| * <p>Bootstrap the current configs for all collections in solr.xml. |
| * |
| * <p>Upload a named config set from a given directory. |
| * |
| * <p>Link a named config set explicity to a collection. |
| * |
| * <p>Clear ZooKeeper info. |
| * |
| * <p>If you also pass a solrPort, it will be used to start an embedded zk useful for single |
| * machine, multi node tests. |
| */ |
| public static void main(String[] args) |
| throws InterruptedException, |
| TimeoutException, |
| IOException, |
| ParserConfigurationException, |
| SAXException, |
| KeeperException { |
| |
| CommandLineParser parser = new PosixParser(); |
| Options options = new Options(); |
| options.addOption( |
| Option.builder(CMD) |
| .hasArg(true) |
| .desc( |
| "cmd to run: " |
| + BOOTSTRAP |
| + ", " |
| + UPCONFIG |
| + ", " |
| + DOWNCONFIG |
| + ", " |
| + LINKCONFIG |
| + ", " |
| + MAKEPATH |
| + ", " |
| + PUT |
| + ", " |
| + PUT_FILE |
| + "," |
| + GET |
| + "," |
| + GET_FILE |
| + ", " |
| + LIST |
| + ", " |
| + CLEAR |
| + ", " |
| + UPDATEACLS |
| + ", " |
| + LS) |
| .build()); |
| |
| Option zkHostOption = new Option("z", ZKHOST, true, "ZooKeeper host address"); |
| options.addOption(zkHostOption); |
| Option solrHomeOption = |
| new Option("s", SOLRHOME, true, "for " + BOOTSTRAP + ", " + RUNZK + ": solrhome location"); |
| options.addOption(solrHomeOption); |
| |
| options.addOption( |
| "d", CONFDIR, true, "for " + UPCONFIG + ": a directory of configuration files"); |
| options.addOption( |
| "n", CONFNAME, true, "for " + UPCONFIG + ", " + LINKCONFIG + ": name of the config set"); |
| |
| options.addOption("c", COLLECTION, true, "for " + LINKCONFIG + ": name of the collection"); |
| |
| options.addOption( |
| EXCLUDE_REGEX_SHORT, |
| EXCLUDE_REGEX, |
| true, |
| "for " + UPCONFIG + ": files matching this regular expression won't be uploaded"); |
| |
| options.addOption( |
| "r", |
| RUNZK, |
| true, |
| "run zk internally by passing the solr run port - only for clusters on one machine (tests, dev)"); |
| |
| options.addOption("h", HELP, false, "bring up this help page"); |
| options.addOption(NAME, true, "name of the cluster property to set"); |
| options.addOption(VALUE_LONG, true, "value of the cluster to set"); |
| options.addOption("v", VERBOSE, false, "enable verbose mode"); |
| |
| try { |
| // parse the command line arguments |
| CommandLine line = parser.parse(options, args); |
| |
| if ((line.hasOption(HELP) || !line.hasOption(ZKHOST) || !line.hasOption(CMD)) |
| && !line.hasOption(VERBOSE)) { |
| // automatically generate the help statement |
| HelpFormatter formatter = new HelpFormatter(); |
| formatter.printHelp(ZK_CLI_NAME, options); |
| stdout.println("Examples:"); |
| stdout.println( |
| "zkcli.sh -zkhost localhost:9983 -cmd " + BOOTSTRAP + " -" + SOLRHOME + " /opt/solr"); |
| stdout.println( |
| "zkcli.sh -zkhost localhost:9983 -cmd " |
| + UPCONFIG |
| + " -" |
| + CONFDIR |
| + " /opt/solr/collection1/conf" |
| + " -" |
| + CONFNAME |
| + " myconf"); |
| stdout.println( |
| "zkcli.sh -zkhost localhost:9983 -cmd " |
| + DOWNCONFIG |
| + " -" |
| + CONFDIR |
| + " /opt/solr/collection1/conf" |
| + " -" |
| + CONFNAME |
| + " myconf"); |
| stdout.println( |
| "zkcli.sh -zkhost localhost:9983 -cmd " |
| + LINKCONFIG |
| + " -" |
| + COLLECTION |
| + " collection1" |
| + " -" |
| + CONFNAME |
| + " myconf"); |
| stdout.println("zkcli.sh -zkhost localhost:9983 -cmd " + MAKEPATH + " /apache/solr"); |
| stdout.println("zkcli.sh -zkhost localhost:9983 -cmd " + PUT + " /solr.conf 'conf data'"); |
| stdout.println( |
| "zkcli.sh -zkhost localhost:9983 -cmd " |
| + PUT_FILE |
| + " /clusterprops.json /User/myuser/solr/clusterprops.json"); |
| stdout.println("zkcli.sh -zkhost localhost:9983 -cmd " + GET + " /clusterprops.json"); |
| stdout.println( |
| "zkcli.sh -zkhost localhost:9983 -cmd " |
| + GET_FILE |
| + " /clusterprops.json clusterprops.json"); |
| stdout.println("zkcli.sh -zkhost localhost:9983 -cmd " + CLEAR + " /solr"); |
| stdout.println("zkcli.sh -zkhost localhost:9983 -cmd " + LIST); |
| stdout.println("zkcli.sh -zkhost localhost:9983 -cmd " + LS + " /solr/live_nodes"); |
| stdout.println( |
| "zkcli.sh -zkhost localhost:9983 -cmd " |
| + CLUSTERPROP |
| + " -" |
| + NAME |
| + " urlScheme -" |
| + VALUE_LONG |
| + " https"); |
| stdout.println("zkcli.sh -zkhost localhost:9983 -cmd " + UPDATEACLS + " /solr"); |
| return; |
| } |
| |
| // start up a tmp zk server first |
| String zkServerAddress = line.getOptionValue(ZKHOST); |
| String solrHome = line.getOptionValue(SOLRHOME); |
| if (StrUtils.isNullOrEmpty(solrHome)) { |
| solrHome = System.getProperty("solr.home"); |
| } |
| if (line.hasOption(VERBOSE)) { |
| stdout.println("Using " + SOLRHOME + "=" + solrHome); |
| return; |
| } |
| |
| String solrPort = null; |
| if (line.hasOption(RUNZK)) { |
| if (!line.hasOption(SOLRHOME)) { |
| stdout.println("-" + SOLRHOME + " is required for " + RUNZK); |
| System.exit(1); |
| } |
| solrPort = line.getOptionValue(RUNZK); |
| } |
| |
| SolrZkServer zkServer = null; |
| if (solrPort != null) { |
| zkServer = |
| new SolrZkServer( |
| "true", |
| null, |
| new File(solrHome, "/zoo_data"), |
| solrHome, |
| Integer.parseInt(solrPort)); |
| zkServer.parseConfig(); |
| zkServer.start(); |
| } |
| |
| int minStateByteLenForCompression = -1; |
| Compressor compressor = new ZLibCompressor(); |
| |
| if (solrHome != null) { |
| try { |
| Path solrHomePath = Paths.get(solrHome); |
| Properties props = new Properties(); |
| props.put(SolrXmlConfig.ZK_HOST, zkServerAddress); |
| NodeConfig nodeConfig = NodeConfig.loadNodeConfig(solrHomePath, props); |
| minStateByteLenForCompression = |
| nodeConfig.getCloudConfig().getMinStateByteLenForCompression(); |
| String stateCompressorClass = nodeConfig.getCloudConfig().getStateCompressorClass(); |
| if (StrUtils.isNotNullOrEmpty(stateCompressorClass)) { |
| Class<? extends Compressor> compressionClass = |
| Class.forName(stateCompressorClass).asSubclass(Compressor.class); |
| compressor = compressionClass.getDeclaredConstructor().newInstance(); |
| } |
| } catch (SolrException e) { |
| // Failed to load solr.xml |
| stdout.println( |
| "Failed to load solr.xml from ZK or SolrHome, put/get operations on compressed data will use data as is. If you intention is to read and de-compress data or compress and write data, then solr.xml must be accessible."); |
| } catch (ClassNotFoundException |
| | NoSuchMethodException |
| | InstantiationException |
| | IllegalAccessException |
| | InvocationTargetException e) { |
| stdout.println("Unable to find or instantiate compression class: " + e.getMessage()); |
| System.exit(1); |
| } |
| } |
| |
| try (SolrZkClient zkClient = |
| new SolrZkClient.Builder() |
| .withUrl(zkServerAddress) |
| .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) |
| .withConnTimeOut( |
| SolrZkClientTimeout.DEFAULT_ZK_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS) |
| .withReconnectListener(() -> {}) |
| // .withCompressor(compressor) |
| .withStateFileCompression(minStateByteLenForCompression, compressor) |
| .build()) { |
| if (line.getOptionValue(CMD).equalsIgnoreCase(BOOTSTRAP)) { |
| if (!line.hasOption(SOLRHOME)) { |
| stdout.println("-" + SOLRHOME + " is required for " + BOOTSTRAP); |
| System.exit(1); |
| } |
| |
| CoreContainer cc = new CoreContainer(Paths.get(solrHome), new Properties()); |
| cc.setCoreConfigService(new ZkConfigSetService(zkClient)); |
| |
| if (!ZkController.checkChrootPath(zkServerAddress, true)) { |
| stdout.println("A chroot was specified in zkHost but the znode doesn't exist. "); |
| System.exit(1); |
| } |
| |
| ConfigSetService.bootstrapConf(cc); |
| |
| // No need to close the CoreContainer, as it wasn't started |
| // up in the first place... |
| |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(UPCONFIG)) { |
| if (!line.hasOption(CONFDIR) || !line.hasOption(CONFNAME)) { |
| stdout.println("-" + CONFDIR + " and -" + CONFNAME + " are required for " + UPCONFIG); |
| System.exit(1); |
| } |
| String confDir = line.getOptionValue(CONFDIR); |
| String confName = line.getOptionValue(CONFNAME); |
| final String excludeExpr = line.getOptionValue(EXCLUDE_REGEX, EXCLUDE_REGEX_DEFAULT); |
| |
| if (!ZkController.checkChrootPath(zkServerAddress, true)) { |
| stdout.println("A chroot was specified in zkHost but the znode doesn't exist. "); |
| System.exit(1); |
| } |
| final Pattern excludePattern = Pattern.compile(excludeExpr); |
| ZkMaintenanceUtils.uploadToZK( |
| zkClient, |
| Paths.get(confDir), |
| ZkMaintenanceUtils.CONFIGS_ZKNODE + "/" + confName, |
| excludePattern); |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(DOWNCONFIG)) { |
| if (!line.hasOption(CONFDIR) || !line.hasOption(CONFNAME)) { |
| stdout.println("-" + CONFDIR + " and -" + CONFNAME + " are required for " + DOWNCONFIG); |
| System.exit(1); |
| } |
| String confDir = line.getOptionValue(CONFDIR); |
| String confName = line.getOptionValue(CONFNAME); |
| ZkMaintenanceUtils.downloadFromZK( |
| zkClient, ZkMaintenanceUtils.CONFIGS_ZKNODE + "/" + confName, Paths.get(confDir)); |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(LINKCONFIG)) { |
| if (!line.hasOption(COLLECTION) || !line.hasOption(CONFNAME)) { |
| stdout.println( |
| "-" + COLLECTION + " and -" + CONFNAME + " are required for " + LINKCONFIG); |
| System.exit(1); |
| } |
| String collection = line.getOptionValue(COLLECTION); |
| String confName = line.getOptionValue(CONFNAME); |
| |
| ZkController.linkConfSet(zkClient, collection, confName); |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(LIST)) { |
| zkClient.printLayoutToStream(stdout); |
| } else if (line.getOptionValue(CMD).equals(LS)) { |
| |
| List<String> argList = line.getArgList(); |
| if (argList.size() != 1) { |
| stdout.println("-" + LS + " requires one arg - the path to list"); |
| System.exit(1); |
| } |
| |
| StringBuilder sb = new StringBuilder(); |
| String path = argList.get(0); |
| zkClient.printLayout(path == null ? "/" : path, 0, sb); |
| stdout.println(sb); |
| |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(CLEAR)) { |
| List<String> arglist = line.getArgList(); |
| if (arglist.size() != 1) { |
| stdout.println("-" + CLEAR + " requires one arg - the path to clear"); |
| System.exit(1); |
| } |
| zkClient.clean(arglist.get(0)); |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(MAKEPATH)) { |
| List<String> arglist = line.getArgList(); |
| if (arglist.size() != 1) { |
| stdout.println("-" + MAKEPATH + " requires one arg - the path to make"); |
| System.exit(1); |
| } |
| if (!ZkController.checkChrootPath(zkServerAddress, true)) { |
| stdout.println("A chroot was specified in zkHost but the znode doesn't exist. "); |
| System.exit(1); |
| } |
| zkClient.makePath(arglist.get(0), true); |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(PUT)) { |
| List<String> arglist = line.getArgList(); |
| if (arglist.size() != 2) { |
| stdout.println( |
| "-" + PUT + " requires two args - the path to create and the data string"); |
| System.exit(1); |
| } |
| String path = arglist.get(0); |
| byte[] data = arglist.get(1).getBytes(StandardCharsets.UTF_8); |
| if (shouldCompressData(data, path, minStateByteLenForCompression)) { |
| // state.json should be compressed before being put to ZK |
| // data = compressor.compressBytes(data, data.length / 10); |
| } |
| if (zkClient.exists(path, true)) { |
| zkClient.setData(path, data, true); |
| } else { |
| zkClient.makePath(path, data, CreateMode.PERSISTENT, true); |
| } |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(PUT_FILE)) { |
| List<String> arglist = line.getArgList(); |
| if (arglist.size() != 2) { |
| stdout.println( |
| "-" |
| + PUT_FILE |
| + " requires two args - the path to create in ZK and the path to the local file"); |
| System.exit(1); |
| } |
| |
| String path = arglist.get(0); |
| byte[] data = Files.readAllBytes(Path.of(arglist.get(1))); |
| if (shouldCompressData(data, path, minStateByteLenForCompression)) { |
| // state.json should be compressed before being put to ZK |
| // data = compressor.compressBytes(data, data.length / 10); |
| } |
| if (zkClient.exists(path, true)) { |
| zkClient.setData(path, data, true); |
| } else { |
| if (!ZkController.checkChrootPath(zkServerAddress, true)) { |
| stdout.println("A chroot was specified in zkHost but the znode doesn't exist. "); |
| System.exit(1); |
| } |
| zkClient.makePath(path, data, CreateMode.PERSISTENT, true); |
| } |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(GET)) { |
| List<String> arglist = line.getArgList(); |
| if (arglist.size() != 1) { |
| stdout.println("-" + GET + " requires one arg - the path to get"); |
| System.exit(1); |
| } |
| byte[] data = zkClient.getData(arglist.get(0), null, null, true); |
| stdout.println(new String(data, StandardCharsets.UTF_8)); |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(GET_FILE)) { |
| List<String> arglist = line.getArgList(); |
| if (arglist.size() != 2) { |
| stdout.println( |
| "-" + GET_FILE + "requires two args - the path to get and the file to save it to"); |
| System.exit(1); |
| } |
| byte[] data = zkClient.getData(arglist.get(0), null, null, true); |
| Files.write(Path.of(arglist.get(1)), data); |
| } else if (line.getOptionValue(CMD).equals(UPDATEACLS)) { |
| List<String> arglist = line.getArgList(); |
| if (arglist.size() != 1) { |
| stdout.println("-" + UPDATEACLS + " requires one arg - the path to update"); |
| System.exit(1); |
| } |
| zkClient.updateACLs(arglist.get(0)); |
| } else if (line.getOptionValue(CMD).equalsIgnoreCase(CLUSTERPROP)) { |
| if (!line.hasOption(NAME)) { |
| stdout.println("-" + NAME + " is required for " + CLUSTERPROP); |
| } |
| if (!ZkController.checkChrootPath(zkServerAddress, true)) { |
| stdout.println("A chroot was specified in zkHost but the znode doesn't exist. "); |
| System.exit(1); |
| } |
| String propertyName = line.getOptionValue(NAME); |
| // If -val option is missing, we will use the null value. This is required to maintain |
| // compatibility with Collections API. |
| String propertyValue = line.getOptionValue(VALUE_LONG); |
| ClusterProperties props = new ClusterProperties(zkClient); |
| try { |
| props.setClusterProperty(propertyName, propertyValue); |
| } catch (IOException ex) { |
| stdout.println( |
| "Unable to set the cluster property due to following error : " |
| + ex.getLocalizedMessage()); |
| System.exit(1); |
| } |
| } else { |
| // If not cmd matches |
| stdout.println("Unknown command " + line.getOptionValue(CMD) + ". Use -h to get help."); |
| System.exit(1); |
| } |
| } finally { |
| if (solrPort != null) { |
| zkServer.stop(); |
| } |
| } |
| } catch (ParseException exp) { |
| stdout.println("Unexpected exception:" + exp.getMessage()); |
| } |
| } |
| |
| private static boolean shouldCompressData( |
| byte[] data, String path, int minStateByteLenForCompression) { |
| if (path.endsWith("state.json") |
| && minStateByteLenForCompression > -1 |
| && data.length > minStateByteLenForCompression) { |
| // state.json should be compressed before being put to ZK |
| return true; |
| } |
| return false; |
| } |
| } |