You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by ha...@apache.org on 2015/08/17 21:17:57 UTC

[26/42] incubator-brooklyn git commit: [BROOKLYN-162] Refactor package in ./core/util

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();
+    }
+}