#!/usr/bin/perl -w
# -*- Perl -*-
use diagnostics;
my $vcid='$Id: fs-check.in.in,v 1.30 2008/03/11 15:14:01 rockyb Exp $ ';

###########################################################################
#  Copyright (C) 2004, 2005, 2006, 2008  R. Bernstein email: <rocky@cpan.org>
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with GNU Make; see the file COPYING.  If not, write to the
#   Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
#   MA 02110-1301 USA.  You should have received a copy of the GNU
#   General Public License
###########################################################################
#  The first cut was done by Jim Kelly in ksh. It was subsequently
#  rewritten in Perl by rocky to add to option processing,
#  configuration file customization, have the looping (optionally) in
#  done in this program. And other goodies, too.
###########################################################################

use vars qw($0 $program $config_file $opt_norun $debug @EXCLUDE_PAT
	    %EXCLUDE %LIMIT %ILIMIT %OWNER %IS_PCT %ACTION %MIN_INTERVAL
	    $df_program $df_opts $mail_program $sleep_interval
	    $config_file $default_script_file $default_sleep
	    $pid_file $DEFAULT_MAX_ACTION_SECONDS $MAX_PCT $HOST $syslog
            $MAX_ACTION_SECONDS $opt_verbose $opt_once $check_inode
	    $logopened);

use strict;

sub check_fs();
sub init();
sub podthis();
sub show_version();
sub daemonize();
sub init_default_config();
sub remove_pid_file();
sub get_limit($);
sub log_and_print($);
sub log_and_quit($);
sub perform_action($$);
sub parse_limit($$$);
sub parse_ilimit($$$);
sub logger($);
sub mount_to_temp_file($);
sub mail_recently_sent($);
sub output_mount_timestamp($);

sub usage {
  my ($full_help) = @_;
  print "
usage:

  $program [-h | -help]
  $program [-f | --config *file*]
           [-v | --verbose | [--debug *n*]
           [-n | --norun]
           [-i | --inode]
           [-1 | --once]
           [-s | --sleep  *seconds*]
           [-t | --timeout *seconds*]
           [--mail *mail program*]
           [--pidfile pid-file-location]
           [--df *program*]
";

  if ($full_help) {
      print "
This program checks the disk usage locally as reported by the command:
$df_program.

The -df switch can be used to set the command run to something else,
however the output produced should be similar to the format of df.

If the disk usage is greater than that specified in the configuration file
or failing that built-in defaults, then mail will be sent out to the people
specified using the mail program $mail_program. You can change the
mail program using the --mail option.

You can parameters or options distined for the mail or df program.
However you may have to enclose the whole string in quotes to
get command parsing right. For example:
  $program --mail 'mailx -v' --config foo

A configuration file determines what to do. The default configuration
file is $config_file, but it can be specified using the -config
option.

Disks can be excluded from the check or have different usage limits;
mail recipients can be set for each disk.

If you want to see the internal LIMIT, ILIMIT, EXCLUDE, EXCLUDE_PAT,
ACTION, MIN_INTERVAL and OWNER tables after configuration file
processing, use the -v option.

The -d option gives more debug output.

For testing, --norun can be used to see what would be mailed without having
any of the actions run, just printed.

Normally the program will check inodes if that's possible from the du
command. If you don't want to check inodes, use --no-inode

Normally the program runs a disk check and then sleeps a bit. However if the
-1 or --once option is used, the program is run once.
To set the sleep interval, use the -s or --sleep option. The default sleep
interval is $default_sleep.

The program records it's process id in the file $pid_file. If it receives
a -HUP signal the configuration file is reread.

When a threshold is exceeded, a program or script is executed. The maximum
time used in executing the program can be set by --timeout. The default
value is $DEFAULT_MAX_ACTION_SECONDS seconds;
";
    }
    exit 100;
}
#
# A word about the overall program and data structures...
# Basically df is used and the output parsed. If the percent column
# exceeds the prescribed limit or is less than the prescribed free
# value then a script is called to take some action.
#
# What should be checked, who gets notified, the limits
# and the script that is to
# be run is determined by various hash tables.
# LIMIT{mount-point-name}   gives limit info about a disk.
# ILIMIT{mount-point-name}  gives limit info for inodes about a disk.
# IS_PCT{mount-point-name} specifies that limit is a percent in use (if 1),
#                          or an amount that needs to be free (if 0).
# NOTIFY{mount-point-name} is string which contains mail addresses of
#                          who to contact.
# MIN_INTERVAL{mount-point-name} time in seconds that has to elapse before
#                          we run notification and and action.
# ACTION{mount-point}      is a string which is the program to run when limit
#                          is exceeded. An argument, the mount-point is passed.
# HEADER{mailto}           is the header of the mail to be sent
# MAIL{mailto}             is the body of the mail to be sent.
#
use Sys::Syslog;

init();
process_options();

# Now do some work...
install_trap_handlers();

read_config_file($config_file);

# Handle verbose processing: print out internal tables built from
# reading the configuration file.
if ($opt_verbose) {
  print "mail program: $mail_program\n";
  print "disk usage program: $df_program\n";
  print "========================================\n";
  foreach my $key (sort(keys(%OWNER))) {
    print "OWNER{$key} = $OWNER{$key}\n";
  }
  foreach my $key (sort(keys(%LIMIT))) {
    print "LIMIT{$key} = $LIMIT{$key}",
      $IS_PCT{$key} ? '%' : ' KB', "\n";
  }
  foreach my $key (sort(keys(%ILIMIT))) {
    print "ILIMIT{$key} = $ILIMIT{$key}",
      $IS_PCT{$key} ? '%' : ' KB', "\n";
  }
  foreach my $key (sort(keys(%MIN_INTERVAL))) {
    print "MIN_INTERVAL{$key} = $MIN_INTERVAL{$key} seconds\n";
  }
  foreach my $key (sort(keys(%ACTION))) {
    print "ACTION{$key} = $ACTION{$key}\n";
  }
  foreach my $key (sort(keys(%EXCLUDE))) {
    print "disk $key excluded\n";
  }
  foreach my $key (sort @EXCLUDE_PAT) {
    print "disks matching pattern $key excluded\n";
  }
  print "========================================\n";
}

if ($opt_once) {
  create_pid_file();
  check_fs();
} else {
  daemonize();
  create_pid_file();
  while (1) {
    check_fs();
    print "Sleeping for $sleep_interval seconds\n" if $opt_verbose;
    sleep $sleep_interval;
  }
}

remove_pid_file();
closelog if $syslog;
exit 0;

# Check out the disks once...
sub check_fs() {
  my %HEADER;
  my %MAIL;
  my($str, $mailto);
  my $df_after_fs = '\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(\S+)$';

  for my $i (0..1) {
    if ($i == 1) {
      last if !0 || !$check_inode;
      $df_program .= ' -i'
    }
    my @CHECK=`$df_program 2>&1`;
  CHECK_LINE:
    while (@CHECK) {
      my($disk, $kb, $used, $avail, $pct, $mount);
      $_ = shift @CHECK;
      # format of lines should look like
      # Filesystem            kbytes     used    avail capacity  Mounted on
      # /dev/ad0a              30799    23532     5728    80%    /
      # or it could be on two lines like this:
      # /dev/vx/dsk/datadg/data
      #          17681780 7491096 10031536    43%    /data
      if (/^(\S+)$/) {
	# Check for the latter case of df output for a filesystem
	# spilling over two lines.
	$disk=$1;
	next if !@CHECK || $CHECK[0] !~ /$df_after_fs/;
	($kb, $used, $avail, $pct, $mount) = ($1, $2, $3, $4, $5);
	shift @CHECK;
      } else {
	# More common case of df output for a filesystem fits on a single
	# line.
	next if !(/^(\S+)$df_after_fs/);
	($disk, $kb, $used, $avail, $pct, $mount)
	  = ($1, $2, $3, $4, $5, $6);
      }
      next if defined($EXCLUDE{$mount});
      foreach my $exclude_pat (@EXCLUDE_PAT) {
	next CHECK_LINE if $mount =~ m{$exclude_pat};
      }
      my($thing, $limit) = get_limit($mount);
      next if ( $IS_PCT{$thing} && $pct <= $limit) ||
	(!$IS_PCT{$thing} && $avail >= $limit) ;
      next if mail_recently_sent($mount);
      $mailto = defined($OWNER{$mount}) ? $mount : 'default' ;
      $MAIL{$mailto} .= sprintf("%-15s %-15s %9d       %3d%%\n",
				$disk, $mount, $avail, $pct);
      $thing = defined($ACTION{$mount}) ? $mount : 'default' ;
      my $output = perform_action($thing, $mount);
      $MAIL{$mailto} .= $output;
      if (defined($IS_PCT{$thing}) ? $IS_PCT{$thing} : $IS_PCT{'default'}) {
	$str = "${mount}: $pct%>=$limit%";
      } else {
	$str = "${mount}: $avail KB<=$limit KB";
      }
      $HEADER{$mailto} .= $HEADER{$mailto} ? ", $str" : "$str";
    }
  }

  #
  # Now mail out results.
  #
  my $title="Disk\t\tMount point\tAvailable\tUsage\n" .
    "====\t\t===========\t=========\t=====";
  foreach my $key (keys(%MAIL)) {
    # Note we can't say what the limit is because it might not be
    # a single value.
    my $HEADER = "ALERT: $HOST disks filling ($HEADER{$key}).";
    my $body = "$HEADER\n$title\n$MAIL{$key}";
    if (!$opt_norun) {
      if ( $mail_program ne 'Mail::Send' ) {
	  my $tmpfile="/tmp/$program$$.txt";
	  open(BODY, ">$tmpfile")
	      || log_and_quit("Can't open $tmpfile for writing: $!");
	  print BODY $body;
	  close(BODY);
	  my $output =
	      `$mail_program -s "$HEADER" $OWNER{$key} < $tmpfile`;
	  print $output if $output;
	  unlink $tmpfile;
      } else {
	  eval 'use Mail::Send';
	  my $msg = new Mail::Send Subject=>$HEADER, To=>$OWNER{$key};
	  my $fh  = $msg->open;
	  print $fh $body;
      }
    } else {
      print "Mail to: $OWNER{$key}\n";
      print $body;
    }
  }
}

# Routine to read the files given as the argument.
# Globals hash tables %LIMIT, %ILIMIT, %OWNER, %ACTION, %EXCLUDE and
# array @EXCLUDE_PAT.
# Blank lines and comments lines---those beginning with # are ignored.
sub read_config_file {
    my($config_file) = @_;
    my ($action_prog, $action_opts);

    init_default_config();
    if (!defined($config_file)) {
      log_and_print("No configuration file given using internal defaults.\n");
      return;
    } elsif (!-f $config_file) {
      log_and_print("Can't find $config_file;" .
		    " using internal defaults instead\n");
      return;
    }
    log_and_quit("Not a text file: $config_file.")
      if ! -T $config_file;
    # Read the data ignoring blank and comment lines.
    open(CONFIG_FILE, $config_file) ||
      log_and_quit("Cannot open configuration file $config_file for reading: $!\n");
    my $use_pat ='\d+([k,m,g]b?|[%]?)';
    for (my $line_no=1; <CONFIG_FILE>; $line_no++) {
      next if /^#/ || /^\s*$/;
      chomp;
      if (/^EXCLUDE\s+(\S+)/i) {
	$EXCLUDE{$1}++;
      } elsif (/^EXCLUDE_PAT\s+(\S+)/i) {
	push @EXCLUDE_PAT, $1;
      } elsif (/^OWNER\s+(\S+)\s+(\S+)\s+($use_pat)/i) {
	$OWNER{$1} = $2;
	parse_limit($line_no, $1, $3);
      } elsif (/^OWNER\s+(\S+)\s+(\S+)/i) {
	$OWNER{$1} = $2;
      } elsif (/^ACTION\s+(\S+)\s+(\S+)(.*)$/i) {
	$action_prog = $2;
	$action_opts = $3;
	if ( ! -x $action_prog ) {
	  log_and_print("Warning: $action_prog is not executable. " .
			"Line $line_no not processed.");
	} else {
	  $ACTION{$1} = "$action_prog$action_opts";
	}
      } elsif (/MIN_INTERVAL\s+(\S+)\s+(\d+)/i) {
	$MIN_INTERVAL{$1} = $2;
      } elsif (/LIMIT\s+(\S+)\s+($use_pat)/i) {
	parse_limit($line_no, $1, $2,);
      } elsif (/ILIMIT\s+(\S+)\s+($use_pat)/i) {
	parse_ilimit($line_no, $1, $2,);
      } elsif (/DEFAULT\s+LIMIT\s+($use_pat)/i) {
	parse_limit($line_no, 'default', $1,);
      } elsif (/DEFAULT\s+ILIMIT\s+($use_pat)/i) {
	parse_ilimit($line_no, 'default', $1,);
      } elsif (/DEFAULT\s+MIN_INTERVAL\s+(\d+)/i) {
	$MIN_INTERVAL{'default'} = $1;
      } elsif (/DEFAULT\s+OWNER\s+(\S+)\s+($use_pat)/i) {
	$OWNER{'default'} = $1;
	parse_limit($line_no, 'default', $2,);
      } elsif (/DEFAULT\s+OWNER\s+(\S+)/i) {
	$OWNER{'default'} = $1;
      } elsif (/DEFAULT\s+ACTION\s+(\S+)(.*)$/i) {
	$action_prog = $1;
	$action_opts = $2;
	if ( ! -x $action_prog ) {
	  log_and_print("Warning: $action_prog is not executable. " .
			"Line $line_no not processed.");
	} else {
	  $ACTION{'default'} = "$action_prog$action_opts";
	}
      }
    }
    # For now no substitutions, so use command file as-is.
    close(CONFIG_FILE) || log_and_quit("close failed for $config_file: $!\n");
}

# Parse:
# <percent full or amount full> in a limit directive.
# <amount full> is either the percentage of the disk full or the number of
# K,M,G bytes that need to be available. The way we distinguish the first
# case from the second is to add a K, KB, M, MB, G, GB  at the end.
# An optional % can be used too.
#
sub parse_limit($$$) {
  my($line_no, $mount, $str) = @_;
  if ($str =~ /(\d+)([k,m,g])b?/i) {
    my($unit) = $2;
    my($amount) = $1;
    if ($unit =~ /[kK]/) {
      $LIMIT{$mount}  = $amount;
    } elsif ($unit =~ /[mM]/) {
      $LIMIT{$mount}  = $amount * 1024;
    } elsif ($unit =~ /[gG]/) {
      $LIMIT{$mount}  = $amount * 1024 * 1024;
    }
    $IS_PCT{$mount} = 0;
  } elsif ($str =~ /(\d{1,3})[%]?/) {
    if ( $1 >= 0 && $1 <= $MAX_PCT ) {
      $LIMIT{$mount} = $1;
      $IS_PCT{$mount} = 1;
    } else {
      log_and_print("Warning: percentage $1 given. Should be be between".
		    " 0 and $MAX_PCT." .
		    "\nLine $line_no not fully processed.");
    }
  } else {
    log_and_print("Warning: something's wrong with program on pat $str".
		  ". Line $line_no not processed.");
  }
}

# Parse:
# <percent full or amount full> in a inode limit directive.
# <amount full> is either the percentage of the disk full or the number of
# K,M,G bytes that need to be available. The way we distinguish the first
# case from the second is to add a K, KB, M, MB, G, GB  at the end.
# An optional % can be used too.
#
sub parse_ilimit($$$) {
  my($line_no, $mount, $str) = @_;
  if ($str =~ /(\d+)([k,m,g])b?/i) {
    my($unit) = $2;
    my($amount) = $1;
    if ($unit =~ /[kK]/) {
      $ILIMIT{$mount}  = $amount;
    } elsif ($unit =~ /[mM]/) {
      $ILIMIT{$mount}  = $amount * 1024;
    } elsif ($unit =~ /[gG]/) {
      $ILIMIT{$mount}  = $amount * 1024 * 1024;
    }
    $IS_PCT{$mount} = 0;
  } elsif ($str =~ /(\d{1,3})[%]?/) {
    if ( $1 >= 0 && $1 <= $MAX_PCT ) {
      $ILIMIT{$mount} = $1;
      $IS_PCT{$mount} = 1;
    } else {
      log_and_print("Warning: percentage $1 given. Should be be between".
		    " 0 and $MAX_PCT." .
		    "\nLine $line_no not fully processed.");
    }
  } else {
    log_and_print("Warning: something's wrong with program on pat $str".
		  ". Line $line_no not processed.");
  }
}

#
# Return the limit for the passed argument, "mount". This is the
# threshhold for "mount" that we want to start taking action on.
#
sub get_limit($) {
    my($mount) = @_;
    return defined($LIMIT{$mount})
	? ($mount,    $LIMIT{$mount})
	: ('default', $LIMIT{'default'});
}

# Convert a mount point name into temporary file name which will be
# used to store when we last warned about that filesystem.  For
# example for /usr/local we'll use /tmp/fs-check--usr--local.timestamp

sub mount_to_temp_file($) {
  my($mount) = @_;
  my $mount_notify_file;
  ($mount_notify_file = $mount) =~ s:/:--:;
  $mount_notify_file = "/tmp/fs-check${mount_notify_file}.timestamp";
  return $mount_notify_file;
}

# Write a timestamp into $timestamp_file.
sub output_mount_timestamp($) {
  my($timestamp_file) = @_;
  open(TIMESTAMP, ">$timestamp_file") or return 0;
  print TIMESTAMP time(), "\n";
  close(TIMESTAMP);
  return 1;
}

# See if mail has been sent about $mount recently.
# If not we'll assume that we're going to send mail and record now
# as the time we last sent a notification.
sub mail_recently_sent($) {
  my($mount) = @_;
  my $mount_notify_file = mount_to_temp_file($mount);
  if (-f $mount_notify_file) {
    # open file and compare timestamp with now.
    # return true if recently sent.

    my $min_interval = defined($MIN_INTERVAL{$mount}) 
			       ? $MIN_INTERVAL{$mount} 
			       : $MIN_INTERVAL{'default'};
    if (open(TIMESTAMP, "<$mount_notify_file")) {
      my $last_notify = <TIMESTAMP>;
      chomp($last_notify);
      close(TIMESTAMP);
      return 1 if
	$last_notify =~ /\d+/ && (time() - $last_notify) < $min_interval;
    }
  }
  output_mount_timestamp($mount_notify_file);
  return 0;
}

#
# Perform some sort of action based on the passed argument.
# A timer is used to limit how long the action will be allowed to run
#

sub perform_action($$) {
  my($mount_key, $mount) = @_;
  my($output, @output);
  print "Running $ACTION{$mount_key} with $mount...\n" if $opt_verbose;
  # Need to fork to put a timer on how long the script is to run.
  my $tmpfile="/tmp/$program$$.txt";
  if (my $pid = fork()) {
    # In parent. put a time limit on how long the script is to run.
    for (my $i=0; $i<$MAX_ACTION_SECONDS; $i++) {
      print "$i secs\n" if $debug > 2;
      goto CHILD_DONE if waitpid($pid,1) == -1;
      sleep 1;
    }
    print "killing $pid\n" if $opt_verbose;
    logger("killing $pid: $ACTION{$mount_key}\n");
    kill 3, $pid;
    @output =
      ("==== Warning: output not complete after more than " .
       "$MAX_ACTION_SECONDS seconds. ====\n", "\n");
  CHILD_DONE:
    open(INPUT, "$tmpfile")
      || log_and_quit("Can't open $tmpfile for reading: $!");
    push(@output, <INPUT>);
    $output .= join('', @output);
    return $output;
  } else {
    # In child. Do it.
    `$ACTION{$mount_key} $mount >$tmpfile 2>&1`;
    exit;
  }
}

# log the passed message to syslog.
sub logger($) {
  my($msg) = shift;

  if (!$logopened) {
    $logopened++;
    openlog($program,'cons,pid', 'err');
  }
  syslog('info', $msg);
}

# log the passed message to syslog and also print it.
sub log_and_print($) {
    my $msg = shift;
    logger($msg);
    print "$msg\n";
}

# log the passed message to syslog and then die with that message.
sub log_and_quit($) {
  my $msg = shift;
  logger($msg);
  die "$msg\n";
}

sub install_trap_handlers {
  $SIG{'HUP'} ='hup_trap_handler';
  $SIG{'QUIT'}='terminate';
  $SIG{'TERM'}='terminate';
}

# Come here on getting TERM and QUIT shutdown signals.
# Clean up and exit program.
sub terminate {
  my ($signo) = @_;
  $SIG{$signo} ='IGNORE';  # Don't need to do this signal more than once.
  &log_and_print("Received shutdown signal: ${signo}.\n");
  remove_pid_file();
  # Convert signal to a number if number wasn't specified (e.g. TERM vs 15).
  # There's probably a way to change the TERM to the corresponding number,
  # but I don't know how to easily
  $signo = 5 if $signo !~ /\d+/;
  closelog if $syslog;
  exit $signo;
}

sub hup_trap_handler {
  my ($signo) = @_;
  $SIG{$signo} ='IGNORE';  # Don't need to do this signal more than once.
  if (defined($config_file)) {
    &log_and_print("Received signal: ${signo}. Rereading config file $config_file\n");
  } else {
    &log_and_print("Received signal: ${signo}; resetting configuration.");
  }

  read_config_file($config_file);
  $SIG{$signo} ='hup_trap_handler';
}

sub create_pid_file {
  if (open(PID_FILE, ">$pid_file")) {
    print PID_FILE "$$\n";
    close(PID_FILE);
  } else {
    log_and_print("Can't open $pid_file to store process id. Skipping.\n");
  }
}

sub remove_pid_file() {
  return unlink $pid_file;
}

sub init_default_config() {
  $LIMIT  {'default'} = 90 if ! defined($LIMIT{'default'});
  $ILIMIT {'default'} = 90 if ! defined($ILIMIT{'default'});
  $OWNER  {'default'} = 'root' if ! defined($OWNER{'default'});
  $IS_PCT {'default'} = 1;
  $MIN_INTERVAL{'default'} = 60 * 60; # 1 hour
  $ACTION {'default'} = $default_script_file;
}

sub init() {
  use File::Basename;
  $program = basename($0); # Who am I today, anyway?

  $mail_program        = 'mailx';
  $config_file         = "/opt/local/etc/fs-check.conf";
  $default_script_file = "/opt/local/bin/fs-report";
  $sleep_interval      = 60*60;
  $pid_file            = "/opt/local/var/run/$program.pid";
  $df_opts             = "";
  $debug               = 0;
  $opt_norun           = 0;
  $opt_verbose         = 0;
  $opt_once            = 0;
  $check_inode         = 1;

  $_ = `uname -s -r`;

  $df_program          = "/bin/df $df_opts";
  # How much time to sleep in-between file-system checks.
  $default_sleep       = 15*60;  # 15 minutes.

  # When a threshold is exceeded, a program or script is executed.
  # Below we set the number of seconds we are willing to wait for completion of
  # such a program if it is not otherwise set.
  $DEFAULT_MAX_ACTION_SECONDS=7*60;  # convert to minutes.
  $MAX_PCT = 120;

  $HOST=`hostname`; chomp($HOST);
}

# Show the CVS version id string and quit.
sub show_version() {
  print "$vcid
Copyright (C) 2004, 2005, 2006 Rocky Bernstein.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
";
  exit 10;
}


sub daemonize() {
  chdir '/'                 or die "Can't chdir to /: $!";
  defined(my $pid = fork)   or die "Can't fork: $!";
  exit 0 if $pid;
  use POSIX qw(setsid);
  setsid()                  or die "Can't start a new session: $!";
  umask 0;
}

# Deal with the f*cking options....
sub process_options {
  use Getopt::Long;
  $Getopt::Long::autoabbrev = 1;

  my ($help, $long_help, $show_version, $opt_timeout);

  my $result = &GetOptions
    (
     'help'        => \$help,
     'doc'         => \$long_help,
     '1|once'      => \$opt_once,
     'sleep:n'     => \$sleep_interval,
     'f|config:s'  => \$config_file,
     'timeout:n'   => \$opt_timeout,
     'df:s'        => \$df_program,
     'mail:s'      => \$mail_program,
     'pidfile:s'   => \$pid_file,
     'inode!'      => \$check_inode,
     'd|debug:n'   => \$debug,
     'v|verbose'   => \$opt_verbose,
     'version'     => \$show_version,
     'n|norun'     => \$opt_norun,
    );

  show_version() if $show_version;
  usage(1) if $help;
  podthis() if $long_help;

  usage(0) if !$result;

  $debug=1 if $opt_verbose;

  $MAX_ACTION_SECONDS = $opt_timeout ? $opt_timeout
    : $DEFAULT_MAX_ACTION_SECONDS;
}

sub podthis() {
  use Pod::Text;
  $^W = 0;
  pod2text $0;
  exit 101;
}

__END__
=pod

=head1 NAME

fs-check - size report daemon/program

=head1 SYNOPSIS

B<fs-check> [I<options>...] [directory]

Checks the disk usage locally for filling or full filesystems.


=head1 DESCRIPTION

fs-check checks the disk usage locally as reported by the command for
filling or full filesystems. It determines what filesystems to check
what threshold to use for an alert, and what to do based on a
user-customizable configuration file.

The program records it's process id in the file If it receives a -HUP
signal the configuration file is reread.

=head2 OPTIONS

=over 4

=item --help

Give rudimentary help and exit

=item --version

show a CVS version string and exit

=item --debug I<integer>

give debugging output. The higher the
number, the more the output. Default is no debug information (0).

=item --df I<path to df-program and options>

In order to get filesystem size information the program "df" is run
and it output has to be in a format that this program recognizes. When
this package was configured we look for a suitably compatible df
program.  However you can override this default or specify the df
program explicitly with this option.

The default is

   /bin/df 

=item --mail I<mail program>

When a threshold has been exceeded a report is usually mailed out.
The Perl module Mail::Send can be used. However another possibility is
an external program. The default program here is mailx.

=item --once

Normally the program runs a disk check and then sleeps a bit. However
if the -1 or --once option is used, the program is run once.

=item --pidfile I<pid file location>

When fs-check start as a daemon it write its process id in a file.

Normally you shouldn't have to set this or worry about it. The default
is /opt/local/var/run/fs-check.pid

=item --sleep I<seconds>

When this program is run as a daemon (e.g. the --once option is not in
effect) the period to wait in seconds between is set by this parameter.

=item --timeout I<seconds>

When a one of the thresholds for a filesystem is exceeded, a program
or script is executed. The maximum time used in executing that program
can be set by C<--timeout>.

=back

=head1 CONFIGURATION FILE FORMAT

A configuration file for fs-check specifies:

=over

=item 1.

what filesystems are checked/excluded. Included filesystems
are specified with OWNER and excluded filesystems are
specified with EXCLUDE or EXCLUDE_PAT with optionally a DEFAULT prefix.

=item 2.

at what point to start to complain. A threshold is specified with
LIMIT for filesystem space or ILIMIT for inode space with optionally
a DEFAULT prefix.

=item 3.

who to send mail when there is trouble. A mail contact is specified
with OWNER with possibly a DEFAULT prefix.

=item 4.

what program to run when there is trouble. A program to run is
specified with ACTION with optionally a DEFAULT prefix.

=item 5.

the shortest interval between which we send notifications. This is
specified via MIN_INTERVAL with optionally a DEFAULT prefix.

=back

Lines with # in column 1 or blank lines are ignored. Actually, any
line that doesn't match a valid line is ignored.

So what I<isn't> ignored? Lines that begin in column 1 with EXCLUDE,
EXCLUDE_PAT, OWNER, LIMIT, ACTION, or DEFAULT. Below we give the
format of each of these directives.

Case is not significant

=over

=item EXCLUDE I<mount-point>

=item EXCLUDE_PAT I<mount-point-regexp>

=item OWNER  I<mount-point> I<email-address> I<amount full>

=item OWNER I<mount-point>  I<email-address>

=item LIMIT I<mount-point> I<amount full>

=item ILIMIT I<mount-point> I<amount full>

=item ACTION I<mount-point> I<script-or-program>

=item MIN_INTERVAL I<mount-point> I<seconds>

=item DEFAULT OWNER I<email-address>

=item DEFAULT OWNER I<email-address> I<amount full>

=item DEFAULT LIMIT I<amount full>

=item DEFAULT ILIMIT I<amount full>

=item DEFAULT ACTION I<script>

=item DEFAULT MIN_INTERVAL I<seconds>

=back


I<amount full> is either the percentage of the disk full or the number of
bytes that need to be available. The way we distinguish the first
case from the second is to add a K, KB, M, MB, G, or GB at the end.
An optional % can be used to specify percent for more clarity, although
if just a number is used it is taken as a percentage.

For example "default 90" and "DEFAULT 90%" are the same thing and mean
that mail should be sent out on any disk that is 90% or more full
unless otherwise specified by a more specific LIMIT, ILIMIT or OWNER
(with amount) line.

In contrast, 4096KB, 4096k, or 4MB means that 4 megabytes must be available
on the disk. You may want to use this form for say the root partition.

If there are many lines which refer to the same disk, or change the default
(which can be done in a couple ways), the last one sticks.

=head2 SAMPLE CONFIGURATION FILE

  # This is who gets mail when something's wrong and no further specification
  default owner  rocky 95%

  # This is the program to run when no other program is specified...
  default action /usr/local/bin/fs-report

  # Limit at which to complain about i-nodes...
  default ilimit 99%

  # Don't sent out notifications if they occur less than 30 minutes
  # (1800 seconds) from the last notification
  default min_interval 1800

  # The limits for the directory which holds /tmp should not be
  # too close to the maximum limit. The file system checker stores its
  # temporary data there. So if this is too full, we won't get a useful
  # report back.
  #limit   /      70%
  #limit   /var   90%
  #limit   /usr	95%
  #limit   /src   90%
  limit    /home	90%
  action  /var /usr/local/bin/fs-report --nocore

  # Don't check any filesystem starting /cdrom...
  exclude_pat ^/mnt/cdrom
  exclude_pat ^/cdrom


  # Filesystems not listed, e.g /home, would be checked as they come
  # under the default. If you want a disk exclude them, create/uncomment
  # an exclude line such as the one below.
  exclude /mnt/floppy
  exclude /mnt/msfloppy
  exclude /mnt/cdrom
  exclude /mnt/cdrom2
  exclude /mnt/dvd

=head1 SECURITY CONSIDERATIONS

Any daemon such as this one which is sufficiently flexible is a
security risk. The configuration file allows arbitrary commands to be
run. In particular if this daemon is run as root and the configuration
file is not protected so that it can't be modified, a bad person could
have their programs run as root.

There's nothing inherent in fs-check, that requires one to run this
daemon as root.

So as with all daemons, one needs to take usual security precautions
that a careful sysadmin/maintainer of a computer would. If you can run
any daemon as an unprivileged user (or with no privileges), do it! If
not, set the permissions on the configuration file and the directory
it lives in. Commands that need to be run as root you can run via
sudo.  On Solaris, I often run process accounting which tracks all
commands run. Tripwire may be useful to track changed configuration
files.

=head1 TROUBLESHOOTING

To debug a configuration file the following options are useful:

   fs-check --norun -1 --debug 2 *configuration-file*

For even more information and control try running the above under the
Perl debugger, e.g.

  perl -d fs-check --norun -1 --debug *configuration-file*

=head1 SEE ALSO

My log rotation program L<http://recycle-logs.sourceforge.net> and
file removal/archival program L<http://rm-old-files.sourceforge.net>
might assist in maintenance the filesystem so it doesn't fill up.

=head1 BUGS

Has a number of Unixisms and non-generalities:

=over

=item * assumes Unix file separator path "/"

=item * assumes spaces delimit filenames (no embedded blanks)

=item * GNU find -ls output format (the number of fields which may
have changed over the years).

=item * Stores temporary files in /tmp and assumes there's space for
whatever we need to do in that

=item * That our mount-pount to filename conversion (/ => -- will not
get confused with a valid mount point. For example one could have a
mount point called /usr--local which we'll confuse with /usr/local.

=back

Please volunteer to fix any of these.

=head1 AUTHOR

The current version is maintained (or not) by C<rocky@cpan.org>.

=head1 COPYRIGHT

  Copyright (C) 2004, 2005, 2005, 2006 Rocky Bernstein, email: rocky@cpan.org.
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with GNU Make; see the file COPYING.  If not, write to the
  Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
  MA 02111-1307, USA.

I<$Id: fs-check.in.in,v 1.30 2008/03/11 15:14:01 rockyb Exp $>

=cut

