File Coverage

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


line stmt bran cond sub pod time code
1 6     6   12670 use 5.12.1;
  6         22  
2 6     6   35 use strict;
  6         11  
  6         128  
3 6     6   28 use warnings;
  6         12  
  6         340  
4              
5             package Opsview::RestAPI;
6             $Opsview::RestAPI::VERSION = '1.210250';
7             # ABSTRACT: Interact with the Opsview Rest API interface
8              
9 6     6   38 use version;
  6         18  
  6         33  
10 6     6   493 use Data::Dump qw(pp);
  6         12  
  6         360  
11 6     6   39 use Carp qw(croak);
  6         8  
  6         307  
12 6     6   3240 use REST::Client;
  6         287462  
  6         226  
13 6     6   4408 use JSON;
  6         51572  
  6         37  
14 6     6   3646 use URI::Encode::XS qw(uri_encode);
  6         2839  
  6         406  
15              
16 6     6   2988 use Opsview::RestAPI::Exception;
  6         20  
  6         17673  
17              
18              
19             sub new {
20 6     6 1 18237 my ( $class, %args ) = @_;
21 6         29 my $self = bless {%args}, $class;
22              
23 6   50     40 $self->{url} ||= 'http://localhost';
24 6 100       26 $self->{ssl_verify_hostname} = defined $args{ssl_verify_hostname} ? $args{ssl_verify_hostname} : 1;
25 6   50     20 $self->{username} ||= 'admin';
26 6   50     18 $self->{password} ||= 'initial';
27 6   50     54 $self->{debug} //= 0;
28              
29             # Create the conenction here to info can be called before logging in
30 6         122 $self->{json} = JSON->new->allow_nonref;
31              
32 6         45 $self->{client} = REST::Client->new();
33 6         31732 $self->_client->setHost( $self->{url} );
34 6         47 $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         90 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         218 $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         20  
46              
47 6         207 return $self;
48             }
49              
50             # internal convenience functions
51 56     56   700 sub _client { return $_[0]->{client} }
52 5     5   162 sub _json { return $_[0]->{json} }
53              
54             sub _log {
55 22     22   768 my ( $self, $level, @message ) = @_;
56 22 50       79 say scalar(localtime), ': ', @message if ( $level <= $self->{debug} );
57 22         51 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 9389 sub url { return $_[0]->{url} }
68 5     5 1 25 sub username { return $_[0]->{username} }
69 5     5 1 26 sub password { return $_[0]->{password} }
70              
71             sub _parse_response_to_json {
72 5     5   84 my($self, $code, $response) = @_;
73              
74 5         21 $self->_log( 3, "Raw response: ", $response );
75              
76 5         16 my $json_result = eval { $self->_json->decode($response); };
  5         23  
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       24 if (my $error = $@) {
86 5         35 my %exception = (
87             eval_error => $error,
88             message => "Failed to read JSON in response from server ($response)",
89             );
90              
91 5         67 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   7500 my ( $self, %args ) = @_;
109              
110 8         20 $args{api} =~ s!^/rest/!!; # tidy any 'ref' URL we may have been given
111              
112 8 100       48 my $url = "/rest" . ( $args{api} ? '/' . $args{api} : '' );
113              
114 8         15 my @param_list;
115              
116 8         20 for my $param ( sort keys( %{ $args{params} } ) ) {
  8         52  
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         16 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         29 my $params = join( '&', @param_list);
131              
132 8 100       25 $url .= '?' . $params if $params;
133              
134 8         36 return $url;
135             }
136              
137             sub _query {
138 5     5   18 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     80 || $args{api} =~ m/login/ );
      33        
      33        
147              
148 5         19 $self->{type} = $args{type};
149              
150 5         23 my $url = $self->_generate_url( %args );
151              
152 5 50       19 my $data = $args{data} ? $self->_json->encode( $args{data} ) : undef;
153              
154 5         49 $self->_log( 2, "TYPE: $self->{type} URL: $url DATA: ",
155             pp($data) );
156              
157 5         14 my $type = $self->{type};
158              
159 5         19 my $deadlock_attempts = 0;
160             DEADLOCK: {
161 5         13 $self->_client->$type( $url, $data );
  5         20  
162              
163 5 50       253206 if ( $self->_client->responseCode ne 200 ) {
164 5         106 $self->_log( 2, "Non-200 response - checking for deadlock" );
165 5 50 33     15 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         131 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 6826 my ($self) = @_;
237 5 50       22 if ( !$self->{api_version} ) {
238 5         24 $self->_log( 2, "Fetching api_version information" );
239 5         21 $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     30 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       31 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         27 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       222 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   2212 my ($self) = @_;
479 1         3 $self->_log( 2, "In DESTROY" );
480 1 50       2 $self->logout if ( $self->_client );
481             }
482              
483              
484             1;
485              
486             __END__