Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #26 from stephenc/jenkins-26558
[FIXED JENKINS-26558] Clients should provide a unique ID to be used for ...
  • Loading branch information
mindjiver committed Apr 27, 2015
2 parents bef8ccb + 96a1a84 commit 5f622da
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 37 deletions.
2 changes: 1 addition & 1 deletion client/src/main/java/hudson/plugins/swarm/Client.java
Expand Up @@ -77,7 +77,7 @@ public void run() throws InterruptedException {
swarmClient.verifyThatUrlIsHudson(target);
}

System.out.println("Attempting to connect to " + target.url + " " + target.secret);
System.out.println("Attempting to connect to " + target.url + " " + target.secret + " with ID " + swarmClient.getHash());

// create a new swarm slave
swarmClient.createSwarmSlave(target);
Expand Down
124 changes: 100 additions & 24 deletions client/src/main/java/hudson/plugins/swarm/SwarmClient.java
Expand Up @@ -2,58 +2,77 @@

import hudson.remoting.Launcher;
import hudson.remoting.jnlp.Main;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Random;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

public class SwarmClient {

private final Options options;

private final String hash;

private String name;

public SwarmClient(Options options) {
this.options = options;
this.hash = hash(options.remoteFsRoot);
this.name = options.name;
}

public String getHash() {
return hash;
}

public Candidate discoverFromBroadcast() throws IOException,
Expand Down Expand Up @@ -195,7 +214,7 @@ protected void connect(Candidate target) throws InterruptedException {
try {
Launcher launcher = new Launcher();

launcher.slaveJnlpURL = new URL(target.url + "computer/" + options.name
launcher.slaveJnlpURL = new URL(target.url + "computer/" + name
+ "/slave-agent.jnlp");

if (options.username != null && options.password != null) {
Expand Down Expand Up @@ -296,7 +315,9 @@ protected void createSwarmSlave(Candidate target) throws IOException, Interrupte
+ param("labels", labelStr)
+ param("toolLocations", toolLocationsStr)
+ "&secret=" + target.secret
+ param("mode", options.mode.toUpperCase()));
+ param("mode", options.mode.toUpperCase(Locale.ENGLISH))
+ param("hash", hash)
);

post.setDoAuthentication(true);

Expand All @@ -307,9 +328,29 @@ protected void createSwarmSlave(Candidate target) throws IOException, Interrupte

int responseCode = client.executeMethod(post);
if (responseCode != 200) {
throw new RetryException(
"Failed to create a slave on Jenkins CODE: " + responseCode);
throw new RetryException(String.format("Failed to create a slave on Jenkins CODE: %s%n%s",responseCode,
post.getResponseBodyAsString()) );
}
Properties props = new Properties();
InputStream stream = post.getResponseBodyAsStream();
if (stream != null) {
try {
props.load(stream);
} finally {
stream.close();
}
}
String name = props.getProperty("name");
if (name == null) {
this.name = options.name;
return;
}
name = name.trim();
if (name.isEmpty()) {
this.name = options.name;
return;
}
this.name = name;
}

private String encode(String value) throws UnsupportedEncodingException {
Expand Down Expand Up @@ -380,6 +421,41 @@ private String printable(InetAddress ia){
}
}

/**
* Returns a hash that should be consistent for any individual swarm client (as long as it has a persistent IP)
* and should be unique to that client.
*
* @param remoteFsRoot the file system root should be part of the hash (to support multiple swarm clients from
* the same machine)
* @return our best effort at a consistent hash
*/
public static String hash(File remoteFsRoot) {
StringBuilder buf = new StringBuilder();
try {
buf.append(remoteFsRoot.getCanonicalPath()).append('\n');
} catch (IOException e) {
buf.append(remoteFsRoot.getAbsolutePath()).append('\n');
}
try {
for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) {
for (InetAddress ia : Collections.list(ni.getInetAddresses())) {
if (ia instanceof Inet4Address) {
buf.append(ia.getHostAddress()).append('\n');
} else if (ia instanceof Inet6Address) {
buf.append(ia.getHostAddress()).append('\n');
}
}
byte[] hardwareAddress = ni.getHardwareAddress();
if (hardwareAddress != null) {
buf.append(Arrays.toString(hardwareAddress));
}
}
} catch (SocketException e) {
// oh well we tried
}
return DigestUtils.md5Hex(buf.toString()).substring(0, 8);
}

private static class DefaultTrustManager implements X509TrustManager {

public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
Expand Down
64 changes: 52 additions & 12 deletions plugin/src/main/java/hudson/plugins/swarm/PluginImpl.java
@@ -1,28 +1,33 @@
package hudson.plugins.swarm;

import static javax.servlet.http.HttpServletResponse.*;
import com.google.common.collect.Lists;
import hudson.Plugin;
import hudson.Util;
import hudson.model.Computer;
import hudson.model.Descriptor.FormException;
import hudson.model.Node;
import hudson.slaves.SlaveComputer;
import hudson.tools.ToolDescriptor;
import hudson.tools.ToolInstallation;
import hudson.tools.ToolLocationNodeProperty;
import hudson.tools.ToolLocationNodeProperty.ToolLocation;

import java.io.IOException;
import java.io.Writer;
import java.util.List;

import jenkins.model.Jenkins;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import com.google.common.collect.Lists;
import javax.servlet.ServletOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Properties;

import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
import static javax.servlet.http.HttpServletResponse.SC_EXPECTATION_FAILED;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;

/**
* Exposes an entry point to add a new swarm slave.
Expand All @@ -36,7 +41,7 @@ public class PluginImpl extends Plugin {
*/
public void doCreateSlave(StaplerRequest req, StaplerResponse rsp, @QueryParameter String name, @QueryParameter String description, @QueryParameter int executors,
@QueryParameter String remoteFsRoot, @QueryParameter String labels, @QueryParameter String secret, @QueryParameter Node.Mode mode,
@QueryParameter String toolLocations) throws IOException {
@QueryParameter String toolLocations, @QueryParameter(fixEmpty = true) String hash) throws IOException {

if (!getSwarmSecret().equals(secret)) {
rsp.setStatus(SC_FORBIDDEN);
Expand All @@ -54,9 +59,34 @@ public void doCreateSlave(StaplerRequest req, StaplerResponse rsp, @QueryParamet
nodeProperties = Lists.newArrayList(new ToolLocationNodeProperty(parsedToolLocations));
}

// try to make the name unique. Swarm clients are often replicated VMs, and they may have the same name.
if (jenkins.getNode(name) != null) {
name = name + '-' + req.getRemoteAddr();
if (hash == null && jenkins.getNode(name) != null) {
// this is a legacy client, they won't be able to pick up the new name, so throw them away
// perhaps they can find another master to connect to
rsp.setStatus(SC_CONFLICT);
rsp.setContentType("text/plain; UTF-8");
rsp.getWriter().printf(
"A slave called '%s' already exists and legacy clients do not support name disambiguation%n",
name);
return;
}
if (hash != null) {
// try to make the name unique. Swarm clients are often replicated VMs, and they may have the same name.
name = name + '-' + hash;
}
// check for existing connections
{
Node n = jenkins.getNode(name);
if (n != null) {
Computer c = n.toComputer();
if (c != null && c.isOnline()) {
// this is an existing connection, we'll only cause issues if we trample over an online connection

rsp.setStatus(SC_CONFLICT);
rsp.setContentType("text/plain; UTF-8");
rsp.getWriter().printf("A slave called '%s' is already created and on-line%n", name);
return;
}
}
}

SwarmSlave slave = new SwarmSlave(name, "Swarm slave from " + req.getRemoteHost() + " : " + description,
Expand All @@ -70,6 +100,16 @@ public void doCreateSlave(StaplerRequest req, StaplerResponse rsp, @QueryParamet
}
jenkins.addNode(slave);
}
rsp.setContentType("text/plain; charset=iso-8859-1");
Properties props = new Properties();
props.put("name", name);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
props.store(bos, "");
byte[] response = bos.toByteArray();
rsp.setContentLength(response.length);
ServletOutputStream outputStream = rsp.getOutputStream();
outputStream.write(response);
outputStream.flush();
} catch (FormException e) {
e.printStackTrace();
}
Expand Down

0 comments on commit 5f622da

Please sign in to comment.