add support for running ad-hoc commands

This commit is contained in:
Vincent Behar 2011-07-05 15:25:44 +02:00
parent 3512577022
commit bd9fcca724
9 changed files with 556 additions and 50 deletions

View file

@ -17,9 +17,10 @@ import org.rundeck.api.parser.JobParser;
import org.rundeck.api.parser.JobsParser;
import org.rundeck.api.parser.ProjectParser;
import org.rundeck.api.parser.ProjectsParser;
import org.rundeck.api.util.ArgsUtil;
import org.rundeck.api.util.AssertUtil;
import org.rundeck.api.util.NodeFiltersBuilder;
import org.rundeck.api.util.OptionsBuilder;
import org.rundeck.api.util.ParametersUtil;
/**
* Main entry point to talk to a RunDeck instance
@ -239,8 +240,9 @@ public class RundeckClient implements Serializable {
RundeckApiLoginException, IllegalArgumentException {
AssertUtil.notBlank(jobId, "jobId is mandatory to trigger a job !");
StringBuilder apiPath = new StringBuilder("/job/").append(jobId).append("/run");
if (options != null) {
apiPath.append("?argString=").append(ArgsUtil.generateUrlEncodedArgString(options));
String argString = ParametersUtil.generateArgString(options);
if (StringUtils.isNotBlank(argString)) {
apiPath.append("?argString=").append(ParametersUtil.urlEncode(argString));
}
return new ApiCall(this).get(apiPath.toString(), new ExecutionParser("result/executions/execution"));
}
@ -320,6 +322,150 @@ public class RundeckClient implements Serializable {
return execution;
}
/*
* Ad-hoc executions
*/
/**
* Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution).
* The command will not be dispatched to nodes, but be executed on the RunDeck server.
*
* @param project name of the project - mandatory
* @param command to be executed - mandatory
* @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
* @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
* @see #triggerAdhocCommand(String, String, Properties)
*/
public RundeckExecution triggerAdhocCommand(String project, String command) throws RundeckApiException,
RundeckApiLoginException, IllegalArgumentException {
return triggerAdhocCommand(project, command, null);
}
/**
* Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution).
* The command will be dispatched to nodes, accordingly to the nodeFilters parameter.
*
* @param project name of the project - mandatory
* @param command to be executed - mandatory
* @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder}
* @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
* @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
* @see #triggerAdhocCommand(String, String)
*/
public RundeckExecution triggerAdhocCommand(String project, String command, Properties nodeFilters)
throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
AssertUtil.notBlank(project, "project is mandatory to trigger an ad-hoc command !");
AssertUtil.notBlank(command, "command is mandatory to trigger an ad-hoc command !");
StringBuilder apiPath = new StringBuilder("/run/command");
apiPath.append("?project=").append(project);
apiPath.append("&exec=").append(ParametersUtil.urlEncode(command));
String filters = ParametersUtil.generateNodeFiltersString(nodeFilters);
if (StringUtils.isNotBlank(filters)) {
apiPath.append("&").append(filters);
}
RundeckExecution execution = new ApiCall(this).get(apiPath.toString(), new ExecutionParser("result/execution"));
// the first call just returns the ID of the execution, so we need another call to get a "real" execution
return getExecution(execution.getId());
}
/**
* Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck
* server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still
* running. The command will not be dispatched to nodes, but be executed on the RunDeck server.
*
* @param project name of the project - mandatory
* @param command to be executed - mandatory
* @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
* @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
*/
public RundeckExecution runAdhocCommand(String project, String command) throws RundeckApiException,
RundeckApiLoginException, IllegalArgumentException {
return runAdhocCommand(project, command, null);
}
/**
* Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck
* server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is
* finished (or aborted) or is still running. The command will not be dispatched to nodes, but be executed on the
* RunDeck server.
*
* @param project name of the project - mandatory
* @param command to be executed - mandatory
* @param poolingInterval for checking the status of the execution. Must be > 0.
* @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
* @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
* @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
*/
public RundeckExecution runAdhocCommand(String project, String command, long poolingInterval, TimeUnit poolingUnit)
throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
return runAdhocCommand(project, command, null, poolingInterval, poolingUnit);
}
/**
* Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck
* server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still
* running. The command will be dispatched to nodes, accordingly to the nodeFilters parameter.
*
* @param project name of the project - mandatory
* @param command to be executed - mandatory
* @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder}
* @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
* @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
*/
public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters)
throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
return runAdhocCommand(project, command, nodeFilters, 5, TimeUnit.SECONDS);
}
/**
* Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck
* server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is
* finished (or aborted) or is still running. The command will be dispatched to nodes, accordingly to the
* nodeFilters parameter.
*
* @param project name of the project - mandatory
* @param command to be executed - mandatory
* @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder}
* @param poolingInterval for checking the status of the execution. Must be > 0.
* @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
* @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
* @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
*/
public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters,
long poolingInterval, TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException,
IllegalArgumentException {
if (poolingInterval <= 0) {
poolingInterval = 5;
poolingUnit = TimeUnit.SECONDS;
}
if (poolingUnit == null) {
poolingUnit = TimeUnit.SECONDS;
}
RundeckExecution execution = triggerAdhocCommand(project, command, nodeFilters);
while (ExecutionStatus.RUNNING.equals(execution.getStatus())) {
try {
Thread.sleep(poolingUnit.toMillis(poolingInterval));
} catch (InterruptedException e) {
break;
}
execution = getExecution(execution.getId());
}
return execution;
}
/*
* Executions
*/

View file

@ -36,11 +36,17 @@ public class ExecutionParser implements NodeParser<RundeckExecution> {
execution.setId(Long.valueOf(execNode.valueOf("@id")));
execution.setUrl(StringUtils.trimToNull(execNode.valueOf("@href")));
execution.setStatus(ExecutionStatus.valueOf(StringUtils.upperCase(execNode.valueOf("@status"))));
try {
execution.setStatus(ExecutionStatus.valueOf(StringUtils.upperCase(execNode.valueOf("@status"))));
} catch (IllegalArgumentException e) {
}
execution.setDescription(StringUtils.trimToNull(execNode.valueOf("description")));
execution.setStartedBy(StringUtils.trimToNull(execNode.valueOf("user")));
execution.setStartedAt(new Date(Long.valueOf(execNode.valueOf("date-started/@unixtime"))));
execution.setAbortedBy(StringUtils.trimToNull(execNode.valueOf("abortedby")));
String startedAt = StringUtils.trimToNull(execNode.valueOf("date-started/@unixtime"));
if (startedAt != null) {
execution.setStartedAt(new Date(Long.valueOf(startedAt)));
}
String endedAt = StringUtils.trimToNull(execNode.valueOf("date-ended/@unixtime"));
if (endedAt != null) {
execution.setEndedAt(new Date(Long.valueOf(endedAt)));

View file

@ -0,0 +1,274 @@
package org.rundeck.api.util;
import java.util.Properties;
import org.apache.commons.lang.StringUtils;
/**
* Builder for node filters
*
* @author Vincent Behar
*/
public class NodeFiltersBuilder {
private final Properties filters;
/**
* Build a new instance. At the end, use {@link #toProperties()}.
*/
public NodeFiltersBuilder() {
filters = new Properties();
}
/**
* Include nodes matching the given hostname
*
* @param hostname
* @return this, for method chaining
* @see #excludeHostname(String)
*/
public NodeFiltersBuilder hostname(String hostname) {
if (StringUtils.isNotBlank(hostname)) {
filters.put("hostname", hostname);
}
return this;
}
/**
* Include nodes matching the given type
*
* @param type
* @return this, for method chaining
* @see #excludeType(String)
*/
public NodeFiltersBuilder type(String type) {
if (StringUtils.isNotBlank(type)) {
filters.put("type", type);
}
return this;
}
/**
* Include nodes matching the given tags
*
* @param tags
* @return this, for method chaining
* @see #excludeTags(String)
*/
public NodeFiltersBuilder tags(String tags) {
if (StringUtils.isNotBlank(tags)) {
filters.put("tags", tags);
}
return this;
}
/**
* Include nodes matching the given name
*
* @param name
* @return this, for method chaining
* @see #excludeName(String)
*/
public NodeFiltersBuilder name(String name) {
if (StringUtils.isNotBlank(name)) {
filters.put("name", name);
}
return this;
}
/**
* Include nodes matching the given OS-name
*
* @param osName
* @return this, for method chaining
* @see #excludeOsName(String)
*/
public NodeFiltersBuilder osName(String osName) {
if (StringUtils.isNotBlank(osName)) {
filters.put("os-name", osName);
}
return this;
}
/**
* Include nodes matching the given OS-family
*
* @param osFamily
* @return this, for method chaining
* @see #excludeOsFamily(String)
*/
public NodeFiltersBuilder osFamily(String osFamily) {
if (StringUtils.isNotBlank(osFamily)) {
filters.put("os-family", osFamily);
}
return this;
}
/**
* Include nodes matching the given OS-arch
*
* @param osArch
* @return this, for method chaining
* @see #excludeOsArch(String)
*/
public NodeFiltersBuilder osArch(String osArch) {
if (StringUtils.isNotBlank(osArch)) {
filters.put("os-arch", osArch);
}
return this;
}
/**
* Include nodes matching the given OS-version
*
* @param osVersion
* @return this, for method chaining
* @see #excludeOsVersion(String)
*/
public NodeFiltersBuilder osVersion(String osVersion) {
if (StringUtils.isNotBlank(osVersion)) {
filters.put("os-version", osVersion);
}
return this;
}
/**
* Exclude nodes matching the given hostname
*
* @param hostname
* @return this, for method chaining
* @see #hostname(String)
* @see #excludePrecedence(boolean)
*/
public NodeFiltersBuilder excludeHostname(String hostname) {
if (StringUtils.isNotBlank(hostname)) {
filters.put("exclude-hostname", hostname);
}
return this;
}
/**
* Exclude nodes matching the given type
*
* @param type
* @return this, for method chaining
* @see #type(String)
* @see #excludePrecedence(boolean)
*/
public NodeFiltersBuilder excludeType(String type) {
if (StringUtils.isNotBlank(type)) {
filters.put("exclude-type", type);
}
return this;
}
/**
* Exclude nodes matching the given tags
*
* @param tags
* @return this, for method chaining
* @see #tags(String)
* @see #excludePrecedence(boolean)
*/
public NodeFiltersBuilder excludeTags(String tags) {
if (StringUtils.isNotBlank(tags)) {
filters.put("exclude-tags", tags);
}
return this;
}
/**
* Exclude nodes matching the given name
*
* @param name
* @return this, for method chaining
* @see #name(String)
* @see #excludePrecedence(boolean)
*/
public NodeFiltersBuilder excludeName(String name) {
if (StringUtils.isNotBlank(name)) {
filters.put("exclude-name", name);
}
return this;
}
/**
* Exclude nodes matching the given OS-name
*
* @param osName
* @return this, for method chaining
* @see #osName(String)
* @see #excludePrecedence(boolean)
*/
public NodeFiltersBuilder excludeOsName(String osName) {
if (StringUtils.isNotBlank(osName)) {
filters.put("exclude-os-name", osName);
}
return this;
}
/**
* Exclude nodes matching the given OS-family
*
* @param osFamily
* @return this, for method chaining
* @see #osFamily(String)
* @see #excludePrecedence(boolean)
*/
public NodeFiltersBuilder excludeOsFamily(String osFamily) {
if (StringUtils.isNotBlank(osFamily)) {
filters.put("exclude-os-family", osFamily);
}
return this;
}
/**
* Exclude nodes matching the given OS-arch
*
* @param osArch
* @return this, for method chaining
* @see #osArch(String)
* @see #excludePrecedence(boolean)
*/
public NodeFiltersBuilder excludeOsArch(String osArch) {
if (StringUtils.isNotBlank(osArch)) {
filters.put("exclude-os-arch", osArch);
}
return this;
}
/**
* Exclude nodes matching the given OS-version
*
* @param osVersion
* @return this, for method chaining
* @see #osVersion(String)
* @see #excludePrecedence(boolean)
*/
public NodeFiltersBuilder excludeOsVersion(String osVersion) {
if (StringUtils.isNotBlank(osVersion)) {
filters.put("exclude-os-version", osVersion);
}
return this;
}
/**
* Whether exclusion filters take precedence (default to yes).
*
* @param excludePrecedence
* @return this, for method chaining
*/
public NodeFiltersBuilder excludePrecedence(boolean excludePrecedence) {
filters.put("exclude-precedence", Boolean.toString(excludePrecedence));
return this;
}
/**
* @return a new {@link Properties} instance
*/
public Properties toProperties() {
Properties filters = new Properties();
filters.putAll(this.filters);
return filters;
}
}

View file

@ -2,33 +2,31 @@ package org.rundeck.api.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Map.Entry;
import org.apache.commons.lang.StringUtils;
/**
* Utility class for RunDeck arguments
* Utility class for API parameters that should be passed in URLs.
*
* @author Vincent Behar
*/
public class ArgsUtil {
public class ParametersUtil {
/**
* Generates and url-encode a RunDeck "argString" representing the given options. Format of the argString is
* <code>"-key1 value1 -key2 'value 2 with spaces'"</code>
* URL-encode the given string
*
* @param options to be converted
* @return an url-encoded string. null if options is null, empty if there are no valid options.
* @see #generateArgString(Properties)
* @param input string to be encoded
* @return an url-encoded string
*/
public static String generateUrlEncodedArgString(Properties options) {
String argString = generateArgString(options);
if (StringUtils.isBlank(argString)) {
return argString;
public static String urlEncode(String input) {
if (StringUtils.isBlank(input)) {
return input;
}
try {
return URLEncoder.encode(argString, "UTF-8");
return URLEncoder.encode(input, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
@ -36,11 +34,10 @@ public class ArgsUtil {
/**
* Generates a RunDeck "argString" representing the given options. Format of the argString is
* <code>"-key1 value1 -key2 'value 2 with spaces'"</code>
* <code>"-key1 value1 -key2 'value 2 with spaces'"</code>. You might want to url-encode this string...
*
* @param options to be converted
* @return a string. null if options is null, empty if there are no valid options.
* @see #generateUrlEncodedArgString(Properties)
*/
public static String generateArgString(Properties options) {
if (options == null) {
@ -69,4 +66,32 @@ public class ArgsUtil {
return argString.toString();
}
/**
* Generates an url-encoded string representing the given nodeFilters. Format of the string is
* <code>"filter1=value1&filter2=value2"</code>.
*
* @param nodeFilters to be converted
* @return an url-encoded string. null if nodeFilters is null, empty if there are no valid filters.
*/
public static String generateNodeFiltersString(Properties nodeFilters) {
if (nodeFilters == null) {
return null;
}
List<String> filters = new ArrayList<String>();
for (Entry<Object, Object> filter : nodeFilters.entrySet()) {
String key = String.valueOf(filter.getKey());
String value = String.valueOf(filter.getValue());
if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
try {
filters.add(URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode(value, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}
return StringUtils.join(filters, "&");
}
}

View file

@ -64,4 +64,44 @@ public class ExecutionParserTest {
Assert.assertEquals("list files", job.getDescription());
}
@Test
public void parseAdhocNode() throws Exception {
InputStream input = getClass().getResourceAsStream("execution-adhoc.xml");
Document document = ParserHelper.loadDocument(input);
RundeckExecution execution = new ExecutionParser("result/executions/execution").parseNode(document);
RundeckJob job = execution.getJob();
Assert.assertEquals(new Long(1), execution.getId());
Assert.assertEquals("http://localhost:4440/execution/follow/1", execution.getUrl());
Assert.assertEquals(ExecutionStatus.SUCCEEDED, execution.getStatus());
Assert.assertEquals("admin", execution.getStartedBy());
Assert.assertEquals(new Date(1309857539137L), execution.getStartedAt());
Assert.assertEquals(new Date(1309857539606L), execution.getEndedAt());
Assert.assertEquals(null, execution.getAbortedBy());
Assert.assertEquals("w", execution.getDescription());
Assert.assertNull(job);
}
@Test
public void parseMinimalistNode() throws Exception {
InputStream input = getClass().getResourceAsStream("execution-minimalist.xml");
Document document = ParserHelper.loadDocument(input);
RundeckExecution execution = new ExecutionParser("result/execution").parseNode(document);
RundeckJob job = execution.getJob();
Assert.assertEquals(new Long(1), execution.getId());
Assert.assertNull(execution.getUrl());
Assert.assertNull(execution.getStatus());
Assert.assertNull(execution.getStartedBy());
Assert.assertNull(execution.getStartedAt());
Assert.assertNull(execution.getEndedAt());
Assert.assertNull(execution.getAbortedBy());
Assert.assertNull(execution.getDescription());
Assert.assertNull(job);
}
}

View file

@ -1,30 +0,0 @@
package org.rundeck.api.util;
import java.util.Properties;
import org.junit.Assert;
import org.junit.Test;
/**
* Test the {@link ArgsUtil}
*
* @author Vincent Behar
*/
public class ArgsUtilTest {
@Test
public void generateArgString() throws Exception {
Assert.assertNull(ArgsUtil.generateArgString(null));
Assert.assertEquals("", ArgsUtil.generateArgString(new Properties()));
Properties options = new Properties();
options.put("key1", "value1");
options.put("key2", "value 2 with spaces");
String argString = ArgsUtil.generateArgString(options);
if (argString.startsWith("-key1")) {
Assert.assertEquals("-key1 value1 -key2 'value 2 with spaces'", argString);
} else {
Assert.assertEquals("-key2 'value 2 with spaces' -key1 value1", argString);
}
}
}

View file

@ -0,0 +1,43 @@
package org.rundeck.api.util;
import java.util.Properties;
import org.junit.Assert;
import org.junit.Test;
/**
* Test the {@link ParametersUtil}
*
* @author Vincent Behar
*/
public class ParametersUtilTest {
@Test
public void generateArgString() throws Exception {
Assert.assertNull(ParametersUtil.generateArgString(null));
Assert.assertEquals("", ParametersUtil.generateArgString(new Properties()));
Properties options = new Properties();
options.put("key1", "value1");
options.put("key2", "value 2 with spaces");
String argString = ParametersUtil.generateArgString(options);
if (argString.startsWith("-key1")) {
Assert.assertEquals("-key1 value1 -key2 'value 2 with spaces'", argString);
} else {
Assert.assertEquals("-key2 'value 2 with spaces' -key1 value1", argString);
}
}
@Test
public void generateNodeFiltersString() throws Exception {
Assert.assertNull(ParametersUtil.generateNodeFiltersString(null));
Assert.assertEquals("", ParametersUtil.generateNodeFiltersString(new Properties()));
Properties filters = new Properties();
filters.put("tags", "appserv+front");
filters.put("exclude-tags", "qa,dev");
filters.put("os-family", "unix");
String result = ParametersUtil.generateNodeFiltersString(filters);
Assert.assertEquals("os-family=unix&exclude-tags=qa%2Cdev&tags=appserv%2Bfront", result);
}
}

View file

@ -0,0 +1 @@
<result success='true' apiversion='1'><executions count='1'><execution id='1' href='http://localhost:4440/execution/follow/1' status='succeeded'><user>admin</user><date-started unixtime='1309857539137'>2011-07-05T09:18:59Z</date-started><date-ended unixtime='1309857539606'>2011-07-05T09:18:59Z</date-ended><description>w</description></execution></executions></result>

View file

@ -0,0 +1 @@
<result success='true' apiversion='1'><success><message>Immediate execution scheduled (1)</message></success><execution id='1'/></result>