Java tutorial
/* * Copyright 2011 DTO Solutions, Inc. (http://dtosolutions.com) * * Licensed 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. */ /* * JschNodeExecutor.java * * User: Greg Schueler <a href="mailto:greg@dtosolutions.com">greg@dtosolutions.com</a> * Created: 3/21/11 4:46 PM * */ package com.dtolabs.rundeck.core.execution.impl.jsch; import com.dtolabs.rundeck.core.Constants; import com.dtolabs.rundeck.core.cli.CLIUtils; import com.dtolabs.rundeck.core.common.Framework; import com.dtolabs.rundeck.core.common.FrameworkProject; import com.dtolabs.rundeck.core.common.INodeEntry; import com.dtolabs.rundeck.core.dispatcher.DataContextUtils; import com.dtolabs.rundeck.core.execution.ExecutionContext; import com.dtolabs.rundeck.core.execution.ExecutionListener; import com.dtolabs.rundeck.core.execution.impl.common.AntSupport; import com.dtolabs.rundeck.core.execution.service.NodeExecutor; import com.dtolabs.rundeck.core.execution.service.NodeExecutorResult; import com.dtolabs.rundeck.core.execution.service.NodeExecutorResultImpl; import com.dtolabs.rundeck.core.execution.utils.LeadPipeOutputStream; import com.dtolabs.rundeck.core.execution.utils.Responder; import com.dtolabs.rundeck.core.execution.utils.ResponderTask; import com.dtolabs.rundeck.core.execution.workflow.steps.FailureReason; import com.dtolabs.rundeck.core.execution.workflow.steps.StepFailureReason; import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepFailureReason; import com.dtolabs.rundeck.core.plugins.configuration.*; import com.dtolabs.rundeck.core.storage.ResourceMeta; import com.dtolabs.rundeck.core.tasks.net.ExtSSHExec; import com.dtolabs.rundeck.core.tasks.net.SSHTaskBuilder; import com.dtolabs.rundeck.core.utils.IPropertyLookup; import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; import com.dtolabs.rundeck.plugins.util.PropertyBuilder; import com.jcraft.jsch.JSchException; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.log4j.Logger; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.rundeck.storage.api.Path; import org.rundeck.storage.api.PathUtil; import java.io.IOException; import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.net.NoRouteToHostException; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.*; import java.util.concurrent.*; import java.util.regex.Pattern; /** * JschNodeExecutor is ... * * @author Greg Schueler <a href="mailto:greg@dtosolutions.com">greg@dtosolutions.com</a> */ public class JschNodeExecutor implements NodeExecutor, Describable { public static final Logger logger = Logger.getLogger(JschNodeExecutor.class.getName()); public static final String SERVICE_PROVIDER_TYPE = "jsch-ssh"; public static final String FWK_PROP_AUTH_CANCEL_MSG = "framework.messages.error.ssh.authcancel"; public static final String FWK_PROP_AUTH_CANCEL_MSG_DEFAULT = "Authentication failure connecting to node: \"{0}\". Make sure your resource definitions and credentials are up to date."; public static final String FWK_PROP_AUTH_FAIL_MSG = "framework.messages.error.ssh.authfail"; public static final String FWK_PROP_AUTH_FAIL_MSG_DEFAULT = "Authentication failure connecting to node: \"{0}\". Password incorrect."; public static final String NODE_ATTR_SSH_KEYPATH = "ssh-keypath"; public static final String NODE_ATTR_SSH_KEY_RESOURCE = "ssh-key-storage-path"; public static final String PROJ_PROP_PREFIX = "project."; public static final String FWK_PROP_PREFIX = "framework."; public static final String FWK_PROP_SSH_KEYPATH = FWK_PROP_PREFIX + NODE_ATTR_SSH_KEYPATH; public static final String PROJ_PROP_SSH_KEYPATH = PROJ_PROP_PREFIX + NODE_ATTR_SSH_KEYPATH; public static final String FWK_PROP_SSH_KEY_RESOURCE = FWK_PROP_PREFIX + NODE_ATTR_SSH_KEY_RESOURCE; public static final String PROJ_PROP_SSH_KEY_RESOURCE = PROJ_PROP_PREFIX + NODE_ATTR_SSH_KEY_RESOURCE; public static final String NODE_ATTR_SSH_AUTHENTICATION = "ssh-authentication"; public static final String NODE_ATTR_SSH_PASSWORD_OPTION = "ssh-password-option"; public static final String DEFAULT_SSH_PASSWORD_OPTION = "option.sshPassword"; public static final String SUDO_OPT_PREFIX = "sudo-"; public static final String SUDO2_OPT_PREFIX = "sudo2-"; public static final String NODE_ATTR_SUDO_PASSWORD_OPTION = "password-option"; public static final String DEFAULT_SUDO_PASSWORD_OPTION = "option.sudoPassword"; public static final String DEFAULT_SUDO2_PASSWORD_OPTION = "option.sudo2Password"; public static final String NODE_ATTR_SSH_KEY_PASSPHRASE_OPTION = "ssh-key-passphrase-option"; public static final String DEFAULT_SSH_KEY_PASSPHRASE_OPTION = "option.sshKeyPassphrase"; public static final String FWK_PROP_SSH_AUTHENTICATION = FWK_PROP_PREFIX + NODE_ATTR_SSH_AUTHENTICATION; public static final String PROJ_PROP_SSH_AUTHENTICATION = PROJ_PROP_PREFIX + NODE_ATTR_SSH_AUTHENTICATION; public static final String NODE_ATTR_SUDO_COMMAND_ENABLED = "command-enabled"; public static final String NODE_ATTR_SUDO_PROMPT_PATTERN = "prompt-pattern"; public static final String DEFAULT_SUDO_PROMPT_PATTERN = "^\\[sudo\\] password for .+: .*"; public static final String NODE_ATTR_SUDO_FAILURE_PATTERN = "failure-pattern"; public static final String DEFAULT_SUDO_FAILURE_PATTERN = "^.*try again.*"; public static final String NODE_ATTR_SUDO_COMMAND_PATTERN = "command-pattern"; public static final String DEFAULT_SUDO_COMMAND_PATTERN = "^sudo$"; public static final String DEFAULT_SUDO2_COMMAND_PATTERN = "^sudo .+? sudo .*$"; public static final String NODE_ATTR_SUDO_PROMPT_MAX_LINES = "prompt-max-lines"; public static final int DEFAULT_SUDO_PROMPT_MAX_LINES = 12; public static final String NODE_ATTR_SUDO_RESPONSE_MAX_LINES = "response-max-lines"; public static final int DEFAULT_SUDO_RESPONSE_MAX_LINES = 2; public static final String NODE_ATTR_SUDO_PROMPT_MAX_TIMEOUT = "prompt-max-timeout"; public static final long DEFAULT_SUDO_PROMPT_MAX_TIMEOUT = 5000; public static final String NODE_ATTR_SUDO_RESPONSE_MAX_TIMEOUT = "response-max-timeout"; public static final long DEFAULT_SUDO_RESPONSE_MAX_TIMEOUT = 5000; public static final String NODE_ATTR_SUDO_FAIL_ON_PROMPT_MAX_LINES = "fail-on-prompt-max-lines"; public static final boolean DEFAULT_SUDO_FAIL_ON_PROMPT_MAX_LINES = false; public static final String NODE_ATTR_SUDO_FAIL_ON_PROMPT_TIMEOUT = "fail-on-prompt-timeout"; public static final boolean DEFAULT_SUDO_FAIL_ON_PROMPT_TIMEOUT = true; public static final String NODE_ATTR_SUDO_FAIL_ON_RESPONSE_TIMEOUT = "fail-on-response-timeout"; public static final boolean DEFAULT_SUDO_FAIL_ON_RESPONSE_TIMEOUT = false; public static final String NODE_ATTR_SUDO_SUCCESS_ON_PROMPT_THRESHOLD = "success-on-prompt-threshold"; public static final boolean DEFAULT_SUDO_SUCCESS_ON_PROMPT_THRESHOLD = true; public static final String PROJECT_SSH_USER = PROJ_PROP_PREFIX + "ssh.user"; public static final String SSH_CONFIG_PREFIX = "ssh-config-"; public static final String FWK_SSH_CONFIG_PREFIX = FWK_PROP_PREFIX + SSH_CONFIG_PREFIX; public static final String PROJ_SSH_CONFIG_PREFIX = PROJ_PROP_PREFIX + SSH_CONFIG_PREFIX; private Framework framework; public JschNodeExecutor(final Framework framework) { this.framework = framework; } public static final String CONFIG_KEYPATH = "keypath"; public static final String CONFIG_KEYSTORE_PATH = "keystoragepath"; public static final String CONFIG_AUTHENTICATION = "authentication"; static final Description DESC; static final Property SSH_KEY_FILE_PROP = PropertyUtil.string(CONFIG_KEYPATH, "SSH Key File path", "File Path to the SSH Key to use", false, null); static final Property SSH_KEY_STORAGE_PROP = PropertyBuilder.builder().string(CONFIG_KEYSTORE_PATH) .required(false).title("SSH Key Storage Path") .description("Path to the SSH Key to use within Rundeck Storage. E.g. \"keys/path/key1.pem\"") .renderingOption(StringRenderingConstants.SELECTION_ACCESSOR_KEY, StringRenderingConstants.SelectionAccessor.STORAGE_PATH) .renderingOption(StringRenderingConstants.STORAGE_PATH_ROOT_KEY, "keys") .renderingOption(StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, "Rundeck-key-type=private") .build(); public static final Property SSH_AUTH_TYPE_PROP = PropertyUtil.select(CONFIG_AUTHENTICATION, "SSH Authentication", "Type of SSH Authentication to use", true, SSHTaskBuilder.AuthenticationType.privateKey.toString(), Arrays.asList(SSHTaskBuilder.AuthenticationType.values()), null, null); static { DescriptionBuilder builder = DescriptionBuilder.builder(); builder.name(SERVICE_PROVIDER_TYPE).title("SSH") .description("Executes a command on a remote node via SSH."); builder.property(SSH_KEY_FILE_PROP); builder.property(SSH_KEY_STORAGE_PROP); builder.property(SSH_AUTH_TYPE_PROP); builder.mapping(CONFIG_KEYPATH, PROJ_PROP_SSH_KEYPATH); builder.frameworkMapping(CONFIG_KEYPATH, FWK_PROP_SSH_KEYPATH); builder.mapping(CONFIG_KEYSTORE_PATH, PROJ_PROP_SSH_KEY_RESOURCE); builder.frameworkMapping(CONFIG_KEYSTORE_PATH, FWK_PROP_SSH_KEY_RESOURCE); builder.mapping(CONFIG_AUTHENTICATION, PROJ_PROP_SSH_AUTHENTICATION); builder.frameworkMapping(CONFIG_AUTHENTICATION, FWK_PROP_SSH_AUTHENTICATION); DESC = builder.build(); } public Description getDescription() { return DESC; } public NodeExecutorResult executeCommand(final ExecutionContext context, final String[] command, final INodeEntry node) { if (null == node.getHostname() || null == node.extractHostname()) { return NodeExecutorResultImpl.createFailure(StepFailureReason.ConfigurationFailure, "Hostname must be set to connect to remote node '" + node.getNodename() + "'", node); } final ExecutionListener listener = context.getExecutionListener(); final Project project = new Project(); AntSupport.addAntBuildListener(listener, project); boolean success = false; final ExtSSHExec sshexec; //perform jsch sssh command final NodeSSHConnectionInfo nodeAuthentication = new NodeSSHConnectionInfo(node, framework, context); final int timeout = nodeAuthentication.getSSHTimeout(); try { sshexec = SSHTaskBuilder.build(node, command, project, context.getDataContext(), nodeAuthentication, context.getLoglevel(), listener); } catch (SSHTaskBuilder.BuilderException e) { return NodeExecutorResultImpl.createFailure(StepFailureReason.ConfigurationFailure, e.getMessage(), node); } //Sudo support final ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() { public Thread newThread(Runnable r) { return new Thread(null, r, "SudoResponder " + node.getNodename() + ": " + System.currentTimeMillis()); } }); final Future<ResponderTask.ResponderResult> responderFuture; final SudoResponder sudoResponder = SudoResponder.create(node, framework, context); Runnable responderCleanup = null; if (sudoResponder.isSudoEnabled() && sudoResponder.matchesCommandPattern(command[0])) { final DisconnectResultHandler resultHandler = new DisconnectResultHandler(); //configure two piped i/o stream pairs, to connect to the input/output of the SSH connection final PipedInputStream responderInput = new PipedInputStream(); final PipedOutputStream responderOutput = new PipedOutputStream(); final PipedInputStream jschInput = new PipedInputStream(); //lead pipe allows connected inputstream to close and not hang the writer to this stream final PipedOutputStream jschOutput = new LeadPipeOutputStream(); try { responderInput.connect(jschOutput); jschInput.connect(responderOutput); } catch (IOException e) { return NodeExecutorResultImpl.createFailure(StepFailureReason.IOFailure, e.getMessage(), node); } //first sudo prompt responder ResponderTask responder = new ResponderTask(sudoResponder, responderInput, responderOutput, resultHandler); /** * Callable will be executed by the ExecutorService */ final Callable<ResponderTask.ResponderResult> responderResultCallable; //if 2nd responder final SudoResponder sudoResponder2 = SudoResponder.create(node, framework, context, SUDO2_OPT_PREFIX, DEFAULT_SUDO2_PASSWORD_OPTION, DEFAULT_SUDO2_COMMAND_PATTERN); if (sudoResponder2.isSudoEnabled() && sudoResponder2.matchesCommandPattern(CLIUtils.generateArgline(null, command, false))) { logger.debug("Enable second sudo responder"); sudoResponder2.setDescription("Second " + SudoResponder.DEFAULT_DESCRIPTION); sudoResponder.setDescription("First " + SudoResponder.DEFAULT_DESCRIPTION); //sequence of the first then the second sudo responder responderResultCallable = responder.createSequence(sudoResponder2); } else { responderResultCallable = responder; } //set up SSH execution sshexec.setAllocatePty(true); sshexec.setInputStream(jschInput); sshexec.setSecondaryStream(jschOutput); sshexec.setDisconnectHolder(resultHandler); responderFuture = executor.submit(responderResultCallable); //close streams after responder is finished responderCleanup = new Runnable() { public void run() { logger.debug("SudoResponder shutting down..."); try { responderInput.close(); } catch (IOException e) { e.printStackTrace(); } try { responderOutput.flush(); responderOutput.close(); } catch (IOException e) { e.printStackTrace(); } //executor pool shutdown executor.shutdownNow(); } }; executor.submit(responderCleanup); } else { responderFuture = null; } if (null != context.getExecutionListener()) { context.getExecutionListener().log(3, "Starting SSH Connection: " + nodeAuthentication.getUsername() + "@" + node.getHostname() + " (" + node.getNodename() + ")"); } String errormsg = null; FailureReason failureReason = null; try { sshexec.execute(); success = true; } catch (BuildException e) { final ExtractFailure extractJschFailure = extractFailure(e, node, timeout, framework); errormsg = extractJschFailure.getErrormsg(); failureReason = extractJschFailure.getReason(); context.getExecutionListener().log(0, errormsg); } if (null != responderCleanup) { responderCleanup.run(); } shutdownAndAwaitTermination(executor); if (null != responderFuture) { try { logger.debug("Waiting 5 seconds for responder future result"); final ResponderTask.ResponderResult result = responderFuture.get(5, TimeUnit.SECONDS); logger.debug("Responder result: " + result); if (!result.isSuccess() && !result.isInterrupted()) { context.getExecutionListener().log(0, result.getResponder().toString() + " failed: " + result.getFailureReason()); } } catch (InterruptedException e) { //ignore } catch (java.util.concurrent.ExecutionException e) { e.printStackTrace(); } catch (TimeoutException e) { //ignore } } final int resultCode = sshexec.getExitStatus(); if (success) { return NodeExecutorResultImpl.createSuccess(node); } else { return NodeExecutorResultImpl.createFailure(failureReason, errormsg, node, resultCode); } } static enum JschFailureReason implements FailureReason { /** * A problem with the SSH connection */ SSHProtocolFailure } static ExtractFailure extractFailure(BuildException e, INodeEntry node, int timeout, Framework framework) { String errormsg; FailureReason failureReason; if (e.getMessage().contains("Timeout period exceeded, connection dropped")) { errormsg = "Failed execution for node: " + node.getNodename() + ": Execution Timeout period exceeded (after " + timeout + "ms), connection dropped"; failureReason = NodeStepFailureReason.ConnectionTimeout; } else if (null != e.getCause() && e.getCause() instanceof JSchException) { JSchException jSchException = (JSchException) e.getCause(); return extractJschFailure(node, timeout, jSchException, framework); } else if (e.getMessage().contains("Remote command failed with exit status")) { errormsg = e.getMessage(); failureReason = NodeStepFailureReason.NonZeroResultCode; } else { failureReason = StepFailureReason.Unknown; errormsg = e.getMessage(); } return new ExtractFailure(errormsg, failureReason); } static ExtractFailure extractJschFailure(final INodeEntry node, final int timeout, final JSchException jSchException, final Framework framework) { String errormsg; FailureReason reason; if (null == jSchException.getCause()) { if (jSchException.getMessage().contains("Auth cancel")) { String msgformat = FWK_PROP_AUTH_CANCEL_MSG_DEFAULT; if (framework.getPropertyLookup().hasProperty(FWK_PROP_AUTH_CANCEL_MSG)) { msgformat = framework.getProperty(FWK_PROP_AUTH_CANCEL_MSG); } errormsg = MessageFormat.format(msgformat, node.getNodename(), jSchException.getMessage()); reason = NodeStepFailureReason.AuthenticationFailure; } else if (jSchException.getMessage().contains("Auth fail")) { String msgformat = FWK_PROP_AUTH_FAIL_MSG_DEFAULT; if (framework.getPropertyLookup().hasProperty(FWK_PROP_AUTH_FAIL_MSG)) { msgformat = framework.getProperty(FWK_PROP_AUTH_FAIL_MSG); } errormsg = MessageFormat.format(msgformat, node.getNodename(), jSchException.getMessage()); reason = NodeStepFailureReason.AuthenticationFailure; } else { reason = JschFailureReason.SSHProtocolFailure; errormsg = jSchException.getMessage(); } } else { Throwable cause = ExceptionUtils.getRootCause(jSchException); errormsg = cause.getMessage(); if (cause instanceof NoRouteToHostException) { reason = NodeStepFailureReason.ConnectionFailure; } else if (cause instanceof UnknownHostException) { reason = NodeStepFailureReason.HostNotFound; } else if (cause instanceof SocketTimeoutException) { errormsg = "Connection Timeout (after " + timeout + "ms): " + cause.getMessage(); reason = NodeStepFailureReason.ConnectionTimeout; } else if (cause instanceof SocketException) { reason = NodeStepFailureReason.ConnectionFailure; } else { reason = StepFailureReason.Unknown; } } return new ExtractFailure(errormsg, reason); } /** * Shutdown the ExecutorService */ void shutdownAndAwaitTermination(ExecutorService pool) { pool.shutdownNow(); // Disable new tasks from being submitted try { logger.debug("Waiting up to 30 seconds for ExecutorService to shut down"); // Wait a while for existing tasks to terminate if (!pool.awaitTermination(30, TimeUnit.SECONDS)) { logger.debug("Pool did not terminate"); } } catch (InterruptedException ie) { // (Re-)Cancel if current thread also interrupted pool.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); } } final static class NodeSSHConnectionInfo implements SSHTaskBuilder.SSHConnectionInfo { final INodeEntry node; final Framework framework; final ExecutionContext context; FrameworkProject frameworkProject; NodeSSHConnectionInfo(final INodeEntry node, final Framework framework, final ExecutionContext context) { this.node = node; this.framework = framework; this.context = context; this.frameworkProject = framework.getFrameworkProjectMgr() .getFrameworkProject(context.getFrameworkProject()); } public SSHTaskBuilder.AuthenticationType getAuthenticationType() { if (null != node.getAttributes().get(NODE_ATTR_SSH_AUTHENTICATION)) { return SSHTaskBuilder.AuthenticationType .valueOf(node.getAttributes().get(NODE_ATTR_SSH_AUTHENTICATION)); } if (frameworkProject.hasProperty(PROJ_PROP_SSH_AUTHENTICATION)) { return SSHTaskBuilder.AuthenticationType .valueOf(frameworkProject.getProperty(PROJ_PROP_SSH_AUTHENTICATION)); } else if (framework.hasProperty(FWK_PROP_SSH_AUTHENTICATION)) { return SSHTaskBuilder.AuthenticationType .valueOf(framework.getProperty(FWK_PROP_SSH_AUTHENTICATION)); } else { return SSHTaskBuilder.AuthenticationType.privateKey; } } public String getPrivateKeyfilePath() { String path = null; if (null != nonBlank(node.getAttributes().get(NODE_ATTR_SSH_KEYPATH))) { path = node.getAttributes().get(NODE_ATTR_SSH_KEYPATH); } else if (frameworkProject.hasProperty(PROJ_PROP_SSH_KEYPATH)) { path = frameworkProject.getProperty(PROJ_PROP_SSH_KEYPATH); } else if (framework.hasProperty(FWK_PROP_SSH_KEYPATH)) { path = framework.getProperty(FWK_PROP_SSH_KEYPATH); } else if (framework.hasProperty(Constants.SSH_KEYPATH_PROP)) { //return default framework level path = framework.getProperty(Constants.SSH_KEYPATH_PROP); } //expand properties in path if (path != null && path.contains("${")) { path = DataContextUtils.replaceDataReferences(path, context.getDataContext()); } return path; } public InputStream getPrivateKeyResourceData() throws IOException { String privateKeyResourcePath = getPrivateKeyResourcePath(); if (null == privateKeyResourcePath) { return null; } Path path = PathUtil.asPath(privateKeyResourcePath); ResourceMeta contents = context.getStorageTree().getResource(path).getContents(); return contents.getInputStream(); } public String getPrivateKeyResourcePath() { String path = null; if (null != nonBlank(node.getAttributes().get(NODE_ATTR_SSH_KEY_RESOURCE))) { path = node.getAttributes().get(NODE_ATTR_SSH_KEY_RESOURCE); } else if (frameworkProject.hasProperty(PROJ_PROP_SSH_KEY_RESOURCE)) { path = frameworkProject.getProperty(PROJ_PROP_SSH_KEY_RESOURCE); } else if (framework.hasProperty(FWK_PROP_SSH_KEY_RESOURCE)) { path = framework.getProperty(FWK_PROP_SSH_KEY_RESOURCE); } else if (framework.hasProperty(Constants.SSH_KEYRESOURCE_PROP)) { //return default framework level path = framework.getProperty(Constants.SSH_KEYRESOURCE_PROP); } //expand properties in path if (path != null && path.contains("${")) { path = DataContextUtils.replaceDataReferences(path, context.getDataContext()); } return path; } public String getPrivateKeyPassphrase() { if (null != node.getAttributes().get(NODE_ATTR_SSH_KEY_PASSPHRASE_OPTION)) { return evaluateSecureOption(node.getAttributes().get(NODE_ATTR_SSH_KEY_PASSPHRASE_OPTION), context); } else { return evaluateSecureOption(DEFAULT_SSH_KEY_PASSPHRASE_OPTION, context); } } static String evaluateSecureOption(final String optionName, final ExecutionContext context) { if (null == optionName) { logger.debug("option name was null"); return null; } if (null == context.getPrivateDataContext()) { logger.debug("private context was null"); return null; } final String[] opts = optionName.split("\\.", 2); if (null != opts && 2 == opts.length) { final Map<String, String> option = context.getPrivateDataContext().get(opts[0]); if (null != option) { final String value = option.get(opts[1]); if (null == value) { logger.debug("private context '" + optionName + "' was null"); } return value; } else { logger.debug("private context '" + opts[0] + "' was null"); } } return null; } public String getPassword() { if (null != node.getAttributes().get(NODE_ATTR_SSH_PASSWORD_OPTION)) { return evaluateSecureOption(node.getAttributes().get(NODE_ATTR_SSH_PASSWORD_OPTION), context); } else { return evaluateSecureOption(DEFAULT_SSH_PASSWORD_OPTION, context); } } public int getSSHTimeout() { int timeout = 0; if (framework.getPropertyLookup().hasProperty(Constants.SSH_TIMEOUT_PROP)) { final String val = framework.getProperty(Constants.SSH_TIMEOUT_PROP); try { timeout = Integer.parseInt(val); } catch (NumberFormatException e) { } } return timeout; } /** * Return null if the input is null or empty or whitespace, otherwise return the input string trimmed. */ public static String nonBlank(final String input) { if (null == input || "".equals(input.trim())) { return null; } else { return input.trim(); } } public String getUsername() { String user; if (null != nonBlank(node.getUsername()) || node.containsUserName()) { user = nonBlank(node.extractUserName()); } else if (frameworkProject.hasProperty(PROJECT_SSH_USER) && null != nonBlank(frameworkProject.getProperty(PROJECT_SSH_USER))) { user = nonBlank(frameworkProject.getProperty(PROJECT_SSH_USER)); } else { user = nonBlank(framework.getProperty(Constants.SSH_USER_PROP)); } if (null != user && user.contains("${")) { return DataContextUtils.replaceDataReferences(user, context.getDataContext()); } return user; } public static Map<String, String> sshConfigFromFramework(Framework framework) { HashMap<String, String> config = new HashMap<String, String>(); IPropertyLookup propertyLookup = framework.getPropertyLookup(); for (Object o : propertyLookup.getPropertiesMap().keySet()) { String key = (String) o; if (key.startsWith(FWK_SSH_CONFIG_PREFIX)) { String name = key.substring(FWK_SSH_CONFIG_PREFIX.length()); config.put(name, propertyLookup.getProperty(key)); } } return config; } public static Map<String, String> sshConfigFromProject(FrameworkProject frameworkProject) { HashMap<String, String> config = new HashMap<String, String>(); for (Object o : frameworkProject.getProperties().keySet()) { String key = (String) o; if (key.startsWith(PROJ_SSH_CONFIG_PREFIX)) { String name = key.substring(PROJ_SSH_CONFIG_PREFIX.length()); config.put(name, frameworkProject.getProperty(key)); } } return config; } public static Map<String, String> sshConfigFromNode(INodeEntry node) { HashMap<String, String> config = new HashMap<String, String>(); for (String s : node.getAttributes().keySet()) { if (s.startsWith(SSH_CONFIG_PREFIX)) { String name = s.substring(SSH_CONFIG_PREFIX.length()); config.put(name, node.getAttributes().get(s)); } } return config; } @Override public Map<String, String> getSshConfig() { Map<String, String> config = new HashMap<String, String>(); Map<String, String> fwkConfig = sshConfigFromFramework(framework); Map<String, String> projConfig = sshConfigFromProject(frameworkProject); Map<String, String> nodeConfig = sshConfigFromNode(node); if (null != fwkConfig) { config.putAll(fwkConfig); } if (null != projConfig) { config.putAll(projConfig); } if (null != nodeConfig) { config.putAll(nodeConfig); } return config; } } /** * Resolve a node/project/framework property by first checking node attributes named X, then project properties * named "project.X", then framework properties named "framework.X". If none of those exist, return the default * value */ private static String resolveProperty(final String nodeAttribute, final String defaultValue, final INodeEntry node, final FrameworkProject frameworkProject, final Framework framework) { if (null != node.getAttributes().get(nodeAttribute)) { return node.getAttributes().get(nodeAttribute); } else if (frameworkProject.hasProperty(PROJ_PROP_PREFIX + nodeAttribute) && !"".equals(frameworkProject.getProperty(PROJ_PROP_PREFIX + nodeAttribute))) { return frameworkProject.getProperty(PROJ_PROP_PREFIX + nodeAttribute); } else if (framework.hasProperty(FWK_PROP_PREFIX + nodeAttribute)) { return framework.getProperty(FWK_PROP_PREFIX + nodeAttribute); } else { return defaultValue; } } private static int resolveIntProperty(final String attribute, final int defaultValue, final INodeEntry iNodeEntry, final FrameworkProject frameworkProject, final Framework framework) { int value = defaultValue; final String string = resolveProperty(attribute, null, iNodeEntry, frameworkProject, framework); if (null != string) { try { value = Integer.parseInt(string); } catch (NumberFormatException e) { } } return value; } private static long resolveLongProperty(final String attribute, final long defaultValue, final INodeEntry iNodeEntry, final FrameworkProject frameworkProject, final Framework framework) { long value = defaultValue; final String string = resolveProperty(attribute, null, iNodeEntry, frameworkProject, framework); if (null != string) { try { value = Long.parseLong(string); } catch (NumberFormatException e) { } } return value; } private static boolean resolveBooleanProperty(final String attribute, final boolean defaultValue, final INodeEntry iNodeEntry, final FrameworkProject frameworkProject, final Framework framework) { boolean value = defaultValue; final String string = resolveProperty(attribute, null, iNodeEntry, frameworkProject, framework); if (null != string) { value = Boolean.parseBoolean(string); } return value; } /** * Disconnects the SSH connection if the result was not successful. */ private static class DisconnectResultHandler implements ResponderTask.ResultHandler, ExtSSHExec.DisconnectHolder { private volatile ExtSSHExec.Disconnectable disconnectable; private volatile boolean needsDisconnect = false; public void setDisconnectable(final ExtSSHExec.Disconnectable disconnectable) { this.disconnectable = disconnectable; checkAndDisconnect(); } public void handleResult(final boolean success, final String reason) { needsDisconnect = !success; checkAndDisconnect(); } /** * synchronize to prevent race condition on setDisconnectable and handleResult */ private synchronized void checkAndDisconnect() { if (null != disconnectable && needsDisconnect) { disconnectable.disconnect(); needsDisconnect = false; } } } /** * Sudo responder that determines response patterns from node attributes and project properties. Also handles * responder thread result by closing the SSH connection on failure. The mechanism for sudo response: <ol> <li>look * for "[sudo] password for <username>: " (inputSuccessPattern)</li> <li>if not seen in 12 lines, then assume * password is not needed (isSuccessOnInputThreshold=true)</li> <li>if fewer than 12 lines, and no new input in 5000 * milliseconds, then fail (isFailOnInputTimeoutThreshold=true)</li> <li>if seen, output password and carriage * return (inputString)</li> <li>look for "Sorry, try again" (responseFailurePattern)</li> <li>if not seen in 3 * lines, or 5000 milliseconds, then assume success (isFailOnResponseThreshold)</li> <li>if seen, then fail</li> * </ol> */ private static class SudoResponder implements Responder { public static final String DEFAULT_DESCRIPTION = "Sudo execution password response"; private String sudoCommandPattern; private String inputSuccessPattern; private String inputFailurePattern; private String responseSuccessPattern; private String responseFailurePattern; private int inputMaxLines = -1; private long inputMaxTimeout = -1; private boolean failOnInputLinesThreshold = false; private boolean failOnInputTimeoutThreshold = true; private boolean successOnInputThreshold = true; private int responseMaxLines = -1; private long responseMaxTimeout = -1; private boolean failOnResponseThreshold = false; private String inputString; private String configPrefix; private String description; private String defaultSudoPasswordOption; private String defaultSudoCommandPattern; private SudoResponder() { description = DEFAULT_DESCRIPTION; defaultSudoPasswordOption = DEFAULT_SUDO_PASSWORD_OPTION; defaultSudoCommandPattern = DEFAULT_SUDO_COMMAND_PATTERN; configPrefix = SUDO_OPT_PREFIX; } private SudoResponder(final String configPrefix, final String defaultSudoPasswordOption, final String defaultSudoCommandPattern) { this(); if (null != configPrefix) { this.configPrefix = configPrefix; } if (null != defaultSudoPasswordOption) { this.defaultSudoPasswordOption = defaultSudoPasswordOption; } if (null != defaultSudoCommandPattern) { this.defaultSudoCommandPattern = defaultSudoCommandPattern; } } static SudoResponder create(final INodeEntry node, final Framework framework, final ExecutionContext context) { return create(node, framework, context, null, null, null); } static SudoResponder create(final INodeEntry node, final Framework framework, final ExecutionContext context, final String configPrefix, final String defaultSudoPasswordOption, final String defaultSudoCommandPattern) { final SudoResponder sudoResponder = new SudoResponder(configPrefix, defaultSudoPasswordOption, defaultSudoCommandPattern); sudoResponder.init(node, framework.getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()), framework, context); return sudoResponder; } public boolean matchesCommandPattern(final String command) { final String sudoCommandPattern1 = getSudoCommandPattern(); if (null != sudoCommandPattern1) { return Pattern.compile(sudoCommandPattern1).matcher(command).matches(); } else { return false; } } private void init(final INodeEntry node, final FrameworkProject frameworkProject, final Framework framework, final ExecutionContext context) { sudoEnabled = resolveBooleanProperty(configPrefix + NODE_ATTR_SUDO_COMMAND_ENABLED, false, node, frameworkProject, framework); if (sudoEnabled) { final String sudoPassOptname = resolveProperty(configPrefix + NODE_ATTR_SUDO_PASSWORD_OPTION, null, node, frameworkProject, framework); final String sudoPassword = NodeSSHConnectionInfo.evaluateSecureOption( null != sudoPassOptname ? sudoPassOptname : defaultSudoPasswordOption, context); inputString = (null != sudoPassword ? sudoPassword : "") + "\n"; sudoCommandPattern = resolveProperty(configPrefix + NODE_ATTR_SUDO_COMMAND_PATTERN, defaultSudoCommandPattern, node, frameworkProject, framework); inputSuccessPattern = resolveProperty(configPrefix + NODE_ATTR_SUDO_PROMPT_PATTERN, DEFAULT_SUDO_PROMPT_PATTERN, node, frameworkProject, framework); inputFailurePattern = null; responseFailurePattern = resolveProperty(configPrefix + NODE_ATTR_SUDO_FAILURE_PATTERN, DEFAULT_SUDO_FAILURE_PATTERN, node, frameworkProject, framework); responseSuccessPattern = null; inputMaxLines = resolveIntProperty(configPrefix + NODE_ATTR_SUDO_PROMPT_MAX_LINES, DEFAULT_SUDO_PROMPT_MAX_LINES, node, frameworkProject, framework); inputMaxTimeout = resolveLongProperty(configPrefix + NODE_ATTR_SUDO_PROMPT_MAX_TIMEOUT, DEFAULT_SUDO_PROMPT_MAX_TIMEOUT, node, frameworkProject, framework); responseMaxLines = resolveIntProperty(configPrefix + NODE_ATTR_SUDO_RESPONSE_MAX_LINES, DEFAULT_SUDO_RESPONSE_MAX_LINES, node, frameworkProject, framework); responseMaxTimeout = resolveLongProperty(configPrefix + NODE_ATTR_SUDO_RESPONSE_MAX_TIMEOUT, DEFAULT_SUDO_RESPONSE_MAX_TIMEOUT, node, frameworkProject, framework); failOnInputLinesThreshold = resolveBooleanProperty( configPrefix + NODE_ATTR_SUDO_FAIL_ON_PROMPT_MAX_LINES, DEFAULT_SUDO_FAIL_ON_PROMPT_MAX_LINES, node, frameworkProject, framework); failOnInputTimeoutThreshold = resolveBooleanProperty( configPrefix + NODE_ATTR_SUDO_FAIL_ON_PROMPT_TIMEOUT, DEFAULT_SUDO_FAIL_ON_PROMPT_TIMEOUT, node, frameworkProject, framework); failOnResponseThreshold = resolveBooleanProperty( configPrefix + NODE_ATTR_SUDO_FAIL_ON_RESPONSE_TIMEOUT, DEFAULT_SUDO_FAIL_ON_RESPONSE_TIMEOUT, node, frameworkProject, framework); successOnInputThreshold = resolveBooleanProperty( configPrefix + NODE_ATTR_SUDO_SUCCESS_ON_PROMPT_THRESHOLD, DEFAULT_SUDO_SUCCESS_ON_PROMPT_THRESHOLD, node, frameworkProject, framework); } } private boolean sudoEnabled = false; /** * Return true if sudo should be used for the command execution on this node */ public boolean isSudoEnabled() { return sudoEnabled; } public String getInputString() { return inputString; } public boolean isFailOnInputLinesThreshold() { return failOnInputLinesThreshold; } public boolean isFailOnInputTimeoutThreshold() { return failOnInputTimeoutThreshold; } public boolean isFailOnResponseThreshold() { return failOnResponseThreshold; } public boolean isSuccessOnInputThreshold() { return successOnInputThreshold; } /** * Return the regular expression to use to match the command to determine if Sudo should be used. */ public String getSudoCommandPattern() { return sudoCommandPattern; } public String getInputFailurePattern() { return inputFailurePattern; } public String getInputSuccessPattern() { return inputSuccessPattern; } public String getResponseSuccessPattern() { return responseSuccessPattern; } public String getResponseFailurePattern() { return responseFailurePattern; } public int getInputMaxLines() { return inputMaxLines; } public long getInputMaxTimeout() { return inputMaxTimeout; } public int getResponseMaxLines() { return responseMaxLines; } public long getResponseMaxTimeout() { return responseMaxTimeout; } @Override public String toString() { return description; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } static class ExtractFailure { private String errormsg; private FailureReason reason; private ExtractFailure(String errormsg, FailureReason reason) { this.errormsg = errormsg; this.reason = reason; } public String getErrormsg() { return errormsg; } public FailureReason getReason() { return reason; } } }