File Coverage

blib/lib/RundeckAPI.pm
Criterion Covered Total %
statement 36 219 16.4
branch 0 54 0.0
condition 0 10 0.0
subroutine 12 24 50.0
pod 7 7 100.0
total 55 314 17.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2              
3             ###########################################################################
4             # $Id: rundeckAPI.pm, v1.0 r1 04/02/2020 13:58:58 CET XH Exp $
5             #
6             # Copyright 2020 Xavier Humbert
7             #
8             # This program is free software; you can redistribute it and/or
9             # modify it under the terms of the GNU General Public License
10             # as published by the Free Software Foundation; either version 2
11             # of the License, or (at your option) any later version.
12             #
13             # This program is distributed in the hope that it will be useful,
14             # but WITHOUT ANY WARRANTY; without even the implied warranty
15             # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16             # GNU General Public License for more details.
17             #
18             # You should have received a copy of the GNU General Public License
19             # along with this program ; if not, write to the
20             # Free Software Foundation, Inc.,
21             # 59 Temple Place - Suite 330,
22             # Boston, MA 02111-1307, USA
23             #
24             ###########################################################################
25              
26             package RundeckAPI;
27              
28 1     1   68570 use strict;
  1         3  
  1         28  
29 1     1   15 use warnings;
  1         1  
  1         26  
30 1     1   608 use POSIX qw(setlocale strftime);
  1         6592  
  1         7  
31 1     1   1575 use File::Basename;
  1         2  
  1         109  
32 1     1   762 use LWP::UserAgent;
  1         53667  
  1         43  
33 1     1   668 use Data::Dumper;
  1         6965  
  1         65  
34 1     1   529 use HTTP::Cookies;
  1         7136  
  1         40  
35 1     1   524 use REST::Client;
  1         2027  
  1         30  
36 1     1   24 use Scalar::Util qw(reftype);
  1         2  
  1         50  
37 1     1   783 use JSON;
  1         11016  
  1         5  
38 1     1   853 use Storable qw(dclone);
  1         2939  
  1         64  
39 1     1   7 use Exporter qw(import);
  1         2  
  1         2077  
40              
41             our @EXPORT_OK = qw(get post put delete postData putData);
42              
43             #####
44             ## CONSTANTS
45             #####
46             our $TIMEOUT = 10;
47             our $VERSION = "1.3.5.0";
48             #####
49             ## VARIABLES
50             #####
51              
52             #####
53             ## CONSTRUCTOR
54             #####
55              
56             sub new {
57 0     0 1   my($class, %args) = @_;
58 0           my $rc = 403;
59             my $self = {
60             'url' => $args{'url'},
61             'login' => $args{'login'},
62             'token' => $args{'token'},
63             'debug' => $args{'debug'} || 0,
64             'verbose' => $args{'verbose'} || 0,
65             'result' => undef,
66 0   0       'timeout' => $args{'timeout'} || $TIMEOUT,
      0        
      0        
67             };
68             # create and store a cookie jar
69 0           my $cookie_jar = HTTP::Cookies->new(
70             autosave => 1,
71             ignore_discard => 1,
72             );
73 0           $self->{'cookie_jar'} = $cookie_jar;
74              
75             # with this cookie, cretae an User-Agent
76 0           my($prog, $dirs, $suffix) = fileparse($0, (".pl"));
77             my $ua = LWP::UserAgent->new(
78             'agent' => $prog . "-" . $VERSION,
79             'timeout' => $self->{'timeout'},
80 0           'cookie_jar' => $self->{'cookie_jar'},
81             'requests_redirectable' => ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
82             );
83 0           $ua->show_progress ($args{'verbose'});
84 0 0         $ua->proxy( ['http', 'https'], $args{'proxy'}) if (defined $args{'proxy'});
85 0           $self->{'ua'} = $ua;
86              
87             # connect to the client
88             my $client = REST::Client->new(
89             host => $self->{'url'},
90 0           timeout => $self->{'timeout'},
91             useragent => $ua,
92             follow => 1,
93             );
94 0           $client->addHeader ("Content-Type", 'application/x-www-form-urlencoded');
95 0           $client->addHeader ("Accept", "application/json");
96 0           $self->{'client'} = $client;
97              
98 0 0         if (defined $self->{'token'}) {
99 0           $client->addHeader ("X-Rundeck-Auth-Token", $self->{'token'});
100 0           $client->GET("/api/21/tokens/$self->{'login'}");
101             # check if token match id
102 0           my $authJSON = $client->responseContent();
103 0           $rc = $client->responseCode ();
104 0 0 0       if (($rc-$rc%100 == 200) && (index($client->{'_res'}{'_content'}, 'alert alert-danger') == -1)) {
105 0           my $jHash = decode_json ($authJSON);
106 0 0         if (defined $jHash->[0]) {
107 0           my $connOK = 0;
108 0           foreach my $tokenInfo (@{$jHash}) {
  0            
109 0 0         if ($tokenInfo->{'user'} eq $self->{'login'}) {
110 0           $connOK = 1;
111 0           last;
112             }
113             }
114 0 0         if ($connOK) {
115 0           $rc = 200;
116             } else {
117 0           $rc = 403;
118             }
119              
120             } else {
121 0           $rc = 403;
122             }
123             } else {
124 0           $rc = 403;
125             }
126             }
127 0 0         if ($rc-$rc%100 != 200) {
128 0           $self->{'result'}->{'reqstatus' } = 'UNKN';
129             } else {
130 0           $self->{'result'}->{'reqstatus'} = 'OK';
131             }
132 0           $self->{'result'}->{'httpstatus'} = $rc;
133              
134             # done, bless object and return it
135 0           bless ($self, $class);
136 0 0         $self->_logV1 ("Connected to $self->{'url'}") if ($rc-$rc%100 == 200);
137 0           $self->_logD($self);
138 0           return $self;
139             }
140              
141             #####
142             ## METHODS
143             #####
144              
145             sub get (){ # endpoint
146 0     0 1   my $self = shift;
147 0           my $endpoint = shift;
148              
149 0           my $responsehash = ();
150 0           my $rc = 0;
151              
152             # Handle secial case where endpoint is /api/XX/job, returns YAML
153 0 0         if ($endpoint =~ /api\/[0-9]+\/job/) {
154 0           $endpoint .= '?format=yaml';
155              
156             }
157              
158 0           $self->{'client'}->GET($endpoint);
159 0           $rc = $self->{'client'}->responseCode ();
160 0           $responsehash->{'httpstatus'} = $rc;
161              
162 0 0         if ($rc-$rc%100 != 200) {
163 0           $responsehash->{'reqstatus'} = 'CRIT';
164 0           $responsehash->{'httpstatus'} = $rc;
165             } else {
166 0           my $responseType = $self->{'client'}->responseHeader('content-type');
167 0           my $responseContent = $self->{'client'}->responseContent();
168 0           $responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
169              
170             }
171 0           return dclone ($responsehash);
172             }
173              
174             sub post(){ # endpoint, json
175 0     0 1   my $self = shift;
176 0           my $endpoint = shift;
177 0           my $json = shift;
178              
179 0           my $responsehash = ();
180 0           my $rc = 0;
181              
182 0           $self->{'client'}->addHeader ("Content-Type", 'application/json');
183 0           $self->{'client'}->POST($endpoint, $json);
184 0           $rc = $self->{'client'}->responseCode ();
185 0           $self->{'result'}->{'httpstatus'} = $rc;
186              
187 0 0         if ($rc-$rc%100 != 200) {
188 0           $responsehash->{'reqstatus'} = 'CRIT';
189 0           $responsehash->{'httpstatus'} = $rc;
190             } else {
191 0           my $responseType = $self->{'client'}->responseHeader('content-type');
192 0           my $responseContent = $self->{'client'}->responseContent();
193 0           $responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
194             }
195 0           return dclone ($responsehash);
196             }
197              
198             sub put(){ # endpoint, json
199 0     0 1   my $self = shift;
200 0           my $endpoint = shift;
201 0           my $json = shift;
202              
203 0           my $responsehash = ();
204 0           my $rc = 0;
205              
206 0           $self->{'client'}->addHeader ("Content-Type", 'application/json');
207 0           $self->{'client'}->PUT($endpoint, $json);
208 0           $rc = $self->{'client'}->responseCode ();
209 0           $self->{'result'}->{'httpstatus'} = $rc;
210              
211 0 0         if ($rc-$rc%100 != 200) {
212 0           $responsehash->{'reqstatus'} = 'CRIT';
213 0           $responsehash->{'httpstatus'} = $rc;
214             } else {
215 0           my $responseType = $self->{'client'}->responseHeader('content-type');
216 0           my $responseContent = $self->{'client'}->responseContent();
217 0           $responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
218             }
219 0           return dclone ($responsehash);
220             }
221              
222             sub delete () { # endpoint
223 0     0 1   my $self = shift;
224 0           my $endpoint = shift;
225              
226 0           my $responsehash = ();
227 0           my $rc = 0;
228              
229 0           $self->{'client'}->DELETE($endpoint);
230 0           $rc = $self->{'client'}->responseCode ();
231 0           $responsehash->{'httpstatus'} = $rc;
232              
233 0 0         if ($rc-$rc%100 != 200) {
234 0           $responsehash->{'reqstatus'} = 'CRIT';
235 0           $responsehash->{'httpstatus'} = $rc;
236             } else {
237 0           my $responseType = $self->{'client'}->responseHeader('content-type');
238 0           my $responseContent = $self->{'client'}->responseContent();
239 0           $responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
240             }
241 0           return dclone ($responsehash);
242             }
243              
244             sub postData() { # endpoint, mimetype, data
245 0     0 1   my $self = shift;
246 0           my $endpoint = shift;
247 0           my $mimetype = shift;
248 0           my $data = shift;
249              
250 0           my $responsehash = ();
251 0           my $rc = 0;
252              
253 0           $self->{'client'}->addHeader ("Content-Type", $mimetype);
254 0           $self->{'client'}->POST($endpoint, $data);
255 0           $rc = $self->{'client'}->responseCode ();
256 0           $self->{'result'}->{'httpstatus'} = $rc;
257              
258 0 0         if ($rc-$rc%100 != 200) {
259 0           $responsehash->{'reqstatus'} = 'CRIT';
260 0           $responsehash->{'httpstatus'} = $rc;
261             } else {
262 0           my $responseType = $self->{'client'}->responseHeader('content-type');
263 0           my $responseContent = $self->{'client'}->responseContent();
264 0           $responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
265             }
266 0           return dclone ($responsehash);
267             }
268              
269             sub putData() { # endpoint, mimetype, data
270 0     0 1   my $self = shift;
271 0           my $endpoint = shift;
272 0           my $mimetype = shift;
273 0           my $data = shift;
274              
275 0           my $responsehash = ();
276 0           my $rc = 0;
277              
278 0           $self->{'client'}->addHeader ("Content-Type", $mimetype);
279 0           $self->{'client'}->PUT($endpoint, $data);
280 0           $rc = $self->{'client'}->responseCode ();
281 0           $self->{'result'}->{'httpstatus'} = $rc;
282              
283 0 0         if ($rc-$rc%100 != 200) {
284 0           $responsehash->{'reqstatus'} = 'CRIT';
285 0           $responsehash->{'httpstatus'} = $rc;
286             } else {
287 0           my $responseType = $self->{'client'}->responseHeader('content-type');
288 0           my $responseContent = $self->{'client'}->responseContent();
289 0           $responsehash = $self->_handleResponse($rc, $responseType, $responseContent);
290             }
291 0           return dclone ($responsehash);
292             }
293              
294              
295             sub _handleResponse () {
296 0     0     my $self = shift;
297 0           my $rc = shift;
298 0           my $responseType = shift;
299 0           my $responseContent = shift;
300              
301 0           my $responseJSON = ();
302 0           my $responsehash = ();
303 0           $responsehash->{'reqstatus'} = 'OK';
304 0           $responsehash->{'httpstatus'} = 200;
305              
306             # is data JSON ?
307 0 0         if ($responseType =~ /^application\/json.*/) {
    0          
308 0           $self->_logV2($responseContent);
309 0 0         $responseJSON = decode_json($responseContent) if $responseContent ne '';
310 0           my $reftype = reftype($responseJSON);
311 0 0         if (not defined $reftype) {
    0          
    0          
    0          
312 0           $self->_bomb("Can't decode undef type");
313             } elsif ($reftype eq 'ARRAY') {
314 0           $self->_logV2("copying array");
315 0           $responsehash->{'content'}{'arraycount'} = $#$responseJSON+1;
316 0           for (my $i = 0; $i <= $#$responseJSON; $i++) {
317 0           $responsehash->{'content'}{$i} = $responseJSON->[$i];
318             }
319             } elsif ($reftype eq 'SCALAR') {
320 0           $self->_bomb("Can't decode scalar type");
321             } elsif ($reftype eq 'HASH') {
322 0           $self->_logV2("copying hash");
323 0           $responsehash->{'content'} = $responseJSON;
324             }
325 0           $responsehash->{'reqstatus'} = 'OK';
326 0           $responsehash->{'httpstatus'} = $rc;
327             } elsif ($responseType =~ /text\/plain.*/) {
328 0           $self->_logV2($responseContent);
329 0           $responsehash->{'content'} = $responseContent;
330             } else { # assume binary, like text, but do not log
331 0           $responsehash->{'content'} = $responseContent;
332             }
333 0           return $responsehash;
334             }
335              
336             sub _logV1() {
337 0     0     my $self = shift;
338 0           my $msg = shift;
339              
340 0 0         if ($self->{'verbose'} >= 1) {
341 0 0         if (defined $msg) {
342 0           print "$msg\n";
343             } else {
344 0           print "unknown $!";
345             }
346             }
347             }
348             sub _logV2() {
349 0     0     my $self = shift;
350 0           my $obj = shift;
351              
352 0 0         if ($self->{'verbose'} >= 2) {
353 0 0         if (defined $obj) {
354 0           print Dumper ($obj);
355             } else {
356 0           print "unknown objetc $!";
357             }
358             }
359             }
360              
361             sub _logD() {
362 0     0     my $self = shift;
363 0           my $object = shift;
364              
365 0 0         print Dumper ($object) if $self->{'debug'};
366             }
367              
368             sub _bomb() {
369 0     0     my $self = shift;
370 0           my $msg = shift;
371              
372 0           $msg .= "\nReport this to xavier.humbert\@ac-nancy-metz.fr with detail of the REST call you made";
373 0           die $msg;
374             }
375             1;
376              
377             =pod
378              
379             =head1 NAME
380              
381             RundeckAPI - simplifies authenticate, connect, queries to a Rundeck instance via REST API
382              
383             =head1 SYNOPSIS
384             use RundeckAPI;
385              
386             # create an object of type RundeckAPI :
387             my $api = RundeckAPI->new(
388             'url' => "https://my.rundeck.instance:4440",
389             'login' => "admin",
390             'token' =>
391             'debug' => 1,
392             'proxy' => "http://proxy.mycompany.com/",
393             );
394             my $hashRef = $api->get("/api/27/system/info");
395             my $json = '{some: value}';
396             $hashRef = $api->put(/api/27/endpoint_for_put, $json);
397              
398             =head1 METHODS
399              
400             =over 12
401              
402             =item C
403              
404             Returns an object authenticated and connected to a Rundeck Instance.
405             The field 'login' is not stricto sensu required, but it is a good security mesure to check if login/token match
406              
407             =item C
408              
409             Sends a GET query. Request one argument, the enpoint to the API. Returns a hash reference
410              
411             =item C
412              
413             Sends a POST query. Request two arguments, the enpoint to the API an the data in json format. Returns a hash reference
414              
415             =item C
416              
417             Sends a PUT query. Similar to post
418              
419             =item C
420              
421             Sends a DELETE query. Similar to get
422              
423             =item C
424              
425             POST some data. Request three arguments : endpoint, mime-type and the appropriate data. Returns a hash reference.
426              
427             =item C
428              
429             PUT some data. Similar to postData
430              
431             =item C
432              
433             Alias for compatibility for postData
434              
435             =item C
436              
437             Alias for compatibility for putData
438              
439             =back
440              
441             =head1 RETURN VALUE
442              
443             Returns a hash reference containing the data sent by Rundeck.
444              
445             The returned value is structured like the following :
446              
447             the fields `httpstatus` (200, 403, etc) and `requstatus` (OK, CRIT) are always present.
448              
449             the field `content` is a hash (if the mime-type of the result is JSON), text or binary
450              
451              
452             =head1 SEE ALSO
453              
454             See documentation for Rundeck's API https://docs.rundeck.com/docs/api/rundeck-api.html and returned data
455              
456             =head1 AUTHOR
457             Xavier Humbert
458              
459             =cut