package hudson.plugins.ec2.ssh; import com.trilead.ssh2.Connection; import com.trilead.ssh2.SCPClient; import com.trilead.ssh2.ServerHostKeyVerifier; import com.trilead.ssh2.Session; import com.xerox.amazonws.ec2.EC2Exception; import com.xerox.amazonws.ec2.KeyPairInfo; import com.xerox.amazonws.ec2.ReservationDescription.Instance; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.plugins.ec2.EC2ComputerLauncher; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.ComputerLauncher; import org.apache.commons.io.IOUtils; import org.jets3t.service.S3ServiceException; import java.io.IOException; import java.io.PrintStream; import java.net.URL; /** * {@link ComputerLauncher} that connects to a Unix slave on EC2 by using SSH. * * @author Kohsuke Kawaguchi */ public class EC2UnixLauncher extends EC2ComputerLauncher { private final int FAILED=-1; private final int SAMEUSER=0; private final int RECONNECT=-2; protected void launch(EC2Computer computer, PrintStream logger, Instance inst) throws IOException, EC2Exception, InterruptedException, S3ServiceException { logger.println("Connecting to "+inst.getDnsName()); final Connection bootstrapConn; final Connection conn; Connection cleanupConn = null; // java's code path analysis for final doesn't work that well. boolean successful = false; try { bootstrapConn = connectToSsh(inst); int bootstrapResult = bootstrap(bootstrapConn, computer, logger); if (bootstrapResult == FAILED) return; // bootstrap closed for us. else if (bootstrapResult == SAMEUSER) cleanupConn = bootstrapConn; // take over the connection else { // connect fresh as ROOT cleanupConn = connectToSsh(inst); KeyPairInfo key = EC2Cloud.get().getKeyPair(); if (!cleanupConn.authenticateWithPublicKey("root", key.getKeyMaterial().toCharArray(), "")) { logger.println("Authentication failed"); return; // failed to connect as root. } } conn = cleanupConn; SCPClient scp = conn.createSCPClient(); String initScript = computer.getNode().initScript; if(initScript!=null && initScript.trim().length()>0 && conn.exec("test -e /.hudson-run-init", logger) !=0) { logger.println("Executing init script"); scp.put(initScript.getBytes("UTF-8"),"init.sh","/tmp","0700"); Session sess = conn.openSession(); sess.requestDumbPTY(); // so that the remote side bundles stdout and stderr sess.execCommand(computer.getRootCommandPrefix() + "/tmp/init.sh"); sess.getStdin().close(); // nothing to write here sess.getStderr().close(); // we are not supposed to get anything from stderr IOUtils.copy(sess.getStdout(),logger); int exitStatus = waitCompletion(sess); if (exitStatus!=0) { logger.println("init script failed: exit code="+exitStatus); return; } // leave the completion marker scp.put(new byte[0],".hudson-run-init","/","0600"); } // TODO: parse the version number. maven-enforcer-plugin might help logger.println("Verifying that java exists"); if(conn.exec("java -fullversion", logger) !=0) { logger.println("Installing Java"); String jdk = "java1.6.0_12"; String path = "/hudson-ci/jdk/linux-i586/" + jdk + ".tgz"; URL url = EC2Cloud.get().buildPresignedURL(path); if(conn.exec("wget -nv -O /usr/" + jdk + ".tgz '" + url + "'", logger) !=0) { logger.println("Failed to download Java"); return; } if(conn.exec("tar xz -C /usr -f /usr/" + jdk + ".tgz", logger) !=0) { logger.println("Failed to install Java"); return; } if(conn.exec("ln -s /usr/" + jdk + "/bin/java /bin/java", logger) !=0) { logger.println("Failed to symlink Java"); return; } } // TODO: on Windows with ec2-sshd, this scp command ends up just putting slave.jar as c:\tmp // bug in ec2-sshd? logger.println("Copying slave.jar"); scp.put(Hudson.getInstance().getJnlpJars("slave.jar").readFully(), "slave.jar","/tmp"); logger.println("Launching slave agent"); final Session sess = conn.openSession(); sess.execCommand("java " + computer.getNode().jvmopts + " -jar /tmp/slave.jar"); computer.setChannel(sess.getStdout(),sess.getStdin(),logger,new Listener() { public void onClosed(Channel channel, IOException cause) { sess.close(); conn.close(); } }); successful = true; } finally { if(cleanupConn != null && !successful) cleanupConn.close(); } } private int bootstrap(Connection bootstrapConn, EC2Computer computer, PrintStream logger) throws IOException, InterruptedException, EC2Exception { boolean closeBootstrap = true; try { int tries = 20; boolean isAuthenticated = false; KeyPairInfo key = EC2Cloud.get().getKeyPair(); while (tries-- > 0) { isAuthenticated = bootstrapConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); if (isAuthenticated) { break; } logger.println("Authentication failed. Trying again..."); Thread.currentThread().sleep(10000); } if (!isAuthenticated) { logger.println("Authentication failed"); return FAILED; } if (!computer.getRemoteAdmin().equals("root")) { // Get root working, so we can scp in etc. Session sess = bootstrapConn.openSession(); sess.requestDumbPTY(); // so that the remote side bundles stdout and stderr sess.execCommand(computer.getRootCommandPrefix() + "cp ~/.ssh/authorized_keys /root/.ssh/"); sess.getStdin().close(); // nothing to write here sess.getStderr().close(); // we are not supposed to get anything from stderr IOUtils.copy(sess.getStdout(), logger); int exitStatus = waitCompletion(sess); if (exitStatus != 0) { logger.println("init script failed: exit code=" + exitStatus); return FAILED; } return RECONNECT; } else { closeBootstrap = false; return SAMEUSER; } } finally { if (closeBootstrap) bootstrapConn.close(); } } private Connection connectToSsh(Instance inst) throws InterruptedException { while(true) { try { Connection conn = new Connection(inst.getDnsName(),22); // currently OpenSolaris offers no way of verifying the host certificate, so just accept it blindly, // hoping that no man-in-the-middle attack is going on. conn.connect(new ServerHostKeyVerifier() { public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { return true; } }); return conn; // successfully connected } catch (IOException e) { // keep retrying until SSH comes up Thread.sleep(5000); } } } private int waitCompletion(Session session) throws InterruptedException { // I noticed that the exit status delivery often gets delayed. Wait up to 1 sec. for( int i=0; i<10; i++ ) { Integer r = session.getExitStatus(); if(r!=null) return r; Thread.sleep(100); } return -1; } public Descriptor<ComputerLauncher> getDescriptor() { throw new UnsupportedOperationException(); } }