Issue 454: added wrapInInitScript option for running scripts on nodes

This commit is contained in:
Adrian Cole 2011-01-25 20:00:30 -08:00
parent b9e60a8b4e
commit d56bc68d1c
6 changed files with 209 additions and 31 deletions

View File

@ -0,0 +1,127 @@
/**
*
* Copyright (C) 2010 Cloud Conscious, LLC. <info@cloudconscious.com>
*
* ====================================================================
* 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<NodeMetadata, SshClient> sshFactory;
protected final NodeMetadata node;
protected final Statement statement;
protected final boolean runAsRoot;
protected SshClient ssh;
@AssistedInject
public RunScriptOnNodeUsingSsh(Function<NodeMetadata, SshClient> 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();
}
}

View File

@ -35,6 +35,7 @@ import org.jclouds.collect.Memoized;
import org.jclouds.compute.callables.RunScriptOnNode; import org.jclouds.compute.callables.RunScriptOnNode;
import org.jclouds.compute.callables.RunScriptOnNodeAsInitScriptUsingSsh; import org.jclouds.compute.callables.RunScriptOnNodeAsInitScriptUsingSsh;
import org.jclouds.compute.callables.RunScriptOnNodeAsInitScriptUsingSshAndBlockUntilComplete; import org.jclouds.compute.callables.RunScriptOnNodeAsInitScriptUsingSshAndBlockUntilComplete;
import org.jclouds.compute.callables.RunScriptOnNodeUsingSsh;
import org.jclouds.compute.domain.ComputeMetadata; import org.jclouds.compute.domain.ComputeMetadata;
import org.jclouds.compute.domain.Hardware; import org.jclouds.compute.domain.Hardware;
import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.Image;
@ -81,7 +82,8 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule {
bind(new TypeLiteral<Function<TemplateOptions, Statement>>() { bind(new TypeLiteral<Function<TemplateOptions, Statement>>() {
}).to(TemplateOptionsToStatement.class); }).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, RunScriptOnNodeAsInitScriptUsingSshAndBlockUntilComplete.class).implement(RunScriptOnNode.class,
Names.named("nonblocking"), RunScriptOnNodeAsInitScriptUsingSsh.class).build( Names.named("nonblocking"), RunScriptOnNodeAsInitScriptUsingSsh.class).build(
RunScriptOnNodeFactoryImpl.Factory.class)); RunScriptOnNodeFactoryImpl.Factory.class));
@ -103,11 +105,14 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule {
static interface Factory { static interface Factory {
@Named("direct")
RunScriptOnNode exec(NodeMetadata node, Statement script, RunScriptOptions options);
@Named("blocking") @Named("blocking")
RunScriptOnNode blockOnComplete(NodeMetadata node, Statement script, RunScriptOptions options); RunScriptOnNode backgroundAndBlockOnComplete(NodeMetadata node, Statement script, RunScriptOptions options);
@Named("nonblocking") @Named("nonblocking")
RunScriptOnNode dontBlockOnComplete(NodeMetadata node, Statement script, RunScriptOptions options); RunScriptOnNode background(NodeMetadata node, Statement script, RunScriptOptions options);
} }
private final Factory factory; private final Factory factory;
@ -122,8 +127,9 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule {
checkNotNull(node, "node"); checkNotNull(node, "node");
checkNotNull(runScript, "runScript"); checkNotNull(runScript, "runScript");
checkNotNull(options, "options"); checkNotNull(options, "options");
return options.shouldBlockOnComplete() ? factory.blockOnComplete(node, runScript, options) : factory return !options.shouldWrapInInitScript() ? factory.exec(node, runScript, options)
.dontBlockOnComplete(node, runScript, options); : (options.shouldBlockOnComplete() ? factory.backgroundAndBlockOnComplete(node, runScript, options)
: factory.background(node, runScript, options));
} }
@Override @Override

View File

@ -54,7 +54,7 @@ public class CreateSshClientOncePortIsListeningOnNode implements Function<NodeMe
checkState(sshFactory != null, "ssh requested, but no SshModule configured"); checkState(sshFactory != null, "ssh requested, but no SshModule configured");
checkNotNull(node.getCredentials(), "no credentials found for node %s", node.getId()); checkNotNull(node.getCredentials(), "no credentials found for node %s", node.getId());
checkNotNull(node.getCredentials().identity, "no login identity found for node %s", node.getId()); checkNotNull(node.getCredentials().identity, "no login identity found for node %s", node.getId());
checkNotNull(node.getCredentials().credential, "no credential found for $s on node %s", node checkNotNull(node.getCredentials().credential, "no credential found for %s on node %s", node
.getCredentials().identity, node.getId()); .getCredentials().identity, node.getId());
IPSocket socket = ComputeServiceUtils.findReachableSocketOnNode(socketTester, node, node.getLoginPort()); IPSocket socket = ComputeServiceUtils.findReachableSocketOnNode(socketTester, node, node.getLoginPort());
return sshFactory.create(socket, node.getCredentials()); return sshFactory.create(socket, node.getCredentials());

View File

@ -118,6 +118,7 @@ public class RunScriptOptions {
protected Credentials overridingCredentials; protected Credentials overridingCredentials;
protected boolean runAsRoot = true; protected boolean runAsRoot = true;
protected boolean blockOnComplete = true; protected boolean blockOnComplete = true;
protected boolean wrapInInitScript = true;
public RunScriptOptions withOverridingCredentials(Credentials overridingCredentials) { public RunScriptOptions withOverridingCredentials(Credentials overridingCredentials) {
checkNotNull(overridingCredentials, "overridingCredentials"); checkNotNull(overridingCredentials, "overridingCredentials");
@ -142,6 +143,20 @@ public class RunScriptOptions {
return this; return this;
} }
/**
* default true
* <p/>
*
* @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) { public RunScriptOptions blockOnComplete(boolean blockOnComplete) {
this.blockOnComplete = blockOnComplete; this.blockOnComplete = blockOnComplete;
return this; return this;
@ -198,6 +213,15 @@ public class RunScriptOptions {
return blockOnComplete; 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 class Builder {
public static RunScriptOptions nameTask(String name) { public static RunScriptOptions nameTask(String name) {
@ -220,6 +244,11 @@ public class RunScriptOptions {
return options.blockOnComplete(value); 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) { public static RunScriptOptions blockOnPort(int port, int seconds) {
RunScriptOptions options = new RunScriptOptions(); RunScriptOptions options = new RunScriptOptions();
return options.blockOnPort(port, seconds); return options.blockOnPort(port, seconds);
@ -230,7 +259,8 @@ public class RunScriptOptions {
@Override @Override
public String toString() { public String toString() {
return "[overridingCredentials=" + (overridingCredentials != null) + ", port:seconds=" + port + ":" + seconds return "[overridingCredentials=" + (overridingCredentials != null) + ", port:seconds=" + port + ":" + seconds
+ ", runAsRoot=" + runAsRoot + ", blockOnComplete=" + blockOnComplete + "]"; + ", runAsRoot=" + runAsRoot + ", blockOnComplete=" + blockOnComplete + ", wrapInInitScript=" + wrapInInitScript
+ "]";
} }
} }

View File

@ -50,6 +50,7 @@ import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -76,6 +77,7 @@ import org.jclouds.predicates.RetryablePredicate;
import org.jclouds.predicates.SocketOpen; import org.jclouds.predicates.SocketOpen;
import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.AuthorizationException;
import org.jclouds.rest.RestContextFactory; import org.jclouds.rest.RestContextFactory;
import org.jclouds.scriptbuilder.domain.Statements;
import org.jclouds.ssh.ExecResponse; import org.jclouds.ssh.ExecResponse;
import org.jclouds.ssh.SshClient; import org.jclouds.ssh.SshClient;
import org.jclouds.ssh.SshException; import org.jclouds.ssh.SshException;
@ -236,6 +238,12 @@ public abstract class BaseComputeServiceLiveTest {
assert getRootCause(e).getMessage().contains("Auth fail") : e; assert getRootCause(e).getMessage().contains("Auth fail") : e;
} }
for (Entry<? extends NodeMetadata, ExecResponse> 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); runScriptWithCreds(tag, os, good);
checkNodes(nodes, tag); checkNodes(nodes, tag);

View File

@ -106,48 +106,55 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes
SshClient client5 = createMock(SshClient.class); SshClient client5 = createMock(SshClient.class);
expect(factory.create(new IPSocket("144.175.1.1", 22), new Credentials("root", "password1"))).andReturn( expect(factory.create(new IPSocket("144.175.1.1", 22), new Credentials("root", "password1"))).andReturn(
client1); client1);
runScriptAndService(client1, 1); runScriptAndService(client1, 1);
expect(factory.create(new IPSocket("144.175.1.2", 22), new Credentials("root", "password2"))).andReturn( 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( 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(); client2.connect();
try { try {
runScript(client2, "runScriptWithCreds", Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class runScript(client2, "runScriptWithCreds",
.getResourceAsStream("/runscript.sh")), 2); Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class
.getResourceAsStream("/runscript.sh")), 2);
} catch (IOException e) { } catch (IOException e) {
Throwables.propagate(e); Throwables.propagate(e);
} }
client2.disconnect(); client2.disconnect();
expect(factory.create(new IPSocket("144.175.1.3", 22), new Credentials("root", "password3"))).andReturn( 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( 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( 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(client3, "bootstrap", 3);
runScriptAndInstallSsh(client4, "bootstrap", 4); runScriptAndInstallSsh(client4, "bootstrap", 4);
runScriptAndInstallSsh(client5, "bootstrap", 5); runScriptAndInstallSsh(client5, "bootstrap", 5);
expect( expect(
factory.create(eq(new IPSocket("144.175.1.1", 22)), factory.create(eq(new IPSocket("144.175.1.1", 22)), eq(new Credentials("root", keyPair
eq(new Credentials("root", keyPair.get("private"))))).andReturn(client1); .get("private"))))).andReturn(client1);
expect( expect(
factory.create(eq(new IPSocket("144.175.1.2", 22)), factory.create(eq(new IPSocket("144.175.1.2", 22)), eq(new Credentials("root", keyPair
eq(new Credentials("root", keyPair.get("private"))))).andReturn(client2); .get("private"))))).andReturn(client2);
expect( expect(
factory.create(eq(new IPSocket("144.175.1.3", 22)), factory.create(eq(new IPSocket("144.175.1.3", 22)), eq(new Credentials("root", keyPair
eq(new Credentials("root", keyPair.get("private"))))).andReturn(client3); .get("private"))))).andReturn(client3);
expect( expect(
factory.create(eq(new IPSocket("144.175.1.4", 22)), factory.create(eq(new IPSocket("144.175.1.4", 22)), eq(new Credentials("root", keyPair
eq(new Credentials("root", keyPair.get("private"))))).andReturn(client4); .get("private"))))).andReturn(client4);
expect( expect(
factory.create(eq(new IPSocket("144.175.1.5", 22)), factory.create(eq(new IPSocket("144.175.1.5", 22)), eq(new Credentials("root", keyPair
eq(new Credentials("root", keyPair.get("private"))))).andReturn(client5); .get("private"))))).andReturn(client5);
helloAndJava(client2); helloAndJava(client2);
helloAndJava(client3); helloAndJava(client3);
@ -169,7 +176,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes
try { try {
runScript(client, "jboss", Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class runScript(client, "jboss", Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class
.getResourceAsStream("/initscript_with_jboss.sh")), nodeId); .getResourceAsStream("/initscript_with_jboss.sh")), nodeId);
} catch (IOException e) { } catch (IOException e) {
Throwables.propagate(e); Throwables.propagate(e);
} }
@ -183,7 +190,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes
try { try {
runScript(client, scriptName, Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class runScript(client, scriptName, Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class
.getResourceAsStream("/initscript_with_java.sh")), nodeId); .getResourceAsStream("/initscript_with_java.sh")), nodeId);
} catch (IOException e) { } catch (IOException e) {
Throwables.propagate(e); Throwables.propagate(e);
} }
@ -236,7 +243,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes
public void testAssignability() throws Exception { public void testAssignability() throws Exception {
@SuppressWarnings("unused") @SuppressWarnings("unused")
RestContext<ConcurrentMap<String, NodeMetadata>, ConcurrentMap<String, NodeMetadata>> stubContext = new ComputeServiceContextFactory() RestContext<ConcurrentMap<String, NodeMetadata>, ConcurrentMap<String, NodeMetadata>> stubContext = new ComputeServiceContextFactory()
.createContext(provider, identity, credential).getProviderSpecificContext(); .createContext(provider, identity, credential).getProviderSpecificContext();
} }
private static class PayloadEquals implements IArgumentMatcher, Serializable { private static class PayloadEquals implements IArgumentMatcher, Serializable {
@ -283,7 +290,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes
return false; return false;
PayloadEquals other = (PayloadEquals) o; PayloadEquals other = (PayloadEquals) o;
return this.expected == null && other.expected == null || this.expected != null return this.expected == null && other.expected == null || this.expected != null
&& this.expected.equals(other.expected); && this.expected.equals(other.expected);
} }
@Override @Override
@ -371,7 +378,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes
super.testListNodes(); super.testListNodes();
} }
@Test(enabled = true, dependsOnMethods = { "testListNodes", "testGetNodesWithDetails" }) @Test(enabled = true, dependsOnMethods = { "testListNodes", "testGetNodesWithDetails" })
public void testDestroyNodes() { public void testDestroyNodes() {
super.testDestroyNodes(); super.testDestroyNodes();
} }