http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/WinRmMachineLocation.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/basic/WinRmMachineLocation.java
 
b/core/src/main/java/org/apache/brooklyn/location/basic/WinRmMachineLocation.java
new file mode 100644
index 0000000..3860eb5
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/basic/WinRmMachineLocation.java
@@ -0,0 +1,360 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.basic;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.location.MachineDetails;
+import org.apache.brooklyn.location.OsDetails;
+import org.apache.commons.codec.binary.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.net.HostAndPort;
+import com.google.common.reflect.TypeToken;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import org.apache.brooklyn.location.MachineLocation;
+import org.apache.brooklyn.location.access.PortForwardManager;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.stream.Streams;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+import io.cloudsoft.winrm4j.winrm.WinRmTool;
+import io.cloudsoft.winrm4j.winrm.WinRmToolResponse;
+
+public class WinRmMachineLocation extends AbstractLocation implements 
MachineLocation {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(WinRmMachineLocation.class);
+
+    // FIXME Respect `port` config when using {@link WinRmTool}
+    public static final ConfigKey<Integer> WINRM_PORT = 
ConfigKeys.newIntegerConfigKey(
+            "port",
+            "WinRM port to use when connecting to the remote machine",
+            5985);
+    
+    // TODO merge with {link SshTool#PROP_USER} and {@link 
SshMachineLocation#user}
+    public static final ConfigKey<String> USER = 
ConfigKeys.newStringConfigKey("user",
+            "Username to use when connecting to the remote machine");
+
+    // TODO merge with {link SshTool#PROP_PASSWORD}
+    public static final ConfigKey<String> PASSWORD = 
ConfigKeys.newStringConfigKey("password",
+            "Password to use when connecting to the remote machine");
+
+    public static final ConfigKey<Integer> COPY_FILE_CHUNK_SIZE_BYTES = 
ConfigKeys.newIntegerConfigKey("windows.copy.file.size.bytes",
+            "Size of file chunks (in bytes) to be used when copying a file to 
the remote server", 1024);
+
+     public static final ConfigKey<InetAddress> ADDRESS = 
ConfigKeys.newConfigKey(
+            InetAddress.class,
+            "address",
+            "Address of the remote machine");
+
+    public static final ConfigKey<Integer> EXECUTION_ATTEMPTS = 
ConfigKeys.newIntegerConfigKey(
+            "windows.exec.attempts",
+            "Number of attempts to execute a remote command",
+            1);
+    
+    // TODO See SshTool#PROP_SSH_TRIES, where it was called "sshTries"; remove 
duplication? Merge into one well-named thing?
+    public static final ConfigKey<Integer> EXEC_TRIES = 
ConfigKeys.newIntegerConfigKey(
+            "execTries", 
+            "Max number of times to attempt WinRM operations", 
+            10);
+
+    public static final ConfigKey<Iterable<String>> PRIVATE_ADDRESSES = 
ConfigKeys.newConfigKey(
+            new TypeToken<Iterable<String>>() {},
+            "privateAddresses",
+            "Private addresses of this machine, e.g. those within the private 
network", 
+            null);
+
+    public static final ConfigKey<Map<Integer, String>> TCP_PORT_MAPPINGS = 
ConfigKeys.newConfigKey(
+            new TypeToken<Map<Integer, String>>() {},
+            "tcpPortMappings",
+            "NAT'ed ports, giving the mapping from private TCP port to a 
public host:port", 
+            null);
+
+    @Override
+    public InetAddress getAddress() {
+        return getConfig(ADDRESS);
+    }
+
+    @Override
+    public OsDetails getOsDetails() {
+        return null;
+    }
+
+    @Override
+    public MachineDetails getMachineDetails() {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public String getHostname() {
+        InetAddress address = getAddress();
+        return (address != null) ? address.getHostAddress() : null;
+    }
+
+    @Nullable
+    protected String getHostAndPort() {
+        String host = getHostname();
+        return (host == null) ? null : host + ":" + config().get(WINRM_PORT);
+    }
+
+    @Override
+    public Set<String> getPublicAddresses() {
+        InetAddress address = getAddress();
+        return (address == null) ? ImmutableSet.<String>of() : 
ImmutableSet.of(address.getHostAddress());
+    }
+    
+    @Override
+    public Set<String> getPrivateAddresses() {
+        Iterable<String> result = getConfig(PRIVATE_ADDRESSES);
+        return (result == null) ? ImmutableSet.<String>of() : 
ImmutableSet.copyOf(result);
+    }
+
+    public WinRmToolResponse executeScript(String script) {
+        return executeScript(ImmutableList.of(script));
+    }
+
+    public WinRmToolResponse executeScript(List<String> script) {
+        int execTries = getRequiredConfig(EXEC_TRIES);
+        Collection<Throwable> exceptions = Lists.newArrayList();
+        for (int i = 0; i < execTries; i++) {
+            try {
+                return executeScriptNoRetry(script);
+            } catch (Exception e) {
+                Exceptions.propagateIfFatal(e);
+                if (i == (execTries+1)) {
+                    LOG.info("Propagating WinRM exception (attempt "+(i+1)+" 
of "+execTries+")", e);
+                } else if (i == 0) {
+                    LOG.warn("Ignoring WinRM exception and retrying (attempt 
"+(i+1)+" of "+execTries+")", e);
+                } else {
+                    LOG.debug("Ignoring WinRM exception and retrying (attempt 
"+(i+1)+" of "+execTries+")", e);
+                }
+                exceptions.add(e);
+            }
+        }
+        throw Exceptions.propagate("failed to execute shell script", 
exceptions);
+    }
+
+    protected WinRmToolResponse executeScriptNoRetry(List<String> script) {
+        WinRmTool winRmTool = WinRmTool.connect(getHostAndPort(), getUser(), 
getPassword());
+        WinRmToolResponse response = winRmTool.executeScript(script);
+        return response;
+    }
+
+    public WinRmToolResponse executePsScript(String psScript) {
+        return executePsScript(ImmutableList.of(psScript));
+    }
+
+    public WinRmToolResponse executePsScript(List<String> psScript) {
+        int execTries = getRequiredConfig(EXEC_TRIES);
+        Collection<Throwable> exceptions = Lists.newArrayList();
+        for (int i = 0; i < execTries; i++) {
+            try {
+                return executePsScriptNoRetry(psScript);
+            } catch (Exception e) {
+                Exceptions.propagateIfFatal(e);
+                if (i == (execTries+1)) {
+                    LOG.info("Propagating WinRM exception (attempt "+(i+1)+" 
of "+execTries+")", e);
+                } else if (i == 0) {
+                    LOG.warn("Ignoring WinRM exception and retrying after 5 
seconds (attempt "+(i+1)+" of "+execTries+")", e);
+                    Time.sleep(Duration.FIVE_SECONDS);
+                } else {
+                    LOG.debug("Ignoring WinRM exception and retrying after 5 
seconds (attempt "+(i+1)+" of "+execTries+")", e);
+                    Time.sleep(Duration.FIVE_SECONDS);
+                }
+                exceptions.add(e);
+            }
+        }
+        throw Exceptions.propagate("failed to execute powershell script", 
exceptions);
+    }
+
+    public WinRmToolResponse executePsScriptNoRetry(List<String> psScript) {
+        WinRmTool winRmTool = WinRmTool.connect(getHostAndPort(), getUser(), 
getPassword());
+        WinRmToolResponse response = winRmTool.executePs(psScript);
+        return response;
+    }
+
+    public int copyTo(File source, String destination) {
+        FileInputStream sourceStream = null;
+        try {
+            sourceStream = new FileInputStream(source);
+            return copyTo(sourceStream, destination);
+        } catch (FileNotFoundException e) {
+            throw Exceptions.propagate(e);
+        } finally {
+            if (sourceStream != null) {
+                Streams.closeQuietly(sourceStream);
+            }
+        }
+    }
+
+    public int copyTo(InputStream source, String destination) {
+        executePsScript(ImmutableList.of("rm -ErrorAction SilentlyContinue " + 
destination));
+        try {
+            int chunkSize = getConfig(COPY_FILE_CHUNK_SIZE_BYTES);
+            byte[] inputData = new byte[chunkSize];
+            int bytesRead;
+            int expectedFileSize = 0;
+            while ((bytesRead = source.read(inputData)) > 0) {
+                byte[] chunk;
+                if (bytesRead == chunkSize) {
+                    chunk = inputData;
+                } else {
+                    chunk = Arrays.copyOf(inputData, bytesRead);
+                }
+                executePsScript(ImmutableList.of("If ((!(Test-Path " + 
destination + ")) -or ((Get-Item '" + destination + "').length -eq " +
+                        expectedFileSize + ")) {Add-Content -Encoding Byte 
-path " + destination +
+                        " -value ([System.Convert]::FromBase64String(\"" + new 
String(Base64.encodeBase64(chunk)) + "\"))}"));
+                expectedFileSize += bytesRead;
+            }
+
+            return 0;
+        } catch (java.io.IOException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    @Override
+    public void init() {
+        super.init();
+
+        // Register any pre-existing port-mappings with the PortForwardManager
+        Map<Integer, String> tcpPortMappings = getConfig(TCP_PORT_MAPPINGS);
+        if (tcpPortMappings != null) {
+            PortForwardManager pfm = (PortForwardManager) 
getManagementContext().getLocationRegistry().resolve("portForwardManager(scope=global)");
+            for (Map.Entry<Integer, String> entry : 
tcpPortMappings.entrySet()) {
+                int targetPort = entry.getKey();
+                HostAndPort publicEndpoint = 
HostAndPort.fromString(entry.getValue());
+                if (!publicEndpoint.hasPort()) {
+                    throw new IllegalArgumentException("Invalid portMapping 
('"+entry.getValue()+"') for port "+targetPort+" in machine "+this);
+                }
+                pfm.associate(publicEndpoint.getHostText(), publicEndpoint, 
this, targetPort);
+            }
+        }
+    }
+    public String getUser() {
+        return config().get(USER);
+    }
+
+    private String getPassword() {
+        return config().get(PASSWORD);
+    }
+
+    private <T> T getRequiredConfig(ConfigKey<T> key) {
+        return checkNotNull(getConfig(key), "key %s must be set", key);
+    }
+    
+    public static String getDefaultUserMetadataString() {
+        // Using an encoded command obviates the need to escape
+        String unencodePowershell = Joiner.on("\r\n").join(ImmutableList.of(
+                // Allow TS connections
+                "$RDP = Get-WmiObject -Class Win32_TerminalServiceSetting 
-ComputerName $env:computername -Namespace root\\CIMV2\\TerminalServices 
-Authentication PacketPrivacy",
+                "$RDP.SetAllowTSConnections(1,1)",
+                "Set-ExecutionPolicy Unrestricted -Force",
+                // Set unlimited values for remote execution limits
+                "Set-Item WSMan:\\localhost\\Shell\\MaxConcurrentUsers 100",
+                "Set-Item WSMan:\\localhost\\Shell\\MaxMemoryPerShellMB 0",
+                "Set-Item WSMan:\\localhost\\Shell\\MaxProcessesPerShell 0",
+                "Set-Item WSMan:\\localhost\\Shell\\MaxShellsPerUser 0",
+                "New-ItemProperty 
\"HKLM:\\System\\CurrentControlSet\\Control\\LSA\" -Name 
\"SuppressExtendedProtection\" -Value 1 -PropertyType \"DWord\"",
+                // The following allows scripts to re-authenticate with local 
credential - this is required
+                // as certain operations cannot be performed with remote 
credentials
+                "$allowed = @('WSMAN/*')",
+                "$key = 
'hklm:\\SOFTWARE\\Policies\\Microsoft\\Windows\\CredentialsDelegation'",
+                "if (!(Test-Path $key)) {",
+                "    md $key",
+                "}",
+                "New-ItemProperty -Path $key -Name AllowFreshCredentials 
-Value 1 -PropertyType Dword -Force",
+                "New-ItemProperty -Path $key -Name 
AllowFreshCredentialsWhenNTLMOnly -Value 1 -PropertyType Dword -Force",
+                "$credKey = Join-Path $key 'AllowFreshCredentials'",
+                "if (!(Test-Path $credKey)) {",
+                "    md $credkey",
+                "}",
+                "$ntlmKey = Join-Path $key 
'AllowFreshCredentialsWhenNTLMOnly'",
+                "if (!(Test-Path $ntlmKey)) {",
+                "    md $ntlmKey",
+                "}",
+                "$i = 1",
+                "$allowed |% {",
+                "    # Script does not take into account existing entries in 
this key",
+                "    New-ItemProperty -Path $credKey -Name $i -Value $_ 
-PropertyType String -Force",
+                "    New-ItemProperty -Path $ntlmKey -Name $i -Value $_ 
-PropertyType String -Force",
+                "    $i++",
+                "}"
+        ));
+
+        String encoded = new 
String(Base64.encodeBase64(unencodePowershell.getBytes(Charsets.UTF_16LE)));
+        return "winrm quickconfig -q & " +
+                "winrm set winrm/config/service/auth @{Basic=\"true\"} & " +
+                "winrm set winrm/config/service/auth @{CredSSP=\"true\"} & " +
+                "winrm set winrm/config/client/auth @{CredSSP=\"true\"} & " +
+                "winrm set winrm/config/client @{AllowUnencrypted=\"true\"} & 
" +
+                "winrm set winrm/config/service @{AllowUnencrypted=\"true\"} & 
" +
+                "winrm set winrm/config/winrs @{MaxConcurrentUsers=\"100\"} & 
" +
+                "winrm set winrm/config/winrs @{MaxMemoryPerShellMB=\"0\"} & " 
+
+                "winrm set winrm/config/winrs @{MaxProcessesPerShell=\"0\"} & 
" +
+                "winrm set winrm/config/winrs @{MaxShellsPerUser=\"0\"} & " +
+                "netsh advfirewall firewall add rule name=RDP dir=in 
protocol=tcp localport=3389 action=allow profile=any & " +
+                "netsh advfirewall firewall add rule name=WinRM dir=in 
protocol=tcp localport=5985 action=allow profile=any & " +
+                "powershell -EncodedCommand " + encoded;
+        /* TODO: Find out why scripts with new line characters aren't working 
on AWS. The following appears as if it *should*
+           work but doesn't - the script simply isn't run. By connecting to 
the machine via RDP, you can get the script
+           from 'http://169.254.169.254/latest/user-data', and running it at 
the command prompt works, but for some
+           reason the script isn't run when the VM is provisioned
+        */
+//        return Joiner.on("\r\n").join(ImmutableList.of(
+//                "winrm quickconfig -q",
+//                "winrm set winrm/config/service/auth @{Basic=\"true\"}",
+//                "winrm set winrm/config/client @{AllowUnencrypted=\"true\"}",
+//                "winrm set winrm/config/service 
@{AllowUnencrypted=\"true\"}",
+//                "netsh advfirewall firewall add rule name=RDP dir=in 
protocol=tcp localport=3389 action=allow profile=any",
+//                "netsh advfirewall firewall add rule name=WinRM dir=in 
protocol=tcp localport=5985 action=allow profile=any",
+//                // Using an encoded command necessitates the need to escape. 
The unencoded command is as follows:
+//                // $RDP = Get-WmiObject -Class Win32_TerminalServiceSetting 
-ComputerName $env:computername -Namespace root\CIMV2\TerminalServices 
-Authentication PacketPrivacy
+//                // $Result = $RDP.SetAllowTSConnections(1,1)
+//                "powershell -EncodedCommand 
JABSAEQAUAAgAD0AIABHAGUAdAAtAFcAbQBpAE8AYgBqAGUAYwB0ACAALQBDAGwAYQBzAHMAI" +
+//                        
"ABXAGkAbgAzADIAXwBUAGUAcgBtAGkAbgBhAGwAUwBlAHIAdgBpAGMAZQBTAGUAdAB0AGkAbgBnACAALQBDAG8AbQBwA"
 +
+//                        
"HUAdABlAHIATgBhAG0AZQAgACQAZQBuAHYAOgBjAG8AbQBwAHUAdABlAHIAbgBhAG0AZQAgAC0ATgBhAG0AZQBzAHAAY"
 +
+//                        
"QBjAGUAIAByAG8AbwB0AFwAQwBJAE0AVgAyAFwAVABlAHIAbQBpAG4AYQBsAFMAZQByAHYAaQBjAGUAcwAgAC0AQQB1A"
 +
+//                        
"HQAaABlAG4AdABpAGMAYQB0AGkAbwBuACAAUABhAGMAawBlAHQAUAByAGkAdgBhAGMAeQANAAoAJABSAGUAcwB1AGwAd"
 +
+//                        
"AAgAD0AIAAkAFIARABQAC4AUwBlAHQAQQBsAGwAbwB3AFQAUwBDAG8AbgBuAGUAYwB0AGkAbwBuAHMAKAAxACwAMQApAA=="
+//        ));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractAvailabilityZoneExtension.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractAvailabilityZoneExtension.java
 
b/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractAvailabilityZoneExtension.java
new file mode 100644
index 0000000..d399b92
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractAvailabilityZoneExtension.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.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.management.ManagementContext;
+
+import org.apache.brooklyn.location.Location;
+
+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/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractCloudMachineProvisioningLocation.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractCloudMachineProvisioningLocation.java
 
b/core/src/main/java/org/apache/brooklyn/location/cloud/AbstractCloudMachineProvisioningLocation.java
new file mode 100644
index 0000000..13707b5
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/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.location.cloud;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.brooklyn.location.LocationSpec;
+import org.apache.brooklyn.location.MachineLocation;
+import org.apache.brooklyn.location.MachineProvisioningLocation;
+import org.apache.brooklyn.location.basic.AbstractLocation;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.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/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/AvailabilityZoneExtension.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/cloud/AvailabilityZoneExtension.java
 
b/core/src/main/java/org/apache/brooklyn/location/cloud/AvailabilityZoneExtension.java
new file mode 100644
index 0000000..1580440
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/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.location.cloud;
+
+import java.util.List;
+
+import brooklyn.entity.group.DynamicCluster;
+import org.apache.brooklyn.location.Location;
+import org.apache.brooklyn.location.basic.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/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/CloudLocationConfig.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/cloud/CloudLocationConfig.java
 
b/core/src/main/java/org/apache/brooklyn/location/cloud/CloudLocationConfig.java
new file mode 100644
index 0000000..9682994
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/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.location.cloud;
+
+import java.util.Collection;
+
+import com.google.common.annotations.Beta;
+import com.google.common.reflect.TypeToken;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.event.basic.BasicConfigKey;
+import org.apache.brooklyn.location.MachineLocationCustomizer;
+import org.apache.brooklyn.location.basic.LocationConfigKeys;
+import brooklyn.util.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/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/names/AbstractCloudMachineNamer.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/cloud/names/AbstractCloudMachineNamer.java
 
b/core/src/main/java/org/apache/brooklyn/location/cloud/names/AbstractCloudMachineNamer.java
new file mode 100644
index 0000000..b06b88a
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/cloud/names/AbstractCloudMachineNamer.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.cloud.names;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.trait.HasShortName;
+
+import org.apache.brooklyn.location.cloud.CloudLocationConfig;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.text.Identifiers;
+import 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/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/names/BasicCloudMachineNamer.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/cloud/names/BasicCloudMachineNamer.java
 
b/core/src/main/java/org/apache/brooklyn/location/cloud/names/BasicCloudMachineNamer.java
new file mode 100644
index 0000000..818053d
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/cloud/names/BasicCloudMachineNamer.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.cloud.names;
+
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.Entity;
+
+import org.apache.brooklyn.location.cloud.CloudLocationConfig;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.text.Identifiers;
+import brooklyn.util.text.StringShortener;
+import 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/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/names/CloudMachineNamer.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/cloud/names/CloudMachineNamer.java
 
b/core/src/main/java/org/apache/brooklyn/location/cloud/names/CloudMachineNamer.java
new file mode 100644
index 0000000..eaf7198
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/cloud/names/CloudMachineNamer.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.cloud.names;
+
+import org.apache.brooklyn.api.entity.Entity;
+
+import org.apache.brooklyn.location.Location;
+import org.apache.brooklyn.location.cloud.CloudLocationConfig;
+import brooklyn.util.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/e2c57058/core/src/main/java/org/apache/brooklyn/location/cloud/names/CustomMachineNamer.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/cloud/names/CustomMachineNamer.java
 
b/core/src/main/java/org/apache/brooklyn/location/cloud/names/CustomMachineNamer.java
new file mode 100644
index 0000000..472adde
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/cloud/names/CustomMachineNamer.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.cloud.names;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.EntityInternal;
+import org.apache.brooklyn.location.cloud.CloudLocationConfig;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.text.Strings;
+import brooklyn.util.text.TemplateProcessor;
+
+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/e2c57058/core/src/main/java/org/apache/brooklyn/location/dynamic/DynamicLocation.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/dynamic/DynamicLocation.java 
b/core/src/main/java/org/apache/brooklyn/location/dynamic/DynamicLocation.java
new file mode 100644
index 0000000..78e6d87
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/dynamic/DynamicLocation.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.dynamic;
+
+import org.apache.brooklyn.api.entity.Entity;
+
+import com.google.common.annotations.Beta;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import org.apache.brooklyn.location.Location;
+import brooklyn.util.flags.SetFromFlag;
+
+/**
+ * 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();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/dynamic/LocationOwner.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/dynamic/LocationOwner.java 
b/core/src/main/java/org/apache/brooklyn/location/dynamic/LocationOwner.java
new file mode 100644
index 0000000..a2b0bfb
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/dynamic/LocationOwner.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.dynamic;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.event.AttributeSensor;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.event.basic.Sensors;
+import org.apache.brooklyn.location.Location;
+import org.apache.brooklyn.location.LocationDefinition;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * An entity that owns a particular location.
+ * <p>
+ * The entity should be able to dynamically create an instance of the required 
type of location, and will manage
+ * the lifecycle of the location in parallel with its own.
+ *
+ * @param L the location type
+ * @param E the entity type
+ */
+@Beta
+public interface LocationOwner<L extends Location & DynamicLocation<E, L>, E 
extends Entity & LocationOwner<L, E>> {
+
+    @SetFromFlag("locationPrefix")
+    ConfigKey<String> LOCATION_NAME_PREFIX = ConfigKeys.newStringConfigKey(
+            "entity.dynamicLocation.prefix", "The name prefix for the location 
owned by this entity", "dynamic");
+
+    @SetFromFlag("locationSuffix")
+    ConfigKey<String> LOCATION_NAME_SUFFIX = ConfigKeys.newStringConfigKey(
+            "entity.dynamicLocation.suffix", "The name suffix for the location 
owned by this entity");
+
+    @SetFromFlag("locationName")
+    BasicAttributeSensorAndConfigKey<String> LOCATION_NAME = new 
BasicAttributeSensorAndConfigKey<String>(String.class,
+            "entity.dynamicLocation.name", "The name of the location owned by 
this entity (default is auto-generated using prefix and suffix keys)");
+
+    ConfigKey<Map<String, Object>> LOCATION_FLAGS = 
ConfigKeys.newConfigKey(new TypeToken<Map<String, Object>>() { },
+            "entity.dynamicLocation.flags", "Extra creation flags for the 
Location owned by this entity",
+            ImmutableMap.<String, Object>of());
+
+    AttributeSensor<Location> DYNAMIC_LOCATION = 
Sensors.newSensor(Location.class,
+            "entity.dynamicLocation", "The location owned by this entity");
+
+    AttributeSensor<String> LOCATION_SPEC = Sensors.newStringSensor(
+            "entity.dynamicLocation.spec", "The specification string for the 
location owned by this entity");
+
+    AttributeSensor<Boolean> DYNAMIC_LOCATION_STATUS = 
Sensors.newBooleanSensor(
+            "entity.dynamicLocation.status", "The status of the location owned 
by this entity");
+
+    AttributeSensor<LocationDefinition> LOCATION_DEFINITION = 
Sensors.newSensor(
+        LocationDefinition.class, "entity.dynamicLocation.definition", "The 
location definition for the location owned by this entity");
+
+    L getDynamicLocation();
+
+    L createLocation(Map<String, ?> flags);
+
+    boolean isLocationAvailable();
+
+    void deleteLocation();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/GeoBytesHostGeoLookup.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/geo/GeoBytesHostGeoLookup.java
 
b/core/src/main/java/org/apache/brooklyn/location/geo/GeoBytesHostGeoLookup.java
new file mode 100644
index 0000000..0ff6e8c
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/geo/GeoBytesHostGeoLookup.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.geo;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.net.Networking;
+
+/** @deprecated Mar 2015 - the API has changed; GetLocation now discouraged 
for free access, and valuepairs.txt not supported */
+@Deprecated
+public class GeoBytesHostGeoLookup implements HostGeoLookup {
+
+    public static final Logger log = 
LoggerFactory.getLogger(GeoBytesHostGeoLookup.class);
+    
+    /*
+    curl 
"http://www.geobytes.com/IpLocator.htm?GetLocation&template=valuepairs.txt&IpAddress=geobytes.com";
+    known=1
+    countryid=254
+    country=United States
+    fips104=US
+    iso2=US
+    iso3=USA
+    ison=840
+    internet=US
+    comment=
+    regionid=142
+    region=Maryland
+    code=MD
+    adm1code=    
+    cityid=8909
+    city=Baltimore
+    latitude=39.2894
+    longitude=-76.6384
+    timezone=-05:00
+    dmaid=512
+    dma=512
+    market=Baltimore
+    certainty=78
+    isproxy=false
+    mapbytesremaining=Free
+    */
+    
+    public String getPropertiesLookupUrlForPublicIp(String ip) {
+        return 
"http://www.geobytes.com/IpLocator.htm?GetLocation&template=valuepairs.txt&IpAddress="+ip.trim();
+    }
+
+    public String getPropertiesLookupUrlForLocalhost() {
+        return 
"http://www.geobytes.com/IpLocator.htm?GetLocation&template=valuepairs.txt";;
+    }
+
+    /** returns URL to get properties for the given address (assuming 
localhost if address is on a subnet) */
+    public String getPropertiesLookupUrlFor(InetAddress address) {
+        if (Networking.isPrivateSubnet(address)) return 
getPropertiesLookupUrlForLocalhost();
+        return getPropertiesLookupUrlForPublicIp(address.getHostAddress());
+    }
+    
+    private static boolean LOGGED_GEO_LOOKUP_UNAVAILABLE = false;
+    
+    public HostGeoInfo getHostGeoInfo(InetAddress address) throws 
MalformedURLException, IOException {
+        String url = getPropertiesLookupUrlFor(address);
+        if (log.isDebugEnabled())
+            log.debug("Geo info lookup for "+address+" at "+url);
+        Properties props = new Properties();
+        try {
+            props.load( new URL(url).openStream() );
+            HostGeoInfo geo = new HostGeoInfo(address.getHostName(), 
props.getProperty("city")+" ("+props.getProperty("iso2")+")", 
+                Double.parseDouble(props.getProperty("latitude")), 
Double.parseDouble(props.getProperty("longitude")));
+            log.info("Geo info lookup for "+address+" returned: "+geo);
+            return geo;
+        } catch (Exception e) {
+            // may be web not available, or gateway giving us funny crap
+            if (log.isDebugEnabled())
+                log.debug("Geo info lookup for "+address+" failed: "+e);
+            if (!LOGGED_GEO_LOOKUP_UNAVAILABLE) {
+                LOGGED_GEO_LOOKUP_UNAVAILABLE = true;
+                log.info("Geo info lookup unavailable (for "+address+"; cause 
"+e+")");
+            }
+            return null;
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/HasHostGeoInfo.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/geo/HasHostGeoInfo.java 
b/core/src/main/java/org/apache/brooklyn/location/geo/HasHostGeoInfo.java
new file mode 100644
index 0000000..9d3c630
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/geo/HasHostGeoInfo.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.geo;
+
+public interface HasHostGeoInfo {
+
+    HostGeoInfo getHostGeoInfo();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoInfo.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoInfo.java 
b/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoInfo.java
new file mode 100644
index 0000000..d159a95
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoInfo.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.geo;
+
+import java.io.Serializable;
+import java.net.InetAddress;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.location.AddressableLocation;
+import org.apache.brooklyn.location.Location;
+import org.apache.brooklyn.location.basic.AbstractLocation;
+import org.apache.brooklyn.location.basic.LocationConfigKeys;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.flags.TypeCoercions;
+import brooklyn.util.guava.Maybe;
+import brooklyn.util.internal.BrooklynSystemProperties;
+
+import com.google.common.base.Objects;
+
+/**
+ * Encapsulates geo-IP information for a given host.
+ */
+public class HostGeoInfo implements Serializable {
+    
+    private static final long serialVersionUID = -5866759901535266181L;
+
+    public static final Logger log = 
LoggerFactory.getLogger(HostGeoInfo.class);
+
+    /** the IP address */
+    public final String address;
+    
+    public final String displayName;
+    
+    public final double latitude;
+    public final double longitude;
+    
+    private static Maybe<HostGeoLookup> cachedLookup = null;
+
+    public static HostGeoInfo create(String address, String displayName, 
double latitude, double longitude) {
+        return new HostGeoInfo(address, displayName, latitude, longitude);
+    }
+    
+    public static HostGeoInfo fromIpAddress(InetAddress address) {
+        try {
+            HostGeoLookup lookup = getDefaultLookup();
+            if (lookup!=null)
+                return lookup.getHostGeoInfo(address);
+        } catch (Exception e) {
+            if (log.isDebugEnabled())
+                log.debug("unable to look up geo DNS info for "+address, e);
+        }
+        return null;
+    }
+
+    @Nullable
+    public static HostGeoLookup getDefaultLookup() throws 
InstantiationException, IllegalAccessException, ClassNotFoundException {
+        if (cachedLookup==null) {
+            cachedLookup = Maybe.of(findHostGeoLookupImpl());
+        }                
+        return cachedLookup.get();
+    }
+    
+    public static void clearCachedLookup() {
+        cachedLookup = null;
+    }
+    
+    /** returns null if cannot be set */
+    public static HostGeoInfo fromLocation(Location l) {
+        if (l==null) return null;
+        
+        Location la = l;
+        HostGeoInfo resultFromLocation = null;
+        while (la!=null) {
+            if (la instanceof HasHostGeoInfo) {
+                resultFromLocation = ((HasHostGeoInfo)l).getHostGeoInfo();
+                if (resultFromLocation!=null) break;
+            }
+            la = la.getParent();
+        }
+        if (resultFromLocation!=null && l==la) {
+            // from the location
+            return resultFromLocation;
+        }
+        // resultFromLocation may be inherited, in which case we will copy it 
later
+        
+        InetAddress address = findIpAddress(l);
+        Object latitude = l.getConfig(LocationConfigKeys.LATITUDE);
+        Object longitude = l.getConfig(LocationConfigKeys.LONGITUDE);
+
+        if (resultFromLocation!=null && (latitude == null || longitude == 
null)) {
+            latitude = resultFromLocation.latitude;
+            longitude = resultFromLocation.longitude;            
+        }
+        if (address!=null && (latitude == null || longitude == null)) {
+            HostGeoInfo geo = fromIpAddress(address);
+            if (geo==null) return null;
+            latitude = geo.latitude;
+            longitude = geo.longitude;
+        }
+        
+        if (latitude==null || longitude==null)
+            return null;
+        
+        Exception error=null;
+        try {
+            latitude = TypeCoercions.castPrimitive(latitude, Double.class);
+            longitude = TypeCoercions.castPrimitive(longitude, Double.class);
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+            error = e;
+        }
+        if (error!=null || !(latitude instanceof Double) || !(longitude 
instanceof Double))
+            throw new IllegalArgumentException("Location "+l+" specifies 
invalid type of lat/long: " +
+                    "lat="+latitude+" (type "+(latitude==null ? null : 
latitude.getClass())+"); " +
+                    "lon="+longitude+" (type "+(longitude==null ? null : 
longitude.getClass())+")", error);
+        
+        HostGeoInfo result = new HostGeoInfo(address!=null ? 
address.getHostAddress() : null, l.getDisplayName(), (Double) latitude, 
(Double) longitude);
+        if (l instanceof AbstractLocation) {
+            ((AbstractLocation)l).setHostGeoInfo(result);
+        }
+        return result;
+    }
+    
+    private static HostGeoLookup findHostGeoLookupImpl() throws 
InstantiationException, IllegalAccessException, ClassNotFoundException {
+        String type = BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL.getValue();
+        /* utrace seems more accurate than geobytes, and it gives a report of 
how many tokens are left;
+         * but maxmind if it's installed locally is even better (does not 
require remote lookup),
+         * so use it if available */
+        if (type==null) {
+            if (MaxMind2HostGeoLookup.getDatabaseReader()!=null)
+                return new MaxMind2HostGeoLookup();
+            log.debug("Using Utrace remote for geo lookup because MaxMind2 is 
not available");
+            return new UtraceHostGeoLookup();
+        }
+        if (type.isEmpty()) return null;
+        return (HostGeoLookup) Class.forName(type).newInstance();
+    }
+
+    public static HostGeoInfo fromEntity(Entity e) {
+        for (Location l : e.getLocations()) {
+            HostGeoInfo hgi = fromLocation(l);
+            if (hgi != null)
+                return hgi;
+        }
+        return null;
+    }
+    
+    public static InetAddress findIpAddress(Location l) {
+        if (l == null)
+            return null;
+        if (l instanceof AddressableLocation)
+            return ((AddressableLocation) l).getAddress();
+        return findIpAddress(l.getParent());
+    }
+    
+    public HostGeoInfo(String address, String displayName, double latitude, 
double longitude) {
+        this.address = address;
+        this.displayName = displayName==null ? "" : displayName;
+        this.latitude = latitude;
+        this.longitude = longitude;
+    }
+
+    public String getAddress() {
+        return address;
+    }
+    
+    @Override
+    public String toString() {
+        return "HostGeoInfo["+displayName+": "+(address!=null ? address : 
"(no-address)")+" at ("+latitude+","+longitude+")]";
+    }
+    
+    @Override
+    public boolean equals(Object o) {
+        // Slight cheat: only includes the address + displayName field 
(displayName to allow overloading localhost etc)
+        return (o instanceof HostGeoInfo) && Objects.equal(address, 
((HostGeoInfo) o).address)
+                && Objects.equal(displayName, ((HostGeoInfo) o).displayName);
+    }
+    
+    @Override
+    public int hashCode() {
+        // Slight cheat: only includes the address + displayName field 
(displayName to allow overloading localhost etc)
+        return Objects.hashCode(address, displayName);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoLookup.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoLookup.java 
b/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoLookup.java
new file mode 100644
index 0000000..db58276
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/geo/HostGeoLookup.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.geo;
+
+import java.net.InetAddress;
+
+public interface HostGeoLookup {
+
+    public HostGeoInfo getHostGeoInfo(InetAddress address) throws Exception;
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/geo/LocalhostExternalIpLoader.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/location/geo/LocalhostExternalIpLoader.java
 
b/core/src/main/java/org/apache/brooklyn/location/geo/LocalhostExternalIpLoader.java
new file mode 100644
index 0000000..1d31f0f
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/location/geo/LocalhostExternalIpLoader.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.geo;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.ResourceUtils;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.text.StringPredicates;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Durations;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Predicates;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class LocalhostExternalIpLoader {
+
+    public static final Logger LOG = 
LoggerFactory.getLogger(LocalhostExternalIpLoader.class);
+
+    private static final AtomicBoolean retrievingLocalExternalIp = new 
AtomicBoolean(false);
+    private static final CountDownLatch triedLocalExternalIp = new 
CountDownLatch(1);
+    private static volatile String localExternalIp;
+
+    private static class IpLoader implements Callable<String> {
+        private static final Pattern ipPattern = Pattern.compile(
+                
"\\b((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\b");
+        final String url;
+
+        protected IpLoader(String url) {
+            this.url = url;
+        }
+
+        @Override
+        public String call() {
+            String response = 
ResourceUtils.create(LocalhostExternalIpLoader.class)
+                    .getResourceAsString(url).trim();
+            return postProcessResponse(response);
+        }
+
+        String postProcessResponse(String response) {
+            Matcher matcher = ipPattern.matcher(response);
+            boolean matched = matcher.find();
+            if (!matched) {
+                LOG.error("No IP address matched in output from {}: {}", url, 
response);
+                return null;
+            } else {
+                return matcher.group();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static List<String> getIpAddressWebsites() {
+        String file = new ResourceUtils(LocalhostExternalIpLoader.class)
+                
.getResourceAsString("classpath://brooklyn/location/geo/external-ip-address-resolvers.txt");
+        Iterable<String> lines = Splitter.on('\n')
+                .omitEmptyStrings()
+                .trimResults()
+                .split(file);
+        List<String> urls = Lists.newArrayList(Iterables.filter(lines, 
Predicates.not(StringPredicates.startsWith("#"))));
+        Collections.shuffle(urls);
+        return urls;
+    }
+
+    @VisibleForTesting
+    static String getIpAddressFrom(String url) {
+        return new IpLoader(url).call();
+    }
+    
+    /** As {@link #getLocalhostIpWithin(Duration)} but returning 127.0.0.1 if 
not accessible */
+    public static String getLocalhostIpQuicklyOrDefault() {
+        String result = doLoad(Duration.seconds(2));
+        if (result==null) return "127.0.0.1";
+        return result;
+    }
+
+    /** As {@link #getLocalhostIpWithin(Duration)} but without the time limit 
cut-off, failing if the load gives an error. */
+    public static String getLocalhostIpWaiting() {
+        return getLocalhostIpWithin(null);
+    }
+
+    /**
+     * Attempts to load the public IP address of localhost, failing if the load
+     * does not complete within the given duration.
+     * @return The public IP address of localhost
+     */
+    public static String getLocalhostIpWithin(Duration timeout) {
+        String result = doLoad(timeout);
+        if (result == null) {
+            throw new IllegalStateException("Unable to retrieve external IP 
for localhost; network may be down or slow or remote service otherwise not 
responding");
+        }
+        return result;
+    }
+
+    /**
+     * Requests URLs returned by {@link #getIpAddressWebsites()} until one 
returns an IP address.
+     * The address is assumed to be the external IP address of localhost.
+     * @param blockFor The maximum duration to wait for the IP address to be 
resolved.
+     *                 An indefinite way if null.
+     * @return A string in IPv4 format, or null if no such address could be 
ascertained.
+     */
+    private static String doLoad(Duration blockFor) {
+        if (localExternalIp != null) {
+            return localExternalIp;
+        }
+
+        final List<String> candidateUrls = getIpAddressWebsites();
+        if (candidateUrls.isEmpty()) {
+            LOG.debug("No candidate URLs to use to determine external IP of 
localhost");
+            return null;
+        }
+
+        // do in private thread, otherwise blocks for 30s+ on dodgy network!
+        // (we can skip it if someone else is doing it, we have synch lock so 
we'll get notified)
+        if (retrievingLocalExternalIp.compareAndSet(false, true)) {
+            new Thread() {
+                public void run() {
+                    for (String url : candidateUrls) {
+                        try {
+                            LOG.debug("Looking up external IP of this host 
from {} in private thread {}", url, Thread.currentThread());
+                            localExternalIp = new IpLoader(url).call();
+                            LOG.debug("Finished looking up external IP of this 
host from {} in private thread, result {}", url, localExternalIp);
+                            break;
+                        } catch (Throwable t) {
+                            LOG.debug("Unable to look up external IP of this 
host from {}, probably offline {})", url, t);
+                        } finally {
+                            retrievingLocalExternalIp.set(false);
+                            triedLocalExternalIp.countDown();
+                        }
+                    }
+                }
+            }.start();
+        }
+
+        try {
+            if (blockFor!=null) {
+                Durations.await(triedLocalExternalIp, blockFor);
+            } else {
+                triedLocalExternalIp.await();
+            }
+        } catch (InterruptedException e) {
+            throw Exceptions.propagate(e);
+        }
+        if (localExternalIp == null) {
+            return null;
+        }
+        return localExternalIp;
+    }
+
+}

Reply via email to