blob: c4033ee2e46170a948d9c728aff06a582ba68a0d [file] [log] [blame]
package brooklyn.location.basic;
import static brooklyn.util.GroovyJavaMethods.truth;
import groovy.lang.Closure;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.Reader;
import java.net.InetAddress;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import brooklyn.config.BrooklynLogging;
import brooklyn.config.ConfigKey;
import brooklyn.config.ConfigKey.HasConfigKey;
import brooklyn.config.ConfigUtils;
import brooklyn.entity.basic.ConfigKeys;
import brooklyn.event.basic.BasicConfigKey;
import brooklyn.event.basic.MapConfigKey;
import brooklyn.location.Location;
import brooklyn.location.MachineLocation;
import brooklyn.location.OsDetails;
import brooklyn.location.PortRange;
import brooklyn.location.PortSupplier;
import brooklyn.location.basic.PortRanges.BasicPortRange;
import brooklyn.location.geo.HasHostGeoInfo;
import brooklyn.location.geo.HostGeoInfo;
import brooklyn.util.ResourceUtils;
import brooklyn.util.collections.MutableMap;
import brooklyn.util.config.ConfigBag;
import brooklyn.util.exceptions.Exceptions;
import brooklyn.util.flags.SetFromFlag;
import brooklyn.util.flags.TypeCoercions;
import brooklyn.util.internal.ssh.SshException;
import brooklyn.util.internal.ssh.SshTool;
import brooklyn.util.internal.ssh.sshj.SshjTool;
import brooklyn.util.mutex.MutexSupport;
import brooklyn.util.mutex.WithMutexes;
import brooklyn.util.pool.BasicPool;
import brooklyn.util.pool.Pool;
import brooklyn.util.stream.KnownSizeInputStream;
import brooklyn.util.stream.ReaderInputStream;
import brooklyn.util.stream.StreamGobbler;
import brooklyn.util.task.Tasks;
import brooklyn.util.text.Strings;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
/**
* Operations on a machine that is accessible via ssh.
* <p>
* We expose two ways of running scripts.
* One (execCommands) passes lines to bash, that is lightweight but fragile.
* Another (execScript) creates a script on the remote machine, more portable but heavier.
* <p>
* Additionally there are routines to copyTo, copyFrom; and installTo (which tries a curl, and falls back to copyTo
* in event the source is accessible by the caller only).
*/
public class SshMachineLocation extends AbstractLocation implements MachineLocation, PortSupplier, WithMutexes, Closeable {
public static final Logger LOG = LoggerFactory.getLogger(SshMachineLocation.class);
public static final Logger logSsh = LoggerFactory.getLogger(BrooklynLogging.SSH_IO);
protected interface ExecRunner {
public int exec(SshTool ssh, Map<String,?> flags, List<String> cmds, Map<String,?> env);
}
@SetFromFlag
String user;
@SetFromFlag
String privateKeyData;
@SetFromFlag(nullable = false)
InetAddress address;
@SetFromFlag
transient WithMutexes mutexSupport;
@SetFromFlag
private Set<Integer> usedPorts;
/** any property that should be passed as ssh config (connection-time)
* can be prefixed with this and . and will be passed through (with the prefix removed),
* e.g. (SSHCONFIG_PREFIX+"."+"StrictHostKeyChecking"):"yes"
* @deprecated use {@link SshTool#BROOKLYN_CONFIG_KEY_PREFIX} */
@Deprecated
public static final String SSHCONFIG_PREFIX = "sshconfig";
public static final ConfigKey<String> SSH_HOST = ConfigKeys.SSH_CONFIG_HOST;
public static final ConfigKey<Integer> SSH_PORT = ConfigKeys.SSH_CONFIG_PORT;
public static final ConfigKey<String> SSH_EXECUTABLE = ConfigKeys.newStringConfigKey("sshExecutable", "Allows an `ssh` executable file to be specified, to be used in place of the default (programmatic) java ssh client", null);
public static final ConfigKey<String> SCP_EXECUTABLE = ConfigKeys.newStringConfigKey("scpExecutable", "Allows an `scp` executable file to be specified, to be used in place of the default (programmatic) java ssh client", null);
// TODO remove
public static final ConfigKey<String> PASSWORD = SshTool.PROP_PASSWORD;
public static final ConfigKey<String> PRIVATE_KEY_FILE = SshTool.PROP_PRIVATE_KEY_FILE;
public static final ConfigKey<String> PRIVATE_KEY_DATA = SshTool.PROP_PRIVATE_KEY_DATA;
public static final ConfigKey<String> PRIVATE_KEY_PASSPHRASE = SshTool.PROP_PRIVATE_KEY_PASSPHRASE;
public static final ConfigKey<String> SCRIPT_DIR = ConfigKeys.newStringConfigKey("scriptDir", "directory where scripts should be placed and executed on the SSH target machine", null);
public static final ConfigKey<Map<String,Object>> SSH_ENV_MAP = new MapConfigKey<Object>(Object.class, "env", "environment variables to pass to the remote SSH shell session", null);
public static final ConfigKey<Boolean> ALLOCATE_PTY = SshTool.PROP_ALLOCATE_PTY;
// TODO remove
// new BasicConfigKey<Boolean>(Boolean.class, "allocatePTY", "whether pseudo-terminal emulation should be turned on; " +
// "this causes stderr to be redirected to stdout, but it may be required for some commands (such as `sudo` when requiretty is set)", false);
public static final ConfigKey<OutputStream> STDOUT = new BasicConfigKey<OutputStream>(OutputStream.class, "out");
public static final ConfigKey<OutputStream> STDERR = new BasicConfigKey<OutputStream>(OutputStream.class, "err");
public static final ConfigKey<Boolean> NO_STDOUT_LOGGING = new BasicConfigKey<Boolean>(Boolean.class, "noStdoutLogging", "whether to disable logging of stdout from SSH commands (e.g. for verbose commands)", false);
public static final ConfigKey<Boolean> NO_STDERR_LOGGING = new BasicConfigKey<Boolean>(Boolean.class, "noStderrLogging", "whether to disable logging of stderr from SSH commands (e.g. for verbose commands)", false);
public static final ConfigKey<String> LOG_PREFIX = ConfigKeys.newStringConfigKey("logPrefix");
public static final ConfigKey<File> LOCAL_TEMP_DIR = SshTool.PROP_LOCAL_TEMP_DIR;
/** specifies config keys where a change in the value does not require a new SshTool instance,
* i.e. these can be specified per command on the tool */
public static final Set<ConfigKey<?>> REUSABLE_SSH_PROPS = ImmutableSet.of(STDOUT, STDERR, SCRIPT_DIR);
public static final Set<HasConfigKey<?>> ALL_SSH_CONFIG_KEYS =
ImmutableSet.<HasConfigKey<?>>builder().
addAll(ConfigUtils.getStaticKeysOnClass(SshMachineLocation.class)).
addAll(ConfigUtils.getStaticKeysOnClass(SshTool.class)).
build();
public static final Set<String> ALL_SSH_CONFIG_KEY_NAMES =
ImmutableSet.copyOf(Iterables.transform(ALL_SSH_CONFIG_KEYS, new Function<HasConfigKey<?>,String>() {
@Override
public String apply(HasConfigKey<?> input) {
return input.getConfigKey().getName();
}
}));
private transient Pool<SshTool> vanillaSshToolPool;
public SshMachineLocation() {
this(MutableMap.of());
}
public SshMachineLocation(Map properties) {
super(properties);
usedPorts = (usedPorts != null) ? Sets.newLinkedHashSet(usedPorts) : Sets.<Integer>newLinkedHashSet();
vanillaSshToolPool = buildVanillaPool();
}
private BasicPool<SshTool> buildVanillaPool() {
return BasicPool.<SshTool>builder()
.name(name+"@"+address+
(hasConfig(SSH_HOST) ? "("+getConfig(SSH_HOST)+":"+getConfig(SSH_PORT)+")" : "")+
":"+
System.identityHashCode(this))
.supplier(new Supplier<SshTool>() {
@Override public SshTool get() {
return connectSsh(Collections.emptyMap());
}})
.viabilityChecker(new Predicate<SshTool>() {
@Override public boolean apply(SshTool input) {
return input != null && input.isConnected();
}})
.closer(new Function<SshTool,Void>() {
@Override public Void apply(SshTool input) {
try {
input.disconnect();
} catch (Exception e) {
if (logSsh.isDebugEnabled()) logSsh.debug("On machine "+SshMachineLocation.this+", ssh-disconnect failed", e);
}
return null;
}})
.build();
}
public void configure(Map properties) {
super.configure(properties);
// TODO Note that check for addresss!=null is done automatically in super-constructor, in FlagUtils.checkRequiredFields
// Yikes, dangerous code for accessing fields of sub-class in super-class' constructor! But getting away with it so far!
if (mutexSupport == null) {
mutexSupport = new MutexSupport();
}
boolean deferConstructionChecks = (properties.containsKey("deferConstructionChecks") && TypeCoercions.coerce(properties.get("deferConstructionChecks"), Boolean.class));
if (!deferConstructionChecks) {
if (properties.containsKey("username")) {
LOG.warn("Using deprecated ssh machine property 'username': use 'user' instead", new Throwable("source of deprecated ssh 'username' invocation"));
user = ""+properties.get("username");
}
if (name == null) {
name = (truth(user) ? user+"@" : "") + address.getHostName();
}
if (getHostGeoInfo() == null) {
Location parentLocation = getParent();
if ((parentLocation instanceof HasHostGeoInfo) && ((HasHostGeoInfo)parentLocation).getHostGeoInfo()!=null)
setHostGeoInfo( ((HasHostGeoInfo)parentLocation).getHostGeoInfo() );
else
setHostGeoInfo(HostGeoInfo.fromLocation(this));
}
}
}
/** @deprecated temporary Beta method introduced in 0.5.0 */
public void addConfig(Map<String, Object> vals) {
// configure(vals);
getConfigBag().putAll(vals);
}
@Override
public void close() throws IOException {
vanillaSshToolPool.close();
}
@Override
protected void finalize() throws Throwable {
close();
}
public InetAddress getAddress() {
return address;
}
public String getUser() {
return user;
}
public int run(String command) {
return run(MutableMap.of(), command, MutableMap.of());
}
public int run(Map props, String command) {
return run(props, command, MutableMap.of());
}
public int run(String command, Map env) {
return run(MutableMap.of(), command, env);
}
public int run(Map props, String command, Map env) {
return run(props, ImmutableList.of(command), env);
}
/**
* @deprecated in 1.4.1, @see execCommand and execScript
*/
public int run(List<String> commands) {
return run(MutableMap.of(), commands, MutableMap.of());
}
public int run(Map props, List<String> commands) {
return run(props, commands, MutableMap.of());
}
public int run(List<String> commands, Map env) {
return run(MutableMap.of(), commands, env);
}
public int run(final Map props, final List<String> commands, final Map env) {
if (commands == null || commands.isEmpty()) return 0;
return execSsh(props, new Function<SshTool, Integer>() {
public Integer apply(SshTool ssh) {
return ssh.execScript(props, commands, env);
}});
}
protected <T> T execSsh(Map props, Function<SshTool,T> task) {
if (props.isEmpty() || Sets.difference(props.keySet(), REUSABLE_SSH_PROPS).isEmpty()) {
return vanillaSshToolPool.exec(task);
} else {
SshTool ssh = connectSsh(props);
try {
return task.apply(ssh);
} finally {
ssh.disconnect();
}
}
}
protected SshTool connectSsh() {
return connectSsh(ImmutableMap.of());
}
protected boolean previouslyConnected = false;
protected SshTool connectSsh(Map props) {
try {
if (!truth(user)) user = System.getProperty("user.name");
ConfigBag args = new ConfigBag().
configure(SshTool.PROP_USER, user).
// default value of host, overridden if SSH_HOST is supplied
configure(SshTool.PROP_HOST, address.getHostName()).
putAll(props);
for (Map.Entry<String,Object> entry: getAllConfig(true).entrySet()) {
String key = entry.getKey();
if (key.startsWith(SshTool.BROOKLYN_CONFIG_KEY_PREFIX)) {
key = Strings.removeFromStart(key, SshTool.BROOKLYN_CONFIG_KEY_PREFIX);
} else if (key.startsWith(SSHCONFIG_PREFIX)) {
key = Strings.removeFromStart(key, SSHCONFIG_PREFIX);
} else if (ALL_SSH_CONFIG_KEY_NAMES.contains(entry.getKey())) {
// key should be included, and does not need to be changed
// TODO make this config-setting mechanism more universal
// currently e.g. it will not admit a tool-specific property.
// thinking either we know about the tool here,
// or we don't allow unadorned keys to be set
// (require use of BROOKLYN_CONFOG_KEY_PREFIX)
} else {
// this key is not applicatble here; ignore it
continue;
}
args.putStringKey(key, entry.getValue());
}
if (LOG.isTraceEnabled()) LOG.trace("creating ssh session for "+args);
// look up tool class
String sshToolClass = args.get(SshTool.PROP_TOOL_CLASS);
if (sshToolClass==null) sshToolClass = SshjTool.class.getName();
SshTool ssh = (SshTool) Class.forName(sshToolClass).getConstructor(Map.class).newInstance(args.getAllConfig());
if (LOG.isTraceEnabled()) LOG.trace("using ssh-tool {} (of type {}); props ", ssh, sshToolClass);
Tasks.setBlockingDetails("Opening ssh connection");
try { ssh.connect(); } finally { Tasks.setBlockingDetails(null); }
previouslyConnected = true;
return ssh;
} catch (Exception e) {
if (previouslyConnected) throw Throwables.propagate(e);
// subsequence connection (above) most likely network failure, our remarks below won't help
// on first connection include additional information if we can't connect, to help with debugging
String rootCause = Throwables.getRootCause(e).getMessage();
throw new IllegalStateException("Cannot establish ssh connection to "+user+" @ "+this+
(rootCause!=null && !rootCause.isEmpty() ? " ("+rootCause+")" : "")+". \n"+
"Ensure that passwordless and passphraseless ssh access is enabled using standard keys from ~/.ssh or " +
"as configured in brooklyn.properties. " +
"Check that the target host is accessible, " +
"that credentials are correct (location and permissions if using a key), " +
"that the SFTP subsystem is available on the remote side, " +
"and that there is sufficient random noise in /dev/random on both ends. " +
"To debug less common causes, see the original error in the trace or log, and/or enable 'net.schmizz' (sshj) logging."
, e);
}
}
/**
* Convenience for running commands using ssh {@literal exec} mode.
* @deprecated in 1.4.1, @see execCommand and execScript
*/
public int exec(List<String> commands) {
return exec(MutableMap.of(), commands, MutableMap.of());
}
public int exec(Map props, List<String> commands) {
return exec(props, commands, MutableMap.of());
}
public int exec(List<String> commands, Map env) {
return exec(MutableMap.of(), commands, env);
}
public int exec(final Map props, final List<String> commands, final Map env) {
Preconditions.checkNotNull(address, "host address must be specified for ssh");
if (commands == null || commands.isEmpty()) return 0;
return execSsh(props, new Function<SshTool, Integer>() {
public Integer apply(SshTool ssh) {
return ssh.execCommands(props, commands, env);
}});
}
// TODO submitCommands and submitScript which submit objects we can subsequently poll (cf JcloudsSshMachineLocation.submitRunScript)
/** executes a set of commands, directly on the target machine (no wrapping in script).
* joined using ' ; ' by default.
* <p>
* Stdout and stderr will be logged automatically to brooklyn.SSH logger, unless the
* flags 'noStdoutLogging' and 'noStderrLogging' are set. To set a logging prefix, use
* the flag 'logPrefix'.
* <p>
* Currently runs the commands in an interactive/login shell
* by passing each as a line to bash. To terminate early, use:
* <pre>
* foo || exit 1
* </pre>
* It may be desirable instead, in some situations, to wrap as:
* <pre>
* { line1 ; } && { line2 ; } ...
* </pre>
* and run as a single command (possibly not as an interacitve/login
* shell) causing the script to exit on the first command which fails.
* <p>
* Currently this has to be done by the caller.
* (If desired we can add a flag {@code exitIfAnyNonZero} to support this mode,
* and/or {@code commandPrepend} and {@code commandAppend} similar to
* (currently supported in SshjTool) {@code separator}.)
*/
public int execCommands(String summaryForLogging, List<String> commands) {
return execCommands(MutableMap.<String,Object>of(), summaryForLogging, commands, MutableMap.<String,Object>of());
}
public int execCommands(Map<String,?> props, String summaryForLogging, List<String> commands) {
return execCommands(props, summaryForLogging, commands, MutableMap.<String,Object>of());
}
public int execCommands(String summaryForLogging, List<String> commands, Map<String,?> env) {
return execCommands(MutableMap.<String,Object>of(), summaryForLogging, commands, env);
}
public int execCommands(Map<String,?> props, String summaryForLogging, List<String> commands, Map<String,?> env) {
return execWithLogging(props, summaryForLogging, commands, env, new ExecRunner() {
@Override public int exec(SshTool ssh, Map<String,?> flags, List<String> cmds, Map<String,?> env) {
return ssh.execCommands(flags, cmds, env);
}});
}
/** executes a set of commands, wrapped as a script sent to the remote machine.
* <p>
* Stdout and stderr will be logged automatically to brooklyn.SSH logger, unless the
* flags 'noStdoutLogging' and 'noStderrLogging' are set. To set a logging prefix, use
* the flag 'logPrefix'.
*/
public int execScript(String summaryForLogging, List<String> commands) {
return execScript(MutableMap.<String,Object>of(), summaryForLogging, commands, MutableMap.<String,Object>of());
}
public int execScript(Map<String,?> props, String summaryForLogging, List<String> commands) {
return execScript(props, summaryForLogging, commands, MutableMap.<String,Object>of());
}
public int execScript(String summaryForLogging, List<String> commands, Map<String,?> env) {
return execScript(MutableMap.<String,Object>of(), summaryForLogging, commands, env);
}
public int execScript(Map<String,?> props, String summaryForLogging, List<String> commands, Map<String,?> env) {
return execWithLogging(props, summaryForLogging, commands, env, new ExecRunner() {
@Override public int exec(SshTool ssh, Map<String, ?> flags, List<String> cmds, Map<String, ?> env) {
return ssh.execScript(flags, cmds, env);
}});
}
protected int execWithLogging(Map<String,?> props, String summaryForLogging, List<String> commands, Map env, final Closure<Integer> execCommand) {
return execWithLogging(props, summaryForLogging, commands, env, new ExecRunner() {
@Override public int exec(SshTool ssh, Map<String, ?> flags, List<String> cmds, Map<String, ?> env) {
return execCommand.call(ssh, flags, cmds, env);
}});
}
@SuppressWarnings("resource")
protected int execWithLogging(Map<String,?> props, final String summaryForLogging, final List<String> commands, final Map<String,?> env, final ExecRunner execCommand) {
if (logSsh.isDebugEnabled()) logSsh.debug("{} on machine {}: {}", new Object[] {summaryForLogging, this, commands});
Preconditions.checkNotNull(address, "host address must be specified for ssh");
if (commands.isEmpty()) {
logSsh.debug("{} on machine {} ending: no commands to run", summaryForLogging, this);
return 0;
}
final ConfigBag execFlags = new ConfigBag().putAll(props);
// some props get overridden in execFlags, so remove them from the ssh flags
final ConfigBag sshFlags = new ConfigBag().putAll(props).removeAll(LOG_PREFIX, STDOUT, STDERR);
PipedOutputStream outO = null;
PipedOutputStream outE = null;
StreamGobbler gO=null, gE=null;
try {
String logPrefix = execFlags.get(LOG_PREFIX);
if (logPrefix == null) {
String hostname = getAddress().getHostName();
Integer port = execFlags.peek(SshTool.PROP_PORT);
if (port == null) port = getConfig(ConfigUtils.prefixedKey(SshTool.BROOKLYN_CONFIG_KEY_PREFIX, SshTool.PROP_PORT));
if (port == null) port = getConfig(ConfigUtils.prefixedKey(SSHCONFIG_PREFIX, SshTool.PROP_PORT));
logPrefix = (user != null ? user+"@" : "") + hostname + (port != null ? ":"+port : "");
}
if (!execFlags.get(NO_STDOUT_LOGGING)) {
PipedInputStream insO = new PipedInputStream();
outO = new PipedOutputStream(insO);
String stdoutLogPrefix = "["+(logPrefix != null ? logPrefix+":stdout" : "stdout")+"] ";
gO = new StreamGobbler(insO, execFlags.get(STDOUT), logSsh).setLogPrefix(stdoutLogPrefix);
gO.start();
execFlags.put(STDOUT, outO);
}
if (!execFlags.get(NO_STDERR_LOGGING)) {
PipedInputStream insE = new PipedInputStream();
outE = new PipedOutputStream(insE);
String stderrLogPrefix = "["+(logPrefix != null ? logPrefix+":stderr" : "stderr")+"] ";
gE = new StreamGobbler(insE, execFlags.get(STDERR), logSsh).setLogPrefix(stderrLogPrefix);
gE.start();
execFlags.put(STDERR, outE);
}
Tasks.setBlockingDetails("SSH executing, "+summaryForLogging);
try {
return execSsh(MutableMap.copyOf(sshFlags.getAllConfig()), new Function<SshTool, Integer>() {
public Integer apply(SshTool ssh) {
int result = execCommand.exec(ssh, MutableMap.copyOf(execFlags.getAllConfig()), commands, env);
if (logSsh.isDebugEnabled()) logSsh.debug("{} on machine {} completed: return status {}", new Object[] {summaryForLogging, this, result});
return result;
}});
} finally {
Tasks.setBlockingDetails(null);
}
} catch (IOException e) {
if (logSsh.isDebugEnabled()) logSsh.debug("{} on machine {} failed: {}", new Object[] {summaryForLogging, this, e});
throw Throwables.propagate(e);
} finally {
// Must close the pipedOutStreams, otherwise input will never read -1 so StreamGobbler thread would never die
if (outO!=null) try { outO.flush(); } catch (IOException e) {}
if (outE!=null) try { outE.flush(); } catch (IOException e) {}
Closeables.closeQuietly(outO);
Closeables.closeQuietly(outE);
try {
if (gE!=null) { gE.join(); }
if (gO!=null) { gO.join(); }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Throwables.propagate(e);
}
}
}
public int copyTo(File src, File destination) {
return copyTo(MutableMap.<String,Object>of(), src, destination);
}
public int copyTo(Map<String,?> props, File src, File destination) {
return copyTo(props, src, destination.getPath());
}
public int copyTo(File src, String destination) {
return copyTo(MutableMap.<String,Object>of(), src, destination);
}
public int copyTo(Map<String,?> props, File src, String destination) {
Preconditions.checkNotNull(address, "Host address must be specified for scp");
Preconditions.checkArgument(src.exists(), "File %s must exist for scp", src.getPath());
try {
return copyTo(props, new FileInputStream(src), src.length(), destination);
} catch (FileNotFoundException e) {
throw Throwables.propagate(e);
}
}
public int copyTo(Reader src, String destination) {
return copyTo(MutableMap.<String,Object>of(), src, destination);
}
public int copyTo(Map<String,?> props, Reader src, String destination) {
return copyTo(props, new ReaderInputStream(src), destination);
}
public int copyTo(InputStream src, String destination) {
return copyTo(MutableMap.<String,Object>of(), src, 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, 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.copyToServer(props, new KnownSizeInputStream(src, filesize), destination);
// return ssh.createFile(props, destination, src, filesize);
}});
}
}
// 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
public int copyFrom(String remote, String local) {
return copyFrom(MutableMap.<String,Object>of(), remote, local);
}
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.copyFromServer(props, remote, new File(local));
}});
}
/** installs the given URL at the indicated destination.
* attempts to curl the sourceUrl on the remote machine,
* then if that fails, loads locally (from classpath or file) and transfers.
* <p>
* accepts either a path (terminated with /) or filename for the destination.
**/
public int installTo(ResourceUtils loader, String url, String destination) {
if (destination.endsWith("/")) {
String destName = url;
destName = destName.contains("?") ? destName.substring(0, destName.indexOf("?")) : destName;
destName = destName.substring(destName.lastIndexOf('/')+1);
destination = destination + destName;
}
LOG.debug("installing {} to {} on {}, attempting remote curl", new Object[] {url, destination, this});
try {
PipedInputStream insO = new PipedInputStream(); OutputStream outO = new PipedOutputStream(insO);
PipedInputStream insE = new PipedInputStream(); OutputStream outE = new PipedOutputStream(insE);
new StreamGobbler(insO, null, LOG).setLogPrefix("[curl @ "+address+":stdout] ").start();
new StreamGobbler(insE, null, LOG).setLogPrefix("[curl @ "+address+":stdout] ").start();
int result = exec(MutableMap.of("out", outO, "err", outE),
ImmutableList.of("curl "+url+" -L --silent --insecure --show-error --fail --connect-timeout 60 --max-time 600 --retry 5 -o "+destination));
if (result!=0 && loader!=null) {
LOG.debug("installing {} to {} on {}, curl failed, attempting local fetch and copy", new Object[] {url, destination, this});
result = copyTo(loader.getResourceFromUrl(url), destination);
}
if (result==0)
LOG.debug("installing {} complete; {} on {}", new Object[] {url, destination, this});
else
LOG.warn("installing {} failed; {} on {}: {}", new Object[] {url, destination, this, result});
return result;
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
@Override
public String toString() {
return "SshMachineLocation["+name+":"+address+"]";
}
@Override
public String toVerboseString() {
return Objects.toStringHelper(this).omitNullValues()
.add("id", getId()).add("name", getDisplayName())
.add("user", getUser()).add("address", getAddress()).add("port", getConfig(SSH_PORT))
.add("parentLocation", getParent())
.toString();
}
/**
* @see #obtainPort(PortRange)
* @see BasicPortRange#ANY_HIGH_PORT
*/
public boolean obtainSpecificPort(int portNumber) {
// TODO Does not yet check if the port really is free on this machine
if (usedPorts.contains(portNumber)) {
return false;
} else {
usedPorts.add(portNumber);
return true;
}
}
public int obtainPort(PortRange range) {
for (int p: range)
if (obtainSpecificPort(p)) return p;
if (LOG.isDebugEnabled()) LOG.debug("unable to find port in {} on {}; returning -1", range, this);
return -1;
}
public void releasePort(int portNumber) {
usedPorts.remove((Object) portNumber);
}
public boolean isSshable() {
String cmd = "date";
try {
int result = run(cmd);
if (result == 0) {
return true;
} else {
if (LOG.isDebugEnabled()) LOG.debug("Not reachable: {}, executing `{}`, exit code {}", new Object[] {this, cmd, result});
return false;
}
} catch (SshException e) {
if (LOG.isDebugEnabled()) LOG.debug("Exception checking if "+this+" is reachable; assuming not", e);
return false;
} catch (IllegalStateException e) {
if (LOG.isDebugEnabled()) LOG.debug("Exception checking if "+this+" is reachable; assuming not", e);
return false;
} catch (RuntimeException e) {
if (Exceptions.getFirstThrowableOfType(e, IOException.class) != null) {
if (LOG.isDebugEnabled()) LOG.debug("Exception checking if "+this+" is reachable; assuming not", e);
return false;
} else {
throw e;
}
}
}
@Override
public OsDetails getOsDetails() {
// TODO ssh and find out what we need to know, or use jclouds...
return BasicOsDetails.Factory.ANONYMOUS_LINUX;
}
protected WithMutexes newMutexSupport() { return new MutexSupport(); }
@Override
public void acquireMutex(String mutexId, String description) throws InterruptedException {
mutexSupport.acquireMutex(mutexId, description);
}
@Override
public boolean tryAcquireMutex(String mutexId, String description) {
return mutexSupport.tryAcquireMutex(mutexId, description);
}
@Override
public void releaseMutex(String mutexId) {
mutexSupport.releaseMutex(mutexId);
}
@Override
public boolean hasMutex(String mutexId) {
return mutexSupport.hasMutex(mutexId);
}
//We want want the SshMachineLocation to be serializable and therefor the pool needs to be dealt with correctly.
//In this case we are not serializing the pool (we made the field transient) and create a new pool when deserialized.
//This fix is currently needed for experiments, but isn't used in normal Brooklyn usage.
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
vanillaSshToolPool = buildVanillaPool();
}
}