/* * 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.common.cloud; import java.io.File; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; import org.apache.solr.client.solrj.SolrServerException; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.data.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class to hold ZK upload/download/move common code. With the advent of the upconfig/downconfig/cp/ls/mv commands * in bin/solr it made sense to keep the individual transfer methods in a central place, so here it is. */ public class ZkMaintenanceUtils { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final String ZKNODE_DATA_FILE = "zknode.data"; private ZkMaintenanceUtils() {} // don't let it be instantiated, all methods are static. /** * Lists a ZNode child and (optionally) the znodes of all the children. No data is dumped. * * @param path The node to remove on Zookeeper * @param recurse Whether to remove children. * @throws KeeperException Could not perform the Zookeeper operation. * @throws InterruptedException Thread interrupted * @throws SolrServerException zookeeper node has children and recurse not specified. * @return an indented list of the znodes suitable for display */ public static String listZnode(SolrZkClient zkClient, String path, Boolean recurse) throws KeeperException, InterruptedException, SolrServerException { String root = path; if (path.toLowerCase(Locale.ROOT).startsWith("zk:")) { root = path.substring(3); } if (root.equals("/") == false && root.endsWith("/")) { root = root.substring(0, root.length() - 1); } StringBuilder sb = new StringBuilder(); if (recurse == false) { for (String node : zkClient.getChildren(root, null, true)) { if (node.equals("zookeeper") == false) { sb.append(node).append(System.lineSeparator()); } } return sb.toString(); } traverseZkTree(zkClient, root, VISIT_ORDER.VISIT_PRE, znode -> { if (znode.startsWith("/zookeeper")) return; // can't do anything with this node! int iPos = znode.lastIndexOf("/"); if (iPos > 0) { for (int idx = 0; idx < iPos; ++idx) sb.append(" "); sb.append(znode.substring(iPos + 1)).append(System.lineSeparator()); } else { sb.append(znode).append(System.lineSeparator()); } }); return sb.toString(); } /** * Copy between local file system and Zookeeper, or from one Zookeeper node to another, * optionally copying recursively. * * @param src Source to copy from. Both src and dst may be Znodes. However, both may NOT be local * @param dst The place to copy the files too. Both src and dst may be Znodes. However both may NOT be local * @param recurse if the source is a directory, reccursively copy the contents iff this is true. * @throws SolrServerException Explanatory exception due to bad params, failed operation, etc. * @throws KeeperException Could not perform the Zookeeper operation. * @throws InterruptedException Thread interrupted */ public static void zkTransfer(SolrZkClient zkClient, String src, Boolean srcIsZk, String dst, Boolean dstIsZk, Boolean recurse) throws SolrServerException, KeeperException, InterruptedException, IOException { if (srcIsZk == false && dstIsZk == false) { throw new SolrServerException("One or both of source or destination must specify ZK nodes."); } // Make sure -recurse is specified if the source has children. if (recurse == false) { if (srcIsZk) { if (zkClient.getChildren(src, null, true).size() != 0) { throw new SolrServerException("Zookeeper node " + src + " has children and recurse is false"); } } else if (Files.isDirectory(Paths.get(src))) { throw new SolrServerException("Local path " + Paths.get(src).toAbsolutePath() + " is a directory and recurse is false"); } } if (dstIsZk && dst.length() == 0) { dst = "/"; // for consistency, one can copy from zk: and send to zk:/ } dst = normalizeDest(src, dst, srcIsZk, dstIsZk); // ZK -> ZK copy. if (srcIsZk && dstIsZk) { traverseZkTree(zkClient, src, VISIT_ORDER.VISIT_PRE, new ZkCopier(zkClient, src, dst)); return; } //local -> ZK copy if (dstIsZk) { uploadToZK(zkClient, Paths.get(src), dst, null); return; } // Copying individual files from ZK requires special handling since downloadFromZK assumes the node has children. // This is kind of a weak test for the notion of "directory" on Zookeeper. // ZK -> local copy where ZK is a parent node if (zkClient.getChildren(src, null, true).size() > 0) { downloadFromZK(zkClient, src, Paths.get(dst)); return; } // Single file ZK -> local copy where ZK is a leaf node if (Files.isDirectory(Paths.get(dst))) { if (dst.endsWith(File.separator) == false) dst += File.separator; dst = normalizeDest(src, dst, srcIsZk, dstIsZk); } byte[] data = zkClient.getData(src, null, null, true); Path filename = Paths.get(dst); Files.createDirectories(filename.getParent()); log.info("Writing file {}", filename); Files.write(filename, data); } // If the dest ends with a separator, it's a directory or non-leaf znode, so return the // last element of the src to appended to the dstName. private static String normalizeDest(String srcName, String dstName, boolean srcIsZk, boolean dstIsZk) { // Special handling for "." if (dstName.equals(".")) { return Paths.get(".").normalize().toAbsolutePath().toString(); } String dstSeparator = (dstIsZk) ? "/" : File.separator; String srcSeparator = (srcIsZk) ? "/" : File.separator; if (dstName.endsWith(dstSeparator)) { // Dest is a directory or non-leaf znode, append last element of the src path. int pos = srcName.lastIndexOf(srcSeparator); if (pos < 0) { dstName += srcName; } else { dstName += srcName.substring(pos + 1); } } log.info("copying from '{}' to '{}'", srcName, dstName); return dstName; } public static void moveZnode(SolrZkClient zkClient, String src, String dst) throws SolrServerException, KeeperException, InterruptedException { String destName = normalizeDest(src, dst, true, true); // Special handling if the source has no children, i.e. copying just a single file. if (zkClient.getChildren(src, null, true).size() == 0) { zkClient.makePath(destName, false, true); zkClient.setData(destName, zkClient.getData(src, null, null, true), true); } else { traverseZkTree(zkClient, src, VISIT_ORDER.VISIT_PRE, new ZkCopier(zkClient, src, destName)); } // Insure all source znodes are present in dest before deleting the source. // throws error if not all there so the source is left intact. Throws error if source and dest don't match. checkAllZnodesThere(zkClient, src, destName); clean(zkClient, src); } // Insure that all the nodes in one path match the nodes in the other as a safety check before removing // the source in a 'mv' command. private static void checkAllZnodesThere(SolrZkClient zkClient, String src, String dst) throws KeeperException, InterruptedException, SolrServerException { for (String node : zkClient.getChildren(src, null, true)) { if (zkClient.exists(dst + "/" + node, true) == false) { throw new SolrServerException("mv command did not move node " + dst + "/" + node + " source left intact"); } checkAllZnodesThere(zkClient, src + "/" + node, dst + "/" + node); } } // This not just a copy operation since the config manager takes care of construction the znode path to configsets public static void downConfig(SolrZkClient zkClient, String confName, Path confPath) throws IOException { ZkConfigManager manager = new ZkConfigManager(zkClient); // Try to download the configset manager.downloadConfigDir(confName, confPath); } // This not just a copy operation since the config manager takes care of construction the znode path to configsets public static void upConfig(SolrZkClient zkClient, Path confPath, String confName) throws IOException { ZkConfigManager manager = new ZkConfigManager(zkClient); // Try to download the configset manager.uploadConfigDir(confPath, confName); } // yeah, it's recursive :( public static void clean(SolrZkClient zkClient, String path) throws InterruptedException, KeeperException { traverseZkTree(zkClient, path, VISIT_ORDER.VISIT_POST, znode -> { try { if (!znode.equals("/")) { try { zkClient.delete(znode, -1, true); } catch (KeeperException.NotEmptyException e) { clean(zkClient, znode); } } } catch (KeeperException.NoNodeException r) { return; } }); } public static void uploadToZK(SolrZkClient zkClient, final Path fromPath, final String zkPath, final Pattern filenameExclusions) throws IOException { String path = fromPath.toString(); if (path.endsWith("*")) { path = path.substring(0, path.length() - 1); } final Path rootPath = Paths.get(path); if (!Files.exists(rootPath)) throw new IOException("Path " + rootPath + " does not exist"); Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String filename = file.getFileName().toString(); if (filenameExclusions != null && filenameExclusions.matcher(filename).matches()) { log.info("uploadToZK skipping '{}' due to filenameExclusions '{}'", filename, filenameExclusions); return FileVisitResult.CONTINUE; } String zkNode = createZkNodeName(zkPath, rootPath, file); try { // if the path exists (and presumably we're uploading data to it) just set its data if (file.toFile().getName().equals(ZKNODE_DATA_FILE) && zkClient.exists(zkNode, true)) { zkClient.setData(zkNode, file.toFile(), true); } else { zkClient.makePath(zkNode, file.toFile(), false, true); } } catch (KeeperException | InterruptedException e) { throw new IOException("Error uploading file " + file.toString() + " to zookeeper path " + zkNode, SolrZkClient.checkInterrupted(e)); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (dir.getFileName().toString().startsWith(".")) return FileVisitResult.SKIP_SUBTREE; return FileVisitResult.CONTINUE; } }); } private static boolean isEphemeral(SolrZkClient zkClient, String zkPath) throws KeeperException, InterruptedException { Stat znodeStat = zkClient.exists(zkPath, null, true); return znodeStat.getEphemeralOwner() != 0; } private static int copyDataDown(SolrZkClient zkClient, String zkPath, File file) throws IOException, KeeperException, InterruptedException { byte[] data = zkClient.getData(zkPath, null, null, true); if (data != null && data.length > 1) { // There are apparently basically empty ZNodes. log.info("Writing file {}", file.toString()); Files.write(file.toPath(), data); return data.length; } return 0; } public static void downloadFromZK(SolrZkClient zkClient, String zkPath, Path file) throws IOException { try { List<String> children = zkClient.getChildren(zkPath, null, true); // If it has no children, it's a leaf node, write the assoicated data from the ZNode. // Otherwise, continue recursing, but write the associated data to a special file if any if (children.size() == 0) { // If we didn't copy data down, then we also didn't create the file. But we still need a marker on the local // disk so create a dir. if (copyDataDown(zkClient, zkPath, file.toFile()) == 0) { Files.createDirectories(file); } } else { Files.createDirectories(file); // Make parent dir. // ZK nodes, whether leaf or not can have data. If it's a non-leaf node and // has associated data write it into the special file. copyDataDown(zkClient, zkPath, new File(file.toFile(), ZKNODE_DATA_FILE)); for (String child : children) { String zkChild = zkPath; if (zkChild.endsWith("/") == false) zkChild += "/"; zkChild += child; if (isEphemeral(zkClient, zkChild)) { // Don't copy ephemeral nodes continue; } // Go deeper into the tree now downloadFromZK(zkClient, zkChild, file.resolve(child)); } } } catch (KeeperException | InterruptedException e) { throw new IOException("Error downloading files from zookeeper path " + zkPath + " to " + file.toString(), SolrZkClient.checkInterrupted(e)); } } @FunctionalInterface public interface ZkVisitor { /** * Visit the target path * * @param path the path to visit */ void visit(String path) throws InterruptedException, KeeperException; } public enum VISIT_ORDER { VISIT_PRE, VISIT_POST } /** * Recursively visit a zk tree rooted at path and apply the given visitor to each path. Exists as a separate method * because some of the logic can get nuanced. * * @param path the path to start from * @param visitOrder whether to call the visitor at the at the ending or beginning of the run. * @param visitor the operation to perform on each path */ public static void traverseZkTree(SolrZkClient zkClient, final String path, final VISIT_ORDER visitOrder, final ZkVisitor visitor) throws InterruptedException, KeeperException { if (visitOrder == VISIT_ORDER.VISIT_PRE) { visitor.visit(path); } List<String> children; try { children = zkClient.getChildren(path, null, true); } catch (KeeperException.NoNodeException r) { return; } for (String string : children) { // we can't do anything to the built-in zookeeper node if (path.equals("/") && string.equals("zookeeper")) continue; if (path.startsWith("/zookeeper")) continue; if (path.equals("/")) { traverseZkTree(zkClient, path + string, visitOrder, visitor); } else { traverseZkTree(zkClient, path + "/" + string, visitOrder, visitor); } } if (visitOrder == VISIT_ORDER.VISIT_POST) { visitor.visit(path); } } // Take into account Windows file separators when making a Znode's name. public static String createZkNodeName(String zkRoot, Path root, Path file) { String relativePath = root.relativize(file).toString(); // Windows shenanigans if ("\\".equals(File.separator)) relativePath = relativePath.replaceAll("\\\\", "/"); // It's possible that the relative path and file are the same, in which case // adding the bare slash is A Bad Idea unless it's a non-leaf data node boolean isNonLeafData = file.toFile().getName().equals(ZKNODE_DATA_FILE); if (relativePath.length() == 0 && isNonLeafData == false) return zkRoot; // Important to have this check if the source is file:whatever/ and the destination is just zk:/ if (zkRoot.endsWith("/") == false) zkRoot += "/"; String ret = zkRoot + relativePath; // Special handling for data associated with non-leaf node. if (isNonLeafData) { // special handling since what we need to do is add the data to the parent. ret = ret.substring(0, ret.indexOf(ZKNODE_DATA_FILE)); if (ret.endsWith("/")) { ret = ret.substring(0, ret.length() - 1); } } return ret; } } class ZkCopier implements ZkMaintenanceUtils.ZkVisitor { String source; String dest; SolrZkClient zkClient; ZkCopier(SolrZkClient zkClient, String source, String dest) { this.source = source; this.dest = dest; if (dest.endsWith("/")) { this.dest = dest.substring(0, dest.length() - 1); } this.zkClient = zkClient; } @Override public void visit(String path) throws InterruptedException, KeeperException { String finalDestination = dest; if (path.equals(source) == false) finalDestination += "/" + path.substring(source.length() + 1); zkClient.makePath(finalDestination, false, true); zkClient.setData(finalDestination, zkClient.getData(path, null, null, true), true); } }