Repository: incubator-brooklyn Updated Branches: refs/heads/master 238816f88 -> 9790e8b6b
Adds SetHostnameCustomizer Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/f792fdfb Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/f792fdfb Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/f792fdfb Branch: refs/heads/master Commit: f792fdfb3a4bccbf863997f401212657619317ef Parents: 76b098b Author: Aled Sage <[email protected]> Authored: Mon Oct 19 18:13:44 2015 +0100 Committer: Aled Sage <[email protected]> Committed: Wed Oct 21 09:50:30 2015 +0100 ---------------------------------------------------------------------- .../core/effector/ssh/SshEffectorTasks.java | 7 + .../entity/machine/SetHostnameCustomizer.java | 201 +++++++++++++++++++ .../machine/SetHostnameCustomizerLiveTest.java | 143 +++++++++++++ .../machine/SetHostnameCustomizerTest.java | 67 +++++++ 4 files changed, 418 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f792fdfb/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshEffectorTasks.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshEffectorTasks.java b/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshEffectorTasks.java index 37e582d..a8e427c 100644 --- a/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshEffectorTasks.java +++ b/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshEffectorTasks.java @@ -200,6 +200,13 @@ public class SshEffectorTasks { } } + /** + * @since 0.9.0 + */ + public static SshEffectorTaskFactory<Integer> ssh(SshMachineLocation machine, String ...commands) { + return new SshEffectorTaskFactory<Integer>(machine, commands); + } + public static SshEffectorTaskFactory<Integer> ssh(String ...commands) { return new SshEffectorTaskFactory<Integer>(commands); } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f792fdfb/software/base/src/main/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizer.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizer.java b/software/base/src/main/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizer.java new file mode 100644 index 0000000..7fdb86a --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizer.java @@ -0,0 +1,201 @@ +/* + * 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.machine; + +import static com.google.common.base.Preconditions.checkArgument; + +import org.apache.brooklyn.api.location.BasicMachineLocationCustomizer; +import org.apache.brooklyn.api.location.MachineLocation; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; +import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks.SshEffectorTaskFactory; +import org.apache.brooklyn.location.ssh.SshMachineLocation; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.task.DynamicTasks; +import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; +import org.apache.brooklyn.util.core.text.TemplateProcessor; +import org.apache.brooklyn.util.net.Networking; +import org.apache.brooklyn.util.ssh.BashCommands; +import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +/** + * Sets the hostname on an ssh'able machine. Currently only CentOS and RHEL are supported. + * <p> + * The customizer can be configured with a hard-coded hostname, or with a freemarker template + * whose value (after substitutions) will be used for the hostname. + */ +public class SetHostnameCustomizer extends BasicMachineLocationCustomizer { + + public static final Logger log = LoggerFactory.getLogger(SetHostnameCustomizer.class); + + public static final ConfigKey<String> FIXED_HOSTNAME = ConfigKeys.newStringConfigKey( + "hostname.fixed", + "The statically defined hostname to be set on the machine (if non-null)"); + + public static final ConfigKey<String> FIXED_DOMAIN = ConfigKeys.newStringConfigKey( + "domain.fixed", + "The statically defined domain name to be set on the machine (if non-null)"); + + // the var?? tests if it exists, passing value to ?string(if_present,if_absent) + // the ! provides a default value afterwards, which is never used, but is required for parsing + // when the config key is not available; + // thus the below prefers the first private address, then the first public address, and then + // substitutes dots for dashes. + public static final ConfigKey<String> HOSTNAME_TEMPLATE = ConfigKeys.newStringConfigKey( + "hostname.templated", + "The hostname template, to be resolved and then set on the machine (if non-null). " + +"Assumed to be in free-marker format.", + "ip-" + + "${((location.privateAddresses[0]??)?string(" + + "location.privateAddresses[0]!'X', location.publicAddresses[0]))" + + "?replace(\".\",\"-\")}" + + "-${location.id}"); + + public static final ConfigKey<String> LOCAL_HOSTNAME = ConfigKeys.newStringConfigKey( + "hostname.local.hostname", + "Host name, as known on the local box. Config is set on the location."); + + public static final ConfigKey<String> LOCAL_IP = ConfigKeys.newStringConfigKey( + "hostname.local.address", + "Host address, as known on the local box. Config is set on the location."); + + private final ConfigBag config; + + public SetHostnameCustomizer(ConfigBag config) { + // TODO Any checks that they've given us sufficient configuration? + this.config = config; + } + + @Override + public void customize(MachineLocation machine) { + String localHostname = setLocalHostname((SshMachineLocation) machine); + machine.config().set(LOCAL_HOSTNAME, localHostname); + + String localIp = execHostnameMinusI((SshMachineLocation) machine); + machine.config().set(LOCAL_IP, localIp); + } + + protected String generateHostname(SshMachineLocation machine) { + String hostnameTemplate = config.get(HOSTNAME_TEMPLATE); + if (Strings.isNonBlank(hostnameTemplate)) { + return TemplateProcessor.processTemplateContents(hostnameTemplate, machine, ImmutableMap.<String, Object>of()); + } else { + return null; + } + } + + /** + * Sets the machine's hostname to the value controlled by fixed_hostname and hostname_template. + * If these are blank (and fixed_domain is blank), then just return the current hostname of + * the machine. + */ + public String setLocalHostname(SshMachineLocation machine) { + String hostFixed = config.get(FIXED_HOSTNAME); + String domainFixed = config.get(FIXED_DOMAIN); + String hostnameTemplate = config.get(HOSTNAME_TEMPLATE); + + String hostname; + if (Strings.isNonBlank(hostFixed)) { + hostname = hostFixed; + } else { + if (Strings.isNonBlank(hostnameTemplate)) { + hostname = generateHostname(machine); + } else { + hostname = execHostname(machine); + if (Strings.isBlank(domainFixed)) { + return hostname; + } + } + } + + return setLocalHostname(machine, hostname, domainFixed); + } + + /** + * Sets the machine's hostname to the given value, ensuring /etc/hosts and /etc/sysconfig/network are both + * correctly updated. + */ + public String setLocalHostname(SshMachineLocation machine, String hostName, String domainFixed) { + log.info("Setting local hostname of " + machine + " to " + hostName + + (Strings.isNonBlank(domainFixed) ? ", " + domainFixed : "")); + + boolean hasDomain = Strings.isNonBlank(domainFixed); + String fqdn = hasDomain ? hostName+"."+domainFixed : hostName; + + exec(machine, true, + BashCommands.sudo(String.format("sed -i.bak -e '1i127.0.0.1 %s %s' -e '/^127.0.0.1/d' /etc/hosts", fqdn, hostName)), + BashCommands.sudo(String.format("sed -i.bak -e 's/^HOSTNAME=.*$/HOSTNAME=%s/' /etc/sysconfig/network", fqdn)), + BashCommands.sudo(String.format("hostname %s", fqdn))); + + return hostName; + } + + protected void registerEtcHosts(SshMachineLocation machine, String ip, Iterable<String> hostnames) { + log.info("Updating /etc/hosts of "+machine+": adding "+ip+" = "+hostnames); + + checkArgument(Strings.isNonBlank(ip) && Networking.isValidIp4(ip), "invalid IPv4 address %s", ip); + if (Strings.isBlank(ip) || Iterables.isEmpty(hostnames)) return; + String line = ip+" "+Joiner.on(" ").join(hostnames); + exec(machine, true, "echo " + line + " >> /etc/hosts"); + } + + protected String execHostname(SshMachineLocation machine) { + if (log.isDebugEnabled()) log.debug("Retrieve `hostname` via ssh for {}", machine); + + ProcessTaskWrapper<Integer> cmd = exec(machine, false, "echo hostname=`hostname`"); +// ProcessTaskWrapper<Integer> cmd = DynamicTasks.queue(SshEffectorTasks.ssh(machine, "echo hostname=`hostname`") +// .summary("getHostname")) +// .block(); + + for (String line : cmd.getStdout().split("\n")) { + if (line.contains("hostname=") && !line.contains("`hostname`")) { + return line.substring(line.indexOf("hostname=") + "hostname=".length()).trim(); + } + } + log.info("No hostname found for {} (got {}; {})", new Object[] {machine, cmd.getStdout(), cmd.getStderr()}); + return null; + } + + protected String execHostnameMinusI(SshMachineLocation machine) { + if (log.isDebugEnabled()) log.debug("Retrieve `hostname -I` via ssh for {}", machine); + + ProcessTaskWrapper<Integer> cmd = exec(machine, false, "echo localip=`hostname -I`"); + + for (String line : cmd.getStdout().split("\n")) { + if (line.contains("localip=") && !line.contains("`hostname -I`")) { + return line.substring(line.indexOf("localip=") + "localip=".length()).trim(); + } + } + log.info("No local ip found for {} (got {}; {})", new Object[] {machine, cmd.getStdout(), cmd.getStderr()}); + return null; + } + + protected ProcessTaskWrapper<Integer> exec(SshMachineLocation machine, boolean asRoot, String... cmds) { + SshEffectorTaskFactory<Integer> taskFactory = SshEffectorTasks.ssh(machine, cmds); + if (asRoot) taskFactory.runAsRoot(); + return DynamicTasks.queue(taskFactory).block(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f792fdfb/software/base/src/test/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizerLiveTest.java ---------------------------------------------------------------------- diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizerLiveTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizerLiveTest.java new file mode 100644 index 0000000..7e139f3 --- /dev/null +++ b/software/base/src/test/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizerLiveTest.java @@ -0,0 +1,143 @@ +/* + * 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.machine; + +import static org.testng.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.util.List; + +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.location.MachineProvisioningLocation; +import org.apache.brooklyn.core.entity.BrooklynConfigKeys; +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.core.location.Machines; +import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; +import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport; +import org.apache.brooklyn.entity.AbstractSoftlayerLiveTest; +import org.apache.brooklyn.location.jclouds.JcloudsLocation; +import org.apache.brooklyn.location.ssh.SshMachineLocation; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.base.CaseFormat; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +public class SetHostnameCustomizerLiveTest extends BrooklynAppLiveTestSupport { + + public static final String PROVIDER = AbstractSoftlayerLiveTest.PROVIDER; + public static final String REGION = "ams01"; + public static final String PROVIDER_IMAGE_ID = "CENTOS_6_64"; + public static final String LOCATION_SPEC = PROVIDER + ":" + REGION; + + public static final int MAX_TAG_LENGTH = AbstractSoftlayerLiveTest.MAX_TAG_LENGTH; + public static final int MAX_VM_NAME_LENGTH = AbstractSoftlayerLiveTest.MAX_VM_NAME_LENGTH; + + protected BrooklynProperties brooklynProperties; + + protected MachineProvisioningLocation<SshMachineLocation> loc; + + @BeforeMethod(alwaysRun=true) + public void setUp() throws Exception { + super.setUp(); + List<String> propsToRemove = ImmutableList.of("imageId", "imageDescriptionRegex", "imageNameRegex", "inboundPorts", "hardwareId", "minRam"); + + // Don't let any defaults from brooklyn.properties (except credentials) interfere with test + brooklynProperties = BrooklynProperties.Factory.newDefault(); + for (String propToRemove : propsToRemove) { + for (String propVariant : ImmutableList.of(propToRemove, CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, propToRemove))) { + brooklynProperties.remove("brooklyn.locations.jclouds."+PROVIDER+"."+propVariant); + brooklynProperties.remove("brooklyn.locations."+propVariant); + brooklynProperties.remove("brooklyn.jclouds."+PROVIDER+"."+propVariant); + brooklynProperties.remove("brooklyn.jclouds."+propVariant); + } + } + + // Also removes scriptHeader (e.g. if doing `. ~/.bashrc` and `. ~/.profile`, then that can cause "stdin: is not a tty") + brooklynProperties.remove("brooklyn.ssh.config.scriptHeader"); + + mgmt = new LocalManagementContext(brooklynProperties); + + super.setUp(); + + loc = (MachineProvisioningLocation<SshMachineLocation>) mgmt.getLocationRegistry().resolve(LOCATION_SPEC); + } + + @Test(groups = {"Live"}) + public void testSetFixedHostname() throws Exception { + SetHostnameCustomizer customizer = new SetHostnameCustomizer(ConfigBag.newInstance() + .configure(SetHostnameCustomizer.FIXED_HOSTNAME, "myhostname")); + + MachineEntity entity = app.createAndManageChild(EntitySpec.create(MachineEntity.class) + .configure(BrooklynConfigKeys.SKIP_ON_BOX_BASE_DIR_RESOLUTION, true) + .configure(MachineEntity.PROVISIONING_PROPERTIES.subKey(JcloudsLocation.MACHINE_LOCATION_CUSTOMIZERS.getName()), ImmutableSet.of(customizer)) + .configure(MachineEntity.PROVISIONING_PROPERTIES.subKey(JcloudsLocation.IMAGE_ID.getName()), PROVIDER_IMAGE_ID)); + + + app.start(ImmutableList.of(loc)); + + SshMachineLocation machine = Machines.findUniqueMachineLocation(entity.getLocations(), SshMachineLocation.class).get(); + + assertEquals(getHostname(machine), "myhostname"); + } + + @Test(groups = {"Live"}) + public void testSetAutogeneratedHostname() throws Exception { + SetHostnameCustomizer customizer = new SetHostnameCustomizer(ConfigBag.newInstance()); + + MachineEntity entity = app.createAndManageChild(EntitySpec.create(MachineEntity.class) + .configure(BrooklynConfigKeys.SKIP_ON_BOX_BASE_DIR_RESOLUTION, true) + .configure(MachineEntity.PROVISIONING_PROPERTIES.subKey(JcloudsLocation.MACHINE_LOCATION_CUSTOMIZERS.getName()), ImmutableSet.of(customizer)) + .configure(MachineEntity.PROVISIONING_PROPERTIES.subKey(JcloudsLocation.IMAGE_ID.getName()), "CENTOS_6_64")); + + + app.start(ImmutableList.of(loc)); + + SshMachineLocation machine = Machines.findUniqueMachineLocation(entity.getLocations(), SshMachineLocation.class).get(); + + String ip; + if (machine.getPrivateAddresses().isEmpty()) { + ip = Iterables.get(machine.getPublicAddresses(), 0); + } else { + ip = Iterables.get(machine.getPrivateAddresses(), 0); + } + String expected = "ip-"+(ip.replace(".", "-")+"-"+machine.getId()); + assertEquals(getHostname(machine), expected); + } + + protected String getHostname(SshMachineLocation machine) { + ByteArrayOutputStream outstream = new ByteArrayOutputStream(); + ByteArrayOutputStream errstream = new ByteArrayOutputStream(); + int result = machine.execScript(ImmutableMap.of("out", outstream, "err", errstream), "getHostname", ImmutableList.of("echo hostname=`hostname`")); + assertEquals(result, 0); + + String out = new String(outstream.toByteArray()); + String err = new String(errstream.toByteArray()); + for (String line : out.split("\n")) { + if (line.contains("hostname=") && !line.contains("`hostname`")) { + return line.substring(line.indexOf("hostname=") + "hostname=".length()).trim(); + } + } + throw new IllegalStateException(String.format("No hostname found for %s (got %s; %s)", machine, out, err)); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f792fdfb/software/base/src/test/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizerTest.java ---------------------------------------------------------------------- diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizerTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizerTest.java new file mode 100644 index 0000000..5ffdcdc --- /dev/null +++ b/software/base/src/test/java/org/apache/brooklyn/entity/machine/SetHostnameCustomizerTest.java @@ -0,0 +1,67 @@ +/* + * 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.machine; + +import static org.testng.Assert.assertEquals; + +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; +import org.apache.brooklyn.location.ssh.SshMachineLocation; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; + +public class SetHostnameCustomizerTest extends BrooklynAppUnitTestSupport { + + private SetHostnameCustomizer customizer; + + @BeforeMethod(alwaysRun=true) + public void setUp() throws Exception { + super.setUp(); + customizer = new SetHostnameCustomizer(ConfigBag.newInstance()); + } + + @Test + public void testGeneratedHostnameUsesPrivateIp() throws Exception { + SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .configure("privateAddresses", ImmutableList.of("1.2.3.4", "5.6.7.8")) + .configure("address", "4.3.2.1")); + + assertEquals(customizer.generateHostname(machine), "ip-1-2-3-4-"+machine.getId()); + } + + @Test + public void testGeneratedHostnameUsesPublicIpIfNoPrivate() throws Exception { + SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .configure("address", "4.3.2.1")); + + assertEquals(customizer.generateHostname(machine), "ip-4-3-2-1-"+machine.getId()); + } + + @Test + public void testGeneratedHostnameUsesPublicIpIfEmptyListOfPrivate() throws Exception { + SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .configure("privateAddresses", ImmutableList.of()) + .configure("address", "4.3.2.1")); + + assertEquals(customizer.generateHostname(machine), "ip-4-3-2-1-"+machine.getId()); + } +}
