File Coverage

blib/lib/Monitoring/Icinga.pm
Criterion Covered Total %
statement 18 126 14.2
branch 0 36 0.0
condition 0 22 0.0
subroutine 6 14 42.8
pod 7 7 100.0
total 31 205 15.1


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Monitoring::Icinga - An object oriented interface to Icinga
4              
5             =head1 SYNOPSIS
6              
7             Simple example:
8              
9             use Monitoring::Icinga;
10            
11             my $api = Monitoring::Icinga->new(
12             AuthKey => 'ThisIsTheAuthKey'
13             );
14            
15             $api->set_columns('HOST_NAME', 'HOST_OUTPUT', 'HOST_CURRENT_STATE');
16             my $hosts = $api->get_hosts(1,2);
17              
18             This will query the Icinga Web REST API on localhost. $hosts is an array
19             reference containing the information for every host object, which is currently
20             is DOWN (1) or UNREACHABLE (2).
21              
22             =head1 DESCRIPTION
23              
24             This module implements an object oriented interface to Icinga using its REST
25             API. It is tested with the Icinga Web REST API v1.2 only, so sending commands
26             via PUT is not yet supported (but will be in the future).
27              
28             =head1 METHODS
29              
30             =cut
31              
32             package Monitoring::Icinga;
33              
34 1     1   24551 use strict;
  1         2  
  1         35  
35 1     1   4 use warnings;
  1         2  
  1         29  
36 1     1   6 use Carp qw(carp croak);
  1         6  
  1         83  
37 1     1   890 use HTTP::Request::Common qw(POST);
  1         33313  
  1         88  
38 1     1   1127 use LWP::UserAgent;
  1         29582  
  1         35  
39 1     1   1202 use JSON::XS;
  1         7724  
  1         1684  
40              
41             our $VERSION = '0.02';
42              
43              
44             =over
45              
46             =item new (%config)
47              
48             Constructor. You can set the following parameters during construction:
49              
50             BaseURL - The URL pointing to the Icinga REST API (default: 'http://localhost/icinga-web/web/api').
51             AuthKey - The Auth key to use (mandatory)
52             Target - 'host' or 'service' (default: 'host')
53             Columns - List (array) of columns to return from API calls
54             Filters - API filter as a hash reference
55             ssl_verify_hostname - Verify the SSL hostname. Sets the 'verify_hostname' option of LWP::UserAgent (default: 1)
56              
57             Example, that returns a hash reference containing some data of all hosts
58             whose state is 1 (that means: WARNING):
59              
60             my $api = Monitoring::Icinga->new(
61             BaseURL => 'https://your.icinga.host/icinga-web/web/api',
62             AuthKey => 'ThisIsTheAuthKey',
63             Target => 'host',
64             Filters => {
65             'type' => 'AND',
66             'field' => [
67             {
68             'type' => 'atom',
69             'field' => [ 'HOST_CURRENT_STATE' ],
70             'method' => [ '=' ],
71             'value' => [ 1 ],
72             },
73             ],
74             },
75             Columns => [ 'HOST_NAME', 'HOST_OUTPUT', 'HOST_CURRENT_STATE' ],
76             );
77            
78             my $result = $api->call;
79              
80             You can recall the setters to thange the parameters, filters and columns for
81             later API calls.
82              
83             =cut
84              
85             sub new {
86 0     0 1   my ($class, %args) = @_;
87 0           my $self = {};
88              
89 0 0         return undef unless defined $args{'AuthKey'};
90              
91 0   0       $self->{'baseurl'} = $args{'BaseURL'} || 'http://localhost/icinga-web/web/api';
92 0           $self->{'authkey'} = $args{'AuthKey'};
93 0   0       $self->{'ssl_verify_hostname'} = $args{'ssl_verify_hostname'} || 1;
94 0           $self->{'params'} = [];
95              
96 0           bless ($self, $class);
97              
98             # Initialize some values
99 0 0         $self->set_target( ($args{'Target'} ? $args{'Target'} : 'host') );
100 0 0         $self->set_filters($args{'Filters'}) if $args{'Filters'};
101              
102             # Initialize columns
103 0 0         if ($args{'Columns'}) {
104 0           $self->set_columns(@{$args{'Columns'}});
  0            
105             }
106             else {
107             # Set some default columns
108 0           $self->set_columns('HOST_NAME', 'SERVICE_NAME');
109             }
110              
111             # Construct the complete URL
112 0           $self->{'url'} = $self->{'baseurl'} . '/authkey=' . $self->{'authkey'} . '/json';
113              
114             # Exit if HTTPS requested, but LWP::Protocol::https not found
115 0 0         if ($self->{'baseurl'} =~ /^https:/i) {
116 0           eval {
117 0           require LWP::Protocol::https;
118             };
119 0 0         croak 'Error: HTTPS requested, but module \'LWP::Protocol::https\' not installed.' if $@;
120             }
121              
122             # Prepare the LWP::UserAgent object for later use
123 0           $self->{'ua'} = LWP::UserAgent->new( ssl_opts => { verify_hostname => $self->{'ssl_verify_hostname'} } );
124 0           $self->{'ua'}->agent('Monitoring::Icinga/' . $VERSION . ' ');
125              
126 0           return $self;
127             }
128              
129              
130             =item set_target ($value)
131              
132             Set target for API call. Can be 'host' or 'service'. See Icinga Web REST API
133             Documentation at http://docs.icinga.org/latest/en/icinga-web-api.html for
134             details on allowed targets.
135              
136             =cut
137              
138             sub set_target {
139 0     0 1   my ($self, $target) = @_;
140 0 0 0       unless (defined $target and ($target eq 'host' or $target eq 'service')) {
      0        
141 0           carp 'Invalid target specified';
142 0           return 0;
143             }
144              
145 0           $self->{'target'} = [];
146 0           push @{$self->{'target'}}, 'target';
  0            
147 0           push @{$self->{'target'}}, $target;
  0            
148             }
149              
150              
151             =item set_columns (@array)
152              
153             Set columns that get returned by a call. The parameters are a list of columns.
154             For a list of valid columns, see the source code of Icinga Web at:
155              
156             app/modules/Api/models/Store/LegacyLayer/TargetModifierModel.class.php
157              
158             Example:
159              
160             $api->set_columns('HOST_NAME', 'HOST_CURRENT_STATE', 'HOST_OUTPUT');
161              
162             =cut
163              
164             sub set_columns {
165 0     0 1   my ($self, @columns) = @_;
166              
167 0           my $columncount = 0;
168 0           $self->{'columns'} = [];
169 0           foreach (@columns) {
170 0           push @{$self->{'columns'}}, 'columns[' . $columncount . ']';
  0            
171 0           push @{$self->{'columns'}}, $_;
  0            
172 0           $columncount++;
173             }
174             }
175              
176              
177             =item set_filters ($hash_reference)
178              
179             Set filters for API call using a hash reference. See Icinga Web REST API
180             Documentation at http://docs.icinga.org/latest/en/icinga-web-api.html for
181             details on how filters need to be defined. Basically, they define it in JSON
182             syntax, but this module requires a Perl hash reference instead.
183              
184             Simple Example:
185              
186             $api->set_filters( {
187             'type' => 'AND',
188             'field' => [
189             {
190             'type' => 'atom',
191             'field' => [ 'HOST_CURRENT_STATE' ],
192             'method' => [ '>' ],
193             'value' => [ 0 ],
194             },
195             ],
196             } );
197              
198             More complex example:
199              
200             $api->set_filters( {
201             'type' => 'AND',
202             'field' => [
203             {
204             'type' => 'atom',
205             'field' => [ 'SERVICE_NAME' ],
206             'method' => [ 'like' ],
207             'value' => [ '*pop*' ],
208             },
209             {
210             'type' => 'OR',
211             'field' => [
212             {
213             'type' => 'atom',
214             'field' => [ 'SERVICE_CURRENT_STATE' ],
215             'method' => [ '>' ],
216             'value' => [ 0 ],
217             },
218             {
219             'type' => 'atom',
220             'field' => [ 'SERVICE_IS_FLAPPING' ],
221             'method' => [ '=' ],
222             'value' => [ 1 ],
223             },
224             ],
225             },
226             ],
227             };
228              
229             You don't actually need a filter for the API calls to work. But it is strongly
230             recommended to define one whenever you fetch any data. Otherwise ALL host or
231             service objects will be returned.
232              
233             By the way: You should filter for host or service objects, not both. Otherwise
234             you will most likely not get the results you want. I.e. if you want to get all
235             hosts and services with problems, you better do two API calls. One for the
236             hosts, another for the services.
237              
238             =cut
239              
240             sub set_filters {
241 0     0 1   my ($self, $filters) = @_;
242 0           my $json_data;
243 0           eval {
244 0           $json_data = encode_json($filters);
245             };
246 0 0         if ($@) {
247 0           chomp $@;
248 0           carp 'JSON ERROR: '.$@;
249 0           return 0;
250             }
251             else {
252 0           $self->{'filters'} = [];
253 0           push @{$self->{'filters'}}, 'filters_json';
  0            
254 0           push @{$self->{'filters'}}, $json_data;
  0            
255             }
256             }
257              
258              
259             =item get_hosts (@states)
260              
261             Return an array of all host objects matching the specified states. The
262             parameters can be:
263              
264             0 - OK
265             1 - DOWN
266             2 - UNREACHABLE
267              
268             You should set the desired columns first, using either the Columns parameter of
269             the constructor or the set_columns() function, i.e.:
270              
271             $api->set_columns('HOST_NAME', 'HOST_CURRENT_STATE', 'HOST_OUTPUT');
272             $hosts_array = $api->get_hosts(1,2);
273              
274             That would return the name, state and check output of all hosts in state DOWN
275             or UNREACHABLE.
276              
277             =cut
278              
279             sub get_hosts {
280 0     0 1   my ($self, @states) = @_;
281 0           return $self->_get('host', @states);
282             }
283              
284              
285             =item get_services (@states)
286              
287             Return an array of all service objects matching the specified states. The
288             parameters can be:
289              
290             0 - OK
291             1 - WARNING
292             2 - CRITICAL
293             3 - UNKNOWN
294              
295             You should set the desired columns first, using either the Columns parameter of
296             the constructor or the set_columns() function, i.e.:
297              
298             $api->set_columns('HOST_NAME', 'SERVICE_NAME', 'HOST_CURRENT_STATE', 'HOST_OUTPUT');
299             $services_array = $api->get_services(1,2,3);
300              
301             That would return the host name, service name, state and check output of all
302             services in state WARNING, CRITICAL or UNKNOWN.
303              
304             =cut
305              
306             sub get_services {
307 0     0 1   my ($self, @states) = @_;
308 0           return $self->_get('service', @states);
309             }
310              
311              
312             =item call
313              
314             Do an API call using the current settings (Target, Columns and Filters) and
315             return the complete result as a hash reference. The data you usually want is in
316             $hash->{'result'}.
317              
318             =cut
319              
320             sub call {
321 0     0 1   my $self = shift;
322              
323 0           my @params;
324 0           push @params, @{$self->{'target'}};
  0            
325 0           push @params, @{$self->{'columns'}};
  0            
326 0           push @params, @{$self->{'filters'}};
  0            
327              
328 0           my $api_request = POST $self->{'url'}, \@params;
329 0           my $api_result = $self->{'ua'}->request($api_request);
330 0           my $content_type = $api_result->headers->header('Content-Type');
331              
332 0 0         if ($content_type =~ /^application\/json/) {
    0          
333             # We got a (hopefully) correct answer
334 0           my $api_result_hash = decode_json($api_result->content);
335 0           return $api_result_hash;
336             }
337             elsif ($content_type =~ /^text\/html/) {
338             # We got an error from the API
339 0           my $errormsg = 'unknown error';
340 0           foreach (split(/\n/, $api_result->content)) {
341             # SQLSTATE[42S22]: Column not found: 1054 Unknown column 'i.service_has_been_acknowleged' in 'field list'
342 0 0         $errormsg = $1 if $_ =~ /(SQLSTATE\[.*\]: .*)<\/div>/;
343 0           $errormsg =~ s/\s+$//g;
344             }
345 0           carp 'JSON ERROR: ' . $errormsg;
346 0           return undef;
347             }
348             }
349              
350              
351             sub _get {
352 0     0     my ($self, $target, @states) = @_;
353 0 0 0       unless (defined $target and ($target eq 'host' or $target eq 'service')) {
      0        
354 0           carp 'No valid target given';
355 0           return undef;
356             }
357 0 0         unless (scalar @states) {
358 0           carp 'No state given';
359 0           return undef;
360             }
361              
362 0           my $field = 'HOST_CURRENT_STATE';
363 0 0         $field = 'SERVICE_CURRENT_STATE' if $target eq 'service';
364              
365 0           my $filters = {
366             'type' => 'OR',
367             'field' => [],
368             };
369              
370 0           foreach my $state (@states) {
371 0 0 0       if ($target eq 'host' and $state !~ /^[012]$/) {
372 0           carp 'Unknown host state: ' . $state;
373 0           next;
374             }
375 0 0 0       if ($target eq 'service' and $state !~ /^[0123]$/) {
376 0           carp 'Unknown service state: ' . $state;
377 0           next;
378             }
379              
380 0           my $subfilter = {
381             'type' => 'atom',
382             'field' => [ $field ],
383             'method' => [ '=' ],
384             'value' => [ $state ],
385             };
386              
387 0           push @{$filters->{'field'}}, $subfilter;
  0            
388             }
389              
390             # Remember the original values to restore them later
391 0           my $temp = {};
392 0           $temp->{'target'} = $self->{'target'};
393 0           $temp->{'filters'} = $self->{'filters'};
394              
395             # Set appropriate values for this call
396 0           $self->set_target($target);
397 0           $self->set_filters($filters);
398              
399             # Do the call
400 0           my $result = $self->call;
401              
402             # Restore the original values
403 0           $self->{'target'} = $temp->{'target'};
404 0           $self->{'filters'} = $temp->{'filters'};
405              
406 0 0         return $result->{'result'} if $result->{'success'};
407 0 0         carp 'API Error: ' . $result->{'result'} if $result->{'result'};
408 0           return undef;
409             }
410              
411             1;
412              
413             __END__