You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by al...@apache.org on 2015/08/12 17:55:39 UTC

[21/35] incubator-brooklyn git commit: [BROOKLYN-162] package rename to org.apache.brooklyn: software/webapp

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingWebClient.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingWebClient.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingWebClient.java
new file mode 100644
index 0000000..7e5c3aa
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingWebClient.java
@@ -0,0 +1,459 @@
+/*
+ * 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.entity.dns.geoscaling;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.tidy.Tidy;
+
+import brooklyn.util.http.HttpTool;
+import brooklyn.util.text.Strings;
+
+/**
+ * For interacting with the www.geoscaling.com DNS service.
+ * 
+ * If you get the SSL error "peer not authenticated", then it means the required certificate is
+ * not in your trust store. For example, see:
+ * {@linkplain http://stackoverflow.com/questions/373295/digital-certificate-how-to-import-cer-file-in-to-truststore-file-using}. 
+ * The chain of certificates (as of October 2014, found by viewing in Chrome) is:
+ * <ol>
+ *   <li> AddTrust External CA root
+ *   <li> COMODO RSA Certification Authority
+ *   <li> COMODO RSA Domain Validation Secure Server CA
+ *   <li> www.geoscaling.com
+ * </ol>
+ */
+public class GeoscalingWebClient {
+    public static final Logger log = LoggerFactory.getLogger(GeoscalingWebClient.class);
+    
+    public static final long PROVIDE_NETWORK_INFO = 1 << 0;
+    public static final long PROVIDE_CITY_INFO    = 1 << 1;
+    public static final long PROVIDE_COUNTRY_INFO = 1 << 2;
+    public static final long PROVIDE_EXTRA_INFO   = 1 << 3;
+    public static final long PROVIDE_UPTIME_INFO  = 1 << 4;
+    
+    private static final String HOST ="www.geoscaling.com";
+    private static final String PATH ="dns2/index.php";
+    private HttpClient httpClient;
+    private Tidy tidy;
+    private List<Domain> primaryDomains = null;
+    
+    
+    public class Domain {
+        public final int id;
+        public final String name;
+        private List<SmartSubdomain> smartSubdomains = null;
+        
+        public Domain(int id, String name) {
+            this.id = id;
+            this.name = name.toLowerCase();
+        }
+        
+        public List<SmartSubdomain> getSmartSubdomains() {
+            if (smartSubdomains == null)
+                smartSubdomains = GeoscalingWebClient.this.fetchSmartSubdomains(this);
+            return smartSubdomains;
+        }
+        
+        public SmartSubdomain getSmartSubdomain(String name) {
+            name = name.toLowerCase();
+            for (SmartSubdomain s : getSmartSubdomains()) {
+                if (s.name.equals(name)) return s;
+            }
+            return null;
+        }
+        
+        /** e.g. editRecord("foo", "A", "1.2.3.4"), which assuming this domain is "bar.com", will create A record for foo.bar.com.
+         * <p>
+         * or editRecord("*.foo", "CNAME", "foo.bar.com") to map everything at *.foo.bar.com to foo.bar.com
+         */
+        public void editRecord(String subdomainPart, String type, String content) {
+            subdomainPart = Strings.removeFromEnd(subdomainPart, "."+name);
+            editSubdomainRecord(id, subdomainPart, type, content);
+        }
+        
+        public SmartSubdomain getSmartSubdomain(int id) {
+            for (SmartSubdomain s : getSmartSubdomains()) {
+                if (s.id == id) return s;
+            }
+            return null;
+        }
+        
+        public void createSmartSubdomain(String name) {
+            GeoscalingWebClient.this.createSmartSubdomain(id, name);
+            smartSubdomains = fetchSmartSubdomains(this);
+        }
+        
+        public void delete() {
+            deletePrimaryDomain(id);
+            primaryDomains = fetchPrimaryDomains();
+        }
+        
+        @Override
+        public String toString() {
+            return "Domain["+name+" ("+id+")]";
+        }
+        
+        @Override
+        public int hashCode() {
+            return id;
+        }
+    }
+    
+    
+    public class SmartSubdomain {
+        public final Domain parent;
+        public final int id;
+        public String name;
+        
+        public SmartSubdomain(Domain parent, int id, String name) {
+            this.parent = parent;
+            this.id = id;
+            this.name = name.toLowerCase();
+        }
+        
+        public void configure(long flags, String phpScript) {
+            configureSmartSubdomain(parent.id, id, name, flags, phpScript);
+        }
+        
+        public void delete() {
+            deleteSmartSubdomain(parent.id, id);
+            parent.smartSubdomains = fetchSmartSubdomains(parent);
+        }
+        
+        @Override
+        public String toString() {
+            return "SmartSubdomain["+name+" ("+id+")]";
+        }
+        
+        @Override
+        public int hashCode() {
+            return id;
+        }
+    }
+    
+    
+    public GeoscalingWebClient() {
+        this(HttpTool.httpClientBuilder().build());
+    }
+
+    public GeoscalingWebClient(HttpClient httpClient) {
+        this.httpClient = httpClient;
+        this.tidy = new Tidy();
+        // Silently swallow all HTML errors/warnings.
+        tidy.setErrout(new PrintWriter(new OutputStream() {
+            @Override public void write(int b) throws IOException { }
+        }));
+    }
+    
+    public GeoscalingWebClient(String username, String password) {
+        this();
+        login(username, password);
+    }
+    
+    public void login(String username, String password) {
+        try {
+            String url = MessageFormat.format("https://{0}/{1}?module=auth", HOST, PATH);
+            
+            HttpPost request = new HttpPost(url);
+            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+            nameValuePairs.add(new BasicNameValuePair("username", username));
+            nameValuePairs.add(new BasicNameValuePair("password", password));
+            request.setEntity(new UrlEncodedFormEntity(nameValuePairs));
+            
+            sendRequest(request, true);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to log-in to GeoScaling service: "+e, e);
+        }
+    }
+    
+    public void logout() {
+        try {
+            String url = MessageFormat.format("https://{0}/{1}?module=auth&logout", HOST, PATH);
+            sendRequest(new HttpGet(url), true);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to log-out of GeoScaling service: "+e, e);
+        }
+    }
+    
+    public List<Domain> getPrimaryDomains() {
+        if (primaryDomains == null)
+            primaryDomains = fetchPrimaryDomains();
+        return primaryDomains;
+    }
+    
+    public Domain getPrimaryDomain(String name) {
+        name = name.toLowerCase();
+        for (Domain d : getPrimaryDomains()) {
+            if (d.name.equals(name)) return d;
+        }
+        return null;
+    }
+    
+    public Domain getPrimaryDomain(int id) {
+        for (Domain d : getPrimaryDomains()) {
+            if (d.id == id) return d;
+        }
+        return null;
+    }
+    
+    public void createPrimaryDomain(String name) {
+        try {
+            name = name.toLowerCase();
+            String url = MessageFormat.format("https://{0}/{1}?module=domains", HOST, PATH);
+            
+            HttpPost request = new HttpPost(url);
+            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+            nameValuePairs.add(new BasicNameValuePair("MAX_FILE_SIZE", "65536"));
+            nameValuePairs.add(new BasicNameValuePair("domain", FilenameUtils.removeExtension(name)));
+            nameValuePairs.add(new BasicNameValuePair("tld", FilenameUtils.getExtension(name)));
+            request.setEntity(new UrlEncodedFormEntity(nameValuePairs));
+            
+            sendRequest(request, true);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create GeoScaling smart subdomain: "+e, e);
+        }
+        
+        primaryDomains = fetchPrimaryDomains();
+    }
+    
+    private List<Domain> fetchPrimaryDomains() {
+        try {
+            List<Domain> domains = new LinkedList<Domain>();
+            String url = MessageFormat.format("https://{0}/{1}?module=domains", HOST, PATH);
+            HttpResponse response = sendRequest(new HttpGet(url), false);
+            HttpEntity entity = response.getEntity();
+            if (entity != null) {
+                Document document = tidy.parseDOM(entity.getContent(), null);
+                NodeList links = document.getElementsByTagName("a");
+                for (int i = 0; i < links.getLength(); ++i) {
+                    Element link = (Element) links.item(i);
+                    String href = link.getAttribute("href");
+                    Pattern p = Pattern.compile("module=domain.*&id=(\\d+)");
+                    Matcher m = p.matcher(href);
+                    if (!m.find(0)) continue;
+                    
+                    int id = Integer.parseInt(m.group(1));
+                    String name = getTextContent(link).trim();
+                    if (name.length() == 0) continue;
+                    
+                    domains.add(new Domain(id, name));
+                }
+                
+                EntityUtils.consume(entity);
+            }
+            
+            return domains;
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to retrieve GeoScaling subdomains: "+e, e);
+        }
+    }
+    
+    private void deletePrimaryDomain(int primaryDomainId) {
+        try {
+            String url = MessageFormat.format(
+                    "https://{0}/{1}?module=domain&id={2,number,#}&delete=1",
+                    HOST, PATH, primaryDomainId);
+            
+            sendRequest(new HttpGet(url), true);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to delete GeoScaling primary domain: "+e, e);
+        }
+    }
+    
+    private List<SmartSubdomain> fetchSmartSubdomains(Domain parent) {
+        try {
+            List<SmartSubdomain> subdomains = new LinkedList<SmartSubdomain>();
+            
+            String url = MessageFormat.format(
+                    "https://{0}/{1}?module=smart_subdomains&id={2,number,#}",
+                    HOST, PATH, parent.id);
+            
+            HttpResponse response = sendRequest(new HttpGet(url), false);
+            HttpEntity entity = response.getEntity();
+            if (entity != null) {
+                Document document = tidy.parseDOM(entity.getContent(), null);
+                NodeList links = document.getElementsByTagName("a");
+                for (int i = 0; i < links.getLength(); ++i) {
+                    Element link = (Element) links.item(i);
+                    String href = link.getAttribute("href");
+                    Pattern p = Pattern.compile("module=smart_subdomain.*&subdomain_id=(\\d+)");
+                    Matcher m = p.matcher(href);
+                    if (!m.find(0)) continue;
+                    
+                    int subdomainId = Integer.parseInt(m.group(1));
+                    String name = getTextContent(link);
+                    if (name.trim().length() == 0) continue;
+                    
+                    name = name.substring(0, name.length() - parent.name.length() - 1);
+                    subdomains.add(new SmartSubdomain(parent, subdomainId, name));
+                }
+                
+                EntityUtils.consume(entity);
+            }
+            
+            return subdomains;
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to retrieve GeoScaling smart subdomains: "+e, e);
+        }
+    }
+    
+    private void createSmartSubdomain(int primaryDomainId, String smartSubdomainName) {
+        try {
+            smartSubdomainName = smartSubdomainName.toLowerCase();
+            String url = MessageFormat.format(
+                    "https://{0}/{1}?module=smart_subdomains&id={2,number,#}",
+                    HOST, PATH, primaryDomainId);
+            
+            HttpPost request = new HttpPost(url);
+            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+            nameValuePairs.add(new BasicNameValuePair("MAX_FILE_SIZE", "65536"));
+            nameValuePairs.add(new BasicNameValuePair("smart_subdomain_name", smartSubdomainName));
+            request.setEntity(new UrlEncodedFormEntity(nameValuePairs));
+                        
+            sendRequest(request, true);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create GeoScaling smart subdomain: "+e, e);
+        }
+    }
+    
+    private void deleteSmartSubdomain(int primaryDomainId, int smartSubdomainId) {
+        try {
+            String url = MessageFormat.format(
+                    "https://{0}/{1}?module=smart_subdomains&id={2,number,#}&delete={3,number,#}",
+                    HOST, PATH, primaryDomainId, smartSubdomainId);
+            
+            sendRequest(new HttpGet(url), true);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to delete GeoScaling smart subdomain: "+e, e);
+        }
+    }
+    
+    private void configureSmartSubdomain(int primaryDomainId, int smartSubdomainId, String smartSubdomainName,
+            long flags, String phpScript) {
+        
+        try {
+            smartSubdomainName = smartSubdomainName.toLowerCase();
+            String url = MessageFormat.format(
+                    "https://{0}/{1}?module=smart_subdomain&id={2,number,#}&subdomain_id={3,number,#}",
+                    HOST, PATH, primaryDomainId, smartSubdomainId);
+            
+            HttpPost request = new HttpPost(url);
+            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+            nameValuePairs.add(new BasicNameValuePair("MAX_FILE_SIZE", "65536"));
+            nameValuePairs.add(new BasicNameValuePair("name", smartSubdomainName));
+            if ((flags & PROVIDE_NETWORK_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_as_info", "on"));
+            if ((flags & PROVIDE_CITY_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_city_info", "on"));
+            if ((flags & PROVIDE_COUNTRY_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_country_info", "on"));
+            if ((flags & PROVIDE_EXTRA_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_extra_info", "on"));
+            if ((flags & PROVIDE_UPTIME_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_uptime_info", "on"));
+            nameValuePairs.add(new BasicNameValuePair("code", phpScript));
+            request.setEntity(new UrlEncodedFormEntity(nameValuePairs));
+            
+            sendRequest(request, true);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to update GeoScaling smart subdomain: "+e, e);
+        }
+    }
+
+    private void editSubdomainRecord(int primaryDomainId, String record, String type, String content) {
+        
+        try {
+            String url = MessageFormat.format(
+                    "https://{0}/{1}?",
+                    HOST, "dns2/ajax/add_record.php");
+
+            HttpPost request = new HttpPost(url);
+            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+            nameValuePairs.add(new BasicNameValuePair("id", ""+primaryDomainId));
+            nameValuePairs.add(new BasicNameValuePair("name", record));
+            nameValuePairs.add(new BasicNameValuePair("type", type));
+            nameValuePairs.add(new BasicNameValuePair("content", content));
+            nameValuePairs.add(new BasicNameValuePair("ttl", "300"));
+            nameValuePairs.add(new BasicNameValuePair("prio", "0"));
+           
+            request.setEntity(new UrlEncodedFormEntity(nameValuePairs));
+            
+            sendRequest(request, true);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to update GeoScaling smart subdomain: "+e, e);
+        }
+    }
+    
+
+    protected HttpResponse sendRequest(HttpUriRequest request, boolean consumeResponse) throws ClientProtocolException, IOException {
+        if (log.isDebugEnabled()) log.debug("Geoscaling request: "+
+                request.getURI()+
+                (request instanceof HttpPost ? " "+((HttpPost)request).getEntity() : ""));
+        HttpResponse response = httpClient.execute(request);
+        if (log.isDebugEnabled()) log.debug("Geoscaling response: "+response);
+        if (consumeResponse)
+            EntityUtils.consume(response.getEntity());
+        return response;
+    }
+
+    private static String getTextContent(Node n) {
+        StringBuffer sb = new StringBuffer();
+        NodeList childNodes = n.getChildNodes();
+        for (int i = 0; i < childNodes.getLength(); ++i) {
+            Node child = childNodes.item(i);
+            if (child.getNodeType() == Node.TEXT_NODE)
+                sb.append(child.getNodeValue());
+            else
+                sb.append(getTextContent(child));
+        }
+        return sb.toString();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractController.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractController.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractController.java
new file mode 100644
index 0000000..00ad028
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractController.java
@@ -0,0 +1,74 @@
+/*
+ * 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.entity.proxy;
+
+import java.util.Set;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.group.Cluster;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.util.flags.SetFromFlag;
+
+/**
+ * Represents a controller mechanism for a {@link Cluster}.
+ */
+@ImplementedBy(AbstractControllerImpl.class)
+public interface AbstractController extends SoftwareProcess, LoadBalancer {
+
+    @SetFromFlag("domain")
+    BasicAttributeSensorAndConfigKey<String> DOMAIN_NAME = new BasicAttributeSensorAndConfigKey<String>(
+            String.class, "proxy.domainName", "Domain name that this controller responds to, or null if it responds to all domains", null);
+
+    @SetFromFlag("ssl")
+    ConfigKey<ProxySslConfig> SSL_CONFIG = ConfigKeys.newConfigKey(ProxySslConfig.class,
+            "proxy.ssl.config", "Configuration (e.g. certificates) for SSL; causes server to run with HTTPS instead of HTTP");
+    
+
+    @SetFromFlag("serviceUpUrlPath")
+    ConfigKey<String> SERVICE_UP_URL_PATH = ConfigKeys.newStringConfigKey(
+            "controller.config.serviceUpUrlPath", "The path that will be appended to the root URL to determine SERVICE_UP", "");
+
+    boolean isActive();
+
+    ProxySslConfig getSslConfig();
+
+    boolean isSsl();
+
+    String getProtocol();
+
+    /** returns primary domain this controller responds to, or null if it responds to all domains */
+    String getDomain();
+
+    Integer getPort();
+
+    /** primary URL this controller serves, if one can / has been inferred */
+    String getUrl();
+
+    AttributeSensor<Integer> getPortNumberSensor();
+
+    AttributeSensor<String> getHostnameSensor();
+
+    AttributeSensor<String> getHostAndPortSensor();
+    
+    Set<String> getServerPoolAddresses();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractControllerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractControllerImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractControllerImpl.java
new file mode 100644
index 0000000..be21228
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractControllerImpl.java
@@ -0,0 +1,516 @@
+/*
+ * 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.entity.proxy;
+
+import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.management.Task;
+import org.apache.brooklyn.policy.Policy;
+import org.apache.brooklyn.policy.PolicySpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.entity.basic.SoftwareProcessImpl;
+import brooklyn.entity.group.AbstractMembershipTrackingPolicy;
+import brooklyn.entity.group.Cluster;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.feed.ConfigToAttributes;
+import brooklyn.location.access.BrooklynAccessUtils;
+import brooklyn.location.basic.Machines;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.guava.Maybe;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.text.Strings;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.net.HostAndPort;
+
+/**
+ * Represents a controller mechanism for a {@link Cluster}.
+ */
+public abstract class AbstractControllerImpl extends SoftwareProcessImpl implements AbstractController {
+    
+    // TODO Should review synchronization model. Currently, all changes to the serverPoolTargets
+    // (and checking for potential changes) is done while synchronized on serverPoolAddresses. That means it 
+    // will also call update/reload while holding the lock. This is "conservative", but means
+    // sub-classes need to be extremely careful about any additional synchronization and of
+    // their implementations of update/reconfigureService/reload.
+    
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractControllerImpl.class);
+
+    protected volatile boolean isActive;
+    protected volatile boolean updateNeeded = true;
+
+    protected AbstractMembershipTrackingPolicy serverPoolMemberTrackerPolicy;
+    // final because this is the synch target
+    final protected Set<String> serverPoolAddresses = Sets.newLinkedHashSet();
+    protected Map<Entity,String> serverPoolTargets = Maps.newLinkedHashMap();
+    
+    public AbstractControllerImpl() {
+        this(MutableMap.of(), null, null);
+    }
+    public AbstractControllerImpl(Map<?, ?> properties) {
+        this(properties, null, null);
+    }
+    public AbstractControllerImpl(Entity parent) {
+        this(MutableMap.of(), parent, null);
+    }
+    public AbstractControllerImpl(Map<?, ?> properties, Entity parent) {
+        this(properties, parent, null);
+    }
+    public AbstractControllerImpl(Entity parent, Cluster cluster) {
+        this(MutableMap.of(), parent, cluster);
+    }
+    public AbstractControllerImpl(Map<?, ?> properties, Entity parent, Cluster cluster) {
+        super(properties, parent);
+    }
+
+    @Override
+    public void init() {
+        super.init();
+        setAttribute(SERVER_POOL_TARGETS, ImmutableMap.<Entity, String>of());
+    }
+    
+    protected void addServerPoolMemberTrackingPolicy() {
+        Group serverPool = getServerPool();
+        if (serverPool == null) {
+            return; // no-op
+        }
+        if (serverPoolMemberTrackerPolicy != null) {
+            LOG.debug("Call to addServerPoolMemberTrackingPolicy when serverPoolMemberTrackingPolicy already exists, removing and re-adding, in {}", this);
+            removeServerPoolMemberTrackingPolicy();
+        }
+        for (Policy p: getPolicies()) {
+            if (p instanceof ServerPoolMemberTrackerPolicy) {
+                // TODO want a more elegant idiom for this!
+                LOG.info(this+" picking up "+p+" as the tracker (already set, often due to rebind)");
+                serverPoolMemberTrackerPolicy = (ServerPoolMemberTrackerPolicy) p;
+                return;
+            }
+        }
+        
+        AttributeSensor<?> hostAndPortSensor = getConfig(HOST_AND_PORT_SENSOR);
+        AttributeSensor<?> hostnameSensor = getConfig(HOSTNAME_SENSOR);
+        AttributeSensor<?> portSensor = getConfig(PORT_NUMBER_SENSOR);
+        Set<AttributeSensor<?>> sensorsToTrack;
+        if (hostAndPortSensor != null) {
+            sensorsToTrack = ImmutableSet.<AttributeSensor<?>>of(hostAndPortSensor);
+        } else {
+            sensorsToTrack = ImmutableSet.<AttributeSensor<?>>of(hostnameSensor, portSensor);
+        }
+        
+        serverPoolMemberTrackerPolicy = addPolicy(PolicySpec.create(ServerPoolMemberTrackerPolicy.class)
+                .displayName("Controller targets tracker")
+                .configure("group", serverPool)
+                .configure("sensorsToTrack", sensorsToTrack));
+
+        LOG.info("Added policy {} to {}", serverPoolMemberTrackerPolicy, this);
+        
+        // Initialize ourselves immediately with the latest set of members; don't wait for
+        // listener notifications because then will be out-of-date for short period (causing 
+        // problems for rebind)
+        Map<Entity,String> serverPoolTargets = Maps.newLinkedHashMap();
+        for (Entity member : getServerPool().getMembers()) {
+            if (belongsInServerPool(member)) {
+                if (LOG.isTraceEnabled()) LOG.trace("Done {} checkEntity {}", this, member);
+                String address = getAddressOfEntity(member);
+                serverPoolTargets.put(member, address);
+            }
+        }
+
+        LOG.info("Resetting {}, server pool targets {}", new Object[] {this, serverPoolTargets});
+        setAttribute(SERVER_POOL_TARGETS, serverPoolTargets);
+    }
+    
+    protected void removeServerPoolMemberTrackingPolicy() {
+        if (serverPoolMemberTrackerPolicy != null) {
+            removePolicy(serverPoolMemberTrackerPolicy);
+        }
+    }
+    
+    public static class ServerPoolMemberTrackerPolicy extends AbstractMembershipTrackingPolicy {
+        @Override
+        protected void onEntityEvent(EventType type, Entity entity) {
+            // relies on policy-rebind injecting the implementation rather than the dynamic-proxy
+            ((AbstractControllerImpl)super.entity).onServerPoolMemberChanged(entity);
+        }
+    }
+    
+    @Override
+    public Set<String> getServerPoolAddresses() {
+        return ImmutableSet.copyOf(Iterables.filter(getAttribute(SERVER_POOL_TARGETS).values(), Predicates.notNull()));
+    }
+
+    /**
+     * Opportunity to do late-binding of the cluster that is being controlled. Must be called before start().
+     * Can pass in the 'serverPool'.
+     */
+    @Override
+    public void bind(Map<?,?> flags) {
+        if (flags.containsKey("serverPool")) {
+            setConfigEvenIfOwned(SERVER_POOL, (Group) flags.get("serverPool"));
+        } 
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public void onManagementNoLongerMaster() {
+        super.onManagementNoLongerMaster(); // TODO remove when deprecated method in parent removed
+        isActive = false;
+        removeServerPoolMemberTrackingPolicy();
+    }
+
+    private Group getServerPool() {
+        return getConfig(SERVER_POOL);
+    }
+    
+    @Override
+    public boolean isActive() {
+        return isActive;
+    }
+    
+    @Override
+    public boolean isSsl() {
+        return getSslConfig() != null;
+    }
+    
+    @Override
+    public ProxySslConfig getSslConfig() {
+        return getConfig(SSL_CONFIG);
+    }
+    
+    @Override
+    public String getProtocol() {
+        return getAttribute(PROTOCOL);
+    }
+
+    /** returns primary domain this controller responds to, or null if it responds to all domains */
+    @Override
+    public String getDomain() {
+        return getAttribute(DOMAIN_NAME);
+    }
+    
+    @Override
+    public Integer getPort() {
+        if (isSsl())
+            return getAttribute(PROXY_HTTPS_PORT);
+        else
+            return getAttribute(PROXY_HTTP_PORT);
+    }
+
+    /** primary URL this controller serves, if one can / has been inferred */
+    @Override
+    public String getUrl() {
+        return Strings.toString( getAttribute(MAIN_URI) );
+    }
+
+    @Override
+    public AttributeSensor<Integer> getPortNumberSensor() {
+        return getAttribute(PORT_NUMBER_SENSOR);
+    }
+
+    @Override
+    public AttributeSensor<String> getHostnameSensor() {
+        return getAttribute(HOSTNAME_SENSOR);
+    }
+
+    @Override
+    public AttributeSensor<String> getHostAndPortSensor() {
+        return getAttribute(HOST_AND_PORT_SENSOR);
+    }
+    
+    @Override
+    public abstract void reload();
+
+    protected String inferProtocol() {
+        return isSsl() ? "https" : "http";
+    }
+    
+    /** returns URL, if it can be inferred; null otherwise */
+    protected String inferUrl(boolean requireManagementAccessible) {
+        String protocol = checkNotNull(getProtocol(), "no protocol configured");
+        String domain = getDomain();
+        if (domain != null && domain.startsWith("*.")) {
+            domain = domain.replace("*.", ""); // Strip wildcard
+        }
+        Integer port = checkNotNull(getPort(), "no port configured (the requested port may be in use)");
+        if (requireManagementAccessible) {
+            HostAndPort accessible = BrooklynAccessUtils.getBrooklynAccessibleAddress(this, port);
+            if (accessible!=null) {
+                domain = accessible.getHostText();
+                port = accessible.getPort();
+            }
+        }
+        if (domain==null) domain = Machines.findSubnetHostname(this).orNull();
+        if (domain==null) return null;
+        return protocol+"://"+domain+":"+port+"/"+getConfig(SERVICE_UP_URL_PATH);
+    }
+
+    protected String inferUrl() {
+        return inferUrl(false);
+    }
+
+    @Override
+    protected Collection<Integer> getRequiredOpenPorts() {
+        Collection<Integer> result = super.getRequiredOpenPorts();
+        if (groovyTruth(getAttribute(PROXY_HTTP_PORT))) result.add(getAttribute(PROXY_HTTP_PORT));
+        if (groovyTruth(getAttribute(PROXY_HTTPS_PORT))) result.add(getAttribute(PROXY_HTTPS_PORT));
+        return result;
+    }
+
+    @Override
+    protected void preStart() {
+        super.preStart();
+        computePortsAndUrls();
+    }
+    
+    protected void computePortsAndUrls() {
+        AttributeSensor<String> hostAndPortSensor = getConfig(HOST_AND_PORT_SENSOR);
+        Maybe<Object> hostnameSensor = config().getRaw(HOSTNAME_SENSOR);
+        Maybe<Object> portSensor = config().getRaw(PORT_NUMBER_SENSOR);
+        if (hostAndPortSensor != null) {
+            checkState(!hostnameSensor.isPresent() && !portSensor.isPresent(), 
+                    "Must not set %s and either of %s or %s", HOST_AND_PORT_SENSOR, HOSTNAME_SENSOR, PORT_NUMBER_SENSOR);
+        }
+
+        ConfigToAttributes.apply(this);
+
+        setAttribute(PROTOCOL, inferProtocol());
+        setAttribute(MAIN_URI, URI.create(inferUrl()));
+        setAttribute(ROOT_URL, inferUrl());
+ 
+        checkNotNull(getPortNumberSensor(), "no sensor configured to infer port number");
+    }
+    
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+        // TODO when rebind policies, and rebind calls connectSensors, then this will cause problems.
+        // Also relying on addServerPoolMemberTrackingPolicy to set the serverPoolAddresses and serverPoolTargets.
+
+        addServerPoolMemberTrackingPolicy();
+    }
+    
+    @Override
+    protected void postStart() {
+        super.postStart();
+        isActive = true;
+        update();
+    }
+
+    @Override
+    protected void postRebind() {
+        super.postRebind();
+        Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
+        if (state != null && state == Lifecycle.RUNNING) {
+            isActive = true;
+            updateNeeded();
+        }
+    }
+
+    @Override
+    protected void preStop() {
+        super.preStop();
+        removeServerPoolMemberTrackingPolicy();
+    }
+
+    /** 
+     * Implementations should update the configuration so that 'serverPoolAddresses' are targeted.
+     * The caller will subsequently call reload to apply the new configuration.
+     */
+    protected abstract void reconfigureService();
+    
+    public void updateNeeded() {
+        synchronized (serverPoolAddresses) {
+            if (updateNeeded) return;
+            updateNeeded = true;
+            LOG.debug("queueing an update-needed task for "+this+"; update will occur shortly");
+            Entities.submit(this, Tasks.builder().name("update-needed").body(new Runnable() {
+                @Override
+                public void run() {
+                    if (updateNeeded)
+                        AbstractControllerImpl.this.update();
+                } 
+            }).build());
+        }
+    }
+    
+    @Override
+    public void update() {
+        try {
+            Task<?> task = updateAsync();
+            if (task != null) task.getUnchecked();
+            ServiceStateLogic.ServiceProblemsLogic.clearProblemsIndicator(this, "update");
+        } catch (Exception e) {
+            ServiceStateLogic.ServiceProblemsLogic.updateProblemsIndicator(this, "update", "update failed with: "+Exceptions.collapseText(e));
+            throw Exceptions.propagate(e);
+        }
+    }
+    
+    public Task<?> updateAsync() {
+        synchronized (serverPoolAddresses) {
+            Task<?> result = null;
+            if (!isActive()) updateNeeded = true;
+            else {
+                updateNeeded = false;
+                LOG.debug("Updating {} in response to changes", this);
+                LOG.info("Updating {}, server pool targets {}", new Object[] {this, getAttribute(SERVER_POOL_TARGETS)});
+                reconfigureService();
+                LOG.debug("Reloading {} in response to changes", this);
+                // reload should happen synchronously
+                result = invoke(RELOAD);
+            }
+            return result;
+        }
+    }
+
+    protected void onServerPoolMemberChanged(Entity member) {
+        synchronized (serverPoolAddresses) {
+            if (LOG.isTraceEnabled()) LOG.trace("For {}, considering membership of {} which is in locations {}", 
+                new Object[] {this, member, member.getLocations()});
+            if (belongsInServerPool(member)) {
+                addServerPoolMember(member);
+            } else {
+                removeServerPoolMember(member);
+            }
+            if (LOG.isTraceEnabled()) LOG.trace("Done {} checkEntity {}", this, member);
+        }
+    }
+    
+    protected boolean belongsInServerPool(Entity member) {
+        if (!groovyTruth(member.getAttribute(Startable.SERVICE_UP))) {
+            if (LOG.isTraceEnabled()) LOG.trace("Members of {}, checking {}, eliminating because not up", this, member);
+            return false;
+        }
+        if (!getServerPool().getMembers().contains(member)) {
+            if (LOG.isTraceEnabled()) LOG.trace("Members of {}, checking {}, eliminating because not member", this, member);
+            return false;
+        }
+        if (LOG.isTraceEnabled()) LOG.trace("Members of {}, checking {}, approving", this, member);
+        return true;
+    }
+    
+    protected void addServerPoolMember(Entity member) {
+        synchronized (serverPoolAddresses) {
+            String oldAddress = getAttribute(SERVER_POOL_TARGETS).get(member);
+            String newAddress = getAddressOfEntity(member);
+            if (Objects.equal(newAddress, oldAddress)) {
+                if (LOG.isTraceEnabled())
+                    if (LOG.isTraceEnabled()) LOG.trace("Ignoring unchanged address {}", oldAddress);
+                return;
+            } else if (newAddress == null) {
+                LOG.info("Removing from {}, member {} with old address {}, because inferred address is now null", new Object[] {this, member, oldAddress});
+            } else {
+                if (oldAddress != null) {
+                    LOG.info("Replacing in {}, member {} with old address {}, new address {}", new Object[] {this, member, oldAddress, newAddress});
+                } else {
+                    LOG.info("Adding to {}, new member {} with address {}", new Object[] {this, member, newAddress});
+                }
+            }
+
+            if (Objects.equal(oldAddress, newAddress)) {
+                if (LOG.isTraceEnabled()) LOG.trace("For {}, ignoring change in member {} because address still {}", new Object[] {this, member, newAddress});
+                return;
+            }
+
+            // TODO this does it synchronously; an async method leaning on `updateNeeded` and `update` might
+            // be more appropriate, especially when this is used in a listener
+            MapAttribute.put(this, SERVER_POOL_TARGETS, member, newAddress);
+            updateAsync();
+        }
+    }
+    
+    protected void removeServerPoolMember(Entity member) {
+        synchronized (serverPoolAddresses) {
+            if (!getAttribute(SERVER_POOL_TARGETS).containsKey(member)) {
+                if (LOG.isTraceEnabled()) LOG.trace("For {}, not removing as don't have member {}", new Object[] {this, member});
+                return;
+            }
+
+            String address = MapAttribute.remove(this, SERVER_POOL_TARGETS, member);
+
+            LOG.info("Removing from {}, member {} with address {}", new Object[] {this, member, address});
+
+            updateAsync();
+        }
+    }
+    
+    protected String getAddressOfEntity(Entity member) {
+        AttributeSensor<String> hostAndPortSensor = getHostAndPortSensor();
+        if (hostAndPortSensor != null) {
+            String result = member.getAttribute(hostAndPortSensor);
+            if (result != null) {
+                return result;
+            } else {
+                LOG.error("No host:port set for {} (using attribute {}); skipping in {}", 
+                        new Object[] {member, hostAndPortSensor, this});
+                return null;
+            }
+        } else {
+            String ip = member.getAttribute(getHostnameSensor());
+            Integer port = member.getAttribute(getPortNumberSensor());
+            if (ip!=null && port!=null) {
+                return ip+":"+port;
+            }
+            LOG.error("Unable to construct hostname:port representation for {} ({}:{}); skipping in {}", 
+                    new Object[] {member, ip, port, this});
+            return null;
+        }
+    }
+
+    // Utilities for modifying an AttributeSensor of type map
+    private static class MapAttribute {
+        public static <K, V> V put(Entity entity, AttributeSensor<Map<K,V>> attribute, K key, V value) {
+            Map<K, V> oldMap = entity.getAttribute(attribute);
+            Map<K, V> newMap = MutableMap.copyOf(oldMap);
+            V oldVal = newMap.put(key, value);
+            ((EntityInternal)entity).setAttribute(attribute, newMap);
+            return oldVal;
+        }
+        
+        public static <K, V> V remove(Entity entity, AttributeSensor<Map<K,V>> attribute, K key) {
+            Map<K, V> oldMap = entity.getAttribute(attribute);
+            Map<K, V> newMap = MutableMap.copyOf(oldMap);
+            V oldVal = newMap.remove(key);
+            ((EntityInternal)entity).setAttribute(attribute, newMap);
+            return oldVal;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractNonProvisionedController.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractNonProvisionedController.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractNonProvisionedController.java
new file mode 100644
index 0000000..52eda51
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractNonProvisionedController.java
@@ -0,0 +1,28 @@
+/*
+ * 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.entity.proxy;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.proxying.ImplementedBy;
+
+@ImplementedBy(AbstractNonProvisionedControllerImpl.class)
+public interface AbstractNonProvisionedController extends LoadBalancer, Entity {
+
+    public boolean isActive();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractNonProvisionedControllerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractNonProvisionedControllerImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractNonProvisionedControllerImpl.java
new file mode 100644
index 0000000..d5cc688
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/AbstractNonProvisionedControllerImpl.java
@@ -0,0 +1,277 @@
+/*
+ * 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.entity.proxy;
+
+import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
+import static com.google.common.base.Preconditions.checkState;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.policy.PolicySpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.entity.group.AbstractMembershipTrackingPolicy;
+import brooklyn.entity.group.Cluster;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.feed.ConfigToAttributes;
+import brooklyn.location.Location;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.guava.Maybe;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+public abstract class AbstractNonProvisionedControllerImpl extends AbstractEntity implements AbstractNonProvisionedController {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractNonProvisionedControllerImpl.class);
+    
+    protected volatile boolean isActive;
+    protected volatile boolean updateNeeded = true;
+    
+    protected AbstractMembershipTrackingPolicy serverPoolMemberTrackerPolicy;
+    protected Set<String> serverPoolAddresses = Sets.newLinkedHashSet();
+    protected Map<Entity,String> serverPoolTargets = Maps.newLinkedHashMap();
+    
+    public AbstractNonProvisionedControllerImpl() {
+        this(MutableMap.of(), null, null);
+    }
+    public AbstractNonProvisionedControllerImpl(Map properties) {
+        this(properties, null, null);
+    }
+    public AbstractNonProvisionedControllerImpl(Entity parent) {
+        this(MutableMap.of(), parent, null);
+    }
+    public AbstractNonProvisionedControllerImpl(Map properties, Entity parent) {
+        this(properties, parent, null);
+    }
+    public AbstractNonProvisionedControllerImpl(Entity parent, Cluster cluster) {
+        this(MutableMap.of(), parent, cluster);
+    }
+    public AbstractNonProvisionedControllerImpl(Map properties, Entity parent, Cluster cluster) {
+    }
+
+    public static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy {
+        @Override protected void onEntityEvent(EventType type, Entity member) {
+            ((AbstractNonProvisionedControllerImpl)super.entity).onServerPoolMemberChanged(member);
+        }
+    }
+
+    /**
+     * Opportunity to do late-binding of the cluster that is being controlled. Must be called before start().
+     * Can pass in the 'serverPool'.
+     */
+    @Override
+    public void bind(Map<?,?> flags) {
+        if (flags.containsKey("serverPool")) {
+            setConfigEvenIfOwned(SERVER_POOL, (Group) flags.get("serverPool"));
+        }
+    }
+
+    @Override
+    public boolean isActive() {
+        return isActive;
+    }
+    
+    @Override
+    public void start(Collection<? extends Location> locations) {
+        preStart();
+    }
+
+    @Override
+    public void stop() {
+        preStop();
+    }
+    
+    protected void preStart() {
+        AttributeSensor<?> hostAndPortSensor = getConfig(HOST_AND_PORT_SENSOR);
+        Maybe<Object> hostnameSensor = getConfigRaw(HOSTNAME_SENSOR, true);
+        Maybe<Object> portSensor = getConfigRaw(PORT_NUMBER_SENSOR, true);
+        if (hostAndPortSensor != null) {
+            checkState(!hostnameSensor.isPresent() && !portSensor.isPresent(), 
+                    "Must not set %s and either of %s or %s", HOST_AND_PORT_SENSOR, HOSTNAME_SENSOR, PORT_NUMBER_SENSOR);
+        }
+        
+        ConfigToAttributes.apply(this);
+        addServerPoolMemberTrackerPolicy();
+    }
+    
+    protected void preStop() {
+        removeServerPoolMemberTrackerPolicy();
+    }
+        
+    protected void addServerPoolMemberTrackerPolicy() {
+        Group serverPool = getServerPool();
+        if (serverPool != null) {
+            serverPoolMemberTrackerPolicy = addPolicy(PolicySpec.create(MemberTrackingPolicy.class)
+                    .displayName("Controller targets tracker")
+                    .configure("group", serverPool));
+            
+            LOG.info("Added policy {} to {}, during start", serverPoolMemberTrackerPolicy, this);
+            
+            serverPoolAddresses.clear();
+            serverPoolTargets.clear();
+                
+            // Initialize ourselves immediately with the latest set of members; don't wait for
+            // listener notifications because then will be out-of-date for short period (causing 
+            // problems for rebind)
+            for (Entity member : getServerPool().getMembers()) {
+                if (belongsInServerPool(member)) {
+                    if (LOG.isTraceEnabled()) LOG.trace("Done {} checkEntity {}", this, member);
+                    String address = getAddressOfEntity(member);
+                    serverPoolTargets.put(member, address);
+                    if (address != null) {
+                        serverPoolAddresses.add(address);
+                    }
+                }
+            }
+            
+            LOG.info("Resetting {}, members {} with addresses {}", new Object[] {this, serverPoolTargets, serverPoolAddresses});
+            setAttribute(SERVER_POOL_TARGETS, serverPoolTargets);
+        }
+    }
+    
+    protected void removeServerPoolMemberTrackerPolicy() {
+        if (serverPoolMemberTrackerPolicy != null) {
+            removePolicy(serverPoolMemberTrackerPolicy);
+        }
+    }
+    
+    /** 
+     * Implementations should update the configuration so that 'serverPoolAddresses' are targeted.
+     * The caller will subsequently call reload to apply the new configuration.
+     */
+    protected abstract void reconfigureService();
+    
+    @Override
+    public synchronized void update() {
+        if (!isActive()) updateNeeded = true;
+        else {
+            updateNeeded = false;
+            LOG.debug("Updating {} in response to changes", this);
+            reconfigureService();
+            LOG.debug("Reloading {} in response to changes", this);
+            invoke(RELOAD);
+        }
+        setAttribute(SERVER_POOL_TARGETS, serverPoolTargets);
+    }
+    
+    protected synchronized void onServerPoolMemberChanged(Entity member) {
+        if (LOG.isTraceEnabled()) LOG.trace("For {}, considering membership of {} which is in locations {}", 
+                new Object[] {this, member, member.getLocations()});
+        if (belongsInServerPool(member)) {
+            addServerPoolMember(member);
+        } else {
+            removeServerPoolMember(member);
+        }
+        if (LOG.isTraceEnabled()) LOG.trace("Done {} checkEntity {}", this, member);
+    }
+    
+    protected boolean belongsInServerPool(Entity member) {
+        if (!groovyTruth(member.getAttribute(Startable.SERVICE_UP))) {
+            if (LOG.isTraceEnabled()) LOG.trace("Members of {}, checking {}, eliminating because not up", this, member);
+            return false;
+        }
+        if (!getServerPool().getMembers().contains(member)) {
+            if (LOG.isTraceEnabled()) LOG.trace("Members of {}, checking {}, eliminating because not member", this, member);
+            return false;
+        }
+        if (LOG.isTraceEnabled()) LOG.trace("Members of {}, checking {}, approving", this, member);
+        return true;
+    }
+    
+    private Group getServerPool() {
+        return getConfig(SERVER_POOL);
+    }
+    
+    protected AttributeSensor<Integer> getPortNumberSensor() {
+        return getAttribute(PORT_NUMBER_SENSOR);
+    }
+    
+    protected AttributeSensor<String> getHostnameSensor() {
+        return getAttribute(HOSTNAME_SENSOR);
+    }
+
+    protected AttributeSensor<String> getHostAndPortSensor() {
+        return getAttribute(HOST_AND_PORT_SENSOR);
+    }
+
+    protected synchronized void addServerPoolMember(Entity member) {
+        if (serverPoolTargets.containsKey(member)) {
+            if (LOG.isTraceEnabled()) LOG.trace("For {}, not adding as already have member {}", new Object[] {this, member});
+            return;
+        }
+        
+        String address = getAddressOfEntity(member);
+        if (address != null) {
+            serverPoolAddresses.add(address);
+        }
+
+        LOG.info("Adding to {}, new member {} with address {}", new Object[] {this, member, address});
+        
+        update();
+        serverPoolTargets.put(member, address);
+    }
+    
+    protected synchronized void removeServerPoolMember(Entity member) {
+        if (!serverPoolTargets.containsKey(member)) {
+            if (LOG.isTraceEnabled()) LOG.trace("For {}, not removing as don't have member {}", new Object[] {this, member});
+            return;
+        }
+        
+        String address = serverPoolTargets.get(member);
+        if (address != null) {
+            serverPoolAddresses.remove(address);
+        }
+        
+        LOG.info("Removing from {}, member {} with address {}", new Object[] {this, member, address});
+        
+        update();
+        serverPoolTargets.remove(member);
+    }
+    
+    protected String getAddressOfEntity(Entity member) {
+        AttributeSensor<String> hostAndPortSensor = getHostAndPortSensor();
+        if (hostAndPortSensor != null) {
+            String result = member.getAttribute(hostAndPortSensor);
+            if (result != null) {
+                return result;
+            } else {
+                LOG.error("No host:port set for {} (using attribute {}); skipping in {}", 
+                        new Object[] {member, hostAndPortSensor, this});
+                return null;
+            }
+        } else {
+            String ip = member.getAttribute(getHostnameSensor());
+            Integer port = member.getAttribute(getPortNumberSensor());
+            if (ip!=null && port!=null) {
+                return ip+":"+port;
+            }
+            LOG.error("Unable to construct hostname:port representation for {} ({}:{}); skipping in {}", 
+                    new Object[] {member, ip, port, this});
+            return null;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancer.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancer.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancer.java
new file mode 100644
index 0000000..f29ea41
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancer.java
@@ -0,0 +1,125 @@
+/*
+ * 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.entity.proxy;
+
+import java.net.URI;
+import java.util.Map;
+
+import org.apache.brooklyn.entity.webapp.WebAppService;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.annotation.Effector;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.MethodEffector;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
+import brooklyn.event.basic.Sensors;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * A load balancer that routes requests to set(s) of servers.
+ * 
+ * There is an optional "serverPool" that will have requests routed to it (e.g. as round-robin). 
+ * This is a group whose members are appropriate servers; membership of that group will be tracked 
+ * to automatically update the load balancer's configuration as appropriate.
+ * 
+ * There is an optional urlMappings group for defining additional mapping rules. Members of this
+ * group (of type UrlMapping) will be tracked, to automatically update the load balancer's configuration.
+ * The UrlMappings can give custom routing rules so that specific urls are routed (and potentially re-written)
+ * to particular sets of servers. 
+ * 
+ * @author aled
+ */
+public interface LoadBalancer extends Entity, Startable {
+
+    @SetFromFlag("serverPool")
+    ConfigKey<Group> SERVER_POOL = new BasicConfigKey<Group>(
+            Group.class, "loadbalancer.serverpool", "The default servers to route messages to");
+
+    @SetFromFlag("urlMappings")
+    ConfigKey<Group> URL_MAPPINGS = new BasicConfigKey<Group>(
+            Group.class, "loadbalancer.urlmappings", "Special mapping rules (e.g. for domain/path matching, rewrite, etc); not supported by all load balancers");
+    
+    /** sensor for port to forward to on target entities */
+    @SuppressWarnings("serial")
+    @SetFromFlag("portNumberSensor")
+    public static final BasicAttributeSensorAndConfigKey<AttributeSensor<Integer>> PORT_NUMBER_SENSOR = new BasicAttributeSensorAndConfigKey<AttributeSensor<Integer>>(
+        new TypeToken<AttributeSensor<Integer>>() {}, "member.sensor.portNumber", "Port number sensor on members (defaults to http.port; not supported in all implementations)", Attributes.HTTP_PORT);
+
+    /** sensor for hostname to forward to on target entities */
+    @SuppressWarnings("serial")
+    @SetFromFlag("hostnameSensor")
+    public static final BasicAttributeSensorAndConfigKey<AttributeSensor<String>> HOSTNAME_SENSOR = new BasicAttributeSensorAndConfigKey<AttributeSensor<String>>(
+        new TypeToken<AttributeSensor<String>>() {}, "member.sensor.hostname", "Hostname/IP sensor on members (defaults to host.subnet.hostname; not supported in all implementations)", Attributes.SUBNET_HOSTNAME);
+
+    /** sensor for hostname to forward to on target entities */
+    @SuppressWarnings("serial")
+    @SetFromFlag("hostAndPortSensor")
+    public static final BasicAttributeSensorAndConfigKey<AttributeSensor<String>> HOST_AND_PORT_SENSOR = new BasicAttributeSensorAndConfigKey<AttributeSensor<String>>(
+            new TypeToken<AttributeSensor<String>>() {}, "member.sensor.hostandport", "host:port sensor on members (invalid to configure this and the portNumber or hostname sensors)", null);
+    
+    @SetFromFlag("port")
+    /** port where this controller should live */
+    public static final PortAttributeSensorAndConfigKey PROXY_HTTP_PORT = new PortAttributeSensorAndConfigKey(
+            "proxy.http.port", "Main port where this proxy listens if using HTTP", ImmutableList.of(8000, "8001+"));
+
+    @SetFromFlag("httpsPort")
+    /** port where this controller should live */
+    public static final PortAttributeSensorAndConfigKey PROXY_HTTPS_PORT = new PortAttributeSensorAndConfigKey(
+            "proxy.https.port", "Main port where this proxy listens if using HTTPS", ImmutableList.of(8443, "8443+"));
+
+    @SetFromFlag("protocol")
+    public static final BasicAttributeSensorAndConfigKey<String> PROTOCOL = new BasicAttributeSensorAndConfigKey<String>(
+            String.class, "proxy.protocol", "Main URL protocol this proxy answers (typically http or https)", null);
+    
+    public static final AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME;
+    
+    public static final AttributeSensor<URI> MAIN_URI = Attributes.MAIN_URI;
+    public static final AttributeSensor<String> ROOT_URL = WebAppService.ROOT_URL;
+
+    @SuppressWarnings("serial")
+    public static final AttributeSensor<Map<Entity, String>> SERVER_POOL_TARGETS = Sensors.newSensor(
+            new TypeToken<Map<Entity, String>>() {},
+            "proxy.serverpool.targets", 
+            "The downstream targets in the server pool");
+    
+    public static final MethodEffector<Void> RELOAD = new MethodEffector<Void>(LoadBalancer.class, "reload");
+    
+    public static final MethodEffector<Void> UPDATE = new MethodEffector<Void>(LoadBalancer.class, "update");
+
+    @Effector(description="Forces reload of the configuration")
+    public void reload();
+
+    @Effector(description="Updates the entities configuration, and then forces reload of that configuration")
+    public void update();
+    
+    /**
+     * Opportunity to do late-binding of the cluster that is being controlled. Must be called before start().
+     * Can pass in the 'serverPool'.
+     */
+    public void bind(Map<?,?> flags);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancerCluster.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancerCluster.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancerCluster.java
new file mode 100644
index 0000000..2e22e64
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancerCluster.java
@@ -0,0 +1,37 @@
+/*
+ * 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.entity.proxy;
+
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.ImplementedBy;
+
+/**
+ * A cluster of load balancers, where configuring the cluster (through the LoadBalancer interface)
+ * will configure all load balancers in the cluster.
+ * 
+ * Config keys (such as LoadBalancer.serverPool and LoadBalancer.urlMappings) are automatically
+ * inherited by the children of the load balancer cluster. It is through that mechanism that
+ * configuration changes on the cluster will be applied to all child load balancers (i.e. by
+ * them all sharing the same serverPool and urlMappings etc).
+ *  
+ * @author aled
+ */
+@ImplementedBy(LoadBalancerClusterImpl.class)
+public interface LoadBalancerCluster extends DynamicCluster, LoadBalancer {
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancerClusterImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancerClusterImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancerClusterImpl.java
new file mode 100644
index 0000000..f833eb7
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/LoadBalancerClusterImpl.java
@@ -0,0 +1,76 @@
+/*
+ * 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.entity.proxy;
+
+import java.util.Map;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.group.DynamicClusterImpl;
+
+/**
+ * A cluster of load balancers, where configuring the cluster (through the LoadBalancer interface)
+ * will configure all load balancers in the cluster.
+ * 
+ * Config keys (such as LoadBalancer.serverPool and LoadBalancer.urlMappings) are automatically
+ * inherited by the children of the load balancer cluster. It is through that mechanism that
+ * configuration changes on the cluster will be applied to all child load balancers (i.e. by
+ * them all sharing the same serverPool and urlMappings etc).
+ *  
+ * @author aled
+ */
+public class LoadBalancerClusterImpl extends DynamicClusterImpl implements LoadBalancerCluster {
+
+    // TODO I suspect there are races with reconfiguring the load-balancers while
+    // the cluster is growing: there is no synchronization around the calls to reload
+    // and the resize, so presumably there's a race where a newly added load-balancer 
+    // could miss the most recent reload call?
+
+    public LoadBalancerClusterImpl() {
+        super();
+    }
+
+    /* NOTE The following methods come from {@link LoadBalancer} but are probably safe to ignore */
+    
+    @Override
+    public void reload() {
+        for (Entity member : getMembers()) {
+            if (member instanceof LoadBalancer) {
+                ((LoadBalancer)member).reload();
+            }
+        }
+    }
+
+    @Override
+    public void update() {
+        for (Entity member : getMembers()) {
+            if (member instanceof LoadBalancer) {
+                ((LoadBalancer)member).update();
+            }
+        }
+    }
+
+    @Override
+    public void bind(Map<?,?> flags) {
+        for (Entity member : getMembers()) {
+            if (member instanceof LoadBalancer) {
+                ((LoadBalancer)member).bind(flags);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/ProxySslConfig.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/ProxySslConfig.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/ProxySslConfig.java
new file mode 100644
index 0000000..93e1b7f
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/ProxySslConfig.java
@@ -0,0 +1,219 @@
+/*
+ * 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.entity.proxy;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.flags.FlagUtils;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.base.Objects;
+
+public class ProxySslConfig implements Serializable {
+
+    private static final long serialVersionUID = -2692754611458939617L;
+
+    private static final Logger log = LoggerFactory.getLogger(ProxySslConfig.class);
+    
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+        @SetFromFlag protected String certificateSourceUrl;
+        @SetFromFlag protected String keySourceUrl;
+        @SetFromFlag protected String certificateDestination;
+        @SetFromFlag protected String keyDestination;
+        @SetFromFlag protected boolean targetIsSsl = false;
+        @SetFromFlag protected boolean reuseSessions = false;
+
+        public Builder certificateSourceUrl(String val) {
+            certificateSourceUrl = val; return this;
+        }
+        public Builder keySourceUrl(String val) {
+            keySourceUrl = val; return this;
+        }
+        public Builder certificateDestination(String val) {
+            certificateDestination = val; return this;
+        }
+        public Builder keyDestination(String val) {
+            keyDestination = val; return this;
+        }
+        public Builder targetIsSsl(boolean val) {
+            targetIsSsl = val; return this;
+        }
+        public Builder reuseSessions(boolean val) {
+            reuseSessions = val; return this;
+        }
+        public ProxySslConfig build() {
+            ProxySslConfig result = new ProxySslConfig(this);
+            return result;
+        }
+    }
+    
+    public static ProxySslConfig fromMap(Map<?,?> map) {
+        Builder b = new Builder();
+        Map<?, ?> unused = FlagUtils.setFieldsFromFlags(map, b);
+        if (!unused.isEmpty()) log.warn("Unused flags when populating "+b+" (ignoring): "+unused);
+        return b.build();
+    }
+
+    private String certificateSourceUrl;
+    private String keySourceUrl;
+    private String certificateDestination;
+    private String keyDestination;
+    private boolean targetIsSsl = false;
+    private boolean reuseSessions = false;
+
+    public ProxySslConfig() { }
+
+    protected ProxySslConfig(Builder builder) {
+        certificateSourceUrl = builder.certificateSourceUrl;
+        keySourceUrl = builder.keySourceUrl;
+        certificateDestination = builder.certificateDestination;
+        keyDestination = builder.keyDestination;
+        targetIsSsl = builder.targetIsSsl;
+        reuseSessions = builder.reuseSessions;
+    }
+
+    /**
+     * URL for the SSL certificates required at the server.
+     * <p>
+     * Corresponding nginx settings:
+     * <pre>
+     *     ssl                  on;
+     *     ssl_certificate      www.example.com.crt;
+     *     ssl_certificate_key  www.example.com.key;
+     * </pre>
+     * Okay (in nginx) for key to be null if certificate contains both as per setup at
+     * http://nginx.org/en/docs/http/configuring_https_servers.html
+     * <p>
+     * Proxy object can be set on nginx instance to apply site-wide,
+     * and to put multiple servers in the certificate file
+     * <p>
+     * The brooklyn entity will install the certificate/key(s) on the server.
+     * (however it will not currently merge multiple certificates.
+     * if conflicting certificates are attempted to be installed nginx will complain.)
+     */
+    public String getCertificateSourceUrl() {
+        return certificateSourceUrl;
+    }
+
+    public void setCertificateSourceUrl(String certificateSourceUrl) {
+        this.certificateSourceUrl = certificateSourceUrl;
+    }
+
+    /** @see #getCertificateSourceUrl()} */
+    public String getKeySourceUrl() {
+        return keySourceUrl;
+    }
+
+    public void setKeySourceUrl(String keySourceUrl) {
+        this.keySourceUrl = keySourceUrl;
+    }
+
+    /**
+     * Sets the {@code ssl_certificate_path} to be used within the generated
+     * {@link LoadBalancer} configuration.
+     * <p>
+     * If set to null, Brooklyn will use an auto generated path.
+     * <p>
+     * If {@link #getCertificateSourceUrl() certificateSourceUrl} is set     *
+     * then Brooklyn will copy the certificate the destination.
+     * <p>
+     * Setting this field is useful if there is a {@code certificate} on the
+     * nginx machine you want to make use of.
+     */
+    public String getCertificateDestination() {
+        return certificateDestination;
+    }
+
+    public void setCertificateDestination(String certificateDestination) {
+        this.certificateDestination = certificateDestination;
+    }
+
+    /**
+     * Sets the {@code ssl_certificate_key} path to be used within the generated
+     * {@link LoadBalancer} configuration.
+     * <p>
+     * If set to null, Brooklyn will use an auto generated path.
+     * <p>
+     * If {@link #getKeySourceUrl() keySourceUrl} is set then Brooklyn will copy the
+     * certificate to the destination.
+     * <p>
+     * Setting this field is useful if there is a {@code certificate_key} on the
+     * nginx machine you want to make use of.
+     */
+    public String getKeyDestination() {
+        return keyDestination;
+    }
+
+    public void setKeyDestination(String keyDestination) {
+        this.keyDestination = keyDestination;
+    }
+
+    /**
+     * Whether the downstream server (if mapping) also expects https; default false.
+     */
+    public boolean getTargetIsSsl() {
+        return targetIsSsl;
+    }
+
+    public void setTargetIsSsl(boolean targetIsSsl) {
+        this.targetIsSsl = targetIsSsl;
+    }
+
+    /**
+     * Whether to reuse SSL validation in the server (performance).
+     * <p>
+     * Corresponds to nginx setting {@code proxy_ssl_session_reuse on|off}.
+     */
+    public boolean getReuseSessions() {
+        return reuseSessions;
+    }
+
+    public void setReuseSessions(boolean reuseSessions) {
+        this.reuseSessions = reuseSessions;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(certificateSourceUrl, keySourceUrl, certificateDestination, keyDestination, reuseSessions, targetIsSsl);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ProxySslConfig other = (ProxySslConfig) obj;
+
+        return Objects.equal(certificateSourceUrl, other.certificateSourceUrl) &&
+                Objects.equal(certificateDestination, other.certificateDestination) &&
+                Objects.equal(keyDestination, other.keyDestination) &&
+                Objects.equal(keySourceUrl, other.keySourceUrl) &&
+                Objects.equal(reuseSessions, other.reuseSessions) &&
+                Objects.equal(targetIsSsl, other.targetIsSsl);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxConfigFileGenerator.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxConfigFileGenerator.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxConfigFileGenerator.java
new file mode 100644
index 0000000..00723dc
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxConfigFileGenerator.java
@@ -0,0 +1,33 @@
+/*
+ * 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.entity.proxy.nginx;
+
+/**
+ * Generates a {@code server.conf} configuration file for an {@link NginxController}.
+ */
+public interface NginxConfigFileGenerator {
+
+    /**
+     * Entry point for the generator.
+     *
+     * @return The contents of the {@code server.conf} file
+     */
+    String generateConfigFile(NginxDriver driver, NginxController entity);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxController.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxController.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxController.java
new file mode 100644
index 0000000..84e8588
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxController.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.entity.proxy.nginx;
+
+import java.util.Map;
+
+import org.apache.brooklyn.catalog.Catalog;
+import org.apache.brooklyn.entity.proxy.AbstractController;
+import org.apache.brooklyn.entity.proxy.ProxySslConfig;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.annotation.Effector;
+import brooklyn.entity.annotation.EffectorParam;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.MethodEffector;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.entity.trait.HasShortName;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.event.basic.Sensors;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * An entity that represents an Nginx proxy (e.g. for routing requests to servers in a cluster).
+ * <p>
+ * The default driver *builds* nginx from source (because binaries are not reliably available, esp not with sticky sessions).
+ * This requires gcc and other build tools installed. The code attempts to install them but inevitably 
+ * this entity may be more finicky about the OS/image where it runs than others.
+ * <p>
+ * Paritcularly on OS X we require Xcode and command-line gcc installed and on the path.
+ * <p>
+ * See {@link http://library.linode.com/web-servers/nginx/configuration/basic} for useful info/examples
+ * of configuring nginx.
+ * <p>
+ * https configuration is supported, with the certificates providable on a per-UrlMapping basis or a global basis.
+ * (not supported to define in both places.) 
+ * per-Url is useful if different certificates are used for different server names,
+ * or different ports if that is supported.
+ * see more info on Ssl in {@link ProxySslConfig}.
+ */
+@Catalog(name="Nginx Server", description="A single Nginx server. Provides HTTP and reverse proxy services", iconUrl="classpath:///nginx-logo.jpeg")
+@ImplementedBy(NginxControllerImpl.class)
+public interface NginxController extends AbstractController, HasShortName {
+
+    MethodEffector<String> GET_CURRENT_CONFIGURATION =
+            new MethodEffector<String>(NginxController.class, "getCurrentConfiguration");
+
+    MethodEffector<Void> DEPLOY =
+            new MethodEffector<Void>(NginxController.class, "deploy");
+    
+    @SetFromFlag("version")
+    ConfigKey<String> SUGGESTED_VERSION =
+            ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "1.8.0");
+
+    @SetFromFlag("stickyVersion")
+    ConfigKey<String> STICKY_VERSION = ConfigKeys.newStringConfigKey(
+            "nginx.sticky.version", "Version of ngnix-sticky-module to be installed, if required", "1.2.5");
+
+    @SetFromFlag("pcreVersion")
+    ConfigKey<String> PCRE_VERSION = ConfigKeys.newStringConfigKey(
+            "pcre.version", "Version of PCRE to be installed, if required", "8.37");
+
+    @SetFromFlag("downloadUrl")
+    BasicAttributeSensorAndConfigKey<String> DOWNLOAD_URL = new BasicAttributeSensorAndConfigKey<String>(
+            SoftwareProcess.DOWNLOAD_URL, "http://nginx.org/download/nginx-${version}.tar.gz");
+
+    @SetFromFlag("downloadAddonUrls")
+    BasicAttributeSensorAndConfigKey<Map<String,String>> DOWNLOAD_ADDON_URLS = new BasicAttributeSensorAndConfigKey<Map<String,String>>(
+            SoftwareProcess.DOWNLOAD_ADDON_URLS, ImmutableMap.of(
+                    "stickymodule", "https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/get/${addonversion}.tar.gz",
+                    "pcre", "ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-${addonversion}.tar.gz"));
+
+    @SetFromFlag("sticky")
+    ConfigKey<Boolean> STICKY = ConfigKeys.newBooleanConfigKey(
+            "nginx.sticky", "Whether to use sticky sessions", true);
+
+    @SetFromFlag("httpPollPeriod")
+    ConfigKey<Long> HTTP_POLL_PERIOD = ConfigKeys.newLongConfigKey(
+            "nginx.sensorpoll.http", "Poll period (in milliseconds)", 1000L);
+
+    @SetFromFlag("withLdOpt")
+    ConfigKey<String> WITH_LD_OPT = ConfigKeys.newStringConfigKey(
+            "nginx.install.withLdOpt", "String to pass in with --with-ld-opt=\"<val>\" (and for OS X has pcre auto-appended to this)", "-L /usr/local/lib");
+
+    @SetFromFlag("withCcOpt")
+    ConfigKey<String> WITH_CC_OPT = ConfigKeys.newStringConfigKey(
+            "nginx.install.withCcOpt", "String to pass in with --with-cc-opt=\"<val>\"", "-I /usr/local/include");
+
+    @SetFromFlag("configGenerator")
+    ConfigKey<NginxConfigFileGenerator> SERVER_CONF_GENERATOR = ConfigKeys.newConfigKey(NginxConfigFileGenerator.class,
+            "nginx.config.generator", "The server.conf generator class", new NginxDefaultConfigGenerator());
+
+    @SetFromFlag("configTemplate")
+    ConfigKey<String> SERVER_CONF_TEMPLATE_URL = NginxTemplateConfigGenerator.SERVER_CONF_TEMPLATE_URL;
+
+    @SetFromFlag("staticContentArchive")
+    ConfigKey<String> STATIC_CONTENT_ARCHIVE_URL = ConfigKeys.newStringConfigKey(
+            "nginx.config.staticContentArchiveUrl", "The URL of an archive file of static content (To be copied to the server)");
+
+    BasicAttributeSensorAndConfigKey<String> ACCESS_LOG_LOCATION = new BasicAttributeSensorAndConfigKey<String>(String.class,
+            "nginx.log.access", "Nginx access log file location", "logs/access.log");
+
+    BasicAttributeSensorAndConfigKey<String> ERROR_LOG_LOCATION = new BasicAttributeSensorAndConfigKey<String>(String.class,
+            "nginx.log.error", "Nginx error log file location", "logs/error.log");
+
+    boolean isSticky();
+
+    @Effector(description="Gets the current server configuration (by brooklyn recalculating what the config should be); does not affect the server")
+    String getCurrentConfiguration();
+
+    @Effector(description="Deploys an archive of static content to the server")
+    void deploy(@EffectorParam(name="archiveUrl", description="The URL of the static content archive to deploy") String archiveUrl);
+
+    String getConfigFile();
+
+    Iterable<UrlMapping> getUrlMappings();
+
+    boolean appendSslConfig(String id, StringBuilder out, String prefix, ProxySslConfig ssl, boolean sslBlock, boolean certificateBlock);
+    
+    public static final AttributeSensor<Boolean> NGINX_URL_ANSWERS_NICELY = Sensors.newBooleanSensor( "nginx.url.answers.nicely");
+    public static final AttributeSensor<String> PID_FILE = Sensors.newStringSensor( "nginx.pid.file", "PID file");
+    
+    public interface NginxControllerInternal {
+        public void doExtraConfigurationDuringStart();
+    }
+
+}