diff --git a/pom.xml b/pom.xml
index d284be1..eebf968 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
org.rundeck
rundeck-api-java-client
- 10.1-SNAPSHOT
+ 11.0-SNAPSHOT
jar
RunDeck API - Java Client
Java client for the RunDeck REST API
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ba52cb2..825c4ca 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -22,6 +22,22 @@
Vincent Behar
+
+ Project creation
+ Get Project configuration
+ Set Project configuration
+ Get/Set Project configuration keys
+ Delete project
+ Export project archive
+ Import project archive
+ Key file upload
+ Key file delete
+ Key file list
+ Key file get
+ API Token create
+ API Token list
+ API Token delete
+
Execution State - Retrieve workflow step and node state information
diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index daac945..71a9808 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -15,23 +15,18 @@
*/
package org.rundeck.api;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
-import org.apache.http.Header;
-import org.apache.http.HttpException;
-import org.apache.http.HttpRequest;
-import org.apache.http.HttpRequestInterceptor;
-import org.apache.http.HttpResponse;
-import org.apache.http.NameValuePair;
-import org.apache.http.ParseException;
+import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.methods.*;
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.entity.BasicHttpEntity;
+import org.apache.http.entity.EntityTemplate;
+import org.apache.http.entity.FileEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;
@@ -48,11 +43,9 @@ import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
import org.rundeck.api.parser.ParserHelper;
import org.rundeck.api.parser.XmlNodeParser;
import org.rundeck.api.util.AssertUtil;
+import org.rundeck.api.util.DocumentContentProducer;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
import java.net.ProxySelector;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
@@ -270,6 +263,27 @@ class ApiCall {
public T post(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
HttpPost httpPost = new HttpPost(client.getUrl() + client.getApiEndpoint() + apiPath);
+ return requestWithEntity(apiPath, parser, httpPost);
+ }
+ /**
+ * Execute an HTTP PUT 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 - see {@link ApiPathBuilder}
+ * @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 (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ */
+ public T put(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
+ RundeckApiLoginException, RundeckApiTokenException {
+ HttpPut httpPut = new HttpPut(client.getUrl() + client.getApiEndpoint() + apiPath);
+ return requestWithEntity(apiPath, parser, httpPut);
+ }
+
+ private T requestWithEntity(ApiPathBuilder apiPath, XmlNodeParser parser, HttpEntityEnclosingRequestBase
+ httpPost) {
if(null!= apiPath.getAccept()) {
httpPost.setHeader("Accept", apiPath.getAccept());
}
@@ -286,6 +300,18 @@ class ApiCall {
} catch (UnsupportedEncodingException e) {
throw new RundeckApiException("Unsupported encoding: " + e.getMessage(), e);
}
+ }else if(apiPath.getContentStream() !=null && apiPath.getContentType()!=null){
+ BasicHttpEntity entity = new BasicHttpEntity();
+ entity.setContent(apiPath.getContentStream());
+ entity.setContentType(apiPath.getContentType());
+ httpPost.setEntity(entity);
+ }else if(apiPath.getContentFile() !=null && apiPath.getContentType()!=null){
+ httpPost.setEntity(new FileEntity(apiPath.getContentFile(), apiPath.getContentType()));
+ }else if(apiPath.getXmlDocument()!=null) {
+ httpPost.setHeader("Content-Type", "application/xml");
+ httpPost.setEntity(new EntityTemplate(new DocumentContentProducer(apiPath.getXmlDocument())));
+ }else if(apiPath.isEmptyContent()){
+ //empty content
}else {
throw new IllegalArgumentException("No Form or Multipart entity for POST content-body");
}
@@ -308,6 +334,22 @@ class ApiCall {
RundeckApiLoginException, RundeckApiTokenException {
return execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath), parser);
}
+ /**
+ * Execute an HTTP DELETE request to the RunDeck instance, on the given path, and expect a 204 response.
+ *
+ * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
+ * @throws RundeckApiException in case of error when calling the API
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ */
+ public void delete(ApiPathBuilder apiPath) throws RundeckApiException,
+ RundeckApiLoginException, RundeckApiTokenException {
+
+ InputStream response = execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath));
+ if(null!=response){
+ throw new RundeckApiException("Unexpected Rundeck response content, expected no content!");
+ }
+ }
/**
* Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call. At the end,
@@ -323,13 +365,39 @@ class ApiCall {
private T execute(HttpRequestBase request, XmlNodeParser parser) throws RundeckApiException,
RundeckApiLoginException, RundeckApiTokenException {
// execute the request
- InputStream response = execute(request);
-
- // read and parse the response
- Document xmlDocument = ParserHelper.loadDocument(response);
- return parser.parseXmlNode(xmlDocument);
+ return new ParserHandler(parser).handle(execute(request, new ResultHandler()));
}
+ /**
+ * 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 - see {@link ApiPathBuilder}
+ * @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 (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ */
+ public int get(ApiPathBuilder apiPath, OutputStream outputStream) throws RundeckApiException,
+ RundeckApiLoginException, RundeckApiTokenException, IOException {
+ HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
+ if (null != apiPath.getAccept()) {
+ request.setHeader("Accept", apiPath.getAccept());
+ }
+ final WriteOutHandler writeOutHandler = new WriteOutHandler(outputStream);
+ Handler handler = writeOutHandler;
+ if(null!=apiPath.getRequiredContentType()){
+ handler = new RequireContentTypeHandler(apiPath.getRequiredContentType(), handler);
+ }
+ final int wrote = execute(request, handler);
+ if(writeOutHandler.thrown!=null){
+ throw writeOutHandler.thrown;
+ }
+ return wrote;
+ }
/**
* Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call.
*
@@ -339,7 +407,129 @@ class ApiCall {
* @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
* @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
*/
- private ByteArrayInputStream execute(HttpRequestBase request) throws RundeckApiException, RundeckApiLoginException,
+ private ByteArrayInputStream execute(HttpUriRequest request) throws RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException {
+ return execute(request, new ResultHandler() );
+ }
+
+ /**
+ * Handles one type into another
+ * @param
+ * @param
+ */
+ private static interface Handler{
+ public V handle(T response);
+ }
+
+ /**
+ * Handles parsing inputstream via a parser
+ * @param
+ */
+ private static class ParserHandler implements Handler {
+ XmlNodeParser parser;
+
+ private ParserHandler(XmlNodeParser parser) {
+ this.parser = parser;
+ }
+
+ @Override
+ public S handle(InputStream response) {
+ // read and parse the response
+ return parser.parseXmlNode(ParserHelper.loadDocument(response));
+ }
+ }
+
+ /**
+ * Handles writing response to an output stream
+ */
+ private static class ChainHandler implements Handler {
+ Handler chain;
+ private ChainHandler(Handler chain) {
+ this.chain=chain;
+ }
+ @Override
+ public T handle(final HttpResponse response) {
+ return chain.handle(response);
+ }
+ }
+
+ /**
+ * Handles writing response to an output stream
+ */
+ private static class RequireContentTypeHandler extends ChainHandler {
+ String contentType;
+
+ private RequireContentTypeHandler(final String contentType, final Handler chain) {
+ super(chain);
+ this.contentType = contentType;
+ }
+
+ @Override
+ public T handle(final HttpResponse response) {
+ final Header firstHeader = response.getFirstHeader("Content-Type");
+ final String[] split = firstHeader.getValue().split(";");
+ boolean matched=false;
+ for (int i = 0; i < split.length; i++) {
+ String s = split[i];
+ if (this.contentType.equalsIgnoreCase(s.trim())) {
+ matched=true;
+ break;
+ }
+ }
+ if(!matched) {
+ throw new RundeckApiException.RundeckApiHttpContentTypeException(firstHeader.getValue(),
+ this.contentType);
+ }
+ return super.handle(response);
+ }
+ }
+
+ /**
+ * Handles writing response to an output stream
+ */
+ private static class WriteOutHandler implements Handler {
+ private WriteOutHandler(OutputStream writeOut) {
+ this.writeOut = writeOut;
+ }
+
+ OutputStream writeOut;
+ IOException thrown;
+ @Override
+ public Integer handle(final HttpResponse response) {
+ try {
+ return IOUtils.copy(response.getEntity().getContent(), writeOut);
+ } catch (IOException e) {
+ thrown=e;
+ }
+ return -1;
+ }
+ }
+
+ /**
+ * Handles reading response into a byte array stream
+ */
+ private static class ResultHandler implements Handler {
+ @Override
+ public ByteArrayInputStream handle(final HttpResponse response) {
+ // return a new inputStream, so that we can close all network resources
+ try {
+ return new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
+ } catch (IOException e) {
+ throw new RundeckApiException("Failed to consume entity and convert the inputStream", e);
+ }
+ }
+ }
+ /**
+ * Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call.
+ *
+ * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
+ * @return a new {@link InputStream} instance, not linked with network resources
+ * @throws RundeckApiException in case of error when calling the API
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ */
+ private T execute(HttpUriRequest request, Handler handler) throws RundeckApiException,
+ RundeckApiLoginException,
RundeckApiTokenException {
HttpClient httpClient = instantiateHttpClient();
try {
@@ -360,7 +550,8 @@ class ApiCall {
// in case of error, we get a redirect to /api/error
// that we need to follow manually for POST and DELETE requests (as GET)
- if (response.getStatusLine().getStatusCode() / 100 == 3) {
+ int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode / 100 == 3) {
String newLocation = response.getFirstHeader("Location").getValue();
try {
EntityUtils.consume(response.getEntity());
@@ -370,33 +561,31 @@ class ApiCall {
request = new HttpGet(newLocation);
try {
response = httpClient.execute(request);
+ statusCode = response.getStatusLine().getStatusCode();
} catch (IOException e) {
throw new RundeckApiException("Failed to execute an HTTP GET on url : " + request.getURI(), e);
}
}
// check the response code (should be 2xx, even in case of error : error message is in the XML result)
- if (response.getStatusLine().getStatusCode() / 100 != 2) {
- if (response.getStatusLine().getStatusCode() == 403 &&
+ if (statusCode / 100 != 2) {
+ if (statusCode == 403 &&
(client.getToken() != null || client.getSessionID() != null)) {
throw new RundeckApiTokenException("Invalid Token or sessionID ! Got HTTP response '" + response.getStatusLine()
+ "' for " + request.getURI());
} else {
- throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' for "
- + request.getURI());
+ throw new RundeckApiException.RundeckApiHttpStatusException("Invalid HTTP response '" + response.getStatusLine() + "' for "
+ + request.getURI(), statusCode);
}
}
+ if(statusCode==204){
+ return null;
+ }
if (response.getEntity() == null) {
throw new RundeckApiException("Empty RunDeck response ! HTTP status line is : "
+ response.getStatusLine());
}
-
- // return a new inputStream, so that we can close all network resources
- try {
- return new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
- } catch (IOException e) {
- throw new RundeckApiException("Failed to consume entity and convert the inputStream", e);
- }
+ return handler.handle(response);
} finally {
httpClient.getConnectionManager().shutdown();
}
diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java
index 22643c8..7ef79ee 100644
--- a/src/main/java/org/rundeck/api/ApiPathBuilder.java
+++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java
@@ -15,6 +15,7 @@
*/
package org.rundeck.api;
+import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
@@ -26,6 +27,8 @@ import java.util.Properties;
import org.apache.commons.lang.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
+import org.dom4j.Document;
+import org.rundeck.api.generator.XmlDocumentGenerator;
import org.rundeck.api.util.ParametersUtil;
/**
@@ -43,6 +46,12 @@ class ApiPathBuilder {
/** When POSTing, we can add attachments */
private final Map attachments;
private final List form = new ArrayList();
+ private Document xmlDocument;
+ private InputStream contentStream;
+ private File contentFile;
+ private String contentType;
+ private String requiredContentType;
+ private boolean emptyContent = false;
/** Marker for using the right separator between parameters ("?" or "&") */
private boolean firstParamDone = false;
@@ -56,6 +65,10 @@ class ApiPathBuilder {
public ApiPathBuilder(String... paths) {
apiPath = new StringBuilder();
attachments = new HashMap();
+ paths(paths);
+ }
+
+ public ApiPathBuilder paths(String... paths) {
if (paths != null) {
for (String path : paths) {
if (StringUtils.isNotBlank(path)) {
@@ -63,6 +76,7 @@ class ApiPathBuilder {
}
}
}
+ return this;
}
/**
@@ -266,6 +280,69 @@ class ApiPathBuilder {
}
return this;
}
+ /**
+ * When POSTing a request, use the given {@link InputStream} as the content of the request. This
+ * will only add the stream if it is not null.
+ *
+ * @param contentType MIME content type ofr hte request
+ * @param stream content stream
+ * @return this, for method chaining
+ */
+ public ApiPathBuilder content(final String contentType, final InputStream stream) {
+ if (stream != null && contentType != null) {
+ this.contentStream=stream;
+ this.contentType=contentType;
+ }
+ return this;
+ }
+ /**
+ * When POSTing a request, use the given {@link File} as the content of the request. This
+ * will only add the stream if it is not null.
+ *
+ * @param contentType MIME content type ofr hte request
+ * @param file content from a file
+ * @return this, for method chaining
+ */
+ public ApiPathBuilder content(final String contentType, final File file) {
+ if (file != null && contentType != null) {
+ this.contentFile=file;
+ this.contentType=contentType;
+ }
+ return this;
+ }
+ /**
+ * When POSTing a request, send an empty request.
+ *
+ * @return this, for method chaining
+ */
+ public ApiPathBuilder emptyContent() {
+ this.emptyContent=true;
+ return this;
+ }
+ /**
+ * When POSTing a request, add the given XMl Document as the content of the request.
+ *
+ * @param document XMl document to send
+ * @return this, for method chaining
+ */
+ public ApiPathBuilder xml(final Document document) {
+ if (document != null) {
+ xmlDocument = document;
+ }
+ return this;
+ }
+ /**
+ * When POSTing a request, add the given XMl Document as the content of the request.
+ *
+ * @param document XMl document to send
+ * @return this, for method chaining
+ */
+ public ApiPathBuilder xml(final XmlDocumentGenerator document) {
+ if (document != null) {
+ xmlDocument = document.generateXmlDocument();
+ }
+ return this;
+ }
/**
* @return all attachments to be POSTed, with their names
@@ -311,7 +388,7 @@ class ApiPathBuilder {
* Return true if there are any Attachments or Form data for a POST request.
*/
public boolean hasPostContent() {
- return getAttachments().size() > 0 || getForm().size() > 0;
+ return getAttachments().size() > 0 || getForm().size() > 0 || null != xmlDocument;
}
/**
@@ -321,6 +398,35 @@ class ApiPathBuilder {
return accept;
}
+ public Document getXmlDocument() {
+ return xmlDocument;
+ }
+
+ public InputStream getContentStream() {
+ return contentStream;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public File getContentFile() {
+ return contentFile;
+ }
+
+ public boolean isEmptyContent() {
+ return emptyContent;
+ }
+
+ public ApiPathBuilder requireContentType(String contentType) {
+ this.requiredContentType=contentType;
+ return this;
+ }
+
+ public String getRequiredContentType() {
+ return requiredContentType;
+ }
+
/**
* BuildsParameters can add URL or POST parameters to an {@link ApiPathBuilder}
*
diff --git a/src/main/java/org/rundeck/api/RundeckApiException.java b/src/main/java/org/rundeck/api/RundeckApiException.java
index 141e905..5a2de8b 100644
--- a/src/main/java/org/rundeck/api/RundeckApiException.java
+++ b/src/main/java/org/rundeck/api/RundeckApiException.java
@@ -82,5 +82,65 @@ public class RundeckApiException extends RuntimeException {
super(message, cause);
}
}
+ /**
+ * Error due to unexpected HTTP status
+ */
+ public static class RundeckApiHttpStatusException extends RundeckApiAuthException {
+
+ private static final long serialVersionUID = 1L;
+ private int statusCode;
+
+ public RundeckApiHttpStatusException(String message, int statusCode) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+
+ public RundeckApiHttpStatusException(String message, Throwable cause, int statusCode) {
+ super(message, cause);
+ this.statusCode = statusCode;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+ }
+
+ /**
+ * Error due to unexpected HTTP content-type
+ */
+ public static class RundeckApiHttpContentTypeException extends RundeckApiAuthException {
+
+ private static final long serialVersionUID = 1L;
+ private String contentType;
+ private String requiredContentType;
+
+ public RundeckApiHttpContentTypeException(final String contentType,
+ final String requiredContentType) {
+ super("Unexpected content-type: '" + contentType + "', expected: '" + requiredContentType + "'");
+ this.contentType = contentType;
+ this.requiredContentType = requiredContentType;
+ }
+ public RundeckApiHttpContentTypeException(final String message, final String contentType,
+ final String requiredContentType) {
+ super(message);
+ this.contentType = contentType;
+ this.requiredContentType = requiredContentType;
+ }
+
+ public RundeckApiHttpContentTypeException(final String message, final Throwable cause, final String contentType,
+ final String requiredContentType) {
+ super(message, cause);
+ this.contentType = contentType;
+ this.requiredContentType = requiredContentType;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public String getRequiredContentType() {
+ return requiredContentType;
+ }
+ }
}
diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 61911bc..8462dba 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -18,25 +18,22 @@ package org.rundeck.api;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
+import org.dom4j.Document;
import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
import org.rundeck.api.domain.*;
import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
+import org.rundeck.api.generator.ProjectConfigGenerator;
+import org.rundeck.api.generator.ProjectConfigPropertyGenerator;
+import org.rundeck.api.generator.ProjectGenerator;
import org.rundeck.api.parser.*;
import org.rundeck.api.query.ExecutionQuery;
import org.rundeck.api.util.AssertUtil;
import org.rundeck.api.util.PagedResults;
import org.rundeck.api.util.ParametersUtil;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Properties;
+import java.io.*;
+import java.util.*;
import java.util.concurrent.TimeUnit;
/**
@@ -84,6 +81,8 @@ public class RundeckClient implements Serializable {
private static final long serialVersionUID = 1L;
public static final String JOBS_IMPORT = "/jobs/import";
+ public static final String STORAGE_ROOT_PATH = "/storage/";
+ public static final String STORAGE_KEYS_PATH = "keys/";
/**
* Supported version numbers
@@ -95,6 +94,7 @@ public class RundeckClient implements Serializable {
V8(8),
V9(9),
V10(10),
+ V11(11),
;
private int versionNumber;
@@ -108,7 +108,7 @@ public class RundeckClient implements Serializable {
}
}
/** Version of the API supported */
- public static final transient int API_VERSION = Version.V10.getVersionNumber();
+ public static final transient int API_VERSION = Version.V11.getVersionNumber();
private static final String API = "/api/";
@@ -274,10 +274,26 @@ public class RundeckClient implements Serializable {
testAuth();
}
+ /**
+ * Return root xpath for xml api results. for v11 and later it is empty, for earlier it is "result"
+ *
+ * @return
+ */
+ private String rootXpath() {
+ return getApiVersion() < Version.V11.getVersionNumber() ? "result" : "";
+ }
/*
* Projects
*/
+ private ProjectParser createProjectParser() {
+ return createProjectParser(null);
+ }
+
+ private ProjectParser createProjectParser(final String xpath) {
+ return new ProjectParserV11(xpath);
+ }
+
/**
* List all projects
*
@@ -289,7 +305,8 @@ public class RundeckClient implements Serializable {
public List getProjects() throws RundeckApiException, RundeckApiLoginException,
RundeckApiTokenException {
return new ApiCall(this).get(new ApiPathBuilder("/projects"),
- new ListParser(new ProjectParser(), "result/projects/project"));
+ new ListParser(createProjectParser(), rootXpath() +
+ "/projects/project"));
}
/**
@@ -306,7 +323,274 @@ public class RundeckClient implements Serializable {
RundeckApiTokenException, IllegalArgumentException {
AssertUtil.notBlank(projectName, "projectName is mandatory to get the details of a project !");
return new ApiCall(this).get(new ApiPathBuilder("/project/", projectName),
- new ProjectParser("result/projects/project"));
+ createProjectParser(rootXpath() +
+ (getApiVersion() < Version.V11.getVersionNumber()
+ ? "/projects/project"
+ : "/project"
+ )));
+ }
+
+ /**
+ * Create a new project, and return the new definition
+ *
+ * @param projectName name of the project - mandatory
+ * @param configuration project configuration properties
+ *
+ * @return a {@link RundeckProject} instance - 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 fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public RundeckProject createProject(String projectName, Map configuration) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to create a project !");
+ return new ApiCall(this)
+ .post(new ApiPathBuilder("/projects").xml(
+ projectDocument(projectName, configuration)
+ ), createProjectParser(rootXpath() +
+ (getApiVersion() < Version.V11.getVersionNumber()
+ ? "/projects/project"
+ : "/project"
+ )));
+ }
+ /**
+ * Delete a project
+ *
+ * @param projectName name of the project - mandatory
+ *
+ * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public void deleteProject(String projectName) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to create a project !");
+ new ApiCall(this).delete(new ApiPathBuilder("/project/", projectName));
+ }
+ /**
+ * Convenience method to export the archive of a project to the specified file.
+ *
+ * @param projectName name of the project - mandatory
+ * @param out file to write to
+ * @return number of bytes written to the stream
+ *
+ * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public int exportProject(final String projectName, final File out) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException, IOException {
+ final FileOutputStream fileOutputStream = new FileOutputStream(out);
+ try {
+ return exportProject(projectName, fileOutputStream);
+ }finally {
+ fileOutputStream.close();
+ }
+ }
+ /**
+ * Export the archive of a project to the specified outputstream
+ *
+ * @param projectName name of the project - mandatory
+ * @return number of bytes written to the stream
+ *
+ * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public int exportProject(String projectName, OutputStream out) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException, IOException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to export a project archive!");
+ return new ApiCall(this).get(
+ new ApiPathBuilder("/project/", projectName, "/export")
+ .accept("application/zip"),
+ out);
+ }
+
+ /**
+ * Import a archive file to the specified project.
+ *
+ * @param projectName name of the project - mandatory
+ * @param archiveFile zip archive file
+ * @param includeExecutions if true, import executions defined in the archive, otherwise skip them
+ * @param preserveJobUuids if true, do not remove UUIDs from imported jobs, otherwise remove them
+ *
+ * @return Result of the import request, may contain a list of import error messages
+ *
+ * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public ArchiveImport importArchive(final String projectName, final File archiveFile,
+ final boolean includeExecutions, final boolean preserveJobUuids) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException, IOException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to import a project archive!");
+ AssertUtil.notNull(archiveFile, "archiveFile is mandatory to import a project archive!"); ;
+ return callImportProject(projectName, includeExecutions, preserveJobUuids,
+ new ApiPathBuilder().content("application/zip", archiveFile));
+ }
+
+ private ArchiveImport callImportProject(final String projectName, final boolean includeExecutions, final boolean preserveJobUuids,
+ final ApiPathBuilder param) {
+ param.paths("/project/", projectName, "/import")
+ .param("importExecutions", includeExecutions)
+ .param("jobUuidOption", preserveJobUuids ? "preserve" : "remove");
+ return new ApiCall(this).put(
+ param,
+ new ArchiveImportParser()
+ );
+ }
+
+ /**
+ * Return the configuration of a project
+ *
+ * @param projectName name of the project - mandatory
+ *
+ * @return a {@link ProjectConfig} instance - 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 fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public ProjectConfig getProjectConfig(String projectName) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
+ return new ApiCall(this)
+ .get(new ApiPathBuilder("/project/", projectName, "/config"), new ProjectConfigParser("/config"));
+ }
+ /**
+ * Get a single project configuration key
+ *
+ * @param projectName name of the project - mandatory
+ * @param key name of the configuration key
+ *
+ * @return value, or null if the value is not set
+ *
+ * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public String getProjectConfig(final String projectName, final String key) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
+ AssertUtil.notBlank(key, "key is mandatory to get the config key value!");
+
+ ConfigProperty configProperty = null;
+ try {
+ configProperty = new ApiCall(this)
+ .get(new ApiPathBuilder("/project/", projectName, "/config/", key),
+ new ProjectConfigPropertyParser("/property"));
+ } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+ if(404==e.getStatusCode()){
+ return null;
+ }
+ throw e;
+ }
+ return configProperty.getValue();
+ }
+ /**
+ * Set a single project configuration property value
+ *
+ * @param projectName name of the project - mandatory
+ * @param key name of the configuration property
+ * @param value value of the property
+ *
+ * @return new value
+ *
+ * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public String setProjectConfig(final String projectName, final String key, final String value) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to set the config of a project !");
+ AssertUtil.notBlank(key, "key is mandatory to set the config key value!");
+ AssertUtil.notBlank(value, "value is mandatory to set the config key value!");
+
+ final ConfigProperty configProperty = new ApiCall(this)
+ .put(new ApiPathBuilder("/project/", projectName, "/config/", key)
+ .xml(new ProjectConfigPropertyGenerator(new ConfigProperty(key, value))),
+ new ProjectConfigPropertyParser("/property"));
+
+ return configProperty.getValue();
+ }
+ /**
+ * Set a single project configuration property value
+ *
+ * @param projectName name of the project - mandatory
+ * @param key name of the configuration property
+ * @param value value of the property
+ *
+ * @return new value
+ *
+ * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
+ * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public void deleteProjectConfig(final String projectName, final String key) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to set the config of a project !");
+ AssertUtil.notBlank(key, "key is mandatory to set the config key value!");
+
+ new ApiCall(this).delete(new ApiPathBuilder("/project/", projectName, "/config/",
+ key).accept("application/xml"));
+ }
+ /**
+ * Return the configuration of a project
+ *
+ * @param projectName name of the project - mandatory
+ *
+ * @return a {@link ProjectConfig} instance - 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 fails (in case of login-based authentication)
+ * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+ * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+ */
+ public ProjectConfig setProjectConfig(String projectName, Map configuration) throws
+ RundeckApiException, RundeckApiLoginException,
+ RundeckApiTokenException, IllegalArgumentException {
+
+ AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
+ return new ApiCall(this)
+ .put(new ApiPathBuilder("/project/", projectName, "/config")
+ .xml(new ProjectConfigGenerator(new ProjectConfig(configuration)))
+ , new ProjectConfigParser("/config"));
+ }
+
+ private Document projectDocument(String projectName, Map configuration) {
+ RundeckProject project = new RundeckProject();
+ project.setName(projectName);
+ if (null != configuration) {
+ project.setProjectConfig(new ProjectConfig(configuration));
+ }
+ return new ProjectGenerator(project).generateXmlDocument();
}
/*
@@ -366,7 +650,7 @@ public class RundeckClient implements Serializable {
.param("jobFilter", jobFilter)
.param("groupPath", groupPath)
.param("idlist", StringUtils.join(jobIds, ",")),
- new ListParser(new JobParser(), "result/jobs/job"));
+ new ListParser(new JobParser(), rootXpath()+"/jobs/job"));
}
/**
@@ -699,7 +983,7 @@ public class RundeckClient implements Serializable {
//API v8
request.param("project", rundeckJobsImport.getProject());
}
- return new ApiCall(this).post(request, new JobsImportResultParser("result"));
+ return new ApiCall(this).post(request, new JobsImportResultParser(rootXpath()));
}
/**
@@ -755,7 +1039,7 @@ public class RundeckClient implements Serializable {
public String deleteJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
RundeckApiTokenException, IllegalArgumentException {
AssertUtil.notBlank(jobId, "jobId is mandatory to delete a job !");
- return new ApiCall(this).delete(new ApiPathBuilder("/job/", jobId), new StringParser("result/success/message"));
+ return new ApiCall(this).delete(new ApiPathBuilder("/job/", jobId), new StringParser(rootXpath()+"/success/message"));
}
/**
* Delete multiple jobs, identified by the given IDs
@@ -773,7 +1057,7 @@ public class RundeckClient implements Serializable {
throw new IllegalArgumentException("jobIds are mandatory to delete a job");
}
return new ApiCall(this).post(new ApiPathBuilder("/jobs/delete").field("ids",jobIds),
- new BulkDeleteParser("result/deleteJobs"));
+ new BulkDeleteParser(rootXpath()+"/deleteJobs"));
}
/**
@@ -798,7 +1082,7 @@ public class RundeckClient implements Serializable {
if(null!=jobRun.getAsUser()) {
apiPath.param("asUser", jobRun.getAsUser());
}
- return new ApiCall(this).get(apiPath, new ExecutionParser("result/executions/execution"));
+ return new ApiCall(this).get(apiPath, new ExecutionParser(rootXpath()+"/executions/execution"));
}
@@ -896,7 +1180,7 @@ public class RundeckClient implements Serializable {
if(null!= command.getAsUser()) {
apiPath.param("asUser", command.getAsUser());
}
- RundeckExecution execution = new ApiCall(this).get(apiPath, new ExecutionParser("result/execution"));
+ RundeckExecution execution = new ApiCall(this).get(apiPath, new ExecutionParser(rootXpath()+"/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());
}
@@ -1030,7 +1314,7 @@ public class RundeckClient implements Serializable {
if(null!=script.getAsUser()) {
apiPath.param("asUser", script.getAsUser());
}
- RundeckExecution execution = new ApiCall(this).post(apiPath, new ExecutionParser("result/execution"));
+ RundeckExecution execution = new ApiCall(this).post(apiPath, new ExecutionParser(rootXpath()+"/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());
}
@@ -1177,7 +1461,7 @@ public class RundeckClient implements Serializable {
AssertUtil.notBlank(project, "project is mandatory get all running executions !");
return new ApiCall(this).get(new ApiPathBuilder("/executions/running").param("project", project),
new ListParser(new ExecutionParser(),
- "result/executions/execution"));
+ rootXpath()+"/executions/execution"));
}
/**
@@ -1275,7 +1559,7 @@ public class RundeckClient implements Serializable {
.param("max", max)
.param("offset", offset),
new ListParser(new ExecutionParser(),
- "result/executions/execution"));
+ rootXpath()+"/executions/execution"));
}
/**
@@ -1302,7 +1586,7 @@ public class RundeckClient implements Serializable {
.param("offset", offset),
new PagedResultParser(
new ListParser(new ExecutionParser(), "execution"),
- "result/executions"
+ rootXpath()+"/executions"
)
);
}
@@ -1321,7 +1605,7 @@ public class RundeckClient implements Serializable {
RundeckApiTokenException, IllegalArgumentException {
AssertUtil.notNull(executionId, "executionId is mandatory to get the details of an execution !");
return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString()),
- new ExecutionParser("result/executions/execution"));
+ new ExecutionParser(rootXpath()+"/executions/execution"));
}
/**
@@ -1356,7 +1640,7 @@ public class RundeckClient implements Serializable {
if(null!=asUser) {
apiPath.param("asUser", asUser);
}
- return new ApiCall(this).get(apiPath, new AbortParser("result/abort"));
+ return new ApiCall(this).get(apiPath, new AbortParser(rootXpath()+"/abort"));
}
/*
@@ -1548,7 +1832,7 @@ public class RundeckClient implements Serializable {
.param("end", end)
.param("max", max)
.param("offset", offset),
- new HistoryParser("result/events"));
+ new HistoryParser(rootXpath()+"/events"));
}
/**
@@ -1595,7 +1879,7 @@ public class RundeckClient implements Serializable {
.param("max", max)
.param("offset", offset);
- return new ApiCall(this).postOrGet(builder, new HistoryParser("result/events"));
+ return new ApiCall(this).postOrGet(builder, new HistoryParser(rootXpath()+"/events"));
}
/*
@@ -1773,7 +2057,7 @@ public class RundeckClient implements Serializable {
param.param("maxlines", maxlines);
}
return new ApiCall(this).get(param,
- new OutputParser("result/output", createOutputEntryParser()));
+ new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
}
/**
* Get the execution state of the given execution
@@ -1792,7 +2076,7 @@ public class RundeckClient implements Serializable {
"/execution/", executionId.toString(),
"/state");
- return new ApiCall(this).get(param, new ExecutionStateParser("result/executionState"));
+ return new ApiCall(this).get(param, new ExecutionStateParser(rootXpath()+"/executionState"));
}
/**
@@ -1834,7 +2118,7 @@ public class RundeckClient implements Serializable {
param.param("maxlines", maxlines);
}
return new ApiCall(this).get(param,
- new OutputParser("result/output", createOutputEntryParser()));
+ new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
}
/**
* Get the execution output of the given execution for the specified step
@@ -1875,7 +2159,7 @@ public class RundeckClient implements Serializable {
param.param("maxlines", maxlines);
}
return new ApiCall(this).get(param,
- new OutputParser("result/output", createOutputEntryParser()));
+ new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
}
/**
* Get the execution output of the given execution for the specified step
@@ -1919,7 +2203,7 @@ public class RundeckClient implements Serializable {
param.param("maxlines", maxlines);
}
return new ApiCall(this).get(param,
- new OutputParser("result/output", createOutputEntryParser()));
+ new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
}
@@ -1966,7 +2250,7 @@ public class RundeckClient implements Serializable {
if (maxlines > 0) {
param.param("maxlines", maxlines);
}
- return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser()));
+ return new ApiCall(this).get(param, new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
}
/**
* Get the execution state output sequence of the given job
@@ -1997,7 +2281,7 @@ public class RundeckClient implements Serializable {
if(stateOnly) {
param.param("stateOnly", true);
}
- return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser()));
+ return new ApiCall(this).get(param, new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
}
private OutputEntryParser createOutputEntryParser() {
@@ -2023,7 +2307,204 @@ public class RundeckClient implements Serializable {
*/
public RundeckSystemInfo getSystemInfo() throws RundeckApiException, RundeckApiLoginException,
RundeckApiTokenException {
- return new ApiCall(this).get(new ApiPathBuilder("/system/info"), new SystemInfoParser("result/system"));
+ return new ApiCall(this).get(new ApiPathBuilder("/system/info"), new SystemInfoParser(rootXpath()+"/system"));
+ }
+
+
+ /*
+ * API token
+ */
+
+ /**
+ * List API tokens for a user.
+ * @param user username
+ * @return list of tokens
+ * @throws RundeckApiException
+ */
+ public List listApiTokens(final String user) throws RundeckApiException {
+ AssertUtil.notNull(user, "user is mandatory to list API tokens for a user.");
+ return new ApiCall(this).
+ get(new ApiPathBuilder("/tokens/", user),
+ new ListParser(new RundeckTokenParser(), "/tokens/token"));
+ }
+
+ /**
+ * List all API tokens
+ * @return list of tokens
+ * @throws RundeckApiException
+ */
+ public List listApiTokens() throws RundeckApiException {
+ return new ApiCall(this).
+ get(new ApiPathBuilder("/tokens"),
+ new ListParser(new RundeckTokenParser(), "/tokens/token"));
+ }
+
+ /**
+ * Generate an API token for a user.
+ * @param user
+ * @return
+ * @throws RundeckApiException
+ */
+ public String generateApiToken(final String user) throws RundeckApiException{
+ AssertUtil.notNull(user, "user is mandatory to generate an API token for a user.");
+ RundeckToken result = new ApiCall(this).
+ post(new ApiPathBuilder("/tokens/", user).emptyContent(),
+ new RundeckTokenParser("/token"));
+ return result.getToken();
+ }
+ /**
+ * Delete an existing token
+ * @param token
+ * @return
+ * @throws RundeckApiException
+ */
+ public boolean deleteApiToken(final String token) throws RundeckApiException{
+ AssertUtil.notNull(token, "token is mandatory to delete an API token.");
+ new ApiCall(this).delete(new ApiPathBuilder("/token/", token));
+ return true;
+ }
+ /**
+ * Return user info for an existing token
+ * @param token
+ * @return token info
+ * @throws RundeckApiException
+ */
+ public RundeckToken getApiToken(final String token) throws RundeckApiException{
+ AssertUtil.notNull(token, "token is mandatory to get an API token.");
+ return new ApiCall(this).get(new ApiPathBuilder("/token/", token), new RundeckTokenParser("/token"));
+ }
+
+ /**
+ * Store an key file
+ * @param path ssh key storage path, must start with "keys/"
+ * @param keyfile key file
+ * @param privateKey true to store private key, false to store public key
+ * @return the key resource
+ * @throws RundeckApiException
+ */
+ public KeyResource storeKey(final String path, final File keyfile, boolean privateKey) throws RundeckApiException{
+ AssertUtil.notNull(path, "path is mandatory to store an key.");
+ AssertUtil.notNull(keyfile, "keyfile is mandatory to store an key.");
+ if (!path.startsWith(STORAGE_KEYS_PATH)) {
+ throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
+ }
+ return new ApiCall(this).post(
+ new ApiPathBuilder(STORAGE_ROOT_PATH, path).content(
+ privateKey ? "application/octet-stream" : "application/pgp-keys",
+ keyfile
+ ),
+ new SSHKeyResourceParser("/resource")
+ );
+ }
+
+ /**
+ * Get metadata for an key file
+ *
+ * @param path ssh key storage path, must start with "keys/"
+ *
+ * @return the ssh key resource
+ *
+ * @throws RundeckApiException if there is an error, or if the path is a directory not a file
+ */
+ public KeyResource getKey(final String path) throws RundeckApiException {
+ AssertUtil.notNull(path, "path is mandatory to get an key.");
+ if (!path.startsWith(STORAGE_KEYS_PATH)) {
+ throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
+ }
+ KeyResource storageResource = new ApiCall(this).get(
+ new ApiPathBuilder(STORAGE_ROOT_PATH, path),
+ new SSHKeyResourceParser("/resource")
+ );
+ if (storageResource.isDirectory()) {
+ throw new RundeckApiException("Key Path is a directory: " + path);
+ }
+ return storageResource;
+ }
+
+ /**
+ * Get content for a public key file
+ * @param path ssh key storage path, must start with "keys/"
+ * @param out outputstream to write data to
+ *
+ * @return length of written data
+ * @throws RundeckApiException
+ */
+ public int getPublicKeyContent(final String path, final OutputStream out) throws
+ RundeckApiException, IOException {
+ AssertUtil.notNull(path, "path is mandatory to get an key.");
+ if (!path.startsWith(STORAGE_KEYS_PATH)) {
+ throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
+ }
+ try {
+ return new ApiCall(this).get(
+ new ApiPathBuilder(STORAGE_ROOT_PATH, path)
+ .accept("application/pgp-keys")
+ .requireContentType("application/pgp-keys"),
+ out
+ );
+ } catch (RundeckApiException.RundeckApiHttpContentTypeException e) {
+ throw new RundeckApiException("Requested Key path was not a Public key: " + path, e);
+ }
+ }
+
+ /**
+ * Get content for a public key file
+ * @param path ssh key storage path, must start with "keys/"
+ * @param out file to write data to
+ * @return length of written data
+ * @throws RundeckApiException
+ */
+ public int getPublicKeyContent(final String path, final File out) throws
+ RundeckApiException, IOException {
+ final FileOutputStream fileOutputStream = new FileOutputStream(out);
+ try {
+ return getPublicKeyContent(path, fileOutputStream);
+ }finally {
+ fileOutputStream.close();
+ }
+ }
+
+ /**
+ * List contents of root key directory
+ *
+ * @return list of key resources
+ * @throws RundeckApiException
+ */
+ public List listKeyDirectoryRoot() throws RundeckApiException {
+ return listKeyDirectory(STORAGE_KEYS_PATH);
+ }
+ /**
+ * List contents of key directory
+ *
+ * @param path ssh key storage path, must start with "keys/"
+ *
+ * @throws RundeckApiException if there is an error, or if the path is a file not a directory
+ */
+ public List listKeyDirectory(final String path) throws RundeckApiException {
+ AssertUtil.notNull(path, "path is mandatory to get an key.");
+ if (!path.startsWith(STORAGE_KEYS_PATH)) {
+ throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
+ }
+ KeyResource storageResource = new ApiCall(this).get(
+ new ApiPathBuilder(STORAGE_ROOT_PATH, path),
+ new SSHKeyResourceParser("/resource")
+ );
+ if(!storageResource.isDirectory()) {
+ throw new RundeckApiException("key path is not a directory path: " + path);
+ }
+ return storageResource.getDirectoryContents();
+ }
+
+ /**
+ * Delete an key file
+ * @param path a path to a key file, must start with "keys/"
+ */
+ public void deleteKey(final String path){
+ AssertUtil.notNull(path, "path is mandatory to delete an key.");
+ if (!path.startsWith(STORAGE_KEYS_PATH)) {
+ throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
+ }
+ new ApiCall(this).delete(new ApiPathBuilder(STORAGE_ROOT_PATH, path));
}
/**
diff --git a/src/main/java/org/rundeck/api/domain/ArchiveImport.java b/src/main/java/org/rundeck/api/domain/ArchiveImport.java
new file mode 100644
index 0000000..7be8112
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/ArchiveImport.java
@@ -0,0 +1,38 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+
+/**
+ * ArchiveImport describes the result of an {@link org.rundeck.api.RundeckClient#importArchive(String, java.io.File,
+ * boolean, boolean)} request.
+ *
+ * @author greg
+ * @since 2014-03-09
+ */
+public class ArchiveImport {
+ private boolean successful;
+ private List errorMessages;
+
+ public ArchiveImport(final boolean successful, final List errorMessages) {
+ this.successful = successful;
+ this.errorMessages = errorMessages;
+ }
+
+ /**
+ * Return true if successful
+ * @return
+ */
+ public boolean isSuccessful() {
+ return successful;
+ }
+
+
+ /**
+ * Return a list of error messages if unsuccessful
+ * @return
+ */
+ public List getErrorMessages() {
+ return errorMessages;
+ }
+
+}
diff --git a/src/main/java/org/rundeck/api/domain/BaseKeyResource.java b/src/main/java/org/rundeck/api/domain/BaseKeyResource.java
new file mode 100644
index 0000000..c1ed3b7
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/BaseKeyResource.java
@@ -0,0 +1,58 @@
+package org.rundeck.api.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * BaseKeyResource is ...
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public class BaseKeyResource extends BaseStorageResource implements KeyResource {
+ private boolean privateKey;
+
+ public BaseKeyResource() {
+ }
+
+
+ public boolean isPrivateKey() {
+ return privateKey;
+ }
+
+ public void setPrivateKey(boolean privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ ArrayList keyResources = new ArrayList();
+
+ @Override
+ public void setDirectoryContents(List extends StorageResource> directoryContents) {
+ for (StorageResource directoryContent : directoryContents) {
+ keyResources.add(from(directoryContent));
+ }
+ }
+
+ @Override
+ public List getDirectoryContents() {
+ return keyResources;
+ }
+
+ public static BaseKeyResource from(final StorageResource source) {
+ final BaseKeyResource baseSshKeyResource = new BaseKeyResource();
+ baseSshKeyResource.setDirectory(source.isDirectory());
+ baseSshKeyResource.setPath(source.getPath());
+ baseSshKeyResource.setName(source.getName());
+ baseSshKeyResource.setMetadata(source.getMetadata());
+ baseSshKeyResource.setUrl(source.getUrl());
+ if (!baseSshKeyResource.isDirectory()) {
+ baseSshKeyResource.setPrivateKey(
+ null != baseSshKeyResource.getMetadata() && "private".equals(baseSshKeyResource.getMetadata().get
+ ("Rundeck-key-type"))
+ );
+ } else if (null != source.getDirectoryContents()) {
+ baseSshKeyResource.setDirectoryContents(source.getDirectoryContents());
+ }
+ return baseSshKeyResource;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/domain/BaseStorageResource.java b/src/main/java/org/rundeck/api/domain/BaseStorageResource.java
new file mode 100644
index 0000000..6639ab4
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/BaseStorageResource.java
@@ -0,0 +1,86 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * BaseStorageResource is ...
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public class BaseStorageResource implements StorageResource {
+ private String path;
+ private String url;
+ private String name;
+ private Map metadata;
+ private boolean directory;
+ private List extends StorageResource> directoryContents;
+
+ public BaseStorageResource() {
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return directory;
+ }
+
+ public void setDirectory(boolean directory) {
+ this.directory = directory;
+ }
+
+ @Override
+ public List extends StorageResource> getDirectoryContents() {
+ return directoryContents;
+ }
+
+ public void setDirectoryContents(List extends StorageResource> directoryContents) {
+ this.directoryContents = directoryContents;
+ }
+
+ @Override
+ public String toString() {
+ return "BaseStorageResource{" +
+ "path='" + path + '\'' +
+ ", url='" + url + '\'' +
+ ", name='" + name + '\'' +
+ ", directory=" + directory +
+ '}';
+ }
+}
diff --git a/src/main/java/org/rundeck/api/domain/ConfigProperty.java b/src/main/java/org/rundeck/api/domain/ConfigProperty.java
new file mode 100644
index 0000000..d15d773
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/ConfigProperty.java
@@ -0,0 +1,68 @@
+package org.rundeck.api.domain;
+
+import java.io.Serializable;
+
+/**
+ * ConfigProperty is a single configuration property key and value.
+ *
+ * @author greg
+ * @since 2014-03-07
+ */
+public class ConfigProperty implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private String key;
+ private String value;
+
+ public ConfigProperty() {
+ }
+
+ public ConfigProperty(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ConfigProperty)) return false;
+
+ ConfigProperty that = (ConfigProperty) o;
+
+ if (!key.equals(that.key)) return false;
+ if (!value.equals(that.value)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = key.hashCode();
+ result = 31 * result + value.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ConfigProperty{" +
+ "key='" + key + '\'' +
+ ", value='" + value + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/org/rundeck/api/domain/KeyResource.java b/src/main/java/org/rundeck/api/domain/KeyResource.java
new file mode 100644
index 0000000..ff1f7e1
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/KeyResource.java
@@ -0,0 +1,23 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+
+/**
+ * KeyResource represents a directory or an SSH key file
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public interface KeyResource extends StorageResource {
+ /**
+ * Return true if this is a file and is a private SSH key file.
+ * @return
+ */
+ public boolean isPrivateKey();
+
+ /**
+ * Return the list of SSH Key resources if this is a directory
+ * @return
+ */
+ public List getDirectoryContents();
+}
diff --git a/src/main/java/org/rundeck/api/domain/ProjectConfig.java b/src/main/java/org/rundeck/api/domain/ProjectConfig.java
new file mode 100644
index 0000000..ca3548f
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/ProjectConfig.java
@@ -0,0 +1,64 @@
+package org.rundeck.api.domain;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * ProjectConfig is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfig implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private LinkedHashMap properties = new LinkedHashMap();
+
+ public ProjectConfig() {
+ }
+
+ public ProjectConfig(Map properties) {
+ setProperties(properties);
+ }
+
+ public void setProperty(final String key, final String value) {
+ getProperties().put(key, value);
+ }
+
+ public void addProperties(final Map values) {
+ getProperties().putAll(values);
+ }
+
+ public Map getProperties() {
+ return properties;
+ }
+
+ public void setProperties(final Map properties) {
+ this.properties = new LinkedHashMap(properties);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ProjectConfig)) return false;
+
+ ProjectConfig that = (ProjectConfig) o;
+
+ if (properties != null ? !properties.equals(that.properties) : that.properties != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return properties != null ? properties.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return "ProjectConfig{" +
+ "properties=" + properties +
+ '}';
+ }
+}
diff --git a/src/main/java/org/rundeck/api/domain/RundeckProject.java b/src/main/java/org/rundeck/api/domain/RundeckProject.java
index c5e3fa4..32e38ea 100644
--- a/src/main/java/org/rundeck/api/domain/RundeckProject.java
+++ b/src/main/java/org/rundeck/api/domain/RundeckProject.java
@@ -32,6 +32,8 @@ public class RundeckProject implements Serializable {
private String resourceModelProviderUrl;
+ private ProjectConfig projectConfig;
+
public String getName() {
return name;
}
@@ -56,10 +58,20 @@ public class RundeckProject implements Serializable {
this.resourceModelProviderUrl = resourceModelProviderUrl;
}
+ public ProjectConfig getProjectConfig() {
+ return projectConfig;
+ }
+
+ public void setProjectConfig(ProjectConfig projectConfig) {
+ this.projectConfig = projectConfig;
+ }
+
@Override
public String toString() {
- return "RundeckProject [name=" + name + ", description=" + description + ", resourceModelProviderUrl="
- + resourceModelProviderUrl + "]";
+ return "RundeckProject [name=" + name + ", description=" + description
+ + (null!=resourceModelProviderUrl? ", resourceModelProviderUrl=" + resourceModelProviderUrl : "")
+ + ", config="
+ + projectConfig + "]";
}
@Override
@@ -69,6 +81,7 @@ public class RundeckProject implements Serializable {
result = prime * result + ((description == null) ? 0 : description.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((resourceModelProviderUrl == null) ? 0 : resourceModelProviderUrl.hashCode());
+ result = prime * result + ((projectConfig == null) ? 0 : projectConfig.hashCode());
return result;
}
@@ -96,7 +109,11 @@ public class RundeckProject implements Serializable {
return false;
} else if (!resourceModelProviderUrl.equals(other.resourceModelProviderUrl))
return false;
+ if (projectConfig == null) {
+ if (other.projectConfig != null)
+ return false;
+ } else if (!projectConfig.equals(other.projectConfig))
+ return false;
return true;
}
-
}
diff --git a/src/main/java/org/rundeck/api/domain/RundeckToken.java b/src/main/java/org/rundeck/api/domain/RundeckToken.java
new file mode 100644
index 0000000..613fc82
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/RundeckToken.java
@@ -0,0 +1,36 @@
+package org.rundeck.api.domain;
+
+/**
+ * RundeckToken is ...
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public class RundeckToken {
+ private String user;
+ private String token;
+
+ public RundeckToken() {
+ }
+
+ public RundeckToken(String user, String token) {
+ this.setUser(user);
+ this.setToken(token);
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/domain/StorageResource.java b/src/main/java/org/rundeck/api/domain/StorageResource.java
new file mode 100644
index 0000000..be02412
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/StorageResource.java
@@ -0,0 +1,54 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * StorageResource represents a directory or a file
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public interface StorageResource {
+ /**
+ * Return the storage path for this resource
+ *
+ * @return
+ */
+ public String getPath();
+
+ /**
+ * Return the URL for this resource
+ *
+ * @return
+ */
+ public String getUrl();
+
+ /**
+ * Return the file name if this is a file
+ *
+ * @return
+ */
+ public String getName();
+
+ /**
+ * Return the metadata for this file if this is a file
+ *
+ * @return
+ */
+ public Map getMetadata();
+
+ /**
+ * Return true if this is a directory, false if this is a file
+ *
+ * @return
+ */
+ public boolean isDirectory();
+
+ /**
+ * Return the list of directory contents if this is a directory
+ *
+ * @return
+ */
+ public List extends StorageResource> getDirectoryContents();
+}
diff --git a/src/main/java/org/rundeck/api/generator/BaseDocGenerator.java b/src/main/java/org/rundeck/api/generator/BaseDocGenerator.java
new file mode 100644
index 0000000..46bf574
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/BaseDocGenerator.java
@@ -0,0 +1,18 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.Document;
+import org.dom4j.DocumentFactory;
+
+/**
+ * BaseDocGenerator generates a document using the element as the root.
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public abstract class BaseDocGenerator implements XmlDocumentGenerator {
+ @Override
+ public Document generateXmlDocument() {
+ return DocumentFactory.getInstance().createDocument(generateXmlElement());
+ }
+
+}
diff --git a/src/main/java/org/rundeck/api/generator/ProjectConfigGenerator.java b/src/main/java/org/rundeck/api/generator/ProjectConfigGenerator.java
new file mode 100644
index 0000000..547b154
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/ProjectConfigGenerator.java
@@ -0,0 +1,34 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.Document;
+import org.dom4j.DocumentFactory;
+import org.dom4j.Element;
+import org.rundeck.api.domain.ProjectConfig;
+
+/**
+ * ProjectConfigGenerator is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfigGenerator extends BaseDocGenerator {
+ private ProjectConfig config;
+
+ public ProjectConfigGenerator(ProjectConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public Element generateXmlElement() {
+ Element configEl = DocumentFactory.getInstance().createElement("config");
+ if (null != config.getProperties()) {
+ for (String s : config.getProperties().keySet()) {
+ Element property = configEl.addElement("property");
+ property.addAttribute("key", s);
+ property.addAttribute("value", config.getProperties().get(s));
+ }
+ }
+ return configEl;
+ }
+
+}
diff --git a/src/main/java/org/rundeck/api/generator/ProjectConfigPropertyGenerator.java b/src/main/java/org/rundeck/api/generator/ProjectConfigPropertyGenerator.java
new file mode 100644
index 0000000..f41766b
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/ProjectConfigPropertyGenerator.java
@@ -0,0 +1,28 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.DocumentFactory;
+import org.dom4j.Element;
+import org.rundeck.api.domain.ConfigProperty;
+
+/**
+ * ProjectConfigPropertyGenerator generates a {@literal } element representing a configuration property.
+ *
+ * @author greg
+ * @since 2014-03-07
+ */
+public class ProjectConfigPropertyGenerator extends BaseDocGenerator {
+ private ConfigProperty property;
+
+ public ProjectConfigPropertyGenerator(ConfigProperty property) {
+ this.property = property;
+ }
+
+ @Override
+ public Element generateXmlElement() {
+ Element propElem = DocumentFactory.getInstance().createElement("property");
+ propElem.addAttribute("key", property.getKey());
+ propElem.addAttribute("value", property.getValue());
+
+ return propElem;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/generator/ProjectGenerator.java b/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
new file mode 100644
index 0000000..8960acf
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
@@ -0,0 +1,32 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.Document;
+import org.dom4j.DocumentFactory;
+import org.dom4j.Element;
+import org.rundeck.api.domain.ProjectConfig;
+import org.rundeck.api.domain.RundeckProject;
+
+/**
+ * ProjectGenerator is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectGenerator extends BaseDocGenerator {
+ RundeckProject project;
+
+ public ProjectGenerator(RundeckProject project) {
+ this.project = project;
+ }
+
+ @Override
+ public Element generateXmlElement() {
+ Element rootElem = DocumentFactory.getInstance().createElement("project");
+ rootElem.addElement("name").setText(project.getName());
+ ProjectConfig configuration = project.getProjectConfig();
+ if (null != configuration) {
+ rootElem.add(new ProjectConfigGenerator(configuration).generateXmlElement());
+ }
+ return rootElem;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/generator/XmlDocumentGenerator.java b/src/main/java/org/rundeck/api/generator/XmlDocumentGenerator.java
new file mode 100644
index 0000000..7d3faf3
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/XmlDocumentGenerator.java
@@ -0,0 +1,29 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.Document;
+import org.dom4j.Element;
+import org.dom4j.Node;
+
+/**
+ * XmlDocumentGenerator is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public interface XmlDocumentGenerator {
+
+ /**
+ * Generate the XML {@link org.dom4j.Node}
+ *
+ * @return any object holding the converted value
+ */
+ Element generateXmlElement();
+ /**
+ * Generate the XML {@link org.dom4j.Node}
+ *
+ * @param node
+ *
+ * @return any object holding the converted value
+ */
+ Document generateXmlDocument();
+}
diff --git a/src/main/java/org/rundeck/api/parser/ArchiveImportParser.java b/src/main/java/org/rundeck/api/parser/ArchiveImportParser.java
new file mode 100644
index 0000000..b4e7bda
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/ArchiveImportParser.java
@@ -0,0 +1,36 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.ArchiveImport;
+
+import java.util.ArrayList;
+
+/**
+ * ArchiveImportParser is ...
+ *
+ * @author greg
+ * @since 2014-03-09
+ */
+public class ArchiveImportParser implements XmlNodeParser {
+ String xpath;
+
+ public ArchiveImportParser() {
+ }
+
+ public ArchiveImportParser(final String xpath) {
+ this.xpath = xpath;
+ }
+
+ @Override
+ public ArchiveImport parseXmlNode(final Node node) {
+ final Node importNode = xpath != null ? node.selectSingleNode(xpath) : node;
+
+ boolean issuccess = "successful".equals(importNode.valueOf("/import/@status"));
+ final ArrayList messages = new ArrayList();
+ for (final Object o : importNode.selectNodes("/import/errors/error")) {
+ messages.add(((Node) o).getText());
+ }
+
+ return new ArchiveImport(issuccess, messages);
+ }
+}
diff --git a/src/main/java/org/rundeck/api/parser/BaseXpathParser.java b/src/main/java/org/rundeck/api/parser/BaseXpathParser.java
new file mode 100644
index 0000000..26be541
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/BaseXpathParser.java
@@ -0,0 +1,29 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+
+/**
+ * BaseXpathParser is ...
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public abstract class BaseXpathParser implements XmlNodeParser {
+ private String xpath;
+
+ public BaseXpathParser() {
+ }
+
+ public BaseXpathParser(String xpath) {
+
+ this.xpath = xpath;
+ }
+
+ public abstract T parse(Node node);
+
+ @Override
+ public T parseXmlNode(Node node) {
+ Node selectedNode = xpath != null ? node.selectSingleNode(xpath) : node;
+ return parse(selectedNode);
+ }
+}
diff --git a/src/main/java/org/rundeck/api/parser/ProjectConfigParser.java b/src/main/java/org/rundeck/api/parser/ProjectConfigParser.java
new file mode 100644
index 0000000..7e92ffe
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/ProjectConfigParser.java
@@ -0,0 +1,43 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.ProjectConfig;
+
+import java.util.List;
+
+/**
+ * ProjectConfigParser parses project "config" element contents
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfigParser implements XmlNodeParser {
+ private String xpath;
+
+ public ProjectConfigParser() {
+ }
+
+ public ProjectConfigParser(String xpath) {
+ this.xpath = xpath;
+ }
+
+ @Override
+ public ProjectConfig parseXmlNode(Node node) {
+ Node config1 = getXpath() != null ? node.selectSingleNode(getXpath()) : node;
+ ProjectConfig config = new ProjectConfig();
+ List property = config1.selectNodes("property");
+ for (Object o : property) {
+ Node propnode = (Node) o;
+ String key = propnode.valueOf("@key");
+ String value = propnode.valueOf("@value");
+ if (null != key && null != value) {
+ config.setProperty(key, value);
+ }
+ }
+ return config;
+ }
+
+ public String getXpath() {
+ return xpath;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/parser/ProjectConfigPropertyParser.java b/src/main/java/org/rundeck/api/parser/ProjectConfigPropertyParser.java
new file mode 100644
index 0000000..9a57114
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/ProjectConfigPropertyParser.java
@@ -0,0 +1,41 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.ConfigProperty;
+
+/**
+ * ProjectConfigPropertyParser parses a {@literal } element representing
+ * a configuration property.
+ *
+ * @author greg
+ * @since 2014-03-07
+ */
+public class ProjectConfigPropertyParser implements XmlNodeParser {
+ private String xpath;
+
+ public ProjectConfigPropertyParser() {
+ }
+
+ public ProjectConfigPropertyParser(final String xpath) {
+ this.setXpath(xpath);
+ }
+
+ @Override
+ public ConfigProperty parseXmlNode(final Node node) {
+ final Node propnode = getXpath() != null ? node.selectSingleNode(getXpath()) : node;
+ final String key = propnode.valueOf("@key");
+ final String value = propnode.valueOf("@value");
+ final ConfigProperty config = new ConfigProperty();
+ config.setKey(key);
+ config.setValue(value);
+ return config;
+ }
+
+ public String getXpath() {
+ return xpath;
+ }
+
+ public void setXpath(final String xpath) {
+ this.xpath = xpath;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/parser/ProjectParser.java b/src/main/java/org/rundeck/api/parser/ProjectParser.java
index 999ec70..ef6ca13 100644
--- a/src/main/java/org/rundeck/api/parser/ProjectParser.java
+++ b/src/main/java/org/rundeck/api/parser/ProjectParser.java
@@ -42,7 +42,7 @@ public class ProjectParser implements XmlNodeParser {
@Override
public RundeckProject parseXmlNode(Node node) {
- Node projectNode = xpath != null ? node.selectSingleNode(xpath) : node;
+ Node projectNode = getXpath() != null ? node.selectSingleNode(getXpath()) : node;
RundeckProject project = new RundeckProject();
@@ -53,4 +53,7 @@ public class ProjectParser implements XmlNodeParser {
return project;
}
+ protected String getXpath() {
+ return xpath;
+ }
}
diff --git a/src/main/java/org/rundeck/api/parser/ProjectParserV11.java b/src/main/java/org/rundeck/api/parser/ProjectParserV11.java
new file mode 100644
index 0000000..631d941
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/ProjectParserV11.java
@@ -0,0 +1,36 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.ProjectConfig;
+import org.rundeck.api.domain.RundeckProject;
+
+import java.util.List;
+
+/**
+ * ProjectParserV11 supports embedded "config" element.
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectParserV11 extends ProjectParser {
+ public ProjectParserV11() {
+ }
+
+ public ProjectParserV11(final String xpath) {
+ super(xpath);
+ }
+
+ @Override
+ public RundeckProject parseXmlNode(final Node node) {
+ final RundeckProject rundeckProject = super.parseXmlNode(node);
+ final Node projectNode = getXpath() != null ? node.selectSingleNode(getXpath()) : node;
+ final Node config1 = projectNode.selectSingleNode("config");
+ if (config1 == null) {
+ return rundeckProject;
+ }
+
+ rundeckProject.setProjectConfig(new ProjectConfigParser().parseXmlNode(config1));
+
+ return rundeckProject;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/parser/RundeckTokenParser.java b/src/main/java/org/rundeck/api/parser/RundeckTokenParser.java
new file mode 100644
index 0000000..b48543d
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/RundeckTokenParser.java
@@ -0,0 +1,33 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.RundeckToken;
+
+/**
+ * RundeckTokenParser is ...
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public class RundeckTokenParser implements XmlNodeParser {
+ String xpath;
+
+ public RundeckTokenParser() {
+ }
+
+ public RundeckTokenParser(String xpath) {
+ this.xpath = xpath;
+ }
+
+ @Override
+ public RundeckToken parseXmlNode(Node node) {
+ Node targetNode = xpath != null ? node.selectSingleNode(xpath) : node;
+ RundeckToken rundeckToken = new RundeckToken();
+ String token = targetNode.valueOf("@id");
+ String user = targetNode.valueOf("@user");
+ rundeckToken.setToken(token);
+ rundeckToken.setUser(user);
+
+ return rundeckToken;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
new file mode 100644
index 0000000..091ab2b
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
@@ -0,0 +1,25 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.BaseKeyResource;
+import org.rundeck.api.domain.KeyResource;
+
+/**
+ * SSHKeyResourceParser is ...
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public class SSHKeyResourceParser extends BaseXpathParser implements XmlNodeParser {
+ public SSHKeyResourceParser() {
+ }
+
+ public SSHKeyResourceParser(String xpath) {
+ super(xpath);
+ }
+
+ @Override
+ public KeyResource parse(Node node) {
+ return BaseKeyResource.from(new StorageResourceParser().parse(node));
+ }
+}
diff --git a/src/main/java/org/rundeck/api/parser/StorageResourceParser.java b/src/main/java/org/rundeck/api/parser/StorageResourceParser.java
new file mode 100644
index 0000000..f1c5e6a
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/StorageResourceParser.java
@@ -0,0 +1,61 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Element;
+import org.dom4j.Node;
+import org.rundeck.api.domain.BaseStorageResource;
+import org.rundeck.api.domain.StorageResource;
+
+import java.util.HashMap;
+
+/**
+ * StorageResourceParser is ...
+ *
+ * @author Greg Schueler
+ * @since 2014-04-04
+ */
+public class StorageResourceParser extends BaseXpathParser {
+ private BaseStorageResource holder;
+ public StorageResourceParser() {
+
+ }
+ public StorageResourceParser(BaseStorageResource holder){
+ this.holder=holder;
+ }
+
+ public StorageResourceParser(String xpath) {
+ super(xpath);
+ }
+
+ @Override
+ public StorageResource parse(Node node) {
+ String path = node.valueOf("@path");
+ String type = node.valueOf("@type");
+ String url = node.valueOf("@url");
+ BaseStorageResource storageResource = null == holder ? new BaseStorageResource() : holder;
+ storageResource.setDirectory("directory".equals(type));
+ storageResource.setPath(path);
+ storageResource.setUrl(url);
+
+ if (storageResource.isDirectory()) {
+ if (node.selectSingleNode("contents") != null) {
+ storageResource.setDirectoryContents(new ListParser(new StorageResourceParser(),
+ "contents/resource").parseXmlNode(node));
+ }
+ } else {
+ String name = node.valueOf("@name");
+ storageResource.setName(name);
+
+ Node meta = node.selectSingleNode("resource-meta");
+ HashMap metamap = new HashMap();
+ if (null != meta) {
+ Element metaEl = (Element) meta;
+ for (Object o : metaEl.elements()) {
+ Element sub = (Element) o;
+ metamap.put(sub.getName(), sub.getText().trim());
+ }
+ }
+ storageResource.setMetadata(metamap);
+ }
+ return storageResource;
+ }
+}
diff --git a/src/main/java/org/rundeck/api/util/DocumentContentProducer.java b/src/main/java/org/rundeck/api/util/DocumentContentProducer.java
new file mode 100644
index 0000000..ace7840
--- /dev/null
+++ b/src/main/java/org/rundeck/api/util/DocumentContentProducer.java
@@ -0,0 +1,38 @@
+package org.rundeck.api.util;
+
+import org.apache.http.entity.ContentProducer;
+import org.dom4j.Document;
+import org.dom4j.io.OutputFormat;
+import org.dom4j.io.XMLWriter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * DocumentContentProducer writes XML document to a stream
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class DocumentContentProducer implements ContentProducer {
+ Document document;
+ private OutputFormat format;
+
+ public DocumentContentProducer(final Document document, final OutputFormat format) {
+ this.document = document;
+ this.format = format;
+ }
+
+ public DocumentContentProducer(final Document document) {
+ this.document = document;
+ format = new OutputFormat("", false, "UTF-8");
+ }
+
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+
+ final XMLWriter xmlWriter = new XMLWriter(outstream, format);
+ xmlWriter.write(document);
+ xmlWriter.flush();
+ }
+}
diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index 3a3d96b..39a2185 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -93,3 +93,23 @@ h2. RunDeck API version 10
* Execution Output - Retrieve log output for a particular node or step - OK
* Execution Info - added successfulNodes and failedNodes detail. - OK
* Deprecation: Remove methods deprecated until version 10. - OK
+
+
+h2. RunDeck API version 11
+
+[Documentation of the RunDeck API version 11|http://rundeck.org/2.1.0/api/index.html]
+
+* Project creation - OK
+* Get Project configuration - OK
+* Set Project configuration - OK
+* Get/Set Project configuration keys - OK
+* Delete project - OK
+* Export project archive - OK
+* Import project archive - OK
+* Key file upload - OK
+* Key file delete - OK
+* Key file list - OK
+* Key file get - OK
+* API Token create - OK
+* API Token list - OK
+* API Token delete - OK
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 6e755db..fb01b80 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -19,6 +19,7 @@ import betamax.Betamax;
import betamax.MatchRule;
import betamax.Recorder;
import betamax.TapeMode;
+import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
@@ -27,8 +28,7 @@ import org.rundeck.api.domain.*;
import org.rundeck.api.query.ExecutionQuery;
import org.rundeck.api.util.PagedResults;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
+import java.io.*;
import java.util.*;
@@ -55,6 +55,7 @@ public class RundeckClientTest {
public static final String TEST_TOKEN_4 = "sN5RRSNvu15DnV6EcNDdc2CkdPcv3s32";
public static final String TEST_TOKEN_5 = "C3O6d5O98Kr6Dpv71sdE4ERdCuU12P6d";
public static final String TEST_TOKEN_6 = "Do4d3NUD5DKk21DR4sNK755RcPk618vn";
+ public static final String TEST_TOKEN_7 = "8Dp9op111ER6opsDRkddvE86K9sE499s";
@Rule
public Recorder recorder = new Recorder();
@@ -82,6 +83,131 @@ public class RundeckClientTest {
Assert.assertNull(projects.get(0).getDescription());
}
@Test
+ @Betamax(tape = "create_projectv11")
+ public void createProject() throws Exception {
+
+ RundeckProject project = createClient(TEST_TOKEN_6,11).createProject("monkey1", null);
+ Assert.assertEquals("monkey1", project.getName());
+ Assert.assertEquals(null, project.getDescription());
+ Assert.assertNotNull(project.getProjectConfig());
+
+ }
+ @Test
+ @Betamax(tape = "delete_projectv11")
+ public void deleteProject() throws Exception {
+ RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+ client1.deleteProject("delete_me");
+ RundeckProject delete_me = null;
+ try {
+ delete_me = client1.getProject("delete_me");
+ Assert.fail();
+ } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+ Assert.assertEquals(404,e.getStatusCode());
+ }
+
+ }
+
+ @Test
+ @Betamax(tape = "get_project_configv11")
+ public void getProjectConfig() throws Exception {
+ ProjectConfig config = createClient(TEST_TOKEN_6, 11).getProjectConfig("monkey1");
+ Assert.assertNotNull(config);
+ Assert.assertNotNull(config.getProperties());
+ Assert.assertEquals(9,config.getProperties().size());
+ Assert.assertEquals("monkey1", config.getProperties().get("project.name"));
+ }
+ @Test
+ @Betamax(tape = "set_project_configv11")
+ public void setProjectConfig() throws Exception {
+ HashMap config = new HashMap();
+ config.put("alphabetty", "spaghetti");
+ config.put("blha.blee", "a big amazing thingy so there.");
+ ProjectConfig result = createClient(TEST_TOKEN_6, 11).setProjectConfig("monkey1", config);
+ Assert.assertNotNull(result);
+ Assert.assertNotNull(result.getProperties());
+ Assert.assertEquals(3, result.getProperties().size());
+ Assert.assertEquals("monkey1", result.getProperties().get("project.name"));
+ Assert.assertEquals("spaghetti", result.getProperties().get("alphabetty"));
+ Assert.assertEquals("a big amazing thingy so there.", result.getProperties().get("blha.blee"));
+ }
+
+ @Test
+ @Betamax(tape = "get_project_config_keyedv11")
+ public void getProjectConfigKeyed() throws Exception {
+ String value = createClient(TEST_TOKEN_6, 11).getProjectConfig("ABC", "project.name");
+ Assert.assertNotNull(value);
+ Assert.assertEquals("ABC", value);
+ }
+ @Test
+ @Betamax(tape = "get_project_config_keyed_dne_v11")
+ public void getProjectConfigKeyedDNE() throws Exception {
+ String value = createClient(TEST_TOKEN_6, 11).getProjectConfig("ABC", "does-not-exist");
+ Assert.assertNull(value);
+ }
+ @Test
+ @Betamax(tape = "set_project_config_keyedv11")
+ public void setProjectConfigKeyed() throws Exception {
+ String value = createClient(TEST_TOKEN_6, 11).setProjectConfig("ABC", "monkey-burrito", "lemon pie");
+ Assert.assertNotNull(value);
+ Assert.assertEquals("lemon pie", value);
+ }
+ @Test
+ @Betamax(tape = "delete_project_config_keyedv11")
+ public void deleteProjectConfigKeyed() throws Exception {
+ RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+ Assert.assertEquals("7up", client1.setProjectConfig("ABC", "monkey-burrito", "7up"));
+ client1.deleteProjectConfig("ABC", "monkey-burrito");
+ String value=client1.getProjectConfig("ABC", "monkey-burrito");
+ Assert.assertNull(value);
+ }
+ @Test
+ @Betamax(tape = "export_projectv11")
+ public void exportProject() throws Exception {
+ RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+ File temp = File.createTempFile("test-archive", ".zip");
+ temp.deleteOnExit();
+ int i = client1.exportProject("DEF1", temp);
+ Assert.assertEquals(8705, i);
+ }
+ @Test
+ @Betamax(tape = "import_project_suv11",mode = TapeMode.READ_ONLY)
+ public void importProjectSuccess() throws Exception {
+ RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+ InputStream resourceAsStream = getClass().getResourceAsStream("test-archive.zip");
+ File temp = File.createTempFile("test-archive", ".zip");
+ temp.deleteOnExit();
+ IOUtils.copy(resourceAsStream, new FileOutputStream(temp));
+ ArchiveImport def1 = client1.importArchive("DEF2", temp, true, true);
+ Assert.assertTrue(def1.isSuccessful());
+ Assert.assertEquals(0, def1.getErrorMessages().size());
+
+ ArchiveImport def2 = client1.importArchive("DEF2", temp, false, true);
+ Assert.assertTrue(def2.isSuccessful());
+ Assert.assertEquals(0, def2.getErrorMessages().size());
+
+ ArchiveImport def3 = client1.importArchive("DEF2", temp, true, false);
+ Assert.assertTrue(def3.isSuccessful());
+ Assert.assertEquals(0, def3.getErrorMessages().size());
+ temp.delete();
+ }
+ @Test
+ @Betamax(tape = "import_project_failure_v11", mode = TapeMode.READ_ONLY)
+ public void importProjectFailure() throws Exception {
+ RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+ InputStream resourceAsStream = getClass().getResourceAsStream("test-archive.zip");
+ File temp = File.createTempFile("test-archive", ".zip");
+ temp.deleteOnExit();
+ IOUtils.copy(resourceAsStream, new FileOutputStream(temp));
+ ArchiveImport def1 = client1.importArchive("DEF1", temp, false, true);
+ Assert.assertFalse(def1.isSuccessful());
+ Assert.assertEquals(10, def1.getErrorMessages().size());
+ Assert.assertEquals("Job at index [1] at archive path: " +
+ "rundeck-DEF1/jobs/job-6fd1808c-eafb-49ac-abf2-4de7ec75872f.xml had errors: Validation errors: Cannot" +
+ " create a Job with UUID 6fd1808c-eafb-49ac-abf2-4de7ec75872f: a Job already exists with this UUID. " +
+ "Change the UUID or delete the other Job.", def1.getErrorMessages().get(0));
+
+ }
+ @Test
@Betamax(tape = "get_history")
public void getHistory() throws Exception {
final RundeckHistory test = client.getHistory("test");
@@ -1072,6 +1198,298 @@ public class RundeckClientTest {
Assert.assertEquals(RundeckWFExecState.SUCCEEDED,output.getExecutionState());
}
+ /**
+ * generate api token
+ */
+ @Test
+ @Betamax(tape = "api_token_generate", mode = TapeMode.READ_ONLY)
+ public void generateApiToken() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ String token = client.generateApiToken("bob");
+
+ Assert.assertNotNull(token);
+ Assert.assertEquals("MiquQjELTrEaugpmdgAKs1W3a7xonAwU", token);
+ }
+ /**
+ * get api token
+ */
+ @Test
+ @Betamax(tape = "api_token_get", mode = TapeMode.READ_ONLY)
+ public void getApiToken() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ RundeckToken token = client.getApiToken("MiquQjELTrEaugpmdgAKs1W3a7xonAwU");
+
+ Assert.assertNotNull(token);
+ Assert.assertEquals("MiquQjELTrEaugpmdgAKs1W3a7xonAwU", token.getToken());
+ Assert.assertEquals("bob", token.getUser());
+ }
+ /**
+ * list api tokens for user
+ */
+ @Test
+ @Betamax(tape = "api_tokens_list_user", mode = TapeMode.READ_ONLY)
+ public void listApiTokens_user() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ List tokens = client.listApiTokens("bob");
+
+ Assert.assertNotNull(tokens);
+ Assert.assertEquals(3, tokens.size());
+ Assert.assertEquals("hINp5eGzvYA9UePbUChaKHd5NiRkwWbx", tokens.get(0).getToken());
+ Assert.assertEquals("bob", tokens.get(0).getUser());
+ Assert.assertEquals("NaNnwVzAHAG83qOS7Wtwh6mjcXViyWUV", tokens.get(1).getToken());
+ Assert.assertEquals("bob", tokens.get(1).getUser());
+ Assert.assertEquals("MiquQjELTrEaugpmdgAKs1W3a7xonAwU", tokens.get(2).getToken());
+ Assert.assertEquals("bob", tokens.get(2).getUser());
+ }
+ /**
+ * list api tokens all
+ */
+ @Test
+ @Betamax(tape = "api_tokens_list_all", mode = TapeMode.READ_ONLY)
+ public void listApiTokens() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ List tokens = client.listApiTokens();
+
+ Assert.assertNotNull(tokens);
+ Assert.assertEquals(4, tokens.size());
+ Assert.assertEquals("8Dp9op111ER6opsDRkddvE86K9sE499s", tokens.get(0).getToken());
+ Assert.assertEquals("admin", tokens.get(0).getUser());
+ Assert.assertEquals("hINp5eGzvYA9UePbUChaKHd5NiRkwWbx", tokens.get(1).getToken());
+ Assert.assertEquals("bob", tokens.get(1).getUser());
+ Assert.assertEquals("NaNnwVzAHAG83qOS7Wtwh6mjcXViyWUV", tokens.get(2).getToken());
+ Assert.assertEquals("bob", tokens.get(2).getUser());
+ Assert.assertEquals("MiquQjELTrEaugpmdgAKs1W3a7xonAwU", tokens.get(3).getToken());
+ Assert.assertEquals("bob", tokens.get(3).getUser());
+ }
+
+ /**
+ * get api token
+ */
+ @Test
+ @Betamax(tape = "api_token_delete", mode = TapeMode.READ_ONLY)
+ public void deleteApiToken() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+
+ client.deleteApiToken("MiquQjELTrEaugpmdgAKs1W3a7xonAwU");
+
+ //get should now return 404
+ try {
+ client.getApiToken("MiquQjELTrEaugpmdgAKs1W3a7xonAwU");
+ Assert.fail("expected failure");
+ } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+ Assert.assertEquals(404, e.getStatusCode());
+ }
+ }
+ /**
+ * Store ssh key
+ */
+ @Test
+ @Betamax(tape = "key_store_private", mode = TapeMode.READ_ONLY)
+ public void storeKey_private() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ File temp = File.createTempFile("test-key", ".tmp");
+ temp.deleteOnExit();
+ FileOutputStream out = new FileOutputStream(temp);
+ try{
+ out.write("test1".getBytes());
+ }finally {
+ out.close();
+ }
+ KeyResource storageResource = client.storeKey("keys/test/example/file1.pem", temp, true);
+ Assert.assertNotNull(storageResource);
+ Assert.assertFalse(storageResource.isDirectory());
+ Assert.assertTrue(storageResource.isPrivateKey());
+ Assert.assertEquals("file1.pem", storageResource.getName());
+ Assert.assertEquals("keys/test/example/file1.pem", storageResource.getPath());
+ Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file1.pem",
+ storageResource.getUrl());
+ Assert.assertEquals(0, storageResource.getDirectoryContents().size());
+ Map metadata = storageResource.getMetadata();
+ Assert.assertNotNull(metadata);
+ Assert.assertEquals("application/octet-stream", metadata.get("Rundeck-content-type"));
+ Assert.assertEquals("private", metadata.get("Rundeck-key-type"));
+ }
+ /**
+ * Store ssh key
+ */
+ @Test
+ @Betamax(tape = "key_store_public", mode = TapeMode.READ_ONLY)
+ public void storeKey_public() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ File temp = File.createTempFile("test-key", ".tmp");
+ temp.deleteOnExit();
+ FileOutputStream out = new FileOutputStream(temp);
+ try{
+ out.write("test1".getBytes());
+ }finally {
+ out.close();
+ }
+ KeyResource storageResource = client.storeKey("keys/test/example/file2.pub", temp, false);
+ Assert.assertNotNull(storageResource);
+ Assert.assertFalse(storageResource.isDirectory());
+ Assert.assertFalse(storageResource.isPrivateKey());
+ Assert.assertEquals("file2.pub", storageResource.getName());
+ Assert.assertEquals("keys/test/example/file2.pub", storageResource.getPath());
+ Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file2.pub",
+ storageResource.getUrl());
+ Assert.assertEquals(0, storageResource.getDirectoryContents().size());
+ Map metadata = storageResource.getMetadata();
+ Assert.assertNotNull(metadata);
+ Assert.assertEquals("application/pgp-keys", metadata.get("Rundeck-content-type"));
+ Assert.assertEquals("public", metadata.get("Rundeck-key-type"));
+ }
+ /**
+ * get ssh key
+ */
+ @Test
+ @Betamax(tape = "key_get_public", mode = TapeMode.READ_ONLY)
+ public void getKey_public() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ KeyResource storageResource = client.getKey("keys/test/example/file2.pub");
+ Assert.assertNotNull(storageResource);
+ Assert.assertFalse(storageResource.isDirectory());
+ Assert.assertFalse(storageResource.isPrivateKey());
+ Assert.assertEquals("file2.pub", storageResource.getName());
+ Assert.assertEquals("keys/test/example/file2.pub", storageResource.getPath());
+ Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file2.pub",
+ storageResource.getUrl());
+ Assert.assertEquals(0, storageResource.getDirectoryContents().size());
+ Map metadata = storageResource.getMetadata();
+ Assert.assertNotNull(metadata);
+ Assert.assertEquals("application/pgp-keys", metadata.get("Rundeck-content-type"));
+ Assert.assertEquals("public", metadata.get("Rundeck-key-type"));
+ }
+ /**
+ * get ssh key
+ */
+ @Test
+ @Betamax(tape = "key_get_private", mode = TapeMode.READ_ONLY)
+ public void getKey_private() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ KeyResource storageResource = client.getKey("keys/test/example/file1.pem");
+ Assert.assertNotNull(storageResource);
+ Assert.assertFalse(storageResource.isDirectory());
+ Assert.assertTrue(storageResource.isPrivateKey());
+ Assert.assertEquals("file1.pem", storageResource.getName());
+ Assert.assertEquals("keys/test/example/file1.pem", storageResource.getPath());
+ Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file1.pem",
+ storageResource.getUrl());
+ Assert.assertEquals(0, storageResource.getDirectoryContents().size());
+ Map metadata = storageResource.getMetadata();
+ Assert.assertNotNull(metadata);
+ Assert.assertEquals("application/octet-stream", metadata.get("Rundeck-content-type"));
+ Assert.assertEquals("private", metadata.get("Rundeck-key-type"));
+ }
+ /**
+ * get ssh key data
+ */
+ @Test
+ @Betamax(tape = "key_get_data_private", mode = TapeMode.READ_ONLY)
+ public void getKeyData_private() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ File temp = File.createTempFile("test-key", ".tmp");
+ temp.deleteOnExit();
+ try {
+ int data = client.getPublicKeyContent("keys/test/example/file1.pem", temp);
+ Assert.fail("expected failure");
+ } catch (RundeckApiException e) {
+ Assert.assertEquals("Requested Key path was not a Public key: keys/test/example/file1.pem",
+ e.getMessage());
+ }
+ }
+ /**
+ * get ssh key data
+ */
+ @Test
+ @Betamax(tape = "key_get_data_public", mode = TapeMode.READ_ONLY)
+ public void getKeyData_public() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ File temp = File.createTempFile("test-key", ".tmp");
+ temp.deleteOnExit();
+ int length = client.getPublicKeyContent("keys/test/example/file2.pub", temp);
+ Assert.assertEquals(5, length);
+ }
+ /**
+ * list directory
+ */
+ @Test
+ @Betamax(tape = "key_list_directory", mode = TapeMode.READ_ONLY)
+ public void listKeyDirectory() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ List list = client.listKeyDirectory("keys/test/example");
+ Assert.assertEquals(2, list.size());
+ KeyResource storageResource1 = list.get(0);
+ KeyResource storageResource2 = list.get(1);
+
+ Assert.assertFalse(storageResource2.isDirectory());
+ Assert.assertTrue(storageResource2.isPrivateKey());
+ Assert.assertEquals("file1.pem", storageResource2.getName());
+ Assert.assertEquals("keys/test/example/file1.pem", storageResource2.getPath());
+ Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file1.pem", storageResource2.getUrl());
+ Assert.assertNotNull(storageResource2.getMetadata());
+
+ Assert.assertEquals("application/octet-stream", storageResource2.getMetadata().get("Rundeck-content-type"));
+ Assert.assertEquals("private", storageResource2.getMetadata().get("Rundeck-key-type"));
+
+ Assert.assertFalse(storageResource1.isDirectory());
+ Assert.assertFalse(storageResource1.isPrivateKey());
+ Assert.assertEquals("file2.pub", storageResource1.getName());
+ Assert.assertEquals("keys/test/example/file2.pub", storageResource1.getPath());
+ Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file2.pub",
+ storageResource1.getUrl());
+ Assert.assertNotNull(storageResource1.getMetadata());
+ Assert.assertEquals("application/pgp-keys", storageResource1.getMetadata().get("Rundeck-content-type"));
+ Assert.assertEquals("public", storageResource1.getMetadata().get("Rundeck-key-type"));
+ }
+ /**
+ * list root
+ */
+ @Test
+ @Betamax(tape = "key_list_root", mode = TapeMode.READ_ONLY)
+ public void listKeyDirectoryRoot() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ List list = client.listKeyDirectoryRoot();
+ Assert.assertEquals(2, list.size());
+ KeyResource storageResource0 = list.get(0);
+ KeyResource storageResource1 = list.get(1);
+
+ Assert.assertFalse(storageResource0.isDirectory());
+ Assert.assertTrue(storageResource0.isPrivateKey());
+ Assert.assertEquals("test1.pem", storageResource0.getName());
+ Assert.assertEquals("keys/test1.pem", storageResource0.getPath());
+ Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test1.pem", storageResource0.getUrl());
+ Assert.assertNotNull(storageResource0.getMetadata());
+
+ Assert.assertEquals("application/octet-stream", storageResource0.getMetadata().get("Rundeck-content-type"));
+ Assert.assertEquals("private", storageResource0.getMetadata().get("Rundeck-key-type"));
+
+ Assert.assertTrue(storageResource1.toString(), storageResource1.isDirectory());
+ Assert.assertEquals(null, storageResource1.getName());
+ Assert.assertEquals("keys/test", storageResource1.getPath());
+ Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test",
+ storageResource1.getUrl());
+ Assert.assertNull(storageResource1.getMetadata());
+
+
+ }
+
+ /**
+ * delete ssh key
+ */
+ @Test
+ @Betamax(tape = "key_delete", mode = TapeMode.READ_ONLY)
+ public void deleteKey() throws Exception {
+ final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+ client.deleteKey("keys/test/example/file2.pub");
+
+ try {
+ client.getKey("keys/test/example/file2.pub");
+ Assert.fail("expected failure");
+ } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+ Assert.assertEquals(404,e.getStatusCode());
+ }
+ }
+
@Before
public void setUp() throws Exception {
// not that you can put whatever here, because we don't actually connect to the RunDeck instance
diff --git a/src/test/java/org/rundeck/api/generator/ProjectConfigGeneratorTest.java b/src/test/java/org/rundeck/api/generator/ProjectConfigGeneratorTest.java
new file mode 100644
index 0000000..aa6d5da
--- /dev/null
+++ b/src/test/java/org/rundeck/api/generator/ProjectConfigGeneratorTest.java
@@ -0,0 +1,41 @@
+package org.rundeck.api.generator;
+
+import junit.framework.Assert;
+import org.dom4j.Document;
+import org.dom4j.Element;
+import org.dom4j.io.XMLWriter;
+import org.junit.Test;
+import org.rundeck.api.domain.ProjectConfig;
+import org.rundeck.api.domain.RundeckProject;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * ProjectConfigGeneratorTest is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfigGeneratorTest {
+ @Test
+ public void generate() throws Exception {
+ ProjectConfig config = new ProjectConfig();
+ config.setProperty("abc", "123");
+ config.setProperty("monkey.bonanza", "pale\ncomparison");
+
+ Document doc = new ProjectConfigGenerator(config).generateXmlDocument();
+ XMLWriter xmlWriter = new XMLWriter(System.out);
+ xmlWriter.write(doc);
+ xmlWriter.flush();
+ Element configElement = doc.getRootElement();
+ Assert.assertEquals("config", configElement.getName());
+ Assert.assertNotNull(configElement.selectSingleNode("property[1]"));
+ Assert.assertEquals("abc", configElement.selectSingleNode("property[1]/@key").getText());
+ Assert.assertEquals("123", configElement.selectSingleNode("property[1]/@value").getText());
+
+ Assert.assertNotNull(configElement.selectSingleNode("property[2]"));
+ Assert.assertEquals("monkey.bonanza", configElement.selectSingleNode("property[2]/@key").getText());
+ Assert.assertEquals("pale\ncomparison", configElement.selectSingleNode("property[2]/@value").getText());
+
+ }
+}
diff --git a/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java b/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java
new file mode 100644
index 0000000..26beb33
--- /dev/null
+++ b/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java
@@ -0,0 +1,25 @@
+package org.rundeck.api.generator;
+
+import junit.framework.Assert;
+import org.dom4j.Document;
+import org.junit.Test;
+import org.rundeck.api.domain.RundeckProject;
+
+/**
+ * ProjectGeneratorTest is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectGeneratorTest {
+ @Test
+ public void generate() {
+ RundeckProject project = new RundeckProject();
+ project.setName("monkey1");
+
+ Document doc = new ProjectGenerator(project).generateXmlDocument();
+ Assert.assertEquals("project", doc.getRootElement().getName());
+ Assert.assertNotNull(doc.selectSingleNode("/project/name"));
+ Assert.assertEquals("monkey1", doc.selectSingleNode("/project/name").getText());
+ }
+}
diff --git a/src/test/java/org/rundeck/api/parser/ProjectConfigParserTest.java b/src/test/java/org/rundeck/api/parser/ProjectConfigParserTest.java
new file mode 100644
index 0000000..cee0f6f
--- /dev/null
+++ b/src/test/java/org/rundeck/api/parser/ProjectConfigParserTest.java
@@ -0,0 +1,51 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Document;
+import org.junit.Assert;
+import org.junit.Test;
+import org.rundeck.api.domain.ProjectConfig;
+import org.rundeck.api.domain.RundeckProject;
+
+import java.io.InputStream;
+
+/**
+ * ProjectConfigParserTest is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfigParserTest {
+ @Test
+ public void parseProject() throws Exception {
+ InputStream input = getClass().getResourceAsStream("projectv11.xml");
+ Document document = ParserHelper.loadDocument(input);
+
+ ProjectConfig config = new ProjectConfigParser("project/config").parseXmlNode(document);
+
+ Assert.assertEquals(10, config.getProperties().size());
+ Assert.assertEquals("ziggy", config.getProperties().get("project.name"));
+ Assert.assertEquals("false", config.getProperties().get("resources.source.1.config.requireFileExists"));
+ Assert.assertEquals("privateKey", config.getProperties().get("project.ssh-authentication"));
+ Assert.assertEquals("jsch-ssh", config.getProperties().get("service.NodeExecutor.default.provider"));
+ Assert.assertEquals("false", config.getProperties().get("resources.source.1.config.includeServerNode"));
+ Assert.assertEquals("false", config.getProperties().get("resources.source.1.config.generateFileAutomatically"));
+ Assert.assertEquals("/var/rundeck/projects/${project.name}/etc/resources.xml",
+ config.getProperties().get("resources.source.1.config.file"));
+ Assert.assertEquals("/var/lib/rundeck/.ssh/id_rsa", config.getProperties().get("project.ssh-keypath"));
+ Assert.assertEquals("jsch-scp", config.getProperties().get("service.FileCopier.default.provider"));
+ Assert.assertEquals("file", config.getProperties().get("resources.source.1.type"));
+ /*
+
+
+
+
+
+
+
+
+
+
+ */
+ }
+}
diff --git a/src/test/java/org/rundeck/api/parser/ProjectConfigPropertyParserTest.java b/src/test/java/org/rundeck/api/parser/ProjectConfigPropertyParserTest.java
new file mode 100644
index 0000000..0fcac10
--- /dev/null
+++ b/src/test/java/org/rundeck/api/parser/ProjectConfigPropertyParserTest.java
@@ -0,0 +1,48 @@
+package org.rundeck.api.parser;
+
+import junit.framework.Assert;
+import org.dom4j.Document;
+import org.junit.Test;
+import org.junit.runners.JUnit4;
+import org.rundeck.api.domain.ConfigProperty;
+import org.rundeck.api.domain.ProjectConfig;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * Test
+ *
+ * @author greg
+ * @since 2014-03-07
+ */
+public class ProjectConfigPropertyParserTest {
+ @Test
+ public void parseFromProject() throws Exception {
+ InputStream input = getClass().getResourceAsStream("projectv11.xml");
+ Document document = ParserHelper.loadDocument(input);
+
+ ConfigProperty config = new ProjectConfigPropertyParser("project/config/property[1]").parseXmlNode(document);
+ Assert.assertEquals("project.name", config.getKey());
+ Assert.assertEquals("ziggy", config.getValue());
+ /**
+ *
+
+
+ */
+ }
+ @Test
+ public void parseProperty() throws Exception {
+ Document document = ParserHelper.loadDocument(new ByteArrayInputStream(("").getBytes()));
+
+ ConfigProperty config = new ProjectConfigPropertyParser("/property").parseXmlNode(document);
+ Assert.assertEquals("project.name", config.getKey());
+ Assert.assertEquals("ABC", config.getValue());
+ /**
+ *
+
+
+ */
+ }
+}
diff --git a/src/test/java/org/rundeck/api/parser/ProjectParserV11Test.java b/src/test/java/org/rundeck/api/parser/ProjectParserV11Test.java
new file mode 100644
index 0000000..cff9fc9
--- /dev/null
+++ b/src/test/java/org/rundeck/api/parser/ProjectParserV11Test.java
@@ -0,0 +1,28 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Document;
+import org.junit.Assert;
+import org.junit.Test;
+import org.rundeck.api.domain.RundeckProject;
+
+import java.io.InputStream;
+
+/**
+ * ProjectParserV11Test is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectParserV11Test {
+ @Test
+ public void parseProject() throws Exception {
+ InputStream input = getClass().getResourceAsStream("projectv11.xml");
+ Document document = ParserHelper.loadDocument(input);
+
+ RundeckProject project = new ProjectParserV11("project").parseXmlNode(document);
+
+ Assert.assertEquals("ziggy", project.getName());
+ Assert.assertNull(project.getDescription());
+ Assert.assertNotNull(project.getProjectConfig());
+ }
+}
diff --git a/src/test/resources/betamax/tapes/api_token_delete.yaml b/src/test/resources/betamax/tapes/api_token_delete.yaml
new file mode 100644
index 0000000..92fc6c0
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_token_delete.yaml
@@ -0,0 +1,36 @@
+!tape
+name: api_token_delete
+interactions:
+- recorded: 2014-04-04T18:38:18.432Z
+ request:
+ method: DELETE
+ uri: http://rundeck.local:4440/api/11/token/MiquQjELTrEaugpmdgAKs1W3a7xonAwU
+ headers:
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 204
+ headers:
+ Content-Type: text/html;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=j0fidhqqsmlt1qmvaawr52a42;Path=/
+- recorded: 2014-04-04T18:38:18.523Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/token/MiquQjELTrEaugpmdgAKs1W3a7xonAwU
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 404
+ headers:
+ Content-Type: text/xml;charset=UTF-8
+ Server: Jetty(7.6.0.v20120127)
+ X-Rundeck-API-Version: '11'
+ body: "\n \n Token does not exist: MiquQjELTrEaugpmdgAKs1W3a7xonAwU\n \n"
diff --git a/src/test/resources/betamax/tapes/api_token_generate.yaml b/src/test/resources/betamax/tapes/api_token_generate.yaml
new file mode 100644
index 0000000..1a5f72d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_token_generate.yaml
@@ -0,0 +1,25 @@
+!tape
+name: api_token_generate
+interactions:
+- recorded: 2014-04-04T18:21:07.759Z
+ request:
+ method: POST
+ uri: http://rundeck.local:4440/api/11/tokens/bob
+ headers:
+ Accept: text/xml
+ Content-Length: '0'
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 201
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=1gt9t2gch2zff1a0werz1us5wk;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PHRva2VuIGlkPSdNaXF1UWpFTFRyRWF1Z3BtZGdBS3MxVzNhN3hvbkF3VScgdXNlcj0nYm9iJyAvPg==
diff --git a/src/test/resources/betamax/tapes/api_token_get.yaml b/src/test/resources/betamax/tapes/api_token_get.yaml
new file mode 100644
index 0000000..8377d6f
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_token_get.yaml
@@ -0,0 +1,24 @@
+!tape
+name: api_token_get
+interactions:
+- recorded: 2014-04-04T18:23:05.986Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/token/MiquQjELTrEaugpmdgAKs1W3a7xonAwU
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=1tdpszk6b3v191p0ng2u94rohw;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PHRva2VuIGlkPSdNaXF1UWpFTFRyRWF1Z3BtZGdBS3MxVzNhN3hvbkF3VScgdXNlcj0nYm9iJyAvPg==
diff --git a/src/test/resources/betamax/tapes/api_tokens_list_all.yaml b/src/test/resources/betamax/tapes/api_tokens_list_all.yaml
new file mode 100644
index 0000000..f1318ad
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_tokens_list_all.yaml
@@ -0,0 +1,24 @@
+!tape
+name: api_tokens_list_all
+interactions:
+- recorded: 2014-04-04T18:32:37.397Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/tokens
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=ixag173yjktz1c5o9yrbe5z5a;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PHRva2VucyBjb3VudD0nNCcgYWxsdXNlcnM9J3RydWUnPgogIDx0b2tlbiBpZD0nOERwOW9wMTExRVI2b3BzRFJrZGR2RTg2SzlzRTQ5OXMnIHVzZXI9J2FkbWluJyAvPgogIDx0b2tlbiBpZD0naElOcDVlR3p2WUE5VWVQYlVDaGFLSGQ1TmlSa3dXYngnIHVzZXI9J2JvYicgLz4KICA8dG9rZW4gaWQ9J05hTm53VnpBSEFHODNxT1M3V3R3aDZtamNYVml5V1VWJyB1c2VyPSdib2InIC8+CiAgPHRva2VuIGlkPSdNaXF1UWpFTFRyRWF1Z3BtZGdBS3MxVzNhN3hvbkF3VScgdXNlcj0nYm9iJyAvPgo8L3Rva2Vucz4=
diff --git a/src/test/resources/betamax/tapes/api_tokens_list_user.yaml b/src/test/resources/betamax/tapes/api_tokens_list_user.yaml
new file mode 100644
index 0000000..15ec26d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_tokens_list_user.yaml
@@ -0,0 +1,24 @@
+!tape
+name: api_tokens_list_user
+interactions:
+- recorded: 2014-04-04T18:26:33.394Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/tokens/bob
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=114794elwavo26cx4ugkv7pe7;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PHRva2VucyBjb3VudD0nMycgdXNlcj0nYm9iJz4KICA8dG9rZW4gaWQ9J2hJTnA1ZUd6dllBOVVlUGJVQ2hhS0hkNU5pUmt3V2J4JyB1c2VyPSdib2InIC8+CiAgPHRva2VuIGlkPSdOYU5ud1Z6QUhBRzgzcU9TN1d0d2g2bWpjWFZpeVdVVicgdXNlcj0nYm9iJyAvPgogIDx0b2tlbiBpZD0nTWlxdVFqRUxUckVhdWdwbWRnQUtzMVczYTd4b25Bd1UnIHVzZXI9J2JvYicgLz4KPC90b2tlbnM+
diff --git a/src/test/resources/betamax/tapes/create_projectv11.yaml b/src/test/resources/betamax/tapes/create_projectv11.yaml
new file mode 100644
index 0000000..e928798
--- /dev/null
+++ b/src/test/resources/betamax/tapes/create_projectv11.yaml
@@ -0,0 +1,26 @@
+!tape
+name: create_projectv11
+interactions:
+- recorded: 2014-02-27T19:45:35.430Z
+ request:
+ method: POST
+ uri: http://rundeck.local:4440/api/11/projects
+ headers:
+ Accept: text/xml
+ Content-Type: application/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ Transfer-Encoding: chunked
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=1h2r5l35j4ynqo19dsm6xa2gv;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PHByb2plY3QgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3Byb2plY3QvbW9ua2V5MSc+CiAgPG5hbWU+bW9ua2V5MTwvbmFtZT4KICA8ZGVzY3JpcHRpb24+PC9kZXNjcmlwdGlvbj4KICA8Y29uZmlnPgogICAgPHByb3BlcnR5IGtleT0ncHJvamVjdC5uYW1lJyB2YWx1ZT0nbW9ua2V5MScgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3Byb2plY3Quc3NoLWF1dGhlbnRpY2F0aW9uJyB2YWx1ZT0ncHJpdmF0ZUtleScgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3NlcnZpY2UuTm9kZUV4ZWN1dG9yLmRlZmF1bHQucHJvdmlkZXInIHZhbHVlPSdqc2NoLXNzaCcgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3Jlc291cmNlcy5zb3VyY2UuMS5jb25maWcuaW5jbHVkZVNlcnZlck5vZGUnIHZhbHVlPSd0cnVlJyAvPgogICAgPHByb3BlcnR5IGtleT0ncmVzb3VyY2VzLnNvdXJjZS4xLmNvbmZpZy5nZW5lcmF0ZUZpbGVBdXRvbWF0aWNhbGx5JyB2YWx1ZT0ndHJ1ZScgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3Jlc291cmNlcy5zb3VyY2UuMS5jb25maWcuZmlsZScgdmFsdWU9Jy9Vc2Vycy9ncmVnL3J1bmRlY2syZC9wcm9qZWN0cy9tb25rZXkxL2V0Yy9yZXNvdXJjZXMueG1sJyAvPgogICAgPHByb3BlcnR5IGtleT0ncHJvamVjdC5zc2gta2V5cGF0aCcgdmFsdWU9Jy9Vc2Vycy9ncmVnLy5zc2gvaWRfcnNhJyAvPgogICAgPHByb3BlcnR5IGtleT0nc2VydmljZS5GaWxlQ29waWVyLmRlZmF1bHQucHJvdmlkZXInIHZhbHVlPSdqc2NoLXNjcCcgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3Jlc291cmNlcy5zb3VyY2UuMS50eXBlJyB2YWx1ZT0nZmlsZScgLz4KICA8L2NvbmZpZz4KPC9wcm9qZWN0Pg==
diff --git a/src/test/resources/betamax/tapes/delete_project_config_keyedv11.yaml b/src/test/resources/betamax/tapes/delete_project_config_keyedv11.yaml
new file mode 100644
index 0000000..c107f56
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_project_config_keyedv11.yaml
@@ -0,0 +1,57 @@
+!tape
+name: delete_project_config_keyedv11
+interactions:
+- recorded: 2014-03-07T19:59:51.228Z
+ request:
+ method: PUT
+ uri: http://rundeck.local:4440/api/11/project/ABC/config/monkey-burrito
+ headers:
+ Accept: text/xml
+ Content-Type: application/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ Transfer-Encoding: chunked
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=bolnwf54stai1bo049hylrsua;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PHByb3BlcnR5IGtleT0nbW9ua2V5LWJ1cnJpdG8nIHZhbHVlPSc3dXAnIC8+
+- recorded: 2014-03-07T19:59:51.325Z
+ request:
+ method: DELETE
+ uri: http://rundeck.local:4440/api/11/project/ABC/config/monkey-burrito
+ headers:
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 204
+ headers:
+ Content-Type: text/html;charset=UTF-8
+ Server: Jetty(7.6.0.v20120127)
+- recorded: 2014-03-07T19:59:51.402Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/project/ABC/config/monkey-burrito
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 404
+ headers:
+ Content-Type: text/xml;charset=UTF-8
+ Server: Jetty(7.6.0.v20120127)
+ X-Rundeck-API-Version: '11'
+ body: "\n \n property does not exist: monkey-burrito\n \n"
diff --git a/src/test/resources/betamax/tapes/delete_projectv11.yaml b/src/test/resources/betamax/tapes/delete_projectv11.yaml
new file mode 100644
index 0000000..4025402
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_projectv11.yaml
@@ -0,0 +1,36 @@
+!tape
+name: delete_projectv11
+interactions:
+- recorded: 2014-03-07T20:34:06.758Z
+ request:
+ method: DELETE
+ uri: http://rundeck.local:4440/api/11/project/delete_me
+ headers:
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 204
+ headers:
+ Content-Type: text/html;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=17vo5m2nghd0e1dcg0hqsuxklu;Path=/
+- recorded: 2014-03-07T20:34:06.903Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/project/delete_me
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 404
+ headers:
+ Content-Type: text/xml;charset=UTF-8
+ Server: Jetty(7.6.0.v20120127)
+ X-Rundeck-API-Version: '11'
+ body: "\n \n project does not exist: delete_me\n \n"
diff --git a/src/test/resources/betamax/tapes/export_projectv11.yaml b/src/test/resources/betamax/tapes/export_projectv11.yaml
new file mode 100644
index 0000000..595c78d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/export_projectv11.yaml
@@ -0,0 +1,23 @@
+!tape
+name: export_projectv11
+interactions:
+- recorded: 2014-03-07T21:12:45.024Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/project/DEF1/export
+ headers:
+ Accept: application/zip
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 200
+ headers:
+ Content-Disposition: attachment; filename="DEF1-20140307-131244.rdproject.jar"
+ Content-Type: application/zip
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=18wgn0qj6x6ho1vpkwj2logftl;Path=/
+ body: !!binary |-
+ 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NDlhYy1hYmYyLTRkZTdlYzc1ODcyZi54bWxQSwECFAAUAAgICACWaWdENmV0rRoBAAAoAgAAPgAAAAAAAAAAAAAAAACqAwAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTA5NGIwZGNjLWVkNzUtNDE1Zi04NDA5LTMxZTZiMjFiNTAyZC54bWxQSwECFAAUAAgICACWaWdEoSpLBSYBAACbAgAAPgAAAAAAAAAAAAAAAAAwBQAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTJiZTk2NmU3LTQ0MGUtNDg5Ny04YTU5LTZhZDEyNmRmNjAzZS54bWxQSwECFAAUAAgICACWaWdE4FnkVG4BAAAcAwAAPgAAAAAAAAAAAAAAAADCBgAAcnVuZGVjay1ERUYxL2pvYnMvam9iLWU2ZjYxYWUyLWFlZTEtNDYxMC05ZmU5LThjNmRmMmNlOGRiYy54bWxQSwECFAAUAAgICACWaWdE0201rJEBAACJAwAAPgAAAAAAAAAAAAAAAACcCAAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTNmNjg1M2Y4LWI1ODktNGRhOC1iYTc2LTE2MzZlZjgxMWNlNi54bWxQSwECFAAUAAgICACWaWdE9sg0SZEBAACJAwAAPgAAAAAAAAAAAAAAAACZCgAAcnVuZGVjay1ERUYxL2pvYnMvam9iLWFjNzhkZmJiLWE0OWMtNDBhYi1hY2Q5LTk0MDhiZTU2MWU3NC54bWxQSwECFAAUAAgICACWaWdEs3XyZB0CAACGBQAAPgAAAAAAAAAAAAAAAACWDAAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTc1NmM5OWY3LTA0ZmItNDM4Yy04NGY1LTE3MjJkOTY3ZDFjYi54bWxQSwECFAAUAAgICACWaWdEg9BA1bgBAADXAwAAPgAAAAAAAAAAAAAAAAAfDwAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTNhZWM3ZGZiLTk1MDItNDg3Ni04NzYzLTFhYTc4OGM4ZmM5Yy54bWxQSwECFAAUAAgICACWaWdE8UF0QDUBAABpAgAAPgAAAAAAAAAAAAAAAABDEQAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTg1OTZjYjljLTY4MzMtNDNmNy1hMGY3LTQ2NjNkZDhhYmMyOS54bWxQSwECFAAUAAgICACWaWdEdesbFksBAAC+AgAAPgAAAAAAAAAAAAAAAADkEgAAcnVuZGVjay1ERUYxL2pvYnMvam9iLWQ0MDIzYTcyLTU0ZTEtNDFiNS1iNGQ0LTRkZDdlNWQ0ZjVlNC54bWxQSwECFAAUAAgICACWaWdEAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAACbFAAAcnVuZGVjay1ERUYxL2V4ZWN1dGlvbnMvUEsBAhQAFAAICAgAlmlnRC1zICneAQAADgQAACoAAAAAAAAAAAAAAAAA4xQAAHJ1bmRlY2stREVGMS9leGVjdXRpb25zL2V4ZWN1dGlvbi0xNDU3LnhtbFBLAQIUABQACAgIAJZpZ0RkWJKhlgAAAKcBAAApAAAAAAAAAAAAAAAAABkXAABydW5kZWNrLURFRjEvZXhlY3V0aW9ucy9vdXRwdXQtMTQ1Ny5yZGxvZ1BLAQIUABQACAgIAJZpZ0QFJ/wt3gAAAJYCAAAtAAAAAAAAAAAAAAAAAAYYAABydW5kZWNrLURFRjEvZXhlY3V0aW9ucy9zdGF0ZS0xNDU3LnN0YXRlLmpzb25QSwECFAAUAAgICACWaWdEAAAAAAIAAAAAAAAAFQAAAAAAAAAAAAAAAAA/GQAAcnVuZGVjay1ERUYxL3JlcG9ydHMvUEsBAhQAFAAICAgAlmlnRGn21Ec3AQAANQIAACQAAAAAAAAAAAAAAAAAhBkAAHJ1bmRlY2stREVGMS9yZXBvcnRzL3JlcG9ydC0xNDc0LnhtbFBLBQYAAAAAEwATAN4GAAANGwAAAAA=
diff --git a/src/test/resources/betamax/tapes/get_project_config_keyed_dne_v11.yaml b/src/test/resources/betamax/tapes/get_project_config_keyed_dne_v11.yaml
new file mode 100644
index 0000000..d17090c
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_project_config_keyed_dne_v11.yaml
@@ -0,0 +1,22 @@
+!tape
+name: get_project_config_keyed_dne_v11
+interactions:
+- recorded: 2014-03-07T20:19:47.533Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/project/ABC/config/does-not-exist
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 404
+ headers:
+ Content-Type: text/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=2367tnltmmec14cn79ps4fam9;Path=/
+ X-Rundeck-API-Version: '11'
+ body: "\n \n property does not exist: does-not-exist\n \n"
diff --git a/src/test/resources/betamax/tapes/get_project_config_keyedv11.yaml b/src/test/resources/betamax/tapes/get_project_config_keyedv11.yaml
new file mode 100644
index 0000000..c0ba023
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_project_config_keyedv11.yaml
@@ -0,0 +1,24 @@
+!tape
+name: get_project_config_keyedv11
+interactions:
+- recorded: 2014-03-07T19:50:29.035Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/project/ABC/config/project.name
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=jgign9nyxeyp4istq65l86lp;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PHByb3BlcnR5IGtleT0ncHJvamVjdC5uYW1lJyB2YWx1ZT0nQUJDJyAvPg==
diff --git a/src/test/resources/betamax/tapes/get_project_configv11.yaml b/src/test/resources/betamax/tapes/get_project_configv11.yaml
new file mode 100644
index 0000000..033aa13
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_project_configv11.yaml
@@ -0,0 +1,24 @@
+!tape
+name: get_project_configv11
+interactions:
+- recorded: 2014-02-27T20:35:47.282Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/project/monkey1/config
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=bo96n10n268hsd1gi9y67nah;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PGNvbmZpZz4KICA8cHJvcGVydHkga2V5PSdwcm9qZWN0Lm5hbWUnIHZhbHVlPSdtb25rZXkxJyAvPgogIDxwcm9wZXJ0eSBrZXk9J3Byb2plY3Quc3NoLWF1dGhlbnRpY2F0aW9uJyB2YWx1ZT0ncHJpdmF0ZUtleScgLz4KICA8cHJvcGVydHkga2V5PSdzZXJ2aWNlLk5vZGVFeGVjdXRvci5kZWZhdWx0LnByb3ZpZGVyJyB2YWx1ZT0nanNjaC1zc2gnIC8+CiAgPHByb3BlcnR5IGtleT0ncmVzb3VyY2VzLnNvdXJjZS4xLmNvbmZpZy5pbmNsdWRlU2VydmVyTm9kZScgdmFsdWU9J3RydWUnIC8+CiAgPHByb3BlcnR5IGtleT0ncmVzb3VyY2VzLnNvdXJjZS4xLmNvbmZpZy5nZW5lcmF0ZUZpbGVBdXRvbWF0aWNhbGx5JyB2YWx1ZT0ndHJ1ZScgLz4KICA8cHJvcGVydHkga2V5PSdyZXNvdXJjZXMuc291cmNlLjEuY29uZmlnLmZpbGUnIHZhbHVlPScvVXNlcnMvZ3JlZy9ydW5kZWNrMmQvcHJvamVjdHMvbW9ua2V5MS9ldGMvcmVzb3VyY2VzLnhtbCcgLz4KICA8cHJvcGVydHkga2V5PSdwcm9qZWN0LnNzaC1rZXlwYXRoJyB2YWx1ZT0nL1VzZXJzL2dyZWcvLnNzaC9pZF9yc2EnIC8+CiAgPHByb3BlcnR5IGtleT0nc2VydmljZS5GaWxlQ29waWVyLmRlZmF1bHQucHJvdmlkZXInIHZhbHVlPSdqc2NoLXNjcCcgLz4KICA8cHJvcGVydHkga2V5PSdyZXNvdXJjZXMuc291cmNlLjEudHlwZScgdmFsdWU9J2ZpbGUnIC8+CjwvY29uZmlnPg==
diff --git a/src/test/resources/betamax/tapes/import_project_failure_v11.yaml b/src/test/resources/betamax/tapes/import_project_failure_v11.yaml
new file mode 100644
index 0000000..1ddcae5
--- /dev/null
+++ b/src/test/resources/betamax/tapes/import_project_failure_v11.yaml
@@ -0,0 +1,27 @@
+!tape
+name: import_project_failure_v11
+interactions:
+- recorded: 2014-03-10T00:01:12.170Z
+ request:
+ method: PUT
+ uri: http://rundeck.local:4440/api/11/project/DEF1/import?importExecutions=false&jobUuidOption=preserve
+ headers:
+ Accept: text/xml
+ Content-Length: '8705'
+ Content-Type: application/zip
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ body: ''
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=1q6qo7ev7f5uz11hlfd8e0sa82;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PGltcG9ydCBzdGF0dXM9J2ZhaWxlZCc+CiAgPGVycm9ycyBjb3VudD0nMTAnPgogICAgPGVycm9yPkpvYiBhdCBpbmRleCBbMV0gYXQgYXJjaGl2ZSBwYXRoOiBydW5kZWNrLURFRjEvam9icy9qb2ItNmZkMTgwOGMtZWFmYi00OWFjLWFiZjItNGRlN2VjNzU4NzJmLnhtbCBoYWQgZXJyb3JzOiBWYWxpZGF0aW9uIGVycm9yczogQ2Fubm90IGNyZWF0ZSBhIEpvYiB3aXRoIFVVSUQgNmZkMTgwOGMtZWFmYi00OWFjLWFiZjItNGRlN2VjNzU4NzJmOiBhIEpvYiBhbHJlYWR5IGV4aXN0cyB3aXRoIHRoaXMgVVVJRC4gQ2hhbmdlIHRoZSBVVUlEIG9yIGRlbGV0ZSB0aGUgb3RoZXIgSm9iLjwvZXJyb3I+CiAgICA8ZXJyb3I+Sm9iIGF0IGluZGV4IFsxXSBhdCBhcmNoaXZlIHBhdGg6IHJ1bmRlY2stREVGMS9qb2JzL2pvYi0wOTRiMGRjYy1lZDc1LTQxNWYtODQwOS0zMWU2YjIxYjUwMmQueG1sIGhhZCBlcnJvcnM6IFZhbGlkYXRpb24gZXJyb3JzOiBDYW5ub3QgY3JlYXRlIGEgSm9iIHdpdGggVVVJRCAwOTRiMGRjYy1lZDc1LTQxNWYtODQwOS0zMWU2YjIxYjUwMmQ6IGEgSm9iIGFscmVhZHkgZXhpc3RzIHdpdGggdGhpcyBVVUlELiBDaGFuZ2UgdGhlIFVVSUQgb3IgZGVsZXRlIHRoZSBvdGhlciBKb2IuPC9lcnJvcj4KICAgIDxlcnJvcj5Kb2IgYXQgaW5kZXggWzFdIGF0IGFyY2hpdmUgcGF0aDogcnVuZGVjay1ERUYxL2pvYnMvam9iLTJiZTk2NmU3LTQ0MGUtNDg5Ny04YTU5LTZhZDEyNmRmNjAzZS54bWwgaGFkIGVycm9yczogVmFsaWRhdGlvbiBlcnJvcnM6IENhbm5vdCBjcmVhdGUgYSBKb2Igd2l0aCBVVUlEIDJiZTk2NmU3LTQ0MGUtNDg5Ny04YTU5LTZhZDEyNmRmNjAzZTogYSBKb2IgYWxyZWFkeSBleGlzdHMgd2l0aCB0aGlzIFVVSUQuIENoYW5nZSB0aGUgVVVJRCBvciBkZWxldGUgdGhlIG90aGVyIEpvYi48L2Vycm9yPgogICAgPGVycm9yPkpvYiBhdCBpbmRleCBbMV0gYXQgYXJjaGl2ZSBwYXRoOiBydW5kZWNrLURFRjEvam9icy9qb2ItZTZmNjFhZTItYWVlMS00NjEwLTlmZTktOGM2ZGYyY2U4ZGJjLnhtbCBoYWQgZXJyb3JzOiBWYWxpZGF0aW9uIGVycm9yczogQ2Fubm90IGNyZWF0ZSBhIEpvYiB3aXRoIFVVSUQgZTZmNjFhZTItYWVlMS00NjEwLTlmZTktOGM2ZGYyY2U4ZGJjOiBhIEpvYiBhbHJlYWR5IGV4aXN0cyB3aXRoIHRoaXMgVVVJRC4gQ2hhbmdlIHRoZSBVVUlEIG9yIGRlbGV0ZSB0aGUgb3RoZXIgSm9iLjwvZXJyb3I+CiAgICA8ZXJyb3I+Sm9iIGF0IGluZGV4IFsxXSBhdCBhcmNoaXZlIHBhdGg6IHJ1bmRlY2stREVGMS9qb2JzL2pvYi0zZjY4NTNmOC1iNTg5LTRkYTgtYmE3Ni0xNjM2ZWY4MTFjZTYueG1sIGhhZCBlcnJvcnM6IFZhbGlkYXRpb24gZXJyb3JzOiBDYW5ub3QgY3JlYXRlIGEgSm9iIHdpdGggVVVJRCAzZjY4NTNmOC1iNTg5LTRkYTgtYmE3Ni0xNjM2ZWY4MTFjZTY6IGEgSm9iIGFscmVhZHkgZXhpc3RzIHdpdGggdGhpcyBVVUlELiBDaGFuZ2UgdGhlIFVVSUQgb3IgZGVsZXRlIHRoZSBvdGhlciBKb2IuPC9lcnJvcj4KICAgIDxlcnJvcj5Kb2IgYXQgaW5kZXggWzFdIGF0IGFyY2hpdmUgcGF0aDogcnVuZGVjay1ERUYxL2pvYnMvam9iLWFjNzhkZmJiLWE0OWMtNDBhYi1hY2Q5LTk0MDhiZTU2MWU3NC54bWwgaGFkIGVycm9yczogVmFsaWRhdGlvbiBlcnJvcnM6IENhbm5vdCBjcmVhdGUgYSBKb2Igd2l0aCBVVUlEIGFjNzhkZmJiLWE0OWMtNDBhYi1hY2Q5LTk0MDhiZTU2MWU3NDogYSBKb2IgYWxyZWFkeSBleGlzdHMgd2l0aCB0aGlzIFVVSUQuIENoYW5nZSB0aGUgVVVJRCBvciBkZWxldGUgdGhlIG90aGVyIEpvYi48L2Vycm9yPgogICAgPGVycm9yPkpvYiBhdCBpbmRleCBbMV0gYXQgYXJjaGl2ZSBwYXRoOiBydW5kZWNrLURFRjEvam9icy9qb2ItNzU2Yzk5ZjctMDRmYi00MzhjLTg0ZjUtMTcyMmQ5NjdkMWNiLnhtbCBoYWQgZXJyb3JzOiBWYWxpZGF0aW9uIGVycm9yczogQ2Fubm90IGNyZWF0ZSBhIEpvYiB3aXRoIFVVSUQgNzU2Yzk5ZjctMDRmYi00MzhjLTg0ZjUtMTcyMmQ5NjdkMWNiOiBhIEpvYiBhbHJlYWR5IGV4aXN0cyB3aXRoIHRoaXMgVVVJRC4gQ2hhbmdlIHRoZSBVVUlEIG9yIGRlbGV0ZSB0aGUgb3RoZXIgSm9iLjwvZXJyb3I+CiAgICA8ZXJyb3I+Sm9iIGF0IGluZGV4IFsxXSBhdCBhcmNoaXZlIHBhdGg6IHJ1bmRlY2stREVGMS9qb2JzL2pvYi0zYWVjN2RmYi05NTAyLTQ4NzYtODc2My0xYWE3ODhjOGZjOWMueG1sIGhhZCBlcnJvcnM6IFZhbGlkYXRpb24gZXJyb3JzOiBDYW5ub3QgY3JlYXRlIGEgSm9iIHdpdGggVVVJRCAzYWVjN2RmYi05NTAyLTQ4NzYtODc2My0xYWE3ODhjOGZjOWM6IGEgSm9iIGFscmVhZHkgZXhpc3RzIHdpdGggdGhpcyBVVUlELiBDaGFuZ2UgdGhlIFVVSUQgb3IgZGVsZXRlIHRoZSBvdGhlciBKb2IuPC9lcnJvcj4KICAgIDxlcnJvcj5Kb2IgYXQgaW5kZXggWzFdIGF0IGFyY2hpdmUgcGF0aDogcnVuZGVjay1ERUYxL2pvYnMvam9iLTg1OTZjYjljLTY4MzMtNDNmNy1hMGY3LTQ2NjNkZDhhYmMyOS54bWwgaGFkIGVycm9yczogVmFsaWRhdGlvbiBlcnJvcnM6IENhbm5vdCBjcmVhdGUgYSBKb2Igd2l0aCBVVUlEIDg1OTZjYjljLTY4MzMtNDNmNy1hMGY3LTQ2NjNkZDhhYmMyOTogYSBKb2IgYWxyZWFkeSBleGlzdHMgd2l0aCB0aGlzIFVVSUQuIENoYW5nZSB0aGUgVVVJRCBvciBkZWxldGUgdGhlIG90aGVyIEpvYi48L2Vycm9yPgogICAgPGVycm9yPkpvYiBhdCBpbmRleCBbMV0gYXQgYXJjaGl2ZSBwYXRoOiBydW5kZWNrLURFRjEvam9icy9qb2ItZDQwMjNhNzItNTRlMS00MWI1LWI0ZDQtNGRkN2U1ZDRmNWU0LnhtbCBoYWQgZXJyb3JzOiBWYWxpZGF0aW9uIGVycm9yczogQ2Fubm90IGNyZWF0ZSBhIEpvYiB3aXRoIFVVSUQgZDQwMjNhNzItNTRlMS00MWI1LWI0ZDQtNGRkN2U1ZDRmNWU0OiBhIEpvYiBhbHJlYWR5IGV4aXN0cyB3aXRoIHRoaXMgVVVJRC4gQ2hhbmdlIHRoZSBVVUlEIG9yIGRlbGV0ZSB0aGUgb3RoZXIgSm9iLjwvZXJyb3I+CiAgPC9lcnJvcnM+CjwvaW1wb3J0Pg==
diff --git a/src/test/resources/betamax/tapes/import_project_suv11.yaml b/src/test/resources/betamax/tapes/import_project_suv11.yaml
new file mode 100644
index 0000000..a8530d8
--- /dev/null
+++ b/src/test/resources/betamax/tapes/import_project_suv11.yaml
@@ -0,0 +1,71 @@
+!tape
+name: import_project_suv11
+interactions:
+- recorded: 2014-03-09T23:57:25.471Z
+ request:
+ method: PUT
+ uri: http://rundeck.local:4440/api/11/project/DEF2/import?importExecutions=true&jobUuidOption=preserve
+ headers:
+ Accept: text/xml
+ Content-Length: '8705'
+ Content-Type: application/zip
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ body: ''
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=10hrj0jebdc621ukdlal6qqyu3;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PGltcG9ydCBzdGF0dXM9J3N1Y2Nlc3NmdWwnIC8+
+- recorded: 2014-03-09T23:57:26.403Z
+ request:
+ method: PUT
+ uri: http://rundeck.local:4440/api/11/project/DEF2/import?importExecutions=false&jobUuidOption=preserve
+ headers:
+ Accept: text/xml
+ Content-Length: '8705'
+ Content-Type: application/zip
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ body: ''
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Server: Jetty(7.6.0.v20120127)
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PGltcG9ydCBzdGF0dXM9J3N1Y2Nlc3NmdWwnIC8+
+- recorded: 2014-03-09T23:57:27.155Z
+ request:
+ method: PUT
+ uri: http://rundeck.local:4440/api/11/project/DEF2/import?importExecutions=true&jobUuidOption=remove
+ headers:
+ Accept: text/xml
+ Content-Length: '8705'
+ Content-Type: application/zip
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ body: ''
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Server: Jetty(7.6.0.v20120127)
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PGltcG9ydCBzdGF0dXM9J3N1Y2Nlc3NmdWwnIC8+
diff --git a/src/test/resources/betamax/tapes/key_delete.yaml b/src/test/resources/betamax/tapes/key_delete.yaml
new file mode 100644
index 0000000..b52ed7f
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_delete.yaml
@@ -0,0 +1,36 @@
+!tape
+name: ssh_key_delete
+interactions:
+- recorded: 2014-04-04T23:16:02.256Z
+ request:
+ method: DELETE
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
+ headers:
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 204
+ headers:
+ Content-Type: text/html;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=3rsjs6vicpc619b1uy7oshp4y;Path=/
+- recorded: 2014-04-04T23:16:02.372Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 404
+ headers:
+ Content-Type: application/xml;charset=utf-8
+ Server: Jetty(7.6.0.v20120127)
+ body: !!binary |-
+ PGVycm9yPnJlc291cmNlIG5vdCBmb3VuZDogL3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YjwvZXJyb3I+
diff --git a/src/test/resources/betamax/tapes/key_get_data_private.yaml b/src/test/resources/betamax/tapes/key_get_data_private.yaml
new file mode 100644
index 0000000..ef0f58f
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_get_data_private.yaml
@@ -0,0 +1,23 @@
+!tape
+name: ssh_key_get_data_private
+interactions:
+- recorded: 2014-04-04T19:50:59.155Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file1.pem
+ headers:
+ Accept: application/pgp-keys
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: application/json;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=1gzu37lkjr0fitxhf5fgkgsfu;Path=/
+ body: !!binary |-
+ eyJwYXRoIjoia2V5cy90ZXN0L2V4YW1wbGUvZmlsZTEucGVtIiwidHlwZSI6ImZpbGUiLCJuYW1lIjoiZmlsZTEucGVtIiwidXJsIjoiaHR0cDovL2RpZ25hbi5sb2NhbDo0NDQwL2FwaS8xMS9zdG9yYWdlL2tleXMvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbSIsIm1ldGEiOnsiUnVuZGVjay1jb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iLCJSdW5kZWNrLWNvbnRlbnQtc2l6ZSI6IjUiLCJSdW5kZWNrLWNvbnRlbnQtbWFzayI6ImNvbnRlbnQiLCJSdW5kZWNrLWtleS10eXBlIjoicHJpdmF0ZSJ9fQo=
+
diff --git a/src/test/resources/betamax/tapes/key_get_data_public.yaml b/src/test/resources/betamax/tapes/key_get_data_public.yaml
new file mode 100644
index 0000000..054599f
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_get_data_public.yaml
@@ -0,0 +1,22 @@
+!tape
+name: ssh_key_get_data_public
+interactions:
+- recorded: 2014-04-04T20:20:44.331Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
+ headers:
+ Accept: application/pgp-keys
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: application/pgp-keys
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=1mrub15qsorpf10cisx24h8h03;Path=/
+ body: !!binary |-
+ dGVzdDE=
diff --git a/src/test/resources/betamax/tapes/key_get_private.yaml b/src/test/resources/betamax/tapes/key_get_private.yaml
new file mode 100644
index 0000000..57802a8
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_get_private.yaml
@@ -0,0 +1,23 @@
+!tape
+name: ssh_key_get_private
+interactions:
+- recorded: 2014-04-04T19:47:29.880Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file1.pem
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=utf-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=nc5p0he3nw19e4gegidc4bs7;Path=/
+ body: !!binary |-
+ PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2sta2V5LXR5cGU+cHJpdmF0ZTwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4K
+
diff --git a/src/test/resources/betamax/tapes/key_get_public.yaml b/src/test/resources/betamax/tapes/key_get_public.yaml
new file mode 100644
index 0000000..32c2510
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_get_public.yaml
@@ -0,0 +1,23 @@
+!tape
+name: ssh_key_get_public
+interactions:
+- recorded: 2014-04-04T19:47:29.626Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=utf-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=r6p6fl87ftrb1mkwwi0pcak5i;Path=/
+ body: !!binary |-
+ PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLWtleS10eXBlPnB1YmxpYzwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4N
+
diff --git a/src/test/resources/betamax/tapes/key_list_directory.yaml b/src/test/resources/betamax/tapes/key_list_directory.yaml
new file mode 100644
index 0000000..fb4c6f6
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_list_directory.yaml
@@ -0,0 +1,21 @@
+!tape
+name: key_list_directory
+interactions:
+- recorded: 2014-04-04T20:33:07.968Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: text/html;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=1stwtoatsosy91ca0gcrzde698;Path=/
+ body: application/pgp-keys5publicapplication/octet-stream5contentprivate
diff --git a/src/test/resources/betamax/tapes/key_list_root.yaml b/src/test/resources/betamax/tapes/key_list_root.yaml
new file mode 100644
index 0000000..b040c3e
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_list_root.yaml
@@ -0,0 +1,21 @@
+!tape
+name: key_list_root
+interactions:
+- recorded: 2014-04-04T20:41:16.501Z
+ request:
+ method: GET
+ uri: http://rundeck.local:4440/api/11/storage/keys/
+ headers:
+ Accept: text/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ response:
+ status: 200
+ headers:
+ Content-Type: text/html;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=5sj36vytg4y1182mziim4868b;Path=/
+ body: application/octet-stream1679contentprivate
diff --git a/src/test/resources/betamax/tapes/key_store_private.yaml b/src/test/resources/betamax/tapes/key_store_private.yaml
new file mode 100644
index 0000000..1a5d0b0
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_store_private.yaml
@@ -0,0 +1,26 @@
+!tape
+name: ssh_key_store_private
+interactions:
+- recorded: 2014-04-04T19:30:35.367Z
+ request:
+ method: POST
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file1.pem
+ headers:
+ Accept: text/xml
+ Content-Length: '5'
+ Content-Type: application/octet-stream
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ body: ''
+ response:
+ status: 201
+ headers:
+ Content-Type: application/xml;charset=utf-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=ktmwc4h53xfud6v2ch67x5p9;Path=/
+ body: !!binary |-
+ PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2sta2V5LXR5cGU+cHJpdmF0ZTwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4N
+
diff --git a/src/test/resources/betamax/tapes/key_store_public.yaml b/src/test/resources/betamax/tapes/key_store_public.yaml
new file mode 100644
index 0000000..fdd7638
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_store_public.yaml
@@ -0,0 +1,26 @@
+!tape
+name: ssh_key_store_public
+interactions:
+- recorded: 2014-04-04T19:34:02.683Z
+ request:
+ method: POST
+ uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
+ headers:
+ Accept: text/xml
+ Content-Length: '5'
+ Content-Type: application/pgp-keys
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+ body: ''
+ response:
+ status: 201
+ headers:
+ Content-Type: application/xml;charset=utf-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=2l3g8m0tvwef19jn2bu23bzk6;Path=/
+ body: !!binary |-
+ PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLWtleS10eXBlPnB1YmxpYzwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4K
+
diff --git a/src/test/resources/betamax/tapes/set_project_config_keyedv11.yaml b/src/test/resources/betamax/tapes/set_project_config_keyedv11.yaml
new file mode 100644
index 0000000..88cfd10
--- /dev/null
+++ b/src/test/resources/betamax/tapes/set_project_config_keyedv11.yaml
@@ -0,0 +1,26 @@
+!tape
+name: set_project_config_keyedv11
+interactions:
+- recorded: 2014-03-07T19:59:51.009Z
+ request:
+ method: PUT
+ uri: http://rundeck.local:4440/api/11/project/ABC/config/monkey-burrito
+ headers:
+ Accept: text/xml
+ Content-Type: application/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ Transfer-Encoding: chunked
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=3ssp8chdwsuw16hihk5frgpzy;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PHByb3BlcnR5IGtleT0nbW9ua2V5LWJ1cnJpdG8nIHZhbHVlPSdsZW1vbiBwaWUnIC8+
diff --git a/src/test/resources/betamax/tapes/set_project_configv11.yaml b/src/test/resources/betamax/tapes/set_project_configv11.yaml
new file mode 100644
index 0000000..573eeaa
--- /dev/null
+++ b/src/test/resources/betamax/tapes/set_project_configv11.yaml
@@ -0,0 +1,26 @@
+!tape
+name: set_project_configv11
+interactions:
+- recorded: 2014-02-27T21:00:27.197Z
+ request:
+ method: PUT
+ uri: http://rundeck.local:4440/api/11/project/monkey1/config
+ headers:
+ Accept: text/xml
+ Content-Type: application/xml
+ Host: rundeck.local:4440
+ Proxy-Connection: Keep-Alive
+ Transfer-Encoding: chunked
+ User-Agent: RunDeck API Java Client 11
+ X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+ response:
+ status: 200
+ headers:
+ Content-Type: application/xml;charset=UTF-8
+ Expires: Thu, 01 Jan 1970 00:00:00 GMT
+ Server: Jetty(7.6.0.v20120127)
+ Set-Cookie: JSESSIONID=19npj7cd0hpm71nfljn7nlbvh8;Path=/
+ X-Rundeck-API-Version: '11'
+ X-Rundeck-API-XML-Response-Wrapper: 'false'
+ body: !!binary |-
+ PGNvbmZpZz4KICA8cHJvcGVydHkga2V5PSdwcm9qZWN0Lm5hbWUnIHZhbHVlPSdtb25rZXkxJyAvPgogIDxwcm9wZXJ0eSBrZXk9J2FscGhhYmV0dHknIHZhbHVlPSdzcGFnaGV0dGknIC8+CiAgPHByb3BlcnR5IGtleT0nYmxoYS5ibGVlJyB2YWx1ZT0nYSBiaWcgYW1hemluZyB0aGluZ3kgc28gdGhlcmUuJyAvPgo8L2NvbmZpZz4=
diff --git a/src/test/resources/org/rundeck/api/parser/projectv11.xml b/src/test/resources/org/rundeck/api/parser/projectv11.xml
new file mode 100644
index 0000000..e47e69d
--- /dev/null
+++ b/src/test/resources/org/rundeck/api/parser/projectv11.xml
@@ -0,0 +1,17 @@
+
+ ziggy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/resources/org/rundeck/api/test-archive.zip b/src/test/resources/org/rundeck/api/test-archive.zip
new file mode 100644
index 0000000..e5a1dab
Binary files /dev/null and b/src/test/resources/org/rundeck/api/test-archive.zip differ