#!/usr/local/bin/perl -w
#
# $Header: /afs/.pdc.kth.se/sgi_63/usr/pdc/bin/RCS/sysdump.pl,v 1.9 2002/10/11 08:46:08 pek Exp $
#
# pek@pdc.kth.se 20020910

# Dump xfs filesystems to MAGSTAR

# 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.

use English;
use strict;
use POSIX;
use FileHandle;
use Time::Local;
use Getopt::Std;
use File::Copy;
use File::Basename;

use vars qw($opt_d $opt_e $opt_f $opt_l $opt_v $opt_n);

$INPUT_RECORD_SEPARATOR = "\n";

# Dump these filesystems 
my (@fslist) = ("/", "/dmf/journals", "/hsm");

# Specify which day to perform lvl 0 bup. First char must be capitalized.
my($lvl0day)	= "Thursday";

# Retention period in days
my ($keepdays)  = 30;

my ($version) = "Version 1.0\n";
my ($usage) = "Usage : [ -fne ] [ -d date ] [ -l level ]\n";
my ($usage_long) = "Options :\n" .
    " -f	Fake run (use simulator)\n" .
    " -e	Run expiry processing on the tape db and exit.\n" .
    " -n	Don't send mail.\n" . 
    " -d date	Use the given date as the dump time.\n" .
    " -l level	Override dump level.\n";

# Lock file
my ($lockfile)	= "/tmp/sysdumplock";

# Tape database directory
my ($tapedir)	= "/var/tapes";
my ($tapecat)	= "0554"; # Tape category to use
my ($tapedb)	= "$tapedir/buptapes.db";

# Remote host destination dir
my ($rpath)	= "/projects/backups/episode4/xfsdump_inventories";
my ($buphost)	= "foo.bar.org";

# Send error reports here
my ($notifyaddress) = "donnie_darko\@foo.bar.org";

# Daily logfile
my ($dlogfile)  = "/var/spool/pdc/sysdump.daily.log";

# Temp file for xfsdump output
my ($tmpfile)	= "/tmp/" . basename $PROGRAM_NAME . ".out.$PID";

# Commands
my ($rcp)	= "/usr/heimdal/bin/rcp";
my ($date)	= "/usr/gnu/bin/date";
my ($gzip)	= "/usr/sbin/gzip";
my ($grep)	= "/usr/bin/grep";
my ($dmmigrate) = "/usr/dmf/etc/dmmigrate";
my ($dmconfig)  = "/usr/dmf/etc/dmconfig";
my ($mtlib)	= "/usr/bin/mtlib";
my ($xfsdump)	= "/usr/sbin/xfsdump";
my ($dm_tape)	= "/usr/dmf/etc/support/dm_tape";
my ($sendmail)  = "/usr/lib/sendmail";

my ($fake)	= 0;
my ($dstr)	= `$date +%A`; chomp $dstr;
my ($datestr)	= `$date +%Y%m%d`; chomp $datestr;
my ($time)	= time - 86400*$keepdays; # 86400 seconds/day
my ($buplvl)	= 5;

autoflush STDOUT 1;

if (!getopts('vnefd:l:')) {
    print $usage;
    print $usage_long;
    exit;
}
if ($opt_v) {
    print $version;
    exit;
}
if ($opt_d) { # Use a specified date instead of todays date
    $datestr = $opt_d; chomp $datestr;
    if ($datestr !~ /^\d{8}$/) {
	print STDERR "$PROGRAM_NAME : Invalid date format \"$datestr\"\n";
	exit 1;
    }
    $time = `$date -d \'$datestr\' +%s` - 86400*$keepdays;
    $dstr = `$date -d \'$datestr\' +%A`; chomp $dstr;
    print "Today is $dstr\n";
    print "Time is $time\n";
}
# Need to do this here to allow another buplvl to be set below
if ($dstr eq $lvl0day) { 
    $buplvl = 0;
}
if (defined $opt_l) {
    $buplvl = $opt_l;
}
if ($opt_f) { # Fake run
    $fake = 1;
}
if ($#ARGV != -1) {
    print $usage;
    exit;
}

my ($retdate)	= `$date -d \'1970-01-01 $time sec\' +%Y%m%d`;
chomp $retdate;

my ($FREETAPE)	= "FREETAPE";
my ($FULLTAPE)	= "FULLTAPE";
my ($NULLDATE)	= "00000000";

# Globals
my ($basedate)	= $NULLDATE;
my ($lastincr)	= $NULLDATE;
my ($vsn)	= "0";
my ($vsndate)	= $NULLDATE;

my (%vsnhash); # Hash keyed on vsn with fields "Date", "Base" and "Full"
my (%usedtapes);
my ($str, $rc, $fs, $dumplabel, $eotcmd, $xfscmd, $dmtcmd);
my ($ovwrt_opt) = "";
my ($resume_opt)= "";
my ($ov_opts)	= "";
my ($EOT)	= 0;

my ($hostname)  = `/usr/bsd/hostname -s`; chomp $hostname;
my ($ovkeyfile);
my ($device);
my ($ixmodes);
if (!$fake) {
    $ovkeyfile	= `$dmconfig -p base OV_KEY_FILE`;
    chomp $ovkeyfile;
    $device	= `$dmconfig dump_tasks DUMP_DEVICE`;
    chomp $device;
    $ixmodes	= `$dmconfig $device OV_INTERCHANGE_MODES`;
    chomp $ixmodes;
    $ov_opts	= "key_file=$ovkeyfile";
    if ($ixmodes) {
	$ov_opts = "$ov_opts,interchange_modes=$ixmodes";
    }
}
else {
    if (! -e "$tapedb.fake") {
	copy($tapedb, "$tapedb.fake");
    }
    $tapedb = "$tapedb.fake";
}

my ($log_ref);
if (open LOG, ">$dlogfile") {
    autoflush LOG 1;
    $log_ref = \*LOG;
}
else {
    autoflush STDERR 1;
    $log_ref = \*STDERR;
    error("Failed to open logfile\n");
}

sub debug ($) {
    $str = shift;
    print "Debug: $str" if ($fake);
}

sub run (@_)
{
    my ($rc);

    print " @_\n"; $rc = 0;
    $rc = system (@_);
    debug "rc is $rc\n";
    return $rc >> 8;
}

# Quote \, $, ', "
sub backq ($) {
    my ($str) = shift;
    my (@chars) = split //, $str;
    my ($i, $res);
    map s/([\$\'\"\\])/\\$1/, @chars;
    $res = join "",@chars;
    return $res;
}

sub slog {
    my ($str) = shift;
    print $log_ref "### [" . localtime() . "] $PROGRAM_NAME: $str";
    print "### [" . localtime() . "] $PROGRAM_NAME: $str";
}

sub bail {
    close LOG;
    unlink $lockfile;
    exit 1;
}

sub send_mail($$$$$)
{
    my $From    = $_[0];
    my $To      = $_[1];
    my $Subject = $_[2];
    my $CC      = $_[3];
    my $Msg	= $_[4];
    
    if ($opt_n) {
	print "Mail : $Msg";
	return;
    }
    # Check real uid
    if ($< eq 0)
    {
        unless (open (M, "| $sendmail -f $From -oi -t")) {
            print STDERR "Failed to run $sendmail\n";
            return 0;
        }
        print M "From: $From\n";
    }
    else
    {
        unless (open (M, "| $sendmail -oi -t")) {
            print STDERR "Failed to run $sendmail\n";
            return 0;
        }
    }   
    
    if ($From ne "") { print M "From: $From\n"; }
    print M "To: $To\n";
    if ($CC ne "") { print M "Cc: $CC\n"; }
    if ($Subject ne "") { print M "Subject: $Subject\n"; }
    print M "\n";
    print M "$Msg\n";
    close (M);
}

sub error { 
    my ($msg) = shift;

    if (!$fake) {
	send_mail("root\@foo.bar.org", $notifyaddress, 
		  "$PROGRAM_NAME failed on $hostname", " ", $msg);
    }
    else {
	print "ERROR! : $msg";
    }    
    slog "ERROR! : $msg";
}

# Return 1 if the tape is in the specified category
my (%vsncathash);
sub check_vsncat($$) {
    my ($vsn) = shift;
    my ($cat) = shift;
    
    if (! %vsncathash) {
	if ($fake) {
	    open FH, "/tmp/tapes.txt" or 
		(error("Failed to get tapelist from robot\n") and bail);
	}
	else {
	    open FH, "$mtlib -l3494a -qI|" 
		or (error("Failed to get tapelist from robot\n") and bail);
	}
	foreach $str (<FH>) {
	    debug "Scanning $str\n";
	    if ($str =~ /^(\S{6})\s(\w{4})\s.+$/) {
		$vsncathash{$1} = $2;
	    }
	    else {
		error "check_vsncat() encountered invalid line in tape inventory :\n"
		    . "$str\n";
		bail;
	    }
	}
	close FH;
    }
    if (! defined $vsncathash{$vsn}) { # Tape does not exist
	error "Invalid or non-existing vsn $vsn found in tape database\n";
	bail;
    }
    if ($vsncathash{$vsn} eq $cat) {
	return 1;
    }
    return 0;
}

# Get a new (empty) vsn, remove it from %vsnhash
sub get_new_vsn { 
    my ($nvsn);
    my ($nvsndate) = $datestr;

    foreach $str (keys %vsnhash) {
#	debug "key $str\n";
	if ($vsnhash{$str}{"Date"} eq $FREETAPE) {
	    $nvsn = $str;
	    $nvsndate = $FREETAPE;
	    last;
	}
    }
    if (! $nvsn) {
	error("Failed to get a tape\n");
	save_db();
	bail;
    }
    if ($nvsndate eq $datestr) {
	error("$PROGRAM_NAME ran out of tapes.\n");
    }
    delete $vsnhash{$nvsn};
    return ($nvsn, $nvsndate);
}

sub expire_tapes {
# Expire tapes in the database. Tapes that has been used this session
# will not be freed no matter what.
    if ($buplvl == 0) {
	my ($basecount) = 0;
	slog "Expiring tapes\n";
	foreach $str (keys %vsnhash) {
	    if ($vsnhash{$str}{"Base"} lt $retdate) {
		debug "Expire $str with base date " . $vsnhash{$str}{"Base"} . "\n";
		$vsnhash{$str}{"Date"} = $FREETAPE;
		$vsnhash{$str}{"Base"} = $NULLDATE;
	    }
	}
	# Check the number of lvl 0 dumps that are left
	foreach $str (keys %vsnhash) {
	    if ($vsnhash{$str}{"Base"} ne $NULLDATE) {
		$basecount++;
	    }
	}
	if ($basecount < 2) {
	    error("Only $basecount base dumps left after expiration processing.\n");
	}
    }
}

sub save_db {
    if (! $fake) {
	rename $tapedb, "$tapedb.bk.$datestr" or error("Failed to rename tapedb.\n");
    }
    $str = "";
    foreach $vsn (keys %vsnhash) {
	$str = $str . "$vsn " . $vsnhash{$vsn}{"Base"} . " " . $vsnhash{$vsn}{"Date"} .
	    "\n";
    }
    if ($buplvl == 0) {
	foreach $vsn (keys %usedtapes) {
	    $str = $str . "$vsn $basedate $NULLDATE\n";
	}
    }
    else {
	foreach $vsn (keys %usedtapes) {
	    $str = $str . "$vsn $basedate $usedtapes{$vsn}\n";
	}
    }
    if (! open FH, ">$tapedb") {
	$str = "Failed to open tapedb.\n" . $str;
	error($str);
    }
    else {
	print FH $str;
	close FH;
    }
}

slog "invoked\n";
# Check lock
if ( -e $lockfile) {
    error "Dump session failed : old lockfile ($lockfile) encountered.\n";
    exit;
}
open FH, ">$lockfile" or (error("Failed to create lockfile $lockfile\n") and die);
close FH;

debug "Date $datestr, retention date is $retdate, default dump level 0\n";
# Load the dump tape database, set $lastincr to the latest incremental
# dump date and $vsn to the corresponding vsn. Set $basedate to the
# latest base dump date. 
# Tape list format is
# <VSN> (<basedate (yyyymmdd)> <date (yyyymmdd)>)
open FH, "$tapedb" or (error ("Failed to open tape db\n") and bail);

foreach $str (<FH>) {
    my ($tvsn, $tbase, $tincr);
    if ($str =~ /^(\S{6})$/) { # Lone VSN
	$tvsn = $1;
	$tbase = $NULLDATE;
	$tincr = $FREETAPE;
    }
    elsif ($str =~ /^(\S{6})\s+(\S{8})\s+(\S{8})$/) {
	($tvsn, $tbase, $tincr) = ($1, $2, $3);
    }
    else {
	error "Invalid tape database entry \"$str\"\n";
	bail;
    }
    $basedate = $tbase if ($tbase gt $basedate && $tincr eq $NULLDATE);
    $vsnhash{$tvsn}{"Base"} = $tbase;	
    $vsnhash{$tvsn}{"Date"} = $tincr;
    debug "Adding $tincr to vsn $tvsn\n";
    if ($tincr gt $lastincr && $tincr ne $FREETAPE && $tincr ne $FULLTAPE) {
	$lastincr = $vsndate = $tincr;
	$vsn = $tvsn;
    }
    close FH;
}

# Run expiry process and then quit if invoked with -e
if ($opt_e) {
    $buplvl = 0;
    expire_tapes();
    save_db();
    # Clean up
    if (! -e $lockfile) {
	error "$PROGRAM_NAME running around in panic :\n lockfile $lockfile has disappeared during execution\n";
	exit 1;
    }
    unlink $lockfile;
    close LOG;
    exit 0;
}

# Revert to lvl 0 if no bases can be found
if ($basedate eq $NULLDATE) {
    error("No level 0 dump tapes found. Reverting to level 0.\n");
    $buplvl = 0;
}

# Find a new tape for lvl 0 dumps
if ($buplvl == 0) {
    ($vsn, $vsndate) = get_new_vsn();
    $usedtapes{$vsn} = $datestr;
    $basedate = $datestr;
}
else {
    # Check if the latest base dump is newer than the latest incremental.
    # If so, a new tape is selected for incremental dumps (the current 
    # tape contains last weeks incrementals).
    if ($basedate gt $lastincr) {
	if ($vsn gt "0") {
	    debug "Rollover of $vsn (newer lvl 0 exists)\n";
	    $vsnhash{$vsn}{"Date"} = $FULLTAPE; # Week rollover
	}
	($vsn, $vsndate) = get_new_vsn();
	$usedtapes{$vsn} = $datestr;
    }
    else {
	$usedtapes{$vsn} = $datestr;
	delete $vsnhash{$vsn};
    }
}

# Verify that tape is kosher (we don't want to accidentally write on someone
# elses tape).
if (! check_vsncat($vsn, $tapecat)) {
    error("VSN $vsn is not in category $tapecat\n");
    bail;
}

debug "Found vsn $vsn, last written date $vsndate.\n";

# Overwrite tape?
if ($buplvl == 0 || $vsndate eq $FREETAPE) {
    $ovwrt_opt = "-o";
}

# Perform dump
foreach $fs (@fslist) {
    # Make sure everything is migrated
    run("$dmmigrate -w $fs") if (!$fake);
    slog "Dumping $fs at level $buplvl\n";
    debug "Dumping $fs\n";
    $resume_opt = "";
    $rc = 1;
    while ($rc != 0) {
	if ($vsndate eq $FREETAPE) {
	    $ovwrt_opt = "-o";
	    $vsndate = $datestr;
	}
	if ($fs eq "/") {
	    $dumplabel = "root\_lvl$buplvl\_$datestr";
	}
	else {
	    $dumplabel = "$fs\_lvl$buplvl\_$datestr";
	}
	# dm_tape will mount the current vsn for us
	$xfscmd = backq "$xfsdump -f \$TAPE -a -v silent -F -M $vsn -l $buplvl " .
	    "-L $dumplabel -p 60 $ovwrt_opt $resume_opt $fs";
	$dmtcmd = "$dm_tape -d ov,$ov_opts -c \"$xfscmd; echo \\\$? >$tmpfile\" $vsn";
	if ($fake) {
	    $rc = run("./dumpsim.pl $vsn 150 $ovwrt_opt > $tmpfile 2>&1");
	}
	else {
	    $rc = run($dmtcmd);
	    debug "Dump return value is $rc\n";
	    $rc = ($CHILD_ERROR >> 8);
	    debug "rc is $rc\n";
	}
	# dm_tape return 0 even if xfsdump fails and the echo succeeds
	if ($rc == 0 && -e $tmpfile) {
	    $rc = 3;
	}

	if ($rc == 1) {
	    error("$dm_tape usage error\n");
	    bail;
	}
	elsif ($rc == 2) {
	    error("Tape mount error (might want $PROGRAM_NAME to retry a few times)\n");
	    bail;
	}
	elsif ($rc == 3) { # Possible EOT
	    debug "Possible EOT\n";
	    if (run("$grep -E \"^2\$\" $tmpfile") == 0) {
		debug "EOT set\n";
		slog "Tape $vsn full.\n";
		# EOT encountered. Mark current tape as full
		$usedtapes{$vsn} = $FULLTAPE;
                # Get new tape and try again with -R (resume) flag
		($vsn, $vsndate) = get_new_vsn();
		if (! check_vsncat($vsn, $tapecat)) {
		    error("VSN $vsn is not in category $tapecat\n");
		    bail;
		}
		slog "Resuming dump of $fs on tape $vsn\n";
		debug "Found vsn $vsn, last written date $vsndate\n";
		# Register the vsn used so the tapedb can be updated
		$usedtapes{$vsn} = $datestr;
		$resume_opt = "-R";
		next;
	    }
	    elsif (run("$grep -E \"^0\$\" $tmpfile") != 0) {
		error("xfsdump error\n");
		bail;
	    }
	    $rc = 0;
	}
	elsif ($rc == 4) {
	    error("Tape dismount errors\n");
	    last;
	}
	elsif ($rc != 0) {
	    error("Failed with return value $rc\n");
	}
    }
    # Clear ovwrt flag to fit more lvl-0 bups on this tape
    $ovwrt_opt = "";
    slog "Finished dumping $fs\n";
}

slog "Update backup tape database\n";
expire_tapes();
save_db();
slog "Done updating backup tape database\n";

# Save inventory off-machine
if (! $fake) {
    slog "Saving xfs inventory to $buphost\n";
    if (run("$xfsdump -I > /tmp/dumpinv.$PID") != 0 
	|| run("$gzip /tmp/dumpinv.$PID; " .
	       "$rcp /tmp/dumpinv.$PID.gz " .
	       "$buphost:\"$rpath/$hostname.dump-inventory.$datestr.gz\"") != 0) {
	error("Failed to copy inventory dump to remote host $buphost.\n" .
	      "Copy inventory manually.\n");
    }
    unlink "/tmp/dumping.$PID.gz";
    slog "Done saving xfs inventory to $buphost\n";
}

slog "All done!\n";
# Clean up
if (! -e $lockfile) {
    error "$PROGRAM_NAME running around in panic :\n lockfile $lockfile has disappeared during execution\n";
    exit 1;
}
unlink $tmpfile;
unlink $lockfile;
close LOG;
exit 0;


