From 6324768f834038ab89f54e7c1fad72babf181bc3 Mon Sep 17 00:00:00 2001 From: Sam Corbett Date: Fri, 17 Oct 2014 17:49:06 +0100 Subject: [PATCH 1/3] BROOKLYN-81: Refuse to reload security provider if none set --- .../java/brooklyn/rest/BrooklynWebConfig.java | 48 +++++++++++-------- .../provider/DelegatingSecurityProvider.java | 6 +++ .../rest/BrooklynRestApiLauncher.java | 4 +- .../java/brooklyn/test/HttpTestUtils.java | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/usage/rest-server/src/main/java/brooklyn/rest/BrooklynWebConfig.java b/usage/rest-server/src/main/java/brooklyn/rest/BrooklynWebConfig.java index 17867a98ca..67fd069fca 100644 --- a/usage/rest-server/src/main/java/brooklyn/rest/BrooklynWebConfig.java +++ b/usage/rest-server/src/main/java/brooklyn/rest/BrooklynWebConfig.java @@ -22,7 +22,7 @@ import brooklyn.config.ConfigMap; import brooklyn.config.ConfigPredicates; import brooklyn.entity.basic.ConfigKeys; -import brooklyn.event.basic.BasicConfigKey; +import brooklyn.rest.security.provider.DelegatingSecurityProvider; import brooklyn.rest.security.provider.ExplicitUsersSecurityProvider; public class BrooklynWebConfig { @@ -30,51 +30,57 @@ public class BrooklynWebConfig { public final static String BASE_NAME = "brooklyn.webconsole"; public final static String BASE_NAME_SECURITY = BASE_NAME+".security"; - /** e.g. brooklyn.webconsole.security.provider=brooklyn.rest.security.provider.AnyoneSecurityProvider will allow anyone to log in; - * default is explicitly named users, using SECURITY_PROVIDER_EXPLICIT_USERS */ - public final static ConfigKey SECURITY_PROVIDER_CLASSNAME = new BasicConfigKey(String.class, + /** + * The security provider to be loaded by {@link DelegatingSecurityProvider}. + * e.g. brooklyn.webconsole.security.provider=brooklyn.rest.security.provider.AnyoneSecurityProvider + * will allow anyone to log in. + */ + public final static ConfigKey SECURITY_PROVIDER_CLASSNAME = ConfigKeys.newStringConfigKey( BASE_NAME_SECURITY+".provider", "class name of a Brooklyn SecurityProvider", ExplicitUsersSecurityProvider.class.getCanonicalName()); - /** explicitly set the users/passwords, e.g. in brooklyn.properties: + /** + * Explicitly set the users/passwords, e.g. in brooklyn.properties: * brooklyn.webconsole.security.users=admin,bob * brooklyn.webconsole.security.user.admin.password=password * brooklyn.webconsole.security.user.bob.password=bobspass */ - - public final static ConfigKey USERS = new BasicConfigKey(String.class, - BASE_NAME_SECURITY+".users"); + public final static ConfigKey USERS = ConfigKeys.newStringConfigKey(BASE_NAME_SECURITY+".users"); public final static ConfigKey PASSWORD_FOR_USER(String user) { - return new BasicConfigKey(String.class, BASE_NAME_SECURITY+".user."+user+".password"); + return ConfigKeys.newStringConfigKey(BASE_NAME_SECURITY + ".user." + user + ".password"); } public final static ConfigKey SALT_FOR_USER(String user) { - return new BasicConfigKey(String.class, BASE_NAME_SECURITY+".user."+user+".salt"); + return ConfigKeys.newStringConfigKey(BASE_NAME_SECURITY + ".user." + user + ".salt"); } public final static ConfigKey SHA256_FOR_USER(String user) { - return new BasicConfigKey(String.class, BASE_NAME_SECURITY+".user."+user+".sha256"); + return ConfigKeys.newStringConfigKey(BASE_NAME_SECURITY + ".user." + user + ".sha256"); } - public final static ConfigKey LDAP_URL = new BasicConfigKey(String.class, + public final static ConfigKey LDAP_URL = ConfigKeys.newStringConfigKey( BASE_NAME_SECURITY+".ldap.url"); - public final static ConfigKey LDAP_REALM = new BasicConfigKey(String.class, + public final static ConfigKey LDAP_REALM = ConfigKeys.newStringConfigKey( BASE_NAME_SECURITY+".ldap.realm"); - public final static ConfigKey HTTPS_REQUIRED = ConfigKeys.newBooleanConfigKey(BASE_NAME+".security.https.required", + public final static ConfigKey HTTPS_REQUIRED = ConfigKeys.newBooleanConfigKey( + BASE_NAME+".security.https.required", "Whether HTTPS is required", false); - public final static ConfigKey KEYSTORE_URL = ConfigKeys.newStringConfigKey(BASE_NAME+".security.keystore.url", - "Keystore from which to take the certificate to present when running HTTPS; " - + "note that normally the password is also required, and an alias for the certificate if the keystore has more than one"); + public final static ConfigKey KEYSTORE_URL = ConfigKeys.newStringConfigKey( + BASE_NAME+".security.keystore.url", + "Keystore from which to take the certificate to present when running HTTPS; " + + "note that normally the password is also required, and an alias for the certificate if the keystore has more than one"); - public final static ConfigKey KEYSTORE_PASSWORD = ConfigKeys.newStringConfigKey(BASE_NAME+".security.keystore.password", - "Password for the "+KEYSTORE_URL); + public final static ConfigKey KEYSTORE_PASSWORD = ConfigKeys.newStringConfigKey( + BASE_NAME+".security.keystore.password", + "Password for the "+KEYSTORE_URL); - public final static ConfigKey KEYSTORE_CERTIFICATE_ALIAS = ConfigKeys.newStringConfigKey(BASE_NAME+".security.keystore.certificate.alias", - "Alias in "+KEYSTORE_URL+" for the certificate to use; defaults to the first if not supplied"); + public final static ConfigKey KEYSTORE_CERTIFICATE_ALIAS = ConfigKeys.newStringConfigKey( + BASE_NAME+".security.keystore.certificate.alias", + "Alias in "+KEYSTORE_URL+" for the certificate to use; defaults to the first if not supplied"); public final static boolean hasNoSecurityOptions(ConfigMap config) { return config.submap(ConfigPredicates.startingWith(BASE_NAME_SECURITY)).isEmpty(); diff --git a/usage/rest-server/src/main/java/brooklyn/rest/security/provider/DelegatingSecurityProvider.java b/usage/rest-server/src/main/java/brooklyn/rest/security/provider/DelegatingSecurityProvider.java index d14e582e97..7b49bdd708 100644 --- a/usage/rest-server/src/main/java/brooklyn/rest/security/provider/DelegatingSecurityProvider.java +++ b/usage/rest-server/src/main/java/brooklyn/rest/security/provider/DelegatingSecurityProvider.java @@ -66,6 +66,12 @@ private synchronized SecurityProvider loadDelegate() { StringConfigMap brooklynProperties = mgmt.getConfig(); String className = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME); + + if (delegate != null && BrooklynWebConfig.hasNoSecurityOptions(mgmt.getConfig())) { + log.debug("{} refusing to change from {}: No security provider set in reloaded properties.", + this, delegate); + return delegate; + } log.info("REST using security provider " + className); try { diff --git a/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java b/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java index 7fb26b904b..4d12d629cf 100644 --- a/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java +++ b/usage/rest-server/src/test/java/brooklyn/rest/BrooklynRestApiLauncher.java @@ -186,8 +186,8 @@ public Server start() { } if (securityProvider != null) { - ((BrooklynProperties) mgmt.getConfig()).put(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME, - securityProvider.getName()); + ((BrooklynProperties) mgmt.getConfig()).put( + BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME, securityProvider.getName()); } if (forceUseOfDefaultCatalogWithJavaClassPath) { diff --git a/usage/test-support/src/main/java/brooklyn/test/HttpTestUtils.java b/usage/test-support/src/main/java/brooklyn/test/HttpTestUtils.java index 6142f7807e..ceee3672bc 100644 --- a/usage/test-support/src/main/java/brooklyn/test/HttpTestUtils.java +++ b/usage/test-support/src/main/java/brooklyn/test/HttpTestUtils.java @@ -331,7 +331,7 @@ public static String getContent(String url) { throw Throwables.propagate(e); } } - + /** * Schedules (with the given executor) a poller that repeatedly accesses the given url, to confirm it always gives * back the expected status code. From 70ca25c014146b19eee43dc4b943dbca813dcdad Mon Sep 17 00:00:00 2001 From: Sam Corbett Date: Tue, 21 Oct 2014 11:45:16 +0100 Subject: [PATCH 2/3] Mark Cidr class Serializable --- utils/common/src/main/java/brooklyn/util/net/Cidr.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/utils/common/src/main/java/brooklyn/util/net/Cidr.java b/utils/common/src/main/java/brooklyn/util/net/Cidr.java index aa57d8f99b..64768f4389 100644 --- a/utils/common/src/main/java/brooklyn/util/net/Cidr.java +++ b/utils/common/src/main/java/brooklyn/util/net/Cidr.java @@ -19,6 +19,7 @@ package brooklyn.util.net; +import java.io.Serializable; import java.net.InetAddress; import java.util.Arrays; import java.util.List; @@ -31,7 +32,9 @@ import com.google.common.collect.ImmutableList; /** represents a CIDR (classless inter-domain routing) token, i.e. 10.0.0.0/8 or 192.168.4.0/24 */ -public class Cidr { +public class Cidr implements Serializable { + + private static final long serialVersionUID = -4605909101590811958L; /** 0.0.0.0/0 -- matches all addresses */ public static final Cidr UNIVERSAL = new Cidr(); @@ -53,7 +56,7 @@ public class Cidr { public static final List NON_PUBLIC_CIDRS = ImmutableList.builder().addAll(PRIVATE_NETWORKS_RFC_1918).add(LINK_LOCAL).add(LOOPBACK).build(); - + final int[] subnetBytes = new int[] { 0, 0, 0, 0 }; final int length; From 3644e1980701faa03b17f1d3478d9b629ecbf7d3 Mon Sep 17 00:00:00 2001 From: Sam Corbett Date: Wed, 29 Oct 2014 12:07:40 +0000 Subject: [PATCH 3/3] BindDnsServer improvements * Rebinds * Supports CNAME records * Integration tests and more live tests --- .../entity/network/bind/BindDnsServer.java | 41 ++- .../network/bind/BindDnsServerDriver.java | 7 +- .../network/bind/BindDnsServerImpl.java | 267 ++++++++++-------- .../network/bind/BindDnsServerSshDriver.java | 23 ++ .../brooklyn/entity/network/bind/domain.zone | 13 +- .../brooklyn/entity/network/bind/reverse.zone | 5 +- .../bind/BindDnsServerIntegrationTest.java | 136 +++++++++ .../network/bind/BindDnsServerLiveTest.java | 72 ++++- .../network/bind/BindDnsServerTest.java | 67 ----- .../bind/DoNothingSoftwareProcessDriver.java | 56 ++++ .../network/bind/PrefixAndIdEnricher.java | 57 ++++ .../network/bind/TestBindDnsServerImpl.java | 60 ++++ 12 files changed, 602 insertions(+), 202 deletions(-) create mode 100644 software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerIntegrationTest.java delete mode 100644 software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerTest.java create mode 100644 software/network/src/test/java/brooklyn/entity/network/bind/DoNothingSoftwareProcessDriver.java create mode 100644 software/network/src/test/java/brooklyn/entity/network/bind/PrefixAndIdEnricher.java create mode 100644 software/network/src/test/java/brooklyn/entity/network/bind/TestBindDnsServerImpl.java diff --git a/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServer.java b/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServer.java index c4e5d834ae..0e7e7c1c5e 100644 --- a/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServer.java +++ b/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServer.java @@ -18,6 +18,7 @@ */ package brooklyn.entity.network.bind; +import java.lang.Long; import java.util.Map; import brooklyn.catalog.Catalog; @@ -26,6 +27,7 @@ import brooklyn.entity.annotation.Effector; import brooklyn.entity.basic.Attributes; import brooklyn.entity.basic.ConfigKeys; +import brooklyn.entity.basic.DynamicGroup; import brooklyn.entity.basic.SoftwareProcess; import brooklyn.entity.proxying.ImplementedBy; import brooklyn.event.AttributeSensor; @@ -39,6 +41,8 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import com.google.common.collect.BiMap; +import com.google.common.collect.Multimap; import com.google.common.reflect.TypeToken; /** @@ -48,7 +52,6 @@ @ImplementedBy(BindDnsServerImpl.class) public interface BindDnsServer extends SoftwareProcess { - @SuppressWarnings({ "unchecked", "rawtypes" }) @SetFromFlag("filter") ConfigKey> ENTITY_FILTER = ConfigKeys.newConfigKey(new TypeToken>() {}, "bind.entity.filter", "Filter for entities which will use the BIND DNS service for name resolution", @@ -66,10 +69,9 @@ public interface BindDnsServer extends SoftwareProcess { ConfigKey MANAGEMENT_CIDR = ConfigKeys.newStringConfigKey( "bind.access.cidr", "Subnet CIDR or ACL allowed to access DNS", "0.0.0.0/0"); - @SuppressWarnings({ "unchecked", "rawtypes" }) @SetFromFlag("hostnameSensor") ConfigKey> HOSTNAME_SENSOR = ConfigKeys.newConfigKey(new TypeToken>() {}, - "bind.sensor.hostname", "Sensor on managed entities that reports the hostname", Attributes.HOSTNAME); + "bind.sensor.hostname", "Sensor on managed entities that reports the hostname"); PortAttributeSensorAndConfigKey DNS_PORT = new PortAttributeSensorAndConfigKey("bind.port", "BIND DNS port for TCP and UDP", PortRanges.fromString("53")); @@ -116,16 +118,33 @@ public interface BindDnsServer extends SoftwareProcess { "bind.template.resolv-conf", "The resolver configuration file for clients (as FreeMarker template)", "classpath://brooklyn/entity/network/bind/resolv.conf"); - /** - * @return the hostname to IP mappings stored in this DNS server's conf file - */ - @Effector(description="Gets the Hostname to IP mappings stored in this DNS server's conf file") - public Map getAddressMappings(); + AttributeSensor ENTITIES = Sensors.newSensor(DynamicGroup.class, + "bind.entities", "The entities being managed by this server"); + + AttributeSensor> ADDRESS_MAPPINGS = Sensors.newSensor(new TypeToken>() {}, + "bind.mappings", "All address mappings maintained by the server, in form address -> [names]"); + + AttributeSensor> A_RECORDS = Sensors.newSensor(new TypeToken>() {}, + "bind.records.a", "All A records for the server, in form name -> address"); + + AttributeSensor> CNAME_RECORDS = Sensors.newSensor(new TypeToken>() {}, + "bind.records.cname", "All CNAME records for the server, in form name -> [names]"); + + AttributeSensor> PTR_RECORDS = Sensors.newSensor(new TypeToken>() {}, + "bind.records.ptr", "All PTR records for the server, in form address -> name. Entries will be in REVERSE_LOOKUP_CIDR. " + + "Entries are guaranteed to have an inverse mapping in A_RECORDS."); + + AttributeSensor SERIAL = Sensors.newLongSensor( + "bind.serial", "A serial number guaranteed to be valid for use in a modified domain.zone or reverse.zone file"); + + public Multimap getAddressMappings(); /** - * @return the IP to hostname mappings stoed in this DNS server's conf file + * @return the IP to hostname mappings stored in this DNS server's conf file + * @deprecated since 0.7.0 use {@link #PTR_RECORDS} instead. */ - @Effector(description="Gets the IP to hostname mappings stoed in this DNS server's conf file") - public Map getReverseMappings(); + @Deprecated + @Effector(description="Gets the IP to hostname mappings stored in this DNS server's conf file") + public Map getReverseMappings(); } diff --git a/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerDriver.java b/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerDriver.java index fce68a4054..7c3d0a226c 100644 --- a/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerDriver.java +++ b/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerDriver.java @@ -21,5 +21,10 @@ import brooklyn.entity.basic.SoftwareProcessDriver; public interface BindDnsServerDriver extends SoftwareProcessDriver { - // Marker Interface + + /** + * Uploads configuration files and restarts the service. + */ + public void updateBindConfiguration(); + } diff --git a/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerImpl.java b/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerImpl.java index a4de54a08a..fc570aa2bb 100644 --- a/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerImpl.java +++ b/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerImpl.java @@ -18,58 +18,61 @@ */ package brooklyn.entity.network.bind; +import static com.google.common.base.Preconditions.checkNotNull; + import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.lang.Long; +import java.util.Collection; import java.util.Map; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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 brooklyn.entity.Entity; +import brooklyn.entity.basic.Attributes; import brooklyn.entity.basic.DynamicGroup; +import brooklyn.entity.basic.EntityFunctions; +import brooklyn.entity.basic.Lifecycle; import brooklyn.entity.basic.SoftwareProcessImpl; import brooklyn.entity.group.AbstractMembershipTrackingPolicy; import brooklyn.entity.proxying.EntitySpec; -import brooklyn.entity.trait.Startable; -import brooklyn.location.Location; -import brooklyn.location.MachineLocation; +import brooklyn.location.basic.Machines; import brooklyn.location.basic.SshMachineLocation; import brooklyn.policy.PolicySpec; -import brooklyn.util.collections.MutableMap; +import brooklyn.util.guava.Maybe; import brooklyn.util.net.Cidr; import brooklyn.util.ssh.BashCommands; import brooklyn.util.text.Strings; -import com.google.common.base.Joiner; -import com.google.common.base.Optional; -import com.google.common.base.Predicates; -import com.google.common.base.Splitter; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableList; -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; - /** * This sets up a BIND DNS server. *

- * NOTE This entity has only been certified on CentOS and RHEL operating systems. + * NOTE This entity has only been certified on CentOS, RHEL, + * Ubuntu and Debian operating systems. */ public class BindDnsServerImpl extends SoftwareProcessImpl implements BindDnsServer { private static final Logger LOG = LoggerFactory.getLogger(BindDnsServerImpl.class); - private AtomicLong serial = new AtomicLong(System.currentTimeMillis()); - private final Object[] mutex = new Object[0]; - private DynamicGroup entities; - private MemberTrackingPolicy policy; - private Multimap entityLocations = HashMultimap.create(); - private ConcurrentMap addressMappings = Maps.newConcurrentMap(); - private ConcurrentMap reverseMappings = Maps.newConcurrentMap(); - public BindDnsServerImpl() { super(); } @@ -86,8 +89,13 @@ public String getDomainName() { return getConfig(DOMAIN_NAME); } + /** + * @return A serial number guaranteed to be valid for use in a modified domain.zone or reverse.zone file. + */ public long getSerial() { - return serial.incrementAndGet(); + long next = getAttribute(SERIAL) + 1; + setAttribute(SERIAL, next); + return next; } public Cidr getReverseLookupNetwork() { @@ -98,26 +106,42 @@ public String getReverseLookupDomain() { return getAttribute(REVERSE_LOOKUP_DOMAIN); } + public DynamicGroup getEntities() { + return getAttribute(ENTITIES); + } + @Override public void init() { super.init(); - entities = addChild(EntitySpec.create(DynamicGroup.class) + checkNotNull(getConfig(HOSTNAME_SENSOR), "{} requires value for {}", getClass().getName(), HOSTNAME_SENSOR); + DynamicGroup entities = addChild(EntitySpec.create(DynamicGroup.class) .configure("entityFilter", getConfig(ENTITY_FILTER))); + setAttribute(ENTITIES, entities); + setAttribute(A_RECORDS, ImmutableMap.of()); + setAttribute(CNAME_RECORDS, ImmutableMultimap.of()); + setAttribute(PTR_RECORDS, ImmutableMap.of()); + setAttribute(ADDRESS_MAPPINGS, ImmutableMultimap.of()); + setAttribute(SERIAL, System.currentTimeMillis()); } @Override - public Class getDriverInterface() { + public void postRebind() { + update(); + } + + @Override + public Class getDriverInterface() { return BindDnsServerDriver.class; } @Override - public Map getAddressMappings() { - return addressMappings; + public Multimap getAddressMappings() { + return getAttribute(ADDRESS_MAPPINGS); } @Override public Map getReverseMappings() { - return reverseMappings; + return getAttribute(PTR_RECORDS); } @Override @@ -140,127 +164,136 @@ protected void preStart() { String reverse = getConfig(REVERSE_LOOKUP_NETWORK); if (Strings.isBlank(reverse)) reverse = getAttribute(ADDRESS); setAttribute(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"; + String reverseLookupDomain = Iterables.toString(Iterables.skip(Lists.reverse(Lists.newArrayList(Splitter.on('.').split(reverse))), 1)) + ".in-addr.arpa"; setAttribute(REVERSE_LOOKUP_DOMAIN, reverseLookupDomain); - Map flags = MutableMap.builder() - .build(); - policy = addPolicy(PolicySpec.create(MemberTrackingPolicy.class) + addPolicy(PolicySpec.create(MemberTrackingPolicy.class) .displayName("Address tracker") .configure("sensorsToTrack", ImmutableSet.of(getConfig(HOSTNAME_SENSOR))) - .configure("group", entities)); + .configure("group", getEntities())); + } - // For any entities that have already come up - for (Entity member : entities.getMembers()) { - if (Strings.isNonBlank(member.getAttribute(getConfig(HOSTNAME_SENSOR)))) added(member); // Ignore, unless hostname set - } + @Override + public void postStart() { + update(); } public static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy { @Override protected void onEntityChange(Entity member) { - // TODO Should we guard to only call if service_up and if hostname set? - ((BindDnsServerImpl)entity).added(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 (Strings.isNonBlank(member.getAttribute(getConfig(HOSTNAME_SENSOR)))) { - ((BindDnsServerImpl)entity).added(member); // Ignore, unless hostname set + if (LOG.isTraceEnabled()) { + LOG.trace("State of {} on added: {}", member, member.getAttribute(Attributes.SERVICE_STATE_ACTUAL).name()); } - } - @Override - protected void onEntityRemoved(Entity member) { - ((BindDnsServerImpl)entity).removed(member); + ((BindDnsServerImpl) entity).configureResolver(member); } } - @Override - public void postStart() { - update(); + private class HasHostnameAndValidLifecycle implements Predicate { + @Override + public boolean apply(Entity input) { + switch (input.getAttribute(Attributes.SERVICE_STATE_ACTUAL)) { + case STOPPED: + case STOPPING: + case DESTROYED: + return false; + } + return input.getAttribute(getConfig(HOSTNAME_SENSOR)) != null; + } } - public void added(Entity member) { - synchronized (mutex) { - Optional location = Iterables.tryFind(member.getLocations(), Predicates.instanceOf(SshMachineLocation.class)); - String hostname = member.getAttribute(getConfig(HOSTNAME_SENSOR)); - if (location.isPresent() && Strings.isNonBlank(hostname)) { - SshMachineLocation machine = (SshMachineLocation) location.get(); - String address = machine.getAddress().getHostAddress(); - if (!entityLocations.containsKey(machine)) { - entityLocations.put(machine, member); - addressMappings.putIfAbsent(address, hostname); + 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 availableEntities = FluentIterable.from(getEntities().getMembers()) + .filter(new HasHostnameAndValidLifecycle()); + LOG.debug("{} updating with entities: {}", this, Iterables.toString(availableEntities)); + ImmutableListMultimap hostnameToEntity = Multimaps.index(availableEntities, + EntityFunctions.attribute(getConfig(HOSTNAME_SENSOR))); + Map octetToName = Maps.newHashMap(); + BiMap ipToARecord = HashBiMap.create(); + Multimap aRecordToCnames = MultimapBuilder.hashKeys().arrayListValues().build(); + Multimap ipToAllNames = MultimapBuilder.hashKeys().arrayListValues().build(); + + for (Map.Entry e : hostnameToEntity.entries()) { + String domainName = e.getKey(); + Maybe location = Machines.findUniqueSshMachineLocation(e.getValue().getLocations()); + if (!location.isPresent()) { + LOG.debug("Member {} of {} does not have an SSH location so will not be configured", e.getValue(), this); + continue; + } + String address = location.get().getAddress().getHostAddress(); + 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); - reverseMappings.putIfAbsent(hostname, octet); - } - if (getAttribute(Startable.SERVICE_UP)) { - update(); + if (!octetToName.containsKey(octet)) octetToName.put(octet, domainName); } - configure(machine); - LOG.info("{} added at location {} with name {}", new Object[] { member, machine, hostname }); + } else { + aRecordToCnames.put(ipToARecord.get(address), domainName); } } - } + setAttribute(A_RECORDS, ImmutableMap.copyOf(ipToARecord.inverse())); + setAttribute(PTR_RECORDS, ImmutableMap.copyOf(octetToName)); + setAttribute(CNAME_RECORDS, Multimaps.unmodifiableMultimap(aRecordToCnames)); + setAttribute(ADDRESS_MAPPINGS, Multimaps.unmodifiableMultimap(ipToAllNames)); + + // Update Bind configuration files and restart the service + getDriver().updateBindConfiguration(); + } } - public void removed(Entity member) { - synchronized (mutex) { - Location location = findLocation(member); - if (location != null) { - entityLocations.remove(location, member); - if (!entityLocations.containsKey(location)) { - addressMappings.remove(((MachineLocation)location).getAddress().getHostAddress()); - } + protected void configureResolver(Entity entity) { + Maybe machine = Machines.findUniqueSshMachineLocation(entity.getLocations()); + 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"))); } - update(); + LOG.info("configured resolver on {}", machine); + } else { + LOG.debug("{} can't configure resolver at {}: no SshMachineLocation", this, entity); } } - private Location findLocation(Entity member) { - // don't use member.getLocations(), because when being stopped the location might not be set - if (entityLocations.containsValue(member)) { - for (Map.Entry entry : entityLocations.entries()) { - if (member.equals(entry.getValue())) { - return entry.getKey(); - } - } - } - return null; + 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))); } - public void update() { - Optional location = Iterables.tryFind(getLocations(), Predicates.instanceOf(SshMachineLocation.class)); - SshMachineLocation machine = (SshMachineLocation) location.get(); - copyTemplate(getConfig(NAMED_CONF_TEMPLATE), "/etc/named.conf", machine); - copyTemplate(getConfig(DOMAIN_ZONE_FILE_TEMPLATE), "/var/named/domain.zone", machine); - copyTemplate(getConfig(REVERSE_ZONE_FILE_TEMPLATE), "/var/named/reverse.zone", machine); - machine.execScript("restart bind", ImmutableList.of(BashCommands.sudo("service named restart"))); - LOG.info("updated named configuration and zone file for '{}' on {}", getDomainName(), this); + public Map getAddressRecords() { + return getAttribute(A_RECORDS); } - public void configure(SshMachineLocation machine) { - if (getConfig(REPLACE_RESOLV_CONF)) { - copyTemplate(getConfig(RESOLV_CONF_TEMPLATE), "/etc/resolv.conf", machine); - } else { - appendTemplate(getConfig(INTERFACE_CONFIG_TEMPLATE), "/etc/sysconfig/network-scripts/ifcfg-eth0", machine); - machine.execScript("reload network", ImmutableList.of(BashCommands.sudo("service network reload"))); - } - LOG.info("configured resolver on {}", machine); + public Multimap getCanonicalNameRecords() { + return getAttribute(CNAME_RECORDS); } - public void copyTemplate(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("copying file", ImmutableList.of(BashCommands.sudo(String.format("mv %s %s", temp, destination)))); + public Map> getCnamesForTemplates() { + return getAttribute(CNAME_RECORDS).asMap(); } - public 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))); + public Map getPointerRecords() { + return getAttribute(PTR_RECORDS); } + } diff --git a/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerSshDriver.java b/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerSshDriver.java index 757e8caa3d..08198f80af 100644 --- a/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerSshDriver.java +++ b/software/network/src/main/java/brooklyn/entity/network/bind/BindDnsServerSshDriver.java @@ -18,9 +18,13 @@ */ package brooklyn.entity.network.bind; +import java.io.ByteArrayInputStream; import java.util.List; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver; import brooklyn.entity.basic.lifecycle.ScriptHelper; import brooklyn.location.basic.SshMachineLocation; @@ -31,11 +35,13 @@ import brooklyn.util.ssh.IptablesCommands; import brooklyn.util.ssh.IptablesCommands.Chain; import brooklyn.util.ssh.IptablesCommands.Policy; +import brooklyn.util.text.Strings; import com.google.common.collect.ImmutableList; public class BindDnsServerSshDriver extends AbstractSoftwareProcessSshDriver implements BindDnsServerDriver { + private static final Logger LOG = LoggerFactory.getLogger(BindDnsServerSshDriver.class); private String serviceName = "named"; public BindDnsServerSshDriver(BindDnsServerImpl entity, SshMachineLocation machine) { @@ -103,4 +109,21 @@ public void stop() { .execute(); } + @Override + public void updateBindConfiguration() { + copyAsRoot(entity.getConfig(BindDnsServer.NAMED_CONF_TEMPLATE), "/etc/named.conf"); + copyAsRoot(entity.getConfig(BindDnsServer.DOMAIN_ZONE_FILE_TEMPLATE), "/var/named/domain.zone"); + copyAsRoot(entity.getConfig(BindDnsServer.REVERSE_ZONE_FILE_TEMPLATE), "/var/named/reverse.zone"); + int result = getMachine().execScript("restart bind", ImmutableList.of(BashCommands.sudo("service "+serviceName+" restart"))); + LOG.info("updated named configuration and zone file for '{}' on {} (exit code {}).", + new Object[]{entity.getConfig(BindDnsServer.DOMAIN_NAME), entity, result}); + } + + private void copyAsRoot(String template, String destination) { + String content = processTemplate(template); + String temp = "/tmp/template-" + Strings.makeRandomId(6); + getMachine().copyTo(new ByteArrayInputStream(content.getBytes()), temp); + getMachine().execScript("copying file", ImmutableList.of(BashCommands.sudo(String.format("mv %s %s", temp, destination)))); + } + } diff --git a/software/network/src/main/resources/brooklyn/entity/network/bind/domain.zone b/software/network/src/main/resources/brooklyn/entity/network/bind/domain.zone index d380298e20..671d234b41 100644 --- a/software/network/src/main/resources/brooklyn/entity/network/bind/domain.zone +++ b/software/network/src/main/resources/brooklyn/entity/network/bind/domain.zone @@ -32,6 +32,15 @@ @ IN NS ns2.${entity.domainName}. ns1 IN A ${driver.address} ns2 IN A ${driver.address} -[#list entity.addressMappings?keys as address] -${entity.addressMappings[address]} IN A ${address} + +;; Addresses +[#list entity.addressRecords?keys as address] +${address} IN A ${entity.addressRecords[address]} +[/#list] + +;; Canonical names +[#list entity.cnamesForTemplates?keys as aRecord] +[#list entity.cnamesForTemplates[aRecord] as cname] +${cname} IN CNAME ${aRecord} +[/#list] [/#list] diff --git a/software/network/src/main/resources/brooklyn/entity/network/bind/reverse.zone b/software/network/src/main/resources/brooklyn/entity/network/bind/reverse.zone index fa21e7e738..6966e1e0fb 100644 --- a/software/network/src/main/resources/brooklyn/entity/network/bind/reverse.zone +++ b/software/network/src/main/resources/brooklyn/entity/network/bind/reverse.zone @@ -31,6 +31,7 @@ ${entity.reverseLookupDomain}. IN NS ns1.${entity.domainName}. ${entity.reverseLookupDomain}. IN NS ns2.${entity.domainName}. ns1 IN A ${driver.address} ns2 IN A ${driver.address} -[#list entity.reverseMappings?keys as hostname] -${entity.reverseMappings[hostname]} IN PTR ${hostname}.${entity.domainName}. + +[#list entity.pointerRecords?keys as address] +${address} IN PTR ${entity.pointerRecords[address]}.${entity.domainName}. [/#list] diff --git a/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerIntegrationTest.java b/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerIntegrationTest.java new file mode 100644 index 0000000000..2ba4e4377e --- /dev/null +++ b/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerIntegrationTest.java @@ -0,0 +1,136 @@ +/* + * 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 brooklyn.entity.network.bind; + +import static org.testng.Assert.assertEquals; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import com.google.common.base.Joiner; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import brooklyn.entity.basic.ApplicationBuilder; +import brooklyn.entity.basic.Attributes; +import brooklyn.entity.basic.EmptySoftwareProcess; +import brooklyn.entity.basic.Entities; +import brooklyn.entity.basic.StartableApplication; +import brooklyn.entity.group.DynamicCluster; +import brooklyn.entity.proxying.EntitySpec; +import brooklyn.entity.rebind.RebindTestFixture; +import brooklyn.location.LocationSpec; +import brooklyn.location.basic.LocalhostMachineProvisioningLocation; +import brooklyn.policy.EnricherSpec; +import brooklyn.test.EntityTestUtils; +import brooklyn.test.entity.TestApplication; + +public class BindDnsServerIntegrationTest extends RebindTestFixture { + + private static final Logger LOG = LoggerFactory.getLogger(BindDnsServerIntegrationTest.class); + private BindDnsServer dns; + private DynamicCluster cluster; + + @Override + protected StartableApplication createApp() { + TestApplication app = ApplicationBuilder.newManagedApp(TestApplication.class, origManagementContext); + dns = app.createAndManageChild(EntitySpec.create(BindDnsServer.class, TestBindDnsServerImpl.class) + .configure(BindDnsServer.ENTITY_FILTER, Predicates.instanceOf(EmptySoftwareProcess.class)) + .configure(BindDnsServer.HOSTNAME_SENSOR, PrefixAndIdEnricher.SENSOR)); + EntitySpec memberSpec = EntitySpec.create(EmptySoftwareProcess.class) + .enricher(EnricherSpec.create(PrefixAndIdEnricher.class) + .configure(PrefixAndIdEnricher.PREFIX, "dns-integration-test-") + .configure(PrefixAndIdEnricher.MONITOR, Attributes.HOSTNAME)); + cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class) + .configure(DynamicCluster.MEMBER_SPEC, memberSpec) + .configure(DynamicCluster.INITIAL_SIZE, 3)); + return app; + } + + @Test(groups = "Integration") + public void testRebindDns() throws Exception { + LocationSpec.create(LocalhostMachineProvisioningLocation.class); + origApp.start(ImmutableList.of(new LocalhostMachineProvisioningLocation())); + logDnsMappings(); + assertEquals(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).keySet().size(), 1); + assertMapSizes(3, 1, 2, 1); + + rebind(); + dns = (BindDnsServer) Iterables.getOnlyElement(Iterables.filter(newApp.getChildren(), Predicates.instanceOf(BindDnsServer.class))); + cluster = (DynamicCluster) Iterables.getOnlyElement(Iterables.filter(newApp.getChildren(), Predicates.instanceOf(DynamicCluster.class))); + + // assert original attributes restored and the server can be updated. + logDnsMappings(); + assertMapSizes(3, 1, 2, 1); + cluster.resize(1); + assertDnsEntityEventuallyHasActiveMembers(1); + logDnsMappings(); + EntityTestUtils.assertAttributeEqualsEventually(cluster, DynamicCluster.GROUP_SIZE, 1); + assertMapSizes(1, 1, 0, 1); + cluster.resize(5); + assertDnsEntityEventuallyHasActiveMembers(5); + logDnsMappings(); + EntityTestUtils.assertAttributeEqualsEventually(cluster, DynamicCluster.GROUP_SIZE, 5); + assertMapSizes(5, 1, 4, 1); + } + + @Test(groups = "Integration") + public void testMapsSeveralEntitiesOnOneMachine() { + origApp.start(ImmutableList.of(new LocalhostMachineProvisioningLocation())); + EntityTestUtils.assertAttributeEqualsEventually(dns, Attributes.SERVICE_UP, true); + logDnsMappings(); + + // One host with one A, two CNAME and one PTR record + assertEquals(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).keySet().size(), 1); + assertMapSizes(3, 1, 2, 1); + String key = Iterables.getOnlyElement(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).keySet()); + assertEquals(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).get(key).size(), 3); + + Entities.dumpInfo(dns); + } + + private void assertMapSizes(int addresses, int aRecords, int cnameRecords, int ptrRecords) { + assertEquals(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).entries().size(), addresses); + assertEquals(dns.getAttribute(BindDnsServer.A_RECORDS).size(), aRecords); + assertEquals(dns.getAttribute(BindDnsServer.CNAME_RECORDS).size(), cnameRecords); + assertEquals(dns.getAttribute(BindDnsServer.PTR_RECORDS).size(), ptrRecords); + } + + private void logDnsMappings() { + LOG.info("A: " + Joiner.on(", ").withKeyValueSeparator("=").join( + dns.getAttribute(BindDnsServer.A_RECORDS))); + LOG.info("CNAME: " + Joiner.on(", ").withKeyValueSeparator("=").join( + dns.getAttribute(BindDnsServer.CNAME_RECORDS).asMap())); + LOG.info("PTR: " + Joiner.on(", ").withKeyValueSeparator("=").join( + dns.getAttribute(BindDnsServer.PTR_RECORDS))); + } + + private void assertDnsEntityEventuallyHasActiveMembers(final int size) { + EntityTestUtils.assertPredicateEventuallyTrue(dns, new Predicate() { + @Override + public boolean apply(BindDnsServer input) { + return input.getAddressMappings().size() == size; + } + }); + } + +} diff --git a/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerLiveTest.java b/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerLiveTest.java index d74c1183d4..4e9dc13f58 100644 --- a/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerLiveTest.java +++ b/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerLiveTest.java @@ -18,6 +18,9 @@ */ package brooklyn.entity.network.bind; +import static brooklyn.test.EntityTestUtils.assertAttributeEqualsEventually; +import static org.testng.Assert.assertEquals; + import java.util.Map; import org.slf4j.Logger; @@ -27,16 +30,25 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import com.google.common.base.Joiner; +import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; +import brooklyn.entity.Entity; import brooklyn.entity.basic.ApplicationBuilder; +import brooklyn.entity.basic.Attributes; +import brooklyn.entity.basic.EmptySoftwareProcess; import brooklyn.entity.basic.Entities; +import brooklyn.entity.basic.SameServerEntity; +import brooklyn.entity.group.DynamicCluster; import brooklyn.entity.proxying.EntitySpec; import brooklyn.location.Location; import brooklyn.location.basic.LocalhostMachineProvisioningLocation; +import brooklyn.policy.EnricherSpec; import brooklyn.test.EntityTestUtils; import brooklyn.test.entity.TestApplication; import brooklyn.util.collections.MutableMap; +import brooklyn.util.time.Duration; public class BindDnsServerLiveTest { @@ -44,6 +56,7 @@ public class BindDnsServerLiveTest { protected TestApplication app; protected Location testLocation; + protected DynamicCluster cluster; protected BindDnsServer dns; @BeforeMethod(alwaysRun = true) @@ -74,11 +87,66 @@ protected void testOperatingSystemProvider(String imageId, String provider) thro Map properties = MutableMap.of("imageId", imageId); testLocation = app.getManagementContext().getLocationRegistry().resolve(provider, properties); - BindDnsServer dns = app.createAndManageChild(EntitySpec.create(BindDnsServer.class)); + BindDnsServer dns = app.createAndManageChild(EntitySpec.create(BindDnsServer.class) + .enricher(EnricherSpec.create(PrefixAndIdEnricher.class) + .configure(PrefixAndIdEnricher.PREFIX, "dns-live-test-") + .configure(PrefixAndIdEnricher.MONITOR, Attributes.HOSTNAME))); dns.start(ImmutableList.of(testLocation)); - EntityTestUtils.assertAttributeEqualsEventually(dns, BindDnsServer.SERVICE_UP, true); + assertAttributeEqualsEventually(dns, BindDnsServer.SERVICE_UP, true); + + assertEquals(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).entries().size(), 1); + assertEquals(dns.getAttribute(BindDnsServer.A_RECORDS).size(), 1); + assertEquals(dns.getAttribute(BindDnsServer.CNAME_RECORDS).size(), 0); + assertEquals(dns.getAttribute(BindDnsServer.PTR_RECORDS).size(), 1); + Entities.dumpInfo(app); } + @Test(groups = "Live", dataProvider = "virtualMachineData") + public void testUpdateWhenNewEntities(String imageId, String provider) { + Map properties = MutableMap.of("imageId", imageId); + testLocation = app.getManagementContext().getLocationRegistry().resolve(provider, properties); + + EntitySpec memberSpec = EntitySpec.create(EmptySoftwareProcess.class) + .enricher(EnricherSpec.create(PrefixAndIdEnricher.class) + .configure(PrefixAndIdEnricher.PREFIX, "dns-live-test-") + .configure(PrefixAndIdEnricher.MONITOR, Attributes.HOSTNAME)); + cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class) + .configure(DynamicCluster.MEMBER_SPEC, memberSpec) + .configure(DynamicCluster.INITIAL_SIZE, 1)); + dns = app.createAndManageChild(EntitySpec.create(BindDnsServer.class) + .configure(BindDnsServer.ENTITY_FILTER, Predicates.instanceOf(EmptySoftwareProcess.class)) + .configure(BindDnsServer.HOSTNAME_SENSOR, PrefixAndIdEnricher.SENSOR)); + + app.start(ImmutableList.of(testLocation)); + assertAttributeEqualsEventually(dns, Attributes.SERVICE_UP, true); + + logDnsMappings(); + assertEquals(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).entries().size(), 1); + assertEquals(dns.getAttribute(BindDnsServer.A_RECORDS).size(), 1); + assertEquals(dns.getAttribute(BindDnsServer.CNAME_RECORDS).size(), 0); + // Harder to make assertions on PTR because the entity servers might not be in the right CIDR + + cluster.resize(2); + logDnsMappings(); + assertEquals(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).entries().size(), 2); + assertEquals(dns.getAttribute(BindDnsServer.A_RECORDS).size(), 2); + assertEquals(dns.getAttribute(BindDnsServer.CNAME_RECORDS).size(), 0); + + cluster.resize(1); + logDnsMappings(); + assertEquals(dns.getAttribute(BindDnsServer.ADDRESS_MAPPINGS).entries().size(), 1); + assertEquals(dns.getAttribute(BindDnsServer.A_RECORDS).size(), 1); + assertEquals(dns.getAttribute(BindDnsServer.CNAME_RECORDS).size(), 0); + } + + private void logDnsMappings() { + LOG.info("A: " + Joiner.on(", ").withKeyValueSeparator("=").join( + dns.getAttribute(BindDnsServer.A_RECORDS))); + LOG.info("CNAME: " + Joiner.on(", ").withKeyValueSeparator("=").join( + dns.getAttribute(BindDnsServer.CNAME_RECORDS).asMap())); + LOG.info("PTR: " + Joiner.on(", ").withKeyValueSeparator("=").join( + dns.getAttribute(BindDnsServer.PTR_RECORDS))); + } } diff --git a/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerTest.java b/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerTest.java deleted file mode 100644 index 250075e83a..0000000000 --- a/software/network/src/test/java/brooklyn/entity/network/bind/BindDnsServerTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 brooklyn.entity.network.bind; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import com.google.common.collect.ImmutableList; - -import brooklyn.entity.basic.ApplicationBuilder; -import brooklyn.entity.basic.Entities; -import brooklyn.entity.proxying.EntitySpec; -import brooklyn.location.Location; -import brooklyn.location.basic.LocalhostMachineProvisioningLocation; -import brooklyn.test.EntityTestUtils; -import brooklyn.test.entity.TestApplication; - -public class BindDnsServerTest { - - @SuppressWarnings("unused") - private static final Logger LOG = LoggerFactory.getLogger(BindDnsServerTest.class); - - protected TestApplication app; - protected Location testLocation; - protected BindDnsServer dns; - - @BeforeMethod(alwaysRun = true) - public void setup() throws Exception { - app = ApplicationBuilder.newManagedApp(TestApplication.class); - testLocation = new LocalhostMachineProvisioningLocation(); - } - - @AfterMethod(alwaysRun = true) - public void shutdown() { - Entities.destroyAll(app.getManagementContext()); - } - - // TODO this needs to be run on a slave VM where we can edit the resolver configuration - @Test(groups = { "WIP", "Integration" }) - protected void testDnsEntity() throws Exception { - BindDnsServer dns = app.createAndManageChild(EntitySpec.create(BindDnsServer.class)); - - app.start(ImmutableList.of(testLocation)); - - EntityTestUtils.assertAttributeEqualsEventually(dns, BindDnsServer.SERVICE_UP, true); - } - -} diff --git a/software/network/src/test/java/brooklyn/entity/network/bind/DoNothingSoftwareProcessDriver.java b/software/network/src/test/java/brooklyn/entity/network/bind/DoNothingSoftwareProcessDriver.java new file mode 100644 index 0000000000..c49e66b942 --- /dev/null +++ b/software/network/src/test/java/brooklyn/entity/network/bind/DoNothingSoftwareProcessDriver.java @@ -0,0 +1,56 @@ +/* + * 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 brooklyn.entity.network.bind; + +import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver; +import brooklyn.entity.basic.EntityLocal; +import brooklyn.location.basic.SshMachineLocation; + +/** + * Implements methods in {@link brooklyn.entity.basic.AbstractSoftwareProcessSshDriver}. + * {@link #isRunning()} returns true. + */ +public class DoNothingSoftwareProcessDriver extends AbstractSoftwareProcessSshDriver { + + public DoNothingSoftwareProcessDriver(EntityLocal entity, SshMachineLocation machine) { + super(entity, machine); + } + + @Override + public boolean isRunning() { + return true; + } + + @Override + public void stop() { + } + + @Override + public void install() { + } + + @Override + public void customize() { + } + + @Override + public void launch() { + } + +} diff --git a/software/network/src/test/java/brooklyn/entity/network/bind/PrefixAndIdEnricher.java b/software/network/src/test/java/brooklyn/entity/network/bind/PrefixAndIdEnricher.java new file mode 100644 index 0000000000..2f2b4b0650 --- /dev/null +++ b/software/network/src/test/java/brooklyn/entity/network/bind/PrefixAndIdEnricher.java @@ -0,0 +1,57 @@ +/* + * 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 brooklyn.entity.network.bind; + +import com.google.common.reflect.TypeToken; + +import brooklyn.config.ConfigKey; +import brooklyn.enricher.basic.AbstractEnricher; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.entity.basic.EntityLocal; +import brooklyn.event.AttributeSensor; +import brooklyn.event.SensorEvent; +import brooklyn.event.SensorEventListener; +import brooklyn.event.basic.Sensors; + +public class PrefixAndIdEnricher extends AbstractEnricher { + + public static final AttributeSensor SENSOR = Sensors.newStringSensor( + "prefixandid.sensor"); + + public static final ConfigKey PREFIX = ConfigKeys.newStringConfigKey( + "prefixandid.prefix", "Sets SENSOR to prefix+entity id"); + + public static final ConfigKey> MONITOR = ConfigKeys.newConfigKey(new TypeToken>() {}, + "prefixandid.attributetomonitor", "Changes on this sensor are monitored and the prefix/id republished"); + + public PrefixAndIdEnricher() { + } + + @Override + public void setEntity(final EntityLocal entity) { + super.setEntity(entity); + subscribe(entity, getConfig(MONITOR), new SensorEventListener() { + @Override + public void onEvent(SensorEvent event) { + entity.setAttribute(SENSOR, getConfig(PREFIX) + entity.getId()); + } + }); + } + +} diff --git a/software/network/src/test/java/brooklyn/entity/network/bind/TestBindDnsServerImpl.java b/software/network/src/test/java/brooklyn/entity/network/bind/TestBindDnsServerImpl.java new file mode 100644 index 0000000000..78f293f4b4 --- /dev/null +++ b/software/network/src/test/java/brooklyn/entity/network/bind/TestBindDnsServerImpl.java @@ -0,0 +1,60 @@ +/* + * 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 brooklyn.entity.network.bind; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.entity.Entity; +import brooklyn.entity.basic.EntityLocal; +import brooklyn.location.basic.SshMachineLocation; + +public class TestBindDnsServerImpl extends BindDnsServerImpl { + + private static final Logger LOG = LoggerFactory.getLogger(TestBindDnsServerImpl.class); + + public static class TestBindDnsServerDriver extends DoNothingSoftwareProcessDriver implements BindDnsServerDriver { + public TestBindDnsServerDriver(EntityLocal entity, SshMachineLocation machine) { + super(entity, machine); + } + + @Override + public void updateBindConfiguration() { + LOG.info("Skipped copy of Bind configuration files to server"); + LOG.debug("Configuration:\n{}", processTemplate(entity.getConfig(BindDnsServer.NAMED_CONF_TEMPLATE))); + LOG.debug("domain.zone:\n{}", processTemplate(entity.getConfig(BindDnsServer.DOMAIN_ZONE_FILE_TEMPLATE))); + LOG.debug("reverse.zone:\n{}", processTemplate(entity.getConfig(BindDnsServer.REVERSE_ZONE_FILE_TEMPLATE))); + } + } + + @Override + public Class getDriverInterface() { + return TestBindDnsServerDriver.class; + } + + @Override + protected void configureResolver(Entity entity) { + LOG.debug("Skipped configuration of resolver on {}", entity); + } + + @Override + protected void appendTemplate(String template, String destination, SshMachineLocation machine) { + LOG.debug("Skipped append of template to {}@{}", destination, machine); + } +}