View Javadoc

1   /*
2    * Copyright 2011 Vincent Behar
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.rundeck.api;
17  
18  import java.io.Serializable;
19  import java.util.ArrayList;
20  import java.util.List;
21  import java.util.Properties;
22  import java.util.concurrent.TimeUnit;
23  import org.apache.commons.lang.StringUtils;
24  import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
25  import org.rundeck.api.domain.RundeckAbort;
26  import org.rundeck.api.domain.RundeckExecution;
27  import org.rundeck.api.domain.RundeckJob;
28  import org.rundeck.api.domain.RundeckNode;
29  import org.rundeck.api.domain.RundeckProject;
30  import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
31  import org.rundeck.api.parser.AbortParser;
32  import org.rundeck.api.parser.ExecutionParser;
33  import org.rundeck.api.parser.ExecutionsParser;
34  import org.rundeck.api.parser.JobParser;
35  import org.rundeck.api.parser.JobsParser;
36  import org.rundeck.api.parser.NodeParser;
37  import org.rundeck.api.parser.NodesParser;
38  import org.rundeck.api.parser.ProjectParser;
39  import org.rundeck.api.parser.ProjectsParser;
40  import org.rundeck.api.util.AssertUtil;
41  import org.rundeck.api.util.ParametersUtil;
42  
43  /**
44   * Main entry point to talk to a RunDeck instance.<br>
45   * Usage : <br>
46   * <code>
47   * <pre>
48   * RundeckClient rundeck = new RundeckClient("http://localhost:4440", "admin", "admin");
49   * List&lt;RundeckJob&gt; jobs = rundeck.getJobs();
50   * 
51   * RundeckJob job = rundeck.findJob("my-project", "main-group/sub-group", "job-name");
52   * RundeckExecution execution = rundeck.triggerJob(job.getId(),
53   *                                                 new OptionsBuilder().addOption("version", "1.2.0").toProperties());
54   * 
55   * List&lt;RundeckExecution&gt; runningExecutions = rundeck.getRunningExecutions("my-project");
56   * </pre>
57   * </code>
58   * 
59   * @author Vincent Behar
60   */
61  public class RundeckClient implements Serializable {
62  
63      private static final long serialVersionUID = 1L;
64  
65      public static final transient int API_VERSION = 1;
66  
67      public static final transient String API_ENDPOINT = "/api/" + API_VERSION;
68  
69      private final String url;
70  
71      private final String login;
72  
73      private final String password;
74  
75      /**
76       * Instantiate a new {@link RundeckClient} for the RunDeck instance at the given url
77       * 
78       * @param url of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc)
79       * @param login
80       * @param password
81       * @throws IllegalArgumentException if the url, login or password is blank (null, empty or whitespace)
82       */
83      public RundeckClient(String url, String login, String password) throws IllegalArgumentException {
84          super();
85          this.url = url;
86          this.login = login;
87          this.password = password;
88          AssertUtil.notBlank(url, "The RunDeck URL is mandatory !");
89          AssertUtil.notBlank(login, "The RunDeck login is mandatory !");
90          AssertUtil.notBlank(password, "The RunDeck password is mandatory !");
91      }
92  
93      /**
94       * Try to "ping" the RunDeck instance to see if it is alive
95       * 
96       * @throws RundeckApiException if the ping fails
97       */
98      public void ping() throws RundeckApiException {
99          new ApiCall(this).ping();
100     }
101 
102     /**
103      * Test your credentials (login/password) on the RunDeck instance
104      * 
105      * @throws RundeckApiLoginException if the login fails
106      */
107     public void testCredentials() throws RundeckApiLoginException {
108         new ApiCall(this).testCredentials();
109     }
110 
111     /*
112      * Projects
113      */
114 
115     /**
116      * List all projects
117      * 
118      * @return a {@link List} of {@link RundeckProject} : might be empty, but won't be null
119      * @throws RundeckApiException in case of error when calling the API
120      * @throws RundeckApiLoginException if the login failed
121      */
122     public List<RundeckProject> getProjects() throws RundeckApiException, RundeckApiLoginException {
123         return new ApiCall(this).get(new ApiPathBuilder("/projects"), new ProjectsParser("result/projects/project"));
124     }
125 
126     /**
127      * Get the definition of a single project, identified by the given name
128      * 
129      * @param projectName name of the project - mandatory
130      * @return a {@link RundeckProject} instance - won't be null
131      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
132      * @throws RundeckApiLoginException if the login failed
133      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
134      */
135     public RundeckProject getProject(String projectName) throws RundeckApiException, RundeckApiLoginException,
136             IllegalArgumentException {
137         AssertUtil.notBlank(projectName, "projectName is mandatory to get the details of a project !");
138         return new ApiCall(this).get(new ApiPathBuilder("/project/", projectName),
139                                      new ProjectParser("result/projects/project"));
140     }
141 
142     /*
143      * Jobs
144      */
145 
146     /**
147      * List all jobs (for all projects)
148      * 
149      * @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null
150      * @throws RundeckApiException in case of error when calling the API
151      * @throws RundeckApiLoginException if the login failed
152      */
153     public List<RundeckJob> getJobs() throws RundeckApiException, RundeckApiLoginException {
154         List<RundeckJob> jobs = new ArrayList<RundeckJob>();
155         for (RundeckProject project : getProjects()) {
156             jobs.addAll(getJobs(project.getName()));
157         }
158         return jobs;
159     }
160 
161     /**
162      * List all jobs that belongs to the given project
163      * 
164      * @param project name of the project - mandatory
165      * @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null
166      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
167      * @throws RundeckApiLoginException if the login failed
168      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
169      * @see #getJobs(String, String, String, String...)
170      */
171     public List<RundeckJob> getJobs(String project) throws RundeckApiException, RundeckApiLoginException,
172             IllegalArgumentException {
173         return getJobs(project, null, null, new String[0]);
174     }
175 
176     /**
177      * List the jobs that belongs to the given project, and matches the given criteria (jobFilter, groupPath and jobIds)
178      * 
179      * @param project name of the project - mandatory
180      * @param jobFilter a filter for the job Name - optional
181      * @param groupPath a group or partial group path to include all jobs within that group path - optional
182      * @param jobIds a list of Job IDs to include - optional
183      * @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null
184      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
185      * @throws RundeckApiLoginException if the login failed
186      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
187      * @see #getJobs(String)
188      */
189     public List<RundeckJob> getJobs(String project, String jobFilter, String groupPath, String... jobIds)
190             throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
191         AssertUtil.notBlank(project, "project is mandatory to get all jobs !");
192         return new ApiCall(this).get(new ApiPathBuilder("/jobs").param("project", project)
193                                                                 .param("jobFilter", jobFilter)
194                                                                 .param("groupPath", groupPath)
195                                                                 .param("idlist", StringUtils.join(jobIds, ",")),
196                                      new JobsParser("result/jobs/job"));
197     }
198 
199     /**
200      * Find a job, identified by its project, group and name. Note that the groupPath is optional, as a job does not
201      * need to belong to a group (either pass null, or an empty string).
202      * 
203      * @param project name of the project - mandatory
204      * @param groupPath group to which the job belongs (if it belongs to a group) - optional
205      * @param name of the job to find - mandatory
206      * @return a {@link RundeckJob} instance - null if not found
207      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
208      * @throws RundeckApiLoginException if the login failed
209      * @throws IllegalArgumentException if the project or the name is blank (null, empty or whitespace)
210      */
211     public RundeckJob findJob(String project, String groupPath, String name) throws RundeckApiException,
212             RundeckApiLoginException, IllegalArgumentException {
213         AssertUtil.notBlank(project, "project is mandatory to find a job !");
214         AssertUtil.notBlank(name, "job name is mandatory to find a job !");
215         List<RundeckJob> jobs = getJobs(project, name, groupPath, new String[0]);
216         return jobs.isEmpty() ? null : jobs.get(0);
217     }
218 
219     /**
220      * Get the definition of a single job, identified by the given ID
221      * 
222      * @param jobId identifier of the job - mandatory
223      * @return a {@link RundeckJob} instance - won't be null
224      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
225      * @throws RundeckApiLoginException if the login failed
226      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
227      */
228     public RundeckJob getJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
229             IllegalArgumentException {
230         AssertUtil.notBlank(jobId, "jobId is mandatory to get the details of a job !");
231         return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId), new JobParser("joblist/job"));
232     }
233 
234     /**
235      * Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the
236      * end of the job execution)
237      * 
238      * @param jobId identifier of the job - mandatory
239      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
240      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
241      * @throws RundeckApiLoginException if the login failed
242      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
243      * @see #triggerJob(String, Properties, Properties)
244      * @see #runJob(String)
245      */
246     public RundeckExecution triggerJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
247             IllegalArgumentException {
248         return triggerJob(jobId, null);
249     }
250 
251     /**
252      * Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the
253      * end of the job execution)
254      * 
255      * @param jobId identifier of the job - mandatory
256      * @param options of the job - optional. See {@link OptionsBuilder}.
257      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
258      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
259      * @throws RundeckApiLoginException if the login failed
260      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
261      * @see #triggerJob(String, Properties, Properties)
262      * @see #runJob(String, Properties)
263      */
264     public RundeckExecution triggerJob(String jobId, Properties options) throws RundeckApiException,
265             RundeckApiLoginException, IllegalArgumentException {
266         return triggerJob(jobId, options, null);
267     }
268 
269     /**
270      * Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the
271      * end of the job execution)
272      * 
273      * @param jobId identifier of the job - mandatory
274      * @param options of the job - optional. See {@link OptionsBuilder}.
275      * @param nodeFilters for overriding the nodes on which the job will be executed - optional. See
276      *            {@link NodeFiltersBuilder}
277      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
278      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
279      * @throws RundeckApiLoginException if the login failed
280      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
281      * @see #triggerJob(String)
282      * @see #runJob(String, Properties, Properties)
283      */
284     public RundeckExecution triggerJob(String jobId, Properties options, Properties nodeFilters)
285             throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
286         AssertUtil.notBlank(jobId, "jobId is mandatory to trigger a job !");
287         return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId, "/run").param("argString",
288                                                                                       ParametersUtil.generateArgString(options))
289                                                                                .nodeFilters(nodeFilters),
290                                      new ExecutionParser("result/executions/execution"));
291     }
292 
293     /**
294      * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return.
295      * We will poll the RunDeck server at regular interval (every 5 seconds) to know if the execution is finished (or
296      * aborted) or is still running.
297      * 
298      * @param jobId identifier of the job - mandatory
299      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
300      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
301      * @throws RundeckApiLoginException if the login failed
302      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
303      * @see #triggerJob(String)
304      * @see #runJob(String, Properties, Properties, long, TimeUnit)
305      */
306     public RundeckExecution runJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
307             IllegalArgumentException {
308         return runJob(jobId, null);
309     }
310 
311     /**
312      * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return.
313      * We will poll the RunDeck server at regular interval (every 5 seconds) to know if the execution is finished (or
314      * aborted) or is still running.
315      * 
316      * @param jobId identifier of the job - mandatory
317      * @param options of the job - optional. See {@link OptionsBuilder}.
318      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
319      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
320      * @throws RundeckApiLoginException if the login failed
321      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
322      * @see #triggerJob(String, Properties)
323      * @see #runJob(String, Properties, Properties, long, TimeUnit)
324      */
325     public RundeckExecution runJob(String jobId, Properties options) throws RundeckApiException,
326             RundeckApiLoginException, IllegalArgumentException {
327         return runJob(jobId, options, null);
328     }
329 
330     /**
331      * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return.
332      * We will poll the RunDeck server at regular interval (every 5 seconds) to know if the execution is finished (or
333      * aborted) or is still running.
334      * 
335      * @param jobId identifier of the job - mandatory
336      * @param options of the job - optional. See {@link OptionsBuilder}.
337      * @param nodeFilters for overriding the nodes on which the job will be executed - optional. See
338      *            {@link NodeFiltersBuilder}
339      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
340      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
341      * @throws RundeckApiLoginException if the login failed
342      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
343      * @see #triggerJob(String, Properties, Properties)
344      * @see #runJob(String, Properties, Properties, long, TimeUnit)
345      */
346     public RundeckExecution runJob(String jobId, Properties options, Properties nodeFilters)
347             throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
348         return runJob(jobId, options, nodeFilters, 5, TimeUnit.SECONDS);
349     }
350 
351     /**
352      * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return.
353      * We will poll the RunDeck server at regular interval (configured by the poolingInterval/poolingUnit couple) to
354      * know if the execution is finished (or aborted) or is still running.
355      * 
356      * @param jobId identifier of the job - mandatory
357      * @param options of the job - optional. See {@link OptionsBuilder}.
358      * @param poolingInterval for checking the status of the execution. Must be > 0.
359      * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
360      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
361      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
362      * @throws RundeckApiLoginException if the login failed
363      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
364      * @see #triggerJob(String, Properties)
365      * @see #runJob(String, Properties, Properties, long, TimeUnit)
366      */
367     public RundeckExecution runJob(String jobId, Properties options, long poolingInterval, TimeUnit poolingUnit)
368             throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
369         return runJob(jobId, options, null, poolingInterval, poolingUnit);
370     }
371 
372     /**
373      * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return.
374      * We will poll the RunDeck server at regular interval (configured by the poolingInterval/poolingUnit couple) to
375      * know if the execution is finished (or aborted) or is still running.
376      * 
377      * @param jobId identifier of the job - mandatory
378      * @param options of the job - optional. See {@link OptionsBuilder}.
379      * @param nodeFilters for overriding the nodes on which the job will be executed - optional. See
380      *            {@link NodeFiltersBuilder}
381      * @param poolingInterval for checking the status of the execution. Must be > 0.
382      * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
383      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
384      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
385      * @throws RundeckApiLoginException if the login failed
386      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
387      * @see #triggerJob(String, Properties)
388      * @see #runJob(String, Properties, Properties, long, TimeUnit)
389      */
390     public RundeckExecution runJob(String jobId, Properties options, Properties nodeFilters, long poolingInterval,
391             TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
392         if (poolingInterval <= 0) {
393             poolingInterval = 5;
394             poolingUnit = TimeUnit.SECONDS;
395         }
396         if (poolingUnit == null) {
397             poolingUnit = TimeUnit.SECONDS;
398         }
399 
400         RundeckExecution execution = triggerJob(jobId, options, nodeFilters);
401         while (ExecutionStatus.RUNNING.equals(execution.getStatus())) {
402             try {
403                 Thread.sleep(poolingUnit.toMillis(poolingInterval));
404             } catch (InterruptedException e) {
405                 break;
406             }
407             execution = getExecution(execution.getId());
408         }
409         return execution;
410     }
411 
412     /*
413      * Ad-hoc commands
414      */
415 
416     /**
417      * Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution).
418      * The command will not be dispatched to nodes, but be executed on the RunDeck server.
419      * 
420      * @param project name of the project - mandatory
421      * @param command to be executed - mandatory
422      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
423      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
424      * @throws RundeckApiLoginException if the login failed
425      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
426      * @see #triggerAdhocCommand(String, String, Properties)
427      * @see #runAdhocCommand(String, String)
428      */
429     public RundeckExecution triggerAdhocCommand(String project, String command) throws RundeckApiException,
430             RundeckApiLoginException, IllegalArgumentException {
431         return triggerAdhocCommand(project, command, null);
432     }
433 
434     /**
435      * Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution).
436      * The command will be dispatched to nodes, accordingly to the nodeFilters parameter.
437      * 
438      * @param project name of the project - mandatory
439      * @param command to be executed - mandatory
440      * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder}
441      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
442      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
443      * @throws RundeckApiLoginException if the login failed
444      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
445      * @see #triggerAdhocCommand(String, String)
446      * @see #runAdhocCommand(String, String, Properties)
447      */
448     public RundeckExecution triggerAdhocCommand(String project, String command, Properties nodeFilters)
449             throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
450         AssertUtil.notBlank(project, "project is mandatory to trigger an ad-hoc command !");
451         AssertUtil.notBlank(command, "command is mandatory to trigger an ad-hoc command !");
452         RundeckExecution execution = new ApiCall(this).get(new ApiPathBuilder("/run/command").param("project", project)
453                                                                                              .param("exec", command)
454                                                                                              .nodeFilters(nodeFilters),
455                                                            new ExecutionParser("result/execution"));
456         // the first call just returns the ID of the execution, so we need another call to get a "real" execution
457         return getExecution(execution.getId());
458     }
459 
460     /**
461      * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck
462      * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still
463      * running. The command will not be dispatched to nodes, but be executed on the RunDeck server.
464      * 
465      * @param project name of the project - mandatory
466      * @param command to be executed - mandatory
467      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
468      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
469      * @throws RundeckApiLoginException if the login failed
470      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
471      * @see #runAdhocCommand(String, String, Properties, long, TimeUnit)
472      * @see #triggerAdhocCommand(String, String)
473      */
474     public RundeckExecution runAdhocCommand(String project, String command) throws RundeckApiException,
475             RundeckApiLoginException, IllegalArgumentException {
476         return runAdhocCommand(project, command, null);
477     }
478 
479     /**
480      * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck
481      * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is
482      * finished (or aborted) or is still running. The command will not be dispatched to nodes, but be executed on the
483      * RunDeck server.
484      * 
485      * @param project name of the project - mandatory
486      * @param command to be executed - mandatory
487      * @param poolingInterval for checking the status of the execution. Must be > 0.
488      * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
489      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
490      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
491      * @throws RundeckApiLoginException if the login failed
492      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
493      * @see #runAdhocCommand(String, String, Properties, long, TimeUnit)
494      * @see #triggerAdhocCommand(String, String)
495      */
496     public RundeckExecution runAdhocCommand(String project, String command, long poolingInterval, TimeUnit poolingUnit)
497             throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
498         return runAdhocCommand(project, command, null, poolingInterval, poolingUnit);
499     }
500 
501     /**
502      * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck
503      * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still
504      * running. The command will be dispatched to nodes, accordingly to the nodeFilters parameter.
505      * 
506      * @param project name of the project - mandatory
507      * @param command to be executed - mandatory
508      * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder}
509      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
510      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
511      * @throws RundeckApiLoginException if the login failed
512      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
513      * @see #runAdhocCommand(String, String, Properties, long, TimeUnit)
514      * @see #triggerAdhocCommand(String, String, Properties)
515      */
516     public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters)
517             throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
518         return runAdhocCommand(project, command, nodeFilters, 5, TimeUnit.SECONDS);
519     }
520 
521     /**
522      * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck
523      * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is
524      * finished (or aborted) or is still running. The command will be dispatched to nodes, accordingly to the
525      * nodeFilters parameter.
526      * 
527      * @param project name of the project - mandatory
528      * @param command to be executed - mandatory
529      * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder}
530      * @param poolingInterval for checking the status of the execution. Must be > 0.
531      * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
532      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
533      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
534      * @throws RundeckApiLoginException if the login failed
535      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
536      * @see #triggerAdhocCommand(String, String, Properties)
537      */
538     public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters,
539             long poolingInterval, TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException,
540             IllegalArgumentException {
541         if (poolingInterval <= 0) {
542             poolingInterval = 5;
543             poolingUnit = TimeUnit.SECONDS;
544         }
545         if (poolingUnit == null) {
546             poolingUnit = TimeUnit.SECONDS;
547         }
548 
549         RundeckExecution execution = triggerAdhocCommand(project, command, nodeFilters);
550         while (ExecutionStatus.RUNNING.equals(execution.getStatus())) {
551             try {
552                 Thread.sleep(poolingUnit.toMillis(poolingInterval));
553             } catch (InterruptedException e) {
554                 break;
555             }
556             execution = getExecution(execution.getId());
557         }
558         return execution;
559     }
560 
561     /*
562      * Executions
563      */
564 
565     /**
566      * Get all running executions (for all projects)
567      * 
568      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
569      * @throws RundeckApiException in case of error when calling the API
570      * @throws RundeckApiLoginException if the login failed
571      */
572     public List<RundeckExecution> getRunningExecutions() throws RundeckApiException, RundeckApiLoginException {
573         List<RundeckExecution> executions = new ArrayList<RundeckExecution>();
574         for (RundeckProject project : getProjects()) {
575             executions.addAll(getRunningExecutions(project.getName()));
576         }
577         return executions;
578     }
579 
580     /**
581      * Get the running executions for the given project
582      * 
583      * @param project name of the project - mandatory
584      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
585      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
586      * @throws RundeckApiLoginException if the login failed
587      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
588      */
589     public List<RundeckExecution> getRunningExecutions(String project) throws RundeckApiException,
590             RundeckApiLoginException, IllegalArgumentException {
591         AssertUtil.notBlank(project, "project is mandatory get all running executions !");
592         return new ApiCall(this).get(new ApiPathBuilder("/executions/running").param("project", project),
593                                      new ExecutionsParser("result/executions/execution"));
594     }
595 
596     /**
597      * Get the executions of the given job
598      * 
599      * @param jobId identifier of the job - mandatory
600      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
601      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
602      * @throws RundeckApiLoginException if the login failed
603      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
604      */
605     public List<RundeckExecution> getJobExecutions(String jobId) throws RundeckApiException, RundeckApiLoginException,
606             IllegalArgumentException {
607         return getJobExecutions(jobId, null);
608     }
609 
610     /**
611      * Get the executions of the given job
612      * 
613      * @param jobId identifier of the job - mandatory
614      * @param status of the executions - optional (null for all)
615      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
616      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
617      * @throws RundeckApiLoginException if the login failed
618      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
619      */
620     public List<RundeckExecution> getJobExecutions(String jobId, ExecutionStatus status) throws RundeckApiException,
621             RundeckApiLoginException, IllegalArgumentException {
622         return getJobExecutions(jobId, status, null, null);
623     }
624 
625     /**
626      * Get the executions of the given job
627      * 
628      * @param jobId identifier of the job - mandatory
629      * @param status of the executions - optional (null for all)
630      * @param max number of results to return - optional (null for all)
631      * @param offset the 0-indexed offset for the first result to return - optional
632      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
633      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
634      * @throws RundeckApiLoginException if the login failed
635      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
636      */
637     public List<RundeckExecution> getJobExecutions(String jobId, ExecutionStatus status, Long max, Long offset)
638             throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException {
639         AssertUtil.notBlank(jobId, "jobId is mandatory to get the executions of a job !");
640         return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId, "/executions").param("status",
641                                                                                              status != null ? StringUtils.lowerCase(status.toString()) : null)
642                                                                                       .param("max", max)
643                                                                                       .param("offset", offset),
644                                      new ExecutionsParser("result/executions/execution"));
645     }
646 
647     /**
648      * Get a single execution, identified by the given ID
649      * 
650      * @param executionId identifier of the execution - mandatory
651      * @return a {@link RundeckExecution} instance - won't be null
652      * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
653      * @throws RundeckApiLoginException if the login failed
654      * @throws IllegalArgumentException if the executionId is null
655      */
656     public RundeckExecution getExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException,
657             IllegalArgumentException {
658         AssertUtil.notNull(executionId, "executionId is mandatory to get the details of an execution !");
659         return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString()),
660                                      new ExecutionParser("result/executions/execution"));
661     }
662 
663     /**
664      * Abort an execution (identified by the given ID). The execution should be running...
665      * 
666      * @param executionId identifier of the execution - mandatory
667      * @return a {@link RundeckAbort} instance - won't be null
668      * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
669      * @throws RundeckApiLoginException if the login failed
670      * @throws IllegalArgumentException if the executionId is null
671      */
672     public RundeckAbort abortExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException,
673             IllegalArgumentException {
674         AssertUtil.notNull(executionId, "executionId is mandatory to abort an execution !");
675         return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString(), "/abort"),
676                                      new AbortParser("result/abort"));
677     }
678 
679     /*
680      * Nodes
681      */
682 
683     /**
684      * List all nodes (for all projects)
685      * 
686      * @return a {@link List} of {@link RundeckNode} : might be empty, but won't be null
687      * @throws RundeckApiException in case of error when calling the API
688      * @throws RundeckApiLoginException if the login failed
689      */
690     public List<RundeckNode> getNodes() throws RundeckApiException, RundeckApiLoginException {
691         List<RundeckNode> nodes = new ArrayList<RundeckNode>();
692         for (RundeckProject project : getProjects()) {
693             nodes.addAll(getNodes(project.getName()));
694         }
695         return nodes;
696     }
697 
698     /**
699      * List all nodes that belongs to the given project
700      * 
701      * @param project name of the project - mandatory
702      * @return a {@link List} of {@link RundeckNode} : might be empty, but won't be null
703      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
704      * @throws RundeckApiLoginException if the login failed
705      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
706      * @see #getNodes(String, Properties)
707      */
708     public List<RundeckNode> getNodes(String project) throws RundeckApiException, RundeckApiLoginException,
709             IllegalArgumentException {
710         return getNodes(project, null);
711     }
712 
713     /**
714      * List nodes that belongs to the given project
715      * 
716      * @param project name of the project - mandatory
717      * @param nodeFilters for filtering the nodes - optional. See {@link NodeFiltersBuilder}
718      * @return a {@link List} of {@link RundeckNode} : might be empty, but won't be null
719      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
720      * @throws RundeckApiLoginException if the login failed
721      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
722      */
723     public List<RundeckNode> getNodes(String project, Properties nodeFilters) throws RundeckApiException,
724             RundeckApiLoginException, IllegalArgumentException {
725         AssertUtil.notBlank(project, "project is mandatory to get all nodes !");
726         return new ApiCall(this).get(new ApiPathBuilder("/resources").param("project", project)
727                                                                      .nodeFilters(nodeFilters),
728                                      new NodesParser("project/node"));
729     }
730 
731     /**
732      * Get the definition of a single node
733      * 
734      * @param name of the node - mandatory
735      * @param project name of the project - mandatory
736      * @return a {@link RundeckNode} instance - won't be null
737      * @throws RundeckApiException in case of error when calling the API (non-existent name or project with this name)
738      * @throws RundeckApiLoginException if the login failed
739      * @throws IllegalArgumentException if the name or project is blank (null, empty or whitespace)
740      */
741     public RundeckNode getNode(String name, String project) throws RundeckApiException, RundeckApiLoginException,
742             IllegalArgumentException {
743         AssertUtil.notBlank(name, "the name of the node is mandatory to get a node !");
744         AssertUtil.notBlank(project, "project is mandatory to get a node !");
745         return new ApiCall(this).get(new ApiPathBuilder("/resource/", name).param("project", project),
746                                      new NodeParser("project/node"));
747     }
748 
749     public String getUrl() {
750         return url;
751     }
752 
753     public String getLogin() {
754         return login;
755     }
756 
757     public String getPassword() {
758         return password;
759     }
760 
761     @Override
762     public String toString() {
763         return "RundeckClient [url=" + url + ", login=" + login + ", password=" + password + "]";
764     }
765 
766     @Override
767     public int hashCode() {
768         final int prime = 31;
769         int result = 1;
770         result = prime * result + ((login == null) ? 0 : login.hashCode());
771         result = prime * result + ((password == null) ? 0 : password.hashCode());
772         result = prime * result + ((url == null) ? 0 : url.hashCode());
773         return result;
774     }
775 
776     @Override
777     public boolean equals(Object obj) {
778         if (this == obj)
779             return true;
780         if (obj == null)
781             return false;
782         if (getClass() != obj.getClass())
783             return false;
784         RundeckClient other = (RundeckClient) obj;
785         if (login == null) {
786             if (other.login != null)
787                 return false;
788         } else if (!login.equals(other.login))
789             return false;
790         if (password == null) {
791             if (other.password != null)
792                 return false;
793         } else if (!password.equals(other.password))
794             return false;
795         if (url == null) {
796             if (other.url != null)
797                 return false;
798         } else if (!url.equals(other.url))
799             return false;
800         return true;
801     }
802 
803 }