001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.oozie.action.ssh;
020
021import java.io.BufferedReader;
022import java.io.File;
023import java.io.FileWriter;
024import java.io.IOException;
025import java.io.InputStreamReader;
026import java.util.Arrays;
027import java.util.List;
028import java.util.concurrent.Callable;
029import org.apache.hadoop.util.StringUtils;
030
031import org.apache.oozie.client.WorkflowAction;
032import org.apache.oozie.client.OozieClient;
033import org.apache.oozie.client.WorkflowAction.Status;
034import org.apache.oozie.action.ActionExecutor;
035import org.apache.oozie.action.ActionExecutorException;
036import org.apache.oozie.service.CallbackService;
037import org.apache.oozie.service.ConfigurationService;
038import org.apache.oozie.servlet.CallbackServlet;
039import org.apache.oozie.service.Services;
040import org.apache.oozie.util.IOUtils;
041import org.apache.oozie.util.PropertiesUtils;
042import org.apache.oozie.util.XLog;
043import org.apache.oozie.util.XmlUtils;
044import org.jdom.Element;
045import org.jdom.JDOMException;
046import org.jdom.Namespace;
047
048/**
049 * Ssh action executor. <p> <ul> <li>Execute the shell commands on the remote host</li> <li>Copies the base and wrapper
050 * scripts on to the remote location</li> <li>Base script is used to run the command on the remote host</li> <li>Wrapper
051 * script is used to check the status of the submitted command</li> <li>handles the submission failures</li> </ul>
052 */
053public class SshActionExecutor extends ActionExecutor {
054    public static final String ACTION_TYPE = "ssh";
055
056    /**
057     * Configuration parameter which specifies whether the specified ssh user is allowed, or has to be the job user.
058     */
059    public static final String CONF_SSH_ALLOW_USER_AT_HOST = CONF_PREFIX + "ssh.allow.user.at.host";
060
061    protected static final String SSH_COMMAND_OPTIONS =
062            "-o PasswordAuthentication=no -o KbdInteractiveDevices=no -o StrictHostKeyChecking=no -o ConnectTimeout=20 ";
063
064    protected static final String SSH_COMMAND_BASE = "ssh " + SSH_COMMAND_OPTIONS;
065    protected static final String SCP_COMMAND_BASE = "scp " + SSH_COMMAND_OPTIONS;
066
067    public static final String ERR_SETUP_FAILED = "SETUP_FAILED";
068    public static final String ERR_EXECUTION_FAILED = "EXECUTION_FAILED";
069    public static final String ERR_UNKNOWN_ERROR = "UNKNOWN_ERROR";
070    public static final String ERR_COULD_NOT_CONNECT = "COULD_NOT_CONNECT";
071    public static final String ERR_HOST_RESOLUTION = "COULD_NOT_RESOLVE_HOST";
072    public static final String ERR_FNF = "FNF";
073    public static final String ERR_AUTH_FAILED = "AUTH_FAILED";
074    public static final String ERR_NO_EXEC_PERM = "NO_EXEC_PERM";
075    public static final String ERR_USER_MISMATCH = "ERR_USER_MISMATCH";
076    public static final String ERR_EXCEDE_LEN = "ERR_OUTPUT_EXCEED_MAX_LEN";
077
078    public static final String DELETE_TMP_DIR = "oozie.action.ssh.delete.remote.tmp.dir";
079
080    public static final String HTTP_COMMAND = "oozie.action.ssh.http.command";
081
082    public static final String HTTP_COMMAND_OPTIONS = "oozie.action.ssh.http.command.post.options";
083
084    private static final String EXT_STATUS_VAR = "#status";
085
086    private static int maxLen;
087    private static boolean allowSshUserAtHost;
088
089    private final XLog LOG = XLog.getLog(getClass())
090;
091    protected SshActionExecutor() {
092        super(ACTION_TYPE);
093    }
094
095    /**
096     * Initialize Action.
097     */
098    @Override
099    public void initActionType() {
100        super.initActionType();
101        maxLen = getOozieConf().getInt(CallbackServlet.CONF_MAX_DATA_LEN, 2 * 1024);
102        allowSshUserAtHost = ConfigurationService.getBoolean(CONF_SSH_ALLOW_USER_AT_HOST);
103        registerError(InterruptedException.class.getName(), ActionExecutorException.ErrorType.ERROR, "SH001");
104        registerError(JDOMException.class.getName(), ActionExecutorException.ErrorType.ERROR, "SH002");
105        initSshScripts();
106    }
107
108    /**
109     * Check ssh action status.
110     *
111     * @param context action execution context.
112     * @param action action object.
113     * @throws org.apache.oozie.action.ActionExecutorException
114     */
115    @Override
116    public void check(Context context, WorkflowAction action) throws ActionExecutorException {
117        LOG.trace("check() start for action={0}", action.getId());
118        Status status = getActionStatus(context, action);
119        boolean captureOutput = false;
120        try {
121            Element eConf = XmlUtils.parseXml(action.getConf());
122            Namespace ns = eConf.getNamespace();
123            captureOutput = eConf.getChild("capture-output", ns) != null;
124        }
125        catch (JDOMException ex) {
126            throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "ERR_XML_PARSE_FAILED",
127                                              "unknown error", ex);
128        }
129        LOG.debug("Capture Output: {0}", captureOutput);
130        if (status == Status.OK) {
131            if (captureOutput) {
132                String outFile = getRemoteFileName(context, action, "stdout", false, true);
133                String dataCommand = SSH_COMMAND_BASE + action.getTrackerUri() + " cat " + outFile;
134                LOG.debug("Ssh command [{0}]", dataCommand);
135                try {
136                    final Process process = Runtime.getRuntime().exec(dataCommand.split("\\s"));
137
138                    final StringBuffer outBuffer = new StringBuffer();
139                    final StringBuffer errBuffer = new StringBuffer();
140                    boolean overflow = false;
141                    drainBuffers(process, outBuffer, errBuffer, maxLen);
142                    LOG.trace("outBuffer={0}", outBuffer);
143                    LOG.trace("errBuffer={0}", errBuffer);
144                    if (outBuffer.length() > maxLen) {
145                        overflow = true;
146                    }
147                    if (overflow) {
148                        throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR,
149                                                          "ERR_OUTPUT_EXCEED_MAX_LEN", "unknown error");
150                    }
151                    context.setExecutionData(status.toString(), PropertiesUtils.stringToProperties(outBuffer.toString()));
152                    LOG.trace("Execution data set. status={0}, properties={1}", status,
153                            PropertiesUtils.stringToProperties(outBuffer.toString()));
154                }
155                catch (Exception ex) {
156                    throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "ERR_UNKNOWN_ERROR",
157                                                      "unknown error", ex);
158                }
159            }
160            else {
161                LOG.trace("Execution data set to null. status={0}", status);
162                context.setExecutionData(status.toString(), null);
163            }
164        }
165        else {
166            if (status == Status.ERROR) {
167                LOG.warn("Execution data set to null in ERROR");
168                context.setExecutionData(status.toString(), null);
169            }
170            else {
171                LOG.warn("Execution data not set");
172                context.setExternalStatus(status.toString());
173            }
174        }
175        LOG.trace("check() end for action={0}", action);
176    }
177
178    /**
179     * Kill ssh action.
180     *
181     * @param context action execution context.
182     * @param action object.
183     * @throws org.apache.oozie.action.ActionExecutorException
184     */
185    @Override
186    public void kill(Context context, WorkflowAction action) throws ActionExecutorException {
187        LOG.info("Killing action");
188        String command = "ssh " + action.getTrackerUri() + " kill  -KILL " + action.getExternalId();
189        int returnValue = getReturnValue(command);
190        if (returnValue != 0) {
191            throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "FAILED_TO_KILL", XLog.format(
192                    "Unable to kill process {0} on {1}", action.getExternalId(), action.getTrackerUri()));
193        }
194        context.setEndData(WorkflowAction.Status.KILLED, "ERROR");
195    }
196
197    /**
198     * Start the ssh action execution.
199     *
200     * @param context action execution context.
201     * @param action action object.
202     * @throws org.apache.oozie.action.ActionExecutorException
203     */
204    @SuppressWarnings("unchecked")
205    @Override
206    public void start(final Context context, final WorkflowAction action) throws ActionExecutorException {
207        LOG.info("Starting action");
208        String confStr = action.getConf();
209        Element conf;
210        try {
211            conf = XmlUtils.parseXml(confStr);
212        }
213        catch (Exception ex) {
214            throw convertException(ex);
215        }
216        Namespace nameSpace = conf.getNamespace();
217        Element hostElement = conf.getChild("host", nameSpace);
218        String hostString = hostElement.getValue().trim();
219        hostString = prepareUserHost(hostString, context);
220        final String host = hostString;
221        final String dirLocation = execute(new Callable<String>() {
222            public String call() throws Exception {
223                return setupRemote(host, context, action);
224            }
225
226        });
227
228        String runningPid = execute(new Callable<String>() {
229            public String call() throws Exception {
230                return checkIfRunning(host, context, action);
231            }
232        });
233        String pid = "";
234
235        LOG.trace("runningPid={0}", runningPid);
236
237        if (runningPid == null) {
238            final Element commandElement = conf.getChild("command", nameSpace);
239            final boolean ignoreOutput = conf.getChild("capture-output", nameSpace) == null;
240
241            boolean preserve = false;
242            if (commandElement != null) {
243                String[] args = null;
244                // Will either have <args>, <arg>, or neither (but not both)
245                List<Element> argsList = conf.getChildren("args", nameSpace);
246                // Arguments in an <args> are "flattened" (spaces are delimiters)
247                if (argsList != null && argsList.size() > 0) {
248                    StringBuilder argsString = new StringBuilder("");
249                    for (Element argsElement : argsList) {
250                        argsString = argsString.append(argsElement.getValue()).append(" ");
251                    }
252                    args = new String[]{argsString.toString()};
253                }
254                else {
255                    // Arguments in an <arg> are preserved, even with spaces
256                    argsList = conf.getChildren("arg", nameSpace);
257                    if (argsList != null && argsList.size() > 0) {
258                        preserve = true;
259                        args = new String[argsList.size()];
260                        for (int i = 0; i < argsList.size(); i++) {
261                            Element argsElement = argsList.get(i);
262                            args[i] = argsElement.getValue();
263                            // Even though we're keeping the args as an array, if they contain a space we still have to either quote
264                            // them or escape their space (because the scripts will split them up otherwise)
265                            if (args[i].contains(" ") &&
266                                    !(args[i].startsWith("\"") && args[i].endsWith("\"") ||
267                                      args[i].startsWith("'") && args[i].endsWith("'"))) {
268                                args[i] = StringUtils.escapeString(args[i], '\\', ' ');
269                            }
270                        }
271                    }
272                }
273                final String[] argsF = args;
274                final String recoveryId = context.getRecoveryId();
275                final boolean preserveF = preserve;
276                pid = execute(new Callable<String>() {
277
278                    @Override
279                    public String call() throws Exception {
280                        return doExecute(host, dirLocation, commandElement.getValue(), argsF, ignoreOutput, action, recoveryId,
281                                preserveF);
282                    }
283
284                });
285            }
286            context.setStartData(pid, host, host);
287        }
288        else {
289            pid = runningPid;
290            context.setStartData(pid, host, host);
291            check(context, action);
292        }
293    }
294
295    private String checkIfRunning(String host, final Context context, final WorkflowAction action) {
296        String pid = null;
297        String outFile = getRemoteFileName(context, action, "pid", false, false);
298        String getOutputCmd = SSH_COMMAND_BASE + host + " cat " + outFile;
299        try {
300            Process process = Runtime.getRuntime().exec(getOutputCmd.split("\\s"));
301            StringBuffer buffer = new StringBuffer();
302            drainBuffers(process, buffer, null, maxLen);
303            pid = getFirstLine(buffer);
304
305            if (Long.valueOf(pid) > 0) {
306                return pid;
307            }
308            else {
309                return null;
310            }
311        }
312        catch (Exception e) {
313            return null;
314        }
315    }
316
317    /**
318     * Get remote host working location.
319     *
320     * @param context action execution context
321     * @param action Action
322     * @param fileExtension Extension to be added to file name
323     * @param dirOnly Get the Directory only
324     * @param useExtId Flag to use external ID in the path
325     * @return remote host file name/Directory.
326     */
327    public String getRemoteFileName(Context context, WorkflowAction action, String fileExtension, boolean dirOnly,
328                                    boolean useExtId) {
329        String path = getActionDirPath(context.getWorkflow().getId(), action, ACTION_TYPE, false) + "/";
330        if (dirOnly) {
331            return path;
332        }
333        if (useExtId) {
334            path = path + action.getExternalId() + ".";
335        }
336        path = path + context.getRecoveryId() + "." + fileExtension;
337        return path;
338    }
339
340    /**
341     * Utility method to execute command.
342     *
343     * @param command Command to execute as String.
344     * @return exit status of the execution.
345     * @throws IOException if processSettings exits with status nonzero.
346     * @throws InterruptedException if processSettings does not run properly.
347     */
348    public int executeCommand(String command) throws IOException, InterruptedException {
349        Runtime runtime = Runtime.getRuntime();
350        Process p = runtime.exec(command.split("\\s"));
351
352        StringBuffer errorBuffer = new StringBuffer();
353        int exitValue = drainBuffers(p, null, errorBuffer, maxLen);
354
355        String error = null;
356        if (exitValue != 0) {
357            error = getTruncatedString(errorBuffer);
358            throw new IOException(XLog.format("Not able to perform operation [{0}]", command) + " | " + "ErrorStream: "
359                    + error);
360        }
361        return exitValue;
362    }
363
364    /**
365     * Do ssh action execution setup on remote host.
366     *
367     * @param host host name.
368     * @param context action execution context.
369     * @param action action object.
370     * @return remote host working directory.
371     * @throws IOException thrown if failed to setup.
372     * @throws InterruptedException thrown if any interruption happens.
373     */
374    protected String setupRemote(String host, Context context, WorkflowAction action) throws IOException, InterruptedException {
375        LOG.info("Attempting to copy ssh base scripts to remote host [{0}]", host);
376        String localDirLocation = Services.get().getRuntimeDir() + "/ssh";
377        if (localDirLocation.endsWith("/")) {
378            localDirLocation = localDirLocation.substring(0, localDirLocation.length() - 1);
379        }
380        File file = new File(localDirLocation + "/ssh-base.sh");
381        if (!file.exists()) {
382            throw new IOException("Required Local file " + file.getAbsolutePath() + " not present.");
383        }
384        file = new File(localDirLocation + "/ssh-wrapper.sh");
385        if (!file.exists()) {
386            throw new IOException("Required Local file " + file.getAbsolutePath() + " not present.");
387        }
388        String remoteDirLocation = getRemoteFileName(context, action, null, true, true);
389        String command = XLog.format("{0}{1}  mkdir -p {2} ", SSH_COMMAND_BASE, host, remoteDirLocation).toString();
390        executeCommand(command);
391        command = XLog.format("{0}{1}/ssh-base.sh {2}/ssh-wrapper.sh {3}:{4}", SCP_COMMAND_BASE, localDirLocation,
392                              localDirLocation, host, remoteDirLocation);
393        executeCommand(command);
394        command = XLog.format("{0}{1}  chmod +x {2}ssh-base.sh {3}ssh-wrapper.sh ", SSH_COMMAND_BASE, host,
395                              remoteDirLocation, remoteDirLocation);
396        executeCommand(command);
397        return remoteDirLocation;
398    }
399
400    /**
401     * Execute the ssh command.
402     *
403     * @param host hostname.
404     * @param dirLocation location of the base and wrapper scripts.
405     * @param cmnd command to be executed.
406     * @param args command arguments.
407     * @param ignoreOutput ignore output option.
408     * @param action action object.
409     * @param recoveryId action id + run number to enable recovery in rerun
410     * @param preserveArgs tell the ssh scripts to preserve or flatten the arguments
411     * @return processSettings id of the running command.
412     * @throws IOException thrown if failed to run the command.
413     * @throws InterruptedException thrown if any interruption happens.
414     */
415    protected String doExecute(String host, String dirLocation, String cmnd, String[] args, boolean ignoreOutput,
416                               WorkflowAction action, String recoveryId, boolean preserveArgs)
417                               throws IOException, InterruptedException {
418        XLog log = XLog.getLog(getClass());
419        Runtime runtime = Runtime.getRuntime();
420        String callbackPost = ignoreOutput ? "_" : ConfigurationService.get(HTTP_COMMAND_OPTIONS).replace(" ", "%%%");
421        String preserveArgsS = preserveArgs ? "PRESERVE_ARGS" : "FLATTEN_ARGS";
422        // TODO check
423        String callBackUrl = Services.get().get(CallbackService.class)
424                .createCallBackUrl(action.getId(), EXT_STATUS_VAR);
425        String command = XLog.format("{0}{1} {2}ssh-base.sh {3} {4} \"{5}\" \"{6}\" {7} {8} ", SSH_COMMAND_BASE, host, dirLocation,
426                preserveArgsS, ConfigurationService.get(HTTP_COMMAND), callBackUrl, callbackPost, recoveryId, cmnd)
427                .toString();
428        String[] commandArray = command.split("\\s");
429        String[] finalCommand;
430        if (args == null) {
431            finalCommand = commandArray;
432        }
433        else {
434            finalCommand = new String[commandArray.length + args.length];
435            System.arraycopy(commandArray, 0, finalCommand, 0, commandArray.length);
436            System.arraycopy(args, 0, finalCommand, commandArray.length, args.length);
437        }
438
439        LOG.trace("Executing SSH command [finalCommand={0}]", Arrays.toString(finalCommand));
440        final Process p = runtime.exec(finalCommand);
441        final String pid;
442
443        final StringBuffer inputBuffer = new StringBuffer();
444        final StringBuffer errorBuffer = new StringBuffer();
445        final int exitValue = drainBuffers(p, inputBuffer, errorBuffer, maxLen);
446
447        pid = getFirstLine(inputBuffer);
448
449        String error = null;
450        if (exitValue != 0) {
451            error = getTruncatedString(errorBuffer);
452            throw new IOException(XLog.format("Not able to execute ssh-base.sh on {0}", host) + " | " + "ErrorStream: "
453                    + error);
454        }
455
456        LOG.trace("After execution pid={0}", pid);
457
458        return pid;
459    }
460
461    /**
462     * End action execution.
463     *
464     * @param context action execution context.
465     * @param action action object.
466     * @throws ActionExecutorException thrown if action end execution fails.
467     */
468    public void end(final Context context, final WorkflowAction action) throws ActionExecutorException {
469        if (action.getExternalStatus().equals("OK")) {
470            context.setEndData(WorkflowAction.Status.OK, WorkflowAction.Status.OK.toString());
471        }
472        else {
473            context.setEndData(WorkflowAction.Status.ERROR, WorkflowAction.Status.ERROR.toString());
474        }
475        boolean deleteTmpDir = ConfigurationService.getBoolean(DELETE_TMP_DIR);
476        if (deleteTmpDir) {
477            String tmpDir = getRemoteFileName(context, action, null, true, false);
478            String removeTmpDirCmd = SSH_COMMAND_BASE + action.getTrackerUri() + " rm -rf " + tmpDir;
479            int retVal = getReturnValue(removeTmpDirCmd);
480            if (retVal != 0) {
481                XLog.getLog(getClass()).warn("Cannot delete temp dir {0}", tmpDir);
482            }
483        }
484        LOG.info("Action ended with external status [{0}]", action.getExternalStatus());
485    }
486
487    /**
488     * Get the return value of a processSettings.
489     *
490     * @param command command to be executed.
491     * @return zero if execution is successful and any non zero value for failure.
492     * @throws ActionExecutorException
493     */
494    private int getReturnValue(String command) throws ActionExecutorException {
495        LOG.trace("Getting return value for command={0}", command);
496
497        int returnValue;
498        Process ps = null;
499        try {
500            ps = Runtime.getRuntime().exec(command.split("\\s"));
501            returnValue = drainBuffers(ps, null, null, 0);
502        }
503        catch (IOException e) {
504            throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "FAILED_OPERATION", XLog.format(
505                    "Not able to perform operation {0}", command), e);
506        }
507        finally {
508            ps.destroy();
509        }
510
511        LOG.trace("returnValue={0}", returnValue);
512
513        return returnValue;
514    }
515
516    /**
517     * Copy the ssh base and wrapper scripts to the local directory.
518     */
519    private void initSshScripts() {
520        String dirLocation = Services.get().getRuntimeDir() + "/ssh";
521        File path = new File(dirLocation);
522        path.mkdirs();
523        if (!path.exists()) {
524            throw new RuntimeException(XLog.format("Not able to create required directory {0}", dirLocation));
525        }
526        try {
527            IOUtils.copyCharStream(IOUtils.getResourceAsReader("ssh-base.sh", -1), new FileWriter(dirLocation
528                    + "/ssh-base.sh"));
529            IOUtils.copyCharStream(IOUtils.getResourceAsReader("ssh-wrapper.sh", -1), new FileWriter(dirLocation
530                    + "/ssh-wrapper.sh"));
531        }
532        catch (IOException ie) {
533            throw new RuntimeException(XLog.format("Not able to copy required scripts file to {0} "
534                    + "for SshActionHandler", dirLocation));
535        }
536    }
537
538    /**
539     * Get action status.
540     *
541     * @param context
542     * @param action action object.
543     * @return status of the action(RUNNING/OK/ERROR).
544     * @throws ActionExecutorException thrown if there is any error in getting status.
545     */
546    protected Status getActionStatus(Context context, WorkflowAction action) throws ActionExecutorException {
547        String command = SSH_COMMAND_BASE + action.getTrackerUri() + " ps -p " + action.getExternalId();
548        Status aStatus;
549        int returnValue = getReturnValue(command);
550        if (returnValue == 0) {
551            aStatus = Status.RUNNING;
552        }
553        else {
554            String outFile = getRemoteFileName(context, action, "error", false, true);
555            String checkErrorCmd = SSH_COMMAND_BASE + action.getTrackerUri() + " ls " + outFile;
556            int retVal = getReturnValue(checkErrorCmd);
557            if (retVal == 0) {
558                aStatus = Status.ERROR;
559            }
560            else {
561                aStatus = Status.OK;
562            }
563        }
564        return aStatus;
565    }
566
567    /**
568     * Execute the callable.
569     *
570     * @param callable required callable.
571     * @throws ActionExecutorException thrown if there is any error in command execution.
572     */
573    private <T> T execute(Callable<T> callable) throws ActionExecutorException {
574        XLog log = XLog.getLog(getClass());
575        try {
576            return callable.call();
577        }
578        catch (IOException ex) {
579            log.warn("Error while executing ssh EXECUTION");
580            String errorMessage = ex.getMessage();
581            if (null == errorMessage) { // Unknown IOException
582                throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, ERR_UNKNOWN_ERROR, ex
583                        .getMessage(), ex);
584            } // Host Resolution Issues
585            else {
586                if (errorMessage.contains("Could not resolve hostname") ||
587                        errorMessage.contains("service not known")) {
588                    throw new ActionExecutorException(ActionExecutorException.ErrorType.TRANSIENT, ERR_HOST_RESOLUTION, ex
589                            .getMessage(), ex);
590                } // Connection Timeout. Host temporarily down.
591                else {
592                    if (errorMessage.contains("timed out")) {
593                        throw new ActionExecutorException(ActionExecutorException.ErrorType.TRANSIENT, ERR_COULD_NOT_CONNECT,
594                                                          ex.getMessage(), ex);
595                    }// Local ssh-base or ssh-wrapper missing
596                    else {
597                        if (errorMessage.contains("Required Local file")) {
598                            throw new ActionExecutorException(ActionExecutorException.ErrorType.TRANSIENT, ERR_FNF,
599                                                              ex.getMessage(), ex); // local_FNF
600                        }// Required oozie bash scripts missing, after the copy was
601                        // successful
602                        else {
603                            if (errorMessage.contains("No such file or directory")
604                                    && (errorMessage.contains("ssh-base") || errorMessage.contains("ssh-wrapper"))) {
605                                throw new ActionExecutorException(ActionExecutorException.ErrorType.TRANSIENT, ERR_FNF,
606                                                                  ex.getMessage(), ex); // remote
607                                // FNF
608                            } // Required application execution binary missing (either
609                            // caught by ssh-wrapper
610                            else {
611                                if (errorMessage.contains("command not found")) {
612                                    throw new ActionExecutorException(ActionExecutorException.ErrorType.NON_TRANSIENT, ERR_FNF, ex
613                                            .getMessage(), ex); // remote
614                                    // FNF
615                                } // Permission denied while connecting
616                                else {
617                                    if (errorMessage.contains("Permission denied")) {
618                                        throw new ActionExecutorException(ActionExecutorException.ErrorType.NON_TRANSIENT,
619                                                ERR_AUTH_FAILED, ex.getMessage(), ex);
620                                    } // Permission denied while executing
621                                    else {
622                                        if (errorMessage.contains(": Permission denied")) {
623                                            throw new ActionExecutorException(ActionExecutorException.ErrorType.NON_TRANSIENT,
624                                                    ERR_NO_EXEC_PERM, ex.getMessage(), ex);
625                                        }
626                                        else {
627                                            throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR,
628                                                    ERR_UNKNOWN_ERROR, ex.getMessage(), ex);
629                                        }
630                                    }
631                                }
632                            }
633                        }
634                    }
635                }
636            }
637        } // Any other type of exception
638        catch (Exception ex) {
639            throw convertException(ex);
640        }
641    }
642
643    /**
644     * Checks whether the system is configured to always use the oozie user for ssh, and injects the user if required.
645     *
646     * @param host the host string.
647     * @param context the execution context.
648     * @return the modified host string with a user parameter added on if required.
649     * @throws ActionExecutorException in case the flag to use the oozie user is turned on and there is a mismatch
650     * between the user specified in the host and the oozie user.
651     */
652    private String prepareUserHost(String host, Context context) throws ActionExecutorException {
653        String oozieUser = context.getProtoActionConf().get(OozieClient.USER_NAME);
654        if (allowSshUserAtHost) {
655            if (!host.contains("@")) {
656                host = oozieUser + "@" + host;
657            }
658        }
659        else {
660            if (host.contains("@")) {
661                if (!host.toLowerCase().startsWith(oozieUser + "@")) {
662                    throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, ERR_USER_MISMATCH,
663                                                      XLog.format("user mismatch between oozie user [{0}] and ssh host [{1}]",
664                                                              oozieUser, host));
665                }
666            }
667            else {
668                host = oozieUser + "@" + host;
669            }
670        }
671
672        LOG.trace("User host is {0}", host);
673
674        return host;
675    }
676
677    @Override
678    public boolean isCompleted(String externalStatus) {
679        return true;
680    }
681
682    /**
683     * Truncate the string to max length.
684     *
685     * @param strBuffer
686     * @return truncated string string
687     */
688    private String getTruncatedString(StringBuffer strBuffer) {
689
690        if (strBuffer.length() <= maxLen) {
691            return strBuffer.toString();
692        }
693        else {
694            return strBuffer.substring(0, maxLen);
695        }
696    }
697
698    /**
699     * Drains the inputStream and errorStream of the Process being executed. The contents of the streams are stored if a
700     * buffer is provided for the stream.
701     *
702     * @param p The Process instance.
703     * @param inputBuffer The buffer into which STDOUT is to be read. Can be null if only draining is required.
704     * @param errorBuffer The buffer into which STDERR is to be read. Can be null if only draining is required.
705     * @param maxLength The maximum data length to be stored in these buffers. This is an indicative value, and the
706     * store content may exceed this length.
707     * @return the exit value of the processSettings.
708     * @throws IOException
709     */
710    private int drainBuffers(Process p, StringBuffer inputBuffer, StringBuffer errorBuffer, int maxLength)
711            throws IOException {
712        int exitValue = -1;
713        BufferedReader ir = new BufferedReader(new InputStreamReader(p.getInputStream()));
714        BufferedReader er = new BufferedReader(new InputStreamReader(p.getErrorStream()));
715
716        int inBytesRead = 0;
717        int errBytesRead = 0;
718
719        boolean processEnded = false;
720
721        try {
722            while (!processEnded) {
723                try {
724                    exitValue = p.waitFor();
725                    processEnded = true;
726                }
727                catch (final IllegalThreadStateException | InterruptedException e) {
728                    LOG.warn("An exception occurred while waiting for the process, continuing to drain. " +
729                            "[e.message={0}]", e.getMessage());
730                }
731
732                inBytesRead += drainBuffer(ir, inputBuffer, maxLength, inBytesRead, processEnded);
733                errBytesRead += drainBuffer(er, errorBuffer, maxLength, errBytesRead, processEnded);
734            }
735        }
736        finally {
737            ir.close();
738            er.close();
739        }
740
741        return exitValue;
742    }
743
744    /**
745     * Reads the contents of a stream and stores them into the provided buffer.
746     *
747     * @param br The stream to be read.
748     * @param storageBuf The buffer into which the contents of the stream are to be stored.
749     * @param maxLength The maximum number of bytes to be stored in the buffer. An indicative value and may be
750     * exceeded.
751     * @param bytesRead The number of bytes read from this stream to date.
752     * @param readAll If true, the stream is drained while their is data available in it. Otherwise, only a single chunk
753     * of data is read, irrespective of how much is available.
754     * @return bReadSession returns drainBuffer for stream of contents
755     * @throws IOException
756     */
757    private int drainBuffer(BufferedReader br, StringBuffer storageBuf, int maxLength, int bytesRead, boolean readAll)
758            throws IOException {
759        int bReadSession = 0;
760        if (br.ready()) {
761            char[] buf = new char[1024];
762            do {
763                int bReadCurrent = br.read(buf, 0, 1024);
764                if (storageBuf != null && bytesRead < maxLength) {
765                    storageBuf.append(buf, 0, bReadCurrent);
766                }
767                bReadSession += bReadCurrent;
768            } while (br.ready() && readAll);
769        }
770        return bReadSession;
771    }
772
773    /**
774     * Returns the first line from a StringBuffer, recognized by the new line character \n.
775     *
776     * @param buffer The StringBuffer from which the first line is required.
777     * @return The first line of the buffer.
778     */
779    private String getFirstLine(StringBuffer buffer) {
780        int newLineIndex = 0;
781        newLineIndex = buffer.indexOf("\n");
782        if (newLineIndex == -1) {
783            return buffer.toString();
784        }
785        else {
786            return buffer.substring(0, newLineIndex);
787        }
788    }
789}