package hudson.plugins.swarm; import hudson.remoting.Launcher; import hudson.remoting.jnlp.Main; 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 org.kohsuke.args4j.Option; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.CmdLineException; 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.net.DatagramPacket; import java.net.DatagramSocket; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLEncoder; import java.util.List; import java.util.ArrayList; import java.util.Random; /** * Swarm client. * * <p> * Discovers nearby Hudson via UDP broadcast, and pick eligible one randomly and joins it. * * @author Kohsuke Kawaguchi */ public class Client { /** * Used to discover the server. */ protected final DatagramSocket socket; /** * The Hudson that we are trying to connect to. */ protected Candidate target; @Option(name="-name",usage="Name of the slave") public String name; @Option(name="-description",usage="Description to be put on the slave") public String description; @Option(name="-labels",usage="Whitespace-separated list of labels to be assigned for this slave. Multiple options are allowed.") public List<String> labels = new ArrayList<String>(); @Option(name="-fsroot",usage="Directory where Hudson places files") public File remoteFsRoot = new File("."); @Option(name="-executors",usage="Number of executors") public int executors = Runtime.getRuntime().availableProcessors(); @Option(name="-master",usage="Host name or IP address of the master. If this option is specified, auto-discovery will be skipped") public String master; @Option(name="-help",aliases="--help",usage="Show the help screen") public boolean help; public static void main(String... args) throws InterruptedException, IOException { Client client = new Client(); CmdLineParser p = new CmdLineParser(client); try { p.parseArgument(args); } catch (CmdLineException e) { System.out.println(e.getMessage()); p.printUsage(System.out); System.exit(-1); } if(client.help) { p.printUsage(System.out); System.exit(0); } client.run(); } public Client() throws IOException { socket = new DatagramSocket(); socket.setBroadcast(true); name = InetAddress.getLocalHost().getCanonicalHostName(); } class Candidate { final String url; final String secret; Candidate(String url, String secret) { this.url = url; this.secret = secret; } } /** * Finds a Hudson master that supports swarming, and join it. * * This method never returns. */ public void run() throws InterruptedException { System.out.println("Discovering Hudson master"); // wait until we get the ACK back while(true) { try { List<Candidate> candidates = new ArrayList<Candidate>(); for (DatagramPacket recv : discover()) { String responseXml = new String(recv.getData(), 0, recv.getLength()); Document xml; try { xml = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( new ByteArrayInputStream(recv.getData(), 0, recv.getLength())); } catch (SAXException e) { System.out.println("Invalid response XML from "+recv.getAddress()+": "+ responseXml); continue; } String swarm = getChildElementString(xml.getDocumentElement(), "swarm"); if(swarm==null) { System.out.println(recv.getAddress()+" doesn't support swarm"); continue; } String url = getChildElementString(xml.getDocumentElement(), "url"); if(url==null) { System.out.println(recv.getAddress()+" doesn't have the configuration set yet. Please go to the sytem configuration page of this Hudson and submit it: "+ responseXml); continue; } candidates.add(new Candidate(url,swarm)); } if(candidates.size()==0) throw new RetryException("No nearby Hudson supports swarming"); System.out.println("Found "+candidates.size()+" eligible Hudson."); // randomly pick up the Hudson to connect to target = candidates.get(new Random().nextInt(candidates.size())); verifyThatUrlIsHudson(); // create a new swarm slave createSwarmSlave(); connect(); } catch (IOException e) { e.printStackTrace(); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (RetryException e) { System.out.println(e.getMessage()); if(e.getCause()!=null) e.getCause().printStackTrace(); } // retry System.out.println("Retrying in 10 seconds"); Thread.sleep(10*1000); } } /** * Discovers Hudson running nearby. * * To give every nearby Hudson a fair chance, wait for some time until we hear all the responses. */ protected List<DatagramPacket> discover() throws IOException, InterruptedException, RetryException { sendBroadcast(); List<DatagramPacket> responses = new ArrayList<DatagramPacket>(); // wait for 5 secs to gather up all the replies long limit = System.currentTimeMillis()+5*1000; while(true) { try { socket.setSoTimeout(Math.max(1,(int)(limit-System.currentTimeMillis()))); DatagramPacket recv = new DatagramPacket(new byte[2048], 2048); socket.receive(recv); responses.add(recv); } catch (SocketTimeoutException e) { // timed out if(responses.isEmpty()) { if (master!=null) throw new RetryException("Failed to receive a reply from "+master); else throw new RetryException("Failed to receive a reply to broadcast."); } return responses; } } } protected void sendBroadcast() throws IOException { DatagramPacket packet = new DatagramPacket(new byte[0], 0); packet.setAddress(InetAddress.getByName(master!=null ? master : "255.255.255.255")); packet.setPort(Integer.getInteger("hudson.udp",33848)); socket.send(packet); } protected void connect() throws InterruptedException { try { Launcher launcher = new Launcher(); launcher.slaveJnlpURL = new URL(target.url+"/computer/"+name+"/slave-agent.jnlp"); List<String> jnlpArgs = launcher.parseJnlpArguments(); jnlpArgs.add("-noreconnect"); Main.main(jnlpArgs.toArray(new String[jnlpArgs.size()])); } catch (Exception e) { System.out.println("Failed to establish JNLP connection to "+target.url); Thread.sleep(10*1000); } } protected void createSwarmSlave() throws IOException, InterruptedException, RetryException { StringBuilder labelStr = new StringBuilder(); for (String l : labels) { if(labelStr.length()>0) labelStr.append(' '); labelStr.append(l); } HttpURLConnection con = (HttpURLConnection)new URL(target.url + "/plugin/swarm/createSlave?name=" + name + "&executors=" + executors + param("remoteFsRoot",remoteFsRoot.getAbsolutePath()) + param("description",description)+ param("labels", labelStr.toString())+ "&secret=" + target.secret).openConnection(); if(con.getResponseCode()!=200) { copy(con.getErrorStream(),System.out); throw new RetryException("Failed to create a slave on Hudson: "+con.getResponseCode()+" "+con.getResponseMessage()); } } private String param(String name, String value) throws UnsupportedEncodingException { if(value==null) return ""; return "&"+name+"="+ URLEncoder.encode(value,"UTF-8"); } protected void verifyThatUrlIsHudson() throws InterruptedException, RetryException { try { System.out.println("Connecting to "+target.url); HttpURLConnection con = (HttpURLConnection)new URL(target.url).openConnection(); con.connect(); String v = con.getHeaderField("X-Hudson"); if(v==null) throw new RetryException("This URL doesn't look like Hudson."); } catch (IOException e) { throw new RetryException("Failed to connect to "+target.url,e); } } private static void copy(InputStream in, OutputStream out) throws IOException { byte[] buf = new byte[8192]; int len; while ((len = in.read(buf)) >= 0) out.write(buf, 0, len); } private static String getChildElementString(Element parent, String tagName) { for (Node n=parent.getFirstChild(); n!=null; n=n.getNextSibling()) { if (n instanceof Element) { Element e = (Element) n; if(e.getTagName().equals(tagName)) { StringBuilder buf = new StringBuilder(); for (n=e.getFirstChild(); n!=null; n=n.getNextSibling()) { if(n instanceof Text) buf.append(n.getTextContent()); } return buf.toString(); } } } return null; } }