blob: a210e55caadbfe383753b5648b67b131459c7352 [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.network.bind;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.util.Collection;
import java.util.Map;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.policy.PolicySpec;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.Sensor;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.location.Machines;
import org.apache.brooklyn.entity.group.AbstractMembershipTrackingPolicy;
import org.apache.brooklyn.entity.group.DynamicGroup;
import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.BiMap;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multimaps;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.net.Cidr;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.text.Strings;
/**
* This sets up a BIND DNS server.
* <p>
* <b>NOTE</b> This entity has only been certified on <i>CentOS</i>, <i>RHEL</i>,
* <i>Ubuntu</i> and <i>Debian</i> operating systems.
*/
public class BindDnsServerImpl extends SoftwareProcessImpl implements BindDnsServer {
private static final Logger LOG = LoggerFactory.getLogger(BindDnsServerImpl.class);
private final Object serialMutex = new Object();
// As per RFC 952 and RFC 1123.
private static final CharMatcher DOMAIN_NAME_FIRST_CHAR_MATCHER = CharMatcher.inRange('a', 'z')
.or(CharMatcher.inRange('A', 'Z'))
.or(CharMatcher.inRange('0', '9'));
private static final CharMatcher DOMAIN_NAME_MATCHER = DOMAIN_NAME_FIRST_CHAR_MATCHER
.or(CharMatcher.is('-'));
private class HostnameTransformer implements Function<Entity, String> {
@Override
public String apply(Entity input) {
String hostname = input.getAttribute(getConfig(HOSTNAME_SENSOR));
hostname = DOMAIN_NAME_FIRST_CHAR_MATCHER.negate().trimFrom(hostname);
hostname = DOMAIN_NAME_MATCHER.negate().trimAndCollapseFrom(hostname, '-');
if (hostname.length() > 63) {
hostname = hostname.substring(0, 63);
}
return hostname;
}
}
public BindDnsServerImpl() {
super();
}
@Override
public void init() {
super.init();
checkNotNull(getConfig(HOSTNAME_SENSOR), "%s requires value for %s", getClass().getName(), HOSTNAME_SENSOR);
checkNotNull(getConfig(ADDRESS_SENSOR), "%s requires value for %s", getClass().getName(), ADDRESS_SENSOR);
DynamicGroup entities = addChild(EntitySpec.create(DynamicGroup.class)
.displayName("BIND-managed entities")
.configure(DynamicGroup.ENTITY_FILTER, getEntityFilter()));
sensors().set(ENTITIES, entities);
sensors().set(A_RECORDS, ImmutableMap.<String, String>of());
sensors().set(CNAME_RECORDS, ImmutableMultimap.<String, String>of());
sensors().set(PTR_RECORDS, ImmutableMap.<String, String>of());
sensors().set(ADDRESS_MAPPINGS, ImmutableMultimap.<String, String>of());
synchronized (serialMutex) {
sensors().set(SERIAL, System.currentTimeMillis() / 1000);
}
}
@Override
public void postRebind() {
update();
}
@Override
public Class<?> getDriverInterface() {
return BindDnsServerDriver.class;
}
@Override
public Multimap<String, String> getAddressMappings() {
return getAttribute(ADDRESS_MAPPINGS);
}
@Override
public Map<String, String> getReverseMappings() {
return getAttribute(PTR_RECORDS);
}
@Override
public BindDnsServerDriver getDriver() {
return (BindDnsServerDriver) super.getDriver();
}
@Override
public void connectSensors() {
connectServiceUpIsRunning();
}
@Override
public void disconnectSensors() {
super.disconnectSensors();
disconnectServiceUpIsRunning();
}
@Override
protected void preStart() {
String reverse = getConfig(REVERSE_LOOKUP_NETWORK);
if (Strings.isBlank(reverse)) reverse = getAttribute(ADDRESS);
sensors().set(REVERSE_LOOKUP_CIDR, new Cidr(reverse + "/24"));
String reverseLookupDomain = Joiner.on('.').join(Iterables.skip(Lists.reverse(Lists.newArrayList(
Splitter.on('.').split(reverse))), 1)) + ".in-addr.arpa";
sensors().set(REVERSE_LOOKUP_DOMAIN, reverseLookupDomain);
policies().add(PolicySpec.create(MemberTrackingPolicy.class)
.displayName("Address tracker")
.configure(AbstractMembershipTrackingPolicy.SENSORS_TO_TRACK, ImmutableSet.<Sensor<?>>of(getConfig(HOSTNAME_SENSOR), getConfig(ADDRESS_SENSOR)))
.configure(AbstractMembershipTrackingPolicy.GROUP, getEntities()));
}
@Override
public void postStart() {
update();
}
public static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy {
@Override
protected void onEntityChange(Entity member) {
if (LOG.isTraceEnabled()) {
LOG.trace("State of {} on change: {}", member, member.getAttribute(Attributes.SERVICE_STATE_ACTUAL).name());
}
((BindDnsServerImpl) entity).update();
}
@Override
protected void onEntityAdded(Entity member) {
if (LOG.isTraceEnabled()) {
LOG.trace("State of {} on added: {}", member, member.getAttribute(Attributes.SERVICE_STATE_ACTUAL).name());
}
((BindDnsServerImpl) entity).configureResolver(member);
}
}
private class HasHostnameAndValidLifecycle implements Predicate<Entity> {
@Override
public boolean apply(Entity input) {
switch (input.getAttribute(Attributes.SERVICE_STATE_ACTUAL)) {
case STOPPED:
case STOPPING:
case DESTROYED:
return false;
default:
return input.getAttribute(getConfig(HOSTNAME_SENSOR)) != null;
}
}
}
private transient boolean hasLoggedDeprecationAboutAddressSensor = false;
public void update() {
Lifecycle serverState = getAttribute(Attributes.SERVICE_STATE_ACTUAL);
if (Lifecycle.STOPPED.equals(serverState) || Lifecycle.STOPPING.equals(serverState)
|| Lifecycle.DESTROYED.equals(serverState) || !getAttribute(Attributes.SERVICE_UP)) {
LOG.debug("Skipped update of {} when service state is {} and running is {}",
new Object[]{this, getAttribute(Attributes.SERVICE_STATE_ACTUAL), getAttribute(SERVICE_UP)});
return;
}
synchronized (this) {
Iterable<Entity> availableEntities = FluentIterable.from(getEntities().getMembers())
.filter(new HasHostnameAndValidLifecycle());
LOG.debug("{} updating with entities: {}", this, Iterables.toString(availableEntities));
ImmutableListMultimap<String, Entity> hostnameToEntity = Multimaps.index(availableEntities,
new HostnameTransformer());
Map<String, String> octetToName = Maps.newHashMap();
BiMap<String, String> ipToARecord = HashBiMap.create();
Multimap<String, String> aRecordToCnames = MultimapBuilder.hashKeys().hashSetValues().build();
Multimap<String, String> ipToAllNames = MultimapBuilder.hashKeys().hashSetValues().build();
for (Map.Entry<String, Entity> entry : hostnameToEntity.entries()) {
String domainName = entry.getKey();
Entity entity = entry.getValue();
String address = null;
AttributeSensor<String> addressSensor = getConfig(ADDRESS_SENSOR);
if (addressSensor!=null) {
address = entity.getAttribute(addressSensor);
} else {
if (!hasLoggedDeprecationAboutAddressSensor) {
LOG.warn("BIND entity "+this+" is using legacy machine inspection to determine IP address; set the "+ADDRESS_SENSOR.getName()+" config to ensure compatibility with future versions");
hasLoggedDeprecationAboutAddressSensor = true;
}
Maybe<SshMachineLocation> location = Machines.findUniqueMachineLocation(entity.getLocations(), SshMachineLocation.class);
if (!location.isPresent()) {
LOG.debug("Member {} of {} does not have an hostname so will not be configured", entity, this);
} else if (ipToARecord.inverse().containsKey(domainName)) {
// already has a hostname, ignore (could log if domain is different?)
} else {
address = location.get().getAddress().getHostAddress();
}
}
if (Strings.isBlank(address)) {
continue;
}
ipToAllNames.put(address, domainName);
if (!ipToARecord.containsKey(address)) {
ipToARecord.put(address, domainName);
if (getReverseLookupNetwork().contains(new Cidr(address + "/32"))) {
String octet = Iterables.get(Splitter.on('.').split(address), 3);
if (!octetToName.containsKey(octet)) octetToName.put(octet, domainName);
}
} else {
aRecordToCnames.put(ipToARecord.get(address), domainName);
}
}
sensors().set(A_RECORDS, ImmutableMap.copyOf(ipToARecord.inverse()));
sensors().set(PTR_RECORDS, ImmutableMap.copyOf(octetToName));
sensors().set(CNAME_RECORDS, Multimaps.unmodifiableMultimap(aRecordToCnames));
sensors().set(ADDRESS_MAPPINGS, Multimaps.unmodifiableMultimap(ipToAllNames));
// Update Bind configuration files and restart the service
getDriver().updateBindConfiguration();
}
}
protected void configureResolver(Entity entity) {
Maybe<SshMachineLocation> machine = Machines.findUniqueMachineLocation(entity.getLocations(), SshMachineLocation.class);
if (machine.isPresent()) {
if (getConfig(REPLACE_RESOLV_CONF)) {
machine.get().copyTo(new StringReader(getConfig(RESOLV_CONF_TEMPLATE)), "/etc/resolv.conf");
} else {
appendTemplate(getConfig(INTERFACE_CONFIG_TEMPLATE), "/etc/sysconfig/network-scripts/ifcfg-eth0", machine.get());
machine.get().execScript("reload network", ImmutableList.of(BashCommands.sudo("service network reload")));
}
LOG.info("configured resolver on {}", machine);
} else {
LOG.debug("{} can't configure resolver at {}: no SshMachineLocation", this, entity);
}
}
protected void appendTemplate(String template, String destination, SshMachineLocation machine) {
String content = ((BindDnsServerSshDriver) getDriver()).processTemplate(template);
String temp = "/tmp/template-" + Strings.makeRandomId(6);
machine.copyTo(new ByteArrayInputStream(content.getBytes()), temp);
machine.execScript("updating file", ImmutableList.of(
BashCommands.sudo(String.format("tee -a %s < %s", destination, temp)),
String.format("rm -f %s", temp)));
}
@Override
public Predicate<? super Entity> getEntityFilter() {
return getConfig(ENTITY_FILTER);
}
// Mostly used in templates
public String getManagementCidr() {
return getConfig(MANAGEMENT_CIDR);
}
public Integer getDnsPort() {
return getAttribute(DNS_PORT);
}
public String getDomainName() {
return getConfig(DOMAIN_NAME);
}
/**
* Increments the serial number sensor and returns it.
* Increment is so that it is guaranteed to be valid for use in a
* modified domain.zone or reverse.zone file.
* <p>
* The side-effect is not entirely obvious by the method name but it
* makes it easier to use from the freemarker templates which call it!
*/
public long getSerial() {
synchronized (serialMutex) {
long next = getAttribute(SERIAL) + 1;
sensors().set(SERIAL, next);
return next;
}
}
public Cidr getReverseLookupNetwork() {
return getAttribute(REVERSE_LOOKUP_CIDR);
}
public String getReverseLookupDomain() {
return getAttribute(REVERSE_LOOKUP_DOMAIN);
}
public DynamicGroup getEntities() {
return getAttribute(ENTITIES);
}
public Map<String, String> getAddressRecords() {
return getAttribute(A_RECORDS);
}
public Multimap<String, String> getCanonicalNameRecords() {
return getAttribute(CNAME_RECORDS);
}
public Map<String, Collection<String>> getCnamesForTemplates() {
return getAttribute(CNAME_RECORDS).asMap();
}
public Map<String, String> getPointerRecords() {
return getAttribute(PTR_RECORDS);
}
}