diff --git a/compute/src/main/java/org/jclouds/compute/callables/RunScriptOnNodeUsingSsh.java b/compute/src/main/java/org/jclouds/compute/callables/RunScriptOnNodeUsingSsh.java new file mode 100644 index 0000000000..cb5902682a --- /dev/null +++ b/compute/src/main/java/org/jclouds/compute/callables/RunScriptOnNodeUsingSsh.java @@ -0,0 +1,127 @@ +/** + * + * Copyright (C) 2010 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.compute.callables; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import javax.annotation.Resource; +import javax.inject.Named; + +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.options.RunScriptOptions; +import org.jclouds.compute.reference.ComputeServiceConstants; +import org.jclouds.logging.Logger; +import org.jclouds.scriptbuilder.domain.OsFamily; +import org.jclouds.scriptbuilder.domain.Statement; +import org.jclouds.ssh.ExecResponse; +import org.jclouds.ssh.SshClient; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +/** + * + * @author Adrian Cole + */ +public class RunScriptOnNodeUsingSsh implements RunScriptOnNode { + @Resource + @Named(ComputeServiceConstants.COMPUTE_LOGGER) + protected Logger logger = Logger.NULL; + + protected final Function sshFactory; + protected final NodeMetadata node; + protected final Statement statement; + protected final boolean runAsRoot; + + protected SshClient ssh; + + @AssistedInject + public RunScriptOnNodeUsingSsh(Function sshFactory, @Assisted NodeMetadata node, + @Assisted Statement statement, @Assisted RunScriptOptions options) { + this.sshFactory = checkNotNull(sshFactory, "sshFactory"); + this.node = checkNotNull(node, "node"); + this.statement = checkNotNull(statement, "statement"); + this.runAsRoot = options.shouldRunAsRoot(); + } + + @Override + public ExecResponse call() { + checkState(ssh != null, "please call init() before invoking call"); + try { + ssh.connect(); + ExecResponse returnVal; + String command = (runAsRoot) ? execAsRoot(statement.render(OsFamily.UNIX)) : execScriptAsDefaultUser(statement + .render(OsFamily.UNIX)); + returnVal = runCommand(command); + if (logger.isTraceEnabled()) + logger.trace("<< %s[%s]", statement, returnVal); + else + logger.debug("<< %s(%d)", statement, returnVal.getExitCode()); + return returnVal; + } finally { + if (ssh != null) + ssh.disconnect(); + } + } + + @Override + public RunScriptOnNode init() { + ssh = sshFactory.apply(node); + return this; + } + + protected ExecResponse runCommand(String command) { + ExecResponse returnVal; + logger.debug(">> running [%s] as %s@%s", command.replace(node.getAdminPassword() != null ? node + .getAdminPassword() : "XXXXX", "XXXXX"), ssh.getUsername(), ssh.getHostAddress()); + returnVal = ssh.exec(command); + return returnVal; + } + + @VisibleForTesting + public String execAsRoot(String command) { + if (node.getCredentials().identity.equals("root")) { + } else if (node.getAdminPassword() != null) { + command = String.format("echo '%s'|sudo -S %s", node.getAdminPassword(), command); + } else { + command = "sudo " + command; + } + return command; + } + + protected String execScriptAsDefaultUser(String command) { + return command; + } + + public NodeMetadata getNode() { + return node; + } + + @Override + public String toString() { + return Objects.toStringHelper(this).add("node", node).add("name", statement).add("runAsRoot", runAsRoot) + .toString(); + } + +} \ No newline at end of file diff --git a/compute/src/main/java/org/jclouds/compute/config/BaseComputeServiceContextModule.java b/compute/src/main/java/org/jclouds/compute/config/BaseComputeServiceContextModule.java index 3c9bd2d0ca..3d2c4f0033 100644 --- a/compute/src/main/java/org/jclouds/compute/config/BaseComputeServiceContextModule.java +++ b/compute/src/main/java/org/jclouds/compute/config/BaseComputeServiceContextModule.java @@ -35,6 +35,7 @@ import org.jclouds.collect.Memoized; import org.jclouds.compute.callables.RunScriptOnNode; import org.jclouds.compute.callables.RunScriptOnNodeAsInitScriptUsingSsh; import org.jclouds.compute.callables.RunScriptOnNodeAsInitScriptUsingSshAndBlockUntilComplete; +import org.jclouds.compute.callables.RunScriptOnNodeUsingSsh; import org.jclouds.compute.domain.ComputeMetadata; import org.jclouds.compute.domain.Hardware; import org.jclouds.compute.domain.Image; @@ -81,11 +82,12 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule { bind(new TypeLiteral>() { }).to(TemplateOptionsToStatement.class); - install(new FactoryModuleBuilder().implement(RunScriptOnNode.class, Names.named("blocking"), + install(new FactoryModuleBuilder().implement(RunScriptOnNode.class, Names.named("direct"), + RunScriptOnNodeUsingSsh.class).implement(RunScriptOnNode.class, Names.named("blocking"), RunScriptOnNodeAsInitScriptUsingSshAndBlockUntilComplete.class).implement(RunScriptOnNode.class, Names.named("nonblocking"), RunScriptOnNodeAsInitScriptUsingSsh.class).build( RunScriptOnNodeFactoryImpl.Factory.class)); - + bind(RunScriptOnNode.Factory.class).to(RunScriptOnNodeFactoryImpl.class); install(new FactoryModuleBuilder().implement(new TypeLiteral>() { @@ -103,11 +105,14 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule { static interface Factory { + @Named("direct") + RunScriptOnNode exec(NodeMetadata node, Statement script, RunScriptOptions options); + @Named("blocking") - RunScriptOnNode blockOnComplete(NodeMetadata node, Statement script, RunScriptOptions options); + RunScriptOnNode backgroundAndBlockOnComplete(NodeMetadata node, Statement script, RunScriptOptions options); @Named("nonblocking") - RunScriptOnNode dontBlockOnComplete(NodeMetadata node, Statement script, RunScriptOptions options); + RunScriptOnNode background(NodeMetadata node, Statement script, RunScriptOptions options); } private final Factory factory; @@ -122,8 +127,9 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule { checkNotNull(node, "node"); checkNotNull(runScript, "runScript"); checkNotNull(options, "options"); - return options.shouldBlockOnComplete() ? factory.blockOnComplete(node, runScript, options) : factory - .dontBlockOnComplete(node, runScript, options); + return !options.shouldWrapInInitScript() ? factory.exec(node, runScript, options) + : (options.shouldBlockOnComplete() ? factory.backgroundAndBlockOnComplete(node, runScript, options) + : factory.background(node, runScript, options)); } @Override diff --git a/compute/src/main/java/org/jclouds/compute/functions/CreateSshClientOncePortIsListeningOnNode.java b/compute/src/main/java/org/jclouds/compute/functions/CreateSshClientOncePortIsListeningOnNode.java index 9f686c12d0..55b798b32e 100644 --- a/compute/src/main/java/org/jclouds/compute/functions/CreateSshClientOncePortIsListeningOnNode.java +++ b/compute/src/main/java/org/jclouds/compute/functions/CreateSshClientOncePortIsListeningOnNode.java @@ -54,7 +54,7 @@ public class CreateSshClientOncePortIsListeningOnNode implements Function + * + * @param wrapInInitScript + * if the command is long-running, use this option to ensure it is wrapInInitScripted + * properly. (ex. have jclouds wrap it an init script, nohup, etc) + * @return + */ + public RunScriptOptions wrapInInitScript(boolean wrapInInitScript) { + this.wrapInInitScript = wrapInInitScript; + return this; + } + public RunScriptOptions blockOnComplete(boolean blockOnComplete) { this.blockOnComplete = blockOnComplete; return this; @@ -198,6 +213,15 @@ public class RunScriptOptions { return blockOnComplete; } + /** + * Whether to wait until the script has completed. By default, true. + * + * @return value + */ + public boolean shouldWrapInInitScript() { + return wrapInInitScript; + } + public static class Builder { public static RunScriptOptions nameTask(String name) { @@ -220,6 +244,11 @@ public class RunScriptOptions { return options.blockOnComplete(value); } + public static RunScriptOptions wrapInInitScript(boolean value) { + RunScriptOptions options = new RunScriptOptions(); + return options.wrapInInitScript(value); + } + public static RunScriptOptions blockOnPort(int port, int seconds) { RunScriptOptions options = new RunScriptOptions(); return options.blockOnPort(port, seconds); @@ -230,7 +259,8 @@ public class RunScriptOptions { @Override public String toString() { return "[overridingCredentials=" + (overridingCredentials != null) + ", port:seconds=" + port + ":" + seconds - + ", runAsRoot=" + runAsRoot + ", blockOnComplete=" + blockOnComplete + "]"; + + ", runAsRoot=" + runAsRoot + ", blockOnComplete=" + blockOnComplete + ", wrapInInitScript=" + wrapInInitScript + + "]"; } } diff --git a/compute/src/test/java/org/jclouds/compute/BaseComputeServiceLiveTest.java b/compute/src/test/java/org/jclouds/compute/BaseComputeServiceLiveTest.java index 67062e0cab..104f801050 100755 --- a/compute/src/test/java/org/jclouds/compute/BaseComputeServiceLiveTest.java +++ b/compute/src/test/java/org/jclouds/compute/BaseComputeServiceLiveTest.java @@ -50,6 +50,7 @@ import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -76,6 +77,7 @@ import org.jclouds.predicates.RetryablePredicate; import org.jclouds.predicates.SocketOpen; import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.RestContextFactory; +import org.jclouds.scriptbuilder.domain.Statements; import org.jclouds.ssh.ExecResponse; import org.jclouds.ssh.SshClient; import org.jclouds.ssh.SshException; @@ -236,6 +238,12 @@ public abstract class BaseComputeServiceLiveTest { assert getRootCause(e).getMessage().contains("Auth fail") : e; } + for (Entry response : client.runScriptOnNodesMatching( + runningWithTag(tag), Statements.exec("echo hello"), overrideCredentialsWith(good).wrapInInitScript(false)) + .entrySet()) + assert response.getValue().getOutput().trim().equals("hello") : response.getKey() + ": " + + response.getValue(); + runScriptWithCreds(tag, os, good); checkNodes(nodes, tag); diff --git a/compute/src/test/java/org/jclouds/compute/StubComputeServiceIntegrationTest.java b/compute/src/test/java/org/jclouds/compute/StubComputeServiceIntegrationTest.java index 556063a666..9eaaef2651 100644 --- a/compute/src/test/java/org/jclouds/compute/StubComputeServiceIntegrationTest.java +++ b/compute/src/test/java/org/jclouds/compute/StubComputeServiceIntegrationTest.java @@ -106,48 +106,55 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes SshClient client5 = createMock(SshClient.class); expect(factory.create(new IPSocket("144.175.1.1", 22), new Credentials("root", "password1"))).andReturn( - client1); + client1); runScriptAndService(client1, 1); expect(factory.create(new IPSocket("144.175.1.2", 22), new Credentials("root", "password2"))).andReturn( - client2).times(2); + client2).times(3); expect(factory.create(new IPSocket("144.175.1.2", 22), new Credentials("root", "romeo"))).andThrow( - new SshException("Auth fail")); + new SshException("Auth fail")); + + // run script without backgrounding + client2.connect(); + expect(client2.exec("echo hello\n")).andReturn(new ExecResponse("hello\n", "", 0)); + client2.disconnect(); + client2.connect(); try { - runScript(client2, "runScriptWithCreds", Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class - .getResourceAsStream("/runscript.sh")), 2); + runScript(client2, "runScriptWithCreds", + Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class + .getResourceAsStream("/runscript.sh")), 2); } catch (IOException e) { Throwables.propagate(e); } client2.disconnect(); expect(factory.create(new IPSocket("144.175.1.3", 22), new Credentials("root", "password3"))).andReturn( - client3).times(2); + client3).times(2); expect(factory.create(new IPSocket("144.175.1.4", 22), new Credentials("root", "password4"))).andReturn( - client4).times(2); + client4).times(2); expect(factory.create(new IPSocket("144.175.1.5", 22), new Credentials("root", "password5"))).andReturn( - client5).times(2); + client5).times(2); runScriptAndInstallSsh(client3, "bootstrap", 3); runScriptAndInstallSsh(client4, "bootstrap", 4); runScriptAndInstallSsh(client5, "bootstrap", 5); expect( - factory.create(eq(new IPSocket("144.175.1.1", 22)), - eq(new Credentials("root", keyPair.get("private"))))).andReturn(client1); + factory.create(eq(new IPSocket("144.175.1.1", 22)), eq(new Credentials("root", keyPair + .get("private"))))).andReturn(client1); expect( - factory.create(eq(new IPSocket("144.175.1.2", 22)), - eq(new Credentials("root", keyPair.get("private"))))).andReturn(client2); + factory.create(eq(new IPSocket("144.175.1.2", 22)), eq(new Credentials("root", keyPair + .get("private"))))).andReturn(client2); expect( - factory.create(eq(new IPSocket("144.175.1.3", 22)), - eq(new Credentials("root", keyPair.get("private"))))).andReturn(client3); + factory.create(eq(new IPSocket("144.175.1.3", 22)), eq(new Credentials("root", keyPair + .get("private"))))).andReturn(client3); expect( - factory.create(eq(new IPSocket("144.175.1.4", 22)), - eq(new Credentials("root", keyPair.get("private"))))).andReturn(client4); + factory.create(eq(new IPSocket("144.175.1.4", 22)), eq(new Credentials("root", keyPair + .get("private"))))).andReturn(client4); expect( - factory.create(eq(new IPSocket("144.175.1.5", 22)), - eq(new Credentials("root", keyPair.get("private"))))).andReturn(client5); + factory.create(eq(new IPSocket("144.175.1.5", 22)), eq(new Credentials("root", keyPair + .get("private"))))).andReturn(client5); helloAndJava(client2); helloAndJava(client3); @@ -169,7 +176,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes try { runScript(client, "jboss", Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class - .getResourceAsStream("/initscript_with_jboss.sh")), nodeId); + .getResourceAsStream("/initscript_with_jboss.sh")), nodeId); } catch (IOException e) { Throwables.propagate(e); } @@ -183,7 +190,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes try { runScript(client, scriptName, Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class - .getResourceAsStream("/initscript_with_java.sh")), nodeId); + .getResourceAsStream("/initscript_with_java.sh")), nodeId); } catch (IOException e) { Throwables.propagate(e); } @@ -236,7 +243,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes public void testAssignability() throws Exception { @SuppressWarnings("unused") RestContext, ConcurrentMap> stubContext = new ComputeServiceContextFactory() - .createContext(provider, identity, credential).getProviderSpecificContext(); + .createContext(provider, identity, credential).getProviderSpecificContext(); } private static class PayloadEquals implements IArgumentMatcher, Serializable { @@ -283,7 +290,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes return false; PayloadEquals other = (PayloadEquals) o; return this.expected == null && other.expected == null || this.expected != null - && this.expected.equals(other.expected); + && this.expected.equals(other.expected); } @Override @@ -371,7 +378,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes super.testListNodes(); } - @Test(enabled = true, dependsOnMethods = { "testListNodes", "testGetNodesWithDetails" }) + @Test(enabled = true, dependsOnMethods = { "testListNodes", "testGetNodesWithDetails" }) public void testDestroyNodes() { super.testDestroyNodes(); }