EC2UnixLauncher.java 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. package hudson.plugins.ec2.ssh;
  2. import hudson.model.Descriptor;
  3. import hudson.model.Hudson;
  4. import hudson.plugins.ec2.EC2ComputerLauncher;
  5. import hudson.plugins.ec2.EC2Cloud;
  6. import hudson.plugins.ec2.EC2Computer;
  7. import hudson.remoting.Channel;
  8. import hudson.remoting.Channel.Listener;
  9. import hudson.slaves.ComputerLauncher;
  10. import java.io.IOException;
  11. import java.io.PrintStream;
  12. import java.net.URL;
  13. import org.apache.commons.io.IOUtils;
  14. import com.amazonaws.AmazonClientException;
  15. import com.amazonaws.services.ec2.model.Instance;
  16. import com.amazonaws.services.ec2.model.KeyPair;
  17. import com.trilead.ssh2.Connection;
  18. import com.trilead.ssh2.SCPClient;
  19. import com.trilead.ssh2.ServerHostKeyVerifier;
  20. import com.trilead.ssh2.Session;
  21. /**
  22. * {@link ComputerLauncher} that connects to a Unix slave on EC2 by using SSH.
  23. *
  24. * @author Kohsuke Kawaguchi
  25. */
  26. public class EC2UnixLauncher extends EC2ComputerLauncher {
  27. private final int FAILED=-1;
  28. private final int SAMEUSER=0;
  29. private final int RECONNECT=-2;
  30. protected String buildUpCommand(EC2Computer computer, String command) {
  31. if (!computer.getRemoteAdmin().equals("root")) {
  32. command = computer.getRootCommandPrefix() + " " + command;
  33. }
  34. return command;
  35. }
  36. @Override
  37. protected void launch(EC2Computer computer, PrintStream logger, Instance inst) throws IOException, AmazonClientException, InterruptedException {
  38. final Connection bootstrapConn;
  39. final Connection conn;
  40. Connection cleanupConn = null; // java's code path analysis for final doesn't work that well.
  41. boolean successful = false;
  42. try {
  43. bootstrapConn = connectToSsh(computer, logger);
  44. int bootstrapResult = bootstrap(bootstrapConn, computer, logger);
  45. if (bootstrapResult == FAILED)
  46. return; // bootstrap closed for us.
  47. else if (bootstrapResult == SAMEUSER)
  48. cleanupConn = bootstrapConn; // take over the connection
  49. else {
  50. // connect fresh as ROOT
  51. cleanupConn = connectToSsh(computer, logger);
  52. KeyPair key = EC2Cloud.get().getKeyPair();
  53. if (!cleanupConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) {
  54. logger.println("Authentication failed");
  55. return; // failed to connect as root.
  56. }
  57. }
  58. conn = cleanupConn;
  59. SCPClient scp = conn.createSCPClient();
  60. String initScript = computer.getNode().initScript;
  61. if(initScript!=null && initScript.trim().length()>0 && conn.exec("test -e ~/.hudson-run-init", logger) !=0) {
  62. logger.println("Executing init script");
  63. scp.put(initScript.getBytes("UTF-8"),"init.sh","/tmp","0700");
  64. Session sess = conn.openSession();
  65. sess.requestDumbPTY(); // so that the remote side bundles stdout and stderr
  66. sess.execCommand(buildUpCommand(computer, "/tmp/init.sh"));
  67. sess.getStdin().close(); // nothing to write here
  68. sess.getStderr().close(); // we are not supposed to get anything from stderr
  69. IOUtils.copy(sess.getStdout(),logger);
  70. int exitStatus = waitCompletion(sess);
  71. if (exitStatus!=0) {
  72. logger.println("init script failed: exit code="+exitStatus);
  73. return;
  74. }
  75. // Needs a tty to run sudo.
  76. sess = conn.openSession();
  77. sess.requestDumbPTY(); // so that the remote side bundles stdout and stderr
  78. sess.execCommand(buildUpCommand(computer, "touch ~/.hudson-run-init"));
  79. }
  80. // TODO: parse the version number. maven-enforcer-plugin might help
  81. logger.println("Verifying that java exists");
  82. if(conn.exec("java -fullversion", logger) !=0) {
  83. logger.println("Installing Java");
  84. String jdk = "java1.6.0_12";
  85. String path = "/hudson-ci/jdk/linux-i586/" + jdk + ".tgz";
  86. URL url = EC2Cloud.get().buildPresignedURL(path);
  87. if(conn.exec("wget -nv -O /tmp/" + jdk + ".tgz '" + url + "'", logger) !=0) {
  88. logger.println("Failed to download Java");
  89. return;
  90. }
  91. if(conn.exec(buildUpCommand(computer, "tar xz -C /usr -f /tmp/" + jdk + ".tgz"), logger) !=0) {
  92. logger.println("Failed to install Java");
  93. return;
  94. }
  95. if(conn.exec(buildUpCommand(computer, "ln -s /usr/" + jdk + "/bin/java /bin/java"), logger) !=0) {
  96. logger.println("Failed to symlink Java");
  97. return;
  98. }
  99. }
  100. // TODO: on Windows with ec2-sshd, this scp command ends up just putting slave.jar as c:\tmp
  101. // bug in ec2-sshd?
  102. logger.println("Copying slave.jar");
  103. scp.put(Hudson.getInstance().getJnlpJars("slave.jar").readFully(),
  104. "slave.jar","/tmp");
  105. String jvmopts = computer.getNode().jvmopts;
  106. String launchString = "java " + (jvmopts != null ? jvmopts : "") + " -jar /tmp/slave.jar";
  107. logger.println("Launching slave agent: " + launchString);
  108. final Session sess = conn.openSession();
  109. sess.execCommand(launchString);
  110. computer.setChannel(sess.getStdout(),sess.getStdin(),logger,new Listener() {
  111. @Override
  112. public void onClosed(Channel channel, IOException cause) {
  113. sess.close();
  114. conn.close();
  115. }
  116. });
  117. successful = true;
  118. } finally {
  119. if(cleanupConn != null && !successful)
  120. cleanupConn.close();
  121. }
  122. }
  123. private int bootstrap(Connection bootstrapConn, EC2Computer computer, PrintStream logger) throws IOException, InterruptedException, AmazonClientException {
  124. boolean closeBootstrap = true;
  125. try {
  126. int tries = 20;
  127. boolean isAuthenticated = false;
  128. KeyPair key = EC2Cloud.get().getKeyPair();
  129. while (tries-- > 0) {
  130. logger.println("Authenticating as " + computer.getRemoteAdmin());
  131. isAuthenticated = bootstrapConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "");
  132. if (isAuthenticated) {
  133. break;
  134. }
  135. logger.println("Authentication failed. Trying again...");
  136. Thread.sleep(10000);
  137. }
  138. if (!isAuthenticated) {
  139. logger.println("Authentication failed");
  140. return FAILED;
  141. }
  142. closeBootstrap = false;
  143. return SAMEUSER;
  144. } finally {
  145. if (closeBootstrap)
  146. bootstrapConn.close();
  147. }
  148. }
  149. private Connection connectToSsh(EC2Computer computer, PrintStream logger) throws AmazonClientException, InterruptedException {
  150. while(true) {
  151. try {
  152. Instance instance = computer.updateInstanceDescription();
  153. String vpc_id = instance.getVpcId();
  154. String host;
  155. if (computer.getNode().usePrivateDnsName) {
  156. host = instance.getPrivateDnsName();
  157. } else {
  158. /* VPC hosts don't have public DNS names, so we need to use an IP address instead */
  159. if (vpc_id == null || vpc_id.equals("")) {
  160. host = instance.getPublicDnsName();
  161. } else {
  162. host = instance.getPrivateIpAddress();
  163. }
  164. }
  165. if ("0.0.0.0".equals(host)) {
  166. logger.println("Invalid host 0.0.0.0, your host is most likely waiting for an ip address.");
  167. throw new IOException("goto sleep");
  168. }
  169. int port = computer.getSshPort();
  170. logger.println("Connecting to " + host + " on port " + port + ". ");
  171. Connection conn = new Connection(host, port);
  172. // currently OpenSolaris offers no way of verifying the host certificate, so just accept it blindly,
  173. // hoping that no man-in-the-middle attack is going on.
  174. conn.connect(new ServerHostKeyVerifier() {
  175. public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception {
  176. return true;
  177. }
  178. });
  179. logger.println("Connected via SSH.");
  180. return conn; // successfully connected
  181. } catch (IOException e) {
  182. // keep retrying until SSH comes up
  183. logger.println("Waiting for SSH to come up. Sleeping 5.");
  184. Thread.sleep(5000);
  185. }
  186. }
  187. }
  188. private int waitCompletion(Session session) throws InterruptedException {
  189. // I noticed that the exit status delivery often gets delayed. Wait up to 1 sec.
  190. for( int i=0; i<10; i++ ) {
  191. Integer r = session.getExitStatus();
  192. if(r!=null) return r;
  193. Thread.sleep(100);
  194. }
  195. return -1;
  196. }
  197. @Override
  198. public Descriptor<ComputerLauncher> getDescriptor() {
  199. throw new UnsupportedOperationException();
  200. }
  201. }