
brooklyn.entity.dns.AbstractGeoDnsServiceImpl Maven / Gradle / Ivy
Show all versions of brooklyn-software-webapp Show documentation
package brooklyn.entity.dns;
import static com.google.common.base.Preconditions.checkNotNull;
import java.net.InetAddress;
import java.net.MalformedURLException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import brooklyn.entity.Entity;
import brooklyn.entity.Group;
import brooklyn.entity.basic.AbstractEntity;
import brooklyn.entity.basic.Attributes;
import brooklyn.entity.basic.DynamicGroup;
import brooklyn.entity.basic.Lifecycle;
import brooklyn.entity.group.AbstractMembershipTrackingPolicy;
import brooklyn.entity.webapp.WebAppService;
import brooklyn.location.geo.HostGeoInfo;
import brooklyn.util.collections.MutableMap;
import brooklyn.util.collections.MutableSet;
import brooklyn.util.flags.SetFromFlag;
import brooklyn.util.net.Networking;
import brooklyn.util.time.Duration;
import 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 targetHosts = Collections.synchronizedMap(new LinkedHashMap());
// 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 entitiesWithoutHostname = new HashSet();
// 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 entitiesWithoutGeoInfo = new HashSet();
public AbstractGeoDnsServiceImpl() {
super();
}
@Override
public Map 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) {
setAttribute(HOSTNAME, getHostname());
setAttribute(SERVICE_STATE, state);
setAttribute(SERVICE_UP, state==Lifecycle.RUNNING);
}
@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 targetHosts);
protected synchronized void startTracker() {
if (targetEntityProvider==null || !getManagementSupport().isDeployed()) {
log.debug("Tracker for "+this+" not yet active: "+targetEntityProvider+" / "+getManagementContext());
return;
}
endTracker();
log.debug("Initializing tracker for "+this+", following "+targetEntityProvider);
tracker = new AbstractMembershipTrackingPolicy(MutableMap.of(
"name", "GeoDNS targets tracker",
"sensorsToTrack", ImmutableSet.of(HOSTNAME, ADDRESS, WebAppService.ROOT_URL)) ) {
@Override
protected void onEntityEvent(EventType type, Entity entity) { refreshGroupMembership(); }
};
addPolicy(tracker);
tracker.setGroup(targetEntityProvider);
refreshGroupMembership();
}
protected synchronized void endTracker() {
if (tracker == null || targetEntityProvider==null) return;
removePolicy(tracker);
tracker = null;
}
@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 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;
Set previousOnes = MutableSet.copyOf(targetHosts.keySet());
for (Entity e: pool) {
previousOnes.remove(e);
changed |= addTargetHost(e);
}
// anything left in previousOnes is no longer applicable
for (Entity e: previousOnes) {
changed = true;
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.
*
* 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!
*
* 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 m;
synchronized(targetHosts) { m = ImmutableMap.copyOf(targetHosts); }
if (log.isDebugEnabled()) log.debug("Full update of "+this+" ("+m.size()+" target hosts)");
Map entityIdToAddress = Maps.newLinkedHashMap();
for (Map.Entry entry : m.entrySet()) {
entityIdToAddress.put(entry.getKey().getId(), entry.getValue().address);
}
reconfigureService(new LinkedHashSet(m.values()));
if (log.isDebugEnabled()) log.debug("Targets being set as "+entityIdToAddress);
setAttribute(TARGETS, entityIdToAddress);
}
protected String inferHostname(Entity entity) {
String hostname = entity.getAttribute(Attributes.HOSTNAME);
String url = entity.getAttribute(WebAppService.ROOT_URL);
if (url!=null) {
try {
URL u = new URL(url);
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 {
// Look up the geo-info from the hostname/ip
HostGeoInfo geoH;
try {
InetAddress addr = (hostname == null) ? null : InetAddress.getByName(hostname);
geoH = (addr == null) ? null : HostGeoInfo.fromIpAddress(addr);
} catch (UnknownHostException e) {
if (ip == null) {
throw e;
} else {
if (log.isTraceEnabled()) log.trace("GeoDns failed to infer GeoInfo from hostname {}; will try with IP {} ({})", new Object[] {hostname, ip, e});
geoH = null;
}
}
// 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;
}
}