brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aleds...@apache.org
Subject [47/62] [abbrv] incubator-brooklyn git commit: rename core’s o.a.b.location to o.a.b.core.location
Date Wed, 19 Aug 2015 21:21:21 GMT
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerClient.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerClient.java b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerClient.java
new file mode 100644
index 0000000..12e15aa
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerClient.java
@@ -0,0 +1,405 @@
+/*
+ * 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 java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.net.HostAndPort;
+
+/**
+ * @deprecated since 0.7.0; just use the {@link PortForwardManager}, or a direct reference to its impl {@link PortForwardManagerImpl}
+ */
+@Deprecated
+public class PortForwardManagerClient implements PortForwardManager {
+
+    private static final long serialVersionUID = -295204304305332895L;
+    
+    protected final Supplier<PortForwardManager> delegateSupplier;
+    private transient volatile PortForwardManager _delegate;
+    
+    protected PortForwardManagerClient(Supplier<PortForwardManager> supplier) {
+        this.delegateSupplier = supplier;
+    }
+    
+    /** creates an instance given a supplier; 
+     * the supplier should be brooklyn-persistable, that is to say
+     * references should be in terms of entities/locations 
+     * which can retrieve an authoritative source even under cloning */
+    public static PortForwardManager fromSupplier(Supplier<PortForwardManager> supplier) {
+        return new PortForwardManagerClient(supplier);
+    }
+
+    /** creates an instance given an entity and an interface method it implements to retrieve the PortForwardManager */ 
+    public static PortForwardManager fromMethodOnEntity(final Entity entity, final String getterMethodOnEntity) {
+        Preconditions.checkNotNull(entity);
+        Preconditions.checkNotNull(getterMethodOnEntity);
+        return new PortForwardManagerClient(new Supplier<PortForwardManager>() {
+            @Override
+            public PortForwardManager get() {
+                PortForwardManager result;
+                try {
+                    result = (PortForwardManager) entity.getClass().getMethod(getterMethodOnEntity).invoke(entity);
+                } catch (Exception e) {
+                    Exceptions.propagateIfFatal(e);
+                    throw new IllegalStateException("Cannot invoke "+getterMethodOnEntity+" on "+entity+" ("+entity.getClass()+"): "+e, e);
+                }
+                if (result==null)
+                    throw new IllegalStateException("No PortForwardManager available via "+getterMethodOnEntity+" on "+entity+" (returned null)");
+                return result;
+            }
+        });
+    }
+
+    /** creates an instance given an entity and {@link AttributeSensor} to retrieve the PortForwardManager */ 
+    public static PortForwardManager fromAttributeOnEntity(final Entity entity, final AttributeSensor<PortForwardManager> attributeOnEntity) {
+        Preconditions.checkNotNull(entity);
+        Preconditions.checkNotNull(attributeOnEntity);
+        return new PortForwardManagerClient(new Supplier<PortForwardManager>() {
+            @Override
+            public PortForwardManager get() {
+                PortForwardManager result = entity.getAttribute(attributeOnEntity);
+                if (result==null)
+                    throw new IllegalStateException("No PortForwardManager available via "+attributeOnEntity+" on "+entity+" (returned null)");
+                return result;
+            }
+        });
+    }
+    
+    protected PortForwardManager getDelegate() {
+        if (_delegate==null) {
+            _delegate = delegateSupplier.get();
+        }
+        return _delegate;
+    }
+
+    @Override
+    public int acquirePublicPort(String publicIpId) {
+        return getDelegate().acquirePublicPort(publicIpId);
+    }
+
+    @Override
+    public void associate(String publicIpId, HostAndPort publicEndpoint, Location l, int privatePort) {
+        getDelegate().associate(publicIpId, publicEndpoint, l, privatePort);
+    }
+
+    @Override
+    public void associate(String publicIpId, HostAndPort publicEndpoint, int privatePort) {
+        getDelegate().associate(publicIpId, publicEndpoint, privatePort);
+    }
+
+    @Override
+    public HostAndPort lookup(Location l, int privatePort) {
+        return getDelegate().lookup(l, privatePort);
+    }
+
+    @Override
+    public HostAndPort lookup(String publicIpId, int privatePort) {
+        return getDelegate().lookup(publicIpId, privatePort);
+    }
+
+    @Override
+    public boolean forgetPortMapping(String publicIpId, int publicPort) {
+        return getDelegate().forgetPortMapping(publicIpId, publicPort);
+    }
+
+    @Override
+    public boolean forgetPortMappings(Location location) {
+        return getDelegate().forgetPortMappings(location);
+    }
+
+    @Override
+    public boolean forgetPortMappings(String publicIpId) {
+        return getDelegate().forgetPortMappings(publicIpId);
+    }
+
+    @Override
+    public String getId() {
+        return getDelegate().getId();
+    }
+
+    @Override
+    public String getScope() {
+        return getDelegate().getScope();
+    }
+
+    @Override
+    public void addAssociationListener(AssociationListener listener, Predicate<? super AssociationMetadata> filter) {
+        getDelegate().addAssociationListener(listener, filter);
+    }
+
+    @Override
+    public void removeAssociationListener(AssociationListener listener) {
+        getDelegate().removeAssociationListener(listener);
+    }
+
+    @Override
+    public String toVerboseString() {
+        return getClass().getName()+"[wrapping="+getDelegate().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)}
+     */
+    @Override
+    @Deprecated
+    public int acquirePublicPort(String publicIpId, Location l, int privatePort) {
+        return getDelegate().acquirePublicPort(publicIpId, l, 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)}
+     */
+    @Override
+    @Deprecated
+    public PortMapping acquirePublicPortExplicit(String publicIpId, int publicPort) {
+        return getDelegate().acquirePublicPortExplicit(publicIpId, publicPort);
+    }
+
+    /**
+     * 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)}
+     */
+    @Override
+    @Deprecated
+    public void associate(String publicIpId, int publicPort, Location l, int privatePort) {
+        getDelegate().associate(publicIpId, publicPort, l, 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)}
+     */
+    @Override
+    @Deprecated
+    public void recordPublicIpHostname(String publicIpId, String hostnameOrPublicIpAddress) {
+        getDelegate().recordPublicIpHostname(publicIpId, hostnameOrPublicIpAddress);
+    }
+
+    /**
+     * Returns a recorded public hostname or address.
+     * 
+     * @deprecated Use {@link #lookup(String, int)} or {@link #lookup(Location, int)}
+     */
+    @Override
+    @Deprecated
+    public String getPublicIpHostname(String publicIpId) {
+        return getDelegate().getPublicIpHostname(publicIpId);
+    }
+    
+    /**
+     * Clears a previous call to {@link #recordPublicIpHostname(String, String)}.
+     * 
+     * @deprecated Use {@link #forgetPortMapping(String, int)} or {@link #forgetPortMapping(Location, int)}
+     */
+    @Override
+    @Deprecated
+    public boolean forgetPublicIpHostname(String publicIpId) {
+        return getDelegate().forgetPublicIpHostname(publicIpId);
+    }
+
+    @Override
+    @Deprecated
+    public boolean isClient() {
+        return true;
+    }
+
+
+    ///////////////////////////////////////////////////////////////////////////////////
+    // 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
+     */
+    @Override
+    @Deprecated
+    public PortMapping getPortMappingWithPublicSide(String publicIpId, int publicPort) {
+        return getDelegate().getPortMappingWithPublicSide(publicIpId, 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
+     */
+    @Override
+    @Deprecated
+    public Collection<PortMapping> getPortMappingWithPublicIpId(String publicIpId) {
+        return getDelegate().getPortMappingWithPublicIpId(publicIpId);
+    }
+
+    /** 
+     * @see #forgetPortMapping(String, int)
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Override
+    @Deprecated
+    public boolean forgetPortMapping(PortMapping m) {
+        return getDelegate().forgetPortMapping(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
+     */
+    @Override
+    @Deprecated
+    public HostAndPort getPublicHostAndPort(PortMapping m) {
+        return getDelegate().getPublicHostAndPort(m);
+    }
+
+    /** 
+     * Returns the subset of port mappings associated with a given location.
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Override
+    @Deprecated
+    public Collection<PortMapping> getLocationPublicIpIds(Location l) {
+        return getDelegate().getLocationPublicIpIds(l);
+    }
+        
+    /** 
+     * Returns the mapping to a given private port, or null if none.
+     * 
+     * @deprecated since 0.7.0; this method will be internal only
+     */
+    @Override
+    @Deprecated
+    public PortMapping getPortMappingWithPrivateSide(Location l, int privatePort) {
+        return getDelegate().getPortMappingWithPrivateSide(l, privatePort);
+    }
+    
+    @Override
+    public String toString() {
+        return getClass().getName()+"[id="+getId()+"]";
+    }
+
+    @Override
+    public String getDisplayName() {
+        return getDelegate().getDisplayName();
+    }
+
+    @Override
+    public Location getParent() {
+        return getDelegate().getParent();
+    }
+
+    @Override
+    public Collection<Location> getChildren() {
+        return getDelegate().getChildren();
+    }
+
+    @Override
+    public void setParent(Location newParent) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean containsLocation(Location potentialDescendent) {
+        return getDelegate().containsLocation(potentialDescendent);
+    }
+
+    @Override
+    public <T> T getConfig(ConfigKey<T> key) {
+        return getDelegate().getConfig(key);
+    }
+
+    @Override
+    public <T> T getConfig(HasConfigKey<T> key) {
+        return getDelegate().getConfig(key);
+    }
+
+    @Override
+    public boolean hasConfig(ConfigKey<?> key, boolean includeInherited) {
+        return getDelegate().hasConfig(key, includeInherited);
+    }
+
+    @Override
+    public Map<String, Object> getAllConfig(boolean includeInherited) {
+        return getDelegate().getAllConfig(includeInherited);
+    }
+
+    @Override
+    public boolean hasExtension(Class<?> extensionType) {
+        return getDelegate().hasExtension(extensionType);
+    }
+
+    @Override
+    public <T> T getExtension(Class<T> extensionType) {
+        return getDelegate().getExtension(extensionType);
+    }
+
+    @Override
+    public String getCatalogItemId() {
+        return getDelegate().getCatalogItemId();
+    }
+
+    @Override
+    public TagSupport tags() {
+        return getDelegate().tags();
+    }
+
+    @Override
+    public <T> T setConfig(ConfigKey<T> key, T val) {
+        return getDelegate().setConfig(key, val);
+    }
+
+    @Override
+    public ConfigurationSupport config() {
+        return getDelegate().config();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerImpl.java b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerImpl.java
new file mode 100644
index 0000000..30be900
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerImpl.java
@@ -0,0 +1,505 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.rebind.RebindContext;
+import org.apache.brooklyn.api.mgmt.rebind.RebindSupport;
+import org.apache.brooklyn.api.mgmt.rebind.mementos.LocationMemento;
+import org.apache.brooklyn.core.location.AbstractLocation;
+import org.apache.brooklyn.core.mgmt.rebind.BasicLocationRebindSupport;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.net.HostAndPort;
+
+/**
+ * 
+ * @author aled
+ *
+ * TODO This implementation is not efficient, and currently has a cap of about 50000 rules.
+ * Need to improve the efficiency and scale.
+ * A quick win could be to use a different portReserved counter for each publicIpId,
+ * when calling acquirePublicPort?
+ * 
+ * TODO Callers need to be more careful in acquirePublicPort for which ports are actually in use.
+ * If multiple apps sharing the same public-ip (e.g. in the same vcloud-director vOrg) then they 
+ * must not allocate the same public port (e.g. ensure they share the same PortForwardManager
+ * by using the same scope in 
+ * {@code managementContext.getLocationRegistry().resolve("portForwardManager(scope=global)")}.
+ * However, this still doesn't check if the port is *actually* available. For example, if a
+ * different Brooklyn instance is also deploying there then we can get port conflicts, or if 
+ * some ports in that range are already in use (e.g. due to earlier dev/test runs) then this
+ * will not be respected. Callers should probably figure out the port number themselves, but
+ * that also leads to concurrency issues.
+ * 
+ * TODO The publicIpId means different things to different callers:
+ * <ul>
+ *   <li> In acquirePublicPort() it is (often?) an identifier of the actual public ip.
+ *   <li> In later calls to associate(), it is (often?) an identifier for the target machine
+ *        such as the jcloudsMachine.getJcloudsId().
+ * </ul>
+ */
+@SuppressWarnings("serial")
+public class PortForwardManagerImpl extends AbstractLocation implements PortForwardManager {
+
+    private static final Logger log = LoggerFactory.getLogger(PortForwardManagerImpl.class);
+    
+    protected final Map<String,PortMapping> mappings = new LinkedHashMap<String,PortMapping>();
+
+    private final Map<AssociationListener, Predicate<? super AssociationMetadata>> associationListeners = new ConcurrentHashMap<AssociationListener, Predicate<? super AssociationMetadata>>();
+
+    @Deprecated
+    protected final Map<String,String> publicIpIdToHostname = new LinkedHashMap<String,String>();
+    
+    // horrible hack -- see javadoc above
+    private final AtomicInteger portReserved = new AtomicInteger(11000);
+
+    private final Object mutex = new Object();
+    
+    public PortForwardManagerImpl() {
+        super();
+        if (isLegacyConstruction()) {
+            log.warn("Deprecated construction of "+PortForwardManagerImpl.class.getName()+"; instead use location resolver");
+        }
+    }
+    
+    @Override
+    public void init() {
+        super.init();
+        Integer portStartingPoint;
+        Object rawPort = getAllConfigBag().getStringKey(PORT_FORWARD_MANAGER_STARTING_PORT.getName());
+        if (rawPort != null) {
+            portStartingPoint = getConfig(PORT_FORWARD_MANAGER_STARTING_PORT);
+        } else {
+            portStartingPoint = getManagementContext().getConfig().getConfig(PORT_FORWARD_MANAGER_STARTING_PORT);
+        }
+        portReserved.set(portStartingPoint);
+        log.debug(this+" set initial port to "+portStartingPoint);
+    }
+
+    // TODO Need to use attributes for these so they are persisted (once a location is an entity),
+    // rather than this deprecated approach of custom fields.
+    @Override
+    public RebindSupport<LocationMemento> getRebindSupport() {
+        return new BasicLocationRebindSupport(this) {
+            @Override public LocationMemento getMemento() {
+                Map<String, PortMapping> mappingsCopy;
+                Map<String,String> publicIpIdToHostnameCopy;
+                synchronized (mutex) {
+                    mappingsCopy = MutableMap.copyOf(mappings);
+                    publicIpIdToHostnameCopy = MutableMap.copyOf(publicIpIdToHostname);
+                }
+                return getMementoWithProperties(MutableMap.<String,Object>of(
+                        "mappings", mappingsCopy, 
+                        "portReserved", portReserved.get(), 
+                        "publicIpIdToHostname", publicIpIdToHostnameCopy));
+            }
+            @Override
+            protected void doReconstruct(RebindContext rebindContext, LocationMemento memento) {
+                super.doReconstruct(rebindContext, memento);
+                mappings.putAll( Preconditions.checkNotNull((Map<String, PortMapping>) memento.getCustomField("mappings"), "mappings was not serialized correctly"));
+                portReserved.set( (Integer)memento.getCustomField("portReserved"));
+                publicIpIdToHostname.putAll( Preconditions.checkNotNull((Map<String, String>)memento.getCustomField("publicIpIdToHostname"), "publicIpIdToHostname was not serialized correctly") );
+            }
+        };
+    }
+    
+    @Override
+    public int acquirePublicPort(String publicIpId) {
+        int port;
+        synchronized (mutex) {
+            // far too simple -- see javadoc above
+            port = getNextPort();
+            
+            // TODO When delete deprecated code, stop registering PortMapping until associate() is called
+            PortMapping mapping = new PortMapping(publicIpId, port, null, -1);
+            log.debug(this+" allocating public port "+port+" on "+publicIpId+" (no association info yet)");
+            
+            mappings.put(makeKey(publicIpId, port), mapping);
+        }
+        onChanged();
+        return port;
+    }
+
+    protected int getNextPort() {
+        // far too simple -- see javadoc above
+        return portReserved.getAndIncrement();
+    }
+    
+    @Override
+    public void associate(String publicIpId, HostAndPort publicEndpoint, Location l, int privatePort) {
+        associateImpl(publicIpId, publicEndpoint, l, privatePort);
+        emitAssociationCreatedEvent(publicIpId, publicEndpoint, l, privatePort);
+    }
+
+    @Override
+    public void associate(String publicIpId, HostAndPort publicEndpoint, int privatePort) {
+        associateImpl(publicIpId, publicEndpoint, null, privatePort);
+        emitAssociationCreatedEvent(publicIpId, publicEndpoint, null, privatePort);
+    }
+
+    protected void associateImpl(String publicIpId, HostAndPort publicEndpoint, Location l, int privatePort) {
+        synchronized (mutex) {
+            String publicIp = publicEndpoint.getHostText();
+            int publicPort = publicEndpoint.getPort();
+            recordPublicIpHostname(publicIpId, publicIp);
+            PortMapping mapping = new PortMapping(publicIpId, publicEndpoint, l, privatePort);
+            PortMapping oldMapping = getPortMappingWithPublicSide(publicIpId, publicPort);
+            log.debug(this+" associating public "+publicEndpoint+" on "+publicIpId+" with private port "+privatePort+" at "+l+" ("+mapping+")"
+                    +(oldMapping == null ? "" : " (overwriting "+oldMapping+" )"));
+            mappings.put(makeKey(publicIpId, publicPort), mapping);
+        }
+        onChanged();
+    }
+
+    private void emitAssociationCreatedEvent(String publicIpId, HostAndPort publicEndpoint, Location location, int privatePort) {
+        AssociationMetadata metadata = new AssociationMetadata(publicIpId, publicEndpoint, location, privatePort);
+        for (Map.Entry<AssociationListener, Predicate<? super AssociationMetadata>> entry : associationListeners.entrySet()) {
+            if (entry.getValue().apply(metadata)) {
+                try {
+                    entry.getKey().onAssociationCreated(metadata);
+                } catch (Exception e) {
+                    Exceptions.propagateIfFatal(e);
+                    log.warn("Exception thrown when emitting association creation event " + metadata, e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public HostAndPort lookup(Location l, int privatePort) {
+        synchronized (mutex) {
+            for (PortMapping m: mappings.values()) {
+                if (l.equals(m.target) && privatePort == m.privatePort)
+                    return getPublicHostAndPort(m);
+            }
+        }
+        return null;
+    }
+    
+    @Override
+    public HostAndPort lookup(String publicIpId, int privatePort) {
+        synchronized (mutex) {
+            for (PortMapping m: mappings.values()) {
+                if (publicIpId.equals(m.publicIpId) && privatePort==m.privatePort)
+                    return getPublicHostAndPort(m);
+            }
+        }
+        return null;
+    }
+    
+    @Override
+    public boolean forgetPortMapping(String publicIpId, int publicPort) {
+        PortMapping old;
+        synchronized (mutex) {
+            old = mappings.remove(makeKey(publicIpId, publicPort));
+            if (old != null) {
+                emitAssociationDeletedEvent(associationMetadataFromPortMapping(old));
+            }
+            log.debug("cleared port mapping for "+publicIpId+":"+publicPort+" - "+old);
+        }
+        if (old != null) onChanged();
+        return (old != null);
+    }
+    
+    @Override
+    public boolean forgetPortMappings(Location l) {
+        List<PortMapping> result = Lists.newArrayList();
+        synchronized (mutex) {
+            for (Iterator<PortMapping> iter = mappings.values().iterator(); iter.hasNext();) {
+                PortMapping m = iter.next();
+                if (l.equals(m.target)) {
+                    iter.remove();
+                    result.add(m);
+                    emitAssociationDeletedEvent(associationMetadataFromPortMapping(m));
+                }
+            }
+        }
+        if (log.isDebugEnabled()) log.debug("cleared all port mappings for "+l+" - "+result);
+        if (!result.isEmpty()) {
+            onChanged();
+        }
+        return !result.isEmpty();
+    }
+    
+    @Override
+    public boolean forgetPortMappings(String publicIpId) {
+        List<PortMapping> result = Lists.newArrayList();
+        synchronized (mutex) {
+            for (Iterator<PortMapping> iter = mappings.values().iterator(); iter.hasNext();) {
+                PortMapping m = iter.next();
+                if (publicIpId.equals(m.publicIpId)) {
+                    iter.remove();
+                    result.add(m);
+                    emitAssociationDeletedEvent(associationMetadataFromPortMapping(m));
+                }
+            }
+        }
+        if (log.isDebugEnabled()) log.debug("cleared all port mappings for "+publicIpId+" - "+result);
+        if (!result.isEmpty()) {
+            onChanged();
+        }
+        return !result.isEmpty();
+    }
+
+    private void emitAssociationDeletedEvent(AssociationMetadata metadata) {
+        for (Map.Entry<AssociationListener, Predicate<? super AssociationMetadata>> entry : associationListeners.entrySet()) {
+            if (entry.getValue().apply(metadata)) {
+                try {
+                    entry.getKey().onAssociationDeleted(metadata);
+                } catch (Exception e) {
+                    Exceptions.propagateIfFatal(e);
+                    log.warn("Exception thrown when emitting association creation event " + metadata, e);
+                }
+            }
+        }
+    }
+    
+    @Override
+    protected ToStringHelper string() {
+        int size;
+        synchronized (mutex) {
+            size = mappings.size();
+        }
+        return super.string().add("scope", getScope()).add("mappingsSize", size);
+    }
+
+    @Override
+    public String toVerboseString() {
+        String mappingsStr;
+        synchronized (mutex) {
+            mappingsStr = mappings.toString();
+        }
+        return string().add("mappings", mappingsStr).toString();
+    }
+
+    @Override
+    public String getScope() {
+        return checkNotNull(getConfig(SCOPE), "scope");
+    }
+
+    @Override
+    public boolean isClient() {
+        return false;
+    }
+
+    @Override
+    public void addAssociationListener(AssociationListener listener, Predicate<? super AssociationMetadata> filter) {
+        associationListeners.put(listener, filter);
+    }
+
+    @Override
+    public void removeAssociationListener(AssociationListener listener) {
+        associationListeners.remove(listener);
+    }
+
+    protected String makeKey(String publicIpId, int publicPort) {
+        return publicIpId+":"+publicPort;
+    }
+
+    private AssociationMetadata associationMetadataFromPortMapping(PortMapping portMapping) {
+        String publicIpId = portMapping.getPublicEndpoint().getHostText();
+        HostAndPort publicEndpoint = portMapping.getPublicEndpoint();
+        Location location = portMapping.getTarget();
+        int privatePort = portMapping.getPrivatePort();
+        return new AssociationMetadata(publicIpId, publicEndpoint, location, privatePort);
+    }
+    
+    ///////////////////////////////////////////////////////////////////////////////////
+    // Internal state, for generating memento
+    ///////////////////////////////////////////////////////////////////////////////////
+
+    public List<PortMapping> getPortMappings() {
+        synchronized (mutex) {
+            return ImmutableList.copyOf(mappings.values());
+        }
+    }
+    
+    public Map<String, Integer> getPortCounters() {
+        return ImmutableMap.of("global", portReserved.get());
+    }
+
+    
+    ///////////////////////////////////////////////////////////////////////////////////
+    // Deprecated
+    ///////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    @Deprecated
+    public PortMapping acquirePublicPortExplicit(String publicIpId, int port) {
+        PortMapping mapping = new PortMapping(publicIpId, port, null, -1);
+        log.debug("assigning explicit public port "+port+" at "+publicIpId);
+        PortMapping result;
+        synchronized (mutex) {
+            result = mappings.put(makeKey(publicIpId, port), mapping);
+        }
+        onChanged();
+        return result;
+    }
+
+    @Override
+    @Deprecated
+    public boolean forgetPortMapping(PortMapping m) {
+        return forgetPortMapping(m.publicIpId, m.publicPort);
+    }
+
+    @Override
+    @Deprecated
+    public void recordPublicIpHostname(String publicIpId, String hostnameOrPublicIpAddress) {
+        log.debug("recording public IP "+publicIpId+" associated with "+hostnameOrPublicIpAddress);
+        synchronized (mutex) {
+            String old = publicIpIdToHostname.put(publicIpId, hostnameOrPublicIpAddress);
+            if (old!=null && !old.equals(hostnameOrPublicIpAddress))
+                log.warn("Changing hostname recorded against public IP "+publicIpId+"; from "+old+" to "+hostnameOrPublicIpAddress);
+        }
+        onChanged();
+    }
+
+    @Override
+    @Deprecated
+    public String getPublicIpHostname(String publicIpId) {
+        synchronized (mutex) {
+            return publicIpIdToHostname.get(publicIpId);
+        }
+    }
+    
+    @Override
+    @Deprecated
+    public boolean forgetPublicIpHostname(String publicIpId) {
+        log.debug("forgetting public IP "+publicIpId+" association");
+        boolean result;
+        synchronized (mutex) {
+            result = (publicIpIdToHostname.remove(publicIpId) != null);
+        }
+        onChanged();
+        return result;
+    }
+
+    @Override
+    @Deprecated
+    public int acquirePublicPort(String publicIpId, Location l, int privatePort) {
+        int publicPort;
+        synchronized (mutex) {
+            PortMapping old = getPortMappingWithPrivateSide(l, privatePort);
+            // only works for 1 public IP ID per location (which is the norm)
+            if (old!=null && old.publicIpId.equals(publicIpId)) {
+                log.debug("request to acquire public port at "+publicIpId+" for "+l+":"+privatePort+", reusing old assignment "+old);
+                return old.getPublicPort();
+            }
+            
+            publicPort = acquirePublicPort(publicIpId);
+            log.debug("request to acquire public port at "+publicIpId+" for "+l+":"+privatePort+", allocating "+publicPort);
+            associateImpl(publicIpId, publicPort, l, privatePort);
+        }
+        onChanged();
+        return publicPort;
+    }
+
+    @Override
+    @Deprecated
+    public void associate(String publicIpId, int publicPort, Location l, int privatePort) {
+        synchronized (mutex) {
+            associateImpl(publicIpId, publicPort, l, privatePort);
+        }
+        onChanged();
+    }
+
+    protected void associateImpl(String publicIpId, int publicPort, Location l, int privatePort) {
+        synchronized (mutex) {
+            PortMapping mapping = new PortMapping(publicIpId, publicPort, l, privatePort);
+            PortMapping oldMapping = getPortMappingWithPublicSide(publicIpId, publicPort);
+            log.debug("associating public port "+publicPort+" on "+publicIpId+" with private port "+privatePort+" at "+l+" ("+mapping+")"
+                    +(oldMapping == null ? "" : " (overwriting "+oldMapping+" )"));
+            mappings.put(makeKey(publicIpId, publicPort), mapping);
+        }
+    }
+
+    ///////////////////////////////////////////////////////////////////////////////////
+    // Internal only; make protected when deprecated interface method removed
+    ///////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public HostAndPort getPublicHostAndPort(PortMapping m) {
+        if (m.publicEndpoint == null) {
+            String hostname = getPublicIpHostname(m.publicIpId);
+            if (hostname==null)
+                throw new IllegalStateException("No public hostname associated with "+m.publicIpId+" (mapping "+m+")");
+            return HostAndPort.fromParts(hostname, m.publicPort);
+        } else {
+            return m.publicEndpoint;
+        }
+    }
+
+    @Override
+    public PortMapping getPortMappingWithPublicSide(String publicIpId, int publicPort) {
+        synchronized (mutex) {
+            return mappings.get(makeKey(publicIpId, publicPort));
+        }
+    }
+
+    @Override
+    public Collection<PortMapping> getPortMappingWithPublicIpId(String publicIpId) {
+        List<PortMapping> result = new ArrayList<PortMapping>();
+        synchronized (mutex) {
+            for (PortMapping m: mappings.values())
+                if (publicIpId.equals(m.publicIpId)) result.add(m);
+        }
+        return result;
+    }
+
+    /** returns the subset of port mappings associated with a given location */
+    @Override
+    public Collection<PortMapping> getLocationPublicIpIds(Location l) {
+        List<PortMapping> result = new ArrayList<PortMapping>();
+        synchronized (mutex) {
+            for (PortMapping m: mappings.values())
+                if (l.equals(m.getTarget())) result.add(m);
+        }
+        return result;
+    }
+
+    @Override
+    public PortMapping getPortMappingWithPrivateSide(Location l, int privatePort) {
+        synchronized (mutex) {
+            for (PortMapping m: mappings.values())
+                if (l.equals(m.getTarget()) && privatePort==m.privatePort) return m;
+        }
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerLocationResolver.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerLocationResolver.java b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerLocationResolver.java
new file mode 100644
index 0000000..3a52877
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/access/PortForwardManagerLocationResolver.java
@@ -0,0 +1,89 @@
+/*
+ * 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.Map;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationRegistry;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.core.location.AbstractLocationResolver;
+import org.apache.brooklyn.core.location.LocationConfigUtils;
+import org.apache.brooklyn.core.location.LocationPredicates;
+import org.apache.brooklyn.core.location.internal.LocationInternal;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+
+public class PortForwardManagerLocationResolver extends AbstractLocationResolver {
+
+    private static final Logger LOG = LoggerFactory.getLogger(PortForwardManagerLocationResolver.class);
+
+    public static final String PREFIX = "portForwardManager";
+
+    @Override
+    public String getPrefix() {
+        return PREFIX;
+    }
+
+    @Override
+    public Location newLocationFromString(Map locationFlags, String spec, LocationRegistry registry) {
+        ConfigBag config = extractConfig(locationFlags, spec, registry);
+        Map globalProperties = registry.getProperties();
+        String namedLocation = (String) locationFlags.get(LocationInternal.NAMED_SPEC_NAME.getName());
+        String scope = config.get(PortForwardManager.SCOPE);
+
+        Optional<Location> result = Iterables.tryFind(managementContext.getLocationManager().getLocations(), 
+                Predicates.and(
+                        Predicates.instanceOf(PortForwardManager.class), 
+                        LocationPredicates.configEqualTo(PortForwardManager.SCOPE, scope)));
+        
+        if (result.isPresent()) {
+            return result.get();
+        } else {
+            PortForwardManager loc = managementContext.getLocationManager().createLocation(LocationSpec.create(PortForwardManagerImpl.class)
+                    .configure(config.getAllConfig())
+                    .configure(LocationConfigUtils.finalAndOriginalSpecs(spec, locationFlags, globalProperties, namedLocation)));
+            
+            if (LOG.isDebugEnabled()) LOG.debug("Created "+loc+" for scope "+scope);
+            return loc;
+        }
+    }
+
+    @Override
+    protected Class<? extends Location> getLocationType() {
+        return PortForwardManager.class;
+    }
+
+    @Override
+    protected SpecParser getSpecParser() {
+        return new AbstractLocationResolver.SpecParser(getPrefix()).setExampleUsage("\"portForwardManager\" or \"portForwardManager(scope=global)\"");
+    }
+    
+    @Override
+    protected ConfigBag extractConfig(Map<?,?> locationFlags, String spec, LocationRegistry registry) {
+        ConfigBag config = super.extractConfig(locationFlags, spec, registry);
+        config.putAsStringKeyIfAbsent("name", "localhost");
+        return config;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/access/PortMapping.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/access/PortMapping.java b/core/src/main/java/org/apache/brooklyn/core/location/access/PortMapping.java
new file mode 100644
index 0000000..06d92cb
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/access/PortMapping.java
@@ -0,0 +1,101 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.api.location.Location;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Objects;
+import com.google.common.net.HostAndPort;
+
+public class PortMapping {
+
+    final String publicIpId;
+    final HostAndPort publicEndpoint;
+    final int publicPort;
+
+    final Location target;
+    final int privatePort;
+    // TODO CIDR's ?
+
+    public PortMapping(String publicIpId, HostAndPort publicEndpoint, Location target, int privatePort) {
+        this.publicIpId = checkNotNull(publicIpId, "publicIpId");
+        this.publicEndpoint = checkNotNull(publicEndpoint, "publicEndpoint");
+        this.publicPort = publicEndpoint.getPort();
+        this.target = target;
+        this.privatePort = privatePort;
+    }
+    
+    public PortMapping(String publicIpId, int publicPort, Location target, int privatePort) {
+        this.publicIpId = checkNotNull(publicIpId, "publicIpId");
+        this.publicEndpoint = null;
+        this.publicPort = publicPort;
+        this.target = target;
+        this.privatePort = privatePort;
+    }
+
+    // In a release after 0.7.0, this will no longer be @Nullable
+    @Beta
+    @Nullable
+    public HostAndPort getPublicEndpoint() {
+        return publicEndpoint;
+    }
+
+    public int getPublicPort() {
+        return publicPort;
+    }
+
+    public Location getTarget() {
+        return target;
+    }
+    
+    public int getPrivatePort() {
+        return privatePort;
+    }
+    
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("publicIpId", publicIpId+":"+publicPort)
+                .add("publicEndpoint", (publicEndpoint == null ? publicPort : publicEndpoint))
+                .add("targetLocation", target)
+                .add("targetPort", privatePort)
+                .toString();
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof PortMapping)) return false;
+        PortMapping opm = (PortMapping)obj;
+        return Objects.equal(publicIpId, opm.publicIpId) &&
+            Objects.equal(publicPort, opm.publicPort) &&
+            Objects.equal(target, opm.target) &&
+            Objects.equal(privatePort, opm.privatePort);
+    }
+    
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(publicIpId, publicPort, target, privatePort);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/cloud/AbstractAvailabilityZoneExtension.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/cloud/AbstractAvailabilityZoneExtension.java b/core/src/main/java/org/apache/brooklyn/core/location/cloud/AbstractAvailabilityZoneExtension.java
new file mode 100644
index 0000000..267f708
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/cloud/AbstractAvailabilityZoneExtension.java
@@ -0,0 +1,82 @@
+/*
+ * 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.cloud;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+@Beta
+public abstract class AbstractAvailabilityZoneExtension implements AvailabilityZoneExtension {
+
+    protected final ManagementContext managementContext;
+    protected final AtomicReference<List<Location>> subLocations = new AtomicReference<List<Location>>();
+    private final Object mutex = new Object();
+    
+    public AbstractAvailabilityZoneExtension(ManagementContext managementContext) {
+        this.managementContext = checkNotNull(managementContext, "managementContext");
+    }
+
+    @Override
+    public List<Location> getSubLocations(int max) {
+        List<Location> all = getAllSubLocations();
+        return all.subList(0, Math.min(max, all.size()));
+    }
+
+    @Override
+    public List<Location> getSubLocationsByName(Predicate<? super String> namePredicate, int max) {
+        List<Location> result = Lists.newArrayList();
+        List<Location> all = getAllSubLocations();
+        for (Location loc : all) {
+            if (isNameMatch(loc, namePredicate)) {
+                result.add(loc);
+            }
+        }
+        return Collections.<Location>unmodifiableList(result);
+    }
+
+    @Override
+    public List<Location> getAllSubLocations() {
+        synchronized (mutex) {
+            if (subLocations.get() == null) {
+                List<Location> result = doGetAllSubLocations();
+                subLocations.set(ImmutableList.copyOf(result));
+            }
+        }
+        return subLocations.get();
+    }
+
+    /**
+     * <strong>Note</strong> this method can be called while synchronized on {@link #mutex}.
+     */
+    // TODO bad pattern, as this will likely call alien code (such as asking cloud provider?!)
+    protected abstract List<Location> doGetAllSubLocations();
+
+    protected abstract boolean isNameMatch(Location loc, Predicate<? super String> namePredicate);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/cloud/AbstractCloudMachineProvisioningLocation.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/cloud/AbstractCloudMachineProvisioningLocation.java b/core/src/main/java/org/apache/brooklyn/core/location/cloud/AbstractCloudMachineProvisioningLocation.java
new file mode 100644
index 0000000..504033a
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/cloud/AbstractCloudMachineProvisioningLocation.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.cloud;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.core.location.AbstractLocation;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+
+public abstract class AbstractCloudMachineProvisioningLocation extends AbstractLocation
+implements MachineProvisioningLocation<MachineLocation>, CloudLocationConfig
+{
+   public AbstractCloudMachineProvisioningLocation() {
+      super();
+   }
+
+    /** typically wants at least ACCESS_IDENTITY and ACCESS_CREDENTIAL */
+    public AbstractCloudMachineProvisioningLocation(Map<?,?> conf) {
+        super(conf);
+    }
+
+    /** uses reflection to create an object of the same type, assuming a Map constructor;
+     * subclasses can extend and downcast the result */
+    @Override
+    public AbstractCloudMachineProvisioningLocation newSubLocation(Map<?,?> newFlags) {
+        return newSubLocation(getClass(), newFlags);
+    }
+
+    public AbstractCloudMachineProvisioningLocation newSubLocation(Class<? extends AbstractCloudMachineProvisioningLocation> type, Map<?,?> newFlags) {
+        // TODO should be able to use ConfigBag.newInstanceExtending; would require moving stuff around to api etc
+        // TODO was previously `return LocationCreationUtils.newSubLocation(newFlags, this)`; need to retest on CloudStack etc
+        return getManagementContext().getLocationManager().createLocation(LocationSpec.create(type)
+                .parent(this)
+                .configure(config().getLocalBag().getAllConfig()) // FIXME Should this just be inherited?
+                .configure(newFlags));
+    }
+    
+    @Override
+    public Map<String, Object> getProvisioningFlags(Collection<String> tags) {
+        if (tags.size() > 0) {
+            LOG.warn("Location {}, ignoring provisioning tags {}", this, tags);
+        }
+        return MutableMap.<String, Object>of();
+    }
+
+    // ---------------- utilities --------------------
+    
+    protected ConfigBag extractSshConfig(ConfigBag setup, ConfigBag alt) {
+        ConfigBag sshConfig = new ConfigBag();
+        
+        if (setup.containsKey(PASSWORD)) {
+            sshConfig.put(SshTool.PROP_PASSWORD, setup.get(PASSWORD));
+        } else if (alt.containsKey(PASSWORD)) {
+            sshConfig.put(SshTool.PROP_PASSWORD, alt.get(PASSWORD));
+        }
+        
+        if (setup.containsKey(PRIVATE_KEY_DATA)) {
+            sshConfig.put(SshTool.PROP_PRIVATE_KEY_DATA, setup.get(PRIVATE_KEY_DATA));
+        } else if (setup.containsKey(PRIVATE_KEY_FILE)) {
+            sshConfig.put(SshTool.PROP_PRIVATE_KEY_FILE, setup.get(PRIVATE_KEY_FILE));
+        } else if (alt.containsKey(PRIVATE_KEY_DATA)) {
+            sshConfig.put(SshTool.PROP_PRIVATE_KEY_DATA, alt.get(PRIVATE_KEY_DATA));
+        }
+        
+        if (setup.containsKey(PRIVATE_KEY_PASSPHRASE)) {
+            // NB: not supported in jclouds (but it is by our ssh tool)
+            sshConfig.put(SshTool.PROP_PRIVATE_KEY_PASSPHRASE, setup.get(PRIVATE_KEY_PASSPHRASE));
+        }
+
+        // TODO extract other SshTool properties ?
+        
+        return sshConfig;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/cloud/AvailabilityZoneExtension.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/cloud/AvailabilityZoneExtension.java b/core/src/main/java/org/apache/brooklyn/core/location/cloud/AvailabilityZoneExtension.java
new file mode 100644
index 0000000..657438d
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/cloud/AvailabilityZoneExtension.java
@@ -0,0 +1,54 @@
+/*
+ * 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.cloud;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.entity.group.DynamicCluster;
+import org.apache.brooklyn.location.multi.MultiLocation;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Predicate;
+
+/**
+ * For a location that has sub-zones within it (e.g. an AWS region has availability zones that can be
+ * mapped as sub-locations), this extension interface allows those to be accessed and used.
+ * For some well-known clouds, the availability zones are automatically set, although for others they may
+ * have to be configured explicitly. The "multi:(locs,...)" location descriptor (cf {@link MultiLocation}) allows
+ * this to be down at runtime.
+ * <p>
+ * Note that only entities which are explicitly aware of the {@link AvailabilityZoneExtension}
+ * will use availability zone information. For example {@link DynamicCluster} 
+ * <p>
+ * Implementers are strongly encouraged to extend {@link AbstractAvailabilityZoneExtension}
+ * which has useful behaviour, rather than attempt to implement this interface directly.
+ * 
+ * @since 0.6.0
+ */
+@Beta
+public interface AvailabilityZoneExtension {
+
+    List<Location> getAllSubLocations();
+
+    List<Location> getSubLocations(int max);
+
+    List<Location> getSubLocationsByName(Predicate<? super String> namePredicate, int max);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/cloud/CloudLocationConfig.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/cloud/CloudLocationConfig.java b/core/src/main/java/org/apache/brooklyn/core/location/cloud/CloudLocationConfig.java
new file mode 100644
index 0000000..37989f0
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/cloud/CloudLocationConfig.java
@@ -0,0 +1,116 @@
+/*
+ * 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.cloud;
+
+import java.util.Collection;
+
+import com.google.common.annotations.Beta;
+import com.google.common.reflect.TypeToken;
+
+import org.apache.brooklyn.api.location.MachineLocationCustomizer;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.BasicConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.location.LocationConfigKeys;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+public interface CloudLocationConfig {
+
+    public static final ConfigKey<String> CLOUD_ENDPOINT = LocationConfigKeys.CLOUD_ENDPOINT;
+    public static final ConfigKey<String> CLOUD_REGION_ID = LocationConfigKeys.CLOUD_REGION_ID;
+    public static final ConfigKey<String> CLOUD_AVAILABILITY_ZONE_ID = LocationConfigKeys.CLOUD_AVAILABILITY_ZONE_ID;
+        
+    @SetFromFlag("identity")
+    public static final ConfigKey<String> ACCESS_IDENTITY = LocationConfigKeys.ACCESS_IDENTITY;
+    @SetFromFlag("credential")
+    public static final ConfigKey<String> ACCESS_CREDENTIAL = LocationConfigKeys.ACCESS_CREDENTIAL;
+
+    public static final ConfigKey<String> USER = LocationConfigKeys.USER;
+    
+    public static final ConfigKey<String> PASSWORD = LocationConfigKeys.PASSWORD;
+    public static final ConfigKey<String> PUBLIC_KEY_FILE = LocationConfigKeys.PUBLIC_KEY_FILE;
+    public static final ConfigKey<String> PUBLIC_KEY_DATA = LocationConfigKeys.PUBLIC_KEY_DATA;
+    public static final ConfigKey<String> PRIVATE_KEY_FILE = LocationConfigKeys.PRIVATE_KEY_FILE;
+    public static final ConfigKey<String> PRIVATE_KEY_DATA = LocationConfigKeys.PRIVATE_KEY_DATA;
+    public static final ConfigKey<String> PRIVATE_KEY_PASSPHRASE = LocationConfigKeys.PRIVATE_KEY_PASSPHRASE;
+
+    /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated
+    public static final ConfigKey<String> LEGACY_PUBLIC_KEY_FILE = LocationConfigKeys.LEGACY_PUBLIC_KEY_FILE;
+    /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated
+    public static final ConfigKey<String> LEGACY_PUBLIC_KEY_DATA = LocationConfigKeys.LEGACY_PUBLIC_KEY_DATA;
+    /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated
+    public static final ConfigKey<String> LEGACY_PRIVATE_KEY_FILE = LocationConfigKeys.LEGACY_PRIVATE_KEY_FILE;
+    /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated
+    public static final ConfigKey<String> LEGACY_PRIVATE_KEY_DATA = LocationConfigKeys.LEGACY_PRIVATE_KEY_DATA;
+    /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated
+    public static final ConfigKey<String> LEGACY_PRIVATE_KEY_PASSPHRASE = LocationConfigKeys.LEGACY_PRIVATE_KEY_PASSPHRASE;
+
+    // default is just shy of common 64-char boundary, leaving 4 chars plus our salt allowance (default 4+1) which allows up to -12345678 by jclouds
+    public static final ConfigKey<Integer> VM_NAME_MAX_LENGTH = ConfigKeys.newIntegerConfigKey(
+        "vmNameMaxLength", "Maximum length of VM name", 60);
+
+    public static final ConfigKey<Integer> VM_NAME_SALT_LENGTH = ConfigKeys.newIntegerConfigKey(
+        "vmNameSaltLength", "Number of characters to use for a random identifier inserted in hostname "
+            + "to uniquely identify machines", 4);
+
+    public static final ConfigKey<String> WAIT_FOR_SSHABLE = ConfigKeys.newStringConfigKey("waitForSshable", 
+            "Whether and how long to wait for a newly provisioned VM to be accessible via ssh; " +
+            "if 'false', won't check; if 'true' uses default duration; otherwise accepts a time string e.g. '5m' (the default) or a number of milliseconds", "5m");
+
+    public static final ConfigKey<String> WAIT_FOR_WINRM_AVAILABLE = ConfigKeys.newStringConfigKey("waitForWinRmAvailable",
+            "Whether and how long to wait for a newly provisioned VM to be accessible via WinRm; " +
+                    "if 'false', won't check; if 'true' uses default duration; otherwise accepts a time string e.g. '30m' (the default) or a number of milliseconds", "30m");
+
+    public static final ConfigKey<Boolean> LOG_CREDENTIALS = ConfigKeys.newBooleanConfigKey(
+            "logCredentials", 
+            "Whether to log credentials of a new VM - strongly recommended never be used in production, as it is a big security hole!",
+            false);
+
+    public static final ConfigKey<Object> CALLER_CONTEXT = LocationConfigKeys.CALLER_CONTEXT;
+
+    public static final ConfigKey<Boolean> DESTROY_ON_FAILURE = ConfigKeys.newBooleanConfigKey("destroyOnFailure", "Whether to destroy the VM if provisioningLocation.obtain() fails", true);
+    
+    public static final ConfigKey<Object> INBOUND_PORTS = new BasicConfigKey<Object>(Object.class, "inboundPorts", 
+        "Inbound ports to be applied when creating a VM, on supported clouds " +
+            "(either a single port as a String, or an Iterable<Integer> or Integer[])", null);
+    @Beta
+    public static final ConfigKey<Object> ADDITIONAL_INBOUND_PORTS = new BasicConfigKey<Object>(Object.class, "required.ports", 
+            "Required additional ports to be applied when creating a VM, on supported clouds " +
+                    "(either a single port as an Integer, or an Iterable<Integer> or Integer[])", null);
+    
+    public static final ConfigKey<Boolean> OS_64_BIT = ConfigKeys.newBooleanConfigKey("os64Bit", 
+        "Whether to require 64-bit OS images (true), 32-bit images (false), or either (null)");
+    
+    public static final ConfigKey<Object> MIN_RAM = new BasicConfigKey<Object>(Object.class, "minRam",
+        "Minimum amount of RAM, either as string (4gb) or number of MB (4096), for use in selecting the machine/hardware profile", null);
+    
+    public static final ConfigKey<Integer> MIN_CORES = new BasicConfigKey<Integer>(Integer.class, "minCores",
+        "Minimum number of cores, for use in selecting the machine/hardware profile", null);
+    
+    public static final ConfigKey<Object> MIN_DISK = new BasicConfigKey<Object>(Object.class, "minDisk",
+        "Minimum size of disk, either as string (100gb) or number of GB (100), for use in selecting the machine/hardware profile", null);
+
+    public static final ConfigKey<String> DOMAIN_NAME = new BasicConfigKey<String>(String.class, "domainName",
+        "DNS domain where the host should be created, e.g. yourdomain.com (selected clouds only)", null);
+
+    @SuppressWarnings("serial")
+    public static final ConfigKey<Collection<MachineLocationCustomizer>> MACHINE_LOCATION_CUSTOMIZERS = ConfigKeys.newConfigKey(
+            new TypeToken<Collection<MachineLocationCustomizer>>() {},
+            "machineCustomizers", "Optional machine customizers");
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/AbstractCloudMachineNamer.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/AbstractCloudMachineNamer.java b/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/AbstractCloudMachineNamer.java
new file mode 100644
index 0000000..7f38964
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/AbstractCloudMachineNamer.java
@@ -0,0 +1,150 @@
+/*
+ * 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.cloud.names;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.objs.HasShortName;
+import org.apache.brooklyn.core.location.cloud.CloudLocationConfig;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.CharMatcher;
+
+/** 
+ * Implements <b>most</b> of {@link CloudMachineNamer},
+ * leaving just one method -- {@link #generateNewIdOfLength(int)} --
+ * for subclasses to provide.
+ * <p>
+ * {@link CloudLocationConfig#VM_NAME_MAX_LENGTH} is used to find the VM length, 
+ * unless {@link #getCustomMaxNameLength(ConfigBag)} is overridden or
+ * {@link #setDefaultMachineNameMaxLength(int)} invoked on the instance supplied.
+ */
+public abstract class AbstractCloudMachineNamer implements CloudMachineNamer {
+
+    int defaultMachineNameMaxLength = CloudLocationConfig.VM_NAME_MAX_LENGTH.getDefaultValue();
+    int defaultMachineNameSaltLength = CloudLocationConfig.VM_NAME_SALT_LENGTH.getDefaultValue();
+    protected String separator = "-";
+
+    public String generateNewMachineUniqueName(ConfigBag setup) {
+        return generateNewIdReservingLength(setup, 0);
+    }
+    
+    public String generateNewMachineUniqueNameFromGroupId(ConfigBag setup, String groupId) {
+        int availSaltLength = getMaxNameLength(setup) - (groupId.length() + separator.length());
+        int requestedSaltLength = getLengthForMachineUniqueNameSalt(setup, false);
+        if (availSaltLength <= 0 || requestedSaltLength <= 0) {
+            return groupId;
+        }
+            
+        return sanitize(groupId + separator + Identifiers.makeRandomId(Math.min(requestedSaltLength, availSaltLength))).toLowerCase();
+    }
+
+    public String generateNewGroupId(ConfigBag setup) {
+        return sanitize(generateNewIdReservingLength(setup, getLengthForMachineUniqueNameSalt(setup, true))).toLowerCase();
+    }
+
+    protected String generateNewIdReservingLength(ConfigBag setup, int lengthToReserve) {
+        int len = getMaxNameLength(setup);
+        // decrement by e.g. 9 chars because jclouds adds that (dash plus 8 for hex id)
+        len -= lengthToReserve;
+        if (len<=0) return "";
+        return Strings.maxlen(generateNewIdOfLength(setup, len), len);
+    }
+    
+    /** Method for subclasses to provide to construct the context-specific part of an identifier,
+     * for use in {@link #generateNewGroupId()} and {@link #generateNewMachineUniqueName()}.
+     * 
+     * @param maxLengthHint an indication of the maximum length permitted for the ID generated,
+     * supplied for implementations which wish to use this information to decide what to truncate.
+     * (This class will truncate any return values longer than this.) 
+     */
+    protected abstract String generateNewIdOfLength(ConfigBag setup, int maxLengthHint);
+
+    /** Returns the max length of a VM name for the cloud specified in setup;
+     * this value is typically decremented by 9 to make room for jclouds labels;
+     * delegates to {@link #getCustomMaxNameLength()} when 
+     * {@link CloudLocationConfig#VM_NAME_MAX_LENGTH} is not set */
+    public int getMaxNameLength(ConfigBag setup) {
+        if (setup.containsKey(CloudLocationConfig.VM_NAME_MAX_LENGTH)) {
+            // if a length is set explicitly, use that (but intercept default behaviour)
+            return setup.get(CloudLocationConfig.VM_NAME_MAX_LENGTH);
+        }
+        
+        Integer custom = getCustomMaxNameLength(setup);
+        if (custom!=null) return custom;
+        
+        // return the default
+        return defaultMachineNameMaxLength;  
+    }
+    
+    // sometimes we create salt string, sometimes jclouds does
+    public int getLengthForMachineUniqueNameSalt(ConfigBag setup, boolean includeSeparator) {
+        int saltLen;
+        if (setup.containsKey(CloudLocationConfig.VM_NAME_SALT_LENGTH)) {
+            saltLen = setup.get(CloudLocationConfig.VM_NAME_SALT_LENGTH);
+        } else {
+            // default value comes from key, but custom default can be set
+            saltLen = defaultMachineNameSaltLength;
+        }
+        
+        if (saltLen>0 && includeSeparator)
+            saltLen += separator.length();
+        
+        return saltLen;
+    }
+    
+    public AbstractCloudMachineNamer setDefaultMachineNameMaxLength(int defaultMaxLength) {
+        this.defaultMachineNameMaxLength = defaultMaxLength;
+        return this;
+    }
+
+    /** Number of chars to use or reserve for the machine identifier when constructing a group identifier;
+     * jclouds for instance uses "-" plus 8 */
+    public AbstractCloudMachineNamer setDefaultMachineNameSeparatorAndSaltLength(String separator, int defaultMachineUniqueNameSaltLength) {
+        this.separator = separator;
+        this.defaultMachineNameSaltLength = defaultMachineUniqueNameSaltLength;
+        return this;
+    }
+    
+    /** Method for overriding to provide custom logic when an explicit config key is not set for the machine length. */
+    public Integer getCustomMaxNameLength(ConfigBag setup) {
+        return null;
+    }
+
+    protected static String shortName(Object x) {
+        if (x instanceof HasShortName) {
+            return ((HasShortName)x).getShortName();
+        }
+        if (x instanceof Entity) {
+            return ((Entity)x).getDisplayName();
+        }
+        return x.toString();
+    }
+
+    @Beta //probably won't live here long-term
+    public static String sanitize(String s) {
+        return CharMatcher.inRange('A', 'Z')
+                .or(CharMatcher.inRange('a', 'z'))
+                .or(CharMatcher.inRange('0', '9'))
+                .negate()
+                .trimAndCollapseFrom(s, '-');
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/BasicCloudMachineNamer.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/BasicCloudMachineNamer.java b/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/BasicCloudMachineNamer.java
new file mode 100644
index 0000000..4777ad4
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/BasicCloudMachineNamer.java
@@ -0,0 +1,91 @@
+/*
+ * 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.cloud.names;
+
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.core.location.cloud.CloudLocationConfig;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.StringShortener;
+import org.apache.brooklyn.util.text.Strings;
+
+/** 
+ * Standard implementation of {@link CloudMachineNamer},
+ * which looks at several of the properties of the context (entity)
+ * and is clever about abbreviating them. */
+public class BasicCloudMachineNamer extends AbstractCloudMachineNamer {
+
+    @Override
+    protected String generateNewIdOfLength(ConfigBag setup, int len) {
+        Object context = setup.peek(CloudLocationConfig.CALLER_CONTEXT);
+        Entity entity = null;
+        if (context instanceof Entity) entity = (Entity) context;
+        
+        StringShortener shortener = Strings.shortener().separator("-");
+        shortener.append("system", "brooklyn");
+        
+        // randId often not necessary, as an 8-char hex identifier is added later (in jclouds? can we override?)
+        // however it can be useful to have this early in the string, to prevent collisions in places where it is abbreviated 
+        shortener.append("randId", Identifiers.makeRandomId(4));
+        
+        String user = System.getProperty("user.name");
+        if (!"brooklyn".equals(user))
+            // include user; unless the user is 'brooklyn', as 'brooklyn-brooklyn-' is just silly!
+            shortener.append("user", user);
+        
+        if (entity!=null) {
+            Application app = entity.getApplication();
+            if (app!=null) {
+                shortener.append("app", shortName(app))
+                        .append("appId", app.getId());
+            }
+            shortener.append("entity", shortName(entity))
+                    .append("entityId", entity.getId());
+        } else if (context!=null) {
+            shortener.append("context", context.toString());
+        }
+        
+        shortener.truncate("user", 12)
+                .truncate("app", 16)
+                .truncate("entity", 16)
+                .truncate("appId", 4)
+                .truncate("entityId", 4)
+                .truncate("context", 12);
+        
+        shortener.canTruncate("user", 8)
+                .canTruncate("app", 5)
+                .canTruncate("entity", 5)
+                .canTruncate("system", 2)
+                .canTruncate("app", 3)
+                .canTruncate("entity", 3)
+                .canRemove("app")
+                .canTruncate("user", 4)
+                .canRemove("entity")
+                .canTruncate("context", 4)
+                .canTruncate("randId", 2)
+                .canRemove("user")
+                .canTruncate("appId", 2)
+                .canRemove("appId");
+        
+        String s = shortener.getStringOfMaxLength(len);
+        return sanitize(s).toLowerCase();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/CloudMachineNamer.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/CloudMachineNamer.java b/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/CloudMachineNamer.java
new file mode 100644
index 0000000..d963a4e
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/CloudMachineNamer.java
@@ -0,0 +1,61 @@
+/*
+ * 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.cloud.names;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.location.cloud.CloudLocationConfig;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+
+/**
+ * Interface used to construct names for individual cloud machines and for groups of machines.
+ * <p>
+ * Implementations <b>must</b> provide a constructor which takes a single argument,
+ * being the {@link ConfigBag} for the context where the machine is being created
+ * (usually a {@link Location}).
+ * <p>
+ * With that bag, the config key {@link CloudLocationConfig#CALLER_CONTEXT}
+ * typically contains the {@link Entity} for which the machine is being created.   
+ */
+public interface CloudMachineNamer {
+
+    /**
+     * Generate a name for a new machine, based on context.
+     * <p>
+     * The name should normally be unique, as a context might produce multiple machines,
+     * for example basing it partially on information from the context but also including some random salt.
+     */
+    public String generateNewMachineUniqueName(ConfigBag setup);
+    /**
+     * Generate a name stem for a group of machines, based on context.
+     * <p>
+     * The name does not need to be unique, as uniqueness will be applied by {@link #generateNewMachineUniqueNameFromGroupId(String)}.
+     */
+    public String generateNewGroupId(ConfigBag setup);
+    
+    /**
+     * Generate a unique name from the given name stem.
+     * <p>
+     * The name stem is normally based on context information so the usual
+     * function of this method is to apply a suffix which helps to uniquely distinguish between machines
+     * in cases where the same name stem ({@link #generateNewGroupId()}) is used for multiple machines.
+     */
+    public String generateNewMachineUniqueNameFromGroupId(ConfigBag setup, String groupId);
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/CustomMachineNamer.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/CustomMachineNamer.java b/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/CustomMachineNamer.java
new file mode 100644
index 0000000..0111f99
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/cloud/names/CustomMachineNamer.java
@@ -0,0 +1,72 @@
+/*
+ * 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.cloud.names;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.core.location.cloud.CloudLocationConfig;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.text.TemplateProcessor;
+import org.apache.brooklyn.util.text.Strings;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.reflect.TypeToken;
+
+/** Provides a machine namer which looks at a location config key {@link #MACHINE_NAME_TEMPLATE}
+ * to construct the hostname.
+ * For instance, setting this to <code>${config.entity_hostname}</code>
+ * will take the hostname from an <code>entity_hostname</code> key passed as entity <code>brooklyn.config</code>.
+ * <p>
+ * Note that this is not jclouds aware, so jclouds-specific cloud max lengths are not observed with this class.
+ */
+public class CustomMachineNamer extends BasicCloudMachineNamer {
+    
+    public static final ConfigKey<String> MACHINE_NAME_TEMPLATE = ConfigKeys.newStringConfigKey("custom.machine.namer.machine", 
+            "Freemarker template format for custom machine name", "${entity.displayName}");
+    @SuppressWarnings("serial")
+    public static final ConfigKey<Map<String, ?>> EXTRA_SUBSTITUTIONS = ConfigKeys.newConfigKey(new TypeToken<Map<String, ?>>() {}, 
+            "custom.machine.namer.substitutions", "Additional substitutions to be used in the template", ImmutableMap.<String, Object>of());
+    
+    @Override
+    protected String generateNewIdOfLength(ConfigBag setup, int len) {
+        Object context = setup.peek(CloudLocationConfig.CALLER_CONTEXT);
+        Entity entity = null;
+        if (context instanceof Entity) {
+            entity = (Entity) context;
+        }
+        
+        String template = setup.get(MACHINE_NAME_TEMPLATE);
+        
+        String processed;
+        if (entity == null) {
+            processed = TemplateProcessor.processTemplateContents(template, setup.get(EXTRA_SUBSTITUTIONS));
+        } else {
+            processed = TemplateProcessor.processTemplateContents(template, (EntityInternal)entity, setup.get(EXTRA_SUBSTITUTIONS));
+        }
+        
+        processed = Strings.removeFromStart(processed, "#ftl\n");
+        
+        return sanitize(processed);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/538324e1/core/src/main/java/org/apache/brooklyn/core/location/dynamic/DynamicLocation.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/dynamic/DynamicLocation.java b/core/src/main/java/org/apache/brooklyn/core/location/dynamic/DynamicLocation.java
new file mode 100644
index 0000000..b04ebac
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/location/dynamic/DynamicLocation.java
@@ -0,0 +1,50 @@
+/*
+ * 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.dynamic;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * A location that is created and owned by an entity at runtime.
+ * <p>
+ * The lifecycle of the location is managed by the owning entity.
+ *
+ * @param E the entity type
+ * @param L the location type
+ */
+@Beta
+public interface DynamicLocation<E extends Entity & LocationOwner<L, E>, L extends Location & DynamicLocation<E, L>> {
+
+    @SetFromFlag("owner")
+    ConfigKey<Entity> OWNER =
+            ConfigKeys.newConfigKey(Entity.class, "owner", "The entity owning this location");
+
+    @SetFromFlag("maxLocations")
+    ConfigKey<Integer> MAX_SUB_LOCATIONS =
+            ConfigKeys.newIntegerConfigKey("maxLocations", "The maximum number of sub-locations that can be created; 0 for unlimited", 0);
+
+    E getOwner();
+
+}


Mime
View raw message