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<RundeckJob> 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<RundeckExecution> 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 }