brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From henev...@apache.org
Subject [12/71] [abbrv] incubator-brooklyn git commit: Merge commit 'e430723' into reorg2
Date Wed, 23 Dec 2015 11:06:35 GMT
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/core/src/main/java/org/apache/brooklyn/location/byon/SingleMachineProvisioningLocation.java
----------------------------------------------------------------------
diff --cc brooklyn-server/core/src/main/java/org/apache/brooklyn/location/byon/SingleMachineProvisioningLocation.java
index 0000000,eb8c4f9..cba4966
mode 000000,100644..100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/location/byon/SingleMachineProvisioningLocation.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/location/byon/SingleMachineProvisioningLocation.java
@@@ -1,0 -1,90 +1,93 @@@
+ /*
+  * 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.location.byon;
+ 
+ import java.util.Map;
+ 
+ import org.apache.brooklyn.api.location.MachineLocation;
+ import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+ import org.apache.brooklyn.api.location.NoMachinesAvailableException;
++import org.apache.brooklyn.core.config.Sanitizer;
+ import org.apache.brooklyn.util.core.flags.SetFromFlag;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ 
+ import com.google.common.collect.ImmutableMap;
+ 
+ public class SingleMachineProvisioningLocation<T extends MachineLocation> extends FixedListMachineProvisioningLocation<T> {
+ 
+     private static final Logger log = LoggerFactory.getLogger(SingleMachineProvisioningLocation.class);
+     
+     @SetFromFlag(nullable=false)
+     private String location;
+     
+     @SetFromFlag(nullable=false)
+     private Map<?,?> locationFlags;
+     
+     private T singleLocation;
+     private int referenceCount;
+     private MachineProvisioningLocation<T> provisioningLocation;
+ 
+ 
+     public SingleMachineProvisioningLocation() {
+     }
+ 
+     @SuppressWarnings("rawtypes")
+     public SingleMachineProvisioningLocation(String location, Map locationFlags) {
+         this.locationFlags = locationFlags;
+         this.location = location;
+     }
+ 
 -    @SuppressWarnings("rawtypes")
++    @SuppressWarnings({ "rawtypes", "unchecked" })
+     @Override
+     public synchronized T obtain(Map flags) throws NoMachinesAvailableException {
 -        log.info("Flags {} passed to newLocationFromString will be ignored, using {}", flags, locationFlags);
++        if (flags != null && !flags.isEmpty()) {
++            log.info("Flags {} passed to SingleMachineProvisioningLocation.obtain will be ignored, using {}", Sanitizer.sanitize(flags), Sanitizer.sanitize(locationFlags));
++        }
+         return obtain();
+     }
+ 
+     @Override
+     @SuppressWarnings({ "unchecked", "rawtypes" })
+     public synchronized T obtain() throws NoMachinesAvailableException {
+         if (singleLocation == null) {
+             if (provisioningLocation == null) {
+                 provisioningLocation = (MachineProvisioningLocation) getManagementContext().getLocationRegistry().resolve(
+                     location, locationFlags);
+             }
+             singleLocation = provisioningLocation.obtain(ImmutableMap.of());
+             inUse.add(singleLocation);
+         }
+         referenceCount++;
+         return singleLocation;
+     }
+ 
+     @Override
+     public synchronized void release(T machine) {
+         if (!machine.equals(singleLocation)) {
+             throw new IllegalArgumentException("Invalid machine " + machine + " passed to release, expecting: " + singleLocation);
+         }
+         if (--referenceCount == 0) {
+             provisioningLocation.release(machine);
+             singleLocation = null;
+         }
+         inUse.remove(machine);
+     };
+ 
+ }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
----------------------------------------------------------------------
diff --cc brooklyn-server/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
index 0000000,0ee5938..5e74e7c
mode 000000,100644..100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
@@@ -1,0 -1,1088 +1,1091 @@@
+ /*
+  * 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.location.ssh;
+ 
+ import static org.apache.brooklyn.util.groovy.GroovyJavaMethods.truth;
+ 
+ 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.io.StringReader;
+ import java.net.InetAddress;
+ import java.net.InetSocketAddress;
+ import java.net.Socket;
+ import java.security.KeyPair;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Set;
+ import java.util.concurrent.Callable;
+ import java.util.concurrent.TimeUnit;
+ 
+ import javax.annotation.Nonnull;
+ import javax.annotation.Nullable;
+ 
+ import org.apache.brooklyn.api.location.MachineDetails;
+ import org.apache.brooklyn.api.location.MachineLocation;
+ import org.apache.brooklyn.api.location.OsDetails;
+ import org.apache.brooklyn.api.location.PortRange;
+ import org.apache.brooklyn.api.location.PortSupplier;
+ import org.apache.brooklyn.api.mgmt.Task;
+ import org.apache.brooklyn.config.ConfigKey;
+ import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
+ import org.apache.brooklyn.core.BrooklynLogging;
+ import org.apache.brooklyn.core.config.BasicConfigKey;
+ import org.apache.brooklyn.core.config.ConfigKeys;
+ import org.apache.brooklyn.core.config.ConfigUtils;
+ import org.apache.brooklyn.core.config.MapConfigKey;
+ import org.apache.brooklyn.core.config.Sanitizer;
+ import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
+ import org.apache.brooklyn.core.location.AbstractLocation;
+ import org.apache.brooklyn.core.location.BasicHardwareDetails;
+ import org.apache.brooklyn.core.location.BasicMachineDetails;
+ import org.apache.brooklyn.core.location.BasicOsDetails;
++import org.apache.brooklyn.core.location.LocationConfigUtils;
++import org.apache.brooklyn.core.location.LocationConfigUtils.OsCredential;
+ import org.apache.brooklyn.core.location.PortRanges;
+ import org.apache.brooklyn.core.location.access.PortForwardManager;
+ import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+ import org.apache.brooklyn.util.collections.MutableMap;
+ import org.apache.brooklyn.util.core.ResourceUtils;
+ import org.apache.brooklyn.util.core.config.ConfigBag;
+ import org.apache.brooklyn.util.core.crypto.SecureKeys;
+ import org.apache.brooklyn.util.core.file.ArchiveUtils;
+ import org.apache.brooklyn.util.core.flags.SetFromFlag;
+ import org.apache.brooklyn.util.core.flags.TypeCoercions;
+ import org.apache.brooklyn.util.core.internal.ssh.ShellTool;
+ import org.apache.brooklyn.util.core.internal.ssh.SshException;
+ import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+ import org.apache.brooklyn.util.core.internal.ssh.sshj.SshjTool;
+ import org.apache.brooklyn.util.core.mutex.MutexSupport;
+ import org.apache.brooklyn.util.core.mutex.WithMutexes;
+ import org.apache.brooklyn.util.core.task.ScheduledTask;
+ import org.apache.brooklyn.util.core.task.Tasks;
+ import org.apache.brooklyn.util.core.task.system.internal.ExecWithLoggingHelpers;
+ import org.apache.brooklyn.util.core.task.system.internal.ExecWithLoggingHelpers.ExecRunner;
+ import org.apache.brooklyn.util.exceptions.Exceptions;
+ import org.apache.brooklyn.util.exceptions.RuntimeInterruptedException;
+ import org.apache.brooklyn.util.guava.KeyTransformingLoadingCache.KeyTransformingSameTypeLoadingCache;
 -import org.apache.brooklyn.util.net.Urls;
+ import org.apache.brooklyn.util.pool.BasicPool;
+ import org.apache.brooklyn.util.pool.Pool;
+ import org.apache.brooklyn.util.ssh.BashCommands;
+ import org.apache.brooklyn.util.stream.KnownSizeInputStream;
+ import org.apache.brooklyn.util.stream.ReaderInputStream;
+ import org.apache.brooklyn.util.stream.StreamGobbler;
+ import org.apache.brooklyn.util.text.Strings;
+ import org.apache.brooklyn.util.time.Duration;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ 
+ import com.google.common.annotations.Beta;
+ import com.google.common.base.Function;
+ import com.google.common.base.Objects;
++import com.google.common.base.Optional;
+ 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.cache.CacheBuilder;
+ import com.google.common.cache.CacheLoader;
+ import com.google.common.cache.LoadingCache;
+ import com.google.common.cache.RemovalListener;
+ import com.google.common.cache.RemovalNotification;
+ 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.Maps;
+ import com.google.common.collect.Sets;
+ import com.google.common.net.HostAndPort;
+ import com.google.common.reflect.TypeToken;
+ 
+ import groovy.lang.Closure;
+ 
+ /**
+  * Operations on a machine that is accessible via ssh.
+  * <p>
+  * We expose two ways of running scripts.
+  * The execCommands method passes lines to bash and is lightweight but fragile.
+  * The execScript method creates a script on the remote machine. It is 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 {
+ 
+     private static final Logger LOG = LoggerFactory.getLogger(SshMachineLocation.class);
+     private static final Logger logSsh = LoggerFactory.getLogger(BrooklynLogging.SSH_IO);
+     
+     // Use a sane timeout when doing a connectivity test
+     private static final int SSHABLE_CONNECT_TIMEOUT = (int)Duration.minutes(2).toMilliseconds();
+ 
+     // Note that WinRmTool's implementation class *must* use a different key name. Both may be used 
+     // within a location's configuration to indicate the implementation to use for WinRmTool and 
+     // for SshTool - that will require two different configuration values.
+     public static final ConfigKey<String> SSH_TOOL_CLASS = ConfigKeys.newConfigKeyWithPrefixRemoved(
+             BrooklynConfigKeys.BROOKLYN_SSH_CONFIG_KEY_PREFIX,
+             Preconditions.checkNotNull(BrooklynConfigKeys.SSH_TOOL_CLASS, "static final initializer classload ordering problem"));
+ 
+     /** 
+      * Prefix for config key:values to be passed to the ssh tool on construction. For example, 
+      * one could define the location below. When executing ssh commands, it would instantiate
+      * an instance of {@code com.acme.brooklyn.MyCustomSshTool}, calling its constructor with a
+      * {@code Map<String, Object>} that contained the configuration. In this case, the map would
+      * include: {@code address=1.2.3.4}; {@code user=myname}; and {@code myparam=myvalue}.
+      * 
+      * <pre>
+      * {@code
+      * brooklyn.location.named.myLocation = byon:(hosts=1.2.3.4,user=myname)
+      * brooklyn.location.named.myLocation.sshToolClass = com.acme.brooklyn.MyCustomSshTool
+      * brooklyn.location.named.myLocation.sshToolClass.myparam = myvalue
+      * }
+      * }
+      * </pre>
+      * <p>
+      */
+     public static final String SSH_TOOL_CLASS_PROPERTIES_PREFIX = SSH_TOOL_CLASS.getName()+".";
+ 
+     public static final ConfigKey<Duration> SSH_CACHE_EXPIRY_DURATION = ConfigKeys.newConfigKey(Duration.class,
+             "sshCacheExpiryDuration", "Expiry time for unused cached ssh connections", Duration.FIVE_MINUTES);
+ 
+     public static final ConfigKey<MachineDetails> MACHINE_DETAILS = ConfigKeys.newConfigKey(
+             MachineDetails.class,
+             "machineDetails");
+ 
+     public static final ConfigKey<Boolean> DETECT_MACHINE_DETAILS = ConfigKeys.newBooleanConfigKey("detectMachineDetails",
+             "Attempt to detect machine details automatically. Works with SSH-accessible Linux instances.", true);
+ 
+     public static final ConfigKey<Iterable<String>> PRIVATE_ADDRESSES = ConfigKeys.newConfigKey(
+             new TypeToken<Iterable<String>>() {},
+             "privateAddresses",
+             "Private addresses of this machine, e.g. those within the private network", 
+             null);
+ 
+     public static final ConfigKey<Map<Integer, String>> TCP_PORT_MAPPINGS = ConfigKeys.newConfigKey(
+             new TypeToken<Map<Integer, String>>() {},
+             "tcpPortMappings",
+             "NAT'ed ports, giving the mapping from private TCP port to a public host:port", 
+             null);
+ 
+     @SetFromFlag
+     protected String user;
+ 
+     @SetFromFlag(nullable = false)
+     protected InetAddress address;
+ 
+     // TODO should not allow this to be set from flag; it is not persisted so that will be lost
+     // (mainly used for localhost currently so not a big problem)
+     @Nullable  // lazily initialized; use getMutexSupport()
+     @SetFromFlag
+     private transient WithMutexes mutexSupport;
+ 
+     @SetFromFlag
+     private Set<Integer> usedPorts;
+ 
+     private volatile MachineDetails machineDetails;
+     private final Object machineDetailsLock = new Object();
+ 
+     public static final ConfigKey<String> SSH_HOST = BrooklynConfigKeys.SSH_CONFIG_HOST;
+     public static final ConfigKey<Integer> SSH_PORT = BrooklynConfigKeys.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");
+     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");
+ 
+     // 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");
+     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");
+ 
+     public static final ConfigKey<Boolean> ALLOCATE_PTY = SshTool.PROP_ALLOCATE_PTY;
+ 
+     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 = ConfigKeys.newBooleanConfigKey(
+             "noStdoutLogging", "whether to disable logging of stdout from SSH commands (e.g. for verbose commands)", false);
+     public static final ConfigKey<Boolean> NO_STDERR_LOGGING = ConfigKeys.newBooleanConfigKey(
+             "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<String> LOCAL_TEMP_DIR = SshTool.PROP_LOCAL_TEMP_DIR;
+ 
+     public static final ConfigKey<Boolean> CLOSE_CONNECTION = ConfigKeys.newBooleanConfigKey("close", "Close the SSH connection after use", false);
+     public static final ConfigKey<String> UNIQUE_ID = ConfigKeys.newStringConfigKey("unique", "Unique ID for the SSH connection");
+ 
+     /**
+      * Specifies config keys where a change in the value does not require a new SshTool instance,
+      * i.e. they can be specified per command on the tool
+      */
+     // TODO: Fully specify.
+     public static final Set<ConfigKey<?>> REUSABLE_SSH_PROPS = ImmutableSet.of(
+             STDOUT, STDERR, SCRIPT_DIR, CLOSE_CONNECTION,
+             SshTool.PROP_SCRIPT_HEADER, SshTool.PROP_PERMISSIONS, SshTool.PROP_LAST_MODIFICATION_DATE,
+             SshTool.PROP_LAST_ACCESS_DATE, SshTool.PROP_OWNER_UID, SshTool.PROP_SSH_RETRY_DELAY);
+ 
+     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();
+                 }
+             }));
+ 
+     /**
+      * The set of config keys on this location which become default values for properties when invoking an SSH
+      * operation.
+      */
+     @Beta
+     public static final Set<ConfigKey<?>> SSH_CONFIG_GIVEN_TO_PROPS = ImmutableSet.<ConfigKey<?>>of(
+             SCRIPT_DIR);
+ 
+     private Task<?> cleanupTask;
+     /** callers should use {@link #getSshPoolCache()} */
+     @Nullable 
+     private transient LoadingCache<Map<String, ?>, Pool<SshTool>> sshPoolCacheOrNull;
+ 
+     private transient volatile boolean loggedLegcySshToolClassConfig;
+     
+     public SshMachineLocation() {
+         this(MutableMap.of());
+     }
+ 
+     public SshMachineLocation(Map properties) {
+         super(properties);
+         usedPorts = (usedPorts != null) ? Sets.newLinkedHashSet(usedPorts) : Sets.<Integer>newLinkedHashSet();
+     }
+ 
+     @Override
+     public void init() {
+         super.init();
+ 
+         // Register any pre-existing port-mappings with the PortForwardManager
+         Map<Integer, String> tcpPortMappings = getConfig(TCP_PORT_MAPPINGS);
+         if (tcpPortMappings != null) {
+             PortForwardManager pfm = (PortForwardManager) getManagementContext().getLocationRegistry().resolve("portForwardManager(scope=global)");
+             for (Map.Entry<Integer, String> entry : tcpPortMappings.entrySet()) {
+                 int targetPort = entry.getKey();
+                 HostAndPort publicEndpoint = HostAndPort.fromString(entry.getValue());
+                 if (!publicEndpoint.hasPort()) {
+                     throw new IllegalArgumentException("Invalid portMapping ('"+entry.getValue()+"') for port "+targetPort+" in machine "+this);
+                 }
+                 pfm.associate(publicEndpoint.getHostText(), publicEndpoint, this, targetPort);
+             }
+         }
+     }
+     
+     private final transient Object poolCacheMutex = new Object();
+     @Nonnull
+     private LoadingCache<Map<String, ?>, Pool<SshTool>> getSshPoolCache() {
+         synchronized (poolCacheMutex) {
+             if (sshPoolCacheOrNull==null) {
+                 sshPoolCacheOrNull = buildSshToolPoolCacheLoader();
+                 addSshPoolCacheCleanupTask();
+             }
+         }
+         return sshPoolCacheOrNull;
+     }
+ 
+     private LoadingCache<Map<String, ?>, Pool<SshTool>> buildSshToolPoolCacheLoader() {
+         // TODO: Appropriate numbers for maximum size and expire after access
+         // At the moment every SshMachineLocation instance creates its own pool.
+         // It might make more sense to create one pool and inject it into all SshMachineLocations.
+         Duration expiryDuration = getConfig(SSH_CACHE_EXPIRY_DURATION);
+         
+         LoadingCache<Map<String, ?>, Pool<SshTool>> delegate = CacheBuilder.newBuilder()
+                 .maximumSize(10)
+                 .expireAfterAccess(expiryDuration.toMilliseconds(), TimeUnit.MILLISECONDS)
+                 .recordStats()
+                 .removalListener(new RemovalListener<Map<String, ?>, Pool<SshTool>>() {
+                     // TODO: Does it matter that this is synchronous? - Can closing pools cause long delays?
+                     @Override
+                     public void onRemoval(RemovalNotification<Map<String, ?>, Pool<SshTool>> notification) {
+                         Pool<SshTool> removed = notification.getValue();
+                         if (removed == null) {
+                             if (LOG.isDebugEnabled()) {
+                                 LOG.debug("Pool evicted from SshTool cache is null so we can't call pool.close(). " +
+                                         "It's probably already been garbage collected. Eviction cause: {} ",
+                                         notification.getCause().name());
+                             }
+                         } else {
+                             if (LOG.isDebugEnabled()) {
+                                 LOG.debug("{} evicted from SshTool cache. Eviction cause: {}",
+                                         removed, notification.getCause().name());
+                             }
+                             try {
+                                 removed.close();
+                             } catch (IOException e) {
+                                 if (LOG.isDebugEnabled()) {
+                                     LOG.debug("Exception closing "+removed, e);
+                                 }
+                             }
+                         }
+                     }
+                 })
+                 .build(new CacheLoader<Map<String, ?>, Pool<SshTool>>() {
+                     public Pool<SshTool> load(Map<String, ?> properties) {
+                         if (LOG.isDebugEnabled()) {
+                             LOG.debug("{} building ssh pool for {} with properties: {}",
+                                     new Object[] {this, getSshHostAndPort(), Sanitizer.sanitize(properties)});
+                         }
+                         return buildPool(properties);
+                     }
+                 });
+ 
+         final Set<String> reusableSshProperties = ImmutableSet.copyOf(
+                 Iterables.transform(REUSABLE_SSH_PROPS, new Function<ConfigKey<?>, String>() {
+                     @Override public String apply(ConfigKey<?> input) {
+                         return input.getName();
+                     }
+                 }));
+         // Groovy-eclipse compiler refused to compile `KeyTransformingSameTypeLoadingCache.from(...)`
+         return new KeyTransformingSameTypeLoadingCache<Map<String, ?>, Pool<SshTool>>(
+                 delegate,
+                 new Function<Map<String, ?>, Map<String, ?>>() {
+                     @Override
+                     public Map<String, ?> apply(@Nullable Map<String, ?> input) {
+                         Map<String, Object> copy = new HashMap<String, Object>(input);
+                         copy.keySet().removeAll(reusableSshProperties);
+                         return copy;
+                     }
+                 });
+     }
+ 
+     private BasicPool<SshTool> buildPool(final Map<String, ?> properties) {
+         return BasicPool.<SshTool>builder()
+                 .name(getDisplayName()+"@"+address+":"+getPort()+
+                         (config().getRaw(SSH_HOST).isPresent() ? "("+getConfig(SSH_HOST)+":"+getConfig(SSH_PORT)+")" : "")+
+                         ":hash"+System.identityHashCode(this))
+                 .supplier(new Supplier<SshTool>() {
+                         @Override public SshTool get() {
+                             return connectSsh(properties);
+                         }})
+                 .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) {
+                             if (LOG.isDebugEnabled()) {
+                                 LOG.debug("{} closing pool for {}", this, input);
+                             }
+                             try {
+                                 input.disconnect();
+                             } catch (Exception e) {
+                                 if (logSsh.isDebugEnabled()) logSsh.debug("On machine "+SshMachineLocation.this+", ssh-disconnect failed", e);
+                             }
+                             return null;
+                         }})
+                 .build();
+     }
+ 
+     @Override
+     public SshMachineLocation 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!
+ 
+         boolean deferConstructionChecks = (properties.containsKey("deferConstructionChecks") && TypeCoercions.coerce(properties.get("deferConstructionChecks"), Boolean.class));
+         if (!deferConstructionChecks) {
+             if (getDisplayName() == null) {
+                 setDisplayName((truth(user) ? user+"@" : "") + address.getHostName());
+             }
+         }
+         return this;
+     }
+     
+     private transient final Object mutexSupportCreationLock = new Object();
+     protected WithMutexes getMutexSupport() {
+         synchronized (mutexSupportCreationLock) {
+             // create on demand so that it is not null after serialization
+             if (mutexSupport == null) {
+                 mutexSupport = new MutexSupport();
+             }
+             return mutexSupport;
+         }
+     }
+     
+     protected void addSshPoolCacheCleanupTask() {
+         if (cleanupTask!=null && !cleanupTask.isDone()) {
+             return;
+         }
+         if (getManagementContext()==null || getManagementContext().getExecutionManager()==null) {
+             LOG.debug("No management context for "+this+"; ssh-pool cache will only be closed when machine is closed");
+             return;
+         }
+         
+         Callable<Task<?>> cleanupTaskFactory = new Callable<Task<?>>() {
+             @Override public Task<Void> call() {
+                 return Tasks.<Void>builder().dynamic(false).tag(BrooklynTaskTags.TRANSIENT_TASK_TAG)
+                     .displayName("ssh-location cache cleaner").body(new Callable<Void>() {
+                     @Override public Void call() {
+                         try {
+                             if (sshPoolCacheOrNull != null) sshPoolCacheOrNull.cleanUp();
+                             if (!SshMachineLocation.this.isManaged()) {
+                                 if (sshPoolCacheOrNull != null) sshPoolCacheOrNull.invalidateAll();
+                                 cleanupTask.cancel(false);
+                                 sshPoolCacheOrNull = null;
+                             }
+                             return null;
+                         } catch (Exception e) {
+                             // Don't rethrow: the behaviour of executionManager is different from a scheduledExecutorService,
+                             // if we throw an exception, then our task will never get executed again
+                             LOG.warn("Problem cleaning up ssh-pool-cache", e);
+                             return null;
+                         } catch (Throwable t) {
+                             LOG.warn("Problem cleaning up ssh-pool-cache (rethrowing)", t);
+                             throw Exceptions.propagate(t);
+                         }
+                     }}).build();
+             }
+         };
+         
+         Duration expiryDuration = getConfig(SSH_CACHE_EXPIRY_DURATION);
+         cleanupTask = getManagementContext().getExecutionManager().submit(new ScheduledTask(
+             MutableMap.of("displayName", "scheduled[ssh-location cache cleaner]"), cleanupTaskFactory).period(expiryDuration));
+     }
+     
+     // TODO close has been used for a long time to perform clean-up wanted on unmanagement, but that's not clear; 
+     // we should probably expose a mechanism such as that in Entity (or re-use Entity for locations!)
+     @Override
+     public void close() throws IOException {
+         if (sshPoolCacheOrNull != null) {
+             if (LOG.isDebugEnabled()) {
+                 LOG.debug("{} invalidating all entries in ssh pool cache. Final stats: {}", this, sshPoolCacheOrNull.stats());
+             }
+             sshPoolCacheOrNull.invalidateAll();
+         }
+         if (cleanupTask != null) {
+             cleanupTask.cancel(false);
+             cleanupTask = null;
+             sshPoolCacheOrNull = null;
+         }
+     }
+ 
+     // should not be necessary, and causes objects to be kept around a lot longer than desired
+ //    @Override
+ //    protected void finalize() throws Throwable {
+ //        try {
+ //            close();
+ //        } finally {
+ //            super.finalize();
+ //        }
+ //    }
+ 
+     @Override
+     public InetAddress getAddress() {
+         return address;
+     }
+ 
+     @Override
+     public String getHostname() {
+         String hostname = address.getHostName();
+         return (hostname == null || hostname.equals(address.getHostAddress())) ? null : hostname;
+     }
+     
+     @Override
+     public Set<String> getPublicAddresses() {
+         return ImmutableSet.of(address.getHostAddress());
+     }
+     
+     @Override
+     public Set<String> getPrivateAddresses() {
+         Iterable<String> result = getConfig(PRIVATE_ADDRESSES);
+         return (result == null) ? ImmutableSet.<String>of() : ImmutableSet.copyOf(result);
+     }
+ 
+     public HostAndPort getSshHostAndPort() {
+         String host = getConfig(SSH_HOST);
+         if (host == null || Strings.isEmpty(host))
+             host = address.getHostName();
+         Integer port = getPort();
+         return HostAndPort.fromParts(host, port);
+     }
+ 
+     public String getUser() {
+         if (!truth(user)) {
+             if (config().getLocalRaw(SshTool.PROP_USER).isPresent()) {
+                 LOG.warn("User configuration for "+this+" set after deployment; deprecated behaviour may not be supported in future versions");
+             }
+             return getConfig(SshTool.PROP_USER);
+         }
+         return user;
+     }
+ 
+     /** port for SSHing */
+     public int getPort() {
+         return getConfig(SshTool.PROP_PORT);
+     }
+ 
+     protected <T> T execSsh(final Map<String, ?> props, final Function<ShellTool, T> task) {
+         final LoadingCache<Map<String, ?>, Pool<SshTool>> sshPoolCache = getSshPoolCache();
+         Pool<SshTool> pool = sshPoolCache.getUnchecked(props);
+         if (LOG.isTraceEnabled()) {
+             LOG.trace("{} execSsh got pool: {}", this, pool);
+         }
+ 
+         if (truth(props.get(CLOSE_CONNECTION.getName()))) {
+             Function<SshTool, T> close = new Function<SshTool, T>() {
+                 @Override
+                 public T apply(SshTool input) {
+                     T result = task.apply(input);
+                     if (LOG.isDebugEnabled()) {
+                         LOG.debug("{} invalidating all sshPoolCache entries: {}", SshMachineLocation.this, sshPoolCache.stats().toString());
+                     }
+                     sshPoolCache.invalidateAll();
+                     sshPoolCache.cleanUp();
+                     return result;
+                 }
+             };
+             return pool.exec(close);
+         } else {
+             return pool.exec(task);
+         }
+     }
+ 
+     protected SshTool connectSsh() {
+         return connectSsh(ImmutableMap.of());
+     }
+ 
+     protected boolean previouslyConnected = false;
+     protected SshTool connectSsh(Map props) {
+         try {
+             if (!truth(user)) {
+                 String newUser = getUser();
+                 if (LOG.isTraceEnabled()) LOG.trace("For "+this+", setting user in connectSsh: oldUser="+user+"; newUser="+newUser);
+                 user = newUser;
+             }
+ 
+             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());
+ 
+             for (Map.Entry<String,Object> entry: config().getBag().getAllConfig().entrySet()) {
+                 boolean include = false;
+                 String key = entry.getKey();
+                 if (key.startsWith(SshTool.BROOKLYN_CONFIG_KEY_PREFIX)) {
+                     key = Strings.removeFromStart(key, SshTool.BROOKLYN_CONFIG_KEY_PREFIX);
+                     include = true;
+                 }
+                 
+                 if (key.startsWith(SSH_TOOL_CLASS_PROPERTIES_PREFIX)) {
+                     key = Strings.removeFromStart(key, SSH_TOOL_CLASS_PROPERTIES_PREFIX);
+                     include = true;
+                 }
+                 
+                 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_CONFIG_KEY_PREFIX)
+                     include = true;
+                 }
+                 
+                 if (include) {
+                     args.putStringKey(key, entry.getValue());
+                 }
+             }
+ 
+             // Explicit props trump all.
+             args.putAll(props);
+ 
+             if (LOG.isTraceEnabled()) LOG.trace("creating ssh session for "+Sanitizer.sanitize(args));
+             if (!user.equals(args.get(SshTool.PROP_USER))) {
+                 LOG.warn("User mismatch configuring ssh for "+this+": preferring user "+args.get(SshTool.PROP_USER)+" over "+user);
+                 user = args.get(SshTool.PROP_USER);
+             }
+ 
+             // look up tool class
+             String sshToolClass = args.get(SSH_TOOL_CLASS);
+             String legacySshToolClass = args.get(SshTool.PROP_TOOL_CLASS);
+             if (Strings.isNonBlank(legacySshToolClass)) {
+                 String msg;
+                 if (Strings.isNonBlank(sshToolClass)) {
+                     msg = "Ignoring deprecated config "+SshTool.PROP_TOOL_CLASS.getName()+"="+legacySshToolClass
+                             +", preferring "+SSH_TOOL_CLASS.getName()+"="+sshToolClass+" for "+SshMachineLocation.this;
+                     
+                 } else {
+                     sshToolClass = legacySshToolClass;
+                     msg = "Using deprecated config "+SshTool.PROP_TOOL_CLASS.getName()+"="+legacySshToolClass
+                             +", preferring "+SSH_TOOL_CLASS.getName()+"="+sshToolClass+" for "+SshMachineLocation.this;
+                 }
+                 if (!loggedLegcySshToolClassConfig) {
+                     LOG.warn(msg);
+                     loggedLegcySshToolClassConfig = true;
+                 }
+             }
+             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);
+         }
+     }
+ 
+     // 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 {@literal ;} 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 newExecWithLoggingHelpers().execCommands(augmentPropertiesWithSshConfigGivenToProps(props), summaryForLogging, commands, 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 newExecWithLoggingHelpers().execScript(augmentPropertiesWithSshConfigGivenToProps(props), summaryForLogging, commands, env);
+     }
+ 
+     private Map<String, Object> augmentPropertiesWithSshConfigGivenToProps(Map<String, ?> props) {
+         Map<String,Object> augmentedProps = Maps.newHashMap(props);
+         for (ConfigKey<?> config : SSH_CONFIG_GIVEN_TO_PROPS) {
+             if (!augmentedProps.containsKey(config.getName()) && hasConfig(config, true))
+                 augmentedProps.put(config.getName(), getConfig(config));
+         }
+         return augmentedProps;
+     }
+ 
+     protected ExecWithLoggingHelpers newExecWithLoggingHelpers() {
+         return new ExecWithLoggingHelpers("SSH") {
+             @Override
+             protected <T> T execWithTool(MutableMap<String, Object> props, Function<ShellTool, T> function) {
+                 return execSsh(props, function);
+             }
+             @Override
+             protected void preExecChecks() {
+                 Preconditions.checkNotNull(address, "host address must be specified for ssh");
+             }
+             @Override
+             protected String constructDefaultLoggingPrefix(ConfigBag execFlags) {
+                 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));
+                 return (user != null ? user+"@" : "") + hostname + (port != null ? ":"+port : "");
+             }
+             @Override
+             protected String getTargetName() {
+                 return ""+SshMachineLocation.this;
+             }
+         }.logger(logSsh);
+     }
+ 
+     /**
+      * @deprecated since 0.7.0; use {@link #execCommands(Map, String, List, Map), and rely on that calling the execWithLogging
+      */
+     @SuppressWarnings({"rawtypes", "unchecked"})
+     @Deprecated
+     protected int execWithLogging(Map<String,?> props, String summaryForLogging, List<String> commands, Map env, final Closure<Integer> execCommand) {
+         return newExecWithLoggingHelpers().execWithLogging(props, summaryForLogging, commands, env, new ExecRunner() {
+                 @Override public int exec(ShellTool ssh, Map<String, ?> flags, List<String> cmds, Map<String, ?> env) {
+                     return execCommand.call(ssh, flags, cmds, env);
+                 }});
+     }
+ 
+     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<ShellTool,Integer>() {
+                 public Integer apply(ShellTool ssh) {
+                     return ((SshTool) ssh).copyToServer(props, new KnownSizeInputStream(src, filesize), destination);
+                 }});
+         }
+     }
+     // 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<ShellTool,Integer>() {
+             public Integer apply(ShellTool ssh) {
+                 return ((SshTool)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<ShellTool,Integer>() {
+             public Integer apply(ShellTool ssh) {
+                 return ((SshTool)ssh).copyFromServer(props, remote, new File(local));
+             }});
+     }
+ 
+     public int installTo(String url, String destPath) {
+         return installTo(MutableMap.<String, Object>of(), url, destPath);
+     }
+ 
+     public int installTo(Map<String,?> props, String url, String destPath) {
+         return installTo(ResourceUtils.create(this), props, url, destPath);
+     }
+ 
+     public int installTo(ResourceUtils loader, String url, String destPath) {
+         return installTo(loader, MutableMap.<String, Object>of(), url, destPath);
+     }
+ 
+     /**
+      * Installs the given URL at the indicated destination path.
+      * <p>
+      * Attempts to curl the source URL on the remote machine,
+      * then if that fails, loads locally (from classpath or file) and transfers.
+      * <p>
+      * Use {@link ArchiveUtils} to handle directories and their contents properly.
+      *
+      * TODO allow s3://bucket/file URIs for AWS S3 resources
+      * TODO use PAX-URL style URIs for maven artifacts
+      * TODO use subtasks here for greater visibility?; deprecate in favour of SshTasks.installFromUrl?
+      *
+      * @param utils A {@link ResourceUtils} that can resolve the source URLs
+      * @param url The source URL to be installed
+      * @param destPath The file to be created on the destination
+      *
+      * @see ArchiveUtils#deploy(String, SshMachineLocation, String)
+      * @see ArchiveUtils#deploy(String, SshMachineLocation, String, String)
+      * @see ResourceUtils#getResourceFromUrl(String)
+      */
+     public int installTo(ResourceUtils utils, Map<String,?> props, String url, String destPath) {
+         LOG.debug("installing {} to {} on {}, attempting remote curl", new Object[] { url, destPath, this });
+ 
+         try {
+             PipedInputStream insO = new PipedInputStream(); OutputStream outO = new PipedOutputStream(insO);
+             PipedInputStream insE = new PipedInputStream(); OutputStream outE = new PipedOutputStream(insE);
+             StreamGobbler sgsO = new StreamGobbler(insO, null, LOG); sgsO.setLogPrefix("[curl @ "+address+":stdout] ").start();
+             StreamGobbler sgsE = new StreamGobbler(insE, null, LOG); sgsE.setLogPrefix("[curl @ "+address+":stdout] ").start();
+             Map<String, ?> sshProps = MutableMap.<String, Object>builder().putAll(props).put("out", outO).put("err", outE).build();
+             int result = execScript(sshProps, "copying remote resource "+url+" to server",  ImmutableList.of(
+                     BashCommands.INSTALL_CURL, // TODO should hold the 'installing' mutex
+                     "mkdir -p `dirname '"+destPath+"'`",
+                     "curl "+url+" -L --silent --insecure --show-error --fail --connect-timeout 60 --max-time 600 --retry 5 -o '"+destPath+"'"));
+             sgsO.close();
+             sgsE.close();
+             if (result != 0) {
+                 LOG.debug("installing {} to {} on {}, curl failed, attempting local fetch and copy", new Object[] { url, destPath, this });
+                 try {
+                     Tasks.setBlockingDetails("retrieving resource "+url+" for copying across");
+                     InputStream stream = utils.getResourceFromUrl(url);
+                     Tasks.setBlockingDetails("copying resource "+url+" to server");
+                     result = copyTo(props, stream, destPath);
+                 } finally {
+                     Tasks.setBlockingDetails(null);
+                 }
+             }
+             if (result == 0) {
+                 LOG.debug("installing {} complete; {} on {}", new Object[] { url, destPath, this });
+             } else {
+                 LOG.warn("installing {} failed; {} on {}: {}", new Object[] { url, destPath, this, result });
+             }
+             return result;
+         } catch (IOException e) {
+             throw Throwables.propagate(e);
+         }
+     }
+ 
+     @Override
+     public String toString() {
+         return "SshMachineLocation["+getDisplayName()+":"+user+"@"+address+":"+getPort()+"(id="+getId()+")]";
+     }
+ 
+     @Override
+     public String toVerboseString() {
+         return Objects.toStringHelper(this).omitNullValues()
+                 .add("id", getId()).add("name", getDisplayName())
+                 .add("user", getUser()).add("address", getAddress()).add("port", getPort())
+                 .add("parentLocation", getParent())
+                 .toString();
+     }
+ 
+     /**
+      * @see #obtainPort(PortRange)
+      * @see PortRanges#ANY_HIGH_PORT
+      */
+     @Override
+     public boolean obtainSpecificPort(int portNumber) {
+         synchronized (usedPorts) {
+             // 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;
+             }
+         }
+     }
+ 
+     @Override
+     public int obtainPort(PortRange range) {
+         synchronized (usedPorts) {
+             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;
+         }
+     }
+ 
+     @Override
+     public void releasePort(int portNumber) {
+         synchronized (usedPorts) {
+             usedPorts.remove((Object) portNumber);
+         }
+     }
+ 
+     public boolean isSshable() {
+         String cmd = "date";
+         try {
+             try {
+                 Socket s = new Socket();
+                 s.connect(new InetSocketAddress(getAddress(), getPort()), SSHABLE_CONNECT_TIMEOUT);
+                 s.close();
+             } catch (IOException e) {
+                 if (LOG.isDebugEnabled()) LOG.debug(""+this+" not [yet] reachable (socket "+getAddress()+":"+getPort()+"): "+e);
+                 return false;
+             }
+             // this should do execCommands because sftp subsystem might not be available (or sometimes seems to take a while for it to become so?)
+             int result = execCommands(MutableMap.<String,Object>of(), "isSshable", ImmutableList.of(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() {
+         return getMachineDetails().getOsDetails();
+     }
+ 
++    /**
++     * Returns the machine details only if they are already loaded, or available directly as 
++     * config.
++     */
++    protected Optional<MachineDetails> getOptionalMachineDetails() {
++        MachineDetails result = machineDetails != null ? machineDetails : config().get(MACHINE_DETAILS);
++        return Optional.fromNullable(result);
++    }
++    
+     @Override
+     public MachineDetails getMachineDetails() {
+         synchronized (machineDetailsLock) {
+             if (machineDetails == null) {
+                 machineDetails = getConfig(MACHINE_DETAILS);
+             }
+             if (machineDetails == null) {
+                 machineDetails = inferMachineDetails();
+             }
+         }
+         return machineDetails;
+     }
+ 
+     protected MachineDetails inferMachineDetails() {
+         boolean detectionEnabled = getConfig(DETECT_MACHINE_DETAILS);
+         if (!detectionEnabled)
+             return new BasicMachineDetails(new BasicHardwareDetails(-1, -1), new BasicOsDetails("UNKNOWN", "UNKNOWN", "UNKNOWN"));
+ 
+         Tasks.setBlockingDetails("Waiting for machine details");
+         try {
+             return BasicMachineDetails.forSshMachineLocationLive(this);
+         } finally {
+             Tasks.resetBlockingDetails();
+         }
+     }
+ 
+     @Override
+     public void acquireMutex(String mutexId, String description) throws RuntimeInterruptedException {
+         try {
+             getMutexSupport().acquireMutex(mutexId, description);
+         } catch (InterruptedException ie) {
+             throw new RuntimeInterruptedException("Interrupted waiting for mutex: " + mutexId, ie);
+         }
+     }
+ 
+     @Override
+     public boolean tryAcquireMutex(String mutexId, String description) {
+         return getMutexSupport().tryAcquireMutex(mutexId, description);
+     }
+ 
+     @Override
+     public void releaseMutex(String mutexId) {
+         getMutexSupport().releaseMutex(mutexId);
+     }
+ 
+     @Override
+     public boolean hasMutex(String mutexId) {
+         return getMutexSupport().hasMutex(mutexId);
+     }
+ 
+     //We want the SshMachineLocation to be serializable and therefore 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();
+         getSshPoolCache();
+     }
+ 
+     /** returns the un-passphrased key-pair info if a key is being used, or else null */
+     public KeyPair findKeyPair() {
 -        String fn = getConfig(SshTool.PROP_PRIVATE_KEY_FILE);
 -        ResourceUtils r = ResourceUtils.create(this);
 -        if (fn!=null) return SecureKeys.readPem(r.getResourceFromUrl(fn), getConfig(SshTool.PROP_PRIVATE_KEY_PASSPHRASE));
 -        String data = getConfig(SshTool.PROP_PRIVATE_KEY_DATA);
 -        if (data!=null) return SecureKeys.readPem(new ReaderInputStream(new StringReader(data)), getConfig(SshTool.PROP_PRIVATE_KEY_PASSPHRASE));
 -        if (findPassword()!=null)
 -            // if above not specified, and password is, use password
++        OsCredential creds = LocationConfigUtils.getOsCredential(config().getBag());
++        if (creds.hasKey()) {
++            String data = creds.getPrivateKeyData();
++            return SecureKeys.readPem(new ReaderInputStream(new StringReader(data)), getConfig(SshTool.PROP_PRIVATE_KEY_PASSPHRASE));
++        } else {
+             return null;
 -        // fall back to id_rsa and id_dsa
 -        if (new File( Urls.mergePaths(System.getProperty("user.home"), ".ssh/id_rsa") ).exists() )
 -            return SecureKeys.readPem(r.getResourceFromUrl("~/.ssh/id_rsa"), getConfig(SshTool.PROP_PRIVATE_KEY_PASSPHRASE));
 -        if (new File( Urls.mergePaths(System.getProperty("user.home"), ".ssh/id_dsa") ).exists() )
 -            return SecureKeys.readPem(r.getResourceFromUrl("~/.ssh/id_dsa"), getConfig(SshTool.PROP_PRIVATE_KEY_PASSPHRASE));
 -        LOG.warn("Unable to extract any key or passphrase data in request to findKeyPair for "+this);
 -        return null;
++        }
+     }
+ 
+     /** returns the password being used to log in, if a password is being used, or else null */
+     public String findPassword() {
+         return getConfig(SshTool.PROP_PASSWORD);
+     }
+ 
+ }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/flags/ClassCoercionException.java
----------------------------------------------------------------------
diff --cc brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/flags/ClassCoercionException.java
index 0000000,6257c9e..cea7484
mode 000000,100644..100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/flags/ClassCoercionException.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/flags/ClassCoercionException.java
@@@ -1,0 -1,39 +1,41 @@@
+ /*
+  * 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.core.flags;
+ 
+ /**
+  * Thrown to indicate that {@link TypeCoercions} could not cast an object from one
+  * class to another.
+  */
+ public class ClassCoercionException extends ClassCastException {
++    private static final long serialVersionUID = -4616045237993172497L;
++
+     public ClassCoercionException() {
+         super();
+     }
+ 
+     /**
+      * Constructs a <code>ClassCoercionException</code> with the specified
+      * detail message.
+      *
+      * @param s the detail message.
+      */
+     public ClassCoercionException(String s) {
+         super(s);
+     }
+ }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/flags/MethodCoercions.java
----------------------------------------------------------------------
diff --cc brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/flags/MethodCoercions.java
index 0000000,2af5f56..a243581
mode 000000,100644..100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/flags/MethodCoercions.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/util/core/flags/MethodCoercions.java
@@@ -1,0 -1,185 +1,185 @@@
+ /*
+  * 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.core.flags;
+ 
+ import com.google.common.base.Optional;
+ import com.google.common.base.Predicate;
+ import com.google.common.collect.Iterables;
+ import com.google.common.reflect.TypeToken;
+ 
+ import javax.annotation.Nullable;
+ 
+ import org.apache.brooklyn.util.exceptions.Exceptions;
+ import org.apache.brooklyn.util.guava.Maybe;
+ 
+ import java.lang.reflect.InvocationTargetException;
+ import java.lang.reflect.Method;
+ import java.lang.reflect.Type;
+ import java.util.Arrays;
+ import java.util.List;
+ 
+ import static com.google.common.base.Preconditions.checkNotNull;
+ 
+ /**
+  * A way of binding a loosely-specified method call into a strongly-typed Java method call.
+  */
+ public class MethodCoercions {
+ 
+     /**
+      * Returns a predicate that matches a method with the given name, and a single parameter that
+      * {@link org.apache.brooklyn.util.core.flags.TypeCoercions#tryCoerce(Object, com.google.common.reflect.TypeToken)} can process
+      * from the given argument.
+      *
+      * @param methodName name of the method
+      * @param argument argument that is intended to be given
+      * @return a predicate that will match a compatible method
+      */
+     public static Predicate<Method> matchSingleParameterMethod(final String methodName, final Object argument) {
+         checkNotNull(methodName, "methodName");
+         checkNotNull(argument, "argument");
+ 
+         return new Predicate<Method>() {
+             @Override
+             public boolean apply(@Nullable Method input) {
+                 if (input == null) return false;
+                 if (!input.getName().equals(methodName)) return false;
+                 Type[] parameterTypes = input.getGenericParameterTypes();
+                 return parameterTypes.length == 1
+                         && TypeCoercions.tryCoerce(argument, TypeToken.of(parameterTypes[0])).isPresentAndNonNull();
+ 
+             }
+         };
+     }
+ 
+     /**
+      * Tries to find a single-parameter method with a parameter compatible with (can be coerced to) the argument, and
+      * invokes it.
+      *
+      * @param instance the object to invoke the method on
+      * @param methodName the name of the method to invoke
+      * @param argument the argument to the method's parameter.
+      * @return the result of the method call, or {@link org.apache.brooklyn.util.guava.Maybe#absent()} if method could not be matched.
+      */
+     public static Maybe<?> tryFindAndInvokeSingleParameterMethod(final Object instance, final String methodName, final Object argument) {
+         Class<?> clazz = instance.getClass();
+         Iterable<Method> methods = Arrays.asList(clazz.getMethods());
+         Optional<Method> matchingMethod = Iterables.tryFind(methods, matchSingleParameterMethod(methodName, argument));
+         if (matchingMethod.isPresent()) {
+             Method method = matchingMethod.get();
+             try {
+                 Type paramType = method.getGenericParameterTypes()[0];
+                 Object coercedArgument = TypeCoercions.coerce(argument, TypeToken.of(paramType));
+                 return Maybe.of(method.invoke(instance, coercedArgument));
+             } catch (IllegalAccessException | InvocationTargetException e) {
+                 throw Exceptions.propagate(e);
+             }
+         } else {
+             return Maybe.absent();
+         }
+     }
+ 
+     /**
+      * Returns a predicate that matches a method with the given name, and parameters that
+      * {@link org.apache.brooklyn.util.core.flags.TypeCoercions#tryCoerce(Object, com.google.common.reflect.TypeToken)} can process
+      * from the given list of arguments.
+      *
+      * @param methodName name of the method
+      * @param arguments arguments that is intended to be given
+      * @return a predicate that will match a compatible method
+      */
+     public static Predicate<Method> matchMultiParameterMethod(final String methodName, final List<?> arguments) {
+         checkNotNull(methodName, "methodName");
+         checkNotNull(arguments, "arguments");
+ 
+         return new Predicate<Method>() {
+             @Override
+             public boolean apply(@Nullable Method input) {
+                 if (input == null) return false;
+                 if (!input.getName().equals(methodName)) return false;
+                 int numOptionParams = arguments.size();
+                 Type[] parameterTypes = input.getGenericParameterTypes();
+                 if (parameterTypes.length != numOptionParams) return false;
+ 
+                 for (int paramCount = 0; paramCount < numOptionParams; paramCount++) {
 -                    if (!TypeCoercions.tryCoerce(((List) arguments).get(paramCount),
++                    if (!TypeCoercions.tryCoerce(((List<?>) arguments).get(paramCount),
+                             TypeToken.of(parameterTypes[paramCount])).isPresentAndNonNull()) return false;
+                 }
+                 return true;
+             }
+         };
+     }
+ 
+     /**
+      * Tries to find a multiple-parameter method with each parameter compatible with (can be coerced to) the
+      * corresponding argument, and invokes it.
+      *
+      * @param instance the object to invoke the method on
+      * @param methodName the name of the method to invoke
+      * @param argument a list of the arguments to the method's parameters.
+      * @return the result of the method call, or {@link org.apache.brooklyn.util.guava.Maybe#absent()} if method could not be matched.
+      */
+     public static Maybe<?> tryFindAndInvokeMultiParameterMethod(final Object instance, final String methodName, final List<?> arguments) {
+         Class<?> clazz = instance.getClass();
+         Iterable<Method> methods = Arrays.asList(clazz.getMethods());
+         Optional<Method> matchingMethod = Iterables.tryFind(methods, matchMultiParameterMethod(methodName, arguments));
+         if (matchingMethod.isPresent()) {
+             Method method = matchingMethod.get();
+             try {
 -                int numOptionParams = ((List)arguments).size();
++                int numOptionParams = ((List<?>)arguments).size();
+                 Object[] coercedArguments = new Object[numOptionParams];
+                 for (int paramCount = 0; paramCount < numOptionParams; paramCount++) {
+                     Object argument = arguments.get(paramCount);
+                     Type paramType = method.getGenericParameterTypes()[paramCount];
+                     coercedArguments[paramCount] = TypeCoercions.coerce(argument, TypeToken.of(paramType));
+                 }
+                 return Maybe.of(method.invoke(instance, coercedArguments));
+             } catch (IllegalAccessException | InvocationTargetException e) {
+                 throw Exceptions.propagate(e);
+             }
+         } else {
+             return Maybe.absent();
+         }
+     }
+ 
+     /**
+      * Tries to find a method with each parameter compatible with (can be coerced to) the corresponding argument, and invokes it.
+      *
+      * @param instance the object to invoke the method on
+      * @param methodName the name of the method to invoke
+      * @param argument a list of the arguments to the method's parameters, or a single argument for a single-parameter method.
+      * @return the result of the method call, or {@link org.apache.brooklyn.util.guava.Maybe#absent()} if method could not be matched.
+      */
+     public static Maybe<?> tryFindAndInvokeBestMatchingMethod(final Object instance, final String methodName, final Object argument) {
+         if (argument instanceof List) {
+             List<?> arguments = (List<?>) argument;
+ 
+             // ambiguous case: we can't tell if the user is using the multi-parameter syntax, or the single-parameter
+             // syntax for a method which takes a List parameter. So we try one, then fall back to the other.
+ 
+             Maybe<?> maybe = tryFindAndInvokeMultiParameterMethod(instance, methodName, arguments);
+             if (maybe.isAbsent())
+                 maybe = tryFindAndInvokeSingleParameterMethod(instance, methodName, argument);
+ 
+             return maybe;
+         } else {
+             return tryFindAndInvokeSingleParameterMethod(instance, methodName, argument);
+         }
+     }
+ 
+ }


Mime
View raw message