Imported Upstream version 3.3.3
[debian/amanda] / perl / Amanda / Report.pm
1 # Copyright (c) 2010-2012 Zmanda, Inc.  All Rights Reserved.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19
20 package Amanda::Report;
21 use strict;
22 use warnings;
23 use Data::Dumper;
24
25 use Amanda::Disklist;
26 use Amanda::Logfile qw/:logtype_t :program_t/;
27 use Amanda::Util;
28 use Amanda::Debug qw( debug warning );
29
30 =head1 NAME
31
32 Amanda::Report -- module for representing report data from logfiles
33
34 =head1 SYNOPSIS
35
36     use Amanda::Report;
37
38     my $report = Amanda::Report->new($logfile);
39     my @hosts  = keys %{$report->{data}{disklist}};
40
41 =head1 INTERFACE
42
43 This module reads the logfile passed to it and aggregates the data in
44 a format of nested hashes for convenient output.  All data read in is
45 stored in C<< $report->{data} >>.
46
47 =head2 Creating a Report
48
49   my $report = Amanda::Report->new($logfile, $historical);
50
51 The constructor reads the logfile and produces the report, which can then be
52 queried with the other methods.  C<$logfile> should specify the path to the
53 logfile from which the report is prepared.  If the logfile is not the "current"
54 logfile, then C<$historical> should be false.  Non-historical reports may draw
55 information from the current Amanda environment, e.g., holding disks and info
56 files.
57
58 =head2 Summary Information
59
60 Note that most of the data provided by these methods is simply a reference to
61 data stored within the report, and should thus be considered read-only.  For
62 example, do not use C<shift> or C<pop> to destructively consume lists.
63
64   my $datestamp = $report->get_timestamp();
65
66 This returns the run timestamp for this dump run.  This is determined from one
67 of several START entries.  This returns a full 14-digit timestamp regardless of
68 the setting of C<usetimestamps> now or during the dump run.
69
70   my @hosts = $report->get_hosts();
71
72 This method returns a list containing the hosts that have been seen in
73 a logfile.  In a scalar context, C<get_hosts> returns the number of
74 hosts seen.
75
76   my @disks = $report->get_disks($hostname);
77
78 This method returns a list of disks that were archived under the given
79 C<$hostname>.  In a scalar context, this method returns the number of
80 disks seen, belonging to the hostname.
81
82   my @dles = $report->get_dles();
83
84 This method returns a list of list references.  Each referenced list
85 contains a hostname & disk pair that has been reported by either the
86 planner or amflush.  The DLEs are stored in the order that they appear
87 in the logfile.
88
89     @dles = (
90         [ 'example1', '/home' ],
91         [ 'example1', '/var/log' ],
92         [ 'example2', '/etc' ],
93         [ 'example2', '/home' ],
94         [ 'example3', '/var/www' ],
95     );
96
97   if ( $report->get_flag($flag) ) { ... }
98
99 The C<get_flag> method accesses a number of flags that represent the state of
100 the dump.  A true value is returned if the flag is set, and undef otherwise.
101 The available flags are:
102
103 =over
104
105 =item C<got_finish>
106
107 This flag is true when the driver finished
108 correctly.  It indicates that the dump run has finished and cleaned
109 up.
110
111 =item C<degraded_mode>
112
113 This flag is set if the taper encounters an
114 error that forces it into degraded mode.
115
116 =item C<amflush_run>
117
118 This flag is set if amflush is run instead of planner.
119
120 =item C<amvault_run>
121
122 This flag is set if the run was by amvault.
123
124 =item C<normal_run>
125
126 This flag is set when planner is run.  Its value
127 should be opposite of C<amflush_run>.
128
129 =item C<dump_failed>
130
131 If a dump failed.
132
133 =item C<dump_strange>
134
135 If a dump end in strange result.
136
137 =item C<results_missing>
138
139 If this was a normal run, but some DLEs named by the
140 planner do not have any results, then this flag is set.  Users should look for
141 DLEs with an empty C<dump> key to enumerate the missing results.
142
143 =item C<historical>
144
145 This flag is set if this is a "historical" report.  It is
146 based on the value passed to the constructor.
147
148 =back
149
150 =head2 Report Data
151
152   my $dle = $report->get_dle_info($hostname, $disk [,$field] );
153
154 This method returns the DLE information for the given C<$hostname> and C<disk>,
155 or if C<$field> is given, returns that field of the DLE information.  See the
156 DATA DESCRIPTION section for the format of this information.
157
158   my $info = $report->get_program_info($program [,$field] );
159
160 This method returns the program information for the given C<$program>, or if
161 C<$field> is given, returns that field of the DLE information.  See the DATA
162 DESCRIPTION section for the format of this information.
163
164 =head1 DATA DESCRIPTION
165
166 =head2 Top Level
167
168 The data in the logfile is stored in the module at C<< $report->{data} >>.
169 Beneath that, there are a number of subdivisions that track both global and
170 per-host status of the given Amanda run that the logfile represents.  Note that
171 these subdivisions are usually accessed via C<get_dle_info> and
172 C<get_program_info>, as described above.
173
174   $data->{programs}
175
176 the C<programs> key of the data points to a hash of global program
177 information, with one element per program.  See the Programs section, below.
178
179   $data->{boguses}
180
181 The C<boguses> key refers to a list of arrayrefs of the form
182
183   [$prog, $type, $str]
184
185 as returned directly by C<Amanda::Logfile::get_logline>.  These lines are not
186 in a recognized trace log format.
187
188   $data->{disklist}
189
190 The C<disklist> key points to a two-level hash of hostnames and
191 disknames as present in the logfile.  It looks something like this:
192
193     $report->{data}{disklist} = {
194         "server.example.org" => {
195             "/home" => {...},
196             "/var"  => {...},
197         },
198         "workstation.example.org" => {
199             "/etc"     => {...},
200             "/var/www" => {...},
201         },
202     };
203
204 Each C<{...}> in the above contains information about the corresponding DLE.  See DLEs, below.
205
206 =head2 Programs
207
208 Each program involved in a dump has a hash giving information about its
209 performance during the run.  A number of fields are common across all of the
210 different programs:
211
212 =over
213
214 =item C<start>
215
216 the numeric timestamp at which the process was started.
217
218 =item C<time>
219
220 the length of time (in seconds) that the program ran.
221
222 =item C<notes>
223
224 a list which stores all notes reported to the logfile
225 by the corresponding program.
226
227 =item C<errors>
228
229 a list which stores all errors reported to the
230 logfile by the corresponding program.
231
232 =back
233
234 Program-specific fields are described in the following sections.
235
236 =head3 planner
237
238 The planner logs very little information other than determining what will be
239 backed up.  It has no special fields other than those given above.
240
241 =head3 driver
242
243 The driver has one field that the other program-specific
244 entries do not:
245
246 =over
247
248 =item C<start_time> - the time it takes for the driver to start up.
249
250 =back
251
252 =head3 amflush and amdump
253
254 No special fields.
255
256 =head3 dumper and chunker
257
258 Most of the chunker's output and the dumper's output can be tied to a
259 particular DLE, so their C<programs> hashes are limited to C<notes> and
260 C<errors>.
261
262 =head3 taper
263
264 The taper hash holds notes and errors for the per-instance runs of the taper
265 program, but also tracks the tapes seen in the logfile:
266
267 =over
268
269 =item C<tapes>
270
271 This field is a hash reference keyed by the label of the tape.
272 each value of the key is another hash which stores date, size, and the
273 number of files seen by this backup on the tape.  For example:
274
275     $report->{data}{programs}{taper}{tapes} = {
276         FakeTape01 => {
277             label => "FakeTape01",
278             date  => "20100318141930",
279             kb    => 7894769,          # data written to tape this session
280             files => 14,               # parts written to tape this session
281             dle   => 13,               # number of dumps that begin on this tape
282             time  => 2.857,            # time spent writing to this tape
283         },
284     };
285
286 =item C<tape_labels>
287
288 The C<tape_labels> field is a reference to a list which records the
289 order that the tapes have been seen.  This list should be used as an
290 ordered index for C<tapes>.
291
292 =back
293
294 =head2 DLEs
295
296 In the below, C<$dle> is the hash representing one disklist entry.
297
298 The C<estimate> key describes the estimate given by the planner.  For
299 example:
300
301     $dle->{estimate} = {
302         level => 0,     # the level of the backup
303         sec   => 20,    # estimated time to back up (seconds)
304         nkb   => 2048,  # expected uncompressed size (kb)
305         ckb   => 1293,  # expected compressed size (kb)
306         kps   => 934.1, # speed of the backup (kb/sec)
307     };
308
309 Each dump of the DLE is represented in C<< $dle->{dumps} >>.  This is a hash,
310 keyed by dump timestamp with a list of tries as the value for each dump.  Each
311 try represents a specific attempt to finish writing this dump to a volume.  If
312 an error occurs during the backup of a DLE and is retried, a second try is
313 pushed to the tries list.  For example:
314
315     $dle->{dumps} = {
316         '20100317142122' => [ $try1 ],
317         '20100318141930' => [ $try1, $try2 ],
318     };
319
320 =head3 Tries
321
322 A try is a hash with at least one dumper, taper, and/or chunker DLE program as
323 a key.  These entries contain the results from the associated program during
324 try.
325
326 There are a number of common fields between all three elements:
327
328 =over
329
330 =item C<date>
331
332 a timestamp of when the program finished (if the program exited)
333
334 =item C<status>
335
336 the status of the dump at this program on this try ("success", "partial",
337 "done", or "failed").  The planner adds an extra "skipped" status which is
338 added when the planner decides to skip a DLE due to user configuration (e.g.,
339 C<skipincr>).
340
341 =item C<level>
342
343 the incremental level of the backup.
344
345 =item C<sec>
346
347 the time in seconds for the program to finish.
348
349 =item C<kb>
350
351 the size of the data dumped in kb.
352
353 =item C<kps>
354
355 the rate at which the program was able to process data,
356 in kb/sec.
357
358 =item C<error>
359
360 if the program fails, this field contains the error message
361
362 =back
363
364 The C<dumper> hash has an C<orig_kb> field, giving the size of the data dumped
365 from the source, before any compression. If encountered, the C<dumper> hash may
366 also contain a C<stranges> field, which is a list of all the messages of type
367 C<L_STRANGE> encountered during the process.
368
369 The C<taper> hash contains all the exit status data given by the taper.
370 Because the same taper process handles multiple dumps, it does not have a
371 C<date> field.  However, the taper does have an additional field, C<parts>,
372 containing a list of parts written for this dump.
373
374 =head3 Parts
375
376 Each item in the list of taper parts is a hash with the following
377 fields:
378
379 =over
380
381 =item C<label>
382
383 the name of the tape that the part was written to.
384
385 =item C<date>
386
387 the datestamp at which this part was written.
388
389 =item C<file>
390
391 the filename of the part.
392
393 =item C<part>
394
395 the sequence number of the part for the DLE that the
396 part is archiving.
397
398 =item C<sec>
399
400 the length of time, in seconds, that the part took to
401 be written.
402
403 =item C<kb>
404
405 the total size of the part.
406
407 =item C<kps>
408
409 the speed at which the part was written.
410
411 =back
412
413 =cut
414
415 use constant STATUS_STRANGE => 2;
416 use constant STATUS_FAILED  => 4;
417 use constant STATUS_MISSING => 8;
418 use constant STATUS_TAPE    => 16;
419
420 sub new
421 {
422     my $class = shift @_;
423     my ($logfname, $historical) = @_;
424
425     my $self = {
426         data => {},
427
428         ## inputs
429         _logfname => $logfname,
430         _historical => $historical,
431
432         ## logfile-parsing state
433
434         # the tape currently being writen
435         _current_tape => undef,
436     };
437     bless $self, $class;
438
439     $self->read_file();
440     return $self;
441 }
442
443
444 sub read_file
445 {
446     my $self       = shift @_;
447     my $data       = $self->{data} = {};
448     my $logfname   = $self->{_logfname};
449
450     # clear the program and DLE data
451     $data->{programs} = {};
452     $data->{disklist} = {};
453     $self->{cache}    = {};
454     $self->{flags}    = {};
455     $self->{run_timestamp} = '00000000000000';
456
457     my $logfh = Amanda::Logfile::open_logfile($logfname)
458       or die "cannot open '$logfname': $!";
459
460     $self->{flags}{exit_status} = 0;
461     $self->{flags}{results_missing} = 0;
462     $self->{flags}{dump_failed} = 0;
463     $self->{flags}{dump_strange} = 0;
464
465     while ( my ( $type, $prog, $str ) = Amanda::Logfile::get_logline($logfh) ) {
466         $self->read_line( $type, $prog, $str );
467     }
468
469     ## set post-run flags
470
471     $self->{flags}{historical} = $self->{_historical};
472     $self->{flags}{amflush_run} = 0;
473     $self->{flags}{amvault_run} = 0;
474     if (!$self->get_flag("normal_run")) {
475         if (   ( defined $self->get_program_info("amflush") )
476             && ( scalar %{ $self->get_program_info("amflush") } ) ) {
477             debug("detected an amflush run");
478             $self->{flags}{amflush_run} = 1;
479         } elsif (   ( defined $self->get_program_info("amvault") )
480                  && ( scalar %{ $self->get_program_info("amvault") } ) ) {
481             debug("detected an amvault run");
482             $self->{flags}{amvault_run} = 1;
483         }
484     }
485
486     # check for missing, fail and strange results
487     $self->check_missing_fail_strange() if $self->get_flag('normal_run');
488
489     # clean up any temporary values in the data
490     $self->cleanup();
491 }
492
493 sub cleanup
494 {
495     my $self = shift;
496
497     #remove last_label field
498     foreach my $dle ($self->get_dles()) {
499         my $dle_info = $self->get_dle_info(@$dle);
500         delete $dle_info->{last_label};
501     }
502
503     return;
504 }
505
506
507 sub read_line
508 {
509     my $self = shift @_;
510     my ( $type, $prog, $str ) = @_;
511
512     if ( $type == $L_CONT ) {
513         ${$self->{nbline_ref}}++;
514         if ($str =~ /^\|/) {
515             $self->{nb_strange}++;
516             push @{$self->{contline}}, $str if $self->{nb_strange} + $self->{nb_error} <= 100;
517         } elsif ($str =~ /^\?/) {
518             $self->{nb_error}++;
519             push @{$self->{contline}}, $str if $self->{nb_error} <= 100;
520         } else {
521             $self->{nb_normal}++;
522             push @{$self->{contline}}, $str if ${$self->{nbline_ref}} <= 100;
523         }
524         return;
525     }
526     $self->{contline} = undef;
527     $self->{nb_normal} = 0;
528     $self->{nb_strange} = 0;
529     $self->{nb_error} = 0;
530
531     if ( $prog == $P_PLANNER ) {
532         return $self->_handle_planner_line( $type, $str );
533
534     } elsif ( $prog == $P_DRIVER ) {
535         return $self->_handle_driver_line( $type, $str );
536
537     } elsif ( $prog == $P_DUMPER ) {
538         return $self->_handle_dumper_line( $type, $str );
539
540     } elsif ( $prog == $P_CHUNKER ) {
541         return $self->_handle_chunker_line( $type, $str );
542
543     } elsif ( $prog == $P_TAPER ) {
544         return $self->_handle_taper_line( $type, $str );
545
546     } elsif ( $prog == $P_AMFLUSH ) {
547         return $self->_handle_amflush_line( $type, $str );
548
549     } elsif ( $prog == $P_AMVAULT ) {
550         return $self->_handle_amvault_line( $type, $str );
551
552     } elsif ( $prog == $P_AMDUMP ) {
553         return $self->_handle_amdump_line( $type, $str );
554
555     } elsif ( $prog == $P_REPORTER ) {
556         return $self->_handle_reporter_line( $type, $str );
557
558     } else {
559         return $self->_handle_bogus_line( $prog, $type, $str );
560     }
561 }
562
563 sub get_timestamp
564 {
565     my $self = shift;
566     return $self->{'run_timestamp'};
567 }
568
569 sub get_hosts
570 {
571     my $self  = shift @_;
572     my $cache = $self->{cache};
573
574     $cache->{hosts} = [ keys %{ $self->{data}{disklist} } ]
575       if ( !defined $cache->{hosts} );
576
577     return @{ $cache->{hosts} };
578 }
579
580 sub get_disks
581 {
582     my $self = shift @_;
583     my ($hostname) = @_;
584     return keys %{ $self->{data}{disklist}{$hostname} };
585 }
586
587 sub get_dles
588 {
589     my $self  = shift @_;
590     my $cache = $self->{cache};
591     my @dles;
592
593     if ( !defined $cache->{dles} ) {
594         foreach my $hostname ( $self->get_hosts() ) {
595             map { push @dles, [ $hostname, $_ ] } $self->get_disks($hostname);
596         }
597         $cache->{dles} = \@dles;
598     }
599     return @{ $cache->{dles} };
600 }
601
602 sub xml_output
603 {
604     my ( $self, $org, $config ) = @_;
605     use Amanda::Report::xml;
606     return Amanda::Report::xml::make_amreport_xml( $self, $org, $config );
607 }
608
609 sub get_dle_info
610 {
611     my $self = shift @_;
612     my ( $hostname, $disk, $field ) = @_;
613
614     return ( defined $field )
615       ? $self->{data}{disklist}{$hostname}{$disk}{$field}
616       : $self->{data}{disklist}{$hostname}{$disk};
617 }
618
619 sub get_program_info
620 {
621     my ($self, $program, $field, $default) = @_;
622     my $prog = $self->{data}{programs}{$program};
623
624     $prog->{$field} = $default if (defined $field && !defined $prog->{$field});
625
626     return (defined $field) ? $prog->{$field} : $prog;
627 }
628
629 sub get_tape
630 {
631     my ($self, $label) = @_;
632
633     my $taper       = $self->get_program_info("taper");
634     my $tapes       = $taper->{tapes}       ||= {};
635     my $tape_labels = $taper->{tape_labels} ||= [];
636
637     if (!exists $tapes->{$label}) {
638         push @$tape_labels, $label;
639         $tapes->{$label} = {date => "",
640                             kb => 0,
641                             files => 0,
642                             dle => 0,
643                             time => 0};
644     }
645
646     return $tapes->{$label};
647 }
648
649 sub get_flag
650 {
651     my ( $self, $flag ) = @_;
652     return $self->{flags}{$flag};
653 }
654
655 sub _handle_planner_line
656 {
657     my $self = shift @_;
658     my ( $type, $str ) = @_;
659     my $data     = $self->{data};
660     my $programs = $data->{programs};
661     my $disklist = $data->{disklist} ||= {};
662     my $planner  = $programs->{planner} ||= {};
663
664     if ( $type == $L_INFO ) {
665         return $self->_handle_info_line( "planner", $str );
666
667     } elsif ( $type == $L_WARNING ) {
668         return $self->_handle_warning_line( "planner", $str );
669
670     } elsif ( $type == $L_START ) {
671
672         $self->{flags}{normal_run} = 1;
673         return $self->_handle_start_line( "planner", $str );
674
675     } elsif ( $type == $L_FINISH ) {
676
677         my @info = Amanda::Util::split_quoted_strings($str);
678         return $planner->{time} = $info[3];
679
680     } elsif ( $type == $L_DISK ) {
681         return $self->_handle_disk_line( "planner", $str );
682
683     } elsif ( $type == $L_SUCCESS ) {
684         return $self->_handle_success_line( "planner", $str );
685
686     } elsif ( $type == $L_ERROR ) {
687         return $self->_handle_error_line( "planner", $str );
688
689     } elsif ( $type == $L_FATAL ) {
690         return $self->_handle_fatal_line( "planner", $str );
691
692     } elsif ( $type == $L_FAIL ) {
693
694         # TODO: these are not like other failure messages: later
695         # handle here
696         return $self->_handle_fail_line( "planner", $str );
697
698     } else {
699         return $self->_handle_bogus_line( $P_PLANNER, $type, $str );
700     }
701 }
702
703
704 sub _handle_driver_line
705 {
706     my $self = shift @_;
707     my ( $type, $str ) = @_;
708     my $data     = $self->{data};
709     my $disklist = $data->{disklist};
710     my $programs = $data->{programs};
711     my $driver_p = $programs->{driver} ||= {};
712
713     if ( $type == $L_INFO ) {
714         return $self->_handle_info_line( "driver", $str );
715
716     } elsif ( $type == $L_START ) {
717         return $self->_handle_start_line( "driver", $str );
718
719     } elsif ( $type == $L_FINISH ) {
720
721         my @info = Amanda::Util::split_quoted_strings($str);
722         $self->{flags}{got_finish} = 1;
723         return $driver_p->{time} = $info[3];
724
725     } elsif ( $type == $L_STATS ) {
726
727         my @info = Amanda::Util::split_quoted_strings($str);
728         if ( $info[0] eq "hostname" ) {
729
730             return $self->{hostname} = $info[1];
731
732         } elsif ( $info[0] eq "startup" ) {
733
734             my @info = Amanda::Util::split_quoted_strings($str);
735             return $driver_p->{start_time} = $info[2];
736
737         } elsif ( $info[0] eq "estimate" ) {
738
739             # estimate format:
740             # STATS driver estimate <hostname> <disk> <timestamp>
741             # <level> [sec <sec> nkb <nkb> ckb <ckb> jps <kps>]
742             # note that the [..] section is *not* quoted properly
743             my ($hostname, $disk, $timestamp, $level) = @info[ 1 .. 4 ];
744
745             # if the planner didn't define the DLE then this is a bad
746             # line
747             unless (exists $disklist->{$hostname}{$disk}) {
748                 return $self->_handle_bogus_line($P_DRIVER, $type, $str);
749             }
750
751             my $dle = $self->get_dle_info($hostname, $disk);
752             my ($sec, $nkb, $ckb, $kps) = @info[ 6, 8, 10, 12 ];
753             $kps =~ s{\]}{};    # strip trailing "]"
754
755             $dle->{estimate} = {
756                 level => $level,
757                 sec   => $sec,
758                 nkb   => $nkb,
759                 ckb   => $ckb,
760                 kps   => $kps,
761             };
762
763         } else {
764             return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
765         }
766
767     } elsif ( $type == $L_WARNING ) {
768
769         $self->{flags}{exit_status} |= STATUS_TAPE
770           if ($str eq "Taper protocol error");
771
772         return $self->_handle_warning_line("driver", $str);
773
774     } elsif ( $type == $L_ERROR ) {
775         return $self->_handle_error_line( "driver", $str );
776
777     } elsif ( $type == $L_FATAL ) {
778         return $self->_handle_fatal_line( "driver", $str );
779
780     } elsif ( $type == $L_FAIL ) {
781         return $self->_handle_fail_line( "driver", $str );
782
783     } else {
784         return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
785     }
786 }
787
788
789 sub _handle_dumper_line
790 {
791     my $self = shift @_;
792     my ( $type, $str ) = @_;
793     my $data     = $self->{data};
794     my $disklist = $data->{disklist};
795     my $programs = $data->{programs};
796     my $dumper_p = $programs->{dumper} ||= {};
797
798     if ( $type == $L_INFO ) {
799         return $self->_handle_info_line( "dumper", $str );
800
801     } elsif ( $type == $L_STRANGE ) {
802
803         my @info = Amanda::Util::split_quoted_strings($str);
804         my ( $hostname, $disk, $level ) = @info[ 0 .. 2 ];
805         my ( $sec, $kb, $kps, $orig_kb ) = @info[ 4, 6, 8, 10 ];
806         $kb = int($kb/1024) if $info[4] eq 'bytes';
807         $orig_kb =~ s{\]$}{};
808
809         my $dle    = $disklist->{$hostname}->{$disk};
810         my $try    = $self->_get_try( $dle, "dumper", $self->{'run_timestamp'});
811         my $dumper = $try->{dumper} ||= {};
812         $dumper->{level} = $level;
813         $dumper->{status} = 'strange';
814         $dumper->{sec}       = $sec;
815         $dumper->{kb}        = $kb;
816         $dumper->{kps}       = $kps;
817         $dumper->{orig_kb}   = $orig_kb;
818
819         $self->{contline} = $dumper->{stranges} ||= [];
820         $dumper->{nb_stranges} = 0;
821         $self->{nbline_ref} = \$dumper->{nb_stranges};
822         $self->{nb_normal} = 0;
823         $self->{nb_strange} = 0;
824         $self->{nb_error} = 0;
825
826         return $self->{flags}{exit_status} |= STATUS_STRANGE
827
828     } elsif ( $type == $L_WARNING ) {
829
830         return $self->_handle_warning_line("dumper", $str);
831
832     } elsif ( $type == $L_SUCCESS ) {
833
834         my @info = Amanda::Util::split_quoted_strings($str);
835         my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
836         my ( $sec, $kb, $kps, $orig_kb ) = @info[ 5, 7, 9, 11 ];
837         $kb = int($kb/1024) if $info[6] eq 'bytes';
838         $orig_kb =~ s{\]$}{};
839
840         my $dle    = $disklist->{$hostname}->{$disk};
841         my $try    = $self->_get_try( $dle, "dumper", $timestamp );
842         my $dumper = $try->{dumper} ||= {};
843
844         $dumper->{date}      = $timestamp;
845         $dumper->{level}     = $level;
846         $dumper->{sec}       = $sec;
847         $dumper->{kb}        = $kb;
848         $dumper->{kps}       = $kps;
849         $dumper->{orig_kb}   = $orig_kb;
850
851         return $dumper->{status} = "success";
852
853     } elsif ( $type == $L_ERROR ) {
854         return $self->_handle_error_line( "dumper", $str );
855
856     } elsif ( $type == $L_FATAL ) {
857         return $self->_handle_fatal_line( "dumper", $str );
858
859     } elsif ( $type == $L_FAIL ) {
860         return $self->_handle_fail_line( "dumper", $str );
861
862     } else {
863         return $self->_handle_bogus_line( $P_DUMPER, $type, $str );
864     }
865 }
866
867
868 sub _handle_chunker_line
869 {
870     my $self = shift @_;
871     my ( $type, $str ) = @_;
872     my $data      = $self->{data};
873     my $disklist  = $data->{disklist};
874     my $programs  = $data->{programs};
875     my $chunker_p = $programs->{chunker} ||= {};
876
877     if ( $type == $L_INFO ) {
878         return $self->_handle_info_line( "chunker", $str );
879
880     } elsif ( $type == $L_SUCCESS || $type == $L_PARTIAL ) {
881
882         my @info = Amanda::Util::split_quoted_strings($str);
883         my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
884         my ( $sec, $kb, $kps ) = @info[ 5, 7, 9 ];
885         $kb = int($kb/1024) if $info[6] eq 'bytes';
886         $kps =~ s{\]$}{};
887
888         my $dle     = $disklist->{$hostname}->{$disk};
889         my $try     = $self->_get_try( $dle, "chunker", $timestamp );
890         my $chunker = $try->{chunker} ||= {};
891
892         $chunker->{date}  = $timestamp;
893         $chunker->{level} = $level;
894         $chunker->{sec}   = $sec;
895         $chunker->{kb}    = $kb;
896         $chunker->{kps}   = $kps;
897
898         return $chunker->{status} =
899           ( $type == $L_SUCCESS ) ? "success" : "partial";
900
901     } elsif ( $type == $L_ERROR ) {
902         return $self->_handle_error_line( "chunker", $str );
903
904     } elsif ( $type == $L_FATAL ) {
905         return $self->_handle_fatal_line( "chunker", $str );
906
907     } elsif ( $type == $L_FAIL ) {
908         return $self->_handle_fail_line( "chunker", $str );
909
910     } else {
911         return $self->_handle_bogus_line( $P_CHUNKER, $type, $str );
912     }
913 }
914
915
916 sub _handle_taper_line
917 {
918     my $self = shift @_;
919     my ( $type, $str ) = @_;
920     my $data     = $self->{data};
921     my $disklist = $data->{disklist};
922     my $programs = $data->{programs};
923     my $taper_p  = $programs->{taper} ||= {};
924
925     if ( $type == $L_START ) {
926         # format is:
927         # START taper datestamp <start> label <label> tape <tapenum>
928         my @info = Amanda::Util::split_quoted_strings($str);
929         my ($datestamp, $label, $tapenum) = @info[ 1, 3, 5 ];
930         my $tape = $self->get_tape($label);
931         $tape->{date} = $datestamp;
932         $tape->{label} = $label;
933
934         # keep this tape for later
935         $self->{'_current_tape'} = $tape;
936
937         # call through to the generic start line function
938         $self->_handle_start_line( "taper", $str );
939     } elsif ( $type == $L_PART || $type == $L_PARTPARTIAL ) {
940
941 # format is:
942 # <label> <tapefile> <hostname> <disk> <timestamp> <currpart>/<predparts> <level> [sec <sec> kb <kb> kps <kps>]
943 #
944 # format for $L_PARTPARTIAL is the same as $L_PART, plus <err> at the end
945         my @info = Amanda::Util::split_quoted_strings($str);
946         my ($label, $tapefile, $hostname, $disk, $timestamp) = @info[ 0 .. 4 ];
947
948         $info[5] =~ m{^(\d+)\/(-?\d+)$};
949         my ( $currpart, $predparts ) = ( $1, $2 );
950
951         my ($level, $sec, $kb, $kps, $orig_kb) = @info[ 6, 8, 10, 12, 14 ];
952         $kb = int($kb/1024) if $info[9] eq 'bytes';
953         $kps =~ s{\]$}{};
954         $orig_kb =~ s{\]$}{} if defined($orig_kb);
955
956         my $dle   = $disklist->{$hostname}{$disk};
957         my $try   = $self->_get_try($dle, "taper", $timestamp);
958         my $taper = $try->{taper} ||= {};
959         my $parts = $taper->{parts} ||= [];
960
961         my $part = {
962             label => $label,
963             date  => $timestamp,
964             file  => $tapefile,
965             sec   => $sec,
966             kb    => $kb,
967             kps   => $kps,
968             partnum  => $currpart,
969         };
970
971         $taper->{orig_kb} = $orig_kb;
972
973         push @$parts, $part;
974
975         my $tape = $self->get_tape($label);
976         # count this as a filesystem if this is the first part
977         $tape->{dle}++ if $currpart == 1;
978         $tape->{kb}   += $kb;
979         $tape->{time} += $sec;
980         $tape->{files}++;
981
982     } elsif ( $type == $L_DONE || $type == $L_PARTIAL ) {
983
984 # format is:
985 # $type = DONE | PARTIAL
986 # $type taper <hostname> <disk> <timestamp> <part> <level> [sec <sec> kb <kb> kps <kps>]
987         my @info = Amanda::Util::split_quoted_strings($str);
988         my ( $hostname, $disk, $timestamp, $part_ct, $level ) = @info[ 0 .. 4 ];
989         my ( $sec, $kb, $kps, $orig_kb ) = @info[ 6, 8, 10, 12 ];
990         $kb = int($kb/1024) if $info[7] eq 'bytes';
991         my $error;
992         if ($type == $L_PARTIAL) {
993             if ($kps =~ /\]$/) {
994                 $error = join " ", @info[ 11 .. $#info ];
995             } else {
996                 $error = join " ", @info[ 13 .. $#info ];
997             }
998         }
999         $kps =~ s{\]$}{};
1000         $orig_kb =~ s{\]$}{} if defined $orig_kb;
1001
1002         my $dle   = $disklist->{$hostname}->{$disk};
1003         my $try   = $self->_get_try($dle, "taper", $timestamp);
1004         my $taper = $try->{taper} ||= {};
1005         my $parts = $taper->{parts};
1006
1007         if ($part_ct - $#$parts != 1) {
1008             ## this should always be true; do nothing right now
1009         }
1010
1011         $taper->{level} = $level;
1012         $taper->{sec}   = $sec;
1013         $taper->{kb}    = $kb;
1014         $taper->{kps}   = $kps;
1015
1016         $taper->{status} = ( $type == $L_DONE ) ? "done" : "partial";
1017         $taper->{error} = $error if $type == $L_PARTIAL;
1018
1019     } elsif ( $type == $L_INFO ) {
1020         $self->_handle_info_line("taper", $str);
1021
1022     } elsif ( $type == $L_WARNING ) {
1023         $self->_handle_warning_line("taper", $str);
1024
1025     } elsif ( $type == $L_ERROR ) {
1026
1027         if ($str =~ m{^no-tape}) {
1028
1029             my @info = Amanda::Util::split_quoted_strings($str);
1030             my $failure_from = $info[1];
1031             my $error = join " ", @info[ 2 .. $#info ];
1032
1033             $self->{flags}{exit_status} |= STATUS_TAPE;
1034             $self->{flags}{degraded_mode} = 1;
1035             $taper_p->{failure_from} = $failure_from;
1036             $taper_p->{tape_error} = $error;
1037
1038         } else {
1039             $self->_handle_error_line("taper", $str);
1040         }
1041
1042     } elsif ( $type == $L_FATAL ) {
1043         return $self->_handle_fatal_line( "taper", $str );
1044
1045     } elsif ( $type == $L_FAIL ) {
1046         $self->_handle_fail_line( "taper", $str );
1047
1048     } else {
1049         $self->_handle_bogus_line( $P_TAPER, $type, $str );
1050     }
1051 }
1052
1053
1054 sub _handle_amflush_line
1055 {
1056     my $self = shift @_;
1057     my ( $type, $str ) = @_;
1058     my $data      = $self->{data};
1059     my $disklist  = $data->{disklist};
1060     my $programs  = $data->{programs};
1061     my $amflush_p = $programs->{amflush} ||= {};
1062
1063     if ( $type == $L_DISK ) {
1064         return $self->_handle_disk_line( "amflush", $str );
1065
1066     } elsif ( $type == $L_START ) {
1067         return $self->_handle_start_line( "amflush", $str );
1068
1069     } elsif ( $type == $L_INFO ) {
1070         return $self->_handle_info_line( "amflush", $str );
1071
1072     } else {
1073         return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
1074     }
1075 }
1076
1077 sub _handle_amvault_line
1078 {
1079     my $self = shift @_;
1080     my ( $type, $str ) = @_;
1081     my $data      = $self->{data};
1082     my $disklist  = $data->{disklist};
1083     my $programs  = $data->{programs};
1084     my $amvault_p = $programs->{amvault} ||= {};
1085
1086     if ( $type == $L_START ) {
1087         return $self->_handle_start_line( "amvault", $str );
1088
1089     } elsif ( $type == $L_INFO ) {
1090         return $self->_handle_info_line( "amvault", $str );
1091
1092     } elsif ( $type == $L_ERROR ) {
1093         return $self->_handle_error_line( "amvault", $str );
1094
1095     } elsif ( $type == $L_FATAL ) {
1096         return $self->_handle_fatal_line( "amvault", $str );
1097
1098     } elsif ( $type == $L_DISK ) {
1099         return $self->_handle_disk_line( "amvault", $str );
1100
1101     } else {
1102         return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
1103     }
1104 }
1105
1106
1107 sub _handle_amdump_line
1108 {
1109     my $self = shift;
1110     my ( $type, $str ) = @_;
1111     my $data     = $self->{data};
1112     my $disklist = $data->{disklist};
1113     my $programs = $data->{programs};
1114     my $amdump = $programs->{amdump} ||= {};
1115
1116     if ( $type == $L_INFO ) {
1117         $self->_handle_info_line("amdump", $str);
1118
1119     } elsif ( $type == $L_START ) {
1120         $self->_handle_start_line("amdump", $str);
1121
1122     } elsif ( $type == $L_FATAL ) {
1123         return $self->_handle_fatal_line( "amdump", $str );
1124
1125     } elsif ( $type == $L_ERROR ) {
1126         $self->_handle_error_line("amdump", $str);
1127     }
1128 }
1129
1130
1131 sub _handle_fail_line
1132 {
1133     my ($self, $program, $str) = @_;
1134
1135     my @info = Amanda::Util::split_quoted_strings($str);
1136     my ($hostname, $disk, $timestamp, $level) = @info;
1137     my $error;
1138     my $failure_from;
1139     if ($program eq 'taper') {
1140         $failure_from = $info[4];
1141         $error = join " ", @info[ 5 .. $#info ];
1142     } else {
1143         $error = join " ", @info[ 4 .. $#info ];
1144     }
1145
1146     #TODO: verify that this reaches the right try.  Also, DLE or
1147     #program?
1148     my $dle = $self->get_dle_info($hostname, $disk);
1149
1150     my $program_d;
1151     if ($program eq "planner" ||
1152         $program eq "driver") {
1153         $program_d = $dle->{$program} ||= {};
1154     } else {
1155         my $try = $self->_get_try($dle, $program, $timestamp);
1156         $program_d = $try->{$program} ||= {};
1157     }
1158
1159     $program_d->{level}  = $level;
1160     $program_d->{status} = "fail";
1161     $program_d->{failure_from}  = $failure_from;
1162     $program_d->{error}  = $error;
1163
1164     my $errors = $self->get_program_info("program", "errors", []);
1165     push @$errors, $error;
1166
1167     $self->{flags}{exit_status} |= STATUS_FAILED;
1168     if ($program eq "dumper") {
1169         $self->{contline} = $program_d->{errors} ||= [];
1170         $program_d->{nb_errors} = 0;
1171         $self->{nbline_ref} = \$program_d->{nb_errors};
1172         $self->{nb_normal} = 0;
1173         $self->{nb_strange} = 0;
1174         $self->{nb_error} = 0;
1175     }
1176 }
1177
1178
1179 sub _handle_error_line
1180 {
1181     my $self = shift @_;
1182     my ( $program, $str ) = @_;
1183
1184     my $data      = $self->{data};
1185     my $programs  = $data->{programs};
1186     my $program_p = $programs->{$program};
1187     my $errors_p  = $program_p->{errors} ||= [];
1188
1189     $self->{flags}{exit_status} |= 1;
1190
1191     push @$errors_p, $str;
1192 }
1193
1194
1195 sub _handle_fatal_line
1196 {
1197     my $self = shift @_;
1198     my ( $program, $str ) = @_;
1199
1200     my $data      = $self->{data};
1201     my $programs  = $data->{programs};
1202     my $program_p = $programs->{$program};
1203     my $fatal_p  = $program_p->{fatal} ||= [];
1204
1205     $self->{flags}{exit_status} |= 1;
1206
1207     push @$fatal_p, $str;
1208 }
1209
1210
1211 sub _handle_start_line
1212 {
1213     my $self = shift @_;
1214     my ( $program, $str ) = @_;
1215
1216     my $data     = $self->{data};
1217     my $disklist = $data->{disklist};
1218     my $programs = $data->{programs};
1219
1220     my $program_p = $programs->{$program} ||= {};
1221
1222     my @info = Amanda::Util::split_quoted_strings($str);
1223     my $timestamp = $info[1];
1224     $program_p->{start} = $info[1];
1225
1226     if ($self->{'run_timestamp'} ne '00000000000000'
1227                 and $self->{'run_timestamp'} ne $timestamp) {
1228         warning("not all timestamps in this file are the same; "
1229                 . "$self->{run_timestamp}; $timestamp");
1230     }
1231     $self->{'run_timestamp'} = $timestamp;
1232 }
1233
1234
1235 sub _handle_disk_line
1236 {
1237     my $self = shift @_;
1238     my ($program, $str) = @_;
1239
1240     my $data     = $self->{data};
1241     my $disklist = $data->{disklist};
1242     my $hosts    = $self->{cache}{hosts} ||= [];
1243     my $dles     = $self->{cache}{dles}  ||= [];
1244
1245     my @info = Amanda::Util::split_quoted_strings($str);
1246     my ($hostname, $disk) = @info;
1247
1248     if (!exists $disklist->{$hostname}) {
1249
1250         $disklist->{$hostname} = {};
1251         push @$hosts, $hostname;
1252     }
1253
1254     if (!exists $disklist->{$hostname}{$disk}) {
1255
1256         push @$dles, [ $hostname, $disk ];
1257         my $dle = $disklist->{$hostname}{$disk} = {};
1258         $dle->{'estimate'} = undef;
1259         $dle->{'dumps'}    = {};
1260     }
1261     return;
1262 }
1263
1264 sub _handle_success_line
1265 {
1266     my $self = shift @_;
1267     my ($program, $str) = @_;
1268
1269     my $data     = $self->{data};
1270     my $disklist = $data->{disklist};
1271     my $hosts    = $self->{cache}{hosts} ||= [];
1272     my $dles     = $self->{cache}{dles}  ||= [];
1273
1274     my @info = Amanda::Util::split_quoted_strings($str);
1275     my ($hostname, $disk, $timestamp, $level, $stat1, $stat2) = @info;
1276
1277     if ($stat1 =~ /skipped/) {
1278         $disklist->{$hostname}{$disk}->{$program}->{'status'} = 'skipped';
1279     }
1280     return;
1281 }
1282
1283
1284 sub _handle_info_line
1285 {
1286     my $self = shift @_;
1287     my ( $program, $str ) = @_;
1288
1289     my $data     = $self->{data};
1290     my $disklist = $data->{disklist};
1291     my $programs = $data->{programs};
1292
1293     my $program_p = $programs->{$program} ||= {};
1294
1295     if ( $str =~ m/^\w+ pid \d+/ || $str =~ m/^pid-done \d+/ ) {
1296
1297         #do not report pid lines
1298         return;
1299
1300     } else {
1301         my $notes = $program_p->{notes} ||= [];
1302         push @$notes, $str;
1303     }
1304 }
1305
1306 sub _handle_warning_line
1307 {
1308     my $self = shift @_;
1309     my ( $program, $str ) = @_;
1310
1311     $self->_handle_info_line($program, $str);
1312 }
1313
1314 sub _handle_bogus_line
1315 {
1316     my $self = shift @_;
1317     my ( $prog, $type, $str ) = @_;
1318
1319     my $data = $self->{data};
1320     my $boguses = $data->{boguses} ||= [];
1321     push @$boguses, [ $prog, $type, $str ];
1322 }
1323
1324 sub check_missing_fail_strange
1325 {
1326     my ($self) = @_;
1327     my @dles = $self->get_dles();
1328
1329     foreach my $dle_entry (@dles) {
1330         my $alldumps = $self->get_dle_info(@$dle_entry, 'dumps');
1331         my $driver = $self->get_dle_info(@$dle_entry, 'driver');
1332         my $planner = $self->get_dle_info(@$dle_entry, 'planner');
1333
1334         if ($planner && $planner->{'status'} eq 'fail') {
1335             $self->{flags}{dump_failed} = 1;
1336         } elsif ($planner && $planner->{'status'} eq 'skipped') {
1337             # We don't want these to be counted as missing below
1338         } elsif (!defined $alldumps->{$self->{'run_timestamp'}} and
1339                  !$driver and
1340                  !$planner) {
1341             $self->{flags}{results_missing} = 1;
1342             $self->{flags}{exit_status} |= STATUS_MISSING;
1343         } else {
1344             #get latest try
1345             my $tries = $alldumps->{$self->{'run_timestamp'}};
1346             my $try = @$tries[-1];
1347
1348             if (exists $try->{dumper} && $try->{dumper}->{status} eq 'fail') {
1349                 $self->{flags}{dump_failed} = 1;
1350             } elsif ((defined($try->{'chunker'}) &&
1351                  $try->{'chunker'}->{status} eq 'success') ||
1352                 (defined($try->{'taper'}) &&
1353                  $try->{'taper'}->{status} eq 'done')) {
1354                 #chunker or taper success, use dumper status
1355                 if (exists $try->{dumper} && $try->{dumper}->{status} eq 'strange') {
1356                     $self->{flags}{dump_strange} = 1;
1357                 }
1358             } else {
1359                 #chunker or taper failed, the dump is not valid.
1360                 $self->{flags}{dump_failed} = 1;
1361             }
1362         }
1363     }
1364 }
1365
1366 #
1367 # NOTE: there may be a complicated state diagram lurking in the midst
1368 # of taper and chunker.  You have been warned.
1369 #
1370 sub _get_try
1371 {
1372     my $self = shift @_;
1373     my ( $dle, $program, $timestamp ) = @_;
1374     my $tries = $dle->{'dumps'}{$timestamp} ||= [];
1375
1376     if (
1377         !@$tries    # no tries
1378         || defined $tries->[-1]->{$program}->{status}
1379         && $self->_program_finished(    # program has finished
1380             $program, $tries->[-1]->{$program}->{status}
1381         )
1382       ) {
1383         push @$tries, {};
1384     }
1385     return $tries->[-1];
1386 }
1387
1388
1389 sub _program_finished
1390 {
1391     my $self = shift @_;
1392     my ( $program, $status ) = @_;
1393
1394     if ( $program eq "chunker" ) {
1395
1396         if ( $status eq "partial" ) {
1397             return;
1398         } else {
1399             return 1;
1400         }
1401
1402     } elsif ( $status eq "done"
1403         || $status eq "success"
1404         || $status eq "fail"
1405         || $status eq "partial" ) {
1406         return 1;
1407
1408     } else {
1409         return 0;
1410     }
1411 }
1412
1413 1;