http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsServiceImpl.java ---------------------------------------------------------------------- diff --cc brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsServiceImpl.java index 0000000,729351a..ca2caf7 mode 000000,100644..100644 --- a/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsServiceImpl.java +++ b/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsServiceImpl.java @@@ -1,0 -1,372 +1,392 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package org.apache.brooklyn.entity.dns; + + import static com.google.common.base.Preconditions.checkNotNull; + + import java.net.InetAddress; + import java.net.MalformedURLException; + import java.net.URI; + import java.net.URL; + import java.net.UnknownHostException; + import java.util.Collection; + import java.util.Collections; + import java.util.HashSet; + import java.util.LinkedHashMap; + import java.util.LinkedHashSet; + import java.util.Map; + import java.util.Set; + + import org.apache.brooklyn.api.entity.Entity; + import org.apache.brooklyn.api.entity.Group; + import org.apache.brooklyn.api.policy.PolicySpec; ++import org.apache.brooklyn.api.sensor.Sensor; + import org.apache.brooklyn.core.entity.AbstractEntity; + import org.apache.brooklyn.core.entity.Attributes; + import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; + import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic; + import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ServiceNotUpLogic; + import org.apache.brooklyn.core.location.geo.HostGeoInfo; + import org.apache.brooklyn.entity.group.AbstractMembershipTrackingPolicy; + import org.apache.brooklyn.entity.group.DynamicGroup; + import org.apache.brooklyn.entity.webapp.WebAppService; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import org.apache.brooklyn.util.collections.MutableSet; + import org.apache.brooklyn.util.core.flags.SetFromFlag; + import org.apache.brooklyn.util.exceptions.Exceptions; + import org.apache.brooklyn.util.net.Networking; + import org.apache.brooklyn.util.time.Duration; + import org.apache.brooklyn.util.time.Time; + + import com.google.common.collect.ImmutableMap; + import com.google.common.collect.ImmutableSet; + import com.google.common.collect.Maps; + + public abstract class AbstractGeoDnsServiceImpl extends AbstractEntity implements AbstractGeoDnsService { + private static final Logger log = LoggerFactory.getLogger(AbstractGeoDnsService.class); + + @SetFromFlag + protected Group targetEntityProvider; + protected AbstractMembershipTrackingPolicy tracker; - ++ + protected Map<Entity, HostGeoInfo> targetHosts = Collections.synchronizedMap(new LinkedHashMap<Entity, HostGeoInfo>()); - ++ + // We complain (at debug) when we encounter a target entity for whom we can't derive hostname/ip information; + // this is the commonest case for the transient condition between the time the entity is created and the time + // it is started (at which point the location is specified). This set contains those entities we've complained + // about already, to avoid repetitive logging. + transient protected Set<Entity> entitiesWithoutHostname = new HashSet<Entity>(); + + // We complain (at info/warn) when we encounter a target entity for whom we can't derive geo information, even + // when hostname/ip is known. This set contains those entities we've complained about already, to avoid repetitive + // logging. + transient protected Set<Entity> entitiesWithoutGeoInfo = new HashSet<Entity>(); + + public AbstractGeoDnsServiceImpl() { + super(); + } - ++ ++ @Override ++ public void init() { ++ super.init(); ++ Group initialProvider = config().get(ENTITY_PROVIDER); ++ if (initialProvider != null) { ++ setTargetEntityProvider(initialProvider); ++ } ++ } ++ + @Override + public Map<Entity, HostGeoInfo> getTargetHosts() { + return targetHosts; + } - ++ + @Override + public void onManagementBecomingMaster() { + super.onManagementBecomingMaster(); + startTracker(); + } ++ + @Override + public void onManagementNoLongerMaster() { + endTracker(); + super.onManagementNoLongerMaster(); + } + + @Override + public void destroy() { + setServiceState(Lifecycle.DESTROYED); + super.destroy(); + } - ++ + @Override + public void setServiceState(Lifecycle state) { + sensors().set(HOSTNAME, getHostname()); + ServiceStateLogic.setExpectedState(this, state); + if (state==Lifecycle.RUNNING) + ServiceNotUpLogic.clearNotUpIndicator(this, SERVICE_STATE_ACTUAL); + else + ServiceNotUpLogic.updateNotUpIndicator(this, SERVICE_STATE_ACTUAL, "Not in RUNNING state"); + } - ++ + @Override + public void setTargetEntityProvider(final Group entityProvider) { + this.targetEntityProvider = checkNotNull(entityProvider, "targetEntityProvider"); + startTracker(); + } - ++ + /** should set up so these hosts are targeted, and setServiceState appropriately */ + protected abstract void reconfigureService(Collection<HostGeoInfo> targetHosts); - ++ + protected synchronized void startTracker() { + if (targetEntityProvider==null || !getManagementSupport().isDeployed()) { + log.debug("Tracker for "+this+" not yet active: "+targetEntityProvider+" / "+getManagementContext()); + return; + } + endTracker(); ++ ++ ImmutableSet.Builder<Sensor<?>> sensorsToTrack = ImmutableSet.<Sensor<?>>builder().add( ++ HOSTNAME, ADDRESS, Attributes.MAIN_URI, WebAppService.ROOT_URL); ++ // Don't subscribe to lifecycle events if entities will be included regardless of their status. ++ if (Boolean.TRUE.equals(config().get(FILTER_FOR_RUNNING))) { ++ sensorsToTrack.add(Attributes.SERVICE_STATE_ACTUAL); ++ } + log.debug("Initializing tracker for "+this+", following "+targetEntityProvider); + tracker = policies().add(PolicySpec.create(MemberTrackingPolicy.class) + .displayName("GeoDNS targets tracker") - .configure("sensorsToTrack", ImmutableSet.of(HOSTNAME, ADDRESS, Attributes.MAIN_URI, WebAppService.ROOT_URL)) - .configure("group", targetEntityProvider)); ++ .configure(AbstractMembershipTrackingPolicy.SENSORS_TO_TRACK, sensorsToTrack.build()) ++ .configure(AbstractMembershipTrackingPolicy.GROUP, targetEntityProvider)); + refreshGroupMembership(); + } - ++ + protected synchronized void endTracker() { + if (tracker == null || targetEntityProvider==null) return; + policies().remove(tracker); + tracker = null; + } - ++ + public static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy { + @Override + protected void onEntityEvent(EventType type, Entity entity) { + ((AbstractGeoDnsServiceImpl)super.entity).refreshGroupMembership(); + } + } + + @Override + public abstract String getHostname(); - ++ + long lastUpdate = -1; - ++ + // TODO: remove group member polling once locations can be determined via subscriptions + protected void refreshGroupMembership() { + try { + if (log.isDebugEnabled()) log.debug("GeoDns {} refreshing targets", this); + if (targetEntityProvider == null) + return; + if (targetEntityProvider instanceof DynamicGroup) + ((DynamicGroup) targetEntityProvider).rescanEntities(); + Set<Entity> pool = MutableSet.copyOf(targetEntityProvider instanceof Group ? ((Group)targetEntityProvider).getMembers(): targetEntityProvider.getChildren()); + if (log.isDebugEnabled()) log.debug("GeoDns {} refreshing targets, pool now {}", this, pool); - ++ + boolean changed = false; ++ boolean filterForRunning = Boolean.TRUE.equals(config().get(FILTER_FOR_RUNNING)); + Set<Entity> previousOnes = MutableSet.copyOf(targetHosts.keySet()); + for (Entity e: pool) { - previousOnes.remove(e); - changed |= addTargetHost(e); ++ if (!filterForRunning || Lifecycle.RUNNING.equals(e.sensors().get(Attributes.SERVICE_STATE_ACTUAL))) { ++ previousOnes.remove(e); ++ changed |= addTargetHost(e); ++ } + } + // anything left in previousOnes is no longer applicable + for (Entity e: previousOnes) { - changed = true; - removeTargetHost(e, false); ++ changed |= removeTargetHost(e, false); + } - ++ + // do a periodic full update hourly once we are active (the latter is probably not needed) - if (changed || (lastUpdate>0 && Time.hasElapsedSince(lastUpdate, Duration.ONE_HOUR))) ++ if (changed || (lastUpdate > 0 && Time.hasElapsedSince(lastUpdate, Duration.ONE_HOUR))) { + update(); - ++ } + } catch (Exception e) { + log.error("Problem refreshing group membership: "+e, e); + } + } - ++ + /** + * Adds this host, if it is absent or if its hostname has changed. + * <p> + * For whether to use hostname or ip, see config and attributes {@link AbstractGeoDnsService#USE_HOSTNAMES}, + * {@link Attributes#HOSTNAME} and {@link Attributes#ADDRESS} (via {@link #inferHostname(Entity)} and {@link #inferIp(Entity)}. + * Note that the "hostname" could in fact be an IP address, if {@link #inferHostname(Entity)} returns an IP! + * <p> + * TODO in a future release, we may change this to explicitly set the sensor(s) to look at on the entity, and + * be stricter about using them in order. + * + * @return true if host is added or changed + */ + protected boolean addTargetHost(Entity entity) { + try { + HostGeoInfo oldGeo = targetHosts.get(entity); + String hostname = inferHostname(entity); + String ip = inferIp(entity); + String addr = (getConfig(USE_HOSTNAMES) || ip == null) ? hostname : ip; - ++ + if (addr==null) addr = ip; + if (addr == null) { + if (entitiesWithoutHostname.add(entity)) { + log.debug("GeoDns ignoring {} (no hostname/ip/URL info yet available)", entity); + } + return false; + } - ++ + // prefer the geo from the entity (or location parent), but fall back to inferring + // e.g. if it supplies a URL + HostGeoInfo geo = HostGeoInfo.fromEntity(entity); + if (geo==null) geo = inferHostGeoInfo(hostname, ip); - ++ + if (Networking.isPrivateSubnet(addr) && ip!=null && !Networking.isPrivateSubnet(ip)) { + // fix for #1216 + log.debug("GeoDns using IP "+ip+" for "+entity+" as addr "+addr+" resolves to private subnet"); + addr = ip; + } + if (Networking.isPrivateSubnet(addr)) { + if (getConfig(INCLUDE_HOMELESS_ENTITIES)) { + if (entitiesWithoutGeoInfo.add(entity)) { + log.info("GeoDns including {}, even though {} is a private subnet (homeless entities included)", entity, addr); + } + } else { + if (entitiesWithoutGeoInfo.add(entity)) { + log.warn("GeoDns ignoring {} (private subnet detected for {})", entity, addr); + } + return false; + } + } + + if (geo == null) { + if (getConfig(INCLUDE_HOMELESS_ENTITIES)) { + if (entitiesWithoutGeoInfo.add(entity)) { + log.info("GeoDns including {}, even though no geography info available for {})", entity, addr); + } + geo = HostGeoInfo.create(addr, "unknownLocation("+addr+")", 0, 0); + } else { + if (entitiesWithoutGeoInfo.add(entity)) { + log.warn("GeoDns ignoring {} (no geography info available for {})", entity, addr); + } + return false; + } + } + + if (!addr.equals(geo.getAddress())) { + // if the location provider did not have an address, create a new one with it + geo = HostGeoInfo.create(addr, geo.displayName, geo.latitude, geo.longitude); + } - ++ + // If we already knew about it, and it hasn't changed, then nothing to do + if (oldGeo != null && geo.getAddress().equals(oldGeo.getAddress())) { + return false; + } - ++ + entitiesWithoutHostname.remove(entity); + entitiesWithoutGeoInfo.remove(entity); + log.info("GeoDns adding "+entity+" at "+geo+(oldGeo != null ? " (previously "+oldGeo+")" : "")); + targetHosts.put(entity, geo); + return true; + + } catch (Exception ee) { + log.warn("GeoDns ignoring "+entity+" (error analysing location): "+ee, ee); + return false; + } + } + + /** remove if host removed */ + protected boolean removeTargetHost(Entity e, boolean doUpdate) { + if (targetHosts.remove(e) != null) { + log.info("GeoDns removing reference to {}", e); + if (doUpdate) update(); + return true; + } + return false; + } - ++ + protected void update() { + lastUpdate = System.currentTimeMillis(); - ++ + Map<Entity, HostGeoInfo> m; + synchronized(targetHosts) { m = ImmutableMap.copyOf(targetHosts); } + if (log.isDebugEnabled()) log.debug("Full update of "+this+" ("+m.size()+" target hosts)"); - ++ + Map<String,String> entityIdToAddress = Maps.newLinkedHashMap(); + for (Map.Entry<Entity, HostGeoInfo> entry : m.entrySet()) { + entityIdToAddress.put(entry.getKey().getId(), entry.getValue().address); + } - ++ + reconfigureService(new LinkedHashSet<HostGeoInfo>(m.values())); - ++ + if (log.isDebugEnabled()) log.debug("Targets being set as "+entityIdToAddress); + sensors().set(TARGETS, entityIdToAddress); + } - ++ + protected String inferHostname(Entity entity) { + String hostname = entity.getAttribute(Attributes.HOSTNAME); + URI url = entity.getAttribute(Attributes.MAIN_URI); + if (url!=null) { + try { + URL u = url.toURL(); - ++ + String hostname2 = u.getHost(); + if (hostname==null) { + if (!entitiesWithoutGeoInfo.contains(entity)) //don't log repeatedly + log.warn("GeoDns "+this+" using URL {} to redirect to {} (HOSTNAME attribute is preferred, but not available)", url, entity); + hostname = hostname2; + } else if (!hostname.equals(hostname2)) { + if (!entitiesWithoutGeoInfo.contains(entity)) //don't log repeatedly + log.warn("GeoDns "+this+" URL {} of "+entity+" does not match advertised HOSTNAME {}; using hostname, not URL", url, hostname); + } - ++ + if (u.getPort() > 0 && u.getPort() != 80 && u.getPort() != 443) { + if (!entitiesWithoutGeoInfo.contains(entity)) //don't log repeatedly + log.warn("GeoDns "+this+" detected non-standard port in URL {} for {}; forwarding may not work", url, entity); + } - ++ + } catch (MalformedURLException e) { + log.warn("Invalid URL {} for entity {} in {}", new Object[] {url, entity, this}); + } + } + return hostname; + } - ++ + protected String inferIp(Entity entity) { + return entity.getAttribute(Attributes.ADDRESS); + } - ++ + protected HostGeoInfo inferHostGeoInfo(String hostname, String ip) throws UnknownHostException { + HostGeoInfo geoH = null; + if (hostname != null) { + try { + // For some entities, the hostname can actually be an IP! Therefore use Networking.getInetAddressWithFixedName + InetAddress addr = Networking.getInetAddressWithFixedName(hostname); + geoH = HostGeoInfo.fromIpAddress(addr); + } catch (RuntimeException e) { + // Most likely caused by (a wrapped) UnknownHostException + Exceptions.propagateIfFatal(e); + if (ip == null) { + if (log.isTraceEnabled()) log.trace("inferHostGeoInfo failing ("+Exceptions.getFirstInteresting(e)+"): hostname="+hostname+"; ip="+ip); + throw e; + } else { + if (log.isTraceEnabled()) log.trace("GeoDns failed to infer GeoInfo from hostname {}; will try with IP {} ({})", new Object[] {hostname, ip, e}); + } + } + } + + // Try IP address (prior to Mar 2014 we did not do this if USE_HOSTNAME was set but don't think that is desirable due to #1216) + if (ip != null) { + if (geoH == null) { + InetAddress addr = Networking.getInetAddressWithFixedName(ip); + geoH = HostGeoInfo.fromIpAddress(addr); + if (log.isTraceEnabled()) log.trace("GeoDns inferred GeoInfo {} from ip {} (could not infer from hostname {})", new Object[] {geoH, ip, hostname}); + } else { + geoH = HostGeoInfo.create(ip, geoH.displayName, geoH.latitude, geoH.longitude); + if (log.isTraceEnabled()) log.trace("GeoDns inferred GeoInfo {} from hostname {}; switching it to ip {}", new Object[] {geoH, hostname, ip}); + } + } else { + if (log.isTraceEnabled()) log.trace("GeoDns inferred GeoInfo {} from hostname {}", geoH, hostname); + } - ++ + return geoH; + } + }
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsService.java ---------------------------------------------------------------------- diff --cc brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsService.java index 0000000,f421df7..58fcca4 mode 000000,100644..100644 --- a/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsService.java +++ b/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsService.java @@@ -1,0 -1,70 +1,86 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package org.apache.brooklyn.entity.dns.geoscaling; + + import java.net.URI; + + import org.apache.brooklyn.api.entity.ImplementedBy; + import org.apache.brooklyn.api.sensor.AttributeSensor; + import org.apache.brooklyn.config.ConfigKey; -import org.apache.brooklyn.core.config.BasicConfigKey; + import org.apache.brooklyn.core.config.ConfigKeys; + import org.apache.brooklyn.core.entity.Attributes; -import org.apache.brooklyn.core.sensor.BasicAttributeSensor; ++import org.apache.brooklyn.core.sensor.Sensors; + import org.apache.brooklyn.entity.dns.AbstractGeoDnsService; + import org.apache.brooklyn.entity.webapp.WebAppServiceConstants; + import org.apache.brooklyn.util.core.flags.SetFromFlag; + ++/** ++ * A geo-DNS service using geoscaling.com. ++ * <p> ++ * AWS users should note that if the Brooklyn server managing this entity is in the same ++ * region as the server being geoscaled then they must set {@link #INCLUDE_HOMELESS_ENTITIES} ++ * to true, as IP lookups of the server will resolve the private address and it will be ++ * ignored by default. ++ */ + @ImplementedBy(GeoscalingDnsServiceImpl.class) + public interface GeoscalingDnsService extends AbstractGeoDnsService { + + @SetFromFlag("sslTrustAll") - public static final ConfigKey<Boolean> SSL_TRUST_ALL = ConfigKeys.newBooleanConfigKey( ++ ConfigKey<Boolean> SSL_TRUST_ALL = ConfigKeys.newBooleanConfigKey( + "ssl.trustAll", + "Whether to trust all certificates, or to fail with 'peer not authenticated' if untrusted (default false)", + false); ++ + @SetFromFlag("randomizeSubdomainName") - public static final ConfigKey<Boolean> RANDOMIZE_SUBDOMAIN_NAME = new BasicConfigKey<Boolean>( - Boolean.class, "randomize.subdomain.name"); ++ ConfigKey<Boolean> RANDOMIZE_SUBDOMAIN_NAME = ConfigKeys.newBooleanConfigKey( ++ "randomize.subdomain.name"); ++ + @SetFromFlag("username") - public static final ConfigKey<String> GEOSCALING_USERNAME = new BasicConfigKey<String>( - String.class, "geoscaling.username"); ++ ConfigKey<String> GEOSCALING_USERNAME = ConfigKeys.newStringConfigKey( ++ "geoscaling.username"); ++ + @SetFromFlag("password") - public static final ConfigKey<String> GEOSCALING_PASSWORD = new BasicConfigKey<String>( - String.class, "geoscaling.password"); ++ ConfigKey<String> GEOSCALING_PASSWORD = ConfigKeys.newStringConfigKey( ++ "geoscaling.password"); ++ + @SetFromFlag("primaryDomainName") - public static final ConfigKey<String> GEOSCALING_PRIMARY_DOMAIN_NAME = new BasicConfigKey<String>( - String.class, "geoscaling.primary.domain.name"); ++ ConfigKey<String> GEOSCALING_PRIMARY_DOMAIN_NAME = ConfigKeys.newStringConfigKey( ++ "geoscaling.primary.domain.name"); ++ + @SetFromFlag("smartSubdomainName") - public static final ConfigKey<String> GEOSCALING_SMART_SUBDOMAIN_NAME = new BasicConfigKey<String>( - String.class, "geoscaling.smart.subdomain.name"); ++ ConfigKey<String> GEOSCALING_SMART_SUBDOMAIN_NAME = ConfigKeys.newStringConfigKey( ++ "geoscaling.smart.subdomain.name"); + - public static final AttributeSensor<String> GEOSCALING_ACCOUNT = new BasicAttributeSensor<String>( - String.class, "geoscaling.account", "Active user account for the GeoScaling.com service"); - public static final AttributeSensor<URI> MAIN_URI = Attributes.MAIN_URI; - public static final AttributeSensor<String> ROOT_URL = WebAppServiceConstants.ROOT_URL; - public static final AttributeSensor<String> MANAGED_DOMAIN = new BasicAttributeSensor<String>( - String.class, "geoscaling.managed.domain", "Fully qualified domain name that will be geo-redirected; " + ++ AttributeSensor<String> GEOSCALING_ACCOUNT = Sensors.newStringSensor( ++ "geoscaling.account", "Active user account for the GeoScaling.com service"); ++ ++ AttributeSensor<URI> MAIN_URI = Attributes.MAIN_URI; ++ ++ AttributeSensor<String> ROOT_URL = WebAppServiceConstants.ROOT_URL; ++ ++ AttributeSensor<String> MANAGED_DOMAIN = Sensors.newStringSensor( ++ "geoscaling.managed.domain", ++ "Fully qualified domain name that will be geo-redirected; " + + "this will be the same as "+ROOT_URL.getName()+" but the latter will only be set when the domain has active targets"); + - public void applyConfig(); ++ void applyConfig(); + + /** minimum/default TTL here is 300s = 5m */ - public long getTimeToLiveSeconds(); ++ long getTimeToLiveSeconds(); + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsServiceImpl.java ---------------------------------------------------------------------- diff --cc brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsServiceImpl.java index 0000000,4273dac..e04b8ec mode 000000,100644..100644 --- a/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsServiceImpl.java +++ b/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsServiceImpl.java @@@ -1,0 -1,199 +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.dns.geoscaling; + + import static com.google.common.base.Preconditions.checkNotNull; + import static org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.PROVIDE_CITY_INFO; + + import java.net.URI; + import java.util.Collection; + import java.util.Set; + + import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; + import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic; + import org.apache.brooklyn.core.location.geo.HostGeoInfo; + import org.apache.brooklyn.entity.dns.AbstractGeoDnsServiceImpl; + import org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.Domain; + import org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.SmartSubdomain; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import org.apache.brooklyn.util.collections.MutableSet; + import org.apache.brooklyn.util.http.HttpTool; + import org.apache.brooklyn.util.text.Identifiers; + import org.apache.brooklyn.util.text.Strings; + + public class GeoscalingDnsServiceImpl extends AbstractGeoDnsServiceImpl implements GeoscalingDnsService { + + private static final Logger log = LoggerFactory.getLogger(GeoscalingDnsServiceImpl.class); + + // Must remember any desired redirection targets if they're specified before configure() has been called. + private Set<HostGeoInfo> rememberedTargetHosts; + private GeoscalingWebClient webClient; + + // These are available only after the configure() method has been invoked. + private boolean randomizeSmartSubdomainName; + private String username; + private String password; + private String primaryDomainName; + private String smartSubdomainName; + + public GeoscalingDnsServiceImpl() { + } + + @Override + public void init() { + super.init(); + + // defaulting to randomized subdomains makes deploying multiple applications easier - if (getConfig(RANDOMIZE_SUBDOMAIN_NAME)==null) config().set(RANDOMIZE_SUBDOMAIN_NAME, true); - - Boolean trustAll = getConfig(SSL_TRUST_ALL); ++ if (config().get(RANDOMIZE_SUBDOMAIN_NAME) == null) { ++ config().set(RANDOMIZE_SUBDOMAIN_NAME, true); ++ } ++ ++ Boolean trustAll = config().get(SSL_TRUST_ALL); + if (Boolean.TRUE.equals(trustAll)) { + webClient = new GeoscalingWebClient(HttpTool.httpClientBuilder().trustAll().build()); + } else { + webClient = new GeoscalingWebClient(); + } + } + + // Ensure our configure() method gets called; may be able to remove this if/when the framework detects this + // and invokes the configure() method automatically? + @Override + public void onManagementBecomingMaster() { + try { + applyConfig(); + } catch (Exception e) { + // don't prevent management coming up, but do mark it as on fire + log.error("Geoscaling did not come up correctly: "+e, e); + ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE); + } + super.onManagementBecomingMaster(); + } + + boolean isConfigured = false; + + public synchronized void applyConfig() { - randomizeSmartSubdomainName = getConfig(RANDOMIZE_SUBDOMAIN_NAME); - username = getConfig(GEOSCALING_USERNAME); - password = getConfig(GEOSCALING_PASSWORD); - primaryDomainName = getConfig(GEOSCALING_PRIMARY_DOMAIN_NAME); - smartSubdomainName = getConfig(GEOSCALING_SMART_SUBDOMAIN_NAME); ++ randomizeSmartSubdomainName = config().get(RANDOMIZE_SUBDOMAIN_NAME); ++ username = config().get(GEOSCALING_USERNAME); ++ password = config().get(GEOSCALING_PASSWORD); ++ primaryDomainName = config().get(GEOSCALING_PRIMARY_DOMAIN_NAME); ++ smartSubdomainName = config().get(GEOSCALING_SMART_SUBDOMAIN_NAME); + + // Ensure all mandatory configuration is provided. + checkNotNull(username, "The GeoScaling username is not specified"); + checkNotNull(password, "The GeoScaling password is not specified"); + checkNotNull(primaryDomainName, "The GeoScaling primary domain name is not specified"); + + if (randomizeSmartSubdomainName) { + // if no smart subdomain specified, but random is, use something random + if (smartSubdomainName != null) smartSubdomainName += "-"; + else smartSubdomainName = ""; + smartSubdomainName += Identifiers.makeRandomId(8); + } + checkNotNull(smartSubdomainName, "The GeoScaling smart subdomain name is not specified or randomized"); + + String fullDomain = smartSubdomainName+"."+primaryDomainName; + log.info("GeoScaling service will configure redirection for '"+fullDomain+"' domain"); + sensors().set(GEOSCALING_ACCOUNT, username); + sensors().set(MANAGED_DOMAIN, fullDomain); + sensors().set(HOSTNAME, getHostname()); + + isConfigured = true; + + if (rememberedTargetHosts != null) { + reconfigureService(rememberedTargetHosts); + rememberedTargetHosts = null; + } + } + + @Override + public String getHostname() { + String result = getAttribute(MANAGED_DOMAIN); + return (Strings.isBlank(result)) ? null : result; + } + + /** minimum/default TTL here is 300s = 5m */ + public long getTimeToLiveSeconds() { return 5*60; } + + @Override + public void destroy() { + setServiceState(Lifecycle.STOPPING); + if (!isConfigured) return; + + // Don't leave randomized subdomains configured on our GeoScaling account. + if (randomizeSmartSubdomainName) { + webClient.login(username, password); + Domain primaryDomain = webClient.getPrimaryDomain(primaryDomainName); + SmartSubdomain smartSubdomain = (primaryDomain != null) ? primaryDomain.getSmartSubdomain(smartSubdomainName) : null; + if (smartSubdomain != null) { + log.info("Deleting randomized GeoScaling smart subdomain '"+smartSubdomainName+"."+primaryDomainName+"'"); + smartSubdomain.delete(); + } + webClient.logout(); + } + + super.destroy(); + + isConfigured = false; + } + + protected void reconfigureService(Collection<HostGeoInfo> targetHosts) { + if (!isConfigured) { + this.rememberedTargetHosts = MutableSet.copyOf(targetHosts); + return; + } + + webClient.login(username, password); + Domain primaryDomain = webClient.getPrimaryDomain(primaryDomainName); + if (primaryDomain==null) + throw new NullPointerException(this+" got null from web client for primary domain "+primaryDomainName); + SmartSubdomain smartSubdomain = primaryDomain.getSmartSubdomain(smartSubdomainName); + + if (smartSubdomain == null) { + log.info("GeoScaling {} smart subdomain '{}.{}' does not exist, creating it now", new Object[] {this, smartSubdomainName, primaryDomainName}); + // TODO use WithMutexes to ensure this is single-entrant + primaryDomain.createSmartSubdomain(smartSubdomainName); + smartSubdomain = primaryDomain.getSmartSubdomain(smartSubdomainName); + } + + if (smartSubdomain != null) { + log.debug("GeoScaling {} being reconfigured to use {}", this, targetHosts); + String script = GeoscalingScriptGenerator.generateScriptString(targetHosts); + smartSubdomain.configure(PROVIDE_CITY_INFO, script); + if (targetHosts.isEmpty()) { + setServiceState(Lifecycle.CREATED); + sensors().set(ROOT_URL, null); + sensors().set(MAIN_URI, null); + } else { + setServiceState(Lifecycle.RUNNING); + String domain = getAttribute(MANAGED_DOMAIN); + if (!Strings.isEmpty(domain)) { + sensors().set(ROOT_URL, "http://"+domain+"/"); + sensors().set(MAIN_URI, URI.create("http://"+domain+"/")); + } + } + } else { + log.warn("Failed to retrieve or create GeoScaling smart subdomain '"+smartSubdomainName+"."+primaryDomainName+ + "', aborting attempt to configure service"); + setServiceState(Lifecycle.ON_FIRE); + } + + webClient.logout(); + } + + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java ---------------------------------------------------------------------- diff --cc brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java index 0000000,7791418..e1e30c3 mode 000000,100644..100644 --- a/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java +++ b/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java @@@ -1,0 -1,205 +1,205 @@@ + /* + * 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 com.google.common.net.HostAndPort; + import org.apache.brooklyn.core.entity.Attributes; + import org.apache.brooklyn.core.location.access.BrooklynAccessUtils; + import org.apache.brooklyn.entity.java.JavaSoftwareProcessSshDriver; + import org.apache.brooklyn.location.ssh.SshMachineLocation; + import org.apache.brooklyn.util.core.task.DynamicTasks; + import org.apache.brooklyn.util.core.task.Tasks; + import org.apache.brooklyn.util.core.task.ssh.SshTasks; + import org.apache.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?"); + HostAndPort accessibleAddress = BrooklynAccessUtils.getBrooklynAccessibleAddress(getEntity(), port); + return String.format("https://%s:%s/", accessibleAddress.getHostText(), accessibleAddress.getPort()); + } else if (isProtocolEnabled("http")) { + Integer port = getHttpPort(); + checkNotNull(port, "HTTP_PORT sensors not set; is an acceptable port available?"); + HostAndPort accessibleAddress = BrooklynAccessUtils.getBrooklynAccessibleAddress(getEntity(), port); + return String.format("http://%s:%s/", accessibleAddress.getHostText(), accessibleAddress.getPort()); + } else { + throw new IllegalStateException("HTTP and HTTPS protocols not enabled for "+entity+"; enabled protocols are "+getEnabledProtocols()); + } + } + + @Override + public void postLaunch() { + String rootUrl = inferRootUrl(); + entity.sensors().set(Attributes.MAIN_URI, URI.create(rootUrl)); + entity.sensors().set(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); ++ throw new IllegalStateException("Invalid 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/018a0e15/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6ServerImpl.java ---------------------------------------------------------------------- diff --cc brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6ServerImpl.java index 0000000,c977a80..e292550 mode 000000,100644..100644 --- a/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6ServerImpl.java +++ b/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6ServerImpl.java @@@ -1,0 -1,112 +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.jboss; + + import java.util.LinkedHashMap; + import java.util.Map; + import java.util.concurrent.TimeUnit; + + import org.apache.brooklyn.api.entity.Entity; ++import org.apache.brooklyn.core.entity.EntityFunctions; + import org.apache.brooklyn.entity.java.UsesJmx; + import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcessImpl; + import org.apache.brooklyn.feed.jmx.JmxAttributePollConfig; + import org.apache.brooklyn.feed.jmx.JmxFeed; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + + import com.google.common.base.Functions; + + public class JBoss6ServerImpl extends JavaWebAppSoftwareProcessImpl implements JBoss6Server { + + public static final Logger log = LoggerFactory.getLogger(JBoss6ServerImpl.class); + + private volatile JmxFeed jmxFeed; + + public JBoss6ServerImpl() { + this(new LinkedHashMap(), null); + } + + public JBoss6ServerImpl(Entity parent) { + this(new LinkedHashMap(), parent); + } + + public JBoss6ServerImpl(Map flags){ + this(flags, null); + } + + public JBoss6ServerImpl(Map flags, Entity parent) { + super(flags, parent); + } + + @Override + public void connectSensors() { + super.connectSensors(); + + String requestProcessorMbeanName = "jboss.web:type=GlobalRequestProcessor,name=http-*"; + String serverMbeanName = "jboss.system:type=Server"; + boolean retrieveUsageMetrics = getConfig(RETRIEVE_USAGE_METRICS); + + if (isJmxEnabled()) { + jmxFeed = JmxFeed.builder() + .entity(this) + .period(500, TimeUnit.MILLISECONDS) + .pollAttribute(new JmxAttributePollConfig<Boolean>(SERVICE_UP) + // TODO instead of setting SERVICE_UP directly, want to use equivalent of + // addEnricher(Enrichers.builder().updatingMap(Attributes.SERVICE_NOT_UP_INDICATORS).key("serverMBean")... + // but not supported in feed? + .objectName(serverMbeanName) + .attributeName("Started") + .onException(Functions.constant(false)) + .suppressDuplicates(true)) + .pollAttribute(new JmxAttributePollConfig<Integer>(ERROR_COUNT) + .objectName(requestProcessorMbeanName) + .attributeName("errorCount") + .enabled(retrieveUsageMetrics)) + .pollAttribute(new JmxAttributePollConfig<Integer>(REQUEST_COUNT) + .objectName(requestProcessorMbeanName) + .attributeName("requestCount") ++ .onFailureOrException(EntityFunctions.attribute(this, REQUEST_COUNT)) + .enabled(retrieveUsageMetrics)) + .pollAttribute(new JmxAttributePollConfig<Integer>(TOTAL_PROCESSING_TIME) + .objectName(requestProcessorMbeanName) + .attributeName("processingTime") + .enabled(retrieveUsageMetrics)) + .build(); + } else { + // if not using JMX + log.warn(this+" running without JMX monitoring; limited visibility of service available"); + connectServiceUpIsRunning(); + } + } + + @Override + public void disconnectSensors() { + super.disconnectSensors(); + if (jmxFeed != null) jmxFeed.stop(); + disconnectServiceUpIsRunning(); + } + + @Override + public Class<JBoss6Driver> getDriverInterface() { + return JBoss6Driver.class; + } + + protected boolean isJmxEnabled() { + return (this instanceof UsesJmx) && Boolean.TRUE.equals(getConfig(UsesJmx.USE_JMX)); + } + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java ---------------------------------------------------------------------- diff --cc brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java index 0000000,e2411a7..10b9564 mode 000000,100644..100644 --- a/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java +++ b/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java @@@ -1,0 -1,212 +1,214 @@@ + /* + * 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.jboss; + + import java.util.Map; + + import org.apache.brooklyn.api.entity.Entity; + import org.apache.brooklyn.core.config.render.RendererHints; + import org.apache.brooklyn.core.entity.Attributes; ++import org.apache.brooklyn.core.entity.EntityFunctions; + import org.apache.brooklyn.core.location.access.BrooklynAccessUtils; + import org.apache.brooklyn.enricher.stock.Enrichers; + import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcessImpl; + import org.apache.brooklyn.feed.http.HttpFeed; + import org.apache.brooklyn.feed.http.HttpPollConfig; + import org.apache.brooklyn.feed.http.HttpValueFunctions; + import org.apache.brooklyn.util.guava.Functionals; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + + import com.google.common.base.Functions; + import com.google.common.collect.ImmutableMap; + import com.google.common.net.HostAndPort; + + public class JBoss7ServerImpl extends JavaWebAppSoftwareProcessImpl implements JBoss7Server { + + public static final Logger log = LoggerFactory.getLogger(JBoss7ServerImpl.class); + + private volatile HttpFeed httpFeed; + + public JBoss7ServerImpl(){ + super(); + } + + public JBoss7ServerImpl(@SuppressWarnings("rawtypes") Map flags){ + this(flags, null); + } + + public JBoss7ServerImpl(@SuppressWarnings("rawtypes") Map flags, Entity parent) { + super(flags, parent); + } + + @Override + public Class<?> getDriverInterface() { + return JBoss7Driver.class; + } + + @Override + public JBoss7Driver getDriver() { + return (JBoss7Driver) super.getDriver(); + } + + static { + RendererHints.register(MANAGEMENT_URL, RendererHints.namedActionWithUrl()); + } + + @Override + protected void connectSensors() { + super.connectSensors(); + + HostAndPort hp = BrooklynAccessUtils.getBrooklynAccessibleAddress(this, + getAttribute(MANAGEMENT_HTTP_PORT) + getConfig(PORT_INCREMENT)); + + String managementUri = String.format("http://%s:%s/management/subsystem/web/connector/http/read-resource", + hp.getHostText(), hp.getPort()); + sensors().set(MANAGEMENT_URL, managementUri); + + if (isHttpMonitoringEnabled()) { + log.debug("JBoss sensors for "+this+" reading from "+managementUri); + Map<String, String> includeRuntimeUriVars = ImmutableMap.of("include-runtime","true"); + boolean retrieveUsageMetrics = getConfig(RETRIEVE_USAGE_METRICS); + + httpFeed = HttpFeed.builder() + .entity(this) + .period(200) + .baseUri(managementUri) + .credentials(getConfig(MANAGEMENT_USER), getConfig(MANAGEMENT_PASSWORD)) + .poll(new HttpPollConfig<Integer>(MANAGEMENT_STATUS) + .onSuccess(HttpValueFunctions.responseCode()) + .suppressDuplicates(true)) + .poll(new HttpPollConfig<Boolean>(MANAGEMENT_URL_UP) + .onSuccess(HttpValueFunctions.responseCodeEquals(200)) + .onFailureOrException(Functions.constant(false)) + .suppressDuplicates(true)) + .poll(new HttpPollConfig<Integer>(REQUEST_COUNT) + .vars(includeRuntimeUriVars) + .onSuccess(HttpValueFunctions.jsonContents("requestCount", Integer.class)) ++ .onFailureOrException(EntityFunctions.attribute(this, REQUEST_COUNT)) + .enabled(retrieveUsageMetrics)) + .poll(new HttpPollConfig<Integer>(ERROR_COUNT) + .vars(includeRuntimeUriVars) + .onSuccess(HttpValueFunctions.jsonContents("errorCount", Integer.class)) + .enabled(retrieveUsageMetrics)) + .poll(new HttpPollConfig<Integer>(TOTAL_PROCESSING_TIME) + .vars(includeRuntimeUriVars) + .onSuccess(HttpValueFunctions.jsonContents("processingTime", Integer.class)) + .enabled(retrieveUsageMetrics)) + .poll(new HttpPollConfig<Integer>(MAX_PROCESSING_TIME) + .vars(includeRuntimeUriVars) + .onSuccess(HttpValueFunctions.jsonContents("maxTime", Integer.class)) + .enabled(retrieveUsageMetrics)) + .poll(new HttpPollConfig<Long>(BYTES_RECEIVED) + .vars(includeRuntimeUriVars) + // jboss seems to report 0 even if it has received lots of requests; dunno why. + .onSuccess(HttpValueFunctions.jsonContents("bytesReceived", Long.class)) + .enabled(retrieveUsageMetrics)) + .poll(new HttpPollConfig<Long>(BYTES_SENT) + .vars(includeRuntimeUriVars) + .onSuccess(HttpValueFunctions.jsonContents("bytesSent", Long.class)) + .enabled(retrieveUsageMetrics)) + .build(); + + enrichers().add(Enrichers.builder().updatingMap(Attributes.SERVICE_NOT_UP_INDICATORS) + .from(MANAGEMENT_URL_UP) + .computing(Functionals.ifNotEquals(true).value("Management URL not reachable") ) + .build()); + } + + connectServiceUpIsRunning(); + } + + /** + * @deprecated since 0.9.0; now a no-op; marked final to force anyone sub-classing + overriding it to update their code. + */ + @Deprecated + protected final void connectServiceUp() { + } + + /** + * @deprecated since 0.9.0; now a no-op; marked final to force anyone sub-classing + overriding it to update their code. + */ + @Deprecated + protected final void disconnectServiceUp() { + } + + @Override + protected void disconnectSensors() { + super.disconnectSensors(); + + if (httpFeed != null) httpFeed.stop(); + disconnectServiceUpIsRunning(); + } + + protected boolean isHttpMonitoringEnabled() { + return Boolean.TRUE.equals(getConfig(USE_HTTP_MONITORING)); + } + + public int getManagementHttpsPort() { + return getAttribute(MANAGEMENT_HTTPS_PORT); + } + + public int getManagementHttpPort() { + return getAttribute(MANAGEMENT_HTTP_PORT); + } + + public int getManagementNativePort() { + return getAttribute(MANAGEMENT_NATIVE_PORT); + } + + public int getPortOffset() { + return getConfig(PORT_INCREMENT); + } + + public boolean isWelcomeRootEnabled() { + return false; + } + + public String getBindAddress() { + return getConfig(BIND_ADDRESS); + } + + public String getManagementBindAddress() { + return getConfig(BIND_ADDRESS); + } + + public String getUnsecureBindAddress() { + return getConfig(BIND_ADDRESS); + } + + // If empty-string, disables Management security (!) by excluding the security-realm attribute + public String getHttpManagementInterfaceSecurityRealm() { + return ""; + } + + public int getDeploymentTimeoutSecs() { + return getConfig(DEPLOYMENT_TIMEOUT); + } + + /** Path of the keystore file on the AS7 server */ + public String getHttpsSslKeystoreFile() { + return getDriver().getSslKeystoreFile(); + } + + @Override + public String getShortName() { + return "JBossAS7"; + } + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6ServerImpl.java ---------------------------------------------------------------------- diff --cc brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6ServerImpl.java index 0000000,c772b51..24a6c6f mode 000000,100644..100644 --- a/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6ServerImpl.java +++ b/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6ServerImpl.java @@@ -1,0 -1,140 +1,142 @@@ + /* + * 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.jetty; + + import java.util.concurrent.TimeUnit; + ++import org.apache.brooklyn.core.entity.EntityFunctions; + import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; + import org.apache.brooklyn.enricher.stock.Enrichers; + import org.apache.brooklyn.entity.java.JavaAppUtils; + import org.apache.brooklyn.entity.java.UsesJmx; + import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcessImpl; + import org.apache.brooklyn.feed.jmx.JmxAttributePollConfig; + import org.apache.brooklyn.feed.jmx.JmxFeed; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + + import com.google.common.base.Functions; + import com.google.common.base.Predicates; + + /** + * An {@link org.apache.brooklyn.api.entity.Entity} that represents a single Jetty instance. + */ + public class Jetty6ServerImpl extends JavaWebAppSoftwareProcessImpl implements Jetty6Server { + + private static final Logger log = LoggerFactory.getLogger(Jetty6ServerImpl.class); + + private volatile JmxFeed jmxFeedJetty, jmxFeedMx; + + @Override + public void connectSensors() { + super.connectSensors(); + + if (getDriver().isJmxEnabled()) { + String serverMbeanName = "org.mortbay.jetty:type=server,id=0"; + String statsMbeanName = "org.mortbay.jetty.handler:type=atomicstatisticshandler,id=0"; + + jmxFeedJetty = JmxFeed.builder() + .entity(this) + .period(500, TimeUnit.MILLISECONDS) + .pollAttribute(new JmxAttributePollConfig<Boolean>(SERVICE_UP) + .objectName(serverMbeanName) + .attributeName("running") + .onSuccess(Functions.forPredicate(Predicates.<Object>equalTo(true))) + .setOnFailureOrException(false)) + .pollAttribute(new JmxAttributePollConfig<Integer>(REQUEST_COUNT) + .objectName(statsMbeanName) - .attributeName("requests")) ++ .attributeName("requests") ++ .onFailureOrException(EntityFunctions.attribute(this, REQUEST_COUNT))) + .pollAttribute(new JmxAttributePollConfig<Integer>(RESPONSES_4XX_COUNT) + .objectName(statsMbeanName) + .attributeName("responses4xx")) + .pollAttribute(new JmxAttributePollConfig<Integer>(RESPONSES_5XX_COUNT) + .objectName(statsMbeanName) + .attributeName("responses5xx")) + .pollAttribute(new JmxAttributePollConfig<Integer>(TOTAL_PROCESSING_TIME) + .objectName(statsMbeanName) + .attributeName("requestTimeTotal")) + .pollAttribute(new JmxAttributePollConfig<Integer>(MAX_PROCESSING_TIME) + .objectName(statsMbeanName) + .attributeName("requestTimeMax")) + // NB: requestsActive may be useful + .build(); + + enrichers().add(Enrichers.builder() + .combining(RESPONSES_4XX_COUNT, RESPONSES_5XX_COUNT) + .publishing(ERROR_COUNT) + .computingSum() + .build()); + + jmxFeedMx = JavaAppUtils.connectMXBeanSensors(this); + } else { + // if not using JMX + log.warn("Jetty running without JMX monitoring; limited visibility of service available"); + // TODO we could do simple things, like check that web server is accepting connections + } + } + + @Override + protected void disconnectSensors() { + if (jmxFeedJetty != null) jmxFeedJetty.stop(); + if (jmxFeedMx != null) jmxFeedMx.stop(); + super.disconnectSensors(); + } + + public Integer getJmxPort() { + if (((Jetty6Driver) getDriver()).isJmxEnabled()) { + return getAttribute(UsesJmx.JMX_PORT); + } else { + return Integer.valueOf(-1); + } + } + + @Override + public Class getDriverInterface() { + return Jetty6Driver.class; + } + + @Override + public String getShortName() { + return "Jetty"; + } + + @Override + public void deploy(String url, String targetName) { + super.deploy(url, targetName); + restartIfRunning(); + } + + @Override + public void undeploy(String targetName) { + super.undeploy(targetName); + restartIfRunning(); + } + + protected void restartIfRunning() { + // TODO for now we simply restart jetty to achieve "hot deployment"; should use the config mechanisms + Lifecycle serviceState = getAttribute(SERVICE_STATE_ACTUAL); + if (serviceState == Lifecycle.RUNNING) + restart(); + // may need a restart also if deploy effector is done in parallel to starting + // but note this routine is used by initialDeployWars so just being in starting state is not enough! + } + + } + http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/tomcat/TomcatServerImpl.java ---------------------------------------------------------------------- diff --cc brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/tomcat/TomcatServerImpl.java index 0000000,22cab1f..6302278 mode 000000,100644..100644 --- a/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/tomcat/TomcatServerImpl.java +++ b/brooklyn-library/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/tomcat/TomcatServerImpl.java @@@ -1,0 -1,119 +1,125 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package org.apache.brooklyn.entity.webapp.tomcat; + + import static java.lang.String.format; + + import java.util.concurrent.TimeUnit; + ++import javax.annotation.Nullable; ++ ++import org.apache.brooklyn.api.sensor.AttributeSensor; ++import org.apache.brooklyn.core.entity.EntityFunctions; + import org.apache.brooklyn.entity.java.JavaAppUtils; + import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcessImpl; + import org.apache.brooklyn.feed.jmx.JmxAttributePollConfig; + import org.apache.brooklyn.feed.jmx.JmxFeed; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + ++import com.google.common.base.Function; + import com.google.common.base.Functions; + import com.google.common.base.Predicates; + + /** + * An {@link org.apache.brooklyn.api.entity.Entity} that represents a single Tomcat instance. + */ + public class TomcatServerImpl extends JavaWebAppSoftwareProcessImpl implements TomcatServer { + + private static final Logger LOG = LoggerFactory.getLogger(TomcatServerImpl.class); + + public TomcatServerImpl() { + super(); + } + + private volatile JmxFeed jmxWebFeed; + private volatile JmxFeed jmxAppFeed; + + @Override + public void connectSensors() { + super.connectSensors(); + + if (getDriver().isJmxEnabled()) { + String requestProcessorMbeanName = "Catalina:type=GlobalRequestProcessor,name=\"http-*\""; + + Integer port = isHttpsEnabled() ? getAttribute(HTTPS_PORT) : getAttribute(HTTP_PORT); + String connectorMbeanName = format("Catalina:type=Connector,port=%s", port); + boolean retrieveUsageMetrics = getConfig(RETRIEVE_USAGE_METRICS); + + jmxWebFeed = JmxFeed.builder() + .entity(this) + .period(3000, TimeUnit.MILLISECONDS) + .pollAttribute(new JmxAttributePollConfig<Boolean>(SERVICE_PROCESS_IS_RUNNING) + // TODO Want to use something different from SERVICE_PROCESS_IS_RUNNING, + // to indicate this is jmx MBean's reported state (or failure to connect) + .objectName(connectorMbeanName) + .attributeName("stateName") + .onSuccess(Functions.forPredicate(Predicates.<Object>equalTo("STARTED"))) + .setOnFailureOrException(false) + .suppressDuplicates(true)) + .pollAttribute(new JmxAttributePollConfig<String>(CONNECTOR_STATUS) + .objectName(connectorMbeanName) + .attributeName("stateName") + .suppressDuplicates(true)) + .pollAttribute(new JmxAttributePollConfig<Integer>(ERROR_COUNT) + .objectName(requestProcessorMbeanName) + .attributeName("errorCount") + .enabled(retrieveUsageMetrics)) + .pollAttribute(new JmxAttributePollConfig<Integer>(REQUEST_COUNT) + .objectName(requestProcessorMbeanName) + .attributeName("requestCount") - .enabled(retrieveUsageMetrics)) ++ .enabled(retrieveUsageMetrics) ++ .onFailureOrException(EntityFunctions.attribute(this, REQUEST_COUNT))) + .pollAttribute(new JmxAttributePollConfig<Integer>(TOTAL_PROCESSING_TIME) + .objectName(requestProcessorMbeanName) + .attributeName("processingTime") + .enabled(retrieveUsageMetrics)) + .build(); + + jmxAppFeed = JavaAppUtils.connectMXBeanSensors(this); + } else { + // if not using JMX + LOG.warn("Tomcat running without JMX monitoring; limited visibility of service available"); + connectServiceUpIsRunning(); + } + } + + @Override + public void disconnectSensors() { + super.disconnectSensors(); + if (getDriver() != null && getDriver().isJmxEnabled()) { + if (jmxWebFeed != null) jmxWebFeed.stop(); + if (jmxAppFeed != null) jmxAppFeed.stop(); + } else { + disconnectServiceUpIsRunning(); + } + } + + @SuppressWarnings("rawtypes") + @Override + public Class getDriverInterface() { + return TomcatDriver.class; + } + + @Override + public String getShortName() { + return "Tomcat"; + } + } +
