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}}++;
514 $self->{nb_strange}++;
515 push @{$self->{contline}}, $str if $self->{nb_strange} + $self->{nb_error} <= 100;
516 } elsif ($str =~ /^\?/) {
518 push @{$self->{contline}}, $str if $self->{nb_error} <= 100;
520 $self->{nb_normal}++;
521 push @{$self->{contline}}, $str if ${$self->{nbline_ref}} <= 100;
525 $self->{contline} = undef;
526 $self->{nb_normal} = 0;
527 $self->{nb_strange} = 0;
528 $self->{nb_error} = 0;
530 if ( $prog == $P_PLANNER ) {
531 return $self->_handle_planner_line( $type, $str );
533 } elsif ( $prog == $P_DRIVER ) {
534 return $self->_handle_driver_line( $type, $str );
536 } elsif ( $prog == $P_DUMPER ) {
537 return $self->_handle_dumper_line( $type, $str );
539 } elsif ( $prog == $P_CHUNKER ) {
540 return $self->_handle_chunker_line( $type, $str );
542 } elsif ( $prog == $P_TAPER ) {
543 return $self->_handle_taper_line( $type, $str );
545 } elsif ( $prog == $P_AMFLUSH ) {
546 return $self->_handle_amflush_line( $type, $str );
548 } elsif ( $prog == $P_AMVAULT ) {
549 return $self->_handle_amvault_line( $type, $str );
551 } elsif ( $prog == $P_AMDUMP ) {
552 return $self->_handle_amdump_line( $type, $str );
554 } elsif ( $prog == $P_REPORTER ) {
555 return $self->_handle_reporter_line( $type, $str );
558 return $self->_handle_bogus_line( $prog, $type, $str );
565 return $self->{'run_timestamp'};
571 my $cache = $self->{cache};
573 $cache->{hosts} = [ keys %{ $self->{data}{disklist} } ]
574 if ( !defined $cache->{hosts} );
576 return @{ $cache->{hosts} };
583 return keys %{ $self->{data}{disklist}{$hostname} };
589 my $cache = $self->{cache};
592 if ( !defined $cache->{dles} ) {
593 foreach my $hostname ( $self->get_hosts() ) {
594 map { push @dles, [ $hostname, $_ ] } $self->get_disks($hostname);
596 $cache->{dles} = \@dles;
598 return @{ $cache->{dles} };
603 my ( $self, $org, $config ) = @_;
604 use Amanda::Report::xml;
605 return Amanda::Report::xml::make_amreport_xml( $self, $org, $config );
611 my ( $hostname, $disk, $field ) = @_;
613 return ( defined $field )
614 ? $self->{data}{disklist}{$hostname}{$disk}{$field}
615 : $self->{data}{disklist}{$hostname}{$disk};
620 my ($self, $program, $field, $default) = @_;
621 my $prog = $self->{data}{programs}{$program};
623 $prog->{$field} = $default if (defined $field && !defined $prog->{$field});
625 return (defined $field) ? $prog->{$field} : $prog;
630 my ($self, $label) = @_;
632 my $taper = $self->get_program_info("taper");
633 my $tapes = $taper->{tapes} ||= {};
634 my $tape_labels = $taper->{tape_labels} ||= [];
636 if (!exists $tapes->{$label}) {
637 push @$tape_labels, $label;
638 $tapes->{$label} = {date => "",
645 return $tapes->{$label};
650 my ( $self, $flag ) = @_;
651 return $self->{flags}{$flag};
654 sub _handle_planner_line
657 my ( $type, $str ) = @_;
658 my $data = $self->{data};
659 my $programs = $data->{programs};
660 my $disklist = $data->{disklist} ||= {};
661 my $planner = $programs->{planner} ||= {};
663 if ( $type == $L_INFO ) {
664 return $self->_handle_info_line( "planner", $str );
666 } elsif ( $type == $L_WARNING ) {
667 return $self->_handle_warning_line( "planner", $str );
669 } elsif ( $type == $L_START ) {
671 $self->{flags}{normal_run} = 1;
672 return $self->_handle_start_line( "planner", $str );
674 } elsif ( $type == $L_FINISH ) {
676 my @info = Amanda::Util::split_quoted_strings($str);
677 return $planner->{time} = $info[3];
679 } elsif ( $type == $L_DISK ) {
680 return $self->_handle_disk_line( "planner", $str );
682 } elsif ( $type == $L_SUCCESS ) {
683 return $self->_handle_success_line( "planner", $str );
685 } elsif ( $type == $L_ERROR ) {
686 return $self->_handle_error_line( "planner", $str );
688 } elsif ( $type == $L_FATAL ) {
689 return $self->_handle_fatal_line( "planner", $str );
691 } elsif ( $type == $L_FAIL ) {
693 # TODO: these are not like other failure messages: later
695 return $self->_handle_fail_line( "planner", $str );
698 return $self->_handle_bogus_line( $P_PLANNER, $type, $str );
703 sub _handle_driver_line
706 my ( $type, $str ) = @_;
707 my $data = $self->{data};
708 my $disklist = $data->{disklist};
709 my $programs = $data->{programs};
710 my $driver_p = $programs->{driver} ||= {};
712 if ( $type == $L_INFO ) {
713 return $self->_handle_info_line( "driver", $str );
715 } elsif ( $type == $L_START ) {
716 return $self->_handle_start_line( "driver", $str );
718 } elsif ( $type == $L_FINISH ) {
720 my @info = Amanda::Util::split_quoted_strings($str);
721 $self->{flags}{got_finish} = 1;
722 return $driver_p->{time} = $info[3];
724 } elsif ( $type == $L_STATS ) {
726 my @info = Amanda::Util::split_quoted_strings($str);
727 if ( $info[0] eq "hostname" ) {
729 return $self->{hostname} = $info[1];
731 } elsif ( $info[0] eq "startup" ) {
733 my @info = Amanda::Util::split_quoted_strings($str);
734 return $driver_p->{start_time} = $info[2];
736 } elsif ( $info[0] eq "estimate" ) {
739 # STATS driver estimate <hostname> <disk> <timestamp>
740 # <level> [sec <sec> nkb <nkb> ckb <ckb> jps <kps>]
741 # note that the [..] section is *not* quoted properly
742 my ($hostname, $disk, $timestamp, $level) = @info[ 1 .. 4 ];
744 # if the planner didn't define the DLE then this is a bad
746 unless (exists $disklist->{$hostname}{$disk}) {
747 return $self->_handle_bogus_line($P_DRIVER, $type, $str);
750 my $dle = $self->get_dle_info($hostname, $disk);
751 my ($sec, $nkb, $ckb, $kps) = @info[ 6, 8, 10, 12 ];
752 $kps =~ s{\]}{}; # strip trailing "]"
763 return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
766 } elsif ( $type == $L_WARNING ) {
768 $self->{flags}{exit_status} |= STATUS_TAPE
769 if ($str eq "Taper protocol error");
771 return $self->_handle_warning_line("driver", $str);
773 } elsif ( $type == $L_ERROR ) {
774 return $self->_handle_error_line( "driver", $str );
776 } elsif ( $type == $L_FATAL ) {
777 return $self->_handle_fatal_line( "driver", $str );
779 } elsif ( $type == $L_FAIL ) {
780 return $self->_handle_fail_line( "driver", $str );
783 return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
788 sub _handle_dumper_line
791 my ( $type, $str ) = @_;
792 my $data = $self->{data};
793 my $disklist = $data->{disklist};
794 my $programs = $data->{programs};
795 my $dumper_p = $programs->{dumper} ||= {};
797 if ( $type == $L_INFO ) {
798 return $self->_handle_info_line( "dumper", $str );
800 } elsif ( $type == $L_STRANGE ) {
802 my @info = Amanda::Util::split_quoted_strings($str);
803 my ( $hostname, $disk, $level ) = @info[ 0 .. 2 ];
804 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 4, 6, 8, 10 ];
805 $kb = int($kb/1024) if $info[4] eq 'bytes';
806 $orig_kb =~ s{\]$}{};
808 my $dle = $disklist->{$hostname}->{$disk};
809 my $try = $self->_get_try( $dle, "dumper", $self->{'run_timestamp'});
810 my $dumper = $try->{dumper} ||= {};
811 $dumper->{level} = $level;
812 $dumper->{status} = 'strange';
813 $dumper->{sec} = $sec;
815 $dumper->{kps} = $kps;
816 $dumper->{orig_kb} = $orig_kb;
818 $self->{contline} = $dumper->{stranges} ||= [];
819 $dumper->{nb_stranges} = 0;
820 $self->{nbline_ref} = \$dumper->{nb_stranges};
821 $self->{nb_normal} = 0;
822 $self->{nb_strange} = 0;
823 $self->{nb_error} = 0;
825 return $self->{flags}{exit_status} |= STATUS_STRANGE
827 } elsif ( $type == $L_WARNING ) {
829 return $self->_handle_warning_line("dumper", $str);
831 } elsif ( $type == $L_SUCCESS ) {
833 my @info = Amanda::Util::split_quoted_strings($str);
834 my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
835 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 5, 7, 9, 11 ];
836 $kb = int($kb/1024) if $info[6] eq 'bytes';
837 $orig_kb =~ s{\]$}{};
839 my $dle = $disklist->{$hostname}->{$disk};
840 my $try = $self->_get_try( $dle, "dumper", $timestamp );
841 my $dumper = $try->{dumper} ||= {};
843 $dumper->{date} = $timestamp;
844 $dumper->{level} = $level;
845 $dumper->{sec} = $sec;
847 $dumper->{kps} = $kps;
848 $dumper->{orig_kb} = $orig_kb;
850 return $dumper->{status} = "success";
852 } elsif ( $type == $L_ERROR ) {
853 return $self->_handle_error_line( "dumper", $str );
855 } elsif ( $type == $L_FATAL ) {
856 return $self->_handle_fatal_line( "dumper", $str );
858 } elsif ( $type == $L_FAIL ) {
859 return $self->_handle_fail_line( "dumper", $str );
862 return $self->_handle_bogus_line( $P_DUMPER, $type, $str );
867 sub _handle_chunker_line
870 my ( $type, $str ) = @_;
871 my $data = $self->{data};
872 my $disklist = $data->{disklist};
873 my $programs = $data->{programs};
874 my $chunker_p = $programs->{chunker} ||= {};
876 if ( $type == $L_INFO ) {
877 return $self->_handle_info_line( "chunker", $str );
879 } elsif ( $type == $L_SUCCESS || $type == $L_PARTIAL ) {
881 my @info = Amanda::Util::split_quoted_strings($str);
882 my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
883 my ( $sec, $kb, $kps ) = @info[ 5, 7, 9 ];
884 $kb = int($kb/1024) if $info[6] eq 'bytes';
887 my $dle = $disklist->{$hostname}->{$disk};
888 my $try = $self->_get_try( $dle, "chunker", $timestamp );
889 my $chunker = $try->{chunker} ||= {};
891 $chunker->{date} = $timestamp;
892 $chunker->{level} = $level;
893 $chunker->{sec} = $sec;
894 $chunker->{kb} = $kb;
895 $chunker->{kps} = $kps;
897 return $chunker->{status} =
898 ( $type == $L_SUCCESS ) ? "success" : "partial";
900 } elsif ( $type == $L_ERROR ) {
901 return $self->_handle_error_line( "chunker", $str );
903 } elsif ( $type == $L_FATAL ) {
904 return $self->_handle_fatal_line( "chunker", $str );
906 } elsif ( $type == $L_FAIL ) {
907 return $self->_handle_fail_line( "chunker", $str );
910 return $self->_handle_bogus_line( $P_CHUNKER, $type, $str );
915 sub _handle_taper_line
918 my ( $type, $str ) = @_;
919 my $data = $self->{data};
920 my $disklist = $data->{disklist};
921 my $programs = $data->{programs};
922 my $taper_p = $programs->{taper} ||= {};
924 if ( $type == $L_START ) {
926 # START taper datestamp <start> label <label> tape <tapenum>
927 my @info = Amanda::Util::split_quoted_strings($str);
928 my ($datestamp, $label, $tapenum) = @info[ 1, 3, 5 ];
929 my $tape = $self->get_tape($label);
930 $tape->{date} = $datestamp;
931 $tape->{label} = $label;
933 # keep this tape for later
934 $self->{'_current_tape'} = $tape;
936 # call through to the generic start line function
937 $self->_handle_start_line( "taper", $str );
938 } elsif ( $type == $L_PART || $type == $L_PARTPARTIAL ) {
941 # <label> <tapefile> <hostname> <disk> <timestamp> <currpart>/<predparts> <level> [sec <sec> kb <kb> kps <kps>]
943 # format for $L_PARTPARTIAL is the same as $L_PART, plus <err> at the end
944 my @info = Amanda::Util::split_quoted_strings($str);
945 my ($label, $tapefile, $hostname, $disk, $timestamp) = @info[ 0 .. 4 ];
947 $info[5] =~ m{^(\d+)\/(-?\d+)$};
948 my ( $currpart, $predparts ) = ( $1, $2 );
950 my ($level, $sec, $kb, $kps, $orig_kb) = @info[ 6, 8, 10, 12, 14 ];
951 $kb = int($kb/1024) if $info[9] eq 'bytes';
953 $orig_kb =~ s{\]$}{} if defined($orig_kb);
955 my $dle = $disklist->{$hostname}{$disk};
956 my $try = $self->_get_try($dle, "taper", $timestamp);
957 my $taper = $try->{taper} ||= {};
958 my $parts = $taper->{parts} ||= [];
967 partnum => $currpart,
970 $taper->{orig_kb} = $orig_kb;
974 my $tape = $self->get_tape($label);
975 # count this as a filesystem if this is the first part
976 $tape->{dle}++ if $currpart == 1;
978 $tape->{time} += $sec;
981 } elsif ( $type == $L_DONE || $type == $L_PARTIAL ) {
984 # $type = DONE | PARTIAL
985 # $type taper <hostname> <disk> <timestamp> <part> <level> [sec <sec> kb <kb> kps <kps>]
986 my @info = Amanda::Util::split_quoted_strings($str);
987 my ( $hostname, $disk, $timestamp, $part_ct, $level ) = @info[ 0 .. 4 ];
988 my ( $sec, $kb, $kps, $orig_kb ) = @info[ 6, 8, 10, 12 ];
989 $kb = int($kb/1024) if $info[7] eq 'bytes';
991 if ($type == $L_PARTIAL) {
993 $error = join " ", @info[ 11 .. $#info ];
995 $error = join " ", @info[ 13 .. $#info ];
999 $orig_kb =~ s{\]$}{} if defined $orig_kb;
1001 my $dle = $disklist->{$hostname}->{$disk};
1002 my $try = $self->_get_try($dle, "taper", $timestamp);
1003 my $taper = $try->{taper} ||= {};
1004 my $parts = $taper->{parts};
1006 if ($part_ct - $#$parts != 1) {
1007 ## this should always be true; do nothing right now
1010 $taper->{level} = $level;
1011 $taper->{sec} = $sec;
1013 $taper->{kps} = $kps;
1015 $taper->{status} = ( $type == $L_DONE ) ? "done" : "partial";
1016 $taper->{error} = $error if $type == $L_PARTIAL;
1018 } elsif ( $type == $L_INFO ) {
1019 $self->_handle_info_line("taper", $str);
1021 } elsif ( $type == $L_WARNING ) {
1022 $self->_handle_warning_line("taper", $str);
1024 } elsif ( $type == $L_ERROR ) {
1026 if ($str =~ m{^no-tape}) {
1028 my @info = Amanda::Util::split_quoted_strings($str);
1029 my $failure_from = $info[1];
1030 my $error = join " ", @info[ 2 .. $#info ];
1032 $self->{flags}{exit_status} |= STATUS_TAPE;
1033 $self->{flags}{degraded_mode} = 1;
1034 $taper_p->{failure_from} = $failure_from;
1035 $taper_p->{tape_error} = $error;
1038 $self->_handle_error_line("taper", $str);
1041 } elsif ( $type == $L_FATAL ) {
1042 return $self->_handle_fatal_line( "taper", $str );
1044 } elsif ( $type == $L_FAIL ) {
1045 $self->_handle_fail_line( "taper", $str );
1048 $self->_handle_bogus_line( $P_TAPER, $type, $str );
1053 sub _handle_amflush_line
1055 my $self = shift @_;
1056 my ( $type, $str ) = @_;
1057 my $data = $self->{data};
1058 my $disklist = $data->{disklist};
1059 my $programs = $data->{programs};
1060 my $amflush_p = $programs->{amflush} ||= {};
1062 if ( $type == $L_DISK ) {
1063 return $self->_handle_disk_line( "amflush", $str );
1065 } elsif ( $type == $L_START ) {
1066 return $self->_handle_start_line( "amflush", $str );
1068 } elsif ( $type == $L_INFO ) {
1069 return $self->_handle_info_line( "amflush", $str );
1072 return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
1076 sub _handle_amvault_line
1078 my $self = shift @_;
1079 my ( $type, $str ) = @_;
1080 my $data = $self->{data};
1081 my $disklist = $data->{disklist};
1082 my $programs = $data->{programs};
1083 my $amvault_p = $programs->{amvault} ||= {};
1085 if ( $type == $L_START ) {
1086 return $self->_handle_start_line( "amvault", $str );
1088 } elsif ( $type == $L_INFO ) {
1089 return $self->_handle_info_line( "amvault", $str );
1091 } elsif ( $type == $L_ERROR ) {
1092 return $self->_handle_error_line( "amvault", $str );
1094 } elsif ( $type == $L_FATAL ) {
1095 return $self->_handle_fatal_line( "amvault", $str );
1097 } elsif ( $type == $L_DISK ) {
1098 return $self->_handle_disk_line( "amvault", $str );
1101 return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
1106 sub _handle_amdump_line
1109 my ( $type, $str ) = @_;
1110 my $data = $self->{data};
1111 my $disklist = $data->{disklist};
1112 my $programs = $data->{programs};
1113 my $amdump = $programs->{amdump} ||= {};
1115 if ( $type == $L_INFO ) {
1116 $self->_handle_info_line("amdump", $str);
1118 } elsif ( $type == $L_START ) {
1119 $self->_handle_start_line("amdump", $str);
1121 } elsif ( $type == $L_FATAL ) {
1122 return $self->_handle_fatal_line( "amdump", $str );
1124 } elsif ( $type == $L_ERROR ) {
1125 $self->_handle_error_line("amdump", $str);
1130 sub _handle_fail_line
1132 my ($self, $program, $str) = @_;
1134 my @info = Amanda::Util::split_quoted_strings($str);
1135 my ($hostname, $disk, $timestamp, $level) = @info;
1138 if ($program eq 'taper') {
1139 $failure_from = $info[4];
1140 $error = join " ", @info[ 5 .. $#info ];
1142 $error = join " ", @info[ 4 .. $#info ];
1145 #TODO: verify that this reaches the right try. Also, DLE or
1147 my $dle = $self->get_dle_info($hostname, $disk);
1150 if ($program eq "planner" ||
1151 $program eq "driver") {
1152 $program_d = $dle->{$program} ||= {};
1154 my $try = $self->_get_try($dle, $program, $timestamp);
1155 $program_d = $try->{$program} ||= {};
1158 $program_d->{level} = $level;
1159 $program_d->{status} = "fail";
1160 $program_d->{failure_from} = $failure_from;
1161 $program_d->{error} = $error;
1163 my $errors = $self->get_program_info("program", "errors", []);
1164 push @$errors, $error;
1166 $self->{flags}{exit_status} |= STATUS_FAILED;
1167 if ($program eq "dumper") {
1168 $self->{contline} = $program_d->{errors} ||= [];
1169 $program_d->{nb_errors} = 0;
1170 $self->{nbline_ref} = \$program_d->{nb_errors};
1171 $self->{nb_normal} = 0;
1172 $self->{nb_strange} = 0;
1173 $self->{nb_error} = 0;
1178 sub _handle_error_line
1180 my $self = shift @_;
1181 my ( $program, $str ) = @_;
1183 my $data = $self->{data};
1184 my $programs = $data->{programs};
1185 my $program_p = $programs->{$program};
1186 my $errors_p = $program_p->{errors} ||= [];
1188 $self->{flags}{exit_status} |= 1;
1190 push @$errors_p, $str;
1194 sub _handle_fatal_line
1196 my $self = shift @_;
1197 my ( $program, $str ) = @_;
1199 my $data = $self->{data};
1200 my $programs = $data->{programs};
1201 my $program_p = $programs->{$program};
1202 my $fatal_p = $program_p->{fatal} ||= [];
1204 $self->{flags}{exit_status} |= 1;
1206 push @$fatal_p, $str;
1210 sub _handle_start_line
1212 my $self = shift @_;
1213 my ( $program, $str ) = @_;
1215 my $data = $self->{data};
1216 my $disklist = $data->{disklist};
1217 my $programs = $data->{programs};
1219 my $program_p = $programs->{$program} ||= {};
1221 my @info = Amanda::Util::split_quoted_strings($str);
1222 my $timestamp = $info[1];
1223 $program_p->{start} = $info[1];
1225 if ($self->{'run_timestamp'} ne '00000000000000'
1226 and $self->{'run_timestamp'} ne $timestamp) {
1227 warning("not all timestamps in this file are the same; "
1228 . "$self->{run_timestamp}; $timestamp");
1230 $self->{'run_timestamp'} = $timestamp;
1234 sub _handle_disk_line
1236 my $self = shift @_;
1237 my ($program, $str) = @_;
1239 my $data = $self->{data};
1240 my $disklist = $data->{disklist};
1241 my $hosts = $self->{cache}{hosts} ||= [];
1242 my $dles = $self->{cache}{dles} ||= [];
1244 my @info = Amanda::Util::split_quoted_strings($str);
1245 my ($hostname, $disk) = @info;
1247 if (!exists $disklist->{$hostname}) {
1249 $disklist->{$hostname} = {};
1250 push @$hosts, $hostname;
1253 if (!exists $disklist->{$hostname}{$disk}) {
1255 push @$dles, [ $hostname, $disk ];
1256 my $dle = $disklist->{$hostname}{$disk} = {};
1257 $dle->{'estimate'} = undef;
1258 $dle->{'dumps'} = {};
1263 sub _handle_success_line
1265 my $self = shift @_;
1266 my ($program, $str) = @_;
1268 my $data = $self->{data};
1269 my $disklist = $data->{disklist};
1270 my $hosts = $self->{cache}{hosts} ||= [];
1271 my $dles = $self->{cache}{dles} ||= [];
1273 my @info = Amanda::Util::split_quoted_strings($str);
1274 my ($hostname, $disk, $timestamp, $level, $stat1, $stat2) = @info;
1276 if ($stat1 =~ /skipped/) {
1277 $disklist->{$hostname}{$disk}->{$program}->{'status'} = 'skipped';
1283 sub _handle_info_line
1285 my $self = shift @_;
1286 my ( $program, $str ) = @_;
1288 my $data = $self->{data};
1289 my $disklist = $data->{disklist};
1290 my $programs = $data->{programs};
1292 my $program_p = $programs->{$program} ||= {};
1294 if ( $str =~ m/^\w+ pid \d+/ || $str =~ m/^pid-done \d+/ ) {
1296 #do not report pid lines
1300 my $notes = $program_p->{notes} ||= [];
1305 sub _handle_warning_line
1307 my $self = shift @_;
1308 my ( $program, $str ) = @_;
1310 $self->_handle_info_line($program, $str);
1313 sub _handle_bogus_line
1315 my $self = shift @_;
1316 my ( $prog, $type, $str ) = @_;
1318 my $data = $self->{data};
1319 my $boguses = $data->{boguses} ||= [];
1320 push @$boguses, [ $prog, $type, $str ];
1323 sub check_missing_fail_strange
1326 my @dles = $self->get_dles();
1328 foreach my $dle_entry (@dles) {
1329 my $alldumps = $self->get_dle_info(@$dle_entry, 'dumps');
1330 my $driver = $self->get_dle_info(@$dle_entry, 'driver');
1331 my $planner = $self->get_dle_info(@$dle_entry, 'planner');
1333 if ($planner && $planner->{'status'} eq 'fail') {
1334 $self->{flags}{dump_failed} = 1;
1335 } elsif ($planner && $planner->{'status'} eq 'skipped') {
1336 # We don't want these to be counted as missing below
1337 } elsif (!defined $alldumps->{$self->{'run_timestamp'}} and
1340 $self->{flags}{results_missing} = 1;
1341 $self->{flags}{exit_status} |= STATUS_MISSING;
1344 my $tries = $alldumps->{$self->{'run_timestamp'}};
1345 my $try = @$tries[-1];
1347 if (exists $try->{dumper} && $try->{dumper}->{status} eq 'fail') {
1348 $self->{flags}{dump_failed} = 1;
1349 } elsif ((defined($try->{'chunker'}) &&
1350 $try->{'chunker'}->{status} eq 'success') ||
1351 (defined($try->{'taper'}) &&
1352 $try->{'taper'}->{status} eq 'done')) {
1353 #chunker or taper success, use dumper status
1354 if (exists $try->{dumper} && $try->{dumper}->{status} eq 'strange') {
1355 $self->{flags}{dump_strange} = 1;
1358 #chunker or taper failed, the dump is not valid.
1359 $self->{flags}{dump_failed} = 1;
1366 # NOTE: there may be a complicated state diagram lurking in the midst
1367 # of taper and chunker. You have been warned.
1371 my $self = shift @_;
1372 my ( $dle, $program, $timestamp ) = @_;
1373 my $tries = $dle->{'dumps'}{$timestamp} ||= [];
1377 || defined $tries->[-1]->{$program}->{status}
1378 && $self->_program_finished( # program has finished
1379 $program, $tries->[-1]->{$program}->{status}
1384 return $tries->[-1];
1388 sub _program_finished
1390 my $self = shift @_;
1391 my ( $program, $status ) = @_;
1393 if ( $program eq "chunker" ) {
1395 if ( $status eq "partial" ) {
1401 } elsif ( $status eq "done"
1402 || $status eq "success"
1403 || $status eq "fail"
1404 || $status eq "partial" ) {