#!/usr/bin/perl
# -------------------------------------------------------
# $Header: /home/cvs/dobackup/dobackup.pl,v 4.26 2003/10/05 16:29:56 imorgan Exp $
# -------------------------------------------------------
#
# Copyright 1997-2002 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/opensource/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>
#

#Becuase we use the new "our" function for global declaration.
require v5.6.0;

# External Calls:
use Config::IniFiles;
use strict;
use warnings;
use Getopt::Long; # For easy command line parsing
Getopt::Long::Configure ("bundling"); # Allow Commandline option bundling:
# 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 global variables using the "our()" funtion.

our($BackupDay);
our($BackupAdministrator); $BackupAdministrator = 'root@localhost'; #overriden by config file
our($ConfigFile); $ConfigFile = '/etc/dobackup.conf';       #overriden by command line arg
our($BackupLogDir); $BackupLogDir = '/var/log/backup';        #overriden by config file
our($BackupTimeStamp); $BackupTimeStamp = '/var/log/backup/LastBackupTimeStamp';
our($SingleHostOverride); $SingleHostOverride = '';
our($TimeStampOverride);
our($StartTime);

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

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

# ------------- Config File Variables
our(@HostsInConfig);
our(@HostsToProcess);
our(%IP);
our(%IncrMode);
our(%Timestamp);
our(%TimestampFull);
our(%Mode);
our(%Destination);
our(%MediaUnit);
our(%MediaSet);
our(%Paths);
our(%Excludes);
our(%Auto);

$StartTime=time; # This is the current timestamp in seconds from the EPOC.

# Lets us get only 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);

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

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

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

# ------- Arguments from here to End Defaults
our($opt_test, $opt_fullbackup, $opt_rotate, $opt_prepdisk);
# --------------- End Defaults

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

#Pre-backup initialization functions

# Deal with Commandline Argument processing
GetOptions ("test" => \$opt_test,
            "fullbackup" => \$opt_fullbackup,
            "rotate" => \$opt_rotate, # says to eject media & update config file for next media after this backup
            "prepdisk" => \$opt_prepdisk, # says to prep the media before this backup
            "host=s" => sub{ $SingleHostOverride = $_[1]; $Debug && print 'host=',$SingleHostOverride,"\n"; },
            "timestamp=i" => sub{ $TimeStampOverride = $_[1]; $Debug && print 'timestamp=',$TimeStampOverride,"\n"; },
            "config=s" => sub{ $ConfigFile = $_[1]; $Debug && print 'config=',$ConfigFile,"\n"; },
            "help|h|version|V" => \&ShowSyntax,
            "debug|d:i" => sub{ $Debug=$_[1]; ($Debug == 0) && ($Debug = 1); } ) 
    or &ShowSyntax;

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

#Per-Host backup functions

# Remove hosts that shouldn't be auto-run
foreach (@HostsInConfig) {
  if ($Auto{$_} eq '1') {
    push(@HostsToProcess,$_);
  } else {
    $Debug && print "Skipping host $_ (Auto=0).\n";
  }
}

if ($SingleHostOverride ne '') {
  @HostsToProcess = ();
  push(@HostsToProcess,$SingleHostOverride);
}

foreach $Host (@HostsToProcess) {
  $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 -----------------------------------------

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

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

sub ShowSyntax {
  my($temp)=('');

  print "\nUsage: $ExecutableName <options>\n";
  $temp = '$Revision: 4.26 $';
  ($temp) = ( $temp =~ /^\$Revision\:\ (.*?)\ \$$/ );
  print 'Version: ',"$temp\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 [1-65534] )
  --debug INT        (If INT is omitted a verbosity level of 1 is assumed)

  -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 LONG Manually override all host timestamps with epoc+LONG.

  --fullbackup     Shortcut form of '--timestamp 0', forces a complete
                   non-incremental backup.

  --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 =~ /^\s*(\S+).*$/ );

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

  # Test BackupProgDebug program
  ($temp) = ($BackupProgDebug =~ /^\s*(\S+).*$/ );

  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;
  }
  # Test Grep program
  if ( ! -e $Grep ) {
    print ("ERROR: Grep program $Grep not found\n");
    return 1;
  }
  # Test LS program
  ($temp) = ($LS =~ /^\s*(\S+).*$/ );
  
  if ( ! -e "$temp") {
    print ("ERROR: LS program $temp not found\n");
    return 1;
  }
  # Test DF program
  ($temp) = ($DF =~ /^\s*(\S+).*$/ );
  if ( ! -e $temp ) {
    print ("ERROR: DF program $temp 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: Backup destination directory ($Destination{$Host}) does not exist!\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. Expecting \"MEDIAUNIT=$temp\".\n");
      return 1;
    }
  } #End FilesystemMode requirements

  #Check TapeMode requirements
  elsif ( $Mode{$Host} eq 'Tape' ) {
    # Check the device exists
    if (! -e $Destination{$Host}) {
      print("ERROR: Backup device ($Destination{$Host}) does not exist! \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";
  binmode ( BACKTEMP, ":raw" );
  if ( (!$opt_fullbackup && $Timestamp{$Host} > 0) or ($TimeStampOverride && $TimeStampOverride > 0) ) { # Prepare Incremental Backup Build
    if ( $TimeStampOverride && $TimeStampOverride > 0 ) {
      $StampCTime=$TimeStampOverride;
    } else {
      if ( $IncrMode{$Host} eq 'Timewarp' ) {
        $StampCTime=$TimestampFull{$Host};
      } else {
        $StampCTime=$Timestamp{$Host};
      }
    }

    $Debug && print "Building for incremental backup from $StampCTime.\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;
  my($FileOwner,$DirOwner)=(0,0);
  my($pat)=('');
  study "$Path";

  if ( -f "$Path/\.nobackup" ) {
    (undef,undef,undef,undef,$FileOwner,undef,undef,undef,undef,undef,undef,undef,undef) = stat("$Path/\.nobackup");
    (undef,undef,undef,undef,$DirOwner,undef,undef,undef,undef,undef,undef,undef,undef) = stat("$Path");

    # Only honour .nobackup file if it's owned by the same user as the
    # directory it's in, or it's owned by root.
    if ( $FileOwner == $DirOwner || $FileOwner == 0 ) {
      # Exclude Anything in or below a directory with a zero-byte .nobackup file
      if ( -z "$Path/\.nobackup") {
        $Debug && print "Excluding all of $Path\n";
        return;
      }
      # Add excludes from file if size is non-zero
      elsif ( -s "$Path/\.nobackup") {

        # Automatically exclude itself
        push(@{$Excludes{$Host}},"^${Path}/\.nobackup\$");

        open('excludes',"$Path/\.nobackup");
        while (<excludes>) {
          chomp();
          # Filter out commented and blank lines
          if ( $_ !~ /^\s*\#/ && $_ !~ /^\s*$/ ) {
            # Filter out anything with a leading path ( "/" or "../" )
            if ( $_ !~ /^\s*\// && $_ !~ /^\s*\.\.\// ) {
              $Debug && print "Appending $Path/$_ to excludes.\n";
              push(@{$Excludes{$Host}},"^${Path}/$_\$");
            } else {
              print "*** Bogus path $_ in $Path/\.nobackup. Ignored.\n";
            }
          }
        }
        close('excludes');
      }
    } else {
      print "*** Bogus ownership on $Path/\.nobackup (File=$FileOwner Dir=$DirOwner). Ignored.\n";
    }
  }

  foreach (@{$Excludes{$Host}}) {
    if ( defined ($_) ) { #take care of "empty" array.
      if ( $Path =~ qr/$_/ ) {
        $Debug && print "Excluding $Path (matches $_)\n";
        return;
      } else {
        ($Debug > 2) && print "Skipping $Path (does not match $_)\n";
      }
    }
  }

  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 ) {
LOOP1:    foreach (@NonDirs) {
      foreach $pat (@{$Excludes{$Host}}) {
        if ( defined ($pat) ) {
          if ( $_ =~ qr/$pat/ ) {
            $Debug && print "Excluding $_ (matches $pat)\n";
            next LOOP1;
          } else {
            ($Debug > 2) && print "Skipping $_ (does not match $pat)\n";
          }
        }
      }
      print BACKTEMP "$_\n";
      ($Debug > 1) && print "Adding $_\n";
    }
  } else { # Must check CTimes
LOOP2:    foreach (@NonDirs) {
      (undef,undef,undef,undef,undef,undef,undef,undef,undef,undef,$TempCTime) = stat("$_");

      foreach $pat (@{$Excludes{$Host}}) {
        if ( defined ($pat) ) {
          if ( $_ =~ qr/$pat/ ) {
            $Debug && print "Excluding $_ (matches $pat)\n";
            next LOOP2;
          } else {
            ($Debug > 2) && print "Skipping $_ (does not match $pat)\n";
          }
        }
      }

      # If it is a broken link it will have an empty CTime, so stat the link itself.
      if ( (! $TempCTime) && (-l "$_") ) {
        (undef,undef,undef,undef,undef,undef,undef,undef,undef,undef,$TempCTime) = lstat("$_");
        ($Debug > 2) && print "Checking broken link: $_\n";
      }

      # Would this _ever_ happen?!
      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);
  my($Stamp);

  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 -f $Destination{$Host} 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 -f $Destination{$Host} 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} > 0) or ($TimeStampOverride && $TimeStampOverride > 0) ) { #Incremental Backup
    if ( $TimeStampOverride && $TimeStampOverride > 0 ) {
      $Stamp=$TimeStampOverride;
    } else {
      if ( $IncrMode{$Host} eq 'Timewarp' ) {
        $Stamp=$TimestampFull{$Host};
      } else {
        $Stamp=$Timestamp{$Host};
      }
    }
    $Debug && print ("Performing Incremental Backup from epoc+$Stamp!\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 -f $Destination{$Host} 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 -f $Destination{$Host} 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;
    if ($opt_fullbackup || $Timestamp{$Host} == 0) {
      $Filename .= ".full";
    } else {
      $Filename .= ".incr" if ($IncrMode{$Host} ne 'Timewarp');
      $Filename .= ".warp" if ($IncrMode{$Host} eq 'Timewarp');
    }
    $Filename .= ".debug" if ($Debug);

    $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 (",$Debug?$BackupProgDebug:$BackupProg,") failed!\n\nAborting backup!\n");
    print("Error log follows:\n");
    print(`$Cat $BackupErrorFile.$Host`);
    exit 1;
  } elsif ($Return > 0x80) {
    $Return >>= 8;
    print("ERROR: cat ($Cat) to backup command (",$Debug?$BackupProgDebug:$BackupProg,") ran with non-zero exit status $Return\n\nAborting backup!\n");
    print("Error log follows:\n");
    print(`$Cat $BackupErrorFile.$Host`);
    exit 1;
  } else {
    print "ERROR: cat ($Cat) to backup command (",$Debug?$BackupProgDebug:$BackupProg,") ran with";
    if ($Return & 0x80){
      $Return &= ~0x80;
      print "coredump from ";
    }
    print "signal $Return\n\nAborting backup!\n";
    print("Error log follows:\n");
    print(`$Cat $BackupErrorFile.$Host`);
    exit 1;
  }

  $Debug && 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 -f $Destination{$Host} tell > $BackupTimeStamp\n";
    $Return = 0xffff & system("$MT -f $Destination{$Host} tell > $BackupTimeStamp");
    if( $Return == 0 ) {
      chomp($Usage = `$Cat $BackupTimeStamp`);
      unlink("$BackupTimeStamp");
      ($Usage) = ( $Usage =~ /^At block (\d+)\./ );
      $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 -h $Destination{$Host} |$Tail -1 |$TR -s ' ' |$Cut -f 4 -d ' ' > $BackupTimeStamp\n";
    $Return = 0xffff & system("$DF -h $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: $Mode{$Host}
# 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 ",($Debug?"Debug-mode":"Automated")," Backup Summary\n\n";

  $temp = '$Revision: 4.26 $';
  ($temp) = ( $temp =~ /^\$Revision\:\ (.*?)\ \$$/ );

  print MAILTHIS "This is an automated backup report from $ExecutableName v$temp\n\n";

  $Debug && print MAILTHIS "Note:            This backup was performed in debug mode!\n";
  ($IncrMode{$Host} eq 'Timewarp') && print MAILTHIS "Note:            This backup was performed in Timewarp mode!\n";

  if ( ($Mode{$Host} eq 'Disk') || ($Mode{$Host} eq 'Filesystem') ) {
    $temp = $Destination{$Host}.'/'.$Host.'_'.$BackupDay;
    if ($opt_fullbackup || $Timestamp{$Host} == 0) {
      $temp .= ".full";
    } else {
      $temp .= ".incr" if ($IncrMode{$Host} ne 'Timewarp');
      $temp .= ".warp" if ($IncrMode{$Host} eq 'Timewarp');
    }
    $temp .= ".debug" if ($Debug);

    print MAILTHIS "File:            $temp\n";
    $temp = `$LS -s -h $temp`;
    ($temp) = ( $temp =~ /^\s*(.*?)\s.*$/ );
    $temp =~ s/\b(\d+)\w*[kK]\b/$1 KiB/g;
    $temp =~ s/\b(\d+)\w*M\b/$1 MiB/g;
    $temp =~ s/\b(\d+)\w*G\b/$1 GiB/g;

    print MAILTHIS "Size:            $temp\n";
    $Debug && print "Size: $temp\n";
  }
  elsif ($Mode{$Host} eq 'Tape') {
    print MAILTHIS "Device:          $Destination{$Host}\n";
  }

  print MAILTHIS "Manifest:        $BackupLogDir/$BackupDay.log.$Host\n";

  $Debug && print "Error file: ${BackupErrorFile}.$Host (", -s "${BackupErrorFile}.$Host", ")\n";
  if ( -e "${BackupErrorFile}.$Host" ) {
    $Debug && print "Error file exists. Yay!\n";
    if ( -s "${BackupErrorFile}.$Host" ) {
      print MAILTHIS "Error Log:       ${BackupErrorFile}.$Host\n";
      print MAILTHIS "\nErrors and/or Debug output:\n---\n";
      print MAILTHIS `$Cat ${BackupErrorFile}.$Host`;
      print MAILTHIS "---\n\n";
    }
  }

  $temp = $IniConfig->val("MediaSet=$MediaSet{$Host}",'CurrentUnit');
  print MAILTHIS "Current Unit:    [$MediaSet{$Host}][$temp]\n";
  $temp = $IniConfig->val("MediaUnit=$temp",'Usage');
  $temp =~ s/\b(\d+)\w*[kK]\b/$1 KiB/g;
  $temp =~ s/\b(\d+)\w*M\b/$1 MiB/g;
  $temp =~ s/\b(\d+)\w*G\b/$1 GiB/g;
  ($Mode{$Host} eq 'Tape') && print MAILTHIS 'Current Tape Position: ' , $temp , "\n";
  ($Mode{$Host} eq 'Disk' || $Mode{$Host} eq 'Filesystem') && print MAILTHIS 'Space Remaining: ' , $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 = Config::IniFiles->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');
  $LS                  = $IniConfig->val('BACKUPCONFIG','LS');

  $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";

    $Auto{$HostName}        = $IniConfig->val($HostName,'Auto');
    # Backward-compat: if missing, assume true.
    if ( ! defined($Auto{$HostName}) ) {
      $Auto{$HostName}      = '1';
    }

    $IP{$HostName}          = $IniConfig->val($HostName,'IP');
    $IncrMode{$HostName}    = $IniConfig->val($HostName,'IncrementalMode');
    # Backward-compat: if missing, assume normal.
    if ( ! defined($IncrMode{$HostName}) ) {
      $IncrMode{$HostName}  = 'Normal';
    }

    $Timestamp{$HostName}   = $IniConfig->val($HostName,'Timestamp');
    $TimestampFull{$HostName}   = $IniConfig->val($HostName,'TimestampFull');
    if ( ! defined($TimestampFull{$HostName}) ) {
      $TimestampFull{$HostName} = $Timestamp{$HostName};
    }

    $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') ];
    $Excludes{$HostName}    = [ $IniConfig->val($HostName,'Excludes') ];

    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 "Auto=",$Auto{$HostName},"\n";
    $Debug && print "IP=",$IP{$HostName},"\n";
    $Debug && print "IncrMode=",$IncrMode{$HostName},"\n";
    $Debug && print "Timestamp=",$Timestamp{$HostName},"\n";
    $Debug && print "TimestampFull=",$TimestampFull{$HostName},"\n";
    $Debug && print "Mode=",$Mode{$HostName},"\n";
    $Debug && print "Destination=",$Destination{$HostName},"\n";
    $Debug && print "Paths=<<EOP\n";
    foreach (@{$Paths{$HostName}}) {
      $Debug && print "$_\n";
    }
    $Debug && print "EOP\n";
    $Debug && print "Excludes=<<EOE\n";
    foreach (@{$Excludes{$HostName}}) {
      $Debug && print "$_\n";
    }
    $Debug && print "EOE\n";
    $Debug && print "\n";
  }
}

# ------------------------------
# UpdateConfig -- subroutine
# ------------------------------
# Description: Updates the Backup timestamps in the config file.
# Calls: IniConf->setval,WriteConfig
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns:
# Syntax: &UpdateConfig
sub UpdateConfig {
  $IniConfig->setval($Host,'Timestamp',$StartTime);
  if ( $opt_fullbackup || $Timestamp{$Host} == 0 ) {
    $IniConfig->setval($Host,'TimestampFull',$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 {
        if ($Eject ne '') {
          $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 ne '') {
    open (MAILTHIS, "|$MailProg $BackupAdministrator");
    print MAILTHIS "To: $BackupAdministrator\nSubject: Automated Backup Media Rotatation\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.
  }
}
