Add BashCommands.setHostname - A good implementation for CentOS/RHEL, and a poor implementation for everything else (because not persisted on reboot). - Also adds BashCommands.prependToEtcHosts and .appendToEtcHosts - checkJavaHostnameBug() also checks for no hostname.
Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/8e4e9e8b Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/8e4e9e8b Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/8e4e9e8b Branch: refs/heads/master Commit: 8e4e9e8bed6ebb2e1fb77d1500cd892d459c17e0 Parents: 5a939a2 Author: Aled Sage <[email protected]> Authored: Mon Feb 23 17:29:15 2015 +0000 Committer: Aled Sage <[email protected]> Committed: Tue Mar 3 15:44:55 2015 +0000 ---------------------------------------------------------------------- .../util/ssh/BashCommandsIntegrationTest.java | 120 ++++++++++++++++++- .../basic/AbstractSoftwareProcessSshDriver.java | 22 ++++ .../java/JavaSoftwareProcessSshDriver.java | 18 +-- .../java/brooklyn/util/ssh/BashCommands.java | 87 ++++++++++++++ 4 files changed, 238 insertions(+), 9 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/8e4e9e8b/core/src/test/java/brooklyn/util/ssh/BashCommandsIntegrationTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/brooklyn/util/ssh/BashCommandsIntegrationTest.java b/core/src/test/java/brooklyn/util/ssh/BashCommandsIntegrationTest.java index 523ae14..966856c 100644 --- a/core/src/test/java/brooklyn/util/ssh/BashCommandsIntegrationTest.java +++ b/core/src/test/java/brooklyn/util/ssh/BashCommandsIntegrationTest.java @@ -18,6 +18,7 @@ */ package brooklyn.util.ssh; +import static brooklyn.util.ssh.BashCommands.sudo; import static java.lang.String.format; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -29,6 +30,7 @@ import java.io.File; import java.io.IOException; import java.net.ServerSocket; import java.util.Arrays; +import java.util.Collection; import java.util.List; import org.apache.commons.io.FileUtils; @@ -124,7 +126,7 @@ public class BashCommandsIntegrationTest { public void testSudo() throws Exception { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); ByteArrayOutputStream errStream = new ByteArrayOutputStream(); - String cmd = BashCommands.sudo("whoami"); + String cmd = sudo("whoami"); int exitcode = loc.execCommands(ImmutableMap.of("out", outStream, "err", errStream), "test", ImmutableList.of(cmd)); String outstr = new String(outStream.toByteArray()); String errstr = new String(errStream.toByteArray()); @@ -361,6 +363,121 @@ public class BashCommandsIntegrationTest { serverSocket.close(); } } + + + // Disabled by default because of risk of overriding /etc/hosts in really bad way if doesn't work properly! + // As a manual visual inspection test, consider first manually creating /etc/hostname and /etc/sysconfig/network + // so that it looks like debian+ubuntu / CentOS/RHEL. + @Test(groups={"Integration"}, enabled=false) + public void testSetHostnameUnqualified() throws Exception { + runSetHostname("br-"+Identifiers.makeRandomId(8).toLowerCase(), null, false); + } + + @Test(groups={"Integration"}, enabled=false) + public void testSetHostnameQualified() throws Exception { + runSetHostname("br-"+Identifiers.makeRandomId(8).toLowerCase()+".brooklyn.incubator.apache.org", null, false); + } + + @Test(groups={"Integration"}, enabled=false) + public void testSetHostnameNullDomain() throws Exception { + runSetHostname("br-"+Identifiers.makeRandomId(8).toLowerCase(), null, true); + } + + @Test(groups={"Integration"}, enabled=false) + public void testSetHostnameNonNullDomain() throws Exception { + runSetHostname("br-"+Identifiers.makeRandomId(8).toLowerCase(), "brooklyn.incubator.apache.org", true); + } + + protected void runSetHostname(String newHostname, String newDomain, boolean includeDomain) throws Exception { + String fqdn = (includeDomain && Strings.isNonBlank(newDomain)) ? newHostname + "." + newDomain : newHostname; + + LocalManagementContextForTests mgmt = new LocalManagementContextForTests(); + SshMachineLocation loc = mgmt.getLocationManager().createLocation(LocalhostMachineProvisioningLocation.spec()).obtain(); + + execRequiringZeroAndReturningStdout(loc, sudo("cp /etc/hosts /etc/hosts-orig-testSetHostname")).get(); + execRequiringZeroAndReturningStdout(loc, BashCommands.ifFileExistsElse0("/etc/hostname", sudo("cp /etc/hostname /etc/hostname-orig-testSetHostname"))).get(); + execRequiringZeroAndReturningStdout(loc, BashCommands.ifFileExistsElse0("/etc/sysconfig/network", sudo("cp /etc/sysconfig/network /etc/sysconfig/network-orig-testSetHostname"))).get(); + + String origHostname = getHostnameNoArgs(loc); + assertTrue(Strings.isNonBlank(origHostname)); + + try { + List<String> cmd = (includeDomain) ? BashCommands.setHostname(newHostname, newDomain) : BashCommands.setHostname(newHostname); + execRequiringZeroAndReturningStdout(loc, cmd).get(); + + String actualHostnameUnqualified = getHostnameUnqualified(loc); + String actualHostnameFullyQualified = getHostnameFullyQualified(loc); + + // TODO On OS X at least, we aren't actually setting the domain name; we're just letting + // the user pass in what the domain name is. We do add this properly to /etc/hosts + // (e.g. first line is "127.0.0.1 br-g4x5wgx8.brooklyn.incubator.apache.org br-g4x5wgx8 localhost") + // but subsequent calls to `hostname -f` returns the unqualified. Similarly, `domainname` + // returns blank. Therefore we can't assert that it equals our expected val (because we just made + // it up - "brooklyn.incubator.apache.org"). + // assertEquals(actualHostnameFullyQualified, fqdn); + assertEquals(actualHostnameUnqualified, Strings.getFragmentBetween(newHostname, null, ".")); + execRequiringZeroAndReturningStdout(loc, "ping -c1 -n -q "+actualHostnameUnqualified).get(); + execRequiringZeroAndReturningStdout(loc, "ping -c1 -n -q "+actualHostnameFullyQualified).get(); + + String result = execRequiringZeroAndReturningStdout(loc, "grep -n "+fqdn+" /etc/hosts").get(); + assertTrue(result.contains("localhost"), "line="+result); + log.info("result="+result); + + } finally { + execRequiringZeroAndReturningStdout(loc, sudo("cp /etc/hosts-orig-testSetHostname /etc/hosts")).get(); + execRequiringZeroAndReturningStdout(loc, BashCommands.ifFileExistsElse0("/etc/hostname-orig-testSetHostname", sudo("cp /etc/hostname-orig-testSetHostname /etc/hostname"))).get(); + execRequiringZeroAndReturningStdout(loc, BashCommands.ifFileExistsElse0("/etc/sysconfig/network-orig-testSetHostname", sudo("cp /etc/sysconfig/network-orig-testSetHostname /etc/sysconfig/network"))).get(); + execRequiringZeroAndReturningStdout(loc, sudo("hostname "+origHostname)).get(); + } + } + + // Marked disabled because not safe to run on your normal machine! It modifies /etc/hosts, which is dangerous if things go wrong! + @Test(groups={"Integration"}, enabled=false) + public void testModifyEtcHosts() throws Exception { + LocalManagementContextForTests mgmt = new LocalManagementContextForTests(); + SshMachineLocation loc = mgmt.getLocationManager().createLocation(LocalhostMachineProvisioningLocation.spec()).obtain(); + + execRequiringZeroAndReturningStdout(loc, sudo("cp /etc/hosts /etc/hosts-orig-testModifyEtcHosts")).get(); + int numLinesOrig = Integer.parseInt(execRequiringZeroAndReturningStdout(loc, "wc -l /etc/hosts").get().trim().split("\\s")[0]); + + try { + String cmd = BashCommands.prependToEtcHosts("1.2.3.4", "myhostnamefor1234.at.start", "myhostnamefor1234b"); + execRequiringZeroAndReturningStdout(loc, cmd).get(); + + String cmd2 = BashCommands.appendToEtcHosts("5.6.7.8", "myhostnamefor5678.at.end", "myhostnamefor5678"); + execRequiringZeroAndReturningStdout(loc, cmd2).get(); + + String grepFirst = execRequiringZeroAndReturningStdout(loc, "grep -n myhostnamefor1234 /etc/hosts").get(); + String grepLast = execRequiringZeroAndReturningStdout(loc, "grep -n myhostnamefor5678 /etc/hosts").get(); + int numLinesAfter = Integer.parseInt(execRequiringZeroAndReturningStdout(loc, "wc -l /etc/hosts").get().trim().split("\\s")[0]); + log.info("result: numLinesBefore="+numLinesOrig+"; numLinesAfter="+numLinesAfter+"; first="+grepFirst+"; last="+grepLast); + + assertTrue(grepFirst.startsWith("1:") && grepFirst.contains("1.2.3.4 myhostnamefor1234.at.start myhostnamefor1234"), "first="+grepFirst); + assertTrue(grepLast.startsWith((numLinesOrig+2)+":") && grepLast.contains("5.6.7.8 myhostnamefor5678.at.end myhostnamefor5678"), "last="+grepLast); + assertEquals(numLinesOrig + 2, numLinesAfter, "lines orig="+numLinesOrig+", after="+numLinesAfter); + } finally { + execRequiringZeroAndReturningStdout(loc, sudo("cp /etc/hosts-orig-testModifyEtcHosts /etc/hosts")).get(); + } + } + + private String getHostnameNoArgs(SshMachineLocation machine) { + String hostnameStdout = execRequiringZeroAndReturningStdout(machine, "echo FOREMARKER; hostname; echo AFTMARKER").get(); + return Strings.getFragmentBetween(hostnameStdout, "FOREMARKER", "AFTMARKER").trim(); + } + + private String getHostnameUnqualified(SshMachineLocation machine) { + String hostnameStdout = execRequiringZeroAndReturningStdout(machine, "echo FOREMARKER; hostname -s 2> /dev/null || hostname; echo AFTMARKER").get(); + return Strings.getFragmentBetween(hostnameStdout, "FOREMARKER", "AFTMARKER").trim(); + } + + private String getHostnameFullyQualified(SshMachineLocation machine) { + String hostnameStdout = execRequiringZeroAndReturningStdout(machine, "echo FOREMARKER; hostname --fqdn 2> /dev/null || hostname -f; echo AFTMARKER").get(); + return Strings.getFragmentBetween(hostnameStdout, "FOREMARKER", "AFTMARKER").trim(); + } + + private ProcessTaskWrapper<String> execRequiringZeroAndReturningStdout(SshMachineLocation loc, Collection<String> cmds) { + return execRequiringZeroAndReturningStdout(loc, cmds.toArray(new String[cmds.size()])); + } private ProcessTaskWrapper<String> execRequiringZeroAndReturningStdout(SshMachineLocation loc, String... cmds) { ProcessTaskWrapper<String> t = SshTasks.newSshExecTaskFactory(loc, cmds) @@ -368,6 +485,7 @@ public class BashCommandsIntegrationTest { exec.submit(t); return t; } + private ServerSocket openServerSocket() { int lowerBound = 40000; int upperBound = 40100; http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/8e4e9e8b/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessSshDriver.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessSshDriver.java b/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessSshDriver.java index 5139a62..d731dc5 100644 --- a/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessSshDriver.java +++ b/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessSshDriver.java @@ -38,6 +38,7 @@ import brooklyn.entity.basic.lifecycle.NaiveScriptRunner; import brooklyn.entity.basic.lifecycle.ScriptHelper; import brooklyn.entity.drivers.downloads.DownloadResolver; import brooklyn.entity.drivers.downloads.DownloadResolverManager; +import brooklyn.entity.effector.EffectorTasks; import brooklyn.entity.software.SshEffectorTasks; import brooklyn.event.feed.ConfigToAttributes; import brooklyn.location.basic.SshMachineLocation; @@ -53,6 +54,7 @@ import brooklyn.util.stream.ReaderInputStream; import brooklyn.util.stream.Streams; import brooklyn.util.task.DynamicTasks; import brooklyn.util.task.Tasks; +import brooklyn.util.task.system.ProcessTaskWrapper; import brooklyn.util.text.StringPredicates; import brooklyn.util.text.Strings; import brooklyn.util.time.Duration; @@ -603,6 +605,26 @@ public abstract class AbstractSoftwareProcessSshDriver extends AbstractSoftwareP return result; } + public void checkNoHostnameBug() { + try { + ProcessTaskWrapper<Integer> hostnameTask = DynamicTasks.queue(SshEffectorTasks.ssh("echo FOREMARKER; hostname; echo AFTMARKER")).block(); + String stdout = Strings.getFragmentBetween(hostnameTask.getStdout(), "FOREMARKER", "AFTMARKER"); + if (hostnameTask.getExitCode() == 0 && Strings.isNonBlank(stdout)) { + String hostname = stdout.trim(); + if (hostname.equals("(none)")) { + String newHostname = "br-"+getEntity().getId().toLowerCase(); + log.info("Detected no-hostname bug with hostname "+hostname+" for "+getEntity()+"; renaming "+getMachine()+" to hostname "+newHostname); + DynamicTasks.queue(SshEffectorTasks.ssh(BashCommands.setHostname(newHostname, null))).block(); + } + } else { + log.debug("Hostname could not be determined for location "+EffectorTasks.findSshMachine()+"; not doing no-hostname bug check"); + } + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.warn("Error checking/fixing no-hostname bug (continuing): "+e, e); + } + } + public static final String INSTALLING = "installing"; public static final String CUSTOMIZING = "customizing"; public static final String LAUNCHING = "launching"; http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/8e4e9e8b/software/base/src/main/java/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java b/software/base/src/main/java/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java index 3847261..420d3e7 100644 --- a/software/base/src/main/java/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java +++ b/software/base/src/main/java/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java @@ -399,6 +399,7 @@ public abstract class JavaSoftwareProcessSshDriver extends AbstractSoftwareProce /** * Checks for Java 6 or 7, installing Java 7 if neither are found. Override this method to * check for and install specific versions of Java. + * * @see #checkForAndInstallJava6() * @see #checkForAndInstallJava7() * @see #checkForAndInstallJava6or7() @@ -415,19 +416,20 @@ public abstract class JavaSoftwareProcessSshDriver extends AbstractSoftwareProce } public void checkJavaHostnameBug() { + checkNoHostnameBug(); + try { - ProcessTaskWrapper<Integer> hostnameLen = DynamicTasks.queue(SshEffectorTasks.ssh("echo FOREMARKER; hostname -f | wc | awk '{print $3}'; echo AFTMARKER")).block(); - String stdout = Strings.getFragmentBetween(hostnameLen.getStdout(), "FOREMARKER", "AFTMARKER"); - if (hostnameLen.getExitCode()==0 && Strings.isNonBlank(stdout)) { - Integer len = Integer.parseInt(stdout.trim()); + ProcessTaskWrapper<Integer> hostnameTask = DynamicTasks.queue(SshEffectorTasks.ssh("echo FOREMARKER; hostname -f; echo AFTMARKER")).block(); + String stdout = Strings.getFragmentBetween(hostnameTask.getStdout(), "FOREMARKER", "AFTMARKER"); + if (hostnameTask.getExitCode() == 0 && Strings.isNonBlank(stdout)) { + String hostname = stdout.trim(); + Integer len = hostname.length(); if (len > 63) { // likely to cause a java crash due to java bug 7089443 -- set a new short hostname // http://mail.openjdk.java.net/pipermail/net-dev/2012-July/004603.html - String newHostname = "br-"+getEntity().getId(); + String newHostname = "br-"+getEntity().getId().toLowerCase(); log.info("Detected likelihood of Java hostname bug with hostname length "+len+" for "+getEntity()+"; renaming "+getMachine()+" to hostname "+newHostname); - DynamicTasks.queue(SshEffectorTasks.ssh( - "hostname "+newHostname, - "echo 127.0.0.1 "+newHostname+" > /etc/hosts").runAsRoot()).block(); + DynamicTasks.queue(SshEffectorTasks.ssh(BashCommands.setHostname(newHostname, null))).block(); } } else { log.debug("Hostname length could not be determined for location "+EffectorTasks.findSshMachine()+"; not doing Java hostname bug check"); http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/8e4e9e8b/utils/common/src/main/java/brooklyn/util/ssh/BashCommands.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/brooklyn/util/ssh/BashCommands.java b/utils/common/src/main/java/brooklyn/util/ssh/BashCommands.java index e60b332..b59d973 100644 --- a/utils/common/src/main/java/brooklyn/util/ssh/BashCommands.java +++ b/utils/common/src/main/java/brooklyn/util/ssh/BashCommands.java @@ -18,6 +18,9 @@ */ package brooklyn.util.ssh; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + import static java.lang.String.format; import java.util.ArrayList; @@ -38,6 +41,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; public class BashCommands { @@ -590,4 +594,87 @@ public class BashCommands { +"\n"+"EOF_"+id+"\n"; } + public static String prependToEtcHosts(String ip, String... hostnames) { + String tempFileId = "bak"+Identifiers.makeRandomId(4); + return sudo(String.format("sed -i."+tempFileId+" -e '1i\\\n%s %s' /etc/hosts", ip, Joiner.on(" ").join(hostnames))); + } + + public static String appendToEtcHosts(String ip, String... hostnames) { + // Using sed rather than `echo ... >> /etc/hosts` because when embedded inside sudo, + // the redirect doesn't get executed by sudo. + String tempFileId = "bak"+Identifiers.makeRandomId(4); + return sudo(String.format("sed -i."+tempFileId+" -e '$a\\\n%s %s' /etc/hosts", ip, Joiner.on(" ").join(hostnames))); + } + + /** + * Sets the hostname, splitting the given hostname if it contains a dot to include the unqualified and fully qualified names. + * + * @see {@link #setHostname(String, String)} + */ + @Beta + public static List<String> setHostname(String newHostname) { + // See http://www.dns-sd.org/trailingdotsindomainnames.html. + // If we are given "abcd." then let's pass that as-is to setHostname("abcd.", null) + + if (newHostname.indexOf(".") > 0) { + String hostPart = newHostname.substring(0, newHostname.indexOf(".")); + String domainPart = newHostname.substring(hostPart.length()+1); + return setHostname(hostPart, domainPart); + } else { + return setHostname(newHostname, null); + } + } + + /** + * Sets the hostname to {@code hostPart + "." + domainPart}, or if domainPart is null/empty then {code hostPart}. + * + * @param hostPart + * @param domainPart + * @return + */ + @Beta + public static List<String> setHostname(String hostPart, String domainPart) { + // See: + // - http://www.rackspace.com/knowledge_center/article/centos-hostname-change + // - https://wiki.debian.org/HowTo/ChangeHostname + // - http://askubuntu.com/questions/9540/how-do-i-change-the-computer-name + // + // We prepend in /etc/hosts, to ensure the right fqn appears first. + // e.g. comment in http://askubuntu.com/questions/158957/how-to-set-the-fully-qualified-domain-name-in-12-04 + // says "It's important to note that the first domain in /etc/hosts should be your FQDN. " + // + // TODO Should we run `sudo service hostname restart` or `sudo /etc/init.d/hostname restart`? + // I don't think we need to because we've run `sudo hostname <newname>` + // + // TODO What if /etc/sysconfig/network doesn't have a line for HOSTNAME=...? + // + // TODO What about hostPart ending in "." - see http://www.dns-sd.org/trailingdotsindomainnames.html + // for what that means in DNS. However, hostname is not the same as the DNS name (hostnames + // predate the invention of DNS! - although frequently the DNS name has the same first portion + // as the hostname) so the link you gave is not relevant. However despite searching Google and + // man pages I [Ricard] am unable to find a reference which clearly states what characters are + // relevant in a hostname. I think it's safest to assume that the hostname is just [a-z,0-9,-] + // and no dots at all. + + checkNotNull(hostPart, "hostPart"); + checkArgument(!hostPart.contains("."), "hostPart '%s' must not contain '.'", hostPart); + + String tempFileId = "bak"+Identifiers.makeRandomId(4); + + List<String> allhostnames = Lists.newArrayList(); + String fqdn = hostPart; + if (Strings.isNonBlank(domainPart)) { + fqdn = hostPart+"."+domainPart; + allhostnames.add(fqdn); + } + allhostnames.add(hostPart); + allhostnames.add("localhost"); + + return ImmutableList.of( + sudo("sed -i."+tempFileId+" -e 's/^127.0.0.1/# Replaced by Brooklyn\\\n#127.0.0.1/' /etc/hosts"), + prependToEtcHosts("127.0.0.1", allhostnames.toArray(new String[allhostnames.size()])), + ifFileExistsElse0("/etc/sysconfig/network", sudo("sed -i."+tempFileId+" -e 's/^HOSTNAME=.*$/HOSTNAME="+hostPart+"/' /etc/sysconfig/network")), + ifFileExistsElse0("/etc/hostname", sudo("sed -i."+tempFileId+" -e 's/^[a-zA-Z_0-9].*$/"+hostPart+"/' /etc/hostname")), + sudo("hostname "+hostPart)); + } }
