http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/SshTool.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/SshTool.java b/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/SshTool.java new file mode 100644 index 0000000..4361f46 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/SshTool.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.core.util.internal.ssh; + +import static brooklyn.entity.basic.ConfigKeys.newConfigKey; +import static brooklyn.entity.basic.ConfigKeys.newStringConfigKey; + +import java.io.File; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.util.stream.KnownSizeInputStream; +import brooklyn.util.time.Duration; + +/** + * Defines the methods available on the various different implementations of SSH, + * and configuration options which are also generally available. + * <p> + * The config keys in this class can be supplied (or their string equivalents, where the flags/props take {@code Map<String,?>}) + * to influence configuration, either for the tool/session itself or for individual commands. + * <p> + * To specify some of these properties on a global basis, use the variants of the keys here + * contained in {@link ConfigKeys} + * (which are generally {@value #BROOKLYN_CONFIG_KEY_PREFIX} prefixed to the names of keys here). + */ +public interface SshTool extends ShellTool { + + /** Public-facing global config keys for Brooklyn are defined in ConfigKeys, + * and have this prefix pre-prended to the config keys in this class. + * These keys are detected from entity/global config and automatically applied to ssh executions. */ + public static final String BROOKLYN_CONFIG_KEY_PREFIX = "brooklyn.ssh.config."; + + public static final ConfigKey<String> PROP_TOOL_CLASS = newStringConfigKey("tool.class", "SshTool implementation to use", null); + + public static final ConfigKey<String> PROP_HOST = newStringConfigKey("host", "Host to connect to (required)", null); + public static final ConfigKey<Integer> PROP_PORT = newConfigKey("port", "Port on host to connect to", 22); + public static final ConfigKey<String> PROP_USER = newConfigKey("user", "User to connect as", System.getProperty("user.name")); + public static final ConfigKey<String> PROP_PASSWORD = newStringConfigKey("password", "Password to use to connect", null); + + public static final ConfigKey<String> PROP_PRIVATE_KEY_FILE = newStringConfigKey("privateKeyFile", "the path of an ssh private key file; leave blank to use defaults (i.e. ~/.ssh/id_rsa and id_dsa)", null); + public static final ConfigKey<String> PROP_PRIVATE_KEY_DATA = newStringConfigKey("privateKeyData", "the private ssh key (e.g. contents of an id_rsa or id_dsa file)", null); + public static final ConfigKey<String> PROP_PRIVATE_KEY_PASSPHRASE = newStringConfigKey("privateKeyPassphrase", "the passphrase for the ssh private key", null); + public static final ConfigKey<Boolean> PROP_STRICT_HOST_KEY_CHECKING = newConfigKey("strictHostKeyChecking", "whether to check the remote host's identification; defaults to false", false); + public static final ConfigKey<Boolean> PROP_ALLOCATE_PTY = newConfigKey("allocatePTY", "whether to allocate PTY (vt100); if true then stderr is sent to stdout, but sometimes required for sudo'ing due to requiretty", false); + + public static final ConfigKey<Long> PROP_CONNECT_TIMEOUT = newConfigKey("connectTimeout", "Timeout in millis when establishing an SSH connection; if 0 then uses default (usually 30s)", 0L); + public static final ConfigKey<Long> PROP_SESSION_TIMEOUT = newConfigKey("sessionTimeout", "Timeout in millis for an ssh session; if 0 then uses default", 0L); + public static final ConfigKey<Integer> PROP_SSH_TRIES = newConfigKey("sshTries", "Max number of times to attempt ssh operations", 4); + public static final ConfigKey<Long> PROP_SSH_TRIES_TIMEOUT = newConfigKey("sshTriesTimeout", "Time limit for attempting retries; will not interrupt tasks, but stops retrying after a total amount of elapsed time", Duration.TWO_MINUTES.toMilliseconds()); + public static final ConfigKey<Long> PROP_SSH_RETRY_DELAY = newConfigKey("sshRetryDelay", "Time (in milliseconds) before first ssh-retry, after which it will do exponential backoff", 50L); + + // NB -- items above apply for _session_ (a tool), below apply for a _call_ + // TODO would be nice to track which arguments are used, so we can indicate whether extras are supplied + + public static final ConfigKey<String> PROP_PERMISSIONS = newConfigKey("permissions", "Default permissions for files copied/created on remote machine; must be four-digit octal string, default '0644'", "0644"); + public static final ConfigKey<Long> PROP_LAST_MODIFICATION_DATE = newConfigKey("lastModificationDate", "Last-modification-date to be set on files copied/created (should be UTC/1000, ie seconds since 1970; default 0 usually means current)", 0L); + public static final ConfigKey<Long> PROP_LAST_ACCESS_DATE = newConfigKey("lastAccessDate", "Last-access-date to be set on files copied/created (should be UTC/1000, ie seconds since 1970; default 0 usually means lastModificationDate)", 0L); + public static final ConfigKey<Integer> PROP_OWNER_UID = newConfigKey("ownerUid", "Default owner UID (not username) for files created on remote machine; default is unset", -1); + + // TODO remove unnecessary "public static final" modifiers + + // TODO Could define the following in SshMachineLocation, or some such? + //public static ConfigKey<String> PROP_LOG_PREFIX = newStringKey("logPrefix", "???", ???); + //public static ConfigKey<Boolean> PROP_NO_STDOUT_LOGGING = newStringKey("noStdoutLogging", "???", ???); + //public static ConfigKey<Boolean> PROP_NO_STDOUT_LOGGING = newStringKey("noStdoutLogging", "???", ???); + + /** + * @throws SshException + */ + public void connect(); + + /** + * @deprecated since 0.7.0; (since much earlier) this ignores the argument in favour of {@link #PROP_SSH_TRIES} + * + * @param maxAttempts + * @throws SshException + */ + public void connect(int maxAttempts); + + public void disconnect(); + + public boolean isConnected(); + + /** + * @see super{@link #execScript(Map, List, Map)} + * @throws SshException If failed to connect + */ + @Override + public int execScript(Map<String,?> props, List<String> commands, Map<String,?> env); + + /** + * @see #execScript(Map, List, Map) + */ + @Override + public int execScript(Map<String,?> props, List<String> commands); + + /** + * @see super{@link #execCommands(Map, List, Map)} + * @throws SshException If failed to connect + */ + @Override + public int execCommands(Map<String,?> properties, List<String> commands, Map<String,?> env); + + /** + * @see #execCommands(Map, List, Map) + */ + @Override + public int execCommands(Map<String,?> properties, List<String> commands); + + /** + * Copies the file to the server at the given path. + * If path is null, empty, '.', '..', or ends with '/' then file name is used. + * <p> + * The file will not preserve the permission of last _access_ date. + * + * Optional properties are: + * <ul> + * <li>'permissions' (e.g. "0644") - see {@link #PROP_PERMISSIONS} + * <li>'lastModificationDate' see {@link #PROP_LAST_MODIFICATION_DATE}; not supported by all SshTool implementations + * <li>'lastAccessDate' see {@link #PROP_LAST_ACCESS_DATE}; not supported by all SshTool implementations + * </ul> + * + * @return exit code (not supported by all SshTool implementations, usually throwing on error; + * sometimes possibly returning 0 even on error (?) ) + */ + public int copyToServer(Map<String,?> props, File localFile, String pathAndFileOnRemoteServer); + + /** + * Closes the given input stream before returning. + * Consider using {@link KnownSizeInputStream} for efficiency when the size of the stream is known. + * + * @see #copyToServer(Map, File, String) + */ + public int copyToServer(Map<String,?> props, InputStream contents, String pathAndFileOnRemoteServer); + + /** + * @see #copyToServer(Map, File, String) + */ + public int copyToServer(Map<String,?> props, byte[] contents, String pathAndFileOnRemoteServer); + + /** + * Copies the file from the server at the given path. + * + * @return exit code (not supported by all SshTool implementations, usually throwing on error; + * sometimes possibly returning 0 even on error (?) ) + */ + public int copyFromServer(Map<String,?> props, String pathAndFileOnRemoteServer, File local); + + // TODO might be more efficicent than copyFrom by way of temp file +// /** +// * Reads from the file at the given path on the remote server. +// */ +// public InputStream streamFromServer(Map<String,?> props, String pathAndFileOnRemoteServer); + +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliTool.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliTool.java b/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliTool.java new file mode 100644 index 0000000..5fe0408 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliTool.java @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.core.util.internal.ssh.cli; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.core.util.internal.ssh.SshAbstractTool; +import org.apache.brooklyn.core.util.internal.ssh.SshTool; +import org.apache.brooklyn.core.util.internal.ssh.cli.SshCliTool; +import org.apache.brooklyn.core.util.internal.ssh.process.ProcessTool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.text.StringEscapes.BashStringEscapes; +import brooklyn.util.text.Strings; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +/** + * For ssh and scp commands, delegating to system calls. + */ +public class SshCliTool extends SshAbstractTool implements SshTool { + + // TODO No retry support, with backoffLimitedRetryHandler + + private static final Logger LOG = LoggerFactory.getLogger(SshCliTool.class); + + public static final ConfigKey<String> PROP_SSH_EXECUTABLE = ConfigKeys.newStringConfigKey("sshExecutable", "command to execute for ssh (defaults to \"ssh\", but could be overridden to sshg3 for Tectia for example)", "ssh"); + public static final ConfigKey<String> PROP_SSH_FLAGS = ConfigKeys.newStringConfigKey("sshFlags", "flags to pass to ssh, as a space separated list", ""); + public static final ConfigKey<String> PROP_SCP_EXECUTABLE = ConfigKeys.newStringConfigKey("scpExecutable", "command to execute for scp (defaults to \"scp\", but could be overridden to scpg3 for Tectia for example)", "scp"); + + public static Builder<SshCliTool,?> builder() { + return new ConcreteBuilder(); + } + + private static class ConcreteBuilder extends Builder<SshCliTool, ConcreteBuilder> { + } + + public static class Builder<T extends SshCliTool, B extends Builder<T,B>> extends AbstractSshToolBuilder<T,B> { + private String sshExecutable; + private String sshFlags; + private String scpExecutable; + + @Override + public B from(Map<String,?> props) { + super.from(props); + sshExecutable = getOptionalVal(props, PROP_SSH_EXECUTABLE); + sshFlags = getOptionalVal(props, PROP_SSH_FLAGS); + scpExecutable = getOptionalVal(props, PROP_SCP_EXECUTABLE); + return self(); + } + public B sshExecutable(String val) { + this.sshExecutable = val; return self(); + } + public B scpExecutable(String val) { + this.scpExecutable = val; return self(); + } + @SuppressWarnings("unchecked") + public T build() { + return (T) new SshCliTool(this); + } + } + + private final String sshExecutable; + private final String sshFlags; + private final String scpExecutable; + + public SshCliTool(Map<String,?> map) { + this(builder().from(map)); + } + + private SshCliTool(Builder<?,?> builder) { + super(builder); + sshExecutable = checkNotNull(builder.sshExecutable); + sshFlags = checkNotNull(builder.sshFlags); + scpExecutable = checkNotNull(builder.scpExecutable); + if (LOG.isTraceEnabled()) LOG.trace("Created SshCliTool {} ({})", this, System.identityHashCode(this)); + } + + @Override + public void connect() { + // no-op + } + + @Override + public void connect(int maxAttempts) { + // no-op + } + + @Override + public void disconnect() { + if (LOG.isTraceEnabled()) LOG.trace("Disconnecting SshCliTool {} ({}) - no-op", this, System.identityHashCode(this)); + // no-op + } + + @Override + public boolean isConnected() { + // TODO Always pretends to be connected + return true; + } + + @Override + public int copyToServer(java.util.Map<String,?> props, byte[] contents, String pathAndFileOnRemoteServer) { + return copyTempFileToServer(props, writeTempFile(contents), pathAndFileOnRemoteServer); + } + + @Override + public int copyToServer(java.util.Map<String,?> props, InputStream contents, String pathAndFileOnRemoteServer) { + return copyTempFileToServer(props, writeTempFile(contents), pathAndFileOnRemoteServer); + } + + @Override + public int copyToServer(Map<String,?> props, File f, String pathAndFileOnRemoteServer) { + if (hasVal(props, PROP_LAST_MODIFICATION_DATE)) { + LOG.warn("Unsupported ssh feature, setting lastModificationDate for {}:{}", this, pathAndFileOnRemoteServer); + } + if (hasVal(props, PROP_LAST_ACCESS_DATE)) { + LOG.warn("Unsupported ssh feature, setting lastAccessDate for {}:{}", this, pathAndFileOnRemoteServer); + } + String permissions = getOptionalVal(props, PROP_PERMISSIONS); + + int uid = getOptionalVal(props, PROP_OWNER_UID); + + int result = scpToServer(props, f, pathAndFileOnRemoteServer); + if (result == 0) { + result = chmodOnServer(props, permissions, pathAndFileOnRemoteServer); + if (result == 0) { + if (uid != -1) { + result = chownOnServer(props, uid, pathAndFileOnRemoteServer); + if (result != 0) { + LOG.warn("Error setting file owner to {}, after copying file {} to {}:{}; exit code {}", new Object[] { uid, pathAndFileOnRemoteServer, this, f, result }); + } + } + } else { + LOG.warn("Error setting file permissions to {}, after copying file {} to {}:{}; exit code {}", new Object[] { permissions, pathAndFileOnRemoteServer, this, f, result }); + } + } else { + LOG.warn("Error copying file {} to {}:{}; exit code {}", new Object[] {pathAndFileOnRemoteServer, this, f, result}); + } + return result; + } + + private int chownOnServer(Map<String,?> props, int uid, String remote) { + return sshExec(props, "chown "+uid+" "+remote); + } + + private int copyTempFileToServer(Map<String,?> props, File f, String pathAndFileOnRemoteServer) { + try { + return copyToServer(props, f, pathAndFileOnRemoteServer); + } finally { + f.delete(); + } + } + + @Override + public int copyFromServer(Map<String,?> props, String pathAndFileOnRemoteServer, File localFile) { + return scpFromServer(props, pathAndFileOnRemoteServer, localFile); + } + + @Override + public int execScript(final Map<String,?> props, final List<String> commands, final Map<String,?> env) { + return new ToolAbstractExecScript(props) { + public int run() { + String scriptContents = toScript(props, commands, env); + if (LOG.isTraceEnabled()) LOG.trace("Running shell command at {} as script: {}", host, scriptContents); + copyTempFileToServer(ImmutableMap.of("permissions", "0700"), writeTempFile(scriptContents), scriptPath); + + String cmd = Strings.join(buildRunScriptCommand(), separator); + return asInt(sshExec(props, cmd), -1); + } + }.run(); + } + + @Override + public int execCommands(Map<String,?> props, List<String> commands, Map<String,?> env) { + Map<String,Object> props2 = new MutableMap<String,Object>(); + if (props!=null) props2.putAll(props); + props2.put(SshTool.PROP_NO_EXTRA_OUTPUT.getName(), true); + return execScript(props2, commands, env); + } + + private int scpToServer(Map<String,?> props, File local, String remote) { + String to = (Strings.isEmpty(getUsername()) ? "" : getUsername()+"@")+getHostAddress()+":"+remote; + return scpExec(props, local.getAbsolutePath(), to); + } + + private int scpFromServer(Map<String,?> props, String remote, File local) { + String from = (Strings.isEmpty(getUsername()) ? "" : getUsername()+"@")+getHostAddress()+":"+remote; + return scpExec(props, from, local.getAbsolutePath()); + } + + private int chmodOnServer(Map<String,?> props, String permissions, String remote) { + return sshExec(props, "chmod "+permissions+" "+remote); + } + + private int scpExec(Map<String,?> props, String from, String to) { + File tempFile = null; + try { + List<String> cmd = Lists.newArrayList(); + cmd.add(getOptionalVal(props, PROP_SCP_EXECUTABLE, scpExecutable)); + if (privateKeyFile != null) { + cmd.add("-i"); + cmd.add(privateKeyFile.getAbsolutePath()); + } else if (privateKeyData != null) { + tempFile = writeTempFile(privateKeyData); + cmd.add("-i"); + cmd.add(tempFile.getAbsolutePath()); + } + if (!strictHostKeyChecking) { + cmd.add("-o"); + cmd.add("StrictHostKeyChecking=no"); + } + if (port != 22) { + cmd.add("-P"); + cmd.add(""+port); + } + cmd.add(from); + cmd.add(to); + + if (LOG.isTraceEnabled()) LOG.trace("Executing with command: {}", cmd); + int result = execProcess(props, cmd); + + if (LOG.isTraceEnabled()) LOG.trace("Executed command: {}; exit code {}", cmd, result); + return result; + + } finally { + if (tempFile != null) tempFile.delete(); + } + } + + private int sshExec(Map<String,?> props, String command) { + File tempKeyFile = null; + try { + List<String> cmd = Lists.newArrayList(); + cmd.add(getOptionalVal(props, PROP_SSH_EXECUTABLE, sshExecutable)); + String propsFlags = getOptionalVal(props, PROP_SSH_FLAGS, sshFlags); + if (propsFlags!=null && propsFlags.trim().length()>0) + cmd.addAll(Arrays.asList(propsFlags.trim().split(" "))); + if (privateKeyFile != null) { + cmd.add("-i"); + cmd.add(privateKeyFile.getAbsolutePath()); + } else if (privateKeyData != null) { + tempKeyFile = writeTempFile(privateKeyData); + cmd.add("-i"); + cmd.add(tempKeyFile.getAbsolutePath()); + } + if (!strictHostKeyChecking) { + cmd.add("-o"); + cmd.add("StrictHostKeyChecking=no"); + } + if (port != 22) { + cmd.add("-P"); + cmd.add(""+port); + } + if (allocatePTY) { + // have to be careful with double -tt as it can leave a shell session active + // when done from bash (ie ssh -tt localhost < /tmp/myscript.sh); + // hover that doesn't seem to be a problem the way we use it from brooklyn + // (and note single -t doesn't work _programmatically_ since the input isn't a terminal) + cmd.add("-tt"); + } + cmd.add((Strings.isEmpty(getUsername()) ? "" : getUsername()+"@")+getHostAddress()); + + cmd.add("bash -c "+BashStringEscapes.wrapBash(command)); + // previously we tried these approaches: + //cmd.add("$(<"+tempCmdFile.getAbsolutePath()+")"); + // only pays attention to the first word; the "; echo Executing ..." get treated as arguments + // to the script in the first word, when invoked from java (when invoked from prompt the behaviour is as desired) + //cmd.add("\""+command+"\""); + // only works if command is a single word + //cmd.add(tempCmdFile.getAbsolutePath()); + // above of course only works if the metafile is copied across + + if (LOG.isTraceEnabled()) LOG.trace("Executing ssh with command: {} (with {})", command, cmd); + int result = execProcess(props, cmd); + + if (LOG.isTraceEnabled()) LOG.trace("Executed command: {}; exit code {}", cmd, result); + return result; + + } finally { + if (tempKeyFile != null) tempKeyFile.delete(); + } + } + + private int execProcess(Map<String,?> props, List<String> cmdWords) { + OutputStream out = getOptionalVal(props, PROP_OUT_STREAM); + OutputStream err = getOptionalVal(props, PROP_ERR_STREAM); + return ProcessTool.execSingleProcess(cmdWords, null, (File)null, out, err, this); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessTool.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessTool.java b/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessTool.java new file mode 100644 index 0000000..8f33143 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessTool.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.core.util.internal.ssh.process; + +import static brooklyn.entity.basic.ConfigKeys.newConfigKey; +import static brooklyn.entity.basic.ConfigKeys.newStringConfigKey; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.core.util.internal.ssh.ShellAbstractTool; +import org.apache.brooklyn.core.util.internal.ssh.ShellTool; +import org.apache.brooklyn.core.util.internal.ssh.SshException; +import org.apache.brooklyn.core.util.internal.ssh.process.ProcessTool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.config.ConfigKey; +import brooklyn.util.collections.MutableList; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.os.Os; +import brooklyn.util.stream.StreamGobbler; +import brooklyn.util.text.Strings; + +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; + +/** Implementation of {@link ShellTool} which runs locally. */ +public class ProcessTool extends ShellAbstractTool implements ShellTool { + + private static final Logger LOG = LoggerFactory.getLogger(ProcessTool.class); + + // applies to calls + + public static final ConfigKey<Boolean> PROP_LOGIN_SHELL = newConfigKey("loginShell", "Causes the commands to be invoked with bash arguments to forcea login shell", Boolean.FALSE); + + public static final ConfigKey<String> PROP_DIRECTORY = newStringConfigKey("directory", "the working directory, for executing commands", null); + + public ProcessTool() { + this(null); + } + + public ProcessTool(Map<String,?> flags) { + super(getOptionalVal(flags, PROP_LOCAL_TEMP_DIR)); + if (flags!=null) { + MutableMap<String, Object> flags2 = MutableMap.copyOf(flags); + // TODO should remember other flags here? (e.g. NO_EXTRA_OUTPUT, RUN_AS_ROOT, etc) + flags2.remove(PROP_LOCAL_TEMP_DIR.getName()); + if (!flags2.isEmpty()) + LOG.warn(""+this+" ignoring unsupported constructor flags: "+flags); + } + } + + @Override + public int execScript(final Map<String,?> props, final List<String> commands, final Map<String,?> env) { + return new ToolAbstractExecScript(props) { + public int run() { + try { + String directory = getOptionalVal(props, PROP_DIRECTORY); + File directoryDir = (directory != null) ? new File(Os.tidyPath(directory)) : null; + + String scriptContents = toScript(props, commands, env); + + if (LOG.isTraceEnabled()) LOG.trace("Running shell process (process) as script:\n{}", scriptContents); + File to = new File(scriptPath); + Files.createParentDirs(to); + ByteSource.wrap(scriptContents.getBytes()).copyTo(Files.asByteSink(to)); + + List<String> cmds = buildRunScriptCommand(); + cmds.add(0, "chmod +x "+scriptPath); + return asInt(execProcesses(cmds, null, directoryDir, out, err, separator, getOptionalVal(props, PROP_LOGIN_SHELL), this), -1); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + }.run(); + } + + @Override + public int execCommands(Map<String,?> props, List<String> commands, Map<String,?> env) { + if (Boolean.FALSE.equals(props.get("blocks"))) { + throw new IllegalArgumentException("Cannot exec non-blocking: command="+commands); + } + OutputStream out = getOptionalVal(props, PROP_OUT_STREAM); + OutputStream err = getOptionalVal(props, PROP_ERR_STREAM); + String separator = getOptionalVal(props, PROP_SEPARATOR); + String directory = getOptionalVal(props, PROP_DIRECTORY); + File directoryDir = (directory != null) ? new File(Os.tidyPath(directory)) : null; + + List<String> allcmds = toCommandSequence(commands, null); + + String singlecmd = Joiner.on(separator).join(allcmds); + if (Boolean.TRUE.equals(getOptionalVal(props, PROP_RUN_AS_ROOT))) { + LOG.warn("Cannot run as root when executing as command; run as a script instead (will run as normal user): "+singlecmd); + } + if (LOG.isTraceEnabled()) LOG.trace("Running shell command (process): {}", singlecmd); + + return asInt(execProcesses(allcmds, env, directoryDir, out, err, separator, getOptionalVal(props, PROP_LOGIN_SHELL), this), -1); + } + + /** + * as {@link #execProcesses(List, Map, OutputStream, OutputStream, String, boolean, Object)} but not using a login shell + * @deprecated since 0.7; use {@link #execProcesses(List, Map, File, OutputStream, OutputStream, String, boolean, Object)} + */ + @Deprecated + public static int execProcesses(List<String> cmds, Map<String,?> env, OutputStream out, OutputStream err, String separator, Object contextForLogging) { + return execProcesses(cmds, env, (File)null, out, err, separator, false, contextForLogging); + } + + /** + * @deprecated since 0.7; use {@link #execProcesses(List, Map, File, OutputStream, OutputStream, String, boolean, Object)} + */ + @Deprecated + public static int execProcesses(List<String> cmds, Map<String,?> env, OutputStream out, OutputStream err, String separator, boolean asLoginShell, Object contextForLogging) { + return execProcesses(cmds, env, (File)null, out, err, separator, asLoginShell, contextForLogging); + } + + /** executes a set of commands by sending them as a single process to `bash -c` + * (single command argument of all the commands, joined with separator) + * <p> + * consequence of this is that you should not normally need to escape things oddly in your commands, + * type them just as you would into a bash shell (if you find exceptions please note them here!) + */ + public static int execProcesses(List<String> cmds, Map<String,?> env, File directory, OutputStream out, OutputStream err, String separator, boolean asLoginShell, Object contextForLogging) { + MutableList<String> commands = new MutableList<String>().append("bash"); + if (asLoginShell) commands.append("-l"); + commands.append("-c", Strings.join(cmds, Preconditions.checkNotNull(separator, "separator"))); + return execSingleProcess(commands, env, directory, out, err, contextForLogging); + } + + /** + * @deprecated since 0.7; use {@link #execSingleProcess(List, Map, File, OutputStream, OutputStream, Object)} + */ + @Deprecated + public static int execSingleProcess(List<String> cmdWords, Map<String,?> env, OutputStream out, OutputStream err, Object contextForLogging) { + return execSingleProcess(cmdWords, env, (File)null, out, err, contextForLogging); + } + + /** executes a single process made up of the given command words (*not* bash escaped); + * should be portable across OS's */ + public static int execSingleProcess(List<String> cmdWords, Map<String,?> env, File directory, OutputStream out, OutputStream err, Object contextForLogging) { + StreamGobbler errgobbler = null; + StreamGobbler outgobbler = null; + + ProcessBuilder pb = new ProcessBuilder(cmdWords); + if (env!=null) { + for (Map.Entry<String,?> kv: env.entrySet()) pb.environment().put(kv.getKey(), String.valueOf(kv.getValue())); + } + if (directory != null) { + pb.directory(directory); + } + + try { + Process p = pb.start(); + + if (out != null) { + InputStream outstream = p.getInputStream(); + outgobbler = new StreamGobbler(outstream, out, (Logger) null); + outgobbler.start(); + } + if (err != null) { + InputStream errstream = p.getErrorStream(); + errgobbler = new StreamGobbler(errstream, err, (Logger) null); + errgobbler.start(); + } + + int result = p.waitFor(); + + if (outgobbler != null) outgobbler.blockUntilFinished(); + if (errgobbler != null) errgobbler.blockUntilFinished(); + + if (result==255) + // this is not definitive, but tests (and code?) expects throw exception if can't connect; + // only return exit code when it is exit code from underlying process; + // we have no way to distinguish 255 from ssh failure from 255 from the command run through ssh ... + // but probably 255 is from CLI ssh + throw new SshException("exit code 255 from CLI ssh; probably failed to connect"); + + return result; + } catch (InterruptedException e) { + throw Exceptions.propagate(e); + } catch (IOException e) { + throw Exceptions.propagate(e); + } finally { + closeWhispering(outgobbler, contextForLogging, "execProcess"); + closeWhispering(errgobbler, contextForLogging, "execProcess"); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjClientConnection.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjClientConnection.java b/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjClientConnection.java new file mode 100644 index 0000000..c042415 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjClientConnection.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.core.util.internal.ssh.sshj; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.File; +import java.io.IOException; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; +import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; +import net.schmizz.sshj.userauth.password.PasswordUtils; + +import org.apache.brooklyn.core.util.internal.ssh.SshAbstractTool.SshAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.util.GroovyJavaMethods; + +import com.google.common.base.Objects; +import com.google.common.net.HostAndPort; + +/** based on code from jclouds */ +public class SshjClientConnection implements SshAction<SSHClient> { + + private static final Logger LOG = LoggerFactory.getLogger(SshjClientConnection.class); + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + protected HostAndPort hostAndPort; + protected String username; + protected String password; + protected String privateKeyPassphrase; + protected String privateKeyData; + protected File privateKeyFile; + protected long connectTimeout; + protected long sessionTimeout; + protected boolean strictHostKeyChecking; + + public Builder hostAndPort(HostAndPort hostAndPort) { + this.hostAndPort = hostAndPort; + return this; + } + + public Builder username(String username) { + this.username = username; + return this; + } + + public Builder password(String val) { + this.password = val; + return this; + } + + /** @deprecated use privateKeyData */ + public Builder privateKey(String val) { + this.privateKeyData = val; + return this; + } + + public Builder privateKeyPassphrase(String val) { + this.privateKeyPassphrase = val; + return this; + } + + public Builder privateKeyData(String val) { + this.privateKeyData = val; + return this; + } + + public Builder privateKeyFile(File val) { + this.privateKeyFile = val; + return this; + } + + public Builder strictHostKeyChecking(boolean val) { + this.strictHostKeyChecking = val; + return this; + } + + public Builder connectTimeout(long connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder sessionTimeout(long sessionTimeout) { + this.sessionTimeout = sessionTimeout; + return this; + } + + public SshjClientConnection build() { + return new SshjClientConnection(this); + } + + protected static Builder fromSSHClientConnection(SshjClientConnection in) { + return new Builder().hostAndPort(in.getHostAndPort()).connectTimeout(in.getConnectTimeout()).sessionTimeout( + in.getSessionTimeout()).username(in.username).password(in.password).privateKey(in.privateKeyData).privateKeyFile(in.privateKeyFile); + } + } + + private final HostAndPort hostAndPort; + private final String username; + private final String password; + private final String privateKeyPassphrase; + private final String privateKeyData; + private final File privateKeyFile; + private final boolean strictHostKeyChecking; + private final int connectTimeout; + private final int sessionTimeout; + + SSHClient ssh; + + private SshjClientConnection(Builder builder) { + this.hostAndPort = checkNotNull(builder.hostAndPort); + this.username = builder.username; + this.password = builder.password; + this.privateKeyPassphrase = builder.privateKeyPassphrase; + this.privateKeyData = builder.privateKeyData; + this.privateKeyFile = builder.privateKeyFile; + this.strictHostKeyChecking = builder.strictHostKeyChecking; + this.connectTimeout = checkInt("connectTimeout", builder.connectTimeout, Integer.MAX_VALUE); + this.sessionTimeout = checkInt("sessionTimeout", builder.sessionTimeout, Integer.MAX_VALUE); + } + + static Integer checkInt(String context, long value, Integer ifTooLarge) { + if (value > Integer.MAX_VALUE) { + LOG.warn("Value '"+value+"' for "+context+" too large in SshjClientConnection; using "+value); + return ifTooLarge; + } + return (int)value; + } + + public boolean isConnected() { + return ssh != null && ssh.isConnected(); + } + + public boolean isAuthenticated() { + return ssh != null && ssh.isAuthenticated(); + } + + @Override + public void clear() { + if (ssh != null && ssh.isConnected()) { + try { + if (LOG.isTraceEnabled()) LOG.trace("Disconnecting SshjClientConnection {} ({})", this, System.identityHashCode(this)); + ssh.disconnect(); + } catch (IOException e) { + if (LOG.isDebugEnabled()) LOG.debug("<< exception disconnecting from {}: {}", e, e.getMessage()); + } + } + ssh = null; + } + + @Override + public SSHClient create() throws Exception { + if (LOG.isTraceEnabled()) LOG.trace("Connecting SshjClientConnection {} ({})", this, System.identityHashCode(this)); + ssh = new net.schmizz.sshj.SSHClient(); + if (!strictHostKeyChecking) { + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + } + if (connectTimeout != 0) { + ssh.setConnectTimeout(connectTimeout); + } + if (sessionTimeout != 0) { + ssh.setTimeout(sessionTimeout); + } + ssh.connect(hostAndPort.getHostText(), hostAndPort.getPortOrDefault(22)); + + if (password != null) { + ssh.authPassword(username, password); + } else if (privateKeyData != null) { + OpenSSHKeyFile key = new OpenSSHKeyFile(); + key.init(privateKeyData, null, + GroovyJavaMethods.truth(privateKeyPassphrase) ? + PasswordUtils.createOneOff(privateKeyPassphrase.toCharArray()) + : null); + ssh.authPublickey(username, key); + } else if (privateKeyFile != null) { + OpenSSHKeyFile key = new OpenSSHKeyFile(); + key.init(privateKeyFile, + GroovyJavaMethods.truth(privateKeyPassphrase) ? + PasswordUtils.createOneOff(privateKeyPassphrase.toCharArray()) + : null); + ssh.authPublickey(username, key); + } else { + // Accept defaults (in ~/.ssh) + ssh.authPublickey(username); + } + + return ssh; + } + + /** + * @return host and port, where port if not present defaults to {@code 22} + */ + public HostAndPort getHostAndPort() { + return hostAndPort; + } + + /** + * @return username used in this ssh + */ + public String getUsername() { + return username; + } + + /** + * + * @return how long to wait for the initial connection to be made + */ + public int getConnectTimeout() { + return connectTimeout; + } + + /** + * + * @return how long to keep the ssh open, or {@code 0} for indefinitely + */ + public int getSessionTimeout() { + return sessionTimeout; + } + + /** + * + * @return the current ssh or {@code null} if not connected + */ + public SSHClient getSSHClient() { + return ssh; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + SshjClientConnection that = SshjClientConnection.class.cast(o); + return equal(this.hostAndPort, that.hostAndPort) && equal(this.username, that.username) + && equal(this.password, that.password) && equal(this.privateKeyData, that.privateKeyData) + && equal(this.privateKeyFile, that.privateKeyFile) && equal(this.ssh, that.ssh); + } + + @Override + public int hashCode() { + return Objects.hashCode(hostAndPort, username, password, privateKeyData, ssh); + } + + @Override + public String toString() { + return Objects.toStringHelper("") + .add("hostAndPort", hostAndPort) + .add("user", username) + .add("ssh", ssh != null ? ssh.hashCode() : null) + .add("password", (password != null ? "xxxxxx" : null)) + .add("privateKeyFile", privateKeyFile) + .add("privateKey", (privateKeyData != null ? "xxxxxx" : null)) + .add("connectTimeout", connectTimeout) + .add("sessionTimeout", sessionTimeout).toString(); + } +}
