brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From hadr...@apache.org
Subject [11/64] [abbrv] incubator-brooklyn git commit: BROOKLYN-162 - apply org.apache package prefix to utils-common
Date Tue, 18 Aug 2015 15:03:20 GMT
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/ssh/BashCommands.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/ssh/BashCommands.java b/utils/common/src/main/java/org/apache/brooklyn/util/ssh/BashCommands.java
new file mode 100644
index 0000000..4019a67
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/ssh/BashCommands.java
@@ -0,0 +1,711 @@
+/*
+ * 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.util.ssh;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+public class BashCommands {
+
+    /**
+     * Returns a string for checking whether the given executable is available,
+     * and installing it if necessary.
+     * <p/>
+     * Uses {@link #installPackage} and accepts the same flags e.g. for apt, yum, rpm.
+     */
+    public static String installExecutable(Map<?,?> flags, String executable) {
+        return onlyIfExecutableMissing(executable, installPackage(flags, executable));
+    }
+
+    public static String installExecutable(String executable) {
+        return installExecutable(MutableMap.of(), executable);
+    }
+
+    /**
+     * Returns a command with all output redirected to /dev/null
+     */
+    public static String quiet(String command) {
+        return format("(%s > /dev/null 2>&1)", command);
+    }
+
+    /**
+     * Returns a command that always exits successfully
+     */
+    public static String ok(String command) {
+        return String.format("(%s || true)", command);
+    }
+
+    /**
+     * Returns a command for safely running as root, using {@code sudo}.
+     * <p/>
+     * Ensuring non-blocking if password not set by using 
+     * {@code -n} which means to exit if password required
+     * (this is unsupported in Ubuntu 8 but all modern OS's seem okay with this!),
+     * and (perhaps unnecessarily ?)
+     * {@code -S} which reads from stdin (routed to {@code /dev/null}, it was claimed here previously, though I'm not sure?).
+     * <p/>
+     * Also specify {@code -E} to pass the parent environment in.
+     * <p/> 
+     * If already root, simply runs the command, wrapped in brackets in case it is backgrounded.
+     * <p/>
+     * The command is not quoted or escaped in any ways. 
+     * If you are doing privileged redirect you may need to pass e.g. "bash -c 'echo hi > file'".
+     * <p/>
+     * If null is supplied, it is returned (sometimes used to indicate no command desired).
+     */
+    public static String sudo(String command) {
+        if (command.startsWith("( ") || command.endsWith(" &"))
+            return sudoNew(command);
+        else
+            return sudoOld(command);
+    }
+
+    // TODO would like to move away from sudoOld -- but needs extensive testing!
+    
+    private static String sudoOld(String command) {
+        if (command==null) return null;
+        // some OS's (which?) fail if you try running sudo when you're already root (dumb but true)
+        return format("( if test \"$UID\" -eq 0; then ( %s ); else sudo -E -n -S -- %s; fi )", command, command);
+    }
+    private static String sudoNew(String command) {
+        if (command==null) return null;
+        // on some OS's e.g. Centos 6.5 in SL, sudo -- X tries to run X as a literal argument;
+        // in particular "( echo foo && echo bar )" fails when passed as an argument in that way,
+        // but works if passed to bash -c;
+        // but others e.g. OS X fail if you say  sudo -- bash -c "( echo foo )"   ... not liking the parentheses
+        // piping to sudo bash seems the most reliable way
+        return "( if test \"$UID\" -eq 0; then ( "+command+" ); else "
+                + "echo " + BashStringEscapes.wrapBash(command) + " | "
+                + "sudo -E -n -S -s -- bash"
+                + " ; fi )";
+    }
+
+    /** sudo to a given user and run the indicated command;
+     * @deprecated since 0.7.0 semantics of this are fiddly, e.g. whether user gets their environment */
+    @Beta
+    public static String sudoAsUser(String user, String command) {
+        return sudoAsUserOld(user, command);
+    }
+    
+    private static String sudoAsUserOld(String user, String command) {
+        if (command == null) return null;
+        return format("{ sudo -E -n -u %s -s -- %s ; }", user, command);
+    }
+    // TODO would like to move away from sudoOld -- but needs extensive testing!
+//    private static String sudoAsUserNew(String user, String command) {
+//        if (command == null) return null;
+//          // no -E, run with permissions of this user
+//          // FIXME still doesn't always work e.g. doesn't have path of user 
+//          // (Alex says: can't find any combinations which work reliably)
+//        return "{ sudo -n -S -i -u "+user+" -- "+BashStringEscapes.wrapBash(command)+" ; }";
+//    }
+
+    public static String addSbinPathCommand() {
+        return "export PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
+    }
+
+    /** executes a command, then as user tees the output to the given file. 
+     * useful e.g. for appending to a file which is only writable by root or a priveleged user. */
+    public static String executeCommandThenAsUserTeeOutputToFile(String commandWhoseOutputToWrite, String user, String file) {
+        return format("{ %s | sudo -E -n -u %s -s -- tee -a %s ; }",
+                commandWhoseOutputToWrite, user, file);
+    }
+
+    /** some machines require a tty for sudo; brooklyn by default does not use a tty
+     * (so that it can get separate error+stdout streams); you can enable a tty as an
+     * option to every ssh command, or you can do it once and 
+     * modify the machine so that a tty is not subsequently required.
+     * <p>
+     * this command must be run with allocatePTY set as a flag to ssh.  see SshTasks.dontRequireTtyForSudo which sets that up. 
+     * <p>
+     * (having a tty for sudo seems like another case of imaginary security which is just irritating.
+     * like water restrictions at airport security.) */
+    public static String dontRequireTtyForSudo() {
+        return ifFileExistsElse0("/etc/sudoers", sudo("sed -i.brooklyn.bak 's/.*requiretty.*/#brooklyn-removed-require-tty/' /etc/sudoers"));
+    }
+
+    /** generates ~/.ssh/id_rsa if that file does not exist */
+    public static String generateKeyInDotSshIdRsaIfNotThere() {
+        return "[ -f ~/.ssh/id_rsa ] || ( mkdir -p ~/.ssh ; chmod 700 ~/.ssh ; ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa )";
+    }
+    
+    // TODO a builder would be better than these ifBlahExistsElseBlah methods!
+    // (ideally formatting better also; though maybe SshTasks would be better?)
+    
+    /**
+     * Returns a command that runs only if the specified file (or link or directory) exists;
+     * if the command runs and fails that exit is preserved (but if the file does not exist exit code is zero).
+     * Executed as  { { not-file-exists && ok ; } || command ; }  for portability.
+     * ("if [ ... ] ; then xxx ; else xxx ; fi" syntax is not quite as portable, I seem to recall (not sure, Alex Aug 2013).) 
+     */
+    public static String ifFileExistsElse0(String path, String command) {
+        return alternativesGroup(
+                chainGroup(format("test ! -e %s", path), "true"),
+                command);
+    }
+    /** as {@link #ifFileExistsElse0(String, String)} but returns non-zero if the test fails (also returns non-zero if the command fails,
+     * so you can't tell the difference :( -- we need if ; then ; else ; fi semantics for that I think, but not sure how portable that is) */
+    public static String ifFileExistsElse1(String path, String command) {
+        return chainGroup(format("test -e %s", path), command);
+    }
+
+    /**
+     * Returns a command that runs only if the specified executable exists on the path (using `which`).
+     * if the command runs and fails that exit is preserved (but if the executable is not on the path exit code is zero).
+     * @see #ifFileExistsElse0(String, String) for implementation discussion, using <code>{ { test -z `which executable` && true ; } || command ; } 
+     */
+    public static String ifExecutableElse0(String executable, String command) {
+        return alternativesGroup(
+                chainGroup(format("test -z `which %s`", executable), "true"),
+                command);
+    }
+
+    /** as {@link #ifExecutableElse0(String, String)} but returns 1 if the test fails (also returns non-zero if the command fails) */
+    public static String ifExecutableElse1(String executable, String command) {
+        return chainGroup(format("which %s", executable), command);
+    }
+
+    /**
+     * Returns a command which
+     * executes <code>statement</code> only if <code>command</code> is NOT found in <code>$PATH</code>
+     *
+     * @param command
+     * @param statement
+     * @return command
+     */
+    public static String ifNotExecutable(String command, String statement) {
+        return String.format("{ { test ! -z `which %s`; } || { %s; } }", command, statement);
+    }
+
+    /**
+     * Returns a command that runs only if the specified executable exists on the path (using `which`).
+     * if the command runs and fails that exit is preserved (but if the executable is not on the path exit code is zero).
+     * @see #ifFileExistsElse0(String, String) for implementation discussion, using <code>{ { test -z `which executable` && true ; } || command ; }
+     */
+    public static String onlyIfExecutableMissing(String executable, String command) {
+        return alternativesGroup(format("which %s", executable), command);
+    }
+
+    public static String ifExecutableElse(String command, String ifTrue, String otherwise) {
+        return com.google.common.base.Joiner.on('\n').join(
+                ifExecutableElse(command, ImmutableList.<String>of(ifTrue), ImmutableList.<String>of(otherwise)));
+    }
+
+    public static ImmutableList<String> ifExecutableElse(String command, List<String> ifTrue, List<String> otherwise) {
+        return ImmutableList.<String>builder()
+                .add(String.format("if test -z `which %s`; then", command))
+                .addAll(ifTrue)
+                .add("else")
+                .addAll(otherwise)
+                .add("fi")
+                .build();
+    }
+
+    /**
+     * Returns a sequence of chained commands that runs until one of them fails (i.e. joined by '&&')
+     * This currently runs as a subshell (so exits are swallowed) but behaviour may be changed imminently. 
+     * (Use {@link #chainGroup(Collection)} or {@link #chainSubshell(Collection)} to be clear.)
+     */
+    public static String chain(Collection<String> commands) {
+        return "( " + Strings.join(commands, " && ") + " )";
+    }
+
+    /** Convenience for {@link #chain(Collection)} */
+    public static String chain(String ...commands) {
+        return "( " + Strings.join(commands, " && ") + " )";
+    }
+
+    /** As {@link #chain(Collection)}, but explicitly using { } grouping characters
+     * to ensure exits are propagated. */
+    public static String chainGroup(Collection<String> commands) {
+        // spaces required around curly braces
+        return "{ " + Strings.join(commands, " && ") + " ; }";
+    }
+
+    /** As {@link #chainGroup(Collection)} */
+    public static String chainGroup(String ...commands) {
+        return "{ " + Strings.join(commands, " && ") + " ; }";
+    }
+
+    /** As {@link #chain(Collection)}, but explicitly using ( ) grouping characters
+     * to ensure exits are caught. */
+    public static String chainSubshell(Collection<String> commands) {
+        // the spaces are not required, but it might be possible that a (( expr )) is interpreted differently
+        // (won't hurt to have the spaces in any case!) 
+        return "( " + Strings.join(commands, " && ") + " )";
+    }
+
+    /** As {@link #chainSubshell(Collection)} */
+    public static String chainSubshell(String ...commands) {
+        return "( " + Strings.join(commands, " && ") + "  )";
+    }
+
+    /**
+     * Returns a sequence of chained commands that runs until one of them succeeds (i.e. joined by '||').
+     * This currently runs as a subshell (so exits are swallowed) but behaviour may be changed imminently. 
+     * (Use {@link #alternativesGroup(Collection)} or {@link #alternativesSubshell(Collection)} to be clear.)
+     */
+    public static String alternatives(Collection<String> commands) {
+        return "( " + Strings.join(commands, " || ") + " )";
+    }
+
+    /** As {@link #alternatives(Collection)} */
+    public static String alternatives(String ...commands) {
+        return "( " + Strings.join(commands, " || ") + " )";
+    }
+
+    /** As {@link #alternatives(Collection)}, but explicitly using { } grouping characters
+     * to ensure exits are propagated. */
+    public static String alternativesGroup(Collection<String> commands) {
+        // spaces required around curly braces
+        return "{ " + Strings.join(commands, " || ") + " ; }";
+    }
+
+    /** As {@link #alternativesGroup(Collection)} */
+    public static String alternativesGroup(String ...commands) {
+        return "{ " + Strings.join(commands, " || ") + " ; }";
+    }
+
+    /** As {@link #alternatives(Collection)}, but explicitly using ( ) grouping characters
+     * to ensure exits are caught. */
+    public static String alternativesSubshell(Collection<String> commands) {
+        // the spaces are not required, but it might be possible that a (( expr )) is interpreted differently
+        // (won't hurt to have the spaces in any case!) 
+        return "( " + Strings.join(commands, " || ") + " )";
+    }
+
+    /** As {@link #alternativesSubshell(Collection)} */
+    public static String alternativesSubshell(String ...commands) {
+        return "( " + Strings.join(commands, " || ") + "  )";
+    }
+
+    /** returns the pattern formatted with the given arg if the arg is not null, otherwise returns null */
+    public static String formatIfNotNull(String pattern, Object arg) {
+        if (arg==null) return null;
+        return format(pattern, arg);
+    }
+    
+    public static String installPackage(String packageDefaultName) {
+        return installPackage(MutableMap.of(), packageDefaultName);
+    }
+    /**
+     * Returns a command for installing the given package.
+     * <p>
+     * Warns, but does not fail or return non-zero if it ultimately fails.
+     * <p>
+     * Flags can contain common overrides for {@code apt}, {@code yum}, {@code port} and {@code brew}
+     * as the package names can be different for each of those. Setting the default package name to
+     * {@literal null} will use only the overridden package manager values. The {@code onlyifmissing} flag
+     * adds a check for an executable, and only attempts to install packages if it is not found.
+     * <pre>
+     * installPackage(ImmutableMap.of("yum", "openssl-devel", "apt", "openssl libssl-dev zlib1g-dev"), "libssl-devel");
+     * installPackage(ImmutableMap.of("apt", "libaio1"), null);
+     * installPackage(ImmutableMap.of("onlyifmissing", "curl"), "curl");
+     * </pre>
+     */
+    public static String installPackage(Map<?,?> flags, String packageDefaultName) {
+        return installPackageOr(flags, packageDefaultName, null);
+    }
+    public static String installPackageOrFail(Map<?,?> flags, String packageDefaultName) {
+        return installPackageOr(flags, packageDefaultName, "exit 9");
+    }
+    public static String installPackageOr(Map<?,?> flags, String packageDefaultName, String optionalCommandToRunIfNone) {
+        String ifMissing = (String) flags.get("onlyifmissing");
+        String zypperInstall = formatIfNotNull("zypper --non-interactive --no-gpg-checks install %s", getFlag(flags, "zypper", packageDefaultName));
+        String aptInstall = formatIfNotNull("apt-get install -y --allow-unauthenticated %s", getFlag(flags, "apt", packageDefaultName));
+        String yumInstall = formatIfNotNull("yum -y --nogpgcheck install %s", getFlag(flags, "yum", packageDefaultName));
+        String brewInstall = formatIfNotNull("brew install %s", getFlag(flags, "brew", packageDefaultName));
+        String portInstall = formatIfNotNull("port install %s", getFlag(flags, "port", packageDefaultName));
+
+        List<String> commands = new LinkedList<String>();
+        if (ifMissing != null)
+            commands.add(format("which %s", ifMissing));
+        if (zypperInstall != null)
+            commands.add(ifExecutableElse1("zypper",
+                    chainGroup(
+                            "echo zypper exists, doing refresh",
+                            ok(sudo("zypper --non-interactive --no-gpg-checks refresh")),
+                            sudo(zypperInstall))));
+        if (aptInstall != null)
+            commands.add(ifExecutableElse1("apt-get",
+                    chainGroup(
+                        "echo apt-get exists, doing update",
+                        "export DEBIAN_FRONTEND=noninteractive",
+                        ok(sudo("apt-get update")), 
+                        sudo(aptInstall))));
+        if (yumInstall != null)
+            commands.add(ifExecutableElse1("yum", 
+                    chainGroup(
+                        "echo yum exists, doing update",
+                        ok(sudo("yum check-update")),
+                        ok(sudo("yum -y install epel-release")),
+                        sudo(yumInstall))));
+        if (brewInstall != null)
+            commands.add(ifExecutableElse1("brew", brewInstall));
+        if (portInstall != null)
+            commands.add(ifExecutableElse1("port", sudo(portInstall)));
+
+        String lastCommand = ok(warn("WARNING: no known/successful package manager to install " +
+                (packageDefaultName!=null ? packageDefaultName : flags.toString()) +
+                ", may fail subsequently"));
+        if (optionalCommandToRunIfNone != null)
+            lastCommand = chain(lastCommand, optionalCommandToRunIfNone);
+        commands.add(lastCommand);
+        
+        return alternatives(commands);
+    }
+    
+    public static String warn(String message) {
+        return "( echo "+BashStringEscapes.wrapBash(message)+" | tee /dev/stderr )";
+    }
+
+    /** returns a command which logs a message to stdout and stderr then exits with the given error code */
+    public static String fail(String message, int code) {
+        return chainGroup(warn(message), "exit "+code);
+    }
+
+    /** requires the command to have a non-zero exit code; e.g.
+     * <code>require("which foo", "Command foo must be found", 1)</code> */
+    public static String require(String command, String failureMessage, int exitCode) {
+        return alternativesGroup(command, fail(failureMessage, exitCode));
+    }
+
+    /** as {@link #require(String, String, int)} but returning the original exit code */
+    public static String require(String command, String failureMessage) {
+        return alternativesGroup(command, chainGroup("EXIT_CODE=$?", warn(failureMessage), "exit $EXIT_CODE"));
+    }
+
+    /** requires the test to pass, as valid bash `test` arguments; e.g.
+     * <code>requireTest("-f /etc/hosts", "Hosts file must exist", 1)</code> */
+    public static String requireTest(String test, String failureMessage, int exitCode) {
+        return require("test "+test, failureMessage, exitCode);
+    }
+
+    /** as {@link #requireTest(String, String, int)} but returning the original exit code */
+    public static String requireTest(String test, String failureMessage) {
+        return require("test "+test, failureMessage);
+    }
+
+    /** fails with nice error if the given file does not exist */
+    public static String requireFile(String file) {
+        return requireTest("-f "+BashStringEscapes.wrapBash(file), "The required file \""+file+"\" does not exist");
+    }
+
+    /** fails with nice error if the given file does not exist */
+    public static String requireExecutable(String command) {
+        return require("which "+BashStringEscapes.wrapBash(command), "The required executable \""+command+"\" does not exist");
+    }
+
+    public static String waitForFileContents(String file, String desiredContent, Duration timeout, boolean failOnTimeout) {
+        long secs = Math.max(timeout.toSeconds(), 1);
+        
+        List<String> commands = ImmutableList.of(
+                "for i in {1.."+secs+"}; do",
+                "    grep '"+desiredContent+"' "+file+" && result=0 || result=$?",
+                "    [ \"$result\" == 0 ] && break",
+                "    sleep 1",
+                "done",
+                "if test \"$result\" -ne 0; then",
+                "    "+ (failOnTimeout ?
+                            "echo \"Couldn't find "+desiredContent+" in "+file+"; aborting\" && exit 1" :
+                            "echo \"Couldn't find "+desiredContent+" in "+file+"; continuing\""),
+                "fi");
+        return Joiner.on("\n").join(commands);
+    }
+
+    public static String waitForPortFree(int port, Duration timeout, boolean failOnTimeout) {
+        long secs = Math.max(timeout.toSeconds(), 1);
+
+        // TODO How platform-dependent are the args + output format of netstat?
+        // TODO Not using sudo as wrapping either netstat call or sudo(alternativesGroup(...)) fails; parentheses too confusing!
+        String netstatCommand = alternativesGroup(
+                "sudo netstat -antp --tcp", // for Centos
+                "sudo netstat -antp TCP"); // for OS X 
+        
+        // number could appear in an IP address or as a port; look for white space at end, and dot or colon before
+        String grepCommand = "grep -E '(:|\\.)"+port+"($|\\s)' > /dev/null";
+        
+        List<String> commands = ImmutableList.of(
+                "for i in {1.."+secs+"}; do",
+                "    "+BashCommands.requireExecutable("netstat"),
+                "    "+alternativesGroup(
+                        chainGroup("which awk", "AWK_EXEC=awk"), 
+                        chainGroup("which gawk", "AWK_EXEC=gawk"), 
+                        chainGroup("which /usr/bin/awk", "AWK_EXEC=/usr/bin/awk"), 
+                        chainGroup("echo \"No awk to determine if Port "+port+" still in use; aborting\"", "exit 1")),
+                "    "+netstatCommand+" | $AWK_EXEC '{print $4}' | "+grepCommand+" && result=0 || result=$?",
+                "    [ \"$result\" != 0 ] && break",
+                "    sleep 1",
+                "done",
+                "if test \"$result\" -eq 0; then",
+                "    "+ (failOnTimeout ?
+                            "echo \"Port "+port+" still in use (according to netstat); aborting\" && exit 1" :
+                            "echo \"Port "+port+" still in use (according to netstat); continuing\""),
+                "fi");
+        return Joiner.on("\n").join(commands);
+    }
+
+    public static String unzip(String file, String targetDir) {
+        return "unzip " + file + (Strings.isNonBlank(targetDir) ? " -d "+targetDir : "");
+    }
+
+    public static final String INSTALL_TAR = installExecutable("tar");
+    public static final String INSTALL_CURL = installExecutable("curl");
+    public static final String INSTALL_WGET = installExecutable("wget");
+    public static final String INSTALL_ZIP = installExecutable("zip");
+    public static final String INSTALL_UNZIP = alternatives(installExecutable("unzip"), installExecutable("zip"));
+    public static final String INSTALL_SYSSTAT = installPackage(ImmutableMap.of("onlyifmissing", "iostat"), "sysstat");
+
+    /**
+     * Returns commands to download the URL, saving as the given file. Will try each URL in turn until one is successful
+     * (see `curl -f` documentation).
+     */
+    public static List<String> commandsToDownloadUrlsAs(List<String> urls, String saveAs) {
+        return Arrays.asList(INSTALL_CURL, 
+                require(simpleDownloadUrlAs(urls, saveAs), "Could not retrieve "+saveAs+". Tried: " + Joiner.on(", ").join(urls), 9));
+    }
+    public static String commandToDownloadUrlsAs(List<String> urls, String saveAs) {
+        return chain(INSTALL_CURL, 
+                require(simpleDownloadUrlAs(urls, saveAs), "Could not retrieve "+saveAs+". Tried: " + Joiner.on(", ").join(urls), 9));
+    }
+    public static String commandToDownloadUrlAs(String url, String saveAs) {
+        return chain(INSTALL_CURL, 
+                require(simpleDownloadUrlAs(Arrays.asList(url), saveAs), "Could not retrieve "+saveAs+" from " + url, 9));
+    }
+
+    /**
+     * Returns command to download the URL, sending the output to stdout --
+     * suitable for redirect by appending " | tar xvf".
+     * Will try each URL in turn until one is successful
+     */
+    public static String downloadToStdout(List<String> urls) {
+        return chain(
+                INSTALL_CURL + " > /dev/null", 
+                require(simpleDownloadUrlAs(urls, null), 
+                        "Could not retrieve file. Tried: " + Joiner.on(", ").join(urls), 9));
+    }
+    
+    /** as {@link #downloadToStdout(List)} but varargs for convenience */
+    public static String downloadToStdout(String ...urls) {
+        return downloadToStdout(Arrays.asList(urls));
+    }
+
+    /**
+     * Same as {@link downloadUrlAs(List, String)}, except does not install curl, and does not exit on failure,
+     * and if saveAs is null it downloads it so stdout.
+     */
+    public static String simpleDownloadUrlAs(List<String> urls, String saveAs) {
+        return simpleDownloadUrlAs(urls, null, null, saveAs);
+    }
+
+    public static String simpleDownloadUrlAs(List<String> urls, String user, String password, String saveAs) {
+        if (urls.isEmpty()) throw new IllegalArgumentException("No URLs supplied to download "+saveAs);
+        
+        List<String> commands = new ArrayList<String>();
+        for (String url : urls) {
+            String command = "curl -f -L -k --retry 10 --keepalive-time 30 --speed-time 30 ";
+            if (user!=null && password!=null) {
+               command = command + format("-u %s:%s ", user, password);
+            }
+            command = command + format("\"%s\"", url);
+            if (saveAs!=null) {
+                command = command + format(" -o %s", saveAs);
+            }
+            commands.add(command);
+        }
+        return alternatives(commands);
+    }
+
+    private static Object getFlag(Map<?,?> flags, String flagName, Object defaultValue) {
+        Object found = flags.get(flagName);
+        return found == null ? defaultValue : found;
+    }
+
+    /**
+     * Install a particular Java runtime, fails if not possible.
+     * <p>
+     * <em><strong>Note</strong> Java 8 is not yet supported on SUSE</em>
+     *
+     * @return The command to install the given Java runtime.
+     * @see #installJava6OrFail()
+     * @see #installJava7Or6OrFail()
+     * @see #installJavaLatestOrFail()
+     */
+    public static String installJava(int version) {
+        Preconditions.checkArgument(version == 6 || version == 7 || version == 8, "Supported Java versions are 6, 7, or 8");
+        return installPackageOr(MutableMap.of("apt", "openjdk-" + version + "-jdk","yum", "java-1." + version + ".0-openjdk-devel"), null,
+                ifExecutableElse1("zypper", chainGroup(
+                        ok(sudo("zypper --non-interactive addrepo http://download.opensuse.org/repositories/Java:/openjdk6:/Factory/SLE_11_SP3 java_sles_11")),
+                        ok(sudo("zypper --non-interactive addrepo http://download.opensuse.org/repositories/Java:/openjdk6:/Factory/openSUSE_11.4 java_suse_11")),
+                        ok(sudo("zypper --non-interactive addrepo http://download.opensuse.org/repositories/Java:/openjdk6:/Factory/openSUSE_12.3 java_suse_12")),
+                        ok(sudo("zypper --non-interactive addrepo http://download.opensuse.org/repositories/Java:/openjdk6:/Factory/openSUSE_13.1 java_suse_13")),
+                        alternatives(installPackageOrFail(MutableMap.of("zypper", "java-1_" + version + "_0-openjdk-devel"), null),
+                                installPackageOrFail(MutableMap.of("zypper", "java-1_" + version + "_0-ibm"), null)))));
+    }
+
+    public static String installJava6() {
+        return installJava(6);
+    }
+    public static String installJava7() {
+        return installJava(7);
+    }
+    public static String installJava8() {
+        return installJava(8);
+    }
+
+    public static String installJava6IfPossible() {
+        return ok(installJava6());
+    }
+    public static String installJava7IfPossible() {
+        return ok(installJava7());
+    }
+    public static String installJava8IfPossible() {
+        return ok(installJava8());
+    }
+
+    public static String installJava6OrFail() {
+        return alternatives(installJava6(), fail("java 6 install failed", 9));
+    }
+    public static String installJava7OrFail() {
+        return alternatives(installJava7(), fail("java 7 install failed", 9));
+    }
+    public static String installJava7Or6OrFail() {
+        return alternatives(installJava7(), installJava6(), fail("java install failed", 9));
+    }
+    public static String installJavaLatestOrFail() {
+        return alternatives(installJava8(), installJava7(), installJava6(), fail("java latest install failed", 9));
+    }
+
+    public static String installJavaLatestOrWarn() {
+        return alternatives(installJava8(), installJava7(), installJava6(), warn("java latest install failed, entity may subsequently fail"));
+    }
+
+    /** cats the given text to the given command, using bash << multi-line input syntax */
+    public static String pipeTextTo(String text, String command) {
+        String id = Identifiers.makeRandomId(8);
+        return "cat << EOF_"+id+" | "+command+"\n"
+                +text
+                +"\n"+"EOF_"+id+"\n";
+    }
+
+    public static String prependToEtcHosts(String ip, String... hostnames) {
+        String tempFileId = "bak"+Identifiers.makeRandomId(4);
+        return sudo(String.format("sed -i."+tempFileId+" -e '1i\\\n%s %s' /etc/hosts", ip, Joiner.on(" ").join(hostnames)));
+    }
+    
+    public static String appendToEtcHosts(String ip, String... hostnames) {
+        // Using sed rather than `echo ... >> /etc/hosts` because when embedded inside sudo, 
+        // the redirect doesn't get executed by sudo.
+        String tempFileId = "bak"+Identifiers.makeRandomId(4);
+        return sudo(String.format("sed -i."+tempFileId+" -e '$a\\\n%s %s' /etc/hosts", ip, Joiner.on(" ").join(hostnames)));
+    }
+
+    /**
+     * Sets the hostname, splitting the given hostname if it contains a dot to include the unqualified and fully qualified names.
+     *  
+     * @see {@link #setHostname(String, String)}
+     */
+    @Beta
+    public static List<String> setHostname(String newHostname) {
+        // See http://www.dns-sd.org/trailingdotsindomainnames.html.
+        // If we are given "abcd." then let's pass that as-is to setHostname("abcd.", null)
+        
+        if (newHostname.indexOf(".") > 0) {
+            String hostPart = newHostname.substring(0, newHostname.indexOf("."));
+            String domainPart = newHostname.substring(hostPart.length()+1);
+            return setHostname(hostPart, domainPart);
+        } else {
+            return setHostname(newHostname, null);
+        }
+    }
+    
+    /**
+     * Sets the hostname to {@code hostPart + "." + domainPart}, or if domainPart is null/empty then {code hostPart}.
+     * 
+     * @param hostPart
+     * @param domainPart
+     * @return
+     */
+    @Beta
+    public static List<String> setHostname(String hostPart, String domainPart) {
+        // See:
+        // - http://www.rackspace.com/knowledge_center/article/centos-hostname-change
+        // - https://wiki.debian.org/HowTo/ChangeHostname
+        // - http://askubuntu.com/questions/9540/how-do-i-change-the-computer-name
+        //
+        // We prepend in /etc/hosts, to ensure the right fqn appears first.
+        //    e.g. comment in http://askubuntu.com/questions/158957/how-to-set-the-fully-qualified-domain-name-in-12-04
+        //    says "It's important to note that the first domain in /etc/hosts should be your FQDN. "
+        //
+        // TODO Should we run `sudo service hostname restart` or `sudo /etc/init.d/hostname restart`?
+        //      I don't think we need to because we've run `sudo hostname <newname>`
+        //
+        // TODO What if /etc/sysconfig/network doesn't have a line for HOSTNAME=...?
+        //
+        // TODO What about hostPart ending in "." - see http://www.dns-sd.org/trailingdotsindomainnames.html
+        //      for what that means in DNS. However, hostname is not the same as the DNS name (hostnames 
+        //      predate the invention of DNS! - although frequently the DNS name has the same first portion 
+        //      as the hostname) so the link you gave is not relevant. However despite searching Google and 
+        //      man pages I [Ricard] am unable to find a reference which clearly states what characters are 
+        //      relevant in a hostname. I think it's safest to assume that the hostname is just [a-z,0-9,-] 
+        //      and no dots at all.
+        
+        checkNotNull(hostPart, "hostPart");
+        checkArgument(!hostPart.contains("."), "hostPart '%s' must not contain '.'", hostPart);
+
+        String tempFileId = "bak"+Identifiers.makeRandomId(4);
+        
+        List<String> allhostnames = Lists.newArrayList();
+        String fqdn = hostPart;
+        if (Strings.isNonBlank(domainPart)) {
+            fqdn = hostPart+"."+domainPart;
+            allhostnames.add(fqdn);
+        }
+        allhostnames.add(hostPart);
+        allhostnames.add("localhost");
+        
+        return ImmutableList.of(
+                sudo("sed -i."+tempFileId+" -e 's/^127.0.0.1/# Replaced by Brooklyn\\\n#127.0.0.1/' /etc/hosts"),
+                prependToEtcHosts("127.0.0.1", allhostnames.toArray(new String[allhostnames.size()])),
+                ifFileExistsElse0("/etc/sysconfig/network", sudo("sed -i."+tempFileId+" -e 's/^HOSTNAME=.*$/HOSTNAME="+hostPart+"/' /etc/sysconfig/network")),
+                ifFileExistsElse0("/etc/hostname", sudo("sed -i."+tempFileId+" -e 's/^[a-zA-Z_0-9].*$/"+hostPart+"/' /etc/hostname")),
+                sudo("hostname "+hostPart));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/ssh/IptablesCommands.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/ssh/IptablesCommands.java b/utils/common/src/main/java/org/apache/brooklyn/util/ssh/IptablesCommands.java
new file mode 100644
index 0000000..2edc9bf
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/ssh/IptablesCommands.java
@@ -0,0 +1,261 @@
+/*
+ * 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.util.ssh;
+
+import static org.apache.brooklyn.util.ssh.BashCommands.alternatives;
+import static org.apache.brooklyn.util.ssh.BashCommands.chain;
+import static org.apache.brooklyn.util.ssh.BashCommands.installPackage;
+import static org.apache.brooklyn.util.ssh.BashCommands.sudo;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Optional;
+
+public class IptablesCommands {
+
+    public enum Chain {
+        INPUT, FORWARD, OUTPUT
+    }
+
+    public enum Policy {
+        ACCEPT, REJECT, DROP, LOG
+    }
+
+    /*** @deprecated since 0.7; use {@link org.apache.brooklyn.util.net.Protocol} */
+    @Deprecated
+    public enum Protocol {
+        TCP("tcp"), UDP("udp"), ALL("all");
+
+        final String protocol;
+
+        private Protocol(String protocol) {
+            this.protocol = protocol;
+        }
+
+        @Override
+        public String toString() {
+            return protocol;
+        }
+
+        org.apache.brooklyn.util.net.Protocol convert() {
+            switch (this) {
+                case TCP: return org.apache.brooklyn.util.net.Protocol.TCP;
+                case UDP: return org.apache.brooklyn.util.net.Protocol.UDP;
+                case ALL: return org.apache.brooklyn.util.net.Protocol.ALL;
+                default: throw new IllegalStateException("Unexpected protocol "+this);
+            }
+        }
+    }
+
+    @Beta // implementation not portable across distros
+    public static String iptablesService(String cmd) {
+        return sudo(alternatives(
+                BashCommands.ifExecutableElse1("service", "service iptables " + cmd),
+                "/sbin/service iptables " + cmd));
+    }
+
+    @Beta // implementation not portable across distros
+    public static String iptablesServiceStop() {
+        return iptablesService("stop");
+    }
+
+    @Beta // implementation not portable across distros
+    public static String iptablesServiceStart() {
+        return iptablesService("start");
+    }
+
+    @Beta // implementation not portable across distros
+    public static String iptablesServiceRestart() {
+        return iptablesService("restart");
+    }
+
+    @Beta // implementation not portable across distros
+    public static String iptablesServiceStatus() {
+        return iptablesService("status");
+    }
+
+    @Beta // implementation not portable across distros
+    public static String firewalldService(String cmd) {
+        return sudo(alternatives(
+                BashCommands.ifExecutableElse1("systemctl", "systemctl " + cmd + " firewalld"),
+                "/usr/bin/systemctl " + cmd + " firewalld"));
+    }
+
+    @Beta // implementation not portable across distros
+    public static String firewalldServiceStop() {
+        return firewalldService("stop");
+    }
+
+    @Beta // implementation not portable across distros
+    public static String firewalldServiceStart() {
+        return firewalldService("start");
+    }
+
+    @Beta // implementation not portable across distros
+    public static String firewalldServiceRestart() {
+        return firewalldService("restart");
+    }
+
+    @Beta // implementation not portable across distros
+    public static String firewalldServiceStatus() {
+        return firewalldService("status");
+    }
+
+    @Beta // implementation not portable across distros
+    public static String firewalldServiceIsActive() {
+        return firewalldService("is-active");
+    }
+
+    /**
+     * Returns the command that saves iptables rules on file.
+     *
+     * @return Returns the command that saves iptables rules on file.
+     *
+     */
+    public static String saveIptablesRules() {
+        return alternatives(sudo("service iptables save"),
+                            chain(installPackage("iptables-persistent"), sudo("/etc/init.d/iptables-persistent save")));
+    }
+
+    /**
+     * Returns the command that cleans up iptables rules.
+     *
+     * @return Returns the command that cleans up iptables rules.
+     */
+    public static String cleanUpIptablesRules() {
+       return sudo("/sbin/iptables -F");
+    }
+
+    /**
+     * Returns the iptables rules.
+     *
+     * @return Returns the command that list all the iptables rules.
+     */
+    public static String listIptablesRule() {
+       return sudo("/sbin/iptables -L -v -n");
+    }
+
+    /**
+     * Returns the command that inserts a rule on top of the iptables' rules to all interfaces.
+     *
+     * @return Returns the command that inserts a rule on top of the iptables'
+     *         rules.
+     */
+    public static String insertIptablesRule(Chain chain, org.apache.brooklyn.util.net.Protocol protocol, int port, Policy policy) {
+        return addIptablesRule("-I", chain, Optional.<String> absent(), protocol, port, policy);
+    }
+
+    /** @deprecated since 0.7.0; use {@link #insertIptablesRule(Chain, org.apache.brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
+    public static String insertIptablesRule(Chain chain, Protocol protocol, int port, Policy policy) {
+        return insertIptablesRule(chain, protocol.convert(), port, policy);
+    }
+
+    /**
+     * Returns the command that inserts a rule on top of the iptables' rules.
+     *
+     * @return Returns the command that inserts a rule on top of the iptables'
+     *         rules.
+     */
+    public static String insertIptablesRule(Chain chain, String networkInterface, org.apache.brooklyn.util.net.Protocol protocol, int port, Policy policy) {
+        return addIptablesRule("-I", chain, Optional.of(networkInterface), protocol, port, policy);
+    }
+
+    /** @deprecated since 0.7.0; use {@link #insertIptablesRule(Chain, String, org.apache.brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
+    public static String insertIptablesRule(Chain chain, String networkInterface, Protocol protocol, int port, Policy policy) {
+        return insertIptablesRule(chain, networkInterface, protocol.convert(), port, policy);
+    }
+
+    /**
+     * Returns the command that appends a rule to iptables to all interfaces.
+     *
+     * @return Returns the command that appends a rule to iptables.
+     */
+    public static String appendIptablesRule(Chain chain, org.apache.brooklyn.util.net.Protocol protocol, int port, Policy policy) {
+        return addIptablesRule("-A", chain, Optional.<String> absent(), protocol, port, policy);
+    }
+
+    /** @deprecated since 0.7.0; use {@link #appendIptablesRule(Chain, org.apache.brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
+    public static String appendIptablesRule(Chain chain, Protocol protocol, int port, Policy policy) {
+        return appendIptablesRule(chain, protocol.convert(), port, policy);
+    }
+
+    /**
+     * Returns the command that appends a rule to iptables.
+     *
+     * @return Returns the command that appends a rule to iptables.
+     */
+    public static String appendIptablesRule(Chain chain, String networkInterface, org.apache.brooklyn.util.net.Protocol protocol, int port, Policy policy) {
+        return addIptablesRule("-A", chain, Optional.of(networkInterface), protocol, port, policy);
+    }
+
+    /** @deprecated since 0.7.0; use {@link #appendIptablesRule(Chain, String, org.apache.brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
+    public static String appendIptablesRule(Chain chain, String networkInterface, Protocol protocol, int port, Policy policy) {
+        return appendIptablesRule(chain, networkInterface, protocol.convert(), port, policy);
+    }
+
+    /**
+     * Returns the command that creates a rule to iptables.
+     *
+     * @return Returns the command that creates a rule for iptables.
+     */
+    public static String addIptablesRule(String direction, Chain chain, Optional<String> networkInterface, org.apache.brooklyn.util.net.Protocol protocol, int port, Policy policy) {
+        String addIptablesRule;
+        if(networkInterface.isPresent()) {
+           addIptablesRule = String.format("/sbin/iptables %s %s -i %s -p %s --dport %d -j %s", direction, chain, networkInterface.get(), protocol, port, policy);
+        } else {
+           addIptablesRule = String.format("/sbin/iptables %s %s -p %s --dport %d -j %s", direction, chain, protocol, port, policy);
+        }
+        return sudo(addIptablesRule);
+    }
+
+    /** @deprecated since 0.7.0; use {@link #addIptablesRule(String, Chain, Optional, org.apache.brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
+    public static String addIptablesRule(String direction, Chain chain, Optional<String> networkInterface, Protocol protocol, int port, Policy policy) {
+        return addIptablesRule(direction, chain, networkInterface, protocol.convert(), port, policy);
+    }
+
+    /**
+     * Returns the command that adds firewalld direct rule.
+     *
+     * @return Returns the command that adds firewalld direct rule.
+     */
+    public static String addFirewalldRule(Chain chain, org.apache.brooklyn.util.net.Protocol protocol, int port, Policy policy) {
+        return addFirewalldRule(chain, Optional.<String>absent(), protocol, port, policy);
+    }
+    
+    /**
+     * Returns the command that adds firewalld direct rule.
+     *
+     * @return Returns the command that adds firewalld direct rule.
+     */
+    public static String addFirewalldRule(Chain chain, Optional<String> networkInterface, org.apache.brooklyn.util.net.Protocol protocol, int port, Policy policy) {
+        String command = new String("/usr/bin/firewall-cmd");
+        String commandPermanent = new String("/usr/bin/firewall-cmd --permanent");
+        
+        String interfaceParameter = String.format("%s", networkInterface.isPresent() ? " -i " + networkInterface.get() : "");
+        
+        String commandParameters = String.format(" --direct --add-rule ipv4 filter %s 0 %s -p %s --dport %d -j %s", 
+                                                                chain, interfaceParameter,  protocol, port, policy);
+        
+        return sudo(chain(command + commandParameters, commandPermanent + commandParameters));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/stream/DelegatingPrintStream.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/stream/DelegatingPrintStream.java b/utils/common/src/main/java/org/apache/brooklyn/util/stream/DelegatingPrintStream.java
new file mode 100644
index 0000000..11728c4
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/stream/DelegatingPrintStream.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.util.stream;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Locale;
+
+/** PrintStream which simply delegates to the implementation of getDelegate() */
+public abstract class DelegatingPrintStream extends PrintStream {
+    
+    public DelegatingPrintStream() {
+        super(new IllegalOutputStream());
+    }
+    
+    protected abstract PrintStream getDelegate();
+
+    public int hashCode() {
+        return getDelegate().hashCode();
+    }
+
+    public void write(byte[] b) throws IOException {
+        getDelegate().write(b);
+    }
+
+    public boolean equals(Object obj) {
+        return getDelegate().equals(obj);
+    }
+
+    public String toString() {
+        return getDelegate().toString();
+    }
+
+    public void flush() {
+        getDelegate().flush();
+    }
+
+    public void close() {
+        getDelegate().close();
+    }
+
+    public boolean checkError() {
+        return getDelegate().checkError();
+    }
+
+    public void write(int b) {
+        getDelegate().write(b);
+    }
+
+    public void write(byte[] buf, int off, int len) {
+        getDelegate().write(buf, off, len);
+    }
+
+    public void print(boolean b) {
+        getDelegate().print(b);
+    }
+
+    public void print(char c) {
+        getDelegate().print(c);
+    }
+
+    public void print(int i) {
+        getDelegate().print(i);
+    }
+
+    public void print(long l) {
+        getDelegate().print(l);
+    }
+
+    public void print(float f) {
+        getDelegate().print(f);
+    }
+
+    public void print(double d) {
+        getDelegate().print(d);
+    }
+
+    public void print(char[] s) {
+        getDelegate().print(s);
+    }
+
+    public void print(String s) {
+        getDelegate().print(s);
+    }
+
+    public void print(Object obj) {
+        getDelegate().print(obj);
+    }
+
+    public void println() {
+        getDelegate().println();
+    }
+
+    public void println(boolean x) {
+        getDelegate().println(x);
+    }
+
+    public void println(char x) {
+        getDelegate().println(x);
+    }
+
+    public void println(int x) {
+        getDelegate().println(x);
+    }
+
+    public void println(long x) {
+        getDelegate().println(x);
+    }
+
+    public void println(float x) {
+        getDelegate().println(x);
+    }
+
+    public void println(double x) {
+        getDelegate().println(x);
+    }
+
+    public void println(char[] x) {
+        getDelegate().println(x);
+    }
+
+    public void println(String x) {
+        getDelegate().println(x);
+    }
+
+    public void println(Object x) {
+        getDelegate().println(x);
+    }
+
+    public PrintStream printf(String format, Object... args) {
+        return getDelegate().printf(format, args);
+    }
+
+    public PrintStream printf(Locale l, String format, Object... args) {
+        return getDelegate().printf(l, format, args);
+    }
+
+    public PrintStream format(String format, Object... args) {
+        return getDelegate().format(format, args);
+    }
+
+    public PrintStream format(Locale l, String format, Object... args) {
+        return getDelegate().format(l, format, args);
+    }
+
+    public PrintStream append(CharSequence csq) {
+        return getDelegate().append(csq);
+    }
+
+    public PrintStream append(CharSequence csq, int start, int end) {
+        return getDelegate().append(csq, start, end);
+    }
+
+    public PrintStream append(char c) {
+        return getDelegate().append(c);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/stream/IllegalOutputStream.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/stream/IllegalOutputStream.java b/utils/common/src/main/java/org/apache/brooklyn/util/stream/IllegalOutputStream.java
new file mode 100644
index 0000000..1fcb1e8
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/stream/IllegalOutputStream.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util.stream;
+
+import java.io.OutputStream;
+
+/** output stream which throws if anyone tries to write to it */
+public class IllegalOutputStream extends OutputStream {
+    @Override public void write(int b) {
+        throw new IllegalStateException("should not write to this output stream");
+    }
+    @Override public void write(byte[] b, int off, int len) {
+        throw new IllegalStateException("should not write to this output stream");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/stream/InputStreamSupplier.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/stream/InputStreamSupplier.java b/utils/common/src/main/java/org/apache/brooklyn/util/stream/InputStreamSupplier.java
new file mode 100644
index 0000000..e1832f1
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/stream/InputStreamSupplier.java
@@ -0,0 +1,49 @@
+/*
+ * 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.util.stream;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.google.common.io.InputSupplier;
+
+public class InputStreamSupplier implements InputSupplier<InputStream> {
+
+    private final InputStream target;
+
+    /** @deprecated since 0.7.0; use {@link InputStreamSupplier#of(InputStream)} instead */
+    @Deprecated
+    public InputStreamSupplier(InputStream target) {
+        this.target = target;
+    }
+
+    @Override
+    public InputStream getInput() throws IOException {
+        return target;
+    }
+
+    public static InputStreamSupplier of(InputStream target) {
+        return new InputStreamSupplier(target);
+    }
+
+    public static InputStreamSupplier fromString(String input) {
+        return new InputStreamSupplier(Streams.newInputStreamWithContents(input));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/stream/KnownSizeInputStream.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/stream/KnownSizeInputStream.java b/utils/common/src/main/java/org/apache/brooklyn/util/stream/KnownSizeInputStream.java
new file mode 100644
index 0000000..2580bb2
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/stream/KnownSizeInputStream.java
@@ -0,0 +1,113 @@
+/*
+ * 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.util.stream;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** an input stream, whose size we know */
+public class KnownSizeInputStream extends InputStream {
+
+    public static KnownSizeInputStream of(String contents) {
+        return of(contents.getBytes());
+    }
+
+    public static KnownSizeInputStream of(byte[] contents) {
+        return new KnownSizeInputStream(new ByteArrayInputStream(contents), contents.length);
+    }
+
+    private final long length;
+    private final InputStream target;
+    
+    public KnownSizeInputStream(InputStream target, long length) {
+        this.target = checkNotNull(target, "target");
+        this.length = length;
+    }
+    
+    public long length() {
+        return length;
+    }
+    
+    public InputStream getTarget() {
+        return target;
+    }
+
+    @Override
+    public int read() throws IOException {
+        return target.read();
+    }
+
+    @Override
+    public int hashCode() {
+        return target.hashCode();
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        return target.read(b);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return target.equals(obj);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        return target.read(b, off, len);
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        return target.skip(n);
+    }
+
+    @Override
+    public int available() throws IOException {
+        return target.available();
+    }
+
+    @Override
+    public String toString() {
+        return target.toString();
+    }
+
+    @Override
+    public void close() throws IOException {
+        target.close();
+    }
+
+    @Override
+    public void mark(int readlimit) {
+        target.mark(readlimit);
+    }
+
+    @Override
+    public void reset() throws IOException {
+        target.reset();
+    }
+
+    @Override
+    public boolean markSupported() {
+        return target.markSupported();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/stream/ReaderInputStream.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/stream/ReaderInputStream.java b/utils/common/src/main/java/org/apache/brooklyn/util/stream/ReaderInputStream.java
new file mode 100644
index 0000000..a142052
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/stream/ReaderInputStream.java
@@ -0,0 +1,202 @@
+/*
+ * 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.util.stream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+public class ReaderInputStream extends InputStream {
+
+    /** Source Reader */
+    private Reader in;
+
+    private String encoding = System.getProperty("file.encoding");
+
+    private byte[] slack;
+
+    private int begin;
+
+    /**
+     * Construct a <{@link ReaderInputStream}
+     * for the specified {@link Reader}.
+     *
+     * @param reader   {@link Reader}; must not be {@code null}.
+     */
+    public ReaderInputStream(Reader reader) {
+        in = reader;
+    }
+
+    /**
+     * Construct a {@link ReaderInputStream}
+     * for the specified {@link Reader},
+     * with the specified encoding.
+     *
+     * @param reader     non-null {@link Reader}.
+     * @param encoding   non-null {@link String} encoding.
+     */
+    public ReaderInputStream(Reader reader, String encoding) {
+        this(reader);
+        if (encoding == null) {
+            throw new IllegalArgumentException("encoding must not be null");
+        } else {
+            this.encoding = encoding;
+        }
+    }
+
+    /**
+     * Reads from the {@link Reader}, returning the same value.
+     *
+     * @return the value of the next character in the {@link Reader}.
+     *
+     * @exception IOException if the original {@link Reader} fails to be read
+     */
+    public synchronized int read() throws IOException {
+        if (in == null) {
+            throw new IOException("Stream Closed");
+        }
+
+        byte result;
+        if (slack != null && begin < slack.length) {
+            result = slack[begin];
+            if (++begin == slack.length) {
+                slack = null;
+            }
+        } else {
+            byte[] buf = new byte[1];
+            if (read(buf, 0, 1) <= 0) {
+                result = -1;
+            }
+            result = buf[0];
+        }
+
+        if (result < -1) {
+            result += 256;
+        }
+
+        return result;
+    }
+
+    /**
+     * Reads from the {@link Reader} into a byte array
+     *
+     * @param b  the byte array to read into
+     * @param off the offset in the byte array
+     * @param len the length in the byte array to fill
+     * @return the actual number read into the byte array, -1 at
+     *         the end of the stream
+     * @exception IOException if an error occurs
+     */
+    public synchronized int read(byte[] b, int off, int len)
+        throws IOException {
+        if (in == null) {
+            throw new IOException("Stream Closed");
+        }
+
+        while (slack == null) {
+            char[] buf = new char[len]; // might read too much
+            int n = in.read(buf);
+            if (n == -1) {
+                return -1;
+            }
+            if (n > 0) {
+                slack = new String(buf, 0, n).getBytes(encoding);
+                begin = 0;
+            }
+        }
+
+        if (len > slack.length - begin) {
+            len = slack.length - begin;
+        }
+
+        System.arraycopy(slack, begin, b, off, len);
+
+        if ((begin += len) >= slack.length) {
+            slack = null;
+        }
+
+        return len;
+    }
+
+    /**
+     * Marks the read limit of the StringReader.
+     *
+     * @param limit the maximum limit of bytes that can be read before the
+     *              mark position becomes invalid
+     */
+    public synchronized void mark(final int limit) {
+        try {
+            in.mark(limit);
+        } catch (IOException ioe) {
+            throw new RuntimeException(ioe.getMessage());
+        }
+    }
+
+
+    /**
+     * @return   the current number of bytes ready for reading
+     * @exception IOException if an error occurs
+     */
+    public synchronized int available() throws IOException {
+        if (in == null) {
+            throw new IOException("Stream Closed");
+        }
+        if (slack != null) {
+            return slack.length - begin;
+        }
+        if (in.ready()) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * @return false - mark is not supported
+     */
+    public boolean markSupported () {
+        return false;   // would be imprecise
+    }
+
+    /**
+     * Resets the StringReader.
+     *
+     * @exception IOException if the StringReader fails to be reset
+     */
+    public synchronized void reset() throws IOException {
+        if (in == null) {
+            throw new IOException("Stream Closed");
+        }
+        slack = null;
+        in.reset();
+    }
+
+    /**
+     * Closes the Stringreader.
+     *
+     * @exception IOException if the original StringReader fails to be closed
+     */
+    public synchronized void close() throws IOException {
+        if (in != null) {
+            in.close();
+            slack = null;
+            in = null;
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/stream/StreamGobbler.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/stream/StreamGobbler.java b/utils/common/src/main/java/org/apache/brooklyn/util/stream/StreamGobbler.java
new file mode 100644
index 0000000..a440336
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/stream/StreamGobbler.java
@@ -0,0 +1,137 @@
+/*
+ * 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.util.stream;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.slf4j.Logger;
+
+public class StreamGobbler extends Thread implements Closeable {
+    
+    protected final InputStream stream;
+    protected final PrintStream out;
+    protected final Logger log;
+    private final AtomicBoolean running = new AtomicBoolean(true);
+    
+    public StreamGobbler(InputStream stream, OutputStream out, Logger log) {
+        this(stream, out != null ? new PrintStream(out) : null, log);
+    }
+
+    public StreamGobbler(InputStream stream, PrintStream out, Logger log) {
+        this.stream = stream;
+        this.out = out;
+        this.log = log;
+    }
+    
+    @Override
+    public void close() {
+        running.set(false);
+        interrupt();
+    }
+
+    /**
+     * @deprecate Use close() instead.
+     */
+    @Deprecated
+    public void shutdown() {
+        close();
+    }
+
+    String logPrefix = "";
+    String printPrefix = "";
+    public StreamGobbler setPrefix(String prefix) {
+        setLogPrefix(prefix);
+        setPrintPrefix(prefix);
+        return this;
+    }
+    public StreamGobbler setPrintPrefix(String prefix) {
+        printPrefix = prefix;
+        return this;
+    }
+    public StreamGobbler setLogPrefix(String prefix) {
+        logPrefix = prefix;
+        return this;
+    }    
+    
+    @Override
+    public void run() {
+        int c = -1;
+        try {
+            while (running.get() && (c=stream.read())>=0) {
+                onChar(c);
+            }
+            onClose();
+        } catch (IOException e) {
+            onClose();
+            //TODO parametrise log level, for this error, and for normal messages
+            if (log!=null && log.isTraceEnabled()) log.trace(logPrefix+"exception reading from stream ("+e+")");
+        }
+    }
+    
+    private final StringBuilder lineSoFar = new StringBuilder(16);
+    public void onChar(int c) {
+        if (c=='\n' || c=='\r') {
+            if (lineSoFar.length()>0)
+                //suppress blank lines, so that we can treat either newline char as a line separator
+                //(eg to show curl updates frequently)
+                onLine(lineSoFar.toString());
+            lineSoFar.setLength(0);
+        } else {
+            lineSoFar.append((char)c);
+        }
+    }
+    
+    public void onLine(String line) {
+        //right trim, in case there is \r or other funnies
+        while (line.length()>0 && Character.isWhitespace(line.charAt(line.length()-1)))
+            line = line.substring(0, line.length()-1);
+        //right trim, in case there is \r or other funnies
+        while (line.length()>0 && (line.charAt(0)=='\n' || line.charAt(0)=='\r'))
+            line = line.substring(1);
+        if (!line.isEmpty()) {
+            if (out!=null) out.println(printPrefix+line);
+            if (log!=null && log.isDebugEnabled()) log.debug(logPrefix+line);
+        }
+    }
+    
+    public void onClose() {
+        onLine(lineSoFar.toString());
+        if (out!=null) out.flush();
+        lineSoFar.setLength(0);
+        finished = true;
+        synchronized (this) { notifyAll(); }
+    }
+    
+    private volatile boolean finished = false;
+
+    /** convenience -- equivalent to calling join() */
+    public void blockUntilFinished() throws InterruptedException {
+        synchronized (this) { while (!finished) wait(); }
+    }
+
+    /** convenience -- similar to !Thread.isAlive() */
+    public boolean isFinished() {
+        return finished;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/stream/Streams.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/stream/Streams.java b/utils/common/src/main/java/org/apache/brooklyn/util/stream/Streams.java
new file mode 100644
index 0000000..6d262a9
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/stream/Streams.java
@@ -0,0 +1,176 @@
+/*
+ * 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.util.stream;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Charsets;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.CharStreams;
+
+/**
+ * Methods to manage byte and character streams.
+ *
+ * @see com.google.common.io.ByteStreams
+ * @see com.google.common.io.CharStreams
+ */
+public class Streams {
+
+    private static final Logger log = LoggerFactory.getLogger(Streams.class);
+
+    /** drop-in non-deprecated replacement for {@link Closeable}'s deprecated closeQuiety;
+     * we may wish to review usages, particularly as we drop support for java 1.6,
+     * but until then use this instead of the deprecated method */
+    @Beta
+    public static void closeQuietly(Closeable x) {
+        try {
+            if (x!=null)
+                x.close();
+        } catch (Exception e) {
+            if (log.isDebugEnabled())
+                log.debug("Error closing (ignored) "+x+": "+e);
+        }
+    }
+
+    /** @deprecated since 0.7.0 use {@link #newInputStreamWithContents(String)} */ @Deprecated
+    public static InputStream fromString(String contents) {
+        return newInputStreamWithContents(contents);
+    }
+    
+    public static InputStream newInputStreamWithContents(String contents) {
+        byte[] bytes = checkNotNull(contents, "contents").getBytes(Charsets.UTF_8);
+        return KnownSizeInputStream.of(bytes);
+    }
+
+    public static Reader newReaderWithContents(String contents) {
+        return new StringReader(contents);
+    }
+    
+    public static Reader reader(InputStream stream) {
+        return new InputStreamReader(stream);
+    }
+    
+    public static Reader reader(InputStream stream, Charset charset) {
+        return new InputStreamReader(stream, charset);
+    }
+
+    /** reads the input stream fully, returning a byte array; throws unchecked exception on failure;
+     *  to get a string, use <code>readFully(reader(is))</code> or <code>readFullyString(is)</code> */
+    public static byte[] readFully(InputStream is) {
+        try {
+            return ByteStreams.toByteArray(is);
+        } catch (IOException ioe) {
+            throw Exceptions.propagate(ioe);
+        }
+    }
+
+    public static String readFullyString(InputStream is) {
+        return readFully(reader(is));
+    }
+    
+    public static String readFully(Reader is) {
+        try {
+            return CharStreams.toString(is);
+        } catch (IOException ioe) {
+            throw Exceptions.propagate(ioe);
+        }
+    }
+
+    public static void copy(InputStream input, OutputStream output) {
+        try {
+            ByteStreams.copy(input, output);
+            output.flush();
+        } catch (IOException ioe) {
+            throw Exceptions.propagate(ioe);
+        }
+    }
+
+    public static void copy(Reader input, Writer output) {
+        try {
+            CharStreams.copy(input, output);
+            output.flush();
+        } catch (IOException ioe) {
+            throw Exceptions.propagate(ioe);
+        }
+    }
+
+    public static Supplier<Integer> sizeSupplier(final ByteArrayOutputStream src) {
+        Preconditions.checkNotNull(src);
+        return new Supplier<Integer>() {
+            @Override
+            public Integer get() {
+                return src.size();
+            }
+        };
+    }
+
+    public static Function<ByteArrayOutputStream,Integer> sizeFunction() {
+        return new Function<ByteArrayOutputStream,Integer>() {
+            @Override
+            public Integer apply(ByteArrayOutputStream input) {
+                return input.size();
+            }
+        };
+    }
+
+    public static ByteArrayOutputStream byteArrayOfString(String in) {
+        return byteArray(in.getBytes(Charsets.UTF_8));
+    }
+
+    public static ByteArrayOutputStream byteArray(byte[] in) {
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        try {
+            stream.write(in);
+        } catch (IOException e) {
+            throw Exceptions.propagate(e);
+        }
+        return stream;
+    }
+
+    public static boolean logStreamTail(Logger log, String message, ByteArrayOutputStream stream, int max) {
+        if (stream!=null && stream.size()>0) {
+            String streamS = stream.toString();
+            if (max>=0 && streamS.length()>max)
+                streamS = "... "+streamS.substring(streamS.length()-max);
+            log.info(message+":\n"+streamS);
+            return true;
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/stream/ThreadLocalPrintStream.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/stream/ThreadLocalPrintStream.java b/utils/common/src/main/java/org/apache/brooklyn/util/stream/ThreadLocalPrintStream.java
new file mode 100644
index 0000000..40633d1
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/stream/ThreadLocalPrintStream.java
@@ -0,0 +1,137 @@
+/*
+ * 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.util.stream;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+import org.apache.commons.io.output.TeeOutputStream;
+
+public class ThreadLocalPrintStream extends DelegatingPrintStream {
+
+    protected PrintStream defaultPrintStream;
+    protected final ThreadLocal<PrintStream> customStream = new ThreadLocal<PrintStream>();
+    
+    public ThreadLocalPrintStream(PrintStream defaultPrintStream) {
+        this.defaultPrintStream = defaultPrintStream;
+    }
+    
+    @Override
+    public PrintStream getDelegate() {
+        PrintStream delegate = customStream.get();
+        if (delegate!=null) return delegate;
+        return defaultPrintStream;
+    }
+
+    /** sets the PrintStream that callers from this thread should see;
+     * returns any previously custom PrintStream for this thread */
+    public PrintStream setThreadLocalPrintStream(OutputStream stream) {
+        PrintStream old = customStream.get();
+        if (!(stream instanceof PrintStream))
+            stream = new PrintStream(stream);
+        customStream.set((PrintStream)stream);
+        return old;
+    }
+
+    public PrintStream clearThreadLocalPrintStream() {
+        PrintStream old = customStream.get();
+        customStream.remove();
+        if (old!=null) old.flush();
+        return old;
+    }
+    
+    /** creates a capturing context which eats the output to this stream, blocking the original target */
+    public OutputCapturingContext capture() {
+        return new OutputCapturingContext(this);
+    }
+    
+    /** creates a capturing context which sees the output to this stream, without interrupting the original target */
+    public OutputCapturingContext captureTee() {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        PrintStream toRestore = setThreadLocalPrintStream(new TeeOutputStream(getDelegate(), out));
+        return new OutputCapturingContext(this, out, toRestore);
+    }
+    
+    public static class OutputCapturingContext {
+        protected final ThreadLocalPrintStream stream;
+        protected final ByteArrayOutputStream out;
+        protected final OutputStream streamToRestore;
+        protected boolean finished = false;
+        /** constructor which installs a ByteArrayOutputStream to this stream */
+        public OutputCapturingContext(ThreadLocalPrintStream stream) {
+            this.stream = stream;
+            this.out = new ByteArrayOutputStream();
+            this.streamToRestore = stream.setThreadLocalPrintStream(out);
+        }
+        /** constructor for a capturing context which is already installed */
+        public OutputCapturingContext(ThreadLocalPrintStream stream, ByteArrayOutputStream capturingStream, OutputStream optionalStreamToRestore) {
+            this.stream = stream;
+            this.out = capturingStream;
+            this.streamToRestore = optionalStreamToRestore;
+        }
+        public String getOutputSoFar() {
+            return out.toString();
+        }
+        public String end() {
+            if (streamToRestore!=null)
+                stream.setThreadLocalPrintStream(streamToRestore);
+            else
+                stream.clearThreadLocalPrintStream();
+            finished = true;
+            return out.toString();
+        }
+        public boolean isActive() {
+            return !finished;
+        }
+        public ByteArrayOutputStream getOutputStream() {
+            return out;
+        }
+        @Override
+        public String toString() {
+            return getOutputSoFar();
+        }
+        public boolean isEmpty() {
+            return out.size()==0;
+        }
+    }
+    
+    /** installs a thread local print stream to System.out if one is not already set;
+     * caller may then #capture and #captureTee on it.
+     * @return the ThreadLocalPrintStream which System.out is using */
+    public synchronized static ThreadLocalPrintStream stdout() {
+        PrintStream oldOut = System.out;
+        if (oldOut instanceof ThreadLocalPrintStream) return (ThreadLocalPrintStream)oldOut;
+        ThreadLocalPrintStream newOut = new ThreadLocalPrintStream(System.out);
+        System.setOut(newOut);
+        return newOut;
+    }
+
+    /** installs a thread local print stream to System.err if one is not already set;
+     * caller may then #capture and #captureTee on it.
+     * @return the ThreadLocalPrintStream which System.err is using */
+    public synchronized static ThreadLocalPrintStream stderr() {
+        PrintStream oldErr = System.err;
+        if (oldErr instanceof ThreadLocalPrintStream) return (ThreadLocalPrintStream)oldErr;
+        ThreadLocalPrintStream newErr = new ThreadLocalPrintStream(System.err);
+        System.setErr(newErr);
+        return newErr;
+    }
+
+}


Mime
View raw message