http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java new file mode 100644 index 0000000..a9e9809 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java @@ -0,0 +1,327 @@ +/* + * 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 java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.entity.proxy.LoadBalancer; +import org.apache.brooklyn.entity.proxy.nginx.NginxController; +import org.apache.brooklyn.entity.webapp.tomcat.TomcatServer; +import org.apache.brooklyn.management.Task; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.enricher.Enrichers; +import brooklyn.entity.Entity; +import brooklyn.entity.Group; +import brooklyn.entity.basic.Attributes; +import brooklyn.entity.basic.ConfigurableEntityFactory; +import brooklyn.entity.basic.DynamicGroupImpl; +import brooklyn.entity.basic.Entities; +import brooklyn.entity.basic.EntityPredicates; +import brooklyn.entity.basic.Lifecycle; +import brooklyn.entity.basic.ServiceStateLogic; +import brooklyn.entity.proxying.EntitySpec; +import brooklyn.entity.trait.Startable; +import brooklyn.entity.trait.StartableMethods; +import brooklyn.event.SensorEvent; +import brooklyn.event.SensorEventListener; +import brooklyn.event.feed.ConfigToAttributes; +import brooklyn.location.Location; +import brooklyn.util.collections.MutableList; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.collections.QuorumCheck.QuorumChecks; +import brooklyn.util.exceptions.Exceptions; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +public class ControlledDynamicWebAppClusterImpl extends DynamicGroupImpl implements ControlledDynamicWebAppCluster { + + public static final Logger log = LoggerFactory.getLogger(ControlledDynamicWebAppClusterImpl.class); + + public ControlledDynamicWebAppClusterImpl() { + this(MutableMap.of(), null); + } + + public ControlledDynamicWebAppClusterImpl(Map<?,?> flags) { + this(flags, null); + } + + public ControlledDynamicWebAppClusterImpl(Entity parent) { + this(MutableMap.of(), parent); + } + + @Deprecated + public ControlledDynamicWebAppClusterImpl(Map<?,?> flags, Entity parent) { + super(flags, parent); + } + + @Override + public void init() { + super.init(); + + ConfigToAttributes.apply(this, FACTORY); + ConfigToAttributes.apply(this, MEMBER_SPEC); + ConfigToAttributes.apply(this, CONTROLLER); + ConfigToAttributes.apply(this, CONTROLLER_SPEC); + ConfigToAttributes.apply(this, WEB_CLUSTER_SPEC); + ConfigToAttributes.apply(this, CONTROLLED_GROUP); + + ConfigurableEntityFactory<? extends WebAppService> webServerFactory = getAttribute(FACTORY); + EntitySpec<? extends WebAppService> webServerSpec = getAttribute(MEMBER_SPEC); + if (webServerFactory == null && webServerSpec == null) { + log.debug("creating default web server spec for {}", this); + webServerSpec = EntitySpec.create(TomcatServer.class); + setAttribute(MEMBER_SPEC, webServerSpec); + } + + log.debug("creating cluster child for {}", this); + // Note relies on initial_size being inherited by DynamicWebAppCluster, because key id is identical + EntitySpec<? extends DynamicWebAppCluster> webClusterSpec = getAttribute(WEB_CLUSTER_SPEC); + Map<String,Object> webClusterFlags; + if (webServerSpec != null) { + webClusterFlags = MutableMap.<String,Object>of("memberSpec", webServerSpec); + } else { + webClusterFlags = MutableMap.<String,Object>of("factory", webServerFactory); + } + if (webClusterSpec == null) { + log.debug("creating default web cluster spec for {}", this); + webClusterSpec = EntitySpec.create(DynamicWebAppCluster.class); + } + boolean hasMemberSpec = webClusterSpec.getConfig().containsKey(DynamicWebAppCluster.MEMBER_SPEC) || webClusterSpec.getFlags().containsKey("memberSpec"); + @SuppressWarnings("deprecation") + boolean hasMemberFactory = webClusterSpec.getConfig().containsKey(DynamicWebAppCluster.FACTORY) || webClusterSpec.getFlags().containsKey("factory"); + if (!(hasMemberSpec || hasMemberFactory)) { + webClusterSpec.configure(webClusterFlags); + } else { + log.warn("In {}, not setting cluster's {} because already set on webClusterSpec", new Object[] {this, webClusterFlags.keySet()}); + } + setAttribute(WEB_CLUSTER_SPEC, webClusterSpec); + + DynamicWebAppCluster cluster = addChild(webClusterSpec); + if (Entities.isManaged(this)) Entities.manage(cluster); + setAttribute(CLUSTER, cluster); + setEntityFilter(EntityPredicates.isMemberOf(cluster)); + + LoadBalancer controller = getAttribute(CONTROLLER); + if (controller == null) { + EntitySpec<? extends LoadBalancer> controllerSpec = getAttribute(CONTROLLER_SPEC); + if (controllerSpec == null) { + log.debug("creating controller using default spec for {}", this); + controllerSpec = EntitySpec.create(NginxController.class); + setAttribute(CONTROLLER_SPEC, controllerSpec); + } else { + log.debug("creating controller using custom spec for {}", this); + } + controller = addChild(controllerSpec); + addEnricher(Enrichers.builder().propagating(LoadBalancer.PROXY_HTTP_PORT, LoadBalancer.PROXY_HTTPS_PORT).from(controller).build()); + if (Entities.isManaged(this)) Entities.manage(controller); + setAttribute(CONTROLLER, controller); + } + + Group controlledGroup = getAttribute(CONTROLLED_GROUP); + if (controlledGroup == null) { + log.debug("using cluster as controlledGroup for {}", this); + controlledGroup = cluster; + setAttribute(CONTROLLED_GROUP, cluster); + } else { + log.debug("using custom controlledGroup {} for {}", controlledGroup, this); + } + + doBind(); + } + + @Override + protected void initEnrichers() { + if (config().getLocalRaw(UP_QUORUM_CHECK).isAbsent()) { + config().set(UP_QUORUM_CHECK, QuorumChecks.newInstance(2, 1.0, false)); + } + super.initEnrichers(); + ServiceStateLogic.newEnricherFromChildrenUp().checkChildrenOnly().requireUpChildren(getConfig(UP_QUORUM_CHECK)).addTo(this); + } + + @Override + public void rebind() { + super.rebind(); + doBind(); + } + + protected void doBind() { + DynamicWebAppCluster cluster = getAttribute(CLUSTER); + if (cluster != null) { + subscribe(cluster, DynamicWebAppCluster.GROUP_MEMBERS, new SensorEventListener<Object>() { + @Override public void onEvent(SensorEvent<Object> event) { + // TODO inefficient impl; also worth extracting this into a mixin of some sort. + rescanEntities(); + }}); + } + } + + @Override + public LoadBalancer getController() { + return getAttribute(CONTROLLER); + } + + @SuppressWarnings("unchecked") + @Override + public synchronized ConfigurableEntityFactory<WebAppService> getFactory() { + return (ConfigurableEntityFactory<WebAppService>) getAttribute(FACTORY); + } + + // TODO convert to an entity reference which is serializable + @Override + public synchronized DynamicWebAppCluster getCluster() { + return getAttribute(CLUSTER); + } + + @Override + public Group getControlledGroup() { + return getAttribute(CONTROLLED_GROUP); + } + + @Override + public void start(Collection<? extends Location> locations) { + ServiceStateLogic.setExpectedState(this, Lifecycle.STARTING); + + try { + if (isLegacyConstruction()) { + init(); + } + + if (locations.isEmpty()) locations = getLocations(); + addLocations(locations); + + LoadBalancer loadBalancer = getController(); + loadBalancer.bind(MutableMap.of("serverPool", getControlledGroup())); + + List<Entity> childrenToStart = MutableList.<Entity>of(getCluster()); + // Set controller as child of cluster, if it does not already have a parent + if (getController().getParent() == null) { + addChild(getController()); + } + + // And only start controller if we are parent. Favour locations defined on controller, if present + Task<List<Void>> startControllerTask = null; + if (this.equals(getController().getParent())) { + if (getController().getLocations().size() == 0) { + childrenToStart.add(getController()); + } else { + startControllerTask = Entities.invokeEffectorList(this, MutableList.<Entity>of(getController()), Startable.START, ImmutableMap.of("locations", getController().getLocations())); + } + } + + Entities.invokeEffectorList(this, childrenToStart, Startable.START, ImmutableMap.of("locations", locations)).get(); + if (startControllerTask != null) { + startControllerTask.get(); + } + + // wait for everything to start, then update controller, to ensure it is up to date + // (will happen asynchronously as members come online, but we want to force it to happen) + getController().update(); + + ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING); + } catch (Exception e) { + ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE); + throw Exceptions.propagate(e); + } finally { + connectSensors(); + } + } + + @Override + public void stop() { + ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING); + + try { + List<Startable> tostop = Lists.newArrayList(); + if (this.equals(getController().getParent())) tostop.add(getController()); + tostop.add(getCluster()); + + StartableMethods.stopSequentially(tostop); + + clearLocations(); + + ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED); + } catch (Exception e) { + ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE); + throw Exceptions.propagate(e); + } + } + + @Override + public void restart() { + // TODO prod the entities themselves to restart, instead? + Collection<Location> locations = Lists.newArrayList(getLocations()); + + stop(); + start(locations); + } + + void connectSensors() { + // FIXME no longer needed + addEnricher(Enrichers.builder() + .propagatingAllButUsualAnd(Attributes.MAIN_URI, ROOT_URL, GROUP_MEMBERS, GROUP_SIZE) + .from(getCluster()) + .build()); + addEnricher(Enrichers.builder() + // include hostname and address of controller (need both in case hostname only resolves to internal/private ip) + .propagating(LoadBalancer.HOSTNAME, Attributes.ADDRESS, Attributes.MAIN_URI, ROOT_URL) + .from(getController()) + .build()); + } + + @Override + public Integer resize(Integer desiredSize) { + return getCluster().resize(desiredSize); + } + + @Override + public String replaceMember(String memberId) { + return getCluster().replaceMember(memberId); + } + + /** + * @return the current size of the group. + */ + @Override + public Integer getCurrentSize() { + return getCluster().getCurrentSize(); + } + + @Override + public void deploy(String url, String targetName) { + DynamicWebAppClusterImpl.addToWarsByContext(this, url, targetName); + getCluster().deploy(url, targetName); + } + + @Override + public void undeploy(String targetName) { + DynamicWebAppClusterImpl.removeFromWarsByContext(this, targetName); + getCluster().undeploy(targetName); + } + + @Override + public void redeployAll() { + getCluster().redeployAll(); + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppCluster.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppCluster.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppCluster.java new file mode 100644 index 0000000..1c196c6 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppCluster.java @@ -0,0 +1,69 @@ +/* + * 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 brooklyn.config.render.RendererHints; +import brooklyn.entity.group.DynamicCluster; +import brooklyn.entity.proxying.ImplementedBy; +import brooklyn.event.AttributeSensor; +import brooklyn.event.basic.BasicAttributeSensor; +import brooklyn.util.time.Duration; + +/** + * DynamicWebAppClusters provide cluster-wide aggregates of entity attributes. Currently totals and averages: + * <ul> + * <li>Entity request counts</li> + * <li>Entity error counts</li> + * <li>Requests per second</li> + * <li>Entity processing time</li> + * </ul> + */ +@Catalog(name="Dynamic Web-app Cluster", description="A cluster of web-apps, which can be dynamically re-sized; this does not include a load-balancer") +@ImplementedBy(DynamicWebAppClusterImpl.class) +public interface DynamicWebAppCluster extends DynamicCluster, WebAppService, JavaWebAppService, + JavaWebAppService.CanDeployAndUndeploy, JavaWebAppService.CanRedeployAll { + + public static final AttributeSensor<Double> REQUEST_COUNT_PER_NODE = new BasicAttributeSensor<Double>( + Double.class, "webapp.reqs.total.perNode", "Cluster entity request average"); + + public static final AttributeSensor<Integer> ERROR_COUNT_PER_NODE = new BasicAttributeSensor<Integer>( + Integer.class, "webapp.reqs.errors.perNode", "Cluster entity request error average"); + + public static final AttributeSensor<Double> REQUESTS_PER_SECOND_LAST_PER_NODE = new BasicAttributeSensor<Double>( + Double.class, "webapp.reqs.perSec.last.perNode", "Reqs/sec (last datapoint) averaged over all nodes"); + + public static final AttributeSensor<Double> REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE = new BasicAttributeSensor<Double>( + Double.class, "webapp.reqs.perSec.windowed.perNode", "Reqs/sec (over time window) averaged over all nodes"); + + public static final AttributeSensor<Integer> TOTAL_PROCESSING_TIME_PER_NODE = ApplyDisplayHints.TOTAL_PROCESSING_TIME_PER_NODE; + + public static final AttributeSensor<Double> PROCESSING_TIME_FRACTION_IN_WINDOW_PER_NODE = new BasicAttributeSensor<Double>( + Double.class, "webapp.reqs.processingTime.fraction.windowed.perNode", "Fraction of time spent processing " + + "reported by webserver (percentage, over time window) averaged over all nodes"); + + class ApplyDisplayHints { + public static final AttributeSensor<Integer> TOTAL_PROCESSING_TIME_PER_NODE = new BasicAttributeSensor<Integer>( + Integer.class, "webapp.reqs.processingTime.perNode", "Total processing time per node (millis)"); + static { + RendererHints.register(TOTAL_PROCESSING_TIME_PER_NODE, RendererHints.displayValue(Duration.millisToStringRounded())); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterImpl.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterImpl.java new file mode 100644 index 0000000..d1f1da7 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterImpl.java @@ -0,0 +1,263 @@ +/* + * 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 static com.google.common.base.Preconditions.checkNotNull; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +import org.apache.brooklyn.management.Task; +import org.apache.brooklyn.management.TaskAdaptable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.enricher.Enrichers; +import brooklyn.entity.Entity; +import brooklyn.entity.basic.Attributes; +import brooklyn.entity.basic.Entities; +import brooklyn.entity.basic.EntityInternal; +import brooklyn.entity.effector.Effectors; +import brooklyn.entity.group.DynamicCluster; +import brooklyn.entity.group.DynamicClusterImpl; +import brooklyn.event.AttributeSensor; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.collections.MutableSet; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.task.DynamicTasks; +import brooklyn.util.task.TaskBuilder; +import brooklyn.util.task.TaskTags; +import brooklyn.util.task.Tasks; +import brooklyn.util.time.Duration; +import brooklyn.util.time.Time; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +/** + * DynamicWebAppClusters provide cluster-wide aggregates of entity attributes. Currently totals and averages: + * <ul> + * <li>Entity request counts</li> + * <li>Entity error counts</li> + * <li>Requests per second</li> + * <li>Entity processing time</li> + * </ul> + */ +public class DynamicWebAppClusterImpl extends DynamicClusterImpl implements DynamicWebAppCluster { + + private static final Logger log = LoggerFactory.getLogger(DynamicWebAppClusterImpl.class); + private static final FilenameToWebContextMapper FILENAME_TO_WEB_CONTEXT_MAPPER = new FilenameToWebContextMapper(); + + /** + * Instantiate a new DynamicWebAppCluster. Parameters as per {@link DynamicCluster#DynamicCluster()} + */ + public DynamicWebAppClusterImpl() { + super(); + } + + @Override + public void init() { + super.init(); + // Enricher attribute setup. A way of automatically discovering these (but avoiding + // averaging things like HTTP port and response codes) would be neat. + List<? extends List<? extends AttributeSensor<? extends Number>>> summingEnricherSetup = ImmutableList.of( + ImmutableList.of(REQUEST_COUNT, REQUEST_COUNT), + ImmutableList.of(ERROR_COUNT, ERROR_COUNT), + ImmutableList.of(REQUESTS_PER_SECOND_LAST, REQUESTS_PER_SECOND_LAST), + ImmutableList.of(REQUESTS_PER_SECOND_IN_WINDOW, REQUESTS_PER_SECOND_IN_WINDOW), + ImmutableList.of(TOTAL_PROCESSING_TIME, TOTAL_PROCESSING_TIME), + ImmutableList.of(PROCESSING_TIME_FRACTION_IN_WINDOW, PROCESSING_TIME_FRACTION_IN_WINDOW) + ); + + List<? extends List<? extends AttributeSensor<? extends Number>>> averagingEnricherSetup = ImmutableList.of( + ImmutableList.of(REQUEST_COUNT, REQUEST_COUNT_PER_NODE), + ImmutableList.of(ERROR_COUNT, ERROR_COUNT_PER_NODE), + ImmutableList.of(REQUESTS_PER_SECOND_LAST, REQUESTS_PER_SECOND_LAST_PER_NODE), + ImmutableList.of(REQUESTS_PER_SECOND_IN_WINDOW, REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE), + ImmutableList.of(TOTAL_PROCESSING_TIME, TOTAL_PROCESSING_TIME_PER_NODE), + ImmutableList.of(PROCESSING_TIME_FRACTION_IN_WINDOW, PROCESSING_TIME_FRACTION_IN_WINDOW_PER_NODE) + ); + + for (List<? extends AttributeSensor<? extends Number>> es : summingEnricherSetup) { + AttributeSensor<? extends Number> t = es.get(0); + AttributeSensor<? extends Number> total = es.get(1); + addEnricher(Enrichers.builder() + .aggregating(t) + .publishing(total) + .fromMembers() + .computingSum() + .build()); + } + + for (List<? extends AttributeSensor<? extends Number>> es : averagingEnricherSetup) { + @SuppressWarnings("unchecked") + AttributeSensor<Number> t = (AttributeSensor<Number>) es.get(0); + @SuppressWarnings("unchecked") + AttributeSensor<Double> average = (AttributeSensor<Double>) es.get(1); + addEnricher(Enrichers.builder() + .aggregating(t) + .publishing(average) + .fromMembers() + .computingAverage() + .defaultValueForUnreportedSensors(0) + .build()); + } + } + + // TODO this will probably be useful elsewhere ... but where to put it? + // TODO add support for this in DependentConfiguration (see TODO there) + /** Waits for the given target to report service up, then runs the given task + * (often an invocation on that entity), with the given name. + * If the target goes away, this task marks itself inessential + * before failing so as not to cause a parent task to fail. */ + static <T> Task<T> whenServiceUp(final Entity target, final TaskAdaptable<T> task, String name) { + return Tasks.<T>builder().name(name).dynamic(true).body(new Callable<T>() { + @Override + public T call() { + try { + while (true) { + if (!Entities.isManaged(target)) { + Tasks.markInessential(); + throw new IllegalStateException("Target "+target+" is no longer managed"); + } + if (Boolean.TRUE.equals(target.getAttribute(Attributes.SERVICE_UP))) { + Tasks.resetBlockingDetails(); + TaskTags.markInessential(task); + DynamicTasks.queue(task); + try { + return task.asTask().getUnchecked(); + } catch (Exception e) { + if (Entities.isManaged(target)) { + throw Exceptions.propagate(e); + } else { + Tasks.markInessential(); + throw new IllegalStateException("Target "+target+" is no longer managed", e); + } + } + } else { + Tasks.setBlockingDetails("Waiting on "+target+" to be ready"); + } + // TODO replace with subscription? + Time.sleep(Duration.ONE_SECOND); + } + } finally { + Tasks.resetBlockingDetails(); + } + } + }).build(); + } + + @Override + public void deploy(String url, String targetName) { + checkNotNull(url, "url"); + checkNotNull(targetName, "targetName"); + targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName); + + // set it up so future nodes get the right wars + addToWarsByContext(this, url, targetName); + + log.debug("Deploying "+targetName+"->"+url+" across cluster "+this+"; WARs now "+getConfig(WARS_BY_CONTEXT)); + + Iterable<CanDeployAndUndeploy> targets = Iterables.filter(getChildren(), CanDeployAndUndeploy.class); + TaskBuilder<Void> tb = Tasks.<Void>builder().parallel(true).name("Deploy "+targetName+" to cluster (size "+Iterables.size(targets)+")"); + for (Entity target: targets) { + tb.add(whenServiceUp(target, Effectors.invocation(target, DEPLOY, MutableMap.of("url", url, "targetName", targetName)), + "Deploy "+targetName+" to "+target+" when ready")); + } + DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked(); + + // Update attribute + // TODO support for atomic sensor update (should be part of standard tooling; NB there is some work towards this, according to @aledsage) + Set<String> deployedWars = MutableSet.copyOf(getAttribute(DEPLOYED_WARS)); + deployedWars.add(targetName); + setAttribute(DEPLOYED_WARS, deployedWars); + } + + @Override + public void undeploy(String targetName) { + checkNotNull(targetName, "targetName"); + targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName); + + // set it up so future nodes get the right wars + if (!removeFromWarsByContext(this, targetName)) { + DynamicTasks.submit(Tasks.warning("Context "+targetName+" not known at "+this+"; attempting to undeploy regardless", null), this); + } + + log.debug("Undeploying "+targetName+" across cluster "+this+"; WARs now "+getConfig(WARS_BY_CONTEXT)); + + Iterable<CanDeployAndUndeploy> targets = Iterables.filter(getChildren(), CanDeployAndUndeploy.class); + TaskBuilder<Void> tb = Tasks.<Void>builder().parallel(true).name("Undeploy "+targetName+" across cluster (size "+Iterables.size(targets)+")"); + for (Entity target: targets) { + tb.add(whenServiceUp(target, Effectors.invocation(target, UNDEPLOY, MutableMap.of("targetName", targetName)), + "Undeploy "+targetName+" at "+target+" when ready")); + } + DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked(); + + // Update attribute + Set<String> deployedWars = MutableSet.copyOf(getAttribute(DEPLOYED_WARS)); + deployedWars.remove( FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName) ); + setAttribute(DEPLOYED_WARS, deployedWars); + } + + static void addToWarsByContext(Entity entity, String url, String targetName) { + targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName); + // TODO a better way to do atomic updates, see comment above + synchronized (entity) { + Map<String,String> newWarsMap = MutableMap.copyOf(entity.getConfig(WARS_BY_CONTEXT)); + newWarsMap.put(targetName, url); + ((EntityInternal)entity).setConfig(WARS_BY_CONTEXT, newWarsMap); + } + } + + static boolean removeFromWarsByContext(Entity entity, String targetName) { + targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName); + // TODO a better way to do atomic updates, see comment above + synchronized (entity) { + Map<String,String> newWarsMap = MutableMap.copyOf(entity.getConfig(WARS_BY_CONTEXT)); + String url = newWarsMap.remove(targetName); + if (url==null) { + return false; + } + ((EntityInternal)entity).setConfig(WARS_BY_CONTEXT, newWarsMap); + return true; + } + } + + @Override + public void redeployAll() { + Map<String, String> wars = MutableMap.copyOf(getConfig(WARS_BY_CONTEXT)); + String redeployPrefix = "Redeploy all WARs (count "+wars.size()+")"; + + log.debug("Redeplying all WARs across cluster "+this+": "+getConfig(WARS_BY_CONTEXT)); + + Iterable<CanDeployAndUndeploy> targetEntities = Iterables.filter(getChildren(), CanDeployAndUndeploy.class); + TaskBuilder<Void> tb = Tasks.<Void>builder().parallel(true).name(redeployPrefix+" across cluster (size "+Iterables.size(targetEntities)+")"); + for (Entity targetEntity: targetEntities) { + TaskBuilder<Void> redeployAllToTarget = Tasks.<Void>builder().name(redeployPrefix+" at "+targetEntity+" (after ready check)"); + for (String warContextPath: wars.keySet()) { + redeployAllToTarget.add(Effectors.invocation(targetEntity, DEPLOY, MutableMap.of("url", wars.get(warContextPath), "targetName", warContextPath))); + } + tb.add(whenServiceUp(targetEntity, redeployAllToTarget.build(), redeployPrefix+" at "+targetEntity+" when ready")); + } + DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabric.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabric.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabric.java new file mode 100644 index 0000000..2399b8c --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabric.java @@ -0,0 +1,48 @@ +/* + * 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 brooklyn.entity.group.DynamicFabric; +import brooklyn.entity.proxying.ImplementedBy; +import brooklyn.event.AttributeSensor; +import brooklyn.event.basic.BasicAttributeSensor; + +/** + * DynamicWebAppFabric provide a fabric of clusters, aggregating the entity attributes. Currently totals and averages: + * <ul> + * <li>Entity request counts</li> + * <li>Entity error counts</li> + * <li>Requests per second</li> + * <li>Entity processing time</li> + * </ul> + */ +@ImplementedBy(DynamicWebAppFabricImpl.class) +public interface DynamicWebAppFabric extends DynamicFabric, WebAppService { + + public static final AttributeSensor<Double> REQUEST_COUNT_PER_NODE = new BasicAttributeSensor<Double>( + Double.class, "webapp.reqs.total.perNode", "Fabric entity request average"); + + public static final AttributeSensor<Integer> ERROR_COUNT_PER_NODE = new BasicAttributeSensor<Integer>( + Integer.class, "webapp.reqs.errors.perNode", "Fabric entity request error average"); + + public static final AttributeSensor<Double> REQUESTS_PER_SECOND_LAST_PER_NODE = DynamicWebAppCluster.REQUESTS_PER_SECOND_LAST_PER_NODE; + public static final AttributeSensor<Double> REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE = DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE; + public static final AttributeSensor<Integer> TOTAL_PROCESSING_TIME_PER_NODE = DynamicWebAppCluster.TOTAL_PROCESSING_TIME_PER_NODE; + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricImpl.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricImpl.java new file mode 100644 index 0000000..f6d8e17 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricImpl.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.webapp; + +import java.util.List; + +import javax.annotation.Nullable; + +import brooklyn.enricher.Enrichers; +import brooklyn.entity.group.DynamicFabricImpl; +import brooklyn.event.AttributeSensor; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; + +public class DynamicWebAppFabricImpl extends DynamicFabricImpl implements DynamicWebAppFabric { + + @Override + public void onManagementBecomingMaster() { + // Enricher attribute setup. A way of automatically discovering these (but avoiding + // averaging things like HTTP port and response codes) would be neat. + List<? extends List<? extends AttributeSensor<? extends Number>>> summingEnricherSetup = ImmutableList.of( + ImmutableList.of(REQUEST_COUNT, REQUEST_COUNT), + ImmutableList.of(ERROR_COUNT, ERROR_COUNT), + ImmutableList.of(REQUESTS_PER_SECOND_LAST, REQUESTS_PER_SECOND_LAST), + ImmutableList.of(REQUESTS_PER_SECOND_IN_WINDOW, REQUESTS_PER_SECOND_IN_WINDOW), + ImmutableList.of(TOTAL_PROCESSING_TIME, TOTAL_PROCESSING_TIME) + ); + + List<? extends List<? extends AttributeSensor<? extends Number>>> averagingEnricherSetup = ImmutableList.of( + ImmutableList.of(REQUEST_COUNT, REQUEST_COUNT_PER_NODE), + ImmutableList.of(ERROR_COUNT, ERROR_COUNT_PER_NODE), + ImmutableList.of(REQUESTS_PER_SECOND_LAST, REQUESTS_PER_SECOND_LAST_PER_NODE), + ImmutableList.of(REQUESTS_PER_SECOND_IN_WINDOW, REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE), + ImmutableList.of(TOTAL_PROCESSING_TIME, TOTAL_PROCESSING_TIME_PER_NODE) + ); + + for (List<? extends AttributeSensor<? extends Number>> es : summingEnricherSetup) { + AttributeSensor<? extends Number> t = es.get(0); + AttributeSensor<? extends Number> total = es.get(1); + addEnricher(Enrichers.builder() + .aggregating(t) + .publishing(total) + .fromMembers() + .computingSum() + .build()); + } + + for (List<? extends AttributeSensor<? extends Number>> es : averagingEnricherSetup) { + @SuppressWarnings("unchecked") + AttributeSensor<Number> t = (AttributeSensor<Number>) es.get(0); + @SuppressWarnings("unchecked") + AttributeSensor<Double> average = (AttributeSensor<Double>) es.get(1); + + // TODO This needs to respond to changes in FABRIC_SIZE as well, to recalculate + addEnricher(Enrichers.builder() + .transforming(t) + .publishing(average) + .computing(new Function<Number, Double>() { + @Override public Double apply(@Nullable Number input) { + Integer size = getAttribute(DynamicWebAppFabric.FABRIC_SIZE); + return (size != null && input != null) ? (input.doubleValue() / size) : null; + }}) + .build()); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ElasticJavaWebAppService.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ElasticJavaWebAppService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ElasticJavaWebAppService.java new file mode 100644 index 0000000..2a2e00b --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ElasticJavaWebAppService.java @@ -0,0 +1,60 @@ +/* + * 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 java.util.Map; + +import brooklyn.entity.Entity; +import brooklyn.entity.basic.AbstractConfigurableEntityFactory; +import brooklyn.entity.basic.ConfigurableEntityFactory; +import brooklyn.entity.basic.EntityFactoryForLocation; +import brooklyn.entity.proxying.EntitySpec; +import brooklyn.entity.trait.Startable; +import brooklyn.location.Location; +import brooklyn.location.MachineProvisioningLocation; + +public interface ElasticJavaWebAppService extends JavaWebAppService, Startable { + + public interface ElasticJavaWebAppServiceAwareLocation { + ConfigurableEntityFactory<ElasticJavaWebAppService> newWebClusterFactory(); + } + + /** @deprecated since 0.7.0 use {@link EntitySpec} */ + @Deprecated + public static class Factory extends AbstractConfigurableEntityFactory<ElasticJavaWebAppService> + implements EntityFactoryForLocation<ElasticJavaWebAppService> { + + private static final long serialVersionUID = 6654647949712073832L; + + public ElasticJavaWebAppService newEntity2(@SuppressWarnings("rawtypes") Map flags, Entity parent) { + return new ControlledDynamicWebAppClusterImpl(flags, parent); + } + + public ConfigurableEntityFactory<ElasticJavaWebAppService> newFactoryForLocation(Location l) { + if (l instanceof ElasticJavaWebAppServiceAwareLocation) { + return ((ElasticJavaWebAppServiceAwareLocation)l).newWebClusterFactory().configure(config); + } + //optional, fail fast if location not supported + if (!(l instanceof MachineProvisioningLocation)) + throw new UnsupportedOperationException("cannot create this entity in location "+l); + return this; + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/FilenameToWebContextMapper.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/FilenameToWebContextMapper.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/FilenameToWebContextMapper.java new file mode 100644 index 0000000..7da2975 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/FilenameToWebContextMapper.java @@ -0,0 +1,92 @@ +/* + * 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 java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** utilities for translating consistently between a filename (http://acme.org/foo.war) and a web context path (/foo) */ +public class FilenameToWebContextMapper { + + public static final Logger log = LoggerFactory.getLogger(FilenameToWebContextMapper.class); + + public String findArchiveNameFromUrl(String url, boolean verbose) { + String name = url.substring(url.lastIndexOf('/') + 1); + if (name.indexOf("?")>0) { + Pattern p = Pattern.compile("[A-Za-z0-9_\\-]+\\..(ar|AR)($|(?=[^A-Za-z0-9_\\-]))"); + Matcher wars = p.matcher(name); + if (wars.find()) { + // take first such string + name = wars.group(); + if (wars.find()) { + if (verbose) log.warn("Not clear which archive to deploy for "+url+": using "+name); + } else { + if (verbose) log.info("Inferred archive to deploy for "+url+": using "+name); + } + } else { + if (verbose) log.warn("Not clear which archive to deploy for "+url+": using "+name); + } + } + return name; + } + + public String convertDeploymentTargetNameToFilename(String targetName) { + String result = targetName; + if (result.isEmpty()) return ""; + if (targetName.startsWith("/")) { + // treat input as a context + result = result.substring(1); + if (result.length()==0) result="ROOT"; + result += ".war"; + } else { + // treat input as a file, unless it has no dots in it + if (result.indexOf('.')==-1) result += ".war"; + } + return result; + } + + public String convertDeploymentTargetNameToContext(String targetName) { + String result = targetName; + if (result.isEmpty()) return ""; + if (targetName.startsWith("/")) { + // treat input as a context - noop + } else { + // make it look like a context + result = "/"+result; + if (result.indexOf('.')==-1) { + // no dot means no more processing + } else { + // look at extension + String extension = result.substring(result.lastIndexOf('.')+1).toUpperCase(); + if (extension.matches(".AR")) { + // looks like it was a WAR/EAR/etc + result = result.substring(0, result.length()-4); + if (result.equalsIgnoreCase("/ROOT")) result = "/"; + } else { + // input didn't look like a war filename, no more processing + } + } + } + return result; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/HttpsSslConfig.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/HttpsSslConfig.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/HttpsSslConfig.java new file mode 100644 index 0000000..a750411 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/HttpsSslConfig.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.webapp; + +import java.util.Map; + +import brooklyn.util.guava.Maybe; + +public class HttpsSslConfig { + + private String keystoreUrl; + private String keystorePassword; + private String keyAlias; + + public HttpsSslConfig() { + } + + public HttpsSslConfig keystoreUrl(String val) { + keystoreUrl = val; return this; + } + + public HttpsSslConfig keystorePassword(String val) { + keystorePassword = val; return this; + } + + public HttpsSslConfig keyAlias(String val) { + keyAlias = val; return this; + } + + public String getKeystoreUrl() { + return keystoreUrl; + } + + public String getKeystorePassword() { + return keystorePassword; + } + + public String getKeyAlias() { + return keyAlias; + } + + // method naming convention allows it to be used by TypeCoercions + public static HttpsSslConfig fromMap(Map<String,String> map) { + HttpsSslConfig result = new HttpsSslConfig(); + result.keystoreUrl = first(map, "keystoreUrl", "url").orNull(); + result.keystorePassword = first(map, "keystorePassword", "password").orNull(); + result.keyAlias = first(map, "keyAlias", "alias", "key").orNull(); + return result; + } + + private static Maybe<String> first(Map<String,String> map, String ...keys) { + for (String key: keys) { + if (map.containsKey(key)) + return Maybe.of(map.get(key)); + } + return Maybe.<String>absent(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppDriver.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppDriver.java new file mode 100644 index 0000000..822519b --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppDriver.java @@ -0,0 +1,54 @@ +/* + * 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 java.io.File; +import java.util.Set; + +import brooklyn.entity.java.JavaSoftwareProcessDriver; + +public interface JavaWebAppDriver extends JavaSoftwareProcessDriver { + + Set<String> getEnabledProtocols(); + + Integer getHttpPort(); + + Integer getHttpsPort(); + + HttpsSslConfig getHttpsSslConfig(); + + void deploy(File file); + + void deploy(File f, String targetName); + + /** + * Deploys a URL as a webapp at the appserver. + * <p> + * See {@link JavaWebAppSoftwareProcess#deploy(String, String)} for details of how input filenames are handled. + * + * @return A token which can be used as an argument to undeploy. + * Typically the web context with leading slash where the app can be reached (just "/" for ROOT) + */ + String deploy(String url, String targetName); + + void undeploy(String targetName); + + FilenameToWebContextMapper getFilenameContextMapper(); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppService.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppService.java new file mode 100644 index 0000000..8b17bc3 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppService.java @@ -0,0 +1,109 @@ +/* + * 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 java.util.List; +import java.util.Map; +import java.util.Set; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.Entity; +import brooklyn.entity.annotation.Effector; +import brooklyn.entity.annotation.EffectorParam; +import brooklyn.entity.basic.MethodEffector; +import brooklyn.entity.java.UsesJava; +import brooklyn.event.AttributeSensor; +import brooklyn.event.basic.BasicAttributeSensor; +import brooklyn.event.basic.BasicConfigKey; +import brooklyn.util.flags.SetFromFlag; + +public interface JavaWebAppService extends WebAppService, UsesJava { + + @SetFromFlag("war") + public static final ConfigKey<String> ROOT_WAR = new BasicConfigKey<String>( + String.class, "wars.root", "WAR file to deploy as the ROOT, as URL (supporting file: and classpath: prefixes)"); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @SetFromFlag("wars") + public static final ConfigKey<List<String>> NAMED_WARS = new BasicConfigKey( + List.class, "wars.named", "Archive files to deploy, as URL strings (supporting file: and classpath: prefixes); context (path in user-facing URL) will be inferred by name"); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @SetFromFlag("warsByContext") + public static final ConfigKey<Map<String,String>> WARS_BY_CONTEXT = new BasicConfigKey( + Map.class, "wars.by.context", "Map of context keys (path in user-facing URL, typically without slashes) to archives (e.g. WARs by URL) to deploy, supporting file: and classpath: prefixes)"); + + /** Optional marker interface for entities which support 'deploy' and 'undeploy' */ + public interface CanDeployAndUndeploy extends Entity { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static final AttributeSensor<Set<String>> DEPLOYED_WARS = new BasicAttributeSensor( + Set.class, "webapp.deployedWars", "Names of archives/contexts that are currently deployed"); + + public static final MethodEffector<Void> DEPLOY = new MethodEffector<Void>(CanDeployAndUndeploy.class, "deploy"); + public static final MethodEffector<Void> UNDEPLOY = new MethodEffector<Void>(CanDeployAndUndeploy.class, "undeploy"); + + /** + * Deploys the given artifact, from a source URL, to a given deployment filename/context. + * There is some variance in expected filename/context at various servers, + * so the following conventions are followed: + * <p> + * either ROOT.WAR or / denotes root context + * <p> + * anything of form FOO.?AR (ending .?AR) is copied with that name (unless copying not necessary) + * and is expected to be served from /FOO + * <p> + * anything of form /FOO (with leading slash) is expected to be served from /FOO + * (and is copied as FOO.WAR) + * <p> + * anything of form FOO (without a dot) is expected to be served from /FOO + * (and is copied as FOO.WAR) + * <p> + * otherwise <i>please note</i> behaviour may vary on different appservers; + * e.g. FOO.FOO would probably be ignored on appservers which expect a file copied across (usually), + * but served as /FOO.FOO on systems that take a deployment context. + * <p> + * See {@link FileNameToContextMappingTest} for definitive examples! + * + * @param url where to get the war, as a URL, either classpath://xxx or file:///home/xxx or http(s)... + * @param targetName where to tell the server to serve the WAR, see above + */ + @Effector(description="Deploys the given artifact, from a source URL, to a given deployment filename/context") + public void deploy( + @EffectorParam(name="url", description="URL of WAR file") String url, + @EffectorParam(name="targetName", description="context path where WAR should be deployed (/ for ROOT)") String targetName); + + /** + * For the DEPLOYED_WARS to be updated, the input must match the result of the call to deploy, + * e.g. the transformed name using + */ + @Effector(description="Undeploys the given context/artifact") + public void undeploy( + @EffectorParam(name="targetName") String targetName); + } + + /** Optional marker interface for entities which support 'redeployAll' */ + public interface CanRedeployAll { + public static final MethodEffector<Void> REDEPLOY_ALL = new MethodEffector<Void>(CanRedeployAll.class, "redeployAll"); + + @Effector(description="Redeploys all web apps known here across the cluster (e.g. if it gets into an inconsistent state)") + public void redeployAll(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcess.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcess.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcess.java new file mode 100644 index 0000000..b0563fd --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcess.java @@ -0,0 +1,34 @@ +/* + * 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 brooklyn.entity.basic.SoftwareProcess; + +public interface JavaWebAppSoftwareProcess extends SoftwareProcess, JavaWebAppService, JavaWebAppService.CanDeployAndUndeploy { + + // exist on the interface for freemarker to pick it up + + public boolean isHttpEnabled(); + public boolean isHttpsEnabled(); + public Integer getHttpPort(); + public Integer getHttpsPort(); + public String getHttpsSslKeyAlias(); + public String getHttpsSslKeystorePassword(); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcessImpl.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcessImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcessImpl.java new file mode 100644 index 0000000..1eb3660 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcessImpl.java @@ -0,0 +1,206 @@ +/* + * 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 static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.entity.Entity; +import brooklyn.entity.annotation.Effector; +import brooklyn.entity.annotation.EffectorParam; +import brooklyn.entity.basic.SoftwareProcessImpl; +import brooklyn.entity.java.JavaAppUtils; + +import com.google.common.base.Throwables; +import com.google.common.collect.Sets; + +public abstract class JavaWebAppSoftwareProcessImpl extends SoftwareProcessImpl implements JavaWebAppService, JavaWebAppSoftwareProcess { + + private static final Logger LOG = LoggerFactory.getLogger(JavaWebAppSoftwareProcessImpl.class); + + public JavaWebAppSoftwareProcessImpl(){ + super(); + } + + @SuppressWarnings("rawtypes") + public JavaWebAppSoftwareProcessImpl(Entity parent){ + this(new LinkedHashMap(),parent); + } + + @SuppressWarnings("rawtypes") + public JavaWebAppSoftwareProcessImpl(Map flags){ + this(flags, null); + } + + @SuppressWarnings("rawtypes") + public JavaWebAppSoftwareProcessImpl(Map flags, Entity parent) { + super(flags, parent); + } + + @Override + public void init() { + super.init(); + + WebAppServiceMethods.connectWebAppServerPolicies(this); + JavaAppUtils.connectJavaAppServerPolicies(this); + } + + //just provide better typing + public JavaWebAppDriver getDriver() { + return (JavaWebAppDriver) super.getDriver(); + } + + // TODO thread-safety issues: if multiple concurrent calls, may break (e.g. deployment_wars being reset) + public void deployInitialWars() { + if (getAttribute(DEPLOYED_WARS) == null) setAttribute(DEPLOYED_WARS, Sets.<String>newLinkedHashSet()); + + String rootWar = getConfig(ROOT_WAR); + if (rootWar!=null) deploy(rootWar, "ROOT.war"); + + List<String> namedWars = getConfig(NAMED_WARS, Collections.<String>emptyList()); + for(String war: namedWars){ + deploy(war, getDriver().getFilenameContextMapper().findArchiveNameFromUrl(war, true)); + } + + Map<String,String> warsByContext = getConfig(WARS_BY_CONTEXT); + if (warsByContext!=null) { + for (String context: warsByContext.keySet()) { + deploy(warsByContext.get(context), context); + } + } + } + + /** + * Deploys the given artifact, from a source URL, to a given deployment filename/context. + * There is some variance in expected filename/context at various servers, + * so the following conventions are followed: + * <p> + * either ROOT.WAR or / denotes root context + * <p> + * anything of form FOO.?AR (ending .?AR) is copied with that name (unless copying not necessary) + * and is expected to be served from /FOO + * <p> + * anything of form /FOO (with leading slash) is expected to be served from /FOO + * (and is copied as FOO.WAR) + * <p> + * anything of form FOO (without a dot) is expected to be served from /FOO + * (and is copied as FOO.WAR) + * <p> + * otherwise <i>please note</i> behaviour may vary on different appservers; + * e.g. FOO.FOO would probably be ignored on appservers which expect a file copied across (usually), + * but served as /FOO.FOO on systems that take a deployment context. + * <p> + * See {@link FileNameToContextMappingTest} for definitive examples! + * + * @param url where to get the war, as a URL, either classpath://xxx or file:///home/xxx or http(s)... + * @param targetName where to tell the server to serve the WAR, see above + */ + @Effector(description="Deploys the given artifact, from a source URL, to a given deployment filename/context") + public void deploy( + @EffectorParam(name="url", description="URL of WAR file") String url, + @EffectorParam(name="targetName", description="context path where WAR should be deployed (/ for ROOT)") String targetName) { + try { + checkNotNull(url, "url"); + checkNotNull(targetName, "targetName"); + JavaWebAppDriver driver = getDriver(); + String deployedName = driver.deploy(url, targetName); + + // Update attribute + Set<String> deployedWars = getAttribute(DEPLOYED_WARS); + if (deployedWars == null) { + deployedWars = Sets.newLinkedHashSet(); + } + deployedWars.add(deployedName); + setAttribute(DEPLOYED_WARS, deployedWars); + } catch (RuntimeException e) { + // Log and propagate, so that log says which entity had problems... + LOG.warn("Error deploying '"+url+"' to "+targetName+" on "+toString()+"; rethrowing...", e); + throw Throwables.propagate(e); + } + } + + /** For the DEPLOYED_WARS to be updated, the input must match the result of the call to deploy */ + @Override + @Effector(description="Undeploys the given context/artifact") + public void undeploy( + @EffectorParam(name="targetName") String targetName) { + try { + JavaWebAppDriver driver = getDriver(); + driver.undeploy(targetName); + + // Update attribute + Set<String> deployedWars = getAttribute(DEPLOYED_WARS); + if (deployedWars == null) { + deployedWars = Sets.newLinkedHashSet(); + } + deployedWars.remove( driver.getFilenameContextMapper().convertDeploymentTargetNameToContext(targetName) ); + setAttribute(DEPLOYED_WARS, deployedWars); + } catch (RuntimeException e) { + // Log and propagate, so that log says which entity had problems... + LOG.warn("Error undeploying '"+targetName+"' on "+toString()+"; rethrowing...", e); + throw Throwables.propagate(e); + } + } + + @Override + protected void postStop() { + super.postStop(); + // zero our workrate derived workrates. + // TODO might not be enough, as policy may still be executing and have a record of historic vals; should remove policies + // (also not sure we want this; implies more generally a responsibility for sensors to announce things when disconnected, + // vs them just showing the last known value...) + setAttribute(REQUESTS_PER_SECOND_LAST, 0D); + setAttribute(REQUESTS_PER_SECOND_IN_WINDOW, 0D); + } + + public boolean isHttpEnabled() { + return WebAppServiceMethods.isProtocolEnabled(this, "HTTP"); + } + + public boolean isHttpsEnabled() { + return WebAppServiceMethods.isProtocolEnabled(this, "HTTPS"); + } + + public Integer getHttpPort() { + return getAttribute(HTTP_PORT); + } + + public Integer getHttpsPort() { + return getAttribute(HTTPS_PORT); + } + + public String getHttpsSslKeyAlias() { + HttpsSslConfig config = getAttribute(HTTPS_SSL_CONFIG); + return (config == null) ? null : config.getKeyAlias(); + } + + public String getHttpsSslKeystorePassword() { + HttpsSslConfig config = getAttribute(HTTPS_SSL_CONFIG); + return (config == null) ? "" : config.getKeystorePassword(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java new file mode 100644 index 0000000..0f2df03 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java @@ -0,0 +1,201 @@ +/* + * 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 static com.google.common.base.Preconditions.checkNotNull; + +import java.io.File; +import java.net.URI; +import java.util.Set; + +import brooklyn.entity.basic.Attributes; +import brooklyn.entity.java.JavaSoftwareProcessSshDriver; +import brooklyn.location.basic.SshMachineLocation; +import brooklyn.util.task.DynamicTasks; +import brooklyn.util.task.Tasks; +import brooklyn.util.task.ssh.SshTasks; +import brooklyn.util.text.Strings; + +import com.google.common.collect.ImmutableList; + +public abstract class JavaWebAppSshDriver extends JavaSoftwareProcessSshDriver implements JavaWebAppDriver { + + public JavaWebAppSshDriver(JavaWebAppSoftwareProcessImpl entity, SshMachineLocation machine) { + super(entity, machine); + } + + public JavaWebAppSoftwareProcessImpl getEntity() { + return (JavaWebAppSoftwareProcessImpl) super.getEntity(); + } + + protected boolean isProtocolEnabled(String protocol) { + Set<String> protocols = getEnabledProtocols(); + for (String contender : protocols) { + if (protocol.equalsIgnoreCase(contender)) { + return true; + } + } + return false; + } + + @Override + public Set<String> getEnabledProtocols() { + return entity.getAttribute(JavaWebAppSoftwareProcess.ENABLED_PROTOCOLS); + } + + @Override + public Integer getHttpPort() { + return entity.getAttribute(Attributes.HTTP_PORT); + } + + @Override + public Integer getHttpsPort() { + return entity.getAttribute(Attributes.HTTPS_PORT); + } + + @Override + public HttpsSslConfig getHttpsSslConfig() { + return entity.getAttribute(WebAppServiceConstants.HTTPS_SSL_CONFIG); + } + + protected String getSslKeystoreUrl() { + HttpsSslConfig ssl = getHttpsSslConfig(); + return (ssl == null) ? null : ssl.getKeystoreUrl(); + } + + protected String getSslKeystorePassword() { + HttpsSslConfig ssl = getHttpsSslConfig(); + return (ssl == null) ? null : ssl.getKeystorePassword(); + } + + protected String getSslKeyAlias() { + HttpsSslConfig ssl = getHttpsSslConfig(); + return (ssl == null) ? null : ssl.getKeyAlias(); + } + + protected String inferRootUrl() { + if (isProtocolEnabled("https")) { + Integer port = getHttpsPort(); + checkNotNull(port, "HTTPS_PORT sensors not set; is an acceptable port available?"); + return String.format("https://%s:%s/", getSubnetHostname(), port); + } else if (isProtocolEnabled("http")) { + Integer port = getHttpPort(); + checkNotNull(port, "HTTP_PORT sensors not set; is an acceptable port available?"); + return String.format("http://%s:%s/", getSubnetHostname(), port); + } else { + throw new IllegalStateException("HTTP and HTTPS protocols not enabled for "+entity+"; enabled protocols are "+getEnabledProtocols()); + } + } + + @Override + public void postLaunch() { + String rootUrl = inferRootUrl(); + entity.setAttribute(Attributes.MAIN_URI, URI.create(rootUrl)); + entity.setAttribute(WebAppService.ROOT_URL, rootUrl); + } + + /** + * if files should be placed on the server for deployment, + * override this to be the sub-directory of the runDir where they should be stored + * (or override getDeployDir() if they should be copied somewhere else, + * and set this null); + * if files are not copied to the server, but injected (e.g. JMX or uploaded) + * then override {@link #deploy(String, String)} as appropriate, + * using getContextFromDeploymentTargetName(targetName) + * and override this to return null + */ + protected abstract String getDeploySubdir(); + + protected String getDeployDir() { + if (getDeploySubdir()==null) + throw new IllegalStateException("no deployment directory available for "+this); + return getRunDir() + "/" + getDeploySubdir(); + } + + @Override + public void deploy(File file) { + deploy(file, null); + } + + @Override + public void deploy(File f, String targetName) { + if (targetName == null) { + targetName = f.getName(); + } + deploy(f.toURI().toASCIIString(), targetName); + } + + /** + * Deploys a URL as a webapp at the appserver. + * + * Returns a token which can be used as an argument to undeploy, + * typically the web context with leading slash where the app can be reached (just "/" for ROOT) + * + * @see JavaWebAppSoftwareProcess#deploy(String, String) for details of how input filenames are handled + */ + @Override + public String deploy(final String url, final String targetName) { + final String canonicalTargetName = getFilenameContextMapper().convertDeploymentTargetNameToFilename(targetName); + final String dest = getDeployDir() + "/" + canonicalTargetName; + //write to a .tmp so autodeploy is not triggered during upload + final String tmpDest = dest + "." + Strings.makeRandomId(8) + ".tmp"; + final String msg = String.format("deploying %s to %s:%s", new Object[]{url, getHostname(), dest}); + log.info(entity + " " + msg); + Tasks.setBlockingDetails(msg); + try { + final String copyTaskMsg = String.format("copying %s to %s:%s", new Object[]{url, getHostname(), tmpDest}); + DynamicTasks.queue(copyTaskMsg, new Runnable() { + @Override + public void run() { + int result = copyResource(url, tmpDest); + if (result != 0) { + throw new IllegalStateException("Invalud result " + result + " while " + copyTaskMsg); + } + } + }); + + // create a backup + DynamicTasks.queue(SshTasks.newSshExecTaskFactory(getMachine(), String.format("mv -f %s %s.bak", dest, dest)) + .allowingNonZeroExitCode()); + + //rename temporary upload file to .war to be picked up for deployment + DynamicTasks.queue(SshTasks.newSshExecTaskFactory(getMachine(), String.format("mv -f %s %s", tmpDest, dest)) + .requiringExitCodeZero()); + log.debug("{} deployed {} to {}:{}", new Object[]{entity, url, getHostname(), dest}); + + DynamicTasks.waitForLast(); + } finally { + Tasks.resetBlockingDetails(); + } + return getFilenameContextMapper().convertDeploymentTargetNameToContext(canonicalTargetName); + } + + @Override + public void undeploy(String targetName) { + String dest = getDeployDir() + "/" + getFilenameContextMapper().convertDeploymentTargetNameToFilename(targetName); + log.info("{} undeploying {}:{}", new Object[]{entity, getHostname(), dest}); + int result = getMachine().execCommands("removing war on undeploy", ImmutableList.of(String.format("rm -f %s", dest))); + log.debug("{} undeployed {}:{}: result {}", new Object[]{entity, getHostname(), dest, result}); + } + + @Override + public FilenameToWebContextMapper getFilenameContextMapper() { + return new FilenameToWebContextMapper(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppService.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppService.java new file mode 100644 index 0000000..f131932 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppService.java @@ -0,0 +1,24 @@ +/* + * 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 brooklyn.entity.Entity; + +public interface WebAppService extends WebAppServiceConstants, Entity { +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceConstants.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceConstants.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceConstants.java new file mode 100644 index 0000000..4c5713c --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceConstants.java @@ -0,0 +1,61 @@ +/* + * 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 java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import brooklyn.config.render.RendererHints; +import brooklyn.entity.basic.Attributes; +import brooklyn.event.AttributeSensor; +import brooklyn.event.basic.BasicAttributeSensorAndConfigKey; +import brooklyn.event.basic.PortAttributeSensorAndConfigKey; +import brooklyn.event.basic.Sensors; +import brooklyn.util.flags.SetFromFlag; + +public interface WebAppServiceConstants extends WebAppServiceMetrics { + + @SetFromFlag("httpPort") + public static final PortAttributeSensorAndConfigKey HTTP_PORT = Attributes.HTTP_PORT; + + @SetFromFlag("httpsPort") + public static final PortAttributeSensorAndConfigKey HTTPS_PORT = Attributes.HTTPS_PORT; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @SetFromFlag("enabledProtocols") + public static final BasicAttributeSensorAndConfigKey<Set<String>> ENABLED_PROTOCOLS = new BasicAttributeSensorAndConfigKey( + Set.class, "webapp.enabledProtocols", "List of enabled protocols (e.g. http, https)", ImmutableSet.of("http")); + + @SetFromFlag("httpsSsl") + public static final BasicAttributeSensorAndConfigKey<HttpsSslConfig> HTTPS_SSL_CONFIG = new BasicAttributeSensorAndConfigKey<HttpsSslConfig>( + HttpsSslConfig.class, "webapp.https.ssl", "SSL Configuration for HTTPS", null); + + public static final AttributeSensor<String> ROOT_URL = RootUrl.ROOT_URL; + +} + +// this class is added because the ROOT_URL relies on a static initialization which unfortunately can't be added to an interface. +class RootUrl { + public static final AttributeSensor<String> ROOT_URL = Sensors.newStringSensor("webapp.url", "URL"); + + static { + RendererHints.register(ROOT_URL, RendererHints.namedActionWithUrl()); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java ---------------------------------------------------------------------- diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java new file mode 100644 index 0000000..e5e0570 --- /dev/null +++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java @@ -0,0 +1,89 @@ +/* + * 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 static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import brooklyn.enricher.RollingTimeWindowMeanEnricher; +import brooklyn.enricher.TimeFractionDeltaEnricher; +import brooklyn.enricher.TimeWeightedDeltaEnricher; +import brooklyn.entity.Entity; +import brooklyn.entity.basic.EntityLocal; +import brooklyn.location.access.BrooklynAccessUtils; +import brooklyn.util.time.Duration; + +import com.google.common.net.HostAndPort; + +public class WebAppServiceMethods implements WebAppServiceConstants { + + public static final Duration DEFAULT_WINDOW_DURATION = Duration.TEN_SECONDS; + + public static void connectWebAppServerPolicies(EntityLocal entity) { + connectWebAppServerPolicies(entity, DEFAULT_WINDOW_DURATION); + } + + public static void connectWebAppServerPolicies(EntityLocal entity, Duration windowPeriod) { + entity.addEnricher(TimeWeightedDeltaEnricher.<Integer>getPerSecondDeltaEnricher(entity, REQUEST_COUNT, REQUESTS_PER_SECOND_LAST)); + + if (windowPeriod!=null) { + entity.addEnricher(new RollingTimeWindowMeanEnricher<Double>(entity, REQUESTS_PER_SECOND_LAST, + REQUESTS_PER_SECOND_IN_WINDOW, windowPeriod)); + } + + entity.addEnricher(new TimeFractionDeltaEnricher<Integer>(entity, TOTAL_PROCESSING_TIME, PROCESSING_TIME_FRACTION_LAST, TimeUnit.MILLISECONDS)); + + if (windowPeriod!=null) { + entity.addEnricher(new RollingTimeWindowMeanEnricher<Double>(entity, PROCESSING_TIME_FRACTION_LAST, + PROCESSING_TIME_FRACTION_IN_WINDOW, windowPeriod)); + } + + } + + public static Set<String> getEnabledProtocols(Entity entity) { + return entity.getAttribute(WebAppService.ENABLED_PROTOCOLS); + } + + public static boolean isProtocolEnabled(Entity entity, String protocol) { + for (String contender : getEnabledProtocols(entity)) { + if (protocol.equalsIgnoreCase(contender)) { + return true; + } + } + return false; + } + + public static String inferBrooklynAccessibleRootUrl(Entity entity) { + if (isProtocolEnabled(entity, "https")) { + Integer rawPort = entity.getAttribute(HTTPS_PORT); + checkNotNull(rawPort, "HTTPS_PORT sensors not set for %s; is an acceptable port available?", entity); + HostAndPort hp = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, rawPort); + return String.format("https://%s:%s/", hp.getHostText(), hp.getPort()); + } else if (isProtocolEnabled(entity, "http")) { + Integer rawPort = entity.getAttribute(HTTP_PORT); + checkNotNull(rawPort, "HTTP_PORT sensors not set for %s; is an acceptable port available?", entity); + HostAndPort hp = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, rawPort); + return String.format("http://%s:%s/", hp.getHostText(), hp.getPort()); + } else { + throw new IllegalStateException("HTTP and HTTPS protocols not enabled for "+entity+"; enabled protocols are "+getEnabledProtocols(entity)); + } + } +}
