com.github.segator.jenkins.scaleway.ScalewayComputerLauncher.java Source code

Java tutorial

Introduction

Here is the source code for com.github.segator.jenkins.scaleway.ScalewayComputerLauncher.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 robert.gruendler@dubture.com
 *               2016 Maxim Biro <nurupo.contributions@gmail.com>
 *               2016 Isaac Aymerich <isaac.aymerich@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.segator.jenkins.scaleway;

import com.google.common.base.Strings;
import com.github.segator.scaleway.api.ScalewayClient;
import com.github.segator.scaleway.api.ScalewayFactory;
import com.github.segator.scaleway.api.entity.ScalewayServer;
import com.github.segator.scaleway.api.entity.exceptions.ScalewayException;
import com.trilead.ssh2.Connection;
import com.trilead.ssh2.SCPClient;
import com.trilead.ssh2.Session;
import hudson.Util;
import hudson.model.TaskListener;
import hudson.remoting.Channel;
import hudson.slaves.SlaveComputer;
import hudson.util.TimeUnit2;
import jenkins.model.Jenkins;
import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.PrintStream;
import java.net.SocketTimeoutException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import static java.lang.String.format;
import static java.lang.String.format;

/**
 * The {@link ScalewayComputerLauncher} is responsible for:
 *
 * <ul>
 * <li>Connecting to a slave via SSH</li>
 * <li>Installing Java and the Jenkins agent to the slave</li>
 * </ul>
 *
 * @author robert.gruendler@dubture.com
 * @author isaac.aymerich@gmail.com
 */
public class ScalewayComputerLauncher extends hudson.slaves.ComputerLauncher {

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

    private static abstract class JavaInstaller {

        protected abstract String getInstallCommand(String javaVersion);

        protected abstract String checkPackageManager();

        protected boolean isUsable(Connection conn, PrintStream logger) throws IOException, InterruptedException {
            return checkCommand(conn, logger, checkPackageManager());
        }

        private boolean checkCommand(Connection conn, PrintStream logger, String command)
                throws IOException, InterruptedException {
            logger.println("Checking: " + command);
            return conn.exec(command, logger) == 0;
        }

        protected int installJava(Connection conn, PrintStream logger, Iterable<String> javaVersion)
                throws IOException, InterruptedException {
            int result = 1;
            for (String version : javaVersion) {
                result = conn.exec(getInstallCommand(version), logger);
                if (result == 0) {
                    return result;
                }
            }
            return result;
        }
    }

    private static final List<String> VALID_VERSIONS = Arrays.asList("1.8", "1.7", "1.9");

    private static final Collection<JavaInstaller> INSTALLERS = new HashSet<JavaInstaller>() {
        {
            add(new JavaInstaller() { // apt
                @Override
                protected String getInstallCommand(String javaVersion) {
                    return "apt-get update -q && apt-get install -y " + getPackageName(javaVersion);
                }

                @Override
                protected String checkPackageManager() {
                    return "which apt-get";
                }

                private String getPackageName(String javaVersion) {
                    return "openjdk-" + javaVersion.replaceFirst("1.", "") + "-jre-headless";
                }
            });
            add(new JavaInstaller() { // yum
                @Override
                protected String getInstallCommand(String javaVersion) {
                    return "yum install -y " + getPackageName(javaVersion);
                }

                @Override
                protected String checkPackageManager() {
                    return "which yum";
                }

                private String getPackageName(String javaVersion) {
                    return "java-" + javaVersion + ".0-openjdk-headless";
                }
            });
        }
    };

    /**
     * Connects to the given {@link Computer} via SSH and installs Java/Jenkins
     * agent if necessary.
     */
    @Override
    public void launch(SlaveComputer _computer, TaskListener listener) {
        Jenkins instance = Jenkins.getInstance();
        if (_computer instanceof Computer && instance != null) {
            Computer computer = (Computer) _computer;

            PrintStream logger = listener.getLogger();

            Date startDate = new Date();
            logger.println("Start time: " + getUtcDate(startDate));

            final Connection conn;
            Connection cleanupConn = null;
            boolean successful = false;
            Slave slave = computer.getNode();
            if (slave != null) {
                try {
                    conn = connectToSsh(computer, logger);

                    cleanupConn = conn;

                    if (conn != null) {
                        logger.println("Authenticating as " + computer.getRemoteAdmin());
                        if (!conn.authenticateWithPublicKey(computer.getRemoteAdmin(),
                                slave.getPrivateKey().toCharArray(), "")) {
                            logger.println("Authentication failed");
                            throw new Exception("Authentication failed");
                        }

                        final SCPClient scp = conn.createSCPClient();

                        if (!runInitScript(computer, logger, conn, scp)) {
                            return;
                        }

                        if (!installJava(logger, conn)) {
                            return;
                        }

                        logger.println("Copying slave.jar");
                        scp.put(instance.getJnlpJars("slave.jar").readFully(), "slave.jar", "/tmp");
                        String jvmOpts = Util.fixNull(slave.getJvmOpts());
                        String launchString = "java " + jvmOpts + " -jar /tmp/slave.jar";
                        logger.println("Launching slave agent: " + launchString);
                        final Session sess = conn.openSession();
                        sess.execCommand(launchString);
                        computer.setChannel(sess.getStdout(), sess.getStdin(), logger, new Channel.Listener() {
                            @Override
                            public void onClosed(Channel channel, IOException cause) {
                                sess.close();
                                conn.close();
                            }
                        });

                        successful = true;
                    }
                } catch (Exception e) {
                    LOGGER.log(Level.WARNING, e.getMessage(), e);
                    try {
                        instance.removeNode(slave);
                    } catch (Exception ee) {
                        ee.printStackTrace(logger);
                    }
                    e.printStackTrace(logger);
                } finally {
                    Date endDate = new Date();
                    logger.println("Done setting up at: " + getUtcDate(endDate));
                    logger.println(
                            "Done in " + TimeUnit2.MILLISECONDS.toSeconds(endDate.getTime() - startDate.getTime())
                                    + " seconds");
                    if (cleanupConn != null && !successful) {
                        cleanupConn.close();
                    }
                }
            }
        }
    }

    private boolean runInitScript(final Computer computer, final PrintStream logger, final Connection conn,
            final SCPClient scp) throws IOException, InterruptedException {
        Slave slave = computer.getNode();
        if (slave != null) {
            String initScript = Util.fixEmptyAndTrim(slave.getInitScript());

            if (initScript == null) {
                return true;
            }
            if (conn.exec("test -e ~/.hudson-run-init", logger) == 0) {
                return true;
            }

            logger.println("Executing init script");
            scp.put(initScript.getBytes("UTF-8"), "init.sh", "/tmp", "0700");
            Session session = conn.openSession();
            session.requestDumbPTY(); // so that the remote side bundles stdout and stderr
            session.execCommand(buildUpCommand(computer, "/tmp/init.sh"));

            session.getStdin().close(); // nothing to write here
            session.getStderr().close(); // we are not supposed to get anything from stderr
            IOUtils.copy(session.getStdout(), logger);

            int exitStatus = waitCompletion(session);
            if (exitStatus != 0) {
                logger.println("init script failed: exit code=" + exitStatus);
                return false;
            }
            session.close();

            // Needs a tty to run sudo.
            session = conn.openSession();
            session.requestDumbPTY(); // so that the remote side bundles stdout and stderr
            session.execCommand(buildUpCommand(computer, "touch ~/.hudson-run-init"));
            session.close();
        }
        return true;
    }

    private boolean installJava(final PrintStream logger, final Connection conn)
            throws IOException, InterruptedException {
        logger.println("Verifying that java exists");
        if (conn.exec("java -fullversion", logger) != 0) {
            logger.println("Try to install one of these Java-versions: " + VALID_VERSIONS);
            //TODO Web UI to let users install a custom java (or any other type of tool) package.
            logger.println("Trying to find a working package manager");
            for (JavaInstaller installer : INSTALLERS) {
                if (!installer.isUsable(conn, logger)) {
                    continue;
                }
                if (installer.installJava(conn, logger, VALID_VERSIONS) == 0) {
                    return true;
                }
            }

            logger.println("Java could not be installed using any of the supported package managers");
            return false;
        }
        return true;
    }

    private Connection connectToSsh(Computer computer, PrintStream logger) throws ScalewayException {
        ScalewayCloud scalewayCloud = computer.getCloud();
        Slave slave = computer.getNode();
        if (scalewayCloud == null || slave == null) {
            throw new ScalewayException(new NullPointerException());
        }

        ScalewayClient scaleway = ScalewayFactory.getScalewayClient(scalewayCloud.getAuthToken(),
                scalewayCloud.getOrgToken(), scalewayCloud.getScalewayClient().getRegion());
        final long timeout = TimeUnit2.MINUTES.toMillis(scalewayCloud.getTimeoutMinutes());
        final long startTime = System.currentTimeMillis();
        final int sleepTime = 10;

        long waitTime;

        while ((waitTime = System.currentTimeMillis() - startTime) < timeout) {

            // Hack to fetch this each time through the loop to get the latest information.
            ScalewayServer server = scaleway.getServer(slave.getServerId());

            if (isServerStarting(server)) {
                logger.println("Waiting for server to enter ACTIVE state. Sleeping " + sleepTime + " seconds.");
            } else {
                try {
                    final String host = getIpAddress(computer);

                    if (Strings.isNullOrEmpty(host) || "0.0.0.0".equals(host)) {
                        logger.println("No ip address yet, your host is most likely waiting for an ip address.");
                    } else {
                        int port = computer.getSshPort();

                        Connection conn = getServerConnection(host, port, logger);
                        if (conn != null) {
                            return conn;
                        }
                    }
                } catch (IOException e) {
                    // Ignore, we'll retry.
                }
                logger.println("Waiting for SSH to come up. Sleeping " + sleepTime + " seconds.");
            }

            sleep(sleepTime);
        }

        throw new RuntimeException(format(
                "Timed out after %d seconds of waiting for ssh to become available (max timeout configured is %s)",
                waitTime / 1000, timeout / 1000));
    }

    private static boolean isServerStarting(final ScalewayServer server) {

        switch (server.getState()) {
        case STARTING:
        case STOPPED:
            return true;

        case RUNNING:
            return false;

        default:
            throw new IllegalStateException("Server has unexpected status: " + server.getState());
        }
    }

    private Connection getServerConnection(String host, int port, PrintStream logger) throws IOException {
        logger.println("Connecting to " + host + " on port " + port + ". ");
        Connection conn = new Connection(host, port);
        try {
            conn.connect(null, 10 * 1000, 10 * 1000);
        } catch (SocketTimeoutException e) {
            return null;
        }
        logger.println("Connected via SSH.");
        return conn;
    }

    private static String getIpAddress(Computer computer) throws ScalewayException {
        ScalewayServer instance = computer.updateInstanceDescription();
        return instance.getPublicIp().getAddress();
    }

    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;
    }

    protected String buildUpCommand(Computer computer, String command) {
        if (!computer.getRemoteAdmin().equals("root")) {
            //            command = computer.getRootCommandPrefix() + " " + command;
        }
        return command;
    }

    private static void sleep(int seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            // Ignore
        }
    }

    private String getUtcDate(Date date) {
        SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
        utcFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        return utcFormat.format(date);
    }
}