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 Creating a Report
48 my $report = Amanda::Report->new($logfile, $historical);
50 The constructor reads the logfile and produces the report, which can then be
51 queried with the other methods. C<$logfile> should specify the path to the
52 logfile from which the report is prepared. If the logfile is not the "current"
53 logfile, then C<$historical> should be false. Non-historical reports may draw
54 information from the current Amanda environment, e.g., holding disks and info
57 =head2 Summary Information
59 Note that most of the data provided by these methods is simply a reference to
60 data stored within the report, and should thus be considered read-only. For
61 example, do not use C<shift> or C<pop> to destructively consume lists.
63 my $datestamp = $report->get_timestamp();
65 This returns the run timestamp for this dump run. This is determined from one
66 of several START entries. This returns a full 14-digit timestamp regardless of
67 the setting of C<usetimestamps> now or during the dump run.
69 my @hosts = $report->get_hosts();
71 This method returns a list containing the hosts that have been seen in
72 a logfile. In a scalar context, C<get_hosts> returns the number of
75 my @disks = $report->get_disks($hostname);
77 This method returns a list of disks that were archived under the given
78 C<$hostname>. In a scalar context, this method returns the number of
79 disks seen, belonging to the hostname.
81 my @dles = $report->get_dles();
83 This method returns a list of list references. Each referenced list
84 contains a hostname & disk pair that has been reported by either the
85 planner or amflush. The DLEs are stored in the order that they appear
89 [ 'example1', '/home' ],
90 [ 'example1', '/var/log' ],
91 [ 'example2', '/etc' ],
92 [ 'example2', '/home' ],
93 [ 'example3', '/var/www' ],
96 if ( $report->get_flag($flag) ) { ... }
98 The C<get_flag> method accesses a number of flags that represent the state of
99 the dump. A true value is returned if the flag is set, and undef otherwise.
100 The available flags are:
106 This flag is true when the driver finished
107 correctly. It indicates that the dump run has finished and cleaned
110 =item C<degraded_mode>
112 This flag is set if the taper encounters an
113 error that forces it into degraded mode.
117 This flag is set if amflush is run instead of planner.
121 This flag is set if the run was by amvault.
125 This flag is set when planner is run. Its value
126 should be opposite of C<amflush_run>.
132 =item C<dump_strange>
134 If a dump end in strange result.
136 =item C<results_missing>
138 If this was a normal run, but some DLEs named by the
139 planner do not have any results, then this flag is set. Users should look for
140 DLEs with an empty C<dump> key to enumerate the missing results.
144 This flag is set if this is a "historical" report. It is
145 based on the value passed to the constructor.
151 my $dle = $report->get_dle_info($hostname, $disk [,$field] );
153 This method returns the DLE information for the given C<$hostname> and C<disk>,
154 or if C<$field> is given, returns that field of the DLE information. See the
155 DATA DESCRIPTION section for the format of this information.
157 my $info = $report->get_program_info($program [,$field] );
159 This method returns the program information for the given C<$program>, or if
160 C<$field> is given, returns that field of the DLE information. See the DATA
161 DESCRIPTION section for the format of this information.
163 =head1 DATA DESCRIPTION
167 The data in the logfile is stored in the module at C<< $report->{data} >>.
168 Beneath that, there are a number of subdivisions that track both global and
169 per-host status of the given Amanda run that the logfile represents. Note that
170 these subdivisions are usually accessed via C<get_dle_info> and
171 C<get_program_info>, as described above.
175 the C<programs> key of the data points to a hash of global program
176 information, with one element per program. See the Programs section, below.
180 The C<boguses> key refers to a list of arrayrefs of the form
184 as returned directly by C<Amanda::Logfile::get_logline>. These lines are not
185 in a recognized trace log format.
189 The C<disklist> key points to a two-level hash of hostnames and
190 disknames as present in the logfile. It looks something like this:
192 $report->{data}{disklist} = {
193 "server.example.org" => {
197 "workstation.example.org" => {
203 Each C<{...}> in the above contains information about the corresponding DLE. See DLEs, below.
207 Each program involved in a dump has a hash giving information about its
208 performance during the run. A number of fields are common across all of the
215 the numeric timestamp at which the process was started.
219 the length of time (in seconds) that the program ran.
223 a list which stores all notes reported to the logfile
224 by the corresponding program.
228 a list which stores all errors reported to the
229 logfile by the corresponding program.
233 Program-specific fields are described in the following sections.
237 The planner logs very little information other than determining what will be
238 backed up. It has no special fields other than those given above.
242 The driver has one field that the other program-specific
247 =item C<start_time> - the time it takes for the driver to start up.
251 =head3 amflush and amdump
255 =head3 dumper and chunker
257 Most of the chunker's output and the dumper's output can be tied to a
258 particular DLE, so their C<programs> hashes are limited to C<notes> and
263 The taper hash holds notes and errors for the per-instance runs of the taper
264 program, but also tracks the tapes seen in the logfile:
270 This field is a hash reference keyed by the label of the tape.
271 each value of the key is another hash which stores date, size, and the
272 number of files seen by this backup on the tape. For example:
274 $report->{data}{programs}{taper}{tapes} = {
276 label => "FakeTape01",
277 date => "20100318141930",
278 kb => 7894769, # data written to tape this session
279 files => 14, # parts written to tape this session
280 dle => 13, # number of dumps that begin on this tape
281 time => 2.857, # time spent writing to this tape
287 The C<tape_labels> field is a reference to a list which records the
288 order that the tapes have been seen. This list should be used as an
289 ordered index for C<tapes>.
295 In the below, C<$dle> is the hash representing one disklist entry.
297 The C<estimate> key describes the estimate given by the planner. For
301 level => 0, # the level of the backup
302 sec => 20, # estimated time to back up (seconds)
303 nkb => 2048, # expected uncompressed size (kb)
304 ckb => 1293, # expected compressed size (kb)
305 kps => 934.1, # speed of the backup (kb/sec)
308 Each dump of the DLE is represented in C<< $dle->{dumps} >>. This is a hash,
309 keyed by dump timestamp with a list of tries as the value for each dump. Each
310 try represents a specific attempt to finish writing this dump to a volume. If
311 an error occurs during the backup of a DLE and is retried, a second try is
312 pushed to the tries list. For example:
315 '20100317142122' => [ $try1 ],
316 '20100318141930' => [ $try1, $try2 ],
321 A try is a hash with at least one dumper, taper, and/or chunker DLE program as
322 a key. These entries contain the results from the associated program during
325 There are a number of common fields between all three elements:
331 a timestamp of when the program finished (if the program exited)
335 the status of the dump at this program on this try ("success", "partial",
336 "done", or "failed"). The planner adds an extra "skipped" status which is
337 added when the planner decides to skip a DLE due to user configuration (e.g.,
342 the incremental level of the backup.
346 the time in seconds for the program to finish.
350 the size of the data dumped in kb.
354 the rate at which the program was able to process data,
359 if the program fails, this field contains the error message
363 The C<dumper> hash has an C<orig_kb> field, giving the size of the data dumped
364 from the source, before any compression. If encountered, the C<dumper> hash may
365 also contain a C<stranges> field, which is a list of all the messages of type
366 C<L_STRANGE> encountered during the process.
368 The C<taper> hash contains all the exit status data given by the taper.
369 Because the same taper process handles multiple dumps, it does not have a
370 C<date> field. However, the taper does have an additional field, C<parts>,
371 containing a list of parts written for this dump.
375 Each item in the list of taper parts is a hash with the following
382 the name of the tape that the part was written to.
386 the datestamp at which this part was written.
390 the filename of the part.
394 the sequence number of the part for the DLE that the
399 the length of time, in seconds, that the part took to
404 the total size of the part.
408 the speed at which the part was written.
414 use constant STATUS_STRANGE => 2;
415 use constant STATUS_FAILED => 4;
416 use constant STATUS_MISSING => 8;
417 use constant STATUS_TAPE => 16;
421 my $class = shift @_;
422 my ($logfname, $historical) = @_;
428 _logfname => $logfname,
429 _historical => $historical,
431 ## logfile-parsing state
433 # the tape currently being writen
434 _current_tape => undef,
446 my $data = $self->{data} = {};
447 my $logfname = $self->{_logfname};
449 # clear the program and DLE data
450 $data->{programs} = {};
451 $data->{disklist} = {};
454 $self->{run_timestamp} = '00000000000000';
456 my $logfh = Amanda::Logfile::open_logfile($logfname)
457 or die "cannot open '$logfname': $!";
459 $self->{flags}{exit_status} = 0;
460 $self->{flags}{results_missing} = 0;
461 $self->{flags}{dump_failed} = 0;
462 $self->{flags}{dump_strange} = 0;
464 while ( my ( $type, $prog, $str ) = Amanda::Logfile::get_logline($logfh) ) {
465 $self->read_line( $type, $prog, $str );
468 ## set post-run flags
470 $self->{flags}{historical} = $self->{_historical};
471 $self->{flags}{amflush_run} = 0;
472 $self->{flags}{amvault_run} = 0;
473 if (!$self->get_flag("normal_run")) {
474 if ( ( defined $self->get_program_info("amflush") )
475 && ( scalar %{ $self->get_program_info("amflush") } ) ) {
476 debug("detected an amflush run");
477 $self->{flags}{amflush_run} = 1;
478 } elsif ( ( defined $self->get_program_info("amvault") )
479 && ( scalar %{ $self->get_program_info("amvault") } ) ) {
480 debug("detected an amvault run");
481 $self->{flags}{amvault_run} = 1;
485 # check for missing, fail and strange results
486 $self->check_missing_fail_strange() if $self->get_flag('normal_run');
488 # clean up any temporary values in the data
496 #remove last_label field
497 foreach my $dle ($self->get_dles()) {
498 my $dle_info = $self->get_dle_info(@$dle);
499 delete $dle_info->{last_label};
509 my ( $type, $prog, $str ) = @_;
511 if ( $type == $L_CONT ) {
512 ${$self->{nbline_ref}}++;
513 push @{$self->{contline}}, $str if ${$self->{nbline_ref}} <= 100;
516 $self->{contline} = undef;
518 if ( $prog == $P_PLANNER ) {
519 return $self->_handle_planner_line( $type, $str );
521 } elsif ( $prog == $P_DRIVER ) {
522 return $self->_handle_driver_line( $type, $str );
524 } elsif ( $prog == $P_DUMPER ) {
525 return $self->_handle_dumper_line( $type, $str );
527 } elsif ( $prog == $P_CHUNKER ) {
528 return $self->_handle_chunker_line( $type, $str );
530 } elsif ( $prog == $P_TAPER ) {
531 return $self->_handle_taper_line( $type, $str );
533 } elsif ( $prog == $P_AMFLUSH ) {
534 return $self->_handle_amflush_line( $type, $str );
536 } elsif ( $prog == $P_AMVAULT ) {
537 return $self->_handle_amvault_line( $type, $str );
539 } elsif ( $prog == $P_AMDUMP ) {
540 return $self->_handle_amdump_line( $type, $str );
542 } elsif ( $prog == $P_REPORTER ) {
543 return $self->_handle_reporter_line( $type, $str );
546 return $self->_handle_bogus_line( $prog, $type, $str );
553 return $self->{'run_timestamp'};
559 my $cache = $self->{cache};
561 $cache->{hosts} = [ keys %{ $self->{data}{disklist} } ]
562 if ( !defined $cache->{hosts} );
564 return @{ $cache->{hosts} };
571 return keys %{ $self->{data}{disklist}{$hostname} };
577 my $cache = $self->{cache};
580 if ( !defined $cache->{dles} ) {
581 foreach my $hostname ( $self->get_hosts() ) {
582 map { push @dles, [ $hostname, $_ ] } $self->get_disks($hostname);
584 $cache->{dles} = \@dles;
586 return @{ $cache->{dles} };
591 my ( $self, $org, $config ) = @_;
592 use Amanda::Report::xml;
593 return Amanda::Report::xml::make_amreport_xml( $self, $org, $config );
599 my ( $hostname, $disk, $field ) = @_;
601 return ( defined $field )
602 ? $self->{data}{disklist}{$hostname}{$disk}{$field}
603 : $self->{data}{disklist}{$hostname}{$disk};
608 my ($self, $program, $field, $default) = @_;
609 my $prog = $self->{data}{programs}{$program};
611 $prog->{$field} = $default if (defined $field && !defined $prog->{$field});
613 return (defined $field) ? $prog->{$field} : $prog;
618 my ($self, $label) = @_;
620 my $taper = $self->get_program_info("taper");
621 my $tapes = $taper->{tapes} ||= {};
622 my $tape_labels = $taper->{tape_labels} ||= [];
624 if (!exists $tapes->{$label}) {
625 push @$tape_labels, $label;
626 $tapes->{$label} = {date => "",
633 return $tapes->{$label};
638 my ( $self, $flag ) = @_;
639 return $self->{flags}{$flag};
642 sub _handle_planner_line
645 my ( $type, $str ) = @_;
646 my $data = $self->{data};
647 my $programs = $data->{programs};
648 my $disklist = $data->{disklist} ||= {};
649 my $planner = $programs->{planner} ||= {};
651 if ( $type == $L_INFO ) {
652 return $self->_handle_info_line( "planner", $str );
654 } elsif ( $type == $L_WARNING ) {
655 return $self->_handle_warning_line( "planner", $str );
657 } elsif ( $type == $L_START ) {
659 $self->{flags}{normal_run} = 1;
660 return $self->_handle_start_line( "planner", $str );
662 } elsif ( $type == $L_FINISH ) {
664 my @info = Amanda::Util::split_quoted_strings($str);
665 return $planner->{time} = $info[3];
667 } elsif ( $type == $L_DISK ) {
668 return $self->_handle_disk_line( "planner", $str );
670 } elsif ( $type == $L_SUCCESS ) {
671 return $self->_handle_success_line( "planner", $str );
673 } elsif ( $type == $L_ERROR ) {
674 return $self->_handle_error_line( "planner", $str );
676 } elsif ( $type == $L_FATAL ) {
677 return $self->_handle_fatal_line( "planner", $str );
679 } elsif ( $type == $L_FAIL ) {
681 # TODO: these are not like other failure messages: later
683 return $self->_handle_fail_line( "planner", $str );
686 return $self->_handle_bogus_line( $P_PLANNER, $type, $str );
691 sub _handle_driver_line
694 my ( $type, $str ) = @_;
695 my $data = $self->{data};
696 my $disklist = $data->{disklist};
697 my $programs = $data->{programs};
698 my $driver_p = $programs->{driver} ||= {};
700 if ( $type == $L_INFO ) {
701 return $self->_handle_info_line( "driver", $str );
703 } elsif ( $type == $L_START ) {
704 return $self->_handle_start_line( "driver", $str );
706 } elsif ( $type == $L_FINISH ) {
708 my @info = Amanda::Util::split_quoted_strings($str);
709 $self->{flags}{got_finish} = 1;
710 return $driver_p->{time} = $info[3];
712 } elsif ( $type == $L_STATS ) {
714 my @info = Amanda::Util::split_quoted_strings($str);
715 if ( $info[0] eq "hostname" ) {
717 return $self->{hostname} = $info[1];
719 } elsif ( $info[0] eq "startup" ) {
721 my @info = Amanda::Util::split_quoted_strings($str);
722 return $driver_p->{start_time} = $info[2];
724 } elsif ( $info[0] eq "estimate" ) {
727 # STATS driver estimate <hostname> <disk> <timestamp>
728 # <level> [sec <sec> nkb <nkb> ckb <ckb> jps <kps>]
729 # note that the [..] section is *not* quoted properly
730 my ($hostname, $disk, $timestamp, $level) = @info[ 1 .. 4 ];
732 # if the planner didn't define the DLE then this is a bad
734 unless (exists $disklist->{$hostname}{$disk}) {
735 return $self->_handle_bogus_line($P_DRIVER, $type, $str);
738 my $dle = $self->get_dle_info($hostname, $disk);
739 my ($sec, $nkb, $ckb, $kps) = @info[ 6, 8, 10, 12 ];
740 $kps =~ s{\]}{}; # strip trailing "]"
751 return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
754 } elsif ( $type == $L_WARNING ) {
756 $self->{flags}{exit_status} |= STATUS_TAPE
757 if ($str eq "Taper protocol error");
759 return $self->_handle_warning_line("driver", $str);
761 } elsif ( $type == $L_ERROR ) {
762 return $self->_handle_error_line( "driver", $str );
764 } elsif ( $type == $L_FATAL ) {
765 return $self->_handle_fatal_line( "driver", $str );
767 } elsif ( $type == $L_FAIL ) {
768 return $self->_handle_fail_line( "driver", $str );
771 return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
776 sub _handle_dumper_line
779 my ( $type, $str ) = @_;
780 my $data = $self->{data};
781 my $disklist = $data->{disklist};
782 my $programs = $data->{programs};
783 my $dumper_p = $programs->{dumper} ||= {};
785 if ( $type == $L_INFO ) {
786 return $self->_handle_info_line( "dumper", $str );
788 } elsif ( $type == $L_STRANGE ) {
790 my @info = Amanda::Util::split_quoted_strings($str);
791 my ( $hostname, $disk, $level ) = @info[ 0 .. 2 ];
792 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 4, 6, 8, 10 ];
793 $orig_kb =~ s{\]$}{};
795 my $dle = $disklist->{$hostname}->{$disk};
796 my $try = $self->_get_try( $dle, "dumper", $self->{'run_timestamp'});
797 my $dumper = $try->{dumper} ||= {};
798 $dumper->{level} = $level;
799 $dumper->{status} = 'strange';
800 $dumper->{sec} = $sec;
802 $dumper->{kps} = $kps;
803 $dumper->{orig_kb} = $orig_kb;
805 $self->{contline} = $dumper->{stranges} ||= [];
806 $dumper->{nb_stranges} = 0;
807 $self->{nbline_ref} = \$dumper->{nb_stranges};
809 return $self->{flags}{exit_status} |= STATUS_STRANGE
811 } elsif ( $type == $L_WARNING ) {
813 return $self->_handle_warning_line("dumper", $str);
815 } elsif ( $type == $L_SUCCESS ) {
817 my @info = Amanda::Util::split_quoted_strings($str);
818 my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
819 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 5, 7, 9, 11 ];
820 $orig_kb =~ s{\]$}{};
822 my $dle = $disklist->{$hostname}->{$disk};
823 my $try = $self->_get_try( $dle, "dumper", $timestamp );
824 my $dumper = $try->{dumper} ||= {};
826 $dumper->{date} = $timestamp;
827 $dumper->{level} = $level;
828 $dumper->{sec} = $sec;
830 $dumper->{kps} = $kps;
831 $dumper->{orig_kb} = $orig_kb;
833 return $dumper->{status} = "success";
835 } elsif ( $type == $L_ERROR ) {
836 return $self->_handle_error_line( "dumper", $str );
838 } elsif ( $type == $L_FATAL ) {
839 return $self->_handle_fatal_line( "dumper", $str );
841 } elsif ( $type == $L_FAIL ) {
842 return $self->_handle_fail_line( "dumper", $str );
845 return $self->_handle_bogus_line( $P_DUMPER, $type, $str );
850 sub _handle_chunker_line
853 my ( $type, $str ) = @_;
854 my $data = $self->{data};
855 my $disklist = $data->{disklist};
856 my $programs = $data->{programs};
857 my $chunker_p = $programs->{chunker} ||= {};
859 if ( $type == $L_INFO ) {
860 return $self->_handle_info_line( "chunker", $str );
862 } elsif ( $type == $L_SUCCESS || $type == $L_PARTIAL ) {
864 my @info = Amanda::Util::split_quoted_strings($str);
865 my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
866 my ( $sec, $kb, $kps ) = @info[ 5, 7, 9 ];
869 my $dle = $disklist->{$hostname}->{$disk};
870 my $try = $self->_get_try( $dle, "chunker", $timestamp );
871 my $chunker = $try->{chunker} ||= {};
873 $chunker->{date} = $timestamp;
874 $chunker->{level} = $level;
875 $chunker->{sec} = $sec;
876 $chunker->{kb} = $kb;
877 $chunker->{kps} = $kps;
879 return $chunker->{status} =
880 ( $type == $L_SUCCESS ) ? "success" : "partial";
882 } elsif ( $type == $L_ERROR ) {
883 return $self->_handle_error_line( "chunker", $str );
885 } elsif ( $type == $L_FATAL ) {
886 return $self->_handle_fatal_line( "chunker", $str );
888 } elsif ( $type == $L_FAIL ) {
889 return $self->_handle_fail_line( "chunker", $str );
892 return $self->_handle_bogus_line( $P_CHUNKER, $type, $str );
897 sub _handle_taper_line
900 my ( $type, $str ) = @_;
901 my $data = $self->{data};
902 my $disklist = $data->{disklist};
903 my $programs = $data->{programs};
904 my $taper_p = $programs->{taper} ||= {};
906 if ( $type == $L_START ) {
908 # START taper datestamp <start> label <label> tape <tapenum>
909 my @info = Amanda::Util::split_quoted_strings($str);
910 my ($datestamp, $label, $tapenum) = @info[ 1, 3, 5 ];
911 my $tape = $self->get_tape($label);
912 $tape->{date} = $datestamp;
913 $tape->{label} = $label;
915 # keep this tape for later
916 $self->{'_current_tape'} = $tape;
918 # call through to the generic start line function
919 $self->_handle_start_line( "taper", $str );
920 } elsif ( $type == $L_PART || $type == $L_PARTPARTIAL ) {
923 # <label> <tapefile> <hostname> <disk> <timestamp> <currpart>/<predparts> <level> [sec <sec> kb <kb> kps <kps>]
925 # format for $L_PARTPARTIAL is the same as $L_PART, plus <err> at the end
926 my @info = Amanda::Util::split_quoted_strings($str);
927 my ($label, $tapefile, $hostname, $disk, $timestamp) = @info[ 0 .. 4 ];
929 $info[5] =~ m{^(\d+)\/(-?\d+)$};
930 my ( $currpart, $predparts ) = ( $1, $2 );
932 my ($level, $sec, $kb, $kps, $orig_kb) = @info[ 6, 8, 10, 12, 14 ];
934 $orig_kb =~ s{\]$}{} if defined($orig_kb);
936 my $dle = $disklist->{$hostname}{$disk};
937 my $try = $self->_get_try($dle, "taper", $timestamp);
938 my $taper = $try->{taper} ||= {};
939 my $parts = $taper->{parts} ||= [];
948 partnum => $currpart,
951 $taper->{orig_kb} = $orig_kb;
955 my $tape = $self->get_tape($label);
956 # count this as a filesystem if this is the first part
957 $tape->{dle}++ if $currpart == 1;
959 $tape->{time} += $sec;
962 } elsif ( $type == $L_DONE || $type == $L_PARTIAL ) {
965 # $type = DONE | PARTIAL
966 # $type taper <hostname> <disk> <timestamp> <part> <level> [sec <sec> kb <kb> kps <kps>]
967 my @info = Amanda::Util::split_quoted_strings($str);
968 my ( $hostname, $disk, $timestamp, $part_ct, $level ) = @info[ 0 .. 4 ];
969 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 6, 8, 10, 12 ];
971 if ($type == $L_PARTIAL) {
973 $error = join " ", @info[ 11 .. $#info ];
975 $error = join " ", @info[ 13 .. $#info ];
979 $orig_kb =~ s{\]$}{} if defined $orig_kb;
981 my $dle = $disklist->{$hostname}->{$disk};
982 my $try = $self->_get_try($dle, "taper", $timestamp);
983 my $taper = $try->{taper} ||= {};
984 my $parts = $taper->{parts};
986 if ($part_ct - $#$parts != 1) {
987 ## this should always be true; do nothing right now
990 $taper->{level} = $level;
991 $taper->{sec} = $sec;
993 $taper->{kps} = $kps;
995 $taper->{status} = ( $type == $L_DONE ) ? "done" : "partial";
996 $taper->{error} = $error if $type == $L_PARTIAL;
998 } elsif ( $type == $L_INFO ) {
999 $self->_handle_info_line("taper", $str);
1001 } elsif ( $type == $L_WARNING ) {
1002 $self->_handle_warning_line("taper", $str);
1004 } elsif ( $type == $L_ERROR ) {
1006 if ($str =~ m{^no-tape}) {
1008 my @info = Amanda::Util::split_quoted_strings($str);
1009 my $failure_from = $info[1];
1010 my $error = join " ", @info[ 2 .. $#info ];
1012 $self->{flags}{exit_status} |= STATUS_TAPE;
1013 $self->{flags}{degraded_mode} = 1;
1014 $taper_p->{failure_from} = $failure_from;
1015 $taper_p->{tape_error} = $error;
1018 $self->_handle_error_line("taper", $str);
1021 } elsif ( $type == $L_FATAL ) {
1022 return $self->_handle_fatal_line( "taper", $str );
1024 } elsif ( $type == $L_FAIL ) {
1025 $self->_handle_fail_line( "taper", $str );
1028 $self->_handle_bogus_line( $P_TAPER, $type, $str );
1033 sub _handle_amflush_line
1035 my $self = shift @_;
1036 my ( $type, $str ) = @_;
1037 my $data = $self->{data};
1038 my $disklist = $data->{disklist};
1039 my $programs = $data->{programs};
1040 my $amflush_p = $programs->{amflush} ||= {};
1042 if ( $type == $L_DISK ) {
1043 return $self->_handle_disk_line( "amflush", $str );
1045 } elsif ( $type == $L_START ) {
1046 return $self->_handle_start_line( "amflush", $str );
1048 } elsif ( $type == $L_INFO ) {
1049 return $self->_handle_info_line( "amflush", $str );
1052 return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
1056 sub _handle_amvault_line
1058 my $self = shift @_;
1059 my ( $type, $str ) = @_;
1060 my $data = $self->{data};
1061 my $disklist = $data->{disklist};
1062 my $programs = $data->{programs};
1063 my $amvault_p = $programs->{amvault} ||= {};
1065 if ( $type == $L_START ) {
1066 return $self->_handle_start_line( "amvault", $str );
1068 } elsif ( $type == $L_INFO ) {
1069 return $self->_handle_info_line( "amvault", $str );
1071 } elsif ( $type == $L_ERROR ) {
1072 return $self->_handle_error_line( "amvault", $str );
1074 } elsif ( $type == $L_FATAL ) {
1075 return $self->_handle_fatal_line( "amvault", $str );
1077 } elsif ( $type == $L_DISK ) {
1078 return $self->_handle_disk_line( "amvault", $str );
1081 return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
1086 sub _handle_amdump_line
1089 my ( $type, $str ) = @_;
1090 my $data = $self->{data};
1091 my $disklist = $data->{disklist};
1092 my $programs = $data->{programs};
1093 my $amdump = $programs->{amdump} ||= {};
1095 if ( $type == $L_INFO ) {
1096 $self->_handle_info_line("amdump", $str);
1098 } elsif ( $type == $L_START ) {
1099 $self->_handle_start_line("amdump", $str);
1101 } elsif ( $type == $L_FATAL ) {
1102 return $self->_handle_fatal_line( "amdump", $str );
1104 } elsif ( $type == $L_ERROR ) {
1105 $self->_handle_error_line("amdump", $str);
1110 sub _handle_fail_line
1112 my ($self, $program, $str) = @_;
1114 my @info = Amanda::Util::split_quoted_strings($str);
1115 my ($hostname, $disk, $timestamp, $level) = @info;
1118 if ($program eq 'taper') {
1119 $failure_from = $info[4];
1120 $error = join " ", @info[ 5 .. $#info ];
1122 $error = join " ", @info[ 4 .. $#info ];
1125 #TODO: verify that this reaches the right try. Also, DLE or
1127 my $dle = $self->get_dle_info($hostname, $disk);
1130 if ($program eq "planner" ||
1131 $program eq "driver") {
1132 $program_d = $dle->{$program} ||= {};
1134 my $try = $self->_get_try($dle, $program, $timestamp);
1135 $program_d = $try->{$program} ||= {};
1138 $program_d->{level} = $level;
1139 $program_d->{status} = "fail";
1140 $program_d->{failure_from} = $failure_from;
1141 $program_d->{error} = $error;
1143 my $errors = $self->get_program_info("program", "errors", []);
1144 push @$errors, $error;
1146 $self->{flags}{exit_status} |= STATUS_FAILED;
1147 if ($program eq "dumper") {
1148 $self->{contline} = $program_d->{errors} ||= [];
1149 $program_d->{nb_errors} = 0;
1150 $self->{nbline_ref} = \$program_d->{nb_errors};
1155 sub _handle_error_line
1157 my $self = shift @_;
1158 my ( $program, $str ) = @_;
1160 my $data = $self->{data};
1161 my $programs = $data->{programs};
1162 my $program_p = $programs->{$program};
1163 my $errors_p = $program_p->{errors} ||= [];
1165 $self->{flags}{exit_status} |= 1;
1167 push @$errors_p, $str;
1171 sub _handle_fatal_line
1173 my $self = shift @_;
1174 my ( $program, $str ) = @_;
1176 my $data = $self->{data};
1177 my $programs = $data->{programs};
1178 my $program_p = $programs->{$program};
1179 my $fatal_p = $program_p->{fatal} ||= [];
1181 $self->{flags}{exit_status} |= 1;
1183 push @$fatal_p, $str;
1187 sub _handle_start_line
1189 my $self = shift @_;
1190 my ( $program, $str ) = @_;
1192 my $data = $self->{data};
1193 my $disklist = $data->{disklist};
1194 my $programs = $data->{programs};
1196 my $program_p = $programs->{$program} ||= {};
1198 my @info = Amanda::Util::split_quoted_strings($str);
1199 my $timestamp = $info[1];
1200 $program_p->{start} = $info[1];
1202 if ($self->{'run_timestamp'} ne '00000000000000'
1203 and $self->{'run_timestamp'} ne $timestamp) {
1204 warning("not all timestamps in this file are the same; "
1205 . "$self->{run_timestamp}; $timestamp");
1207 $self->{'run_timestamp'} = $timestamp;
1211 sub _handle_disk_line
1213 my $self = shift @_;
1214 my ($program, $str) = @_;
1216 my $data = $self->{data};
1217 my $disklist = $data->{disklist};
1218 my $hosts = $self->{cache}{hosts} ||= [];
1219 my $dles = $self->{cache}{dles} ||= [];
1221 my @info = Amanda::Util::split_quoted_strings($str);
1222 my ($hostname, $disk) = @info;
1224 if (!exists $disklist->{$hostname}) {
1226 $disklist->{$hostname} = {};
1227 push @$hosts, $hostname;
1230 if (!exists $disklist->{$hostname}{$disk}) {
1232 push @$dles, [ $hostname, $disk ];
1233 my $dle = $disklist->{$hostname}{$disk} = {};
1234 $dle->{'estimate'} = undef;
1235 $dle->{'dumps'} = {};
1240 sub _handle_success_line
1242 my $self = shift @_;
1243 my ($program, $str) = @_;
1245 my $data = $self->{data};
1246 my $disklist = $data->{disklist};
1247 my $hosts = $self->{cache}{hosts} ||= [];
1248 my $dles = $self->{cache}{dles} ||= [];
1250 my @info = Amanda::Util::split_quoted_strings($str);
1251 my ($hostname, $disk, $timestamp, $level, $stat1, $stat2) = @info;
1253 if ($stat1 =~ /skipped/) {
1254 $disklist->{$hostname}{$disk}->{$program}->{'status'} = 'skipped';
1260 sub _handle_info_line
1262 my $self = shift @_;
1263 my ( $program, $str ) = @_;
1265 my $data = $self->{data};
1266 my $disklist = $data->{disklist};
1267 my $programs = $data->{programs};
1269 my $program_p = $programs->{$program} ||= {};
1271 if ( $str =~ m/^\w+ pid \d+/ || $str =~ m/^pid-done \d+/ ) {
1273 #do not report pid lines
1277 my $notes = $program_p->{notes} ||= [];
1282 sub _handle_warning_line
1284 my $self = shift @_;
1285 my ( $program, $str ) = @_;
1287 $self->_handle_info_line($program, $str);
1290 sub _handle_bogus_line
1292 my $self = shift @_;
1293 my ( $prog, $type, $str ) = @_;
1295 my $data = $self->{data};
1296 my $boguses = $data->{boguses} ||= [];
1297 push @$boguses, [ $prog, $type, $str ];
1300 sub check_missing_fail_strange
1303 my @dles = $self->get_dles();
1305 foreach my $dle_entry (@dles) {
1306 my $alldumps = $self->get_dle_info(@$dle_entry, 'dumps');
1307 my $planner = $self->get_dle_info(@$dle_entry, 'planner');
1309 if ($planner && $planner->{'status'} eq 'fail') {
1310 $self->{flags}{dump_failed} = 1;
1311 } elsif ($planner && $planner->{'status'} eq 'skipped') {
1312 # We don't want these to be counted as missing below
1313 } elsif (!defined $alldumps->{$self->{'run_timestamp'}}) {
1314 $self->{flags}{results_missing} = 1;
1315 $self->{flags}{exit_status} |= STATUS_MISSING;
1318 my $tries = $alldumps->{$self->{'run_timestamp'}};
1319 my $try = @$tries[-1];
1321 if (exists $try->{dumper} && $try->{dumper}->{status} eq 'fail') {
1322 $self->{flags}{dump_failed} = 1;
1323 } elsif ((defined($try->{'chunker'}) &&
1324 $try->{'chunker'}->{status} eq 'success') ||
1325 (defined($try->{'taper'}) &&
1326 $try->{'taper'}->{status} eq 'done')) {
1327 #chunker or taper success, use dumper status
1328 if (exists $try->{dumper} && $try->{dumper}->{status} eq 'strange') {
1329 $self->{flags}{dump_strange} = 1;
1332 #chunker or taper failed, the dump is not valid.
1333 $self->{flags}{dump_failed} = 1;
1340 # NOTE: there may be a complicated state diagram lurking in the midst
1341 # of taper and chunker. You have been warned.
1345 my $self = shift @_;
1346 my ( $dle, $program, $timestamp ) = @_;
1347 my $tries = $dle->{'dumps'}{$timestamp} ||= [];
1351 || defined $tries->[-1]->{$program}->{status}
1352 && $self->_program_finished( # program has finished
1353 $program, $tries->[-1]->{$program}->{status}
1358 return $tries->[-1];
1362 sub _program_finished
1364 my $self = shift @_;
1365 my ( $program, $status ) = @_;
1367 if ( $program eq "chunker" ) {
1369 if ( $status eq "partial" ) {
1375 } elsif ( $status eq "done"
1376 || $status eq "success"
1377 || $status eq "fail"
1378 || $status eq "partial" ) {