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