#!./perl.exe -w
#
# windschecker - Windows-based disk space monitor for mon.
# Copyright (C) 2002  Ryan VanderBijl
# 
# 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.
#
# See http://www.gnu.org/copyleft/gpl.html for the GPL license.
#======================================================================
# Windows-based Disk Space Checker
#  - Ryan VanderBijl, Dec 2002
#  - http://www.vanderbijlfamily/~rvbijl/software/
#
# Requires ActiveState Perl (for Windows).
#
# On the Windows box to check:
#   Creates a Scripting.FileSystemObject to get drive information
#   Compares drive info against predefined values (percent or MBytes)
#   Sends a mon trap with status information to a mon server
#
# * Able check multiple drives.
# * Stateless (depends on Mon to keep the state/generate alerts)
#
# Note: as of Mon 0.99.2, trap support wasn't very intelligent.
#       You may need to apply a patch to mon to make it work
#       "right".
#
# To use:
#   Make a scheduled tasks for the program, running every 
#   so often. (I run it every 15 or so minutes). 
#
#   Then, in the mon configuration file, put something like:
#   
#   hostgroup group computer
#
#   watch group
#      service diskspace.error
#         trapinterval 20m   
#         period wd{Mon-Sun}
#              alert qpage.alert person
#            upalert qpage.alert person
#      service diskspace.warnr
#         trapinterval 20m   
#         period wd{Mon-Sun}
#              alert mail.alert person
#            upalert mail.alert person
#
#
#  If you dont want this script to also send a "heartbeat"
#  (to prove that it is still being run), take out the
#  "trapinterval" lines, and set $HEARTBEAT = 0; below
#
#  If you don't want to the 'tiered' error/warning 
#  set the $TIERED = 0;  
#

use strict;
use Socket;
use Win32::OLE;   ## needed to get Scripting.FileSystemObject

## config options
my $SENDTO = 'monserver';   # host/ip to send trap to
my $GROUP  = 'winboxen';    # watchgroup on server
my $HEARTBEAT = 1;          # send a trap if nothing is wrong 
                            #  (e.g. tell the server we're still alive)

# You must specify 'M' or '%' as part of the number
# If not tiered, specify valid values for both,
# but only the errorbelow will actually generate warnings.
#               'Drive' => [ 'warnbelow', 'errorbelow' ]   
my $DRIVES   = { 'C:\\' => ['15%', '0%'],
                 'D:\\' => ['15%', '5%'],
                 'E:\\' => ['15%', '5%'] };


######################################################################
## You probably don't need to modify anything below here
######################################################################
## the program:
my $DEBUG  = @ARGV; ## if any parameters are given, it'll run in DEBUG mode.
my $TIERED = 1;     ## both error and warnings
my(@WARN_SUMMARY , @WARN_DETAILS ) = ();
my(@ERROR_SUMMARY, @ERROR_DETAILS) = ();

doChecks($DRIVES);
doTraps();
exit;


#
# Iterate through all the specified drives
# and check that drive for low diskspace
#
sub doChecks {
    my $drives = shift;

    my @ALERTS;

    my $fso = Win32::OLE->new("Scripting.FileSystemObject");
    unless (defined($fso)) {
        push @ERROR_SUMMARY, "Could not load Scripting.FileSystemObject: " . Win32::OLE->LastError();
        push @ERROR_DETAILS, "Could not load Scripting.FileSystemObject: " . Win32::OLE->LastError();
        return;
    }

    foreach my $drive (sort keys %$drives) {
        my $dobj = $fso->GetDrive($fso->GetDriveName($drive));
        unless (defined($dobj)) {
            push @ERROR_SUMMARY, "Could not stat drive '$drive'.";
            push @ERROR_DETAILS, "Could not get drive information (obj) for '$drive': " . Win32::OLE->LastError();
            next;
        }

        checkDriveObj($dobj, @{$drives->{$drive}});
    }
}


#
# For a given drive, check the warning and error thershold
# Add any warnings/errors to the proper list.
#
sub checkDriveObj {
    my($dobj, $warnbelow, $errorbelow) = @_;
    my($amountfree, $alertbelow, $check);

    my $dl   = $dobj->DriveLetter;
    my $info = getDriveInfo($dobj);

    print "$info\n" if ($DEBUG);

    if (drivePastLimit($dobj, $errorbelow)) {
       print "critical low space on $dl\n" if $DEBUG;
       push @ERROR_SUMMARY, "$GROUP: critical disk space on $dl";
       push @ERROR_DETAILS , "$info\nFail below: $errorbelow\n";

    } elsif (drivePastLimit($dobj, $warnbelow)) {
       print "warnng on low space on $dl\n" if $DEBUG;
       push @WARN_SUMMARY, "$GROUP: low disk space on $dl";
       push @WARN_DETAILS , "$info\nWarn below: $errorbelow\n";

    } else {
       print "Plenty of disk space on $dl\n" if $DEBUG;

    }
}


#
# Check if the drive info is below the threshold
#    (called twice, once for warn, once for error)
#    (can't "cache" the amountfree because 
#       it is computed differently based on
#       what unit the threshold is)
#
sub drivePastLimit {
    my ($dobj, $threshold) = @_;

    unless ($threshold =~ /^(\d+)(M|%)$/i) {
        die $dobj->DriveLetter;
    }
    my ($alertbelow, $check) = ($1, $2);
    my $amountfree = ($check eq 'm' 
         ?  $dobj->freespace / 1024 / 1024 
         :  (100*$dobj->FreeSpace)/$dobj->TotalSize);

    return $amountfree <= $alertbelow;
}

#
# Get a user-friendly summary of the dirve info
#
sub getDriveInfo {
    my $dobj = shift;

    my $nfree = reverse int(($dobj->FreeSpace/1024)/1024);
    my $nall  = reverse int(($dobj->TotalSize/1024)/1024);
    $nfree =~ s/(\d{3})(?!$)/$1,/g;
    $nall  =~ s/(\d{3})(?!$)/$1,/g;
    $nfree = reverse $nfree;
    $nall  = reverse $nall ;

    my $dl = $dobj->DriveLetter;
    my $percent = 100 * $dobj->FreeSpace / $dobj->TotalSize;

    return sprintf("Drive $dl: %7sM  / %7sM (%2d%%) Free", $nfree, $nall, $percent);
}



#
# send the actual traps
#
sub doTraps {
    ## for errors
    if (@ERROR_SUMMARY > 0) {
       sendTrap("diskspace.error", 0, join(", ", @ERROR_SUMMARY), join("", @ERROR_DETAILS));
    } else {
       sendTrap("diskspace.error", 1, "okay", "okay") if $HEARTBEAT;
    }

    ## if not TIERED, ignore warnings.
    return unless $TIERED;

    ## for warnings
    if (@WARN_SUMMARY > 0) {
       sendTrap("diskspace.warn", 0, join(", ", @WARN_SUMMARY), join("", @WARN_DETAILS));
    } else {
       sendTrap("diskspace.warn", 1, "okay", "okay") if $HEARTBEAT;
    }
}


#
# Based on trap.alert
#
sub sendTrap {
    my($service, $status, $summary, $detail) = @_;

    $summary = _esc_str($summary);
    $detail = _esc_str($detail);

    my $TRAP_PRO_VERSION = 0.3807;

    my $pkt = "";
    $pkt .= "pro=$TRAP_PRO_VERSION\n";
    #$pkt .= "usr=$USER\n" . "pas=$PASS\n" if ($USER ne "");
    $pkt .= "typ='trap'\n";
    $pkt .= "grp='$GROUP'\n";
    $pkt .= "svc='$service'\n";
    $pkt .= "sta='1234'\n";
    $pkt .= "spc='$status'\n";
    $pkt .= "tsp='" . time . "'\n";
    $pkt .= "sum='$summary'\n";
    $pkt .= "dtl='$detail'\n";

    print "SENDING PACKET TO $SENDTO:\n---\n$pkt\n---\n" if $DEBUG;

    ##
    ## Not much we can do here with regards to error checking ...
    ##  if this trap isn't sent, then mon should timeout and
    ##  send error messages.
    ##
    my $proto = getprotobyname ("udp") || die "could not get proto\n";
    my $port  = getservbyname ('mon', 'udp') || 2583;
    my $paddr = sockaddr_in ($port, inet_aton ($SENDTO));

    socket (TRAP, AF_INET, SOCK_DGRAM, $proto) || die "could not create UDP socket: $!\n";
    if (!defined (send (TRAP, $pkt, 0, $paddr))) {
        print STDERR "could not send trap to $SENDTO: $!\n";
        next;
    }
    close (TRAP);
}


#
# Copied shamelessly(?) from Mon::Client
#
sub _esc_str {
    my $str = shift;
    my $inquotes = shift;
    my $escstr = "";

    for (my $i = 0; $i < length ($str); $i++)
    {
        my $c = substr ($str, $i, 1);

        if (ord ($c) < 32 ||
            ord ($c) > 126 ||
            $c eq "\"" ||
            $c eq "\'")
        {
            $c = sprintf ("\\%02x", ord($c));
        }
        elsif ($inquotes && $c eq "\\")
        {
            $c = "\\\\";
        }

        $escstr .= $c;
    }

    $escstr;
}

