#!/usr/bin/perl -w
#############################################################################
# License:
#
# This software (hereafter referred to as "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.
# Note that when redistributing modified versions of this source code, you
#   must ensure that this disclaimer and the above coder's names are included
#   VERBATIM in the modified code.
#
# Disclaimer:
# This program is provided with no warranty of any kind, either expressed or
#   implied.  It is the responsibility of the user (you) to fully research and
#   comprehend the usage of this program.  As with any tool, it can be misused,
#   either intentionally (you're a vandal) or unintentionally (you're a moron).
#   THE AUTHOR(S) IS(ARE) NOT RESPONSIBLE FOR ANYTHING YOU DO WITH THIS PROGRAM
#   or anything that happens because of your use (or misuse) of this program,
#   including but not limited to anything you, your lawyers, or anyone else
#   can dream up.  And now, a relevant quote directly from the GPL:
#
#                           NO WARRANTY
#
#  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
# FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
# OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
# PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
# OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
# TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
# PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
# REPAIR OR CORRECTION.
#
#   ---
#
# Whee, that was fun, wasn't it?  Now let's all get together and think happy
#   thoughts - and remember, the same class of people who made all the above
#   legal spaghetti necessary are the same ones who are stripping your rights
#   away with DVD CCA, DMCA, stupid software patents, and retarded legal
#   challenges to everything we've come to hold dear about our internet.
#   So enjoy your dwindling "freedom" while you can, because 1984 is coming
#   sooner than you think.  :[
#
#######################################################################################
# 
# Purpose:
#     To create a program that checks available system file handles.
#     Output is sent to syslogd or STDOUT (console)
# 
# 
# Technical Description:
#     Script reads /proc/sys/fs/file-nr and calculates percentage of available
#     file handles. Thresholds that trigger syslogging of OS-WARN, OS-ERROR, OS-CRIT,
#     and OS-EMERG are configurable.
# 
# Changelog:
#     01/20/2004 - v1.20 (brandon zehm)
#         - Require a -r option to run.
#         - Added a -l option for logging to a file
#         - Textual changes to most error messages.
#
#     08/14/2003 - v1.10 Altered code to allow threshold values to be variables, instead
#                        of fixed. Re-arranged field order of syslog messages to match
#                        field order of other monitoring scripts. Minor cleanups
#                        elsewhere. (Paul Kreiner  deacon@thedeacon.org)
# 
#     11/19/2002 - v1.00 Initial creation date
#
#######################################################################################
use strict;
use IO::Socket;


## Global Variable(s)
my %conf = (
    "programName"          => 'monitorFilehandles.pl',           ## Program name and version
    "version"              => '1.20',                            ## Version
    "authorName"           => 'Greg Byrd',                       ## Author's Name
    "authorEmail"          => 'gregory.byrd@hp.com',             ## Author's Email Address
    "debug"                => 0,                                 ## Debug level - this should be changed via -v command line option
    "hostname"             => $ENV{'HOSTNAME'},                  ## Used in printmsg() for all output.
        
    ## The following entries are used later in the program
    "error"                => '',                                ## Functions that exit with an error status should put their error message here
    "threshold"            => '',                                ## When calling printmsg(), the logging threshold (text) gets put here
    "facility"             => 1,                                 ## The default syslog facility - can be overriden with command line args
    "priority"             => 6,                                 ## The default syslog priority.
    "run"                  => 0,                                 ## Whether we should run script or not
    "syslog"               => 1,                                 ## Syslog messages by default (--stdout disables this)
    "syslogProgram"        => "/usr/local/bin/syslog.pl",
    "stdout"               => 0,                                 ## Enabled via --stdout.  Tells printmsg to print to stdout rather than syslogd
    "logging"              => '',                                ## If this is true the printmsg function prints to the log file
    "logFile"              => '',                                ## If this is specified (form the command line via -l) this file will be used for logging.

    ## Set some thresholds, these are the threshold percentages that trigger various warnings.
    "thresholdWARNING"     => 75,
    "thresholdERROR"       => 85,
    "thresholdCRIT"        => 95,
    "thresholdEMERG"       => 99,

);
$conf{"programName"} =~ s/(.)*[\/,\\]//;                         ## Remove path from programName









#############################
##                          ##
##      MAIN PROGRAM         ## 
##                          ##
#############################




## Initialize
initialize();

## Process Command Line
processCommandLine();

## Open the log file if we need to
if ($conf{'logFile'}) {
    if ($conf{'logging'}) {
        $conf{'logging'} = 0;
        close LOGFILE;
    }
    if (openLogFile($conf{'logFile'})) { quit("OS-ERROR => Opening the log file [$conf{'logFile'}] returned the error: $!", 1); }
}

## Get the load average (try up to 10 times)
my @input = ();
for (my $counter = 0; $counter <= 10; $counter++, sleep(10)) {
    quit("OS-ERROR - 10 consecutive errors while trying to read /proc/sys/fs/file-nr", 1) if ($counter >= 10);
    open(FILE, "/proc/sys/fs/file-nr") or next;
    @input = split(/\s+/, <FILE>);
    close FILE;
    chomp @input;
    last if ($input[2]);
}


my $usage = ($input[2] - $input[1]);
my $percent_usage = sprintf("%.02f", ($input[1] / $input[2]) * 100 );

# DEBUG
printmsg("OS-DEBUG => The number of allocated filehandles is: $input[0]", 1);


## Compare percent of file handles available, and send syslog message when percent used is greater 
## than configured trigger thresholds.

  ## EMERGENCY ##
  if ( $percent_usage >= $conf{'thresholdEMERG'}) {
    $conf{'threshold'} = "EMERG";
  }

  ## CRIT ##
  elsif ( $percent_usage >= $conf{'thresholdCRIT'} ) {
    $conf{'threshold'} = "CRIT";
  }

  ## ERROR ##
  elsif ( $percent_usage >= $conf{'thresholdERROR'} ) {
    $conf{'threshold'} = "ERROR";
  }

  ## WARNING ##
  elsif ( $percent_usage >= $conf{'thresholdWARNING'} ) {
    $conf{'threshold'} = "WARNING";
  }

  else {
    $conf{'threshold'} = "INFO";
  }

## Generate the output message
printmsg("OS-$conf{'threshold'} => $percent_usage percent of system filehandles are in use [$input[1] of $input[2]]");

## Quit
quit("",0);











######################################################################
## Function:    help ()
##
## Description: For all those newbies ;) 
##              Prints a help message and exits the program.
## 
######################################################################
sub help {
print <<EOM;

$conf{'programName'}-$conf{'version'} by $conf{'authorName'} <$conf{'authorEmail'}>

Synopsis: Checks available file handles via /proc, 
          and generates output messages to syslog.

Usage:  $conf{'programName'} -r [options]

  Required:
    -r                        run script
  
  Optional:
    --stdout                  print messages to STDOUT rather than the syslog.
    --facility=[0-11]         syslog facility (1/USER is used by default)
    -l <logfile>              enable logging to the specified file
    -v                        verbosity - use multiple times for greater effect

EOM
exit(1);
}










######################################################################
##  Function: initialize ()
##  
##  Does all the script startup jibberish.
##  
######################################################################
sub initialize {
    
    ## Set STDOUT to flush immediatly after each print  
    $| = 1;
    
    ## Intercept signals
    $SIG{'QUIT'}  = sub { quit("EXITING - Received SIG$_[0]", 1); };
    $SIG{'INT'}   = sub { quit("EXITING - Received SIG$_[0]", 1); };
    $SIG{'KILL'}  = sub { quit("EXITING - Received SIG$_[0]", 1); };
    $SIG{'TERM'}  = sub { quit("EXITING - Received SIG$_[0]", 1); };
    
    ## ALARM and HUP signals are not supported in Win32
    unless ($^O =~ /win/i) {
        $SIG{'HUP'}   = sub { quit("EXITING - Received SIG$_[0]", 1); };
        $SIG{'ALRM'}  = sub { quit("EXITING - Received SIG$_[0]", 1); };
    }
    
    ## Fixup $conf{'hostname'}
    if ($conf{'hostname'}) {
        $conf{'hostname'} = $conf{'hostname'};
        $conf{'hostname'} =~ s/\..*$//;
    }
    else {
        $conf{'hostname'} = "unknown";
    }
  
    return(1);
}













######################################################################
##  Function: processCommandLine ()
##  
##  Processes command line storing important data in global var %conf
##  
######################################################################
sub processCommandLine {
    
    
    ############################
    ##  Process command line  ##
    ############################
    
    my @ARGS = @ARGV;
    my $numargv = @ARGS;
    help() unless ($numargv);  ## Print help if there are no command line parameters
    for (my $counter = 0; $counter < $numargv; $counter++) {
        
        if ($ARGS[$counter] =~ /^-h$|^--help$|help/io) {     ## Help ##
            help();
        }
        
        elsif ($ARGS[$counter] =~ s/^-v+//i) {               ## Verbosity ##
            $conf{'debug'} += (length($&) - 1);
        }
        
        elsif ($ARGS[$counter] =~ /^-l$/) {                  ## Log File ##
            $counter++;
            $conf{'logFile'} = $ARGS[$counter];
        }
        
        elsif ($ARGS[$counter] =~ /^-r$/) {                  ## Run
            $conf{'run'} = 1;
        }
        
        elsif ($ARGS[$counter] =~ /^--stdout$/) {            ## Print to STDOUT
            $conf{'stdout'} = 1;
            $conf{'syslog'} = 0;
        }
        
        elsif ($ARGS[$counter] =~ s/^--facility=//) {        ## Facility
            $conf{'facility'} = $ARGS[$counter];
        }
        
        else {                                               ## Invalid Option ##
            quit("OS-ERROR => The option [$ARGS[$counter]] is unknown.  Try --help.", 1);
        }
        
    }
    
    
    ###################################################
    ##  Verify required variables are set correctly  ##
    ###################################################
    
    ## If the user didn't use a -r print the help
    if ($conf{'run'} == 0) {
        help();
    }
    
    return(0);
}















###############################################################################################
##  Function:    printmsg (string $message, int $level)
##
##  Description: Handles all messages - 
##               Depending on the state of the program it will log
##               messages to a log file, print them to STDOUT or both.
##               
##
##  Input:       $message          A message to be printed, logged, etc.
##               $level            The debug level of the message. If not defined 0
##                                 will be assumed.  0 is considered a normal message, 
##                                 1 and higher is considered a debug message.
##  
##  Output:      Prints to STDOUT, to LOGFILE, both, or none depending 
##               on the state of the program and the debug level specified.
##  
##  Example:     printmsg("ERROR => The file could not be opened!", 0);
###############################################################################################
sub printmsg {
    ## Assign incoming parameters to variables
    my ( $message, $level ) = @_;
    
    ## Make sure input is sane
    $level = 0 if (!defined($level));
    
    ## Continue only if the debug level of the program is >= message debug level.
    if ($conf{'debug'} >= $level) {
        
        ## Get the date in the format: Dec  3 11:14:04
        my ($sec, $min, $hour, $mday, $mon) = localtime();
        $mon = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon];
        my $date = sprintf("%s %02d %02d:%02d:%02d", $mon, $mday, $hour, $min, $sec);
    
        ## Syslog the message is needed
        if ($conf{'syslog'}) {
            system( qq{$conf{'syslogProgram'} --facility=$conf{'facility'} --priority=$conf{'priority'} "$conf{'programName'}: $message" });
        }
        
        ## Print to STDOUT always if debugging is enabled, or if conf{stdout} is true.
        if ( ($conf{'debug'} >= 1) or ($conf{'stdout'} == 1) ) {
            print "$date $conf{'hostname'} $conf{'programName'}: $message\n";
        }
        
        ## Print to the log file if $conf{'logging'} is true
        if ($conf{'logging'}) {
            print LOGFILE "$date $conf{'hostname'} $conf{'programName'}: $message\n";
        }
        
    }
    
    ## Return 0 errors
    return(0);
}














###############################################################################################
## FUNCTION:    
##   openLogFile ( $filename )
## 
## 
## DESCRIPTION: 
##   Opens the file $filename and attaches it to the filehandle "LOGFILE".  Returns 0 on success
##   and non-zero on failure.  Error codes are listed below, and the error message gets set in
##   global variable $!.
##   
##   
## Example: 
##   openFile ("/var/log/scanAlert.log");
##
###############################################################################################
sub openLogFile {
    ## Get the incoming filename
    my $filename = $_[0];
    
    ## Make sure our file exists, and if the file doesn't exist then create it
    if ( ! -f $filename ) {
        printmsg("NOTICE => The file [$filename] does not exist.  Creating it now with mode [0600].", 0);
        open (LOGFILE, ">>$filename");
        close LOGFILE;
        chmod (0600, $filename);
    }
    
    ## Now open the file and attach it to a filehandle
    open (LOGFILE,">>$filename") or return (1);
    
    ## Put the file into non-buffering mode
    select LOGFILE;
    $| = 1;
    select STDOUT;
    
    ## Tell the rest of the program that we can log now
    $conf{'logging'} = "yes";
    
    ## Return success
    return(0);
}











######################################################################
##  Function:    quit (string $message, int $errorLevel)
##  
##  Description: Exits the program, optionally printing $message.  It 
##               returns an exit error level of $errorLevel to the 
##               system  (0 means no errors, and is assumed if empty.)
##               If your exiting with a non-zero error status $message
##               is also sent to syslogd.
##
##  Example:     quit("Exiting program normally", 0);
######################################################################
sub quit {
    my %incoming = ();
    (
        $incoming{'message'},
        $incoming{'errorLevel'}
    ) = @_;
    $incoming{'errorLevel'} = 0 if (!defined($incoming{'errorLevel'}));
    
    ## Quit messages get printed to the console
    $conf{'stdout'} = 1;
    
    ## Print exit message
    if ($incoming{'message'}) { 
        printmsg($incoming{'message'}, 0);
    }
    
    ## Exit
    exit($incoming{'errorLevel'});
}






