http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/LocationPredicates.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/LocationPredicates.java 
b/core/src/main/java/org/apache/brooklyn/core/location/LocationPredicates.java
new file mode 100644
index 0000000..fc336ec
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/location/LocationPredicates.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+
+public class LocationPredicates {
+
+    public static <T> Predicate<Location> idEqualTo(final T val) {
+        return new Predicate<Location>() {
+            @Override
+            public boolean apply(@Nullable Location input) {
+                return (input != null) && Objects.equal(input.getId(), val);
+            }
+        };
+    }
+    
+    public static <T> Predicate<Location> displayNameEqualTo(final T val) {
+        return new Predicate<Location>() {
+            @Override
+            public boolean apply(@Nullable Location input) {
+                return (input != null) && 
Objects.equal(input.getDisplayName(), val);
+            }
+        };
+    }
+    
+    public static <T> Predicate<Location> configEqualTo(final ConfigKey<T> 
configKey, final T val) {
+        return new Predicate<Location>() {
+            @Override
+            public boolean apply(@Nullable Location input) {
+                return (input != null) && 
Objects.equal(input.getConfig(configKey), val);
+            }
+        };
+    }
+
+    public static <T> Predicate<Location> configEqualTo(final HasConfigKey<T> 
configKey, final T val) {
+        return new Predicate<Location>() {
+            @Override
+            public boolean apply(@Nullable Location input) {
+                return (input != null) && 
Objects.equal(input.getConfig(configKey), val);
+            }
+        };
+    }
+
+    /**
+     * Returns a predicate that determines if a given location is a direct 
child of this {@code parent}.
+     */
+    public static <T> Predicate<Location> isChildOf(final Location parent) {
+        return new Predicate<Location>() {
+            @Override
+            public boolean apply(@Nullable Location input) {
+                return (input != null) && Objects.equal(input.getParent(), 
parent);
+            }
+        };
+    }
+
+    /**
+     * Returns a predicate that determines if a given location is a descendant 
of this {@code ancestor}.
+     */
+    public static <T> Predicate<Location> isDescendantOf(final Location 
ancestor) {
+        return new Predicate<Location>() {
+            @Override
+            public boolean apply(@Nullable Location input) {
+                // assumes impossible to have cycles in location-hierarchy
+                Location contenderAncestor = (input == null) ? input : 
input.getParent();
+                while (contenderAncestor != null) {
+                    if (Objects.equal(contenderAncestor, ancestor)) {
+                        return true;
+                    }
+                    contenderAncestor = contenderAncestor.getParent();
+                }
+                return false;
+            }
+        };
+    }
+
+    public static <T> Predicate<Location> managed() {
+        return new Predicate<Location>() {
+            @Override
+            public boolean apply(@Nullable Location input) {
+                return (input != null) && Locations.isManaged(input);
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/LocationPropertiesFromBrooklynProperties.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/LocationPropertiesFromBrooklynProperties.java
 
b/core/src/main/java/org/apache/brooklyn/core/location/LocationPropertiesFromBrooklynProperties.java
new file mode 100644
index 0000000..c6ada78
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/location/LocationPropertiesFromBrooklynProperties.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.io.File;
+import java.util.Map;
+
+import org.apache.brooklyn.core.config.ConfigUtils;
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.core.server.BrooklynServerConfig;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+import org.apache.brooklyn.util.os.Os;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+
+/**
+ * The properties to use for locations, loaded from brooklyn.properties file.
+ * 
+ * @author aledsage
+ **/
+public class LocationPropertiesFromBrooklynProperties {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(LocationPropertiesFromBrooklynProperties.class);
+
+    @SuppressWarnings("deprecation")
+    protected static final Map<String, String> DEPRECATED_KEYS_MAPPING = new 
DeprecatedKeysMappingBuilder(LOG)
+            .camelToHyphen(LocationConfigKeys.DISPLAY_NAME)
+            .camelToHyphen(LocationConfigKeys.PRIVATE_KEY_FILE)
+            .camelToHyphen(LocationConfigKeys.PRIVATE_KEY_DATA)
+            .camelToHyphen(LocationConfigKeys.PRIVATE_KEY_PASSPHRASE)
+            .camelToHyphen(LocationConfigKeys.PUBLIC_KEY_FILE)
+            .camelToHyphen(LocationConfigKeys.PUBLIC_KEY_DATA)
+            .camelToHyphen(LocationConfigKeys.CALLER_CONTEXT)
+            .build();
+    
+    /**
+     * Finds the properties that apply to location, stripping off the prefixes.
+     * 
+     * Order of preference (in ascending order) is:
+     * <ol>
+     * <li>brooklyn.location.*
+     * <li>brooklyn.location.provider.*
+     * <li>brooklyn.location.named.namedlocation.*
+     * </ol>
+     * <p>
+     * Converts deprecated hyphenated properties to the non-deprecated 
camelCase format. 
+     */
+    public Map<String, Object> getLocationProperties(String provider, String 
namedLocation, Map<String, ?> properties) {
+        ConfigBag result = ConfigBag.newInstance();
+        
+        if (!Strings.isNullOrEmpty(provider)) 
+            result.put(LocationConfigKeys.CLOUD_PROVIDER, provider);
+        // named properties are preferred over providerOrApi properties
+        
result.putAll(transformDeprecated(getGenericLocationSingleWordProperties(properties)));
+        if (!Strings.isNullOrEmpty(provider)) 
result.putAll(transformDeprecated(getScopedLocationProperties(provider, 
properties)));
+        if (!Strings.isNullOrEmpty(namedLocation)) 
result.putAll(transformDeprecated(getNamedLocationProperties(namedLocation, 
properties)));
+        
+        setLocalTempDir(properties, result);
+        
+        return result.getAllConfigRaw();
+    }
+
+    /** allow the temp dir where ssh temporary files on the brooklyn server 
side are placed */
+    public static void setLocalTempDir(Map<String,?> source, ConfigBag target) 
{
+        // TODO better would be to use BrooklynServerConfig, requiring 
management passed in
+        String brooklynDataDir = (String) 
source.get(BrooklynServerConfig.getMgmtBaseDir(source));
+        if (brooklynDataDir != null && brooklynDataDir.length() > 0) {
+            String tempDir = Os.mergePaths(brooklynDataDir, "tmp", "ssh");
+            target.putIfAbsentAndNotNull(SshTool.PROP_LOCAL_TEMP_DIR, tempDir);
+            Os.deleteOnExitEmptyParentsUpTo(new File(tempDir), new 
File(brooklynDataDir));
+        }
+    }
+    
+    /**
+     * Gets the named provider (e.g. if using a property like {@code 
brooklyn.location.named.myfavourite=localhost}, then
+     * {@code getNamedProvider("myfavourite", properties)} will return {@code 
"localhost"}).
+     */
+    protected String getNamedProvider(String namedLocation, Map<String, ?> 
properties) {
+        String key = String.format("brooklyn.location.named.%s", 
namedLocation);
+        return (String) properties.get(key);
+    }
+    
+    /**
+     * Returns those properties in the form "brooklyn.location.xyz", where 
"xyz" is any
+     * key that does not contain dots. We do this special (sub-optimal!) 
filtering
+     * because we want to exclude brooklyn.location.named.*, 
brooklyn.location.jclouds.*, etc.
+     * We only want those properties that are to be generic for all locations.
+     * 
+     * Strips off the prefix in the returned map.
+     */
+    protected Map<String, Object> 
getGenericLocationSingleWordProperties(Map<String, ?> properties) {
+        return getMatchingSingleWordProperties("brooklyn.location.", 
properties);
+    }
+
+    /**
+     * Gets all properties that start with {@code 
"brooklyn.location."+scopeSuffix+"."}, stripping off
+     * the prefix in the returned map.
+     */
+    protected Map<String, Object> getScopedLocationProperties(String 
scopeSuffix, Map<String, ?> properties) {
+        checkArgument(!scopeSuffix.startsWith("."), "scopeSuffix \"%s\" should 
not start with \".\"", scopeSuffix);
+        checkArgument(!scopeSuffix.endsWith("."), "scopeSuffix \"%s\" should 
not end with \".\"", scopeSuffix);
+        String prefix = String.format("brooklyn.location.%s.", scopeSuffix);
+        return ConfigUtils.filterForPrefixAndStrip(properties, 
prefix).asMapWithStringKeys();
+    }
+
+    /**
+     * Gets all properties that start with the given {@code fullPrefix}, 
stripping off
+     * the prefix in the returned map.
+     */
+    protected Map<String, Object> getMatchingProperties(String fullPrefix, 
Map<String, ?> properties) {
+        return ConfigUtils.filterForPrefixAndStrip(properties, 
fullPrefix).asMapWithStringKeys();
+    }
+
+    /**
+     * Gets all properties that start with either of the given prefixes. The 
{@code fullPreferredPrefix} 
+     * properties will override any duplicates in {@code 
fullDeprecatedPrefix}. If there are any
+     * properties that match the {@code fullDeprecatedPrefix}, then a warning 
will be logged.
+     * 
+     * @see #getMatchingProperties(String, Map)
+     */
+    protected Map<String, Object> getMatchingProperties(String 
fullPreferredPrefix, String fullDeprecatedPrefix, Map<String, ?> properties) {
+        Map<String, Object> deprecatedResults = 
getMatchingProperties(fullDeprecatedPrefix, properties);
+        Map<String, Object> results = 
getMatchingProperties(fullPreferredPrefix, properties);
+        
+        if (deprecatedResults.size() > 0) {
+            LOG.warn("Deprecated use of properties prefix 
"+fullDeprecatedPrefix+"; instead use "+fullPreferredPrefix);
+            return MutableMap.<String, Object>builder()
+                    .putAll(deprecatedResults)
+                    .putAll(results)
+                    .build();
+        } else {
+            return results;
+        }
+    }
+
+    /**
+     * Gets all properties that start with the given {@code fullPrefix}, 
stripping off
+     * the prefix in the returned map.
+     * 
+     * Returns only those properties whose key suffix is a single word (i.e. 
contains no dots).
+     * We do this special (sub-optimal!) filtering because we want sub-scoped 
things 
+     * (e.g. could want brooklyn.location.privateKeyFile, but not 
brooklyn.location.named.*). 
+     */
+    protected Map<String, Object> getMatchingSingleWordProperties(String 
fullPrefix, Map<String, ?> properties) {
+        BrooklynProperties filteredProperties = 
ConfigUtils.filterForPrefixAndStrip(properties, fullPrefix);
+        return ConfigUtils.filterFor(filteredProperties, 
Predicates.not(Predicates.containsPattern("\\."))).asMapWithStringKeys();
+    }
+
+    /**
+     * Gets all single-word properties that start with either of the given 
prefixes. The {@code fullPreferredPrefix} 
+     * properties will override any duplicates in {@code 
fullDeprecatedPrefix}. If there are any
+     * properties that match the {@code fullDeprecatedPrefix}, then a warning 
will be logged.
+     * 
+     * @see #getMatchingSingleWordProperties(String, Map)
+     */
+    protected Map<String, Object> getMatchingSingleWordProperties(String 
fullPreferredPrefix, String fullDeprecatedPrefix, Map<String, ?> properties) {
+        Map<String, Object> deprecatedResults = 
getMatchingSingleWordProperties(fullDeprecatedPrefix, properties);
+        Map<String, Object> results = 
getMatchingSingleWordProperties(fullPreferredPrefix, properties);
+        
+        if (deprecatedResults.size() > 0) {
+            LOG.warn("Deprecated use of properties prefix 
"+fullDeprecatedPrefix+"; instead use "+fullPreferredPrefix);
+            return MutableMap.<String, Object>builder()
+                    .putAll(deprecatedResults)
+                    .putAll(results)
+                    .build();
+        } else {
+            return results;
+        }
+    }
+
+    protected Map<String, Object> getNamedLocationProperties(String 
locationName, Map<String, ?> properties) {
+        checkArgument(!Strings.isNullOrEmpty(locationName), "locationName 
should not be blank");
+        String prefix = String.format("brooklyn.location.named.%s.", 
locationName);
+        return ConfigUtils.filterForPrefixAndStrip(properties, 
prefix).asMapWithStringKeys();
+    }
+
+    protected Map<String, Object> transformDeprecated(Map<String, ?> 
properties) {
+        Map<String,Object> result = Maps.newLinkedHashMap();
+        Map<String, String> deprecatedKeysMapping = getDeprecatedKeysMapping();
+        
+        for (Map.Entry<String,?> entry : properties.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            if (deprecatedKeysMapping.containsKey(key)) {
+                String transformedKey = deprecatedKeysMapping.get(key);
+                LOG.warn("Deprecated key {}, transformed to {}; will not be 
supported in future versions", new Object[] {key, transformedKey});
+                result.put(transformedKey, value);
+            } else {
+                result.put(key, value);
+            }
+        }
+        
+        return result;
+    }
+    
+    protected Map<String,String> getDeprecatedKeysMapping() {
+        return DEPRECATED_KEYS_MAPPING;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/Locations.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/Locations.java 
b/core/src/main/java/org/apache/brooklyn/core/location/Locations.java
new file mode 100644
index 0000000..2cae5d7
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/Locations.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.mgmt.LocationManager;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.location.internal.LocationInternal;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.yaml.Yamls;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableList;
+
+public class Locations {
+
+    private static final Logger log = LoggerFactory.getLogger(Locations.class);
+
+    public static final LocationsFilter USE_FIRST_LOCATION = new 
LocationsFilter() {
+        private static final long serialVersionUID = 3100091615409115890L;
+
+        @Override
+        public List<Location> filterForContext(List<Location> locations, 
Object context) {
+            if (locations.size()<=1) return locations;
+            return ImmutableList.of(locations.get(0));
+        }
+    };
+
+    public interface LocationsFilter extends Serializable {
+        public List<Location> filterForContext(List<Location> locations, 
Object context);
+    }
+    
+    /** as {@link Machines#findUniqueMachineLocation(Iterable)} */
+    public static Maybe<MachineLocation> findUniqueMachineLocation(Iterable<? 
extends Location> locations) {
+        return Machines.findUniqueMachineLocation(locations);
+    }
+    
+    /** as {@link Machines#findUniqueSshMachineLocation(Iterable)} */
+    public static Maybe<SshMachineLocation> 
findUniqueSshMachineLocation(Iterable<? extends Location> locations) {
+        return Machines.findUniqueSshMachineLocation(locations);
+    }
+
+    /** if no locations are supplied, returns locations on the entity, or in 
the ancestors, until it finds a non-empty set,
+     * or ultimately the empty set if no locations are anywhere */ 
+    public static Collection<? extends Location> 
getLocationsCheckingAncestors(Collection<? extends Location> locations, Entity 
entity) {
+        // look in ancestors if location not set here
+        Entity ancestor = entity;
+        while ((locations==null || locations.isEmpty()) && ancestor!=null) {
+            locations = ancestor.getLocations();
+            ancestor = ancestor.getParent();
+        }
+        return locations;
+    }
+    
+    public static boolean isManaged(Location loc) {
+        ManagementContext mgmt = 
((LocationInternal)loc).getManagementContext();
+        return (mgmt != null) && mgmt.isRunning() && 
mgmt.getLocationManager().isManaged(loc);
+    }
+
+    public static void unmanage(Location loc) {
+        if (isManaged(loc)) {
+            ManagementContext mgmt = 
((LocationInternal)loc).getManagementContext();
+            mgmt.getLocationManager().unmanage(loc);
+        }
+    }
+    
+    /**
+     * Registers the given location (and all its children) with the management 
context. 
+     * @throws IllegalStateException if the parent location is not already 
managed
+     * 
+     * @since 0.6.0 (added only for backwards compatibility, where locations 
are being created directly; previously in {@link Entities}).
+     * @deprecated in 0.6.0; use {@link 
LocationManager#createLocation(LocationSpec)} instead.
+     */
+    public static void manage(Location loc, ManagementContext 
managementContext) {
+        if (!managementContext.getLocationManager().isManaged(loc)) {
+            log.warn("Deprecated use of unmanaged location ("+loc+"); will be 
managed automatically now but not supported in future versions");
+            // FIXME this occurs MOST OF THE TIME e.g. including 
BrooklynLauncher.location(locationString)
+            // not sure what is the recommend way to convert from 
locationString to locationSpec, or the API we want to expose;
+            // deprecating some of the LocationRegistry methods seems sensible?
+            log.debug("Stack trace for location of: Deprecated use of 
unmanaged location; will be managed automatically now but not supported in 
future versions", new Exception("TRACE for: Deprecated use of unmanaged 
location"));
+            managementContext.getLocationManager().manage(loc);
+        }
+    }
+
+    public static Location coerce(ManagementContext mgmt, Object rawO) {
+        if (rawO==null)
+            return null;
+        if (rawO instanceof Location)
+            return (Location)rawO;
+        
+        Object raw = rawO;
+        if (raw instanceof String)
+            raw = Yamls.parseAll((String)raw).iterator().next();
+        
+        String name;
+        Map<?, ?> flags = null;
+        if (raw instanceof Map) {
+            // for yaml, take the key, and merge with locationFlags
+            Map<?,?> tm = ((Map<?,?>)raw);
+            if (tm.size()!=1) {
+                throw new IllegalArgumentException("Location "+rawO+" is 
invalid; maps must have only one key, being the location spec string");
+            }
+            name = (String) tm.keySet().iterator().next();
+            flags = (Map<?, ?>) tm.values().iterator().next();
+            
+        } else if (raw instanceof String) {
+            name = (String)raw;
+            
+        } else {
+            throw new IllegalArgumentException("Location "+rawO+" is invalid; 
can only parse strings or maps");
+        }
+        return mgmt.getLocationRegistry().resolve(name, flags);
+    }
+    
+    public static Collection<? extends Location> 
coerceToCollection(ManagementContext mgmt, Object rawO) {
+        if (rawO==null) return null;
+        Object raw = rawO;
+        if (raw instanceof Collection) {
+            List<Location> result = MutableList.<Location>of();
+            for (Object o: (Collection<?>)raw)
+                result.add(coerce(mgmt, o));
+            return result;
+        }
+        if (raw instanceof String) {
+            raw = Yamls.parseAll((String)raw).iterator().next();
+            if (raw instanceof Collection)
+                return coerceToCollection(mgmt, raw);
+        }
+        return Collections.singletonList( coerce(mgmt, raw) );
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/Machines.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/Machines.java 
b/core/src/main/java/org/apache/brooklyn/core/location/Machines.java
new file mode 100644
index 0000000..b95d42f
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/Machines.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import java.net.InetAddress;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Iterables;
+
+import 
org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import 
org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation.LocalhostMachine;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.net.HasNetworkAddresses;
+
+/** utilities for working with MachineLocations */
+public class Machines {
+
+    private static final Logger log = LoggerFactory.getLogger(Machines.class);
+    
+    public static Maybe<String> getSubnetHostname(Location where) {
+        // TODO Should we look at HasNetworkAddresses? But that's not a 
hostname.
+        String hostname = null;
+        if (where instanceof HasSubnetHostname) {
+            hostname = ((HasSubnetHostname) where).getSubnetHostname();
+        }
+        if (hostname == null && where instanceof MachineLocation) {
+            InetAddress addr = ((MachineLocation) where).getAddress();
+            if (addr != null) hostname = addr.getHostAddress();
+        }
+        log.debug("computed subnet hostname {} for {}", hostname, where);
+        // TODO if Maybe.absent(message) appears, could/should use that
+        // TODO If no machine available, should we throw new 
IllegalStateException("Cannot find hostname for "+where);
+        return Maybe.fromNullable(hostname);
+    }
+
+    public static Maybe<String> getSubnetIp(Location where) {
+        // TODO Too much duplication between the ip and hostname methods
+        String result = null;
+        if (where instanceof HasSubnetHostname) {
+            result = ((HasSubnetHostname) where).getSubnetIp();
+        }
+        if (where instanceof HasNetworkAddresses) {
+            Set<String> privateAddrs = ((HasNetworkAddresses) 
where).getPrivateAddresses();
+            if (privateAddrs.size() > 0) {
+                result = Iterables.get(privateAddrs, 0);
+            }
+        }
+        if (result == null && where instanceof MachineLocation) {
+            InetAddress addr = ((MachineLocation) where).getAddress();
+            if (addr != null) result = addr.getHostAddress();
+        }
+        log.debug("computed subnet host ip {} for {}", result, where);
+        return Maybe.fromNullable(result);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> Maybe<T> findUniqueElement(Iterable<?> items, Class<T> 
type) {
+        if (items==null) return null;
+        Iterator<?> i = items.iterator();
+        T result = null;
+        while (i.hasNext()) {
+            Object candidate = i.next();
+            if (type.isInstance(candidate)) {
+                if (result==null) result = (T)candidate;
+                else {
+                    if (log.isTraceEnabled())
+                        log.trace("Multiple instances of "+type+" in 
"+items+"; ignoring");
+                    return Maybe.absent(new IllegalStateException("Multiple 
instances of "+type+" in "+items+"; expected a single one"));
+                }
+            }
+        }
+        if (result==null) 
+            return Maybe.absent(new IllegalStateException("No instances of 
"+type+" available (in "+items+")"));
+        return Maybe.of(result);
+    }
+    
+    public static Maybe<MachineLocation> findUniqueMachineLocation(Iterable<? 
extends Location> locations) {
+        return findUniqueElement(locations, MachineLocation.class);
+    }
+
+    public static Maybe<SshMachineLocation> 
findUniqueSshMachineLocation(Iterable<? extends Location> locations) {
+        return findUniqueElement(locations, SshMachineLocation.class);
+    }
+
+    public static Maybe<WinRmMachineLocation> 
findUniqueWinRmMachineLocation(Iterable<? extends Location> locations) {
+        return findUniqueElement(locations, WinRmMachineLocation.class);
+    }
+
+    public static Maybe<String> findSubnetHostname(Iterable<? extends 
Location> ll) {
+        Maybe<MachineLocation> l = findUniqueMachineLocation(ll);
+        if (!l.isPresent()) {
+            return Maybe.absent();
+//            throw new IllegalStateException("Cannot find hostname for among 
"+ll);
+        }
+        return Machines.getSubnetHostname(l.get());
+    }
+
+    public static Maybe<String> findSubnetHostname(Entity entity) {
+        String sh = entity.getAttribute(Attributes.SUBNET_HOSTNAME);
+        if (sh!=null) return Maybe.of(sh);
+        return findSubnetHostname(entity.getLocations());
+    }
+    
+    public static Maybe<String> findSubnetOrPublicHostname(Entity entity) {
+        String hn = entity.getAttribute(Attributes.HOSTNAME);
+        if (hn!=null) {
+            // attributes already set, see if there was a SUBNET_HOSTNAME set
+            // note we rely on (public) hostname being set _after_ 
subnet_hostname,
+            // to prevent tiny possibility of races resulting in hostname 
being returned
+            // becasue subnet is still being looked up -- see 
MachineLifecycleEffectorTasks
+            Maybe<String> sn = findSubnetHostname(entity);
+            if (sn.isPresent()) return sn;
+            // short-circuit discovery if attributes have been set already
+            return Maybe.of(hn);
+        }
+        
+        Maybe<MachineLocation> l = 
findUniqueMachineLocation(entity.getLocations());
+        if (!l.isPresent()) return Maybe.absent();
+        InetAddress addr = l.get().getAddress();
+        if (addr==null) return Maybe.absent();
+        return Maybe.fromNullable(addr.getHostName());
+    }
+
+    public static Maybe<String> findSubnetOrPrivateIp(Entity entity) {
+        // see comments in findSubnetOrPrivateHostname
+        String hn = entity.getAttribute(Attributes.ADDRESS);
+        if (hn!=null) {
+            Maybe<String> sn = findSubnetIp(entity);
+            if (sn.isPresent()) return sn;
+            return Maybe.of(hn);
+        }
+        
+        Maybe<MachineLocation> l = 
findUniqueMachineLocation(entity.getLocations());
+        if (!l.isPresent()) return Maybe.absent();
+        InetAddress addr = l.get().getAddress();
+        if (addr==null) return Maybe.absent();
+        return Maybe.fromNullable(addr.getHostAddress());
+    }
+
+    public static Maybe<String> findSubnetIp(Entity entity) {
+        String sh = entity.getAttribute(Attributes.SUBNET_ADDRESS);
+        if (sh!=null) return Maybe.of(sh);
+        return findSubnetIp(entity.getLocations());
+    }
+    
+    public static Maybe<String> findSubnetIp(Iterable<? extends Location> ll) {
+        // TODO Or if can't find MachineLocation, should we throw new 
IllegalStateException("Cannot find hostname for among "+ll);
+        Maybe<MachineLocation> l = findUniqueMachineLocation(ll);
+        return (l.isPresent()) ? Machines.getSubnetIp(l.get()) : 
Maybe.<String>absent();
+    }
+
+    /** returns whether it is localhost (and has warned) */
+    public static boolean warnIfLocalhost(Collection<? extends Location> 
locations, String message) {
+        if (locations.size()==1) {
+            Location l = locations.iterator().next();
+            if (l instanceof LocalhostMachineProvisioningLocation || l 
instanceof LocalhostMachine) {
+                log.warn(message);
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/NamedLocationResolver.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/NamedLocationResolver.java
 
b/core/src/main/java/org/apache/brooklyn/core/location/NamedLocationResolver.java
new file mode 100644
index 0000000..6dbe4d7
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/location/NamedLocationResolver.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationDefinition;
+import org.apache.brooklyn.api.location.LocationRegistry;
+import org.apache.brooklyn.api.location.LocationResolver;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.location.internal.LocationInternal;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Allows you to say, in your brooklyn.properties:
+ * 
+ * brooklyn.location.named.foo=localhost
+ * brooklyn.location.named.foo.user=bob
+ * brooklyn.location.named.foo.privateKeyFile=~/.ssh/custom-key-for-bob
+ * brooklyn.location.named.foo.privateKeyPassphrase=WithAPassphrase
+ * <p>
+ * or
+ * <p>
+ * brooklyn.location.named.bob-aws-east=jclouds:aws-ec2:us-east-1
+ * brooklyn.location.named.bob-aws-east.identity=BobId
+ * brooklyn.location.named.bob-aws-east.credential=BobCred
+ * <p>
+ * then you can simply refer to:   foo   or   named:foo   (or bob-aws-east or 
named:bob-aws-east)   in any location spec
+ */
+public class NamedLocationResolver implements LocationResolver {
+
+    public static final Logger log = 
LoggerFactory.getLogger(NamedLocationResolver.class);
+
+    public static final String NAMED = "named";
+    
+    @SuppressWarnings("unused")
+    private ManagementContext managementContext;
+
+    @Override
+    public void init(ManagementContext managementContext) {
+        this.managementContext = checkNotNull(managementContext, 
"managementContext");
+    }
+    
+    @Override
+    @SuppressWarnings({ "rawtypes" })
+    public Location newLocationFromString(Map locationFlags, String spec, 
LocationRegistry registry) {
+        String name = spec;
+        ConfigBag lfBag = 
ConfigBag.newInstance(locationFlags).putIfAbsent(LocationInternal.ORIGINAL_SPEC,
 name);
+        name = Strings.removeFromStart(spec, getPrefix()+":");
+        if (name.toLowerCase().startsWith(NAMED+":")) {
+            // since 0.7.0
+            log.warn("Deprecated use of 'named:' prefix with wrong case 
("+spec+"); support may be removed in future versions");
+            name = spec.substring( (NAMED+":").length() );
+        }
+        
+        LocationDefinition ld = registry.getDefinedLocationByName(name);
+        if (ld==null) throw new NoSuchElementException("No named location 
defined matching '"+name+"'");
+        return ((BasicLocationRegistry)registry).resolveLocationDefinition(ld, 
lfBag.getAllConfig(), name);
+    }
+
+    @Override
+    public String getPrefix() {
+        return NAMED;
+    }
+    
+    /** accepts anything starting  named:xxx  or  xxx where xxx is a defined 
location name */
+    @Override
+    public boolean accepts(String spec, LocationRegistry registry) {
+        if (BasicLocationRegistry.isResolverPrefixForSpec(this, spec, false)) 
return true;
+        if (registry.getDefinedLocationByName(spec)!=null) return true;
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/PortRanges.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/PortRanges.java 
b/core/src/main/java/org/apache/brooklyn/core/location/PortRanges.java
new file mode 100644
index 0000000..07daba5
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/PortRanges.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.brooklyn.api.location.PortRange;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class PortRanges {
+
+    public static final int MAX_PORT = 65535;
+    public static final PortRange ANY_HIGH_PORT = new LinearPortRange(1024, 
MAX_PORT);
+    
+    public static class SinglePort implements PortRange, Serializable {
+        private static final long serialVersionUID = 7446781416534230401L;
+        
+        final int port;
+        private SinglePort(int port) { this.port = port; }
+        
+        @Override
+        public Iterator<Integer> iterator() {
+            return Collections.singletonList(port).iterator();
+        }
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+        @Override
+        public boolean asBoolean() {
+            return true;
+        }
+        @Override
+        public String toString() {
+            return //getClass().getName()+"["+
+                    ""+port; //+"]";
+        }
+        public int hashCode() {
+            return Objects.hashCode(port);
+        }
+        @Override
+        public boolean equals(Object obj) {
+            return (obj instanceof SinglePort) && port == 
((SinglePort)obj).port;
+        }
+    }
+
+    public static class LinearPortRange implements PortRange, Serializable {
+        private static final long serialVersionUID = -9165280509363743508L;
+        
+        final int start, end, delta;
+        private LinearPortRange(int start, int end, int delta) {
+            this.start = start;
+            this.end = end;
+            this.delta = delta;
+            checkArgument(start > 0 && start <= MAX_PORT, "start port %s out 
of range", start);
+            checkArgument(end > 0 && end <= MAX_PORT, "end port %s out of 
range", end);
+            checkArgument(delta > 0 ? start <= end : start >= end, "start and 
end out of order: %s to %s, delta %s", start, end, delta);
+            checkArgument(delta != 0, "delta must be non-zero");
+        }
+        public LinearPortRange(int start, int end) {
+            this(start, end, (start<=end?1:-1));
+        }
+        
+        @Override
+        public Iterator<Integer> iterator() {
+            return new Iterator<Integer>() {
+                int next = start;
+                boolean hasNext = true;
+                
+                @Override
+                public boolean hasNext() {
+                    return hasNext;
+                }
+
+                @Override
+                public Integer next() {
+                    if (!hasNext)
+                        throw new NoSuchElementException("Exhausted available 
ports");
+                    int result = next;
+                    next += delta;
+                    if ((delta>0 && next>end) || (delta<0 && next<end)) 
hasNext = false;
+                    return result;
+                }
+                
+                @Override
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+            };
+        }
+        
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+        @Override
+        public boolean asBoolean() {
+            return true;
+        }
+        @Override
+        public String toString() {
+            return //getClass().getName()+"["+
+                    start+"-"+end; //+"]";
+        }
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(start, end, delta);
+        }
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof LinearPortRange)) return false;
+            LinearPortRange o = (LinearPortRange) obj;
+            return start == o.start && end == o.end && delta == o.delta;
+        }
+    }
+    
+    public static class AggregatePortRange implements PortRange, Serializable {
+        private static final long serialVersionUID = 7332682500816739660L;
+        
+        final List<PortRange> ranges;
+        private AggregatePortRange(List<PortRange> ranges) {
+            this.ranges = ImmutableList.copyOf(ranges);
+        }
+        @Override
+        public Iterator<Integer> iterator() {
+            return Iterables.concat(ranges).iterator();
+        }
+        @Override
+        public boolean isEmpty() {
+            for (PortRange r: ranges)
+                if (!r.isEmpty()) return false;
+            return true;
+        }
+        @Override
+        public boolean asBoolean() {
+            return !isEmpty();
+        }
+        @Override
+        public String toString() {
+            String s = "";
+            for (PortRange r: ranges) {
+                if (s.length()>0) s+=",";
+                s += r;
+            }
+            return //getClass().getName()+"["+
+                s; //+"]";
+        }
+        public int hashCode() {
+            return Objects.hashCode(ranges);
+        }
+        @Override
+        public boolean equals(Object obj) {
+            return (obj instanceof AggregatePortRange) && 
ranges.equals(((AggregatePortRange)obj).ranges);
+        }
+    }
+
+    public static PortRange fromInteger(int x) {
+        return new SinglePort(x);
+    }
+    
+    public static PortRange fromCollection(Collection<?> c) {
+        List<PortRange> l = new ArrayList<PortRange>();
+        for (Object o: c) {
+            if (o instanceof Integer) l.add(fromInteger((Integer)o));
+            else if (o instanceof String) l.add(fromString((String)o));
+            else if (o instanceof Collection) 
l.add(fromCollection((Collection<?>)o));
+            else l.add(TypeCoercions.coerce(o, PortRange.class));
+        }
+        return new AggregatePortRange(l);
+    }
+
+    /** parses a string representation of ports, as "80,8080,8000,8080-8099" */
+    public static PortRange fromString(String s) {
+        List<PortRange> l = new ArrayList<PortRange>();
+        for (String si: s.split(",")) {
+            si = si.trim();
+            int start, end;
+            if (si.endsWith("+")) {
+                String si2 = si.substring(0, si.length()-1).trim();
+                start = Integer.parseInt(si2);
+                end = MAX_PORT;
+            } else if (si.indexOf('-')>0) {
+                int v = si.indexOf('-');
+                start = Integer.parseInt(si.substring(0, v).trim());
+                end = Integer.parseInt(si.substring(v+1).trim());
+            } else if (si.length()==0) {
+                //nothing, ie empty range, just continue
+                continue;
+            } else {
+                //should be number on its own
+                l.add(new SinglePort(Integer.parseInt(si)));
+                continue;
+            }
+            l.add(new LinearPortRange(start, end));
+        }
+        if (l.size() == 1) {
+            return l.get(0);
+        } else {
+            return new AggregatePortRange(l);
+        }
+    }
+
+    private static AtomicBoolean initialized = new AtomicBoolean(false); 
+    /** performs the language extensions required for this project */
+    @SuppressWarnings("rawtypes")
+    public static void init() {
+        if (initialized.get()) return;
+        synchronized (initialized) {
+            if (initialized.get()) return;
+            TypeCoercions.registerAdapter(Integer.class, PortRange.class, new 
Function<Integer,PortRange>() {
+                public PortRange apply(Integer x) { return fromInteger(x); }
+            });
+            TypeCoercions.registerAdapter(String.class, PortRange.class, new 
Function<String,PortRange>() {
+                public PortRange apply(String x) { return fromString(x); }
+            });
+            TypeCoercions.registerAdapter(Collection.class, PortRange.class, 
new Function<Collection,PortRange>() {
+                public PortRange apply(Collection x) { return 
fromCollection(x); }
+            });
+            initialized.set(true);
+        }
+    }
+    
+    static {
+        init();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/RegistryLocationResolver.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/RegistryLocationResolver.java
 
b/core/src/main/java/org/apache/brooklyn/core/location/RegistryLocationResolver.java
new file mode 100644
index 0000000..a2cf5fa
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/location/RegistryLocationResolver.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationRegistry;
+import org.apache.brooklyn.api.location.LocationResolver;
+
+/**
+ * Extension to LocationResolver which can take a registry.
+ * 
+ * @deprecated since 0.6; the LocationResolver always takes the 
LocationRegistry now
+ */
+@Deprecated
+public interface RegistryLocationResolver extends LocationResolver {
+
+    @Override
+    @SuppressWarnings("rawtypes")
+    Location newLocationFromString(Map locationFlags, String spec, 
LocationRegistry registry);
+
+    @Override
+    boolean accepts(String spec, LocationRegistry registry);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/SupportsPortForwarding.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/SupportsPortForwarding.java
 
b/core/src/main/java/org/apache/brooklyn/core/location/SupportsPortForwarding.java
new file mode 100644
index 0000000..3510230
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/location/SupportsPortForwarding.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location;
+
+import org.apache.brooklyn.util.net.Cidr;
+
+import com.google.common.net.HostAndPort;
+
+public interface SupportsPortForwarding {
+
+    /** returns an endpoint suitable for contacting the indicated private port 
on this object,
+     * from the given Cidr, creating it if necessary and possible; 
+     * may return null if forwarding not available 
+     */
+    public HostAndPort getSocketEndpointFor(Cidr accessor, int privatePort);
+    
+    /** marker on a location to indicate that port forwarding should be done 
automatically
+     * for attempts to access from Brooklyn
+     */
+    public interface RequiresPortForwarding extends SupportsPortForwarding {
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/access/BrooklynAccessUtils.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/access/BrooklynAccessUtils.java
 
b/core/src/main/java/org/apache/brooklyn/core/location/access/BrooklynAccessUtils.java
new file mode 100644
index 0000000..1ac354f
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/location/access/BrooklynAccessUtils.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location.access;
+
+import java.util.Collection;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.BasicConfigKey;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.location.Machines;
+import org.apache.brooklyn.core.location.SupportsPortForwarding;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.core.task.ssh.SshTasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.net.Cidr;
+import org.apache.brooklyn.util.text.Strings;
+import org.python.google.common.base.Predicates;
+import org.python.google.common.collect.Iterables;
+
+import com.google.common.base.Supplier;
+import com.google.common.net.HostAndPort;
+
+public class BrooklynAccessUtils {
+
+    private static final Logger log = 
LoggerFactory.getLogger(BrooklynAccessUtils.class);
+    
+    public static final ConfigKey<PortForwardManager> PORT_FORWARDING_MANAGER 
= new BasicConfigKey<PortForwardManager>(
+            PortForwardManager.class, "brooklyn.portforwarding.manager", "A 
port-forwarding manager to use at an entity "
+                + "or a location, where supported; note this should normally 
be a serializable client instance to prevent "
+                + "the creation of multiple disconnected instances via config 
duplication");
+    
+    public static final ConfigKey<Cidr> MANAGEMENT_ACCESS_CIDR = new 
BasicConfigKey<Cidr>(
+            Cidr.class, "brooklyn.portforwarding.management.cidr", "CIDR to 
enable by default for port-forwarding for management",
+            null);  // TODO should be a list
+
+    public static HostAndPort getBrooklynAccessibleAddress(Entity entity, int 
port) {
+        String host;
+        
+        // look up port forwarding
+        PortForwardManager pfw = entity.getConfig(PORT_FORWARDING_MANAGER);
+        if (pfw!=null) {
+            Collection<Location> ll = entity.getLocations();
+            
+            synchronized (BrooklynAccessUtils.class) {
+                // TODO finer-grained synchronization
+                
+                for (MachineLocation machine : Iterables.filter(ll, 
MachineLocation.class)) {
+                    HostAndPort hp = pfw.lookup(machine, port);
+                    if (hp!=null) {
+                        log.debug("BrooklynAccessUtils found port-forwarded 
address {} for entity {}, port {}, using machine {}",
+                                new Object[] {hp, entity, port, machine});
+                        return hp;
+                    }
+                }
+                
+                Maybe<SupportsPortForwarding> supportPortForwardingLoc = 
Machines.findUniqueElement(ll, SupportsPortForwarding.class);
+                if (supportPortForwardingLoc.isPresent()) {
+                    Cidr source = entity.getConfig(MANAGEMENT_ACCESS_CIDR);
+                    SupportsPortForwarding loc = 
supportPortForwardingLoc.get();
+                    if (source!=null) {
+                        log.debug("BrooklynAccessUtils requesting new 
port-forwarding rule to access "+port+" on "+entity+" (at "+loc+", enabled for 
"+source+")");
+                        // TODO discuss, is this the best way to do it
+                        // (will probably _create_ the port forwarding rule!)
+                        HostAndPort hp = loc.getSocketEndpointFor(source, 
port);
+                        if (hp!=null) {
+                            log.debug("BrooklynAccessUtils created 
port-forwarded address {} for entity {}, port {}, using {}",
+                                    new Object[] {hp, entity, port, loc});
+                            return hp;
+                        }
+                    } else {
+                        log.warn("No "+MANAGEMENT_ACCESS_CIDR.getName()+" 
configured for "+entity+", so cannot forward "
+                                +"port "+port+" "+"even though 
"+PORT_FORWARDING_MANAGER.getName()+" was supplied, and "
+                                +"have location supporting port forwarding 
"+loc);
+                    }
+                }
+            }
+        }
+        
+        host = entity.getAttribute(Attributes.HOSTNAME);
+        if (host!=null) return HostAndPort.fromParts(host, port);
+        
+        throw new IllegalStateException("Cannot find way to access port 
"+port+" on "+entity+" from Brooklyn (no host.name)");
+    }
+
+    /** attempts to resolve hostnameTarget from origin
+     * @return null if it definitively can't be resolved,  
+     * best-effort IP address if possible, or blank if we could not run ssh or 
make sense of the output */
+    public static String getResolvedAddress(Entity entity, SshMachineLocation 
origin, String hostnameTarget) {
+        ProcessTaskWrapper<Integer> task = 
SshTasks.newSshExecTaskFactory(origin, "ping -c 1 -t 1 "+hostnameTarget)
+            .summary("checking resolution of 
"+hostnameTarget).allowingNonZeroExitCode().newTask();
+        
DynamicTasks.queueIfPossible(task).orSubmitAndBlock(entity).asTask().blockUntilEnded();
+        if (task.asTask().isError()) {
+            log.warn("ping could not be run, at "+entity+" / "+origin+": 
"+Tasks.getError(task.asTask()));
+            return "";
+        }
+        if (task.getExitCode()==null || task.getExitCode()!=0) {
+            if (task.getExitCode()!=null && task.getExitCode()<10) {
+                // small number means ping failed to resolve or ping the 
hostname
+                log.debug("not able to resolve "+hostnameTarget+" from 
"+origin+" for "+entity+" because exit code was "+task.getExitCode());
+                return null;
+            }
+            // large number means ping probably did not run
+            log.warn("ping not run as expected, at "+entity+" / "+origin+" 
(code "+task.getExitCode()+"):\n"+task.getStdout().trim()+" --- 
"+task.getStderr().trim());
+            return "";
+        }
+        String out = task.getStdout();
+        try {
+            String line1 = Strings.getFirstLine(out);
+            String ip = Strings.getFragmentBetween(line1, "(", ")");
+            if (Strings.isNonBlank(ip)) 
+                return ip;
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+            /* ignore non-parseable output */ 
+        }
+        if (out.contains("127.0.0.1")) return "127.0.0.1";
+        return "";
+    }
+
+    public static Supplier<String> resolvedAddressSupplier(final Entity 
entity, final SshMachineLocation origin, final String hostnameTarget) {
+        return new Supplier<String>() {
+            @Override
+            public String get() {
+                return getResolvedAddress(entity, origin, hostnameTarget);
+            }
+        };
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManager.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManager.java
 
b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManager.java
new file mode 100644
index 0000000..21e579c
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManager.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location.access;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.net.HostAndPort;
+
+import java.util.Collection;
+
+/**
+ * Acts as a registry for existing port mappings (e.g. the public endpoints 
for accessing specific
+ * ports on private VMs). This could be using DNAT, or iptables 
port-forwarding, or Docker port-mapping 
+ * via the host, or any other port mapping approach.
+ * 
+ * Also controls the allocation of ports via {@link #acquirePublicPort(String)}
+ * (e.g. for port-mapping with DNAT, then which port to use for the public 
side).
+ * 
+ * Implementations typically will not know anything about what the firewall/IP 
actually is, they just 
+ * handle a unique identifier for it.
+ * 
+ * To use, see {@link PortForwardManagerLocationResolver}, with code such as 
+ * {@code 
managementContext.getLocationRegistry().resolve("portForwardManager(scope=global)")}.
+ * 
+ * @see PortForwardManagerImpl for implementation notes and considerations.
+ */
+@Beta
+public interface PortForwardManager extends Location {
+
+    @Beta
+    class AssociationMetadata {
+        private final String publicIpId;
+        private final HostAndPort publicEndpoint;
+        private final Location location;
+        private final int privatePort;
+
+        /**
+         * Users are discouraged from calling this constructor; the signature 
may change in future releases.
+         * Instead, instances will be created automatically by Brooklyn to be 
passed to the
+         * {@link 
AssociationListener#onAssociationCreated(AssociationMetadata)} method.
+         */
+        public AssociationMetadata(String publicIpId, HostAndPort 
publicEndpoint, Location location, int privatePort) {
+            this.publicIpId = publicIpId;
+            this.publicEndpoint = publicEndpoint;
+            this.location = location;
+            this.privatePort = privatePort;
+        }
+
+        public String getPublicIpId() {
+            return publicIpId;
+        }
+
+        public HostAndPort getPublicEndpoint() {
+            return publicEndpoint;
+        }
+
+        public Location getLocation() {
+            return location;
+        }
+
+        public int getPrivatePort() {
+            return privatePort;
+        }
+
+        public String toString() {
+            return Objects.toStringHelper(this)
+                    .add("publicIpId", publicIpId)
+                    .add("publicEndpoint", publicEndpoint)
+                    .add("location", location)
+                    .add("privatePort", privatePort)
+                    .toString();
+        }
+    }
+
+    @Beta
+    interface AssociationListener {
+        void onAssociationCreated(AssociationMetadata metadata);
+        void onAssociationDeleted(AssociationMetadata metadata);
+    }
+
+    /**
+     * The intention is that there is one PortForwardManager instance per 
"scope". If you 
+     * use global, then it will be a shared instance (for that management 
context). If you 
+     * pass in your own name (e.g. "docker-fjie3") then it will shared with 
just any other
+     * places that use that same location spec (e.g. {@code 
portForwardManager(scope=docker-fjie3)}).
+     */
+    // TODO Note: using name "scope" rather than 
"brooklyn.portForwardManager.scope" so that location spec 
+    // "portForwardManager(scope=global)" works, rather than having to do 
+    // portForwardManager(brooklyn.portForwardManager.scope=global).
+    // The config being read by the PortForwardManagerLocationResolver doesn't 
respect @SetFromFlag("scope").
+    public static final ConfigKey<String> SCOPE = 
ConfigKeys.newStringConfigKey(
+            "scope",
+            "The scope that this applies to, defaulting to global",
+            "global");
+
+    @Beta
+    public static final ConfigKey<Integer> PORT_FORWARD_MANAGER_STARTING_PORT 
= ConfigKeys.newIntegerConfigKey(
+            "brooklyn.portForwardManager.startingPort",
+            "The starting port for assigning port numbers, such as for DNAT",
+            11000);
+
+    public String getScope();
+
+    /**
+     * Reserves a unique public port on the given publicIpId.
+     * <p>
+     * Often followed by {@link #associate(String, HostAndPort, int)} or 
{@link #associate(String, HostAndPort, Location, int)}
+     * to enable {@link #lookup(String, int)} or {@link #lookup(Location, 
int)} respectively.
+     */
+    public int acquirePublicPort(String publicIpId);
+
+    /**
+     * Records a location and private port against a public endpoint (ip and 
port),
+     * to support {@link #lookup(Location, int)}.
+     * <p>
+     * Superfluous if {@link #acquirePublicPort(String, Location, int)} was 
used,
+     * but strongly recommended if {@link #acquirePublicPortExplicit(String, 
int)} was used
+     * e.g. if the location is not known ahead of time.
+     */
+    public void associate(String publicIpId, HostAndPort publicEndpoint, 
Location l, int privatePort);
+
+    /**
+     * Records a mapping for publicIpId:privatePort to a public endpoint, such 
that it can
+     * subsequently be looked up using {@link #lookup(String, int)}.
+     */
+    public void associate(String publicIpId, HostAndPort publicEndpoint, int 
privatePort);
+
+    /**
+     * Registers a listener, which will be notified each time a new port 
mapping is associated. See {@link #associate(String, HostAndPort, int)}
+     * and {@link #associate(String, HostAndPort, Location, int)}.
+     */
+    @Beta
+    public void addAssociationListener(AssociationListener listener, 
Predicate<? super AssociationMetadata> filter);
+
+    @Beta
+    public void removeAssociationListener(AssociationListener listener);
+    
+    /**
+     * Returns the public ip hostname and public port for use contacting the 
given endpoint.
+     * <p>
+     * Will return null if:
+     * <ul>
+     * <li>No publicPort is associated with this location and private port.
+     * <li>No publicIpId is associated with this location and private port.
+     * <li>No publicIpHostname is recorded against the associated publicIpId.
+     * </ul>
+     * Conceivably this may have to be access-location specific.
+     *
+     * @see #recordPublicIpHostname(String, String)
+     */
+    public HostAndPort lookup(Location l, int privatePort);
+
+    /**
+     * Returns the public endpoint (host and port) for use contacting the 
given endpoint.
+     * 
+     * Expects a previous call to {@link #associate(String, HostAndPort, 
int)}, to register
+     * the endpoint.
+     * 
+     * Will return null if there has not been a public endpoint associated 
with this pairing.
+     */
+    public HostAndPort lookup(String publicIpId, int privatePort);
+
+    /** 
+     * Clears the given port mapping, returning true if there was a match.
+     */
+    public boolean forgetPortMapping(String publicIpId, int publicPort);
+    
+    /** 
+     * Clears the port mappings associated with the given location, returning 
true if there were any matches.
+     */
+    public boolean forgetPortMappings(Location location);
+    
+    /** 
+     * Clears the port mappings associated with the given publicIpId, 
returning true if there were any matches.
+     */
+    public boolean forgetPortMappings(String publicIpId);
+    
+    public String toVerboseString();
+
+    
+    
///////////////////////////////////////////////////////////////////////////////////
+    // Deprecated
+    
///////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Reserves a unique public port for the purpose of forwarding to the 
given target,
+     * associated with a given location for subsequent lookup purpose.
+     * <p>
+     * If already allocated, returns the previously allocated.
+     * 
+     * @deprecated since 0.7.0; use {@link #acquirePublicPort(String)}, and 
then use {@link #associate(String, HostAndPort, int)} or {@link 
#associate(String, HostAndPort, Location, int)}
+     */
+    @Deprecated
+    public int acquirePublicPort(String publicIpId, Location l, int 
privatePort);
+
+    /** 
+     * Returns old mapping if it existed, null if it is new.
+     * 
+     * @deprecated since 0.7.0; use {@link #associate(String, HostAndPort, 
int)} or {@link #associate(String, HostAndPort, Location, int)}
+     */
+    @Deprecated
+    public PortMapping acquirePublicPortExplicit(String publicIpId, int port);
+
+    /**
+     * Records a location and private port against a publicIp and public port,
+     * to support {@link #lookup(Location, int)}.
+     * <p>
+     * Superfluous if {@link #acquirePublicPort(String, Location, int)} was 
used,
+     * but strongly recommended if {@link #acquirePublicPortExplicit(String, 
int)} was used
+     * e.g. if the location is not known ahead of time.
+     * 
+     * @deprecated Use {@link #associate(String, HostAndPort, Location, int)}
+     */
+    @Deprecated
+    public void associate(String publicIpId, int publicPort, Location l, int 
privatePort);
+
+    /**
+     * Records a public hostname or address to be associated with the given 
publicIpId for lookup purposes.
+     * <p>
+     * Conceivably this may have to be access-location specific.
+     * 
+     * @deprecated Use {@link #associate(String, HostAndPort, int)} or {@link 
#associate(String, HostAndPort, Location, int)}
+     */
+    @Deprecated
+    public void recordPublicIpHostname(String publicIpId, String 
hostnameOrPublicIpAddress);
+
+    /**
+     * Returns a recorded public hostname or address.
+     * 
+     * @deprecated Use {@link #lookup(String, int)} or {@link 
#lookup(Location, int)}
+     */
+    @Deprecated
+    public String getPublicIpHostname(String publicIpId);
+    
+    /**
+     * Clears a previous call to {@link #recordPublicIpHostname(String, 
String)}.
+     * 
+     * @deprecated Use {@link #forgetPortMapping(String, int)} or {@link 
#forgetPortMappings(Location)}
+     */
+    @Deprecated
+    public boolean forgetPublicIpHostname(String publicIpId);
+
+    /**
+     * Returns true if this implementation is a client which is immutable/safe 
for serialization
+     * i.e. it delegates to something on an entity or location elsewhere.
+     * 
+     * @deprecated since 0.7.0; no need to separate client-proxy from impl
+     */
+    @Deprecated
+    public boolean isClient();
+    
+
+    
///////////////////////////////////////////////////////////////////////////////////
+    // Deprecated; just internal
+    
///////////////////////////////////////////////////////////////////////////////////
+
+    /** 
+     * Returns the port mapping for a given publicIpId and public port.
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Deprecated
+    public PortMapping getPortMappingWithPublicSide(String publicIpId, int 
publicPort);
+
+    /** 
+     * Returns the subset of port mappings associated with a given public IP 
ID.
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Deprecated
+    public Collection<PortMapping> getPortMappingWithPublicIpId(String 
publicIpId);
+
+    /** 
+     * @see {@link #forgetPortMapping(String, int)} and {@link 
#forgetPortMappings(Location)}
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Deprecated
+    public boolean forgetPortMapping(PortMapping m);
+
+    /**
+     * Returns the public host and port for use accessing the given mapping.
+     * <p>
+     * Conceivably this may have to be access-location specific.
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Deprecated
+    public HostAndPort getPublicHostAndPort(PortMapping m);
+
+    /** 
+     * Returns the subset of port mappings associated with a given location.
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Deprecated
+    public Collection<PortMapping> getLocationPublicIpIds(Location l);
+        
+    /** 
+     * Returns the mapping to a given private port, or null if none.
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Deprecated
+    public PortMapping getPortMappingWithPrivateSide(Location l, int 
privatePort);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerAuthority.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerAuthority.java
 
b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerAuthority.java
new file mode 100644
index 0000000..89c57f3
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerAuthority.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.location.access;
+
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.core.entity.EntityInternal;
+
+/**
+ * @deprecated since 0.7.0; use {@link PortForwardManagerImpl}
+ */
+@Deprecated
+public class PortForwardManagerAuthority extends PortForwardManagerImpl {
+    private Entity owningEntity;
+
+    public PortForwardManagerAuthority() {
+    }
+
+    public PortForwardManagerAuthority(Entity owningEntity) {
+        this.owningEntity = owningEntity;
+    }
+
+    protected void onChanged() {
+        if (owningEntity != null) {
+            ((EntityInternal) owningEntity).requestPersist();
+        } else {
+            super.onChanged();
+        }
+    }
+}

Reply via email to