Merge remote-tracking branch 'aledsage/fix/IntegrationTests-20130124'

Fix conflicts in core/src/test/java/brooklyn/entity/basic/EffectorConcatenateTest
(converted to java in other pull request, adds destroyAll here)
diff --git a/core/src/main/java/brooklyn/location/basic/SshMachineLocation.java b/core/src/main/java/brooklyn/location/basic/SshMachineLocation.java
index 4fd3eae..65b58c9 100644
--- a/core/src/main/java/brooklyn/location/basic/SshMachineLocation.java
+++ b/core/src/main/java/brooklyn/location/basic/SshMachineLocation.java
@@ -530,47 +530,27 @@
     public int copyTo(InputStream src, String destination) {
         return copyTo(MutableMap.<String,Object>of(), src, destination);
     }
-	public int copyTo(Map<String,?> props, InputStream src, String destination) {
-		return copyTo(props, src, -1, destination);
-	}
     public int copyTo(InputStream src, long filesize, String destination) {
         return copyTo(MutableMap.<String,Object>of(), src, filesize, destination);
     }
     // FIXME the return code is not a reliable indicator of success or failure
-    public int copyTo(final Map<String,?> props, InputStream src, long filesize, final String destination) {
-	    final long finalFilesize;
-	    final InputStream finalSrc;
-	    File tempFile = null;
-	    
-	    try {
-    		if (filesize==-1) {
-    		    try {
-    		        // TODO Use ConfigKeys.BROOKLYN_DATA_DIR, but how to get access to that here?
-    		        tempFile = ResourceUtils.writeToTempFile(src, localTempDir, "sshcopy", "data");
-    		        tempFile.setReadable(false, false);
-                    tempFile.setReadable(true, true);
-    	            tempFile.setWritable(false);
-    	            tempFile.setExecutable(false);
-    		        finalFilesize = tempFile.length();
-    		        finalSrc = new FileInputStream(tempFile);
-    		    } catch (IOException e) {
-    		        throw Throwables.propagate(e);
-    		    } finally {
-    		        Closeables.closeQuietly(src);
-    		    }
-    		} else {
-    		    finalFilesize = filesize;
-    		    finalSrc = src;
-    		}
-    		
+    public int copyTo(final Map<String,?> props, final InputStream src, final long filesize, final String destination) {
+        if (filesize == -1) {
+            return copyTo(props, src, destination);
+        } else {
             return execSsh(props, new Function<SshTool,Integer>() {
                 public Integer apply(SshTool ssh) {
-                    return ssh.createFile(props, destination, finalSrc, finalFilesize);
+                    return ssh.createFile(props, destination, src, filesize);
                 }});
-            
-	    } finally {
-	        if (tempFile != null) tempFile.delete();
-	    }
+        }
+    }
+    // FIXME the return code is not a reliable indicator of success or failure
+    // Closes input stream before returning
+    public int copyTo(final Map<String,?> props, final InputStream src, final String destination) {
+        return execSsh(props, new Function<SshTool,Integer>() {
+            public Integer apply(SshTool ssh) {
+                return ssh.copyToServer(props, src, destination);
+            }});
     }
 
     // FIXME the return code is not a reliable indicator of success or failure
@@ -580,7 +560,7 @@
     public int copyFrom(final Map<String,?> props, final String remote, final String local) {
         return execSsh(props, new Function<SshTool,Integer>() {
             public Integer apply(SshTool ssh) {
-                return ssh.transferFileFrom(props, remote, local);
+                return ssh.copyFromServer(props, remote, new File(local));
             }});
     }
 
diff --git a/core/src/main/java/brooklyn/util/internal/ssh/SshAbstractTool.java b/core/src/main/java/brooklyn/util/internal/ssh/SshAbstractTool.java
index b807543..f45cfbf 100644
--- a/core/src/main/java/brooklyn/util/internal/ssh/SshAbstractTool.java
+++ b/core/src/main/java/brooklyn/util/internal/ssh/SshAbstractTool.java
@@ -4,21 +4,17 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 
-import org.jclouds.io.Payload;
-import org.jclouds.io.payloads.ByteArrayPayload;
-import org.jclouds.io.payloads.InputStreamPayload;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -28,11 +24,8 @@
 import brooklyn.util.text.StringEscapes.BashStringEscapes;
 
 import com.google.common.base.Objects;
-import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.LimitInputStream;
 
 public abstract class SshAbstractTool implements SshTool {
 
@@ -93,28 +86,28 @@
 
         @SuppressWarnings("unchecked")
         public B from(Map<String,?> props) {
-            host = getMandatoryVal(props, "host", String.class);
-            port = getOptionalVal(props, "port", Integer.class, port);
-            user = getOptionalVal(props, "user", String.class, user);
+            host = getMandatoryVal(props, PROP_HOST);
+            port = getOptionalVal(props, PROP_PORT);
+            user = getOptionalVal(props, PROP_USER);
             
-            password = getOptionalVal(props, "password", String.class, password);
+            password = getOptionalVal(props, PROP_PASSWORD);
             
             warnOnDeprecated(props, "privateKey", "privateKeyData");
-            privateKeyData = getOptionalVal(props, "privateKey", String.class, privateKeyData);
-            privateKeyData = getOptionalVal(props, "privateKeyData", String.class, privateKeyData);
-            privateKeyPassphrase = getOptionalVal(props, "privateKeyPassphrase", String.class, privateKeyPassphrase);
+            privateKeyData = getOptionalVal(props, PROP_PRIVATE_KEY);
+            privateKeyData = getOptionalVal(props, PROP_PRIVATE_KEY_DATA);
+            privateKeyPassphrase = getOptionalVal(props, PROP_PRIVATE_KEY_PASSPHRASE);
             
             // for backwards compatibility accept keyFiles and privateKey
             // but sshj accepts only a single privateKeyFile; leave blank to use defaults (i.e. ~/.ssh/id_rsa and id_dsa)
             warnOnDeprecated(props, "keyFiles", null);
-            privateKeyFiles.addAll(getOptionalVal(props, "keyFiles", List.class, Collections.emptyList()));
-            String privateKeyFile = getOptionalVal(props, "privateKeyFile", String.class, null);
+            privateKeyFiles.addAll(getOptionalVal(props, PROP_KEY_FILES));
+            String privateKeyFile = getOptionalVal(props, PROP_PRIVATE_KEY_FILE);
             if (privateKeyFile != null) privateKeyFiles.add(privateKeyFile);
             
-            strictHostKeyChecking = getOptionalVal(props, "strictHostKeyChecking", Boolean.class, strictHostKeyChecking);
-            allocatePTY = getOptionalVal(props, "allocatePTY", Boolean.class, allocatePTY);
+            strictHostKeyChecking = getOptionalVal(props, PROP_STRICT_HOST_KEY_CHECKING);
+            allocatePTY = getOptionalVal(props, PROP_ALLOCATE_PTY);
             
-            localTempDir = getOptionalVal(props, "localTempDir", File.class, localTempDir);
+            localTempDir = getOptionalVal(props, PROP_LOCAL_TEMP_DIR);
             
             return self();
         }
@@ -179,39 +172,16 @@
         toString = String.format("%s@%s:%d", user, host, port);
     }
 
-    protected Payload toPayload(InputStream input, long length) {
-        InputStreamPayload payload = new InputStreamPayload(new LimitInputStream(input, length));
-        payload.getContentMetadata().setContentLength(length);
-        return payload;
-    }
-    
-    protected Payload toPayload(InputStream input) {
-        /*
-         * TODO sshj needs to know the length of the InputStream to copy the file:
-         *   java.lang.NullPointerException
-         *     at brooklyn.util.internal.ssh.SshjTool$PutFileAction$1.getLength(SshjTool.java:574)
-         *     at net.schmizz.sshj.sftp.SFTPFileTransfer$Uploader.upload(SFTPFileTransfer.java:174)
-         *     at net.schmizz.sshj.sftp.SFTPFileTransfer$Uploader.access$100(SFTPFileTransfer.java:162)
-         *     at net.schmizz.sshj.sftp.SFTPFileTransfer.upload(SFTPFileTransfer.java:61)
-         *     at net.schmizz.sshj.sftp.SFTPClient.put(SFTPClient.java:248)
-         *     at brooklyn.util.internal.ssh.SshjTool$PutFileAction.create(SshjTool.java:569)
-         * 
-         * Unfortunately that requires consuming the input stream to find out! We can't just do:
-         *   new InputStreamPayload(input)
-         * 
-         * This is nasty: we have to hold the entire file in-memory.
-         * It's worth a look at changing sshj to not need the length, if possible.
-         */
-        try {
-            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-            ByteStreams.copy(input, byteArrayOutputStream);
-            return new ByteArrayPayload(byteArrayOutputStream.toByteArray());
-        } catch (IOException e) {
-            LOG.warn("Error consuming stream", e);
-            throw Throwables.propagate(e);
-        }
+    @Override
+    public String toString() {
+        return toString;
     }
 
+    protected static Boolean hasVal(Map<String,?> map, ConfigKey<?> keyC) {
+        String key = keyC.getName();
+        return map.containsKey(key);
+    }
+    
     protected static <T> T getMandatoryVal(Map<String,?> map, ConfigKey<T> keyC) {
         String key = keyC.getName();
         checkArgument(map.containsKey(key), "must contain key '"+keyC+"'");
@@ -275,11 +245,6 @@
         throw new SshException("(" + toString() + ") " + message + ":" + e.getMessage(), e);
     }
     
-    @Override
-    public String toString() {
-        return toString;
-    }
-
     protected File writeTempFile(InputStream contents) {
         // TODO Use ConfigKeys.BROOKLYN_DATA_DIR, but how to get access to that here?
         File tempFile = ResourceUtils.writeToTempFile(contents, localTempDir, "sshcopy", "data");
@@ -290,6 +255,14 @@
         return tempFile;
     }
 
+    protected File writeTempFile(String contents) {
+        return writeTempFile(contents.getBytes());
+    }
+
+    protected File writeTempFile(byte[] contents) {
+        return writeTempFile(new ByteArrayInputStream(contents));
+    }
+
     public String getHostAddress() {
         return this.host;
     }
diff --git a/core/src/main/java/brooklyn/util/internal/ssh/SshTool.java b/core/src/main/java/brooklyn/util/internal/ssh/SshTool.java
index d2ac669..b716d3e 100644
--- a/core/src/main/java/brooklyn/util/internal/ssh/SshTool.java
+++ b/core/src/main/java/brooklyn/util/internal/ssh/SshTool.java
@@ -26,8 +26,8 @@
     
     public static final ConfigKey<String> PROP_HOST = new StringConfigKey("host", "Host to connect to (required)", null);
     public static final ConfigKey<Integer> PROP_PORT = new BasicConfigKey<Integer>(Integer.class, "port", "Port on host to connect to", 22);
-    public static final ConfigKey<String> PROP_USER = new StringConfigKey("user", "User to connect as", null);
-    public static final ConfigKey<String> PROP_PASSWORD = new StringConfigKey("user", "Password to use to connect", null);
+    public static final ConfigKey<String> PROP_USER = new StringConfigKey("user", "User to connect as", System.getProperty("user.name"));
+    public static final ConfigKey<String> PROP_PASSWORD = new StringConfigKey("password", "Password to use to connect", null);
     
     public static final ConfigKey<OutputStream> PROP_OUT_STREAM = new BasicConfigKey<OutputStream>(OutputStream.class, "out", "Stream to which to capture stdout");
     public static final ConfigKey<OutputStream> PROP_ERR_STREAM = new BasicConfigKey<OutputStream>(OutputStream.class, "err", "Stream to which to capture stderr");
@@ -50,11 +50,14 @@
     public static final ConfigKey<Integer> PROP_SSH_TRIES_TIMEOUT = new BasicConfigKey<Integer>(Integer.class, "sshTriesTimeout", "Timeout when attempting to connect for ssh operations; so if too slow trying sshTries times, will abort anyway", 2*60*1000);
     public static final ConfigKey<Long> PROP_SSH_RETRY_DELAY = new BasicConfigKey<Long>(Long.class, "sshRetryDelay", "Time (in milliseconds) before first ssh-retry, after which it will do exponential backoff", 50L);
 
+    public static final ConfigKey<File> PROP_LOCAL_TEMP_DIR = new BasicConfigKey<File>(File.class, "localTempDir", "The directory on the local machine (i.e. running brooklyn) for writing temp files", 
+            new File(System.getProperty("java.io.tmpdir"), "tmpssh"));
+    
     public static final ConfigKey<String> PROP_PERMISSIONS = new StringConfigKey("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 = new BasicConfigKey<Long>(Long.class, "lastModificationDate", "Last-modification-date to be set on files copied/created (should be UTC/1000, ie seconds since 1970; defaults to current)", 0L);
     public static final ConfigKey<Long> PROP_LAST_ACCESS_DATE = new BasicConfigKey<Long>(Long.class, "lastAccessDate", "Last-access-date to be set on files copied/created (should be UTC/1000, ie seconds since 1970; defaults to lastModificationDate)", 0L);
 
-    // FIXME Defined/used only in SshMachineLocation?
+    // TODO Could define the following in SshMachineLocation, or some such?
     //public static ConfigKey<String> PROP_LOG_PREFIX = new StringConfigKey("logPrefix", "???", ???);
     //public static ConfigKey<Boolean> PROP_NO_STDOUT_LOGGING = new StringConfigKey("noStdoutLogging", "???", ???);
     //public static ConfigKey<Boolean> PROP_NO_STDOUT_LOGGING = new StringConfigKey("noStdoutLogging", "???", ???);
@@ -86,14 +89,18 @@
     public boolean isConnected();
 
     /**
-     * Executes the set of commands in a shell script; optional property 'out'
-     * should be an output stream. Blocks until completion (unless property
-     * 'block' set as false).
+     * Executes the set of commands in a shell script. Blocks until completion.
      * <p>
-     * values in environment parameters are wrapped in double quotes, with double quotes escaped 
+     * 
+     * Optional properties are:
+     * <ul>
+     *   <li>'out' {@link OutputStream} - see {@link PROP_OUT_STREAM}
+     *   <li>'err' {@link OutputStream} - see {@link PROP_ERR_STREAM}
+     * </ul>
      * 
      * @return exit status of script
-     * @throws SshException
+     * 
+     * @throws SshException If failed to connect
      */
     public int execScript(Map<String,?> props, List<String> commands, Map<String,?> env);
 
@@ -102,22 +109,27 @@
      */
     public int execScript(Map<String,?> props, List<String> commands);
 
-    /** @deprecated @see execScript(Map, List, Map) */
+    /** @deprecated since 0.4; use execScript(...) */
     public int execShell(Map<String,?> props, List<String> commands);
-    /** @deprecated @see execScript(Map, List, Map) */
+    
+    /** @deprecated since 0.4; execScript(...) */
     public int execShell(Map<String,?> props, List<String> commands, Map<String,?> env);
 
     /**
-     * Executes the set of commands using ssh exec, ";" separated (overridable
-     * with property 'separator'.
-     *
-     * Optional properties 'out' and 'err' should be streams.
-     * <p>
-     * This is generally simpler/preferable to shell, but is not suitable if you need 
-     * env values whare are only set on a fully-fledged shell.
+     * Executes the set of commands using ssh exec.
      * 
-     * @return exit status
-     * @throws SshException
+     * This is generally more efficient than shell, but is not suitable if you need 
+     * env values which are only set on a fully-fledged shell.
+     *
+     * Optional properties are:
+     * <ul>
+     *   <li>'out' {@link OutputStream} - see {@link PROP_OUT_STREAM}
+     *   <li>'err' {@link OutputStream} - see {@link PROP_ERR_STREAM}
+     *   <li>'separator', defaulting to ";" - see {@link PROP_SEPARATOR}
+     * </ul>
+     * 
+     * @return exit status of commands
+     * @throws SshException If failed to connect
      */
     public int execCommands(Map<String,?> properties, List<String> commands, Map<String,?> env);
 
@@ -127,53 +139,69 @@
     public int execCommands(Map<String,?> properties, List<String> commands);
 
     /**
-     * @see #createFile(Map, String, InputStream, long)
+     * 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, sometimes just returning 0)
+     */
+    public int copyToServer(Map<String,?> props, File localFile, String pathAndFileOnRemoteServer);
+
+    /**
+     * Closes the given input stream before returning.
+     * 
+     * @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 to the server at the given path.
+     * If path is null, empty, '.', '..', or ends with '/' then file name is used.
+     * <p>
+     * Optional properties are:
+     * <ul>
+     *   <li>'permissions' (e.g. "0644") - see {@link PROP_PERMISSIONS}
+     * </ul>
+     *
+     * @return exit code (not supported by all SshTool implementations, sometimes just returning 0)
+     */
+    public int copyFromServer(Map<String,?> props, String pathAndFileOnRemoteServer, File local);
+
+    /**
+     * @deprecated since 0.5; See copyToServer(Map, InputStream, String)
      */
     public int transferFileTo(Map<String,?> props, InputStream input, String pathAndFileOnRemoteServer);
     
     /**
-     * @see #createFile(Map, String, InputStream, long)
+     * @deprecated since 0.5; See copyFromServer(Map, InputStream, String)
      */
     public int transferFileFrom(Map<String,?> props, String pathAndFileOnRemoteServer, String pathAndFileOnLocalServer);
 
     /**
-     * Creates the given file with the given contents.
-     * 
-     * Properties can be:
-     * <ul>
-     * <li>permissions (must be four-digit octal string, default '0644');
-     * <li>lastModificationDate (should be UTC/1000, ie seconds since 1970; defaults to current);
-     * <li>lastAccessDate (again UTC/1000; defaults to lastModificationDate);
-     * </ul>
-     * If neither lastXxxDate set it does not send that line (unless property ptimestamp set true)
-     * 
-     * Closes the input stream before returning.
-     * 
-     * @param props
-     * @param pathAndFileOnRemoteServer
-     * @param input
-     * @param size
-     * @throws SshException
+     * @deprecated since 0.5; See copyToServer(Map, InputStream, String)
      */
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, InputStream input, long size);
 
     /**
-     * @see #createFile(Map, String, InputStream, long)
+     * @deprecated since 0.5; See copyToServer(Map, byte[], String)
      */
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, String contents);
 
     /**
-     * @see #createFile(Map, String, InputStream, long)
+     * @deprecated since 0.5; See copyToServer(Map, byte[], String)
      */
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, byte[] contents);
-
-    /**
-     * Copies file, but won't preserve permission of last _access_ date. 
-     * If path is null, empty, '.', '..', or ends with '/' then file name is used.
-     * <p>
-     * To set permissions (or override mod date) use for example 'permissions:"0644"',
-     *
-     * @see #createFile(Map, String, InputStream, long)
-     */
-    public int copyToServer(Map<String,?> props, File f, String pathAndFileOnRemoteServer);
 }
diff --git a/core/src/main/java/brooklyn/util/internal/ssh/cli/SshCliTool.java b/core/src/main/java/brooklyn/util/internal/ssh/cli/SshCliTool.java
index 7206ce7..4633337 100644
--- a/core/src/main/java/brooklyn/util/internal/ssh/cli/SshCliTool.java
+++ b/core/src/main/java/brooklyn/util/internal/ssh/cli/SshCliTool.java
@@ -2,7 +2,6 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -105,44 +104,42 @@
         return true;
     }
 
-    private File writeTempFile(String contents) {
-        return writeTempFile(contents.getBytes());
+    @Override
+    public int copyToServer(java.util.Map<String,?> props, byte[] contents, String pathAndFileOnRemoteServer) {
+        return copyTempFileToServer(props, writeTempFile(contents), pathAndFileOnRemoteServer);
     }
-
-    private File writeTempFile(byte[] contents) {
-        return writeTempFile(new ByteArrayInputStream(contents));
+    
+    @Override
+    public int copyToServer(java.util.Map<String,?> props, InputStream contents, String pathAndFileOnRemoteServer) {
+        return copyTempFileToServer(props, writeTempFile(contents), pathAndFileOnRemoteServer);
     }
-
+    
     @Override
     public int transferFileTo(Map<String,?> props, InputStream input, String pathAndFileOnRemoteServer) {
-        return copyTempFileToServer(props, writeTempFile(input), pathAndFileOnRemoteServer);
+        return copyToServer(props, input, pathAndFileOnRemoteServer);
     }
     
     @Override
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, InputStream input, long size) {
-        return copyTempFileToServer(props, writeTempFile(input), pathAndFileOnRemoteServer);
+        return copyToServer(props, input, pathAndFileOnRemoteServer);
     }
 
     @Override
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, String contents) {
-        return copyTempFileToServer(props, writeTempFile(contents), pathAndFileOnRemoteServer);
+        return copyToServer(props, contents.getBytes(), pathAndFileOnRemoteServer);
     }
 
-    /** Creates the given file with the given contents.
-     *
-     * Permissions specified using 'permissions:0755'.
-     */
     @Override
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, byte[] contents) {
-        return copyTempFileToServer(props, writeTempFile(contents), pathAndFileOnRemoteServer);
+        return copyToServer(props, contents, pathAndFileOnRemoteServer);
     }
 
     @Override
     public int copyToServer(Map<String,?> props, File f, String pathAndFileOnRemoteServer) {
-        if (props.containsKey("lastModificationDate")) {
+        if (hasVal(props, PROP_LAST_MODIFICATION_DATE)) {
             LOG.warn("Unsupported ssh feature, setting lastModificationDate for {}:{}", this, pathAndFileOnRemoteServer);
         }
-        if (props.containsKey("lastAccessDate")) {
+        if (hasVal(props, PROP_LAST_ACCESS_DATE)) {
             LOG.warn("Unsupported ssh feature, setting lastAccessDate for {}:{}", this, pathAndFileOnRemoteServer);
         }
         String permissions = getOptionalVal(props, PROP_PERMISSIONS);
@@ -169,7 +166,12 @@
 
     @Override
     public int transferFileFrom(Map<String,?> props, String pathAndFileOnRemoteServer, String pathAndFileOnLocalServer) {
-        return scpFromServer(props, pathAndFileOnRemoteServer, new File(pathAndFileOnLocalServer));
+        return copyFromServer(props, pathAndFileOnRemoteServer, new File(pathAndFileOnLocalServer));
+    }
+
+    @Override
+    public int copyFromServer(Map<String,?> props, String pathAndFileOnRemoteServer, File localFile) {
+        return scpFromServer(props, pathAndFileOnRemoteServer, localFile);
     }
 
     @Override
@@ -207,7 +209,7 @@
                 "rm -f "+scriptPath+" < /dev/null"+separator+
                 "exit $RESULT";
         
-        Integer result = ssh(props, cmd);
+        Integer result = sshExec(props, cmd);
         return result != null ? result : -1;
     }
 
@@ -222,41 +224,20 @@
     }
     
     private int scpToServer(Map<String,?> props, File local, String remote) {
-        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(local.getAbsolutePath());
-            cmd.add((Strings.isEmpty(getUsername()) ? "" : getUsername()+"@")+getHostAddress()+":"+remote);
-            
-            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();
-        }
+        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();
@@ -277,8 +258,8 @@
                 cmd.add("-P");
                 cmd.add(""+port);
             }
-            cmd.add((Strings.isEmpty(getUsername()) ? "" : getUsername()+"@")+getHostAddress()+":"+remote);
-            cmd.add(local.getAbsolutePath());
+            cmd.add(from);
+            cmd.add(to);
             
             if (LOG.isTraceEnabled()) LOG.trace("Executing with command: {}", cmd);
             int result = execProcess(props, cmd);
@@ -291,11 +272,7 @@
         }
     }
     
-    private int chmodOnServer(Map<String,?> props, String permissions, String remote) {
-        return ssh(props, "chmod "+permissions+" "+remote);
-    }
-    
-    private int ssh(Map<String,?> props, String command) {
+    private int sshExec(Map<String,?> props, String command) {
         File tempCmdFile = writeTempFile(command);
         File tempKeyFile = null;
         try {
@@ -344,23 +321,21 @@
         try {
             Process p = pb.start();
             
-            if (true) {// FIXME
-//            if (out != null) {
+            if (out != null) {
                 InputStream outstream = p.getInputStream();
-                outgobbler = new StreamGobbler(outstream, out, LOG).setLogPrefix("[stdout] ");// FIXME (Logger) null);
+                outgobbler = new StreamGobbler(outstream, out, (Logger) null);
                 outgobbler.start();
             }
-            if (true) {// FIXME
-//            if (err != null) {
+            if (err != null) {
                 InputStream errstream = p.getErrorStream();
-                errgobbler = new StreamGobbler(errstream, err, LOG).setLogPrefix("[stdout] ");// FIXME (Logger) null);
+                errgobbler = new StreamGobbler(errstream, err, (Logger) null);
                 errgobbler.start();
             }
             
             int result = p.waitFor();
             
-            outgobbler.blockUntilFinished();
-            errgobbler.blockUntilFinished();
+            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;
diff --git a/core/src/main/java/brooklyn/util/internal/ssh/sshj/SshjTool.java b/core/src/main/java/brooklyn/util/internal/ssh/sshj/SshjTool.java
index f72e149..6f87f45 100644
--- a/core/src/main/java/brooklyn/util/internal/ssh/sshj/SshjTool.java
+++ b/core/src/main/java/brooklyn/util/internal/ssh/sshj/SshjTool.java
@@ -31,7 +31,7 @@
 import org.jclouds.io.Payloads;
 import org.jclouds.io.payloads.ByteArrayPayload;
 import org.jclouds.io.payloads.FilePayload;
-import org.jclouds.io.payloads.StringPayload;
+import org.jclouds.io.payloads.InputStreamPayload;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -50,6 +50,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.Files;
+import com.google.common.io.LimitInputStream;
 import com.google.common.net.HostAndPort;
 
 /**
@@ -188,58 +189,89 @@
     }
     
     @Override
+    public int copyToServer(java.util.Map<String,?> props, byte[] contents, String pathAndFileOnRemoteServer) {
+        return copyToServer(props, toPayload(contents), pathAndFileOnRemoteServer);
+    }
+    
+    @Override
+    public int copyToServer(java.util.Map<String,?> props, InputStream contents, String pathAndFileOnRemoteServer) {
+        /*
+         * TODO sshj needs to know the length of the InputStream to copy the file:
+         *   java.lang.NullPointerException
+         *     at brooklyn.util.internal.ssh.SshjTool$PutFileAction$1.getLength(SshjTool.java:574)
+         *     at net.schmizz.sshj.sftp.SFTPFileTransfer$Uploader.upload(SFTPFileTransfer.java:174)
+         *     at net.schmizz.sshj.sftp.SFTPFileTransfer$Uploader.access$100(SFTPFileTransfer.java:162)
+         *     at net.schmizz.sshj.sftp.SFTPFileTransfer.upload(SFTPFileTransfer.java:61)
+         *     at net.schmizz.sshj.sftp.SFTPClient.put(SFTPClient.java:248)
+         *     at brooklyn.util.internal.ssh.SshjTool$PutFileAction.create(SshjTool.java:569)
+         * 
+         * Unfortunately that requires consuming the input stream to find out! We can't just do:
+         *   new InputStreamPayload(input)
+         * 
+         * This is nasty: we have to either write it to a temp file, or hold the entire contents in-memory.
+         * It's worth a look at changing sshj to not need the length, if possible.
+         * 
+         * TODO Could have a switch where we hold it in memory if less than some max size, but write it to the file if
+         * too big.
+         */
+        File tempFile = writeTempFile(contents);
+        try {
+            return copyToServer(props, tempFile, pathAndFileOnRemoteServer);
+        } finally {
+            tempFile.delete();
+        }
+    }
+    
+    @Override
+    public int copyToServer(java.util.Map<String,?> props, File localFile, String pathAndFileOnRemoteServer) {
+        return copyToServer(props, toPayload(localFile), pathAndFileOnRemoteServer);
+    }
+    
+    @Override
     public int transferFileTo(Map<String,?> props, InputStream input, String pathAndFileOnRemoteServer) {
-        return createFile(props, pathAndFileOnRemoteServer, toPayload(input));
+        return copyToServer(props, input, pathAndFileOnRemoteServer);
     }
     
     @Override
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, InputStream input, long size) {
-        return createFile(props, pathAndFileOnRemoteServer, toPayload(input, size));
+        return copyToServer(props, toPayload(input, size), pathAndFileOnRemoteServer);
     }
 
-    /**
-     * Creates the given file with the given contents.
-     *
-     * Permissions specified using 'permissions:0755'.
-     */
     @Override
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, String contents) {
-        return createFile(props, pathAndFileOnRemoteServer, new StringPayload(contents));
+        return copyToServer(props, contents.getBytes(), pathAndFileOnRemoteServer);
     }
 
-    /** Creates the given file with the given contents.
-     *
-     * Permissions specified using 'permissions:0755'.
-     */
     @Override
     public int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, byte[] contents) {
-        return createFile(props, pathAndFileOnRemoteServer, new ByteArrayPayload(contents));
+        return copyToServer(props, contents, pathAndFileOnRemoteServer);
     }
 
-    @Override
-    public int copyToServer(Map<String,?> props, File f, String pathAndFileOnRemoteServer) {
-        return createFile(props, pathAndFileOnRemoteServer, new FilePayload(f));
+    private int copyToServer(Map<String,?> props, Payload contents, String pathAndFileOnRemoteServer) {
+        acquire(new PutFileAction(props, pathAndFileOnRemoteServer, contents));
+        return 0; // TODO Can we assume put will have thrown exception if failed? Rather than exit code != 0?
     }
 
     @Override
     public int transferFileFrom(Map<String,?> props, String pathAndFileOnRemoteServer, String pathAndFileOnLocalServer) {
+        return copyFromServer(props, pathAndFileOnRemoteServer, new File(pathAndFileOnLocalServer));
+    }
+    
+    @Override
+    public int copyFromServer(Map<String,?> props, String pathAndFileOnRemoteServer, File localFile) {
         Payload payload = acquire(new GetFileAction(pathAndFileOnRemoteServer));
         try {
-            Files.copy(InputSuppliers.of(payload.getInput()), new File(pathAndFileOnLocalServer));
+            Files.copy(InputSuppliers.of(payload.getInput()), localFile);
             return 0; // TODO Can we assume put will have thrown exception if failed? Rather than exit code != 0?
         } catch (IOException e) {
             throw Throwables.propagate(e);
         }
     }
 
-    private int createFile(Map<String,?> props, String pathAndFileOnRemoteServer, Payload payload) {
-        acquire(new PutFileAction(props, pathAndFileOnRemoteServer, payload));
-        return 0; // TODO Can we assume put will have thrown exception if failed? Rather than exit code != 0?
-    }
-
     public int execShell(Map<String,?> props, List<String> commands) {
         return execScript(props, commands, Collections.<String,Object>emptyMap());
     }
+    
     public int execShell(Map<String,?> props, List<String> commands, Map<String,?> env) {
         return execScript(props, commands, env);
     }
@@ -286,7 +318,7 @@
         
         if (LOG.isTraceEnabled()) LOG.trace("Running shell command at {} as script: {}", host, scriptContents);
         
-        createFile(ImmutableMap.of("permissions", "0700"), scriptPath, scriptContents);
+        copyToServer(ImmutableMap.of("permissions", "0700"), scriptContents.getBytes(), scriptPath);
         
         // use "-f" because some systems have "rm" aliased to "rm -i"; use "< /dev/null" to guarantee doesn't hang
         List<String> cmds = ImmutableList.of(
@@ -453,10 +485,10 @@
         private long lastAccessDate;
         
         PutFileAction(Map<String,?> props, String path, Payload contents) {
-            String permissions = getOptionalVal(props, "permissions", String.class, "0644");
+            String permissions = getOptionalVal(props, PROP_PERMISSIONS, "0644");
             permissionsMask = Integer.parseInt(permissions, 8);
-            lastModificationDate = getOptionalVal(props, "lastModificationDate", Long.class, 0L);
-            lastAccessDate = getOptionalVal(props, "lastAccessDate", Long.class, 0L);
+            lastModificationDate = getOptionalVal(props, PROP_LAST_MODIFICATION_DATE, 0L);
+            lastAccessDate = getOptionalVal(props, PROP_LAST_ACCESS_DATE, 0L);
             if (lastAccessDate <= 0 ^ lastModificationDate <= 0) {
                 lastAccessDate = Math.max(lastAccessDate, lastModificationDate);
                 lastModificationDate = Math.max(lastAccessDate, lastModificationDate);
@@ -743,4 +775,17 @@
         }
     }
 
+    protected Payload toPayload(byte[] input) {
+        return new ByteArrayPayload(input);
+    }
+    
+    protected Payload toPayload(File input) {
+        return new FilePayload(input);
+    }
+    
+    protected Payload toPayload(InputStream input, long length) {
+        InputStreamPayload payload = new InputStreamPayload(new LimitInputStream(input, length));
+        payload.getContentMetadata().setContentLength(length);
+        return payload;
+    }
 }
diff --git a/core/src/test/java/brooklyn/entity/basic/EffectorConcatenateTest.groovy b/core/src/test/java/brooklyn/entity/basic/EffectorConcatenateTest.groovy
deleted file mode 100644
index 06e8098..0000000
--- a/core/src/test/java/brooklyn/entity/basic/EffectorConcatenateTest.groovy
+++ /dev/null
@@ -1,178 +0,0 @@
-package brooklyn.entity.basic;
-
-import static org.testng.Assert.*
-import groovy.transform.InheritConstructors
-
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicReference
-
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import org.testng.annotations.AfterMethod
-import org.testng.annotations.BeforeMethod
-import org.testng.annotations.Test
-
-import brooklyn.entity.Application
-import brooklyn.entity.Effector
-import brooklyn.entity.Entity
-import brooklyn.management.ExecutionManager
-import brooklyn.management.Task
-import brooklyn.test.entity.TestApplication
-import brooklyn.util.task.BasicExecutionContext
-import brooklyn.util.task.Tasks
-
-public class EffectorConcatenateTest {
-
-    
-    private static final Logger log = LoggerFactory.getLogger(EffectorConcatenateTest.class);
-    private static final long TIMEOUT = 10*1000
-    
-    public class MyEntity extends AbstractEntity {
-
-        public static Effector<String> CONCATENATE = new MethodEffector<Void>(MyEntity.class, "concatenate");
-    
-        public MyEntity(Map flags) {
-            super(flags)
-        }
-        public MyEntity(Entity parent) {
-            super(parent)
-        }
-        public MyEntity(Map flags, Entity parent) {
-            super(flags, parent)
-        }
-
-        AtomicReference concatTask = new AtomicReference();
-        // FIXME instead of waiting on this we should use semaphores -- seems we very occasionally get spurious wakes
-        AtomicReference response = new AtomicReference();
-        
-        @Description("sample effector concatenating strings and sometimes waiting")
-        String concatenate(@NamedParameter("first") @Description("first argument") String first,
-                @NamedParameter("second") @Description("2nd arg") String second) {
-            if ("wait".equals(first)) {
-                // if first arg is wait, spawn a child, then wait
-                BasicExecutionContext.getCurrentExecutionContext().submit(
-                    displayName: "SarcyResponse",
-                    {
-                        log.info("beginning scary response "+Tasks.current()+", with tags "+Tasks.current().tags)
-                        synchronized (response) {
-                            Tasks.setBlockingDetails("looks like the backstroke to me");
-                            response.notifyAll();
-                            response.wait(TIMEOUT);
-                        }
-                    });
-                
-                Tasks.setExtraStatusDetails("What's the soup du jour? That's the soup of the day!");
-                
-                // wait, setting task info from the second arg
-                // (test will assert that status details are reported correctly)
-                long startTime = System.currentTimeMillis();
-                synchronized (concatTask) {
-                    concatTask.set(Tasks.current());
-                    concatTask.notifyAll();
-                    Tasks.withBlockingDetails(second) {
-                        concatTask.wait(TIMEOUT);
-                    }
-                    concatTask.set(null);
-                }
-                if (System.currentTimeMillis()-startTime >= TIMEOUT)
-                    fail("took too long, probably wasn't notified");
-            }
-            return first+second
-        }
-    }
-            
-    private Application app;
-    private MyEntity e;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() {
-        app = new TestApplication();
-        e = new MyEntity(app);
-        Entities.startManagement(app);
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() {
-        if (app != null) Entities.destroyAll(app);
-    }
-    
-    @Test
-    public void testCanInvokeEffector() {
-        // invocation map syntax
-        Task<String> task = e.invoke(MyEntity.CONCATENATE, [first:"a",second:"b"])
-        assertEquals(task.get(TIMEOUT, TimeUnit.MILLISECONDS), "ab")
-
-        // method syntax
-        assertEquals("xy", e.concatenate("x", "y"));
-    }
-    
-    @Test
-    public void testTaskReporting() {
-        final AtomicReference<String> result = new AtomicReference<String>();
-
-        Thread bg = new Thread({
-            try {
-                long startTime = System.currentTimeMillis();
-                synchronized (e.concatTask) {
-                    try {
-                        while (e.concatTask.get()==null) {
-                            e.concatTask.wait(1000);
-                            if (System.currentTimeMillis()-startTime >= TIMEOUT) {
-                                result.set("took too long, probably wasn't notified");
-                                return;
-                            }
-                        }
-
-                        Task t = e.concatTask.get();
-                        String status = t.getStatusDetail(true);
-                        log.info("concat task says:\n"+status);
-                        if (!status.startsWith("waiter, what's this fly doing")) {
-                            result.set("Status not in expected format: doesn't start with blocking details 'waiter...'\n"+status);
-                            return;
-                        }
-                        if (!status.contains("du jour")) {
-                            result.set("Status not in expected format: doesn't contain extra status details phrase 'du jour'\n"+status);
-                            return;
-                        }
-                        // looks healthy
-                    } finally {
-                        e.concatTask.notifyAll();
-                    }
-                }
-
-                ExecutionManager em = e.getExecutionContext().getExecutionManager();
-                synchronized (e.response) {
-                    Task reply=null;
-                    while (reply==null) {
-                        Collection<Task> entityTasks = em.getTasksWithTag(e);
-                        log.info("entity "+e+" running: "+entityTasks);
-                        reply = entityTasks.find { Task t -> t.displayName=="SarcyResponse" }
-                        if (reply!=null) break;
-                        if (System.currentTimeMillis()-startTime >= TIMEOUT) {
-                            result.set("response took too long, probably wasn't notified");
-                            return;
-                        }
-                        e.response.wait(TIMEOUT);
-                    }
-                    String status = reply.getStatusDetail(true);
-                    log.info("reply task says:\n"+status);
-                    if (!status.contains("backstroke")) {
-                        result.set("Status not in expected format: doesn't contain blocking details phrase 'backstroke'\n"+status);
-                        return;
-                    }
-                    e.response.notifyAll();
-                }
-            } catch (Throwable t) {
-                log.warn("Failure: "+t, t);
-                result.set("Failure: "+t);
-            }
-        });
-        bg.start();
-    
-        e.concatenate("wait", "waiter, what's this fly doing in my soup?");
-        
-        bg.join();
-        String problem = result.get();
-        if (problem!=null) fail(problem);
-    }
-}
diff --git a/core/src/test/java/brooklyn/entity/basic/EffectorConcatenateTest.java b/core/src/test/java/brooklyn/entity/basic/EffectorConcatenateTest.java
new file mode 100644
index 0000000..3d8732f
--- /dev/null
+++ b/core/src/test/java/brooklyn/entity/basic/EffectorConcatenateTest.java
@@ -0,0 +1,219 @@
+package brooklyn.entity.basic;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.fail;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.Application;
+import brooklyn.entity.Effector;
+import brooklyn.entity.Entity;
+import brooklyn.management.ExecutionManager;
+import brooklyn.management.Task;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.util.MutableMap;
+import brooklyn.util.task.BasicExecutionContext;
+import brooklyn.util.task.Tasks;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+
+public class EffectorConcatenateTest {
+
+    
+    private static final Logger log = LoggerFactory.getLogger(EffectorConcatenateTest.class);
+    private static final long TIMEOUT = 10*1000;
+    
+    public static class MyEntity extends AbstractEntity {
+
+        public static Effector<String> CONCATENATE = new MethodEffector<String>(MyEntity.class, "concatenate");
+        public static Effector<Void> WAIT_A_BIT = new MethodEffector<Void>(MyEntity.class, "waitabit");
+        public static Effector<Void> SPAWN_CHILD = new MethodEffector<Void>(MyEntity.class, "spawnchild");
+    
+        public MyEntity() {
+            super();
+        }
+        public MyEntity(Entity parent) {
+            super(parent);
+        }
+
+        /** The "current task" representing the effector currently executing */
+        AtomicReference<Task<?>> waitingTask = new AtomicReference<Task<?>>();
+        
+        /** latch is .countDown'ed by the effector at the beginning of the "waiting" point */
+        CountDownLatch nowWaitingLatch = new CountDownLatch(1);
+        
+        /** latch is await'ed on by the effector when it is in the "waiting" point */
+        CountDownLatch continueFromWaitingLatch = new CountDownLatch(1);
+        
+        @Description("sample effector concatenating strings")
+        public String concatenate(@NamedParameter("first") @Description("first argument") String first,
+                @NamedParameter("second") @Description("2nd arg") String second) throws Exception {
+            return first+second;
+        }
+        
+        @Description("sample effector doing some waiting")
+        public void waitabit() throws Exception {
+            waitingTask.set(Tasks.current());
+            
+            Tasks.setExtraStatusDetails("waitabit extra status details");
+            
+            Tasks.withBlockingDetails("waitabit.blocking", new Callable<Void>() {
+                    public Void call() throws Exception {
+                        nowWaitingLatch.countDown();
+                        if (!continueFromWaitingLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
+                            fail("took too long to be told to continue");
+                        }
+                        return null;
+                    }});
+        }
+        
+        @Description("sample effector that spawns a child task that waits a bit")
+        public void spawnchild() throws Exception {
+            // spawn a child, then wait
+            BasicExecutionContext.getCurrentExecutionContext().submit(
+                    MutableMap.of("displayName", "SpawnedChildName"),
+                    new Callable<Void>() {
+                        public Void call() throws Exception {
+                            log.info("beginning spawned child response "+Tasks.current()+", with tags "+Tasks.current().getTags());
+                            Tasks.setBlockingDetails("spawned child blocking details");
+                            nowWaitingLatch.countDown();
+                            if (!continueFromWaitingLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
+                                fail("took too long to be told to continue");
+                            }
+                            return null;
+                        }});
+        }
+    }
+            
+    private Application app;
+    private MyEntity e;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() {
+        app = new TestApplication();
+        e = new MyEntity(app);
+        Entities.startManagement(app);
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() {
+        if (app != null) Entities.destroyAll(app);
+    }
+    
+    @Test
+    public void testCanInvokeEffector() throws Exception {
+        // invocation map syntax
+        Task<String> task = e.invoke(MyEntity.CONCATENATE, ImmutableMap.of("first", "a", "second", "b"));
+        assertEquals(task.get(TIMEOUT, TimeUnit.MILLISECONDS), "ab");
+
+        // method syntax
+        assertEquals("xy", e.concatenate("x", "y"));
+    }
+    
+    @Test
+    public void testReportsTaskDetails() throws Exception {
+        final AtomicReference<String> result = new AtomicReference<String>();
+
+        Thread bg = new Thread(new Runnable() {
+            public void run() {
+                try {
+                    // Expect "wait a bit" to tell us it's blocking 
+                    if (!e.nowWaitingLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
+                        result.set("took too long for waitabit to be waiting");
+                        return;
+                    }
+
+                    // Expect "wait a bit" to have retrieved and set its task
+                    try {
+                        Task<?> t = e.waitingTask.get();
+                        String status = t.getStatusDetail(true);
+                        log.info("waitabit task says:\n"+status);
+                        if (!status.contains("waitabit extra status details")) {
+                            result.set("Status not in expected format: doesn't contain extra status details phrase 'My extra status details'\n"+status);
+                            return;
+                        }
+                        if (!status.startsWith("waitabit.blocking")) {
+                            result.set("Status not in expected format: doesn't start with blocking details 'waitabit.blocking'\n"+status);
+                            return;
+                        }
+                    } finally {
+                        e.continueFromWaitingLatch.countDown();
+                    }
+                } catch (Throwable t) {
+                    log.warn("Failure: "+t, t);
+                    result.set("Failure: "+t);
+                }
+            }});
+        bg.start();
+    
+        e.invoke(MyEntity.WAIT_A_BIT, ImmutableMap.<String,Object>of())
+                .get(TIMEOUT, TimeUnit.MILLISECONDS);
+        
+        bg.join(TIMEOUT*2);
+        assertFalse(bg.isAlive());
+        
+        String problem = result.get();
+        if (problem!=null) fail(problem);
+    }
+    
+    @Test
+    public void testReportsSpawnedTaskDetails() throws Exception {
+        final AtomicReference<String> result = new AtomicReference<String>();
+
+        Thread bg = new Thread(new Runnable() {
+            public void run() {
+                try {
+                    // Expect "spawned child" to tell us it's blocking 
+                    if (!e.nowWaitingLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
+                        result.set("took too long for spawnchild's sub-task to be waiting");
+                        return;
+                    }
+
+                    // Expect spawned task to be have been tagged with entity
+                    ExecutionManager em = e.getManagementContext().getExecutionManager();
+                    Task<?> subtask = Iterables.find(em.getTasksWithTag(e), new Predicate<Task<?>>() {
+                        public boolean apply(Task<?> input) {
+                            return "SpawnedChildName".equals(input.getDisplayName());
+                        }
+                    });
+                    
+                    // Expect spawned task to haev correct "blocking details"
+                    try {
+                        String status = subtask.getStatusDetail(true);
+                        log.info("subtask task says:\n"+status);
+                        if (!status.contains("spawned child blocking details")) {
+                            result.set("Status not in expected format: doesn't contain blocking details phrase 'spawned child blocking details'\n"+status);
+                            return;
+                        }
+                    } finally {
+                        e.continueFromWaitingLatch.countDown();
+                    }
+                } catch (Throwable t) {
+                    log.warn("Failure: "+t, t);
+                    result.set("Failure: "+t);
+                }
+            }});
+        bg.start();
+    
+        e.invoke(MyEntity.SPAWN_CHILD, ImmutableMap.<String,Object>of())
+                .get(TIMEOUT, TimeUnit.MILLISECONDS);
+        
+        bg.join(TIMEOUT*2);
+        assertFalse(bg.isAlive());
+        
+        String problem = result.get();
+        if (problem!=null) fail(problem);
+    }
+}
diff --git a/core/src/test/java/brooklyn/util/internal/ssh/SshToolIntegrationTest.java b/core/src/test/java/brooklyn/util/internal/ssh/SshToolIntegrationTest.java
index fc87a55..87c84a0 100644
--- a/core/src/test/java/brooklyn/util/internal/ssh/SshToolIntegrationTest.java
+++ b/core/src/test/java/brooklyn/util/internal/ssh/SshToolIntegrationTest.java
@@ -17,12 +17,12 @@
 import java.util.Map;
 import java.util.concurrent.Executors;
 
+import org.jclouds.util.Throwables2;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.util.MutableMap;
-import brooklyn.util.internal.ssh.sshj.SshjTool;
 import brooklyn.util.text.Identifiers;
 
 import com.beust.jcommander.internal.Lists;
@@ -37,11 +37,17 @@
 import com.google.common.util.concurrent.MoreExecutors;
 
 /**
- * Test the operation of the {@link SshJschTool} utility class.
+ * Test the operation of the {@link SshTool} utility class; to be extended to test concrete implementations.
  */
 public abstract class SshToolIntegrationTest {
 
+    // FIXME need tests which take properties set in entities and brooklyn.properties;
+    // but not in this class because it is lower level than entities, Aled would argue.
+
     // TODO No tests for retry logic and exception handing yet
+
+    public static final String SSH_KEY_WITH_PASSPHRASE = System.getProperty("sshPrivateKeyWithPassphrase", "~/.ssh/id_rsa_with_passphrase");
+    public static final String SSH_PASSPHRASE = System.getProperty("sshPrivateKeyPassphrase", "mypassphrase");
     
     protected List<SshTool> tools = Lists.newArrayList();
     protected SshTool tool;
@@ -51,7 +57,7 @@
     
     protected abstract SshTool newSshTool(Map<String,?> flags);
 
-    @BeforeMethod(alwaysRun=true)//(groups = {"Integration"})
+    @BeforeMethod(alwaysRun=true)
     public void setUp() throws Exception {
         localFilePath = "/tmp/ssh-test-local-"+Identifiers.makeRandomId(8);
         remoteFilePath = "/tmp/ssh-test-remote-"+Identifiers.makeRandomId(8);
@@ -283,18 +289,36 @@
     }
 
     @Test(groups = {"Integration"})
-    public void testCreateFileWithPermissions() throws Exception {
-        tool.createFile(ImmutableMap.of("permissions","0754"), remoteFilePath, "echo hello world!\n");
+    public void testCopyToServerFromBytes() throws Exception {
+        String contents = "echo hello world!\n";
+        byte[] contentBytes = contents.getBytes();
+        tool.copyToServer(MutableMap.<String,Object>of(), contentBytes, remoteFilePath);
+
+        assertRemoteFileContents(remoteFilePath, contents);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testCopyToServerFromInputStream() throws Exception {
+        String contents = "echo hello world!\n";
+        ByteArrayInputStream contentsStream = new ByteArrayInputStream(contents.getBytes());
+        tool.copyToServer(MutableMap.<String,Object>of(), contentsStream, remoteFilePath);
+
+        assertRemoteFileContents(remoteFilePath, contents);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testCopyToServerWithPermissions() throws Exception {
+        tool.copyToServer(ImmutableMap.of("permissions","0754"), "echo hello world!\n".getBytes(), remoteFilePath);
 
         String out = execCommands("ls -l "+remoteFilePath);
         assertTrue(out.contains("-rwxr-xr--"), out);
     }
     
     @Test(groups = {"Integration"})
-    public void testCreateFileWithLastModifiedDate() throws Exception {
+    public void testCopyToServerWithLastModifiedDate() throws Exception {
         long lastModificationTime = 1234567;
         Date lastModifiedDate = new Date(lastModificationTime);
-        tool.createFile(ImmutableMap.of("lastModificationDate", lastModificationTime), remoteFilePath, "echo hello world!\n");
+        tool.copyToServer(ImmutableMap.of("lastModificationDate", lastModificationTime), "echo hello world!\n".getBytes(), remoteFilePath);
 
         String lsout = execCommands("ls -l "+remoteFilePath);//+" | awk '{print \$6 \" \" \$7 \" \" \$8}'"])
         //execCommands([ "ls -l "+remoteFilePath+" | awk '{print \$6 \" \" \$7 \" \" \$8}'"])
@@ -316,6 +340,19 @@
     }
 
     @Test(groups = {"Integration"})
+    public void testCopyFromServer() throws Exception {
+        String contentsWithoutLineBreak = "echo hello world!";
+        String contents = contentsWithoutLineBreak+"\n";
+        tool.copyToServer(MutableMap.<String,Object>of(), contents.getBytes(), remoteFilePath);
+        
+        tool.copyFromServer(MutableMap.<String,Object>of(), remoteFilePath, new File(localFilePath));
+
+        List<String> actual = Files.readLines(new File(localFilePath), Charsets.UTF_8);
+        assertEquals(actual, ImmutableList.of(contentsWithoutLineBreak));
+    }
+    
+    @Test(groups = {"Integration"})
+    @Deprecated // tests deprecated code
     public void testTransferFileToServer() throws Exception {
         String contents = "echo hello world!\n";
         ByteArrayInputStream contentsStream = new ByteArrayInputStream(contents.getBytes());
@@ -325,6 +362,7 @@
     }
 
     @Test(groups = {"Integration"})
+    @Deprecated // tests deprecated code
     public void testCreateFileFromBytes() throws Exception {
         String contents = "echo hello world!\n";
         byte[] contentBytes = contents.getBytes();
@@ -334,6 +372,7 @@
     }
 
     @Test(groups = {"Integration"})
+    @Deprecated // tests deprecated code
     public void testCreateFileFromInputStream() throws Exception {
         String contents = "echo hello world!\n";
         ByteArrayInputStream contentsStream = new ByteArrayInputStream(contents.getBytes());
@@ -343,10 +382,11 @@
     }
 
     @Test(groups = {"Integration"})
+    @Deprecated // tests deprecated code
     public void testTransferFileFromServer() throws Exception {
         String contentsWithoutLineBreak = "echo hello world!";
         String contents = contentsWithoutLineBreak+"\n";
-        tool.createFile(MutableMap.<String,Object>of(), remoteFilePath, contents);
+        tool.copyToServer(MutableMap.<String,Object>of(), contents.getBytes(), remoteFilePath);
         
         tool.transferFileFrom(MutableMap.<String,Object>of(), remoteFilePath, localFilePath);
 
@@ -356,20 +396,21 @@
     
     // TODO No config options in sshj or scp for auto-creating the parent directories
     @Test(enabled=false, groups = {"Integration"})
-    public void testCreateFileInNonExistantDir() throws Exception {
+    public void testCopyFileToNonExistantDir() throws Exception {
         String contents = "echo hello world!\n";
         String remoteFileDirPath = "/tmp/ssh-test-remote-dir-"+Identifiers.makeRandomId(8);
         String remoteFileInDirPath = remoteFileDirPath + File.separator + "ssh-test-remote-"+Identifiers.makeRandomId(8);
         filesCreated.add(remoteFileInDirPath);
         filesCreated.add(remoteFileDirPath);
         
-        tool.createFile(MutableMap.<String,Object>of(), remoteFileInDirPath, contents);
+        tool.copyToServer(MutableMap.<String,Object>of(), contents.getBytes(), remoteFileInDirPath);
 
         assertRemoteFileContents(remoteFileInDirPath, contents);
     }
     
     // fails if terminal enabled
     @Test(groups = {"Integration"})
+    @Deprecated // tests deprecated code
     public void testExecShellCapturesStderr() throws Exception {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ByteArrayOutputStream err = new ByteArrayOutputStream();
@@ -380,6 +421,7 @@
 
     // fails if terminal enabled
     @Test(groups = {"Integration"})
+    @Deprecated // tests deprecated code
     public void testExecCapturesStderr() throws Exception {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ByteArrayOutputStream err = new ByteArrayOutputStream();
@@ -412,7 +454,33 @@
         assertTrue(out.contains("hello world"), "no hello in output: "+out);
     }
 
-//    XXX; // new tests which take properties set in entities and brooklyn.properties
+    // Requires setting up an extra ssh key, with a passphrase, and adding it to ~/.ssh/authorized_keys
+    @Test(groups = {"Integration"})
+    public void testSshKeyWithPassphrase() throws Exception {
+        final SshTool localtool = newSshTool(ImmutableMap.<String,Object>builder()
+                .put(SshTool.PROP_HOST.getName(), "localhost")
+                .put(SshTool.PROP_PRIVATE_KEY_FILE.getName(), SSH_KEY_WITH_PASSPHRASE)
+                .put(SshTool.PROP_PRIVATE_KEY_PASSPHRASE.getName(), SSH_PASSPHRASE)
+                .build());
+        tools.add(localtool);
+        localtool.connect();
+        
+        assertEquals(tool.execScript(MutableMap.<String,Object>of(), ImmutableList.of("date")), 0);
+
+        // Also needs the negative test to prove that we're really using an ssh-key with a passphrase
+        try {
+            final SshTool localtool2 = newSshTool(ImmutableMap.<String,Object>builder()
+                    .put(SshTool.PROP_HOST.getName(), "localhost")
+                    .put(SshTool.PROP_PRIVATE_KEY_FILE.getName(), SSH_KEY_WITH_PASSPHRASE)
+                    .build());
+            tools.add(localtool2);
+            localtool2.connect();
+            fail();
+        } catch (Exception e) {
+            SshException se = Throwables2.getFirstThrowableOfType(e, SshException.class);
+            if (se == null) throw e;
+        }
+    }
     
     private void assertRemoteFileContents(String remotePath, String expectedContents) {
         String catout = execCommands("cat "+remotePath);
diff --git a/core/src/test/java/brooklyn/util/internal/ssh/cli/SshCliToolIntegrationTest.java b/core/src/test/java/brooklyn/util/internal/ssh/cli/SshCliToolIntegrationTest.java
index bd69203..a6c8a2f 100644
--- a/core/src/test/java/brooklyn/util/internal/ssh/cli/SshCliToolIntegrationTest.java
+++ b/core/src/test/java/brooklyn/util/internal/ssh/cli/SshCliToolIntegrationTest.java
@@ -1,13 +1,11 @@
 package brooklyn.util.internal.ssh.cli;
 
-import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.fail;
 
 import java.util.Map;
 
 import org.testng.annotations.Test;
 
-import brooklyn.util.MutableMap;
 import brooklyn.util.internal.ssh.SshException;
 import brooklyn.util.internal.ssh.SshTool;
 import brooklyn.util.internal.ssh.SshToolIntegrationTest;
@@ -44,20 +42,16 @@
         }
     }
     
+    // TODO ssh-cli doesn't support pass-phrases yet
+    @Test(enabled=false, groups = {"Integration"})
+    public void testSshKeyWithPassphrase() throws Exception {
+        super.testSshKeyWithPassphrase();
+    }
+
     // Setting last modified date not yet supported for cli-based ssh
     @Override
     @Test(enabled=false, groups = {"Integration"})
-    public void testCreateFileWithLastModifiedDate() throws Exception {
-        super.testCreateFileWithLastModifiedDate();
-    }
-    
-    @Test(groups = {"Integration"})
-    public void testCreateFileFromBytes() throws Exception {
-        super.testCreateFileFromBytes();
-    }
-    
-    @Test(groups = {"Integration"})
-    public void testExecShellReturningZeroExitCode() throws Exception {
-        super.testExecShellReturningZeroExitCode();
+    public void testCopyToServerWithLastModifiedDate() throws Exception {
+        super.testCopyToServerWithLastModifiedDate();
     }
 }
diff --git a/examples/pom.xml b/examples/pom.xml
index 6c29dd4..95ff73a 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -19,6 +19,17 @@
         <relativePath>../pom.xml</relativePath>
     </parent>
 
+    <repositories>
+        <!-- enable sonatype snapshots repo (only for snapshots) -->
+        <repository>
+            <id>sonatype-nexus-snapshots</id>
+            <name>Sonatype Nexus Snapshots</name>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+            <releases> <enabled>false</enabled> </releases>
+            <snapshots> <enabled>true</enabled> </snapshots>
+        </repository>
+    </repositories>
+
     <modules>
         <module>webapps</module>
         <module>simple-web-cluster</module>