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}