You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/08/18 13:00:38 UTC
[23/64] incubator-brooklyn git commit: [BROOKLYN-162] Refactor
package in ./core/util
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/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/699b3f65/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/699b3f65/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/699b3f65/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();
+ }
+}