| /* |
| * 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); |
| } |
| |
| } |