File Coverage

blib/lib/Nagios/Report.pm
Criterion Covered Total %
statement 167 449 37.1
branch 29 134 21.6
condition 6 59 10.1
subroutine 31 59 52.5
pod 9 22 40.9
total 242 723 33.4


.*)/s ;
line stmt bran cond sub pod time code
1             package Nagios::Report;
2              
3 11     11   84734 use strict;
  11         25  
  11         470  
4              
5             # $Id: Report.pm,v 1.87 2006-08-23 14:26:17+10 sh1517 Exp sh1517 $
6              
7 11     11   67 use base 'Exporter' ;
  11         63  
  11         1760  
8 11     11   12282 use Time::Local ;
  11         21782  
  11         853  
9             # use POSIX qw(mktime) ;
10              
11 11     11   73 use vars qw($VERSION @EXPORT @EXPORT_OK %stime_etime);
  11         23  
  11         1514  
12              
13             *t2hms = \&time2ddhhmmss ;
14             *d2t = \&date2time ;
15             *i2t = \&interval2time ;
16             *st_et = \%stime_etime ;
17              
18             @EXPORT = qw(d2t t2hms comp max_l %st_et i2t) ;
19              
20             $VERSION = '0.003';
21              
22 11     11   58 use constant REQUEST_METHOD => 'GET' ;
  11         31  
  11         523  
23              
24             # Will be hacked by Makefile.PL
25              
26 11     11   54 use constant CGI_PATH => '' ;
  11         18  
  11         431  
27 11     11   51 use constant LYNX => '' ;
  11         19  
  11         502  
28 11     11   52 use constant WGET => '/usr/bin/wget' ;
  11         20  
  11         548  
29             # EURO_DATE => \d\d?-\d\d?-\d\d\d\d
30             # will be interpreted as DD-MM-YYYY
31 11     11   52 use constant EURO_DATE => 1 ;
  11         18  
  11         704  
32              
33             # End scope of expected changes by Makefile.PL
34              
35             # NB. I choose lynx etc over LWP because
36             # LWP is a big module (LWP::Simple doesn't
37             # fetch protected pages).
38            
39              
40 11     11   142 use constant NAG_AVAIL_CGI => CGI_PATH . 'avail.cgi' ;
  11         23  
  11         889  
41 11     11   52 use constant WEB_PAGE => !! (LYNX || WGET) ;
  11         16  
  11         544  
42 11     11   54 use constant LOCAL_CGI => !! CGI_PATH ;
  11         18  
  11         808  
43 11     11   56 use constant USE_LYNX => !! (WEB_PAGE && LYNX) ;
  11         21  
  11         450  
44              
45 11     11   59 use constant EXCEL_FILENAME => './Nagios_Avail.xls' ;
  11         14  
  11         531  
46              
47 11         462 use constant QS_TEMP_HOSTREP =>
48 11     11   51 q(show_log_entries=&host=all&t1=T1&t2=T2&rpttimeperiod=REPORT_PERIOD&assumeinitialstates=yes&assumestateretention=yes&assumestatesduringnotrunning=yes&includesoftstates=no&initialassumedhoststate=3&initialassumedservicestate=6&backtrack=0&csvoutput) ;
  11         17  
49              
50 11         436 use constant QS_TEMP_SVCREP =>
51 11     11   49 q(show_log_entries=&host=FOO&service=all&t1=T1&t2=T2&rpttimeperiod=REPORT_PERIOD&assumeinitialstates=yes&assumestateretention=yes&assumestatesduringnotrunning=yes&includesoftstates=no&initialassumedservicestate=6&backtrack=0&csvoutput=) ;
  11         18  
52              
53 11         463 use constant AVAIL_URL_TEMP_HOST =>
54 11     11   48 q(http://SERVER/nagios/cgi-bin/avail.cgi?host=HOST&t1=T1&t2=T2&show_log_entries=&assumeinitialstates=yes&assumestateretention=yes&includesoftstates=no&backtrack=0) ;
  11         19  
55              
56 11         515 use constant TREND_URL_TEMP_HOST =>
57 11     11   53 q(http://SERVER/nagios/cgi-bin/trends.cgi?host=HOST&t1=T1&t2=T2&assumeinitialstates=yes&assumestateretention=yes&includesoftstates=no) ;
  11         30  
58              
59 11         566 use constant AVAIL_URL_TEMP_SVC =>
60 11     11   57 q(http://SERVER/nagios/cgi-bin/avail.cgi?host=HOST&service=SVC&t1=T1&t2=T2&show_log_entries=&assumeinitialstates=yes&assumestateretention=yes&includesoftstates=no&assumestatesduringnotrunning=yes&backtrack=0&initialassumedservicestate=6) ;
  11         17  
61              
62 11         739 use constant TREND_URL_TEMP_SVC =>
63 11     11   50 q(http://SERVER/nagios/cgi-bin/trends.cgi?host=HOST&service=SVC&t1=T1&t2=T2&assumeinitialstates=yes&assumestatesduringnotrunning=yes&initialassumedhoststate=0&initialassumedservicestate=0&assumestateretention=yes&includesoftstates=no) ;
  11         21  
64              
65             use constant {
66 11         1694 SEC => 0,
67             MIN => 1,
68             HOUR => 2,
69             MDAY => 3,
70             MON => 4,
71             YEAR => 5,
72             WDAY => 6,
73             YDAY => 7,
74             ISDST => 8,
75 11     11   53 };
  11         17  
76              
77 11         1022 use constant MONTH => {
78             Jan => 0,
79             Feb => 1,
80             Mar => 2,
81             Apr => 3,
82             May => 4,
83             Jun => 5,
84             Jul => 6,
85             Aug => 7,
86             Sep => 8,
87             Oct => 9,
88             Nov => 10,
89             Dec => 11,
90 11     11   58 } ;
  11         54  
91              
92 11     11   53 use constant T => { w => 7*86_400, d => 86_400, h => 60*60, m => 60, s => 1 } ;
  11         19  
  11         26391  
93              
94 0     0 0 0 sub Iterator (&) { $_[0] } ;
95              
96             my %default_sort = (
97             alpha => 1, # 0 implies numeric
98             ascend => 1,
99             fields => [ qw(HOST_NAME) ],
100             ) ;
101            
102             %stime_etime = (
103             last12hours => sub { my $t = defined($_[1]) ? timelocal(@{$_[1]}) : time(); ( $t - 12 * 3_600, $t) },
104             last24hours => sub { my $t = defined($_[1]) ? timelocal(@{$_[1]}) : time(); ( $t - 1 * 86_400, $t) },
105             last7days => sub { my $t = defined($_[1]) ? timelocal(@{$_[1]}) : time(); ( $t - 7 * 86_400, $t) },
106             last31days => sub { my $t = defined($_[1]) ? timelocal(@{$_[1]}) : time(); ( $t - 31 * 86_400, $t) },
107             today => sub {
108             $_[1] ||= [localtime];
109             my $t = timelocal( @{$_[1]} );
110             #( mktime(0, 0, 0, $_[1]->[MDAY], $_[1]->[MON], $_[1]->[YEAR]),
111             (
112             $t - ( $_[1]->[SEC] + 60 *( $_[1]->[MIN] + 60*$_[1]->[HOUR] ) ),
113             $t
114             )
115             },
116             yesterday => sub {
117             $_[1] ||= [localtime] ;
118             my $midnight = timelocal( @{$_[1]} ) - ( $_[1]->[SEC] + 60 *( $_[1]->[MIN] + 60*$_[1]->[HOUR] ) ) ;
119             (
120             $midnight - 86_400,
121             $midnight
122             )
123             },
124             last_n_mins => sub {
125             $_[1] ||= [localtime] ;
126             my ($mins, $t_ar) = @_ ;
127             my $t = timelocal( @$t_ar ) ;
128             (
129             $t - $mins * 60,
130             $t
131             )
132             },
133             last_n_hours => sub {
134             $_[1] ||= [localtime] ;
135             my ($hours, $t_ar) = @_ ;
136             my $t = timelocal(@$t_ar) ;
137             (
138             $t - $hours * 3_600,
139             $t
140             )
141             },
142             last_n_days => sub {
143             $_[1] ||= [localtime] ;
144             my ($days, $t_ar) = @_ ;
145             my $t = timelocal(@$t_ar ) ;
146             # XXX Fails for days > 120.
147             (
148             $t - $days * 86_400,
149             $t,
150             )
151             },
152             thisweek => sub {
153             # thisweek is wrt to MONDAY.
154             $_[1] ||= [localtime] ;
155             my $t = timelocal( @{$_[1]} ) ;
156             (
157             $t - ( $_[1]->[SEC] + 60*($_[1]->[MIN] + 60*$_[1]->[HOUR]) + ($_[1]->[WDAY] - 1)*86_400 ),
158             $t
159             )
160             },
161             lastweek => sub {
162             $_[1] ||= [localtime] ;
163             my $sweek =
164             timelocal( @{$_[1]} ) - ( $_[1]->[SEC] + 60*($_[1]->[MIN] + 60*$_[1]->[HOUR]) + ($_[1]->[WDAY]-1)*86_400 ) ;
165             (
166             $sweek - 7 * 86_400,
167             $sweek - 1
168             )
169             },
170             thismonth => sub {
171             $_[1] ||= [localtime] ;
172             (
173             timelocal( 0, 0, 0, 1, $_[1]->[MON], $_[1]->[YEAR] ),
174             timelocal( @{$_[1]} )
175             )
176             },
177             lastmonth => sub {
178             $_[1] ||= [localtime] ;
179             my ($m, $y) = @{$_[1]}[MON, YEAR] ;
180             (
181             timelocal( 0, 0, 0, 1, ($m - 1) % 12, $y - ($m == 0), ),
182             timelocal( 0, 0, 0, 1, $m , $y ) - 1
183             )
184             },
185             thisyear => sub {
186             $_[1] ||= [localtime] ;
187             (
188             timelocal( 0, 0, 0, 1, 0, $_[1]->[YEAR]),
189             timelocal( @{$_[1]} )
190             )
191             },
192             lastyear => sub {
193             $_[1] ||= [localtime] ;
194             ( timelocal( 0, 0, 0, 1, 0, $_[1]->[YEAR] - 1 ),
195             timelocal( 0, 0, 0, 1, 0, $_[1]->[YEAR] ) -1
196             )
197             },
198             __DEFAULT__ => sub {
199             # The 'tag' or the name of the interval supplied to the constructor
200             # The tag describes the interval either absolutely (eg DD.MM.YY) or
201             # wrt to the second arg (the right edge of the interval).
202             local $_= shift @_ ;
203             # Usually this arg is the upper bound of the interval.
204             # Most often it is [localtime].
205             my $t = @_ ? shift @_ : [localtime] ;
206              
207             my @t = @$t ;
208             my ($t1, $t2) =
209             /^ last(\d+)day /x ? $stime_etime{last_n_days}->( $1, $t ) :
210             /^ last(\d+)hour /x ? $stime_etime{last_n_hours}->( $1, $t ) :
211             /^ last(\d+)min /x ? $stime_etime{last_n_mins}->( $1, $t ) :
212             # Define a time period with a string like so
213             # time: HHMM | HH:MM => from that time today to now
214             # date: DD.MM.YY | DD.MM.YYYY | MM/DD/YYYY | MM/DD/YY
215             # => midnight on that day to now
216             # time date => that time on that day to now
217             # The time and date formats are inspired by the at command.
218             /^ (\d\d) :? (\d\d) $/x
219             ? do { # HHMM | HH:MM
220             @t[SEC, MIN, HOUR] =
221             (0, $2, $1 ) ;
222             ( timelocal(@t), timelocal(@$t) ) ;
223             } :
224             m{^ (\d\d?) [\./] (\d\d?) [\./] ( \d\d (?:\d\d)? ) $}x
225             ? do { # DD.MM.YY(YY)? | MM/DD/YY(YY)?
226             my ($dd, $mm, $yy) = ($1, $2, $3) ;
227             ($mm, $dd) = ($dd, $mm)
228             if m{/} ;
229             @t[SEC, MIN, HOUR, MDAY, MON, YEAR] =
230             (0, 0, 0, $dd, $mm - 1, $yy ) ;
231             ( timelocal(@t), timelocal(@$t) ) ;
232             } :
233             m{^ (\d\d) :? (\d\d) \s+ (\d\d?) [\./] (\d\d?) [\./] ( \d\d (?:\d\d)? ) $ }x
234             ? do { # HHMM | HH:MM DD.MM.YY(YY)? | MM/DD/YY(YY)?
235             my ($h, $m, $dd, $mm, $yy) = ($1, $2, $3, $4, $5) ;
236             ($mm, $dd) = ($dd, $mm)
237             if m{/} ;
238             @t[SEC, MIN, HOUR, MDAY, MON, YEAR] =
239             (0, $m, $h, $dd, $mm - 1, $yy ) ;
240             ( timelocal(@t), timelocal(@$t) ) ;
241             } :
242             m{^ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \s* (\d{4})? }ix
243             ? do {
244             # I'd like to be able to use 'Mar-2006' here but I can't because
245             # the constructor will recognise date ranges (intervals) as strings
246             # containing '-'.
247             my ($mm, $yy, @t1, @t2) ;
248             $mm = MONTH->{"\u$1"} ;
249             $yy = $2 || $t[YEAR] ;
250             @t1[SEC, MIN, HOUR, MDAY, MON, YEAR] =
251             (0, 0, 0, 1, $mm, $yy ) ;
252             @t2[SEC, MIN, HOUR, MDAY, MON, YEAR] =
253             (0, 0, 0, 1, ($mm + 1) % 12, $yy + ($mm == 11) ) ;
254             ( timelocal(@t1), timelocal(@t2) - 1 ) ;
255             } :
256             (999_999, 999_999) ;
257             die "\%stime_etime: non existent tag '$_' - no handler defined for this tag. Outahere."
258             if $t1 == 999_999 && $t2 == 999_999 ;
259             return ($t1, $t2) ;
260             },
261             ) ;
262              
263             my %data_source = (
264             # All these subroutines return a code reference
265             # (a closure encapsulating auth data) that actually gets the data.
266             web_page => WEB_PAGE ? \&gen_web_page :
267             sub { die 'No CLI web browser (lynx or wget) found. Use local_cgi or report bug' },
268             local_cgi => LOCAL_CGI ? \&gen_local_cgi :
269             sub { die 'Nagios availability CGI not found on this host. Use web_page or report bug' },
270             # Return a ref to the subroutine in main:: (the client code)
271             # that will return the CSV data.
272             dev_debug => sub {
273             die "dev_debug source tag must be followed by the name of a callback in main::."
274             unless $_[0] ;
275 11     11   89 no strict 'refs' ;
  11         25  
  11         17476  
276             my $cb = 'main::' . $_[0] ;
277             return \&$cb ;
278             },
279             __DEFAULT__ => sub { die "Outahere: bad source tag '$_[0]'" },
280             ) ;
281              
282             sub new {
283              
284 5     5 1 8867 my ($class, $source_tag, $rpt_period, $tme_period, $rep_type, $pre_filter) = @_ ;
285              
286 5 50       29 my @report_period = $rpt_period ? @$rpt_period : qw(24x7) ;
287 5 50       21 my $time_period = $tme_period ? $tme_period : q(thismonth) ;
288 5 50       17 my $report_type = $rep_type ? q(service) : q(host) ;
289              
290 5 50 33     34 die 'new() called with a pre_filter parm that is _not_ a code ref. Caller ' . join(' ', caller)
291             if $pre_filter and ref($pre_filter) ne 'CODE' ;
292              
293 5   50 20   114 $pre_filter ||= sub { 1 } ;
  20         55  
294              
295 5         12 my $me = {} ;
296 5         12 my $schema = q{} ;
297 5         12 my @fieldnames= () ;
298 5         12 my @fieldnums = () ;
299 5         13 my %fields = () ;
300              
301 5         19 $me->{REPORTS}= {} ;
302 5         16 $me->{REPORT_PERIODS} = [ @report_period ] ;
303             # Date ranges: $time_period =~ start_at_date / \s* - \s*/x end_at_date
304             # start_at_date and end_at_dates look like time stamps.
305 5         11 my ($t1, $t2) ;
306 5 50       27 if ( my ($s, $e) = $time_period =~ /^([^-]+)-([^-]+)$/ ) {
307             # XXX
308 0         0 $s =~ s/\s+$// ;
309 0         0 $e =~ s/^\s+// ;
310 0         0 ($t1, undef) = $stime_etime{__DEFAULT__}->( $s, [ localtime ] ) ;
311 0         0 ($t2, undef) = $stime_etime{__DEFAULT__}->( $e, [ localtime ] )
312             } else {
313 5 50       164 ($t1, $t2) = exists $stime_etime{$time_period} ? $stime_etime{$time_period}->( $time_period, [localtime] )
314             : $stime_etime{__DEFAULT__}->( $time_period, [localtime] )
315             }
316              
317 5         214 my $data ;
318 5 50       42 $source_tag =~ s/dev_debug/dev_debug BOGON_SERVER/
319             if $source_tag =~ /^dev_debug/ ;
320 5         26 ($source_tag, $data) = split /\s+/, $source_tag, 2 ;
321              
322 5 50 33     43 die "new() called with either null source_tag or null parms: \$source_tag: '$source_tag' \$data: '$data'. Caller " .
323             join(' ', caller)
324             unless $source_tag && $data ;
325              
326 5         19 my ($server, $auth) = split /\s+/, $data, 2 ;
327              
328 5 50       22 die "new() called with a source_tag that failed to contain a non null server name. Caller " . join(' ', caller)
329             unless $server ;
330              
331 5         31 $me->{DATA_SOURCE} = $data_source{$source_tag}->(split /\s+/, $auth) ;
332 5         14 $me->{SERVER} = $server ;
333 5         16 $me->{SOURCE_TAG} = $source_tag ;
334 5         11 $me->{REPORT_TYPE} = $report_type ;
335 5         15 $me->{T1} = $t1 ;
336 5         15 $me->{T2} = $t2 ;
337              
338 5         12 foreach my $rep_period ( @report_period ) {
339              
340 5         10 my $user_data ;
341 5 50       22 $user_data = $report_type eq 'host' ? QS_TEMP_HOSTREP : QS_TEMP_SVCREP ;
342 5         75 $user_data =~ s/t1=T1&t2=T2/t1=$t1&t2=$t2/ ;
343 5         40 $user_data =~ s/REPORT_PERIOD/$rep_period/ ;
344 5 0       37 $user_data =
    50          
    50          
345             $me->{SOURCE_TAG} eq 'web_page' ? "http://$me->{SERVER}/nagios/cgi-bin/avail.cgi?$user_data" :
346             $me->{SOURCE_TAG} eq 'dev_debug' ? $rep_period :
347             $me->{SOURCE_TAG} eq 'local_cgi' ? $user_data :
348             '__DEFAULT__' ;
349              
350 5         24 my @avail_rep = $me->{DATA_SOURCE}->($user_data) ;
351              
352 5         856 $schema = shift @avail_rep ;
353 5         27 $schema .= ', AVAIL_URL, TREND_URL' ;
354 5   50     230 $me->{FIELDNAMES} ||= [ @fieldnames = split /,\s+/, $schema ] ;
355 5         241 @fields{@fieldnames}= (0 .. $#fieldnames) ;
356 5   50     146 $me->{FIELDS} ||= { %fields } ;
357              
358 5         20 my @avail_report = () ;
359              
360 5         12 local $_ ;
361              
362 5         14 foreach (@avail_rep) {
363 20         528 my @vals = split /,\s+/ ;
364              
365 20         63 my $host = $vals[$fields{HOST_NAME}] ;
366 20         24 my $svc ;
367 20         72 $host =~ s/"//g ;
368 20         40 $vals[$fields{HOST_NAME}] = $host ;
369 20 50       51 if ( $report_type eq 'service' ) {
370 0         0 $svc = $vals[$fields{SERVICE_DESCRIPTION}] ;
371 0         0 $svc =~ s/"//g ;
372 0         0 $vals[$fields{SERVICE_DESCRIPTION}] = $svc ;
373             }
374              
375 20 50       50 my $avail_url = $report_type eq 'host' ? AVAIL_URL_TEMP_HOST : AVAIL_URL_TEMP_SVC ;
376 20         86 $avail_url =~ s/HOST/$host/ ;
377 20 50       54 $avail_url =~ s/SVC/$svc/
378             if $report_type eq 'service' ;
379 20         222 $avail_url =~ s/t1=T1&t2=T2/t1=$t1&t2=$t2/ ;
380 20         81 $avail_url =~ s/SERVER/$me->{SERVER}/ ;
381 20         41 $vals[$fields{AVAIL_URL}] = $avail_url ;
382              
383 20 50       46 my $trend_url = $report_type eq 'host' ? TREND_URL_TEMP_HOST : TREND_URL_TEMP_SVC ;
384 20         60 $trend_url =~ s/HOST/$host/ ;
385 20 50       48 $trend_url =~ s/SVC/$svc/
386             if $report_type eq 'service' ;
387 20         125 $trend_url =~ s/t1=T1&t2=T2/t1=$t1&t2=$t2/ ;
388 20         79 $trend_url =~ s/SERVER/$me->{SERVER}/ ;
389 20         39 $vals[$fields{TREND_URL}] = $trend_url ;
390              
391 20         21 my %F ;
392 20         340 @F{@fieldnames} = @vals ;
393              
394             next
395 20 50       172 unless $pre_filter->(%F) ;
396              
397 20         328 push @avail_report, [ @vals ] ;
398             }
399              
400 5         39 $me->{AVAIL_REPORTS}{$rep_period} = [ @avail_report ] ;
401              
402             }
403              
404 5   33     104 bless $me, ref($class) || $class ;
405              
406             }
407              
408             # Runs while object 'loads'
409             # Creates accessors for the callers convenience.
410              
411             # XXX
412             # This class will _not_ use the accessors itself
413             # since it doesn't believe it will be inherited from.
414              
415             foreach my $acc (qw(FIELDS FIELDNAMES REPORT_PERIODS T1 T2 SERVER AVAIL_REPORTS REPORTS DATA_SOURCE SOURCE_TAG REPORT_TYPE)) {
416 11     11   81 no strict 'refs' ;
  11         19  
  11         55599  
417             *$acc = sub {
418 0     0   0 my $me = shift @_ ;
419 0 0       0 $me->{$acc} = @_
420             if @_ ;
421 0         0 $me->{$acc} ;
422             }
423             }
424              
425             sub mkreport {
426 0     0 1 0 my ($me, $these_fields, $select_these, $this_order, $alter, $add_downs) = @_ ;
427              
428             # $these_fields: fields which will appear in output
429             # in the same order as specified (in
430             # the array pointed to).
431             # $select_these: callback specifying which records
432             # to report on.
433             # $this_order : callback for sort to order the
434             # records.
435             # $alter : callback to add fields to or other
436             # wise mangle a record. Should return
437             # the names of any added fields.
438             # $add_downs : duplicate the rec by the
439             # number of outage recs, appending
440             # time down, up and outage duration.
441              
442             # XXX
443             # $these_fields should specify the names and orders
444             # of any fields added by $alter->().
445              
446 0         0 my $usage =<<'USAGE' ;
447              
448             mkreport( $these_fields, $select_these, $this_order, $alter, $add_downs )
449              
450             $these_fields := optional array ref enumerating the fields, and there order to appear in the report.
451             $select_these := optional callback returning true if the availability record is to appear in the report. Called
452             with pairs of all fields and values for this record.
453             $this_order := optional callback specifiying sort order. Called with pairs of all fieldnames and their offsets
454             $alter := optional callback that munges fields (transforming field values or adding new fields).
455             $add_downs := optional flag. If set, each availability record is duplicated by the number of outage records,
456             and the time down, time up and duration of the outage is appended to each record.
457              
458             USAGE
459              
460 0 0 0     0 die $usage
461             if $these_fields and ref($these_fields) ne 'ARRAY' ;
462              
463 0 0 0     0 die $usage
464             if $select_these and ref($select_these) ne 'CODE' ;
465              
466 0 0 0     0 die $usage
467             if $this_order and ref($this_order) ne 'CODE' ;
468              
469 0 0 0     0 die $usage
470             if $alter and ref($alter) ne 'CODE' ;
471              
472 0   0     0 $these_fields ||= [] ;
473 0   0 0   0 $select_these ||= sub { 1 } ;
  0         0  
474 0   0 0   0 $this_order ||= sub { my %f = @_ ; $a->[$f{HOST_NAME}] cmp $b->[$f{HOST_NAME}] } ;
  0         0  
  0         0  
475              
476 0 0       0 my @field_names = scalar(@$these_fields) ? @$these_fields : @{ $me->{FIELDNAMES} } ;
  0         0  
477 0         0 my @fieldnames = @{ $me->{FIELDNAMES} } ;
  0         0  
478 0         0 my %fields = %{ $me->{FIELDS} } ;
  0         0  
479              
480 0         0 $me->{REPORTS}{$_}{RECORDS} = []
481 0         0 foreach keys %{ $me->{AVAIL_REPORTS} } ;
482              
483             # map
484             # slice
485             # sort
486             # filter
487             # alter
488             # add_downs list
489              
490             # Do the transforms - to add
491             # fields - first, and then
492             # update the fieldnames so
493             # the new fields can be used
494             # to select and sort.
495              
496 0         0 foreach my $rep ( keys %{ $me->{AVAIL_REPORTS} } ) {
  0         0  
497              
498 0         0 my (%F, @r, @avail_recs, $avail_url) ;
499              
500 0         0 @avail_recs = @{ $me->{AVAIL_REPORTS}{$rep} } ;
  0         0  
501              
502             next
503 0 0       0 unless @avail_recs ;
504              
505 0         0 @r = shift @avail_recs ;
506              
507 0 0       0 $add_downs = 0
508             unless $me->{SOURCE_TAG} =~ /^(?:web_page|local_cgi)/ ;
509              
510 0 0       0 if ( $add_downs ) {
511 0         0 my @ofn = qw(DOWN UP OUTAGE) ;
512 0         0 push @fieldnames, @ofn ;
513             # The new fields are added to the list
514             # of those appearing in the report
515 0 0       0 push @field_names, @ofn
516             unless grep $_ eq $ofn[0], @field_names ;
517 0         0 @r =
518 0         0 map { get_downs($_, $me->{DATA_SOURCE}, $_->[$fields{AVAIL_URL}], $me->{T1}) } shift @r ;
519             }
520              
521 0 0       0 if ( $alter ) {
522 0         0 my @downs = @r ;
523 0         0 @F{@fieldnames} = @{$r[0]} ;
  0         0  
524             # XXX - don't modify the rec, only get the added fields.
525 0         0 my @afn = $alter->(\%F) ;
526             # my @afn = $alter->($r[0], \%F) ;
527             # my @afn = $alter->($r[0], %F) ;
528 0         0 push @fieldnames, @afn ;
529 0 0       0 push @field_names, @afn
530             unless grep $_ eq $afn[0], @field_names ;
531 0         0 @r =
532 0         0 map { @F{@fieldnames} = @$_; $alter->(\%F); [ @F{@fieldnames} ] } @downs ;
  0         0  
  0         0  
533             }
534              
535 0         0 $me->{FIELDNAMES} = [ @fieldnames ] ;
536 0         0 @fields{@fieldnames}= 0 .. $#fieldnames ;
537 0         0 $me->{FIELDS} = { %fields } ;
538              
539             # Transform rec by adding or munging fields
540             # NB, the one attempt I made to move the
541             # tests outside map actually slowed the benchmark
542             # 'make tests'.
543 0         0 push @r,
544             map {
545 0 0       0 @F{@fieldnames} = @$_ ;
546 0 0       0 $alter->(\%F)
547             if $alter ;
548 0         0 [ @F{@fieldnames} ]
549             }
550             map {
551 0         0 $add_downs
552             ? get_downs($_, $me->{DATA_SOURCE}, $_->[$fields{AVAIL_URL}], $me->{T1})
553             : $_
554             }
555             @avail_recs ;
556              
557             # Slice
558 0         0 my @rep = map [ @{$_}[@fields{@field_names}] ],
  0         0  
559             # Sort
560 0         0 sort { $this_order->(%fields) }
561             # Filter
562 0         0 grep { @F{@{$me->{FIELDNAMES}}} = @$_; $select_these->(%F) }
  0         0  
  0         0  
563             @r ;
564              
565 0         0 $me->{REPORTS}{$rep}{RECORDS} = [ @rep ] ;
566 0         0 $me->{REPORTS}{$rep}{FIELDNAMES} = [ @field_names ] ;
567             }
568              
569             }
570              
571              
572             sub excel_dump {
573 0     0 1 0 my ($me, $excel_filename, $chart_details) = @_ ;
574              
575 0         0 eval { require Spreadsheet::WriteExcel } ;
  0         0  
576 0 0       0 die "John McNamara's _excellent_ CPAN module, Spreadsheet::WriteExcel is needed by excel_dump(). Outahere. "
577             if $@ ;
578              
579 0   0     0 my $workbook = Spreadsheet::WriteExcel->new($excel_filename || EXCEL_FILENAME) ;
580 0 0       0 die "Spreadsheet::WriteExcel constructor failed, prob opening '$excel_filename': $!"
581             unless $workbook ;
582              
583 0         0 my $format = $workbook->add_format() ;
584 0         0 $format->set_bold() ;
585              
586 0         0 foreach my $rep (@{ $me->{REPORT_PERIODS} }) {
  0         0  
587              
588 0         0 my $down_times = $me->{REPORTS}{$rep}{RECORDS} ;
589              
590 0         0 my $worksheet = $workbook->addworksheet($rep) ;
591              
592 0         0 my @max_col_width = () ;
593             # The general syntax is write($row, $column, $token).
594             # Note that row and column are zero indexed
595 0         0 my ($row, $col) = (0, 0) ;
596             # XXX
597             # Scan all data to determine widest column.
598             # The column width needs to be set before any
599             # cells are written.
600              
601 0         0 foreach my $c ( @{ $me->{REPORTS}{$rep}{FIELDNAMES} } ) {
  0         0  
602 0         0 $max_col_width[$col++] = 1.5 * length($c) ;
603             # Fieldnames are bold so are wider than usual.
604             }
605              
606 0         0 foreach my $r (@$down_times) {
607 0         0 $col = 0 ;
608 0         0 foreach my $c (@$r) {
609 0         0 my $h ;
610             # XXX
611             # Set col width to width of label _not_ the URL.
612              
613 0 0       0 my $len = (($h) = $c =~ m#^http://.+?host=([^&]+)&#) ? length($h) : length($c) ;
614 0 0       0 $max_col_width[$col] = $len
615             if $len >= $max_col_width[$col] ;
616 0         0 $col++ ;
617             }
618             }
619              
620 0         0 $col = 0 ;
621              
622 0         0 foreach my $c ( @{ $me->{REPORTS}{$rep}{FIELDNAMES} } ) {
  0         0  
623 0         0 $worksheet->set_column($col, $col, $max_col_width[$col]) ;
624 0         0 $worksheet->write(0, $col, $c, $format) ;
625 0         0 $col++
626             }
627            
628 0         0 $row = 1 ;
629              
630 0         0 foreach my $r (@$down_times) {
631 0         0 $col = 0 ;
632 0         0 foreach my $c ( @$r ) {
633 0 0       0 if ( my ($h) = $c =~ m#^http://.+?host=([^&]+)&# ) {
634 0 0       0 $worksheet->write_url($row, $col, $c, $h ? $h : "URL without hostname: $c") ;
635             } else {
636 0         0 $worksheet->write($row, $col, $c)
637             }
638 0         0 $col++ ;
639             }
640 0         0 $row++ ;
641             }
642             }
643              
644 0 0       0 &add_chart($workbook, $chart_details)
645             if $chart_details ;
646              
647             }
648              
649              
650             sub csv_dump {
651 0     0 1 0 my $me = shift @_ ;
652              
653 0         0 foreach my $rep (@{ $me->{REPORT_PERIODS} }) {
  0         0  
654              
655 0         0 print "==> $rep reporting period.\n" ;
656              
657 0         0 my $down_times = $me->{REPORTS}{$rep}{RECORDS} ;
658              
659 0         0 print
660 0         0 join(',', @{ $me->{REPORTS}{$rep}{FIELDNAMES} }), "\n" ;
661              
662 0         0 foreach my $r ( @{$down_times} ) {
  0         0  
663 0         0 print
664             join(',', @$r), "\n" ;
665             }
666              
667 0         0 print "\n\n" ;
668              
669             }
670             }
671              
672             sub debug_dump {
673 0     0 0 0 my ($me, $field_width, $fields_per_line) = @_ ;
674              
675 0   0     0 $field_width ||= 15 ;
676 0   0     0 $fields_per_line||= 7 ;
677              
678 0         0 my $format = 0 ;
679              
680 0         0 foreach my $rep (@{ $me->{REPORT_PERIODS} }) {
  0         0  
681              
682             format STDOUT_TOP =
683              
684             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
685             $rep
686              
687             .
688              
689 0         0 $- = 0 ;
690              
691 0         0 my @line ;
692 0         0 @line = map { my $x = $_ ; $x =~ s/PERCENT/%/; $x }
  0         0  
  0         0  
  0         0  
693 0         0 @{ $me->{REPORTS}{$rep}{FIELDNAMES} } ;
694              
695 0 0       0 &mkform($field_width, $fields_per_line, @line)
696             unless $format++ ;
697 0         0 &do_write(@line) ;
698              
699 0         0 foreach my $r ( @{ $me->{REPORTS}{$rep}{RECORDS} } ) {
  0         0  
700 0         0 @line = @$r ;
701 0         0 &do_write(@line) ;
702             }
703             }
704             }
705              
706             {
707             my @base = (
708             { b => 60, u => 's' },
709             { b => 60, u => 'm' },
710             { b => 24, u => 'h' },
711             { b => 7, u => 'd' },
712             { b => 4, u => 'w' },
713             { b => 12, u => 'm' },
714             ) ;
715              
716             sub time2ddhhmmss {
717             # Rounds arg to nearest 5 minute interval
718             # and returns hhmmss string.
719 23     23 0 6349 my $d = shift @_ ;
720              
721 23         28 $d += 150 ;
722 23         38 $d = int($d/300) * 300 ;
723              
724 23 100       60 return ' '
725             unless $d ;
726              
727 21         22 my ($s, $r, $i) ;
728 21         21 $s = ' ' ;
729              
730 21         43 for ($i = 0; $d > 0; $i++) {
731 80         97 $r = $d % $base[$i]{b} ;
732 80 100       156 $s = "${r}$base[$i]{u} $s"
733             if $r > 0 ;
734 80         170 $d = ($d - $r) / $base[$i]{b} ;
735             }
736              
737 21         220 $s ;
738              
739             }
740              
741             }
742              
743             sub mkform {
744              
745 0     0 0 0 my ($field_width, $fields_per_line) = @_ ;
746              
747 0         0 my ($pics, $vals, $f1, $f, $form_body) ;
748              
749 0         0 $f1 = '@' . '<' x 24 ;
750 0         0 $f = '@' . '<' x $field_width ;
751              
752 0         0 $pics = $f1 . " $f" x ($fields_per_line - 1) ;
753 0         0 $vals = 'shift(@_), ' x $fields_per_line ;
754 0         0 $form_body = "format STDOUT = \n" ;
755              
756 0         0 $form_body .= <
757             $pics
758             $vals
759             EOFORM
760              
761 0         0 my $fields_remaining = @_ - $fields_per_line ;
762              
763 0 0       0 if ( $fields_remaining <= 0 ) {
764 0         0 $form_body .= <
765             .
766             EOFORM
767              
768 0         0 eval $form_body ;
769              
770             } else {
771 0         0 my $lines_remaining = int($fields_remaining / ($fields_per_line - 1)) ;
772 0         0 my $fields_on_last_line = $fields_remaining % ($fields_per_line - 1) ;
773 0         0 my $spaces = ' ' x 25 ;
774              
775 0         0 $pics = $spaces . " $f" x ($fields_per_line - 1) ;
776 0         0 $vals = $spaces . ' shift(@_), ' x ($fields_per_line - 1) ;
777              
778 0         0 $form_body .= <
779             $pics
780             $vals
781             EOFORM
782              
783 0         0 $pics = $spaces . " $f" x $fields_on_last_line ;
784 0         0 $vals = $spaces . ' shift(@_), ' x $fields_on_last_line ;
785              
786 0         0 $form_body .= <
787             $pics
788             $vals
789             .
790             EOFORM
791              
792 0         0 eval $form_body ;
793             }
794              
795             }
796              
797             sub do_write {
798             # Only exists to get vals into @_ ;
799 0     0 0 0 write ;
800             }
801              
802             sub max_l {
803             # my $max = shift @_ or
804             return undef
805 7 100   7 1 2896 unless @_ ;
806              
807 6         8 my $max = shift @_ ;
808              
809 6         7 local $_ ;
810 6         16 while ( defined($_ = shift @_) ) {
811 30 100       66 $max = $_
812             if $_ >= $max
813             }
814              
815 6         66 $max ;
816              
817             }
818              
819             sub comp {
820             # Returns a comparator function to sort the avail records
821              
822             # Call with (alpha => 0|1, ascend => 0|1, fields =. [ f1.. ])
823              
824 0     0 1 0 my %sort_parms = (%default_sort, @_) ;
825              
826             # eg
827             # alpha => 0, $ascend => 1, fields = [TOTAL_TIME_DOWN, TOTAL_TIME_UNREACH]
828             # TIME_DOWN and TIME_UNREACH are the same to the reader of the report;
829             # they should be consolidated with some consolidation function (eg max
830             # or sum of values).
831              
832 0         0 my $r ;
833              
834 0 0       0 if ( $sort_parms{alpha} ) {
835             $r = $sort_parms{ascend}
836             ? sub {
837 0     0   0 my %f = @_ ;
838              
839             # package Nagios::Report ;
840 0         0 $a->[$f{HOST_NAME}] cmp $b->[$f{HOST_NAME}]
841             }
842             : sub {
843 0     0   0 my %f = @_ ;
844              
845             # package Nagios::Report ;
846 0         0 $b->[$f{HOST_NAME}] cmp $a->[$f{HOST_NAME}]
847 0 0       0 } ;
848 0         0 return $r ;
849             } else {
850 0         0 my $fields = $sort_parms{fields} ;
851             $r = $sort_parms{ascend}
852             ? sub {
853 0     0   0 my %f = @_ ;
854              
855 0         0 my @field_indices = @f{@$fields} ;
856              
857 0         0 my $a_max = &max_l( @{$a}[@field_indices] ) ;
  0         0  
858 0         0 my $b_max = &max_l( @{$b}[@field_indices] ) ;
  0         0  
859 0         0 $a_max <=> $b_max ;
860             }
861             : sub {
862 0     0   0 my %f = @_ ;
863              
864 0         0 my @field_indices = @f{@$fields} ;
865              
866 0         0 my $a_max = &max_l( @{$a}[@field_indices] ) ;
  0         0  
867 0         0 my $b_max = &max_l( @{$b}[@field_indices] ) ;
  0         0  
868 0         0 $b_max <=> $a_max ;
869 0 0       0 } ;
870 0         0 return $r ;
871             }
872            
873             }
874              
875             sub gen_local_cgi {
876 0     0 0 0 my $user = shift @_ ;
877             # Generate a closure that will get an
878             # availability report by running the CGI
879             # from the shell.
880 0 0       0 die "gen_local_cgi() called without username for access to local availability CGI: \$user: '$user'. Caller "
881             . join(' ', caller)
882             unless $user ;
883              
884 0         0 $ENV{REMOTE_USER} = $user ;
885 0         0 $ENV{REQUEST_METHOD} = REQUEST_METHOD ;
886              
887             return sub {
888 0     0   0 my $url = shift @_ ;
889             # Must convert URL to query_string
890 0         0 my $qs ;
891 0         0 ($qs = $url) =~ s|http:.*?\?|| ;
892              
893 0         0 $ENV{QUERY_STRING} = $qs ;
894 0         0 my @x = `${\NAG_AVAIL_CGI}` ;
  0         0  
895             # Drop HTTP headers
896 0         0 splice @x, 0, 6 ;
897 0         0 chomp @x ;
898 0 0       0 wantarray ? @x : join('', @x) ;
899 0         0 } ;
900              
901             }
902              
903             sub gen_web_page {
904 0     0 0 0 my ($user, $pass) = @_ ;
905              
906 0 0 0     0 die "gen_web_page() called without username or password for web page access: \$user: '$user', \$pass: '$pass'."
907             . ' Caller ' . join(' ', caller)
908             unless $user && $pass ;
909              
910             return sub {
911             # lynx -dump will render the page as text
912             # regardless of its content.
913 0     0   0 USE_LYNX ? `${\LYNX} -nolist -dump -width=1000 -auth=$user:$pass '$_[0]'`
914 0         0 : `${\WGET} --output-document=- --http-user=$user --http-passwd=$pass '$_[0]' 2>/dev/null` ;
915 0         0 } ;
916             }
917              
918             sub down_records {
919 0     0 0 0 my ($data_source, $avail_url, $start_time) = @_ ;
920              
921 0         0 my $avail_rep = $data_source->($avail_url) ;
922              
923 0         0 my ($downs, @down_recs, @downs) ;
924              
925 0 0       0 if ( $avail_rep =~ //i ) {
926             # XXX
927             # Will fail if the availability CGI contains
928             # different tags
929             #
930             # 01-11-2005 00:00:00
931             # 09-11-2005 15:09:59
932             # 8d 15h 9m 59s
933             # HOST UP (HARD)
934              
935 0         0 ($downs) = $avail_rep =~ /(
936 0         0 my @rows = $downs =~ m|()|g ;
937 0         0 @down_recs = map { s/<.*?>/ /g; $_ } @rows ;
  0         0  
  0         0  
938             } else {
939             # Parsed HTML (by lynx -dump) looks like
940             # 01-11-2005 00:00:00 09-11-2005 15:09:59 8d 15h 9m 59s
941             # HOST UP (HARD)
942 0         0 ($downs) = $avail_rep =~ m|( \d+-\d+-\d+ \s+ \d+:\d+:\d+ \s+ \d+-\d+-\d+ \s+ \d+:\d+:\d+ .* \Z) |msx ;
943 0         0 @down_recs = split /\n/, $downs ;
944             }
945              
946 0         0 local $_ ;
947              
948 0         0 foreach (@down_recs) {
949              
950             =begin comment
951              
952             # Must also deal with scheduled downtime.
953              
954             01-02-2006 00:00:00 01-02-2006 14:27:54 0d 14h 27m 54s HOST UP (HARD) PING OK - Packet loss = 0%, RTA = 7.26 ms
955             02-02-2006 20:24:42 02-02-2006 20:24:42 0d 0h 0m 0s HOST DOWN (HARD) CRITICAL - Plugin timed out after 10 seconds
956              
957             VVVV This appears to be the correct entry
958             02-02-2006 20:24:42 02-02-2006 20:47:54 0d 0h 23m 12s HOST DOWNTIME START Start of scheduled downtime
959             ^^^^
960             02-02-2006 20:47:54 02-02-2006 22:24:42 0d 1h 36m 48s HOST UP (HARD) PING OK - Packet loss = 0%, RTA = 0.66 ms
961             02-02-2006 22:24:42 03-02-2006 12:54:29 0d 14h 29m 47s HOST DOWNTIME END End of scheduled downtime
962              
963             =end comment
964              
965             =cut
966              
967             next
968 0 0       0 if /HOST UP|SERVICE OK|HOST DOWNTIME END/ ;
969              
970 0         0 my ($down, $up, $outage) = /
971             (\d+-\d+-\d+ \s+ \d+:\d+:\d+) \s+
972             (\d+-\d+-\d+ \s+ \d+:\d+:\d+) \s+
973             ( (?:\d+[wdhms] \s*)+ )
974             /x ;
975              
976             next
977 0 0       0 if &before_start($down, $start_time) ;
978              
979 0         0 $outage =~ s/^(?:0[wdh] )*//g ;
980              
981 0         0 push @downs, [$down, $up, $outage] ;
982             }
983              
984 0 0       0 @downs = ( [ (' ') x 3 ] )
985             unless @downs ;
986             @downs
987              
988 0         0 }
989              
990             sub before_start {
991 3     3 0 1090 my ($t1_str, $t2) = @_ ;
992 3         8 my $t1 = &date2time($t1_str) ;
993              
994 3 100       214 return $t1 < $t2 ? 1 : 0 ;
995              
996             }
997              
998             sub date2time {
999 3     3 0 4 local $_ = shift @_ ;
1000              
1001 3         4 my ($dd, $mm, $mon, $yyyy, $h, $m, $s) ;
1002              
1003 3 50       28 if ( ($dd, $mm, $yyyy, $h, $m, $s) = m< (\d+)-(\d+)-(\d+) \s+ (\d+):(\d+):(\d+) >x ) {
    0          
    0          
1004             # 01-11-2005 00:00:00
1005              
1006             # DD-MM-YYYY HH:MM:SS Euro style dates with day first
1007 3         15 return EURO_DATE ? timelocal($s, $m, $h, $dd, $mm - 1, $yyyy)
1008             # MM-DD-YYYY HH:MM:SS US style dates with month first
1009             : timelocal($s, $m, $h, $mm, $dd - 1, $yyyy)
1010              
1011             } elsif ( ($mon, $dd, $h, $m, $s, $yyyy) = /\w{3} (\w{3})\s+(\d+) (\d+):(\d+):(\d+) \w{3} (\d{4})/ ) {
1012             # Tue Nov 29 20:19:17 EST 2005
1013 0           $mon = "\u\L$mon" ;
1014 0           $mm = MONTH->{$mon} ;
1015 0           return timelocal($s, $m, $h, $dd, $mm - 1, $yyyy) ;
1016             } elsif ( /now/ ) {
1017 0           return time() + 0 ;
1018             } else {
1019              
1020             }
1021              
1022             }
1023              
1024             sub interval2time {
1025 0     0 0   local $_ = shift @_ ;
1026 0           my $t = 0 ;
1027 0           foreach my $x ( /(\d+[wdhms])/g ) {
1028             # XXX
1029             # Would be nice to do this without two matches but
1030             # /g only counts the first match not the pair.
1031 0           my ($u, $b) = $x =~ /(\d+)([wdhms])/ ;
1032 0           $t += T->{$b} * $u ;
1033             }
1034              
1035 0           return $t ;
1036              
1037             }
1038              
1039             sub get_downs {
1040 0     0 0   my $r = shift @_ ;
1041              
1042 0           my @downs = &down_records(@_) ;
1043 0           my @r = map [ @$r, @$_ ], @downs ;
1044 0           @r ;
1045             }
1046              
1047             sub avail {
1048 0     0 1   my ($me, $rep_period) = @_ ;
1049 0   0       $rep_period ||= '24x7' ;
1050 0 0         die "Non existent report period '$rep_period': choose from " . join(' ', keys %{$me->{AVAIL_REPORTS}}) . '. Outahere.'
  0            
1051             unless exists $me->{AVAIL_REPORTS}{$rep_period} ;
1052              
1053 0           my @avail_recs = @{ $me->{AVAIL_REPORTS}{$rep_period} } ;
  0            
1054             return wantarray
1055             ? @avail_recs
1056 0 0   0     : Iterator { shift @avail_recs ; } ;
  0            
1057             }
1058              
1059             sub report {
1060 0     0 1   my ($me, $rep_period) = @_ ;
1061 0   0       $rep_period ||= '24x7' ;
1062              
1063 0 0         die "Non existent report period '$rep_period': choose from " . join(' ', keys %{$me->{REPORTS}}) . '. Outahere.'
  0            
1064             unless exists $me->{REPORTS}{$rep_period} ;
1065              
1066 0           my @report_recs = @{ $me->{REPORTS}{$rep_period}{RECORDS} } ;
  0            
1067             return wantarray
1068             ? @report_recs
1069 0 0   0     : Iterator { shift @report_recs ; } ;
  0            
1070             }
1071              
1072              
1073             sub to_dbh {
1074 0     0 1   my $me = shift @_ ;
1075              
1076 0           eval { require DBI } ;
  0            
1077 0 0         die "Jeff Zuckers's _excellent_ CPAN module, DBI is needed by to_dbh(). Outahere. "
1078             if $@ ;
1079              
1080 0           my @dbh = () ;
1081             # Fruitless attempts to silence DBI -
1082             # this appears to be a known issue of DBD::AnyData
1083             # as of Mar 2006.
1084              
1085             # local $SIG{__WARN__} = sub {} ;
1086              
1087 0           foreach my $rep_period ( @{$me->REPORT_PERIODS} ) {
  0            
1088              
1089 0           my $dbh = DBI->connect('dbi:AnyData:') ;
1090             # my $dbh = DBI->connect('dbi:AnyData(Warn => 0, ...
1091             # Also fails to check warnings.
1092 0           $dbh->func(
1093             "tab_${rep_period}",
1094             'ARRAY',
1095             $me->{AVAIL_REPORTS}{$rep_period},
1096             # [ $me->avail($rep_period) ],
1097 0           { col_names => join(',', @{$me->FIELDNAMES}) },
1098             'ad_import'
1099             ) ;
1100              
1101 0           push @dbh, $dbh ;
1102              
1103             }
1104              
1105 0           @dbh ;
1106              
1107             }
1108              
1109             sub add_chart {
1110 0     0 0   my ($wkbook, $chart) = @_ ;
1111              
1112 0 0         die "add_chart() called without hash ref to chart details eg { template => '/path/to/chart/template', link => .. }. Outahere."
1113             unless ref($chart) eq 'HASH' ;
1114              
1115 0 0 0       die "add_chart() called without a well formed 'template' value in \$chart_detail. Must be to a filename containing the Chart template. Outahere."
      0        
      0        
1116             unless exists($chart->{template}) && $chart->{template} && -e $chart->{template} && -s $chart->{template} ;
1117              
1118             # Add some extra formats to cover formats used in the charts.
1119              
1120 0           $wkbook->add_format(color => 1, bold => 1);
1121 0           $wkbook->add_format(color => 2);
1122 0           $wkbook->add_format(color => 3);
1123              
1124 0   0       $chart->{title} ||= 'Chart 24x7' ;
1125 0   0       $chart->{link} ||= '=24x7!A1' ;
1126              
1127 0           $wkbook->add_chart_ext($chart->{template}, $chart->{title}) ;
1128              
1129 0           $wkbook->sheets(0)->store_formula($chart->{link}) ;
1130              
1131             }
1132            
1133             1 ;
1134              
1135             # ---> That's all folks <-----
1136              
1137             =head1 NAME
1138              
1139             Nagios::Report - Perl class to filter and munge Nagios availability data
1140              
1141             =head1 SYNOPSIS
1142              
1143             use Nagios::Report ;
1144              
1145             my $x = Nagios::Report->new(q, [ '24x7' ], 'thismonth')
1146             or die "Can't construct Nagios::Report object." ;
1147              
1148             my @these_fields = qw(
1149             HOST_NAME
1150             PERCENT_TOTAL_TIME_UP
1151             TOTAL_TIME_DOWN
1152             TIME_DOWN_HHMMSS
1153             TOTAL_TIME_UNREACHABLE
1154             TIME_UNREACH_HHMMSS
1155             AVAIL_URL
1156             TREND_URL
1157             ) ;
1158              
1159             $x->mkreport(
1160             # Field selector; display these fields only (in the listed order)
1161              
1162             # [] means display all the fields.
1163              
1164             \@these_fields,
1165             # Record selector
1166              
1167             # Called with @_ loaded # with a list of field names and
1168             # their vals for this record. Usually copied to a hash
1169             # so it can be used as one.
1170              
1171             # All records
1172             # sub { 1 },
1173             # All records whose HOST_NAME starts with 'Alb'
1174             # sub { my %F = @_; my $h = $F{HOST_NAME}; $h =~ /^Alb/ },
1175             # Regrettably, this is _NOT_ the same since
1176             # @_ can't be used as a hash.
1177             # sub { $_{HOST_NAME} =~ /^Alb/ }
1178             # All records with an up time percent < 98%
1179              
1180             sub { my %F = @_; my $u = $F{PERCENT_TOTAL_TIME_UP}; $u =~ s/%//; $u < 98 },
1181              
1182             # Sort order
1183              
1184             &comp( alpha => 0, ascend => 0, fields => [ qw(TOTAL_TIME_DOWN TOTAL_TIME_UNREACHABLE) ]),
1185              
1186             # Sorts descending by max of TOTAL_TIME_DOWN and TOTAL_TIME_UNREACHABLE
1187              
1188             # DIY sorters remember that $a and $b _must_ be in Nagios::Report package.
1189             # eg by TOTAL_DOWN_TIME descending.
1190             # sub { my %f = @_ ;
1191             # package Nagios::Report;
1192             # $b->[$f{TOTAL_TIME_DOWN}] <=> $a->[$f{TOTAL_TIME_DOWN}]
1193             # },
1194             # Same as
1195             # &comp(alpha => 0, ascend => 0, fields => ['TOTAL_TIME_DOWN'])
1196             # Same but harder,
1197             # sub { package Nagios::Report; $b->[16] <=> $a->[16] },
1198              
1199             # Optional callback to add or mangle fields.
1200              
1201             # Add 2 fields for downtime vals in hours minutes and secs.
1202              
1203             sub { $F = shift @_;
1204             $F->{TIME_DOWN_HHMMSS} = t2hms( $F->{TOTAL_TIME_DOWN} ),
1205             $F->{TIME_UNREACH_HHMMSS}= t2hms( $F->{TOTAL_TIME_UNREACHABLE} ) ;
1206             qw(TIME_DOWN_HHMMSS TIME_UNREACH_HHMMSS)
1207             }
1208              
1209             ) ;
1210              
1211             $x->debug_dump ;
1212             # $x->csv_dump ;
1213              
1214              
1215              
1216             =head1 DESCRIPTION
1217              
1218             Gets the Nagios (http://wwww.Nagios.ORG/) B availability report (getting the results in CSV format)
1219             and applies grep like filters, map like munging, and slice like field masks to produce a report which can be output in
1220             various ways.
1221              
1222             This class provides extra control over the content and disposition of the data produced by the
1223             Nagios availability CGI, by writing for example a spreadsheet containing the selected data.
1224              
1225             Since the data originates from standard Nagios availability CGI, its results are no more accurate - and should be
1226             exactly the same - as that CGI.
1227              
1228              
1229             =head1 METHODS
1230              
1231             =over 4
1232              
1233             =item * new (DATA_SOURCE, REPORT_PERIODS, TIME_PERIOD, HOST_OR_SERVICE, PRE_FILTER)
1234              
1235              
1236             This is the constructor of the Nagios::Report object.
1237              
1238             C is one of
1239              
1240             1 local_cgi - get the data by running the availability report CGI on the local host.
1241             Space separated values of the Nagios web server name/address and a Nagios user should follow.
1242              
1243             2 web_page - get the data with LYNX or WGET from the named web server with the credential.
1244             Space separated values of the Nagios web server name/address, a Nagios user and that users password
1245             should follow.
1246              
1247             3 dev_debug - get __development__ data by running a client supplied callback.
1248             The _name_ of the callback should follow the tag, separated by spaces.
1249             NB the callback is assumed to be in the __main__ package.
1250             The callback is expected to return a string consisting of a schema
1251             (of CSV fieldnames) followed by lines of CSV data.
1252              
1253             C is an optional reference to a list of names of Nagios time periods (conventionally defined in timeperiods.cfg) for which the availability data will be computed (by the CGI).
1254              
1255             C is an optional specification of the interval containing eligible availability records. It is scalar whose value
1256             is one of the Nagios interval names such as C, C, or B of the time forms used by the B command.
1257             (These forms include HHMM, HH:MM, DD.MM.YYYY MM/DD/YYYY and 24hour-time date). It can also be a month abbreviation followed by
1258             any amount of white space and an optional 4 digit year. This form, like Mar 2006, selects all the days in that month. If the year
1259             is missing, the month refers to this year.
1260              
1261              
1262             Usually the timeperiod specifies an interval from some time in the past to now from which availability data will be selected.
1263              
1264              
1265             However, if the argument is of the form B B<-> B, the availability data will be extracted from
1266             the corresponding interval.
1267              
1268              
1269             If this argument is omitted, the report is compiled for the B time period (ie any host availability record from the first of the current
1270             month to the current time).
1271              
1272             C is an optional scalar specifying the service report instead of the host report. If not set, the host
1273             report is produced.
1274              
1275             C is a callback that is called with the B<%F> hash (vi) set to the values of the field names for this availability
1276             record. The constructor saves the availability report for B the hosts and therefore if mkreport() then
1277             requests the down records (vi), the availability CGI will be run for every host, whether or not the
1278             filter in mkreport() actually selects them. To eliminate this waste and speed up the report, supply a callback like
1279             sub { my %F = @_; $u = $F{PERCENT_TOTAL_TIME_UP}; $u =~ s/%//; $u < 99 } or
1280             sub { my %F = @_; $F{TOTAL_TIME_DOWN} >= 600 }
1281              
1282             The constructor gets the Nagios availability data by running the all hosts or all services report (in CSV format)
1283             and storing it in the object.
1284              
1285             =item * mkreport (FIELD_LIST, SELECTOR_CALLBACK, ORDER_CALLBACK, MUNGE_CALLBACK, DOWNS)
1286              
1287             E<10>
1288             C is a reference to an array containing the field names that will appear in the report (a logical slice of the reports fields).
1289             The fields appear in the report in the same order they have in this list. Fields added by C B be specified
1290             by C or they will not be shown in the report B this parameter is omitted. If the C is omitted,
1291             B the fields appear in the report, no matter where they come from (ie if fields are added by the C, the
1292             new fields will appear in the report). If a field in C does not exist in the schema, it will not
1293             appear in the report; the caller B spell the field names correctly.
1294              
1295              
1296             C is a reference to a user supplied subroutine that will return B if the record is to be included in the report.
1297             The subroutine is called with an argument list of field names and their values for this record. This argument list
1298             can be copied to a hash in the callback, conventionally named B<%F>, so that the field names can be used in expressions like
1299              
1300             $F{HOST_NAME} =~ /^foo/
1301              
1302             to select eligible records.
1303              
1304             C is a reference to a user supplied sort subroutine that determines the order of the records in the report. The
1305             subroutine is called with an argument list of field names and their offsets in the records (eg (HOST_NAME, 0)). This argument list
1306             can be copied to a hash in the callback, conventionally named B<%f>, so that the field names can be used in expressions like
1307              
1308             $a->[$f{TOTAL_TIME_DOWN}] <=> $b->[$f{TOTAL_TIME_DOWN}]
1309              
1310             to sort the records based on the field values.
1311              
1312             C is a reference to a user supplied subroutine that is used to munge (transform input to output) the records. The subroutine
1313             is called with a pointer to a hash of field names and their values for this record. The callback is expected to modify the record
1314             by setting the values of this hash, munging fields with expressions like
1315              
1316             $F->{TOTAL_TIME_DOWN} = 0
1317              
1318             If the callback adds fields to the record, it should add the new field value in the same way (by setting a value for
1319             a new key in the hash, like for example,
1320              
1321             $F->{TOTAL_TIME_DOWN_HMS} = t2hms($F->{TOTAL_TIME_DOWN})
1322              
1323             ).
1324              
1325             If the callback adds no fields (modifies values only), it B return an empty list.
1326              
1327             A complete callback to set TOTAL_TIME_DOWN to zero is therefore
1328              
1329             sub {
1330             my $F = shift @_;
1331             $F->{TOTAL_TIME_DOWN} = 0;
1332             ()
1333             }
1334              
1335             mkreport() takes the availability data for each time period, adds outage data (which involves duplicating the
1336             C availability record as many times as there are outages), does any specified munging, applies the filter discarding
1337             records not rated as C by the selector callback, before sorting and
1338             slicing the results - dropping fields unwanted fields - and storing them as a C for each time period.
1339              
1340             mkreport() must be run before any of the output methods.
1341              
1342             C is an optional scalar flag. If the flag is set the availability
1343             report - the detailed report with the outage log - for B host is fetched and the outage records extracted.
1344              
1345             Then, for each of the outage records, the availability record is duplicated followed by the outage data: when the host went down, when
1346             it came back up, and the hours minutes seconds formatted value of the outage. These fields are named DOWN, UP, and OUTAGE.
1347              
1348             Since the availability data is repeated for each outage record, C can make the report look messy. It is best used
1349             with a small number of report fields (eg HOST_NAME, PERCENT_TOTAL_UP_TIME). Also, since the outage records are added B
1350             filtering by the selector callback, you B set a pre-filter in the constructor.
1351              
1352              
1353             The callbacks are run in this order
1354              
1355             =over 4
1356              
1357             =item 1 DOWNS (the availability report is retrieved for B hosts/services so that the selector
1358             can filter on the added fields, by for example, discarding all records with small outages).
1359              
1360             =item 2 MUNGE_CALLBACK
1361              
1362             =item 3 SELECTOR_CALLBACK
1363              
1364             =item 4 ORDER_CALLBACK
1365              
1366             =item 5 the field slice (ie discard all but the FIELD_LIST fields)
1367              
1368             =back
1369              
1370             =item * to_dbh
1371              
1372             If the DBD::AnyData module is installed, returns an array of DBI data handles
1373             connected to the pseudo-databases containing one table populated
1374             with the availability data corresponding to each of the REPORT_PERIODS
1375             specified in the constructor call.
1376              
1377             Each table is named C. For example, after constructing the
1378             Nagios::Report object with the default report period, the table is named B.
1379              
1380             This method allows the use of SQL statements as an alternative means of filtering
1381             and modifying the data. For example,
1382              
1383             $SQL =<
1384             SELECT host_name,
1385             total_time_down,
1386             time_down_scheduled,
1387             time_down_unscheduled,
1388             FROM tab_24x7
1389             WHERE total_time_down >= 300
1390             SQL
1391              
1392             $x = Nagios::Report->new(local_cgi Nagios-server Nagios-contact) ;
1393             ($d) = $x->to_dbh ;
1394             $s = $d->prepare($SQL) ;
1395             $s->execute ;
1396             $s->dump_results ;
1397              
1398             Unfortunately, the use of DBD::AnyData does not invalidate this module because
1399             the SQL grammar implemented by SQL::Statement (and used by DBD::AnyData) is not as
1400             extensive as the SQL grammar provided by an RDBMS, or as useful as the record selector
1401             mechanism provided by this.
1402              
1403             If one is determined to process with SELECT, load the data into an RDBMS of choice (
1404             MySQL comes to mind) and use the corresponding Perl DBD module on that. There is an example of
1405             a script to do so included in the distribution.
1406              
1407             Also, as of version 0.002, there were noisy complaints from DBI when DBD::AnyData
1408             processes SQL.
1409              
1410             =item * excel_dump (EXCEL_FILENAME, CHART_DETAIL)
1411              
1412             excel_dump writes a Workbook in the specified filename. The workbook contains a worksheet for each report (ie one for each time period
1413             specified by the constructor). excel_dump() requires the John McNamara's B CPAN module, Spreadsheet::WriteExcel.
1414              
1415             E<10>
1416             C is the path of the file in which the Excel workbook will be written.
1417              
1418             C is an optional reference to a hash that specifies the details of an Excel Chart that will be linked to the
1419             data from the report.
1420              
1421             The keys of his hash are
1422              
1423             E<10>
1424             template - the path to a binary file, produced by the Spreadsheet::WriteExcel chartex utility, containing the
1425             chart to be linked to the data.
1426              
1427             link - an optional scalar that specifies the data to be linked to the chart. Defaults to "q<'24x7'!A1>".
1428             You almost certainly will need to change this.
1429              
1430             title - an optional scalar that specifies the name of the chart to be added to the workbook. Defaults to '24x7 Chart'.
1431              
1432             Note that only one chart can be added to a workbook (by this module).
1433              
1434             =item * csv_dump
1435              
1436             CSV formatted output on STDOUT. Note that this mainly useful for debugging since the
1437             cell data is not formatted should it be imported by Excel
1438              
1439             =item * dev_debug (FIELD_WIDTH, FIELDS_PER_LINE)
1440              
1441             report formatted output on STDOUT.
1442              
1443             =head1 ACCESSORS/MUTATORS
1444              
1445             Acessors and mutators are provided for most of the attributes in the object. The module makes B
1446             use of them and, except for those below, are probably of little interest. Unless noted, the caller is
1447             responsible for processing the attribute type correctly; the acessor does nothing more
1448             than hand back a ref.
1449              
1450             =over 4
1451              
1452             =item * report (REPORT_PERIOD)
1453              
1454             Accessor that returns, in scalar context, an iterator that when kicked returns each of the
1455             records produced by mkreport() for that report period; in array context, returns the list of those records.
1456              
1457             Note that the 'records' are refs to anonymous lists containing only those fields specified by the field list parameter of
1458             mkreport().
1459              
1460             =item * avail (REPORT_PERIOD)
1461              
1462             Accessor that returns, in scalar context, an iterator that when kicked returns each of the
1463             records returned by the constructor; in array context, returns the list of those records.
1464             Note that the 'records' are refs to anonymous lists containing all of the Nagios availability report fields
1465             augmented by the two extra fields AVAIL_URL and TREND_URL (vi).
1466              
1467             The iterator is a code ref that, when called, will return a ref to the next
1468             availability record. Without a REPORT_PERIOD,
1469             returns an iterator to the B<24x7> data, otherwise the availability data corresponding
1470             to that report period (if it exists). If the methods of this class are not
1471             useful, then the iterator allows the caller transform the availability data with
1472             B and filter it with B.
1473              
1474             =item * FIELDNAMES
1475              
1476             Ref to a list of field names eg @fields = @{$me->FIELDNAMES}
1477              
1478             =item * FIELDS
1479              
1480             Ref to a hash of field indices keyed by field name eg %f = %{$me->FIELDS}}
1481              
1482             =item * SERVER
1483              
1484             Hostname of the server on which the Nagios CGIs can be found.
1485              
1486             =item * SOURCE_TAG
1487              
1488             How the availability data will be fetched.
1489              
1490             =item * REPORT_TYPE
1491              
1492             host | service report
1493              
1494             =item * DATA_SOURCE
1495              
1496             Reference to a subroutine that will fetch the availability data.
1497              
1498             =item * REPORTS
1499              
1500             Ref to a hash keyed by report period containing a hash keyed by FIELDNAMES and RECORDS.
1501             The latter key refers to a list containing the records selected and munged by mkreport().
1502             Note that each record contains only those fields specified by the field list parm of
1503             mkreport().
1504              
1505             See report (REPORT_PERIOD).
1506              
1507             =item * AVAIL_REPORTS
1508              
1509             Ref to a hash keyed by report period containing a ref to a list containing all
1510             those records returned by the Nagios availability report that are accepted by the
1511             pre-filter.
1512              
1513             Unlike REPORTS, each record contains all the Nagios reporting fields including
1514             the fields AVAIL_URL and TREND_URL whose values are hyperlinks to the Nagios
1515             trend and availability reports for the host named HOST_NAME.
1516              
1517              
1518             See avail (REPORT_PERIOD).
1519              
1520             =back
1521              
1522             =head1 SUBROUTINES
1523              
1524             =over 4
1525              
1526             =item * max_l ( LIST_OF_NUMERIC_VALS)
1527              
1528             Returns the maximum value in the list. Does not handle non-numeric values.
1529              
1530             =item * comp ( OPTION_HASH )
1531              
1532             Returns a ref to a comparator function that determines the order of the records and can be used
1533             as the B argument of B.
1534              
1535             C is an B hash with keys
1536              
1537             C Sort by the HOST_NAME field if set, otherwise by the maximum of the fields value.
1538              
1539             C Sort in ascending order if set.
1540              
1541             C The comparator function orders the records based on the B of the B field values. Only applies if alpha
1542             is not set. C is a reference to an array of numeric field names.
1543              
1544             eg &comp( alpha => 1, ascend => 1 )
1545              
1546             Returns a ref to function that orders the records by the HOST_NAME field.
1547              
1548             eg &comp()
1549              
1550             Same as calling comp with ( alpha => 1, ascend => 1 )
1551              
1552             eg &comp( alpha => 0, ascend => 1, fields => [TOTAL_TIME_DOWN, TOTAL_TIME_UNREACHABLE] )
1553              
1554             Returns a ref to a function that orders the records by the maximum of the values of the
1555             TOTAL_TIME_DOWN and TOTAL_TIME_UNREACHABLE fields.
1556              
1557              
1558             =item * t2hms (TIME_T)
1559              
1560             Returns the argument formatted by weeks, days, hours, minutes and seconds eg &t2hms(300) -> 5m.
1561              
1562             =item * d2t (time_string)
1563              
1564             Returns the time_t value of the string formatted as either a localtime, US (MM-DD-YYYY) or EURO date (DD-MM-YYY).
1565              
1566             =item * i2t (interval)
1567              
1568             Returns the time_t value of the string formatted as an interval of time ie one that matches (?:\d+[wdhms]\s*)+
1569             eg 3h 5m 30s (3 hours 5 minutes and 30 seconds).
1570              
1571             =item * $st_et{TAG}->(TAG, [ STRUCT TM ] )
1572              
1573             A hash of code refs indexed by an interval selecting tag. The second optinal argument is a ref to
1574             a STRUCT TM. If this arg is missing, [localtime] is used.
1575              
1576             Returns the pair of time_ts bounding the interval named by the tag. The
1577             upper boundary of the interval is usually the time_t value corresponding to the second argument. The lower boundary is this less
1578             the tags value.
1579              
1580             Tags include last_n_days, last_n_hours, last_n_mins, thisweek, lastweek, thismonth, lastmonth, thisyear, lastyear and variable strings like the 'at' style time stamps such as 'MMM YYYY' (eg Mar 2006) or HHMM DD.MM.YYYY. When the tag represents a fixed date or time, the interval
1581             returned is from 'then' to 'now'.
1582              
1583             When called by this module, the second argument is always 'now' (ie [localtime]).
1584             eg join('-', map { scalar(localtime($_)) } $st_et{today}->('today') eq 'Fri Aug 18 00:00:00 2006-Fri Aug 18 10:13:00 2006'
1585             eg join('-', map { scalar(localtime($_)) } $st_et{today}->('today', [ 0, 0, 0, 18, 7, 106])) eq 'Fri Aug 18 00:00:00 2006-Fri Aug 18 10:13:00 2006'
1586              
1587              
1588              
1589             =back
1590              
1591             =head1 BUGS
1592              
1593             =over 4
1594              
1595             =item * Does not do much more than the standard availability CGI. The reports
1596             has the same bugs as those produced by the CGI.
1597              
1598             =item * The B subroutine does not behave well if called with fields whose values are non numeric.
1599              
1600             =item * The hand done date and time functions are poor.
1601              
1602             =item * Anything good in this module comes from B; the rest comes from the author.
1603              
1604             =item * The module does not buy much more than rolling ones own with DBD::AnyData. Had I been more
1605             aware of the fundamental utility and importance of RDBMS data, this module would probably
1606             not exist. OTOH, this module now provides some sort of programmatic access to Nagios availability
1607             data that Nagios has not had (AFAIK) hitherto.
1608              
1609             =back
1610              
1611              
1612             =head1 SEE ALSO
1613              
1614             perl(1).
1615              
1616             Nagios (http://www.Nagios.ORG/)
1617              
1618              
1619             =head1 AUTHOR
1620              
1621             Stanley Hopcroft
1622              
1623             =head1 COPYRIGHT
1624              
1625             Copyright (c) 2006 Stanley Hopcroft. All rights reserved.
1626             This program is free software; you can redistribute it and/or modify
1627             it under the same terms as Perl itself.
1628              
1629             =cut
1630              
1631              
1632