initial commit

This commit is contained in:
Vincent Behar 2011-07-01 17:13:26 +02:00
commit d294673b45
29 changed files with 1652 additions and 0 deletions

8
README Normal file
View file

@ -0,0 +1,8 @@
Java client for the RunDeck REST API
RunDeck Home : http://rundeck.org/
LICENSE : The Apache Software License, Version 2.0
See http://www.apache.org/licenses/LICENSE-2.0.txt

81
pom.xml Normal file
View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.sonatype.oss</groupId>
<artifactId>oss-parent</artifactId>
<version>7</version>
</parent>
<groupId>org.rundeck</groupId>
<artifactId>rundeck-api-java-client</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>RunDeck API - Java Client</name>
<description>Java client for the RunDeck REST API</description>
<url>https://github.com/vbehar/rundeck-api-java-client</url>
<scm>
<url>https://github.com/vbehar/rundeck-api-java-client</url>
<connection>scm:git:git://github.com/vbehar/rundeck-api-java-client.git</connection>
<developerConnection>scm:git:git@github.com:vbehar/rundeck-api-java-client.git</developerConnection>
</scm>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>vbehar</id>
<name>Vincent Behar</name>
</developer>
</developers>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- HTTP -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.1.1</version>
</dependency>
<!-- Commons -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!-- XML Parsing -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.1</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,220 @@
package org.rundeck.api;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.dom4j.Document;
import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
import org.rundeck.api.parser.NodeParser;
import org.rundeck.api.parser.ParserHelper;
/**
* Class responsible for making the HTTP API calls
*
* @author Vincent Behar
*/
class ApiCall {
private final RundeckClient client;
/**
* Build a new instance, linked to the given RunDeck client
*
* @param client holding the RunDeck url and the credentials
*/
public ApiCall(RundeckClient client) {
super();
this.client = client;
}
/**
* Try to "ping" the RunDeck instance to see if it is alive
*
* @throws RundeckApiException if the ping fails
*/
public void ping() throws RundeckApiException {
HttpClient httpClient = instantiateHttpClient();
try {
HttpResponse response = httpClient.execute(new HttpGet(client.getUrl()));
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' when pinging "
+ client.getUrl());
}
} catch (IOException e) {
throw new RundeckApiException("Failed to ping RunDeck instance at " + client.getUrl(), e);
} finally {
httpClient.getConnectionManager().shutdown();
}
}
/**
* Test the credentials (login/password) on the RunDeck instance
*
* @throws RundeckApiLoginException if the login fails
*/
public void testCredentials() throws RundeckApiLoginException {
HttpClient httpClient = instantiateHttpClient();
try {
login(httpClient);
} finally {
httpClient.getConnectionManager().shutdown();
}
}
/**
* Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the
* API call. At the end, the given parser will be used to convert the response to a more useful result object.
*
* @param apiPath on which we will make the HTTP request
* @param parser used to parse the response
* @return the result of the call, as formatted by the parser
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login fails
*/
public <T> T get(String apiPath, NodeParser<T> parser) throws RundeckApiException, RundeckApiLoginException {
String apiUrl = client.getUrl() + RundeckClient.API_ENDPOINT + apiPath;
HttpClient httpClient = instantiateHttpClient();
try {
login(httpClient);
// execute the HTTP request
HttpResponse response = null;
try {
response = httpClient.execute(new HttpGet(apiUrl));
} catch (IOException e) {
throw new RundeckApiException("Failed to execute an HTTP GET on url : " + apiUrl, e);
}
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' for " + apiUrl);
}
if (response.getEntity() == null) {
throw new RundeckApiException("Empty RunDeck response ! HTTP status line is : "
+ response.getStatusLine());
}
// read and parse the response
Document xmlDocument = ParserHelper.loadDocument(response);
T result = parser.parseNode(xmlDocument);
// release the connection
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RundeckApiException("Failed to consume entity (release connection)", e);
}
return result;
} finally {
httpClient.getConnectionManager().shutdown();
}
}
/**
* Do the actual work of login, using the given {@link HttpClient} instance. You'll need to re-use this instance
* when making API calls (such as running a job).
*
* @param httpClient pre-instantiated
* @throws RundeckApiLoginException if the login failed
*/
private void login(HttpClient httpClient) throws RundeckApiLoginException {
String location = client.getUrl() + "/j_security_check";
while (true) {
HttpPost postLogin = new HttpPost(location);
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("j_username", client.getLogin()));
params.add(new BasicNameValuePair("j_password", client.getPassword()));
params.add(new BasicNameValuePair("action", "login"));
HttpResponse response = null;
try {
postLogin.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
response = httpClient.execute(postLogin);
} catch (IOException e) {
throw new RundeckApiLoginException("Failed to post login form on " + location, e);
}
if (response.getStatusLine().getStatusCode() / 100 == 3) {
// HTTP client refuses to handle redirects (code 3xx) for POST, so we have to do it manually...
location = response.getFirstHeader("Location").getValue();
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
}
continue;
}
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new RundeckApiLoginException("Invalid HTTP response '" + response.getStatusLine() + "' for "
+ location);
}
try {
String content = EntityUtils.toString(response.getEntity(), HTTP.UTF_8);
if (StringUtils.contains(content, "j_security_check")) {
throw new RundeckApiLoginException("Login failed for user " + client.getLogin());
}
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
throw new RundeckApiLoginException("Failed to consume entity (release connection)", e);
}
} catch (IOException io) {
throw new RundeckApiLoginException("Failed to read RunDeck result", io);
} catch (ParseException p) {
throw new RundeckApiLoginException("Failed to parse RunDeck response", p);
}
break;
}
}
/**
* Instantiate a new {@link HttpClient} instance, configured to accept all SSL certificates
*
* @return an {@link HttpClient} instance - won't be null
*/
private HttpClient instantiateHttpClient() {
SSLSocketFactory socketFactory = null;
try {
socketFactory = new SSLSocketFactory(new TrustStrategy() {
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
}, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
} catch (KeyManagementException e) {
throw new RuntimeException(e);
} catch (UnrecoverableKeyException e) {
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
HttpClient httpClient = new DefaultHttpClient();
httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", 443, socketFactory));
return httpClient;
}
}

View file

@ -0,0 +1,36 @@
package org.rundeck.api;
/**
* A generic (unchecked) exception when using the RunDeck API
*
* @author Vincent Behar
*/
public class RundeckApiException extends RuntimeException {
private static final long serialVersionUID = 1L;
public RundeckApiException(String message) {
super(message);
}
public RundeckApiException(String message, Throwable cause) {
super(message, cause);
}
/**
* Specific login-related error
*/
public static class RundeckApiLoginException extends RundeckApiException {
private static final long serialVersionUID = 1L;
public RundeckApiLoginException(String message) {
super(message);
}
public RundeckApiLoginException(String message, Throwable cause) {
super(message, cause);
}
}
}

View file

@ -0,0 +1,231 @@
package org.rundeck.api;
import java.io.Serializable;
import java.util.List;
import java.util.Properties;
import org.apache.commons.lang.StringUtils;
import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
import org.rundeck.api.domain.RundeckExecution;
import org.rundeck.api.domain.RundeckJob;
import org.rundeck.api.parser.ExecutionParser;
import org.rundeck.api.parser.ExecutionsParser;
import org.rundeck.api.parser.JobParser;
import org.rundeck.api.parser.JobsParser;
import org.rundeck.api.util.ArgsUtil;
import org.rundeck.api.util.AssertUtil;
/**
* Main entry point to talk to a RunDeck instance
*
* @author Vincent Behar
*/
public class RundeckClient implements Serializable {
private static final long serialVersionUID = 1L;
public static final transient int API_VERSION = 1;
public static final transient String API_ENDPOINT = "/api/" + API_VERSION;
private final String url;
private final String login;
private final String password;
/**
* Instantiate a new {@link RundeckClient} for the RunDeck instance at the given url
*
* @param url of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc)
* @param login
* @param password
*/
public RundeckClient(String url, String login, String password) {
super();
this.url = url;
this.login = login;
this.password = password;
}
/**
* Try to "ping" the RunDeck instance to see if it is alive
*
* @throws RundeckApiException if the ping fails
*/
public void ping() throws RundeckApiException {
new ApiCall(this).ping();
}
/**
* Test your credentials (login/password) on the RunDeck instance
*
* @throws RundeckApiLoginException if the login fails
*/
public void testCredentials() throws RundeckApiLoginException {
new ApiCall(this).testCredentials();
}
/**
* List all jobs that belongs to the given project
*
* @param project name of the project - mandatory
* @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
* @see #getJobs(String, String, String, String...)
*/
public List<RundeckJob> getJobs(String project) throws RundeckApiException, RundeckApiLoginException,
IllegalArgumentException {
return getJobs(project, null, null, new String[0]);
}
/**
* List the jobs that belongs to the given project, and matches the given criteria (jobFilter, groupPath and jobIds)
*
* @param project name of the project - mandatory
* @param jobFilter a filter for the job Name - optional
* @param groupPath a group or partial group path to include all jobs within that group path - optional
* @param jobIds a list of Job IDs to include - optional
* @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
* @see #getJobs(String)
*/
public List<RundeckJob> getJobs(String project, String jobFilter, String groupPath, String... jobIds)
throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
AssertUtil.notBlank(project, "project is mandatory to get all jobs !");
StringBuilder apiPath = new StringBuilder("/jobs");
apiPath.append("?project=").append(project);
if (StringUtils.isNotBlank(jobFilter)) {
apiPath.append("&jobFilter=").append(jobFilter);
}
if (StringUtils.isNotBlank(groupPath)) {
apiPath.append("&groupPath=").append(groupPath);
}
if (jobIds != null && jobIds.length > 0) {
apiPath.append("&idlist=").append(StringUtils.join(jobIds, ","));
}
return new ApiCall(this).get(apiPath.toString(), new JobsParser("result/jobs/job"));
}
/**
* Get the definition of a single job, identified by the given ID
*
* @param jobId identifier of the job - mandatory
* @return a {@link RundeckJob} instance
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
*/
public RundeckJob getJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
IllegalArgumentException {
AssertUtil.notBlank(jobId, "jobId is mandatory to get the details of a job !");
return new ApiCall(this).get("/job/" + jobId, new JobParser("joblist/job"));
}
/**
* Get the executions of the given job
*
* @param jobId identifier of the job - mandatory
* @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
*/
public List<RundeckExecution> getJobExecutions(String jobId) throws RundeckApiException, RundeckApiLoginException,
IllegalArgumentException {
AssertUtil.notBlank(jobId, "jobId is mandatory to get the executions of a job !");
return new ApiCall(this).get("/job/" + jobId + "/executions",
new ExecutionsParser("result/executions/execution"));
}
/**
* Get a single execution, identified by the given ID
*
* @param executionId identifier of the execution - mandatory
* @return a {@link RundeckExecution} instance
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the executionId is null
*/
public RundeckExecution getExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException,
IllegalArgumentException {
AssertUtil.notNull(executionId, "executionId is mandatory to get the details of an execution !");
return new ApiCall(this).get("/execution/" + executionId, new ExecutionParser("result/executions/execution"));
}
/**
* Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the
* end of the job execution)
*
* @param jobId identifier of the job - mandatory
* @param options of the job - optional
* @return a {@link RundeckExecution} instance representing the newly created (and running) execution
* @throws RundeckApiException in case of error when calling the API
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
*/
public RundeckExecution triggerJob(String jobId, Properties options) throws RundeckApiException,
RundeckApiLoginException, IllegalArgumentException {
AssertUtil.notBlank(jobId, "jobId is mandatory to trigger a job !");
String apiPath = "/job/" + jobId + "/run?argString=" + ArgsUtil.generateUrlEncodedArgString(options);
return new ApiCall(this).get(apiPath, new ExecutionParser("result/executions/execution"));
}
public String getUrl() {
return url;
}
public String getLogin() {
return login;
}
public String getPassword() {
return password;
}
@Override
public String toString() {
return "RundeckClient [url=" + url + ", login=" + login + ", password=" + password + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((login == null) ? 0 : login.hashCode());
result = prime * result + ((password == null) ? 0 : password.hashCode());
result = prime * result + ((url == null) ? 0 : url.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RundeckClient other = (RundeckClient) obj;
if (login == null) {
if (other.login != null)
return false;
} else if (!login.equals(other.login))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (url == null) {
if (other.url != null)
return false;
} else if (!url.equals(other.url))
return false;
return true;
}
}

View file

@ -0,0 +1,196 @@
package org.rundeck.api.domain;
import java.io.Serializable;
import java.util.Date;
/**
* Represents a RunDeck execution, usually triggered by an API call. An execution could be a {@link RundeckJob}
* execution or an "ad-hoc" execution.
*
* @author Vincent Behar
*/
public class RundeckExecution implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String url;
private ExecutionStatus status;
/** Optional - only if it is a job execution */
private RundeckJob job;
private String startedBy;
private Date startedAt;
/** only if the execution has ended */
private Date endedAt;
/** only if the execution was aborted */
private String abortedBy;
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public ExecutionStatus getStatus() {
return status;
}
public void setStatus(ExecutionStatus status) {
this.status = status;
}
public RundeckJob getJob() {
return job;
}
public void setJob(RundeckJob job) {
this.job = job;
}
public String getStartedBy() {
return startedBy;
}
public void setStartedBy(String startedBy) {
this.startedBy = startedBy;
}
public Date getStartedAt() {
return startedAt;
}
public void setStartedAt(Date startedAt) {
this.startedAt = startedAt;
}
public Date getEndedAt() {
return endedAt;
}
public void setEndedAt(Date endedAt) {
this.endedAt = endedAt;
}
public String getAbortedBy() {
return abortedBy;
}
public void setAbortedBy(String abortedBy) {
this.abortedBy = abortedBy;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "RundeckExecution [abortedBy=" + abortedBy + ", description=" + description + ", endedAt=" + endedAt
+ ", id=" + id + ", job=" + job + ", startedAt=" + startedAt + ", startedBy=" + startedBy + ", status="
+ status + ", url=" + url + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((abortedBy == null) ? 0 : abortedBy.hashCode());
result = prime * result + ((description == null) ? 0 : description.hashCode());
result = prime * result + ((endedAt == null) ? 0 : endedAt.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((job == null) ? 0 : job.hashCode());
result = prime * result + ((startedAt == null) ? 0 : startedAt.hashCode());
result = prime * result + ((startedBy == null) ? 0 : startedBy.hashCode());
result = prime * result + ((status == null) ? 0 : status.hashCode());
result = prime * result + ((url == null) ? 0 : url.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RundeckExecution other = (RundeckExecution) obj;
if (abortedBy == null) {
if (other.abortedBy != null)
return false;
} else if (!abortedBy.equals(other.abortedBy))
return false;
if (description == null) {
if (other.description != null)
return false;
} else if (!description.equals(other.description))
return false;
if (endedAt == null) {
if (other.endedAt != null)
return false;
} else if (!endedAt.equals(other.endedAt))
return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (job == null) {
if (other.job != null)
return false;
} else if (!job.equals(other.job))
return false;
if (startedAt == null) {
if (other.startedAt != null)
return false;
} else if (!startedAt.equals(other.startedAt))
return false;
if (startedBy == null) {
if (other.startedBy != null)
return false;
} else if (!startedBy.equals(other.startedBy))
return false;
if (status == null) {
if (other.status != null)
return false;
} else if (!status.equals(other.status))
return false;
if (url == null) {
if (other.url != null)
return false;
} else if (!url.equals(other.url))
return false;
return true;
}
/**
* The status of an execution
*/
public static enum ExecutionStatus {
RUNNING, SUCCEEDED, FAILED, ABORTED;
}
}

View file

@ -0,0 +1,132 @@
package org.rundeck.api.domain;
import java.io.Serializable;
import org.apache.commons.lang.StringUtils;
/**
* Represents a RunDeck job
*
* @author Vincent Behar
*/
public class RundeckJob implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String name;
private String group;
private String project;
private String description;
/**
* @return the fullname : group + name (exact format is : "group/name")
*/
public String getFullName() {
StringBuilder fullName = new StringBuilder();
if (StringUtils.isNotBlank(group)) {
fullName.append(group).append("/");
}
fullName.append(name);
return fullName.toString();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getProject() {
return project;
}
public void setProject(String project) {
this.project = project;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "RundeckJob [id=" + id + ", name=" + name + ", group=" + group + ", project=" + project
+ ", description=" + description + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((description == null) ? 0 : description.hashCode());
result = prime * result + ((group == null) ? 0 : group.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((project == null) ? 0 : project.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RundeckJob other = (RundeckJob) obj;
if (description == null) {
if (other.description != null)
return false;
} else if (!description.equals(other.description))
return false;
if (group == null) {
if (other.group != null)
return false;
} else if (!group.equals(other.group))
return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (project == null) {
if (other.project != null)
return false;
} else if (!project.equals(other.project))
return false;
return true;
}
}

View file

@ -0,0 +1,58 @@
package org.rundeck.api.parser;
import java.util.Date;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Node;
import org.rundeck.api.domain.RundeckExecution;
import org.rundeck.api.domain.RundeckJob;
import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
/**
* Parser for a single {@link RundeckExecution}
*
* @author Vincent Behar
*/
public class ExecutionParser implements NodeParser<RundeckExecution> {
private String xpath;
public ExecutionParser() {
super();
}
/**
* @param xpath of the execution element if it is not the root node
*/
public ExecutionParser(String xpath) {
super();
this.xpath = xpath;
}
@Override
public RundeckExecution parseNode(Node node) {
Node execNode = xpath != null ? node.selectSingleNode(xpath) : node;
RundeckExecution execution = new RundeckExecution();
execution.setId(Long.valueOf(execNode.valueOf("@id")));
execution.setUrl(StringUtils.trimToNull(execNode.valueOf("@href")));
execution.setStatus(ExecutionStatus.valueOf(StringUtils.upperCase(execNode.valueOf("@status"))));
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 endedAt = StringUtils.trimToNull(execNode.valueOf("date-ended/@unixtime"));
if (endedAt != null) {
execution.setEndedAt(new Date(Long.valueOf(endedAt)));
}
Node jobNode = execNode.selectSingleNode("job");
if (jobNode != null) {
RundeckJob job = new JobParser().parseNode(jobNode);
execution.setJob(job);
}
return execution;
}
}

View file

@ -0,0 +1,40 @@
package org.rundeck.api.parser;
import java.util.ArrayList;
import java.util.List;
import org.dom4j.Node;
import org.rundeck.api.domain.RundeckExecution;
/**
* Parser for a {@link List} of {@link RundeckExecution}
*
* @author Vincent Behar
*/
public class ExecutionsParser implements NodeParser<List<RundeckExecution>> {
private final String xpath;
/**
* @param xpath of the executions elements
*/
public ExecutionsParser(String xpath) {
super();
this.xpath = xpath;
}
@Override
public List<RundeckExecution> parseNode(Node node) {
List<RundeckExecution> executions = new ArrayList<RundeckExecution>();
@SuppressWarnings("unchecked")
List<Node> execNodes = node.selectNodes(xpath);
for (Node execNode : execNodes) {
RundeckExecution execution = new ExecutionParser().parseNode(execNode);
executions.add(execution);
}
return executions;
}
}

View file

@ -0,0 +1,57 @@
package org.rundeck.api.parser;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Node;
import org.rundeck.api.domain.RundeckJob;
/**
* Parser for a single {@link RundeckJob}
*
* @author Vincent Behar
*/
public class JobParser implements NodeParser<RundeckJob> {
private String xpath;
public JobParser() {
super();
}
/**
* @param xpath of the job element if it is not the root node
*/
public JobParser(String xpath) {
super();
this.xpath = xpath;
}
@Override
public RundeckJob parseNode(Node node) {
Node jobNode = xpath != null ? node.selectSingleNode(xpath) : node;
RundeckJob job = new RundeckJob();
job.setName(StringUtils.trimToNull(jobNode.valueOf("name")));
job.setDescription(StringUtils.trimToNull(jobNode.valueOf("description")));
job.setGroup(StringUtils.trimToNull(jobNode.valueOf("group")));
// ID is either an attribute or an child element...
String jobId = null;
jobId = jobNode.valueOf("id");
if (StringUtils.isBlank(jobId)) {
jobId = jobNode.valueOf("@id");
}
job.setId(jobId);
// project is either a nested element of context, or just a child element
Node contextNode = jobNode.selectSingleNode("context");
if (contextNode != null) {
job.setProject(StringUtils.trimToNull(contextNode.valueOf("project")));
} else {
job.setProject(StringUtils.trimToNull(jobNode.valueOf("project")));
}
return job;
}
}

View file

@ -0,0 +1,40 @@
package org.rundeck.api.parser;
import java.util.ArrayList;
import java.util.List;
import org.dom4j.Node;
import org.rundeck.api.domain.RundeckJob;
/**
* Parser for a {@link List} of {@link RundeckJob}
*
* @author Vincent Behar
*/
public class JobsParser implements NodeParser<List<RundeckJob>> {
private final String xpath;
/**
* @param xpath of the jobs elements
*/
public JobsParser(String xpath) {
super();
this.xpath = xpath;
}
@Override
public List<RundeckJob> parseNode(Node node) {
List<RundeckJob> jobs = new ArrayList<RundeckJob>();
@SuppressWarnings("unchecked")
List<Node> jobNodes = node.selectNodes(xpath);
for (Node jobNode : jobNodes) {
RundeckJob job = new JobParser().parseNode(jobNode);
jobs.add(job);
}
return jobs;
}
}

View file

@ -0,0 +1,20 @@
package org.rundeck.api.parser;
import org.dom4j.Node;
/**
* Interface to be implemented for parsers that handle XML {@link Node}s
*
* @author Vincent Behar
*/
public interface NodeParser<T> {
/**
* Parse the given XML {@link Node}
*
* @param node
* @return any object holding the converted value
*/
T parseNode(Node node);
}

View file

@ -0,0 +1,72 @@
package org.rundeck.api.parser;
import java.io.IOException;
import java.io.InputStream;
import org.apache.http.HttpResponse;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.rundeck.api.RundeckApiException;
/**
* Helper for parsing RunDeck responses
*
* @author Vincent Behar
*/
public class ParserHelper {
/**
* Load an XML {@link Document} from the given RunDeck {@link HttpResponse}.
*
* @param httpResponse from an API call to RunDeck
* @return an XML {@link Document}
* @throws RundeckApiException if we failed to read the response, or if the response is an error
* @see #loadDocument(InputStream)
*/
public static Document loadDocument(HttpResponse httpResponse) throws RundeckApiException {
InputStream inputStream = null;
try {
inputStream = httpResponse.getEntity().getContent();
} catch (IllegalStateException e) {
throw new RundeckApiException("Failed to read RunDeck reponse", e);
} catch (IOException e) {
throw new RundeckApiException("Failed to read RunDeck reponse", e);
}
return loadDocument(inputStream);
}
/**
* Load an XML {@link Document} from the given {@link InputStream}
*
* @param inputStream from an API call to RunDeck
* @return an XML {@link Document}
* @throws RundeckApiException if we failed to read the response, or if the response is an error
* @see #loadDocument(HttpResponse)
*/
public static Document loadDocument(InputStream inputStream) throws RundeckApiException {
SAXReader reader = new SAXReader();
reader.setEncoding("UTF-8");
Document document;
try {
document = reader.read(inputStream);
} catch (DocumentException e) {
throw new RundeckApiException("Failed to read RunDeck reponse", e);
}
document.setXMLEncoding("UTF-8");
Node result = document.selectSingleNode("result");
if (result != null) {
Boolean failure = Boolean.valueOf(result.valueOf("@error"));
if (failure) {
throw new RundeckApiException(result.valueOf("error/message"));
}
}
return document;
}
}

View file

@ -0,0 +1,72 @@
package org.rundeck.api.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Properties;
import java.util.Map.Entry;
import org.apache.commons.lang.StringUtils;
/**
* Utility class for RunDeck arguments
*
* @author Vincent Behar
*/
public class ArgsUtil {
/**
* 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>
*
* @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)
*/
public static String generateUrlEncodedArgString(Properties options) {
String argString = generateArgString(options);
if (StringUtils.isBlank(argString)) {
return argString;
}
try {
return URLEncoder.encode(argString, "UTF8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Generates a RunDeck "argString" representing the given options. Format of the argString is
* <code>"-key1 value1 -key2 'value 2 with spaces'"</code>
*
* @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) {
return null;
}
StringBuilder argString = new StringBuilder();
for (Entry<Object, Object> option : options.entrySet()) {
String key = String.valueOf(option.getKey());
String value = String.valueOf(option.getValue());
if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
if (argString.length() > 0) {
argString.append(" ");
}
argString.append("-").append(key);
argString.append(" ");
if (value.indexOf(" ") >= 0
&& !(0 == value.indexOf("'") && (value.length() - 1) == value.lastIndexOf("'"))) {
argString.append("'").append(value).append("'");
} else {
argString.append(value);
}
}
}
return argString.toString();
}
}

View file

@ -0,0 +1,38 @@
package org.rundeck.api.util;
import org.apache.commons.lang.StringUtils;
/**
* Utility class for assertions
*
* @author Vincent Behar
*/
public class AssertUtil {
/**
* Test if the given {@link Object} is null
*
* @param object
* @param errorMessage to be used if the object is null
* @throws IllegalArgumentException if the given object is null
*/
public static void notNull(Object object, String errorMessage) throws IllegalArgumentException {
if (object == null) {
throw new IllegalArgumentException(errorMessage);
}
}
/**
* Test if the given {@link String} is blank (null, empty or only whitespace)
*
* @param input string
* @param errorMessage to be used if the string is blank
* @throws IllegalArgumentException if the given string is blank
*/
public static void notBlank(String input, String errorMessage) throws IllegalArgumentException {
if (StringUtils.isBlank(input)) {
throw new IllegalArgumentException(errorMessage);
}
}
}

View file

@ -0,0 +1,67 @@
package org.rundeck.api.parser;
import java.io.InputStream;
import java.util.Date;
import org.dom4j.Document;
import org.junit.Assert;
import org.junit.Test;
import org.rundeck.api.domain.RundeckExecution;
import org.rundeck.api.domain.RundeckJob;
import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
/**
* Test the {@link ExecutionParser}
*
* @author Vincent Behar
*/
public class ExecutionParserTest {
@Test
public void parseRunningNode() throws Exception {
InputStream input = getClass().getResourceAsStream("execution-running.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.RUNNING, execution.getStatus());
Assert.assertEquals("admin", execution.getStartedBy());
Assert.assertEquals(new Date(1302183830082L), execution.getStartedAt());
Assert.assertEquals(null, execution.getEndedAt());
Assert.assertEquals(null, execution.getAbortedBy());
Assert.assertEquals("ls ${option.dir}", execution.getDescription());
Assert.assertEquals("1", job.getId());
Assert.assertEquals("ls", job.getName());
Assert.assertEquals("system", job.getGroup());
Assert.assertEquals("test", job.getProject());
Assert.assertEquals("list files", job.getDescription());
}
@Test
public void parseSucceededNode() throws Exception {
InputStream input = getClass().getResourceAsStream("execution-succeeded.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(1308322895104L), execution.getStartedAt());
Assert.assertEquals(new Date(1308322959420L), execution.getEndedAt());
Assert.assertEquals(null, execution.getAbortedBy());
Assert.assertEquals("ls ${option.dir}", execution.getDescription());
Assert.assertEquals("1", job.getId());
Assert.assertEquals("ls", job.getName());
Assert.assertEquals("system", job.getGroup());
Assert.assertEquals("test", job.getProject());
Assert.assertEquals("list files", job.getDescription());
}
}

View file

@ -0,0 +1,65 @@
package org.rundeck.api.parser;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import org.dom4j.Document;
import org.junit.Assert;
import org.junit.Test;
import org.rundeck.api.domain.RundeckExecution;
import org.rundeck.api.domain.RundeckJob;
import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
/**
* Test the {@link ExecutionsParser}
*
* @author Vincent Behar
*/
public class ExecutionsParserTest {
@Test
public void parseNode() throws Exception {
InputStream input = getClass().getResourceAsStream("executions.xml");
Document document = ParserHelper.loadDocument(input);
List<RundeckExecution> executions = new ExecutionsParser("result/executions/execution").parseNode(document);
Assert.assertEquals(2, executions.size());
RundeckExecution exec1 = executions.get(0);
Assert.assertEquals(new Long(1), exec1.getId());
Assert.assertEquals("http://localhost:4440/execution/follow/1", exec1.getUrl());
Assert.assertEquals(ExecutionStatus.SUCCEEDED, exec1.getStatus());
Assert.assertEquals("admin", exec1.getStartedBy());
Assert.assertEquals(new Date(1308322895104L), exec1.getStartedAt());
Assert.assertEquals(new Date(1308322959420L), exec1.getEndedAt());
Assert.assertEquals(null, exec1.getAbortedBy());
Assert.assertEquals("ls ${option.dir}", exec1.getDescription());
RundeckExecution exec2 = executions.get(1);
Assert.assertEquals(new Long(2), exec2.getId());
Assert.assertEquals("http://localhost:4440/execution/follow/2", exec2.getUrl());
Assert.assertEquals(ExecutionStatus.SUCCEEDED, exec2.getStatus());
Assert.assertEquals("admin", exec2.getStartedBy());
Assert.assertEquals(new Date(1309524165388L), exec2.getStartedAt());
Assert.assertEquals(new Date(1309524174635L), exec2.getEndedAt());
Assert.assertEquals(null, exec2.getAbortedBy());
Assert.assertEquals("ls ${option.dir}", exec2.getDescription());
RundeckJob job1 = exec1.getJob();
Assert.assertEquals("1", job1.getId());
Assert.assertEquals("ls", job1.getName());
Assert.assertEquals("system", job1.getGroup());
Assert.assertEquals("test", job1.getProject());
Assert.assertEquals("list files", job1.getDescription());
RundeckJob job2 = exec2.getJob();
Assert.assertEquals("1", job2.getId());
Assert.assertEquals("ls", job2.getName());
Assert.assertEquals("system", job2.getGroup());
Assert.assertEquals("test", job2.getProject());
Assert.assertEquals("list files", job2.getDescription());
Assert.assertEquals(job1, job2);
}
}

View file

@ -0,0 +1,30 @@
package org.rundeck.api.parser;
import java.io.InputStream;
import org.dom4j.Document;
import org.junit.Assert;
import org.junit.Test;
import org.rundeck.api.domain.RundeckJob;
/**
* Test the {@link JobParser}
*
* @author Vincent Behar
*/
public class JobParserTest {
@Test
public void parseNode() throws Exception {
InputStream input = getClass().getResourceAsStream("job.xml");
Document document = ParserHelper.loadDocument(input);
RundeckJob job = new JobParser("joblist/job").parseNode(document);
Assert.assertEquals("1", job.getId());
Assert.assertEquals("job-name", job.getName());
Assert.assertEquals("job description", job.getDescription());
Assert.assertEquals("group-name", job.getGroup());
Assert.assertEquals("project-name", job.getProject());
}
}

View file

@ -0,0 +1,40 @@
package org.rundeck.api.parser;
import java.io.InputStream;
import java.util.List;
import org.dom4j.Document;
import org.junit.Assert;
import org.junit.Test;
import org.rundeck.api.domain.RundeckJob;
/**
* Test the {@link JobsParser}
*
* @author Vincent Behar
*/
public class JobsParserTest {
@Test
public void parseNode() throws Exception {
InputStream input = getClass().getResourceAsStream("jobs.xml");
Document document = ParserHelper.loadDocument(input);
List<RundeckJob> jobs = new JobsParser("result/jobs/job").parseNode(document);
Assert.assertEquals(2, jobs.size());
RundeckJob job1 = jobs.get(0);
Assert.assertEquals("1", job1.getId());
Assert.assertEquals("ls", job1.getName());
Assert.assertEquals("list files", job1.getDescription());
Assert.assertEquals("system", job1.getGroup());
Assert.assertEquals("test", job1.getProject());
RundeckJob job2 = jobs.get(1);
Assert.assertEquals("2", job2.getId());
Assert.assertEquals("ps", job2.getName());
Assert.assertEquals("list processes", job2.getDescription());
Assert.assertEquals("system", job2.getGroup());
Assert.assertEquals("test", job2.getProject());
}
}

View file

@ -0,0 +1,50 @@
package org.rundeck.api.parser;
import java.io.InputStream;
import org.dom4j.Document;
import org.junit.Assert;
import org.junit.Test;
import org.rundeck.api.RundeckApiException;
/**
* Test the {@link ParserHelper}
*
* @author Vincent Behar
*/
public class ParserHelperTest {
/**
* XML with an explicit "error" result should throw an exception
*/
@Test
public void loadErrorDocument() throws Exception {
InputStream input = getClass().getResourceAsStream("error.xml");
try {
ParserHelper.loadDocument(input);
Assert.fail("should have thrown an exception !");
} catch (RundeckApiException e) {
Assert.assertEquals("This is the error message", e.getMessage());
}
}
/**
* XML with an explicit "success" result should NOT throw an exception
*/
@Test
public void loadSuccessDocument() throws Exception {
InputStream input = getClass().getResourceAsStream("success.xml");
Document document = ParserHelper.loadDocument(input);
Assert.assertNotNull(document);
}
/**
* XML with no result should NOT throw an exception
*/
@Test
public void loadEmptyDocument() throws Exception {
InputStream input = getClass().getResourceAsStream("empty.xml");
Document document = ParserHelper.loadDocument(input);
Assert.assertNotNull(document);
}
}

View file

@ -0,0 +1,30 @@
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 @@
<whatever></whatever>

View file

@ -0,0 +1 @@
<result error="true" apiversion="1"><error><message>This is the error message</message></error></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='running'><user>admin</user><date-started unixtime='1302183830082'>2011-04-07T13:43:50Z</date-started><job id='1'><name>ls</name><group>system</group><project>test</project><description>list files</description></job><description>ls ${option.dir}</description></execution></executions></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="1308322895104">2011-06-17T15:01:35Z</date-started><date-ended unixtime="1308322959420">2011-06-17T15:02:39Z</date-ended><job id='1'><name>ls</name><group>system</group><project>test</project><description>list files</description></job><description>ls ${option.dir}</description></execution></executions></result>

View file

@ -0,0 +1 @@
<result success='true' apiversion='1'><executions count='2'><execution id='1' href='http://localhost:4440/execution/follow/1' status='succeeded'><user>admin</user><date-started unixtime="1308322895104">2011-06-17T15:01:35Z</date-started><date-ended unixtime="1308322959420">2011-06-17T15:02:39Z</date-ended><job id='1'><name>ls</name><group>system</group><project>test</project><description>list files</description></job><description>ls ${option.dir}</description></execution><execution id='2' href='http://localhost:4440/execution/follow/2' status='succeeded'><user>admin</user><date-started unixtime="1309524165388">2011-07-01T12:42:45Z</date-started><date-ended unixtime="1309524174635">2011-07-01T12:42:54Z</date-ended><job id='1'><name>ls</name><group>system</group><project>test</project><description>list files</description></job><description>ls ${option.dir}</description></execution></executions></result>

View file

@ -0,0 +1,62 @@
<joblist>
<job>
<id>1</id>
<schedule>
<time hour='20' seconds='0' minute='42' />
<weekday day='*' />
<month month='*' />
<year year='*' />
</schedule>
<loglevel>INFO</loglevel>
<sequence keepgoing='false' strategy='node-first'>
<command>
<exec>echo ${option.opt1} ${option.opt2} ${option.opt3}</exec>
</command>
<command>
<scriptargs>options</scriptargs>
<script><![CDATA[script]]></script>
</command>
</sequence>
<description>job description</description>
<name>job-name</name>
<context>
<project>project-name</project>
<options>
<option name='opt1'>
<description>description for opt1</description>
</option>
<option name='opt2' required='true' value='default value'>
<description>description for opt2</description>
</option>
<option name='opt3' enforcedvalues='true' value='value1, value2'>
<description>description for opt3</description>
<multivalued>true</multivalued>
<delimiter>|</delimiter>
</option>
<option name='opt4' regex='plop' valuesUrl='http://www.google.com'>
<description>description for opt4</description>
</option>
</options>
</context>
<notification>
<onsuccess>
<email recipients='mail1@toto.com' />
</onsuccess>
<onfailure>
<email recipients='mail2@toto.com' />
</onfailure>
</notification>
<dispatch>
<threadcount>2</threadcount>
<keepgoing>true</keepgoing>
<excludePrecedence>true</excludePrecedence>
</dispatch>
<nodefilters>
<include>
<tags>tag</tags>
<name>name</name>
</include>
</nodefilters>
<group>group-name</group>
</job>
</joblist>

View file

@ -0,0 +1 @@
<result success='true' apiversion='1'><jobs count='2'><job id='1'><name>ls</name><group>system</group><project>test</project><description>list files</description></job><job id='2'><name>ps</name><group>system</group><project>test</project><description>list processes</description></job></jobs></result>

View file

@ -0,0 +1 @@
<result success='true' apiversion='1'></result>