diff --git a/bin/alluxio b/bin/alluxio index 6bedb9479bab..4f02c90b43af 100755 --- a/bin/alluxio +++ b/bin/alluxio @@ -264,6 +264,11 @@ function main { ALLUXIO_SHELL_JAVA_OPTS+=" -Dalluxio.conf.validation.enabled=false" runJavaClass "$@" ;; + "collectInfo") + CLASS="alluxio.cli.bundler.CollectInfo" + CLASSPATH=${ALLUXIO_CLIENT_CLASSPATH} + runJavaClass "$@" + ;; "job") CLASS="alluxio.cli.job.JobShell" CLASSPATH=${ALLUXIO_CLIENT_CLASSPATH} diff --git a/core/common/src/main/java/alluxio/cli/CommandUtils.java b/core/common/src/main/java/alluxio/cli/CommandUtils.java index 14c7a322fa31..4f15306d7365 100644 --- a/core/common/src/main/java/alluxio/cli/CommandUtils.java +++ b/core/common/src/main/java/alluxio/cli/CommandUtils.java @@ -18,10 +18,17 @@ import org.apache.commons.cli.CommandLine; import org.reflections.Reflections; +import java.io.IOException; import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; /** @@ -104,4 +111,33 @@ public static void checkNumOfArgsNoMoreThan(Command cmd, CommandLine cl, int n) .getMessage(cmd.getCommandName(), n, cl.getArgs().length)); } } + + /** + * Reads a list of nodes from given file name ignoring comments and empty lines. + * Can be used to read conf/workers or conf/masters. + * @param confDir directory that holds the configuration + * @param fileName name of a file that contains the list of the nodes + * @return list of the node names, null when file fails to read + */ + @Nullable + public static List readNodeList(String confDir, String fileName) { + List lines; + try { + lines = Files.readAllLines(Paths.get(confDir, fileName), StandardCharsets.UTF_8); + } catch (IOException e) { + System.err.format("Failed to read file %s/%s. Ignored.", confDir, fileName); + return new ArrayList<>(); + } + + List nodes = new ArrayList<>(); + for (String line : lines) { + String node = line.trim(); + if (node.startsWith("#") || node.length() == 0) { + continue; + } + nodes.add(node); + } + + return nodes; + } } diff --git a/core/common/src/main/java/alluxio/shell/CommandReturn.java b/core/common/src/main/java/alluxio/shell/CommandReturn.java index 80a7334ad9a4..cf6f8fc53d3d 100644 --- a/core/common/src/main/java/alluxio/shell/CommandReturn.java +++ b/core/common/src/main/java/alluxio/shell/CommandReturn.java @@ -11,11 +11,14 @@ package alluxio.shell; +import java.util.Arrays; + /** * Object representation of a command execution. */ public class CommandReturn { private int mExitCode; + private String[] mCmd; private String mOutput; /** @@ -26,16 +29,22 @@ public class CommandReturn { */ public CommandReturn(int code, String output) { mExitCode = code; + mCmd = new String[]{}; mOutput = output; } /** - * Gets the stdout content. + * Creates object from the contents. + * Copy the command array. * - * @return stdout content + * @param code exit code + * @param cmd the command executed + * @param output stdout content */ - public String getOutput() { - return mOutput; + public CommandReturn(int code, String[] cmd, String output) { + mExitCode = code; + mCmd = Arrays.copyOfRange(cmd, 0, cmd.length); + mOutput = output; } /** @@ -47,6 +56,24 @@ public int getExitCode() { return mExitCode; } + /** + * Gets the command run. + * + * @return the command + * */ + public String[] getCmd() { + return mCmd; + } + + /** + * Gets the stdout content. + * + * @return stdout content + */ + public String getOutput() { + return mOutput; + } + /** * Formats the object to more readable format. * This is not done in toString() because stdout and stderr may be long. @@ -54,7 +81,7 @@ public int getExitCode() { * @return pretty formatted output */ public String getFormattedOutput() { - return String.format("StatusCode:%s%nOutput:%n%s", getExitCode(), - getOutput()); + return String.format("ExitCode:%s%nCommand:%s%nOutput:%n%s", getExitCode(), + Arrays.toString(getCmd()), getOutput()); } } diff --git a/core/common/src/main/java/alluxio/shell/ShellCommand.java b/core/common/src/main/java/alluxio/shell/ShellCommand.java index 06968c3f1c90..9d0cb21015b8 100644 --- a/core/common/src/main/java/alluxio/shell/ShellCommand.java +++ b/core/common/src/main/java/alluxio/shell/ShellCommand.java @@ -99,15 +99,20 @@ public String run() throws IOException { /** * Runs a command and returns its output and exit code in Object. * Preserves the output when the execution fails. + * If the command execution fails (not by an interrupt), + * try to wrap the Exception in the {@link CommandReturn}. * Stderr is redirected to stdout. * * @return {@link CommandReturn} object representation of stdout, stderr and exit code */ public CommandReturn runWithOutput() throws IOException { - Process process = new ProcessBuilder(mCommand).redirectErrorStream(true).start(); + Process process = null; + BufferedReader inReader = null; + try { + process = new ProcessBuilder(mCommand).redirectErrorStream(true).start(); + inReader = + new BufferedReader(new InputStreamReader(process.getInputStream())); - try (BufferedReader inReader = - new BufferedReader(new InputStreamReader(process.getInputStream()))) { // read the output of the command StringBuilder stdout = new StringBuilder(); String outLine = inReader.readLine(); @@ -125,7 +130,7 @@ public CommandReturn runWithOutput() throws IOException { exitCode, Arrays.toString(mCommand))); } - CommandReturn cr = new CommandReturn(exitCode, stdout.toString()); + CommandReturn cr = new CommandReturn(exitCode, mCommand, stdout.toString()); // destroy the process if (process != null) { @@ -136,10 +141,34 @@ public CommandReturn runWithOutput() throws IOException { } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(e); + } catch (Exception e) { + return new CommandReturn(1, String.format("Command %s failed, exception is %s", + Arrays.toString(mCommand), e.getMessage())); } finally { + if (inReader != null) { + inReader.close(); + } if (process != null) { process.destroy(); } } } + + /** + * Converts the command to string repr. + * + * @return the shell command + * */ + public String toString() { + return Arrays.toString(mCommand); + } + + /** + * Gets the command. The original array is immutable. + * + * @return a copy of the command string array + * */ + public String[] getCommand() { + return Arrays.copyOfRange(mCommand, 0, mCommand.length); + } } diff --git a/core/common/src/main/java/alluxio/util/ShellUtils.java b/core/common/src/main/java/alluxio/util/ShellUtils.java index 658ed468a779..bd379044b5c2 100644 --- a/core/common/src/main/java/alluxio/util/ShellUtils.java +++ b/core/common/src/main/java/alluxio/util/ShellUtils.java @@ -240,5 +240,23 @@ public static CommandReturn scpCommandWithOutput( return new ScpCommand(hostname, fromFile, toFile, isDir).runWithOutput(); } + /** + * Executes a shell command. + * If it fails, try the backup command. + * + * @param cmd the primary command + * @param backupCmd a backup option + * @return the {@link CommandReturn} with combined output + */ + public static CommandReturn execCmdWithBackup(ShellCommand cmd, ShellCommand backupCmd) + throws IOException { + CommandReturn cr = cmd.runWithOutput(); + // If the command works or there is no backup option, return + if (cr.getExitCode() == 0 || backupCmd == null) { + return cr; + } + return backupCmd.runWithOutput(); + } + private ShellUtils() {} // prevent instantiation } diff --git a/core/common/src/test/java/alluxio/shell/ShellCommandTest.java b/core/common/src/test/java/alluxio/shell/ShellCommandTest.java index b47260dbbf0c..cea347fe6468 100644 --- a/core/common/src/test/java/alluxio/shell/ShellCommandTest.java +++ b/core/common/src/test/java/alluxio/shell/ShellCommandTest.java @@ -93,13 +93,13 @@ public void execCommandTolerateFailureFailed() throws Exception { public void execCommandTolerateFailureInvalidCommand() throws Exception { // create temp file File testDir = AlluxioTestDirectory.createTemporaryDirectory("command"); - - // if there's no such command there will be IOException - mExceptionRule.expect(IOException.class); - mExceptionRule.expectMessage("No such file or directory"); + // For a non-existent command the command return contains the err msg String[] testCommandExcept = new String[]{"lsa", String.format("%s", testDir.getAbsolutePath())}; // lsa is not a valid executable - new ShellCommand(testCommandExcept).runWithOutput(); + CommandReturn crf = new ShellCommand(testCommandExcept).runWithOutput(); + System.out.println(crf.getFormattedOutput()); + assertNotEquals(0, crf.getExitCode()); + assertTrue(crf.getOutput().contains("No such file or directory")); } } diff --git a/core/server/common/src/main/java/alluxio/cli/validation/Utils.java b/core/server/common/src/main/java/alluxio/cli/validation/Utils.java index f11283a65d4a..fd03dedc84b0 100644 --- a/core/server/common/src/main/java/alluxio/cli/validation/Utils.java +++ b/core/server/common/src/main/java/alluxio/cli/validation/Utils.java @@ -11,6 +11,7 @@ package alluxio.cli.validation; +import alluxio.cli.CommandUtils; import alluxio.conf.ServerConfiguration; import alluxio.conf.PropertyKey; import alluxio.util.ShellUtils; @@ -22,10 +23,6 @@ import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; @@ -83,24 +80,7 @@ public static boolean isAlluxioRunning(String className) { @Nullable public static List readNodeList(String fileName) { String confDir = ServerConfiguration.get(PropertyKey.CONF_DIR); - List lines; - try { - lines = Files.readAllLines(Paths.get(confDir, fileName), StandardCharsets.UTF_8); - } catch (IOException e) { - System.err.format("Failed to read file %s/%s.%n", confDir, fileName); - return null; - } - - List nodes = new ArrayList<>(); - for (String line : lines) { - String node = line.trim(); - if (node.startsWith("#") || node.length() == 0) { - continue; - } - nodes.add(node); - } - - return nodes; + return CommandUtils.readNodeList(confDir, fileName); } /** diff --git a/shell/pom.xml b/shell/pom.xml index 2b4a24a2a701..5fe459e74f1a 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -81,5 +81,9 @@ alluxio-job-client ${project.version} + + org.apache.commons + commons-compress + diff --git a/shell/src/main/java/alluxio/cli/bundler/CollectInfo.java b/shell/src/main/java/alluxio/cli/bundler/CollectInfo.java new file mode 100644 index 000000000000..66fe6c52947d --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/CollectInfo.java @@ -0,0 +1,466 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler; + +import alluxio.cli.AbstractShell; +import alluxio.cli.Command; +import alluxio.cli.CommandUtils; +import alluxio.cli.bundler.command.AbstractCollectInfoCommand; +import alluxio.client.file.FileSystemContext; +import alluxio.conf.InstancedConfiguration; +import alluxio.conf.PropertyKey; +import alluxio.conf.Source; +import alluxio.shell.CommandReturn; +import alluxio.util.ConfigurationUtils; +import alluxio.util.ShellUtils; +import alluxio.util.io.FileUtils; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Files; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.lang.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +/** + * Class for collecting various information about all nodes in the cluster. + */ +public class CollectInfo extends AbstractShell { + private static final Logger LOG = LoggerFactory.getLogger(CollectInfo.class); + private static final String FINAL_TARBALL_NAME = "alluxio-cluster-info-%s.tar.gz"; + + private static final Map CMD_ALIAS = ImmutableMap.of(); + + // In order for a warning to be displayed for an unstable alias, it must also exist within the + // CMD_ALIAS map. + private static final Set UNSTABLE_ALIAS = ImmutableSet.of(); + + private static final String MAX_THREAD_OPTION_NAME = "max-threads"; + private static final String LOCAL_OPTION_NAME = "local"; + private static final Option THREAD_NUM_OPTION = + Option.builder().required(false).longOpt(MAX_THREAD_OPTION_NAME).hasArg(true) + .desc("the maximum number of threads to use for collecting information remotely") + .build(); + private static final Option LOCAL_OPTION = + Option.builder().required(false).longOpt(LOCAL_OPTION_NAME).hasArg(false) + .desc("running only on localhost").build(); + private static final Options OPTIONS = + new Options().addOption(THREAD_NUM_OPTION).addOption(LOCAL_OPTION); + + private static final String TARBALL_NAME = "alluxio-info.tar.gz"; + + private ExecutorService mExecutor; + + /** + * Creates a new instance of {@link CollectInfo}. + * + * @param alluxioConf Alluxio configuration + */ + public CollectInfo(InstancedConfiguration alluxioConf) { + super(CMD_ALIAS, UNSTABLE_ALIAS, alluxioConf); + } + + /** + * Finds all hosts in the Alluxio cluster. + * We assume the masters and workers cover all the nodes in the cluster. + * This command now relies on conf/masters and conf/workers to contain + * the nodes in the cluster. + * This is the same requirement as bin/alluxio-start.sh. + * TODO(jiacheng): phase 2 specify hosts from cmdline + * TODO(jiacheng): phase 2 cross-check with the master for which nodes are in the cluster + * + * @return a set of hostnames in the cluster + * */ + public Set getHosts() { + String confDirPath = mConfiguration.get(PropertyKey.CONF_DIR); + System.out.format("Looking for masters and workers in %s%n", confDirPath); + Set hosts = new HashSet<>(); + hosts.addAll(CommandUtils.readNodeList(confDirPath, "masters")); + hosts.addAll(CommandUtils.readNodeList(confDirPath, "workers")); + System.out.format("Found %s hosts%n", hosts.size()); + return hosts; + } + + /** + * Main method, starts a new CollectInfo shell. + * CollectInfo will SSH to all hosts and invoke {@link CollectInfo} with --local option. + * Then collect the tarballs generated on each of the hosts to the localhost. + * And tarball all results into one final tarball. + * + * @param argv array of arguments given by the user's input from the terminal + */ + public static void main(String[] argv) throws IOException { + // Parse cmdline args + CommandLineParser parser = new DefaultParser(); + CommandLine cmd; + try { + cmd = parser.parse(OPTIONS, argv, true /* stopAtNonOption */); + } catch (ParseException e) { + return; + } + String[] args = cmd.getArgs(); + + // Create the shell instance + InstancedConfiguration conf = new InstancedConfiguration(ConfigurationUtils.defaults()); + + // Reduce the RPC retry max duration to fail earlier for CLIs + conf.set(PropertyKey.USER_RPC_RETRY_MAX_DURATION, "5s", Source.DEFAULT); + CollectInfo shell = new CollectInfo(conf); + + // Validate command args + if (args.length < 2) { + System.out.format("Command %s requires at least %s arguments (%s provided)%n", + 2, argv.length); + shell.printUsage(); + System.exit(-1); + } + + // Choose mode based on option + int ret; + if (cmd.hasOption(LOCAL_OPTION_NAME)) { + System.out.println("Executing collectInfo locally"); + ret = shell.collectInfoLocal(cmd); + } else { + System.out.println("Executing collectInfo on all nodes in the cluster"); + ret = shell.collectInfoRemote(cmd); + } + + // Clean up before exiting + shell.close(); + System.exit(ret); + } + + /** + * Finds all nodes in the cluster. + * Then invokes collectInfo with --local option on each of them locally. + * Collects the generated tarball from each node. + * And generates a final tarball as the result. + * + * @param cmdLine the parsed CommandLine + * @return exit code + * */ + private int collectInfoRemote(CommandLine cmdLine) throws IOException { + int ret = 0; + String[] args = cmdLine.getArgs(); + String targetDir = args[1]; + + // Execute the command on all hosts + List allHosts = new ArrayList<>(getHosts()); + System.out.format("Init thread pool for %s hosts%n", allHosts.size()); + int threadNum = allHosts.size(); + if (cmdLine.hasOption("threads")) { + int maxThreadNum = Integer.parseInt(cmdLine.getOptionValue(MAX_THREAD_OPTION_NAME)); + LOG.info("Max thread number is {}", maxThreadNum); + threadNum = Math.min(maxThreadNum, threadNum); + } + LOG.info("Use {} threads", threadNum); + mExecutor = Executors.newFixedThreadPool(threadNum); + + // Invoke collectInfo locally on each host + List> sshFutureList = new ArrayList<>(); + for (String host : allHosts) { + System.out.format("Execute collectInfo on host %s%n", host); + + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + // We make the assumption that the Alluxio WORK_DIR is the same + String workDir = mConfiguration.get(PropertyKey.WORK_DIR); + String alluxioBinPath = Paths.get(workDir, "bin/alluxio") + .toAbsolutePath().toString(); + System.out.format("host: %s, alluxio path %s%n", host, alluxioBinPath); + + String[] collectInfoArgs = + (String[]) ArrayUtils.addAll( + new String[]{alluxioBinPath, "collectInfo", "--local"}, args); + try { + CommandReturn cr = ShellUtils.sshExecCommandWithOutput(host, collectInfoArgs); + return cr; + } catch (Exception e) { + LOG.error("Execution failed %s", e); + return new CommandReturn(1, collectInfoArgs, e.toString()); + } + }, mExecutor); + sshFutureList.add(future); + System.out.format("Invoked local collectInfo command on host %s%n", host); + } + + // Collect SSH execution results + List sshSucceededHosts = + collectCommandReturnsFromHosts(sshFutureList, allHosts); + + // If all executions failed, skip the next step + if (sshSucceededHosts.size() == 0) { + System.err.println("Failed to invoke local collectInfo command on all hosts!"); + return 1; + } + + // Collect tarballs from where the SSH command completed + File tempDir = Files.createTempDir(); + List filesFromHosts = new ArrayList<>(); + List> scpFutures = + new ArrayList<>(allHosts.size()); + for (String host : sshSucceededHosts) { + // Create dir for the host + File tarballFromHost = new File(tempDir, host); + tarballFromHost.mkdir(); + filesFromHosts.add(tarballFromHost); + + // Async execute the SCP step + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + System.out.format("Collecting tarball from host %s%n", host); + String fromPath = Paths.get(targetDir, CollectInfo.TARBALL_NAME) + .toAbsolutePath().toString(); + String toPath = tarballFromHost.getAbsolutePath(); + LOG.debug("Copying %s:%s to %s", host, fromPath, toPath); + + try { + CommandReturn cr = + ShellUtils.scpCommandWithOutput(host, fromPath, toPath, false); + return cr; + } catch (IOException e) { + // An unexpected error occurred that caused this IOException + LOG.error("Execution failed %s", e); + return new CommandReturn(1, e.toString()); + } + }, mExecutor); + scpFutures.add(future); + } + + List scpSucceededHosts = + collectCommandReturnsFromHosts(scpFutures, sshSucceededHosts); + System.out.format("Tarballs of %d/%d hosts copied%n", + scpSucceededHosts.size(), allHosts.size()); + + // If all executions failed, clean up and exit + if (scpSucceededHosts.size() == 0) { + System.err.println("Failed to collect tarballs from all hosts!"); + return 2; + } + + // Generate a final tarball containing tarballs from each host + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); + String finalTarballpath = Paths.get(targetDir, + String.format(FINAL_TARBALL_NAME, dtf.format(LocalDateTime.now()))) + .toAbsolutePath().toString(); + TarUtils.compress(finalTarballpath, filesFromHosts.toArray(new File[0])); + System.out.println("Final tarball compressed to " + finalTarballpath); + + // Delete the temp dir + try { + FileUtils.delete(tempDir.getPath()); + } catch (IOException e) { + LOG.warn("Failed to delete temp dir {}", tempDir.toString()); + } + + return ret; + } + + /** + * Executes collectInfo command locally. + * And generates a tarball with all the information collected. + * + * @param cmdLine the parsed CommandLine + * @return exit code + * */ + private int collectInfoLocal(CommandLine cmdLine) throws IOException { + int ret = 0; + String[] args = cmdLine.getArgs(); + + // Determine the command and working dir path + String subCommand = args[0]; + String targetDirPath = args[1]; + + // There are 2 cases: + // 1. Execute "all" commands + // 2. Execute a single command + List filesToCollect = new ArrayList<>(); + if (subCommand.equals("all")) { + // Case 1. Execute "all" commands + System.out.println("Execute all child commands"); + String[] childArgs = Arrays.copyOf(args, args.length); + for (Command cmd : getCommands()) { + System.out.format("Executing %s%n", cmd.getCommandName()); + + // TODO(jiacheng): phase 2 handle argv difference? + // Replace the action with the command to execute + childArgs[0] = cmd.getCommandName(); + int childRet = executeAndAddFile(childArgs, filesToCollect); + + // If any of the commands failed, treat as failed + if (ret == 0 && childRet != 0) { + System.err.format("Command %s failed%n", cmd.getCommandName()); + ret = childRet; + } + } + } else { + // Case 2. Execute a single command + int childRet = executeAndAddFile(args, filesToCollect); + if (ret == 0 && childRet != 0) { + ret = childRet; + } + } + + // TODO(jiacheng): phase 2 add an option to disable bundle + // Generate bundle + System.out.format("Archiving dir %s%n", targetDirPath); + + String tarballPath = Paths.get(targetDirPath, TARBALL_NAME).toAbsolutePath().toString(); + if (filesToCollect.size() == 0) { + System.err.format("No files to add. Tarball %s will be empty!%n", tarballPath); + return 2; + } + TarUtils.compress(tarballPath, filesToCollect.toArray(new File[0])); + System.out.println("Archiving finished"); + + return ret; + } + + private int executeAndAddFile(String[] argv, List filesToCollect) throws IOException { + // The argv length has been validated + String subCommand = argv[0]; + String targetDirPath = argv[1]; + + AbstractCollectInfoCommand cmd = this.findCommand(subCommand); + + if (cmd == null) { + // Unknown command (we did not find the cmd in our dict) + System.err.format("%s is an unknown command.%n", subCommand); + printUsage(); + return 1; + } + int ret = run(argv); + + // File to collect + File infoCmdOutputFile = cmd.generateOutputFile(targetDirPath, + cmd.getCommandName()); + filesToCollect.add(infoCmdOutputFile); + + return ret; + } + + private AbstractCollectInfoCommand findCommand(String cmdName) { + for (Command c : this.getCommands()) { + if (c.getCommandName().equals(cmdName)) { + return (AbstractCollectInfoCommand) c; + } + } + return null; + } + + /** + * Collects the results of ALL futures from the hosts. + * Returns the list of hosts where the execution was successful, + * for the next step. + * */ + private List collectCommandReturnsFromHosts( + List> futureList, List hosts) { + // Collect the execution results + List results; + try { + results = collectAllFutures(futureList).get(); + System.out.format("Results collected from %d hosts%n", results.size()); + } catch (InterruptedException | ExecutionException e) { + System.err.format("Failed to collect the results. Error is %s%n", e.getMessage()); + LOG.error("Error: %s", e); + return Collections.EMPTY_LIST; + } + + // Record the list of hosts where the results are successfully collected + if (results.size() != hosts.size()) { + System.out.format("Error occurred while collecting information on %d/%d hosts%n", + hosts.size() - results.size()); + // TODO(jiacheng): phase 2 find out what error occurred + return Collections.EMPTY_LIST; + } else { + List successfulHosts = new ArrayList<>(); + for (int i = 0; i < hosts.size(); i++) { + CommandReturn c = results.get(i); + String host = hosts.get(i); + if (c.getExitCode() != 0) { + System.out.format("Execution failed on host %s%n", host); + System.out.println(c.getFormattedOutput()); + continue; + } + successfulHosts.add(host); + } + System.out.format("Command executed successfully on %d/%d hosts.", + successfulHosts.size(), hosts.size()); + return successfulHosts; + } + } + /** + * Waits for ALL futures to complete and returns a list of results. + * If any future completes exceptionally then the resulting future + * will also complete exceptionally. + * + * @param this is the type the {@link CompletableFuture} contains + * @param futures a list of futures to collect + * @return a {@link CompletableFuture} of all futures + */ + public static CompletableFuture> collectAllFutures( + List> futures) { + CompletableFuture[] cfs = futures.toArray(new CompletableFuture[futures.size()]); + + return CompletableFuture.allOf(cfs) + .thenApply(f -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()) + ); + } + + @Override + protected String getShellName() { + return "collectInfo"; + } + + @Override + protected Map loadCommands() { + // Give each command the configuration + Map commands = CommandUtils.loadCommands( + CollectInfo.class.getPackage().getName(), + new Class[] {FileSystemContext.class}, + new Object[] {FileSystemContext.create(mConfiguration)}); + return commands; + } + + @Override + public void close() throws IOException { + super.close(); + // Shutdown thread pool if not empty + if (mExecutor != null && !mExecutor.isShutdown()) { + mExecutor.shutdownNow(); + } + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/TarUtils.java b/shell/src/main/java/alluxio/cli/bundler/TarUtils.java new file mode 100644 index 000000000000..76c715b4db1b --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/TarUtils.java @@ -0,0 +1,100 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.utils.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Utilities for generating .tar.gz files. + * + * Ref: https://memorynotfound.com/java-tar-example-compress-decompress-tar-tar-gz-files/ + * */ +public class TarUtils { + /** + * Compresses a list of files to one destination. + * + * @param tarballName destination path of the .tar.gz file + * @param files a list of files to add to the tarball + * */ + public static void compress(String tarballName, File... files) throws IOException { + try (TarArchiveOutputStream out = getTarArchiveOutputStream(tarballName)) { + for (File file : files) { + addToArchiveCompression(out, file, "."); + } + } + } + + /** + * Decompresses a tarball to one destination. + * + * @param in the input file path + * @param out destination to decompress files to + * */ + public static void decompress(String in, File out) throws IOException { + try (TarArchiveInputStream fin = + new TarArchiveInputStream(new GzipCompressorInputStream(new FileInputStream(in)))) { + TarArchiveEntry entry; + while ((entry = fin.getNextTarEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + File curfile = new File(out, entry.getName()); + File parent = curfile.getParentFile(); + if (!parent.exists()) { + parent.mkdirs(); + } + IOUtils.copy(fin, new FileOutputStream(curfile)); + } + } + } + + private static void addToArchiveCompression(TarArchiveOutputStream out, File file, String dir) + throws IOException { + String entry = dir + File.separator + file.getName(); + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null && children.length > 0) { + for (File child : children) { + addToArchiveCompression(out, child, entry); + } + } + } else { + out.putArchiveEntry(new TarArchiveEntry(file, entry)); + try (FileInputStream in = new FileInputStream(file)) { + IOUtils.copy(in, out); + } + out.closeArchiveEntry(); + } + } + + private static TarArchiveOutputStream getTarArchiveOutputStream(String path) throws IOException { + // Generate tar.gz file + TarArchiveOutputStream taos = + new TarArchiveOutputStream(new GzipCompressorOutputStream(new FileOutputStream(path))); + // TAR has an 8G file limit by default, this gets around that + taos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR); + // TAR originally does not support long file names, enable the support for it + taos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU); + taos.setAddPaxHeadersForNonAsciiNames(true); + return taos; + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/command/AbstractCollectInfoCommand.java b/shell/src/main/java/alluxio/cli/bundler/command/AbstractCollectInfoCommand.java new file mode 100644 index 000000000000..7350714d239c --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/command/AbstractCollectInfoCommand.java @@ -0,0 +1,83 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import alluxio.cli.Command; +import alluxio.cli.CommandUtils; +import alluxio.client.file.FileSystemContext; +import alluxio.exception.status.InvalidArgumentException; + +import org.apache.commons.cli.CommandLine; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +/** + * Abstraction of a command under CollectInfo. + * */ +public abstract class AbstractCollectInfoCommand implements Command { + private static final Logger LOG = LoggerFactory.getLogger(AbstractCollectInfoCommand.class); + + protected FileSystemContext mFsContext; + protected String mWorkingDirPath; + + /** + * Creates an instance of {@link AbstractCollectInfoCommand}. + * + * @param fsContext {@link FileSystemContext} the context to run in + * */ + public AbstractCollectInfoCommand(FileSystemContext fsContext) { + mFsContext = fsContext; + } + + @Override + public void validateArgs(CommandLine cl) throws InvalidArgumentException { + CommandUtils.checkNumOfArgsEquals(this, cl, 1); + } + + /** + * Gets the directory that this command should output to. + * Creates the directory if it does not exist. + * + * @param cl the parsed {@link CommandLine} + * @return the directory path + * */ + public String getWorkingDirectory(CommandLine cl) { + String[] args = cl.getArgs(); + String baseDirPath = args[0]; + String workingDirPath = Paths.get(baseDirPath, this.getCommandName()).toString(); + LOG.debug("Command %s works in %s", this.getCommandName(), workingDirPath); + // mkdirs checks existence of the path + File workingDir = new File(workingDirPath); + workingDir.mkdirs(); + return workingDirPath; + } + + /** + * Generates the output file for the command to write printouts to. + * + * @param workingDirPath the base directory this command should output to + * @param fileName name of the output file + * @return the output file + * */ + public File generateOutputFile(String workingDirPath, String fileName) throws IOException { + String outputFilePath = Paths.get(workingDirPath, fileName).toString(); + File outputFile = new File(outputFilePath); + if (!outputFile.exists()) { + outputFile.createNewFile(); + } + return outputFile; + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/command/CollectAlluxioInfoCommand.java b/shell/src/main/java/alluxio/cli/bundler/command/CollectAlluxioInfoCommand.java new file mode 100644 index 000000000000..ad0ad0be046b --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/command/CollectAlluxioInfoCommand.java @@ -0,0 +1,106 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import alluxio.client.file.FileSystemContext; +import alluxio.conf.PropertyKey; +import alluxio.shell.ShellCommand; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Paths; + +/** + * Command to run a set of Alluxio commands. + * Collects information about the Alluxio cluster. + * */ +public class CollectAlluxioInfoCommand extends ExecuteShellCollectInfoCommand { + public static final String COMMAND_NAME = "collectAlluxioInfo"; + private static final Logger LOG = LoggerFactory.getLogger(CollectAlluxioInfoCommand.class); + + private String mAlluxioPath; + + /** + * Creates a new instance of {@link CollectAlluxioInfoCommand}. + * + * @param fsContext the {@link FileSystemContext} to execute in + * */ + public CollectAlluxioInfoCommand(FileSystemContext fsContext) { + super(fsContext); + mAlluxioPath = Paths.get(fsContext.getClusterConf().get(PropertyKey.WORK_DIR), "bin/alluxio") + .toAbsolutePath().toString(); + registerCommands(); + } + + /** + * A special shell command that runs an Alluxio cmdline operation. + * */ + public static class AlluxioCommand extends ShellCommand { + /** + * Creates an instance of {@link AlluxioCommand}. + * + * @param alluxioPath where Alluxio can be found + * @param cmd Alluxio cmd to run + */ + public AlluxioCommand(String alluxioPath, String cmd) { + super((alluxioPath + " " + cmd).split(" ")); + } + } + + @Override + protected void registerCommands() { + // TODO(jiacheng): a command to find lost blocks? + registerCommand("getConf", + new AlluxioCommand(mAlluxioPath, "getConf --master --source"), null); + registerCommand("fsadmin", + new AlluxioCommand(mAlluxioPath, "fsadmin report"), null); + registerCommand("mount", + new AlluxioCommand(mAlluxioPath, "fs mount"), null); + registerCommand("version", + new AlluxioCommand(mAlluxioPath, "version -r"), null); + registerCommand("job", + new AlluxioCommand(mAlluxioPath, "job ls"), null); + registerCommand("journal", + new AlluxioCommand(mAlluxioPath, String.format("fs ls -R %s", + mFsContext.getClusterConf().get(PropertyKey.MASTER_JOURNAL_FOLDER))), + getListJournalCommand()); + } + + /** + * Determine how to list the journal based on the type of UFS. + * TODO(jiacheng): phase 2 support smarter detection + * */ + private ShellCommand getListJournalCommand() { + String journalPath = mFsContext.getClusterConf().get(PropertyKey.MASTER_JOURNAL_FOLDER); + if (journalPath.startsWith("hdfs:")) { + return new ShellCommand(new String[]{"hdfs", "dfs", "-ls", "-R", journalPath}); + } else { + return new ShellCommand(new String[]{"ls", "-al", "-R", journalPath}); + } + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public String getUsage() { + return "collectAlluxioInfo "; + } + + @Override + public String getDescription() { + return "Run a list of Alluxio commands to collect Alluxio cluster information"; + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/command/CollectConfigCommand.java b/shell/src/main/java/alluxio/cli/bundler/command/CollectConfigCommand.java new file mode 100644 index 000000000000..90ad6a2ba072 --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/command/CollectConfigCommand.java @@ -0,0 +1,72 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import alluxio.client.file.FileSystemContext; +import alluxio.conf.PropertyKey; +import alluxio.exception.AlluxioException; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +/** + * Command to collect Alluxio config files. + * */ +public class CollectConfigCommand extends AbstractCollectInfoCommand { + public static final String COMMAND_NAME = "collectConfig"; + private static final Logger LOG = LoggerFactory.getLogger(CollectConfigCommand.class); + + /** + * Creates a new instance of {@link CollectConfigCommand}. + * + * @param fsContext the {@link FileSystemContext} to execute in + * */ + public CollectConfigCommand(FileSystemContext fsContext) { + super(fsContext); + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public boolean hasSubCommand() { + return false; + } + + @Override + public int run(CommandLine cl) throws AlluxioException, IOException { + mWorkingDirPath = getWorkingDirectory(cl); + String confDir = mFsContext.getClusterConf().get(PropertyKey.CONF_DIR); + + // TODO(jiacheng): phase 2 copy intelligently, check security risks + FileUtils.copyDirectory(new File(confDir), new File(mWorkingDirPath), true); + + return 0; + } + + @Override + public String getUsage() { + return "collectConfig "; + } + + @Override + public String getDescription() { + return "Collect Alluxio configurations files"; + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/command/CollectEnvCommand.java b/shell/src/main/java/alluxio/cli/bundler/command/CollectEnvCommand.java new file mode 100644 index 000000000000..93c9e857518e --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/command/CollectEnvCommand.java @@ -0,0 +1,89 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import alluxio.client.file.FileSystemContext; +import alluxio.conf.PropertyKey; +import alluxio.shell.ShellCommand; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Command to run a set of bash commands to get system information. + * */ +public class CollectEnvCommand extends ExecuteShellCollectInfoCommand { + public static final String COMMAND_NAME = "collectEnv"; + private static final Logger LOG = LoggerFactory.getLogger(CollectEnvCommand.class); + + /** + * Creates a new instance of {@link CollectEnvCommand}. + * + * @param fsContext the {@link FileSystemContext} to execute in + * */ + public CollectEnvCommand(FileSystemContext fsContext) { + super(fsContext); + registerCommands(); + } + + @Override + protected void registerCommands() { + registerCommand("Alluxio ps", + new ShellCommand(new String[]{"bash", "-c", "'ps -ef | grep alluxio'"}), null); + registerCommand("Spark ps", + new ShellCommand(new String[]{"bash", "-c", "'ps -ef | grep spark'"}), null); + registerCommand("Yarn ps", + new ShellCommand(new String[]{"bash", "-c", "'ps -ef | grep yarn'"}), null); + registerCommand("Hdfs ps", + new ShellCommand(new String[]{"bash", "-c", "'ps -ef | grep hdfs'"}), null); + registerCommand("Presto ps", + new ShellCommand(new String[]{"bash", "-c", "'ps -ef | grep presto'"}), null); + registerCommand("env", + new ShellCommand(new String[]{"env"}), null); + registerCommand("top", new ShellCommand(new String[]{"atop", "-b", "-n", "1"}), + new ShellCommand(new String[]{"top", "-b", "-n", "1"})); + registerCommand("mount", new ShellCommand(new String[]{"mount"}), null); + registerCommand("df", new ShellCommand(new String[]{"df", "-H"}), null); + registerCommand("ulimit", new ShellCommand(new String[]{"ulimit -Ha"}), null); + registerCommand("uname", new ShellCommand(new String[]{"uname", "-a"}), null); + registerCommand("hostname", new ShellCommand(new String[]{"hostname"}), null); + registerCommand("host ip", new ShellCommand(new String[]{"hostname", "-i"}), null); + registerCommand("host fqdn", new ShellCommand(new String[]{"hostname", "-f"}), null); + registerCommand("list Alluxio home", + new ShellCommand(new String[]{String.format("ls -al -R %s", + mFsContext.getClusterConf().get(PropertyKey.HOME))}), null); + registerCommand("dig", new ShellCommand(new String[]{"dig $(hostname -i)"}), null); + registerCommand("nslookup", new ShellCommand(new String[]{"nslookup", "$(hostname -i)"}), null); + // TODO(jiacheng): does this stop? + registerCommand("dstat", new ShellCommand(new String[]{"dstat", "-cdgilmnprsty"}), null); + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public boolean hasSubCommand() { + return false; + } + + @Override + public String getUsage() { + return "collectEnv "; + } + + @Override + public String getDescription() { + return "Collect environment information by running a set of shell commands. "; + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/command/CollectJvmInfoCommand.java b/shell/src/main/java/alluxio/cli/bundler/command/CollectJvmInfoCommand.java new file mode 100644 index 000000000000..6afb738b1c59 --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/command/CollectJvmInfoCommand.java @@ -0,0 +1,158 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import alluxio.client.file.FileSystemContext; +import alluxio.exception.AlluxioException; +import alluxio.shell.CommandReturn; +import alluxio.shell.ShellCommand; +import alluxio.util.ShellUtils; +import alluxio.util.SleepUtils; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Command that collects information about the JVMs. + * */ +public class CollectJvmInfoCommand extends AbstractCollectInfoCommand { + public static final String COMMAND_NAME = "collectJvmInfo"; + private static final Logger LOG = LoggerFactory.getLogger(CollectJvmInfoCommand.class); + private static final int COLLECT_JSTACK_TIMES = 3; + private static final int COLLECT_JSTACK_INTERVAL = 3 * 1000; + + /** + * Creates an instance of {@link CollectJvmInfoCommand}. + * + * @param fsContext the {@link FileSystemContext} to execute in + * */ + public CollectJvmInfoCommand(FileSystemContext fsContext) { + super(fsContext); + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public int run(CommandLine cl) throws AlluxioException, IOException { + // Determine the working dir path + mWorkingDirPath = getWorkingDirectory(cl); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + for (int i = 0; i < COLLECT_JSTACK_TIMES; i++) { + // Use time as output file name + LocalDateTime now = LocalDateTime.now(); + String timeString = dtf.format(now); + LOG.info(String.format("Collecting JVM info at %s", timeString)); + + LOG.info("Checking current JPS"); + Map procs = getJps(); + String jstackContent = dumpJstack(procs); + + File outputFile = generateOutputFile(mWorkingDirPath, + String.format("%s-%s", getCommandName(), timeString)); + FileUtils.writeStringToFile(outputFile, jstackContent); + + // Interval + LOG.info(String.format("Wait for an interval of %s seconds", COLLECT_JSTACK_INTERVAL)); + SleepUtils.sleepMs(LOG, COLLECT_JSTACK_INTERVAL); + } + + return 0; + } + + private Map getJps() throws IOException { + Map procs = new HashMap<>(); + + // Attempt to sudo jps all existing JVMs + ShellCommand sudoJpsCommand = new ShellCommand(new String[]{"sudo", "jps"}); + ShellCommand jpsCommand = new ShellCommand(new String[]{"jps"}); + + CommandReturn cr = ShellUtils.execCmdWithBackup(sudoJpsCommand, jpsCommand); + if (cr.getExitCode() != 0) { + LOG.warn(cr.getFormattedOutput()); + return procs; + } + + LOG.info("JPS succeeded"); + int idx = 0; + for (String row : cr.getOutput().split("\n")) { + String[] parts = row.split(" "); + if (parts.length == 0) { + LOG.error(String.format("Failed to parse row %s", row)); + continue; + } else if (parts.length == 1) { + // If the JVM has no name, assign it one. + LOG.info(String.format("Row %s has no process name", row)); + procs.put(parts[0], "unknown" + idx); + idx++; + } else { + LOG.info(String.format("Found JVM %s %s", parts[0], parts[1])); + procs.put(parts[0], parts[1]); + } + } + + return procs; + } + + private String dumpJstack(Map procs) throws IOException { + StringWriter outputBuffer = new StringWriter(); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + LocalDateTime now = LocalDateTime.now(); + String timeString = dtf.format(now); + String timeMsg = String.format("Dumping jstack at approximately %s", timeString); + LOG.info(timeMsg); + outputBuffer.write(timeMsg); + + for (Map.Entry entry : procs.entrySet()) { + String pid = entry.getKey(); + String pname = entry.getValue(); + String jstackMsg = String.format("Jstack PID:%s Name:%s", pid, pname); + LOG.info(jstackMsg); + outputBuffer.write(jstackMsg); + + // If normal jstack fails, attemp to sudo jstack this process + ShellCommand jstackCmd = new ShellCommand(new String[]{"jstack", pid}); + ShellCommand sudoJstackCmd = new ShellCommand(new String[]{"sudo", "jstack", pid}); + CommandReturn cr = ShellUtils.execCmdWithBackup(jstackCmd, sudoJstackCmd); + LOG.info("{} finished", Arrays.toString(cr.getCmd())); + outputBuffer.write(cr.getFormattedOutput()); + } + + LOG.info("Jstack dump finished on all processes"); + return outputBuffer.toString(); + } + + @Override + public String getUsage() { + return "collectJvmInfo "; + } + + @Override + public String getDescription() { + return "Collect JVM information by collecting jstack"; + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/command/CollectLogCommand.java b/shell/src/main/java/alluxio/cli/bundler/command/CollectLogCommand.java new file mode 100644 index 000000000000..efc636cd89f0 --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/command/CollectLogCommand.java @@ -0,0 +1,74 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import alluxio.client.file.FileSystemContext; +import alluxio.conf.PropertyKey; +import alluxio.exception.AlluxioException; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +/** + * Command to collect Alluxio logs. + * */ +public class CollectLogCommand extends AbstractCollectInfoCommand { + public static final String COMMAND_NAME = "collectLog"; + private static final Logger LOG = LoggerFactory.getLogger(CollectLogCommand.class); + + /** + * Creates a new instance of {@link CollectLogCommand}. + * + * @param fsContext the {@link FileSystemContext} to execute in + * */ + public CollectLogCommand(FileSystemContext fsContext) { + super(fsContext); + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public boolean hasSubCommand() { + return false; + } + + @Override + public int run(CommandLine cl) throws AlluxioException, IOException { + // Determine the working dir path + mWorkingDirPath = getWorkingDirectory(cl); + String logDir = mFsContext.getClusterConf().get(PropertyKey.LOGS_DIR); + + // TODO(jiacheng): phase 2 Copy intelligently find security risks + // TODO(jiacheng): phase 2 components option + FileUtils.copyDirectory(new File(logDir), new File(mWorkingDirPath), true); + + return 0; + } + + @Override + public String getUsage() { + return "collectLogs "; + } + + @Override + public String getDescription() { + return "Collect Alluxio log files"; + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/command/CollectMetricsCommand.java b/shell/src/main/java/alluxio/cli/bundler/command/CollectMetricsCommand.java new file mode 100644 index 000000000000..906ce0372755 --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/command/CollectMetricsCommand.java @@ -0,0 +1,131 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import alluxio.client.file.FileSystemContext; +import alluxio.conf.PropertyKey; +import alluxio.exception.AlluxioException; +import alluxio.exception.status.UnavailableException; +import alluxio.util.SleepUtils; +import alluxio.util.network.HttpUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Command to probe Alluxio metrics for a few times. + * */ +public class CollectMetricsCommand extends AbstractCollectInfoCommand { + public static final String COMMAND_NAME = "collectMetrics"; + private static final Logger LOG = LoggerFactory.getLogger(CollectMetricsCommand.class); + private static final int COLLECT_METRICS_INTERVAL = 3 * 1000; + private static final int COLLECT_METRICS_TIMES = 3; + private static final int COLLECT_METRICS_TIMEOUT = 5 * 1000; + private static final String METRICS_SERVLET_PATH = "/metrics/json/"; + + /** + * Creates a new instance of {@link CollectMetricsCommand}. + * + * @param fsContext the {@link FileSystemContext} to execute in + * */ + public CollectMetricsCommand(FileSystemContext fsContext) { + super(fsContext); + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public boolean hasSubCommand() { + return false; + } + + @Override + public int run(CommandLine cl) throws AlluxioException, IOException { + // Determine the working dir path + mWorkingDirPath = getWorkingDirectory(cl); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + StringWriter outputBuffer = new StringWriter(); + for (int i = 0; i < COLLECT_METRICS_TIMES; i++) { + LocalDateTime now = LocalDateTime.now(); + String timeMsg = String.format("Collecting metrics at %s", dtf.format(now)); + LOG.info(timeMsg); + outputBuffer.write(timeMsg); + + // Generate URL from config properties + String masterAddr; + try { + masterAddr = mFsContext.getMasterAddress().getHostName(); + } catch (UnavailableException e) { + String noMasterMsg = "No Alluxio master available. Skip metrics collection."; + LOG.warn(noMasterMsg); + outputBuffer.write(noMasterMsg); + break; + } + String url = String.format("http://%s:%s%s", masterAddr, + mFsContext.getClusterConf().get(PropertyKey.MASTER_WEB_PORT), + METRICS_SERVLET_PATH); + LOG.info(String.format("Metric address URL: %s", url)); + + // Get metrics + String metricsResponse = getMetricsJson(url); + outputBuffer.write(metricsResponse); + + // Write to file + File outputFile = generateOutputFile(mWorkingDirPath, + String.format("%s-%s", getCommandName(), i)); + FileUtils.writeStringToFile(outputFile, metricsResponse); + + // Wait for an interval + SleepUtils.sleepMs(LOG, COLLECT_METRICS_INTERVAL); + } + + // TODO(jiacheng): phase 2 consider outputting partial results in a finally block + File outputFile = generateOutputFile(mWorkingDirPath, + String.format("%s.txt", getCommandName())); + FileUtils.writeStringToFile(outputFile, outputBuffer.toString()); + + return 0; + } + + @Override + public String getUsage() { + return "collectMetrics "; + } + + @Override + public String getDescription() { + return "Collect Alluxio metrics"; + } + + /** + * Probes Alluxio metrics json sink. + * + * @param url URL that serves Alluxio metrics + * @return HTTP response in JSON string + */ + public String getMetricsJson(String url) throws IOException { + String responseJson = HttpUtils.get(url, COLLECT_METRICS_TIMEOUT); + return String.format("Url: %s%nResponse: %s", url, responseJson); + } +} diff --git a/shell/src/main/java/alluxio/cli/bundler/command/ExecuteShellCollectInfoCommand.java b/shell/src/main/java/alluxio/cli/bundler/command/ExecuteShellCollectInfoCommand.java new file mode 100644 index 000000000000..ea2e510ecd17 --- /dev/null +++ b/shell/src/main/java/alluxio/cli/bundler/command/ExecuteShellCollectInfoCommand.java @@ -0,0 +1,96 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import alluxio.client.file.FileSystemContext; +import alluxio.exception.AlluxioException; +import alluxio.shell.CommandReturn; +import alluxio.shell.ShellCommand; +import alluxio.util.ShellUtils; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * Command to run a set of shell commands to get system information. + * */ +public abstract class ExecuteShellCollectInfoCommand extends AbstractCollectInfoCommand { + private static final Logger LOG = LoggerFactory.getLogger(ExecuteShellCollectInfoCommand.class); + + protected Map mCommands; + protected Map mCommandsAlt; + + /** + * Creates a new instance of {@link ExecuteShellCollectInfoCommand}. + * + * @param fsContext the {@link FileSystemContext} to execute in + * */ + public ExecuteShellCollectInfoCommand(FileSystemContext fsContext) { + super(fsContext); + mCommands = new HashMap<>(); + mCommandsAlt = new HashMap<>(); + } + + protected abstract void registerCommands(); + + // TODO(jiacheng): phase 2 refactor to use a chainable ShellCommand structure + protected void registerCommand(String name, ShellCommand cmd, ShellCommand alternativeCmd) { + mCommands.put(name, cmd); + if (alternativeCmd != null) { + mCommandsAlt.put(name, alternativeCmd); + } + } + + @Override + public int run(CommandLine cl) throws AlluxioException, IOException { + // Determine the working dir path + mWorkingDirPath = getWorkingDirectory(cl); + + // Output buffer stream + StringWriter outputBuffer = new StringWriter(); + + for (Map.Entry entry : mCommands.entrySet()) { + String cmdName = entry.getKey(); + ShellCommand cmd = entry.getValue(); + ShellCommand backupCmd = mCommandsAlt.getOrDefault(cmdName, null); + CommandReturn cr = ShellUtils.execCmdWithBackup(cmd, backupCmd); + outputBuffer.write(cr.getFormattedOutput()); + + // If neither command works, log a warning instead of returning with error state + // This is because a command can err due to: + // 1. The executable does not exist. This results in an IOException. + // 2. The command is not compatible to the system, eg. Mac. + // 3. The command is wrong. + // We choose to tolerate the error state due since we cannot correct state 1 or 2. + if (cr.getExitCode() != 0) { + LOG.warn("Command %s failed with exit code %d", cmdName, cr.getExitCode()); + } + } + + // output the logs + File outputFile = generateOutputFile(mWorkingDirPath, + String.format("%s.txt", getCommandName())); + LOG.info(String.format("Finished all commands. Writing to output file %s", + outputFile.getAbsolutePath())); + FileUtils.writeStringToFile(outputFile, outputBuffer.toString()); + + return 0; + } +} diff --git a/shell/src/test/java/alluxio/cli/bundler/CollectInfoTest.java b/shell/src/test/java/alluxio/cli/bundler/CollectInfoTest.java new file mode 100644 index 000000000000..b94f3e2e7223 --- /dev/null +++ b/shell/src/test/java/alluxio/cli/bundler/CollectInfoTest.java @@ -0,0 +1,49 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler; + +import static org.junit.Assert.assertEquals; + +import alluxio.cli.bundler.command.AbstractCollectInfoCommand; +import alluxio.conf.InstancedConfiguration; +import alluxio.cli.Command; +import alluxio.util.ConfigurationUtils; + +import org.junit.Test; +import org.reflections.Reflections; + +import java.lang.reflect.Modifier; +import java.util.Collection; + +public class CollectInfoTest { + private static InstancedConfiguration sConf = + new InstancedConfiguration(ConfigurationUtils.defaults()); + + private int getNumberOfCommands() { + Reflections reflections = + new Reflections(AbstractCollectInfoCommand.class.getPackage().getName()); + int cnt = 0; + for (Class cls : reflections.getSubTypesOf(Command.class)) { + if (!Modifier.isAbstract(cls.getModifiers())) { + cnt++; + } + } + return cnt; + } + + @Test + public void loadedCommands() { + CollectInfo ic = new CollectInfo(sConf); + Collection commands = ic.getCommands(); + assertEquals(getNumberOfCommands(), commands.size()); + } +} diff --git a/shell/src/test/java/alluxio/cli/bundler/InfoCollectorTestUtils.java b/shell/src/test/java/alluxio/cli/bundler/InfoCollectorTestUtils.java new file mode 100644 index 000000000000..43b34b9dcbe2 --- /dev/null +++ b/shell/src/test/java/alluxio/cli/bundler/InfoCollectorTestUtils.java @@ -0,0 +1,48 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler; + +import com.google.common.io.Files; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +public class InfoCollectorTestUtils { + private static final Logger LOG = LoggerFactory.getLogger(InfoCollectorTestUtils.class); + + public static File createTemporaryDirectory() { + final File file = Files.createTempDir(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + FileUtils.deleteDirectory(file); + } catch (IOException e) { + LOG.warn("Failed to clean up {} : {}", file.getAbsolutePath(), e.toString()); + } + })); + return file; + } + + public static File createFileInDir(File dir, String fileName) throws IOException { + File newFile = new File(Paths.get(dir.getAbsolutePath(), fileName).toString()); + newFile.createNewFile(); + return newFile; + } + + public static void create() { + Files.createTempDir(); + } +} diff --git a/shell/src/test/java/alluxio/cli/bundler/TarUtilsTest.java b/shell/src/test/java/alluxio/cli/bundler/TarUtilsTest.java new file mode 100644 index 000000000000..000bdeacc131 --- /dev/null +++ b/shell/src/test/java/alluxio/cli/bundler/TarUtilsTest.java @@ -0,0 +1,106 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +public class TarUtilsTest { + @Test + public void compressAndDecompressFiles() throws IOException { + // create temp dir + File targetDir = InfoCollectorTestUtils.createTemporaryDirectory(); + + int fileCount = 10; + // create a list of files in the folder + for (int i = 0; i < fileCount; i++) { + File subFile = new File(targetDir, "file_" + i); + subFile.createNewFile(); + } + createFilesInDir(targetDir, 10); + File[] filesToCompress = targetDir.listFiles(); + + // Compress the file + String tarballName = "tarball.tar.gz"; + String gzFilePath = new File(targetDir, tarballName).getAbsolutePath(); + TarUtils.compress(gzFilePath, filesToCompress); + assertTrue(new File(gzFilePath).exists()); + + // Decompress the compressed file + File outputDir = new File(targetDir, "recovered"); + outputDir.mkdir(); + TarUtils.decompress(gzFilePath, outputDir); + + // Verify the recovered files + compareFiles(filesToCompress, outputDir.listFiles()); + } + + @Test + public void compressAndDecompressFilesAndFolder() throws IOException { + // create temp dir + File source1 = InfoCollectorTestUtils.createTemporaryDirectory(); + File source2 = InfoCollectorTestUtils.createTemporaryDirectory(); + File targetDir = InfoCollectorTestUtils.createTemporaryDirectory(); + + createFilesInDir(source1, 10); + createFilesInDir(source2, 10); + File[] filesToCompress = new File[]{source1, source2}; + + // Compress the file + String tarballName = "tarball.tar.gz"; + String gzFilePath = new File(targetDir, tarballName).getAbsolutePath(); + TarUtils.compress(gzFilePath, filesToCompress); + assertTrue(new File(gzFilePath).exists()); + + // Decompress the compressed file + File outputDir = new File(targetDir, "recovered"); + outputDir.mkdir(); + TarUtils.decompress(gzFilePath, outputDir); + + // Verify the recovered files + compareFiles(filesToCompress, outputDir.listFiles()); + } + + private static void createFilesInDir(File dir, int numOfFiles) throws IOException { + int fileCount = numOfFiles; + // create a list of files + for (int i = 0; i < fileCount; i++) { + File subFile = new File(dir, "file_" + i); + subFile.createNewFile(); + } + } + + private static void compareFiles(File[] expected, File[] recovered) { + assertEquals(expected.length, recovered.length); + + Arrays.sort(expected); + Arrays.sort(recovered); + for (int i = 0; i < expected.length; i++) { + assertTrue(recovered[i].exists()); + if (expected[i].isDirectory()) { + assertTrue(recovered[i].isDirectory()); + // recursively compare children + compareFiles(expected[i].listFiles(), recovered[i].listFiles()); + } else { + // compare two files + assertEquals(expected[i].getName(), recovered[i].getName()); + assertEquals(expected[i].length(), recovered[i].length()); + } + } + } +} diff --git a/shell/src/test/java/alluxio/cli/bundler/command/CollectAlluxioInfoCommandTest.java b/shell/src/test/java/alluxio/cli/bundler/command/CollectAlluxioInfoCommandTest.java new file mode 100644 index 000000000000..8cc2f0bdb08f --- /dev/null +++ b/shell/src/test/java/alluxio/cli/bundler/command/CollectAlluxioInfoCommandTest.java @@ -0,0 +1,134 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import alluxio.cli.bundler.InfoCollectorTestUtils; +import alluxio.client.file.FileSystemContext; +import alluxio.conf.InstancedConfiguration; +import alluxio.exception.AlluxioException; +import alluxio.shell.CommandReturn; +import alluxio.shell.ShellCommand; + +import org.apache.commons.cli.CommandLine; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +public class CollectAlluxioInfoCommandTest { + private static InstancedConfiguration sConf; + + @BeforeClass + public static void initConf() { + sConf = InstancedConfiguration.defaults(); + } + + @Test + public void alluxioCmdExecuted() + throws IOException, AlluxioException, NoSuchFieldException, IllegalAccessException { + CollectAlluxioInfoCommand cmd = new CollectAlluxioInfoCommand(FileSystemContext.create(sConf)); + + // Write to temp dir + File targetDir = InfoCollectorTestUtils.createTemporaryDirectory(); + CommandLine mockCommandLine = mock(CommandLine.class); + String[] mockArgs = new String[]{targetDir.getAbsolutePath()}; + when(mockCommandLine.getArgs()).thenReturn(mockArgs); + + // Replace commands to execute + Field f = cmd.getClass().getSuperclass().getDeclaredField("mCommands"); + f.setAccessible(true); + + CollectAlluxioInfoCommand.AlluxioCommand mockCommand = + mock(CollectAlluxioInfoCommand.AlluxioCommand.class); + when(mockCommand.runWithOutput()) + .thenReturn(new CommandReturn(0, "nothing happens")); + Map mockCommandMap = new HashMap<>(); + mockCommandMap.put("mockCommand", mockCommand); + f.set(cmd, mockCommandMap); + + int ret = cmd.run(mockCommandLine); + assertEquals(0, ret); + + // Verify the command has been run + verify(mockCommand).runWithOutput(); + + // Files will be copied to sub-dir of target dir + File subDir = new File(Paths.get(targetDir.getAbsolutePath(), + cmd.getCommandName()).toString()); + assertEquals(new String[]{"collectAlluxioInfo.txt"}, subDir.list()); + + // Verify the command output is found + String fileContent = new String(Files.readAllBytes(subDir.listFiles()[0].toPath())); + assertTrue(fileContent.contains("nothing happens")); + } + + @Test + public void backupCmdExecuted() + throws IOException, AlluxioException, NoSuchFieldException, IllegalAccessException { + CollectAlluxioInfoCommand cmd = new CollectAlluxioInfoCommand(FileSystemContext.create(sConf)); + + // Write to temp dir + File targetDir = InfoCollectorTestUtils.createTemporaryDirectory(); + CommandLine mockCommandLine = mock(CommandLine.class); + String[] mockArgs = new String[]{targetDir.getAbsolutePath()}; + when(mockCommandLine.getArgs()).thenReturn(mockArgs); + + // Replace commands to execute + Field f = cmd.getClass().getSuperclass().getDeclaredField("mCommands"); + f.setAccessible(true); + CollectAlluxioInfoCommand.AlluxioCommand mockCommandFail = + mock(CollectAlluxioInfoCommand.AlluxioCommand.class); + when(mockCommandFail.runWithOutput()).thenReturn( + new CommandReturn(255, "command failed")); + Map mockCommandMap = new HashMap<>(); + mockCommandMap.put("mockCommand", mockCommandFail); + f.set(cmd, mockCommandMap); + + // Replace better command to execute + Field cb = cmd.getClass().getSuperclass().getDeclaredField("mCommandsAlt"); + cb.setAccessible(true); + ShellCommand mockCommandBackup = mock(ShellCommand.class); + when(mockCommandBackup.runWithOutput()).thenReturn( + new CommandReturn(0, "backup command executed")); + Map mockBetterMap = new HashMap<>(); + mockBetterMap.put("mockCommand", mockCommandBackup); + cb.set(cmd, mockBetterMap); + + // The backup command worked so exit code is 0 + int ret = cmd.run(mockCommandLine); + assertEquals(0, ret); + + // Verify the 1st option command failed, then backup executed + verify(mockCommandFail).runWithOutput(); + verify(mockCommandBackup).runWithOutput(); + + // Files will be copied to sub-dir of target dir + File subDir = new File(Paths.get(targetDir.getAbsolutePath(), cmd.getCommandName()).toString()); + assertEquals(new String[]{"collectAlluxioInfo.txt"}, subDir.list()); + + // Verify only the better version command output is found + String fileContent = new String(Files.readAllBytes(subDir.listFiles()[0].toPath())); + assertTrue(fileContent.contains("backup command executed")); + } +} diff --git a/shell/src/test/java/alluxio/cli/bundler/command/CollectConfCommandTest.java b/shell/src/test/java/alluxio/cli/bundler/command/CollectConfCommandTest.java new file mode 100644 index 000000000000..d0f5c20765be --- /dev/null +++ b/shell/src/test/java/alluxio/cli/bundler/command/CollectConfCommandTest.java @@ -0,0 +1,74 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import alluxio.cli.bundler.InfoCollectorTestUtils; +import alluxio.client.file.FileSystemContext; +import alluxio.conf.InstancedConfiguration; +import alluxio.conf.PropertyKey; +import alluxio.exception.AlluxioException; + +import org.apache.commons.cli.CommandLine; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Arrays; + +public class CollectConfCommandTest { + private static InstancedConfiguration sConf; + private static File sTestDir; + + @BeforeClass + public static void initConf() throws IOException { + sTestDir = prepareConfDir(); + sConf = InstancedConfiguration.defaults(); + sConf.set(PropertyKey.CONF_DIR, sTestDir.getAbsolutePath()); + } + + // Prepare a temp dir with some log files + private static File prepareConfDir() throws IOException { + // The dir path will contain randomness so will be different every time + File testConfDir = InfoCollectorTestUtils.createTemporaryDirectory(); + InfoCollectorTestUtils.createFileInDir(testConfDir, "alluxio-site.properties"); + InfoCollectorTestUtils.createFileInDir(testConfDir, "alluxio-env.sh"); + return testConfDir; + } + + @Test + public void confDirCopied() throws IOException, AlluxioException { + CollectConfigCommand cmd = new CollectConfigCommand(FileSystemContext.create(sConf)); + + File targetDir = InfoCollectorTestUtils.createTemporaryDirectory(); + CommandLine mockCommandLine = mock(CommandLine.class); + String[] mockArgs = new String[]{targetDir.getAbsolutePath()}; + when(mockCommandLine.getArgs()).thenReturn(mockArgs); + int ret = cmd.run(mockCommandLine); + Assert.assertEquals(0, ret); + + // Files will be copied to sub-dir of target dir + File subDir = new File(Paths.get(targetDir.getAbsolutePath(), cmd.getCommandName()).toString()); + + // Check the dir copied + String[] files = subDir.list(); + Arrays.sort(files); + String[] expectedFiles = sTestDir.list(); + Arrays.sort(expectedFiles); + Assert.assertEquals(expectedFiles, files); + } +} diff --git a/shell/src/test/java/alluxio/cli/bundler/command/CollectEnvCommandTest.java b/shell/src/test/java/alluxio/cli/bundler/command/CollectEnvCommandTest.java new file mode 100644 index 000000000000..672a89206cbf --- /dev/null +++ b/shell/src/test/java/alluxio/cli/bundler/command/CollectEnvCommandTest.java @@ -0,0 +1,130 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import alluxio.cli.bundler.InfoCollectorTestUtils; +import alluxio.client.file.FileSystemContext; +import alluxio.conf.InstancedConfiguration; +import alluxio.exception.AlluxioException; +import alluxio.shell.CommandReturn; +import alluxio.shell.ShellCommand; + +import org.apache.commons.cli.CommandLine; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +public class CollectEnvCommandTest { + private static InstancedConfiguration sConf; + + @BeforeClass + public static void initConf() { + sConf = InstancedConfiguration.defaults(); + } + + @Test + public void linuxCmdExecuted() + throws IOException, AlluxioException, NoSuchFieldException, IllegalAccessException { + CollectEnvCommand cmd = new CollectEnvCommand(FileSystemContext.create(sConf)); + + // Write to temp dir + File targetDir = InfoCollectorTestUtils.createTemporaryDirectory(); + CommandLine mockCommandLine = mock(CommandLine.class); + String[] mockArgs = new String[]{targetDir.getAbsolutePath()}; + when(mockCommandLine.getArgs()).thenReturn(mockArgs); + + // Replace commands to execute + Field f = cmd.getClass().getSuperclass().getDeclaredField("mCommands"); + f.setAccessible(true); + + ShellCommand mockCommand = mock(ShellCommand.class); + when(mockCommand.runWithOutput()).thenReturn(new CommandReturn(0, "nothing happens")); + Map mockCommandMap = new HashMap<>(); + mockCommandMap.put("mockCommand", mockCommand); + f.set(cmd, mockCommandMap); + + int ret = cmd.run(mockCommandLine); + assertEquals(0, ret); + + // Verify the command has been run + verify(mockCommand).runWithOutput(); + + // Files will be copied to sub-dir of target dir + File subDir = new File(Paths.get(targetDir.getAbsolutePath(), cmd.getCommandName()).toString()); + assertEquals(new String[]{"collectEnv.txt"}, subDir.list()); + + // Verify the command output is found + String fileContent = new String(Files.readAllBytes(subDir.listFiles()[0].toPath())); + assertTrue(fileContent.contains("nothing happens")); + } + + @Test + public void backupCmdExecuted() + throws IOException, AlluxioException, NoSuchFieldException, IllegalAccessException { + CollectEnvCommand cmd = new CollectEnvCommand(FileSystemContext.create(sConf)); + + // Write to temp dir + File targetDir = InfoCollectorTestUtils.createTemporaryDirectory(); + CommandLine mockCommandLine = mock(CommandLine.class); + String[] mockArgs = new String[]{targetDir.getAbsolutePath()}; + when(mockCommandLine.getArgs()).thenReturn(mockArgs); + + // Replace commands to execute + Field f = cmd.getClass().getSuperclass().getDeclaredField("mCommands"); + f.setAccessible(true); + ShellCommand mockCommandFail = mock(ShellCommand.class); + when(mockCommandFail.runWithOutput()).thenReturn( + new CommandReturn(255, "command failed")); + Map mockCommandMap = new HashMap<>(); + mockCommandMap.put("mockCommand", mockCommandFail); + f.set(cmd, mockCommandMap); + + // Replace better command to execute + Field cb = cmd.getClass().getSuperclass().getDeclaredField("mCommandsAlt"); + cb.setAccessible(true); + ShellCommand mockCommandBackup = mock(ShellCommand.class); + when(mockCommandBackup.runWithOutput()).thenReturn( + new CommandReturn(0, "backup command executed")); + Map mockBetterMap = new HashMap<>(); + mockBetterMap.put("mockCommand", mockCommandBackup); + cb.set(cmd, mockBetterMap); + + // The backup command worked so exit code is 0 + int ret = cmd.run(mockCommandLine); + assertEquals(0, ret); + + // Verify the 1st option command failed, then backup executed + verify(mockCommandFail).runWithOutput(); + verify(mockCommandBackup).runWithOutput(); + + // Files will be copied to sub-dir of target dir + File subDir = new File(Paths.get(targetDir.getAbsolutePath(), cmd.getCommandName()).toString()); + assertEquals(new String[]{"collectEnv.txt"}, subDir.list()); + + // Verify only the better version command output is found + String fileContent = new String(Files.readAllBytes(subDir.listFiles()[0].toPath())); + assertTrue(fileContent.contains("backup command executed")); + } +} diff --git a/shell/src/test/java/alluxio/cli/bundler/command/CollectLogCommandTest.java b/shell/src/test/java/alluxio/cli/bundler/command/CollectLogCommandTest.java new file mode 100644 index 000000000000..95e280d7729b --- /dev/null +++ b/shell/src/test/java/alluxio/cli/bundler/command/CollectLogCommandTest.java @@ -0,0 +1,74 @@ +/* + * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 + * (the "License"). You may not use this work except in compliance with the License, which is + * available at www.apache.org/licenses/LICENSE-2.0 + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied, as more fully set forth in the License. + * + * See the NOTICE file distributed with this work for information regarding copyright ownership. + */ + +package alluxio.cli.bundler.command; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import alluxio.cli.bundler.InfoCollectorTestUtils; +import alluxio.client.file.FileSystemContext; +import alluxio.conf.InstancedConfiguration; +import alluxio.conf.PropertyKey; +import alluxio.exception.AlluxioException; + +import org.apache.commons.cli.CommandLine; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Arrays; + +public class CollectLogCommandTest { + private static InstancedConfiguration sConf; + private static File sTestDir; + + @BeforeClass + public static void initConf() throws IOException { + sTestDir = prepareLogDir("testLog"); + sConf = InstancedConfiguration.defaults(); + sConf.set(PropertyKey.LOGS_DIR, sTestDir.getAbsolutePath()); + } + + // Prepare a temp dir with some log files + private static File prepareLogDir(String prefix) throws IOException { + // The dir path will contain randomness so will be different every time + File testConfDir = InfoCollectorTestUtils.createTemporaryDirectory(); + InfoCollectorTestUtils.createFileInDir(testConfDir, "master.log"); + InfoCollectorTestUtils.createFileInDir(testConfDir, "worker.log"); + return testConfDir; + } + + @Test + public void logDirCopied() throws IOException, AlluxioException { + CollectLogCommand cmd = new CollectLogCommand(FileSystemContext.create(sConf)); + + File targetDir = InfoCollectorTestUtils.createTemporaryDirectory(); + CommandLine mockCommandLine = mock(CommandLine.class); + String[] mockArgs = new String[]{targetDir.getAbsolutePath()}; + when(mockCommandLine.getArgs()).thenReturn(mockArgs); + int ret = cmd.run(mockCommandLine); + Assert.assertEquals(0, ret); + + // Files will be copied to sub-dir of target dir + File subDir = new File(Paths.get(targetDir.getAbsolutePath(), cmd.getCommandName()).toString()); + + // Check the dir copied + String[] files = subDir.list(); + Arrays.sort(files); + String[] expectedFiles = sTestDir.list(); + Arrays.sort(expectedFiles); + Assert.assertEquals(expectedFiles, files); + } +}