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();
+ }
+
+}