#!/usr/bin/perl -w
# -------------------------------------------------------
# $Header: /home/imorgan/coding/dobackup/RCS/dobackup.pl,v 4.3 2000/06/11 08:05:48 imorgan Exp $
# -------------------------------------------------------
#
# Copyright 1997-2000 Webcon, Inc.
#
# This code is available for public use under the terms of the GNU Public
# License (GPL). See http://www.gnu.org/copyleft/gpl.html for license
# details.
#
# We reserve the right to modify the licensing terms for any future version
# of this product.
#
# Product website: http://www.webcon.net/products/dobackup/
#
# Original Program by: Robert Hardy <rhardy@webcon.net>
# Prior to first public release, all updates by:
#   Robert Hardy <rhardy@webcon.net>, and
#   Ian Morgan <imorgan@webcon.net>
#

# External Calls:
use IniConf;
use strict;
BEGIN {unshift @INC, 'lib'} # So you don't have to install Getopt::Mixed first
use Getopt::Mixed 1.006, 'getOptions'; # For Command Line Processing
# End External Calls

# ------------- Cron Setup
# Set environment variables that aren't present when run as a cron job.
$ENV{'TERM'} = "vt100";
$ENV{'LINES'} = "25";
$ENV{'COLUMNS'} = "80";
# --------- End Cron Setup

# ------------- Begin Defaults
# Define all variables using my() statements
my($BackupDay, $ToBackup);
my($BackupAdministrator)= 'root@localhost'; #overriden by config file
my($ConfigFile)='/etc/dobackup.conf';       #overriden by command line arg
my($BackupLogDir)='/var/log/backup';        #overriden by config file
my($BackupTimeStamp)='/var/log/backup/LastBackupTimeStamp';
my($SingleHostOveride)='';
my($TimeStampOveride);
my($StartTime)=0;

my($Host); #iterates through hosts to be backed up
my($IniConfig); #the IniConf object is loaded in &LoadConfig;
my($NeedTapeRewind)=1; #rewind tape for first host only

#A bunch of temp variables used in date manipulation
my($Seconds); my($Minutes); my($Hours); my($MDay); my($Month); my($Year);

# ------------- Config File Variables
my(@HostsInConfig)=();
my(%IP);
my(%Timestamp)=();
my(%Mode)=();
my(%Destination)=();
my(%MediaUnit)=();
my(%MediaSet)=();
my(%Paths)=();

$StartTime=time; # This is the current time stamp in seconds from EPOC (~1970).

# Let get what we need out of $StartTime. Ugly but more accurate.
($Seconds,$Minutes,$Hours,$MDay,$Month,$Year,undef,undef,undef) = localtime($StartTime);

$Year+=1900; # Year returned by localtime is Real Year - 1900: compensating.
$Month+=1; # Month returned by localtime is 0..11: compensating.

# Must pad numbers w/ zeros to get YYYYMMDD_HHMMSS !
$BackupDay=sprintf('%04u%02u%02u_%02u%02u%02u', $Year, $Month, $MDay, $Hours, $Minutes, $Seconds);

my($Arg);
my($BackupErrorFile)="$BackupLogDir/$BackupDay.errors";
my($Debug)=0; # Debug off by default
my($ExecutableName) = ( $0 =~ /([^\/]*)$/ );

# ------------- Defaults overrideable by Config File
my($BackupProg)="/usr/bin/afio -o -T 3k -G 9 -Z";
my($BackupProgDebug)="/usr/bin/afio -o -v -T 3k -G 9 -z -Z";
my($Cat)="/bin/cat";
my($Cut)="/usr/bin/cut";
my($DF)="/bin/df";
my($Echo)="/bin/echo";
my($MT)="/bin/mt";
my($MV)="/bin/mv";
my($MailProg)="/usr/sbin/sendmail";
my($Ping)="/bin/ping -q -c 1";
my($TR)="/usr/bin/tr";
my($Tail)="/usr/bin/tail";
my($Mount)="/bin/mount";
my($Umount)="/bin/umount";
my($Eject)="/usr/bin/eject";
my($Grep)="/bin/grep";
my($Umask)="umask"; #shell internal (no path)
# -------------

my($ToBackupTempFile)="$BackupLogDir/BackupTemp.$$";

# ------- Arguments from here to End Defaults
my($opt_test,$opt_disk,$opt_tape,$opt_host,$opt_timestamp,$opt_config);
my($opt_version,$opt_debug,$opt_help,$opt_fullbackup);
# --------------- End Defaults

# ------------- Begin Main Program -----------------------------------------

#Pre-backup initialization functions

getOptions("test fullbackup rotate prepdisk host=s timestamp=f config=s",
           "help h>help version V>version debug:i d>debug");

&ProcessOptions;
$Debug && print "Debug: ProcessOptions complete\n";
&LoadConfig;
$Debug && print "Debug: LoadConfig complete\n";

#Per-Host backup functions

if ($SingleHostOveride ne '') {
  @HostsInConfig = ();
  push(@HostsInConfig,$SingleHostOveride);
}

foreach $::Host (@HostsInConfig) {
  $Debug && print "Checking Backup OK to run for host $::Host ($IP{$::Host})...\n";

  if ( $::opt_test ){
    if ( &BackupNotOkToRun ) {
      print "Backup Requirements NOT satisfied for host $::Host!\n";
    }
    else {
      print "Backup Requirements have been satisfied for host $::Host.\n";
    }
    next;
  }
  else {
    if ( &BackupNotOkToRun ) {
      #FIXME: Add an outgoing tech support page here!
      next;
    }
    $Debug && print "Preparing Backup for host $::Host...\n";
    &PrepareBackup;
    $Debug && print "Performing Backup for host $::Host...\n";
    &PerformBackup;
    $Debug && print "Updating config parameters for host $::Host...\n";
    &UpdateConfig;
    $Debug && print "Cleaning up.\n\n";
    &Cleanup;
  }
}

#Post-backup functions

# NOTE: This specifically allows the '--test' option, so a MediaSet can be
# rotated manually from the command line with: dobackup.pl --test --rotate
if ($::opt_rotate) {
  &RotateMedia;
}

exit;
# --------------- End Main Program -----------------------------------------

# ----------------------------
# ProcessOptions -- subroutine
# ----------------------------

# Description: Processes commandline options and sets program variables
# Reads: $::opt_debug, $::opt_config,$::opt_timestamp,
#        $::opt_version,$::opt_help
# Sets: $Debug, $SingleHostOveride, $ConfigFile, $TimeStampOveride, 

sub ProcessOptions {
  if (defined $::opt_prepdisk) { #says to prep the media before this backup
    $::opt_prepdisk=1;
  }

  if (defined $::opt_rotate) { #says to eject media and update config file
    $::opt_rotate=1;           #for next media after this backup
  }

  if (defined $::opt_debug) {
    if ( ($::opt_debug =~ /\d+/) && ($::opt_debug >= 1) ) {
      $Debug=$::opt_debug; # Simplifies Debug Statements later.
    } else {
      $Debug=1;
    }
  }

  if (defined $::opt_test) {
    $::opt_test=1;
  }

  if ($::opt_host) {
    $Debug && print "host=",$::opt_host,"\n";
    $SingleHostOveride=$::opt_host;
  }

  if ($::opt_config) {
    $Debug && print('config=',$::opt_config,"\n");
    $ConfigFile=$::opt_config;
  }

  if ($::opt_timestamp) {
    $Debug && print('timestamp=',$opt_timestamp,"\n");
    $TimeStampOveride = $::opt_timestamp;
  }

  (defined $::opt_help || defined $::opt_version ) && &ShowSyntax if (defined $::opt_help || defined $::opt_version);

  #print "ARGV = ",join(' ',@ARGV),"\n"; # For reference later. An error if too much perhaps?
} # -------------------------- End ProcessOptions

# ------------------------
# ShowSyntax -- subroutine TESTED
# ------------------------

# Description: Prints Syntax for program
# Reads: $ExecutableName

sub ShowSyntax {
  print "\nUsage: $ExecutableName <options>\n";
  print '$Revision: 4.3 $',"\n";
  print <<EOF;

Make backups of file systems based on a configuration file.

Supported options are:
  -h, --help       Prints this help message
  -d=INT           Enable debugging mode (verbosity level = INT)
  --debug=INT
  -v, --version    Prints version information
  --test           Tests to see if backup requirements have been satisfied.
                   Running in --test mode does not perform a backup.
  --config=STRING  Use Configuration File named STRING.
                   (Default=$ConfigFile)
  --timestamp=REAL Manually override all host timestamps with epoc+REAL.
  --host=STRING    Backup only host STRING. Host must exist in config file.
  --rotate         Eject current media and prepare config for next media
                   upon completion of this backup. Used with --test will
                   rotate media immediately without doing a backup.

  Refer to the README for more verbose help.

EOF

  exit;
} # ---------------------- End ShowSyntax

# ------------------------------
# BackupNotOkToRun -- subroutine
# ------------------------------
# Description: Verifies that all is ok for a backup to proceed.
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns: 0 if ok to proceed with a backup
# Syntax: &BackupNotOkToRun
# Todo: Check System Load and for Stale Mounts

sub BackupNotOkToRun{
  my($Return);
  my ($temp);

  # Test General Backup Requirements
  if ( $> != 0 ) {
    print("ERROR: $ExecutableName must be run as root!\nAborting backup!\n");
    return 1;
  }
  # Test Connectivity and ping program
  if ( &PingCheck($IP{$::Host}) ) {
    print("ERROR: Cannot Ping host $::Host\nAborting backup!\n");
    return 1;
  }
  # Test LogDir
  if ( ! -e $BackupLogDir ) {
    print("Warning: BackupLogDir $BackupLogDir does not exist.\n",
           "Attempting to Create Directory ...\n");
    if ( (! (mkdir($BackupLogDir, 0640)) &&  (! -e $BackupLogDir)) ) {
       print ("ERROR: Creation of $BackupLogDir Failed!\n");
       return 1;
    } else {
       print ("Directory $BackupLogDir successfully created!\n");
    }
  }

  # Test BackupProg program

  # Cleanout any options from program
  $temp = $BackupProg;
  while ( $temp =~ /^([^' -']+) \-.*/ ) {
     $temp = $1;
  }

  if ( ! -e "$temp") {
    print ("ERROR: BackupProg: $temp not found!\n");
    return 1;
  }
  $temp = '';

  # Test BackupProgDebug program
  $temp = $BackupProgDebug;
  while ( $temp =~ /^([^' -']+) \-.*/ ) {
     $temp = $1;
  }

  if ( ! -e "$temp") {
    print ("ERROR: BackupProgDebug: $temp not found!\n");
    return 1;
  }
  $temp = '';

  # Test MailProg program
  if ( ! -e $MailProg ) {
    print ("ERROR: MailProg program $MailProg not found\n");
    return 1;
  }
  # Test Cat program
  if ( ! -e $Cat ) {
    print ("ERROR: Cat program $Cat not found\n");
    return 1;
  }
  # Test Cut program
  if ( ! -e $Cut ) {
    print ("ERROR: Cut program $Cut not found\n");
    return 1;
  }
  # Test Echo program
  if ( ! -e $Echo ) {
    print ("ERROR: Echo program $Echo not found\n");
    return 1;
  }
  # Test MV program
  if ( ! -e $MV ) {
    print ("ERROR: MV program $MV not found\n");
    return 1;
  }
  # Test TR program
  if ( ! -e $TR ) {
    print ("ERROR: TR program $TR not found\n");
    return 1;
  }
  # Test Tail program    
  if ( ! -e $Tail ) {
    print ("ERROR: Tail program $Tail not found\n");
    return 1;
  }

  #Check DiskMode requirements
  if ( $Mode{$::Host} eq 'Disk' ) {
    if (! -e $Destination{$::Host}) {
      print("ERROR: No backup destination directory! ($Destination{$::Host})\nAborting backup!\n");
      return 1;
    }
    $temp = $MediaUnit{$::Host};
    $temp =~ tr/a-z/A-Z/;
    if (! -e "$Destination{$::Host}/MEDIAUNIT=$temp"){
      $Debug && print "Warning: Cannot find MEDIAUNIT tag. Checking mountpoint.\n";
      $Return = 0xffff & system ("$Mount | $Grep \"$Destination{$::Host}\" >/dev/null");
      if ($Return != 0) {
        $Debug && print "Warning: $Destination{$::Host} not mounted. Trying to mount.\n";
        $Return = 0xffff & system ("$Mount $Destination{$::Host} >/dev/null");
        if ($Return != 0){
          $Debug && print "Error: Mount failed! Trying a second time.\n";
          sleep(1); #give the disk a second before trying again.
          $Return = 0xffff & system ("$Mount $Destination{$::Host} >/dev/null");
          if ($Return != 0) {
            print "Failure: Cannot mount $Destination{$::Host}. RC=$Return.\nAborting Backup!\n";
            return 1;
          } else {
            $Debug && print "Mount of $Destination{$::Host} successful.\n";
            if (! -e "$Destination{$::Host}/MEDIAUNIT=$temp"){
              print "Error: Disk not prepped, or wrong disk inserted (Want $temp).\nAborting Backup!\n";
              return 1;
            }
          }
        } else {
          $Debug && print "Mount of $Destination{$::Host} successful.\n";
          if (! -e "$Destination{$::Host}/MEDIAUNIT=$temp"){
            print "Error: Disk not prepped, or wrong disk inserted (Want $temp).\nAborting Backup!\n";
            return 1;
          }
        }
      } else {
        print("ERROR: Encountered an unnexpected disk. Please insert $temp.\nAborting backup!\n");
        return 1;
      }
    }
#   else {
      # found proper disk mounted and previously prepped.
      # check if it is supposed to be AutoPrepped
      $Return = &PrepDisk;
      if ($Return != 0) {
        return 1; #backup conditions not met, disk cannot be prepped.
      }
#   }
  } #End DiskMode requirements

  #Check FilesystemMode requirements
  elsif ( $Mode{$::Host} eq 'Filesystem' ) {
    if (! -e $Destination{$::Host}) {
      print("ERROR: No backup destination directory! ($Destination{$::Host})\nAborting backup!\n");
      return 1;
    }
    $temp = $MediaUnit{$::Host};
    $temp =~ tr/a-z/A-Z/;
    if (! -e "$Destination{$::Host}/MEDIAUNIT=$temp") {
      print("ERROR: Missing or incorrect MEDIAUNIT tag in destination directory.\n");
      return 1;
    }
  } #End FilesystemMode requirements

  #Check TapeMode requirements
  elsif ( $Mode{$::Host} eq 'Tape' ) {
    # Check the device exists
    if (! -e $Destination{$::Host}) {
      print("ERROR: No backup device! ($Destination{$::Host})\nAborting backup!\n");
      return 1;
    }

    # Check for mt
    if (! -e "$MT" ) {
      print("ERROR: MT doesn't exist!\nmt=$MT\n\nAborting backup!\n");
      return 1;
    }

    # Check tape drive and mt command work.
    $Return = 0xffff & system( "$MT -f $Destination{$::Host} stat >/dev/null");
    if( $Return == 0 ) {}
    elsif ($Return == 0xff00 ) {
       print("ERROR: MT command failed!\n\nAborting backup!\n");
       return 1;
    } elsif ($Return > 0x80) {
       $Return >>= 8;
       print("ERROR: MT ran with non-zero exit status $Return\n\nAborting backup!\n");
       return 1;
    } else {
      print "ERROR: MT ran with";
      if ($Return & 0x80){
        $Return &= ~0x80;
        print "coredump from ";
      }
      print "signal $Return\n\nAborting backup!\n";
      return 1;
    }
  } # End TapeMode requirements

  # Check backup stamp and warn the administrator if bogus
  if ( $Timestamp{$::Host} > time ) {
    print "WARNING *** Timestamp for host $::Host is in the future! ***\n *** Probably nothing will be backed up! ***\n";
  }
  elsif ( ( $Timestamp{$::Host} > 0 ) && ( $Timestamp{$::Host} < (time - 1536000) ) ) {
    print "WARNING *** Timestamp for host $::Host is more than a year old! ***\n        *** Probably everything will be backed up! ***\n";
  }
  return 0;
} # ---------------------------- BackupNotOkToRun

# -----------------------
# PingCheck -- subroutine
# -----------------------
# Description: Check to see if a host is up to backup purposes.
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads: $Ping
# Returns: 0 if the host responds as ok.
# Syntax: &PingCheck($::Host);
# Todo:

sub PingCheck {
  my($IPAddress)=$_[0];
  my($Return);
  my($temp);
  
  #Test that Ping command without options exists
  $temp = $Ping;
  while ( $temp =~ /^([^' -']+) \-.*/ ) {
     $temp = $1;
  }

  if ( ! -e "$temp") {
    print ("ERROR: Ping command: $temp not found!\n");
    return 1;
  }
  $temp = '';

  $Return = 0xffff & system("$Ping $IPAddress >/dev/null");
  if( $Return == 0 ) { }
  elsif ($Return == 0xff00 ) {
     print("ERROR: Ping command ($Ping) failed for host $::Host ($IPAddress)!\n");
     return 1;
  } elsif ($Return > 0x80) {
     $Return >>= 8;
     print("ERROR: Ping ran with non-zero exit status $Return\n",
           "Ping=$Ping\nHost=$::Host\nAborting backup!\n");
     return 1;
  } else {
    print "ERROR: Ping ran with";
    if ($Return & 0x80){
      $Return &= ~0x80;
      print "coredump from ";
    }
    print "signal $Return\nPing=$Ping\nHost=$::Host\nAborting backup!\n";
    return 1;
  }
  return 0;
} # --------------------- PingCheck


# ---------------------------
# PrepareBackup -- subroutine
# ---------------------------
# Description: Prepares for a backup
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed: $BackupDay,$BackupErrorFile
# Global Reads:
# Returns:
# Syntax: &PrepareBackup
# Todo:

sub PrepareBackup{
  my ($ToBackup); # Temporary Backup Variable
  my ($StampCTime);
  my ($CTime);

  # Get rid of the old errors log
  if (-e "$BackupErrorFile") {unlink "$BackupErrorFile";}

  open (BACKTEMP, ">$ToBackupTempFile") || die "Couldn\'t Create ToBackupTempFile: $ToBackupTempFile\n";
  if ( !$::opt_fullbackup && $Timestamp{$::Host} > 0 ) { # Prepare Incremental Backup Build
  $StampCTime=$Timestamp{$::Host};

    $Debug && print "Building for incremental backup from $Timestamp{$::Host}.\n";
    #Get the file list and parse it one by one
    foreach $ToBackup (@{$Paths{$::Host}}) {
      $Debug && print("Indexing:$ToBackup\n");
      if ( -d "$ToBackup" && (! -l "$ToBackup") ) {
        &ProcessDir("$ToBackup","$StampCTime");
      }
      elsif ( ((undef,undef,undef,undef,undef,undef,undef,undef,undef,undef,$CTime) = stat("$ToBackup")) && ($CTime >= $StampCTime) ) {
        print BACKTEMP "$ToBackup\n";
      }
    }
  } # End Incremental Backup Build
  else { # Do Full Backup Build
    $Debug && print "Building for full backup.\n";
    #Get the file list and parse it one by one
    foreach $ToBackup (@{$Paths{$::Host}}) {
      $Debug && print("Indexing:$ToBackup\n");
      if ( -d "$ToBackup" && (! -l "$ToBackup") ) {
        &ProcessDir("$ToBackup",0);
      } else {
        print BACKTEMP "$ToBackup\n";
      }
    }
  } # End Full Backup Build
  close(BACKTEMP);
} # ------------------- End PrepareBackup

# ------------------------
# ProcessDir -- subroutine
# ------------------------
# Description: Recursive Function to Process Directories
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed: Prints to BACKTEMP filehandle
# Global Reads:
# Returns:
# Syntax: &ProcessDir($Directory,$StampCTIME);
# Todo: Document
# Assumptions: This procedure assumes it is given a directory. For example,
# it does not check if argument given is a link vs. a directory. This must
# be handled before ProcessDir is called.
# Note: This code does NOT follow links

# Test Code Chunk:
# open ( BACKTEMP, ">/tmp/ProcessDirTest.$$");
# March 15th 1997 14:28 = 858454120
# &ProcessDir("/users/rhardy", 858454120);
# close BACKTEMP;
# exit 0;

sub ProcessDir {
  my($Path)=$_[0];
  my($CTime)=$_[1];
  my($TempCTime);
  my(@SubDirs)=();
  my(@NonDirs)=();
  my($DirCount)=0;
  my($SubDebug)=0;
  study "$Path";

  # Exclude Anything in or below a directory with a .nobackup file
  if ( -e "$Path/\.nobackup" ) { return; }
  # Exclude ~/.netscape/cache
  if ( $Path =~ /\/\.netscape\/cache$/ ) { return; }
  # Exclude ~/*.(lha|zip|t?gz|Z|tar)
  if ( $Path =~ /^.*\/(?:users|home)\/[^\/]+\/[^\/]\.(?:lha|zip|t?gz|Z|tar)$/i ) { return; }
  # Exclude ~/(excludes|storage|dloads)
  if ( $Path =~ /^.*\/(?:users|home)\/[^\/]+\/(?:storage|excludes|dloads)$/ ) { return; }
  # Exclude ~/temp & ~/tmp
  if ( $Path =~ /^.*\/(?:users|home)\/[^\/]+\/te?mp$/ ) { return; }

  opendir(TOPROCESS, "$Path");
  foreach (grep !/^\.\.?$/, readdir TOPROCESS) {
    if (-d "$Path/$_" && (! -l "$Path/$_") ) { push @SubDirs, "$Path/$_";}
    else { push @NonDirs, "$Path/$_"; }
    ++$DirCount;
  }
  if ( $DirCount == 0 ) {
    (undef,undef,undef,undef,undef,undef,undef,undef,undef,undef,$TempCTime) = stat("$Path");
    if ( $TempCTime >= $CTime ) {
      print BACKTEMP "$Path\n";
      ($Debug > 1) && print "Adding $Path\n";
    }
    return;
  }

  if ( $CTime == 0 ) {
    foreach (@NonDirs) {
      print BACKTEMP "$_\n";
      ($Debug > 1) && print "Adding $_\n";
    }
  } else { # Must check CTimes
    foreach (@NonDirs) {
      (undef,undef,undef,undef,undef,undef,undef,undef,undef,undef,$TempCTime) = stat("$_");

      # If it is a broken link it will have an empty CTime. This always backs up broken links!
      if ( (! $TempCTime) && (-l "$_") ) { 
        print BACKTEMP "$_\n"; 
        next; 
        ($Debug > 1) && print "Adding $_\n";
      }

      if ( ! $TempCTime ) { # Always backup files non-links with Empty CTimes
        print "TempCTTime Empty for $_\n"; 
        print BACKTEMP "$_\n";
        ($Debug > 1) && print "Adding $_\n";
        next;
      }

      if ( $TempCTime >= $CTime ) {
        print BACKTEMP "$_\n";
        ($Debug > 1) && print "Adding $_\n";
      }
    }
  }
  undef @NonDirs;
  foreach (@SubDirs) {
    &ProcessDir($_,$CTime);
  }
  return;
}
# ---------------------- End ProcessDir

# ---------------------------
# PerformBackup -- subroutine
# ---------------------------
# Description: Performs the Actual Backup
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns: BackupStatus
# Syntax: &PerformBackup
# Todo: Add BackupStamp for DiskMode

sub PerformBackup{
  my($Temp);
  my($SeekTo);
  my($Return)=0;
  my($Filename)='';
  my($Usage);

  if ( $::opt_fullbackup || $Timestamp{$::Host} == 0 ) { #Full Backup
    $Debug && print ("Performing Full Backup!\n");

    if( ($Mode{$::Host} eq 'Tape')) {
      if ($::NeedTapeRewind == 1) {
        $::NeedTapeRewind = 0;
        $Return = 0xffff & system( "$MT rewind >/dev/null");
        if( $Return == 0 ) {}
        elsif ($Return == 0xff00 ) {
           print("ERROR: MT rewind command failed!\n\nAborting backup!\n");
           exit 1;
        } elsif ($Return > 0x80) {
           $Return >>= 8;
           print("ERROR: MT rewind ran with non-zero exit status $Return\n\nAborting backup!\n");
           exit 1;
        } else {
          print "ERROR: MT rewind ran with";
          if ($Return & 0x80){
            $Return &= ~0x80;
            print "coredump from ";
          }
          print "signal $Return\n\nAborting backup!\n";
          exit 1;
        }
      }
      $Debug && print "Current Position of Tape:\n";
      $Debug && ($Return = 0xffff & system( "$MT tell"));
      if( $Return == 0 ) {}
      elsif ($Return == 0xff00 ) {
         print("ERROR: MT tell command failed!\n\nAborting backup!\n");
         exit 1;
      } elsif ($Return > 0x80) {
         $Return >>= 8;
         print("ERROR: MT tell ran with non-zero exit status $Return\n\nAborting backup!\n");
         exit 1;
      } else {
        print "ERROR: MT tell ran with";
        if ($Return & 0x80){
          $Return &= ~0x80;
          print "coredump from ";
        }
        print "signal $Return\n\nAborting backup!\n";
        exit 1;
      }
    } # End Tape Mode Section
    # Special Considerations for DiskMode full backup here
    # Special Considerations for FilesystemMode full backup here
  } # End Full Backup Specific Section

  if ( !$::opt_fullbackup && $Timestamp{$::Host} ) { #Incremental Backup
    $Debug && print ("Performing Incremental Backup from epoc+$Timestamp{$::Host}!\n");

    # In Tape Mode $Usage{$::Host} contains the last block used on the tape.
    # In Disk and Filesystem Mode $Usage{$::Host} will contain the remaining disk space as of last backup.

    $Temp=$IniConfig->val($::Host,'MediaSet');
    $Temp=$IniConfig->val("MediaSet=$Temp",'CurrentUnit');
    $Temp=$IniConfig->val("MediaUnit=$Temp",'Usage');

    if ( ($Mode{$::Host} eq 'Tape')) {
      if ( $Temp && $Temp =~ /^ *(\d+) */) {
        $SeekTo = $1;
        open MTSpool, "$MT tell |" or die "Can't Fork: $!";
        local $SIG{PIPE} = sub {die "$MT tell caused a broken pipe !\nAborting Backup!\n"};
        $Temp = <MTSpool>;
        close MTSpool;
        if ( $Temp =~ /^At block (\d+)\./) {
           if ( $1 != $SeekTo ) {
              print "Seeking Tape to Block $SeekTo.\n";
              print "Tape now $Temp\n";
              $Return = 0xffff & system( "$MT seek $SeekTo");
              if( $Return == 0 ) {}
              elsif ($Return == 0xff00 ) {
                 print("ERROR: MT seek command failed!\n\nAborting backup!\n");
                 exit 1;
              } elsif ($Return > 0x80) {
                 $Return >>= 8;
                 print("ERROR: MT seek ran with non-zero exit status $Return\n\nAborting backup!\n");
                 exit 1;
              } else {
                print "ERROR: MT seek ran with";
                if ($Return & 0x80){
                  $Return &= ~0x80;
                  print "coredump from ";
                }
                print "signal $Return\n\nAborting backup!\n";
                exit 1;
              }
           } else {
              $Debug && print "Tape already seeked to $SeekTo!\n"
           }
        } else {
          die "Bad MT response! Reponse=$Temp\nAborting Backup!\n";
        }
      } else {
        if ( ! $SeekTo ) { $SeekTo = "undefined" }
        if ( ! $Temp ) { $Temp = "undefined" }
        print("Error bad TimeStamp!\nSeek= $SeekTo\nTemp=$Temp\n");
        exit 1;
      }
    } # End Tape Mode
  } # End Incremental Backup Specific Section

  if ( ! -e "$BackupErrorFile" ) { system("touch ${BackupErrorFile}.$::Host"); }

  if ($Mode{$::Host} eq 'Tape') {
    $Debug && ($Return = 0xffff & system("$Cat $ToBackupTempFile | $BackupProgDebug $Destination{$::Host} 2>> $BackupErrorFile.$::Host"));
    ( ! $Debug ) && ($Return = 0xffff & system("$Cat $ToBackupTempFile | $BackupProg $Destination{$::Host} 2>> $BackupErrorFile.$::Host"));
  }
  elsif ($Mode{$::Host} eq 'Disk' || $Mode{$::Host} eq 'Filesystem') {
    #Filenames in Disk and Filesystem mode must be adapted for uniqueness
    $Filename=$::Host."_".$BackupDay;
    $Debug && print "Command: $Umask 0277; $Cat $ToBackupTempFile | $BackupProgDebug $Destination{$::Host}/$Filename 2>> $BackupErrorFile.$::Host";
    $Debug && ($Return = 0xffff & system("$Umask 0277; $Cat $ToBackupTempFile | $BackupProgDebug $Destination{$::Host}/$Filename 2>> $BackupErrorFile.$::Host"));
    ( ! $Debug ) && ($Return = 0xffff & system("$Umask 0277; $Cat $ToBackupTempFile | $BackupProg $Destination{$::Host}/$Filename 2>> $BackupErrorFile.$::Host"));
  }

  if( $Return == 0 ) {} # no problem
  elsif ($Return == 0xff00 ) {
     print("ERROR: cat ($Cat) to backup command ($BackupProg) failed!\n\nAborting backup!\n");
     exit 1;
  } elsif ($Return > 0x80) {
     $Return >>= 8;
     print("ERROR: cat ($Cat) to backup command ($BackupProg) ran with non-zero exit status $Return\n\nAborting backup!\n");
     exit 1;
  } else {
    print "ERROR: cat ($Cat) to backup command ($BackupProg) ran with";
    if ($Return & 0x80){
      $Return &= ~0x80;
      print "coredump from ";
    }
    print "signal $Return\n\nAborting backup!\n";
    exit 1;
  }

  system("$Echo Backup_Completed for host $::Host>> $BackupErrorFile.$::Host");
  $Debug && print "Backup Complete for host $::Host\n";

  $Debug && print "Updating MediaUnit Usage\n";
  if ( ($Mode{$::Host} eq 'Tape') ) {
    $Debug && print "Command: $MT tell > $BackupTimeStamp\n";
    $Return = 0xffff & system("$MT tell > $BackupTimeStamp");
    if( $Return == 0 ) {
      chomp($Usage = `$Cat $BackupTimeStamp`);
      unlink("$BackupTimeStamp");
      $Debug && print "Usage=$Usage\n";
      $Temp=$IniConfig->val($::Host,'MediaSet');
      $Temp=$IniConfig->val("MediaSet=$Temp",'CurrentUnit');
      $IniConfig->setval("MediaUnit=$Temp",'Usage',$Usage);
      $IniConfig->WriteConfig($ConfigFile);
    }
    elsif ($Return == 0xff00 ) {
       print("ERROR: BackupTimeStamp $MT tell command failed!\n\nAborting backup!\n");
       exit 1;
    } elsif ($Return > 0x80) {
       $Return >>= 8;
       print("ERROR: BackupTimeStamp $MT tell ran with non-zero exit status $Return\n\nAborting backup!\n");

       # This section sometimes leaves an empty timestamp with a complete backup.
       # This should somehow preserve the backup.
       # We should gracefully finish the backup successfully and eject the tape.
       # mt tell fails with an I/O error for some reason
       # > mt tell
       # /dev/tape: I/O error
       # > mt stat
       # SCSI 2 tape drive:
       # File number=-1, block number=-1, partition=0.
       # Tape block size 1024 bytes. Density code 0x15 (EXB-8500 (RLL 45434 bpi)).
       # Soft error count since last status=0
       # General status bits on (1010000):
       #  ONLINE IM_REP_EN

        $Debug && print("Since the writing of the Backup location failed, the next backup will be a _FULL_ backup!\n");
        $IniConfig->setval($::Host,'Timestamp',0); #write a zero timestamp to force full backup
        $IniConfig->WriteConfig($ConfigFile);
        return;
    } else {
      print "ERROR: BackupTimeStamp $MT tell ran with";
      if ($Return & 0x80){
        $Return &= ~0x80;
        print "coredump from ";
      }
      print "signal $Return\n\nAborting backup!\n";
      exit 1;
    }
  } # End Tape Mode usage update

  elsif ( ($Mode{$::Host} eq 'Disk') || ($Mode{$::Host} eq 'Filesystem') ) {
    $Debug && print "Command: $DF $Destination{$::Host} |$Tail -1 |$TR -s ' ' |$Cut -f 4 -d ' ' > $BackupTimeStamp\n";
    $Return = 0xffff & system("$DF $Destination{$::Host} |$Tail -1 |$TR -s ' ' |$Cut -f 4 -d ' ' > $BackupTimeStamp");
    if ($Return == 0) {
      chomp($Usage = `$Cat $BackupTimeStamp`);
      unlink("$BackupTimeStamp");
      $Debug && print "Usage=$Usage\n";
      $Temp=$IniConfig->val($::Host,'MediaSet');
      $Temp=$IniConfig->val("MediaSet=$Temp",'CurrentUnit');
      $IniConfig->setval("MediaUnit=$Temp",'Usage',$Usage);
      $IniConfig->WriteConfig($ConfigFile);
    }
    elsif ($Return == 0xff00 ) {
       print("ERROR: BackupTimeStamp $DF command failed!\n\nAborting backup!\n");
       exit 1;
    } elsif ($Return > 0x80) {
       $Return >>= 8;
       print("ERROR: BackupTimeStamp $DF ran with non-zero exit status $Return\n\nAborting backup!\n");

       print("Since the writing of the DF output failed, the next backup will be a _FULL_ backup!\n");
       $IniConfig->setval($::Host,'Timestamp',0); #write a zero timestamp to force full backup
       $IniConfig->WriteConfig($ConfigFile);
       return;
    } else {
      print "ERROR: BackupTimeStamp $DF ran with";
      if ($Return & 0x80){
        $Return &= ~0x80;
        print "coredump from ";
      }
      print "signal $Return\n\nAborting backup!\n";
      exit 1;
    }
  }
  return;
}
# ------------------------- End PerformBackup

# ---------------------
# Cleanup -- subroutine
# ---------------------
# Description: Does Cleanup Bassed on Status of Backup and Mails out Report
# Calls: sendmail
# Global Vars/Files Created: $BackupLogDir/$BackupDay\.log
# Global Vars/Files Changed: $ToBackupTempFile
# Global Reads: $::opt_tape & ($Mode{$::Host} eq 'Disk')
# Returns:
# Syntax: &Cleanup(BackupStatus);
# Todo:

sub Cleanup{
  my($temp)=0;
  system("$MV $ToBackupTempFile $BackupLogDir/$BackupDay.log.$::Host");

  # Mail off the results

  open (MAILTHIS, "|$MailProg $BackupAdministrator");
  print MAILTHIS "To: $BackupAdministrator\nSubject: $::Host Automated Backup Summary\n\n";
  print MAILTHIS "Note: This message is automatically generated by $ExecutableName\n";
  print MAILTHIS "\nIf any errors occured they should be listed below:\n---\n";
  print MAILTHIS `$Cat $BackupErrorFile.$::Host`;
  print MAILTHIS "---\n(1 block = 1024 bytes)\n";

  $temp = $IniConfig->val("MediaSet=$MediaSet{$::Host}",'CurrentUnit');
  $temp = $IniConfig->val("MediaUnit=$temp",'Usage');
  ($Mode{$::Host} eq 'Tape') && print MAILTHIS 'Current Tape Position ' , $temp , "\n";
  ($Mode{$::Host} eq 'Disk' || $Mode{$::Host} eq 'Filesystem') && print MAILTHIS 'Current Disk Space Free ' , $temp , "\n";
  close(MAILTHIS);
}
# ------------------- End Cleanup


# ------------------------------
# LoadConfig -- subroutine
# ------------------------------
# Description: Reads the config file.
# Calls: IniConf->new,Sections,val
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns:
# Syntax: &LoadConfig
sub LoadConfig {
  my($HostName,$temp,@temphosts);
  $IniConfig = IniConf->new( -file => $ConfigFile );
  if (!defined($IniConfig)) {
    if ( defined(@IniConf::Errors) ) {
      die("Could not read $ConfigFile:\n",@IniConf::Errors,"\n");
    }
    else {
      die("Could not read $ConfigFile: $!\n");
    }
  }
  $Debug && print "Debug: new iniconf object created\n";

  #Read globals from BACKUPCONFIG Section.
  $BackupAdministrator = $IniConfig->val('BACKUPCONFIG','BackupAdministrator');
  $BackupLogDir        = $IniConfig->val('BACKUPCONFIG','BackupLogDir');
  $BackupProg          = $IniConfig->val('BACKUPCONFIG','BackupProg');
  $BackupProgDebug     = $IniConfig->val('BACKUPCONFIG','BackupProgDebug');
  $Cat                 = $IniConfig->val('BACKUPCONFIG','Cat');
  $Cut                 = $IniConfig->val('BACKUPCONFIG','Cut');
  $DF                  = $IniConfig->val('BACKUPCONFIG','DF');
  $Echo                = $IniConfig->val('BACKUPCONFIG','Echo');
  $MT                  = $IniConfig->val('BACKUPCONFIG','MT');
  $MV                  = $IniConfig->val('BACKUPCONFIG','MV');
  $MailProg            = $IniConfig->val('BACKUPCONFIG','MailProg');
  $Ping                = $IniConfig->val('BACKUPCONFIG','Ping');
  $TR                  = $IniConfig->val('BACKUPCONFIG','TR');
  $Tail                = $IniConfig->val('BACKUPCONFIG','Tail');
  $Mount               = $IniConfig->val('BACKUPCONFIG','Mount');
  $Umount              = $IniConfig->val('BACKUPCONFIG','Umount');
  $Grep                = $IniConfig->val('BACKUPCONFIG','Grep');
  $Eject               = $IniConfig->val('BACKUPCONFIG','Eject');
  $Umask               = $IniConfig->val('BACKUPCONFIG','Umask');

  $Debug && print "Debug: going to read per-host configs\n";
  #Read Per-Host configs
  @temphosts = $IniConfig->Sections;
  foreach $HostName (@temphosts) {
    next if ( ($HostName =~ /^BACKUPCONFIG$/i) || ($HostName =~ /=/) );
    push (@HostsInConfig,$HostName);
    $Debug && print "Debug: reading config for $HostName\n";

    $IP{$HostName}          = $IniConfig->val($HostName,'IP');
    $Timestamp{$HostName}   = $IniConfig->val($HostName,'Timestamp');

    $temp= $IniConfig->val($HostName,'MediaSet');
    $MediaSet{$HostName}    = $temp;
    $Mode{$HostName}        = $IniConfig->val("MediaSet=$MediaSet{$HostName}",'Mode');
    $Destination{$HostName} = $IniConfig->val("MediaSet=$MediaSet{$HostName}",'Destination');
    $MediaUnit{$HostName}   = $IniConfig->val("MediaSet=$MediaSet{$HostName}",'CurrentUnit');

    $Paths{$HostName}       = [ $IniConfig->val($HostName,'Paths') ];

    if ($IP{$HostName} !~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/) {
      print "ERROR: Invalid IP address ($IP{$HostName}) for host $HostName.\n";
      exit 1;
    }
    elsif ($1 < 0 or $1 > 255 or $2 < 0 or $2 > 255 or $3 < 0 or $3 > 255 or $4 < 1 or $4 > 254) {
      print "ERROR: Invalid IP address ($IP{$HostName}) for host $HostName.\n";
      exit 1;
    }
    ($Mode{$HostName}) = ($Mode{$HostName} =~ /(Disk|Tape|Filesystem)/i);
    $Mode{$HostName} = ucfirst lc $Mode{$HostName};
    if ( ($Mode{$HostName} ne 'Disk') && ($Mode{$HostName} ne 'Tape') && ($Mode{$HostName} ne 'Filesystem')) {
      print "ERROR: Invalid Mode ($Mode{$HostName}) for MediaSet $temp.\n";
      exit 1;
    }
    $Debug && print "[$HostName]\n";
    $Debug && print "IP=",$IP{$HostName},"\n";
    $Debug && print "Timestamp=",$Timestamp{$HostName},"\n";
    $Debug && print "Mode=",$Mode{$HostName},"\n";
    $Debug && print "Destination=",$Destination{$HostName},"\n";
    $Debug && print "Paths=";
    foreach (@{$Paths{$HostName}}) {
      $Debug && print "$_\n";
    }
    $Debug && print "\n";
  }
}

# ------------------------------
# UpdateConfig -- subroutine
# ------------------------------
# Description: Updates the Backup timestamps in the config file.
# Calls: IniConf->new,Sections,setval
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns:
# Syntax: &UpdateConfig
sub UpdateConfig {
  $IniConfig->setval($::Host,'Timestamp',$StartTime);
  $IniConfig->WriteConfig($ConfigFile);
  $Debug && print "Config file backup timestamp updated for host $::Host.\n";
}

# -------------------------
# RotateMedia -- subroutine
# -------------------------
# Description: Updates the CurrentUnit in the MediaSet=X section of config file.
# Calls: IniConf->val,setval,writeconfig
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns:
# Syntax: &RotateMedia
sub RotateMedia {
  my($temp)='';
  my($host)='';
  my(@sets,@usets)=((),());
  my(@units)=();
  my($cur,$i)=(0,0);
  my($Return)=0;
  my($actions)='';

  foreach $host (@HostsInConfig){
    $temp = $IniConfig->val("$host",'MediaSet');
    push(@sets,$temp);
  }

  #Uniq the array of MediaSets used in this backup
  $Debug && print "Sets: @sets\n";
  @sets=sort(@sets);
  $Debug && print "SortedSets: @sets\n";
  push(@usets,$sets[0]);
  foreach (@sets) {
    if ($_ ne $usets[$#usets]) {
      push(@usets,$_);
    }
  }

  foreach (@usets) {
    $temp = $IniConfig->val("MediaSet=$_",'Mode');
    if ($temp eq 'Filesystem') {
      $Debug && print("Debug: Filesystem mode MediaSet $_ not rotatable.");
    }
    else {
      @units= $IniConfig->val("MediaSet=$_",'Units');
      $Debug && print "Units: @units\n";
      $cur  = $IniConfig->val("MediaSet=$_",'CurrentUnit');
      $Debug && print "CurrentUnit: $cur\n";

      $actions=$actions."[$_][$cur] must be replaced by ";

      foreach $i (0..$#units) {
        if ($units[$i] eq $cur) {
          $cur = $i;
          $Debug && print "Index of Current: $cur\n";
          last;
        }
      }
      $cur=(($cur+1) % ($#units+1));
      $Debug && print "New Index: $cur\n";
      $cur=$units[$cur];
      $Debug && print "New Unit: $cur\n";

      $actions=$actions."[$_][$cur].\n";

      $IniConfig->setval("MediaSet=$_",'CurrentUnit',$cur);
      $Debug && print "Updated MediaSet $_\'s CurrentUnit to $cur.\n";
      $IniConfig->setval("MediaSet=$_",'NeedsPrep',"1");
      $Debug && print "Updated MediaSet $_\'s NeedsPrep to 1.\n";

      $temp = $IniConfig->val("MediaSet=$_",'Device');
      $Return = 0xffff & system("$Umount $temp");
      if ($Return != 0) {
        print "Error: failure unmounting $temp during rotation\n";
      } else {
        $Return = 0xffff & system("$Eject $temp");
        if ($Return != 0) {
          print "Error: failure ejecting $temp during rotation\n";
        }
      }
    }
  }
  $IniConfig->WriteConfig($ConfigFile);
  $Debug && print "Wrote updated config file.\n";

  if ($actions != '') {
    open (MAILTHIS, "|$MailProg $BackupAdministrator");
    print MAILTHIS "To: $BackupAdministrator\nSubject: Automated Backup Media Rotated\n\n";
    print MAILTHIS "Note: This message is automatically generated by $ExecutableName\n\n";
    print MAILTHIS "The following actions need to be taken:\n---\n";
    print MAILTHIS $actions;
    print MAILTHIS "---\n";
    close(MAILTHIS);
  }
}

# ----------------------
# PrepDisk -- subroutine
# ----------------------
# Description: Prepares the disk for backup:
#              -does unit need prepping?
#              -format unit device
#              -touch MEDIAUNIT=X
#              -chmod 000 MEDIAUNIT=X
# Calls: IniConf->val,setval,writeconfig
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns: 1 for failure, 0 for success
# Syntax: &PrepDisk
sub PrepDisk {
  my($Return)=0;
  my($Formatter,$Device)=('','');
  my($temp)='';

  $Debug && print "Host: $::Host, MediaSet: $MediaSet{$::Host}\n";
  $temp = $IniConfig->val("MediaSet=$MediaSet{$::Host}",'NeedsPrep');
  $Debug && print "Disk Needs Prep? = $temp\n";
  if ($temp eq "1") {
    $Debug && print "Going to prep disk.\n";
    $Debug && print "Going to unmount disk.\n";
    $Return = 0xffff & system("umount $Destination{$::Host} >/dev/null");
    if ($Return != 0) {
      print "Error: Cannot unmount disk. Unable to prep.\nAborting Backup!\n";
      return 1;
    } else {
      $Formatter = $IniConfig->val("MediaSet=$MediaSet{$::Host}",'Formatter');
      $Device = $IniConfig->val("MediaSet=$MediaSet{$::Host}",'Device');
      $Debug && print "Going to format disk.\n";
      $Return = 0xffff & system("$Formatter $Device >/dev/null 2>/dev/null");
      if ($Return != 0) {
        print "Error: Formatter failed ($Formatter $Device).\nAborting Backup!\n";
        return 1;
      } else { #remount
        $Debug && print "Going to remount the disk.\n";
        $Return = 0xffff & system ("$Mount $Destination{$::Host} >/dev/null");
        if ($Return != 0){
          $Debug && print "Error: Mount failed! Trying a second time.\n";
          sleep(1); #give the disk a second before trying again.
          $Return = 0xffff & system ("$Mount $Destination{$::Host} >/dev/null");
          if ($Return != 0) {
            print "Failure: Cannot mount $Destination{$::Host}. RC=$Return.\nAborting Backup!\n";
            return 1;
          } else {
            $Debug && print "Mount of $Destination{$::Host} successful.\n";
          }
        } else {
          $Debug && print "Mount of $Destination{$::Host} successful.\n";
        }
        #done remount
        $temp = $IniConfig->val("MediaSet=$MediaSet{$::Host}",'CurrentUnit');
        $temp = uc $temp;
        $Debug && print "Going to touch MEDIAUNIT tag.\n";
        $Return = 0xfff & system("$Echo >$Destination{$::Host}/MEDIAUNIT=$temp");
        if ($Return != 0) {
          print "Error: Unable to create MEDIAUNIT tag.\nAborting Backup!\n";
          return 1;
        } else {
          $Debug && print "Going to chmod 000 MEDIAUNIT tag.\n";
          $Return = chmod 0000, "$Destination{$::Host}/MEDIAUNIT=$temp";
          if ($Return != 1) { #chmod returns how many files were changed
            print "Error: Unable to chmod 000 the MEDIAUNIT tag.\nAborting Backup!\n";
            return 1;
          }
        }
      }
    }
    $Debug && print "Resetting NeedsPrep flag for $MediaSet{$::Host}.\n";
    $IniConfig->setval("MediaSet=$MediaSet{$::Host}",'NeedsPrep',"0");
    $Debug && print "Writing config file.\n";
    $IniConfig->WriteConfig($ConfigFile);
    return 0; # prep sucessful.
  } else {
    $Debug && print "Disk does need not need prepping.\n";
    return 0; # prep not needed.
  }
}
