| /* |
| * 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.ambari.server.utils; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.commons.lang.StringUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| |
| /** |
| * Logs OpenSsl command exit code with description |
| */ |
| public class ShellCommandUtil { |
| private static final Logger LOG = LoggerFactory.getLogger(ShellCommandUtil.class); |
| private static final Object WindowsProcessLaunchLock = new Object(); |
| private static final String PASS_TOKEN = "pass:"; |
| private static final String KEY_TOKEN = "-key "; |
| private static final String AMBARI_SUDO = "ambari-sudo.sh"; |
| |
| private static final int MODE_OWNER_READABLE = 400; |
| private static final int MODE_OWNER_WRITABLE = 200; |
| private static final int MODE_OWNER_EXECUTABLE = 100; |
| private static final int MODE_GROUP_READABLE = 40; |
| private static final int MODE_GROUP_WRITABLE = 20; |
| private static final int MODE_GROUP_EXECUTABLE = 10; |
| private static final int MODE_OTHER_READABLE = 4; |
| private static final int MODE_OTHER_WRITABLE = 2; |
| private static final int MODE_OTHER_EXECUTABLE = 1; |
| |
| /* |
| public static String LogAndReturnOpenSslExitCode(String command, int exitCode) { |
| logOpenSslExitCode(command, exitCode); |
| return getOpenSslCommandResult(command, exitCode); |
| } |
| */ |
| public static void logOpenSslExitCode(String command, int exitCode) { |
| if (exitCode == 0) { |
| LOG.info(getOpenSslCommandResult(command, exitCode)); |
| } else { |
| LOG.warn(getOpenSslCommandResult(command, exitCode)); |
| } |
| |
| } |
| |
| public static String hideOpenSslPassword(String command) { |
| int start; |
| if (command.contains(PASS_TOKEN)) { |
| start = command.indexOf(PASS_TOKEN) + PASS_TOKEN.length(); |
| } else if (command.contains(KEY_TOKEN)) { |
| start = command.indexOf(KEY_TOKEN) + KEY_TOKEN.length(); |
| } else { |
| return command; |
| } |
| CharSequence cs = command.subSequence(start, command.indexOf(" ", start)); |
| return command.replace(cs, "****"); |
| } |
| |
| public static String getOpenSslCommandResult(String command, int exitCode) { |
| return new StringBuilder().append("Command ").append(hideOpenSslPassword(command)).append(" was finished with exit code: ") |
| .append(exitCode).append(" - ").append(getOpenSslExitCodeDescription(exitCode)).toString(); |
| } |
| |
| private static String getOpenSslExitCodeDescription(int exitCode) { |
| switch (exitCode) { |
| case 0: { |
| return "the operation was completely successfully."; |
| } |
| case 1: { |
| return "an error occurred parsing the command options."; |
| } |
| case 2: { |
| return "one of the input files could not be read."; |
| } |
| case 3: { |
| return "an error occurred creating the PKCS#7 file or when reading the MIME message."; |
| } |
| case 4: { |
| return "an error occurred decrypting or verifying the message."; |
| } |
| case 5: { |
| return "the message was verified correctly but an error occurred writing out the signers certificates."; |
| } |
| default: |
| return "unsupported code"; |
| } |
| } |
| |
| |
| /** |
| * Set to true when run on Windows platforms |
| */ |
| public static final boolean WINDOWS |
| = System.getProperty("os.name").startsWith("Windows"); |
| |
| /** |
| * Set to true when run on Linux platforms |
| */ |
| public static final boolean LINUX |
| = System.getProperty("os.name").startsWith("Linux"); |
| |
| /** |
| * Set to true when run on Mac OS platforms |
| */ |
| public static final boolean MAC |
| = System.getProperty("os.name").startsWith("Mac"); |
| |
| /** |
| * Set to true when run if platform is detected to be UNIX compatible |
| */ |
| public static final boolean UNIX_LIKE = LINUX || MAC; |
| |
| /** |
| * Permission mask 600 allows only owner to read and modify file. |
| * Other users (except root) can't read/modify file |
| */ |
| public static final String MASK_OWNER_ONLY_RW = "600"; |
| |
| /** |
| * Permission mask 700 allows only owner to read, modify and execute file. |
| * Other users (except root) can't read/modify/execute file |
| */ |
| public static final String MASK_OWNER_ONLY_RWX = "700"; |
| |
| /** |
| * Permission mask 777 allows everybody to read/modify/execute file |
| */ |
| public static final String MASK_EVERYBODY_RWX = "777"; |
| |
| |
| /** |
| * Gets file permissions on Linux systems. |
| * Under Windows/Mac, command always returns MASK_EVERYBODY_RWX |
| * |
| * @param path |
| */ |
| public static String getUnixFilePermissions(String path) { |
| String result = MASK_EVERYBODY_RWX; |
| if (LINUX) { |
| try { |
| result = runCommand(new String[]{"stat", "-c", "%a", path}).getStdout(); |
| } catch (IOException | InterruptedException e) { |
| // Improbable |
| LOG.warn(String.format("Can not perform stat on %s", path), e); |
| } |
| } else { |
| LOG.debug(String.format("Not performing stat -s \"%%a\" command on file %s " + |
| "because current OS is not Linux. Returning 777", path)); |
| } |
| return result.trim(); |
| } |
| |
| /** |
| * Sets file permissions to a given value on Linux systems. |
| * On Windows/Mac, command is silently ignored |
| * |
| * @param mode |
| * @param path |
| */ |
| public static void setUnixFilePermissions(String mode, String path) { |
| if (LINUX) { |
| try { |
| runCommand(new String[]{"chmod", mode, path}); |
| } catch (IOException | InterruptedException e) { |
| // Improbable |
| LOG.warn(String.format("Can not perform chmod %s %s", mode, path), e); |
| } |
| } else { |
| LOG.debug(String.format("Not performing chmod %s command for file %s " + |
| "because current OS is not Linux ", mode, path)); |
| } |
| } |
| |
| /** |
| * Sets the owner for a file. |
| * |
| * @param path the path to the file |
| * @param ownerName the owner's local username |
| * @return the result of the operation |
| */ |
| public static Result setFileOwner(String path, String ownerName) { |
| if (LINUX) { |
| // Set the file owner, if the owner's username is given |
| if (!StringUtils.isEmpty(ownerName)) { |
| try { |
| return runCommand(new String[]{"chown", ownerName, path}, null, null, true); |
| } catch (IOException | InterruptedException e) { |
| // Improbable |
| LOG.warn(String.format("Can not perform chown %s %s", ownerName, path), e); |
| return new Result(-1, "", "Cannot perform operation: " + e.getLocalizedMessage()); |
| } |
| } else { |
| return new Result(0, "", ""); |
| } |
| } else { |
| LOG.debug(String.format("Not performing chown command for file %s " + |
| "because current OS is not Linux ", path)); |
| return new Result(-1, "", "Cannot perform operation: The current OS is not Linux"); |
| } |
| } |
| |
| /** |
| * Sets the group for a file. |
| * |
| * @param path the path to the file |
| * @param groupName the group name |
| * @return the result of the operation |
| */ |
| public static Result setFileGroup(String path, String groupName) { |
| if (LINUX) { |
| // Set the file's group, if the group name is given |
| if (!StringUtils.isEmpty(groupName)) { |
| try { |
| return runCommand(new String[]{"chgrp", groupName, path}, null, null, true); |
| } catch (IOException | InterruptedException e) { |
| // Improbable |
| LOG.warn(String.format("Can not perform chgrp %s %s", groupName, path), e); |
| return new Result(-1, "", "Cannot perform operation: " + e.getLocalizedMessage()); |
| } |
| } else { |
| return new Result(0, "", ""); |
| } |
| } else { |
| LOG.debug(String.format("Not performing chgrp command for file %s " + |
| "because current OS is not Linux ", path)); |
| return new Result(-1, "", "Cannot perform operation: The current OS is not Linux"); |
| } |
| } |
| |
| /** |
| * Set the access modes for a file |
| * |
| * @param path the path to the file |
| * @param ownerWritable true if the owner should be able to write to this file; otherwise false |
| * @param ownerReadable true if the owner should be able to read this file; otherwise false |
| * @param ownerExecutable true if the owner should be able to execute this file; otherwise false |
| * @param groupWritable true if the group should be able to write to this file; otherwise false |
| * @param groupReadable true if the group should be able to read this file; otherwise false |
| * @param groupExecutable true if the group should be able to execute this file; otherwise false |
| * @param otherReadable true if other users should be able to read this file; otherwise false |
| * @param otherWritable true if other users should be able to write to this file; otherwise false |
| * @param otherExecutable true if other users should be able to execute this file; otherwise false |
| * @return the result of the operation |
| */ |
| public static Result setFileMode(String path, |
| boolean ownerReadable, boolean ownerWritable, boolean ownerExecutable, |
| boolean groupReadable, boolean groupWritable, boolean groupExecutable, |
| boolean otherReadable, boolean otherWritable, boolean otherExecutable) { |
| if (LINUX) { |
| int modeValue = ((ownerReadable) ? MODE_OWNER_READABLE : 0) + |
| ((ownerWritable) ? MODE_OWNER_WRITABLE : 0) + |
| ((ownerExecutable) ? MODE_OWNER_EXECUTABLE : 0) + |
| ((groupReadable) ? MODE_GROUP_READABLE : 0) + |
| ((groupWritable) ? MODE_GROUP_WRITABLE : 0) + |
| ((groupExecutable) ? MODE_GROUP_EXECUTABLE : 0) + |
| ((otherReadable) ? MODE_OTHER_READABLE : 0) + |
| ((otherWritable) ? MODE_OTHER_WRITABLE : 0) + |
| ((otherExecutable) ? MODE_OTHER_EXECUTABLE : 0); |
| String mode = String.format("%04d", modeValue); |
| |
| try { |
| return runCommand(new String[]{"chmod", mode, path}, null, null, true); |
| } catch (IOException | InterruptedException e) { |
| // Improbable |
| LOG.warn(String.format("Can not perform chmod %s %s", mode, path), e); |
| return new Result(-1, "", "Cannot perform operation: " + e.getLocalizedMessage()); |
| } |
| } else { |
| LOG.debug(String.format("Not performing chmod command for file %s " + |
| "because current OS is not Linux ", path)); |
| return new Result(-1, "", "Cannot perform operation: The current OS is not Linux"); |
| } |
| } |
| |
| /** |
| * Test if a file or directory exists |
| * |
| * @param path the path to test |
| * @param sudo true to execute the command using sudo (ambari-sudo); otherwise false |
| * @return the shell command result, success indicates the file or directory exists |
| * @throws IOException |
| * @throws InterruptedException |
| */ |
| public static Result pathExists(String path, boolean sudo) throws IOException, InterruptedException { |
| String[] command = { |
| (WINDOWS) ? "dir" : "/bin/ls", |
| path |
| }; |
| |
| return runCommand(command, null, null, sudo); |
| } |
| |
| /** |
| * Creates the specified directory and any directories in the path |
| * |
| * @param directoryPath the directory to create |
| * @param sudo true to execute the command using sudo (ambari-sudo); otherwise false |
| * @return the shell command result |
| */ |
| public static Result mkdir(String directoryPath, boolean sudo) throws IOException, InterruptedException { |
| |
| // If this directory already exists, do not try to create it |
| if (pathExists(directoryPath, sudo).isSuccessful()) { |
| return new Result(0, "The directory already exists, skipping.", ""); // Success! |
| } else { |
| ArrayList<String> command = new ArrayList<>(); |
| |
| command.add("/bin/mkdir"); |
| |
| if (!WINDOWS) { |
| command.add("-p"); // create parent directories |
| } |
| |
| command.add(directoryPath); |
| |
| return runCommand(command, null, null, sudo); |
| } |
| } |
| |
| |
| /** |
| * Copies a source file to the specified destination. |
| * |
| * @param srcFile the path to the source file |
| * @param destFile the path to the destination file |
| * @param force true to force copy even if the file exists |
| * @param sudo true to execute the command using sudo (ambari-sudo); otherwise false |
| * @return the shell command result |
| */ |
| public static Result copyFile(String srcFile, String destFile, boolean force, boolean sudo) throws IOException, InterruptedException { |
| ArrayList<String> command = new ArrayList<>(); |
| |
| if (WINDOWS) { |
| command.add("copy"); |
| |
| if (force) { |
| command.add("/Y"); // force overwrite |
| } |
| } else { |
| command.add("cp"); |
| command.add("-p"); // preserve mode, ownership, timestamps |
| |
| if (force) { |
| command.add("-f"); // force overwrite |
| } |
| } |
| |
| command.add(srcFile); |
| command.add(destFile); |
| |
| return runCommand(command, null, null, sudo); |
| } |
| |
| /** |
| * Deletes the <code>file</code>. |
| * |
| * @param file the path to the file to be deleted |
| * @param force true to force copy even if the file exists |
| * @param sudo true to execute the command using sudo (ambari-sudo); otherwise false |
| * @return the shell command result |
| */ |
| public static Result delete(String file, boolean force, boolean sudo) throws IOException, InterruptedException { |
| List<String> command = new ArrayList<>(); |
| |
| if (WINDOWS) { |
| command.add("del"); |
| if (force) { |
| command.add("/f"); |
| } |
| } else { |
| command.add("/bin/rm"); |
| if (force) { |
| command.add("-f"); |
| } |
| } |
| |
| command.add(file); |
| |
| return runCommand(command, null, null, sudo); |
| } |
| |
| /** |
| * @see #runCommand(String[], Map, InteractiveHandler, boolean) |
| */ |
| public static Result runCommand(List<String> args, Map<String, String> vars, InteractiveHandler interactiveHandler, boolean sudo) |
| throws IOException, InterruptedException { |
| return runCommand(args.toArray(new String[args.size()]), vars, interactiveHandler, sudo); |
| } |
| |
| /** |
| * Runs a command with a given set of environment variables |
| * |
| * @param args a String[] of the command and its arguments |
| * @param vars a Map of String,String setting an environment variable to run the command with |
| * @param interactiveHandler a handler to provide responses to queries from the command, |
| * or null if no queries are expected |
| * @param sudo true to execute the command using sudo (ambari-sudo); otherwise false |
| * @return Result |
| * @throws IOException |
| * @throws InterruptedException |
| */ |
| public static Result runCommand(String[] args, Map<String, String> vars, InteractiveHandler interactiveHandler, boolean sudo) |
| throws IOException, InterruptedException { |
| |
| String[] processArgs; |
| |
| if (sudo) { |
| processArgs = new String[args.length + 1]; |
| processArgs[0] = AMBARI_SUDO; |
| System.arraycopy(args, 0, processArgs, 1, args.length); |
| } else { |
| processArgs = args; |
| } |
| |
| ProcessBuilder builder = new ProcessBuilder(processArgs); |
| |
| if (vars != null) { |
| Map<String, String> env = builder.environment(); |
| env.putAll(vars); |
| } |
| |
| Process process; |
| if (WINDOWS) { |
| synchronized (WindowsProcessLaunchLock) { |
| // To workaround the race condition issue with child processes |
| // inheriting unintended handles during process launch that can |
| // lead to hangs on reading output and error streams, we |
| // serialize process creation. More info available at: |
| // http://support.microsoft.com/kb/315939 |
| process = builder.start(); |
| } |
| } else { |
| process = builder.start(); |
| } |
| |
| // If an interactiveHandler is supplied ask it for responses to queries from the command |
| // using the InputStream and OutputStream retrieved from the Process object. Use the remainder |
| // of the data from the InputStream as the data for stdout. |
| InputStream inputStream = process.getInputStream(); |
| if (interactiveHandler != null) { |
| BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); |
| BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); |
| |
| interactiveHandler.start(); |
| |
| try { |
| while (!interactiveHandler.done()) { |
| StringBuilder query = new StringBuilder(); |
| |
| while (reader.ready()) { |
| query.append((char) reader.read()); |
| } |
| |
| String response = interactiveHandler.getResponse(query.toString()); |
| |
| if (response != null) { |
| writer.write(response); |
| writer.newLine(); |
| writer.flush(); |
| } |
| } |
| } catch (IOException ex){ |
| // ignore exception as command possibly can be finished before writer.flush() or writer.write() called |
| } finally { |
| writer.close(); |
| } |
| } |
| |
| //TODO: not sure whether output buffering will work properly |
| // if command output is too intensive |
| process.waitFor(); |
| String stdout = streamToString(inputStream); |
| String stderr = streamToString(process.getErrorStream()); |
| int exitCode = process.exitValue(); |
| return new Result(exitCode, stdout, stderr); |
| } |
| |
| /** |
| * Runs a command with a given set of environment variables |
| * |
| * @param args a String[] of the command and its arguments |
| * @param vars a Map of String,String setting an environment variable to run the command with |
| * @return Result |
| * @throws IOException |
| * @throws InterruptedException |
| */ |
| public static Result runCommand(String[] args, Map<String, String> vars) |
| throws IOException, InterruptedException { |
| return runCommand(args, vars, null, false); |
| } |
| |
| /** |
| * Run a command |
| * |
| * @param args A String[] of the command and its arguments |
| * @return Result |
| * @throws IOException |
| * @throws InterruptedException |
| */ |
| public static Result runCommand(String[] args) throws IOException, |
| InterruptedException { |
| return runCommand(args, null); |
| } |
| |
| private static String streamToString(InputStream is) throws IOException { |
| InputStreamReader isr = new InputStreamReader(is); |
| BufferedReader reader = new BufferedReader(isr); |
| StringBuilder sb = new StringBuilder(); |
| String line = null; |
| while ((line = reader.readLine()) != null) { |
| sb.append(line).append("\n"); |
| } |
| return sb.toString(); |
| } |
| |
| public static class Result { |
| |
| public Result(int exitCode, String stdout, String stderr) { |
| this.exitCode = exitCode; |
| this.stdout = stdout; |
| this.stderr = stderr; |
| } |
| |
| private final int exitCode; |
| private final String stdout; |
| private final String stderr; |
| |
| public int getExitCode() { |
| return exitCode; |
| } |
| |
| public String getStdout() { |
| return stdout; |
| } |
| |
| public String getStderr() { |
| return stderr; |
| } |
| |
| public boolean isSuccessful() { |
| return exitCode == 0; |
| } |
| } |
| |
| /** |
| * InteractiveHandler is a handler for interactive sessions with command line commands. |
| * <p> |
| * Classes should implement this interface if there is a need to supply responses to queries from |
| * the executed command (interactively, via stdin). |
| */ |
| public interface InteractiveHandler { |
| |
| /** |
| * Indicates whether this {@link InteractiveHandler} expects more queries (<code>true</code> |
| * or not (<code>false</code>) |
| * |
| * @return true if more queries are expected; false otherwise |
| */ |
| boolean done(); |
| |
| /** |
| * Given a query, returns the relative response to send to the shell command (via stdin) |
| * |
| * @param query a string containing the query that needs a response |
| * @return a string or null if no response is needed |
| */ |
| String getResponse(String query); |
| |
| /** |
| * Starts or resets this handler. |
| * <p> |
| * It is expected that the caller calls this before using handler. |
| */ |
| void start(); |
| } |
| } |