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:38 UTC
[20/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/proxy/nginx/NginxControllerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxControllerImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxControllerImpl.java
new file mode 100644
index 0000000..6b7a921
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxControllerImpl.java
@@ -0,0 +1,370 @@
+/*
+ * 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.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.entity.proxy.AbstractControllerImpl;
+import org.apache.brooklyn.entity.proxy.ProxySslConfig;
+import org.apache.brooklyn.entity.proxy.nginx.NginxController.NginxControllerInternal;
+import org.apache.brooklyn.management.SubscriptionHandle;
+import org.apache.brooklyn.policy.PolicySpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.enricher.Enrichers;
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.annotation.Effector;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic.ServiceNotUpLogic;
+import brooklyn.entity.group.AbstractMembershipTrackingPolicy;
+import brooklyn.event.SensorEvent;
+import brooklyn.event.SensorEventListener;
+import brooklyn.event.feed.ConfigToAttributes;
+import brooklyn.event.feed.http.HttpFeed;
+import brooklyn.event.feed.http.HttpPollConfig;
+import brooklyn.event.feed.http.HttpValueFunctions;
+import brooklyn.util.ResourceUtils;
+import brooklyn.util.file.ArchiveUtils;
+import brooklyn.util.guava.Functionals;
+import brooklyn.util.http.HttpTool;
+import brooklyn.util.http.HttpToolResponse;
+import brooklyn.util.stream.Streams;
+import brooklyn.util.text.Strings;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+
+/**
+ * Implementation of the {@link NginxController} entity.
+ */
+public class NginxControllerImpl extends AbstractControllerImpl implements NginxController, NginxControllerInternal {
+
+ private static final Logger LOG = LoggerFactory.getLogger(NginxControllerImpl.class);
+
+ private volatile HttpFeed httpFeed;
+ private final Set<String> installedKeysCache = Sets.newLinkedHashSet();
+ protected UrlMappingsMemberTrackerPolicy urlMappingsMemberTrackerPolicy;
+ protected SubscriptionHandle targetAddressesHandler;
+
+ @Override
+ public void reload() {
+ NginxSshDriver driver = (NginxSshDriver)getDriver();
+ if (driver==null) {
+ Lifecycle state = getAttribute(NginxController.SERVICE_STATE_ACTUAL);
+ throw new IllegalStateException("Cannot reload (no driver instance; stopped? (state="+state+")");
+ }
+
+ driver.reload();
+ }
+
+ @Override
+ public boolean isSticky() {
+ return getConfig(STICKY);
+ }
+
+ private class UrlInferencer implements Supplier<URI> {
+ private Map<String, String> parameters;
+ private UrlInferencer(Map<String,String> parameters) {
+ this.parameters = parameters;
+ }
+ @Override public URI get() {
+ String baseUrl = inferUrl(true);
+ if (parameters==null || parameters.isEmpty())
+ return URI.create(baseUrl);
+ return URI.create(baseUrl+"?"+HttpTool.encodeUrlParams(parameters));
+ }
+ }
+
+ @Override
+ public void connectSensors() {
+ super.connectSensors();
+
+ ConfigToAttributes.apply(this);
+
+ // "up" is defined as returning a valid HTTP response from nginx (including a 404 etc)
+ httpFeed = addFeed(HttpFeed.builder()
+ .uniqueTag("nginx-poll")
+ .entity(this)
+ .period(getConfig(HTTP_POLL_PERIOD))
+ .baseUri(new UrlInferencer(null))
+ .poll(new HttpPollConfig<Boolean>(NGINX_URL_ANSWERS_NICELY)
+ // Any response from Nginx is good.
+ .checkSuccess(Predicates.alwaysTrue())
+ // Accept any nginx response (don't assert specific version), so that sub-classing
+ // for a custom nginx build is not strict about custom version numbers in headers
+ .onResult(HttpValueFunctions.containsHeader("Server"))
+ .setOnException(false)
+ .suppressDuplicates(true))
+ .build());
+
+ // TODO PERSISTENCE WORKAROUND kept anonymous function in case referenced in persisted state
+ new Function<HttpToolResponse, Boolean>() {
+ @Override
+ public Boolean apply(HttpToolResponse input) {
+ // Accept any nginx response (don't assert specific version), so that sub-classing
+ // for a custom nginx build is not strict about custom version numbers in headers
+ List<String> actual = input.getHeaderLists().get("Server");
+ return actual != null && actual.size() == 1;
+ }
+ };
+
+ if (!Lifecycle.RUNNING.equals(getAttribute(SERVICE_STATE_ACTUAL))) {
+ // TODO when updating the map, if it would change from empty to empty on a successful run
+ // gate with the above check to prevent flashing on ON_FIRE during rebind (this is invoked on rebind as well as during start)
+ ServiceNotUpLogic.updateNotUpIndicator(this, NGINX_URL_ANSWERS_NICELY, "No response from nginx yet");
+ }
+ addEnricher(Enrichers.builder().updatingMap(Attributes.SERVICE_NOT_UP_INDICATORS)
+ .uniqueTag("not-up-unless-url-answers")
+ .from(NGINX_URL_ANSWERS_NICELY)
+ .computing(Functionals.ifNotEquals(true).value("URL where nginx listens is not answering correctly (with expected header)") )
+ .build());
+ connectServiceUpIsRunning();
+
+ // Can guarantee that parent/managementContext has been set
+ Group urlMappings = getConfig(URL_MAPPINGS);
+ if (urlMappings!=null && urlMappingsMemberTrackerPolicy==null) {
+ // Listen to the targets of each url-mapping changing
+ targetAddressesHandler = subscribeToMembers(urlMappings, UrlMapping.TARGET_ADDRESSES, new SensorEventListener<Collection<String>>() {
+ @Override public void onEvent(SensorEvent<Collection<String>> event) {
+ updateNeeded();
+ }
+ });
+
+ // Listen to url-mappings being added and removed
+ urlMappingsMemberTrackerPolicy = addPolicy(PolicySpec.create(UrlMappingsMemberTrackerPolicy.class)
+ .configure("group", urlMappings));
+ }
+ }
+
+ protected void removeUrlMappingsMemberTrackerPolicy() {
+ if (urlMappingsMemberTrackerPolicy != null) {
+ removePolicy(urlMappingsMemberTrackerPolicy);
+ urlMappingsMemberTrackerPolicy = null;
+ }
+ Group urlMappings = getConfig(URL_MAPPINGS);
+ if (urlMappings!=null && targetAddressesHandler!=null) {
+ unsubscribe(urlMappings, targetAddressesHandler);
+ targetAddressesHandler = null;
+ }
+ }
+
+ public static class UrlMappingsMemberTrackerPolicy extends AbstractMembershipTrackingPolicy {
+ @Override
+ protected void onEntityEvent(EventType type, Entity entity) {
+ // relies on policy-rebind injecting the implementation rather than the dynamic-proxy
+ ((NginxControllerImpl)super.entity).updateNeeded();
+ }
+ }
+
+ @Override
+ protected void preStop() {
+ super.preStop();
+ removeUrlMappingsMemberTrackerPolicy();
+ }
+
+ @Override
+ protected void postStop() {
+ // TODO don't want stop to race with the last poll.
+ super.postStop();
+ setAttribute(SERVICE_UP, false);
+ }
+
+ @Override
+ protected void disconnectSensors() {
+ if (httpFeed != null) httpFeed.stop();
+ disconnectServiceUpIsRunning();
+ super.disconnectSensors();
+ }
+
+ @Override
+ public Class<?> getDriverInterface() {
+ return NginxDriver.class;
+ }
+
+ @Override
+ public NginxDriver getDriver() {
+ return (NginxDriver) super.getDriver();
+ }
+
+ public void doExtraConfigurationDuringStart() {
+ computePortsAndUrls();
+ reconfigureService();
+ // reconnect sensors if ports have changed
+ connectSensors();
+ }
+
+ @Override
+ @Effector(description="Gets the current server configuration (by brooklyn recalculating what the config should be); does not affect the server")
+ public String getCurrentConfiguration() {
+ return getConfigFile();
+ }
+
+ @Override
+ @Effector(description="Deploys an archive of static content to the server")
+ public void deploy(String archiveUrl) {
+ NginxSshDriver driver = (NginxSshDriver) getDriver();
+ if (driver==null) {
+ if (LOG.isDebugEnabled())
+ LOG.debug("No driver for {}, so not deploying archive (is entity stopping? state={})",
+ this, getAttribute(NginxController.SERVICE_STATE_ACTUAL));
+ return;
+ }
+
+ // Copy to the destination machine and extract contents
+ ArchiveUtils.deploy(archiveUrl, driver.getMachine(), driver.getRunDir());
+ }
+
+ @Override
+ public void reconfigureService() {
+ String cfg = getConfigFile();
+ if (cfg == null) return;
+
+ if (LOG.isDebugEnabled()) LOG.debug("Reconfiguring {}, targetting {} and {}", new Object[] {this, getServerPoolAddresses(), getUrlMappings()});
+ if (LOG.isTraceEnabled()) LOG.trace("Reconfiguring {}, config file:\n{}", this, cfg);
+
+ NginxSshDriver driver = (NginxSshDriver) getDriver();
+ if (!driver.isCustomizationCompleted()) {
+ if (LOG.isDebugEnabled()) LOG.debug("Reconfiguring {}, but driver's customization not yet complete so aborting", this);
+ return;
+ }
+
+ driver.getMachine().copyTo(Streams.newInputStreamWithContents(cfg), driver.getRunDir()+"/conf/server.conf");
+
+ installSslKeys("global", getSslConfig());
+
+ for (UrlMapping mapping : getUrlMappings()) {
+ //cache ensures only the first is installed, which is what is assumed below
+ installSslKeys(mapping.getDomain(), mapping.getConfig(UrlMapping.SSL_CONFIG));
+ }
+ }
+
+ /**
+ * Installs SSL keys named as {@code id.crt} and {@code id.key} where nginx can find them.
+ * <p>
+ * Currently skips re-installs (does not support changing)
+ */
+ public void installSslKeys(String id, ProxySslConfig ssl) {
+ if (ssl == null) return;
+
+ if (installedKeysCache.contains(id)) return;
+
+ NginxSshDriver driver = (NginxSshDriver) getDriver();
+
+ if (!Strings.isEmpty(ssl.getCertificateSourceUrl())) {
+ String certificateDestination = Strings.isEmpty(ssl.getCertificateDestination()) ? driver.getRunDir() + "/conf/" + id + ".crt" : ssl.getCertificateDestination();
+ driver.getMachine().copyTo(ImmutableMap.of("permissions", "0600"),
+ ResourceUtils.create(this).getResourceFromUrl(ssl.getCertificateSourceUrl()),
+ certificateDestination);
+ }
+
+ if (!Strings.isEmpty(ssl.getKeySourceUrl())) {
+ String keyDestination = Strings.isEmpty(ssl.getKeyDestination()) ? driver.getRunDir() + "/conf/" + id + ".key" : ssl.getKeyDestination();
+ driver.getMachine().copyTo(ImmutableMap.of("permissions", "0600"),
+ ResourceUtils.create(this).getResourceFromUrl(ssl.getKeySourceUrl()),
+ keyDestination);
+ }
+
+ installedKeysCache.add(id);
+ }
+
+ @Override
+ public String getConfigFile() {
+ NginxSshDriver driver = (NginxSshDriver) getDriver();
+ if (driver==null) {
+ LOG.debug("No driver for {}, so not generating config file (is entity stopping? state={})",
+ this, getAttribute(NginxController.SERVICE_STATE_ACTUAL));
+ return null;
+ }
+
+ NginxConfigFileGenerator templateGenerator = getConfig(NginxController.SERVER_CONF_GENERATOR);
+ return templateGenerator.generateConfigFile(driver, this);
+ }
+
+ @Override
+ public Iterable<UrlMapping> getUrlMappings() {
+ // For mapping by URL
+ Group urlMappingGroup = getConfig(NginxController.URL_MAPPINGS);
+ if (urlMappingGroup != null) {
+ return Iterables.filter(urlMappingGroup.getMembers(), UrlMapping.class);
+ } else {
+ return Collections.<UrlMapping>emptyList();
+ }
+ }
+
+ @Override
+ public String getShortName() {
+ return "Nginx";
+ }
+
+ public boolean appendSslConfig(String id,
+ StringBuilder out,
+ String prefix,
+ ProxySslConfig ssl,
+ boolean sslBlock,
+ boolean certificateBlock) {
+ if (ssl == null)
+ return false;
+ if (sslBlock) {
+ out.append(prefix);
+ out.append("ssl on;\n");
+ }
+ if (ssl.getReuseSessions()) {
+ out.append(prefix);
+ out.append("proxy_ssl_session_reuse on;");
+ }
+ if (certificateBlock) {
+ String cert;
+ if (Strings.isEmpty(ssl.getCertificateDestination())) {
+ cert = "" + id + ".crt";
+ } else {
+ cert = ssl.getCertificateDestination();
+ }
+
+ out.append(prefix);
+ out.append("ssl_certificate " + cert + ";\n");
+
+ String key;
+ if (!Strings.isEmpty(ssl.getKeyDestination())) {
+ key = ssl.getKeyDestination();
+ } else if (!Strings.isEmpty(ssl.getKeySourceUrl())) {
+ key = "" + id + ".key";
+ } else {
+ key = null;
+ }
+
+ if (key != null) {
+ out.append(prefix);
+ out.append("ssl_certificate_key " + key + ";\n");
+ }
+ }
+ return true;
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxDefaultConfigGenerator.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxDefaultConfigGenerator.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxDefaultConfigGenerator.java
new file mode 100644
index 0000000..dafa9cd
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxDefaultConfigGenerator.java
@@ -0,0 +1,258 @@
+/*
+ * 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 static java.lang.String.format;
+
+import java.util.Collection;
+
+import org.apache.brooklyn.entity.proxy.ProxySslConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+
+import brooklyn.util.text.Strings;
+
+/**
+ * Generates the {@code server.conf} configuration file using sensors on an {@link NginxController}.
+ */
+public class NginxDefaultConfigGenerator implements NginxConfigFileGenerator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(NginxDefaultConfigGenerator.class);
+
+ public NginxDefaultConfigGenerator() { }
+
+ @Override
+ public String generateConfigFile(NginxDriver driver, NginxController nginx) {
+ StringBuilder config = new StringBuilder();
+ config.append("\n");
+ config.append(format("pid %s;\n", driver.getPidFile()));
+ config.append("events {\n");
+ config.append(" worker_connections 8196;\n");
+ config.append("}\n");
+ config.append("http {\n");
+
+ ProxySslConfig globalSslConfig = nginx.getSslConfig();
+
+ if (nginx.isSsl()) {
+ verifyConfig(globalSslConfig);
+ appendSslConfig("global", config, " ", globalSslConfig, true, true);
+ }
+
+ // If no servers, then defaults to returning 404
+ // TODO Give nicer page back
+ if (nginx.getDomain()!=null || nginx.getServerPoolAddresses() == null || nginx.getServerPoolAddresses().isEmpty()) {
+ config.append(" server {\n");
+ config.append(getCodeForServerConfig());
+ config.append(" listen "+nginx.getPort()+";\n");
+ config.append(getCodeFor404());
+ config.append(" }\n");
+ }
+
+ // For basic round-robin across the server-pool
+ if (nginx.getServerPoolAddresses() != null && nginx.getServerPoolAddresses().size() > 0) {
+ config.append(format(" upstream "+nginx.getId()+" {\n"));
+ if (nginx.isSticky()){
+ config.append(" sticky;\n");
+ }
+ for (String address : nginx.getServerPoolAddresses()) {
+ config.append(" server "+address+";\n");
+ }
+ config.append(" }\n");
+ config.append(" server {\n");
+ config.append(getCodeForServerConfig());
+ config.append(" listen "+nginx.getPort()+";\n");
+ if (nginx.getDomain()!=null)
+ config.append(" server_name "+nginx.getDomain()+";\n");
+ config.append(" location / {\n");
+ config.append(" proxy_pass "+(globalSslConfig != null && globalSslConfig.getTargetIsSsl() ? "https" : "http")+"://"+nginx.getId()+";\n");
+ config.append(" }\n");
+ config.append(" }\n");
+ }
+
+ // For mapping by URL
+ Iterable<UrlMapping> mappings = nginx.getUrlMappings();
+ Multimap<String, UrlMapping> mappingsByDomain = LinkedHashMultimap.create();
+ for (UrlMapping mapping : mappings) {
+ Collection<String> addrs = mapping.getAttribute(UrlMapping.TARGET_ADDRESSES);
+ if (addrs != null && addrs.size() > 0) {
+ mappingsByDomain.put(mapping.getDomain(), mapping);
+ }
+ }
+
+ for (UrlMapping um : mappings) {
+ Collection<String> addrs = um.getAttribute(UrlMapping.TARGET_ADDRESSES);
+ if (addrs != null && addrs.size() > 0) {
+ config.append(format(" upstream "+um.getUniqueLabel()+" {\n"));
+ if (nginx.isSticky()){
+ config.append(" sticky;\n");
+ }
+ for (String address: addrs) {
+ config.append(" server "+address+";\n");
+ }
+ config.append(" }\n");
+ }
+ }
+
+ for (String domain : mappingsByDomain.keySet()) {
+ config.append(" server {\n");
+ config.append(getCodeForServerConfig());
+ config.append(" listen "+nginx.getPort()+";\n");
+ config.append(" server_name "+domain+";\n");
+ boolean hasRoot = false;
+
+ // set up SSL
+ ProxySslConfig localSslConfig = null;
+ for (UrlMapping mappingInDomain : mappingsByDomain.get(domain)) {
+ ProxySslConfig sslConfig = mappingInDomain.getConfig(UrlMapping.SSL_CONFIG);
+ if (sslConfig!=null) {
+ verifyConfig(sslConfig);
+ if (localSslConfig!=null) {
+ if (localSslConfig.equals(sslConfig)) {
+ //ignore identical config specified on multiple mappings
+ } else {
+ LOG.warn("{} mapping {} provides SSL config for {} when a different config had already been provided by another mapping, ignoring this one",
+ new Object[] {this, mappingInDomain, domain});
+ }
+ } else if (globalSslConfig!=null) {
+ if (globalSslConfig.equals(sslConfig)) {
+ //ignore identical config specified on multiple mappings
+ } else {
+ LOG.warn("{} mapping {} provides SSL config for {} when a different config had been provided at root nginx scope, ignoring this one",
+ new Object[] {this, mappingInDomain, domain});
+ }
+ } else {
+ //new config, is okay
+ localSslConfig = sslConfig;
+ }
+ }
+ }
+ if (localSslConfig != null) {
+ appendSslConfig(domain, config, " ", localSslConfig, true, true);
+ }
+
+ for (UrlMapping mappingInDomain : mappingsByDomain.get(domain)) {
+ // TODO Currently only supports "~" for regex. Could add support for other options,
+ // such as "~*", "^~", literals, etc.
+ boolean isRoot = mappingInDomain.getPath()==null || mappingInDomain.getPath().length()==0 || mappingInDomain.getPath().equals("/");
+ if (isRoot && hasRoot) {
+ LOG.warn(""+this+" mapping "+mappingInDomain+" provides a duplicate / proxy, ignoring");
+ } else {
+ hasRoot |= isRoot;
+ String location = isRoot ? "/" : "~ " + mappingInDomain.getPath();
+ config.append(" location "+location+" {\n");
+ Collection<UrlRewriteRule> rewrites = mappingInDomain.getConfig(UrlMapping.REWRITES);
+ if (rewrites != null && rewrites.size() > 0) {
+ for (UrlRewriteRule rule: rewrites) {
+ config.append(" rewrite \"^"+rule.getFrom()+"$\" \""+rule.getTo()+"\"");
+ if (rule.isBreak()) config.append(" break");
+ config.append(" ;\n");
+ }
+ }
+ config.append(" proxy_pass "+
+ (localSslConfig != null && localSslConfig.getTargetIsSsl() ? "https" :
+ (localSslConfig == null && globalSslConfig != null && globalSslConfig.getTargetIsSsl()) ? "https" :
+ "http")+
+ "://"+mappingInDomain.getUniqueLabel()+" ;\n");
+ config.append(" }\n");
+ }
+ }
+ if (!hasRoot) {
+ //provide a root block giving 404 if there isn't one for this server
+ config.append(" location / { \n"+getCodeFor404()+" }\n");
+ }
+ config.append(" }\n");
+ }
+
+ config.append("}\n");
+
+ return config.toString();
+ }
+
+ protected String getCodeForServerConfig() {
+ // See http://wiki.nginx.org/HttpProxyModule
+ return ""+
+ // this prevents nginx from reporting version number on error pages
+ " server_tokens off;\n"+
+
+ // this prevents nginx from using the internal proxy_pass codename as Host header passed upstream.
+ // Not using $host, as that causes integration test to fail with a "connection refused" testing
+ // url-mappings, at URL "http://localhost:${port}/atC0" (with a trailing slash it does work).
+ " proxy_set_header Host $http_host;\n"+
+
+ // following added, as recommended for wordpress in:
+ // http://zeroturnaround.com/labs/wordpress-protips-go-with-a-clustered-approach/#!/
+ " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n"+
+ " proxy_set_header X-Real-IP $remote_addr;\n";
+ }
+
+ protected String getCodeFor404() {
+ return " return 404;\n";
+ }
+
+ protected void verifyConfig(ProxySslConfig proxySslConfig) {
+ if(Strings.isEmpty(proxySslConfig.getCertificateDestination()) && Strings.isEmpty(proxySslConfig.getCertificateSourceUrl())){
+ throw new IllegalStateException("ProxySslConfig can't have a null certificateDestination and null certificateSourceUrl. One or both need to be set");
+ }
+ }
+
+ protected boolean appendSslConfig(String id, StringBuilder out, String prefix, ProxySslConfig ssl,
+ boolean sslBlock, boolean certificateBlock) {
+ if (ssl == null) return false;
+ if (sslBlock) {
+ out.append(prefix);
+ out.append("ssl on;\n");
+ }
+ if (ssl.getReuseSessions()) {
+ out.append(prefix);
+ out.append("");
+ }
+ if (certificateBlock) {
+ String cert;
+ if (Strings.isEmpty(ssl.getCertificateDestination())) {
+ cert = "" + id + ".crt";
+ } else {
+ cert = ssl.getCertificateDestination();
+ }
+
+ out.append(prefix);
+ out.append("ssl_certificate " + cert + ";\n");
+
+ String key;
+ if (!Strings.isEmpty(ssl.getKeyDestination())) {
+ key = ssl.getKeyDestination();
+ } else if (!Strings.isEmpty(ssl.getKeySourceUrl())) {
+ key = "" + id + ".key";
+ } else {
+ key = null;
+ }
+
+ if (key != null) {
+ out.append(prefix);
+ out.append("ssl_certificate_key " + key + ";\n");
+ }
+
+ out.append("ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n");
+ }
+ return true;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxDriver.java
new file mode 100644
index 0000000..48a0534
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxDriver.java
@@ -0,0 +1,31 @@
+/*
+ * 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 brooklyn.entity.basic.SoftwareProcessDriver;
+
+public interface NginxDriver extends SoftwareProcessDriver {
+
+ String getRunDir();
+
+ String getPidFile();
+
+ boolean isCustomizationCompleted();
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxSshDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxSshDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxSshDriver.java
new file mode 100644
index 0000000..27ab94c
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxSshDriver.java
@@ -0,0 +1,477 @@
+/*
+ * 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 static java.lang.String.format;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.brooklyn.entity.proxy.AbstractController;
+import org.apache.brooklyn.management.ManagementContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.lifecycle.ScriptHelper;
+import brooklyn.entity.drivers.downloads.DownloadResolver;
+import brooklyn.location.OsDetails;
+import brooklyn.location.basic.SshMachineLocation;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.net.Networking;
+import brooklyn.util.os.Os;
+import brooklyn.util.ssh.BashCommands;
+import brooklyn.util.stream.Streams;
+import brooklyn.util.task.DynamicTasks;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.task.ssh.SshTasks;
+import brooklyn.util.task.ssh.SshTasks.OnFailingTask;
+import brooklyn.util.text.Strings;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+/**
+ * Start a {@link NginxController} in a {@link brooklyn.location.Location} accessible over ssh.
+ */
+public class NginxSshDriver extends AbstractSoftwareProcessSshDriver implements NginxDriver {
+
+ // TODO An alternative way of installing nginx is described at:
+ // http://sjp.co.nz/posts/building-nginx-for-debian-systems/
+ // It's use of `apt-get source nginx` and `apt-get build-dep nginx` makes
+ // it look higher level and therefore more appealing.
+
+ public static final Logger log = LoggerFactory.getLogger(NginxSshDriver.class);
+ public static final String NGINX_PID_FILE = "logs/nginx.pid";
+
+ private boolean customizationCompleted = false;
+
+ public NginxSshDriver(NginxControllerImpl entity, SshMachineLocation machine) {
+ super(entity, machine);
+
+ entity.setAttribute(Attributes.LOG_FILE_LOCATION, getLogFileLocation());
+ entity.setAttribute(NginxController.ACCESS_LOG_LOCATION, getAccessLogLocation());
+ entity.setAttribute(NginxController.ERROR_LOG_LOCATION, getErrorLogLocation());
+ }
+
+ @Override
+ public NginxControllerImpl getEntity() {
+ return (NginxControllerImpl) super.getEntity();
+ }
+
+ public String getLogFileLocation() {
+ return format("%s/console", getRunDir());
+ }
+
+ public String getAccessLogLocation() {
+ String accessLog = entity.getConfig(NginxController.ACCESS_LOG_LOCATION);
+ return format("%s/%s", getRunDir(), accessLog);
+ }
+
+ public String getErrorLogLocation() {
+ String errorLog = entity.getConfig(NginxController.ERROR_LOG_LOCATION);
+ return format("%s/%s", getRunDir(), errorLog);
+ }
+
+ /** By default Nginx writes the pid of the master process to {@code logs/nginx.pid} */
+ @Override
+ public String getPidFile() {
+ return format("%s/%s", getRunDir(), NGINX_PID_FILE);
+ }
+
+ @Deprecated /** @deprecated since 0.7.0 use #getPort */
+ public Integer getHttpPort() {
+ return getEntity().getPort();
+ }
+
+ public Integer getPort() {
+ return getEntity().getPort();
+ }
+
+ @Override
+ public void rebind() {
+ customizationCompleted = true;
+ }
+
+ @Override
+ public void postLaunch() {
+ entity.setAttribute(NginxController.PID_FILE, getRunDir() + "/" + AbstractSoftwareProcessSshDriver.PID_FILENAME);
+ if (((AbstractController)entity).isSsl()) {
+ entity.setAttribute(Attributes.HTTPS_PORT, getPort());
+ ((EntityInternal)entity).removeAttribute(Attributes.HTTP_PORT);
+ } else {
+ entity.setAttribute(Attributes.HTTP_PORT, getPort());
+ ((EntityInternal)entity).removeAttribute(Attributes.HTTPS_PORT);
+ }
+ super.postLaunch();
+ }
+
+ @Override
+ public void preInstall() {
+ resolver = Entities.newDownloader(this);
+ setExpandedInstallDir(Os.mergePaths(getInstallDir(), resolver.getUnpackedDirectoryName(format("nginx-%s", getVersion()))));
+ }
+
+ @Override
+ public void install() {
+ // inessential here, installation will fail later if it needs to sudo (eg if using port 80)
+ DynamicTasks.queueIfPossible(SshTasks.dontRequireTtyForSudo(getMachine(), OnFailingTask.WARN_OR_IF_DYNAMIC_FAIL_MARKING_INESSENTIAL)).orSubmitAndBlock();
+
+ List<String> nginxUrls = resolver.getTargets();
+ String nginxSaveAs = resolver.getFilename();
+
+ boolean sticky = ((NginxController) entity).isSticky();
+ boolean isMac = getMachine().getOsDetails().isMac();
+
+ MutableMap<String, String> installGccPackageFlags = MutableMap.of(
+ "onlyifmissing", "gcc",
+ "yum", "gcc",
+ "apt", "gcc",
+ "zypper", "gcc",
+ "port", null);
+ MutableMap<String, String> installMakePackageFlags = MutableMap.of(
+ "onlyifmissing", "make",
+ "yum", "make",
+ "apt", "make",
+ "zypper", "make",
+ "port", null);
+ MutableMap<String, String> installPackageFlags = MutableMap.of(
+ "yum", "openssl-devel pcre-devel",
+ "apt", "libssl-dev zlib1g-dev libpcre3-dev",
+ "zypper", "libopenssl-devel pcre-devel",
+ "port", null);
+
+ String stickyModuleVersion = entity.getConfig(NginxController.STICKY_VERSION);
+ DownloadResolver stickyModuleResolver = mgmt().getEntityDownloadsManager().newDownloader(
+ this, "stickymodule", ImmutableMap.of("addonversion", stickyModuleVersion));
+ List<String> stickyModuleUrls = stickyModuleResolver.getTargets();
+ String stickyModuleSaveAs = stickyModuleResolver.getFilename();
+ String stickyModuleExpandedInstallDir = String.format("%s/src/%s", getExpandedInstallDir(),
+ stickyModuleResolver.getUnpackedDirectoryName("nginx-sticky-module-"+stickyModuleVersion));
+
+ List<String> cmds = Lists.newArrayList();
+
+ cmds.add(BashCommands.INSTALL_TAR);
+ cmds.add(BashCommands.alternatives(
+ BashCommands.ifExecutableElse0("apt-get", BashCommands.installPackage("build-essential")),
+ BashCommands.ifExecutableElse0("yum", BashCommands.sudo("yum -y --nogpgcheck groupinstall \"Development Tools\""))));
+ cmds.add(BashCommands.installPackage(installGccPackageFlags, "nginx-prerequisites-gcc"));
+ cmds.add(BashCommands.installPackage(installMakePackageFlags, "nginx-prerequisites-make"));
+ cmds.add(BashCommands.installPackage(installPackageFlags, "nginx-prerequisites"));
+ cmds.addAll(BashCommands.commandsToDownloadUrlsAs(nginxUrls, nginxSaveAs));
+
+ String pcreExpandedInstallDirname = "";
+ if (isMac) {
+ String pcreVersion = entity.getConfig(NginxController.PCRE_VERSION);
+ DownloadResolver pcreResolver = mgmt().getEntityDownloadsManager().newDownloader(
+ this, "pcre", ImmutableMap.of("addonversion", pcreVersion));
+ List<String> pcreUrls = pcreResolver.getTargets();
+ String pcreSaveAs = pcreResolver.getFilename();
+ pcreExpandedInstallDirname = pcreResolver.getUnpackedDirectoryName("pcre-"+pcreVersion);
+
+ // Install PCRE
+ cmds.addAll(BashCommands.commandsToDownloadUrlsAs(pcreUrls, pcreSaveAs));
+ cmds.add(format("mkdir -p %s/pcre-dist", getInstallDir()));
+ cmds.add(format("tar xvzf %s", pcreSaveAs));
+ cmds.add(format("cd %s", pcreExpandedInstallDirname));
+ cmds.add(format("./configure --prefix=%s/pcre-dist", getInstallDir()));
+ cmds.add("make");
+ cmds.add("make install");
+ cmds.add("cd ..");
+ }
+
+ cmds.add(format("tar xvzf %s", nginxSaveAs));
+ cmds.add(format("cd %s", getExpandedInstallDir()));
+
+ if (sticky) {
+ // Latest versions of sticky module expand to a different folder than the file name.
+ // Extract to folder set by us so we know where the sources are.
+ cmds.add(format("mkdir -p %s", stickyModuleExpandedInstallDir));
+ cmds.add(format("pushd %s", stickyModuleExpandedInstallDir));
+ cmds.addAll(BashCommands.commandsToDownloadUrlsAs(stickyModuleUrls, stickyModuleSaveAs));
+ cmds.add(format("tar --strip-component=1 -xvzf %s", stickyModuleSaveAs));
+ cmds.add("popd");
+ }
+
+ // Note that for OS X, not including space after "-L" because broken in 10.6.8 (but fixed in 10.7.x)
+ // see http://trac.nginx.org/nginx/ticket/227
+ String withLdOpt = entity.getConfig(NginxController.WITH_LD_OPT);
+ if (isMac) withLdOpt = format("-L%s/pcre-dist/lib", getInstallDir()) + (Strings.isBlank(withLdOpt) ? "" : " " + withLdOpt);
+ String withCcOpt = entity.getConfig(NginxController.WITH_CC_OPT);
+
+ if (isMac) {
+ // TODO Upgrade sticky module as soon as a fix for https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/issue/16/can-not-compile-on-macosx-yosemite
+ // is released and remove this block.
+ withCcOpt = (Strings.isBlank(withCcOpt) ? "" : (withCcOpt + " ")) + "-Wno-error";
+ }
+
+ StringBuilder configureCommand = new StringBuilder("./configure")
+ .append(format(" --prefix=%s/dist", getExpandedInstallDir()))
+ .append(" --with-http_ssl_module")
+ .append(sticky ? format(" --add-module=%s ", stickyModuleExpandedInstallDir) : "")
+ .append(!Strings.isBlank(withLdOpt) ? format(" --with-ld-opt=\"%s\"", withLdOpt) : "")
+ .append(!Strings.isBlank(withCcOpt) ? format(" --with-cc-opt=\"%s\"", withCcOpt) : "")
+ ;
+ if (isMac) {
+ configureCommand.append(" --with-pcre=")
+ .append(getInstallDir()).append("/").append(pcreExpandedInstallDirname);
+ }
+
+ cmds.addAll(ImmutableList.of(
+ "mkdir -p dist",
+ configureCommand.toString(),
+ "make install"));
+
+ ScriptHelper script = newScript(INSTALLING)
+ .body.append(cmds)
+ .header.prepend("set -x")
+ .gatherOutput()
+ .failOnNonZeroResultCode(false);
+
+ int result = script.execute();
+
+ if (result != 0) {
+ String notes = "likely an error building nginx. consult the brooklyn log ssh output for further details.\n"+
+ "note that this Brooklyn nginx driver compiles nginx from source. " +
+ "it attempts to install common prerequisites but this does not always succeed.\n";
+ OsDetails os = getMachine().getOsDetails();
+ if (os.isMac()) {
+ notes += "deploying to Mac OS X, you will require Xcode and Xcode command-line tools, and on " +
+ "some versions the pcre library (e.g. using macports, sudo port install pcre).\n";
+ }
+ if (os.isWindows()) {
+ notes += "this nginx driver is not designed for windows, unless cygwin is installed, and you are patient.\n";
+ }
+ if (getEntity().getApplication().getClass().getCanonicalName().startsWith("brooklyn.demo.")) {
+ // this is maybe naughty ... but since we use nginx in the first demo example,
+ // and since it's actually pretty complicated, let's give a little extra hand-holding
+ notes +=
+ "if debugging this is all a bit much and you just want to run a demo, " +
+ "you have two fairly friendly options.\n" +
+ "1. you can use a well known cloud, like AWS or Rackspace, where this should run " +
+ "in a tried-and-tested Ubuntu or CentOS environment, without any problems " +
+ "(and if it does let us know and we'll fix it!).\n"+
+ "2. or you can just use the demo without nginx, instead access the appserver instances directly.\n";
+ }
+
+ if (!script.getResultStderr().isEmpty()) {
+ notes += "\n" + "STDERR\n" + script.getResultStderr()+"\n";
+ Streams.logStreamTail(log, "STDERR of problem in "+Tasks.current(), Streams.byteArrayOfString(script.getResultStderr()), 1024);
+ }
+ if (!script.getResultStdout().isEmpty()) {
+ notes += "\n" + "STDOUT\n" + script.getResultStdout()+"\n";
+ Streams.logStreamTail(log, "STDOUT of problem in "+Tasks.current(), Streams.byteArrayOfString(script.getResultStdout()), 1024);
+ }
+
+ Tasks.setExtraStatusDetails(notes.trim());
+
+ throw new IllegalStateException("Installation of nginx failed (shell returned non-zero result "+result+")");
+ }
+ }
+
+ private ManagementContext mgmt() {
+ return ((EntityInternal) entity).getManagementContext();
+ }
+
+ @Override
+ public void customize() {
+ newScript(CUSTOMIZING)
+ .body.append(
+ format("mkdir -p %s", getRunDir()),
+ format("cp -R %s/dist/{conf,html,logs,sbin} %s", getExpandedInstallDir(), getRunDir()))
+ .execute();
+
+ // Install static content archive, if specified
+ String archiveUrl = entity.getConfig(NginxController.STATIC_CONTENT_ARCHIVE_URL);
+ if (Strings.isNonBlank(archiveUrl)) {
+ getEntity().deploy(archiveUrl);
+ }
+
+ customizationCompleted = true;
+ }
+
+ @Override
+ public boolean isCustomizationCompleted() {
+ return customizationCompleted;
+ }
+
+ @Override
+ public void launch() {
+ // TODO if can't be root, and ports > 1024 are in the allowed port range,
+ // prefer that; could do this on SshMachineLocation which implements PortSupplier,
+ // invoked from PortAttrSensorAndConfigKey, which is invoked from MachineLifecycleTasks.preStartCustom
+ Networking.checkPortsValid(MutableMap.of("port", getPort()));
+
+ getEntity().doExtraConfigurationDuringStart();
+
+ // We wait for evidence of running because, using
+ // brooklyn.ssh.config.tool.class=brooklyn.util.internal.ssh.cli.SshCliTool,
+ // we saw the ssh session return before the tomcat process was fully running
+ // so the process failed to start.
+ newScript(MutableMap.of("usePidFile", false), LAUNCHING)
+ .body.append(
+ format("cd %s", getRunDir()),
+ BashCommands.requireExecutable("./sbin/nginx"),
+ sudoBashCIfPrivilegedPort(getPort(), format(
+ "nohup ./sbin/nginx -p %s/ -c conf/server.conf > %s 2>&1 &", getRunDir(), getLogFileLocation())),
+ format("for i in {1..10}\n" +
+ "do\n" +
+ " test -f %1$s && ps -p `cat %1$s` && exit\n" +
+ " sleep 1\n" +
+ "done\n" +
+ "echo \"No explicit error launching nginx but couldn't find process by pid; continuing but may subsequently fail\"\n" +
+ "cat %2$s | tee /dev/stderr",
+ getPidFile(), getLogFileLocation()))
+ .execute();
+ }
+
+ public static String sudoIfPrivilegedPort(int port, String command) {
+ return port < 1024 ? BashCommands.sudo(command) : command;
+ }
+
+ public static String sudoBashCIfPrivilegedPort(int port, String command) {
+ return port < 1024 ? BashCommands.sudo("bash -c '"+command+"'") : command;
+ }
+
+ @Override
+ public boolean isRunning() {
+ return newScript(MutableMap.of("usePidFile", getPidFile()), CHECK_RUNNING).execute() == 0;
+ }
+
+ @Override
+ public void stop() {
+ // Don't `kill -9`, as that doesn't stop the worker processes
+ newScript(MutableMap.of("usePidFile", false), STOPPING).
+ body.append(
+ format("cd %s", getRunDir()),
+ format("export PID=`cat %s`", getPidFile()),
+ "test -n \"$PID\" || exit 0",
+ sudoIfPrivilegedPort(getPort(), "kill $PID"))
+ .execute();
+ }
+
+ @Override
+ public void kill() {
+ stop();
+ }
+
+ private final ExecController reloadExecutor = new ExecController(
+ entity+"->reload",
+ new Runnable() {
+ @Override
+ public void run() {
+ reloadImpl();
+ }
+ });
+
+ public void reload() {
+ // If there are concurrent calls to reload (such that some calls come in when another call is queued), then
+ // don't bother doing the subsequent calls. Instead just rely on the currently queued call.
+ //
+ // Motivation is that calls to nginx.reload were backing up: we ended up executing lots of them in parallel
+ // when there were several changes to the nginx conifg that requiring a reload. The problem can be particularly
+ // bad because the ssh commands take a second or two - if 10 changes were made to the config in that time, we'd
+ // end up executing reload 10 times in parallel.
+
+ reloadExecutor.run();
+ }
+
+ private void reloadImpl() {
+ // Note that previously, if serviceUp==false then we'd restart nginx.
+ // That caused a race on stop()+reload(): nginx could simultaneously be stopping and also reconfiguring
+ // (e.g. due to a cluster-resize), the restart() would leave nginx running even after stop() had returned.
+ //
+ // Now we rely on NginxController always calling update (and thus reload) once it has started. This is
+ // done in AbstractController.postActivation().
+ //
+ // If our blocking check sees that !isRunning() (and if a separate thread is starting it, and subsequently
+ // calling waitForEntityStart()), we can guarantee that the start-thread's call to update will happen after
+ // this call to reload. So we this can be a no-op, and just rely on that subsequent call to update.
+
+ Lifecycle lifecycle = entity.getAttribute(NginxController.SERVICE_STATE_ACTUAL);
+ if (lifecycle==Lifecycle.STOPPING || lifecycle==Lifecycle.STOPPED || !isRunning()) {
+ log.debug("Ignoring reload of nginx "+entity+", because service is not running (state "+lifecycle+")");
+ return;
+ }
+
+ doReloadNow();
+ }
+
+ /**
+ * Instructs nginx to reload its configuration (without restarting, so don't lose any requests).
+ * Can be overridden if necessary, to change the call used for reloading.
+ */
+ private void doReloadNow() {
+ // We use kill -HUP because that is recommended at http://wiki.nginx.org/CommandLine,
+ // but there is no noticeable difference (i.e. no impact on #365) compared to:
+ // sudoIfPrivilegedPort(getHttpPort(), format("./sbin/nginx -p %s/ -c conf/server.conf -s reload", getRunDir()))
+ //
+ // Note that if conf file is invalid, you'll get no stdout/stderr from `kill` but you
+ // do from using `nginx ... -s reload` so that can be handy when manually debugging.
+
+ log.debug("reloading nginx by simularing restart (kill -HUP) - {}", entity);
+ newScript(RESTARTING)
+ .body.append(
+ format("cd %s", getRunDir()),
+ format("export PID=`cat %s`", getPidFile()),
+ sudoIfPrivilegedPort(getPort(), "kill -HUP $PID"))
+ .execute();
+ }
+
+ /**
+ * Executes the given task, but only if another thread hasn't executed it for us (where the other thread
+ * began executing it after the current caller of {@link #run()} began attempting to do so itself).
+ *
+ * @author aled
+ */
+ private static class ExecController {
+ private final String summary;
+ private final Runnable task;
+ private final AtomicLong counter = new AtomicLong();
+
+ ExecController(String summary, Runnable task) {
+ this.summary = summary;
+ this.task = task;
+ }
+
+ void run() {
+ long preCount = counter.get();
+ synchronized (this) {
+ if (counter.compareAndSet(preCount, preCount+1)) {
+ try {
+ if (log.isDebugEnabled()) log.debug("Executing {}; incremented count to {}", new Object[] {summary, counter});
+ task.run();
+ } catch (Exception e) {
+ if (log.isDebugEnabled()) log.debug("Failed executing {}; reseting count to {} and propagating exception: {}", new Object[] {summary, preCount, e});
+ counter.set(preCount);
+ throw Exceptions.propagate(e);
+ }
+ } else {
+ if (log.isDebugEnabled()) log.debug("Not executing {} because executed by another thread subsequent to us attempting (preCount {}; count {})", new Object[] {summary, preCount, counter});
+ }
+ }
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxTemplateConfigGenerator.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxTemplateConfigGenerator.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxTemplateConfigGenerator.java
new file mode 100644
index 0000000..34432dd7
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/NginxTemplateConfigGenerator.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.brooklyn.entity.proxy.ProxySslConfig;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.util.ResourceUtils;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.text.Strings;
+import brooklyn.util.text.TemplateProcessor;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+
+/**
+ * Processes a FreeMarker template to generate the {@code server.conf} configuration file for an
+ * {@link NginxController}.
+ * <p>
+ * Note this must be explicitly enabled via {@link NginxController#SERVER_CONF_GENERATOR}.
+ */
+public class NginxTemplateConfigGenerator implements NginxConfigFileGenerator {
+
+ public static final ConfigKey<String> SERVER_CONF_TEMPLATE_URL = ConfigKeys.newStringConfigKey(
+ "nginx.config.templateUrl", "The server.conf configuration file URL (FreeMarker template). "
+ + "Only applies if 'nginx.config.generator' specifies a generator which uses a template.",
+ "classpath://org/apache/brooklyn/entity/proxy/nginx/server.conf");
+
+ public NginxTemplateConfigGenerator() { }
+
+ @Override
+ public String generateConfigFile(NginxDriver driver, NginxController nginx) {
+ // Check template URL exists
+ String templateUrl = driver.getEntity().getConfig(NginxController.SERVER_CONF_TEMPLATE_URL);
+ ResourceUtils.create(this).checkUrlExists(templateUrl);
+
+ // Check SSL configuration
+ ProxySslConfig ssl = driver.getEntity().getConfig(NginxController.SSL_CONFIG);
+ if (ssl != null && Strings.isEmpty(ssl.getCertificateDestination()) && Strings.isEmpty(ssl.getCertificateSourceUrl())) {
+ throw new IllegalStateException("ProxySslConfig can't have a null certificateDestination and null certificateSourceUrl. One or both need to be set");
+ }
+
+ // For mapping by URL
+ Iterable<UrlMapping> mappings = ((NginxController) driver.getEntity()).getUrlMappings();
+ Multimap<String, UrlMapping> mappingsByDomain = LinkedHashMultimap.create();
+ for (UrlMapping mapping : mappings) {
+ Collection<String> addrs = mapping.getAttribute(UrlMapping.TARGET_ADDRESSES);
+ if (addrs != null && addrs.size() > 0) {
+ mappingsByDomain.put(mapping.getDomain(), mapping);
+ }
+ }
+ Map<String, Object> substitutions = MutableMap.<String, Object>builder()
+ .putIfNotNull("ssl", ssl)
+ .put("urlMappings", mappings)
+ .put("domainMappings", mappingsByDomain)
+ .build();
+
+ // Get template contents and process
+ String contents = ResourceUtils.create(driver.getEntity()).getResourceAsString(templateUrl);
+ return TemplateProcessor.processTemplateContents(contents, driver, substitutions);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlMapping.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlMapping.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlMapping.java
new file mode 100644
index 0000000..fc2cedb
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlMapping.java
@@ -0,0 +1,103 @@
+/*
+ * 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.Collection;
+
+import org.apache.brooklyn.entity.proxy.AbstractController;
+import org.apache.brooklyn.entity.proxy.ProxySslConfig;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Entity;
+import brooklyn.entity.annotation.Effector;
+import brooklyn.entity.basic.AbstractGroup;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.MethodEffector;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.Sensors;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.reflect.TypeToken;
+
+/**
+ * This is a group whose members will be made available to a load-balancer / URL forwarding service (such as nginx).
+ * Configuration requires a <b>domain</b> and some mechanism for finding members.
+ * The easiest way to find members is using a <b>target</b> whose children will be tracked,
+ * but alternative membership policies can also be used.
+ */
+@ImplementedBy(UrlMappingImpl.class)
+public interface UrlMapping extends AbstractGroup {
+
+ MethodEffector<Void> DISCARD = new MethodEffector<Void>(UrlMapping.class, "discard");
+
+ @SetFromFlag("label")
+ ConfigKey<String> LABEL = ConfigKeys.newStringConfigKey(
+ "urlmapping.label", "optional human-readable label to identify a server");
+
+ @SetFromFlag("domain")
+ ConfigKey<String> DOMAIN = ConfigKeys.newStringConfigKey(
+ "urlmapping.domain", "domain (hostname, e.g. www.foo.com) to present for this URL map rule; required.");
+
+ @SetFromFlag("path")
+ ConfigKey<String> PATH = ConfigKeys.newStringConfigKey(
+ "urlmapping.path", "URL path (pattern) for this URL map rule. Currently only supporting regex matches "+
+ "(if not supplied, will match all paths at the indicated domain)");
+
+ @SetFromFlag("ssl")
+ ConfigKey<ProxySslConfig> SSL_CONFIG = AbstractController.SSL_CONFIG;
+
+ @SetFromFlag("rewrites")
+ @SuppressWarnings("serial")
+ ConfigKey<Collection<UrlRewriteRule>> REWRITES = ConfigKeys.newConfigKey(new TypeToken<Collection<UrlRewriteRule>>() { },
+ "urlmapping.rewrites", "Set of URL rewrite rules to apply");
+
+ @SetFromFlag("target")
+ ConfigKey<Entity> TARGET_PARENT = ConfigKeys.newConfigKey(Entity.class,
+ "urlmapping.target.parent", "optional target entity whose children will be pointed at by this mapper");
+
+ @SuppressWarnings("serial")
+ AttributeSensor<Collection<String>> TARGET_ADDRESSES = Sensors.newSensor(new TypeToken<Collection<String>>() { },
+ "urlmapping.target.addresses", "set of addresses which should be forwarded to by this URL mapping");
+
+ String getUniqueLabel();
+
+ /** Adds a rewrite rule, must be called at config time. See {@link UrlRewriteRule} for more info. */
+ UrlMapping addRewrite(String from, String to);
+
+ /** Adds a rewrite rule, must be called at config time. See {@link UrlRewriteRule} for more info. */
+ UrlMapping addRewrite(UrlRewriteRule rule);
+
+ String getDomain();
+
+ String getPath();
+
+ Entity getTarget();
+
+ void setTarget(Entity target);
+
+ void recompute();
+
+ Collection<String> getTargetAddresses();
+
+ ProxySslConfig getSsl();
+
+ @Effector(description="Unmanages the url-mapping, so it is discarded and no longer applies")
+ void discard();
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlMappingImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlMappingImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlMappingImpl.java
new file mode 100644
index 0000000..8252b05
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlMappingImpl.java
@@ -0,0 +1,223 @@
+/*
+ * 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 static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+import org.apache.brooklyn.entity.proxy.ProxySslConfig;
+import org.apache.brooklyn.entity.webapp.WebAppServiceConstants;
+import org.apache.brooklyn.management.SubscriptionHandle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.AbstractGroupImpl;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityPredicates;
+import brooklyn.entity.trait.Changeable;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.SensorEvent;
+import brooklyn.event.SensorEventListener;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+/**
+ * This is a group whose members will be made available to a load-balancer / URL forwarding service (such as nginx).
+ * <p>
+ * Configuration requires a <b>domain</b> and some mechanism for finding members.
+ * The easiest way to find members is using a <b>target</b> whose children will be tracked,
+ * but alternative membership policies can also be used.
+ */
+public class UrlMappingImpl extends AbstractGroupImpl implements UrlMapping {
+
+ private static final Logger log = LoggerFactory.getLogger(UrlMapping.class);
+
+ public UrlMappingImpl() {
+ super();
+ }
+
+ @Override
+ public String getUniqueLabel() {
+ String l = getConfig(LABEL);
+ if (groovyTruth(l)) return getId()+"-"+l;
+ else return getId();
+ }
+
+ /** adds a rewrite rule, must be called at config time. see {@link UrlRewriteRule} for more info. */
+ @Override
+ public synchronized UrlMapping addRewrite(String from, String to) {
+ return addRewrite(new UrlRewriteRule(from, to));
+ }
+
+ /** adds a rewrite rule, must be called at config time. see {@link UrlRewriteRule} for more info. */
+ @Override
+ public synchronized UrlMapping addRewrite(UrlRewriteRule rule) {
+ Collection<UrlRewriteRule> rewrites = getConfig(REWRITES);
+ if (rewrites==null) {
+ rewrites = new ArrayList<UrlRewriteRule>();
+ }
+ rewrites.add(rule);
+ setConfig(REWRITES, rewrites);
+ return this;
+ }
+
+ @Override
+ public String getDomain() {
+ return Preconditions.checkNotNull( getConfig(DOMAIN), "domain config argument required");
+ }
+
+ @Override
+ public String getPath() {
+ return getConfig(PATH);
+ }
+
+ @Override
+ public Entity getTarget() {
+ return getConfig(TARGET_PARENT);
+ }
+
+ @Override
+ public void setTarget(Entity target) {
+ setConfig(TARGET_PARENT, target);
+ recompute();
+ }
+
+ @Override
+ public void onManagementStarting() {
+ super.onManagementStarting();
+
+ if (getConfig(TARGET_PARENT) != null) {
+ recompute();
+ // following line could be more efficient (just modify the addresses set, not clearing it each time;
+ // but since addresses is lazy loaded not that big a deal)
+ // subscribe(this, Changeable.GROUP_SIZE, { resetAddresses(true) } as SensorEventListener);
+ // above not needed since our target tracking figures this out
+ }
+ }
+
+ /** defines how address string, ie hostname:port, is constructed from a given entity.
+ * returns null if not possible.
+ * <p>
+ * the default is to look at HOSTNAME and HTTPS_PORT or HTTP_PORT attribute sensors (depending on SSL_CONFIG being set with targetIsSsl).
+ * <p>
+ * this method is suitable (intended) for overriding if needed.
+ */
+ protected String getAddressOfEntity(Entity s) {
+ String h = s.getAttribute(Attributes.HOSTNAME);
+
+ Integer p = null;
+ Set<String> protos = s.getAttribute(WebAppServiceConstants.ENABLED_PROTOCOLS);
+ ProxySslConfig sslConfig = getConfig(SSL_CONFIG);
+ if (sslConfig != null && sslConfig.getTargetIsSsl()) {
+ // use ssl
+ if (protos != null && hasProtocol(protos, "https")) {
+ // proto configured correctly
+ } else {
+ // proto not defined; use https anyway, but it might fail
+ log.warn("Misconfiguration for "+this+": ENABLED_PROTOCOLS='"+protos+"' for "+s+" but sslConfig="+sslConfig);
+ }
+ p = s.getAttribute(Attributes.HTTPS_PORT);
+ if (p == null)
+ log.warn("Misconfiguration for "+this+": sslConfig="+sslConfig+" but no HTTPS_PORT on "+s);
+ }
+ if (p == null) {
+ // default to http
+ p = s.getAttribute(Attributes.HTTP_PORT);
+ }
+
+ if (groovyTruth(h) && p != null) return h+":"+p;
+ log.error("Unable to construct hostname:port representation for "+s+"; skipping in "+this);
+ return null;
+ }
+
+ protected synchronized void recomputeAddresses() {
+ Set<String> resultM = Sets.newLinkedHashSet();
+ for (Entity s: getMembers()) {
+ String hp = getAddressOfEntity(s);
+ if (hp != null) resultM.add(hp);
+ }
+ Set<String> result = Collections.unmodifiableSet(resultM);
+ Collection<String> oldAddresses = getAttribute(TARGET_ADDRESSES);
+ if (oldAddresses == null || !result.equals(ImmutableSet.copyOf(oldAddresses))) {
+ setAttribute(TARGET_ADDRESSES, result);
+ }
+ }
+
+ public Collection<String> getTargetAddresses() {
+ return getAttribute(TARGET_ADDRESSES);
+ }
+
+ public ProxySslConfig getSsl() {
+ return getConfig(SSL_CONFIG);
+ }
+
+ // FIXME Do we really need this?!
+ protected SubscriptionHandle getSubscriptionHandle() {
+ return subscriptionHandle;
+ }
+
+ private SubscriptionHandle subscriptionHandle;
+ private SubscriptionHandle subscriptionHandle2;
+
+ @Override
+ public synchronized void recompute() {
+ if (subscriptionHandle != null) getSubscriptionContext().unsubscribe(subscriptionHandle);
+ if (subscriptionHandle2 != null) getSubscriptionContext().unsubscribe(subscriptionHandle2);
+
+ Entity t = getTarget();
+ if (t != null) {
+ subscriptionHandle = subscribeToChildren(t, Startable.SERVICE_UP, new SensorEventListener<Boolean>() {
+ @Override public void onEvent(SensorEvent<Boolean> event) {
+ boolean changed = (event.getValue()) ? addMember(event.getSource()) : removeMember(event.getSource());
+ if (changed) {
+ recomputeAddresses();
+ }
+ }});
+ subscriptionHandle2 = subscribe(t, Changeable.MEMBER_REMOVED, new SensorEventListener<Entity>() {
+ @Override public void onEvent(SensorEvent<Entity> event) {
+ removeMember(event.getValue());
+ // recompute, irrespective of change, because framework may have already invoked the removeMember call
+ recomputeAddresses();
+ }});
+ setMembers(t.getChildren(), EntityPredicates.attributeEqualTo(Startable.SERVICE_UP, true));
+ }
+
+ recomputeAddresses();
+ }
+
+ @Override
+ public void discard() {
+ Entities.unmanage(this);
+ }
+
+ private boolean hasProtocol(Collection<String> protocols, String desired) {
+ for (String contender : protocols) {
+ if ("https".equals(contender.toLowerCase())) return true;
+ }
+ return false;
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlRewriteRule.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlRewriteRule.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlRewriteRule.java
new file mode 100644
index 0000000..687fb63
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/proxy/nginx/UrlRewriteRule.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.nginx;
+
+import java.io.Serializable;
+
+/** records a rewrite rule for use in URL rewriting such as by nginx;
+ * from and to are expected to be usual regex replacement strings,
+ * with the convention here (for portability) that:
+ * <li>
+ * <it> from should match the entire path (internally is wrapped with ^ and $ for nginx);
+ * <it> to can refer to $1, $2 from the groups in from
+ * </li>
+ * so eg use from = (.*)A(.*) and to = $1B$2 to change all occurrences of A to B
+ */
+public class UrlRewriteRule implements Serializable {
+
+ private static final long serialVersionUID = -8457441487467968553L;
+
+ String from, to;
+ boolean isBreak;
+
+ /* there is also a flag "last" possible on nginx which might be useful,
+ * but i don't know how portable that is --
+ * we'll know e.g. when we support HA Proxy and others.
+ * presumably everything has at least one "break-after-this-rewrite" mode
+ * so i think we're safe having one in here.
+ */
+
+ public UrlRewriteRule() {}
+ public UrlRewriteRule(String from, String to) {
+ this.from = from;
+ this.to = to;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+ public void setFrom(String from) {
+ this.from = from;
+ }
+ public String getTo() {
+ return to;
+ }
+ public void setTo(String to) {
+ this.to = to;
+ }
+
+ public boolean isBreak() {
+ return isBreak;
+ }
+ public void setBreak(boolean isBreak) {
+ this.isBreak = isBreak;
+ }
+
+ public UrlRewriteRule setBreak() { setBreak(true); return this; }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppCluster.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppCluster.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppCluster.java
new file mode 100644
index 0000000..18d2eb8
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppCluster.java
@@ -0,0 +1,114 @@
+/*
+ * 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.webapp;
+
+import org.apache.brooklyn.catalog.Catalog;
+import org.apache.brooklyn.entity.proxy.LoadBalancer;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.ConfigurableEntityFactory;
+import brooklyn.entity.basic.DynamicGroup;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.group.Cluster;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.entity.trait.MemberReplaceable;
+import brooklyn.entity.trait.Resizable;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.util.flags.SetFromFlag;
+
+/**
+ * This entity contains the sub-groups and entities that go in to a single location (e.g. datacenter)
+ * to provide web-app cluster functionality, viz load-balancer (controller) and webapp software processes.
+ * <p>
+ * You can customise the web server by customising the memberSpec.
+ * <p>
+ * The children of this entity are:
+ * <ul>
+ * <li>a {@link brooklyn.entity.group.DynamicCluster} of {@link WebAppService}s (defaults to JBoss7Server)
+ * <li>a cluster controller (defaulting to Nginx if none supplied)
+ * </ul>
+ *
+ * This entity is also a group whose members mirror those of the child DynamicCluster (so do not include the load balancer).
+ * This is convenient for associating policies such as ServiceReplacer with this entity, rather
+ * than with the child {@link brooklyn.entity.group.DynamicCluster}. However, note that changing this entity's
+ * members has no effect on the members of the underlying DynamicCluster - treat this as a read-only view.
+ */
+@Catalog(name="Controlled Dynamic Web-app Cluster", description="A cluster of load-balanced web-apps, which can be dynamically re-sized")
+@ImplementedBy(ControlledDynamicWebAppClusterImpl.class)
+public interface ControlledDynamicWebAppCluster extends DynamicGroup, Entity, Startable, Resizable, MemberReplaceable,
+ Group, ElasticJavaWebAppService, JavaWebAppService.CanDeployAndUndeploy, JavaWebAppService.CanRedeployAll {
+
+ @SetFromFlag("initialSize")
+ public static ConfigKey<Integer> INITIAL_SIZE = ConfigKeys.newConfigKeyWithDefault(Cluster.INITIAL_SIZE, 1);
+
+ @SetFromFlag("controller")
+ public static BasicAttributeSensorAndConfigKey<LoadBalancer> CONTROLLER = new BasicAttributeSensorAndConfigKey<LoadBalancer>(
+ LoadBalancer.class, "controlleddynamicwebappcluster.controller", "Controller for the cluster; if null a default will created (using controllerSpec)");
+
+ @SetFromFlag("controlledGroup")
+ public static BasicAttributeSensorAndConfigKey<Group> CONTROLLED_GROUP = new BasicAttributeSensorAndConfigKey<Group>(
+ Group.class, "controlleddynamicwebappcluster.controlledgroup", "The group of web servers that the controller should point at; if null, will use the CLUSTER");
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @SetFromFlag("controllerSpec")
+ public static BasicAttributeSensorAndConfigKey<EntitySpec<? extends LoadBalancer>> CONTROLLER_SPEC = new BasicAttributeSensorAndConfigKey(
+ EntitySpec.class, "controlleddynamicwebappcluster.controllerSpec", "Spec for creating the controller (if one not supplied explicitly); if null an NGINX instance will be created");
+
+ @SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
+ /** factory (or closure) to create the web server, given flags */
+ @SetFromFlag("factory")
+ public static BasicAttributeSensorAndConfigKey<ConfigurableEntityFactory<? extends WebAppService>> FACTORY = new BasicAttributeSensorAndConfigKey(
+ ConfigurableEntityFactory.class, DynamicCluster.FACTORY.getName(), "factory (or closure) to create the web server");
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ /** Spec for web server entiites to be created */
+ @SetFromFlag("memberSpec")
+ public static BasicAttributeSensorAndConfigKey<EntitySpec<? extends WebAppService>> MEMBER_SPEC = new BasicAttributeSensorAndConfigKey(
+ EntitySpec.class, DynamicCluster.MEMBER_SPEC.getName(), "Spec for web server entiites to be created");
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @SetFromFlag("webClusterSpec")
+ public static BasicAttributeSensorAndConfigKey<EntitySpec<? extends DynamicWebAppCluster>> WEB_CLUSTER_SPEC = new BasicAttributeSensorAndConfigKey(
+ EntitySpec.class, "controlleddynamicwebappcluster.webClusterSpec", "Spec for creating the cluster; if null a DynamicWebAppCluster will be created");
+
+ public static AttributeSensor<DynamicWebAppCluster> CLUSTER = new BasicAttributeSensor<DynamicWebAppCluster>(
+ DynamicWebAppCluster.class, "controlleddynamicwebappcluster.cluster", "Underlying web-app cluster");
+
+ public static final AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME;
+
+ public static final AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = Attributes.SERVICE_STATE_ACTUAL;
+
+
+ public LoadBalancer getController();
+
+ public ConfigurableEntityFactory<WebAppService> getFactory();
+
+ public DynamicWebAppCluster getCluster();
+
+ public Group getControlledGroup();
+}