#!/usr/bin/perl
# -------------------------------------------------------
# $Header: /home/cvs/dobackup/dobackup.pl,v 4.41 2008/07/31 19:39:47 rhardy Exp $
# -------------------------------------------------------
#
# Copyright 1997-2004 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.ca/opensource/dobackup/
#
# Original Program by: Robert Hardy <rhardy@webcon.ca>
# Prior to first public release, all updates by:
#   Robert Hardy <rhardy@webcon.ca>, and
#   Ian Morgan <imorgan@webcon.ca>
#

#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 10k -G 9 -Z -x -1 a";
our($BackupProgDebug); $BackupProgDebug = "/usr/bin/afio -o -v -T 10k -G 9 -z -Z -x -1 a";
our($Cat);             $Cat = "/bin/cat";
our($Cut);             $Cut = "/usr/bin/cut";
our($DF);              $DF = "/bin/df";
our($DU);              $DU = "/usr/bin/du";
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 -w 10 -W 10";
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($IONice);          $IONice = "/usr/bin/ionice -c3 -p";
our($Renice);          $Renice = "/usr/bin/renice +20";
# -------------

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

# ------- Arguments from here to End Defaults
our($opt_test, $opt_fullbackup, $opt_rotate, $opt_prepdisk, $opt_setauto);
# --------------- 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); },
            "setauto=s" => \$opt_setauto )
    or &ShowSyntax;

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

#Special config-altering functions

if ($opt_setauto) {
  if ($SingleHostOverride) {
    if ($opt_setauto eq 'on') {
      $IniConfig->setval($SingleHostOverride,'Auto',1);
      $IniConfig->RewriteConfig;
      $Debug && print "Config file auto flag set on for host $SingleHostOverride.\n";
    } elsif ($opt_setauto eq 'off') {
      $IniConfig->setval($SingleHostOverride,'Auto',0);
      $IniConfig->RewriteConfig;
      $Debug && print "Config file auto flag set off for host $SingleHostOverride.\n";
    } elsif ($opt_setauto eq 'toggle') {
      our($temp);
      $temp=$Auto{$SingleHostOverride};
      $Debug && print "Auto flag for host $SingleHostOverride before=$temp.\n";
      $temp=abs($temp-1);
      $Debug && print "Auto flag for host $SingleHostOverride after=$temp.\n";
      $IniConfig->setval($SingleHostOverride,'Auto',$temp);
      $IniConfig->RewriteConfig;
      print "Config file auto flag toggled ",($temp==1)?'on':'off'," for host $SingleHostOverride.\n";
    } else {
      print "Invalid argument \'$opt_setauto\' to --setauto.\n";
      exit(1);
    }
  } else {
    print "Option --setauto requires a --host. No host specified.\n";
    exit(1);
  }
  exit(0);
}

#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.41 $';
  ($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.

  --setauto STRING When used in conjunction with --host, alter the automatic
                   backup flag for the given host according to STRING, which
                   can be one of 'on', 'off', or 'toggle', and then exit.

  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 Renice program
  ($temp) = ($Renice =~ /^\s*(\S+).*$/ );
  if ( ! -e "$temp" ) {
    print ("ERROR: Renice program $Renice 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 - 31536000) ) ) {
    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);
  my ($temp);

  # backups should be a background idle task and not break server I/O. Be nice/ionice.
  # If IONice is there use it.
  if ($IONice && $IONice ne "") {
    ($temp) = ($IONice =~ /^\s*(\S+).*$/ );
    if ( -e "$temp" ) {
      $Debug && print ("Running $IONice $$\n");
      `$IONice $$`;
    } else {
      $Debug && print ("ERROR: IONice program $IONice not found\n");
    }
  }
  $Debug && print ("Running $Renice $$\n");
  `$Renice $$`;

  # 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");
      }
      # Handle case where an individual file is listed in the list of paths to backup.
      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)=0;
  my($DirElement)='';
  my($SubDebug)=0;
  my($FileOwner,$DirOwner)=(0,0);
  my($pat)='';
  study "$Path";

  # Check for and process .nobackup files
  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";
    }
  }

  # Process the exclusion lists
  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";
      }
    }
  }

  # Read the contents of the directory
  opendir(TOPROCESS, "$Path");
ELEMENT:
  foreach (grep !/^\.\.?$/, readdir TOPROCESS) { # Exclude "." and ".."

    $DirElement="$Path/$_";

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

    if ( $CTime != 0 ) { # Incremental backup
      (undef,undef,undef,undef,undef,undef,undef,undef,undef,undef,$TempCTime) = stat("$DirElement");

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

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

      if ( $TempCTime >= $CTime ) {
        print BACKTEMP "$DirElement\n";
        ($Debug > 1) && print "Adding $DirElement\n";
      }
    } else { # Full backup
      print BACKTEMP "$DirElement\n";
      ($Debug > 1) && print "Adding $DirElement\n";
    }

    if (-d "$DirElement" && (! -l "$DirElement") ) {
      &ProcessDir("$DirElement",$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)=0;
  my($UsageExact)=0;
  my($Stamp)=0;

  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 0077; $Cat $ToBackupTempFile | $BackupProgDebug $Destination{$Host}/$Filename 2>> $BackupErrorFile.$Host";
    $Debug && ($Return = 0xffff & system("$Umask 0077; $Cat $ToBackupTempFile | $BackupProgDebug $Destination{$Host}/$Filename 2>> $BackupErrorFile.$Host"));
    ( ! $Debug ) && ($Return = 0xffff & system("$Umask 0077; $Cat $ToBackupTempFile | $BackupProg $Destination{$Host}/$Filename 2>> $BackupErrorFile.$Host"));
    # Now we've finished the backup, make the file read-only
    chmod 0400, "$Destination{$Host}/$Filename" or print "Warning: Could not make backup file read-only\n";
  }

  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->RewriteConfig;
    }
    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->RewriteConfig;
        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`);
      $Debug && print "Command: $DF --block-size=1 $Destination{$Host} |$Tail -1 |$TR -s ' ' |$Cut -f 4 -d ' ' > $BackupTimeStamp\n";
      $Return = 0xffff & system("$DF --block-size=1 $Destination{$Host} |$Tail -1 |$TR -s ' ' |$Cut -f 4 -d ' ' > $BackupTimeStamp");
      chomp($UsageExact = `$Cat $BackupTimeStamp`);
      unlink("$BackupTimeStamp");
      $Debug && print "Usage=$Usage ($UsageExact)\n";
      $Temp=$IniConfig->val($Host,'MediaSet');
      $Temp=$IniConfig->val("MediaSet=$Temp",'CurrentUnit');
      $IniConfig->setval("MediaUnit=$Temp",'Usage',$Usage);
      $IniConfig->setval("MediaUnit=$Temp",'UsageExact',$UsageExact);
      $IniConfig->RewriteConfig;
    }
    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->RewriteConfig;
       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;
  my($IgnoreFreeSpace)=0;
  my($Usage)=0;
  my($UsageExact)=0;
  my($LastSize)=0;
  my($LastSizeBytes)=0;
  my($LastSizeFull)=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.41 $';
  ($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";

    $LastSizeBytes = `$DU -bsc ${temp}* | $Tail -1`;
    ($LastSizeBytes) = ( $LastSizeBytes =~ /^\s*(\d*?)\s.*$/ );
    $LastSize = `$DU -sch ${temp}* | $Tail -1`;
    ($LastSize) = ( $LastSize =~ /^\s*(.*?)\s.*$/ );
    $LastSize =~ s/\b(\d+)\w*[kK]\b/$1 KiB/g;
    $LastSize =~ s/\b(\d+)\w*M\b/$1 MiB/g;
    $LastSize =~ s/\b(\d+)\w*G\b/$1 GiB/g;

    print MAILTHIS "Size:            $LastSize\n";
    $Debug && print "Size: $LastSize ($LastSizeBytes)\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";
  $Usage = $IniConfig->val("MediaUnit=$temp",'Usage');
  $Usage =~ s/\b(\d+)\w*[kK]\b/$1 KiB/g;
  $Usage =~ s/\b(\d+)\w*M\b/$1 MiB/g;
  $Usage =~ s/\b(\d+)\w*G\b/$1 GiB/g;
  ($Mode{$Host} eq 'Tape') && print MAILTHIS 'Current Tape Position: ' , $Usage , "\n";

  $IgnoreFreeSpace = $IniConfig->val("MediaSet=$MediaSet{$Host}",'IgnoreFreeSpace');
  if ( ! defined($IgnoreFreeSpace) ) {
    $IniConfig->newval("MediaSet=$MediaSet{$Host}",'IgnoreFreeSpace',0);
    $IgnoreFreeSpace=0;
  }
  ($Mode{$Host} eq 'Disk' || $Mode{$Host} eq 'Filesystem') && print MAILTHIS 'Space Remaining: ' , $IgnoreFreeSpace?'Ignored':$Usage , "\n";

  # write LastSizeBytes to lastsizefull if in full mode
  if ($opt_fullbackup || $Timestamp{$Host} == 0) {
    $IniConfig->setval("$Host",'LastSizeFull',"$LastSizeBytes");
  } else {
    $IniConfig->setval("$Host",'LastSize',"$LastSizeBytes");
  }
  $IniConfig->RewriteConfig;

  $temp = $IniConfig->val("MediaSet=$MediaSet{$Host}",'CurrentUnit');
  $UsageExact = $IniConfig->val("MediaUnit=$temp",'UsageExact');
  $LastSizeFull = $IniConfig->val("$Host",'LastSizeFull');
  $LastSizeBytes = $IniConfig->val("$Host",'LastSize');
  $Debug && print "Free: $UsageExact\n";
  $Debug && print "Full: $LastSizeFull\n";
  $Debug && print "Last: $LastSizeBytes\n";

  if (! $IgnoreFreeSpace) {
    if ( $UsageExact < $LastSizeFull ) {
     $Debug && print "WARNING: free space may be to low for next FULL backup\n";
     print MAILTHIS "\nWARNING: Space Remaining may be too low for the next FULL backup to complete!\n";
    }
    if ( $UsageExact < $LastSizeBytes ) {
     $Debug && print "WARNING: free space may be to low for next backup\n";
     print MAILTHIS "\nWARNING: Space Remaining may be too low for the next regular backup to complete!\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');
  if ( defined $IniConfig->val('BACKUPCONFIG','Renice') ) {
    $Renice              = $IniConfig->val('BACKUPCONFIG','Renice');
  } else {
    $IniConfig->newval('BACKUPCONFIG','Renice', "$Renice");
  }
  if ( defined $IniConfig->val('BACKUPCONFIG','IONice') ) {
    $IONice              = $IniConfig->val('BACKUPCONFIG','IONice');
  } else {
    $IniConfig->newval('BACKUPCONFIG','IONice', "$IONice");
  }

  $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';
      $IniConfig->newval($HostName,'Auto','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';
      $IniConfig->newval($HostName,'IncrementalMode','Normal');
    }

    $Timestamp{$HostName}   = $IniConfig->val($HostName,'Timestamp');
    $TimestampFull{$HostName}   = $IniConfig->val($HostName,'TimestampFull');
    # Backward-compat: if missing, assume last timestamp.
    if ( ! defined($TimestampFull{$HostName}) ) {
      $TimestampFull{$HostName} = $Timestamp{$HostName};
      $IniConfig->newval($HostName,'TimestampFull',$Timestamp{$HostName});
    }

    $temp = $IniConfig->val($HostName,'LastSize');
    # Backward-compat: if missing, assume 0.
    if ( ! defined($temp) ) {
      $IniConfig->newval($HostName,'LastSize',0);
    }

    $temp = $IniConfig->val($HostName,'LastSizeFull');
    # Backward-compat: if missing, assume lastsize.
    if ( ! defined($temp) ) {
      $IniConfig->newval($HostName,'LastSizeFull',$IniConfig->val($HostName,'LastSize'));
    }

    $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');

    $temp = $IniConfig->val("MediaUnit=$MediaUnit{$HostName}",'UsageExact');
    if ( ! defined($temp)) {
      $IniConfig->newval("MediaUnit=$MediaUnit{$HostName}",'UsageExact','0');
    }

    $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 < 1 or $1 > 254 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 "MediaSet=",$MediaSet{$HostName},"\n";
    $Debug && print "CurrentUnit=",$MediaUnit{$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->RewriteConfig;
  $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->RewriteConfig;
  $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->RewriteConfig;
    return 0; # prep sucessful.
  } else {
    $Debug && print "Disk does need not need prepping.\n";
    return 0; # prep not needed.
  }
}
