All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.plugins.sshslaves.SSHLauncher Maven / Gradle / Ivy

There is a newer version: 2.0.1
Show newest version
package hudson.plugins.sshslaves;

import static hudson.Util.fixEmpty;
import static java.util.logging.Level.FINE;
import hudson.AbortException;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Util;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.JDK;
import hudson.model.TaskListener;
import hudson.remoting.Channel;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.EnvironmentVariablesNodeProperty;
import hudson.slaves.NodeProperty;
import hudson.slaves.NodePropertyDescriptor;
import hudson.slaves.SlaveComputer;
import hudson.tools.JDKInstaller;
import hudson.tools.JDKInstaller.CPU;
import hudson.tools.JDKInstaller.Platform;
import hudson.tools.ToolLocationNodeProperty;
import hudson.tools.ToolLocationNodeProperty.ToolLocation;
import hudson.util.DescribableList;
import hudson.util.IOException2;
import hudson.util.Secret;
import hudson.util.StreamCopyThread;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.StringWriter;
import java.net.URL;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import org.apache.commons.io.output.CountingOutputStream;
import org.apache.commons.io.output.TeeOutputStream;
import org.kohsuke.putty.PuTTYKey;
import org.kohsuke.stapler.DataBoundConstructor;

import com.trilead.ssh2.Connection;
import com.trilead.ssh2.SFTPv3Client;
import com.trilead.ssh2.SFTPv3FileAttributes;
import com.trilead.ssh2.Session;
import com.trilead.ssh2.StreamGobbler;

/**
 * A computer launcher that tries to start a linux slave by opening an SSH connection and trying to find java.
 */
public class SSHLauncher extends ComputerLauncher {

    /**
     * Field host
     */
    private final String host;

    /**
     * Field port
     */
    private final int port;

    /**
     * Field username
     */
    private final String username;

    /**
     * Field password
     *
     * @todo remove password once authentication is stored in the descriptor.
     */
    private final Secret password;

    /**
     * File path of the private key.
     */
    private final String privatekey;

    /**
     * Field jvmOptions.
     */
    private final String jvmOptions;

    /**
     * Field connection
     */
    private transient Connection connection;

    /**
     * Constructor SSHLauncher creates a new SSHLauncher instance.
     *
     * @param host       The host to connect to.
     * @param port       The port to connect on.
     * @param username   The username to connect as.
     * @param password   The password to connect with.
     * @param privatekey The ssh privatekey to connect with.
     * @param jvmOptions
     */
    @DataBoundConstructor
    public SSHLauncher(String host, int port, String username, String password, String privatekey, String jvmOptions) {
        this.host = host;
        this.jvmOptions = jvmOptions;
        this.port = port == 0 ? 22 : port;
        this.username = username;
        this.password = fixEmpty(password)!=null ? Secret.fromString(password) : null;
        this.privatekey = privatekey;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isLaunchSupported() {
        return true;
    }

    /**
     * Gets the JVM Options used to launch the slave JVM.
     * @return
     */
    public String getJvmOptions() {
        return jvmOptions == null ? "" : jvmOptions;
    }

    /**
     * Gets the formatted current time stamp.
     *
     * @return the formatted current time stamp.
     */
    private static String getTimestamp() {
        return String.format("[%1$tD %1$tT]", new Date());
    }

    /**
     * Returns the remote root workspace (without trailing slash).
     *
     * @param computer The slave computer to get the root workspace of.
     *
     * @return the remote root workspace (without trailing slash).
     */
    private static String getWorkingDirectory(SlaveComputer computer) {
        String workingDirectory = computer.getNode().getRemoteFS();
        while (workingDirectory.endsWith("/")) {
            workingDirectory = workingDirectory.substring(0, workingDirectory.length() - 1);
        }
        return workingDirectory;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void launch(final SlaveComputer computer, final TaskListener listener) throws InterruptedException {
        connection = new Connection(host, port);
        try {
            openConnection(listener);

            verifyNoHeaderJunk(listener);
            reportEnvironment(listener);

            String java = null;
            List tried = new ArrayList();
            outer:
            for (JavaProvider provider : JavaProvider.all()) {
                for (String javaCommand : provider.getJavas(computer, listener, connection)) {
                    LOGGER.fine("Trying Java at "+javaCommand);
                    try {
                        tried.add(javaCommand);
                        java = checkJavaVersion(listener, javaCommand);
                        if (java != null) {
                            break outer;
                        }
                    } catch (IOException e) {
                        LOGGER.log(FINE, "Failed to check the Java version",e);
                        // try the next one
                    }
                }
            }

            final String workingDirectory = getWorkingDirectory(computer);

            if (java == null) {
                // attempt auto JDK installation
                ByteArrayOutputStream buf = new ByteArrayOutputStream();
                try {
                    java = attemptToInstallJDK(listener, workingDirectory, buf);
                } catch (IOException e) {
                    throw new IOException2("Could not find any known supported java version in "+tried+", and we also failed to install JDK as a fallback",e);
                }
            }


            copySlaveJar(listener, workingDirectory);

            startSlave(computer, listener, java, workingDirectory);

            PluginImpl.register(connection);
        } catch (RuntimeException e) {
            e.printStackTrace(listener.error(Messages.SSHLauncher_UnexpectedError()));
        } catch (Error e) {
            e.printStackTrace(listener.error(Messages.SSHLauncher_UnexpectedError()));
        } catch (IOException e) {
            e.printStackTrace(listener.getLogger());
            connection.close();
            connection = null;
            listener.getLogger().println(Messages.SSHLauncher_ConnectionClosed(getTimestamp()));
        }
    }

    /**
     * Makes sure that SSH connection won't produce any unwanted text, which will interfere with sftp execution.
     */
    private void verifyNoHeaderJunk(TaskListener listener) throws IOException, InterruptedException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        connection.exec("true",baos);
        String s = baos.toString();
        if (s.length()!=0) {
            listener.getLogger().println(Messages.SSHLauncher_SSHHeeaderJunkDetected());
            listener.getLogger().println(s);
            throw new AbortException();
        }
    }

    /**
     * Attempts to install JDK, and return the path to Java.
     */
    private String attemptToInstallJDK(TaskListener listener, String workingDirectory, ByteArrayOutputStream buf) throws IOException, InterruptedException {
        if (connection.exec("uname -a",new TeeOutputStream(buf,listener.getLogger()))!=0)
            throw new IOException("Failed to run 'uname' to obtain the environment");

        // guess the platform from uname output. I don't use the specific options because I'm not sure
        // if various platforms have the consistent options
        //
        // === some of the output collected ====
        // Linux bear 2.6.28-15-generic #49-Ubuntu SMP Tue Aug 18 19:25:34 UTC 2009 x86_64 GNU/Linux
        // Linux wssqe20 2.6.24-24-386 #1 Tue Aug 18 16:24:26 UTC 2009 i686 GNU/Linux
        // SunOS hudson 5.11 snv_79a i86pc i386 i86pc
        // SunOS legolas 5.9 Generic_112233-12 sun4u sparc SUNW,Sun-Fire-280R
        // CYGWIN_NT-5.1 franz 1.7.0(0.185/5/3) 2008-07-22 19:09 i686 Cygwin
        // Windows_NT WINXPIE7 5 01 586
        //        (this one is from MKS)

        String uname = buf.toString();
        Platform p = null;
        CPU cpu = null;
        if (uname.contains("GNU/Linux"))        p = Platform.LINUX;
        if (uname.contains("SunOS"))            p = Platform.SOLARIS;
        if (uname.contains("CYGWIN"))           p = Platform.WINDOWS;
        if (uname.contains("Windows_NT"))       p = Platform.WINDOWS;

        if (uname.contains("sparc"))            cpu = CPU.Sparc;
        if (uname.contains("x86_64"))           cpu = CPU.amd64;
        if (Pattern.compile("\\bi?[3-6]86\\b").matcher(uname).find())           cpu = CPU.i386;  // look for ix86 as a word

        if (p==null || cpu==null)
            throw new IOException(Messages.SSHLauncher_FailedToDetectEnvironment(uname));

        String javaDir = workingDirectory + "/jdk"; // this is where we install Java to
        String bundleFile = workingDirectory + "/" + p.bundleFileName; // this is where we download the bundle to

        SFTPClient sftp = new SFTPClient(connection);
        // wipe out and recreate the Java directory
        connection.exec("rm -rf "+javaDir,listener.getLogger());
        sftp.mkdirs(javaDir, 0755);

        JDKInstaller jdk = new JDKInstaller("jdk-6u16-oth-JPR@CDS-CDS_Developer",true);
        URL bundle = jdk.locate(listener, p, cpu);

        listener.getLogger().println("Downloading JDK6u16");
        Util.copyStreamAndClose(bundle.openStream(),new BufferedOutputStream(sftp.writeToFile(bundleFile),32*1024));
        sftp.chmod(bundleFile,0755);

        jdk.install(new RemoteLauncher(listener,connection),p,new SFTPFileSystem(sftp),listener, javaDir,bundleFile);
        return javaDir+"/bin/java";
    }

    /**
     * Starts the slave process.
     *
     * @param computer         The computer.
     * @param listener         The listener.
     * @param java             The full path name of the java executable to use.
     * @param workingDirectory The working directory from which to start the java process.
     *
     * @throws IOException If something goes wrong.
     */
    private void startSlave(SlaveComputer computer, final TaskListener listener, String java,
                            String workingDirectory) throws IOException {
        final Session session = connection.openSession();
        String cmd = "cd '" + workingDirectory + "' && " + java + (jvmOptions == null ? "" : " " + jvmOptions) + " -jar slave.jar";
        listener.getLogger().println(Messages.SSHLauncher_StartingSlaveProcess(getTimestamp(), cmd));
        session.execCommand(cmd);
        final StreamGobbler out = new StreamGobbler(session.getStdout());
        final StreamGobbler err = new StreamGobbler(session.getStderr());

        // capture error information from stderr. this will terminate itself
        // when the process is killed.
        new StreamCopyThread("stderr copier for remote agent on " + computer.getDisplayName(),
                err, listener.getLogger()).start();

        try {
            computer.setChannel(out, session.getStdin(), listener.getLogger(), new Channel.Listener() {
                @Override
                public void onClosed(Channel channel, IOException cause) {
                    if (cause != null) {
                        cause.printStackTrace(listener.error(hudson.model.Messages.Slave_Terminated(getTimestamp())));
                    }
                    try {
                        session.close();
                    } catch (Throwable t) {
                        t.printStackTrace(listener.error(Messages.SSHLauncher_ErrorWhileClosingConnection()));
                    }
                    try {
                        out.close();
                    } catch (Throwable t) {
                        t.printStackTrace(listener.error(Messages.SSHLauncher_ErrorWhileClosingConnection()));
                    }
                    try {
                        err.close();
                    } catch (Throwable t) {
                        t.printStackTrace(listener.error(Messages.SSHLauncher_ErrorWhileClosingConnection()));
                    }
                }
            });

        } catch (InterruptedException e) {
            session.close();
            throw new IOException2(Messages.SSHLauncher_AbortedDuringConnectionOpen(), e);
        }
    }

    /**
     * Method copies the slave jar to the remote system.
     *
     * @param listener         The listener.
     * @param workingDirectory The directory into whihc the slave jar will be copied.
     *
     * @throws IOException If something goes wrong.
     */
    private void copySlaveJar(TaskListener listener, String workingDirectory) throws IOException {
        String fileName = workingDirectory + "/slave.jar";

        listener.getLogger().println(Messages.SSHLauncher_StartingSFTPClient(getTimestamp()));
        SFTPClient sftpClient = null;
        try {
            sftpClient = new SFTPClient(connection);

            try {
                SFTPv3FileAttributes fileAttributes = sftpClient._stat(workingDirectory);
                if (fileAttributes==null) {
                    listener.getLogger().println(Messages.SSHLauncher_RemoteFSDoesNotExist(getTimestamp(),
                            workingDirectory));
                    sftpClient.mkdirs(workingDirectory, 0700);
                } else if (fileAttributes.isRegularFile()) {
                    throw new IOException(Messages.SSHLauncher_RemoteFSIsAFile(workingDirectory));
                }

                try {
                    // try to delete the file in case the slave we are copying is shorter than the slave
                    // that is already there
                    sftpClient.rm(fileName);
                } catch (IOException e) {
                    // the file did not exist... so no need to delete it!
                }

                listener.getLogger().println(Messages.SSHLauncher_CopyingSlaveJar(getTimestamp()));

                try {
                    CountingOutputStream os = new CountingOutputStream(sftpClient.writeToFile(fileName));
                    Util.copyStreamAndClose(
                            Hudson.getInstance().servletContext.getResourceAsStream("/WEB-INF/slave.jar"),
                            os);
                    listener.getLogger().println(Messages.SSHLauncher_CopiedXXXBytes(getTimestamp(), os.getByteCount()));
                } catch (Exception e) {
                    throw new IOException2(Messages.SSHLauncher_ErrorCopyingSlaveJar(), e);
                }
            } catch (Exception e) {
                throw new IOException2(Messages.SSHLauncher_ErrorCopyingSlaveJar(), e);
            }
        } finally {
            if (sftpClient != null) {
                sftpClient.close();
            }
        }
    }

    private void reportEnvironment(TaskListener listener) throws IOException, InterruptedException {
        listener.getLogger().println(Messages._SSHLauncher_RemoteUserEnvironment(getTimestamp()));
        connection.exec("set",listener.getLogger());
    }

    private String checkJavaVersion(TaskListener listener, String javaCommand) throws IOException, InterruptedException {
        listener.getLogger().println(Messages.SSHLauncher_CheckingDefaultJava(getTimestamp(),javaCommand));
        String line;
        StringWriter output = new StringWriter();   // record output from Java

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        connection.exec(javaCommand + " "+jvmOptions + " -version",out);
        BufferedReader r = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(out.toByteArray())));
        final String result = checkJavaVersion(listener.getLogger(), javaCommand, r, output);

        if(null == result) {
        	listener.getLogger().println(Messages.SSHLauncher_UknownJavaVersion(javaCommand));
        	listener.getLogger().println(output);
        	throw new IOException(Messages.SSHLauncher_UknownJavaVersion(javaCommand));
        } else {
        	return result;
        }
    }

	/**
	 * Given the output of "java -version" in r, determine if this
	 * version of Java is supported. This method has default visiblity for testing.
	 * 
	 * @param logger
	 *            where to log the output
	 * @param javaCommand
	 *            the command executed, used for logging
	 * @param r
	 *            the output of "java -version"
	 * @param out
	 *            copy the data from r into this output buffer
	 */
	static String checkJavaVersion(final PrintStream logger, String javaCommand,
			final BufferedReader r, final StringWriter output)
			throws IOException {
		String line;
		while (null != (line = r.readLine())) {
			output.write(line);
			output.write("\n");
			line = line.toLowerCase();
			if (line.startsWith("java version \"")
					|| line.startsWith("openjdk version \"")) {
				final String versionStr = line.substring(
						line.indexOf('\"') + 1, line.lastIndexOf('\"'));
				logger.println(Messages.SSHLauncher_JavaVersionResult(
						getTimestamp(), javaCommand, versionStr));

				// parse as a number and we should be OK as all we care about is up through the first dot.
				try {
					final Number version =
						NumberFormat.getNumberInstance().parse(versionStr);
					if(version.doubleValue() < 1.5) {
						throw new IOException(Messages
								.SSHLauncher_NoJavaFound(line));
					}
				} catch(final ParseException e) {
					throw new IOException(Messages.SSHLauncher_NoJavaFound(line));
				}
				return javaCommand;
			}
		}
		return null;
	}

    private void openConnection(TaskListener listener) throws IOException {
        listener.getLogger().println(Messages.SSHLauncher_OpeningSSHConnection(getTimestamp(), host + ":" + port));
        connection.connect();
        
        String username = this.username;
        if(fixEmpty(username)==null) {
            username = System.getProperty("user.name");
            LOGGER.fine("Defaulting the user name to "+username);
        }

        String pass = getPassword();

        boolean isAuthenticated = false;
        if(fixEmpty(privatekey)==null && fixEmpty(pass)==null) {
            // check the default key locations if no authentication method is explicitly configured.
            File home = new File(System.getProperty("user.home"));
            for (String keyName : Arrays.asList("id_rsa","id_dsa","identity")) {
                File key = new File(home,".ssh/"+keyName);
                if (key.exists()) {
                    listener.getLogger()
                            .println(Messages.SSHLauncher_AuthenticatingPublicKey(getTimestamp(), username, key));
                    isAuthenticated = connection.authenticateWithPublicKey(username, key, null);
                }
                if (isAuthenticated)
                    break;
            }
        }
        if (!isAuthenticated && fixEmpty(privatekey)!=null) {
            File key = new File(privatekey);
            if (key.exists()) {
                listener.getLogger()
                        .println(Messages.SSHLauncher_AuthenticatingPublicKey(getTimestamp(), username, privatekey));
                if (PuTTYKey.isPuTTYKeyFile(key)) {
                    LOGGER.fine(key+" is a PuTTY key file");
                    String openSshKey = new PuTTYKey(key, pass).toOpenSSH();
                    isAuthenticated = connection.authenticateWithPublicKey(username, openSshKey.toCharArray(), pass);
                } else {
                    isAuthenticated = connection.authenticateWithPublicKey(username, key, pass);
                }
            }
        }
        if (!isAuthenticated) {
            listener.getLogger()
                    .println(Messages.SSHLauncher_AuthenticatingUserPass(getTimestamp(), username, "******"));
            isAuthenticated = connection.authenticateWithPassword(username, pass);
        }

        if (isAuthenticated && connection.isAuthenticationComplete()) {
            listener.getLogger().println(Messages.SSHLauncher_AuthenticationSuccessful(getTimestamp()));
        } else {
            listener.getLogger().println(Messages.SSHLauncher_AuthenticationFailed(getTimestamp()));
            connection.close();
            connection = null;
            listener.getLogger().println(Messages.SSHLauncher_ConnectionClosed(getTimestamp()));
            throw new AbortException(Messages.SSHLauncher_AuthenticationFailedException());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void afterDisconnect(SlaveComputer slaveComputer, TaskListener listener) {
        String workingDirectory = getWorkingDirectory(slaveComputer);
        String fileName = workingDirectory + "/slave.jar";

        if (connection != null) {

            SFTPv3Client sftpClient = null;
            try {
                sftpClient = new SFTPv3Client(connection);
                sftpClient.rm(fileName);
            } catch (Exception e) {
                e.printStackTrace(listener.error(Messages.SSHLauncher_ErrorDeletingFile(getTimestamp())));
            } finally {
                if (sftpClient != null) {
                    sftpClient.close();
                }
            }

            connection.close();
            PluginImpl.unregister(connection);
            connection = null;
            listener.getLogger().println(Messages.SSHLauncher_ConnectionClosed(getTimestamp()));
        }
        super.afterDisconnect(slaveComputer, listener);
    }

    /**
     * Getter for property 'host'.
     *
     * @return Value for property 'host'.
     */
    public String getHost() {
        return host;
    }

    /**
     * Getter for property 'port'.
     *
     * @return Value for property 'port'.
     */
    public int getPort() {
        return port;
    }

    /**
     * Getter for property 'username'.
     *
     * @return Value for property 'username'.
     */
    public String getUsername() {
        return username;
    }

    /**
     * Getter for property 'password'.
     *
     * @return Value for property 'password'.
     */
    public String getPassword() {
        return password!=null ? password.toString() : null;
    }

    /**
     * Getter for property 'privatekey'.
     *
     * @return Value for property 'privatekey'.
     */
    public String getPrivatekey() {
        return privatekey;
    }

    @Extension
    public static class DescriptorImpl extends Descriptor {

        // TODO move the authentication storage to descriptor... see SubversionSCM.java

        // TODO add support for key files

        /**
         * {@inheritDoc}
         */
        public String getDisplayName() {
            return Messages.SSHLauncher_DescriptorDisplayName();
        }

    }

    @Extension
    public static class DefaultJavaProvider extends JavaProvider {
        @Override
        public List getJavas(SlaveComputer computer, TaskListener listener, Connection connection) {
            List javas = new ArrayList(Arrays.asList(
                    "java",
                    "/usr/bin/java",
                    "/usr/java/default/bin/java",
                    "/usr/java/latest/bin/java",
                    "/usr/local/bin/java",
                    "/usr/local/java/bin/java",
                    getWorkingDirectory(computer)+"/jdk/bin/java")); // this is where we attempt to auto-install

            DescribableList,NodePropertyDescriptor> list = computer.getNode().getNodeProperties();
            if (list != null) {
                Descriptor jdk = Hudson.getInstance().getDescriptorByType(JDK.DescriptorImpl.class);
                for (NodeProperty prop : list) {
                    if (prop instanceof EnvironmentVariablesNodeProperty) {
                        EnvVars env = ((EnvironmentVariablesNodeProperty)prop).getEnvVars();
                        if (env != null && env.containsKey("JAVA_HOME"))
                            javas.add(env.get("JAVA_HOME") + "/bin/java");
                    }
                    else if (prop instanceof ToolLocationNodeProperty) {
                        for (ToolLocation tool : ((ToolLocationNodeProperty)prop).getLocations())
                            if (tool.getType() == jdk)
                                javas.add(tool.getHome() + "/bin/java");
                    }
                }
            }
            return javas;
        }
    }

    private static final Logger LOGGER = Logger.getLogger(SSHLauncher.class.getName());

//    static {
//        com.trilead.ssh2.log.Logger.enabled = true;
//        com.trilead.ssh2.log.Logger.logger = new DebugLogger() {
//            public void log(int level, String className, String message) {
//                System.out.println(className+"\n"+message);
//            }
//        };
//    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy