
hudson.plugins.sshslaves.SSHLauncher Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ssh-slaves Show documentation
Show all versions of ssh-slaves Show documentation
Plugin allows you to manage slaves running on *nix machines over SSH.
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