brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From hadr...@apache.org
Subject [33/54] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package brooklyn.location
Date Fri, 14 Aug 2015 03:43:02 GMT
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/LocationConfigUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/LocationConfigUtils.java b/core/src/main/java/org/apache/brooklyn/location/basic/LocationConfigUtils.java
new file mode 100644
index 0000000..ce3c444
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/LocationConfigUtils.java
@@ -0,0 +1,559 @@
+/*
+ * 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 brooklyn.util.JavaGroovyEquivalents.groovyTruth;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.internal.BrooklynFeatureEnablement;
+import org.apache.brooklyn.location.cloud.CloudLocationConfig;
+import brooklyn.util.ResourceUtils;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.crypto.AuthorizedKeysParser;
+import brooklyn.util.crypto.SecureKeys;
+import brooklyn.util.crypto.SecureKeys.PassphraseProblem;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.os.Os;
+import brooklyn.util.text.StringFunctions;
+import brooklyn.util.text.Strings;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class LocationConfigUtils {
+
+    private static final Logger log = LoggerFactory.getLogger(LocationConfigUtils.class);
+
+    /** Creates an instance of {@link OsCredential} by inspecting {@link LocationConfigKeys#PASSWORD}; 
+     * {@link LocationConfigKeys#PRIVATE_KEY_DATA} and {@link LocationConfigKeys#PRIVATE_KEY_FILE};
+     * {@link LocationConfigKeys#PRIVATE_KEY_PASSPHRASE} if needed, and
+     * {@link LocationConfigKeys#PRIVATE_KEY_DATA} and {@link LocationConfigKeys#PRIVATE_KEY_FILE}
+     * (defaulting to the private key file + ".pub"). 
+     **/
+    public static OsCredential getOsCredential(ConfigBag config) {
+        return OsCredential.newInstance(config);
+    }
+    
+    /** Convenience class for holding private/public keys and passwords, inferring from config keys.
+     * See {@link LocationConfigUtils#getOsCredential(ConfigBag)}. */
+    @Beta // would be nice to replace with a builder pattern 
+    public static class OsCredential {
+        private final ConfigBag config;
+        private boolean preferPassword = false;
+        private boolean tryDefaultKeys = true;
+        private boolean requirePublicKey = true;
+        private boolean doKeyValidation = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_VALIDATE_LOCATION_SSH_KEYS);
+        private boolean warnOnErrors = true;
+        private boolean throwOnErrors = false;
+        
+        private boolean dirty = true;;
+        
+        private String privateKeyData;
+        private String publicKeyData;
+        private String password;
+        
+        private OsCredential(ConfigBag config) {
+            this.config = config;
+        }
+
+        /** throws if there are any problems */
+        public OsCredential checkNotEmpty() {
+            checkNoErrors();
+            
+            if (!hasKey() && !hasPassword()) {
+                if (warningMessages.size()>0)
+                    throw new IllegalStateException("Could not find credentials: "+warningMessages);
+                else 
+                    throw new IllegalStateException("Could not find credentials");
+            }
+            return this;
+        }
+
+        /** throws if there were errors resolving (e.g. explicit keys, none of which were found/valid, or public key required and not found) 
+         * @return */
+        public OsCredential checkNoErrors() {
+            throwOnErrors(true);
+            dirty();
+            infer();
+            return this;
+        }
+        
+        public OsCredential logAnyWarnings() {
+            if (!warningMessages.isEmpty())
+                log.warn("When reading credentials: "+warningMessages);
+            return this;
+        }
+
+        public Set<String> getWarningMessages() {
+            return warningMessages;
+        }
+        
+        /** returns either the key or password or null; if both a key and a password this prefers the key unless otherwise set
+         * via {@link #preferPassword()} */
+        public synchronized String getPreferredCredential() {
+            infer();
+            
+            if (isUsingPassword()) return password;
+            if (hasKey()) return privateKeyData;
+            return null;
+        }
+
+        /** if there is no credential (ignores public key) */
+        public boolean isEmpty() {
+            return !hasKey() && !hasPassword();
+        }
+        public boolean hasKey() {
+            infer();
+            // key has stricter non-blank check than password
+            return Strings.isNonBlank(privateKeyData);
+        }
+        public boolean hasPassword() {
+            infer();
+            // blank, even empty passwords are allowed
+            return password!=null;
+        }
+        /** if a password is available, and either this is preferred over a key or there is no key */
+        public boolean isUsingPassword() {
+            return hasPassword() && (!hasKey() || preferPassword);
+        }
+        
+        public String getPrivateKeyData() {
+            infer();
+            return privateKeyData;
+        }
+        public String getPublicKeyData() {
+            infer();
+            return publicKeyData;
+        }
+        public String getPassword() {
+            infer();
+            return password;
+        }
+        
+        /** if both key and password supplied, prefer the key; the default */
+        public OsCredential preferKey() { preferPassword = false; return dirty(); }
+        /** if both key and password supplied, prefer the password; see {@link #preferKey()} */
+        public OsCredential preferPassword() { preferPassword = true; return dirty(); }
+        
+        /** if false, do not mind if there is no public key corresponding to any private key;
+         * defaults to true; only applies if a private key is set */
+        public OsCredential requirePublicKey(boolean requirePublicKey) {
+            this.requirePublicKey = requirePublicKey;
+            return dirty(); 
+        }
+        /** whether to check the private/public keys and passphrase are coherent; default true */
+        public OsCredential doKeyValidation(boolean doKeyValidation) {
+            this.doKeyValidation = doKeyValidation;
+            return dirty();
+        }
+        /** if true (the default) this will look at default locations set on keys */
+        public OsCredential useDefaultKeys(boolean tryDefaultKeys) {
+            this.tryDefaultKeys = tryDefaultKeys;
+            return dirty(); 
+        }
+        /** whether to log warnings on problems */
+        public OsCredential warnOnErrors(boolean warnOnErrors) {
+            this.warnOnErrors = warnOnErrors;
+            return dirty(); 
+        }
+        /** whether to throw on problems */
+        public OsCredential throwOnErrors(boolean throwOnErrors) {
+            this.throwOnErrors = throwOnErrors;
+            return dirty(); 
+        }
+        
+        private OsCredential dirty() { dirty = true; return this; }
+            
+        public static OsCredential newInstance(ConfigBag config) {
+            return new OsCredential(config);
+        }
+        
+        private synchronized void infer() {
+            if (!dirty) return;
+            warningMessages.clear(); 
+            
+            log.debug("Inferring OS credentials");
+            privateKeyData = config.get(LocationConfigKeys.PRIVATE_KEY_DATA);
+            password = config.get(LocationConfigKeys.PASSWORD);
+            publicKeyData = getKeyDataFromDataKeyOrFileKey(config, LocationConfigKeys.PUBLIC_KEY_DATA, LocationConfigKeys.PUBLIC_KEY_FILE);
+
+            KeyPair privateKey = null;
+            
+            if (Strings.isBlank(privateKeyData)) {
+                // look up private key files
+                String privateKeyFiles = null;
+                boolean privateKeyFilesExplicitlySet = config.containsKey(LocationConfigKeys.PRIVATE_KEY_FILE);
+                if (privateKeyFilesExplicitlySet || (tryDefaultKeys && password==null)) 
+                    privateKeyFiles = config.get(LocationConfigKeys.PRIVATE_KEY_FILE);
+                if (Strings.isNonBlank(privateKeyFiles)) {
+                    Iterator<String> fi = Arrays.asList(privateKeyFiles.split(File.pathSeparator)).iterator();
+                    while (fi.hasNext()) {
+                        String file = fi.next();
+                        if (Strings.isNonBlank(file)) {
+                            try {
+                                // real URL's won't actual work, due to use of path separator above 
+                                // not real important, but we get it for free if "files" is a list instead.
+                                // using ResourceUtils is useful for classpath resources
+                                if (file!=null)
+                                    privateKeyData = ResourceUtils.create().getResourceAsString(file);
+                                // else use data already set
+                                
+                                privateKey = getValidatedPrivateKey(file);
+                                
+                                if (privateKeyData==null) {
+                                    // was cleared due to validation error
+                                } else if (Strings.isNonBlank(publicKeyData)) {
+                                    log.debug("Loaded private key data from "+file+" (public key data explicitly set)");
+                                    break;
+                                } else {
+                                    String publicKeyFile = (file!=null ? file+".pub" : "(data)");
+                                    try {
+                                        publicKeyData = ResourceUtils.create().getResourceAsString(publicKeyFile);
+                                        
+                                        log.debug("Loaded private key data from "+file+
+                                            " and public key data from "+publicKeyFile);
+                                        break;
+                                    } catch (Exception e) {
+                                        Exceptions.propagateIfFatal(e);
+                                        log.debug("No public key file "+publicKeyFile+"; will try extracting from private key");
+                                        publicKeyData = AuthorizedKeysParser.encodePublicKey(privateKey.getPublic());
+                                        
+                                        if (publicKeyData==null) {
+                                            if (requirePublicKey) {
+                                                addWarning("Unable to find or extract public key for "+file, "skipping");
+                                            } else {
+                                                log.debug("Loaded private key data from "+file+" (public key data not found but not required)");
+                                                break;
+                                            }
+                                        } else {
+                                            log.debug("Loaded private key data from "+file+" (public key data extracted)");
+                                            break;
+                                        }
+                                        privateKeyData = null;
+                                    }
+                                }
+
+                            } catch (Exception e) {
+                                Exceptions.propagateIfFatal(e);
+                                String message = "Missing/invalid private key file "+file;
+                                if (privateKeyFilesExplicitlySet) addWarning(message, (!fi.hasNext() ? "no more files to try" : "trying next file")+": "+e);
+                            }
+                        }
+                    }
+                    if (privateKeyFilesExplicitlySet && Strings.isBlank(privateKeyData))
+                        error("No valid private keys found", ""+warningMessages);
+                }
+            } else {
+                privateKey = getValidatedPrivateKey("(data)");
+            }
+            
+            if (privateKeyData!=null) {
+                if (requirePublicKey && Strings.isBlank(publicKeyData)) {
+                    if (privateKey!=null) {
+                        publicKeyData = AuthorizedKeysParser.encodePublicKey(privateKey.getPublic());
+                    }
+                    if (Strings.isBlank(publicKeyData)) {
+                        error("If explicit "+LocationConfigKeys.PRIVATE_KEY_DATA.getName()+" is supplied, then "
+                            + "the corresponding "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" must also be supplied.", null);
+                    } else {
+                        log.debug("Public key data extracted");
+                    }
+                }
+                if (doKeyValidation && privateKey!=null && privateKey.getPublic()!=null && Strings.isNonBlank(publicKeyData)) {
+                    PublicKey decoded = null;
+                    try {
+                        decoded = AuthorizedKeysParser.decodePublicKey(publicKeyData);
+                    } catch (Exception e) {
+                        Exceptions.propagateIfFatal(e);
+                        addWarning("Invalid public key: "+decoded);
+                    }
+                    if (decoded!=null && !privateKey.getPublic().equals( decoded )) {
+                        error("Public key inferred from does not match public key extracted from private key", null);
+                    }
+                }
+            }
+
+            log.debug("OS credential inference: "+this);
+            dirty = false;
+        }
+
+        private KeyPair getValidatedPrivateKey(String label) {
+            KeyPair privateKey = null;
+            String passphrase = config.get(CloudLocationConfig.PRIVATE_KEY_PASSPHRASE);
+            try {
+                privateKey = SecureKeys.readPem(new ByteArrayInputStream(privateKeyData.getBytes()), passphrase);
+                if (passphrase!=null) {
+                    // get the unencrypted key data for our internal use (jclouds requires this)
+                    privateKeyData = SecureKeys.toPem(privateKey);
+                }
+            } catch (PassphraseProblem e) {
+                if (doKeyValidation) {
+                    log.debug("Encountered error handling key "+label+": "+e, e);
+                    if (Strings.isBlank(passphrase))
+                        addWarning("Passphrase required for key '"+label+"'");
+                    else
+                        addWarning("Invalid passphrase for key '"+label+"'");
+                    privateKeyData = null;
+                }
+            } catch (Exception e) {
+                Exceptions.propagateIfFatal(e);
+                if (doKeyValidation) {
+                    addWarning("Unable to parse private key from '"+label+"': unknown format");
+                    privateKeyData = null;
+                }
+            }
+            return privateKey;
+        }
+        
+        Set<String> warningMessages = MutableSet.of();
+        
+        private void error(String msg, String logExtension) {
+            addWarning(msg);
+            if (warnOnErrors) log.warn(msg+(logExtension==null ? "" : ": "+logExtension));
+            if (throwOnErrors) throw new IllegalStateException(msg+(logExtension==null ? "" : "; "+logExtension));
+        }
+
+        private void addWarning(String msg) {
+            addWarning(msg, null);
+        }
+        private void addWarning(String msg, String debugExtension) {
+            log.debug(msg+(debugExtension==null ? "" : "; "+debugExtension));
+            warningMessages.add(msg);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName()+"["+
+                (Strings.isNonBlank(publicKeyData) ? publicKeyData : "no-public-key")+";"+
+                (Strings.isNonBlank(privateKeyData) ? "private-key-present" : "no-private-key")+","+
+                (password!=null ? "password(len="+password.length()+")" : "no-password")+"]";
+        }
+    }
+
+    /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated
+    public static String getPrivateKeyData(ConfigBag config) {
+        return getKeyData(config, LocationConfigKeys.PRIVATE_KEY_DATA, LocationConfigKeys.PRIVATE_KEY_FILE);
+    }
+    
+    /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated
+    public static String getPublicKeyData(ConfigBag config) {
+        String data = getKeyData(config, LocationConfigKeys.PUBLIC_KEY_DATA, LocationConfigKeys.PUBLIC_KEY_FILE);
+        if (groovyTruth(data)) return data;
+        
+        String privateKeyFile = config.get(LocationConfigKeys.PRIVATE_KEY_FILE);
+        if (groovyTruth(privateKeyFile)) {
+            List<String> privateKeyFiles = Arrays.asList(privateKeyFile.split(File.pathSeparator));
+            List<String> publicKeyFiles = ImmutableList.copyOf(Iterables.transform(privateKeyFiles, StringFunctions.append(".pub")));
+            List<String> publicKeyFilesTidied = tidyFilePaths(publicKeyFiles);
+            
+            String fileData = getFileContents(publicKeyFilesTidied);
+            if (groovyTruth(fileData)) {
+                if (log.isDebugEnabled()) log.debug("Loaded "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" from inferred files, based on "+LocationConfigKeys.PRIVATE_KEY_FILE.getName() + ": used " + publicKeyFilesTidied + " for "+config.getDescription());
+                config.put(LocationConfigKeys.PUBLIC_KEY_DATA, fileData);
+                return fileData;
+            } else {
+                log.info("Not able to load "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" from inferred files, based on "+LocationConfigKeys.PRIVATE_KEY_FILE.getName() + ": tried " + publicKeyFilesTidied + " for "+config.getDescription());
+            }
+        }
+        
+        return null;
+    }
+
+    /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated
+    public static String getKeyData(ConfigBag config, ConfigKey<String> dataKey, ConfigKey<String> fileKey) {
+        return getKeyDataFromDataKeyOrFileKey(config, dataKey, fileKey);
+    }
+    
+    private static String getKeyDataFromDataKeyOrFileKey(ConfigBag config, ConfigKey<String> dataKey, ConfigKey<String> fileKey) {
+        boolean unused = config.isUnused(dataKey);
+        String data = config.get(dataKey);
+        if (groovyTruth(data) && !unused) {
+            return data;
+        }
+        
+        String file = config.get(fileKey);
+        if (groovyTruth(file)) {
+            List<String> files = Arrays.asList(file.split(File.pathSeparator));
+            List<String> filesTidied = tidyFilePaths(files);
+            String fileData = getFileContents(filesTidied);
+            if (fileData == null) {
+                log.warn("Invalid file" + (files.size() > 1 ? "s" : "") + " for " + fileKey + " (given " + files + 
+                        (files.equals(filesTidied) ? "" : "; converted to " + filesTidied) + ") " +
+                        "may fail provisioning " + config.getDescription());
+            } else if (groovyTruth(data)) {
+                if (!fileData.trim().equals(data.trim()))
+                    log.warn(dataKey.getName()+" and "+fileKey.getName()+" both specified; preferring the former");
+            } else {
+                data = fileData;
+                config.put(dataKey, data);
+                config.get(dataKey);
+            }
+        }
+        
+        return data;
+    }
+    
+    /**
+     * Reads the given file(s) in-order, returning the contents of the first file that can be read.
+     * Returns the file contents, or null if none of the files can be read.
+     *  
+     * @param files             list of file paths
+     */
+    private static String getFileContents(Iterable<String> files) {
+        Iterator<String> fi = files.iterator();
+        while (fi.hasNext()) {
+            String file = fi.next();
+            if (groovyTruth(file)) {
+                try {
+                    // see comment above
+                    String result = ResourceUtils.create().getResourceAsString(file);
+                    if (result!=null) return result;
+                    log.debug("Invalid file "+file+" ; " + (!fi.hasNext() ? "no more files to try" : "trying next file")+" (null)");
+                } catch (Exception e) {
+                    Exceptions.propagateIfFatal(e);
+                    log.debug("Invalid file "+file+" ; " + (!fi.hasNext() ? "no more files to try" : "trying next file"), e);
+                }
+            }
+        }
+        return null;
+    }
+
+    private static List<String> tidyFilePaths(Iterable<String> files) {
+        List<String> result = Lists.newArrayList();
+        for (String file : files) {
+            result.add(Os.tidyPath(file));
+        }
+        return result;
+    }
+
+    /** @deprecated since 0.6.0 use configBag.getWithDeprecation */
+    @Deprecated
+    @SuppressWarnings("unchecked")
+    public static <T> T getConfigCheckingDeprecatedAlternatives(ConfigBag configBag, ConfigKey<T> preferredKey,
+            ConfigKey<?> ...deprecatedKeys) {
+        T value1 = (T) configBag.getWithDeprecation(preferredKey, deprecatedKeys);
+        T value2 = getConfigCheckingDeprecatedAlternativesInternal(configBag, preferredKey, deprecatedKeys);
+        if (!Objects.equal(value1, value2)) {
+            // points to a bug in one of the get-with-deprecation methods
+            log.warn("Deprecated getConfig with deprecated keys "+Arrays.toString(deprecatedKeys)+" gets different value with " +
+                    "new strategy "+preferredKey+" ("+value1+") and old ("+value2+"); preferring old value for now, but this behaviour will change");
+            return value2;
+        }
+        return value1;
+    }
+    
+    @SuppressWarnings("unchecked")
+    private static <T> T getConfigCheckingDeprecatedAlternativesInternal(ConfigBag configBag, ConfigKey<T> preferredKey,
+            ConfigKey<?> ...deprecatedKeys) {
+        ConfigKey<?> keyProvidingValue = null;
+        T value = null;
+        boolean found = false;
+        if (configBag.containsKey(preferredKey)) {
+            value = configBag.get(preferredKey);
+            found = true;
+            keyProvidingValue = preferredKey;
+        }
+        
+        for (ConfigKey<?> deprecatedKey: deprecatedKeys) {
+            T altValue = null;
+            boolean altFound = false;
+            if (configBag.containsKey(deprecatedKey)) {
+                altValue = (T) configBag.get(deprecatedKey);
+                altFound = true;
+                
+                if (altFound) {
+                    if (found) {
+                        if (Objects.equal(value, altValue)) {
+                            // fine -- nothing
+                        } else {
+                            log.warn("Detected deprecated key "+deprecatedKey+" with value "+altValue+" used in addition to "+keyProvidingValue+" " +
+                                    "with value "+value+" for "+configBag.getDescription()+"; ignoring");
+                            configBag.remove(deprecatedKey);
+                        }
+                    } else {
+                        log.warn("Detected deprecated key "+deprecatedKey+" with value "+altValue+" used instead of recommended "+preferredKey+"; " +
+                                "promoting to preferred key status; will not be supported in future versions");
+                        configBag.put(preferredKey, altValue);
+                        configBag.remove(deprecatedKey);
+                        value = altValue;
+                        found = true;
+                        keyProvidingValue = deprecatedKey;
+                    }
+                }
+            }
+        }
+        
+        if (found) {
+            return value;
+        } else {
+            return configBag.get(preferredKey); // get the default
+        }
+    }
+
+    public static Map<ConfigKey<String>,String> finalAndOriginalSpecs(String finalSpec, Object ...sourcesForOriginalSpec) {
+        // yuck!: TODO should clean up how these things get passed around
+        Map<ConfigKey<String>,String> result = MutableMap.of();
+        if (finalSpec!=null) 
+            result.put(LocationInternal.FINAL_SPEC, finalSpec);
+        
+        String originalSpec = null;
+        for (Object source: sourcesForOriginalSpec) {
+            if (source instanceof CharSequence) originalSpec = source.toString();
+            else if (source instanceof Map) {
+                if (originalSpec==null) originalSpec = Strings.toString( ((Map<?,?>)source).get(LocationInternal.ORIGINAL_SPEC) );
+                if (originalSpec==null) originalSpec = Strings.toString( ((Map<?,?>)source).get(LocationInternal.ORIGINAL_SPEC.getName()) );
+            }
+            if (originalSpec!=null) break; 
+        }
+        if (originalSpec==null) originalSpec = finalSpec;
+        if (originalSpec!=null)
+            result.put(LocationInternal.ORIGINAL_SPEC, originalSpec);
+        
+        return result;
+    }
+
+    public static boolean isEnabled(ManagementContext mgmt, String prefix) {
+        ConfigKey<Boolean> key = ConfigKeys.newConfigKeyWithPrefix(prefix+".", LocationConfigKeys.ENABLED);
+        Boolean enabled = mgmt.getConfig().getConfig(key);
+        if (enabled!=null) return enabled.booleanValue();
+        return true;
+    }
+    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/LocationDynamicType.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/LocationDynamicType.java b/core/src/main/java/org/apache/brooklyn/location/basic/LocationDynamicType.java
new file mode 100644
index 0000000..a289090
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/LocationDynamicType.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.basic;
+
+import brooklyn.basic.BrooklynDynamicType;
+import org.apache.brooklyn.location.Location;
+import org.apache.brooklyn.location.LocationType;
+
+public class LocationDynamicType extends BrooklynDynamicType<Location, AbstractLocation> {
+
+    public LocationDynamicType(AbstractLocation location) {
+        super(location);
+    }
+    
+    public LocationType getSnapshot() {
+        return (LocationType) super.getSnapshot();
+    }
+
+    @Override
+    protected LocationTypeSnapshot newSnapshot() {
+        return new LocationTypeSnapshot(name, value(configKeys));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/LocationInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/LocationInternal.java b/core/src/main/java/org/apache/brooklyn/location/basic/LocationInternal.java
new file mode 100644
index 0000000..429e671
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/LocationInternal.java
@@ -0,0 +1,94 @@
+/*
+ * 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 java.util.Map;
+
+import org.apache.brooklyn.api.entity.rebind.RebindSupport;
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.location.Location;
+import org.apache.brooklyn.mementos.LocationMemento;
+
+import brooklyn.basic.BrooklynObjectInternal;
+import brooklyn.config.ConfigInheritance;
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.util.config.ConfigBag;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Information about locations private to Brooklyn.
+ */
+public interface LocationInternal extends BrooklynObjectInternal, Location {
+
+    @Beta
+    public static final ConfigKey<String> ORIGINAL_SPEC = ConfigKeys.newStringConfigKey("spec.original", "The original spec used to instantiate a location");
+    @Beta
+    public static final ConfigKey<String> FINAL_SPEC = ConfigKeys.newStringConfigKey("spec.final", "The actual spec (in a chain) which instantiates a location");
+    @Beta
+    public static final ConfigKey<String> NAMED_SPEC_NAME = ConfigKeys.newStringConfigKey("spec.named.name", "The name on the (first) named spec in a chain");
+    
+    /**
+     * Registers the given extension for the given type. If an extension already existed for
+     * this type, then this will override it.
+     * 
+     * @throws NullPointerException if extensionType or extension are null
+     * @throws IllegalArgumentException if extension does not implement extensionType
+     */
+    <T> void addExtension(Class<T> extensionType, T extension);
+
+    /**
+     * Get a record of the metadata of this location.
+     * <p/>
+     * <p>Metadata records are used to record an audit trail of events relating to location usage
+     * (for billing purposes, for example). Implementations (and subclasses) should override this
+     * method to return information useful for this purpose.</p>
+     *
+     * @return
+     */
+    public Map<String, String> toMetadataRecord();
+
+    /**
+     * @deprecated since 0.7.0; use {@link #config()}, such as {@code ((LocationInternal)location).config().getLocalBag()}
+     */
+    @Deprecated
+    ConfigBag getLocalConfigBag();
+
+    /**
+     * Returns all config, including that inherited from parents.
+     * 
+     * This method does not respect {@link ConfigInheritance} and so usage is discouraged.
+     * 
+     * @deprecated since 0.7.0; use {@link #config()}, such as {@code ((LocationInternal)location).config().getBag()}
+     */
+    @Deprecated
+    ConfigBag getAllConfigBag();
+
+    /**
+     * Users are strongly discouraged from calling or overriding this method.
+     * It is for internal calls only, relating to persisting/rebinding entities.
+     * This method may change (or be removed) in a future release without notice.
+     */
+    @Override
+    @Beta
+    RebindSupport<LocationMemento> getRebindSupport();
+    
+    ManagementContext getManagementContext();
+}

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

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/LocationPropertiesFromBrooklynProperties.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/LocationPropertiesFromBrooklynProperties.java b/core/src/main/java/org/apache/brooklyn/location/basic/LocationPropertiesFromBrooklynProperties.java
new file mode 100644
index 0000000..e1feb18
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/LocationPropertiesFromBrooklynProperties.java
@@ -0,0 +1,224 @@
+/*
+ * 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.checkArgument;
+
+import java.io.File;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.BrooklynProperties;
+import brooklyn.config.BrooklynServerConfig;
+import brooklyn.config.ConfigUtils;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.internal.ssh.SshTool;
+import brooklyn.util.os.Os;
+
+import com.google.common.base.Predicates;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+
+/**
+ * The properties to use for locations, loaded from brooklyn.properties file.
+ * 
+ * @author aledsage
+ **/
+public class LocationPropertiesFromBrooklynProperties {
+
+    private static final Logger LOG = LoggerFactory.getLogger(LocationPropertiesFromBrooklynProperties.class);
+
+    @SuppressWarnings("deprecation")
+    protected static final Map<String, String> DEPRECATED_KEYS_MAPPING = new DeprecatedKeysMappingBuilder(LOG)
+            .camelToHyphen(LocationConfigKeys.DISPLAY_NAME)
+            .camelToHyphen(LocationConfigKeys.PRIVATE_KEY_FILE)
+            .camelToHyphen(LocationConfigKeys.PRIVATE_KEY_DATA)
+            .camelToHyphen(LocationConfigKeys.PRIVATE_KEY_PASSPHRASE)
+            .camelToHyphen(LocationConfigKeys.PUBLIC_KEY_FILE)
+            .camelToHyphen(LocationConfigKeys.PUBLIC_KEY_DATA)
+            .camelToHyphen(LocationConfigKeys.CALLER_CONTEXT)
+            .build();
+    
+    /**
+     * Finds the properties that apply to location, stripping off the prefixes.
+     * 
+     * Order of preference (in ascending order) is:
+     * <ol>
+     * <li>brooklyn.location.*
+     * <li>brooklyn.location.provider.*
+     * <li>brooklyn.location.named.namedlocation.*
+     * </ol>
+     * <p>
+     * Converts deprecated hyphenated properties to the non-deprecated camelCase format. 
+     */
+    public Map<String, Object> getLocationProperties(String provider, String namedLocation, Map<String, ?> properties) {
+        ConfigBag result = ConfigBag.newInstance();
+        
+        if (!Strings.isNullOrEmpty(provider)) 
+            result.put(LocationConfigKeys.CLOUD_PROVIDER, provider);
+        // named properties are preferred over providerOrApi properties
+        result.putAll(transformDeprecated(getGenericLocationSingleWordProperties(properties)));
+        if (!Strings.isNullOrEmpty(provider)) result.putAll(transformDeprecated(getScopedLocationProperties(provider, properties)));
+        if (!Strings.isNullOrEmpty(namedLocation)) result.putAll(transformDeprecated(getNamedLocationProperties(namedLocation, properties)));
+        
+        setLocalTempDir(properties, result);
+        
+        return result.getAllConfigRaw();
+    }
+
+    /** allow the temp dir where ssh temporary files on the brooklyn server side are placed */
+    public static void setLocalTempDir(Map<String,?> source, ConfigBag target) {
+        // TODO better would be to use BrooklynServerConfig, requiring management passed in
+        String brooklynDataDir = (String) source.get(BrooklynServerConfig.getMgmtBaseDir(source));
+        if (brooklynDataDir != null && brooklynDataDir.length() > 0) {
+            String tempDir = Os.mergePaths(brooklynDataDir, "tmp", "ssh");
+            target.putIfAbsentAndNotNull(SshTool.PROP_LOCAL_TEMP_DIR, tempDir);
+            Os.deleteOnExitEmptyParentsUpTo(new File(tempDir), new File(brooklynDataDir));
+        }
+    }
+    
+    /**
+     * Gets the named provider (e.g. if using a property like {@code brooklyn.location.named.myfavourite=localhost}, then
+     * {@code getNamedProvider("myfavourite", properties)} will return {@code "localhost"}).
+     */
+    protected String getNamedProvider(String namedLocation, Map<String, ?> properties) {
+        String key = String.format("brooklyn.location.named.%s", namedLocation);
+        return (String) properties.get(key);
+    }
+    
+    /**
+     * Returns those properties in the form "brooklyn.location.xyz", where "xyz" is any
+     * key that does not contain dots. We do this special (sub-optimal!) filtering
+     * because we want to exclude brooklyn.location.named.*, brooklyn.location.jclouds.*, etc.
+     * We only want those properties that are to be generic for all locations.
+     * 
+     * Strips off the prefix in the returned map.
+     */
+    protected Map<String, Object> getGenericLocationSingleWordProperties(Map<String, ?> properties) {
+        return getMatchingSingleWordProperties("brooklyn.location.", properties);
+    }
+
+    /**
+     * Gets all properties that start with {@code "brooklyn.location."+scopeSuffix+"."}, stripping off
+     * the prefix in the returned map.
+     */
+    protected Map<String, Object> getScopedLocationProperties(String scopeSuffix, Map<String, ?> properties) {
+        checkArgument(!scopeSuffix.startsWith("."), "scopeSuffix \"%s\" should not start with \".\"", scopeSuffix);
+        checkArgument(!scopeSuffix.endsWith("."), "scopeSuffix \"%s\" should not end with \".\"", scopeSuffix);
+        String prefix = String.format("brooklyn.location.%s.", scopeSuffix);
+        return ConfigUtils.filterForPrefixAndStrip(properties, prefix).asMapWithStringKeys();
+    }
+
+    /**
+     * Gets all properties that start with the given {@code fullPrefix}, stripping off
+     * the prefix in the returned map.
+     */
+    protected Map<String, Object> getMatchingProperties(String fullPrefix, Map<String, ?> properties) {
+        return ConfigUtils.filterForPrefixAndStrip(properties, fullPrefix).asMapWithStringKeys();
+    }
+
+    /**
+     * Gets all properties that start with either of the given prefixes. The {@code fullPreferredPrefix} 
+     * properties will override any duplicates in {@code fullDeprecatedPrefix}. If there are any
+     * properties that match the {@code fullDeprecatedPrefix}, then a warning will be logged.
+     * 
+     * @see #getMatchingProperties(String, Map)
+     */
+    protected Map<String, Object> getMatchingProperties(String fullPreferredPrefix, String fullDeprecatedPrefix, Map<String, ?> properties) {
+        Map<String, Object> deprecatedResults = getMatchingProperties(fullDeprecatedPrefix, properties);
+        Map<String, Object> results = getMatchingProperties(fullPreferredPrefix, properties);
+        
+        if (deprecatedResults.size() > 0) {
+            LOG.warn("Deprecated use of properties prefix "+fullDeprecatedPrefix+"; instead use "+fullPreferredPrefix);
+            return MutableMap.<String, Object>builder()
+                    .putAll(deprecatedResults)
+                    .putAll(results)
+                    .build();
+        } else {
+            return results;
+        }
+    }
+
+    /**
+     * Gets all properties that start with the given {@code fullPrefix}, stripping off
+     * the prefix in the returned map.
+     * 
+     * Returns only those properties whose key suffix is a single word (i.e. contains no dots).
+     * We do this special (sub-optimal!) filtering because we want sub-scoped things 
+     * (e.g. could want brooklyn.location.privateKeyFile, but not brooklyn.location.named.*). 
+     */
+    protected Map<String, Object> getMatchingSingleWordProperties(String fullPrefix, Map<String, ?> properties) {
+        BrooklynProperties filteredProperties = ConfigUtils.filterForPrefixAndStrip(properties, fullPrefix);
+        return ConfigUtils.filterFor(filteredProperties, Predicates.not(Predicates.containsPattern("\\."))).asMapWithStringKeys();
+    }
+
+    /**
+     * Gets all single-word properties that start with either of the given prefixes. The {@code fullPreferredPrefix} 
+     * properties will override any duplicates in {@code fullDeprecatedPrefix}. If there are any
+     * properties that match the {@code fullDeprecatedPrefix}, then a warning will be logged.
+     * 
+     * @see #getMatchingSingleWordProperties(String, Map)
+     */
+    protected Map<String, Object> getMatchingSingleWordProperties(String fullPreferredPrefix, String fullDeprecatedPrefix, Map<String, ?> properties) {
+        Map<String, Object> deprecatedResults = getMatchingSingleWordProperties(fullDeprecatedPrefix, properties);
+        Map<String, Object> results = getMatchingSingleWordProperties(fullPreferredPrefix, properties);
+        
+        if (deprecatedResults.size() > 0) {
+            LOG.warn("Deprecated use of properties prefix "+fullDeprecatedPrefix+"; instead use "+fullPreferredPrefix);
+            return MutableMap.<String, Object>builder()
+                    .putAll(deprecatedResults)
+                    .putAll(results)
+                    .build();
+        } else {
+            return results;
+        }
+    }
+
+    protected Map<String, Object> getNamedLocationProperties(String locationName, Map<String, ?> properties) {
+        checkArgument(!Strings.isNullOrEmpty(locationName), "locationName should not be blank");
+        String prefix = String.format("brooklyn.location.named.%s.", locationName);
+        return ConfigUtils.filterForPrefixAndStrip(properties, prefix).asMapWithStringKeys();
+    }
+
+    protected Map<String, Object> transformDeprecated(Map<String, ?> properties) {
+        Map<String,Object> result = Maps.newLinkedHashMap();
+        Map<String, String> deprecatedKeysMapping = getDeprecatedKeysMapping();
+        
+        for (Map.Entry<String,?> entry : properties.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            if (deprecatedKeysMapping.containsKey(key)) {
+                String transformedKey = deprecatedKeysMapping.get(key);
+                LOG.warn("Deprecated key {}, transformed to {}; will not be supported in future versions", new Object[] {key, transformedKey});
+                result.put(transformedKey, value);
+            } else {
+                result.put(key, value);
+            }
+        }
+        
+        return result;
+    }
+    
+    protected Map<String,String> getDeprecatedKeysMapping() {
+        return DEPRECATED_KEYS_MAPPING;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/LocationTypeSnapshot.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/LocationTypeSnapshot.java b/core/src/main/java/org/apache/brooklyn/location/basic/LocationTypeSnapshot.java
new file mode 100644
index 0000000..4dec614
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/LocationTypeSnapshot.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.basic;
+
+import java.util.Map;
+
+import org.apache.brooklyn.policy.EnricherType;
+
+import brooklyn.basic.BrooklynTypeSnapshot;
+import brooklyn.config.ConfigKey;
+
+public class LocationTypeSnapshot extends BrooklynTypeSnapshot implements EnricherType {
+    
+    private static final long serialVersionUID = 9150132836104748237L;
+
+    LocationTypeSnapshot(String name, Map<String, ConfigKey<?>> configKeys) {
+        super(name, configKeys);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        return (obj instanceof LocationTypeSnapshot) && super.equals(obj);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/Locations.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/Locations.java b/core/src/main/java/org/apache/brooklyn/location/basic/Locations.java
new file mode 100644
index 0000000..31855ed
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/Locations.java
@@ -0,0 +1,159 @@
+/*
+ * 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 java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.management.LocationManager;
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.location.Location;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.Entities;
+import org.apache.brooklyn.location.LocationSpec;
+import org.apache.brooklyn.location.MachineLocation;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.guava.Maybe;
+import brooklyn.util.yaml.Yamls;
+
+import com.google.common.collect.ImmutableList;
+
+public class Locations {
+
+    private static final Logger log = LoggerFactory.getLogger(Locations.class);
+
+    public static final LocationsFilter USE_FIRST_LOCATION = new LocationsFilter() {
+        private static final long serialVersionUID = 3100091615409115890L;
+
+        @Override
+        public List<Location> filterForContext(List<Location> locations, Object context) {
+            if (locations.size()<=1) return locations;
+            return ImmutableList.of(locations.get(0));
+        }
+    };
+
+    public interface LocationsFilter extends Serializable {
+        public List<Location> filterForContext(List<Location> locations, Object context);
+    }
+    
+    /** as {@link Machines#findUniqueMachineLocation(Iterable)} */
+    public static Maybe<MachineLocation> findUniqueMachineLocation(Iterable<? extends Location> locations) {
+        return Machines.findUniqueMachineLocation(locations);
+    }
+    
+    /** as {@link Machines#findUniqueSshMachineLocation(Iterable)} */
+    public static Maybe<SshMachineLocation> findUniqueSshMachineLocation(Iterable<? extends Location> locations) {
+        return Machines.findUniqueSshMachineLocation(locations);
+    }
+
+    /** if no locations are supplied, returns locations on the entity, or in the ancestors, until it finds a non-empty set,
+     * or ultimately the empty set if no locations are anywhere */ 
+    public static Collection<? extends Location> getLocationsCheckingAncestors(Collection<? extends Location> locations, Entity entity) {
+        // look in ancestors if location not set here
+        Entity ancestor = entity;
+        while ((locations==null || locations.isEmpty()) && ancestor!=null) {
+            locations = ancestor.getLocations();
+            ancestor = ancestor.getParent();
+        }
+        return locations;
+    }
+    
+    public static boolean isManaged(Location loc) {
+        ManagementContext mgmt = ((LocationInternal)loc).getManagementContext();
+        return (mgmt != null) && mgmt.isRunning() && mgmt.getLocationManager().isManaged(loc);
+    }
+
+    public static void unmanage(Location loc) {
+        if (isManaged(loc)) {
+            ManagementContext mgmt = ((LocationInternal)loc).getManagementContext();
+            mgmt.getLocationManager().unmanage(loc);
+        }
+    }
+    
+    /**
+     * Registers the given location (and all its children) with the management context. 
+     * @throws IllegalStateException if the parent location is not already managed
+     * 
+     * @since 0.6.0 (added only for backwards compatibility, where locations are being created directly; previously in {@link Entities}).
+     * @deprecated in 0.6.0; use {@link LocationManager#createLocation(LocationSpec)} instead.
+     */
+    public static void manage(Location loc, ManagementContext managementContext) {
+        if (!managementContext.getLocationManager().isManaged(loc)) {
+            log.warn("Deprecated use of unmanaged location ("+loc+"); will be managed automatically now but not supported in future versions");
+            // FIXME this occurs MOST OF THE TIME e.g. including BrooklynLauncher.location(locationString)
+            // not sure what is the recommend way to convert from locationString to locationSpec, or the API we want to expose;
+            // deprecating some of the LocationRegistry methods seems sensible?
+            log.debug("Stack trace for location of: Deprecated use of unmanaged location; will be managed automatically now but not supported in future versions", new Exception("TRACE for: Deprecated use of unmanaged location"));
+            managementContext.getLocationManager().manage(loc);
+        }
+    }
+
+    public static Location coerce(ManagementContext mgmt, Object rawO) {
+        if (rawO==null)
+            return null;
+        if (rawO instanceof Location)
+            return (Location)rawO;
+        
+        Object raw = rawO;
+        if (raw instanceof String)
+            raw = Yamls.parseAll((String)raw).iterator().next();
+        
+        String name;
+        Map<?, ?> flags = null;
+        if (raw instanceof Map) {
+            // for yaml, take the key, and merge with locationFlags
+            Map<?,?> tm = ((Map<?,?>)raw);
+            if (tm.size()!=1) {
+                throw new IllegalArgumentException("Location "+rawO+" is invalid; maps must have only one key, being the location spec string");
+            }
+            name = (String) tm.keySet().iterator().next();
+            flags = (Map<?, ?>) tm.values().iterator().next();
+            
+        } else if (raw instanceof String) {
+            name = (String)raw;
+            
+        } else {
+            throw new IllegalArgumentException("Location "+rawO+" is invalid; can only parse strings or maps");
+        }
+        return mgmt.getLocationRegistry().resolve(name, flags);
+    }
+    
+    public static Collection<? extends Location> coerceToCollection(ManagementContext mgmt, Object rawO) {
+        if (rawO==null) return null;
+        Object raw = rawO;
+        if (raw instanceof Collection) {
+            List<Location> result = MutableList.<Location>of();
+            for (Object o: (Collection<?>)raw)
+                result.add(coerce(mgmt, o));
+            return result;
+        }
+        if (raw instanceof String) {
+            raw = Yamls.parseAll((String)raw).iterator().next();
+            if (raw instanceof Collection)
+                return coerceToCollection(mgmt, raw);
+        }
+        return Collections.singletonList( coerce(mgmt, raw) );
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/Machines.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/Machines.java b/core/src/main/java/org/apache/brooklyn/location/basic/Machines.java
new file mode 100644
index 0000000..621e1e2
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/Machines.java
@@ -0,0 +1,188 @@
+/*
+ * 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 java.net.InetAddress;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.location.Location;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Iterables;
+
+import brooklyn.entity.basic.Attributes;
+import org.apache.brooklyn.location.MachineLocation;
+import org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation.LocalhostMachine;
+import brooklyn.util.guava.Maybe;
+import brooklyn.util.net.HasNetworkAddresses;
+
+/** utilities for working with MachineLocations */
+public class Machines {
+
+    private static final Logger log = LoggerFactory.getLogger(Machines.class);
+    
+    public static Maybe<String> getSubnetHostname(Location where) {
+        // TODO Should we look at HasNetworkAddresses? But that's not a hostname.
+        String hostname = null;
+        if (where instanceof HasSubnetHostname) {
+            hostname = ((HasSubnetHostname) where).getSubnetHostname();
+        }
+        if (hostname == null && where instanceof MachineLocation) {
+            InetAddress addr = ((MachineLocation) where).getAddress();
+            if (addr != null) hostname = addr.getHostAddress();
+        }
+        log.debug("computed subnet hostname {} for {}", hostname, where);
+        // TODO if Maybe.absent(message) appears, could/should use that
+        // TODO If no machine available, should we throw new IllegalStateException("Cannot find hostname for "+where);
+        return Maybe.fromNullable(hostname);
+    }
+
+    public static Maybe<String> getSubnetIp(Location where) {
+        // TODO Too much duplication between the ip and hostname methods
+        String result = null;
+        if (where instanceof HasSubnetHostname) {
+            result = ((HasSubnetHostname) where).getSubnetIp();
+        }
+        if (where instanceof HasNetworkAddresses) {
+            Set<String> privateAddrs = ((HasNetworkAddresses) where).getPrivateAddresses();
+            if (privateAddrs.size() > 0) {
+                result = Iterables.get(privateAddrs, 0);
+            }
+        }
+        if (result == null && where instanceof MachineLocation) {
+            InetAddress addr = ((MachineLocation) where).getAddress();
+            if (addr != null) result = addr.getHostAddress();
+        }
+        log.debug("computed subnet host ip {} for {}", result, where);
+        return Maybe.fromNullable(result);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> Maybe<T> findUniqueElement(Iterable<?> items, Class<T> type) {
+        if (items==null) return null;
+        Iterator<?> i = items.iterator();
+        T result = null;
+        while (i.hasNext()) {
+            Object candidate = i.next();
+            if (type.isInstance(candidate)) {
+                if (result==null) result = (T)candidate;
+                else {
+                    if (log.isTraceEnabled())
+                        log.trace("Multiple instances of "+type+" in "+items+"; ignoring");
+                    return Maybe.absent(new IllegalStateException("Multiple instances of "+type+" in "+items+"; expected a single one"));
+                }
+            }
+        }
+        if (result==null) 
+            return Maybe.absent(new IllegalStateException("No instances of "+type+" available (in "+items+")"));
+        return Maybe.of(result);
+    }
+    
+    public static Maybe<MachineLocation> findUniqueMachineLocation(Iterable<? extends Location> locations) {
+        return findUniqueElement(locations, MachineLocation.class);
+    }
+
+    public static Maybe<SshMachineLocation> findUniqueSshMachineLocation(Iterable<? extends Location> locations) {
+        return findUniqueElement(locations, SshMachineLocation.class);
+    }
+
+    public static Maybe<WinRmMachineLocation> findUniqueWinRmMachineLocation(Iterable<? extends Location> locations) {
+        return findUniqueElement(locations, WinRmMachineLocation.class);
+    }
+
+    public static Maybe<String> findSubnetHostname(Iterable<? extends Location> ll) {
+        Maybe<MachineLocation> l = findUniqueMachineLocation(ll);
+        if (!l.isPresent()) {
+            return Maybe.absent();
+//            throw new IllegalStateException("Cannot find hostname for among "+ll);
+        }
+        return Machines.getSubnetHostname(l.get());
+    }
+
+    public static Maybe<String> findSubnetHostname(Entity entity) {
+        String sh = entity.getAttribute(Attributes.SUBNET_HOSTNAME);
+        if (sh!=null) return Maybe.of(sh);
+        return findSubnetHostname(entity.getLocations());
+    }
+    
+    public static Maybe<String> findSubnetOrPublicHostname(Entity entity) {
+        String hn = entity.getAttribute(Attributes.HOSTNAME);
+        if (hn!=null) {
+            // attributes already set, see if there was a SUBNET_HOSTNAME set
+            // note we rely on (public) hostname being set _after_ subnet_hostname,
+            // to prevent tiny possibility of races resulting in hostname being returned
+            // becasue subnet is still being looked up -- see MachineLifecycleEffectorTasks
+            Maybe<String> sn = findSubnetHostname(entity);
+            if (sn.isPresent()) return sn;
+            // short-circuit discovery if attributes have been set already
+            return Maybe.of(hn);
+        }
+        
+        Maybe<MachineLocation> l = findUniqueMachineLocation(entity.getLocations());
+        if (!l.isPresent()) return Maybe.absent();
+        InetAddress addr = l.get().getAddress();
+        if (addr==null) return Maybe.absent();
+        return Maybe.fromNullable(addr.getHostName());
+    }
+
+    public static Maybe<String> findSubnetOrPrivateIp(Entity entity) {
+        // see comments in findSubnetOrPrivateHostname
+        String hn = entity.getAttribute(Attributes.ADDRESS);
+        if (hn!=null) {
+            Maybe<String> sn = findSubnetIp(entity);
+            if (sn.isPresent()) return sn;
+            return Maybe.of(hn);
+        }
+        
+        Maybe<MachineLocation> l = findUniqueMachineLocation(entity.getLocations());
+        if (!l.isPresent()) return Maybe.absent();
+        InetAddress addr = l.get().getAddress();
+        if (addr==null) return Maybe.absent();
+        return Maybe.fromNullable(addr.getHostAddress());
+    }
+
+    public static Maybe<String> findSubnetIp(Entity entity) {
+        String sh = entity.getAttribute(Attributes.SUBNET_ADDRESS);
+        if (sh!=null) return Maybe.of(sh);
+        return findSubnetIp(entity.getLocations());
+    }
+    
+    public static Maybe<String> findSubnetIp(Iterable<? extends Location> ll) {
+        // TODO Or if can't find MachineLocation, should we throw new IllegalStateException("Cannot find hostname for among "+ll);
+        Maybe<MachineLocation> l = findUniqueMachineLocation(ll);
+        return (l.isPresent()) ? Machines.getSubnetIp(l.get()) : Maybe.<String>absent();
+    }
+
+    /** returns whether it is localhost (and has warned) */
+    public static boolean warnIfLocalhost(Collection<? extends Location> locations, String message) {
+        if (locations.size()==1) {
+            Location l = locations.iterator().next();
+            if (l instanceof LocalhostMachineProvisioningLocation || l instanceof LocalhostMachine) {
+                log.warn(message);
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/MultiLocation.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/MultiLocation.java b/core/src/main/java/org/apache/brooklyn/location/basic/MultiLocation.java
new file mode 100644
index 0000000..bbf45f0
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/MultiLocation.java
@@ -0,0 +1,167 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkState;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.location.Location;
+import org.apache.brooklyn.location.LocationSpec;
+import org.apache.brooklyn.location.NoMachinesAvailableException;
+import org.apache.brooklyn.location.cloud.AbstractAvailabilityZoneExtension;
+import org.apache.brooklyn.location.cloud.AvailabilityZoneExtension;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import org.apache.brooklyn.location.MachineLocation;
+import org.apache.brooklyn.location.MachineProvisioningLocation;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.CompoundRuntimeException;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.flags.SetFromFlag;
+import brooklyn.util.text.Strings;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.reflect.TypeToken;
+
+/** A location which consists of multiple locations stitched together to form availability zones.
+ * The first location will be used by default, but if an {@link AvailabilityZoneExtension}-aware entity
+ * is used, it may stripe across each of the locations.  See notes at {@link AvailabilityZoneExtension}. */
+public class MultiLocation<T extends MachineLocation> extends AbstractLocation implements MachineProvisioningLocation<T> {
+
+    private static final long serialVersionUID = 7993091317970457862L;
+    
+    @SuppressWarnings("serial")
+    @SetFromFlag("subLocations")
+    public static final ConfigKey<List<MachineProvisioningLocation<?>>> SUB_LOCATIONS = ConfigKeys.newConfigKey(
+            new TypeToken<List<MachineProvisioningLocation<?>>>() {},
+            "subLocations", 
+            "The sub-machines that this location can delegate to");
+    
+    @Override
+    public void init() {
+        super.init();
+        List<MachineProvisioningLocation<?>> subLocs = getSubLocations();
+        checkState(subLocs.size() >= 1, "sub-locations must not be empty");
+        AvailabilityZoneExtension azExtension = new AvailabilityZoneExtensionImpl(getManagementContext(), subLocs);
+        addExtension(AvailabilityZoneExtension.class, azExtension);
+    }
+
+    public T obtain() throws NoMachinesAvailableException {
+        return obtain(MutableMap.of());
+    }
+    
+    /** finds (creates) and returns a {@link MachineLocation}; 
+     * this always tries the first sub-location, moving on the second and subsequent if the first throws {@link NoMachinesAvailableException}.
+     * (if you want striping across locations, see notes in {@link AvailabilityZoneExtension}.) */
+    @SuppressWarnings("unchecked")
+    @Override
+    public T obtain(Map<?, ?> flags) throws NoMachinesAvailableException {
+        List<MachineProvisioningLocation<?>> sublocsList = getSubLocations();
+        Iterator<MachineProvisioningLocation<?>> sublocs = sublocsList.iterator();
+        List<NoMachinesAvailableException> errors = MutableList.of();
+        while (sublocs.hasNext()) {
+            try {
+                return (T) sublocs.next().obtain(flags);
+            } catch (NoMachinesAvailableException e) {
+                errors.add(e);
+            }
+        }
+        Exception wrapped;
+        String msg;
+        if (errors.size()>1) {
+            wrapped = new CompoundRuntimeException(errors.size()+" sublocation exceptions, including: "+
+                Exceptions.collapseText(errors.get(0)), errors);
+            msg = Exceptions.collapseText(wrapped);
+        } else if (errors.size()==1) {
+            wrapped = errors.get(0);
+            msg = wrapped.getMessage();
+            if (Strings.isBlank(msg)) msg = wrapped.toString();
+        } else {
+            msg = "no sub-locations set for this multi-location";
+            wrapped = null;
+        }
+        throw new NoMachinesAvailableException("No machines available in any of the "+sublocsList.size()+" location"+Strings.s(sublocsList.size())+
+            " configured here: "+msg, wrapped);
+    }
+
+    public List<MachineProvisioningLocation<?>> getSubLocations() {
+        return getRequiredConfig(SUB_LOCATIONS);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public MachineProvisioningLocation<T> newSubLocation(Map<?, ?> newFlags) {
+        // TODO shouldn't have to copy config bag as it should be inherited (but currently it is not used inherited everywhere; just most places)
+        return getManagementContext().getLocationManager().createLocation(LocationSpec.create(getClass())
+                .parent(this)
+                .configure(config().getLocalBag().getAllConfig())  // FIXME Should this just be inherited?
+                .configure(newFlags));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void release(T machine) {
+        ((MachineProvisioningLocation<T>)machine.getParent()).release(machine);
+    }
+
+    @Override
+    public Map<String, Object> getProvisioningFlags(Collection<String> tags) {
+        return Maps.<String,Object>newLinkedHashMap();
+    }
+
+    @SuppressWarnings("unchecked")
+    protected MachineProvisioningLocation<T> firstSubLoc() {
+        return (MachineProvisioningLocation<T>) Iterables.get(getSubLocations(), 0);
+    }
+
+    protected <K> K getRequiredConfig(ConfigKey<K> key) {
+        return checkNotNull(getConfig(key), key.getName());
+    }
+
+    public static class AvailabilityZoneExtensionImpl extends AbstractAvailabilityZoneExtension implements AvailabilityZoneExtension {
+
+        private final List<MachineProvisioningLocation<?>> subLocations;
+        
+        public AvailabilityZoneExtensionImpl(ManagementContext managementContext, List<MachineProvisioningLocation<?>> subLocations) {
+            super(managementContext);
+            this.subLocations = ImmutableList.copyOf(subLocations);
+        }
+        
+        @Override
+        protected List<Location> doGetAllSubLocations() {
+            return ImmutableList.<Location>copyOf(subLocations);
+        }
+        
+        @Override
+        protected boolean isNameMatch(Location loc, Predicate<? super String> namePredicate) {
+            return namePredicate.apply(loc.getDisplayName());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/MultiLocationResolver.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/MultiLocationResolver.java b/core/src/main/java/org/apache/brooklyn/location/basic/MultiLocationResolver.java
new file mode 100644
index 0000000..ac4b7b3
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/basic/MultiLocationResolver.java
@@ -0,0 +1,146 @@
+/*
+ * 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.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.location.Location;
+import org.apache.brooklyn.location.LocationRegistry;
+import org.apache.brooklyn.location.LocationResolver;
+import org.apache.brooklyn.location.LocationSpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.text.KeyValueParser;
+import brooklyn.util.text.StringEscapes.JavaStringEscapes;
+
+import com.google.common.collect.Lists;
+
+public class MultiLocationResolver implements LocationResolver {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MultiLocationResolver.class);
+
+    private static final String MULTI = "multi";
+    
+    private static final Pattern PATTERN = Pattern.compile("(" + MULTI + "|" + MULTI.toUpperCase() + ")" + ":" + "\\((.*)\\)$");
+    
+    private volatile ManagementContext managementContext;
+
+    @Override
+    public void init(ManagementContext managementContext) {
+        this.managementContext = checkNotNull(managementContext, "managementContext");
+    }
+
+    @Override
+    public Location newLocationFromString(Map locationFlags, String spec, LocationRegistry registry) {
+        // FIXME pass all flags into the location
+        
+        Map globalProperties = registry.getProperties();
+        Map<String,?> locationArgs;
+        if (spec.equalsIgnoreCase(MULTI)) {
+            locationArgs = MutableMap.copyOf(locationFlags);
+        } else {
+            Matcher matcher = PATTERN.matcher(spec);
+            if (!matcher.matches()) {
+                throw new IllegalArgumentException("Invalid location '" + spec + "'; must specify something like multi(targets=named:foo)");
+            }
+            String args = matcher.group(2);
+            // TODO we are ignoring locationFlags after this (apart from named), looking only at these args
+            locationArgs = KeyValueParser.parseMap(args);
+        }
+        String namedLocation = (String) locationFlags.get("named");
+
+        Map<String, Object> filteredProperties = new LocationPropertiesFromBrooklynProperties().getLocationProperties(null, namedLocation, globalProperties);
+        MutableMap<String, Object> flags = MutableMap.<String, Object>builder()
+                .putAll(filteredProperties)
+                .putAll(locationFlags)
+                .removeAll("named")
+                .putAll(locationArgs).build();
+        
+        if (locationArgs.get("targets") == null) {
+            throw new IllegalArgumentException("target must be specified in single-machine spec");
+        }
+        
+        // TODO do we need to pass location flags etc into the children to ensure they are inherited?
+        List<Location> targets = Lists.newArrayList();
+        Object targetSpecs = locationArgs.remove("targets");
+        try {
+            if (targetSpecs instanceof String) {
+                for (String targetSpec : JavaStringEscapes.unwrapJsonishListIfPossible((String)targetSpecs)) {
+                    targets.add(managementContext.getLocationRegistry().resolve(targetSpec));
+                }
+            } else if (targetSpecs instanceof Iterable) {
+                for (Object targetSpec: (Iterable<?>)targetSpecs) {
+                    if (targetSpec instanceof String) {
+                        targets.add(managementContext.getLocationRegistry().resolve((String)targetSpec));
+                    } else {
+                        Set<?> keys = ((Map<?,?>)targetSpec).keySet();
+                        if (keys.size()!=1) 
+                            throw new IllegalArgumentException("targets supplied to MultiLocation must be a list of single-entry maps (got map of size "+keys.size()+": "+targetSpec+")");
+                        Object key = keys.iterator().next();
+                        Object flagsS = ((Map<?,?>)targetSpec).get(key);
+                        targets.add(managementContext.getLocationRegistry().resolve((String)key, (Map<?,?>)flagsS));
+                    }
+                }
+            } else throw new IllegalArgumentException("targets must be supplied to MultiLocation, either as string spec or list of single-entry maps each being a location spec");
+            
+            MultiLocation result = managementContext.getLocationManager().createLocation(LocationSpec.create(MultiLocation.class)
+                    .configure(flags)
+                    .configure("subLocations", targets)
+                    .configure(LocationConfigUtils.finalAndOriginalSpecs(spec, locationFlags, globalProperties, namedLocation)));
+
+            // TODO Important workaround for BasicLocationRegistry.resolveForPeeking.
+            // That creates a location (from the resolver) and immediately unmanages it.
+            // The unmanage *must* remove all the targets created here; otherwise we have a leak.
+            // Adding the targets as children achieves this.
+            for (Location target : targets) {
+                target.setParent(result);
+            }
+            return result;
+
+        } catch (Exception e) {
+            // Must clean up after ourselves: don't leak sub-locations on error
+            if (LOG.isDebugEnabled()) LOG.debug("Problem resolving MultiLocation; cleaning up any sub-locations and rethrowing: "+e);
+            for (Location target : targets) {
+                Locations.unmanage(target);
+            }
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    @Override
+    public String getPrefix() {
+        return MULTI;
+    }
+
+    @Override
+    public boolean accepts(String spec, LocationRegistry registry) {
+        return BasicLocationRegistry.isResolverPrefixForSpec(this, spec, true);
+    }
+    
+}

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



Mime
View raw message