blob: d94d5510ff03f992adca1f077ad428efc12ff761 [file] [log] [blame]
/*
* 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.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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
/** Guards access to {@link #tracker}. */
protected final Object trackerLock = new Object[0];
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 onManagementStarted() {
super.onManagementStarted();
startTracker();
}
@Override
public void onManagementStopped() {
endTracker();
super.onManagementStopped();
}
@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);
/**
* Start the tracker.
* <p>
* Subclasses should take care to synchronize on {@link #trackerLock}.
*/
protected void startTracker() {
synchronized (trackerLock) {
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(AbstractMembershipTrackingPolicy.SENSORS_TO_TRACK, sensorsToTrack.build())
.configure(AbstractMembershipTrackingPolicy.GROUP, targetEntityProvider));
refreshGroupMembership();
}
}
/**
* End the tracker.
* <p>
* Subclasses should take care to synchronize on {@link #trackerLock}.
*/
protected void endTracker() {
synchronized (trackerLock) {
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) {
defaultHighlightAction(type, 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 ? 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) {
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 |= 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))) {
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;
}
}