diff --git a/pom.xml b/pom.xml index 5e2d9b8..f063c6e 100644 --- a/pom.xml +++ b/pom.xml @@ -396,6 +396,11 @@ httpclient 4.1.1 + + org.apache.httpcomponents + httpmime + 4.1.1 + commons-lang diff --git a/src/changes/changes.xml b/src/changes/changes.xml index e464c00..03e0a1a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,10 +22,12 @@ Vincent Behar + + Import jobs + Support for using an HTTP proxy + - - Initial release - + Initial release diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index e701034..0403c47 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -18,6 +18,7 @@ package org.rundeck.api; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.ProxySelector; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -26,6 +27,7 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; +import java.util.Map.Entry; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; @@ -39,7 +41,11 @@ import org.apache.http.client.methods.HttpRequestBase; 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.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntity; +import org.apache.http.entity.mime.content.InputStreamBody; import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.ProxySelectorRoutePlanner; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; @@ -138,6 +144,30 @@ class ApiCall { return response; } + /** + * Execute an HTTP POST 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 + */ + public T post(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, + RundeckApiLoginException { + HttpPost httpPost = new HttpPost(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath); + + // POST a multi-part request, with all attachments + MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE); + for (Entry attachment : apiPath.getAttachments().entrySet()) { + entity.addPart(attachment.getKey(), new InputStreamBody(attachment.getValue(), attachment.getKey())); + } + httpPost.setEntity(entity); + + return execute(httpPost, parser); + } + /** * Execute an HTTP DELETE 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. @@ -316,6 +346,9 @@ class ApiCall { * @return an {@link HttpClient} instance - won't be null */ private HttpClient instantiateHttpClient() { + DefaultHttpClient httpClient = new DefaultHttpClient(); + + // configure SSL SSLSocketFactory socketFactory = null; try { socketFactory = new SSLSocketFactory(new TrustStrategy() { @@ -334,9 +367,13 @@ class ApiCall { } catch (KeyStoreException e) { throw new RuntimeException(e); } - - HttpClient httpClient = new DefaultHttpClient(); httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", 443, socketFactory)); + + // configure proxy (use system env : http.proxyHost / http.proxyPort) + System.setProperty("java.net.useSystemProxies", "true"); + httpClient.setRoutePlanner(new ProxySelectorRoutePlanner(httpClient.getConnectionManager().getSchemeRegistry(), + ProxySelector.getDefault())); + return httpClient; } } diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java index 960928b..b9c6fad 100644 --- a/src/main/java/org/rundeck/api/ApiPathBuilder.java +++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java @@ -15,6 +15,9 @@ */ package org.rundeck.api; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; import java.util.Properties; import org.apache.commons.lang.StringUtils; import org.rundeck.api.util.ParametersUtil; @@ -29,7 +32,10 @@ class ApiPathBuilder { /** Internally, we store everything in a {@link StringBuilder} */ private final StringBuilder apiPath; - /** Maker for using the right separator between parameters ("?" or "&") */ + /** When POSTing, we can add attachments */ + private final Map attachments; + + /** Marker for using the right separator between parameters ("?" or "&") */ private boolean firstParamDone = false; /** @@ -40,6 +46,7 @@ class ApiPathBuilder { */ public ApiPathBuilder(String... paths) { apiPath = new StringBuilder(); + attachments = new HashMap(); if (paths != null) { for (String path : paths) { if (StringUtils.isNotBlank(path)) { @@ -68,6 +75,22 @@ class ApiPathBuilder { return this; } + /** + * Append the given parameter (key and value). This will only append the parameter if it is not null, and make sure + * to add the right separator ("?" or "&") before. The key and value will be separated by the "=" character. Also, + * the value will be converted to lower-case. + * + * @param key of the parameter. Must not be null or empty + * @param value of the parameter. May be null + * @return this, for method chaining + */ + public ApiPathBuilder param(String key, Enum value) { + if (value != null) { + param(key, StringUtils.lowerCase(value.toString())); + } + return this; + } + /** * Append the given parameter (key and value). This will only append the parameter if it is not null, and make sure * to add the right separator ("?" or "&") before. The key and value will be separated by the "=" character. @@ -129,6 +152,28 @@ class ApiPathBuilder { return this; } + /** + * When POSTing a request, add the given {@link InputStream} as an attachment to the content of the request. This + * will only add the stream if it is not null. + * + * @param name of the attachment. Must not be null or empty + * @param stream. May be null + * @return this, for method chaining + */ + public ApiPathBuilder attach(String name, InputStream stream) { + if (stream != null) { + attachments.put(name, stream); + } + return this; + } + + /** + * @return all attachments to be POSTed, with their names + */ + public Map getAttachments() { + return attachments; + } + @Override public String toString() { return apiPath.toString(); diff --git a/src/main/java/org/rundeck/api/FileType.java b/src/main/java/org/rundeck/api/FileType.java new file mode 100644 index 0000000..ef6f473 --- /dev/null +++ b/src/main/java/org/rundeck/api/FileType.java @@ -0,0 +1,25 @@ +/* + * Copyright 2011 Vincent Behar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.rundeck.api; + +/** + * All supported types of files. + * + * @author Vincent Behar + */ +public enum FileType { + XML, YAML; +} diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 859c62a..c2b3a70 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -16,6 +16,7 @@ package org.rundeck.api; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; @@ -30,6 +31,8 @@ import org.rundeck.api.RundeckApiException.RundeckApiLoginException; import org.rundeck.api.domain.RundeckAbort; import org.rundeck.api.domain.RundeckExecution; import org.rundeck.api.domain.RundeckJob; +import org.rundeck.api.domain.RundeckJobsImportMethod; +import org.rundeck.api.domain.RundeckJobsImportResult; import org.rundeck.api.domain.RundeckNode; import org.rundeck.api.domain.RundeckProject; import org.rundeck.api.domain.RundeckSystemInfo; @@ -37,6 +40,7 @@ import org.rundeck.api.domain.RundeckExecution.ExecutionStatus; import org.rundeck.api.parser.AbortParser; import org.rundeck.api.parser.ExecutionParser; import org.rundeck.api.parser.JobParser; +import org.rundeck.api.parser.JobsImportResultParser; import org.rundeck.api.parser.ListParser; import org.rundeck.api.parser.NodeParser; import org.rundeck.api.parser.ProjectParser; @@ -316,6 +320,173 @@ public class RundeckClient implements Serializable { return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId)); } + /** + * Import the definitions of jobs, from the given file + * + * @param filename of the file containing the jobs definitions - mandatory + * @param fileType type of the file. See {@link FileType} - mandatory + * @return a {@link RundeckJobsImportResult} instance - won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the filename or fileType is blank (null, empty or whitespace), or the + * fileType is invalid + * @throws IOException if we failed to read the file + * @see #importJobs(InputStream, String) + * @see #importJobs(String, FileType, RundeckJobsImportMethod) + */ + public RundeckJobsImportResult importJobs(String filename, String fileType) throws RundeckApiException, + RundeckApiLoginException, IllegalArgumentException, IOException { + AssertUtil.notBlank(fileType, "fileType is mandatory to import jobs !"); + return importJobs(filename, FileType.valueOf(StringUtils.upperCase(fileType))); + } + + /** + * Import the definitions of jobs, from the given file + * + * @param filename of the file containing the jobs definitions - mandatory + * @param fileType type of the file. See {@link FileType} - mandatory + * @return a {@link RundeckJobsImportResult} instance - won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the filename is blank (null, empty or whitespace), or the fileType is null + * @throws IOException if we failed to read the file + * @see #importJobs(InputStream, FileType) + * @see #importJobs(String, FileType, RundeckJobsImportMethod) + */ + public RundeckJobsImportResult importJobs(String filename, FileType fileType) throws RundeckApiException, + RundeckApiLoginException, IllegalArgumentException, IOException { + return importJobs(filename, fileType, (RundeckJobsImportMethod) null); + } + + /** + * Import the definitions of jobs, from the given file, using the given behavior + * + * @param filename of the file containing the jobs definitions - mandatory + * @param fileType type of the file. See {@link FileType} - mandatory + * @param importBehavior see {@link RundeckJobsImportMethod} + * @return a {@link RundeckJobsImportResult} instance - won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the filename or fileType is blank (null, empty or whitespace), or the + * fileType or behavior is not valid + * @throws IOException if we failed to read the file + * @see #importJobs(InputStream, String, String) + * @see #importJobs(String, FileType, RundeckJobsImportMethod) + */ + public RundeckJobsImportResult importJobs(String filename, String fileType, String importBehavior) + throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException, IOException { + AssertUtil.notBlank(fileType, "fileType is mandatory to import jobs !"); + return importJobs(filename, + FileType.valueOf(StringUtils.upperCase(fileType)), + RundeckJobsImportMethod.valueOf(StringUtils.upperCase(importBehavior))); + } + + /** + * Import the definitions of jobs, from the given file, using the given behavior + * + * @param filename of the file containing the jobs definitions - mandatory + * @param fileType type of the file. See {@link FileType} - mandatory + * @param importBehavior see {@link RundeckJobsImportMethod} + * @return a {@link RundeckJobsImportResult} instance - won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the filename is blank (null, empty or whitespace), or the fileType is null + * @throws IOException if we failed to read the file + * @see #importJobs(InputStream, FileType, RundeckJobsImportMethod) + */ + public RundeckJobsImportResult importJobs(String filename, FileType fileType, RundeckJobsImportMethod importBehavior) + throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException, IOException { + AssertUtil.notBlank(filename, "filename (of jobs file) is mandatory to import jobs !"); + FileInputStream stream = null; + try { + stream = FileUtils.openInputStream(new File(filename)); + return importJobs(stream, fileType, importBehavior); + } finally { + IOUtils.closeQuietly(stream); + } + } + + /** + * Import the definitions of jobs, from the given input stream + * + * @param stream inputStream for reading the definitions - mandatory + * @param fileType type of the file. See {@link FileType} - mandatory + * @return a {@link RundeckJobsImportResult} instance - won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the stream is null, or the fileType is blank (null, empty or whitespace) or + * invalid + * @see #importJobs(String, String) + * @see #importJobs(InputStream, FileType, RundeckJobsImportMethod) + */ + public RundeckJobsImportResult importJobs(InputStream stream, String fileType) throws RundeckApiException, + RundeckApiLoginException, IllegalArgumentException { + AssertUtil.notBlank(fileType, "fileType is mandatory to import jobs !"); + return importJobs(stream, FileType.valueOf(StringUtils.upperCase(fileType))); + } + + /** + * Import the definitions of jobs, from the given input stream + * + * @param stream inputStream for reading the definitions - mandatory + * @param fileType type of the file. See {@link FileType} - mandatory + * @return a {@link RundeckJobsImportResult} instance - won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the stream or fileType is null + * @see #importJobs(String, FileType) + * @see #importJobs(InputStream, FileType, RundeckJobsImportMethod) + */ + public RundeckJobsImportResult importJobs(InputStream stream, FileType fileType) throws RundeckApiException, + RundeckApiLoginException, IllegalArgumentException { + return importJobs(stream, fileType, (RundeckJobsImportMethod) null); + } + + /** + * Import the definitions of jobs, from the given input stream, using the given behavior + * + * @param stream inputStream for reading the definitions - mandatory + * @param fileType type of the file. See {@link FileType} - mandatory + * @param importBehavior see {@link RundeckJobsImportMethod} + * @return a {@link RundeckJobsImportResult} instance - won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the stream is null, or the fileType is blank (null, empty or whitespace), or + * the fileType or behavior is not valid + * @see #importJobs(String, String, String) + * @see #importJobs(InputStream, FileType, RundeckJobsImportMethod) + */ + public RundeckJobsImportResult importJobs(InputStream stream, String fileType, String importBehavior) + throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException { + AssertUtil.notBlank(fileType, "fileType is mandatory to import jobs !"); + return importJobs(stream, + FileType.valueOf(StringUtils.upperCase(fileType)), + RundeckJobsImportMethod.valueOf(StringUtils.upperCase(importBehavior))); + } + + /** + * Import the definitions of jobs, from the given input stream, using the given behavior + * + * @param stream inputStream for reading the definitions - mandatory + * @param fileType type of the file. See {@link FileType} - mandatory + * @param importBehavior see {@link RundeckJobsImportMethod} + * @return a {@link RundeckJobsImportResult} instance - won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the stream or fileType is null + * @see #importJobs(String, FileType, RundeckJobsImportMethod) + */ + public RundeckJobsImportResult importJobs(InputStream stream, FileType fileType, + RundeckJobsImportMethod importBehavior) throws RundeckApiException, RundeckApiLoginException, + IllegalArgumentException { + AssertUtil.notNull(stream, "inputStream of jobs is mandatory to import jobs !"); + AssertUtil.notNull(fileType, "fileType is mandatory to import jobs !"); + return new ApiCall(this).post(new ApiPathBuilder("/jobs/import").param("format", fileType) + .param("dupeOption", importBehavior) + .attach("xmlBatch", stream), + new JobsImportResultParser("result")); + } + /** * Find a job, identified by its project, group and name. Note that the groupPath is optional, as a job does not * need to belong to a group (either pass null, or an empty string). @@ -851,8 +1022,7 @@ public class RundeckClient implements Serializable { public List getJobExecutions(String jobId, ExecutionStatus status, Long max, Long offset) throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException { AssertUtil.notBlank(jobId, "jobId is mandatory to get the executions of a job !"); - return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId, "/executions").param("status", - status != null ? StringUtils.lowerCase(status.toString()) : null) + return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId, "/executions").param("status", status) .param("max", max) .param("offset", offset), new ListParser(new ExecutionParser(), diff --git a/src/main/java/org/rundeck/api/domain/RundeckJobsImportMethod.java b/src/main/java/org/rundeck/api/domain/RundeckJobsImportMethod.java new file mode 100644 index 0000000..1bcb452 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/RundeckJobsImportMethod.java @@ -0,0 +1,25 @@ +/* + * Copyright 2011 Vincent Behar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.rundeck.api.domain; + +/** + * The behavior when importing jobs (which may already exist). + * + * @author Vincent Behar + */ +public enum RundeckJobsImportMethod { + CREATE, UPDATE, SKIP; +} diff --git a/src/main/java/org/rundeck/api/domain/RundeckJobsImportResult.java b/src/main/java/org/rundeck/api/domain/RundeckJobsImportResult.java new file mode 100644 index 0000000..6e6ee1d --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/RundeckJobsImportResult.java @@ -0,0 +1,106 @@ +/* + * Copyright 2011 Vincent Behar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.rundeck.api.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Result of importing some jobs into RunDeck + * + * @author Vincent Behar + */ +public class RundeckJobsImportResult implements Serializable { + + private static final long serialVersionUID = 1L; + + private final List succeededJobs = new ArrayList(); + + private final List skippedJobs = new ArrayList(); + + private final Map failedJobs = new HashMap(); + + public void addSucceededJob(RundeckJob job) { + succeededJobs.add(job); + } + + public void addSkippedJob(RundeckJob job) { + skippedJobs.add(job); + } + + public void addFailedJob(RundeckJob job, String errorMessage) { + failedJobs.put(job, errorMessage); + } + + public List getSucceededJobs() { + return succeededJobs; + } + + public List getSkippedJobs() { + return skippedJobs; + } + + public Map getFailedJobs() { + return failedJobs; + } + + @Override + public String toString() { + return "RundeckJobsImportResult [succeededJobs=" + succeededJobs + ", skippedJobs=" + skippedJobs + + ", failedJobs=" + failedJobs + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((failedJobs == null) ? 0 : failedJobs.hashCode()); + result = prime * result + ((skippedJobs == null) ? 0 : skippedJobs.hashCode()); + result = prime * result + ((succeededJobs == null) ? 0 : succeededJobs.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RundeckJobsImportResult other = (RundeckJobsImportResult) obj; + if (failedJobs == null) { + if (other.failedJobs != null) + return false; + } else if (!failedJobs.equals(other.failedJobs)) + return false; + if (skippedJobs == null) { + if (other.skippedJobs != null) + return false; + } else if (!skippedJobs.equals(other.skippedJobs)) + return false; + if (succeededJobs == null) { + if (other.succeededJobs != null) + return false; + } else if (!succeededJobs.equals(other.succeededJobs)) + return false; + return true; + } + +} diff --git a/src/main/java/org/rundeck/api/parser/JobsImportResultParser.java b/src/main/java/org/rundeck/api/parser/JobsImportResultParser.java new file mode 100644 index 0000000..8aaab20 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/JobsImportResultParser.java @@ -0,0 +1,80 @@ +/* + * Copyright 2011 Vincent Behar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.rundeck.api.parser; + +import java.util.List; +import org.dom4j.Node; +import org.rundeck.api.domain.RundeckJob; +import org.rundeck.api.domain.RundeckJobsImportResult; + +/** + * Parser for a single {@link RundeckJobsImportResult} + * + * @author Vincent Behar + */ +public class JobsImportResultParser implements XmlNodeParser { + + private String xpath; + + public JobsImportResultParser() { + super(); + } + + /** + * @param xpath of the result element if it is not the root node + */ + public JobsImportResultParser(String xpath) { + super(); + this.xpath = xpath; + } + + @Override + public RundeckJobsImportResult parseXmlNode(Node node) { + Node resultNode = xpath != null ? node.selectSingleNode(xpath) : node; + + RundeckJobsImportResult result = new RundeckJobsImportResult(); + + @SuppressWarnings("unchecked") + List succeededJobsNodes = resultNode.selectNodes("succeeded/job"); + if (succeededJobsNodes != null) { + for (Node succeededJobNode : succeededJobsNodes) { + RundeckJob job = new JobParser().parseXmlNode(succeededJobNode); + result.addSucceededJob(job); + } + } + + @SuppressWarnings("unchecked") + List skippedJobsNodes = resultNode.selectNodes("skipped/job"); + if (skippedJobsNodes != null) { + for (Node skippedJobNode : skippedJobsNodes) { + RundeckJob job = new JobParser().parseXmlNode(skippedJobNode); + result.addSkippedJob(job); + } + } + + @SuppressWarnings("unchecked") + List failedJobsNodes = resultNode.selectNodes("failed/job"); + if (failedJobsNodes != null) { + for (Node failedJobNode : failedJobsNodes) { + RundeckJob job = new JobParser().parseXmlNode(failedJobNode); + result.addFailedJob(job, failedJobNode.valueOf("error")); + } + } + + return result; + } + +} diff --git a/src/site/confluence/groovy.confluence b/src/site/confluence/groovy.confluence index 9423d11..f8d459c 100644 --- a/src/site/confluence/groovy.confluence +++ b/src/site/confluence/groovy.confluence @@ -85,6 +85,16 @@ rundeck.exportJobsToFile("/tmp/jobs.xml", "my-project") rundeck.exportJobToFile("/tmp/job.xml", "job-id") {code} +h2. Importing jobs + +{code} +import org.rundeck.api.RundeckClient +rundeck = new RundeckClient("http://localhost:4440", "admin", "admin") + +result = rundeck.importJobs("/tmp/jobs.xml", "xml") +println "${result.succeededJobs.size} jobs successfully imported, ${result.skippedJobs.size} jobs skipped, and ${result.failedJobs.size} jobs failed" +{code} + h2. And more... See the API documentation of the [RundeckClient|./apidocs/org/rundeck/api/RundeckClient.html] class for more interactions with your RunDeck instance... diff --git a/src/site/confluence/jruby.confluence b/src/site/confluence/jruby.confluence index afcc9f6..fee02db 100644 --- a/src/site/confluence/jruby.confluence +++ b/src/site/confluence/jruby.confluence @@ -90,6 +90,16 @@ rundeck.exportJobsToFile("/tmp/jobs.xml", "my-project") rundeck.exportJobToFile("/tmp/job.xml", "job-id") {code} +h2. Importing jobs + +{code} +import org.rundeck.api.RundeckClient +rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin") + +result = rundeck.importJobs("/tmp/jobs.xml", "xml") +puts "#{result.succeededJobs.size} jobs successfully imported, #{result.skippedJobs.size} jobs skipped, and #{result.failedJobs.size} jobs failed" +{code} + h2. And more... See the API documentation of the [RundeckClient|./apidocs/org/rundeck/api/RundeckClient.html] class for more interactions with your RunDeck instance... diff --git a/src/site/confluence/jython.confluence b/src/site/confluence/jython.confluence index 0d25926..846e4dc 100644 --- a/src/site/confluence/jython.confluence +++ b/src/site/confluence/jython.confluence @@ -74,6 +74,16 @@ rundeck.exportJobsToFile("/tmp/jobs.xml", "my-project") rundeck.exportJobToFile("/tmp/job.xml", "job-id") {code} +h2. Importing jobs + +{code} +from org.rundeck.api import RundeckClient +rundeck = RundeckClient("http://localhost:4440", "admin", "admin") + +result = rundeck.importJobs("/tmp/jobs.xml", "xml") +print("%s jobs successfully imported, %s jobs skipped, and %s jobs failed" % (result.succeededJobs.size, result.skippedJobs.size, result.failedJobs.size)) +{code} + h2. And more... See the API documentation of the [RundeckClient|./apidocs/org/rundeck/api/RundeckClient.html] class for more interactions with your RunDeck instance... diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence index f9310e8..5701067 100644 --- a/src/site/confluence/status.confluence +++ b/src/site/confluence/status.confluence @@ -10,7 +10,7 @@ h2. RunDeck API version 1 * Listing Jobs - OK * Running a Job - OK * Exporting Jobs - OK (XML only, YAML not supported yet) -* Importing Jobs - *TODO* +* Importing Jobs - OK * Getting a Job Definition - OK (XML only, YAML not supported yet) * Deleting a Job Definition - OK * Getting Executions for a Job - OK diff --git a/src/site/fml/faq.fml b/src/site/fml/faq.fml index 38db9c9..50f75c7 100644 --- a/src/site/fml/faq.fml +++ b/src/site/fml/faq.fml @@ -36,6 +36,30 @@ + + + + + HTTP connections to RunDeck + + + + What about SSL (HTTPS) ? + + +

The lib will trust any certificate when connecting to a RunDeck instance running on HTTPS (even self-signed certificates).

+
+
+ + + + How do I use a proxy ? + + +

See the Java doc on proxies. You will need a JAVA_OPTS env variable with a value like "-Dhttp.proxyHost=localhost -Dhttp.proxyPort=8888".

+
+
+
diff --git a/src/test/java/org/rundeck/api/parser/JobsImportResultParserTest.java b/src/test/java/org/rundeck/api/parser/JobsImportResultParserTest.java new file mode 100644 index 0000000..a2bc99b --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/JobsImportResultParserTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011 Vincent Behar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.rundeck.api.parser; + +import java.io.InputStream; +import org.dom4j.Document; +import org.junit.Assert; +import org.junit.Test; +import org.rundeck.api.domain.RundeckJob; +import org.rundeck.api.domain.RundeckJobsImportResult; + +/** + * Test the {@link JobsImportResultParser} + * + * @author Vincent Behar + */ +public class JobsImportResultParserTest { + + @Test + public void parseResult() throws Exception { + InputStream input = getClass().getResourceAsStream("jobs-import.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckJobsImportResult result = new JobsImportResultParser("result").parseXmlNode(document); + + Assert.assertEquals(2, result.getSucceededJobs().size()); + Assert.assertEquals(0, result.getSkippedJobs().size()); + Assert.assertEquals(1, result.getFailedJobs().size()); + + Assert.assertEquals("job-one", result.getSucceededJobs().get(0).getName()); + Assert.assertEquals("job-two", result.getSucceededJobs().get(1).getName()); + + RundeckJob failedJob = result.getFailedJobs().keySet().iterator().next(); + Assert.assertEquals("job-three", failedJob.getName()); + Assert.assertEquals("Error message", result.getFailedJobs().get(failedJob)); + } + +} diff --git a/src/test/resources/org/rundeck/api/parser/jobs-import.xml b/src/test/resources/org/rundeck/api/parser/jobs-import.xml new file mode 100644 index 0000000..437c6d0 --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/jobs-import.xml @@ -0,0 +1 @@ +1job-oneimporttest/job/show/12job-twoimporttest/job/show/2job-threeimporttestError message \ No newline at end of file