From 4d162b8553d6351d9b5799855426a679f94ca420 Mon Sep 17 00:00:00 2001 From: Christian McHugh Date: Sun, 7 Sep 2014 09:14:41 -0500 Subject: [PATCH] Add ability to block waiting for all minions to return --- pom.xml | 2 +- src/main/java/com/waytta/SaltAPIBuilder.java | 463 +++++++++++------- src/main/java/com/waytta/Utils.java | 67 ++- .../com/waytta/SaltAPIBuilder/config.jelly | 5 + .../com/waytta/SaltAPIBuilder/global.jelly | 10 + .../SaltAPIBuilder/help-blockbuild.html | 4 + 6 files changed, 350 insertions(+), 201 deletions(-) create mode 100644 src/main/resources/com/waytta/SaltAPIBuilder/global.jelly create mode 100644 src/main/resources/com/waytta/SaltAPIBuilder/help-blockbuild.html diff --git a/pom.xml b/pom.xml index 183b7c9..a88cc0b 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.waytta saltapi - 1.0 + 1.1 hpi diff --git a/src/main/java/com/waytta/SaltAPIBuilder.java b/src/main/java/com/waytta/SaltAPIBuilder.java index dd47301..d882f37 100644 --- a/src/main/java/com/waytta/SaltAPIBuilder.java +++ b/src/main/java/com/waytta/SaltAPIBuilder.java @@ -1,4 +1,5 @@ package com.waytta; + import hudson.Launcher; import hudson.Extension; import hudson.util.FormValidation; @@ -46,220 +47,316 @@ public class SaltAPIBuilder extends Builder { private final String targettype; private final String function; private final String arguments; + private final Boolean blockbuild; // Fields in config.jelly must match the parameter names in the "DataBoundConstructor" @DataBoundConstructor - public SaltAPIBuilder(String servername, String username, String userpass, String authtype, String target, String targettype, String function, String arguments) { - this.servername = servername; - this.username = username; - this.userpass = userpass; - this.authtype = authtype; - this.target = target; - this.targettype = targettype; - this.function = function; - this.arguments = arguments; - } + public SaltAPIBuilder(String servername, String username, String userpass, String authtype, String target, String targettype, String function, String arguments, Boolean blockbuild) { + this.servername = servername; + this.username = username; + this.userpass = userpass; + this.authtype = authtype; + this.target = target; + this.targettype = targettype; + this.function = function; + this.arguments = arguments; + this.blockbuild = blockbuild; + } /* * We'll use this from the config.jelly. */ public String getServername() { - return servername; + return servername; } public String getUsername() { - return username; + return username; } public String getUserpass() { - return userpass; + return userpass; } public String getAuthtype() { - return authtype; + return authtype; } public String getTarget() { - return target; + return target; } public String getTargettype() { - return this.targettype; + return this.targettype; } public String getFunction() { - return function; + return function; } public String getArguments() { - return arguments; + return arguments; + } + public Boolean getBlockbuild() { + return blockbuild; } @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { - // This is where you 'build' the project. - String mytarget = target; - String myfunction = function; - String myarguments = arguments; - //listener.getLogger().println("Salt Arguments before: "+myarguments); - mytarget = Utils.paramorize(build, listener, target); - myfunction = Utils.paramorize(build, listener, function); - myarguments = Utils.paramorize(build, listener, arguments); - //listener.getLogger().println("Salt Arguments after: "+myarguments); - - //Setup connection for auth - String auth = "username="+username+"&password="+userpass+"&eauth="+authtype; - String httpResponse = new String(); - String token = new String(); - httpResponse = Utils.sendJSON(servername, auth, null); - if (httpResponse.contains("java.io.IOException") || httpResponse.contains("java.net.SocketTimeoutException")) { - listener.getLogger().println("Error: "+httpResponse); - return false; - } - try { - JSONObject authresp = (JSONObject) JSONSerializer.toJSON(httpResponse); - JSONArray returnArray = authresp.getJSONArray("return"); - for (Object o : returnArray ) { - JSONObject line = (JSONObject) o; - token = line.getString("token"); - } - } catch (Exception e) { - listener.getLogger().println("JSON Error: "+e+"\n\n"+httpResponse); - return false; - } - - //If we got this far, auth must have been pretty good and we've got a token - String saltFunc = new String(); - if (myarguments.length() > 0){ - saltFunc = "client=local&tgt="+mytarget+"&expr_form="+targettype+"&fun="+myfunction+"&arg="+myarguments; - } else { - saltFunc = "client=local&tgt="+mytarget+"&expr_form="+targettype+"&fun="+myfunction; - } - httpResponse = Utils.sendJSON(servername, saltFunc, token); - if (httpResponse.contains("java.io.IOException") || - httpResponse.contains("java.net.SocketTimeoutException") || - httpResponse.contains("TypeError") - ) { - listener.getLogger().println("Error: "+myfunction+" "+myarguments+" to "+servername+" for "+mytarget+":\n"+httpResponse); - return false; - } - try { - JSONObject jsonResp = (JSONObject) JSONSerializer.toJSON(httpResponse); - //Print out success - listener.getLogger().println("Response on "+myfunction+" "+myarguments+" for "+mytarget+":\n"+jsonResp.toString(2)); - } catch (Exception e) { - listener.getLogger().println("Problem: "+myfunction+" "+myarguments+" to "+servername+" for "+mytarget+":\n"+e+"\n\n"+httpResponse); - return false; - } - return true; - } + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { + // This is where you 'build' the project. + String mytarget = target; + String myfunction = function; + String myarguments = arguments; + //listener.getLogger().println("Salt Arguments before: "+myarguments); + mytarget = Utils.paramorize(build, listener, target); + myfunction = Utils.paramorize(build, listener, function); + myarguments = Utils.paramorize(build, listener, arguments); + //listener.getLogger().println("Salt Arguments after: "+myarguments); + + //Setup connection for auth + String token = new String(); + String auth = "username="+username+"&password="+userpass+"&eauth="+authtype; + JSONObject httpResponse = new JSONObject(); + + //Get an auth token + token = Utils.getToken(servername, auth); + if (token.contains("Error")) { + listener.getLogger().println(token); + return false; + } + + //If we got this far, auth must have been pretty good and we've got a token + String saltFunc = new String(); + if (myarguments.length() > 0){ + saltFunc = "client=local&tgt="+mytarget+"&expr_form="+targettype+"&fun="+myfunction+"&arg="+myarguments; + } else { + saltFunc = "client=local&tgt="+mytarget+"&expr_form="+targettype+"&fun="+myfunction; + } + + Boolean myBlockBuild = blockbuild; + if (myBlockBuild == null) { + //Set a sane default if uninitialized + myBlockBuild = false; + } + + //blocking request + if (myBlockBuild) { + String jid = new String(); + //Send request to /minion url. This will give back a jid which we will need to poll and lookup for completion + httpResponse = Utils.getJSON(servername+"/minions", saltFunc, token); + try { + JSONArray returnArray = httpResponse.getJSONArray("return"); + for (Object o : returnArray ) { + JSONObject line = (JSONObject) o; + jid = line.getString("jid"); + } + //Print out success + listener.getLogger().println("Running jid: " + jid); + } catch (Exception e) { + listener.getLogger().println("Problem: "+myfunction+" "+myarguments+" to "+servername+" for "+mytarget+":\n"+e+"\n\n"+httpResponse.toString(2)); + return false; + } + + //Request successfully sent. Now use jid to check if job complete + int numMinions = 0; + int numMinionsDone = 0; + JSONArray returnArray = new JSONArray(); + httpResponse = Utils.getJSON(servername+"/jobs/"+jid, null, token); + try { + //info array will tell us how many minions were targeted + returnArray = httpResponse.getJSONArray("info"); + for (Object o : returnArray ) { + JSONObject line = (JSONObject) o; + JSONArray minionsArray = line.getJSONArray("Minions"); + //Check the info[Minions[]] array to see how many nodes we expect to hear back from + numMinions = minionsArray.size(); + listener.getLogger().println("Waiting for " + numMinions + " minions"); + } + returnArray = httpResponse.getJSONArray("return"); + //Check the return[] array to see how many minions have responded + if (!returnArray.getJSONObject(0).names().isEmpty()) { + numMinionsDone = returnArray.getJSONObject(0).names().size(); + } else { + numMinionsDone = 0; + } + listener.getLogger().println(numMinionsDone + " minions are done"); + } catch (Exception e) { + listener.getLogger().println("Problem: "+myfunction+" "+myarguments+" to "+servername+" for "+mytarget+":\n"+e+"\n\n"+httpResponse.toString(2)); + return false; + } + + //Figure out how often we should poll from configuration screen + int waitTime = getDescriptor().getPollTime(); + if (waitTime < 3) { + //Set a sane default on first install + waitTime = 10; + } + + //Now that we know how many minions have responded, and how many we are waiting on. Let's see more have finished + if (numMinionsDone < numMinions) { + //Don't print annying messages unless we really are waiting for more minions to return + listener.getLogger().println("Will check status every "+String.valueOf(waitTime)+" seconds..."); + } + while (numMinionsDone < numMinions) { + try { + Thread.sleep(waitTime*1000); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + //Allow user to cancel job in jenkins interface + listener.getLogger().println("Cancelling job"); + return false; + } + httpResponse = Utils.getJSON(servername+"/jobs/"+jid, null, token); + try { + returnArray = httpResponse.getJSONArray("return"); + numMinionsDone = returnArray.getJSONObject(0).names().size(); + } catch (Exception e) { + listener.getLogger().println("Problem: "+myfunction+" "+myarguments+" for "+mytarget+":\n"+e+"\n\n"+httpResponse.toString(2).split("\\\\n")[0]); + return false; + } + } + //Loop is done. We have heard back from everybody. Good work team! + listener.getLogger().println("Response on "+myfunction+" "+myarguments+" for "+mytarget+":\n"+returnArray.toString(2)); + } else { + //Just send a salt request. Don't wait for reply + httpResponse = Utils.getJSON(servername, saltFunc, token); + try { + if (httpResponse.getJSONArray("return").isArray()) { + //Print out success + listener.getLogger().println("Response on "+myfunction+" "+myarguments+" for "+mytarget+":\n"+httpResponse.toString(2)); + } + } catch (Exception e) { + listener.getLogger().println("Problem with "+myfunction+" "+myarguments+" for "+mytarget+":\n"+e+"\n\n"+httpResponse.toString(2).split("\\\\n")[0]); + return false; + } + } + //No fail condition reached. Must be good. + return true; + } // Overridden for better type safety. // If your plugin doesn't really define any property on Descriptor, // you don't have to do this. @Override - public DescriptorImpl getDescriptor() { - return (DescriptorImpl)super.getDescriptor(); - } + public DescriptorImpl getDescriptor() { + return (DescriptorImpl)super.getDescriptor(); + } - /** - * Descriptor for {@link SaltAPIBuilder}. Used as a singleton. - * The class is marked as public so that it can be accessed from views. - */ @Extension // This indicates to Jenkins that this is an implementation of an extension point. - public static final class DescriptorImpl extends BuildStepDescriptor { - - public FormValidation doTestConnection(@QueryParameter("servername") final String servername, - @QueryParameter("username") final String username, - @QueryParameter("userpass") final String userpass, - @QueryParameter("authtype") final String authtype) - throws IOException, ServletException { - String httpResponse = new String(); - String auth = "username="+username+"&password="+userpass+"&eauth="+authtype; - try { - httpResponse = Utils.sendJSON(servername, auth, null); - if (httpResponse.contains("java.io.IOException") ) { - return FormValidation.error("Connection error: "+httpResponse); - } else if ( httpResponse.contains("java.net.SocketTimeoutException")) { - return FormValidation.error("Connection error: "+servername+" timed out"); - } - JSONObject authresp = (JSONObject) JSONSerializer.toJSON(httpResponse); - JSONArray returnArray = authresp.getJSONArray("return"); - //print response from salt api - String token = new String(); - for (Object o : returnArray ) { - JSONObject line = (JSONObject) o; - token = line.getString("token"); - } - return FormValidation.ok("Success"); - } catch (Exception e) { - return FormValidation.error("Client error : "+e.getMessage()+" "+httpResponse); - } - } - - - - /** - * Performs on-the-fly validation of the form fields - * - * @param value - * This parameter receives the value that the user has typed. - * @return - * Indicates the outcome of the validation. This is sent to the browser. - */ - public FormValidation doCheckServername(@QueryParameter String value) - throws IOException, ServletException { - if (value.length() == 0) - return FormValidation.error("Please specify a name"); - if (value.length() < 10) - return FormValidation.warning("Isn't the name too short?"); - if (!value.contains("https://") && !value.contains("http://")) - return FormValidation.warning("Missing protocol: Servername should be in the format https://host.domain:8000"); - if (!value.substring(7).contains(":")) - return FormValidation.warning("Missing port: Servername should be in the format https://host.domain:8000"); - return FormValidation.ok(); - } - - public FormValidation doCheckUsername(@QueryParameter String value) - throws IOException, ServletException { - if (value.length() == 0) - return FormValidation.error("Please specify a name"); - if (value.length() < 3) - return FormValidation.warning("Isn't the name too short?"); - return FormValidation.ok(); - } - - public FormValidation doCheckUserpass(@QueryParameter String value) - throws IOException, ServletException { - if (value.length() == 0) - return FormValidation.error("Please specify a password"); - if (value.length() < 3) - return FormValidation.warning("Isn't it too short?"); - return FormValidation.ok(); - } - - public FormValidation doCheckTarget(@QueryParameter String value) - throws IOException, ServletException { - if (value.length() == 0) - return FormValidation.error("Please specify a salt target"); - if (value.length() < 3) - return FormValidation.warning("Isn't it too short?"); - return FormValidation.ok(); - } - - public FormValidation doCheckFunction(@QueryParameter String value) - throws IOException, ServletException { - if (value.length() == 0) - return FormValidation.error("Please specify a salt function"); - if (value.length() < 3) - return FormValidation.warning("Isn't it too short?"); - return FormValidation.ok(); - } - - public boolean isApplicable(Class aClass) { - // Indicates that this builder can be used with all kinds of project types - return true; - } - - /** - * This human readable name is used in the configuration screen. - */ - public String getDisplayName() { - return "Send a message to Salt API"; - } - } + public static final class DescriptorImpl extends BuildStepDescriptor { + + private int pollTime; + + public DescriptorImpl() { + load(); + } + + @Override + public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { + try { + //Test that value entered in config is an integer + pollTime = formData.getInt("pollTime"); + } catch (Exception e) { + //Fall back to default + pollTime = 10; + } + save(); + return super.configure(req,formData); + } + + public int getPollTime() { + return pollTime; + } + + public FormValidation doTestConnection(@QueryParameter("servername") final String servername, + @QueryParameter("username") final String username, + @QueryParameter("userpass") final String userpass, + @QueryParameter("authtype") final String authtype) + throws IOException, ServletException { + JSONObject httpResponse = new JSONObject(); + String auth = "username="+username+"&password="+userpass+"&eauth="+authtype; + String token = Utils.getToken(servername, auth); + if (token.contains("Error")) { + return FormValidation.error("Client error : "+token); + } else { + return FormValidation.ok("Success"); + } + } + + + /** + * Performs on-the-fly validation of the form fields + * + * @param value + * This parameter receives the value that the user has typed. + * @return + * Indicates the outcome of the validation. This is sent to the browser. + */ + public FormValidation doCheckServername(@QueryParameter String value) + throws IOException, ServletException { + if (value.length() == 0) + return FormValidation.error("Please specify a name"); + if (value.length() < 10) + return FormValidation.warning("Isn't the name too short?"); + if (!value.contains("https://") && !value.contains("http://")) + return FormValidation.warning("Missing protocol: Servername should be in the format https://host.domain:8000"); + if (!value.substring(7).contains(":")) + return FormValidation.warning("Missing port: Servername should be in the format https://host.domain:8000"); + return FormValidation.ok(); + } + + public FormValidation doCheckUsername(@QueryParameter String value) + throws IOException, ServletException { + if (value.length() == 0) + return FormValidation.error("Please specify a name"); + if (value.length() < 3) + return FormValidation.warning("Isn't the name too short?"); + return FormValidation.ok(); + } + + public FormValidation doCheckUserpass(@QueryParameter String value) + throws IOException, ServletException { + if (value.length() == 0) + return FormValidation.error("Please specify a password"); + if (value.length() < 3) + return FormValidation.warning("Isn't it too short?"); + return FormValidation.ok(); + } + + public FormValidation doCheckTarget(@QueryParameter String value) + throws IOException, ServletException { + if (value.length() == 0) + return FormValidation.error("Please specify a salt target"); + if (value.length() < 3) + return FormValidation.warning("Isn't it too short?"); + return FormValidation.ok(); + } + + public FormValidation doCheckFunction(@QueryParameter String value) + throws IOException, ServletException { + if (value.length() == 0) + return FormValidation.error("Please specify a salt function"); + if (value.length() < 3) + return FormValidation.warning("Isn't it too short?"); + return FormValidation.ok(); + } + + public FormValidation doCheckPollTime(@QueryParameter String value) + throws IOException, ServletException { + try { + Integer.parseInt(value); + } catch(NumberFormatException e) { + return FormValidation.error("Specify a number larger than 3"); + } + if (Integer.parseInt(value) < 3) + return FormValidation.warning("Specify a number larger than 3"); + return FormValidation.ok(); + } + + public boolean isApplicable(Class aClass) { + // Indicates that this builder can be used with all kinds of project types + return true; + } + + /** + * This human readable name is used in the configuration screen. + */ + public String getDisplayName() { + return "Send a message to Salt API"; + } + } } diff --git a/src/main/java/com/waytta/Utils.java b/src/main/java/com/waytta/Utils.java index 39aec04..591f306 100644 --- a/src/main/java/com/waytta/Utils.java +++ b/src/main/java/com/waytta/Utils.java @@ -7,36 +7,40 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.io.*; +import net.sf.json.JSONObject; +import net.sf.json.JSONArray; +import net.sf.json.JSONSerializer; public class Utils { //Thinger to connect to saltmaster over rest interface - public static String sendJSON(String targetURL, String urlParams, String auth) { + public static JSONObject getJSON(String targetURL, String urlParams, String auth) { HttpURLConnection connection = null; String serverUrl = new String(); - if (auth != null && !auth.isEmpty()){ - serverUrl = targetURL; - } else { - serverUrl = targetURL+"/login"; - } + JSONObject responseJSON = new JSONObject(); + try { //Create connection - URL url = new URL(serverUrl); + URL url = new URL(targetURL); connection = (HttpURLConnection)url.openConnection(); - connection.setRequestMethod("POST"); connection.setRequestProperty("Accept", "application/json"); connection.setUseCaches (false); - connection.setDoInput(true); - connection.setDoOutput(true); + if (urlParams != null && !urlParams.isEmpty()) { + //We have stuff to send, so do an HTTP POST not GET + connection.setDoOutput(true); + } connection.setConnectTimeout(5000); //set timeout to 5 seconds if (auth != null && !auth.isEmpty()){ connection.setRequestProperty("X-Auth-Token", auth); } //Send request - DataOutputStream wr = new DataOutputStream ( connection.getOutputStream()); - wr.writeBytes(urlParams); - wr.flush (); - wr.close (); + if (urlParams != null && !urlParams.isEmpty()) { + //only necessary if we have stuff to send + DataOutputStream wr = new DataOutputStream ( connection.getOutputStream()); + wr.writeBytes(urlParams); + wr.flush (); + wr.close (); + } //Get Response InputStream is = connection.getInputStream(); @@ -48,12 +52,24 @@ public static String sendJSON(String targetURL, String urlParams, String auth) { response.append('\r'); } rd.close(); - return response.toString(); + String responseText = response.toString(); + if (responseText.contains("java.io.IOException") || responseText.contains("java.net.SocketTimeoutException")) { + responseJSON.put("Error", responseText); + return responseJSON; + } + try { + //Server response should be json so this should work + responseJSON = (JSONObject) JSONSerializer.toJSON(responseText); + return responseJSON; + } catch (Exception e) { + responseJSON.put("Error",e); + return responseJSON; + } } catch (Exception e) { StringWriter errors = new StringWriter(); e.printStackTrace(new PrintWriter(errors)); - //return null; - return errors.toString(); + responseJSON.put("Error",errors.toString()); + return responseJSON; } finally { if(connection != null) { connection.disconnect(); @@ -61,6 +77,23 @@ public static String sendJSON(String targetURL, String urlParams, String auth) { } } + public static String getToken(String servername, String auth) { + String token = new String(); + JSONObject httpResponse = getJSON(servername+"/login", auth, null); + try { + JSONArray returnArray = httpResponse.getJSONArray("return"); + for (Object o : returnArray ) { + JSONObject line = (JSONObject) o; + //This token will be used for all subsequent connections + token = line.getString("token"); + } + } catch (Exception e) { + token = "Auth Error: "+e+"\n\n"+httpResponse.toString(2).split("\\\\n")[0]; + return token; + } + return token; + } + //replaces $string with value of env($string). Used in conjunction with parameterized builds public static String paramorize(AbstractBuild build, BuildListener listener, String paramer) { Pattern pattern = Pattern.compile("\\{\\{\\w+\\}\\}"); diff --git a/src/main/resources/com/waytta/SaltAPIBuilder/config.jelly b/src/main/resources/com/waytta/SaltAPIBuilder/config.jelly index c1ee61b..4169236 100644 --- a/src/main/resources/com/waytta/SaltAPIBuilder/config.jelly +++ b/src/main/resources/com/waytta/SaltAPIBuilder/config.jelly @@ -19,6 +19,11 @@ title="${%Test Connection}" progress="${%Testing...}" method="testConnection" with="servername,username,userpass,authtype" /> + + + + diff --git a/src/main/resources/com/waytta/SaltAPIBuilder/global.jelly b/src/main/resources/com/waytta/SaltAPIBuilder/global.jelly new file mode 100644 index 0000000..5ae3019 --- /dev/null +++ b/src/main/resources/com/waytta/SaltAPIBuilder/global.jelly @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/main/resources/com/waytta/SaltAPIBuilder/help-blockbuild.html b/src/main/resources/com/waytta/SaltAPIBuilder/help-blockbuild.html new file mode 100644 index 0000000..465b63e --- /dev/null +++ b/src/main/resources/com/waytta/SaltAPIBuilder/help-blockbuild.html @@ -0,0 +1,4 @@ +
+ Unchecked, a Salt command will be issued, and the results returned if the command completes within a short timeout. (default)
+ When checked, the build step will wait for commands that take longer than the timeout. +