#!/usr/bin/perl
# ft-backup - backup using ftape and dump
#
# Usage:
# ft-backup [-f] [filesystem ...]
#  -f for faster backup run
#      (i.e. don't do anything that requires extra tape movement)
#  If no filesystems listed on command line, backup all filesystems with
#  nonzero dump flag in /etc/fstab (second-to-last field).  Otherwise
#  backup only filesystems given on command line.

# configurable parameters
$TAPE = "/dev/nzqft0"; # use compressed device
$ratio = 1.7; # avg compression ratio, >= 1.0
$threshold = "1400"; # if tape is bigger than $threshold megs, do full dump
$blocksize=  "10"; # in KB, ftape defaults to 10
# don't forget to specify the blocksize to restore(8) using 'b'

# program locations
$date = "/bin/date";
$mt = "/usr/bin/ftmt";
$dump = "/sbin/dump";
$tar = "/bin/tar";
$logger = "/usr/bin/logger";
$swapout = "/sbin/swapout";

use Sys::Hostname;
$host = hostname;

sub log {
    # log message to syslog
    my($message,$loglevel) = @_;
    $loglevel = "info" unless ($loglevel);
    ! system "$logger -t ft-backup -p daemon.$loglevel $message";
}

sub string2k {
    # get size as string, return size in K
    local($_) = @_;
    my($size,$mult);

    # initialize multiplier table values in K
    my(%multable) =
	(
	"G" => 1024*1024,
	"M" => 1024,
	"K" => 1,
	"" => 1/1024,
	 );

    ($size,$mult) = /^\s*([\d.]+)\s+([a-z]?)[a-z]*bytes/i;
    $mult = uc($mult);

    return ($size * $multable{$mult});
}

sub mt {
    my(@command) = @_;
    my($command) = "@command";
    @command = split(/\s+/,$command);

    ! system($mt,"-f","$TAPE",@command);
}

sub status {
    my($verbose) = @_;
    my(@line,$rest,%stat);
    local($_);

    open(STATUS,"$mt -f $TAPE status|");
    while (<STATUS>) {
	print if $verbose;
	chomp;
	if (s/^This is a /This is a,/) {
	    (@line[0,1],$rest) = split(/,/,$_);
	    $stat{$line[0]} = $line[1];
	    $rest =~ /^ \(([^\)]*)\).*$/; $_ = $1;
	}
	if (/^\(/) {
	    # catch: (In particular: * whatever * )
	    /^\(([^:]*): (.*) \)$/; $_ = "$1 = $2";
	}
	next unless (/ = /);
	@line = split(/\s+=\s+/);
	$stat{$line[0]} = $line[1];
    }
    close(STATUS);

    # return reference to %stat
    return \%stat;
}

sub getsize {
    # return size in K
    my $stat = &status();
    return(string2k($$stat{'total bytes on tape'}));
}

sub fslist {
    my(@list) = @_;
    my(@fs);

    # for each possible filesystem, define whether to use dump, tar, or null
    my %backtype = (
		    "auto" => "tar", # tar can do anything
		    "minix" => "tar",
		    "ext" => "tar",
		    "ext2" => "dump",
		    "xiafs" => "tar",
		    "ufs" => "tar",
		    "sysv" => "tar",
		    "xenix" => "tar",
		    "coherent" => "tar",
		    "msdos" => "tar",
		    "fat" => "tar",
		    "fat32" => "tar",
		    "vfat" => "tar",
		    "umsdos"=> "tar",
		    "hpfs" => "tar",
		    "iso9660" => "tar", # probably shouldn't backup CDs
		    "nfs" => "tar", # ideally, NFS-mounts should backup there
		    "smb" => "tar",
		    "ncp" => "tar",
		    "affs" => "tar",
		    "swap" => "null",
		    "proc" => "null",
		    "ignore" => "null",
		    "" => "null",
		    );

    open(FSTAB,"/etc/fstab");
    while(<FSTAB>) {
	next unless (/^[^\#]/);
	@line=split;
	next if ($line[1] eq "none");
	if (@list) {
	    if (grep($_ eq $line[0],@list) or grep($_ eq $line[1],@list)) {
		push(@fs,"$line[1] $backtype{$line[2]}");
	    }
	} else {
	    push(@fs,"$line[1] $backtype{$line[2]}") if ($line[4]);
	}
    }
    close(FSTAB);
    return(@fs);
}

sub dump {
    my($level,$fs) = @_;
    my $label;
    my $labelfile;
    my $retval;
    my @dumpargs;
    my $now = time;

    $label = "$host:$fs";
    $label =~ s|/|,|g; # use "," instead of "/"
    $label =~ s/:,/:/; # but omit initial marker
    # first touch the label file
    if (-d $fs) {
	$labelfile = "$fs/.$label";
	if (-e $labelfile) {
	    utime($now,$now,$labelfile);
	} else {
	    open(LABEL,">$labelfile");
	    close(LABEL);
	}
    }

    &log("Dumping $fs");
    print "\n+ dumping $fs....\n";
    @dumpargs = ($dump,"${level}ufbB",
	     $TAPE,
	     $blocksize,
	     int($ratio*$size), # take compression ratio into account
	     $fs
	     );
    print "  @dumpargs\n";
    $retval = system(@dumpargs);

    return !$retval;
}

sub tar {
    my($fs) = @_;
    my $retval,@tarargs;

    &log("Tarring $fs");
    print "\n+ tarring $fs....\n";
    @tarargs = ($tar,"cf", $TAPE,
	     "--one-file-system",
	     "--sparse",
	     "--blocking-factor", 2*$blocksize,
	     "--totals",
	     "--atime-preserve",
	     "--preserve-permissions",
	     "--label", "$host:$fs",
	     "--directory", $fs,
	     "."
	     );
    print "  @tarargs\n";

    # emulate dump's verbosity
    print "  TAR: Date of this tar: ", `$date '+%a %b %d %T %Y'`;
    print "  TAR: Tarring $fs to $TAPE\n  TAR: ";
    $retval = system(@tarargs);
    print "  TAR: TAR IS DONE\n";

    return !$retval;
}

sub null {
    my($fs) = @_;

    &log("Not backing up $fs");
    print "+ Not backing up $fs\n";
}

sub getratio {
    my $physspace = 0;
    my $realsize = 0;
    my $phystot = 0;
    my $realtot = 0;
    my $ratio = 0;
    my $stat; # reference to status hash

    &log("Calculating compression ratio");
    &mt("rewind"); # back to beginning
    $stat = &status();
    $physspace = string2k($$stat{'physical space used'});
    $realsize = string2k($$stat{'real size of volume'});

    while ($realsize) {
	# real size is zero when done
	$phystot += $physspace;
	$realtot += $realsize;

	&mt("fsf 1"); # next file
	$stat = &status();
	$physspace = string2k($$stat{'physical space used'});
	$realsize = string2k($$stat{'real size of volume'});
    }
    &mt("rewind");
    if ($phystot) {
	$ratio = ($realtot/$phystot);
    } else {
	$ratio = 0;
    }

    # round to 3 dec places
    $ratio = sprintf('%.3f',$ratio);
    &log("Compression ratio was 1:$ratio");
    return $ratio;
}

#####

# unbuffer STDOUT and STDERR, and select STDERR (which dump uses)
$|=1; select STDERR; $|=1;

@opts = grep(/^-/,@ARGV);
@argv = grep(!/^-/,@ARGV);
$fast=1 if (grep(/^-f/,@opts));

$datenow = `$date`;
&log("Starting backup");
print "Starting backup at $datenow\n\n";
system($swapout,"30");

if (${&status(1)}{"In particular"} =~ /(\(no tape\)|^\*?$)/) {
    &log("Tape offline or missing -- aborting");
    print "\nTape offline or missing -- aborting\n";
    exit 1;
}
if (${&status(1)}{"In particular"} =~ / tape write protected /) {
    &log("Tape write-protected -- aborting");
    print "\nTape write-protected -- aborting\n";
    exit 1;
}

unless ($fast) {
    &log("Retensioning tape");
    print "\nRetensioning tape.\n";
    &mt("reten")
	or die "Unable to retension tape -- No tape?  Device permissions?\n";
}
&log("Rewinding tape");
print "Rewinding tape.\n";
&mt("rewind");

# get real and compressed size of tape
$size = &getsize;
$ratio = 1 if ($device !~ /z/); # non-compressed device gets ratio 1:1
$csize = $ratio*$size;

# allow for doing different kind of backup
# depending on length of tape
if ($size > $threshold*1024) {
    $level = 0;
} elsif ($size > 0) {
    $level = 3;
} else {
    &log("Tape is size $size?  Aborting");
    print "Tape size is $size ???  Aborting!\n";
    exit 1;
}
$size_M = int(sprintf('%.0f',$size/1024));
$csize_M = int(sprintf('%.0f',$csize/1024));
print "Tape size is ${size_M}M";
print " (${csize_M}M with 1:${ratio} compression)" if ($ratio != 1);
print "\n";
print "...doing level $level backup" if (defined ($dump));
print "\n\n";

# get filesystems to backup, either from command line or from fstab
@fs = &fslist(@argv);

# backup each one individually
foreach (@fs) {
    ($fs,$type) = split;
    if ($type eq "dump") {
	&dump($level,$fs);
    } elsif ($type eq "tar") {
	&tar($fs);
    } elsif ($type eq "null") {
	&null($fs);
    } else {
	print "unknown type $type: ";
	&null($fs);
    }
}
print "\n";

# show new tape status
&status(1);

unless ($fast or ($ratio==1)) {
    # get (new) average compression ratio
    $ratio = &getratio();
    print "\nActual compression ratio for this backup was 1:$ratio\n";
}

# rewind and take tape offline
&mt("rewoffl");

$datenow = `$date`;
&log("Backup done");
print "\nDone at $datenow\n";

__END__
