Amanda-Users

Re: Printing tape labels

2004-02-24 17:37:44
Subject: Re: Printing tape labels
From: Josef Wolf <jw AT raven.inka DOT de>
To: amanda-users AT amanda DOT org
Date: Tue, 24 Feb 2004 23:31:43 +0100
On Fri, Feb 13, 2004 at 01:22:16AM +0100, Josef Wolf wrote:

> I have written a script to print amanda tape labels. I think this script
> might be interesting to other amanda users, so I offer to include it in
> the amanda distribution.

OK, here it comes:

-------------------------snip-----------------------
#! /usr/bin/perl -w

# A utility to print amanda tape labels for DAT and CD.
#
# 2004-02-12 Josef Wolf  (jw AT raven.inka DOT de)
#
# Portions of this program which I authored may be used for any purpose
# so long as this notice is left intact.
#
# 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.

# Comments, suggestions, bugfixes and improvements are welcome.

# I wrote this program because I was dissatisfied with the original label
# printing program that comes with the amanda distribution. I wanted to see
# from one glance on the newest tape which tapes in which order I need to
# recover a specific DLE.
#
# This program prints tapelabels for the amanda backup system. The output
# can be in plain ASCII or in postscript. The postscript output is formatted
# so that it can be folded to fit into a DAT case or into a CD jewel case.
#
# An example ASCII output (somewhat stripped to make it fit into 80 columns)
# is attached below. Here is an explanation of the example output:
#
# The columns in the output have following meanings:
#
# date:  This name seems to be intuitive, but unfortunately, it is somewhat
#        misleading. Actually, this is the name of the logfile that provided
#        the corresponding information.
# label: The label of the tape.
# fn:    File number on the tape.
# fm:    Filemark
# Osize: Original (that is, uncompressed) size of the dump(s).
# Dsize: Size of the dump(s). This is usually pretty close to Tsize so it
#        is of very little interest.
# Tsize: The size of dump(s) on the tape.
# Dtime: Dumper time.
# Ttime: Taper Time.
# Dspd:  Dumper speed.
# Tspd:  Taper speed.
# DLE:   Disk list entry.
# lv:    Dump-level.
# dpl:   "Dumps per level". This is a list of dump levels (starting with
#        level 0).
# error: An error message.
#
# The output is split into four sections. The first section (if present)
# lists errors. In the example below we can see that there were two taper
# errors and a warning that a DLE must be flushed to tape.
#
# The second section contains only one line with date, label and amount of
# data on the tape.
#
# The third section lists all the tapes that are needed to recover all
# DLEs. In the example below we can see that three level0, four level1
# and one level3 from tape VOL01 are needed to do a full restore of all
# DLEs. In addition, three level0, one level1 and one level2 from VOL09
# are still needed. Everything else on VOL09 is obsoleted by the dumps
# on VOL01. VOL08 contains two level0 and one leve1 that are not obsoleted
# by newer tapes. Finally, VOL07 contain one level0 dump that is not
# obsolted by newer tapes.
# Tapes that contain only obsoleted data are not mentioned at all unless you
# supply the -t command line option.
#
# The fourth section is the main section and is itself split into multiple
# sections, one for each DLE. In this section we can see which tapes we
# need to recover a specific DLE. For example, we can see that in order
# to recover raven:/u4 we need file 1 from VOL07, file 6 from VOL08,
# file 7 from VOL09 and file 1 from VOL01, in this order.
#
# Here is the example output:
#
#         date Tsize  label fm lv error
# 2004-02-20.0 1499M  VOL06  8  ? writing file: No space left on device
# 2004-02-23.0 1499M  VOL01  9  ? writing file: No space left on device
# 2004-02-23.0  670M    ???  ?  0 raven:/m/u1 not on tape yet
# 
# 2004-02-23.0   VOL01   Total size: 834M
# 
#         date  label Osize Tsize    Ttime dpl
# 2004-02-23.0  VOL01 1616M  834M  0:01:06 3/4/0/1
# 2004-02-22.0  VOL09 2885M 1164M  0:01:12 3/1/1
# 2004-02-21.0  VOL08 3427M 1127M  0:01:12 2/1
# 2004-02-20.1  VOL07 2077M 1376M  0:01:24 1
# 
#  Dspd  Tspd   Dtime Osize Tsize   Ttime date          label:fn lv DLE
#  461k  105M 0:00:06   15M 2963k 0:00:00 2004-02-23.0  VOL01:04  1 raven:/
#  803k   11M 0:09:13 1163M  434M 0:00:37 2004-02-21.0  VOL08:08  0 raven:/
# 
#    0k 1647k 0:00:00   10k    1k 0:00:00 2004-02-23.0  VOL01:03  1 raven:/boot
# 2515k   40M 0:00:01 4520k 3487k 0:00:00 2004-02-22.0  VOL09:03  0 raven:/boot
# 
#  190k   25M 0:00:00  290k   22k 0:00:00 2004-02-22.0  VOL09:02  1 raven:/u1
#  971k   19M 0:11:46 1757M  670M 0:00:34 2004-02-21.0  VOL08:09  0 raven:/u1
# 
#    0k 1237k 0:00:00   10k    1k 0:00:00 2004-02-23.0  VOL01:02  0 raven:/u2
# 
#  925k   13M 0:00:40   94M   36M 0:00:02 2004-02-23.0  VOL01:06  1 raven:/u3
# 2913k   13M 0:03:56  884M  672M 0:00:49 2004-02-22.0  VOL09:08  0 raven:/u3
# 
#  222k   54M 0:00:00  830k  113k 0:00:00 2004-02-23.0  VOL01:01  3 raven:/u4
#  373k   47M 0:00:50  459M   18M 0:00:00 2004-02-22.0  VOL09:07  2 raven:/u4
#  314k   42M 0:01:14  505M   22M 0:00:00 2004-02-21.0  VOL08:06  1 raven:/u4
# 3254k   16M 0:07:13 2077M 1376M 0:01:24 2004-02-20.1  VOL07:01  0 raven:/u4
# 
#  270k  113M 0:00:14   39M 3885k 0:00:00 2004-02-23.0  VOL01:05  1 raven:/u5
#  593k   21M 0:13:29 1536M  469M 0:00:22 2004-02-22.0  VOL09:09  0 raven:/u5
# 
# 1003k   13M 0:06:04  863M  356M 0:00:26 2004-02-23.0  VOL01:08  0 raven:/usr
# 
# 1960k   11M 0:03:46  602M  434M 0:00:36 2004-02-23.0  VOL01:07  0 raven:/var

# bugs:
# - Parses amanda's log files instead of reading its database.
# - Depends on assumption that only one amdump per day is run.
# - Nuber of output lines limited to paper size.
# - Output formats hardcoded.
# - Postscript output is not split onto multiple sheets.

# todo:
# - remove some kludges in the code.

use strict;
use Data::Dumper;
use PostScript::Simple;
use Getopt::Std; $Getopt::Std::STANDARD_HELP_VERSION=1;

my $version=0.1;

sub VERSION_MESSAGE {
    my ($fh) = @_;
    print $fh "$0 Version $version\n";
}
sub help_message {
    return
        "Usage: $0 [-t] [-p <file> [-f <format>]] <conf>\n" .
        " -f <format>: Format postscript for DAT/DDS/CD/DVD cover.\n" .
        " -p <file>:   Output postscript into <file>.\n" .
        " -i <cnt>:    Ignore <cnt> newest logfiles. Used to print older\n" .
        "              tapelabels.\n";
        " -t:          Output all dumps available on tapes instead of only\n" .
        "              the latest for every dumplevel.\n";
}
sub HELP_MESSAGE {
    my ($fh) = @_;
    print $fh &help_message;
}
sub usage {
    print STDERR &help_message;
    exit 1;
}

my %opt; getopts("tp:f:i:", \%opt);
my $postscript = $opt{p};
my $format     = $opt{f};
my $ignore     = $opt{i};
my $verbose    = exists $opt{t};
my $config     = shift;

&usage if !defined $config || (defined $format && !defined $postscript);
$format="DAT" unless defined $format;
$format="DAT" if $format eq "DDS";

my $LOGDIR = `amgetconf $config logdir`;  chomp $LOGDIR;
$LOGDIR= "/m/c/misc" unless -d $LOGDIR;  # FIXME: to be removed

my @logfiles; # sorted names of logfiles from current dumpcycle
my $curlabel; # the label of the latest (i.e. current) tape
my %dumper;   # info about dumped disks
my %taper;    # info about tapings of disks
my %tape;     # info about tape contents
my %dumped;   # log-filename with the latest dump of a disk at a level
my %taped;    # log-filename with the latest taping of a disk at a level
my %needed;   # still needed taping levels from given logfile

my $xoff=3;         # offset of x-coordinate in postscript
my $yoff=29;        # offset of y-coordinate in postscript
my $width=7.3;      # width of boxes in postscript output.
my $tapedest="top"; # Destination of the tape summaries

# Default formatting for DAT
#
my %out=(
         top=>{
             ys=> 0.13, # distance of lines
             x => 0.25, # x offset
             y =>-0.35, # y offset
             fs=>4,     # font size
             la=>5.5,   # length of area
             st=>0.1,   # shrink stepping
             V =>[],    # array of output sections
         },
         bot=>{ys=> 0.13,x=>-2.25,y=>-1.5,fs=>4,la=>  5.5,st=>0.1,V=>[]},
         err=>{ys=> 0.2 ,x=> 8,   y=>-0.5,fs=>6,la=>120,  st=>0.1,V=>[]},
         );

# change formatting for CD
#
if ($format eq "CD" || $format eq "DVD") {
    $width = 12;
    $tapedest = "bot";
    $out{"top"}{la}=12;    $out{"bot"}{la}=11;
    $out{"top"}{fs}=5;     $out{"bot"}{fs}=5;
    $out{"top"}{ys}=0.15;  $out{"bot"}{ys}=0.15;
    $out{"err"}{x}=0.25;   $out{"bot"}{x}=0.25;
}

# Insert separation into an output field.
#
sub sep {
    my ($which) = @_;
    my $w = $out{$which};
    my $y = $w->{ys}*4/4 + $w->{y};
    $w->{y} = $y;
    push (@{$w->{V}}, $w->{v});
    $w->{v} = [];
}

# Output a line.
#
sub out {
    my ($which, $val) = @_;
    my $w = $out{$which};
    my $y = $w->{ys} + $w->{y};
    if (!exists $w->{v}) {
        $w->{v} = [];
        if ($which eq "err") {
                &sep ("err");
                &out ("err", sprintf("%12s %5s %6s %2s %2s %s",
                                     qw/date Tsize label fm lv error/));
                &out;
                return;
        }
    }
    my $v=$w->{v};
    $w->{y} = $y;
    push (@$v, {x=>$w->{x}, y=>$w->{y}, v=>$val, la=>$w->{la}});
}

# Determine which logfiles belong to the current dump cycle. This is done by
# searching backwards from newest to oldest logfiles for already seen
# tape-labels.
#
{
    my %labels; # which tape labels we already have seen

    FILE: foreach my $logfile (reverse sort <$LOGDIR/log.*>) {
        next if defined $ignore && $ignore-- > 0;
        open (IN, $logfile) or next;
        while (my $l=<IN>) {
            if ($l=~/^START taper\s+.*label\s+(\S+)\s+/) {
                last FILE if exists $labels{$1};
                $labels{$1}=1;
                unshift (@logfiles, $logfile);
            }
        }
        close (IN);
    }
}

# Parse the logfiles. This constructs %dumper, %taper, %dumped, %taped, %tape
# and $curlabel. In addition, any errors from taper are remembered.
#
foreach my $logfile (@logfiles) {
    open (IN, $logfile) or next;
    my $label;
    my $nr=0;
    $logfile =~ s/^.*(\d\d\d\d)(\d\d)(\d\d)\.(\d+)$/$1-$2-$3.$4/;
    while (my $line=<IN>) {
        chomp $line;
        if ($line=~/^STRANGE dumper (.*)/) {
            my ($host, $filesystem, $level, $rest) = split (/\s+/, $1, 4);
            my $disk="$host:$filesystem";
            $dumped{$disk,$level} = $logfile;
            $dumper{$disk}{$level} = {%{&hash($rest)}};
        }
        if ($line=~/^SUCCESS dumper (.*)/) {
            my ($host, $filesystem, $d, $level, $rest) = split (/\s+/, $1, 5);
            my $disk="$host:$filesystem";
            $dumped{$disk,$level} = $logfile;
            $dumper{$disk}{$level} = {%{&hash($rest)}};
        }
        if ($line=~/^START taper\s+(.*)/) {
            my $hash = &hash ($1, (files=>{}));
            $label = $hash->{"label"};
            $curlabel = $label;
            $tape{$label}{"kb"} = 0;
#           $tape{$label}{"date"} = $hash->{"datestamp"};
#           $tape{$label}{"date"} =~ s/^(....)(..)(..)/$1-$2-$3/;
            $tape{$label}{"date"} = $logfile;
        }
        if ($line=~/^SUCCESS taper (.*)/) {
            my ($host, $filesystem, $d, $level, $rest) = split (/\s+/, $1, 5);
            my $disk="$host:$filesystem";
            $taper{$disk}{$logfile} = {
                %{&hash($rest, label=>$label, level=>$level, nr=>++$nr,
                        dump=>$dumper{$disk}{$level})}};
            $taped{$disk,$level} = $logfile;
            $tape{$label}{"kb"} += $taper{$disk}{$logfile}{"kb"};
            $tape{$label}{"count"}{$level}++;
        }
        if ($line=~/^INFO taper (.*)/) {
            my ($d1, $t, $d2, $kb, $d3, $fm, $rest) = split (/\s+/, $1, 7);
            if ($rest ne "[OK]") {
                &out("err",sprintf("%12s %5s %6s %2s %2s %s",
                                   $tape{$t}{"date"}, &kb($kb),
                                   $t, $fm, "?", $rest));
            }
        }
    }
    close (IN);
}

&out("bot", sprintf("%5s %5s %8s %5s    %5s %5s %8s %-12s %6s:%2s %2s %s",
     qw/Dspd Tspd Dtime Dsize Osize Tsize Ttime date label fn lv DLE/));

# Determine dumps/tapes that are needed in addition to the newest tape in order
# to make a full restore.
#
foreach my $disk (sort keys %taper) {
    my $lastlevel=10000;
    foreach my $logfile (reverse sort keys %{$taper{$disk}}) {
        my $taping=$taper{$disk}{$logfile};
        next if !$verbose && $taping->{"level"}>=$lastlevel;
        $lastlevel = $taping->{"level"};
        &out("bot",
             sprintf ("%5s %5s %8s %5s    %5s %5s %8s %-12s %6s:%02d %2s %s",
                      &kb ($taping->{"dump"}{"kps"}),
                      &kb ($taping->{"kps"}),
                      &sec($taping->{"dump"}{"sec"}),
                      &kb ($taping->{"dump"}{"kb"}),
                      &kb ($taping->{"dump"}{"orig-kb"}),
                      &kb ($taping->{"kb"}),
                      &sec($taping->{"sec"}), $logfile,
                      $taping->{"label"},
                      $taping->{nr},
                      $taping->{"level"}, $disk
                      ));
        $needed{$logfile}{"label"} = $taping->{"label"};
        $needed{$logfile}{"sec"}  += $taping->{"sec"};
        $needed{$logfile}{"kb"}   += $taping->{"kb"};
        $needed{$logfile}{"okb"}  += $taping->{"dump"}{"orig-kb"};
        $needed{$logfile}{"level"}[$lastlevel]++;
    }
    &sep("bot");
}

# Output the number of still needed tapings for every tape.
#
{
    my $headline = sprintf ("%12s %6s %5s %5s %8s %s",
                            qw/date label Osize Tsize Ttime dpl/);
    my $dir = !defined $postscript || $tapedest eq "bot";
    &out($tapedest, $headline) if $dir==1;
    foreach my $logfile (reverse sort keys %needed) {
        my $t=$needed{$logfile};
        foreach my $l (@{$t->{"level"}}) {
            $l=0 unless defined $l;
        }
        &out($tapedest, sprintf ("%12s %6s %5s %5s %8s %s",
                                 $logfile, $t->{"label"}, &kb($t->{"okb"}),
                                 &kb($t->{"kb"}), &sec($t->{"sec"}),
                                 join("/", @{$t->{"level"}})));
    }
    &out($tapedest, $headline) if $dir!=1;
    &sep ($tapedest);
}

# Output the dumps that are not written to a tape yet.
{
    my $printed=0;
    foreach my $d (sort keys %dumped) {
        if (!defined $taped{$d} || $taped{$d} lt $dumped{$d}) {
            my ($disk, $level) = split (/$;/, $d);
            if (!$printed) {
                $printed=1;
            }
            &out ("err", sprintf("%12s %5s %6s %2s %2s %s",
                                 $dumped{$d},
                                 &kb($dumper{$disk}{$level}{kb}),
                                 "???", "?",
                                 $level, "$disk not on tape yet"));
        }
    }
}

# Start output.
#
my $out;
if (defined $postscript) {
    $out = new PostScript::Simple(papersize => "A4", units => "cm");
    $out->setcolour("black");
    $out->setlinewidth(0.01);
}

# Dispatch the output blocks into appropriate output fields.
#
my ($htop, @topbox) = (0);
if ($tapedest eq "top") {
    ($htop, @topbox) = &shrinking_boxes ($out{top}, @{$out{top}{V}});
}
my ($hbot, @botbox) = &shrinking_boxes ($out{bot}, @{$out{bot}{V}});

# output errors and warnings
#
my $w=$out{"err"};
foreach my $o (@{$w->{v}}) {
    if (defined $postscript) {
        $out->setfont ("Courier-Bold", $w->{fs});
        $out->text ($xoff+$o->{x}, $yoff-$o->{y}, $o->{v});
    } else {
        print "$o->{v}\n";
    }
}
if ($#{$w->{v}} >= 0) {
    if (defined $postscript) {
        if ($w->{x} < 8) {
            $yoff -= ($w->{ys} * ($#{$w->{v}}));
        }
    } else {
        print "\n";
    }
}

# Print date, label and total size
#
if (defined $postscript) {
    $out->box ($xoff, $yoff-$htop, $xoff+$width, $yoff-$htop-1.2);
    $out->setfont ("Helvetica-Bold", 20);
    $out->text($xoff+2.7,$yoff-$htop-0.9, $curlabel);
    $out->setfont ("Helvetica-Bold", 11);
    $out->text($xoff+0.25,$yoff-$htop-0.9, $tape{$curlabel}{"date"});
    $out->setfont ("Courier-Bold", 6);
    $out->text($xoff+0.25,$yoff-$htop-0.52,
               "Total size: " . &kb($tape{$curlabel}{"kb"}));
} else {
    print "$tape{$curlabel}{'date'}   $curlabel   Total size: ",
    &kb($tape{$curlabel}{"kb"}), "\n\n";
}

# Print resulting output boxes.
#
if ($tapedest eq "top") {
    &draw_boxes ($xoff, $yoff,           $xoff+$out{top}->{x}, 1,
                 $out{top}->{fs}, @topbox);
}
&draw_boxes ($xoff, $yoff-$htop-1.2, $xoff+$out{bot}->{x}, -1,
             $out{bot}->{fs}, @botbox);

$out->output($postscript) if defined $postscript;

# Dispatch output fields into a set of shrinking boxes that can be folded.
#
sub shrinking_boxes {
    my ($conf, @contents) = @_;
    my ($h, @box) = (0);
    for (my $bh=$conf->{la}; $#contents>=0; $bh-=$conf->{st}) {
        my @l;
        while ($#contents>=0 && ($#l+1+$#{$contents[0]}+2)*0.13<$bh) {
            push(@l, @{shift @contents}, {v=>""});
        }
        if ($#l<0) {
            push(@l, splice(@{$contents[0]},0,int($bh/0.13)-2));
        }
        push(@box, {h=>$bh,w=>$width,ys=>0.13,lines=>[@l]});
        $h += $bh;
    }

    return ($h, @box);
}

sub draw_boxes {
    my ($xoff, $yoff, $toff, $dir, $font, @box) = @_;
    my $y;
    if (defined $postscript) {
        $out->setfont ("Courier-Bold", $font);
    }
    foreach my $b (@box) {
        if (defined $postscript) {
            $out->box($xoff, $yoff, $xoff+$b->{w}, $yoff-$b->{h});
        }
        $yoff-=$b->{h};
        foreach my $i (0..$#{$b->{lines}}) {
            if ($dir>0) {
                $y=$yoff+0.05+$i*$b->{ys};
            } else {
                $y=$yoff-0.26-$i*$b->{ys}+$b->{h};
            }
            if (defined $postscript) {
                $out->text ($toff, $y, $b->{lines}[$i]{v});
            } else {
                print "$b->{lines}[$i]{v}\n";
            }
        }
    }
}

# construct an associative array from key/value pairs which amanda puts into
# sqare brackets. 
#
sub hash {
    my ($input)=shift;
    $input =~ s/[\[\]]//g;
    $input =~ s/\{.*\}//;
    $input =~ s/\s+$//;
    my %h = (@_, split (/\s+/, $input));
    return \%h;
}

# translate seconds into h:mm:ss
#
sub sec {
    my ($v)=@_;
    my $h = int ($v / 3600);
    my $m = int ($v / 60) % 60;
    my $s = $v % 60;
    return sprintf "%2d:%02d:%02d", $h, $m, $s;
}

# translate kbytes into a unit with 2..4 digits
#
sub kb {
    my ($v)=@_;
    my $app = "k";
    if ($v>9999) { $app="M"; $v /= 1024; }
    if ($v>9999) { $app="G"; $v /= 1024; }
    if ($v>9999) { $app="T"; $v /= 1024; }
    return int ($v) . $app;
}
-------------------------snip-----------------------

-- 
-- Josef Wolf -- jw AT raven.inka DOT de --

<Prev in Thread] Current Thread [Next in Thread>