On Tue, Feb 24, 2004 at 11:31:43PM +0100, Josef Wolf wrote:
Here comes a repost of the "amandatape" label printing script. First,
a short summary of the changes since the last post:
Bug fixes:
* Fix dumpcycle search when more than one tape is written in a single run.
* Fix dumper date for runs without taping.
* Fix printing when several levels of one DLE are on a single tape.
* Fix handling of runs without tape when -l option is used.
* Fix bug not outputting dumps that are not taped yet.
* Don't choke on "INFO taper retrying" line.
* Don't choke when a dump is no longer in a logfile.
* Adjust geometry of PS output.
* Error out properly when no logfiles could be found or can't be read.
Improvements:
* Search logfiles in oldlog directory in addition to standard logfiles.
* Print warning on missing backup levels.
* Create real postscript instead of EPS.
* Add -l option to print label for specific tape.
* Add -d option to ignore logfiles not older than a specific date.
* Add -p option to specify paper type.
* Add -o option to specify output file.
* Options parsing made consistently
* Fall back to use <conf> as a directory where logfiles are located if
amgetconf fails to get configuration for <conf>.
Many thanks to Jon LaBadie who greatly helped to shake out bugs and
gave valuable feedback for improvements.
---- 8< ---- 8< ---- 8< ---- 8< ---- 8< ---- 8< ---- 8< ----
#! /usr/bin/perl -w
# amandatape -- 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.
# 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 dumpdate label:fn lv DLE
# 461k 105M 0:00:06 15M 2963k 0:00:00 2004-02-23 VOL01:004 1 raven:/
# 803k 11M 0:09:13 1163M 434M 0:00:37 2004-02-21 VOL08:008 0 raven:/
#
# 0k 1647k 0:00:00 10k 1k 0:00:00 2004-02-23 VOL01:003 1 raven:/boot
# 2515k 40M 0:00:01 4520k 3487k 0:00:00 2004-02-22 VOL09:003 0 raven:/boot
#
# 190k 25M 0:00:00 290k 22k 0:00:00 2004-02-22 VOL09:002 1 raven:/u1
# 971k 19M 0:11:46 1757M 670M 0:00:34 2004-02-21 VOL08:009 0 raven:/u1
#
# 0k 1237k 0:00:00 10k 1k 0:00:00 2004-02-23 VOL01:002 0 raven:/u2
#
# 925k 13M 0:00:40 94M 36M 0:00:02 2004-02-23 VOL01:006 1 raven:/u3
# 2913k 13M 0:03:56 884M 672M 0:00:49 2004-02-22 VOL09:008 0 raven:/u3
#
# 222k 54M 0:00:00 830k 113k 0:00:00 2004-02-23 VOL01:001 3 raven:/u4
# 373k 47M 0:00:50 459M 18M 0:00:00 2004-02-22 VOL09:007 2 raven:/u4
# 314k 42M 0:01:14 505M 22M 0:00:00 2004-02-21 VOL08:006 1 raven:/u4
# 3254k 16M 0:07:13 2077M 1376M 0:01:24 2004-02-20 VOL07:001 0 raven:/u4
#
# 270k 113M 0:00:14 39M 3885k 0:00:00 2004-02-23 VOL01:005 1 raven:/u5
# 593k 21M 0:13:29 1536M 469M 0:00:22 2004-02-22 VOL09:009 0 raven:/u5
#
# 1003k 13M 0:06:04 863M 356M 0:00:26 2004-02-23 VOL01:008 0 raven:/usr
#
# 1960k 11M 0:03:46 602M 434M 0:00:36 2004-02-23 VOL01:007 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.2;
sub VERSION_MESSAGE {
my ($fh) = @_;
print $fh "$0 Version $version ", '($Rev: 199 $)', "\n";
}
sub help_message {
return
"Usage: $0 [options] <conf>\n" .
" -f <format>: Choose output format. Existing formats are:\n" .
" Text, DAT, DDS, CD, DVD.\n" .
" The formats DDS and DVD are synonyms for DAT/CD.\n" .
" All formats except 'Text' are in postscript.\n" .
" -p <paper>: Select A4 or Letter as paper type. A4 is default.\n" .
" -o <file>: Write output into <file> instead of stdout.\n" .
" -d <date>: Take only logfiles older than <date> into account.\n" .
" <date> must be given in the format yyyymmdd.\n" .
" -l <label>: Output label for tape named <label>. Used to print\n" .
" older tapelabels.\n" .
" -i <cnt>: Ignore <cnt> newest logfiles. Used for debugging.\n" .
" -t: Output all dumps available on tapes instead of only\n" .
" the latest for every dumplevel.\n" .
" <conf>: The amanda configuration. If configuration <conf>\n" .
" does not exist, <conf> is interpreted as a directory\n".
" where the logfiles are expected to be found.\n";
}
sub HELP_MESSAGE {
my ($fh) = @_;
print $fh &help_message;
}
sub usage {
print STDERR &help_message;
exit 1;
}
my %opt; exit 1 unless getopts("tp:o:f:i:l:d:", \%opt);
my $paper = $opt{p};
my $output = $opt{o};
my $format = $opt{f};
my $ignore = $opt{i};
my $lignore = $opt{l};
my $dignore = $opt{d};
my $verbose = exists $opt{t};
my $config = shift;
&usage unless defined $config;
$output="-" unless defined $output;
$paper = "a4" unless defined $paper;
$format="text" unless defined $format;
$paper = lc $paper;
$format= lc $format;
$format="dat" if $format eq "dds";
my %papertype=(a4=>{yoff=>29}, letter=>{yoff=>27});
die "$0: unknown paper type $paper" unless exists $papertype{$paper};
if ($output ne "-" && $format eq "text") {
open (STDOUT, ">$output") or die "$output: $!";
}
my $LOGDIR = `amgetconf $config logdir`;
chomp $LOGDIR if defined $LOGDIR;
$LOGDIR= $config unless defined $LOGDIR && -d $LOGDIR;
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=$papertype{$paper}{yoff}; # 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.2,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}});
}
sub bydate ($$) {
my ($a, $b) = @_;
$a=~s!^.*?/log.([^/]+)$!$1!;
$b=~s!^.*?/log.([^/]+)$!$1!;
return $a cmp $b;
}
# 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
my $lastvol=-1;
my @logs = reverse sort bydate (<$LOGDIR/log.*>, <$LOGDIR/oldlog/log.*>);
splice @logs, 0, $ignore if defined $ignore;
FILE: foreach my $logfile (@logs) {
if (defined $dignore && &bydate("$dignore.0", $logfile) > 0) {
$dignore = undef;
splice @logfiles, -$lastvol, $lastvol if $lastvol>0;
$lastvol = -1;
}
open (IN, $logfile) or die "$logfile: $!";
my @l=reverse <IN>;
close (IN);
foreach my $l (@l) {
if ($l=~/^START taper\s+.*label\s+(\S+)\s+/) {
if (defined $lignore && ($lignore eq $1)) {
$lignore = undef;
splice @logfiles, -$lastvol, $lastvol if $lastvol>0;
}
$lastvol=$#logfiles+1;
last FILE if exists $labels{$1};
$labels{$1}=1;
}
}
if (!defined $lignore && !defined $dignore) {
unshift (@logfiles, $logfile);
}
}
}
if (defined $lignore) {
print STDERR "Can't find tape '$lignore'.\n";
exit 1;
}
if (defined $dignore) {
print STDERR "Can't find date '$dignore'.\n";
exit 1;
}
if ($#logfiles<0) {
print STDERR "Could not find logfiles to parse.\n";
exit 1;
}
# Parse the logfiles. This time we go from oldest to newest. This pass
# constructs %dumper, %taper, %dumped, %taped, %tape and $curlabel.
# In addition, any errors from taper are remembered.
#
foreach my $logfile (@logfiles) {
open (IN, $logfile) or die "$logfile: $!";
my $driverdate="unknown";
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=~/^START driver date (.*)/) {
$driverdate = $1;
}
if ($line=~/^STRANGE dumper (.*)/) {
my ($host, $filesystem, $level, $rest) = split (/\s+/, $1, 4);
my $disk="$host:$filesystem";
$dumped{$disk,$level} = $logfile;
$dumper{$disk}{$level} = {dumpdate=>$driverdate, %{&hash($rest)}};
}
if ($line=~/^SUCCESS dumper (.*)/) {
my ($host,$filesystem,$date,$level,$rest) = split (/\s+/, $1, 5);
$date=~s/(....)(..)(..)/$1-$2-$3/;
my $disk="$host:$filesystem";
$dumped{$disk,$level} = $logfile;
$dumper{$disk}{$level} = {dumpdate=>$date, %{&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,$date,$level,$rest) = split (/\s+/, $1, 5);
my $disk="$host:$filesystem";
$date=~s/(....)(..)(..)/$1-$2-$3/;
$taper{$disk}{$logfile}{$level} = {
%{&hash($rest, label=>$label, level=>$level, nr=>++$nr,
dumpdate=>$date, dump=>$dumper{$disk}{$level})}};
$taped{$disk,$level} = $logfile;
$tape{$label}{"kb"} += $taper{$disk}{$logfile}{$level}{"kb"};
$tape{$label}{"count"}{$level}++;
}
if ($line=~/^INFO taper tape (.*)/) {
my ($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 %-10s %6s:%-3s %2s %s",
qw/Dspd Tspd Dtime Dsize Osize Tsize Ttime dumpdate 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}}) {
foreach my $level (reverse sort keys %{$taper{$disk}{$logfile}}) {
my $taping=$taper{$disk}{$logfile}{$level};
my $lv = $taping->{"level"};
next if !$verbose && $lv>=$lastlevel;
if ($lastlevel<10000 && $lv!=$lastlevel-1) {
&out("err",sprintf("%12s %5s %6s %2s %2s %s",
$logfile, "?", "?", "?", "?",
"Missing level $lv of $disk"));
}
$lastlevel = $lv;
&out("bot",
sprintf ("%5s %5s %8s %5s %5s %5s %8s %-10s %6s:%03d %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"}),
$taping->{"dumpdate"},
$taping->{"label"},
$taping->{"nr"},
$taping->{"level"}, $disk
));
$needed{$logfile}{"label"} = $taping->{"label"};
$needed{$logfile}{"sec"} += $taping->{"sec"};
$needed{$logfile}{"kb"} += $taping->{"kb"};
my $okb = $taping->{"dump"}{"orig-kb"};
$needed{$logfile}{"okb"} += $okb if defined $okb;
$needed{$logfile}{"level"}[$lastlevel]++;
}
}
if ($lastlevel != 0) {
&out("err",sprintf("%12s %5s %6s %2s %2s %s",
"?", "?", "?", "?", "?",
"Missing level 0 of $disk"));
}
&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 = $format eq "text" || $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;
unless ($format eq "text") {
$out = new PostScript::Simple(eps=>0, papersize=>$paper, units=>"cm");
$out->setcolour("black");
$out->setlinewidth(0.01);
$out->newpage;
}
# 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 ($format eq "text") {
print "$o->{v}\n";
} else {
$out->setfont ("Courier-Bold", $w->{fs});
$out->text ($xoff+$o->{x}, $yoff-$o->{y}, $o->{v});
}
}
if ($#{$w->{v}} >= 0) {
if ($format eq "text") {
print "\n";
} else {
if ($w->{x} < 8) {
$yoff -= ($w->{ys} * (1.5+$#{$w->{v}}));
}
}
}
# Print date, label and total size
#
if ($format eq "text") {
print "$tape{$curlabel}{'date'} $curlabel Total size: ",
&kb($tape{$curlabel}{"kb"}), "\n\n";
} else {
$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"}));
}
# 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($output) unless $format eq "text";
# 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;
unless ($format eq "text") {
$out->setfont ("Courier-Bold", $font);
}
foreach my $b (@box) {
unless ($format eq "text") {
$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 ($format eq "text") {
print "$b->{lines}[$i]{v}\n";
} else {
$out->text ($toff, $y, $b->{lines}[$i]{v});
}
}
}
}
# 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)=@_;
return "?" unless defined $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)=@_;
return "?" unless defined $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;
}
---- 8< ---- 8< ---- 8< ---- 8< ---- 8< ---- 8< ---- 8< ----
--
-- Josef Wolf -- jw AT raven.inka DOT de --
|