# Copyright (c) 2010-2012 Zmanda, Inc. All Rights Reserved.
# Copyright (c) 2013-2016 Carbonite, 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
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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 Mathlida Ave, Suite 300
# Sunnyvale, CA 94085, or: http://www.zmanda.com
#
=head1 NAME
Amanda::Report::json -- Generate an amanda report in json format.
=over
=item report format
$report->{'head'}->{'config_name'} => $config
{'org'} => $org
{'date'} => "July 24, 2013"
{'timestamp'} => $run_timestamp
{'hostname'} => "localhost.localdomain"
{'exit_status'} => $exit_status
{'status'} => "done" | "running" | "aborted"
$report->{'tapeinfo'}->{'storage'}->{$storage}->{'use'} => @labels #label used for each storage
{'next'} => @labels #labels to use on next run
{'next_to_use'} => $nb #
{'new_labelled'} => @labels # new labelled tapes.
$report->{'usage_by_tape'}[]->{'tape_label'} => $label
{'nb'}
{'size'}
{'dump_timestamp'}
{'configuration_id'}
{'time_duration'}
{'nc'}
{'percent_use'}
$report->{'notes'}[] # array of text lines;
$report->{'summary'}[]->{'last_tape_label'}
{'dle_status'}
{'tape_duration'}
{'backup_level'}
{'configuration_id'}
{'hostname'}
{'dump_partial'}
{'tape_rate'}
{'dump_rate'}
{'disk_name'}
{'dump_duration'}
{'dump_timestamp'}
{'dump_orig_kb'}
{'dump_comp'}
{'dump_out_kb'}
$report->{'statistic'}->{'tape_size'}->{'incr'}
{'full'}
{'total'}
{'parts_taped'}->{'incr'}
{'full'}
{'total'}
{'dles_taped'}->{'incr'}
{'full'}
{'total'}
{'Avg_tape_write_speed'}->{'incr'}
{'full'}
{'total'}
{'dump_time'}->{'incr'}
{'full'}
{'total'}
{'tape_used'}->{'incr'}
{'full'}
{'total'}
{'tape_time'}->{'incr'}
{'full'}
{'total'}
{'original_size'}->{'incr'}
{'full'}
{'total'}
{'output_size'}->{'incr'}
{'full'}
{'total'}
{'dles_dumped'}->{'incr'}
{'full'}
{'total'}
{'avg_compression'}->{'incr'}
{'full'}
{'total'}
{'avg_dump_rate'}->{'incr'}
{'full'}
{'total'}
{'dumpdisks'} => "1:1"
{'tapeparts'} => "1:1"
{'tapedisks'} => "1:1"
{'run_time'}
{'estimate_time'}
=back
=cut
package Amanda::Report::json;
use strict;
use warnings;
use base qw( Amanda::Report::human );
use Carp;
use POSIX;
use Data::Dumper;
use JSON;
use Amanda::Config qw(:getconf config_dir_relative);
use Amanda::Util qw(:constants quote_string );
use Amanda::Holding;
use Amanda::Tapelist;
use Amanda::Debug qw( debug );
use Amanda::Util qw( quote_string );
use Amanda::Report;
## constants that define the column specification output format.
use constant COLSPEC_NAME => 0; # column name; used internally
use constant COLSPEC_PRE_SPACE => 1; # prefix spaces
use constant COLSPEC_WIDTH => 2; # column width
use constant COLSPEC_PREC => 3; # post-decimal precision
use constant COLSPEC_MAXWIDTH => 4; # resize if set
use constant COLSPEC_FORMAT => 5; # sprintf format
use constant COLSPEC_TITLE => 6; # column title
my $opt_zmc_cid = 1;
## class functions
sub new
{
my ($class, $report, $config_name, $logfname) = @_;
my $self = {
report => $report,
config_name => $config_name,
logfname => $logfname,
## config info
disp_unit => getconf($CNF_DISPLAYUNIT),
unit_div => getconf_unit_divisor(),
## statistics
incr_stats => {},
full_stats => {},
total_stats => {},
dumpdisks => [ 0, 0 ], # full_count, incr_count
tapedisks => [ 0, 0 ],
tapeparts => [ 0, 0 ],
sections => {},
section => 'header',
};
if (defined $report) {
my (@errors, @stranges, @notes);
@errors =
map { @{ $report->get_program_info($_, "errors", []) }; }
Amanda::Report::human::PROGRAM_ORDER;
## prepend program name to notes lines.
foreach my $program (Amanda::Report::human::PROGRAM_ORDER) {
push @notes,
map { "$program: $_" }
@{ $report->get_program_info($program, "notes", []) };
}
$self->{errors} = \@errors;
$self->{notes} = \@notes;
}
bless $self, $class;
return $self;
}
sub divzero
{
my ($self) = shift;
my ($a, $b) = @_;
return ($b == 0) ? undef : 0+$a / $b;
}
sub divzero_wide
{
my ($self) = shift;
my ($a, $b) = @_;
return ($b == 0) ? undef : 0+$a / $b;
}
sub divzero_col
{
my ($self) = shift;
my ($a, $b, $col) = @_;
return ($b == 0) ? undef : 0+($a / $b);
}
sub hrmn
{
my ($self) = shift;
my ($sec) = @_;
return $sec;
}
sub mnsc
{
my ($self) = shift;
my ($sec) = @_;
return $sec;
}
sub tounits
{
my ($self) = shift;
my ($val, %params) = @_;
return $val;
}
sub zprint
{
my $self = shift;
my $line = join('', @_);
chomp $line;
chomp $line;
chomp $line;
push @{$self->{'sections'}{$self->{'section'}}}, $line unless $line eq "";
}
sub zsprint
{
my $self = shift;
my $section = shift;
$section = lc($section);
$section =~ s/\s*:?\s*$//;
$section =~ s/^\s+//;
$section =~ s/\s/_/g;
$section =~ s/_dump_/_/;
$section =~ s/failed_details/failure_details/;
$self->{'section'} = $section;
#FAILURE DUMP SUMMARY => failure_summary
#STRANGE DUMP SUMMARY => strange_summary
#DUMP SUMMARY => dump_summary
#FAILED DUMP DETAILS => failure_details
#STRANGE DUMP DETAILS => strange_details
#NOTES => notes
#USAGE BY TAPE => usage_by_tape
#statistics
}
sub generate_report
{
my $self = shift;
## collect statistics
$self->calculate_stats();
## print the basic info header
$self->print_header();
## print out statements about past and predicted tape usage
$self->output_tapeinfo();
## print out error messages from the run
$self->output_error_summaries();
## print out aggregated statistics for the whole dump
$self->output_stats();
## print out statistics for each tape used
$self->output_tape_stats();
## print out all errors & comments
$self->output_details();
## print out dump statistics per DLE
$self->output_summary();
}
sub write_report
{
my $self = shift;
my $fh = shift;
$fh || confess "error: no file handle given to Amanda::Report::json::write_report\n";
$self->{fh} = $fh;
$self->generate_report();
my $json = JSON->new->allow_nonref;
print {$self->{'fh'}} $json->pretty->allow_blessed->encode($self->{'sections'});
return;
}
sub print_header
{
my ($self) = @_;
my $report = $self->{report};
my $fh = $self->{fh};
my $config_name = $self->{config_name};
my $hostname = $report->{hostname};
my $run_timestamp = $report->get_timestamp();
my $exit_status = $report->get_exit_status();
my $status = $report->get_status();
my $org = getconf($CNF_ORG);
# TODO: this should be a shared method somewhere
my $timestamp = $report->get_timestamp();
my ($year, $month, $day) = ($timestamp =~ m/^(\d\d\d\d)(\d\d)(\d\d)/);
my $date = POSIX::strftime('%B %e, %Y', 0, 0, 0, $day, $month - 1, $year - 1900);
$date =~ s/ / /g; # get rid of intervening space
$self->zprint("*** THE DUMPS DID NOT FINISH PROPERLY!\n\n")
unless ($report->{flags}{got_finish});
if ($hostname) {
$self->{'sections'}{'head'}{"hostname"} = $hostname;
}
$self->{'sections'}{'head'}{"org"} = $org;
$self->{'sections'}{'head'}{"config_name"} = $config_name;
$self->{'sections'}{'head'}{"date"} = $date;
$self->{'sections'}{'head'}{"timestamp"} = $run_timestamp;
$self->{'sections'}{'head'}{"exit_status"} = $exit_status;
$self->{'sections'}{'head'}{"status"} = $status;
return;
}
sub output_tapeinfo
{
my ($self) = @_;
my $report = $self->{report};
my $fh = $self->{fh};
my $logfname = $self->{logfname};
my $taper = $report->get_program_info("taper");
my $tapes = $taper->{tapes} || {};
my $tape_labels = $taper->{tape_labels} || [];
my %full_stats = %{ $self->{full_stats} };
my %incr_stats = %{ $self->{incr_stats} };
my %total_stats = %{ $self->{total_stats} };
for my $storage_n (@{$report->{'storage_list'}}) {
my $st = Amanda::Config::lookup_storage($storage_n);
if (!$st) {
debug("Storage '%s' not found", $storage_n);
next;
}
if (storage_getconf($st, $STORAGE_REPORT_USE_MEDIA)) {
# find and count label use for the storage
my @storage_tape_labels;
foreach my $tape_label (@$tape_labels) {
my $tape = $tapes->{$tape_label};
if ($tape->{'storage'} eq $storage_n) {
push @storage_tape_labels, $tape_label;
}
}
if (@storage_tape_labels > 0) {
$self->{'sections'}->{'tapeinfo'}->{'storage'}->{$storage_n}->{'use'} = \@storage_tape_labels;
}
}
}
if (my $tape_error =
$report->get_program_info("taper", "tape_error", undef)) {
if ($report->get_program_info("taper", "failure_from", undef) eq "config") {
# remove leading [ and trailling ]
$tape_error =~ s/^\[//;
$tape_error =~ s/\]$//;
$self->zprint("Not using all tapes because $tape_error.\n");
} else {
$self->zprint("*** A TAPE ERROR OCCURRED: $tape_error.\n");
}
#$tape_error =~ s{^no-tape }{};
}
## if this is a historical report, do not generate holding disk
## information. If this dump is the most recent, output holding
## disk info.
if ($report->get_flag("historical")) {
$self->{'sections'}->{'tapeinfo'}->{'hdisk'}->{'some_dump'} = 1
if $report->get_flag("degraded_mode")
} else {
my @holding_list = Amanda::Holding::get_files_for_flush(0);
my $h_size = 0;
foreach my $holding_file (@holding_list) {
$h_size += (0 + Amanda::Holding::file_size($holding_file, 1));
}
if ($h_size > 0) {
$self->{'sections'}->{'tapeinfo'}->{'hdisk'}->{'size'} = $h_size;
(getconf($CNF_AUTOFLUSH))
? $self->{'sections'}->{'tapeinfo'}->{'hdisk'}->{'flush_next_run'} = 1
: $self->{'sections'}->{'tapeinfo'}->{'hdisk'}->{'run_amflush'} = 1;
} elsif ($report->get_flag("degraded_mode")) {
$self->{'sections'}->{'tapeinfo'}->{'hdisk'}->{'size'} = 0;
}
}
for my $storage_n (@{$report->{'storage_list'}}) {
my $st = Amanda::Config::lookup_storage($storage_n);
if (!$st) {
debug("Storage '%s' not found", $storage_n);
next;
}
if (storage_getconf($st, $STORAGE_REPORT_NEXT_MEDIA)) {
my $run_tapes = storage_getconf($st, $STORAGE_RUNTAPES);
my $nb_new_tape = 0;
my $for_storage = '';
$for_storage = " for storage '$storage_n'" if @{$report->{'storage_list'}} > 1;
my @tape_labels;
if ($run_tapes) {
$self->{'sections'}->{'tapeinfo'}->{'storage'}->{$storage_n}->{'next_to_use'} = $run_tapes;
}
my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
my ($tl, $message) = Amanda::Tapelist->new($tlf);
my $labelstr = storage_getconf($st, $STORAGE_LABELSTR);
my $tapepool = storage_getconf($st, $STORAGE_TAPEPOOL);
my $policy = Amanda::Policy->new(policy => storage_getconf($st, $STORAGE_POLICY));
my $retention_tapes = $policy->{'retention_tapes'};
my $retention_days = $policy->{'retention_days'};
my $retention_recover = $policy->{'retention_recover'};
my $retention_full = $policy->{'retention_full'};
foreach my $i ( 0 .. ( $run_tapes - 1 ) ) {
if ( my $tape_label =
Amanda::Tapelist::get_last_reusable_tape_label(
$labelstr->{'template'},
$tapepool,
$storage_n,
$retention_tapes,
$retention_days,
$retention_recover,
$retention_full,
$i) ) {
push @tape_labels, $tape_label;
} else {
$nb_new_tape++;
}
}
if (@tape_labels) {
$self->{'sections'}->{'tapeinfo'}->{'storage'}->{$storage_n}->{'next'} = \@tape_labels;
}
if ($nb_new_tape) {
$self->{'sections'}->{'tapeinfo'}->{'storage'}->{$storage_n}->{'new'} = $nb_new_tape;
}
my @new_tapes = Amanda::Tapelist::list_new_tapes(
$storage_n,
$run_tapes);
if (@new_tapes) {
$self->{'sections'}->{'tapeinfo'}->{'storage'}->{$storage_n}->{'new_labelled'} = \@new_tapes;
}
}
}
return;
}
sub output_stats
{
my ($self) = @_;
my $fh = $self->{fh};
my $report = $self->{report};
$self->zsprint("STATISTICS:");
# TODO: the hashes are a cheap fix. fix these.
my $full_stats = $self->{full_stats};
my $incr_stats = $self->{incr_stats};
my $total_stats = $self->{total_stats};
my ( $ttyp, $tt, $tapesize, $marksize );
$ttyp = getconf($CNF_TAPETYPE);
$tt = lookup_tapetype($ttyp) if $ttyp;
if ( $ttyp && $tt ) {
$tapesize = "".tapetype_getconf( $tt, $TAPETYPE_LENGTH );
$marksize = "".tapetype_getconf( $tt, $TAPETYPE_FILEMARK );
}
# these values should never be zero; assign defaults
$tapesize = 100 * 1024 * 1024 if !$tapesize;
$marksize = 1 * 1024 * 1024 if !$marksize;
$self->{'sections'}{'statistic'}{'estimate_time'} = $total_stats->{planner_time};
$self->{'sections'}{'statistic'}{'run_time'} = $total_stats->{total_time};
$self->{'sections'}{'statistic'}{'dump_time'} = {
total => $total_stats->{dumper_time},
full => $full_stats->{dumper_time},
incr => $incr_stats->{dumper_time}
};
$self->{'sections'}{'statistic'}{'output_size'} = {
total => $total_stats->{outsize},
full => $full_stats->{outsize},
incr => $incr_stats->{outsize}
};
$self->{'sections'}{'statistic'}{'original_size'} = {
total => $total_stats->{origsize},
full => $full_stats->{origsize},
incr => $incr_stats->{origsize}
};
my $comp_size = sub {
my ($stats) = @_;
return $self->divzero(100 * $stats->{outsize}, $stats->{origsize});
};
$self->{'sections'}{'statistic'}{'avg_compression'} = {
total => $comp_size->($total_stats),
full => $comp_size->($full_stats),
incr => $comp_size->($incr_stats)
};
$self->{'sections'}{'statistic'}{'dles_dumped'} = {
total => $total_stats->{dumpdisk_count},
full => $full_stats->{dumpdisk_count},
incr => $incr_stats->{dumpdisk_count},
};
$self->{'sections'}{'statistic'}{'avg_dump_rate'} = {
total => $self->divzero_wide( $total_stats->{outsize}, $total_stats->{dumper_time} ),
full => $self->divzero_wide( $full_stats->{outsize}, $full_stats->{dumper_time} ),
incr => $self->divzero_wide( $incr_stats->{outsize}, $incr_stats->{dumper_time} )
};
$self->{'sections'}{'statistic'}{'dumpdisks'} =
Amanda::Report::human::has_incrementals($self->{dumpdisks}) ? Amanda::Report::human::by_level_count($self->{dumpdisks}) : "";
$self->{'sections'}{'statistic'}{'tape_time'} = {
total => $total_stats->{taper_time},
full => $full_stats->{taper_time},
incr => $incr_stats->{taper_time}
};
$self->{'sections'}{'statistic'}{'tape_size'} = {
total => $total_stats->{tapesize},
full => $full_stats->{tapesize},
incr => $incr_stats->{tapesize}
};
my $tape_usage = sub {
my ($stat_ref) = @_;
return $self->divzero(
100 * (
$marksize *
($stat_ref->{tapedisk_count} + $stat_ref->{tapepart_count}) +
$stat_ref->{tapesize}
),
$tapesize
);
};
$self->{'sections'}{'statistic'}{'tape_used'} = {
total => $tape_usage->($total_stats),
full => $tape_usage->($full_stats),
incr => $tape_usage->($incr_stats)
};
my $nb_incr_dle = 0;
my @incr_dle = @{$self->{tapedisks}};
foreach my $level (1 .. $#incr_dle) {
$nb_incr_dle += $incr_dle[$level];
}
$self->{'sections'}{'statistic'}{'dles_taped'} = {
total => $self->{tapedisks}[0] + $nb_incr_dle,
full => $self->{tapedisks}[0],
incr => $nb_incr_dle
};
$self->{'sections'}{'statistic'}{'tapedisks'} =
Amanda::Report::human::has_incrementals($self->{tapedisks}) ? Amanda::Report::human::by_level_count($self->{tapedisks}) : "";
$self->{'sections'}{'statistic'}{'parts_taped'} = {
total => $total_stats->{tapepart_count},
full => $full_stats->{tapepart_count},
incr => $incr_stats->{tapepart_count}
};
$self->{'sections'}{'statistic'}{'tapeparts'} =
$self->{tapeparts}[1] > 0 ? Amanda::Report::human::by_level_count($self->{tapeparts}) : "";
$self->{'sections'}{'statistic'}{'Avg_tape_write_speed'} = {
total => $self->divzero_wide( $total_stats->{tapesize}, $total_stats->{taper_time} ),
full => $self->divzero_wide( $full_stats->{tapesize}, $full_stats->{taper_time} ),
incr => $self->divzero_wide( $incr_stats->{tapesize}, $incr_stats->{taper_time} )
};
return;
}
sub output_tape_stats
{
my ($self) = @_;
my $fh = $self->{fh};
my $report = $self->{report};
my $taper = $report->get_program_info("taper");
my $tapes = $taper->{tapes} || {};
my $tape_labels = $taper->{tape_labels} || [];
# if no tapes used, do nothing
return if (!@$tape_labels);
my $label_length = 19;
foreach my $label (@$tape_labels) {
$label_length = length($label) if length($label) > $label_length;
}
my $ts_format = " @"
. '<' x ($label_length - 1)
. "@>>>> @>>>>>>>>>>> @>>>>> @>>>> @>>>>\n";
$self->zsprint("USAGE BY TAPE:\n");
my $tapetype_name = getconf($CNF_TAPETYPE);
my $tapetype = lookup_tapetype($tapetype_name);
my $tapesize = "" . tapetype_getconf($tapetype, $TAPETYPE_LENGTH);
my $marksize = "" . tapetype_getconf($tapetype, $TAPETYPE_FILEMARK);
foreach my $label (@$tape_labels) {
my $tape = $tapes->{$label};
my $tapeused = $tape->{'kb'};
$tapeused += $marksize * (1 + $tape->{'files'});
push @{$self->{'sections'}{$self->{'section'}}}, {
'configuration_id' => $opt_zmc_cid,
'dump_timestamp' => $self->{'_current_tape'}->{'date'},
'nb' => int($tape->{dle}),
'nc' => int($tape->{files}),
'percent_use' => $self->divzero(100 * $tapeused, $tapesize),
'size' => $tape->{kb},
'tape_label' => $label,
'time_duration' => $tape->{time},
};
}
return;
}
sub output_details
{
## takes no arguments
my ($self) = @_;
my $fh = $self->{fh};
my $errors = $self->{errors};
my $notes = $self->{notes};
my $report = $self->{report};
my $stranges = $report->{stranges};
my $disp_unit = $self->{disp_unit};
my @failed_dump_details;
my @strange_dump_details;
my @dles = $report->get_dles();
foreach my $dle_entry (@dles) {
my ($hostname, $disk) = @$dle_entry;
my $dle = $report->get_dle_info(@$dle_entry);
my $alldumps = $dle->{'dumps'} || {};
my $qdisk = quote_string($disk);
my $outsize = undef;
while( my ($timestamp, $tries) = each %$alldumps ) {
foreach my $try (@$tries) {
#
# check for failed dumper details
#
if (defined $try->{dumper}
&& $try->{dumper}->{status} eq 'fail') {
push @failed_dump_details,
"/-- $hostname $qdisk lev $try->{dumper}->{level} FAILED [$try->{dumper}->{error}]",
@{ $try->{dumper}->{errors} },
"\\--------";
if ($try->{dumper}->{nb_errors} > 100) {
my $nb = $try->{dumper}->{nb_errors} - 100;
push @failed_dump_details,
"$nb lines follow, see the corresponding log.* file for the complete list",
"\\--------";
}
}
#
# check for strange dumper details
#
if (defined $try->{dumper}
&& $try->{dumper}->{status} eq 'strange') {
push @strange_dump_details,
"/-- $hostname $qdisk lev $try->{dumper}->{level} STRANGE",
@{ $try->{dumper}->{stranges} },
"\\--------";
if ($try->{dumper}->{nb_stranges} > 100) {
my $nb = $try->{dumper}->{nb_stranges} - 100;
push @strange_dump_details,
"$nb lines follow, see the corresponding log.* file for the complete list",
"\\--------";
}
}
# note: copied & modified from calculate_stats.
if (
exists $try->{dumper}
&& exists $try->{chunker}
&& defined $try->{chunker}->{kb}
&& ( $try->{chunker}{status} eq 'success'
|| $try->{chunker}{status} eq 'partial')
) {
$outsize = $try->{chunker}->{kb};
} elsif (
exists $try->{dumper}
&& exists $try->{taper}
&& defined $try->{taper}->{kb}
&& ( $try->{taper}{status} eq 'done'
|| $try->{taper}{status} eq 'partial')
) {
$outsize = $try->{taper}->{kb};
}
}
}
#
# check for bad estimates
#
if (exists $dle->{estimate} && defined $outsize) {
my $est = $dle->{estimate};
push @$notes,
"big estimate: $hostname $qdisk $dle->{estimate}{level}",
sprintf(' est: %.0f%s out %.0f%s',
$self->tounits($est->{ckb}), $disp_unit,
$self->tounits($outsize), $disp_unit)
if (defined $est->{'ckb'} && ($est->{ckb} * .9 > $outsize)
&& ($est->{ckb} - $outsize > 1.0e5));
}
}
$self->print_if_def(\@failed_dump_details, "FAILED DUMP DETAILS:");
$self->print_if_def(\@strange_dump_details, "STRANGE DUMP DETAILS:");
$self->print_if_def($notes, "NOTES:");
return;
}
sub output_summary
{
## takes no arguments
my ($self) = @_;
my $fh = $self->{fh};
my $report = $self->{report};
## get the dles
my @dles =
sort { ( $a->[0] cmp $b->[0] ) || ( $a->[1] cmp $b->[1] ) }
$report->get_dles();
## set the col_spec, which is the configuration for the summary
## output.
my $col_spec = $self->set_col_spec();
## collect all the output line specs (see get_summary_info)
my @summary_linespecs = ();
$self->{'sections'}{'summary'} = ();
foreach my $dle (@dles) {
my @records = $self->get_summary_info($dle, $self->{report}, $col_spec);
foreach my $record (@records) {
push @{$self->{'sections'}{'summary'}}, {
'dump_timestamp' => $$record[13],
'configuration_id' => $opt_zmc_cid,
'hostname' => $$record[1],
'disk_name' => $$record[2],
'dle_status' => $$record[0],
'backup_level' => $$record[3],
'dump_orig_kb' => $$record[4],
'dump_out_kb' => $$record[5],
'dump_comp' => $$record[6],
'dump_duration' => $$record[7],
'dump_rate' => $$record[8],
'tape_duration' => $$record[9],
'tape_rate' => $$record[10],
'dump_partial' => $$record[11],
'last_tape_label' => $$record[12]
}
}
}
return;
}
1;