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