Merge branch 'master' into squeeze
[debian/amanda] / perl / Amanda / Report.pm
diff --git a/perl/Amanda/Report.pm b/perl/Amanda/Report.pm
new file mode 100644 (file)
index 0000000..f94b0fc
--- /dev/null
@@ -0,0 +1,1412 @@
+# Copyright (c) 2010 Zmanda, Inc.  All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+#
+# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
+# Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
+
+package Amanda::Report;
+use strict;
+use warnings;
+use Data::Dumper;
+
+use Amanda::Disklist;
+use Amanda::Logfile qw/:logtype_t :program_t/;
+use Amanda::Util;
+use Amanda::Debug qw( debug warning );
+
+=head1 NAME
+
+Amanda::Report -- module for representing report data from logfiles
+
+=head1 SYNOPSIS
+
+    use Amanda::Report;
+
+    my $report = Amanda::Report->new($logfile);
+    my @hosts  = keys %{$report->{data}{disklist}};
+
+=head1 INTERFACE
+
+This module reads the logfile passed to it and aggregates the data in
+a format of nested hashes for convenient output.  All data read in is
+stored in C<< $report->{data} >>.
+
+=head2 Creating a Report
+
+  my $report = Amanda::Report->new($logfile, $historical);
+
+The constructor reads the logfile and produces the report, which can then be
+queried with the other methods.  C<$logfile> should specify the path to the
+logfile from which the report is prepared.  If the logfile is not the "current"
+logfile, then C<$historical> should be false.  Non-historical reports may draw
+information from the current Amanda environment, e.g., holding disks and info
+files.
+
+=head2 Summary Information
+
+Note that most of the data provided by these methods is simply a reference to
+data stored within the report, and should thus be considered read-only.  For
+example, do not use C<shift> or C<pop> to destructively consume lists.
+
+  my $datestamp = $report->get_timestamp();
+
+This returns the run timestamp for this dump run.  This is determined from one
+of several START entries.  This returns a full 14-digit timestamp regardless of
+the setting of C<usetimestamps> now or during the dump run.
+
+  my @hosts = $report->get_hosts();
+
+This method returns a list containing the hosts that have been seen in
+a logfile.  In a scalar context, C<get_hosts> returns the number of
+hosts seen.
+
+  my @disks = $report->get_disks($hostname);
+
+This method returns a list of disks that were archived under the given
+C<$hostname>.  In a scalar context, this method returns the number of
+disks seen, belonging to the hostname.
+
+  my @dles = $report->get_dles();
+
+This method returns a list of list references.  Each referenced list
+contains a hostname & disk pair that has been reported by either the
+planner or amflush.  The DLEs are stored in the order that they appear
+in the logfile.
+
+    @dles = (
+        [ 'example1', '/home' ],
+        [ 'example1', '/var/log' ],
+        [ 'example2', '/etc' ],
+        [ 'example2', '/home' ],
+        [ 'example3', '/var/www' ],
+    );
+
+  if ( $report->get_flag($flag) ) { ... }
+
+The C<get_flag> method accesses a number of flags that represent the state of
+the dump.  A true value is returned if the flag is set, and undef otherwise.
+The available flags are:
+
+=over
+
+=item C<got_finish>
+
+This flag is true when the driver finished
+correctly.  It indicates that the dump run has finished and cleaned
+up.
+
+=item C<degraded_mode>
+
+This flag is set if the taper encounters an
+error that forces it into degraded mode.
+
+=item C<amflush_run>
+
+This flag is set if amflush is run instead of planner.
+
+=item C<amvault_run>
+
+This flag is set if the run was by amvault.
+
+=item C<normal_run>
+
+This flag is set when planner is run.  Its value
+should be opposite of C<amflush_run>.
+
+=item C<dump_failed>
+
+If a dump failed.
+
+=item C<dump_strange>
+
+If a dump end in strange result.
+
+=item C<results_missing>
+
+If this was a normal run, but some DLEs named by the
+planner do not have any results, then this flag is set.  Users should look for
+DLEs with an empty C<dump> key to enumerate the missing results.
+
+=item C<historical>
+
+This flag is set if this is a "historical" report.  It is
+based on the value passed to the constructor.
+
+=back
+
+=head2 Report Data
+
+  my $dle = $report->get_dle_info($hostname, $disk [,$field] );
+
+This method returns the DLE information for the given C<$hostname> and C<disk>,
+or if C<$field> is given, returns that field of the DLE information.  See the
+DATA DESCRIPTION section for the format of this information.
+
+  my $info = $report->get_program_info($program [,$field] );
+
+This method returns the program information for the given C<$program>, or if
+C<$field> is given, returns that field of the DLE information.  See the DATA
+DESCRIPTION section for the format of this information.
+
+=head1 DATA DESCRIPTION
+
+=head2 Top Level
+
+The data in the logfile is stored in the module at C<< $report->{data} >>.
+Beneath that, there are a number of subdivisions that track both global and
+per-host status of the given Amanda run that the logfile represents.  Note that
+these subdivisions are usually accessed via C<get_dle_info> and
+C<get_program_info>, as described above.
+
+  $data->{programs}
+
+the C<programs> key of the data points to a hash of global program
+information, with one element per program.  See the Programs section, below.
+
+  $data->{boguses}
+
+The C<boguses> key refers to a list of arrayrefs of the form
+
+  [$prog, $type, $str]
+
+as returned directly by C<Amanda::Logfile::get_logline>.  These lines are not
+in a recognized trace log format.
+
+  $data->{disklist}
+
+The C<disklist> key points to a two-level hash of hostnames and
+disknames as present in the logfile.  It looks something like this:
+
+    $report->{data}{disklist} = {
+        "server.example.org" => {
+            "/home" => {...},
+            "/var"  => {...},
+        },
+        "workstation.example.org" => {
+            "/etc"     => {...},
+            "/var/www" => {...},
+        },
+    };
+
+Each C<{...}> in the above contains information about the corresponding DLE.  See DLEs, below.
+
+=head2 Programs
+
+Each program involved in a dump has a hash giving information about its
+performance during the run.  A number of fields are common across all of the
+different programs:
+
+=over
+
+=item C<start>
+
+the numeric timestamp at which the process was started.
+
+=item C<time>
+
+the length of time (in seconds) that the program ran.
+
+=item C<notes>
+
+a list which stores all notes reported to the logfile
+by the corresponding program.
+
+=item C<errors>
+
+a list which stores all errors reported to the
+logfile by the corresponding program.
+
+=back
+
+Program-specific fields are described in the following sections.
+
+=head3 planner
+
+The planner logs very little information other than determining what will be
+backed up.  It has no special fields other than those given above.
+
+=head3 driver
+
+The driver has one field that the other program-specific
+entries do not:
+
+=over
+
+=item C<start_time> - the time it takes for the driver to start up.
+
+=back
+
+=head3 amflush and amdump
+
+No special fields.
+
+=head3 dumper and chunker
+
+Most of the chunker's output and the dumper's output can be tied to a
+particular DLE, so their C<programs> hashes are limited to C<notes> and
+C<errors>.
+
+=head3 taper
+
+The taper hash holds notes and errors for the per-instance runs of the taper
+program, but also tracks the tapes seen in the logfile:
+
+=over
+
+=item C<tapes>
+
+This field is a hash reference keyed by the label of the tape.
+each value of the key is another hash which stores date, size, and the
+number of files seen by this backup on the tape.  For example:
+
+    $report->{data}{programs}{taper}{tapes} = {
+        FakeTape01 => {
+            label => "FakeTape01",
+            date  => "20100318141930",
+            kb    => 7894769,          # data written to tape this session
+            files => 14,               # parts written to tape this session
+            dle   => 13,               # number of dumps that begin on this tape
+            time  => 2.857,            # time spent writing to this tape
+        },
+    };
+
+=item C<tape_labels>
+
+The C<tape_labels> field is a reference to a list which records the
+order that the tapes have been seen.  This list should be used as an
+ordered index for C<tapes>.
+
+=back
+
+=head2 DLEs
+
+In the below, C<$dle> is the hash representing one disklist entry.
+
+The C<estimate> key describes the estimate given by the planner.  For
+example:
+
+    $dle->{estimate} = {
+       level => 0,     # the level of the backup
+       sec   => 20,    # estimated time to back up (seconds)
+       nkb   => 2048,  # expected uncompressed size (kb)
+       ckb   => 1293,  # expected compressed size (kb)
+       kps   => 934.1, # speed of the backup (kb/sec)
+    };
+
+Each dump of the DLE is represented in C<< $dle->{dumps} >>.  This is a hash,
+keyed by dump timestamp with a list of tries as the value for each dump.  Each
+try represents a specific attempt to finish writing this dump to a volume.  If
+an error occurs during the backup of a DLE and is retried, a second try is
+pushed to the tries list.  For example:
+
+    $dle->{dumps} = {
+       '20100317142122' => [ $try1 ],
+       '20100318141930' => [ $try1, $try2 ],
+    };
+
+=head3 Tries
+
+A try is a hash with at least one dumper, taper, and/or chunker DLE program as
+a key.  These entries contain the results from the associated program during
+try.
+
+There are a number of common fields between all three elements:
+
+=over
+
+=item C<date>
+
+a timestamp of when the program finished (if the program exited)
+
+=item C<status>
+
+the status of the dump at this program on this try ("success", "partial",
+"done", or "failed").  The planner adds an extra "skipped" status which is
+added when the planner decides to skip a DLE due to user configuration (e.g.,
+C<skipincr>).
+
+=item C<level>
+
+the incremental level of the backup.
+
+=item C<sec>
+
+the time in seconds for the program to finish.
+
+=item C<kb>
+
+the size of the data dumped in kb.
+
+=item C<kps>
+
+the rate at which the program was able to process data,
+in kb/sec.
+
+=item C<error>
+
+if the program fails, this field contains the error message
+
+=back
+
+The C<dumper> hash has an C<orig_kb> field, giving the size of the data dumped
+from the source, before any compression. If encountered, the C<dumper> hash may
+also contain a C<stranges> field, which is a list of all the messages of type
+C<L_STRANGE> encountered during the process.
+
+The C<taper> hash contains all the exit status data given by the taper.
+Because the same taper process handles multiple dumps, it does not have a
+C<date> field.  However, the taper does have an additional field, C<parts>,
+containing a list of parts written for this dump.
+
+=head3 Parts
+
+Each item in the list of taper parts is a hash with the following
+fields:
+
+=over
+
+=item C<label>
+
+the name of the tape that the part was written to.
+
+=item C<date>
+
+the datestamp at which this part was written.
+
+=item C<file>
+
+the filename of the part.
+
+=item C<part>
+
+the sequence number of the part for the DLE that the
+part is archiving.
+
+=item C<sec>
+
+the length of time, in seconds, that the part took to
+be written.
+
+=item C<kb>
+
+the total size of the part.
+
+=item C<kps>
+
+the speed at which the part was written.
+
+=back
+
+=cut
+
+use constant STATUS_STRANGE => 2;
+use constant STATUS_FAILED  => 4;
+use constant STATUS_MISSING => 8;
+use constant STATUS_TAPE    => 16;
+
+sub new
+{
+    my $class = shift @_;
+    my ($logfname, $historical) = @_;
+
+    my $self = {
+        data => {},
+
+       ## inputs
+       _logfname => $logfname,
+       _historical => $historical,
+
+       ## logfile-parsing state
+
+       # the tape currently being writen
+       _current_tape => undef,
+    };
+    bless $self, $class;
+
+    $self->read_file();
+    return $self;
+}
+
+
+sub read_file
+{
+    my $self       = shift @_;
+    my $data       = $self->{data} = {};
+    my $logfname   = $self->{_logfname};
+
+    # clear the program and DLE data
+    $data->{programs} = {};
+    $data->{disklist} = {};
+    $self->{cache}    = {};
+    $self->{flags}    = {};
+    $self->{run_timestamp} = '00000000000000';
+
+    my $logfh = Amanda::Logfile::open_logfile($logfname)
+      or die "cannot open '$logfname': $!";
+
+    $self->{flags}{exit_status} = 0;
+    $self->{flags}{results_missing} = 0;
+    $self->{flags}{dump_failed} = 0;
+    $self->{flags}{dump_strange} = 0;
+
+    while ( my ( $type, $prog, $str ) = Amanda::Logfile::get_logline($logfh) ) {
+        $self->read_line( $type, $prog, $str );
+    }
+
+    ## set post-run flags
+
+    $self->{flags}{historical} = $self->{_historical};
+    $self->{flags}{amflush_run} = 0;
+    $self->{flags}{amvault_run} = 0;
+    if (!$self->get_flag("normal_run")) {
+        if (   ( defined $self->get_program_info("amflush") )
+            && ( scalar %{ $self->get_program_info("amflush") } ) ) {
+           debug("detected an amflush run");
+           $self->{flags}{amflush_run} = 1;
+       } elsif (   ( defined $self->get_program_info("amvault") )
+                 && ( scalar %{ $self->get_program_info("amvault") } ) ) {
+           debug("detected an amvault run");
+           $self->{flags}{amvault_run} = 1;
+       }
+    }
+
+    # check for missing, fail and strange results
+    $self->check_missing_fail_strange() if $self->get_flag('normal_run');
+
+    # clean up any temporary values in the data
+    $self->cleanup();
+}
+
+sub cleanup
+{
+    my $self = shift;
+
+    #remove last_label field
+    foreach my $dle ($self->get_dles()) {
+        my $dle_info = $self->get_dle_info(@$dle);
+        delete $dle_info->{last_label};
+    }
+
+    return;
+}
+
+
+sub read_line
+{
+    my $self = shift @_;
+    my ( $type, $prog, $str ) = @_;
+
+    if ( $type == $L_CONT ) {
+       ${$self->{nbline_ref}}++;
+       if ($str =~ /^\|/) {
+           $self->{nb_strange}++;
+           push @{$self->{contline}}, $str if $self->{nb_strange} + $self->{nb_error} <= 100;
+       } elsif ($str =~ /^\?/) {
+           $self->{nb_error}++;
+           push @{$self->{contline}}, $str if $self->{nb_error} <= 100;
+       } else {
+           $self->{nb_normal}++;
+           push @{$self->{contline}}, $str if ${$self->{nbline_ref}} <= 100;
+       }
+       return;
+    }
+    $self->{contline} = undef;
+    $self->{nb_normal} = 0;
+    $self->{nb_strange} = 0;
+    $self->{nb_error} = 0;
+
+    if ( $prog == $P_PLANNER ) {
+        return $self->_handle_planner_line( $type, $str );
+
+    } elsif ( $prog == $P_DRIVER ) {
+        return $self->_handle_driver_line( $type, $str );
+
+    } elsif ( $prog == $P_DUMPER ) {
+        return $self->_handle_dumper_line( $type, $str );
+
+    } elsif ( $prog == $P_CHUNKER ) {
+        return $self->_handle_chunker_line( $type, $str );
+
+    } elsif ( $prog == $P_TAPER ) {
+        return $self->_handle_taper_line( $type, $str );
+
+    } elsif ( $prog == $P_AMFLUSH ) {
+        return $self->_handle_amflush_line( $type, $str );
+
+    } elsif ( $prog == $P_AMVAULT ) {
+        return $self->_handle_amvault_line( $type, $str );
+
+    } elsif ( $prog == $P_AMDUMP ) {
+        return $self->_handle_amdump_line( $type, $str );
+
+    } elsif ( $prog == $P_REPORTER ) {
+        return $self->_handle_reporter_line( $type, $str );
+
+    } else {
+        return $self->_handle_bogus_line( $prog, $type, $str );
+    }
+}
+
+sub get_timestamp
+{
+    my $self = shift;
+    return $self->{'run_timestamp'};
+}
+
+sub get_hosts
+{
+    my $self  = shift @_;
+    my $cache = $self->{cache};
+
+    $cache->{hosts} = [ keys %{ $self->{data}{disklist} } ]
+      if ( !defined $cache->{hosts} );
+
+    return @{ $cache->{hosts} };
+}
+
+sub get_disks
+{
+    my $self = shift @_;
+    my ($hostname) = @_;
+    return keys %{ $self->{data}{disklist}{$hostname} };
+}
+
+sub get_dles
+{
+    my $self  = shift @_;
+    my $cache = $self->{cache};
+    my @dles;
+
+    if ( !defined $cache->{dles} ) {
+        foreach my $hostname ( $self->get_hosts() ) {
+            map { push @dles, [ $hostname, $_ ] } $self->get_disks($hostname);
+        }
+        $cache->{dles} = \@dles;
+    }
+    return @{ $cache->{dles} };
+}
+
+sub xml_output
+{
+    my ( $self, $org, $config ) = @_;
+    use Amanda::Report::xml;
+    return Amanda::Report::xml::make_amreport_xml( $self, $org, $config );
+}
+
+sub get_dle_info
+{
+    my $self = shift @_;
+    my ( $hostname, $disk, $field ) = @_;
+
+    return ( defined $field )
+      ? $self->{data}{disklist}{$hostname}{$disk}{$field}
+      : $self->{data}{disklist}{$hostname}{$disk};
+}
+
+sub get_program_info
+{
+    my ($self, $program, $field, $default) = @_;
+    my $prog = $self->{data}{programs}{$program};
+
+    $prog->{$field} = $default if (defined $field && !defined $prog->{$field});
+
+    return (defined $field) ? $prog->{$field} : $prog;
+}
+
+sub get_tape
+{
+    my ($self, $label) = @_;
+
+    my $taper       = $self->get_program_info("taper");
+    my $tapes       = $taper->{tapes}       ||= {};
+    my $tape_labels = $taper->{tape_labels} ||= [];
+
+    if (!exists $tapes->{$label}) {
+        push @$tape_labels, $label;
+        $tapes->{$label} = {date => "",
+                           kb => 0,
+                           files => 0,
+                           dle => 0,
+                           time => 0};
+    }
+
+    return $tapes->{$label};
+}
+
+sub get_flag
+{
+    my ( $self, $flag ) = @_;
+    return $self->{flags}{$flag};
+}
+
+sub _handle_planner_line
+{
+    my $self = shift @_;
+    my ( $type, $str ) = @_;
+    my $data     = $self->{data};
+    my $programs = $data->{programs};
+    my $disklist = $data->{disklist} ||= {};
+    my $planner  = $programs->{planner} ||= {};
+
+    if ( $type == $L_INFO ) {
+        return $self->_handle_info_line( "planner", $str );
+
+    } elsif ( $type == $L_WARNING ) {
+        return $self->_handle_warning_line( "planner", $str );
+
+    } elsif ( $type == $L_START ) {
+
+        $self->{flags}{normal_run} = 1;
+        return $self->_handle_start_line( "planner", $str );
+
+    } elsif ( $type == $L_FINISH ) {
+
+        my @info = Amanda::Util::split_quoted_strings($str);
+        return $planner->{time} = $info[3];
+
+    } elsif ( $type == $L_DISK ) {
+        return $self->_handle_disk_line( "planner", $str );
+
+    } elsif ( $type == $L_SUCCESS ) {
+        return $self->_handle_success_line( "planner", $str );
+
+    } elsif ( $type == $L_ERROR ) {
+        return $self->_handle_error_line( "planner", $str );
+
+    } elsif ( $type == $L_FATAL ) {
+        return $self->_handle_fatal_line( "planner", $str );
+
+    } elsif ( $type == $L_FAIL ) {
+
+        # TODO: these are not like other failure messages: later
+        # handle here
+        return $self->_handle_fail_line( "planner", $str );
+
+    } else {
+        return $self->_handle_bogus_line( $P_PLANNER, $type, $str );
+    }
+}
+
+
+sub _handle_driver_line
+{
+    my $self = shift @_;
+    my ( $type, $str ) = @_;
+    my $data     = $self->{data};
+    my $disklist = $data->{disklist};
+    my $programs = $data->{programs};
+    my $driver_p = $programs->{driver} ||= {};
+
+    if ( $type == $L_INFO ) {
+        return $self->_handle_info_line( "driver", $str );
+
+    } elsif ( $type == $L_START ) {
+        return $self->_handle_start_line( "driver", $str );
+
+    } elsif ( $type == $L_FINISH ) {
+
+        my @info = Amanda::Util::split_quoted_strings($str);
+        $self->{flags}{got_finish} = 1;
+        return $driver_p->{time} = $info[3];
+
+    } elsif ( $type == $L_STATS ) {
+
+        my @info = Amanda::Util::split_quoted_strings($str);
+        if ( $info[0] eq "hostname" ) {
+
+            return $self->{hostname} = $info[1];
+
+        } elsif ( $info[0] eq "startup" ) {
+
+            my @info = Amanda::Util::split_quoted_strings($str);
+            return $driver_p->{start_time} = $info[2];
+
+        } elsif ( $info[0] eq "estimate" ) {
+
+            # estimate format:
+            # STATS driver estimate <hostname> <disk> <timestamp>
+            # <level> [sec <sec> nkb <nkb> ckb <ckb> jps <kps>]
+            # note that the [..] section is *not* quoted properly
+            my ($hostname, $disk, $timestamp, $level) = @info[ 1 .. 4 ];
+
+            # if the planner didn't define the DLE then this is a bad
+            # line
+            unless (exists $disklist->{$hostname}{$disk}) {
+                return $self->_handle_bogus_line($P_DRIVER, $type, $str);
+            }
+
+            my $dle = $self->get_dle_info($hostname, $disk);
+            my ($sec, $nkb, $ckb, $kps) = @info[ 6, 8, 10, 12 ];
+            $kps =~ s{\]}{};    # strip trailing "]"
+
+            $dle->{estimate} = {
+                level => $level,
+                sec   => $sec,
+                nkb   => $nkb,
+                ckb   => $ckb,
+                kps   => $kps,
+            };
+
+        } else {
+            return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
+        }
+
+    } elsif ( $type == $L_WARNING ) {
+
+        $self->{flags}{exit_status} |= STATUS_TAPE
+          if ($str eq "Taper protocol error");
+
+        return $self->_handle_warning_line("driver", $str);
+
+    } elsif ( $type == $L_ERROR ) {
+        return $self->_handle_error_line( "driver", $str );
+
+    } elsif ( $type == $L_FATAL ) {
+        return $self->_handle_fatal_line( "driver", $str );
+
+    } elsif ( $type == $L_FAIL ) {
+        return $self->_handle_fail_line( "driver", $str );
+
+    } else {
+        return $self->_handle_bogus_line( $P_DRIVER, $type, $str );
+    }
+}
+
+
+sub _handle_dumper_line
+{
+    my $self = shift @_;
+    my ( $type, $str ) = @_;
+    my $data     = $self->{data};
+    my $disklist = $data->{disklist};
+    my $programs = $data->{programs};
+    my $dumper_p = $programs->{dumper} ||= {};
+
+    if ( $type == $L_INFO ) {
+        return $self->_handle_info_line( "dumper", $str );
+
+    } elsif ( $type == $L_STRANGE ) {
+
+        my @info = Amanda::Util::split_quoted_strings($str);
+        my ( $hostname, $disk, $level ) = @info[ 0 .. 2 ];
+        my ( $sec, $kb, $kps, $orig_kb ) = @info[ 4, 6, 8, 10 ];
+       $kb = int($kb/1024) if $info[4] eq 'bytes';
+        $orig_kb =~ s{\]$}{};
+
+        my $dle    = $disklist->{$hostname}->{$disk};
+        my $try    = $self->_get_try( $dle, "dumper", $self->{'run_timestamp'});
+        my $dumper = $try->{dumper} ||= {};
+       $dumper->{level} = $level;
+       $dumper->{status} = 'strange';
+        $dumper->{sec}       = $sec;
+        $dumper->{kb}        = $kb;
+        $dumper->{kps}       = $kps;
+        $dumper->{orig_kb}   = $orig_kb;
+
+       $self->{contline} = $dumper->{stranges} ||= [];
+       $dumper->{nb_stranges} = 0;
+       $self->{nbline_ref} = \$dumper->{nb_stranges};
+       $self->{nb_normal} = 0;
+       $self->{nb_strange} = 0;
+       $self->{nb_error} = 0;
+
+        return $self->{flags}{exit_status} |= STATUS_STRANGE
+
+    } elsif ( $type == $L_WARNING ) {
+
+       return $self->_handle_warning_line("dumper", $str);
+
+    } elsif ( $type == $L_SUCCESS ) {
+
+        my @info = Amanda::Util::split_quoted_strings($str);
+        my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
+        my ( $sec, $kb, $kps, $orig_kb ) = @info[ 5, 7, 9, 11 ];
+       $kb = int($kb/1024) if $info[6] eq 'bytes';
+        $orig_kb =~ s{\]$}{};
+
+        my $dle    = $disklist->{$hostname}->{$disk};
+        my $try    = $self->_get_try( $dle, "dumper", $timestamp );
+        my $dumper = $try->{dumper} ||= {};
+
+        $dumper->{date}      = $timestamp;
+        $dumper->{level}     = $level;
+        $dumper->{sec}       = $sec;
+        $dumper->{kb}        = $kb;
+        $dumper->{kps}       = $kps;
+        $dumper->{orig_kb}   = $orig_kb;
+
+        return $dumper->{status} = "success";
+
+    } elsif ( $type == $L_ERROR ) {
+        return $self->_handle_error_line( "dumper", $str );
+
+    } elsif ( $type == $L_FATAL ) {
+        return $self->_handle_fatal_line( "dumper", $str );
+
+    } elsif ( $type == $L_FAIL ) {
+        return $self->_handle_fail_line( "dumper", $str );
+
+    } else {
+        return $self->_handle_bogus_line( $P_DUMPER, $type, $str );
+    }
+}
+
+
+sub _handle_chunker_line
+{
+    my $self = shift @_;
+    my ( $type, $str ) = @_;
+    my $data      = $self->{data};
+    my $disklist  = $data->{disklist};
+    my $programs  = $data->{programs};
+    my $chunker_p = $programs->{chunker} ||= {};
+
+    if ( $type == $L_INFO ) {
+        return $self->_handle_info_line( "chunker", $str );
+
+    } elsif ( $type == $L_SUCCESS || $type == $L_PARTIAL ) {
+
+        my @info = Amanda::Util::split_quoted_strings($str);
+        my ( $hostname, $disk, $timestamp, $level ) = @info[ 0 .. 3 ];
+        my ( $sec, $kb, $kps ) = @info[ 5, 7, 9 ];
+       $kb = int($kb/1024) if $info[6] eq 'bytes';
+        $kps =~ s{\]$}{};
+
+        my $dle     = $disklist->{$hostname}->{$disk};
+        my $try     = $self->_get_try( $dle, "chunker", $timestamp );
+        my $chunker = $try->{chunker} ||= {};
+
+        $chunker->{date}  = $timestamp;
+        $chunker->{level} = $level;
+        $chunker->{sec}   = $sec;
+        $chunker->{kb}    = $kb;
+        $chunker->{kps}   = $kps;
+
+        return $chunker->{status} =
+          ( $type == $L_SUCCESS ) ? "success" : "partial";
+
+    } elsif ( $type == $L_ERROR ) {
+        return $self->_handle_error_line( "chunker", $str );
+
+    } elsif ( $type == $L_FATAL ) {
+        return $self->_handle_fatal_line( "chunker", $str );
+
+    } elsif ( $type == $L_FAIL ) {
+        return $self->_handle_fail_line( "chunker", $str );
+
+    } else {
+        return $self->_handle_bogus_line( $P_CHUNKER, $type, $str );
+    }
+}
+
+
+sub _handle_taper_line
+{
+    my $self = shift @_;
+    my ( $type, $str ) = @_;
+    my $data     = $self->{data};
+    my $disklist = $data->{disklist};
+    my $programs = $data->{programs};
+    my $taper_p  = $programs->{taper} ||= {};
+
+    if ( $type == $L_START ) {
+        # format is:
+        # START taper datestamp <start> label <label> tape <tapenum>
+        my @info = Amanda::Util::split_quoted_strings($str);
+        my ($datestamp, $label, $tapenum) = @info[ 1, 3, 5 ];
+        my $tape = $self->get_tape($label);
+        $tape->{date} = $datestamp;
+        $tape->{label} = $label;
+
+       # keep this tape for later
+       $self->{'_current_tape'} = $tape;
+
+       # call through to the generic start line function
+        $self->_handle_start_line( "taper", $str );
+    } elsif ( $type == $L_PART || $type == $L_PARTPARTIAL ) {
+
+# format is:
+# <label> <tapefile> <hostname> <disk> <timestamp> <currpart>/<predparts> <level> [sec <sec> kb <kb> kps <kps>]
+#
+# format for $L_PARTPARTIAL is the same as $L_PART, plus <err> at the end
+        my @info = Amanda::Util::split_quoted_strings($str);
+        my ($label, $tapefile, $hostname, $disk, $timestamp) = @info[ 0 .. 4 ];
+
+        $info[5] =~ m{^(\d+)\/(-?\d+)$};
+        my ( $currpart, $predparts ) = ( $1, $2 );
+
+        my ($level, $sec, $kb, $kps, $orig_kb) = @info[ 6, 8, 10, 12, 14 ];
+       $kb = int($kb/1024) if $info[9] eq 'bytes';
+        $kps =~ s{\]$}{};
+        $orig_kb =~ s{\]$}{} if defined($orig_kb);
+
+        my $dle   = $disklist->{$hostname}{$disk};
+        my $try   = $self->_get_try($dle, "taper", $timestamp);
+        my $taper = $try->{taper} ||= {};
+        my $parts = $taper->{parts} ||= [];
+
+        my $part = {
+            label => $label,
+            date  => $timestamp,
+            file  => $tapefile,
+            sec   => $sec,
+            kb    => $kb,
+            kps   => $kps,
+            partnum  => $currpart,
+        };
+
+       $taper->{orig_kb} = $orig_kb;
+
+        push @$parts, $part;
+
+        my $tape = $self->get_tape($label);
+       # count this as a filesystem if this is the first part
+        $tape->{dle}++ if $currpart == 1;
+        $tape->{kb}   += $kb;
+        $tape->{time} += $sec;
+        $tape->{files}++;
+
+    } elsif ( $type == $L_DONE || $type == $L_PARTIAL ) {
+
+# format is:
+# $type = DONE | PARTIAL
+# $type taper <hostname> <disk> <timestamp> <part> <level> [sec <sec> kb <kb> kps <kps>]
+        my @info = Amanda::Util::split_quoted_strings($str);
+        my ( $hostname, $disk, $timestamp, $part_ct, $level ) = @info[ 0 .. 4 ];
+        my ( $sec, $kb, $kps, $orig_kb ) = @info[ 6, 8, 10, 12 ];
+       $kb = int($kb/1024) if $info[7] eq 'bytes';
+       my $error;
+       if ($type == $L_PARTIAL) {
+           if ($kps =~ /\]$/) {
+               $error = join " ", @info[ 11 .. $#info ];
+           } else {
+               $error = join " ", @info[ 13 .. $#info ];
+           }
+       }
+        $kps =~ s{\]$}{};
+        $orig_kb =~ s{\]$}{} if defined $orig_kb;
+
+        my $dle   = $disklist->{$hostname}->{$disk};
+        my $try   = $self->_get_try($dle, "taper", $timestamp);
+        my $taper = $try->{taper} ||= {};
+        my $parts = $taper->{parts};
+
+        if ($part_ct - $#$parts != 1) {
+            ## this should always be true; do nothing right now
+        }
+
+        $taper->{level} = $level;
+        $taper->{sec}   = $sec;
+        $taper->{kb}    = $kb;
+        $taper->{kps}   = $kps;
+
+        $taper->{status} = ( $type == $L_DONE ) ? "done" : "partial";
+       $taper->{error} = $error if $type == $L_PARTIAL;
+
+    } elsif ( $type == $L_INFO ) {
+        $self->_handle_info_line("taper", $str);
+
+    } elsif ( $type == $L_WARNING ) {
+       $self->_handle_warning_line("taper", $str);
+
+    } elsif ( $type == $L_ERROR ) {
+
+        if ($str =~ m{^no-tape}) {
+
+           my @info = Amanda::Util::split_quoted_strings($str);
+           my $failure_from = $info[1];
+           my $error = join " ", @info[ 2 .. $#info ];
+
+            $self->{flags}{exit_status} |= STATUS_TAPE;
+            $self->{flags}{degraded_mode} = 1;
+           $taper_p->{failure_from} = $failure_from;
+            $taper_p->{tape_error} = $error;
+
+        } else {
+            $self->_handle_error_line("taper", $str);
+        }
+
+    } elsif ( $type == $L_FATAL ) {
+        return $self->_handle_fatal_line( "taper", $str );
+
+    } elsif ( $type == $L_FAIL ) {
+        $self->_handle_fail_line( "taper", $str );
+
+    } else {
+        $self->_handle_bogus_line( $P_TAPER, $type, $str );
+    }
+}
+
+
+sub _handle_amflush_line
+{
+    my $self = shift @_;
+    my ( $type, $str ) = @_;
+    my $data      = $self->{data};
+    my $disklist  = $data->{disklist};
+    my $programs  = $data->{programs};
+    my $amflush_p = $programs->{amflush} ||= {};
+
+    if ( $type == $L_DISK ) {
+        return $self->_handle_disk_line( "amflush", $str );
+
+    } elsif ( $type == $L_START ) {
+        return $self->_handle_start_line( "amflush", $str );
+
+    } elsif ( $type == $L_INFO ) {
+        return $self->_handle_info_line( "amflush", $str );
+
+    } else {
+        return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
+    }
+}
+
+sub _handle_amvault_line
+{
+    my $self = shift @_;
+    my ( $type, $str ) = @_;
+    my $data      = $self->{data};
+    my $disklist  = $data->{disklist};
+    my $programs  = $data->{programs};
+    my $amvault_p = $programs->{amvault} ||= {};
+
+    if ( $type == $L_START ) {
+        return $self->_handle_start_line( "amvault", $str );
+
+    } elsif ( $type == $L_INFO ) {
+        return $self->_handle_info_line( "amvault", $str );
+
+    } elsif ( $type == $L_ERROR ) {
+        return $self->_handle_error_line( "amvault", $str );
+
+    } elsif ( $type == $L_FATAL ) {
+        return $self->_handle_fatal_line( "amvault", $str );
+
+    } elsif ( $type == $L_DISK ) {
+        return $self->_handle_disk_line( "amvault", $str );
+
+    } else {
+        return $self->_handle_bogus_line( $P_AMFLUSH, $type, $str );
+    }
+}
+
+
+sub _handle_amdump_line
+{
+    my $self = shift;
+    my ( $type, $str ) = @_;
+    my $data     = $self->{data};
+    my $disklist = $data->{disklist};
+    my $programs = $data->{programs};
+    my $amdump = $programs->{amdump} ||= {};
+
+    if ( $type == $L_INFO ) {
+        $self->_handle_info_line("amdump", $str);
+
+    } elsif ( $type == $L_START ) {
+        $self->_handle_start_line("amdump", $str);
+
+    } elsif ( $type == $L_FATAL ) {
+        return $self->_handle_fatal_line( "amdump", $str );
+
+    } elsif ( $type == $L_ERROR ) {
+        $self->_handle_error_line("amdump", $str);
+    }
+}
+
+
+sub _handle_fail_line
+{
+    my ($self, $program, $str) = @_;
+
+    my @info = Amanda::Util::split_quoted_strings($str);
+    my ($hostname, $disk, $timestamp, $level) = @info;
+    my $error;
+    my $failure_from;
+    if ($program eq 'taper') {
+       $failure_from = $info[4];
+       $error = join " ", @info[ 5 .. $#info ];
+    } else {
+       $error = join " ", @info[ 4 .. $#info ];
+    }
+
+    #TODO: verify that this reaches the right try.  Also, DLE or
+    #program?
+    my $dle = $self->get_dle_info($hostname, $disk);
+
+    my $program_d;
+    if ($program eq "planner" ||
+        $program eq "driver") {
+       $program_d = $dle->{$program} ||= {};
+    } else {
+        my $try = $self->_get_try($dle, $program, $timestamp);
+        $program_d = $try->{$program} ||= {};
+    }
+
+    $program_d->{level}  = $level;
+    $program_d->{status} = "fail";
+    $program_d->{failure_from}  = $failure_from;
+    $program_d->{error}  = $error;
+
+    my $errors = $self->get_program_info("program", "errors", []);
+    push @$errors, $error;
+
+    $self->{flags}{exit_status} |= STATUS_FAILED;
+    if ($program eq "dumper") {
+        $self->{contline} = $program_d->{errors} ||= [];
+       $program_d->{nb_errors} = 0;
+       $self->{nbline_ref} = \$program_d->{nb_errors};
+       $self->{nb_normal} = 0;
+       $self->{nb_strange} = 0;
+       $self->{nb_error} = 0;
+    }
+}
+
+
+sub _handle_error_line
+{
+    my $self = shift @_;
+    my ( $program, $str ) = @_;
+
+    my $data      = $self->{data};
+    my $programs  = $data->{programs};
+    my $program_p = $programs->{$program};
+    my $errors_p  = $program_p->{errors} ||= [];
+
+    $self->{flags}{exit_status} |= 1;
+
+    push @$errors_p, $str;
+}
+
+
+sub _handle_fatal_line
+{
+    my $self = shift @_;
+    my ( $program, $str ) = @_;
+
+    my $data      = $self->{data};
+    my $programs  = $data->{programs};
+    my $program_p = $programs->{$program};
+    my $fatal_p  = $program_p->{fatal} ||= [];
+
+    $self->{flags}{exit_status} |= 1;
+
+    push @$fatal_p, $str;
+}
+
+
+sub _handle_start_line
+{
+    my $self = shift @_;
+    my ( $program, $str ) = @_;
+
+    my $data     = $self->{data};
+    my $disklist = $data->{disklist};
+    my $programs = $data->{programs};
+
+    my $program_p = $programs->{$program} ||= {};
+
+    my @info = Amanda::Util::split_quoted_strings($str);
+    my $timestamp = $info[1];
+    $program_p->{start} = $info[1];
+
+    if ($self->{'run_timestamp'} ne '00000000000000'
+               and $self->{'run_timestamp'} ne $timestamp) {
+       warning("not all timestamps in this file are the same; "
+               . "$self->{run_timestamp}; $timestamp");
+    }
+    $self->{'run_timestamp'} = $timestamp;
+}
+
+
+sub _handle_disk_line
+{
+    my $self = shift @_;
+    my ($program, $str) = @_;
+
+    my $data     = $self->{data};
+    my $disklist = $data->{disklist};
+    my $hosts    = $self->{cache}{hosts} ||= [];
+    my $dles     = $self->{cache}{dles}  ||= [];
+
+    my @info = Amanda::Util::split_quoted_strings($str);
+    my ($hostname, $disk) = @info;
+
+    if (!exists $disklist->{$hostname}) {
+
+        $disklist->{$hostname} = {};
+        push @$hosts, $hostname;
+    }
+
+    if (!exists $disklist->{$hostname}{$disk}) {
+
+        push @$dles, [ $hostname, $disk ];
+        my $dle = $disklist->{$hostname}{$disk} = {};
+        $dle->{'estimate'} = undef;
+        $dle->{'dumps'}    = {};
+    }
+    return;
+}
+
+sub _handle_success_line
+{
+    my $self = shift @_;
+    my ($program, $str) = @_;
+
+    my $data     = $self->{data};
+    my $disklist = $data->{disklist};
+    my $hosts    = $self->{cache}{hosts} ||= [];
+    my $dles     = $self->{cache}{dles}  ||= [];
+
+    my @info = Amanda::Util::split_quoted_strings($str);
+    my ($hostname, $disk, $timestamp, $level, $stat1, $stat2) = @info;
+
+    if ($stat1 =~ /skipped/) {
+        $disklist->{$hostname}{$disk}->{$program}->{'status'} = 'skipped';
+    }
+    return;
+}
+
+
+sub _handle_info_line
+{
+    my $self = shift @_;
+    my ( $program, $str ) = @_;
+
+    my $data     = $self->{data};
+    my $disklist = $data->{disklist};
+    my $programs = $data->{programs};
+
+    my $program_p = $programs->{$program} ||= {};
+
+    if ( $str =~ m/^\w+ pid \d+/ || $str =~ m/^pid-done \d+/ ) {
+
+        #do not report pid lines
+        return;
+
+    } else {
+        my $notes = $program_p->{notes} ||= [];
+        push @$notes, $str;
+    }
+}
+
+sub _handle_warning_line
+{
+    my $self = shift @_;
+    my ( $program, $str ) = @_;
+
+    $self->_handle_info_line($program, $str);
+}
+
+sub _handle_bogus_line
+{
+    my $self = shift @_;
+    my ( $prog, $type, $str ) = @_;
+
+    my $data = $self->{data};
+    my $boguses = $data->{boguses} ||= [];
+    push @$boguses, [ $prog, $type, $str ];
+}
+
+sub check_missing_fail_strange
+{
+    my ($self) = @_;
+    my @dles = $self->get_dles();
+
+    foreach my $dle_entry (@dles) {
+        my $alldumps = $self->get_dle_info(@$dle_entry, 'dumps');
+       my $driver = $self->get_dle_info(@$dle_entry, 'driver');
+       my $planner = $self->get_dle_info(@$dle_entry, 'planner');
+
+       if ($planner && $planner->{'status'} eq 'fail') {
+           $self->{flags}{dump_failed} = 1;
+       } elsif ($planner && $planner->{'status'} eq 'skipped') {
+           # We don't want these to be counted as missing below
+       } elsif (!defined $alldumps->{$self->{'run_timestamp'}} and
+                !$driver and
+                !$planner) {
+           $self->{flags}{results_missing} = 1;
+           $self->{flags}{exit_status} |= STATUS_MISSING;
+       } else {
+           #get latest try
+           my $tries = $alldumps->{$self->{'run_timestamp'}};
+           my $try = @$tries[-1];
+
+           if (exists $try->{dumper} && $try->{dumper}->{status} eq 'fail') {
+               $self->{flags}{dump_failed} = 1;
+           } elsif ((defined($try->{'chunker'}) &&
+                $try->{'chunker'}->{status} eq 'success') ||
+               (defined($try->{'taper'}) &&
+                $try->{'taper'}->{status} eq 'done')) {
+               #chunker or taper success, use dumper status
+               if (exists $try->{dumper} && $try->{dumper}->{status} eq 'strange') {
+                   $self->{flags}{dump_strange} = 1;
+               }
+           } else {
+               #chunker or taper failed, the dump is not valid.
+               $self->{flags}{dump_failed} = 1;
+           }
+       }
+    }
+}
+
+#
+# NOTE: there may be a complicated state diagram lurking in the midst
+# of taper and chunker.  You have been warned.
+#
+sub _get_try
+{
+    my $self = shift @_;
+    my ( $dle, $program, $timestamp ) = @_;
+    my $tries = $dle->{'dumps'}{$timestamp} ||= [];
+
+    if (
+        !@$tries    # no tries
+        || defined $tries->[-1]->{$program}->{status}
+        && $self->_program_finished(    # program has finished
+            $program, $tries->[-1]->{$program}->{status}
+        )
+      ) {
+        push @$tries, {};
+    }
+    return $tries->[-1];
+}
+
+
+sub _program_finished
+{
+    my $self = shift @_;
+    my ( $program, $status ) = @_;
+
+    if ( $program eq "chunker" ) {
+
+        if ( $status eq "partial" ) {
+            return;
+        } else {
+            return 1;
+        }
+
+    } elsif ( $status eq "done"
+        || $status eq "success"
+        || $status eq "fail"
+        || $status eq "partial" ) {
+        return 1;
+
+    } else {
+        return 0;
+    }
+}
+
+1;