#!/usr/bin/perl
#===============================================================================
#
#         FILE:  check_file_numbers.pl
#
#        USAGE:  ./check_file_numbers.pl  
#
#  DESCRIPTION:  Nagios plugin to check numbers of a file matching a pattern
#                Either works by applying the supplied file globs directly - in which case the
#                glob could be anchored as an absolute path e.g. /tmp/*.xml
#
#                Or if the optional -b for basedir is given we do a File::Find tree walk from that base and for each
#                sub directory the specified file globs are applied. In this case it is an error for any of the 
#                globs to be an absolute path. This has to be checked for in a portable way so that the code will work for
#                Windows as well as Unix - the way we choose to do this is to insist that the glob cannot have any directory
#                name component - i.e. that it must equal its own basename. 
#                
#
#      OPTIONS:  ---
# REQUIREMENTS:  ---
#         BUGS:  ---
#        NOTES:  Warning to implementors: this check has the potential to be very IO intensive if too large a tree is chosen for scanning!
#       AUTHOR:  Martin Houston 
#      COMPANY:  
#      VERSION:  0.2
#      CREATED:  16/05/2012
#     REVISION:  ---
#===============================================================================

use warnings;
use strict;
use Getopt::Long;
use File::Glob;
use File::Find;
no warnings 'File::Find'; # shut up about unrechable directories etc.
use File::Basename;

my %RETCODES = ('OK' => 0, 'WARNING' => 1, 'CRITICAL' => 2, 'UNKNOWN' => 3);

# Help
sub help
{
    print "Usage : check_file_numbers.pl\n\n";
    print "Options :\n";
    print " -f| --fileglob\n\tFile wildcard to match (can have multiple)\n";
    print " -b| --basedir\n\tBase directory to search from (can have multiple)\n\t- note if this option is used fileglobs must not contain any directory path information!\n";
    print " -a| --any\n\tANY wildcard can break the threshholds (default is to sum counts)\n";
    print " -w| --warning\n\tNumber of matches to wildcard to consider a warning\n";
    print " -c| --critical\n\tNumber of matches to wildcard to consider a critical\n";
    print " -h| --help\n\tPrint this help screen\n";
    print "\nExample : check_file_numbers.pl -f '/tmp/*.xml' -w 100 -c 1000\n";
    print "NOTE: the wildcard will need to be protected by quoting under Unix shells (as the shell does the wildcard expansion), \n";
    print "but quoting NOT needed when used under DOS/Windows as any wildcard expansion is always the responsibility of the program not the shell\n";
    print "the above example for Windows: check_file_numbers -f c:\temp\*.xml -w  100 -c 1000\n";
    exit $RETCODES{"UNKNOWN"};
}


=head2 check_glob

Do the actual check - apply each glob in the current directory context.
if the any option was chosen we break if any one glob breaks the total
thresholds, otherwise they all have to contribute.
we do it this way so we can cope with filesystem globbing being a lot less sophisticated
than proper regexes.

=cut

sub check_glob
{
    my($w,$c,$any,@globs) = @_;
    my %seen = ();
    my $totseen = 0;
    my $stats = '';
	
    for my $f(@globs) {
	for my $match (glob($f)) {
            $seen{$f}=0 unless defined $seen{$f};
	    $seen{$f}++;
            $totseen++;
        }
    }
    # get our data line ready
    $stats = join(',', map {"$_=$seen{$_}"} sort keys %seen) if scalar(keys %seen);
    
    # and choose or exit state
    my $estate = 'OK';
    if($any) {
	for(keys %seen) {
            if($estate eq 'OK' and $seen{$_} >= $w) {
                $totseen = "$seen{$_} $_";
		$estate = 'WARNING';
	    }
	    # first critcal state wins
	    if($seen{$_} >= $c) {
		$totseen = "$seen{$_} $_";
		$estate = 'CRITICAL';
	    }
        }
    }
    else {
        # just go on totals - files matcing all the patterns contribute to the total.
	$estate = 'WARNING' if $totseen >= $w;
	$estate = 'CRITICAL' if $totseen >= $c;
    }
    
    return($estate,$totseen,$stats,''); # 4th paraeter not important for a single scan case, we use the slot for totals on the recursive case
    
    
}

=head2 check_recursive

This is called if the -b flag is used - we do a check_glob on each directory in the tree and collect the results.

We report on the worst case be it critical or warning or OK and have some information on th number of directories scanned.

We need to collect data for all sub-directories and then only then decide.
It is a shame that we are throwing away a lot of info here but the point of Nagios is only to alert that
system admin attention is needed in a particular area.

=cut
  
sub check_recursive {
    my ($warnlev,$critlev,$any,$fileglob,$basedir) = @_;
    my %subdirs = ();
    my %statetotals = ();
    my $wanted = sub {    
        if(-d $_) {
	    $subdirs{$File::Find::name} = [check_glob($warnlev,$critlev,$any,@$fileglob)];
            # keep running totals of each state as we go.
            $statetotals{$subdirs{$File::Find::name}->[0]}++;
            $statetotals{ALL}++;
	}
	# else no action
    };

    for my $base (@{$basedir}) {
	find($wanted, $base);
    }
    # now find the worst result to report as our global status
    # we do this by reverse sorting %subdirs on estate value
    my @worst = sort { $RETCODES{$subdirs{$b}->[0]} <=> $RETCODES{$subdirs{$a}->[0]} ||  $subdirs{$b}->[1] <=> $subdirs{$a}->[1] } keys %subdirs;
    # the first of these is the file path containing the most instances of the glob, then sorted on count - so even if all OK we get a record of the biggest
    my @res = @{$subdirs{$worst[0]}};
    # let us count the criticals, warnings and oks - it gives some useful summary data for graphing
    my $tots = join(',', map {"$_=$statetotals{$_}"} sort keys %statetotals);
    # print "$worst[0] $tots\n";
    # replace the empty 4th element with our total data.
    $res[3] = "$worst[0] $tots\n";
    
    return (@res);
   
}


=head1 main program

Check that the arguments to this probe are as expected.

We can scan for multiple patterns optionally in multiple base directories.

The --any flag will only trigger an issue if any one pattern breaches the warning or critical levels.

Without the flag the patterns together are taken together to count towards the thresholds.

Code flows through to do the actual checks.
 
=cut

help if !(defined(@ARGV));
        
my (@fileglob,@basedir,$warnlev,$critlev,$any);
my $num=1;
my @res = ();
# Set options
GetOptions( 	"help|h" => \&help,
		"fileglob|f=s" => \@fileglob,
		'basedir|b=s' => \@basedir,
		"any|a" => \$any,
		"warning|w=s"		=> \$warnlev,
		"critical|c=s"		=> \$critlev);
                		

unless ((@fileglob) and $warnlev and $critlev)
{
    # need at least this
    help();
    # never returns
}
# are we the recursive variant?
if((@basedir)) {
    # need to check that all the fileglobs are just relative
    map { help() if $_ ne basename($_) } @fileglob;
    @res = check_recursive($warnlev,$critlev,$any,\@fileglob,\@basedir);

}
else
{   
    # normal check of all the globs
    @res = check_glob($warnlev,$critlev,$any,@fileglob);
    
}

print "$res[0] - $res[1] files seen " .  $res[3] . " | $res[2]\n";
exit $RETCODES{$res[0]};
