1 # Copyright (c) 2010-2012 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
20 package Amanda::Report;
26 use Amanda::Logfile qw/:logtype_t :program_t/;
28 use Amanda::Debug qw( debug warning );
32 Amanda::Report -- module for representing report data from logfiles
38 my $report = Amanda::Report->new($logfile);
39 my @hosts = keys %{$report->{data}{disklist}};
43 This module reads the logfile passed to it and aggregates the data in
44 a format of nested hashes for convenient output. All data read in is
45 stored in C<< $report->{data} >>.
47 =head2 Creating a Report
49 my $report = Amanda::Report->new($logfile, $historical);
51 The constructor reads the logfile and produces the report, which can then be
52 queried with the other methods. C<$logfile> should specify the path to the
53 logfile from which the report is prepared. If the logfile is not the "current"
54 logfile, then C<$historical> should be false. Non-historical reports may draw
55 information from the current Amanda environment, e.g., holding disks and info
58 =head2 Summary Information
60 Note that most of the data provided by these methods is simply a reference to
61 data stored within the report, and should thus be considered read-only. For
62 example, do not use C<shift> or C<pop> to destructively consume lists.
64 my $datestamp = $report->get_timestamp();
66 This returns the run timestamp for this dump run. This is determined from one
67 of several START entries. This returns a full 14-digit timestamp regardless of
68 the setting of C<usetimestamps> now or during the dump run.
70 my @hosts = $report->get_hosts();
72 This method returns a list containing the hosts that have been seen in
73 a logfile. In a scalar context, C<get_hosts> returns the number of
76 my @disks = $report->get_disks($hostname);
78 This method returns a list of disks that were archived under the given
79 C<$hostname>. In a scalar context, this method returns the number of
80 disks seen, belonging to the hostname.
82 my @dles = $report->get_dles();
84 This method returns a list of list references. Each referenced list
85 contains a hostname & disk pair that has been reported by either the
86 planner or amflush. The DLEs are stored in the order that they appear
90 [ 'example1', '/home' ],
91 [ 'example1', '/var/log' ],
92 [ 'example2', '/etc' ],
93 [ 'example2', '/home' ],
94 [ 'example3', '/var/www' ],
97 if ( $report->get_flag($flag) ) { ... }
99 The C<get_flag> method accesses a number of flags that represent the state of
100 the dump. A true value is returned if the flag is set, and undef otherwise.
101 The available flags are:
107 This flag is true when the driver finished
108 correctly. It indicates that the dump run has finished and cleaned
111 =item C<degraded_mode>
113 This flag is set if the taper encounters an
114 error that forces it into degraded mode.
118 This flag is set if amflush is run instead of planner.
122 This flag is set if the run was by amvault.
126 This flag is set when planner is run. Its value
127 should be opposite of C<amflush_run>.
133 =item C<dump_strange>
135 If a dump end in strange result.
137 =item C<results_missing>
139 If this was a normal run, but some DLEs named by the
140 planner do not have any results, then this flag is set. Users should look for
141 DLEs with an empty C<dump> key to enumerate the missing results.
145 This flag is set if this is a "historical" report. It is
146 based on the value passed to the constructor.
152 my $dle = $report->get_dle_info($hostname, $disk [,$field] );
154 This method returns the DLE information for the given C<$hostname> and C<disk>,
155 or if C<$field> is given, returns that field of the DLE information. See the
156 DATA DESCRIPTION section for the format of this information.
158 my $info = $report->get_program_info($program [,$field] );
160 This method returns the program information for the given C<$program>, or if
161 C<$field> is given, returns that field of the DLE information. See the DATA
162 DESCRIPTION section for the format of this information.
164 =head1 DATA DESCRIPTION
168 The data in the logfile is stored in the module at C<< $report->{data} >>.
169 Beneath that, there are a number of subdivisions that track both global and
170 per-host status of the given Amanda run that the logfile represents. Note that
171 these subdivisions are usually accessed via C<get_dle_info> and
172 C<get_program_info>, as described above.
176 the C<programs> key of the data points to a hash of global program
177 information, with one element per program. See the Programs section, below.
181 The C<boguses> key refers to a list of arrayrefs of the form
185 as returned directly by C<Amanda::Logfile::get_logline>. These lines are not
186 in a recognized trace log format.
190 The C<disklist> key points to a two-level hash of hostnames and
191 disknames as present in the logfile. It looks something like this:
193 $report->{data}{disklist} = {
194 "server.example.org" => {
198 "workstation.example.org" => {
204 Each C<{...}> in the above contains information about the corresponding DLE. See DLEs, below.
208 Each program involved in a dump has a hash giving information about its
209 performance during the run. A number of fields are common across all of the
216 the numeric timestamp at which the process was started.
220 the length of time (in seconds) that the program ran.
224 a list which stores all notes reported to the logfile
225 by the corresponding program.
229 a list which stores all errors reported to the
230 logfile by the corresponding program.
234 Program-specific fields are described in the following sections.
238 The planner logs very little information other than determining what will be
239 backed up. It has no special fields other than those given above.
243 The driver has one field that the other program-specific
248 =item C<start_time> - the time it takes for the driver to start up.
252 =head3 amflush and amdump
256 =head3 dumper and chunker
258 Most of the chunker's output and the dumper's output can be tied to a
259 particular DLE, so their C<programs> hashes are limited to C<notes> and
264 The taper hash holds notes and errors for the per-instance runs of the taper
265 program, but also tracks the tapes seen in the logfile:
271 This field is a hash reference keyed by the label of the tape.
272 each value of the key is another hash which stores date, size, and the
273 number of files seen by this backup on the tape. For example:
275 $report->{data}{programs}{taper}{tapes} = {
277 label => "FakeTape01",
278 date => "20100318141930",
279 kb => 7894769, # data written to tape this session
280 files => 14, # parts written to tape this session
281 dle => 13, # number of dumps that begin on this tape
282 time => 2.857, # time spent writing to this tape
288 The C<tape_labels> field is a reference to a list which records the
289 order that the tapes have been seen. This list should be used as an
290 ordered index for C<tapes>.
296 In the below, C<$dle> is the hash representing one disklist entry.
298 The C<estimate> key describes the estimate given by the planner. For
302 level => 0, # the level of the backup
303 sec => 20, # estimated time to back up (seconds)
304 nkb => 2048, # expected uncompressed size (kb)
305 ckb => 1293, # expected compressed size (kb)
306 kps => 934.1, # speed of the backup (kb/sec)
309 Each dump of the DLE is represented in C<< $dle->{dumps} >>. This is a hash,
310 keyed by dump timestamp with a list of tries as the value for each dump. Each
311 try represents a specific attempt to finish writing this dump to a volume. If
312 an error occurs during the backup of a DLE and is retried, a second try is
313 pushed to the tries list. For example:
316 '20100317142122' => [ $try1 ],
317 '20100318141930' => [ $try1, $try2 ],
322 A try is a hash with at least one dumper, taper, and/or chunker DLE program as
323 a key. These entries contain the results from the associated program during
326 There are a number of common fields between all three elements:
332 a timestamp of when the program finished (if the program exited)
336 the status of the dump at this program on this try ("success", "partial",
337 "done", or "failed"). The planner adds an extra "skipped" status which is
338 added when the planner decides to skip a DLE due to user configuration (e.g.,
343 the incremental level of the backup.
347 the time in seconds for the program to finish.
351 the size of the data dumped in kb.
355 the rate at which the program was able to process data,
360 if the program fails, this field contains the error message
364 The C<dumper> hash has an C<orig_kb> field, giving the size of the data dumped
365 from the source, before any compression. If encountered, the C<dumper> hash may
366 also contain a C<stranges> field, which is a list of all the messages of type
367 C<L_STRANGE> encountered during the process.
369 The C<taper> hash contains all the exit status data given by the taper.
370 Because the same taper process handles multiple dumps, it does not have a
371 C<date> field. However, the taper does have an additional field, C<parts>,
372 containing a list of parts written for this dump.
376 Each item in the list of taper parts is a hash with the following
383 the name of the tape that the part was written to.
387 the datestamp at which this part was written.
391 the filename of the part.
395 the sequence number of the part for the DLE that the
400 the length of time, in seconds, that the part took to
405 the total size of the part.
409 the speed at which the part was written.
415 use constant STATUS_STRANGE => 2;
416 use constant STATUS_FAILED => 4;
417 use constant STATUS_MISSING => 8;
418 use constant STATUS_TAPE => 16;
422 my $class = shift @_;
423 my ($logfname, $historical) = @_;
429 _logfname => $logfname,
430 _historical => $historical,
432 ## logfile-parsing state
434 # the tape currently being writen
435 _current_tape => undef,
447 my $data = $self->{data} = {};
448 my $logfname = $self->{_logfname};
450 # clear the program and DLE data
451 $data->{programs} = {};
452 $data->{disklist} = {};
455 $self->{run_timestamp} = '00000000000000';
457 my $logfh = Amanda::Logfile::open_logfile($logfname)
458 or die "cannot open '$logfname': $!";
460 $self->{flags}{exit_status} = 0;
461 $self->{flags}{results_missing} = 0;
462 $self->{flags}{dump_failed} = 0;
463 $self->{flags}{dump_strange} = 0;
465 while ( my ( $type, $prog, $str ) = Amanda::Logfile::get_logline($logfh) ) {
466 $self->read_line( $type, $prog, $str );
469 ## set post-run flags
471 $self->{flags}{historical} = $self->{_historical};
472 $self->{flags}{amflush_run} = 0;
473 $self->{flags}{amvault_run} = 0;
474 if (!$self->get_flag("normal_run")) {
475 if ( ( defined $self->get_program_info("amflush") )
476 && ( scalar %{ $self->get_program_info("amflush") } ) ) {
477 debug("detected an amflush run");
478 $self->{flags}{amflush_run} = 1;
479 } elsif ( ( defined $self->get_program_info("amvault") )
480 && ( scalar %{ $self->get_program_info("amvault") } ) ) {
481 debug("detected an amvault run");
482 $self->{flags}{amvault_run} = 1;
486 # check for missing, fail and strange results
487 $self->check_missing_fail_strange() if $self->get_flag('normal_run');
489 # clean up any temporary values in the data
497 #remove last_label field
498 foreach my $dle ($self->get_dles()) {
499 my $dle_info = $self->get_dle_info(@$dle);
500 delete $dle_info->{last_label};
510 my ( $type, $prog, $str ) = @_;
512 if ( $type == $L_CONT ) {
513 ${$self->{nbline_ref}}++;
515 $self->{nb_strange}++;
516 push @{$self->{contline}}, $str if $self->{nb_strange} + $self->{nb_error} <= 100;
517 } elsif ($str =~ /^\?/) {
519 push @{$self->{contline}}, $str if $self->{nb_error} <= 100;
521 $self->{nb_normal}++;
522 push @{$self->{contline}}, $str if ${$self->{nbline_ref}} <= 100;
526 $self->{contline} = undef;
527 $self->{nb_normal} = 0;
528 $self->{nb_strange} = 0;
529 $self->{nb_error} = 0;
531 if ( $prog == $P_PLANNER ) {
532 return $self->_handle_planner_line( $type, $str );
534 } elsif ( $prog == $P_DRIVER ) {
535 return $self->_handle_driver_line( $type, $str );
537 } elsif ( $prog == $P_DUMPER ) {
538 return $self->_handle_dumper_line( $type, $str );
540 } elsif ( $prog == $P_CHUNKER ) {
541 return $self->_handle_chunker_line( $type, $str );
543 } elsif ( $prog == $P_TAPER ) {
544 return $self->_handle_taper_line( $type, $str );
546 } elsif ( $prog == $P_AMFLUSH ) {
547 return $self->_handle_amflush_line( $type, $str );
549 } elsif ( $prog == $P_AMVAULT ) {
550 return $self->_handle_amvault_line( $type, $str );
552 } elsif ( $prog == $P_AMDUMP ) {
553 return $self->_handle_amdump_line( $type, $str );
555 } elsif ( $prog == $P_REPORTER ) {
556 return $self->_handle_reporter_line( $type, $str );
559 return $self->_handle_bogus_line( $prog, $type, $str );
566 return $self->{'run_timestamp'};
572 my $cache = $self->{cache};
574 $cache->{hosts} = [ keys %{ $self->{data}{disklist} } ]
575 if ( !defined $cache->{hosts} );
577 return @{ $cache->{hosts} };
584 return keys %{ $self->{data}{disklist}{$hostname} };
590 my $cache = $self->{cache};
593 if ( !defined $cache->{dles} ) {
594 foreach my $hostname ( $self->get_hosts() ) {
595 map { push @dles, [ $hostname, $_ ] } $self->get_disks($hostname);
597 $cache->{dles} = \@dles;
599 return @{ $cache->{dles} };
604 my ( $self, $org, $config ) = @_;
605 use Amanda::Report::xml;
606 return Amanda::Report::xml::make_amreport_xml( $self, $org, $config );
612 my ( $hostname, $disk, $field ) = @_;
614 return ( defined $field )
615 ? $self->{data}{disklist}{$hostname}{$disk}{$field}
616 : $self->{data}{disklist}{$hostname}{$disk};
621 my ($self, $program, $field, $default) = @_;
622 my $prog = $self->{data}{programs}{$program};
624 $prog->{$field} = $default if (defined $field && !defined $prog->{$field});
626 return (defined $field) ? $prog->{$field} : $prog;
631 my ($self, $label) = @_;
633 my $taper = $self->get_program_info("taper");
634 my $tapes = $taper->{tapes} ||= {};
635 my $tape_labels = $taper->{tape_labels} ||= [];
637 if (!exists $tapes->{$label}) {
638 push @$tape_labels, $label;
639 $tapes->{$label} = {date => "",
646 return $tapes->{$label};
651 my ( $self, $flag ) = @_;
652 return $self->{flags}{$flag};
655 sub _handle_planner_line
658 my ( $type, $str ) = @_;
659 my $data = $self->{data};
660 my $programs = $data->{programs};
661 my $disklist = $data->{disklist} ||= {};
662 my $planner = $programs->{planner} ||= {};
664 if ( $type == $L_INFO ) {
665 return $self->_handle_info_line( "planner", $str );
667 } elsif ( $type == $L_WARNING ) {
668 return $self->_handle_warning_line( "planner", $str );
670 } elsif ( $type == $L_START ) {
672 $self->{flags}{normal_run} = 1;
673 return $self->_handle_start_line( "planner", $str );
675 } elsif ( $type == $L_FINISH ) {
677 my @info = Amanda::Util::split_quoted_strings($str);
678 return $planner->{time} = $info[3];
680 } elsif ( $type == $L_DISK ) {
681 return $self->_handle_disk_line( "planner", $str );
683 } elsif ( $type == $L_SUCCESS ) {
684 return $self->_handle_success_line( "planner", $str );
686 } elsif ( $type == $L_ERROR ) {
687 return $self->_handle_error_line( "planner", $str );
689 } elsif ( $type == $L_FATAL ) {
690 return $self->_handle_fatal_line( "planner", $str );
692 } elsif ( $type == $L_FAIL ) {
694 # TODO: these are not like other failure messages: later
696 return $self->_handle_fail_line( "planner", $str );
699 return $self->_handle_bogus_line( $P_PLANNER, $type, $str );
704 sub _handle_driver_line
707 my ( $type, $str ) = @_;
708 my $data = $self->{data};
709 my $disklist = $data->{disklist};
710 my $programs = $data->{programs};
711 my $driver_p = $programs->{driver} ||= {};
713 if ( $type == $L_INFO ) {
714 return $self->_handle_info_line( "driver", $str );
716 } elsif ( $type == $L_START ) {
717 return $self->_handle_start_line( "driver", $str );
719 } elsif ( $type == $L_FINISH ) {
721 my @info = Amanda::Util::split_quoted_strings($str);
722 $self->{flags}{got_finish} = 1;
723 return $driver_p->{time} = $info[3];
725 } elsif ( $type == $L_STATS ) {
727 my @info = Amanda::Util::split_quoted_strings($str);
728 if ( $info[0] eq "hostname" ) {
730 return $self->{hostname} = $info[1];
732 } elsif ( $info[0] eq "startup" ) {
734 my @info = Amanda::Util::split_quoted_strings($str);
735 return $driver_p->{start_time} = $info[2];
737 } elsif ( $info[0] eq "estimate" ) {
740 # STATS driver estimate <hostname> <disk> <timestamp>
741 # <level> [sec <sec> nkb <nkb> ckb <ckb> jps <kps>]
742 # note that the [..] section is *not* quoted properly
743 my ($hostname, $disk, $timestamp, $level) = @info[ 1 .. 4 ];
745 # if the planner didn't define the DLE then this is a bad
747 unless (exists $disklist->{$hostname}{$disk}) {
748 return $self->_handle_bogus_line($P_DRIVER, $type, $str);
751 my $dle = $self->get_dle_info($hostname, $disk);
752 my ($sec, $nkb, $ckb, $kps) = @info[ 6, 8, 10, 12 ];
753 $kps =~ s{\]}{}; # strip trailing "]"
764 return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
767 } elsif ( $type == $L_WARNING ) {
769 $self->{flags}{exit_status} |= STATUS_TAPE
770 if ($str eq "Taper protocol error");
772 return $self->_handle_warning_line("driver", $str);
774 } elsif ( $type == $L_ERROR ) {
775 return $self->_handle_error_line( "driver", $str );
777 } elsif ( $type == $L_FATAL ) {
778 return $self->_handle_fatal_line( "driver", $str );
780 } elsif ( $type == $L_FAIL ) {
781 return $self->_handle_fail_line( "driver", $str );
784 return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
789 sub _handle_dumper_line
792 my ( $type, $str ) = @_;
793 my $data = $self->{data};
794 my $disklist = $data->{disklist};
795 my $programs = $data->{programs};
796 my $dumper_p = $programs->{dumper} ||= {};
798 if ( $type == $L_INFO ) {
799 return $self->_handle_info_line( "dumper", $str );
801 } elsif ( $type == $L_STRANGE ) {
803 my @info = Amanda::Util::split_quoted_strings($str);
804 my ( $hostname, $disk, $level ) = @info[ 0 .. 2 ];
805 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 4, 6, 8, 10 ];
806 $kb = int($kb/1024) if $info[4] eq 'bytes';
807 $orig_kb =~ s{\]$}{};
809 my $dle = $disklist->{$hostname}->{$disk};
810 my $try = $self->_get_try( $dle, "dumper", $self->{'run_timestamp'});
811 my $dumper = $try->{dumper} ||= {};
812 $dumper->{level} = $level;
813 $dumper->{status} = 'strange';
814 $dumper->{sec} = $sec;
816 $dumper->{kps} = $kps;
817 $dumper->{orig_kb} = $orig_kb;
819 $self->{contline} = $dumper->{stranges} ||= [];
820 $dumper->{nb_stranges} = 0;
821 $self->{nbline_ref} = \$dumper->{nb_stranges};
822 $self->{nb_normal} = 0;
823 $self->{nb_strange} = 0;
824 $self->{nb_error} = 0;
826 return $self->{flags}{exit_status} |= STATUS_STRANGE
828 } elsif ( $type == $L_WARNING ) {
830 return $self->_handle_warning_line("dumper", $str);
832 } elsif ( $type == $L_SUCCESS ) {
834 my @info = Amanda::Util::split_quoted_strings($str);
835 my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
836 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 5, 7, 9, 11 ];
837 $kb = int($kb/1024) if $info[6] eq 'bytes';
838 $orig_kb =~ s{\]$}{};
840 my $dle = $disklist->{$hostname}->{$disk};
841 my $try = $self->_get_try( $dle, "dumper", $timestamp );
842 my $dumper = $try->{dumper} ||= {};
844 $dumper->{date} = $timestamp;
845 $dumper->{level} = $level;
846 $dumper->{sec} = $sec;
848 $dumper->{kps} = $kps;
849 $dumper->{orig_kb} = $orig_kb;
851 return $dumper->{status} = "success";
853 } elsif ( $type == $L_ERROR ) {
854 return $self->_handle_error_line( "dumper", $str );
856 } elsif ( $type == $L_FATAL ) {
857 return $self->_handle_fatal_line( "dumper", $str );
859 } elsif ( $type == $L_FAIL ) {
860 return $self->_handle_fail_line( "dumper", $str );
863 return $self->_handle_bogus_line( $P_DUMPER, $type, $str );
868 sub _handle_chunker_line
871 my ( $type, $str ) = @_;
872 my $data = $self->{data};
873 my $disklist = $data->{disklist};
874 my $programs = $data->{programs};
875 my $chunker_p = $programs->{chunker} ||= {};
877 if ( $type == $L_INFO ) {
878 return $self->_handle_info_line( "chunker", $str );
880 } elsif ( $type == $L_SUCCESS || $type == $L_PARTIAL ) {
882 my @info = Amanda::Util::split_quoted_strings($str);
883 my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
884 my ( $sec, $kb, $kps ) = @info[ 5, 7, 9 ];
885 $kb = int($kb/1024) if $info[6] eq 'bytes';
888 my $dle = $disklist->{$hostname}->{$disk};
889 my $try = $self->_get_try( $dle, "chunker", $timestamp );
890 my $chunker = $try->{chunker} ||= {};
892 $chunker->{date} = $timestamp;
893 $chunker->{level} = $level;
894 $chunker->{sec} = $sec;
895 $chunker->{kb} = $kb;
896 $chunker->{kps} = $kps;
898 return $chunker->{status} =
899 ( $type == $L_SUCCESS ) ? "success" : "partial";
901 } elsif ( $type == $L_ERROR ) {
902 return $self->_handle_error_line( "chunker", $str );
904 } elsif ( $type == $L_FATAL ) {
905 return $self->_handle_fatal_line( "chunker", $str );
907 } elsif ( $type == $L_FAIL ) {
908 return $self->_handle_fail_line( "chunker", $str );
911 return $self->_handle_bogus_line( $P_CHUNKER, $type, $str );
916 sub _handle_taper_line
919 my ( $type, $str ) = @_;
920 my $data = $self->{data};
921 my $disklist = $data->{disklist};
922 my $programs = $data->{programs};
923 my $taper_p = $programs->{taper} ||= {};
925 if ( $type == $L_START ) {
927 # START taper datestamp <start> label <label> tape <tapenum>
928 my @info = Amanda::Util::split_quoted_strings($str);
929 my ($datestamp, $label, $tapenum) = @info[ 1, 3, 5 ];
930 my $tape = $self->get_tape($label);
931 $tape->{date} = $datestamp;
932 $tape->{label} = $label;
934 # keep this tape for later
935 $self->{'_current_tape'} = $tape;
937 # call through to the generic start line function
938 $self->_handle_start_line( "taper", $str );
939 } elsif ( $type == $L_PART || $type == $L_PARTPARTIAL ) {
942 # <label> <tapefile> <hostname> <disk> <timestamp> <currpart>/<predparts> <level> [sec <sec> kb <kb> kps <kps>]
944 # format for $L_PARTPARTIAL is the same as $L_PART, plus <err> at the end
945 my @info = Amanda::Util::split_quoted_strings($str);
946 my ($label, $tapefile, $hostname, $disk, $timestamp) = @info[ 0 .. 4 ];
948 $info[5] =~ m{^(\d+)\/(-?\d+)$};
949 my ( $currpart, $predparts ) = ( $1, $2 );
951 my ($level, $sec, $kb, $kps, $orig_kb) = @info[ 6, 8, 10, 12, 14 ];
952 $kb = int($kb/1024) if $info[9] eq 'bytes';
954 $orig_kb =~ s{\]$}{} if defined($orig_kb);
956 my $dle = $disklist->{$hostname}{$disk};
957 my $try = $self->_get_try($dle, "taper", $timestamp);
958 my $taper = $try->{taper} ||= {};
959 my $parts = $taper->{parts} ||= [];
968 partnum => $currpart,
971 $taper->{orig_kb} = $orig_kb;
975 my $tape = $self->get_tape($label);
976 # count this as a filesystem if this is the first part
977 $tape->{dle}++ if $currpart == 1;
979 $tape->{time} += $sec;
982 } elsif ( $type == $L_DONE || $type == $L_PARTIAL ) {
985 # $type = DONE | PARTIAL
986 # $type taper <hostname> <disk> <timestamp> <part> <level> [sec <sec> kb <kb> kps <kps>]
987 my @info = Amanda::Util::split_quoted_strings($str);
988 my ( $hostname, $disk, $timestamp, $part_ct, $level ) = @info[ 0 .. 4 ];
989 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 6, 8, 10, 12 ];
990 $kb = int($kb/1024) if $info[7] eq 'bytes';
992 if ($type == $L_PARTIAL) {
994 $error = join " ", @info[ 11 .. $#info ];
996 $error = join " ", @info[ 13 .. $#info ];
1000 $orig_kb =~ s{\]$}{} if defined $orig_kb;
1002 my $dle = $disklist->{$hostname}->{$disk};
1003 my $try = $self->_get_try($dle, "taper", $timestamp);
1004 my $taper = $try->{taper} ||= {};
1005 my $parts = $taper->{parts};
1007 if ($part_ct - $#$parts != 1) {
1008 ## this should always be true; do nothing right now
1011 $taper->{level} = $level;
1012 $taper->{sec} = $sec;
1014 $taper->{kps} = $kps;
1016 $taper->{status} = ( $type == $L_DONE ) ? "done" : "partial";
1017 $taper->{error} = $error if $type == $L_PARTIAL;
1019 } elsif ( $type == $L_INFO ) {
1020 $self->_handle_info_line("taper", $str);
1022 } elsif ( $type == $L_WARNING ) {
1023 $self->_handle_warning_line("taper", $str);
1025 } elsif ( $type == $L_ERROR ) {
1027 if ($str =~ m{^no-tape}) {
1029 my @info = Amanda::Util::split_quoted_strings($str);
1030 my $failure_from = $info[1];
1031 my $error = join " ", @info[ 2 .. $#info ];
1033 $self->{flags}{exit_status} |= STATUS_TAPE;
1034 $self->{flags}{degraded_mode} = 1;
1035 $taper_p->{failure_from} = $failure_from;
1036 $taper_p->{tape_error} = $error;
1039 $self->_handle_error_line("taper", $str);
1042 } elsif ( $type == $L_FATAL ) {
1043 return $self->_handle_fatal_line( "taper", $str );
1045 } elsif ( $type == $L_FAIL ) {
1046 $self->_handle_fail_line( "taper", $str );
1049 $self->_handle_bogus_line( $P_TAPER, $type, $str );
1054 sub _handle_amflush_line
1056 my $self = shift @_;
1057 my ( $type, $str ) = @_;
1058 my $data = $self->{data};
1059 my $disklist = $data->{disklist};
1060 my $programs = $data->{programs};
1061 my $amflush_p = $programs->{amflush} ||= {};
1063 if ( $type == $L_DISK ) {
1064 return $self->_handle_disk_line( "amflush", $str );
1066 } elsif ( $type == $L_START ) {
1067 return $self->_handle_start_line( "amflush", $str );
1069 } elsif ( $type == $L_INFO ) {
1070 return $self->_handle_info_line( "amflush", $str );
1073 return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
1077 sub _handle_amvault_line
1079 my $self = shift @_;
1080 my ( $type, $str ) = @_;
1081 my $data = $self->{data};
1082 my $disklist = $data->{disklist};
1083 my $programs = $data->{programs};
1084 my $amvault_p = $programs->{amvault} ||= {};
1086 if ( $type == $L_START ) {
1087 return $self->_handle_start_line( "amvault", $str );
1089 } elsif ( $type == $L_INFO ) {
1090 return $self->_handle_info_line( "amvault", $str );
1092 } elsif ( $type == $L_ERROR ) {
1093 return $self->_handle_error_line( "amvault", $str );
1095 } elsif ( $type == $L_FATAL ) {
1096 return $self->_handle_fatal_line( "amvault", $str );
1098 } elsif ( $type == $L_DISK ) {
1099 return $self->_handle_disk_line( "amvault", $str );
1102 return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
1107 sub _handle_amdump_line
1110 my ( $type, $str ) = @_;
1111 my $data = $self->{data};
1112 my $disklist = $data->{disklist};
1113 my $programs = $data->{programs};
1114 my $amdump = $programs->{amdump} ||= {};
1116 if ( $type == $L_INFO ) {
1117 $self->_handle_info_line("amdump", $str);
1119 } elsif ( $type == $L_START ) {
1120 $self->_handle_start_line("amdump", $str);
1122 } elsif ( $type == $L_FATAL ) {
1123 return $self->_handle_fatal_line( "amdump", $str );
1125 } elsif ( $type == $L_ERROR ) {
1126 $self->_handle_error_line("amdump", $str);
1131 sub _handle_fail_line
1133 my ($self, $program, $str) = @_;
1135 my @info = Amanda::Util::split_quoted_strings($str);
1136 my ($hostname, $disk, $timestamp, $level) = @info;
1139 if ($program eq 'taper') {
1140 $failure_from = $info[4];
1141 $error = join " ", @info[ 5 .. $#info ];
1143 $error = join " ", @info[ 4 .. $#info ];
1146 #TODO: verify that this reaches the right try. Also, DLE or
1148 my $dle = $self->get_dle_info($hostname, $disk);
1151 if ($program eq "planner" ||
1152 $program eq "driver") {
1153 $program_d = $dle->{$program} ||= {};
1155 my $try = $self->_get_try($dle, $program, $timestamp);
1156 $program_d = $try->{$program} ||= {};
1159 $program_d->{level} = $level;
1160 $program_d->{status} = "fail";
1161 $program_d->{failure_from} = $failure_from;
1162 $program_d->{error} = $error;
1164 my $errors = $self->get_program_info("program", "errors", []);
1165 push @$errors, $error;
1167 $self->{flags}{exit_status} |= STATUS_FAILED;
1168 if ($program eq "dumper") {
1169 $self->{contline} = $program_d->{errors} ||= [];
1170 $program_d->{nb_errors} = 0;
1171 $self->{nbline_ref} = \$program_d->{nb_errors};
1172 $self->{nb_normal} = 0;
1173 $self->{nb_strange} = 0;
1174 $self->{nb_error} = 0;
1179 sub _handle_error_line
1181 my $self = shift @_;
1182 my ( $program, $str ) = @_;
1184 my $data = $self->{data};
1185 my $programs = $data->{programs};
1186 my $program_p = $programs->{$program};
1187 my $errors_p = $program_p->{errors} ||= [];
1189 $self->{flags}{exit_status} |= 1;
1191 push @$errors_p, $str;
1195 sub _handle_fatal_line
1197 my $self = shift @_;
1198 my ( $program, $str ) = @_;
1200 my $data = $self->{data};
1201 my $programs = $data->{programs};
1202 my $program_p = $programs->{$program};
1203 my $fatal_p = $program_p->{fatal} ||= [];
1205 $self->{flags}{exit_status} |= 1;
1207 push @$fatal_p, $str;
1211 sub _handle_start_line
1213 my $self = shift @_;
1214 my ( $program, $str ) = @_;
1216 my $data = $self->{data};
1217 my $disklist = $data->{disklist};
1218 my $programs = $data->{programs};
1220 my $program_p = $programs->{$program} ||= {};
1222 my @info = Amanda::Util::split_quoted_strings($str);
1223 my $timestamp = $info[1];
1224 $program_p->{start} = $info[1];
1226 if ($self->{'run_timestamp'} ne '00000000000000'
1227 and $self->{'run_timestamp'} ne $timestamp) {
1228 warning("not all timestamps in this file are the same; "
1229 . "$self->{run_timestamp}; $timestamp");
1231 $self->{'run_timestamp'} = $timestamp;
1235 sub _handle_disk_line
1237 my $self = shift @_;
1238 my ($program, $str) = @_;
1240 my $data = $self->{data};
1241 my $disklist = $data->{disklist};
1242 my $hosts = $self->{cache}{hosts} ||= [];
1243 my $dles = $self->{cache}{dles} ||= [];
1245 my @info = Amanda::Util::split_quoted_strings($str);
1246 my ($hostname, $disk) = @info;
1248 if (!exists $disklist->{$hostname}) {
1250 $disklist->{$hostname} = {};
1251 push @$hosts, $hostname;
1254 if (!exists $disklist->{$hostname}{$disk}) {
1256 push @$dles, [ $hostname, $disk ];
1257 my $dle = $disklist->{$hostname}{$disk} = {};
1258 $dle->{'estimate'} = undef;
1259 $dle->{'dumps'} = {};
1264 sub _handle_success_line
1266 my $self = shift @_;
1267 my ($program, $str) = @_;
1269 my $data = $self->{data};
1270 my $disklist = $data->{disklist};
1271 my $hosts = $self->{cache}{hosts} ||= [];
1272 my $dles = $self->{cache}{dles} ||= [];
1274 my @info = Amanda::Util::split_quoted_strings($str);
1275 my ($hostname, $disk, $timestamp, $level, $stat1, $stat2) = @info;
1277 if ($stat1 =~ /skipped/) {
1278 $disklist->{$hostname}{$disk}->{$program}->{'status'} = 'skipped';
1284 sub _handle_info_line
1286 my $self = shift @_;
1287 my ( $program, $str ) = @_;
1289 my $data = $self->{data};
1290 my $disklist = $data->{disklist};
1291 my $programs = $data->{programs};
1293 my $program_p = $programs->{$program} ||= {};
1295 if ( $str =~ m/^\w+ pid \d+/ || $str =~ m/^pid-done \d+/ ) {
1297 #do not report pid lines
1301 my $notes = $program_p->{notes} ||= [];
1306 sub _handle_warning_line
1308 my $self = shift @_;
1309 my ( $program, $str ) = @_;
1311 $self->_handle_info_line($program, $str);
1314 sub _handle_bogus_line
1316 my $self = shift @_;
1317 my ( $prog, $type, $str ) = @_;
1319 my $data = $self->{data};
1320 my $boguses = $data->{boguses} ||= [];
1321 push @$boguses, [ $prog, $type, $str ];
1324 sub check_missing_fail_strange
1327 my @dles = $self->get_dles();
1329 foreach my $dle_entry (@dles) {
1330 my $alldumps = $self->get_dle_info(@$dle_entry, 'dumps');
1331 my $driver = $self->get_dle_info(@$dle_entry, 'driver');
1332 my $planner = $self->get_dle_info(@$dle_entry, 'planner');
1334 if ($planner && $planner->{'status'} eq 'fail') {
1335 $self->{flags}{dump_failed} = 1;
1336 } elsif ($planner && $planner->{'status'} eq 'skipped') {
1337 # We don't want these to be counted as missing below
1338 } elsif (!defined $alldumps->{$self->{'run_timestamp'}} and
1341 $self->{flags}{results_missing} = 1;
1342 $self->{flags}{exit_status} |= STATUS_MISSING;
1345 my $tries = $alldumps->{$self->{'run_timestamp'}};
1346 my $try = @$tries[-1];
1348 if (exists $try->{dumper} && $try->{dumper}->{status} eq 'fail') {
1349 $self->{flags}{dump_failed} = 1;
1350 } elsif ((defined($try->{'chunker'}) &&
1351 $try->{'chunker'}->{status} eq 'success') ||
1352 (defined($try->{'taper'}) &&
1353 $try->{'taper'}->{status} eq 'done')) {
1354 #chunker or taper success, use dumper status
1355 if (exists $try->{dumper} && $try->{dumper}->{status} eq 'strange') {
1356 $self->{flags}{dump_strange} = 1;
1359 #chunker or taper failed, the dump is not valid.
1360 $self->{flags}{dump_failed} = 1;
1367 # NOTE: there may be a complicated state diagram lurking in the midst
1368 # of taper and chunker. You have been warned.
1372 my $self = shift @_;
1373 my ( $dle, $program, $timestamp ) = @_;
1374 my $tries = $dle->{'dumps'}{$timestamp} ||= [];
1378 || defined $tries->[-1]->{$program}->{status}
1379 && $self->_program_finished( # program has finished
1380 $program, $tries->[-1]->{$program}->{status}
1385 return $tries->[-1];
1389 sub _program_finished
1391 my $self = shift @_;
1392 my ( $program, $status ) = @_;
1394 if ( $program eq "chunker" ) {
1396 if ( $status eq "partial" ) {
1402 } elsif ( $status eq "done"
1403 || $status eq "success"
1404 || $status eq "fail"
1405 || $status eq "partial" ) {