#
# Copyright (c) 1990,1991,1992 The Ohio State University.
# All rights reserved.
#
# Redistribution and use in source and binary forms are permitted
# provided that: (1) source distributions retain this entire copyright
# notice and comment, and (2) distributions including binaries display
# the following acknowledgement:  ``This product includes software
# developed by The Ohio State University and its contributors''
# in the documentation or other materials provided with the distribution
# and in all advertising materials mentioning features or use of this
# software. Neither the name of the University nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#

require $lib_log_perl;
require $lib_lock_perl;
require $lib_db_perl;
require $lib_misc_perl;
require $lib_tape_perl;

#
# Heck, we almost always need the darned hostname, might as well get it 
# once and for all...
#
if (! defined($hostname)) {
    open(HOST, "$hostname_prog |") || die "error ($0): can't get the hostname";
    chop($hostname=<HOST>);
    close(HOST);

    ($hostname) = split(/\./, $hostname);
}


#
# These are lists of the legit keywords for each of the database types.
# These are used in update_db for detecting programming errors.
#
# Use a value of 1 if the field is required (should always be present), 
# 2 if it is optional.
#

$ok_keys{"tape_db uses"} = 1;
$ok_keys{"tape_db add_date"} = 1;
$ok_keys{"tape_db used_date"} = 1;
$ok_keys{"tape_db drive"} = 1;
$ok_keys{"tape_db flags"} = 1;
$ok_keys{"tape_db comment"} = 2;
$ok_keys{"tape_db run_id"} = 2;
$ok_keys{"tape_db type"} = 1;
$ok_keys{"tape_db format"} = 1;
$ok_keys{"tape_db file_number"} = 2;
$ok_keys{"tape_db accuracy"} = 2;
$ok_keys{"tape_db location"} = 2;
$ok_keys{"tape_db home"} = 2;

$ok_keys{"run_db chain"} = 1;
$ok_keys{"run_db level"} = 1;
$ok_keys{"run_db hosts"} = 1;
$ok_keys{"run_db flags"} = 1;
$ok_keys{"run_db active"} = 1;
$ok_keys{"run_db start_date"} = 1;
$ok_keys{"run_db done_date"} = 2;
$ok_keys{"run_db scheduled_for"} = 2;

$ok_keys{"run_host buid_list"} = 2;

$ok_keys{"backup_host run"} = 1;
$ok_keys{"backup_host fs"} = 1;
$ok_keys{"backup_host fromdate"} = 2;
$ok_keys{"backup_host todate"} = 2;
$ok_keys{"backup_host flags"} = 2;
$ok_keys{"backup_host d-flags"} = 2;
$ok_keys{"backup_host tflist"} = 2;
$ok_keys{"backup_host comments"} = 2;
$ok_keys{"backup_host afile"} = 2;
$ok_keys{"backup_host volhdr"} = 2;
$ok_keys{"backup_host type"} = 1;
$ok_keys{"backup_host accuracy"} = 2;

$ok_keys{"test shape"} = 1;
$ok_keys{"test color"} = 1;
$ok_keys{"test mass"} = 1;
$ok_keys{"test length"} = 1;
$ok_keys{"test books"} = 1;


#
# Debug levels (powers of 2, so that we set/test bits in $opt_d by or'ing 
# them together.
#
$DEBUG_LOCK =		1;
$DEBUG_SYSTEM =		2;
$DEBUG_INFO =		4;
$DEBUG_PROC =		8;
$DEBUG_DATABASE =	16;


#
# Misc routines to get info from the user, check it, etc.
#

#
# Ask a question with a default answer in a standard fashion, return 
# the default if a return is typed, otherwise return the answer.
#

sub askdef {
    local($message, $default, $help) = @_;
    local($oldflush, $answer);

    $oldflush = $|;
    $| = 1;

prompt_loop: {
        print "$message? [$default] ";
	chop ($answer = <stdin>);

	if (length($answer) == 0) {
	    $answer = $default;
	} elsif ($help ne "" && $answer eq "?") {
	    print "\n", $help, "\n";
	    redo prompt_loop;
	}
    }

    $| = $oldflush;
    $answer;
}

#
# Get a chain number.  We assume that anything uses this routine uses 
# the getopt library, and uses -c as an option that specifies a chain
# number.  We also assume that read_config has been called to define
# $num_chains...
#

sub get_chain {
    local($result, $help);
    
    if ($num_chains > 1) {
	while (1) {
	    if (defined($opt_c)) {
		$result = $opt_c;
		undef $opt_c;
	    } else {
		$help = sprintf(
"Enter the chain number, a number between 0 and %d (inclusive).\n", 
				$num_chains - 1);
		$result = do askdef("Which chain", "0", $help);
	    }

	    if ($result < 0 || $result >= $num_chains) {
		printf("The chain number must be in the range [0..%d]\n", 
		       $num_chains-1);
	    } else {
		last;
	    }
	}
    } else {
	$result = 0;
    }

    print "get_chain: chain # $result\n" if $opt_d & $DEBUG_INFO;

    $result;
}

#
# Get a level name.  We assume that the caller uses the getopt
# library, and uses -l to specify a level number, and that read_config
# has been called to define %levels.
#

sub get_level {
    local ($search_level, $t_level, $t_name, $help);

    $help = 
"Enter the name or number of a backup level for this chain.  The defined
level names for chain $chain are '$level_name_list{$chain}'.\n";

till_ok:
    while (1) {
	    if (! defined($opt_l)) {
		    $search_level = do askdef("Which level", "daily", $help);
	    } else {
		    $search_level = $opt_l;
		    undef $opt_l;
	    }
	    
	    if ($search_level =~ /\d+/) {
		if (defined($level_number2name{$chain,$search_level})) {
		    $search_level = $search_level + 0;
		    last till_ok;
		}

		print "warning: a level $search_level backup has not been declared for chain $chain.
The known levels for chain $chain are '$level_number_list{$chain}'\n";
		  
	    } else {
		if (defined($level_name2number{$chain,$search_level})) {
		    $search_level = $level_name2number{$chain,$search_level} + 0;
		    last till_ok;
		}
		
		print "warning: a level named '$search_level' has not been defined for chain $chain.
The known levels are '$level_name_list{$chain}'.\n";
	    }
    }

    print "get_level: level $search_level\n" if $opt_d & $DEBUG_INFO;

    $search_level;
}

#
# determine whether the given drive can handle of tape of the given type 
# and format for the $req_op.
#
sub tape_and_drive_match {
    local($req_type, $req_format, $req_op, $name) = @_;
    local($is_valid_format);

    if (defined($tape_device{$name}) && $req_type ne "") {
	# Its a known tape device, and there's a requested tape type.  Does 
	# this tape drive match the request?
	#
	if ($tape_type{$name} ne $req_type) {
	    return(0);
	}

	if ($req_op eq "write") {
	    $is_valid_format = &valid_format($req_format, split(/\|/, $tape_writes{$name}));
	} else {
	    $is_valid_format = &valid_format($req_format, split(/\|/, $tape_reads{$name}));
	}

	if (! $is_valid_format) {
	    return(0);
	}
    }

    return(1);
}

#
# get_tape_name - get a tape alias name, or host/device.  Assumes that the
# caller uses the getopt package, and -f to specify a tapehost:device
# pair.  Also assume that $default_tape is set to some default
# host:device spec.  
#
# Has the side effect of nuking opt_f if it was given.
#

sub get_tape_name {
    local($req_type, $req_format, $req_op) = @_;
    local($name, $query, $default_name, $tname, $ttype, @format_choices);
    local($tape_user, $tape_host, $tape_dev);

  tape_name_loop: {
      if (defined($opt_f)) {
	  if (defined($tape_device{$opt_f})) {
	      $name = $opt_f;
	  } elsif (defined($tape_name{$opt_f})) {
	      $name = $tape_name{$opt_f};
	  } else {
	      $name = "";
	  }
	  undef $opt_f;
      } else {
	  #
	  # If there's no particular request, ask a simple question.
          #
	  if ($req_type eq "") {
	      $query = "Which tape drive do you want to use";
	      $default_name = $default_tape{"*", "*"};
	  } else {
	      #
	      # We're looking for a drive that can read/write a particular 
	      # type and format of tape.
	      #
	      $query = "Which tape drive do you want to use (must be able to $req_op
$req_type $req_format tapes)";
	      if (defined($default_tape{$req_type, $req_format})) {
		  $default_name = $default_tape{$req_type, $req_format};
	      } else {
		  #
		  # No default, find a drive that meets the requested specs,
		  # if possible.
		  #
		  while (($tname, $ttype) = each %tape_type) {
		      if ($req_op eq "read") {
			  @format_choices = split(/\|/, $tape_reads{$tname});
		      } else {
			  @format_choices = split(/\|/, $tape_writes{$tname});
		      }
		      if ($ttype eq $req_type &&
			  &valid_format($req_format, @format_choices)) {
		          $default_name = $tname;
			  last;
		      }
		  }
	      }
	  }

	  $name = &askdef($query, $default_name,
"Give the name or host:device spec of the non-rewinding tape drive that you
want to use.  Make sure it matches the type of tape that you will be using,
if you know.\n");
      }

      if (! &tape_and_drive_match($req_type, $req_format, $req_op, $name)) {
	  print "warning: drive $name cannot $req_op $req_type $req_format tapes.\n\n";
	  redo tape_name_loop;
      }
    }

    ($tape_user, $tape_host, $tape_dev) = &splittapedev($name);

    if ($tape_dev !~ m,^/dev/,) {
	print "warning: $tape_dev doesn't look like a device name.  Try again.\n";
	return(&get_tape_name($req_type, $req_format, $req_op));
    }

    print "get_tape_name: $name $tape_user $tape_host $tape_dev\n" 
      if $opt_d & $DEBUG_INFO;

    return($name, $tape_user, $tape_host, $tape_dev);
}

#
# valid_format compares a tape format against a list of valid tape formats
# and returns 1 if it matches one of them, and 0 if it doesn't.  * matches 
# anything.
#
sub valid_format {
    local($target, @choices) = @_;
    local($item);

    foreach $item (@choices) {
        if ($item eq "*" || $item eq $target) {
	    return(1);
        }
    }

    return(0);
}
    
#
# split-tapedev takes a host:dev string and splits out the host and
# device portions.  If the host: portion is missing, then return $hostname for 
# the host name.  unfortunately, we can't simply do a split on :...
#
# also might as well use alias lookups here...
#

sub splittapedev {
    local($input) = @_;
    local($user, $host, $dev);

    $user = $default_remote_user;
    $host = $hostname;

    if (defined($tape_device{$input})) {
	$input = $tape_device{$input};
    }

    if ($input =~ /^(.+)@(.+):(.+)$/) { # user@host:dev
	$user = $1;
	$host = $2;
	$dev = $3;
    } elsif ($input =~ /^(.+):(.+)$/) {	# host:dev
	$host = $1;
	$dev = $2;
    } else {			# tape
	$dev = $input;
    }

    return($user, $host, $dev);
}

#
# Create a rsh command for accessing a tape drive's host.
#

sub make_rsh_cmd {
    local($user, $host) = @_;
    local($rsh_cmd);
    #
    # don't need to use remote user if we aren't running as root, eh?
    #
    #  Not quite - using root tickets makes this tricky.  (foley)
    #$user = "" if $> != 0;

    #
    # Remote host is local host, no rsh needed
    #
    if ($host eq $hostname) {
	$rsh_cmd = "";
    } else {
	#
	# Create the rsh command, using -l user if $user isn't ''
	#
	if ($user eq "") {
	    $rsh_cmd = "$rsh_prog $host ";
	} else {
	    #
	    # I hate Ultrix...
	    #
	    if ($ultrix) {
		$rsh_cmd = "$rsh_prog $host -l $user ";
	    } else {
		# athena rsh reverses args (foley)
		if ($rsh_prog eq "/usr/athena/bin/rsh") {
		    $rsh_cmd = "$rsh_prog $host -l $user";
		} else {
		    $rsh_cmd = "$rsh_prog -l $user $host ";
		}
	    }
	}
    }
    return($rsh_cmd);
}

#
# get tape drive type and write format
#

sub get_tape_extra_info {
    local($name) = @_;
    local($taf, $type, $read_formats, $write_format, $format, $status_ok);

  taf_loop: {
      $type = &askdef("What type of drive is $name? ", "exb", "Enter the tape drive type for this tape device.\n");

      if (! defined($tape_type_list{$type})) {
          print "warning: unknown tape type, try again.\n";
	  redo taf_loop;
      }

      $read_formats = &askdef("What formats does this tape device read? ", "8200", "Enter the tape drive read formats for this tape device.\n");

      foreach $format (split(/[ \t]+/, $read_formats)) {
	  if (! defined($tape_type_formats{$type,$format})) {
	      print "warning: unknown tape format for $type tapes, try again.\n";
	      redo taf_loop;
	  }
      }

      $write_format = &askdef("What format does this tape device write? ", "8200", "Enter the tape drive write format for this tape device.\n");

      if (! defined($tape_type_formats{$type,$write_format})) {
	  print "warning: unknown tape format for $type tapes, try again.\n";
	  redo taf_loop;
      }
    }

  status_loop: {
      $status_ok = &askdef(
"Does the 'mt status' command on that host give correct tape position information",
"no",
"Type 'yes' if the host that this tape is on gives correct file and block number
information, and 'no' otherwise.  If you aren't sure, type 'no'.\n");
      if ($status_ok ne "yes" && $status_ok ne "no") {
          print "warning: type 'yes' or 'no', please.\n";
	  redo status_loop;
      }
    }

    $status_ok = ($status_ok eq "yes");

    $read_formats =~ s/[ \t]+/|/g;

    return($type, $read_formats, $write_format, $status_ok);
}

#
# verify_tape checks to see that tape number $tape_id is mounted on
# drive $tapedev on host $tapehost.
#

sub verify_tape {
    local($tape_id, $tape_host, $tape_dev, $rsh, $this_tape_type) = @_;
    local($tmp, $cmd, $garbage, $this_id);

    #
    # Ask to have the tape mounted in the appropriate place...
    #

    $| = 1;
    if ($tape_host eq "") {
        $tmp = $tape_dev;
    } else {
        $tmp = "$tape_host:$tape_dev";
    }

    if (! defined($opt_V)) {
        print "Please mount tape $tape_id in drive $tmp\n  and press <RETURN> when ready.";

        <stdin>;
    } else {
        undef $opt_V;
    }

    #
    # Rewind the tape...
    #
    if (&tape_rewind($rsh, $tape_dev)) {
        print stderr "warning: can't rewind the tape.  Is it mounted?\n";
	return 0;
    };

    #
    # Read the label off of the tape...
    #
    if (&tape_get_id($rsh, $tape_dev, *this_id)) {
	return 0;
    }

    #
    # If the ID is just a number, or $config_name-number, then its an old
    # fashioned ID.  Turn it into a modern tape ID and then do the compare...
    #
    if ($this_id =~ m/^$config_name-([0-9]+)$/o ||
	$this_id =~ m/^([0-9]+)$/o) {
	$this_id = &make_tape_id_from_number($this_tape_type, $1 + 0, "");
    }
	
    if ($this_id eq $tape_id) {
        return 1;
    }

    print "warning: drive $tape_host:$tape_dev contains tape $this_id, not tape $tape_id\n";
    return 0;
}


#
# get_recent_backups - return a list of the backup DB entries made before the
# time specified.
#
# args: 
#    host	       	host we are interested in
#    directory		directory on that host
#    date		date (in timestamp form) 
#    *list		associative array to put backups in
#
# Note that %list actually contains all backups that were done before the 
# given time, so this routine can be used in a variety of ways.  List is 
# indexed by "buid,chain,level".  The value is the entry from the backup 
# database.
#

sub get_recent_backups {
    local($host, $directory, $timestamp, *list, *times) = @_;
    local(%RUN_DB, %TAPE_DB, %BUDB);
    local($budb_key, $budb_value, %budb_array);
    local($rdb_value, %rdb_array);
    local($tdb_value, %tdb_array);
    local($key);
    local($tapeid, $filenumber);
    local($home, $location);

    %list = (); %times = ();

    #
    # open the databases...we need the run database to get the chain and level 
    # for each backup...
    #
    &open_db(*RUN_DB, $run_db_file, $db_mode) ||
      &sigh("$0: can't open the database $run_db_file on $hostname in $0");

    &open_db(*TAPE_DB, $tape_db_file, $db_mode) ||
      &sigh("$0: can't open the database $tape_db_file on $hostname in $0");

    &open_db(*BUDB, $backup_host_file . $host, $db_mode) ||
      return;

    #
    # Wend our way through the backup database.  We're only interested in 
    # entries for the given directory done before the stated time.
    #

budb_loop:
    foreach $budb_key (&keys_db(*BUDB)) {
	&read_db(*BUDB, $budb_key, *budb_value, *budb_array);

	#
        # Check the directory, time and status.  If its good, add it to the 
	# %list.  
        #
	if (($directory eq $budb_array{"fs"}) && 
	    ($timestamp > $budb_array{"todate"} + 0) &&
	    ($budb_array{"flags"} eq "done" ||
	     $budb_array{"d-flags"} eq "done")) {

            #
            # If we are only using onsite backups, and it was done to 
	    # tape and not to disk, then we better check the tape 
	    # location.
	    #
	    if ($ignore_offsite_tapes &&
		$budb_array{"flags"} eq "done" &&
		$budb_array{"d-flags"} ne "done") {

		($tapeid, $filenumber) = split(/\./, $budb_array{"tflist"});
		if (! &read_db(*TAPE_DB, $tapeid, *tdb_value, *tdb_array)) {
		    printf stderr "warning: tape $tapeid doesn't exist for backup $budb_key...skipping.\n";
		    next budb_loop;
		}

		#
		# if the tape has a home and its location isn't the same, or it
		# doesn't have a home (old tape) and the location is set, then it
		# is offsite...
		#
		$home = $tdb_array{"home"};
		$location = $tdb_array{"location"};

		if (($home ne "" && $location ne $home) ||
		    ($home eq "" && $location ne "")) {
		    next budb_loop;
		}
	    }

	    #
	    # Gotta get the $chain and $level...
	    #
	    if (! &read_db(*RUN_DB, $budb_array{"run"}, *rdb_value, *rdb_array)) {
		printf(stderr "warning: there's no RUN database entry %d for backup ID $budb_key for host $host\n",
		       $budb_array{"run"});
		next budb_loop;
	    }

	    #
	    # Add it to the list...
	    #
	    $key = join(',', $budb_key, $rdb_array{"chain"}, $rdb_array{"level"});
	    $list{$key} = $budb_value;
	    $times{$key} = $budb_array{"todate"} + 0;
	}
    }

    &close_db(*BUDB, $backup_host_file);
    &close_db(*TAPE_DB, $tape_db_file);
    &close_db(*RUN_DB, $run_db_file);
}


#
# Gets the latest backup from the backup list from the same chain that 
# has a level less than or equal to the given level number.
#

#
# backup_list is an assoc array containing the values of the entries 
# in the backup database, and backup_keys is an array of the keys, 
# sorted in reverse chronological order.
#
# RUNDB must be open so that we can read the chain and level numbers.

sub get_latest_lower_or_same_level {
    local($target_chain, $target_level, *budb_values, *budb_keys) = @_;
    local($key, %backup_entry, $run_value, %run_entry, $run, $buid, @tmp);

    $target_chain = $target_chain+0;
    $target_level = $target_level+0;

    foreach $key (@budb_keys) {
	&parse_db_entry($budb_values{$key}, *backup_entry);

	$run = $backup_entry{"run"};
	&read_db(*RUNDB, $run, *run_value, *run_entry);

	if (($run_entry{"chain"}+0) == $target_chain &&
	    ($run_entry{"level"}+0) <= $target_level) {
	    ($buid, @tmp) = split(/,/, $key);
	    return($run, $run_entry{"chain"}, $run_entry{"level"}, 
		   $buid, $backup_entry{"todate"});
	}
    }

    return(-1, -1, -1, -1, 0);
}


# Parse a config file.  Valid entries are:
#
# chains NUMBER
#	sets $num_chains
#
# level CHAIN NUMBER NAME EVERY KEEP
#	@level_zero[$chain] = 1 if chain has a level 0 backup
#	@max_level[$chain] = max level defined for each chain
#	%level_name2number{$chain, $name} = level # for that name
#	%level_number2name{$chain, $level} = level name for that number
#	%level_keep{$chain, $number} = keep value
#	%level_every{$chain, $number} = every value
#	%level_name_list{$chain} = list of level names for that chain
#	%level_number_list{$chain} = list of level numbers for that chain
#
# offsite LOCATION CHAIN WAIT WARN
#	%known_offsite_chains{CHAIN} = LOCATION
#	%known_offsite_locations{LOCATION} = 1
#	%offsite_location{CHAIN} = LOCATION 
#	%offsite_wait{CHAIN} = WAIT
#	%offsite_warn{CHAIN} = WARN
#
# group NAME MEMBER MEMBER...
#	%group_aliases indexed by name, value is list of members 
#
# tape-types TYPE AGE USES FORMAT+ 
#	%tape_type_list indexed by TYPE, value is 1
#	%expire_age indexed by TYPE set to AGE 
#	%expire_uses indexed by TYPE set to USES
#	%tape_type_formats{$type, $format}, value is 1
#	%tape_format_list{$type}, value is "format format format"
#
# tape NAME HOST:DEV TYPE READS WRITES STATUS_OK
#	%tape_device indexed by name, value is HOST:DEV
#	%tape_type indexed by name, value is HOST:DEV
#	%tape_reads indexed by name, value is HOST:DEV
#	%tape_writes indexed by name, value is HOST:DEV
#	%tape_name indexed by host:device, value is NAME.
#	%tape_status_ok indexed by name, value is 1 if 'mt status' 
#	    works right, 0 otherwise.
#	%tape_location indexed by name, value is location
#
# location name
#	%known_locations indexed by name, value is 1.
#
# default-tape NAME TYPE FORMATS
#	$default_tape{$type, $format} set to NAME
#
# mail-to ADDRESS ADDRESS...
#	$mail_to set to list of mail addresses [root]
#
# config-name NAME
#	$config_name set to NAME [""]
#
# The minimal config file must contain at least 1 level definition.

sub read_config {
    #
    # @fields, $errors and $got_chains are global to the read_* routines.
    #
    local($text, $i, @fields, $errors, $got_chains);

    $got_chains = 0;
    $errors = 0;

    #
    # Set default values for some things
    #
    $mail_to = "root";
    $config_name = "";

    #
    # open and deal with the config file...
    #
    if (! open(CONFIG, $backup_config)) {
	&sigh("$0: can't open config file '$backup_config'\n");
    }

    $text = "";

line_loop:
    while (<CONFIG>) {
	#
	# nuke newline, comments, split into space separated fields...
	#
	chop;
	$text .= $_;
	next line_loop if ($text =~ s/\\$//);
	$text =~ s/#.*$//;
	@fields = split(/[ \t]+/, $text);

	if ($#fields == -1) {
	    $text = "";
	    next line_loop;
	}

      foo: {
	  if ($fields[0] eq "chains") {
	      &read_chains(); last foo;
	  }

	  if ($fields[0] eq "level") {
	      &read_level(); last foo;
	  }

	  if ($fields[0] eq "offsite") {
	      &read_offsite(); last foo;
	  }

	  if ($fields[0] eq "location") {
	      &read_location(); last foo;
	  }

	  if ($fields[0] eq "group") {
	      &read_group(); last foo;
	  }

	  if ($fields[0] eq "tape-type") {
	      &read_tape_type(); last foo;
	  }

	  if ($fields[0] eq "tape") {
	      &read_tape(); last foo;
	  }

	  if ($fields[0] eq "default-tape") {
	      &read_default_tape(); last foo;
	  }

	  if ($fields[0] eq "mail-to") {
	      &read_mail_to(); last foo;
	  }

	  if ($fields[0] eq "config-name") {
	      &read_config_name(); last foo;
	  }

	  printf(stderr "warning: '$backup_config' line $., unrecognized keyword '%s'.\n",
		 $fields[0]);
	  $errors++;
      }

      $text = "";
    }

    close(CONFIG);

    #
    # ensure that all chains have at least a level 0 backup defined.
    #
    for ($i = 0; $i < $num_chains; $i++) {
	if ($level_zero[$i] != 1) {
	    print stderr "warning: '$backup_config', you haven't defined a level 0 backup for chain $i.\n";
	    $errors++;
	}
    }

    if ($errors != 0) {
	&sigh("Too many errors to continue, bailing out...\n");
    }
}


#
# These are the routines that deal with the various types of entries that
# can appear in the backup config file.
#
# They use @fields, $errors and $got_chains from read_config.
#

#
# read a "chains N" entry from the config file and deal with it.
#

sub read_chains {
    local($i);

    if ($#fields != 1) { 
	printf(stderr "warning: '$backup_config', line $., correct syntax is be 'chains number'\n");
	$errors++;
	return;
    }
	
    $num_chains = $fields[1] + 0;

    if ($num_chains <= 0) {
	printf(stderr "warning: 'backup_config', line $., chains must be > 0\n");
	$errors++;
    }

    #
    # initialize the level zero array, max level array
    #
    for ($i = 0; $i < $num_chains; $i++) {
	$max_level[$i] = -1;
	$level_zero[$i] = 0;
    }

    $got_chains = 1;
}

#
# read a "level" entry from the config file.  
#

sub read_level {
    local($tmp_chain, $tmp_number, $tmp_name, $tmp_keep, $tmp_every);

    if (! $got_chains) {
	print stderr "warning: '$backup_config', line $., you must define the number of chains first.\n";
	$errors++;
    }

    if ($#fields != 5) {
	print stderr "warning: '$backup_config', line $., correct syntax is 'level chain number name every keep'\n";
	$errors++;
	return;
    }

    $tmp_chain = $fields[1] + 0;
    $tmp_number = $fields[2] + 0;
    $tmp_name = $fields[3];
    $tmp_every = $fields[4] + 0;
    $tmp_keep = $fields[5];

    if ($tmp_chain < 0 || $tmp_chain >= $num_chains) {
	print stderr "warning: '$backup_config', line $., chain number must be >= 0 and < $num_chains.\n";
	$errors++;
    }

    if ($tmp_number < 0) {
	print stderr "warning: '$backup_config', line $., level number must be > 0.\n";
	$errors++;
    }

    if (defined($level_keep{$tmp_chain,$tmp_number})) {
	print stderr "warning: '$backup_config', line $., you've already defined a level $tmp_number for chain $tmp_chain.\n";
	$errors++;
    }

    if (defined($level_name2number{$tmp_chain, $tmp_name})) {
	print stderr "warning: '$backup_config', line $., you've already defined a level named $tmp_name for chain $tmp_chain.\n";
	$errors++;
    }

    if ($tmp_every <= 0) {
	print stderr "warning: '$backup_config', line $., EVERY must be > 0.\n";
	$errors++;
    }

    #
    # Technically, keep should be big enough that we never recycle 
    # a tape that is current...
    #
    if ($tmp_keep ne "forever") {
	$tmp_keep += 0;
	if ($tmp_keep <= 0) {
	    print stderr "warning: '$backup_config', line $., KEEP must be > 0.\n";
	    $errors++;
	}

# fixme - was tmp_every * 2
	if ($tmp_keep < $tmp_every) {
	    print stderr "warning: '$backup_config', line $., KEEP should be at least as big as EVER.\n";
	}

	$level_keep{$tmp_chain, $tmp_number} = $tmp_keep * $DAYS;
    } else {
	$level_keep{$tmp_chain, $tmp_number} = 0x7fffffff;
    }

    #
    # Set flag if this is a level 0 backup...
    #
    if ($tmp_number == 0 && $tmp_chain >= 0) {
	$level_zero[$tmp_chain] = 1;
    }

    #
    # Record max level number for this chain
    #
    if ($tmp_number > $max_level[$tmp_chain]) {
	$max_level[$tmp_chain] = $tmp_number;
    }

    #
    # append this level number to the list of levels for this chain
    # (needs to be sorted later)
    #
    if (defined($level_number_list{$tmp_chain})) {
	$level_number_list{$tmp_chain} .= " $tmp_number";
    } else {
	$level_number_list{$tmp_chain} = $tmp_number;
    }

    #
    # append this level name to the list of level names for this chain
    #
    if (defined($level_name_list{$tmp_chain})) {
	$level_name_list{$tmp_chain} .= " $tmp_name";
    } else {
	$level_name_list{$tmp_chain} = $tmp_name;
    }

    $level_number2name{$tmp_chain, $tmp_number} = $tmp_name;
    $level_name2number{$tmp_chain, $tmp_name} = $tmp_number;

    $level_every{$tmp_chain, $tmp_number} = $tmp_every * $DAYS;
}

#
# read a "offsite" entry from the config file.  
#

sub read_offsite {
    local($tmp_location, $tmp_chain, $tmp_wait, $tmp_warn);

    if (! $got_chains) {
	print stderr "warning: '$backup_config', line $., you must define the number of chains first.\n";
	$errors++;
    }

    if ($#fields != 4) {
	print stderr "warning: '$backup_config', line $., correct syntax is 'offsite location chain wait warn'\n";
	$errors++;
	return;
    }

    $tmp_location = $fields[1];
    $tmp_chain = $fields[2] + 0;
    $tmp_wait = $fields[3] + 0;
    $tmp_warn = $fields[4] + 0;

    if (! defined($known_locations{$tmp_location})) {
	print stderr "warning: '$backup_config', line $., location $tmp_location has not been declared yet.\n";
	$errors++;
    }

    if ($tmp_chain < 0 || $tmp_chain >= $num_chains) {
	print stderr "warning: '$backup_config', line $., chain number must be >= 0 and < $num_chains.\n";
	$errors++;
    }

    if ($tmp_wait < 0) {
	print stderr "warning: '$backup_config', line $., waiting period must be >= 0 (days).\n";
	$errors++;
    }

    if ($tmp_warn < 0) {
	print stderr "warning: '$backup_config', line $., warning time must be >= 0 (days).\n";
	$errors++;
    }

    # Save everything...
    
    if (defined($known_offsite_chains{$tmp_chain})) {
	printf stderr "warning: '$backup_config', line $., chain $tmp_chain was already assigned to offsite location %s.\n",
	    $known_offsite_chains{$tmp_chain};
	$errors++;
    } else {
	$known_offsite_chains{$tmp_chain} = $tmp_location;
    }
    $known_offsite_locations{$tmp_location} = 1;
    $offsite_location{$tmp_chain} = $tmp_location;
    $offsite_wait{$tmp_chain} = $tmp_wait * $DAYS;
    $offsite_warn{$tmp_chain} = $tmp_warn * $DAYS;
}

#
# read a "location" entry from the config file.  
#

sub read_location {
    local($tmp_name);

    if ($#fields != 1) {
	print stderr "warning: '$backup_config', line $., correct syntax is 'location name'\n";
	$errors++;
	return;
    }

    $tmp_name = $fields[1];

    if (defined($known_locations{$tmp_name})) {
	print stderr "warning: '$backup_config', line $., location $tmp_name was already declared.\n";
	$errors++;
    } else {
	$known_locations{$tmp_name} = 1;
    }
}

#
# read a "group" entry from the config file.
#

sub read_group {
    local($host);

    if ($#fields < 2) {
	print stderr "warning: '$backup_config', line $., syntax should be 'group name member...'\n";
	$errors++;
	return;
    }

    if (defined($group_aliases{$fields[1]})) {
	printf(stderr "warning: '$backup_config', line $., you've already defined a group named '%s'.\n",
	       $fields[1]);
	$errors++;
	return;
    }

    if (defined($known_hosts{$fields[1]})) {
	printf(stderr "warning: '$backup_config', line $., you've already defined a host named named '%s'.\n",
	       $fields[1]);
	$errors++;
	return;
    }

    foreach $host (@fields[2..$#fields]) {
	if (defined($group_aliases{$host})) {
	    $host = $group_aliases{$host};
	} else {
	    $known_hosts{$host} = 1;
	}

	if ($group_aliases{$fields[1]} ne "") {
	    $group_aliases{$fields[1]} .= " ";
	}
	$group_aliases{$fields[1]} .= $host;
    }
}

#
# read a "tape-type" entry from the config file.
#

sub read_tape_type {
    local($tmp_type, $tmp_age, $tmp_uses, $tmp_format, $expr);

    if ($#fields < 4) {
	print stderr "warning: '$backup_config', line $., syntax should be 'tape-type name age uses format [format]*'\n";
	$errors++;
	return;
    }

    $tmp_type = $fields[1];
    $tmp_age = ($fields[2] + 0);
    $tmp_uses = ($fields[3] + 0);

    if (defined($tape_type_list{$tmp_type})) {
	print stderr "warning: '$backup_config', line $., you've already defined a tape type named $tmp_type.\n";
	$errors++;
    } else {
	$tape_type_list{$tmp_type} = 1;
    }

    if ($tmp_age < 1) {
	print stderr "warning: '$backup_config', line $., expiration age for tape type '$tmp_type' must be at least 1.\n";
	$errors++;
    }
    $expire_age{$tmp_type} = $tmp_age * $DAYS;

    if ($tmp_uses < 1) {
	print stderr "warning: '$backup_config', line $., expiration uses for tape type '$tmp_type' must be at least 1.\n";
	$errors++;
    }
    $expire_uses{$tmp_type} = $tmp_uses;

    foreach $tmp_format (@fields[4..$#fields]) {
	if (defined($tape_type_formats{$tmp_type,$tmp_format})) {
	    print stderr "warning: '$backup_config', line $., redeclared tape format '$tmp_format' for type '$tmp_type'\n";
	    $errors++;
	}

	$tape_type_formats{$tmp_type,$tmp_format} = 1;

	if (defined($tape_format_list{$tmp_type})) {
	    $tape_format_list{$tmp_type} .= " $tmp_format";
	} else {
	    $tape_format_list{$tmp_type} = $tmp_format;
	}
    }
}

#
# read a "tape" entry from the config file.  Must have declared the type 
# of this tape with a previous "tape-type" entry.
#

sub read_tape {
    local($tmp_name, $tmp_dev, $tmp_type, $tmp_reads, $tmp_writes, 
	  $tmp_status_ok, $tmp_location);

    if ($#fields != 7) {
	print stderr "warning: '$backup_config', line $., syntax should be 'tape name device type read-formats write-formats status-ok location'\n";
	$errors++;
	return;
    }

    $tmp_name = $fields[1];
    $tmp_dev = $fields[2];
    $tmp_type = $fields[3];
    $tmp_reads = $fields[4];
    $tmp_writes = $fields[5];
    $tmp_status_ok = $fields[6];
    $tmp_location = $fields[7];

    if (defined($tape_device{$tmp_name})) {
	print stderr "warning: '$backup_config', line $., you've already defined a tape named '$tmp_name'.\n";
	$errors++;
    }

    if (defined($tape_name{$tmp_dev})) {
	print stderr "warning: '$backup_config', line $., you've already defined a tape for device '$tmp_dev'.\n";
	$errors++;
    }

    if (! defined($tape_type_list{$tmp_type})) {
	print stderr "warning: '$backup_config', line $., unknown tape type '$tmp_type'.\n";
	$errors++;
    }

    #
    # check read formats for unknown tape formats
    #
    foreach $tmp_entry (split(/\|/, $tmp_reads)) {
	if ($tmp_entry != "*" && 
	    ! defined($tape_type_formats{$tmp_type,$tmp_entry})) {
	    print stderr "warning: '$backup_config', line $., unknown tape format '$tmp_entry' for tape '$tmp_name'.\n";
	    $errors++;
	}
    }

    #
    # check write formats for unknown tape formats
    #
    foreach $tmp_entry (split(/\|/, $tmp_writes)) {
	if ($tmp_entry != "*" && 
	    ! defined($tape_type_formats{$tmp_type,$tmp_entry})) {
	    print stderr "warning: '$backup_config', line $., unknown tape format '$tmp_entry' for tape '$tmp_name'.\n";
	    $errors++;
	}
    }

    #
    # Check 'status ok' to see if it is 'yes' or 'no'.
    #
    if ($tmp_status_ok ne "yes" && $tmp_status_ok ne "no") {
        print stderr "warning: '$backup_config', line $., 'status' for '$tmp_name' must be 'yes' or 'no'  (not '$tmp_status_ok').\n";
        $errors++;
    }

    #
    # Check 'location' to see if its known
    #
    if (! defined($known_locations{$tmp_location})) {
        print stderr "warning: '$backup_config', line $., location '$tmp_location' has not been declared yet.\n";
        $errors++;
    }

    $tape_device{$tmp_name} = $tmp_dev;
    $tape_type{$tmp_name} = $tmp_type;
    $tape_reads{$tmp_name} = $tmp_reads;
    $tape_writes{$tmp_name} = $tmp_writes;
    $tape_name{$tmp_dev} = $tmp_name;
    $tape_status_ok{$tmp_name} = ($tmp_status_ok eq "yes");
    $tape_location{$tmp_name} = $tmp_location;
}

#
# read a "default-tape" entry from the config file.  The tape must be 
# defined first (to ensure that it is defined when the program wants to 
# use it). 
#

sub read_default_tape {
    local($type, $format, @format_list);

    if ($#fields != 3) {
	print stderr "warning: '$backup_config', line $., syntax should be 'default-tape name type formats'\n";
	$errors++;
	return;
    }

    $type = $fields[2];

    if (! defined($tape_device{$fields[1]})) {
	printf stderr "warning: '$backup_config', line $., tape '%s' hasn't been defined yet.\n",
	       $fields[1];
	$errors++;
	return;
    }

    if ($type eq "*") {
	$default_tape{"*", "*"} = $fields[1];
	return;
    }

    if (! defined($tape_type_list{$type})) {
	printf stderr 
"warning: '$backup_config', line $., tape type '$type' hasn't been defined yet.\n";
	$errors++;
	return;
    }

    if ($fields[3] eq "*") {
	@format_list = split(/ /, $tape_format_list{$type});
    } else {
	@format_list = split(/,\|/, $fields[3]);
    }

    foreach $format (@format_list) {
	if (! defined($tape_type_formats{$type, $format})) {
	    printf stderr 
"warning: '$backup_config', line $., tape type '$type' doesn't have a '$format' format.\n";
	    $errors++;
	    return;
	}

	$default_tape{$type, $format} = $fields[1];
    }
}

#
# read a "mail-to" entry from the config file
#

sub read_mail_to {
    if ($#fields < 1) {
	print stderr "warning: '$backup_config', line $., syntax should be 'mail-to address...'.\n";
	$errors++;
    }

    $mail_to = join(' ', @fields[1..$#fields]);
}

#
# read a "config-name" entry from the config file.
#

sub read_config_name {
    if ($#fields != 1) {
	print stderr "warning: '$backup_config', line $., syntax should be 'config-name name'\n";
	$errors++;
    }

    $config_name = $fields[1];
}


#
# Read a line from the given file descr, assumed to be opened to an /etc/backups
# file, and parse it.
#
# Returns (through the arg list):
#    $filesystem	name of the file system
#    $type		type of archive program to run
#    $online		directory in which to place on-line copies
#    @tags		list of tags associated with this entry
#    %chains		list of chain specs associated with this entry,
#			indexed by chain number
#    $script		script to run at start/end of backup
#
# Returns 0 at EOF, otherwise 1.
#

sub read_backups_entry {
    local(*INPUT, *host, *filesystem, *type, *stuff, *tags, *chains) = @_;
    local($line, @fields, $option, $chain, $level);

    %stuff = ();
    $stuff{"online"}  = $backup_dir;
    @tags = ();
    %chains = ();
    $stuff{"script"} = "";
    $stuff{"timeout"} = 0;

  try_again: {
      return(0) if ! ($line = <INPUT>);	# EOF, return false
      chop($line);		# remove newline
      ($line) = split(/\#/, $line);	# remove comments
      @fields = split(/[ \t]+/, $line);

      redo try_again if $#fields == -1;	# nothing in this line, try next one

      if ($#fields < 1) {
	  print stderr "warning: $hostname:$backup_file line $., syntax is 'filesystem type [tag=tag1,tag2...] [chain=N[,M]] [online=directory]\n";
	  print stderr "warning: skipping this entry.\n";
	  redo try_again;
      }

      $filesystem = shift(@fields);
      $host = $hostname;
      ($host, $filesystem) = split(/\:/, $filesystem) if $filesystem =~ m/\:/;

      $type = shift(@fields);
      @tags = ("$hostname:$filesystem");

      #
      # If the type is funky, complain about it and skip
      # it.
      #
      if ($type eq "" || $type =~ /[0-9].*/ || 
	  $valid_archive_types{$type} != 1) {
	  print stderr "warning: $hostname:$backup_file line $., unknown archive type '$type'\n";
	  print stderr "warning: skipping this entry.\n";
	  redo try_again;
      }

    option_loop:
      while ($option = shift(@fields)) {
	  if ($option =~ /^tag=(.+)$/) {
	      push(@tags, $1);
	      next option_loop;
	  }

	  if ($option =~ /^chain=(.+)$/) {
	      ($chain,$level) = split(/,/, $1);

	      if ($level eq "") {
		  $chains{$chain+0} = -1;
	      } else {
		  $chains{$chain+0} = $level + 0;
	      }

	      next option_loop;
	  }

	  if ($option =~ /^online=(.+)$/) {
	      $stuff{"online"} = $1;
	      next option_loop;
	  }

	  if ($option =~ /^script=(.+)$/) {
	      $stuff{"script"} = $1;
	      next option_loop;
	  }

	  if ($option =~ /^timeout=(.+)$/) {
	      $stuff{"timeout"} = $1;
	      next option_loop;
	  }

	  print stderr "warning: $hostname:$backup_file line $., unknown option $option, skipping this entry.\n";
	  redo try_again;
      }

    }

    return(1);
}

#
# Hack, hack.  If a full restore was done more recently than a
# level 0 save, then incrementals effectively become full saves,
# and that's a drag.  We create and use the .chainN files to
# determine when a restore was done (since restores will change
# the ctime), and if a restore was done most recently than the
# latest backup (incremental or otherwise), we use "now" as the
# backup time.  We still "do" a backup, but it almost certainly
# isn't correct.  checkbackups should check for this type of
# situation and bitch about it long and loud to ensure that
# someone does a full save soon to set things right.
#
sub check_funky_file {
    local($directory, $chain, $level, $last_level, $last_timestamp) = @_;
    local($insert, $funky_file, $funky_time);

    if ($config_name ne "") {
	$insert = ".$config_name.";
    } else {
	$insert = "";
    }

    if ($directory eq "/") {
	$funky_file = "/.chain$insert$chain";
    } else {
	$funky_file = "$directory/.chain$insert$chain";
    }

    $funky_time = time;

    #
    # If this was a level 0 backup, update the funky_file
    #
    if ($level == 0) {
	if (! open(FF, ">$funky_file")) {
	    print stderr "warning: can't open '$funky_file'\n";
	    return(0);
	}
	print FF "don't remove me - I'm part of the backup system!\n";
	close(FF);
    } else {
	if ($last_level == -1) {
	    print stderr "warning - Skipping $directory since it looks like a lower level save has never been done.\n";
	    return(0);
	} else {
	    if (! -f $funky_file) {
		print stderr "warning: - Skipping $directory since the .chain file is missing.\n";
		return(0);
	    } 
	    # fixme: if the last backup aborted, then we're in deep doo-doo 
	    # since the .chain file was touched anyway.
	    elsif (-f $funky_file && 	     
		   (@stats = stat(_)) && 
		   $stats[10] > $last_timestamp) {
	        printf stderr "warning - Looks like a full restore was done lately on $directory\n(or a level 0 backup failed).I'll do the backup anyway.\n    $funky_file touched at %s, last backup at %s\n",
	    	&ctime($stats[10]), &ctime($last_timestamp);
	    }
	}
    }

    return(1);
}


#
# Check to see if this tape claims to have a run on it, and if so, ask the 
# user whether they really want to reuse this tape.  Return 1 for yes, 0 
# for no. 
#

sub confirm_tape_usage {
    local($tape_id, $run_id) = @_;
    local(%replace, %add, %result, $answer);

    #
    # There is an entry - confirm that they really want to do this.  Read
    # the run database, print the info, and ask them.
    #
    if (&update_db($run_db_file, $run_db_file, $run_id, "run_db", $DB_READ, 
		   *replace, *add, *result)) {
	return(1);
    } else {
	print "warning: tape $tape_id contains:\n\n";
	eval "&print_run_entry($run_id, %result)";
	print $@;

	$answer = &askdef("\nDo you really want to reuse that tape", "no",
"Enter 'no' if you do not want to reuse this tape.  If you are certain that you
want to reuse this tape, then enter 'yes'.  Note that if you reuse a tape, the 
entries for all backups on that tape will be removed from the database.\n");
	if ($answer eq "y" || $answer eq "yes") {
	    return(1);
	}
    }

    return(0);
}

#
# Return the parent directory of the named directory (by removing 
# the last path component, so this might not be the real parent.
#

sub parent {
    local($directory) = @_;

    if ($directory =~ m,^(.+)/[^/]+$,) {
	return($1);
    } else {
	return("/");
    }
}

#
# Makes directories as needed to create the given path name.
#
sub make_directory {
    local($directory) = @_;
    local($i);

    @dirs = split(/\//, $directory);
    for ($i = 0; $i <= $#dirs; $i++) {
	$dir_to_make = join('/', @dirs[0 .. $i]);
	system("$mkdir_prog $dir_to_make") if (! -d $dir_to_make);
    }
}

#
# scavange for a usable tape in the named drive
#
# returns 0 if it found a usable tape, 1 if it didn't
#

sub scavange_for_tape {
    local($rsh, $dev, *id) = @_;
    local(%replace, %add, %result);
    local($retry_count);

    $retry_count = 0;

    while (1) {
	#
	# if we can't read the tape label or find a db entry for the tape,
	# complain LOUDLY and skip to the next tape
	#
	if (&tape_get_id($rsh, $dev, *id) || 
	    &update_db($tape_db_file, $tape_db_file, $id, "tape_db", 
		       $DB_READ, *replace, *add, *result)) {
	    warn "error ($0): scavange mode couldn't read the tape label (or find it in the database), skipping this tape\n";
	} else {
	    #
	    # if its promised to *, or free, success!
	    #
	    if (($result{"flags"} eq "promised" && $result{"run_id"} eq "*") ||
		($result{"flags"} eq "free")) {
		return(0);
	    }
	}

	#
	# fixme: ought to call device specific routine to cycle to next tape
	# in stacker/jukebox
	#
	# fail if we can't eject the tape
	#
	return (1) if (&tape_offload($rsh, $dev));

	# delay to wait for the stacker (if this is one) 
	# to load a new tape
	sleep($stacker_delay);

	$retry_count++;
	if ($retry_count > 10) {
	    warn "error ($0): scavange mode failed - retry count exceeded\n";
	    return(1);
	}
    }
}

1;
