/* * 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.accumulo.cluster.standalone; import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.URL; import java.security.CodeSource; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map.Entry; import org.apache.accumulo.cluster.ClusterControl; import org.apache.accumulo.cluster.RemoteShell; import org.apache.accumulo.cluster.RemoteShellOptions; import org.apache.accumulo.core.master.thrift.MasterGoalState; import org.apache.accumulo.master.state.SetGoalState; import org.apache.accumulo.minicluster.ServerType; import org.apache.accumulo.server.util.Admin; import org.apache.commons.lang.StringUtils; import org.apache.hadoop.util.Shell.ExitCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Maps; /** * Use the {@link RemoteShell} to control a standalone (possibly distibuted) Accumulo instance */ public class StandaloneClusterControl implements ClusterControl { private static final Logger log = LoggerFactory.getLogger(StandaloneClusterControl.class); private static final String ACCUMULO_SERVICE_SCRIPT = "accumulo-service", ACCUMULO_SCRIPT = "accumulo", ACCUMULO_UTIL_SCRIPT = "accumulo-util"; private static final String MASTER_HOSTS_FILE = "masters", GC_HOSTS_FILE = "gc", TSERVER_HOSTS_FILE = "tservers", TRACER_HOSTS_FILE = "tracers", MONITOR_HOSTS_FILE = "monitor"; String accumuloHome; String clientAccumuloConfDir; String serverAccumuloConfDir; private String clientCmdPrefix; private String serverCmdPrefix; protected RemoteShellOptions options; protected String accumuloServicePath, accumuloPath, accumuloUtilPath; public StandaloneClusterControl(String accumuloHome, String clientAccumuloConfDir, String serverAccumuloConfDir, String clientCmdPrefix, String serverCmdPrefix) { this.options = new RemoteShellOptions(); this.accumuloHome = accumuloHome; this.clientAccumuloConfDir = clientAccumuloConfDir; this.serverAccumuloConfDir = serverAccumuloConfDir; this.clientCmdPrefix = clientCmdPrefix; this.serverCmdPrefix = serverCmdPrefix; File bin = new File(accumuloHome, "bin"); this.accumuloServicePath = new File(bin, ACCUMULO_SERVICE_SCRIPT).getAbsolutePath(); this.accumuloPath = new File(bin, ACCUMULO_SCRIPT).getAbsolutePath(); this.accumuloUtilPath = new File(bin, ACCUMULO_UTIL_SCRIPT).getAbsolutePath(); } String getAccumuloUtilPath() { return this.accumuloUtilPath; } protected Entry<Integer,String> exec(String hostname, String[] command) throws IOException { RemoteShell shell = new RemoteShell(hostname, command, options); try { shell.execute(); } catch (ExitCodeException e) { // capture the stdout of the process as well. String output = shell.getOutput(); // add output for the ExitCodeException. ExitCodeException ece = new ExitCodeException(e.getExitCode(), "stderr: " + e.getMessage() + ", stdout: " + output); log.error("Failed to run command", ece); return Maps.immutableEntry(e.getExitCode(), output); } return Maps.immutableEntry(shell.getExitCode(), shell.getOutput()); } @Override public int exec(Class<?> clz, String[] args) throws IOException { return execWithStdout(clz, args).getKey(); } @Override public Entry<Integer,String> execWithStdout(Class<?> clz, String[] args) throws IOException { String master = getHosts(MASTER_HOSTS_FILE).get(0); List<String> cmd = new ArrayList<>(); cmd.add(clientCmdPrefix); cmd.add(accumuloPath); cmd.add(clz.getName()); // Quote the arguments to prevent shell expansion for (String arg : args) { cmd.add("'" + arg + "'"); } log.info("Running: '{}' on {}", StringUtils.join(cmd, " "), master); return exec(master, cmd.toArray(new String[cmd.size()])); } public Entry<Integer,String> execMapreduceWithStdout(Class<?> clz, String[] args) throws IOException { String host = "localhost"; List<String> cmd = new ArrayList<>(); cmd.add(getAccumuloUtilPath()); cmd.add("hadoop-jar"); cmd.add(getJarFromClass(clz)); cmd.add(clz.getName()); for (String arg : args) { cmd.add("'" + arg + "'"); } log.info("Running: '{}' on {}", StringUtils.join(cmd, " "), host); return exec(host, cmd.toArray(new String[cmd.size()])); } String getJarFromClass(Class<?> clz) { CodeSource source = clz.getProtectionDomain().getCodeSource(); if (null == source) { throw new RuntimeException("Could not get CodeSource for class"); } URL jarUrl = source.getLocation(); String jar = jarUrl.getPath(); if (!jar.endsWith(".jar")) { throw new RuntimeException("Need to have a jar to run mapreduce: " + jar); } return jar; } @Override public void adminStopAll() throws IOException { String master = getHosts(MASTER_HOSTS_FILE).get(0); String[] cmd = new String[] {serverCmdPrefix, accumuloPath, Admin.class.getName(), "stopAll"}; // Directly invoke the RemoteShell Entry<Integer,String> pair = exec(master, cmd); if (0 != pair.getKey().intValue()) { throw new IOException("stopAll did not finish successfully, retcode=" + pair.getKey() + ", stdout=" + pair.getValue()); } } /** * Wrapper around SetGoalState * * @param goalState * The goal state to set * @throws IOException * If SetGoalState returns a non-zero result */ public void setGoalState(String goalState) throws IOException { requireNonNull(goalState, "Goal state must not be null"); checkArgument(MasterGoalState.valueOf(goalState) != null, "Unknown goal state: " + goalState); String master = getHosts(MASTER_HOSTS_FILE).get(0); String[] cmd = new String[] {serverCmdPrefix, accumuloPath, SetGoalState.class.getName(), goalState}; Entry<Integer,String> pair = exec(master, cmd); if (0 != pair.getKey().intValue()) { throw new IOException("SetGoalState did not finish successfully, retcode=" + pair.getKey() + ", stdout=" + pair.getValue()); } } @Override public void startAllServers(ServerType server) throws IOException { switch (server) { case TABLET_SERVER: for (String tserver : getHosts(TSERVER_HOSTS_FILE)) { start(server, tserver); } break; case MASTER: for (String master : getHosts(MASTER_HOSTS_FILE)) { start(server, master); } break; case GARBAGE_COLLECTOR: List<String> hosts = getHosts(GC_HOSTS_FILE); if (hosts.isEmpty()) { hosts = getHosts(MASTER_HOSTS_FILE); if (hosts.isEmpty()) { throw new IOException("Found hosts to run garbage collector on"); } hosts = Collections.singletonList(hosts.get(0)); } for (String gc : hosts) { start(server, gc); } break; case TRACER: for (String tracer : getHosts(TRACER_HOSTS_FILE)) { start(server, tracer); } break; case MONITOR: for (String monitor : getHosts(MONITOR_HOSTS_FILE)) { start(server, monitor); } break; case ZOOKEEPER: default: throw new UnsupportedOperationException("Could not start servers for " + server); } } @Override public void start(ServerType server, String hostname) throws IOException { String[] cmd = new String[] {serverCmdPrefix, accumuloServicePath, getProcessString(server), "start"}; Entry<Integer,String> pair = exec(hostname, cmd); if (0 != pair.getKey()) { throw new IOException("Start " + server + " on " + hostname + " failed for execute successfully"); } } @Override public void stopAllServers(ServerType server) throws IOException { switch (server) { case TABLET_SERVER: for (String tserver : getHosts(TSERVER_HOSTS_FILE)) { stop(server, tserver); } break; case MASTER: for (String master : getHosts(MASTER_HOSTS_FILE)) { stop(server, master); } break; case GARBAGE_COLLECTOR: for (String gc : getHosts(GC_HOSTS_FILE)) { stop(server, gc); } break; case TRACER: for (String tracer : getHosts(TRACER_HOSTS_FILE)) { stop(server, tracer); } break; case MONITOR: for (String monitor : getHosts(MONITOR_HOSTS_FILE)) { stop(server, monitor); } break; case ZOOKEEPER: default: throw new UnsupportedOperationException("Could not start servers for " + server); } } @Override public void stop(ServerType server, String hostname) throws IOException { // TODO Use `accumulo admin stop` for tservers, instrument clean stop for GC, monitor, tracer instead kill kill(server, hostname); } @Override public void signal(ServerType server, String hostname, String signal) throws IOException { String pid = getPid(server, accumuloHome, hostname); if (pid.trim().isEmpty()) { log.debug("Found no processes for {} on {}", server, hostname); return; } boolean isSignalNumber = false; try { Integer.parseInt(signal); isSignalNumber = true; } catch (NumberFormatException e) {} String[] stopCmd; if (isSignalNumber) { stopCmd = new String[] {serverCmdPrefix, "kill", "-" + signal, pid}; } else { stopCmd = new String[] {serverCmdPrefix, "kill", "-s", signal, pid}; } Entry<Integer,String> pair = exec(hostname, stopCmd); if (0 != pair.getKey()) { throw new IOException("Signal " + signal + " to " + server + " on " + hostname + " failed for execute successfully. stdout=" + pair.getValue()); } } @Override public void suspend(ServerType server, String hostname) throws IOException { signal(server, hostname, "SIGSTOP"); } @Override public void resume(ServerType server, String hostname) throws IOException { signal(server, hostname, "SIGCONT"); } @Override public void kill(ServerType server, String hostname) throws IOException { signal(server, hostname, "SIGKILL"); } protected String getPid(ServerType server, String accumuloHome, String hostname) throws IOException { String[] getPidCommand = getPidCommand(server, accumuloHome); Entry<Integer,String> ret = exec(hostname, getPidCommand); if (0 != ret.getKey()) { throw new IOException("Could not locate PID for " + getProcessString(server) + " on " + hostname); } return ret.getValue(); } protected String[] getPidCommand(ServerType server, String accumuloHome) { // Lifted from stop-server.sh to get the PID return new String[] {"ps", "aux", "|", "fgrep", accumuloHome, "|", "fgrep", getProcessString(server), "|", "fgrep", "-v", "grep", "|", "fgrep", "-v", "ssh", "|", "awk", "'{print \\$2}'", "|", "head", "-1", "|", "tr", "-d", "'\\n'"}; } protected String getProcessString(ServerType server) { switch (server) { case TABLET_SERVER: return "tserver"; case GARBAGE_COLLECTOR: return "gc"; case MASTER: return "master"; case TRACER: return "tracer"; case MONITOR: return "monitor"; default: throw new UnsupportedOperationException("Unhandled ServerType " + server); } } protected File getClientConfDir() { File confDir = new File(clientAccumuloConfDir); if (!confDir.exists() || !confDir.isDirectory()) { throw new IllegalStateException("Accumulo client conf dir does not exist or is not a directory: " + confDir); } return confDir; } protected File getServerConfDir() { File confDir = new File(serverAccumuloConfDir); if (!confDir.exists() || !confDir.isDirectory()) { throw new IllegalStateException("Accumulo server conf dir does not exist or is not a directory: " + confDir); } return confDir; } /** * Read hosts in file named by 'fn' in Accumulo conf dir */ protected List<String> getHosts(String fn) throws IOException { return getHosts(new File(getServerConfDir(), fn)); } /** * Read the provided file and return all lines which don't start with a '#' character */ protected List<String> getHosts(File f) throws IOException { BufferedReader reader = new BufferedReader(new FileReader(f)); try { List<String> hosts = new ArrayList<>(); String line; while ((line = reader.readLine()) != null) { line = line.trim(); if (!line.isEmpty() && !line.startsWith("#")) { hosts.add(line); } } return hosts; } finally { reader.close(); } } }