57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
66 |
--------------------------------------------------------------------------------
/src/main/java/com/netflix/jenkins/plugins/DynaSlavePlugin.java:
--------------------------------------------------------------------------------
1 | package com.netflix.jenkins.plugins;
2 |
3 | import hudson.Plugin;
4 | import hudson.Util;
5 | import hudson.model.Descriptor.FormException;
6 | import hudson.model.Node;
7 | import hudson.security.ACL;
8 | import jenkins.model.Jenkins;
9 | import net.sf.json.JSONObject;
10 | import org.acegisecurity.context.SecurityContextHolder;
11 | import org.kohsuke.stapler.QueryParameter;
12 | import org.kohsuke.stapler.StaplerRequest;
13 | import org.kohsuke.stapler.StaplerResponse;
14 |
15 | import javax.servlet.ServletException;
16 | import java.io.IOException;
17 | import java.util.logging.Level;
18 | import java.util.logging.Logger;
19 |
20 | /**
21 | * Exposes an entry point to add a new slave.
22 | * Based upon the Jenkins Swarm Plugin
23 | */
24 | public class DynaSlavePlugin extends Plugin {
25 | private static final Logger LOG = Logger.getLogger(DynaSlavePlugin.class.getName());
26 |
27 | private String defaultPrefix = "dynaslave";
28 | private String defaultLabels = "";
29 | private String defaultRemoteSlaveUser = "jenkins";
30 | private String defaultBaseLauncherCommand = "/apps/jenkins/tools/start-remote-dynaslave";
31 | private String defaultIdleTerminationMinutes = "30";
32 |
33 | public String getDefaultPrefix() {
34 | return defaultPrefix;
35 | }
36 |
37 | public String getDefaultLabels() {
38 | return defaultLabels;
39 | }
40 |
41 | public String getDefaultBaseLauncherCommand() {
42 | return defaultBaseLauncherCommand;
43 | }
44 |
45 | public String getDefaultRemoteSlaveUser() {
46 | return defaultRemoteSlaveUser;
47 | }
48 |
49 | public String getDefaultIdleTerminationMinutes() {
50 | return defaultIdleTerminationMinutes;
51 | }
52 |
53 | /**
54 | * Adds a new slave.
55 | */
56 | public void doCreateSlave(StaplerRequest req, StaplerResponse rsp, @QueryParameter String name,
57 | @QueryParameter String description, @QueryParameter int executors,
58 | @QueryParameter String remoteFsRoot, @QueryParameter String labels,
59 | @QueryParameter String hostname)
60 | throws IOException, FormException {
61 | // bypass the regular security check
62 | SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM);
63 | try {
64 | final Jenkins jenkins = Jenkins.getInstance();
65 |
66 | if (defaultPrefix != null && !defaultPrefix.isEmpty()) {
67 | name = defaultPrefix + "-" + name;
68 | }
69 |
70 | if (description == null) {
71 | description = "";
72 | }
73 |
74 | labels = defaultLabels + " " + Util.fixNull(labels);
75 |
76 | DynaSlave slave = new DynaSlave(name, "Dynamic slave at " + hostname + ": " + description,
77 | remoteFsRoot, String.valueOf(executors), labels, hostname, defaultRemoteSlaveUser,
78 | defaultBaseLauncherCommand, defaultIdleTerminationMinutes);
79 |
80 | synchronized (jenkins) {
81 | Node n = jenkins.getNode(name);
82 | if (n != null) jenkins.removeNode(n);
83 | jenkins.addNode(slave);
84 | }
85 | } catch (FormException e) {
86 | LOG.log(Level.WARNING, "Unable to create dynaslave:", e);
87 |
88 | } finally {
89 | SecurityContextHolder.clearContext();
90 | }
91 | }
92 |
93 | @Override
94 | public void configure(StaplerRequest req, JSONObject formData)
95 | throws FormException, ServletException, IOException {
96 |
97 | defaultPrefix = formData.optString("defaultPrefix");
98 | defaultLabels = formData.optString("defaultLabels");
99 | defaultRemoteSlaveUser = formData.optString("defaultRemoteSlaveUser");
100 | defaultBaseLauncherCommand = formData.optString("defaultBaseLauncherCommand");
101 | defaultIdleTerminationMinutes = formData.optString("defaultIdleTerminationMinutes");
102 | save();
103 | }
104 |
105 | @Override
106 | public void start() throws IOException {
107 | load();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jenkins DynaSlave plugin
2 | ================
3 |
4 | ## Background
5 | ================
6 | In 2011, Netflix began looking at better ways to manage the Jenkins build infrastructure driving our continuous integration and deployment efforts. Part of that involved forklifting our slave build nodes to the cloud so we could easily add (and remove) capacity as needed. We took this on ahead of wide deployment of IAM and VPC within our organization which presented some unique challenges. In searching for an ideal plugin, we came across the Swarm plugin which uses a UDP registration mechanism that esentially allows slaves to announce their presence to Jenkins for registration. Using that as a foundation (sans UDP), we blended in concepts from other plugins and wound up with the DynaSlave in its current form.
7 |
8 | ## Overview
9 | ================
10 | The Jenkins DynaSlave plugin was written to allow Jenkins slave workers to register themselves.
11 |
12 | The plugin exposes an endpoint slaves can call with parameters that describe things about them like the labels they support, the number of executors they can handle, and what their name is.
13 |
14 | The plugin handles creating the slave object, firing off the launcher to bring up slave.jar on the node, and will handle cleanup of the node should it terminate unexpectedly.
15 |
16 | Consider a scenario whereby your slave nodes exist within an Amazon EC2 auto scaling group. Assuming you have a mechanism to inform the nodes what their autoscaling group name is, one can use that bit of information to specially label nodes in that group and tie jobs to that label, effectively creating a specialized slave cluster.
17 |
18 | Another possibility is team self-service. Consider a model whereby teams manage their own jobs and are also responsible for their own slave nodes. Rather than delegate permissions via Jenkins, one can give teams a script they will run from their slave at launch time that will kick off the registration process. Can the nodes run a JVM? They can likely become a slave with the DynaSlave plugin with little additional effort.
19 |
20 | ## Usage
21 | ================
22 |
23 | ### Defaults
24 | ================
25 | The plugin exposes five global defaults that will be applied to slaves that don't override those values (or that slaves inherit automatically regardless of what they provide).
26 | * Default Labels - this is a space-separated list of labels that all slaves registering via the plugin will inherit
27 | * Dynaslave Remote User - the user account passed to the launch script for remote access and activation of slave.jar on the remote end. Depending on one's approach, this may be unnecessary.
28 | * Base Launcher Command - The path to the command on the master that's used to trigger the launch of slave.jar on the remote end. Again, depending on approach, this may be a noop.
29 | * Default prefix - A special naming prefix that is prepended to all slaves registering via the plugin.
30 | * Idle Timeout Period - If a slave node loses connectivity or terminates abnormally, this period defines how long it will be allowed to remain in such a state before being cleaned up
31 |
32 | The base launcher command will be executed as .
33 |
34 | ### URL Parameters
35 | ===============
36 | The plugin registers http://jenkins_base_url/plugins/doCreateSlave as an entrypoint. doCreateSlave recognizes the following query parameters:
37 |
38 | * name (the name of the dynamic slave, always automatically prepended w/ the global prefix)
39 | * description (optional description of the slave node)
40 | * executors (integer representing the max number of simultaneous builds per slave)
41 | * remoteFsRoot (the base dir where the slave process lives)
42 | * labels (additional labels to apply to the base set of default labels)
43 | * hostname (the addressable hostname (or ip address) of the node)
44 |
45 | An example
46 | ```
47 | http://jenkinshost/plugin/dynaslave/createSlave?name=foobar&executors=2&remoteFsRoot=/apps/jenkins&description=foobar&labels=foo%20bar%20baz%20quux&hostname=foobarbaz.com
48 | ```
49 |
50 | This creates a slave named ds-foobar (ds- is added implicitly and is derived from the global default prefix) with two executors, remoteFsRoot in /apps/jenkins, a simple description "foobar," adds labels (foo, bar, baz, and quux), having hostname foobarbaz.com. Once polled, Jenkins creates internal structures registering the node and fires the launcher command.
51 |
52 | ### Caveats
53 | ===============
54 | * The plugin does not provide any application-level security around registration. Future enhancements may allow for optional mechanisms for tightening up registration.
55 | * If one deletes the slave from Jenkins, the node lingers externally. You will want to have your slave nodes periodically phone home to ensure they're still registered, and if not, re-register.
56 |
57 | ### Near Term Enhancements
58 | ===============
59 | * Use of the built-in Cloud abstraction to assist with grouping and isolation of nodes (Netflix currently handles this purely through naming conventions and slave labels)
60 | * Use of that same abstraction to help feed external systems information on the build queue pressure and influence slave pool size (Netflix is currently using some system groovy scripts to this effect)
61 |
--------------------------------------------------------------------------------