From 564a3ce90f61dcb10c46c4553e68e6221f516248 Mon Sep 17 00:00:00 2001 From: Vincent Behar Date: Mon, 1 Aug 2011 17:29:58 +0200 Subject: [PATCH] start working on a new-style api (fluent api / method chaining) --- src/main/java/org/rundeck/api/ApiCall.java | 17 +- .../org/rundeck/api/OldApiPathBuilder.java | 220 +++++++++ .../java/org/rundeck/api/RundeckClient.java | 188 ++++---- .../api/{ => request}/ApiPathBuilder.java | 2 +- .../org/rundeck/api/request/ApiRequest.java | 444 ++++++++++++++++++ .../org/rundeck/api/request/AuthRequest.java | 36 ++ .../rundeck/api/request/HistoryRequest.java | 83 ++++ .../rundeck/api/request/JobRunRequest.java | 72 +++ .../api/request/JobTriggerRequest.java | 64 +++ .../api/request/JobsListingRequest.java | 53 +++ .../org/rundeck/api/request/PingRequest.java | 37 ++ .../api/request/ProjectDetailsRequest.java | 43 ++ .../api/request/ProjectsListingRequest.java | 41 ++ 13 files changed, 1208 insertions(+), 92 deletions(-) create mode 100644 src/main/java/org/rundeck/api/OldApiPathBuilder.java rename src/main/java/org/rundeck/api/{ => request}/ApiPathBuilder.java (99%) create mode 100644 src/main/java/org/rundeck/api/request/ApiRequest.java create mode 100644 src/main/java/org/rundeck/api/request/AuthRequest.java create mode 100644 src/main/java/org/rundeck/api/request/HistoryRequest.java create mode 100644 src/main/java/org/rundeck/api/request/JobRunRequest.java create mode 100644 src/main/java/org/rundeck/api/request/JobTriggerRequest.java create mode 100644 src/main/java/org/rundeck/api/request/JobsListingRequest.java create mode 100644 src/main/java/org/rundeck/api/request/PingRequest.java create mode 100644 src/main/java/org/rundeck/api/request/ProjectDetailsRequest.java create mode 100644 src/main/java/org/rundeck/api/request/ProjectsListingRequest.java diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index 7fee618..a485351 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -66,6 +66,7 @@ import org.rundeck.api.util.AssertUtil; * * @author Vincent Behar */ +@Deprecated class ApiCall { /** RunDeck HTTP header for the auth-token (in case of token-based authentication) */ @@ -158,14 +159,14 @@ class ApiCall { * 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 apiPath on which we will make the HTTP request - see {@link OldApiPathBuilder} * @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 get(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, + public T get(OldApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { return execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), parser); } @@ -174,13 +175,13 @@ class ApiCall { * Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the * API call. * - * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder} + * @param apiPath on which we will make the HTTP request - see {@link OldApiPathBuilder} * @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) */ - public InputStream get(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException, + public InputStream get(OldApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { ByteArrayInputStream response = execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath)); @@ -195,14 +196,14 @@ class ApiCall { * 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 apiPath on which we will make the HTTP request - see {@link OldApiPathBuilder} * @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 post(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, + public T post(OldApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { HttpPost httpPost = new HttpPost(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath); @@ -220,14 +221,14 @@ class ApiCall { * 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. * - * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder} + * @param apiPath on which we will make the HTTP request - see {@link OldApiPathBuilder} * @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 delete(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, + public T delete(OldApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { return execute(new HttpDelete(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), parser); } diff --git a/src/main/java/org/rundeck/api/OldApiPathBuilder.java b/src/main/java/org/rundeck/api/OldApiPathBuilder.java new file mode 100644 index 0000000..aaaf29a --- /dev/null +++ b/src/main/java/org/rundeck/api/OldApiPathBuilder.java @@ -0,0 +1,220 @@ +/* + * 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; + +import java.io.InputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import org.apache.commons.lang.StringUtils; +import org.rundeck.api.util.ParametersUtil; + +/** + * Builder for API paths + * + * @author Vincent Behar + */ +@Deprecated +class OldApiPathBuilder { + + /** Internally, we store everything in a {@link StringBuilder} */ + private final StringBuilder apiPath; + + /** When POSTing, we can add attachments */ + private final Map attachments; + + /** Marker for using the right separator between parameters ("?" or "&") */ + private boolean firstParamDone = false; + + /** + * Build a new instance, for the given "path" (the "path" is the part before the parameters. The path and the + * parameters are separated by a "?") + * + * @param paths elements of the path + */ + public OldApiPathBuilder(String... paths) { + apiPath = new StringBuilder(); + attachments = new HashMap(); + if (paths != null) { + for (String path : paths) { + if (StringUtils.isNotBlank(path)) { + append(path); + } + } + } + } + + /** + * Append the given parameter (key and value). This will only append the parameter if it is not blank (null, empty + * or whitespace), 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 url-encoded. + * + * @param key of the parameter. Must not be null or empty + * @param value of the parameter. May be null/empty/blank. Will be url-encoded. + * @return this, for method chaining + */ + public OldApiPathBuilder param(String key, String value) { + if (StringUtils.isNotBlank(value)) { + appendSeparator(); + append(key); + append("="); + append(ParametersUtil.urlEncode(value)); + } + 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 OldApiPathBuilder 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. + * + * @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 OldApiPathBuilder param(String key, Date value) { + if (value != null) { + param(key, value.getTime()); + } + 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. + * + * @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 OldApiPathBuilder param(String key, Long value) { + if (value != null) { + param(key, 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. + * + * @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 OldApiPathBuilder param(String key, Integer value) { + if (value != null) { + param(key, 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. + * + * @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 OldApiPathBuilder param(String key, Boolean value) { + if (value != null) { + param(key, value.toString()); + } + return this; + } + + /** + * Append the given node filters, only if it is not null/empty + * + * @param nodeFilters may be null/empty + * @return this, for method chaining + * @see ParametersUtil#generateNodeFiltersString(Properties) + */ + public OldApiPathBuilder nodeFilters(Properties nodeFilters) { + String filters = ParametersUtil.generateNodeFiltersString(nodeFilters); + if (StringUtils.isNotBlank(filters)) { + appendSeparator(); + append(filters); + } + 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 OldApiPathBuilder 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(); + } + + /** + * Append the given string + * + * @param str to append + */ + private void append(String str) { + apiPath.append(str); + } + + /** + * Append the right separator "?" or "&" between 2 parameters + */ + private void appendSeparator() { + if (firstParamDone) { + append("&"); + } else { + append("?"); + firstParamDone = true; + } + } + +} diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index ec919de..82f8f77 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -47,9 +47,16 @@ 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; import org.rundeck.api.parser.StringParser; import org.rundeck.api.parser.SystemInfoParser; +import org.rundeck.api.request.AuthRequest; +import org.rundeck.api.request.HistoryRequest; +import org.rundeck.api.request.JobRunRequest; +import org.rundeck.api.request.JobTriggerRequest; +import org.rundeck.api.request.JobsListingRequest; +import org.rundeck.api.request.PingRequest; +import org.rundeck.api.request.ProjectDetailsRequest; +import org.rundeck.api.request.ProjectsListingRequest; import org.rundeck.api.util.AssertUtil; import org.rundeck.api.util.ParametersUtil; @@ -154,7 +161,7 @@ public class RundeckClient implements Serializable { * @throws RundeckApiException if the ping fails */ public void ping() throws RundeckApiException { - new ApiCall(this).ping(); + new PingRequest(this).execute(); } /** @@ -164,7 +171,7 @@ public class RundeckClient implements Serializable { * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) */ public void testAuth() throws RundeckApiLoginException, RundeckApiTokenException { - new ApiCall(this).testAuth(); + new AuthRequest(this).execute(); } /** @@ -176,6 +183,38 @@ public class RundeckClient implements Serializable { testAuth(); } + /* + * New-style API + */ + + public ProjectsListingRequest newProjectsListingRequest() { + return new ProjectsListingRequest(this); + } + + public ProjectDetailsRequest newProjectDetailsRequest(String projectName) { + return new ProjectDetailsRequest(this, projectName); + } + + public JobsListingRequest newJobsListingRequest(String project) { + return new JobsListingRequest(this, project); + } + + public JobTriggerRequest newJobTriggerRequest(String jobId) { + return new JobTriggerRequest(this, jobId); + } + + public JobRunRequest newJobRunRequest(String jobId) { + return new JobRunRequest(this, jobId); + } + + public HistoryRequest newHistoryRequest(String project) { + return new HistoryRequest(this, project); + } + + /* + * Old-style API + */ + /* * Projects */ @@ -190,8 +229,7 @@ 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")); + return newProjectsListingRequest().execute(); } /** @@ -206,9 +244,7 @@ public class RundeckClient implements Serializable { */ public RundeckProject getProject(String projectName) throws RundeckApiException, RundeckApiLoginException, 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")); + return newProjectDetailsRequest(projectName).execute(); } /* @@ -244,7 +280,7 @@ public class RundeckClient implements Serializable { */ public List getJobs(String project) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return getJobs(project, null, null, new String[0]); + return newJobsListingRequest(project).execute(); } /** @@ -264,10 +300,10 @@ public class RundeckClient implements Serializable { public List getJobs(String project, String jobFilter, String groupPath, String... jobIds) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(project, "project is mandatory to get all jobs !"); - return new ApiCall(this).get(new ApiPathBuilder("/jobs").param("project", project) - .param("jobFilter", jobFilter) - .param("groupPath", groupPath) - .param("idlist", StringUtils.join(jobIds, ",")), + return new ApiCall(this).get(new OldApiPathBuilder("/jobs").param("project", project) + .param("jobFilter", jobFilter) + .param("groupPath", groupPath) + .param("idlist", StringUtils.join(jobIds, ",")), new ListParser(new JobParser(), "result/jobs/job")); } @@ -450,11 +486,12 @@ public class RundeckClient implements Serializable { throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(format, "format is mandatory to export jobs !"); AssertUtil.notBlank(project, "project is mandatory to export jobs !"); - return new ApiCall(this).get(new ApiPathBuilder("/jobs/export").param("format", format) - .param("project", project) - .param("jobFilter", jobFilter) - .param("groupPath", groupPath) - .param("idlist", StringUtils.join(jobIds, ","))); + return new ApiCall(this).get(new OldApiPathBuilder("/jobs/export").param("format", format) + .param("project", project) + .param("jobFilter", jobFilter) + .param("groupPath", groupPath) + .param("idlist", + StringUtils.join(jobIds, ","))); } /** @@ -538,7 +575,7 @@ public class RundeckClient implements Serializable { RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(format, "format is mandatory to export a job !"); AssertUtil.notBlank(jobId, "jobId is mandatory to export a job !"); - return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId).param("format", format)); + return new ApiCall(this).get(new OldApiPathBuilder("/job/", jobId).param("format", format)); } /** @@ -712,9 +749,9 @@ public class RundeckClient implements Serializable { RundeckApiTokenException, 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), + return new ApiCall(this).post(new OldApiPathBuilder("/jobs/import").param("format", fileType) + .param("dupeOption", importBehavior) + .attach("xmlBatch", stream), new JobsImportResultParser("result")); } @@ -755,7 +792,7 @@ public class RundeckClient implements Serializable { public RundeckJob getJob(String jobId) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(jobId, "jobId is mandatory to get the details of a job !"); - return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId), new JobParser("joblist/job")); + return new ApiCall(this).get(new OldApiPathBuilder("/job/", jobId), new JobParser("joblist/job")); } /** @@ -771,7 +808,8 @@ 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 OldApiPathBuilder("/job/", jobId), + new StringParser("result/success/message")); } /** @@ -829,11 +867,7 @@ public class RundeckClient implements Serializable { */ public RundeckExecution triggerJob(String jobId, Properties options, Properties nodeFilters) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - AssertUtil.notBlank(jobId, "jobId is mandatory to trigger a job !"); - return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId, "/run").param("argString", - ParametersUtil.generateArgString(options)) - .nodeFilters(nodeFilters), - new ExecutionParser("result/executions/execution")); + return newJobTriggerRequest(jobId).addOptions(options).filterNodes(nodeFilters).execute(); } /** @@ -941,24 +975,10 @@ public class RundeckClient implements Serializable { public RundeckExecution runJob(String jobId, Properties options, Properties nodeFilters, long poolingInterval, TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - if (poolingInterval <= 0) { - poolingInterval = DEFAULT_POOLING_INTERVAL; - poolingUnit = DEFAULT_POOLING_UNIT; - } - if (poolingUnit == null) { - poolingUnit = DEFAULT_POOLING_UNIT; - } - - RundeckExecution execution = triggerJob(jobId, options, nodeFilters); - while (ExecutionStatus.RUNNING.equals(execution.getStatus())) { - try { - Thread.sleep(poolingUnit.toMillis(poolingInterval)); - } catch (InterruptedException e) { - break; - } - execution = getExecution(execution.getId()); - } - return execution; + return newJobRunRequest(jobId).addOptions(options) + .filterNodes(nodeFilters) + .poolingInterval(poolingInterval, poolingUnit) + .execute(); } /* @@ -1026,13 +1046,14 @@ public class RundeckClient implements Serializable { RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(project, "project is mandatory to trigger an ad-hoc command !"); AssertUtil.notBlank(command, "command is mandatory to trigger an ad-hoc command !"); - RundeckExecution execution = new ApiCall(this).get(new ApiPathBuilder("/run/command").param("project", project) - .param("exec", command) - .param("nodeThreadcount", - nodeThreadcount) - .param("nodeKeepgoing", - nodeKeepgoing) - .nodeFilters(nodeFilters), + RundeckExecution execution = new ApiCall(this).get(new OldApiPathBuilder("/run/command").param("project", + project) + .param("exec", command) + .param("nodeThreadcount", + nodeThreadcount) + .param("nodeKeepgoing", + nodeKeepgoing) + .nodeFilters(nodeFilters), new ExecutionParser("result/execution")); // the first call just returns the ID of the execution, so we need another call to get a "real" execution return getExecution(execution.getId()); @@ -1385,16 +1406,17 @@ public class RundeckClient implements Serializable { RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(project, "project is mandatory to trigger an ad-hoc script !"); AssertUtil.notNull(script, "script is mandatory to trigger an ad-hoc script !"); - RundeckExecution execution = new ApiCall(this).post(new ApiPathBuilder("/run/script").param("project", project) - .attach("scriptFile", - script) - .param("argString", - ParametersUtil.generateArgString(options)) - .param("nodeThreadcount", - nodeThreadcount) - .param("nodeKeepgoing", - nodeKeepgoing) - .nodeFilters(nodeFilters), + RundeckExecution execution = new ApiCall(this).post(new OldApiPathBuilder("/run/script").param("project", + project) + .attach("scriptFile", + script) + .param("argString", + ParametersUtil.generateArgString(options)) + .param("nodeThreadcount", + nodeThreadcount) + .param("nodeKeepgoing", + nodeKeepgoing) + .nodeFilters(nodeFilters), new ExecutionParser("result/execution")); // the first call just returns the ID of the execution, so we need another call to get a "real" execution return getExecution(execution.getId()); @@ -1890,7 +1912,7 @@ public class RundeckClient implements Serializable { public List getRunningExecutions(String project) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(project, "project is mandatory get all running executions !"); - return new ApiCall(this).get(new ApiPathBuilder("/executions/running").param("project", project), + return new ApiCall(this).get(new OldApiPathBuilder("/executions/running").param("project", project), new ListParser(new ExecutionParser(), "result/executions/execution")); } @@ -1986,9 +2008,9 @@ public class RundeckClient implements Serializable { public List getJobExecutions(String jobId, ExecutionStatus status, Long max, Long offset) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, 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) - .param("max", max) - .param("offset", offset), + return new ApiCall(this).get(new OldApiPathBuilder("/job/", jobId, "/executions").param("status", status) + .param("max", max) + .param("offset", offset), new ListParser(new ExecutionParser(), "result/executions/execution")); } @@ -2006,7 +2028,7 @@ public class RundeckClient implements Serializable { public RundeckExecution getExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException, 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()), + return new ApiCall(this).get(new OldApiPathBuilder("/execution/", executionId.toString()), new ExecutionParser("result/executions/execution")); } @@ -2023,7 +2045,7 @@ public class RundeckClient implements Serializable { public RundeckAbort abortExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to abort an execution !"); - return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString(), "/abort"), + return new ApiCall(this).get(new OldApiPathBuilder("/execution/", executionId.toString(), "/abort"), new AbortParser("result/abort")); } @@ -2207,15 +2229,15 @@ public class RundeckClient implements Serializable { Date begin, Date end, Long max, Long offset) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(project, "project is mandatory to get the history !"); - return new ApiCall(this).get(new ApiPathBuilder("/history").param("project", project) - .param("jobIdFilter", jobId) - .param("reportIdFilter", reportId) - .param("userFilter", user) - .param("recentFilter", recent) - .param("begin", begin) - .param("end", end) - .param("max", max) - .param("offset", offset), + return new ApiCall(this).get(new OldApiPathBuilder("/history").param("project", project) + .param("jobIdFilter", jobId) + .param("reportIdFilter", reportId) + .param("userFilter", user) + .param("recentFilter", recent) + .param("begin", begin) + .param("end", end) + .param("max", max) + .param("offset", offset), new HistoryParser("result/events")); } @@ -2269,8 +2291,8 @@ public class RundeckClient implements Serializable { public List getNodes(String project, Properties nodeFilters) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(project, "project is mandatory to get all nodes !"); - return new ApiCall(this).get(new ApiPathBuilder("/resources").param("project", project) - .nodeFilters(nodeFilters), + return new ApiCall(this).get(new OldApiPathBuilder("/resources").param("project", project) + .nodeFilters(nodeFilters), new ListParser(new NodeParser(), "project/node")); } @@ -2289,7 +2311,7 @@ public class RundeckClient implements Serializable { RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(name, "the name of the node is mandatory to get a node !"); AssertUtil.notBlank(project, "project is mandatory to get a node !"); - return new ApiCall(this).get(new ApiPathBuilder("/resource/", name).param("project", project), + return new ApiCall(this).get(new OldApiPathBuilder("/resource/", name).param("project", project), new NodeParser("project/node")); } @@ -2307,7 +2329,7 @@ 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 OldApiPathBuilder("/system/info"), new SystemInfoParser("result/system")); } /** diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/request/ApiPathBuilder.java similarity index 99% rename from src/main/java/org/rundeck/api/ApiPathBuilder.java rename to src/main/java/org/rundeck/api/request/ApiPathBuilder.java index 39afd74..ab74f45 100644 --- a/src/main/java/org/rundeck/api/ApiPathBuilder.java +++ b/src/main/java/org/rundeck/api/request/ApiPathBuilder.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.rundeck.api; +package org.rundeck.api.request; import java.io.InputStream; import java.util.Date; diff --git a/src/main/java/org/rundeck/api/request/ApiRequest.java b/src/main/java/org/rundeck/api/request/ApiRequest.java new file mode 100644 index 0000000..b171210 --- /dev/null +++ b/src/main/java/org/rundeck/api/request/ApiRequest.java @@ -0,0 +1,444 @@ +/* + * 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.request; + +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; +import java.security.UnrecoverableKeyException; +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.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.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.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.params.HttpProtocolParams; +import org.apache.http.protocol.HTTP; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.dom4j.Document; +import org.rundeck.api.RundeckApiException; +import org.rundeck.api.RundeckClient; +import org.rundeck.api.RundeckApiException.RundeckApiLoginException; +import org.rundeck.api.RundeckApiException.RundeckApiTokenException; +import org.rundeck.api.parser.ParserHelper; +import org.rundeck.api.parser.XmlNodeParser; +import org.rundeck.api.util.AssertUtil; + +/** + * TODO + * + * @author Vincent Behar + */ +abstract class ApiRequest { + + /** RunDeck HTTP header for the auth-token (in case of token-based authentication) */ + private static final transient String AUTH_TOKEN_HEADER = "X-RunDeck-Auth-Token"; + + /** {@link RundeckClient} instance holding the RunDeck url and the credentials */ + protected final RundeckClient client; + + /** + * TODO + * + * @param client + */ + protected ApiRequest(RundeckClient client) { + super(); + this.client = client; + AssertUtil.notNull(client, "The RunDeck Client must not be null !"); + } + + /** + * TODO + * + * @return + */ + public abstract T execute(); + + /** + * Try to "ping" the RunDeck instance to see if it is alive + * + * @throws RundeckApiException if the ping fails + */ + protected void ping() throws RundeckApiException { + HttpClient httpClient = instantiateHttpClient(); + try { + HttpResponse response = httpClient.execute(new HttpGet(client.getUrl())); + if (response.getStatusLine().getStatusCode() / 100 != 2) { + throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' when pinging " + + client.getUrl()); + } + } catch (IOException e) { + throw new RundeckApiException("Failed to ping RunDeck instance at " + client.getUrl(), e); + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * Test the authentication on the RunDeck instance. Will delegate to either {@link #testLoginAuth()} (in case of + * login-based auth) or {@link #testTokenAuth()} (in case of token-based auth). + * + * @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) + * @see #testLoginAuth() + * @see #testTokenAuth() + */ + protected void testAuth() throws RundeckApiLoginException, RundeckApiTokenException { + if (client.getToken() != null) { + testTokenAuth(); + } else { + testLoginAuth(); + } + } + + /** + * Test the login-based authentication on the RunDeck instance + * + * @throws RundeckApiLoginException if the login fails + * @see #testAuth() + */ + protected void testLoginAuth() throws RundeckApiLoginException { + HttpClient httpClient = instantiateHttpClient(); + try { + login(httpClient); + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * Test the token-based authentication on the RunDeck instance + * + * @throws RundeckApiTokenException if the token is invalid + * @see #testAuth() + */ + protected void testTokenAuth() throws RundeckApiTokenException { + try { + execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + "/system/info")); + } catch (RundeckApiTokenException e) { + throw e; + } catch (RundeckApiException e) { + throw new RundeckApiTokenException("Failed to verify token", e); + } + } + + /** + * 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) + */ + protected T get(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, + RundeckApiLoginException, RundeckApiTokenException { + return execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), parser); + } + + /** + * Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the + * API call. + * + * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder} + * @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) + */ + protected InputStream get(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException, + RundeckApiTokenException { + ByteArrayInputStream response = execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath)); + + // try to load the document, to throw an exception in case of error + ParserHelper.loadDocument(response); + response.reset(); + + 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 (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + */ + protected T post(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, + RundeckApiLoginException, RundeckApiTokenException { + 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. + * + * @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) + */ + protected T delete(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, + RundeckApiLoginException, RundeckApiTokenException { + return execute(new HttpDelete(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), parser); + } + + /** + * Execute an HTTP request to the RunDeck instance. 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 request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on... + * @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) + */ + 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); + } + + /** + * 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 ByteArrayInputStream execute(HttpRequestBase request) throws RundeckApiException, RundeckApiLoginException, + RundeckApiTokenException { + HttpClient httpClient = instantiateHttpClient(); + try { + // we only need to manually login in case of login-based authentication + // note that in case of token-based auth, the auth (via an HTTP header) is managed by an interceptor. + if (client.getToken() == null) { + login(httpClient); + } + + // execute the HTTP request + HttpResponse response = null; + try { + response = httpClient.execute(request); + } catch (IOException e) { + throw new RundeckApiException("Failed to execute an HTTP " + request.getMethod() + " on url : " + + request.getURI(), e); + } + + // 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) { + String newLocation = response.getFirstHeader("Location").getValue(); + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiException("Failed to consume entity (release connection)", e); + } + request = new HttpGet(newLocation); + try { + response = httpClient.execute(request); + } 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 && client.getToken() != null) { + throw new RundeckApiTokenException("Invalid Token ! Got HTTP response '" + response.getStatusLine() + + "' for " + request.getURI()); + } else { + throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' for " + + request.getURI()); + } + } + 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); + } + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * Do the actual work of login, using the given {@link HttpClient} instance. You'll need to re-use this instance + * when making API calls (such as running a job). Only use this in case of login-based authentication. + * + * @param httpClient pre-instantiated + * @throws RundeckApiLoginException if the login failed + */ + private void login(HttpClient httpClient) throws RundeckApiLoginException { + String location = client.getUrl() + "/j_security_check"; + + while (true) { + HttpPost postLogin = new HttpPost(location); + List params = new ArrayList(); + params.add(new BasicNameValuePair("j_username", client.getLogin())); + params.add(new BasicNameValuePair("j_password", client.getPassword())); + params.add(new BasicNameValuePair("action", "login")); + + HttpResponse response = null; + try { + postLogin.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8)); + response = httpClient.execute(postLogin); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to post login form on " + location, e); + } + + if (response.getStatusLine().getStatusCode() / 100 == 3) { + // HTTP client refuses to handle redirects (code 3xx) for POST, so we have to do it manually... + location = response.getFirstHeader("Location").getValue(); + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); + } + continue; + } + if (response.getStatusLine().getStatusCode() / 100 != 2) { + throw new RundeckApiLoginException("Invalid HTTP response '" + response.getStatusLine() + "' for " + + location); + } + try { + String content = EntityUtils.toString(response.getEntity(), HTTP.UTF_8); + if (StringUtils.contains(content, "j_security_check")) { + throw new RundeckApiLoginException("Login failed for user " + client.getLogin()); + } + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); + } + } catch (IOException io) { + throw new RundeckApiLoginException("Failed to read RunDeck result", io); + } catch (ParseException p) { + throw new RundeckApiLoginException("Failed to parse RunDeck response", p); + } + break; + } + } + + /** + * Instantiate a new {@link HttpClient} instance, configured to accept all SSL certificates + * + * @return an {@link HttpClient} instance - won't be null + */ + private HttpClient instantiateHttpClient() { + DefaultHttpClient httpClient = new DefaultHttpClient(); + + // configure user-agent + HttpProtocolParams.setUserAgent(httpClient.getParams(), "RunDeck API Java Client " + RundeckClient.API_VERSION); + + // configure SSL + SSLSocketFactory socketFactory = null; + try { + socketFactory = new SSLSocketFactory(new TrustStrategy() { + + @Override + public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { + return true; + } + }, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + } catch (KeyManagementException e) { + throw new RuntimeException(e); + } catch (UnrecoverableKeyException e) { + throw new RuntimeException(e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + httpClient.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())); + + // in case of token-based authentication, add the correct HTTP header to all requests via an interceptor + httpClient.addRequestInterceptor(new HttpRequestInterceptor() { + + @Override + public void process(HttpRequest request, HttpContext context) throws HttpException, IOException { + if (client.getToken() != null) { + request.addHeader(AUTH_TOKEN_HEADER, client.getToken()); + } + } + }); + + return httpClient; + } + +} diff --git a/src/main/java/org/rundeck/api/request/AuthRequest.java b/src/main/java/org/rundeck/api/request/AuthRequest.java new file mode 100644 index 0000000..77ef5cd --- /dev/null +++ b/src/main/java/org/rundeck/api/request/AuthRequest.java @@ -0,0 +1,36 @@ +/* + * 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.request; + +import org.rundeck.api.RundeckClient; + +/** + * TODO + * + * @author Vincent Behar + */ +public class AuthRequest extends ApiRequest { + + public AuthRequest(RundeckClient client) { + super(client); + } + + @Override + public Void execute() { + testAuth(); + return null; + } +} diff --git a/src/main/java/org/rundeck/api/request/HistoryRequest.java b/src/main/java/org/rundeck/api/request/HistoryRequest.java new file mode 100644 index 0000000..d133547 --- /dev/null +++ b/src/main/java/org/rundeck/api/request/HistoryRequest.java @@ -0,0 +1,83 @@ +/* + * 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.request; + +import java.util.Date; +import org.rundeck.api.RundeckClient; +import org.rundeck.api.domain.RundeckHistory; +import org.rundeck.api.parser.HistoryParser; +import org.rundeck.api.util.AssertUtil; + +/** + * TODO + * + * @author Vincent Behar + */ +public class HistoryRequest extends ApiRequest { + + private final String project; + + private String jobId; + + private String reportId; + + private String user; + + private String recent; + + private Date beginAt; + + private Date end; + + private Long max; + + private Long offset; + + public HistoryRequest(RundeckClient client, String project) { + super(client); + this.project = project; + AssertUtil.notBlank(project, "project is mandatory to get the history !"); + } + + public HistoryRequest beginAt(Date beginAt) { + this.beginAt = beginAt; + return this; + } + + public HistoryRequest max(Long max) { + this.max = max; + return this; + } + + public HistoryRequest offset(Long offset) { + this.offset = offset; + return this; + } + + @Override + public RundeckHistory execute() { + return get(new ApiPathBuilder("/history").param("project", project) + .param("jobIdFilter", jobId) + .param("reportIdFilter", reportId) + .param("userFilter", user) + .param("recentFilter", recent) + .param("begin", beginAt) + .param("end", end) + .param("max", max) + .param("offset", offset), new HistoryParser("result/events")); + } + +} diff --git a/src/main/java/org/rundeck/api/request/JobRunRequest.java b/src/main/java/org/rundeck/api/request/JobRunRequest.java new file mode 100644 index 0000000..fe5ad19 --- /dev/null +++ b/src/main/java/org/rundeck/api/request/JobRunRequest.java @@ -0,0 +1,72 @@ +/* + * 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.request; + +import java.util.concurrent.TimeUnit; +import org.rundeck.api.RundeckClient; +import org.rundeck.api.domain.RundeckExecution; +import org.rundeck.api.domain.RundeckExecution.ExecutionStatus; + +/** + * TODO + * + * @author Vincent Behar + */ +public class JobRunRequest extends JobTriggerRequest { + + /** Default value for the "pooling interval" used when running jobs/commands/scripts */ + public static final transient long DEFAULT_POOLING_INTERVAL = 5; + + /** Default unit of the "pooling interval" used when running jobs/commands/scripts */ + public static final transient TimeUnit DEFAULT_POOLING_UNIT = TimeUnit.SECONDS; + + private Long poolingInterval; + + private TimeUnit poolingUnit; + + public JobRunRequest(RundeckClient client, String project) { + super(client, project); + } + + public JobRunRequest poolingInterval(Long poolingInterval, TimeUnit poolingUnit) { + this.poolingInterval = poolingInterval; + this.poolingUnit = poolingUnit; + return this; + } + + @Override + public RundeckExecution execute() { + if (poolingInterval <= 0) { + poolingInterval = DEFAULT_POOLING_INTERVAL; + poolingUnit = DEFAULT_POOLING_UNIT; + } + if (poolingUnit == null) { + poolingUnit = DEFAULT_POOLING_UNIT; + } + + RundeckExecution execution = super.execute(); + while (ExecutionStatus.RUNNING.equals(execution.getStatus())) { + try { + Thread.sleep(poolingUnit.toMillis(poolingInterval)); + } catch (InterruptedException e) { + break; + } + execution = client.getExecution(execution.getId()); + } + return execution; + } + +} diff --git a/src/main/java/org/rundeck/api/request/JobTriggerRequest.java b/src/main/java/org/rundeck/api/request/JobTriggerRequest.java new file mode 100644 index 0000000..52bf29d --- /dev/null +++ b/src/main/java/org/rundeck/api/request/JobTriggerRequest.java @@ -0,0 +1,64 @@ +/* + * 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.request; + +import java.util.Properties; +import org.rundeck.api.RundeckClient; +import org.rundeck.api.domain.RundeckExecution; +import org.rundeck.api.parser.ExecutionParser; +import org.rundeck.api.util.AssertUtil; +import org.rundeck.api.util.ParametersUtil; + +/** + * TODO + * + * @author Vincent Behar + */ +public class JobTriggerRequest> extends ApiRequest { + + private final String jobId; + + private Properties options; + + private Properties nodeFilters; + + public JobTriggerRequest(RundeckClient client, String jobId) { + super(client); + this.jobId = jobId; + AssertUtil.notBlank(jobId, "jobId is mandatory !"); + } + + @SuppressWarnings("unchecked") + public T addOptions(Properties options) { + this.options = options; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T filterNodes(Properties nodeFilters) { + this.nodeFilters = nodeFilters; + return (T) this; + } + + @Override + public RundeckExecution execute() { + return get(new ApiPathBuilder("/job/", jobId, "/run").param("argString", + ParametersUtil.generateArgString(options)) + .nodeFilters(nodeFilters), + new ExecutionParser("result/executions/execution")); + } + +} diff --git a/src/main/java/org/rundeck/api/request/JobsListingRequest.java b/src/main/java/org/rundeck/api/request/JobsListingRequest.java new file mode 100644 index 0000000..d39ac2a --- /dev/null +++ b/src/main/java/org/rundeck/api/request/JobsListingRequest.java @@ -0,0 +1,53 @@ +/* + * 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.request; + +import java.util.List; +import org.rundeck.api.RundeckClient; +import org.rundeck.api.domain.RundeckJob; +import org.rundeck.api.parser.JobParser; +import org.rundeck.api.parser.ListParser; +import org.rundeck.api.util.AssertUtil; + +/** + * TODO + * + * @author Vincent Behar + */ +public class JobsListingRequest extends ApiRequest> { + + private final String project; + + private String jobFilter; + + public JobsListingRequest(RundeckClient client, String project) { + super(client); + this.project = project; + AssertUtil.notBlank(project, "project is mandatory to get all jobs !"); + } + + public JobsListingRequest jobFilter(String jobFilter) { + this.jobFilter = jobFilter; + return this; + } + + @Override + public List execute() { + return get(new ApiPathBuilder("/jobs").param("project", project).param("jobFilter", jobFilter), + new ListParser(new JobParser(), "result/jobs/job")); + } + +} diff --git a/src/main/java/org/rundeck/api/request/PingRequest.java b/src/main/java/org/rundeck/api/request/PingRequest.java new file mode 100644 index 0000000..5329159 --- /dev/null +++ b/src/main/java/org/rundeck/api/request/PingRequest.java @@ -0,0 +1,37 @@ +/* + * 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.request; + +import org.rundeck.api.RundeckClient; + +/** + * TODO + * + * @author Vincent Behar + */ +public class PingRequest extends ApiRequest { + + public PingRequest(RundeckClient client) { + super(client); + } + + @Override + public Void execute() { + ping(); + return null; + } + +} diff --git a/src/main/java/org/rundeck/api/request/ProjectDetailsRequest.java b/src/main/java/org/rundeck/api/request/ProjectDetailsRequest.java new file mode 100644 index 0000000..173fb4b --- /dev/null +++ b/src/main/java/org/rundeck/api/request/ProjectDetailsRequest.java @@ -0,0 +1,43 @@ +/* + * 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.request; + +import org.rundeck.api.RundeckClient; +import org.rundeck.api.domain.RundeckProject; +import org.rundeck.api.parser.ProjectParser; +import org.rundeck.api.util.AssertUtil; + +/** + * TODO + * + * @author Vincent Behar + */ +public class ProjectDetailsRequest extends ApiRequest { + + private final String projectName; + + public ProjectDetailsRequest(RundeckClient client, String projectName) { + super(client); + this.projectName = projectName; + AssertUtil.notBlank(projectName, "projectName is mandatory to get the details of a project !"); + } + + @Override + public RundeckProject execute() { + return get(new ApiPathBuilder("/project/", projectName), new ProjectParser("result/projects/project")); + } + +} diff --git a/src/main/java/org/rundeck/api/request/ProjectsListingRequest.java b/src/main/java/org/rundeck/api/request/ProjectsListingRequest.java new file mode 100644 index 0000000..76bc965 --- /dev/null +++ b/src/main/java/org/rundeck/api/request/ProjectsListingRequest.java @@ -0,0 +1,41 @@ +/* + * 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.request; + +import java.util.List; +import org.rundeck.api.RundeckClient; +import org.rundeck.api.domain.RundeckProject; +import org.rundeck.api.parser.ListParser; +import org.rundeck.api.parser.ProjectParser; + +/** + * TODO + * + * @author Vincent Behar + */ +public class ProjectsListingRequest extends ApiRequest> { + + public ProjectsListingRequest(RundeckClient client) { + super(client); + } + + @Override + public List execute() { + return get(new ApiPathBuilder("/projects"), new ListParser(new ProjectParser(), + "result/projects/project")); + } + +}