1 # Copyright (c) 2010 Zmanda, Inc. All Rights Reserved.
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.
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
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
16 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19 package Amanda::Report;
25 use Amanda::Logfile qw/:logtype_t :program_t/;
27 use Amanda::Debug qw( debug warning );
31 Amanda::Report -- module for representing report data from logfiles
37 my $report = Amanda::Report->new($logfile);
38 my @hosts = keys %{$report->{data}{disklist}};
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} >>.
46 =head2 my $report = Amanda::Report->new($logfile, $historical);
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
55 =head2 my $datestamp = $report->get_timestamp();
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.
61 =head2 my @hosts = $report->get_hosts();
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
67 =head2 my @disks = $report->get_disks($hostname);
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.
73 =head2 my @dles = $report->get_dles();
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
81 [ 'example1', '/home' ],
82 [ 'example1', '/var/log' ],
83 [ 'example2', '/etc' ],
84 [ 'example2', '/home' ],
85 [ 'example3', '/var/www' ],
88 =head2 my $dle = $report->get_dle_info($hostname, $disk [,$field] );
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.
96 =head2 my $info = $report->get_program_info($program [,$field, $default] );
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.
105 =head2 if ( $report->get_flag($flag) ) { ... }
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
111 =head1 DATA DESCRIPTION
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.
118 =head2 $data->{programs}
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.
126 =item C<start> - the numeric timestamp at which the process was
129 =item C<time> - the length of time (in seconds) that the program ran.
131 =item C<notes> - a list which stores all notes reported to the logfile
132 by the corresponding program.
134 =item C<errors> - a list which stores all errors reported to the
135 logfile by the corresponding program.
141 my $programs = $report->{data}{programs}
143 =head3 $programs->{planner}
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.
149 =head3 $programs->{driver}
151 The driver has one unique field that the other program-specific
156 =item C<start_time> - the time it takes for the driver to start up.
160 =head3 $programs->{amflush}
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.
165 =head3 $programs->{amdump}
167 This program is a control program that spawns off dumper programs. It
168 has no special fields.
170 =head3 $programs->{dumper} and $programs->{chunker}
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>
176 =head3 $programs->{taper}
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
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:
190 $report->{data}{programs}{taper}{tapes} = {
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
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>.
209 =head2 $data->{boguses}
211 The C<boguses> key refers to a list of arrayrefs of the form
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.
218 =head2 $data->{disklist}
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:
223 $report->{data}{disklist} = {
224 "server.example.org" => {
228 "workstation.example.org" => {
234 In the below, C<$dle> represents one disklist entry (C<{ ... }> in the
235 above). Each DLE has three major components: estimates, tries, and
240 The value of C<< $dle->{estimate} >> describes the estimate given by
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)
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.
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.
262 There are a number of common fields between all three elements:
266 =item C<date> - a timestamp of when the program finished.
268 =item C<status> - the exit status of the program on this try.
270 =item C<level> - the incremental level of the backup.
272 =item C<sec> - the time in seconds for the program to finish.
274 =item C<kb> - the size of the data dumped in kb.
276 =item C<kps> - the rate at which the program was able to process data,
279 =item C<error> - if the program fails, this field is set to record the
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
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.
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:
304 =item C<label> - the name of the tape that the part was written to.
306 =item C<date> - the datestamp at which this part was written.
308 =item C<file> - the filename of the part.
310 =item C<part> - the sequence number of the part for the DLE that the
313 =item C<sec> - the length of time, in seconds, that the part took to
316 =item C<kb> - the total size of the part.
318 =item C<kps> - the speed at which the part was written.
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.
328 The following is a list of currently recognized flags:
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
336 =item C<degraded_mode> - This flag is set if the taper encounters an
337 error that forces it into degraded mode.
339 =item C<amflush_run> - This flag is set if amflush is run instead of planner.
341 =item C<normal_run> - This flag is set when planner is run. Its value
342 should be opposite of C<amflush_run>.
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.
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.
355 use constant STATUS_STRANGE => 2;
356 use constant STATUS_FAILED => 4;
357 use constant STATUS_MISSING => 8;
358 use constant STATUS_TAPE => 16;
362 my $class = shift @_;
363 my ($logfname, $historical) = @_;
369 _logfname => $logfname,
370 _historical => $historical,
372 ## logfile-parsing state
374 # the tape currently being writen
375 _current_tape => undef,
387 my $data = $self->{data} = {};
388 my $logfname = $self->{_logfname};
390 # clear the program and DLE data
391 $data->{programs} = {};
392 $data->{disklist} = {};
395 $self->{run_timestamp} = '00000000000000';
397 my $logfh = Amanda::Logfile::open_logfile($logfname)
398 or die "cannot open '$logfname': $!";
400 $self->{flags}{exit_status} = 0;
402 while ( my ( $type, $prog, $str ) = Amanda::Logfile::get_logline($logfh) ) {
403 $self->read_line( $type, $prog, $str );
406 ## set post-run flags
408 $self->{flags}{historical} = $self->{_historical};
409 $self->{flags}{amflush_run} = 0;
411 !$self->get_flag("normal_run")
412 && ( ( defined $self->get_program_info("amflush") )
413 && ( scalar %{ $self->get_program_info("amflush") } ) )
415 debug("detected an amflush run");
416 $self->{flags}{amflush_run} = 1;
419 # check for missing results
420 $self->check_missing() if $self->get_flag('normal_run');
422 # clean up any temporary values in the data
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};
443 my ( $type, $prog, $str ) = @_;
445 if ( $type == $L_CONT ) {
446 ${$self->{nbline_ref}}++;
447 push @{$self->{contline}}, $str if ${$self->{nbline_ref}} <= 100;
450 $self->{contline} = undef;
452 if ( $prog == $P_PLANNER ) {
453 return $self->_handle_planner_line( $type, $str );
455 } elsif ( $prog == $P_DRIVER ) {
456 return $self->_handle_driver_line( $type, $str );
458 } elsif ( $prog == $P_DUMPER ) {
459 return $self->_handle_dumper_line( $type, $str );
461 } elsif ( $prog == $P_CHUNKER ) {
462 return $self->_handle_chunker_line( $type, $str );
464 } elsif ( $prog == $P_TAPER ) {
465 return $self->_handle_taper_line( $type, $str );
467 } elsif ( $prog == $P_AMFLUSH ) {
468 return $self->_handle_amflush_line( $type, $str );
470 } elsif ( $prog == $P_AMDUMP ) {
471 return $self->_handle_amdump_line( $type, $str );
473 } elsif ( $prog == $P_REPORTER ) {
474 return $self->_handle_reporter_line( $type, $str );
477 return $self->_handle_bogus_line( $prog, $type, $str );
484 return $self->{'run_timestamp'};
490 my $cache = $self->{cache};
492 $cache->{hosts} = [ keys %{ $self->{data}{disklist} } ]
493 if ( !defined $cache->{hosts} );
495 return @{ $cache->{hosts} };
502 return keys %{ $self->{data}{disklist}{$hostname} };
508 my $cache = $self->{cache};
511 if ( !defined $cache->{dles} ) {
512 foreach my $hostname ( $self->get_hosts() ) {
513 map { push @dles, [ $hostname, $_ ] } $self->get_disks($hostname);
515 $cache->{dles} = \@dles;
517 return @{ $cache->{dles} };
522 my ( $self, $org, $config ) = @_;
523 use Amanda::Report::xml;
524 return Amanda::Report::xml::make_amreport_xml( $self, $org, $config );
530 my ( $hostname, $disk, $field ) = @_;
532 return ( defined $field )
533 ? $self->{data}{disklist}{$hostname}{$disk}{$field}
534 : $self->{data}{disklist}{$hostname}{$disk};
539 my ($self, $program, $field, $default) = @_;
540 my $prog = $self->{data}{programs}{$program};
542 $prog->{$field} = $default if (defined $field && !defined $prog->{$field});
544 return (defined $field) ? $prog->{$field} : $prog;
549 my ($self, $label) = @_;
551 my $taper = $self->get_program_info("taper");
552 my $tapes = $taper->{tapes} ||= {};
553 my $tape_labels = $taper->{tape_labels} ||= [];
555 if (!exists $tapes->{$label}) {
556 push @$tape_labels, $label;
557 $tapes->{$label} = {date => "",
564 return $tapes->{$label};
569 my ( $self, $flag ) = @_;
570 return $self->{flags}{$flag};
573 sub _handle_planner_line
576 my ( $type, $str ) = @_;
577 my $data = $self->{data};
578 my $programs = $data->{programs};
579 my $disklist = $data->{disklist} ||= {};
580 my $planner = $programs->{planner} ||= {};
582 if ( $type == $L_INFO ) {
583 return $self->_handle_info_line( "planner", $str );
585 } elsif ( $type == $L_WARNING ) {
586 return $self->_handle_warning_line( "planner", $str );
588 } elsif ( $type == $L_START ) {
590 $self->{flags}{normal_run} = 1;
591 return $self->_handle_start_line( "planner", $str );
593 } elsif ( $type == $L_FINISH ) {
595 my @info = Amanda::Util::split_quoted_strings($str);
596 return $planner->{time} = $info[3];
598 } elsif ( $type == $L_DISK ) {
599 return $self->_handle_disk_line( "planner", $str );
601 } elsif ( $type == $L_ERROR ) {
602 return $self->_handle_error_line( "planner", $str );
604 } elsif ( $type == $L_FATAL ) {
605 return $self->_handle_fatal_line( "planner", $str );
607 } elsif ( $type == $L_FAIL ) {
609 # TODO: these are not like other failure messages: later
611 return $self->_handle_fail_line( "planner", $str );
614 return $self->_handle_bogus_line( $P_PLANNER, $type, $str );
619 sub _handle_driver_line
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} ||= {};
628 if ( $type == $L_INFO ) {
629 return $self->_handle_info_line( "driver", $str );
631 } elsif ( $type == $L_START ) {
632 return $self->_handle_start_line( "driver", $str );
634 } elsif ( $type == $L_FINISH ) {
636 my @info = Amanda::Util::split_quoted_strings($str);
637 $self->{flags}{got_finish} = 1;
638 return $driver_p->{time} = $info[3];
640 } elsif ( $type == $L_STATS ) {
642 my @info = Amanda::Util::split_quoted_strings($str);
643 if ( $info[0] eq "hostname" ) {
645 return $self->{hostname} = $info[1];
647 } elsif ( $info[0] eq "startup" ) {
649 my @info = Amanda::Util::split_quoted_strings($str);
650 return $driver_p->{start_time} = $info[2];
652 } elsif ( $info[0] eq "estimate" ) {
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 ];
660 # if the planner didn't define the DLE then this is a bad
662 unless (exists $disklist->{$hostname}{$disk}) {
663 return $self->_handle_bogus_line($P_DRIVER, $type, $str);
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 "]"
679 return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
682 } elsif ( $type == $L_WARNING ) {
684 $self->{flags}{exit_status} |= STATUS_TAPE
685 if ($str eq "Taper protocol error");
687 return $self->_handle_warning_line("driver", $str);
689 } elsif ( $type == $L_ERROR ) {
690 return $self->_handle_error_line( "driver", $str );
692 } elsif ( $type == $L_FATAL ) {
693 return $self->_handle_fatal_line( "driver", $str );
695 } elsif ( $type == $L_FAIL ) {
696 return $self->_handle_fail_line( "driver", $str );
699 return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
704 sub _handle_dumper_line
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} ||= {};
713 if ( $type == $L_INFO ) {
714 return $self->_handle_info_line( "dumper", $str );
716 } elsif ( $type == $L_STRANGE ) {
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{\]$}{};
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;
730 $dumper->{kps} = $kps;
731 $dumper->{orig_kb} = $orig_kb;
733 $self->{contline} = $dumper->{stranges} ||= [];
734 $dumper->{nb_stranges} = 0;
735 $self->{nbline_ref} = \$dumper->{nb_stranges};
737 return $self->{flags}{exit_status} |= STATUS_STRANGE
739 } elsif ( $type == $L_WARNING ) {
741 return $self->_handle_warning_line("dumper", $str);
743 } elsif ( $type == $L_SUCCESS ) {
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{\]$}{};
750 my $dle = $disklist->{$hostname}->{$disk};
751 my $try = $self->_get_try( $dle, "dumper" );
752 my $dumper = $try->{dumper} ||= {};
754 $dumper->{date} = $timestamp;
755 $dumper->{level} = $level;
756 $dumper->{sec} = $sec;
758 $dumper->{kps} = $kps;
759 $dumper->{orig_kb} = $orig_kb;
761 return $dumper->{status} = "success";
763 } elsif ( $type == $L_ERROR ) {
764 return $self->_handle_error_line( "dumper", $str );
766 } elsif ( $type == $L_FATAL ) {
767 return $self->_handle_fatal_line( "dumper", $str );
769 } elsif ( $type == $L_FAIL ) {
770 return $self->_handle_fail_line( "dumper", $str );
773 return $self->_handle_bogus_line( $P_DUMPER, $type, $str );
778 sub _handle_chunker_line
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} ||= {};
787 if ( $type == $L_INFO ) {
788 return $self->_handle_info_line( "chunker", $str );
790 } elsif ( $type == $L_SUCCESS || $L_PARTIAL ) {
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 ];
797 my $dle = $disklist->{$hostname}->{$disk};
798 my $try = $self->_get_try( $dle, "chunker" );
799 my $chunker = $try->{chunker} ||= {};
801 $chunker->{date} = $timestamp;
802 $chunker->{level} = $level;
803 $chunker->{sec} = $sec;
804 $chunker->{kb} = $kb;
805 $chunker->{kps} = $kps;
807 return $chunker->{status} =
808 ( $type == $L_SUCCESS ) ? "success" : "partial";
810 } elsif ( $type == $L_ERROR ) {
811 return $self->_handle_error_line( "chunker", $str );
813 } elsif ( $type == $L_FATAL ) {
814 return $self->_handle_fatal_line( "chunker", $str );
816 } elsif ( $type == $L_FAIL ) {
817 return $self->_handle_fail_line( "chunker", $str );
820 return $self->_handle_bogus_line( $P_CHUNKER, $type, $str );
825 sub _handle_taper_line
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} ||= {};
834 if ( $type == $L_START ) {
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;
843 # keep this tape for later
844 $self->{'_current_tape'} = $tape;
846 # call through to the generic start line function
847 $self->_handle_start_line( "taper", $str );
848 } elsif ( $type == $L_PART || $type == $L_PARTPARTIAL ) {
851 # <label> <tapefile> <hostname> <disk> <timestamp> <currpart>/<predparts> <level> [sec <sec> kb <kb> kps <kps>]
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 ];
857 $info[5] =~ m{^(\d+)\/(-?\d+)$};
858 my ( $currpart, $predparts ) = ( $1, $2 );
860 my ($level, $sec, $kb, $kps, $orig_kb) = @info[ 6, 8, 10, 12, 14 ];
862 $orig_kb =~ s{\]$}{} if defined($orig_kb);
864 if (!$self->{'_current_tape'} || $label ne $self->{'_current_tape'}->{'label'}) {
865 warning("corrupted logfile - PART or PARTPARTIAL does not match previous START taper");
868 # count this as a filesystem if this is the first part
869 $self->{'_current_tape'}->{dle}++ if $currpart == 1;
871 my $dle = $disklist->{$hostname}{$disk};
872 my $try = $self->_get_try($dle, "taper");
873 my $taper = $try->{taper} ||= {};
874 my $parts = $taper->{parts} ||= [];
883 partnum => $currpart,
886 $taper->{orig_kb} = $orig_kb;
890 my $tape = $self->get_tape($label);
892 $tape->{time} += $sec;
895 } elsif ( $type == $L_DONE || $type == $L_PARTIAL ) {
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 ];
904 if ($type == $L_PARTIAL) {
906 $error = join " ", @info[ 11 .. $#info ];
908 $error = join " ", @info[ 13 .. $#info ];
912 $orig_kb =~ s{\]$}{} if defined $orig_kb;
914 my $dle = $disklist->{$hostname}->{$disk};
915 my $try = $self->_get_try($dle, "taper");
916 my $taper = $try->{taper} ||= {};
917 my $parts = $taper->{parts};
919 if ($part_ct - $#$parts != 1) {
920 ## this should always be true; do nothing right now
923 $taper->{level} = $level;
924 $taper->{sec} = $sec;
926 $taper->{kps} = $kps;
928 $taper->{status} = ( $type == $L_DONE ) ? "done" : "partial";
929 $taper->{error} = $error if $type == $L_PARTIAL;
931 } elsif ( $type == $L_INFO ) {
932 $self->_handle_info_line("taper", $str);
934 } elsif ( $type == $L_WARNING ) {
935 $self->_handle_warning_line("taper", $str);
937 } elsif ( $type == $L_ERROR ) {
939 if ($str =~ m{^no-tape}) {
941 $self->{flags}{exit_status} |= STATUS_TAPE;
942 $self->{flags}{degraded_mode} = 1;
943 $taper_p->{tape_error} = $str;
946 $self->_handle_error_line("taper", $str);
949 } elsif ( $type == $L_FATAL ) {
950 return $self->_handle_fatal_line( "taper", $str );
952 } elsif ( $type == $L_FAIL ) {
953 $self->_handle_fail_line( "taper", $str );
956 $self->_handle_bogus_line( $P_TAPER, $type, $str );
961 sub _handle_amflush_line
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} ||= {};
970 if ( $type == $L_DISK ) {
971 return $self->_handle_disk_line( "amflush", $str );
973 } elsif ( $type == $L_START ) {
974 return $self->_handle_start_line( "amflush", $str );
976 } elsif ( $type == $L_INFO ) {
977 return $self->_handle_info_line( "amflush", $str );
980 return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
985 sub _handle_amdump_line
988 my ( $type, $str ) = @_;
989 my $data = $self->{data};
990 my $disklist = $data->{disklist};
991 my $programs = $data->{programs};
992 my $amdump = $programs->{amdump} ||= {};
994 if ( $type == $L_INFO ) {
995 $self->_handle_info_line("amdump", $str);
997 } elsif ( $type == $L_START ) {
998 $self->_handle_start_line("amdump", $str);
1000 } elsif ( $type == $L_FATAL ) {
1001 return $self->_handle_fatal_line( "amdump", $str );
1003 } elsif ( $type == $L_ERROR ) {
1004 $self->_handle_error_line("amdump", $str);
1009 sub _handle_fail_line
1011 my ($self, $program, $str) = @_;
1013 my @info = Amanda::Util::split_quoted_strings($str);
1014 my ($hostname, $disk, $date, $level) = @info;
1015 my $error = join " ", @info[ 4 .. $#info ];
1017 #TODO: verify that this reaches the right try. Also, DLE or
1019 my $dle = $self->get_dle_info($hostname, $disk);
1022 if ($program eq "planner" ||
1023 $program eq "driver") {
1024 $program_d = $dle->{$program} ||= {};
1026 my $try = $self->_get_try($dle, $program);
1027 $program_d = $try->{$program} ||= {};
1030 $program_d->{level} = $level;
1031 $program_d->{status} = "fail";
1032 $program_d->{error} = $error;
1034 my $errors = $self->get_program_info("program", "errors", []);
1035 push @$errors, $error;
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};
1046 sub _handle_error_line
1048 my $self = shift @_;
1049 my ( $program, $str ) = @_;
1051 my $data = $self->{data};
1052 my $programs = $data->{programs};
1053 my $program_p = $programs->{$program};
1054 my $errors_p = $program_p->{errors} ||= [];
1056 $self->{flags}{exit_status} |= 1;
1058 push @$errors_p, $str;
1062 sub _handle_fatal_line
1064 my $self = shift @_;
1065 my ( $program, $str ) = @_;
1067 my $data = $self->{data};
1068 my $programs = $data->{programs};
1069 my $program_p = $programs->{$program};
1070 my $fatal_p = $program_p->{fatal} ||= [];
1072 $self->{flags}{exit_status} |= 1;
1074 push @$fatal_p, $str;
1078 sub _handle_start_line
1080 my $self = shift @_;
1081 my ( $program, $str ) = @_;
1083 my $data = $self->{data};
1084 my $disklist = $data->{disklist};
1085 my $programs = $data->{programs};
1087 my $program_p = $programs->{$program} ||= {};
1089 my @info = Amanda::Util::split_quoted_strings($str);
1090 my $timestamp = $info[1];
1091 $program_p->{start} = $info[1];
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");
1100 $self->{'run_timestamp'} = $timestamp;
1104 sub _handle_disk_line
1106 my $self = shift @_;
1107 my ($program, $str) = @_;
1109 my $data = $self->{data};
1110 my $disklist = $data->{disklist};
1111 my $hosts = $self->{cache}{hosts} ||= [];
1112 my $dles = $self->{cache}{dles} ||= [];
1114 my @info = Amanda::Util::split_quoted_strings($str);
1115 my ($hostname, $disk) = @info;
1117 if (!exists $disklist->{$hostname}) {
1119 $disklist->{$hostname} = {};
1120 push @$hosts, $hostname;
1123 if (!exists $disklist->{$hostname}{$disk}) {
1125 push @$dles, [ $hostname, $disk ];
1126 my $dle = $disklist->{$hostname}{$disk} = {};
1127 $dle->{estimate} = undef;
1134 sub _handle_info_line
1136 my $self = shift @_;
1137 my ( $program, $str ) = @_;
1139 my $data = $self->{data};
1140 my $disklist = $data->{disklist};
1141 my $programs = $data->{programs};
1143 my $program_p = $programs->{$program} ||= {};
1145 if ( $str =~ m/^\w+ pid \d+/ || $str =~ m/^pid-done \d+/ ) {
1147 #do not report pid lines
1151 my $notes = $program_p->{notes} ||= [];
1156 sub _handle_warning_line
1158 my $self = shift @_;
1159 my ( $program, $str ) = @_;
1161 $self->_handle_info_line($program, $str);
1164 sub _handle_bogus_line
1166 my $self = shift @_;
1167 my ( $prog, $type, $str ) = @_;
1169 my $data = $self->{data};
1170 my $boguses = $data->{boguses} ||= [];
1171 push @$boguses, [ $prog, $type, $str ];
1177 my @dles = $self->get_dles();
1179 foreach my $dle_entry (@dles) {
1181 my $tries = $self->get_dle_info(@$dle_entry, "tries");
1184 $self->{flags}{results_missing} = 1;
1185 $self->{flags}{exit_status} |= STATUS_MISSING;
1192 # NOTE: there may be a complicated state diagram lurking in the midst
1193 # of taper and chunker. You have been warned.
1197 my $self = shift @_;
1198 my ( $dle, $program ) = @_;
1199 my $tries = $dle->{tries} ||= [];
1203 || defined $tries->[-1]->{$program}->{status}
1204 && $self->_program_finished( # program has finished
1205 $program, $tries->[-1]->{$program}->{status}
1210 return $tries->[-1];
1214 sub _program_finished
1216 my $self = shift @_;
1217 my ( $program, $status ) = @_;
1219 if ( $program eq "chunker" ) {
1221 if ( $status eq "partial" ) {
1227 } elsif ( $status eq "done"
1228 || $status eq "success"
1229 || $status eq "fail"
1230 || $status eq "partial" ) {