File Coverage

blib/lib/Opsview/RestAPI.pm
Criterion Covered Total %
statement 101 229 44.1
branch 18 74 24.3
condition 9 23 39.1
subroutine 24 37 64.8
pod 19 19 100.0
total 171 382 44.7


line stmt bran cond sub pod time code
1 6     6   13255 use 5.12.1;
  6         23  
2 6     6   35 use strict;
  6         12  
  6         128  
3 6     6   44 use warnings;
  6         10  
  6         327  
4              
5             package Opsview::RestAPI;
6             $Opsview::RestAPI::VERSION = '1.210670';
7             # ABSTRACT: Interact with the Opsview Rest API interface
8              
9 6     6   38 use version;
  6         12  
  6         32  
10 6     6   502 use Data::Dump qw(pp);
  6         20  
  6         355  
11 6     6   37 use Carp qw(croak);
  6         12  
  6         302  
12 6     6   3883 use REST::Client;
  6         283884  
  6         215  
13 6     6   4456 use JSON;
  6         52067  
  6         38  
14 6     6   3675 use URI::Encode::XS qw(uri_encode);
  6         2978  
  6         385  
15              
16 6     6   2976 use Opsview::RestAPI::Exception;
  6         18  
  6         16526  
17              
18              
19             sub new {
20 6     6 1 17748 my ( $class, %args ) = @_;
21 6         28 my $self = bless {%args}, $class;
22              
23 6   50     40 $self->{url} ||= 'http://localhost';
24 6 50       40 $self->{ssl_verify_hostname} = defined $args{ssl_verify_hostname} ? $args{ssl_verify_hostname} : 1;
25 6   50     23 $self->{username} ||= 'admin';
26 6   50     22 $self->{password} ||= 'initial';
27 6   50     57 $self->{debug} //= 0;
28              
29             # Create the conenction here to info can be called before logging in
30 6         131 $self->{json} = JSON->new->allow_nonref;
31              
32 6         47 $self->{client} = REST::Client->new();
33 6         31783 $self->_client->setHost( $self->{url} );
34 6         52 $self->_client->addHeader( 'Content-Type', 'application/json' );
35              
36             # Set the SSL options for use with https connections
37             $self->_client->getUseragent->ssl_opts(
38 6         91 verify_hostname => $self->{ssl_verify_hostname} );
39              
40             # Make sure we follow any redirects if originally given
41             # http but get redirected to https
42 6         215 $self->_client->setFollow(1);
43              
44             # and make sure POST will also redirect correctly (doesn't by default)
45 6         40 push @{ $self->_client->getUseragent->requests_redirectable }, 'POST';
  6         18  
46              
47 6         193 return $self;
48             }
49              
50             # internal convenience functions
51 56     56   683 sub _client { return $_[0]->{client} }
52 5     5   173 sub _json { return $_[0]->{json} }
53              
54             sub _log {
55 22     22   722 my ( $self, $level, @message ) = @_;
56 22 50       79 say scalar(localtime), ': ', @message if ( $level <= $self->{debug} );
57 22         65 return $self;
58             }
59              
60             sub _dump {
61 0     0   0 my ( $self, $level, $object ) = @_;
62 0 0       0 say scalar(localtime), ': ', pp($object) if ( $level <= $self->{debug} );
63 0         0 return $self;
64             }
65              
66              
67 10     10 1 10104 sub url { return $_[0]->{url} }
68 5     5 1 26 sub username { return $_[0]->{username} }
69 5     5 1 28 sub password { return $_[0]->{password} }
70              
71             sub _parse_response_to_json {
72 5     5   139 my($self, $code, $response) = @_;
73              
74 5         22 $self->_log( 3, "Raw response: ", $response );
75              
76 5         11 my $json_result = eval { $self->_json->decode($response); };
  5         22  
77              
78             my %call_info = (
79             type => $self->{type},
80 5         35 url => $self->url,
81             http_code => $code,
82             response => $response,
83             );
84              
85 5 50       25 if (my $error = $@) {
86 5         33 my %exception = (
87             eval_error => $error,
88             message => "Failed to read JSON in response from server ($response)",
89             );
90              
91 5         84 croak( Opsview::RestAPI::Exception->new(%call_info, %exception) );
92             }
93              
94 0         0 $self->_log( 2, "result: ", pp($json_result) );
95              
96 0 0       0 if ( $json_result->{message}) {
97             croak( Opsview::RestAPI::Exception->new(
98             %call_info,
99             response => $response,
100             message => $json_result->{message},
101 0         0 ));
102             }
103              
104 0         0 return $json_result;
105             }
106              
107             sub _generate_url {
108 8     8   8265 my ( $self, %args ) = @_;
109              
110 8         19 $args{api} =~ s!^/rest/!!; # tidy any 'ref' URL we may have been given
111              
112 8 100       35 my $url = "/rest" . ( $args{api} ? '/' . $args{api} : '' );
113              
114 8         29 my @param_list;
115              
116 8         15 for my $param ( sort keys( %{ $args{params} } ) ) {
  8         47  
117 4 50       13 if ( ! defined $args{params}{$param} ) {
    50          
    0          
118 0         0 croak( Opsview::RestAPI::Exception->new( message => "Parameter '$param' is not valid" ) );
119             } elsif ( ! ref($args{params}{$param}) ) {
120 4         18 push(@param_list, $param . '=' . uri_encode( $args{params}{$param} ) );
121             } elsif (ref($args{params}{$param}) eq "ARRAY" ) {
122 0         0 for my $arg ( @{ $args{params}{$param} }) {
  0         0  
123 0         0 push(@param_list, $param . '=' . uri_encode( $arg ) );
124             }
125             } else {
126 0         0 croak( Opsview::RestAPI::Exception->new( message => "Parameter '$param' is not an accepted type: " . ref( $args{params}{$param} ) ) );
127             }
128             }
129              
130 8         26 my $params = join( '&', @param_list);
131              
132 8 100       47 $url .= '?' . $params if $params;
133              
134 8         35 return $url;
135             }
136              
137             sub _query {
138 5     5   27 my ( $self, %args ) = @_;
139             croak "Unknown type '$args{type}'"
140 5 50       55 if ( $args{type} !~ m/^(GET|POST|PUT|DELETE)$/ );
141              
142             croak( Opsview::RestAPI::Exception->new( message => "Not logged in" ) )
143             unless ( $self->{token}
144             || !defined( $args{api} )
145             || !$args{api}
146 5 50 33     67 || $args{api} =~ m/login/ );
      33        
      33        
147              
148 5         19 $self->{type} = $args{type};
149              
150 5         24 my $url = $self->_generate_url( %args );
151              
152 5 50       20 my $data = $args{data} ? $self->_json->encode( $args{data} ) : undef;
153              
154 5         51 $self->_log( 2, "TYPE: $self->{type} URL: $url DATA: ",
155             pp($data) );
156              
157 5         15 my $type = $self->{type};
158              
159 5         11 my $deadlock_attempts = 0;
160             DEADLOCK: {
161 5         9 $self->_client->$type( $url, $data );
  5         19  
162              
163 5 50       253340 if ( $self->_client->responseCode ne 200 ) {
164 5         104 $self->_log( 2, "Non-200 response - checking for deadlock" );
165 5 50 33     21 if ( $self->_client->responseContent =~ m/deadlock/i
166             && $deadlock_attempts < 5 )
167             {
168 0         0 $deadlock_attempts++;
169 0         0 $self->_log( 1, "Encountered deadlock: ",
170             $self->_client->responseContent());
171 0         0 $self->_log( 1, "Retrying (count: $deadlock_attempts)");
172 0         0 sleep 1;
173 0         0 redo DEADLOCK;
174             }
175             }
176             }
177              
178 5         128 return $self->_parse_response_to_json( $self->_client->responseCode, $self->_client->responseContent() )
179             }
180              
181              
182             sub login {
183 0     0 1 0 my ($self) = @_;
184              
185             # make sure we are communicating with at least Opsview v4.0
186 0         0 my $api_version = $self->api_version;
187 0 0       0 if ( $api_version->{api_version} < 4.0 ) {
188             croak(
189             Opsview::RestPI::Exception->new(
190             message => $self->{url}
191             . " is running Opsview version "
192             . $api_version->{api_version}
193 0         0 . ". Need at least version 4.0",
194             http_code => 505,
195             )
196             );
197             }
198              
199 0         0 $self->_log( 2, "About to login" );
200              
201 0 0       0 if ( $self->{token} ) {
202 0         0 $self->_log( 1, "Already have token $self->{token}" );
203 0         0 return $self;
204             }
205              
206             my $result = eval {
207             $self->post(
208             api => "login",
209             params => {
210             username => $self->{username},
211             password => $self->{password},
212             },
213 0         0 );
214 0 0       0 } or do {
215 0         0 my $e = $@;
216 0         0 $self->_log( 2, "Exception object:" );
217 0         0 $self->_dump( 2, $e );
218 0         0 die $e->message, $/;
219             };
220              
221 0         0 $self->{token} = $result->{token};
222              
223 0         0 $self->_client->addHeader( 'X-Opsview-Username', $self->{username} );
224 0         0 $self->_client->addHeader( 'X-Opsview-Token', $result->{token} );
225              
226 0         0 $self->opsview_info();
227              
228 0         0 $self->_log( 1,
229             "Successfully logged in to '$self->{url}' as '$self->{username}'" );
230              
231 0         0 return $self;
232             }
233              
234              
235             sub api_version {
236 5     5 1 7876 my ($self) = @_;
237 5 50       22 if ( !$self->{api_version} ) {
238 5         28 $self->_log( 2, "Fetching api_version information" );
239 5         20 $self->{api_version} = $self->get( api => '' );
240             }
241 0         0 return $self->{api_version};
242             }
243              
244              
245             sub opsview_info {
246 0     0 1 0 my ($self) = @_;
247 0 0       0 if ( !$self->{opsview_info} ) {
248 0         0 $self->_log( 2, "Fetching opsview_info information" );
249 0         0 $self->{opsview_info} = $self->get( api => 'info' );
250             }
251 0         0 return $self->{opsview_info};
252             }
253              
254              
255             sub opsview_version {
256 0     0 1 0 my ($self) = @_;
257              
258 0         0 return qv( $self->opsview_info->{opsview_version} );
259             }
260              
261              
262              
263             sub opsview_build {
264 0     0 1 0 my ($self) = @_;
265 0         0 $self->opsview_info;
266 0         0 return $self->{opsview_info}->{opsview_build};
267             }
268              
269              
270             sub interval {
271 0     0 1 0 my ( $self, $interval ) = @_;
272              
273             # if this is a 4.6 system, adjust the interval to be minutes
274 0 0       0 if ( $self->{api_version}->{api_version} < 5.0 ) {
275 0         0 $interval = int( $interval / 60 );
276 0         0 $interval += 1;
277             }
278 0         0 return $interval;
279             }
280              
281              
282             sub post {
283 0     0 1 0 my ( $self, %args ) = @_;
284 0         0 return $self->_query( %args, type => 'POST' );
285             }
286              
287             sub get {
288 5     5 1 19 my ( $self, %args ) = @_;
289              
290 5 0 33     32 if ( $args{batch_size} && $args{params}{rows} ) {
291 0         0 croak(
292             Opsview::RestAPI::Exception->new(
293             message => "Cannot specify both 'batch_size' and 'rows'"
294             )
295             );
296             }
297              
298 5 50       32 if ( $args{batch_size} ) {
299 0         0 my @data;
300              
301 0         0 my %hash = (
302             list => \@data,
303             summary => {
304             allrows => 0,
305             rows => 0,
306             }
307             );
308              
309             # fetch just summary information for what we are after
310 0         0 my %get_args = %args;
311 0         0 $get_args{params}{rows} = 0;
312 0         0 delete( $get_args{batch_size} );
313              
314 0         0 $self->_log( 1, "batch_size request: fetching summary data only" );
315 0         0 my $summary = $self->_query( %get_args, type => 'GET' );
316              
317             # This is reassembled to make it look like everything was fetched in one go
318 0         0 $hash{summary}{allrows} = $summary->{summary}->{allrows};
319 0         0 $hash{summary}{rows} = $summary->{summary}->{allrows};
320              
321             my $totalpages
322 0         0 = int( $summary->{summary}->{allrows} / $args{batch_size} ) + 1;
323              
324 0         0 $self->_log( 2,
325             "Fetching $hash{summary}{allrows} rows in batches of $args{batch_size}, $totalpages pages to fetch"
326             );
327 0         0 my $start_time = time();
328              
329             # now start fetching the data in batch_size increments
330 0         0 my $page = 0;
331 0         0 $get_args{params}{rows} = $args{batch_size};
332 0         0 while ( $page++ < $totalpages ) {
333              
334 0         0 $get_args{params}{page} = $page;
335 0         0 $self->_log( 3, "About to fetch page $page" );
336 0         0 my $result = $self->_query( %get_args, type => 'GET' );
337              
338 0         0 push( @data, @{ $result->{list} } );
  0         0  
339             }
340              
341 0         0 my $elapsed_time = time() - $start_time;
342 0         0 $self->_log( 2, "Fetch completed in ${elapsed_time}s" );
343              
344 0         0 return \%hash;
345             }
346 5         28 return $self->_query( %args, type => 'GET' );
347             }
348              
349             sub put {
350 0     0 1 0 my ( $self, %args ) = @_;
351 0         0 return $self->_query( %args, type => 'PUT' );
352             }
353              
354             sub delete {
355 0     0 1 0 my ( $self, %args ) = @_;
356 0         0 return $self->_query( %args, type => 'DELETE' );
357             }
358              
359              
360 0     0 1 0 sub reload { return $_[0]->post( api => 'reload' ) }
361              
362              
363             sub reload_pending {
364 0     0 1 0 my $result = $_[0]->get( api => 'reload' );
365 0 0       0 if(! defined $result->{configuration_status}) {
366 0         0 croak( Opsview::RestAPI::Exception->new(message => "'configuration_status' not found", result => $result ) );
367             }
368 0 0       0 return $result->{configuration_status} eq 'pending' ? 1 : 0;
369             }
370              
371              
372             # NOTE: use LWP::UserAgent directly to make use of its file upload functionality
373             # as REST::Client doesn't allow it to work as expected
374             sub file_upload {
375 0     0 1 0 my ( $self, %args ) = @_;
376              
377 0         0 my $ua = $self->_client->getUseragent();
378 0         0 $ua->default_header( 'Content-Type', 'application/json' );
379 0         0 $ua->default_header( 'X-Opsview-Username', $self->{username} );
380 0         0 $ua->default_header( 'X-Opsview-Token', $self->{token} );
381              
382 0         0 my $url = $self->{url}."/rest/$args{api}";
383 0 0       0 $url .= '/upload' unless $url =~ m!/upload$!;
384              
385             #warn "url=$url";
386              
387             my $response = $ua->post(
388             $url,
389             Accept => "text/html",
390             Content_Type => 'form-data',
391 0         0 Content => [ filename => [ $args{local_file} => $args{remote_file} ] ]
392             );
393              
394 0         0 return $self->_parse_response_to_json( $response->code, $response->content );
395             }
396              
397              
398             sub logout {
399 1     1 1 3 my ($self) = @_;
400              
401 1         3 $self->_log( 2, "In logout" );
402              
403 1 50       184 return unless ( $self->{token} );
404 0         0 $self->_log( 2, "found token, on to logout" );
405              
406 0         0 $self->post( api => 'logout' );
407 0         0 $self->_log( 1, "Successfully logged out from $self->{url}" );
408              
409             # invalidate all the info held internally
410 0         0 $self->{token} = undef;
411 0         0 $self->{api_version} = undef;
412 0         0 $self->{opsview_info} = undef;
413              
414 0         0 $self->_log( 2, "Token removed" );
415 0         0 return $self;
416             }
417              
418             # Copied from Opsview::Utils so that module does not need to be installed
419             #
420              
421              
422             sub remove_keys_from_hash {
423 0     0 1 0 my ( $class, $hash, $allowed_keys, $do_not_die_on_non_hash ) = @_;
424              
425 0 0       0 if ( ref $hash ne "HASH" ) {
426              
427             # Double negative as default is to die
428 0 0       0 unless ($do_not_die_on_non_hash) {
429 0         0 die "Not a HASH: $hash";
430             }
431 0         0 return $hash;
432             }
433              
434             # We cache the keys_list into
435 0 0       0 if ( !defined $allowed_keys ) {
    0          
    0          
    0          
436 0         0 die "Must specify $allowed_keys";
437             }
438              
439             # OK
440             elsif ( ref $allowed_keys eq "HASH" ) {
441              
442             }
443             elsif ( ref $allowed_keys eq "ARRAY" ) {
444 0         0 my @temp = @$allowed_keys;
445 0         0 $allowed_keys = {};
446 0         0 map { $allowed_keys->{$_} = 1 } @temp;
  0         0  
447             }
448             elsif ( ref $allowed_keys ) {
449 0         0 $allowed_keys = { $allowed_keys => 1 };
450             }
451             else {
452 0         0 die "allowed_keys incorrect";
453             }
454              
455 0         0 foreach my $k ( keys %$hash ) {
456 0 0       0 if ( exists $allowed_keys->{$k} ) {
    0          
    0          
457 0         0 delete $hash->{$k};
458             }
459             elsif ( ref $hash->{$k} eq "ARRAY" ) {
460 0         0 my @new_list;
461 0         0 foreach my $item ( @{ $hash->{$k} } ) {
  0         0  
462 0         0 push @new_list,
463             $class->remove_keys_from_hash( $item, $allowed_keys,
464             $do_not_die_on_non_hash );
465             }
466 0         0 $hash->{$k} = \@new_list;
467             }
468             elsif ( ref $hash->{$k} eq "HASH" ) {
469             $hash->{$k}
470 0         0 = $class->remove_keys_from_hash( $hash->{$k}, $allowed_keys,
471             $do_not_die_on_non_hash );
472             }
473             }
474 0         0 return $hash;
475             }
476              
477             sub DESTROY {
478 1     1   2789 my ($self) = @_;
479 1         5 $self->_log( 2, "In DESTROY" );
480 1 50       2 $self->logout if ( $self->_client );
481             }
482              
483              
484             1;
485              
486             __END__