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