I have recently come in need of a monitor to watch quotas on a Network
Appliance filer, and found that the na_quota.monitor script included in
mon wasn't fully functional.  I decided to take the code and rework it
into a fully functional monitor script.

The patch to the distributed na_quota is 20k, whereas the new version
is just 14.5k, so I've attached the whole script.  The top of the script
has documentation about how to configure/install na_quota.

Note: I finished writing the code yesterday and have only done testing in my
environment (4 netapps with the same basic setup), so please let me know if
there are any issues.

-- 
Randomly Generated Tagline:
"Damn you, damn the broccoli, and damn the Wright Brothers!"
         - Stewie from Family Guy
#!/usr/bin/perl -w
#
# "mon" monitor to detect quotas near their limits on Network Appliance
# filers using SNMP
# 
# Originally by Jim Trocki
# Updated by Theo Van Dinter ([EMAIL PROTECTED], [EMAIL PROTECTED]) (c) 2001
# $Id: na_quota.monitor,v 1.2 2001/08/07 22:18:56 tvd Stab $
# 
# Invoke from mon via:
# monitor na_quota.monitor [-c snmp community] [-f configuration file] ;;
# 
# The ";;" at the end prevents mon from appending the hostnames of the
# filers to the command.  There aren't any problems from having the
# hostnames appended, but waste not want not. :)
# 
# If you don't want an alert to be sent each time the monitor is run,
# you'll want to use "alertevery <timeval> summary".  "summary" will
# cause mon to not alert if the detailed error messages change (which
# they are likely to do since the detailed information shows current %/#
# of KB/files left available. See the mon man page for more information.
# 
# This script uses a configuration file to determine the alert points and
# which filers to probe.  The configuration file format is as follows:
#               filer_name type volume name size_diff files_diff
# 
# filer_name    = hostname of a filer (ie: toaster)
# type          = quota type, either tree or user
# volume        = the volume to check, can be "*" to set the default
# name          = the name of the qtree, can be "*" to set the default
# size_diff     = the amount of free space available to cause an alert.
#                 "# [kmgt]b", "#.# [kmgt]b", "#" (assumes KB), or "#%"
# files_diff    = the number of files available to cause an alert.
#                 "#" or "#%"
# 
# For values with whitespace, enclose in quotes.  (ie: "30 KB")
# Use "-" for no alert, files_diff can be left off if unused.
# Either size_diff or files_diff must be defined ("-" for both isn't allowed).
# Volume and Name can be "*" for "all".  It will be overridden as appropriate:
# i.e.: toaster tree vol0 *   20%       # default all trees in vol0 to 20%
#       toaster tree vol0 foo 10%       # vol0 tree foo will use 10% instead
#       toaster tree vol0 bar 1GB       # vol0 tree bar will use 1GB instead
#       toaster tree vol0 baz 0         # vol0 tree baz will only alert when
#                                       # size used == size limit
# 
# Alerts occur when the used space/files is within "size_diff" or
# "files_diff" from the limit.  So if a size limit is at 100KB and used
# is 80KB, the free space is 20KB or 20%.  An alert will occur if the
# size_diff is >= 20KB or >=20%.  Note: The percentage is a rounded
# integer, so 10% actually means 9.5 - 10.4% or below 10.4%...
# 
#
# 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
use SNMP;
use strict;
use Getopt::Std;
use Text::ParseWords;
use vars qw/ $opt_f $opt_c $host %failures /;

$|++;
$ENV{"MIBS"} = "NETWORK-APPLIANCE-MIB"; # We need the NetApp MIB loaded ...

getopts ('c:f:');
$opt_f ||= "/usr/lib/mon/na_quota.cf";  # Configuration File
$opt_c ||= "public";                    # Community

my $CONF = read_cf($opt_f);
die "error reading config file $opt_f: $CONF\n" unless ( ref $CONF );

# Go through each specified host
foreach $host ( keys %{$CONF} ) {
        my $q = retrieve_quotas ($host, $opt_c); # Grab the quotas from $host

        # If $q isn't a reference, it's an error string.
        unless (ref $q) {
                $failures{$host} = "could not retrieve quotas from $host: $q";
                next;
        }

        # Check for user quotas, then tree quotas.
        foreach ( "user", "tree" ) {
                my $fails = quota_check($host,$_,$CONF,$q);
                @failures{keys %{$fails}} = values %{$fails} if ( defined $fails );
        }
}

# Display failures if there were any, then exit with 0 or 1 appropriately.
my $retval = 0;
if (defined %failures) {
        print join("\n",join(" ",sort keys %failures),"",values %failures,"");
        $retval = 1;
}

exit $retval;


# Read in the configuration file
#
# Input : Path to the configuration file
# Output: Hash reference of the configuration
#  $ref->{filer_name}->{quota_type}->{volume}->{quota_name}->{"files" or "size"} = diff
#
sub read_cf {
        my $cf = shift;
        my $error = undef;
        my($filer,$type,$user,$path);
        my $CONF = undef;

        # Open the configuration file or return the error string
        open(CF, "<$cf") || return $!;

        while (defined($_=<CF>)) {
                chomp;
                s/\s*#.*$//;            # kill comments
                s/^\s*//; s/\s*$//;     # get rid of pre-suff whitespace
                next unless /\S/;       # skip blank lines

                # Use quotewords to allow for quotas with spaces, etc.
                my($filer,$type,$volume,$name,$size,$files) = &quotewords('\s+',0,$_);

                # Allow "-" to mean undefined.
                $size = undef if ( defined $size and $size eq "-" );
                $files = undef if ( defined $files and $files eq "-" );

                unless ( defined $filer && $filer =~ /^[a-z0-9_.-]+$/i ) {
                        $error = "invalid filer name specified, $filer, line $.";
                        last;
                }
                unless ( defined $type && $type =~ /^(tree|user)$/i ) {
                        $error = "invalid quota type specified, $type, line $.";
                        last;
                }
                unless ( defined $volume && ( $volume eq "*" ||
                                $volume =~ /^[_a-z][_a-z0-9]*$/i ) ) {
                        $error = "invalid volume specified, $volume, line $.";
                        last;
                }
                unless ( defined $name ) {
                        $error = "invalid name specified, $name, line $.";
                        last;
                }
                unless ( defined $size || defined $files ) {
                        $error = "invalid line, no size_quota or file_quota, line $.";
                        last;
                }

                # Convert the filer and type to lowercase, the rest are case-sensitive
                $filer = lc $filer;
                $type = lc $type;

                # If we have a KB limit and it's a valid limit ...
                if ( defined $size && defined($size = to_kb($size)) ) {
                        $CONF->{$filer}->{$type}->{$volume}->{$name}->{"size"} = $size;
                }
                elsif ( defined $size ) {
                        $error = "invalid size specification, $size, line $.";
                        last;
                }

                # If we have a file limit and it's a valid limit ...
                if ( defined $files && $files =~ /^\d+\s*\%?$/ ) {
                        $CONF->{$filer}->{$type}->{$volume}->{$name}->{"files"} = 
$files;
                }
                elsif ( defined $files ) {
                        $error = "invalid files specification, $files, line $.";
                        last;
                }
        }

        close (CF);

        # Return either the configuration HASH or the error string.
        return ( defined $error ) ? $error : $CONF;
}

# Convert given units to KB
# 
# Input : Scalar value that is one of the following:
#       "# xB" (x=[kmgt]), "#" (assume KB), or "#%"
# Output: integer "#" in KB or "#%" (passthru)
#
sub to_kb {
        my $value = shift;
        my ($num, $unit);

        if ($value =~ /^(\d+(?:\.\d+)?)\s*([kmgt])b$/i) {       # "# xB"
                ($num, $unit) = ($1, lc $2);
        } elsif ( $value =~ /^\d+\s*\%?$/ ) {                   # "#%" or "#" (assume 
KB)
                return $value;
        } else {                                                # who knows?  error 
out.
                return undef;
        }

        # Figure out the prefix xB -> KB conversion ratio.  Leave as KB by default.
        my $mval = ($unit eq "m") ? 1024 : ($unit eq "g") ? 1048576 : 
                        ($unit eq "t") ? 1073741824 : 1;
        return (int ($num*$mval));
}

# Convert given # of KB into a more displayable string (MB, GB, etc.)
# 
# Input : Scalar value of KB.  Any non-numeric chars are stripped out.
# Output: String in the format "#.##xB" where x is [KMGT].  ie: 10 becomes "10KB".
# 
sub from_kb {
        my $value = shift;
        my @prefix = qw/ T G M K /;
        my $index = $#prefix;

        return undef unless defined $value;

        $value =~ tr/0-9//cd; # we only handle numbers (KB)
        while ( $value > 1024 && $index >= 0 ) { # Run until we can't go any further!
                $index--;
                $value /= 1024;
        }
        return sprintf "%.2f%sB", $value, $prefix[$index]; # Return the formatted 
string
}

# Retrieve the quota information from the NetApp via SNMP.
#
# Input : Hostname and SNMP Community (defaults to public)
# Output: Hash reference of the quota information
#  $ref->{"user"}->{volume}->{username or uid}->{info} = value
#  $ref->{"tree"}->{volume}->{tree_name}->{info} = value
#  where info is: "qrVKBytesUsed", "qrVKBytesLimit", "qrVFilesUsed", "qrVFileLimit"
#
sub retrieve_quotas {
        my $host = shift;                       # Hostname
        my $community = shift || "public";      # SNMP Community, "public" by default
        my $quotas = undef;                     # Hash of quota information
        my %volnames = ();                      # Hash of volume names for reference

        # Establish the SNMP session if possible.
        my $s = new SNMP::Session (
                DestHost => $host,
                Community => $community || "public",
                UseEnums => 1,
        );
        if (!defined $s) { return "could not create SNMP session" }

        # Map volume numbers to names for use later on
        my $v = new SNMP::Varbind (["qvStateVolume"]);
        $s->getnext ($v);
        while (!$s->{"ErrorStr"} && $v->tag eq "qvStateVolume") {
                my @q = $s->get ([
                        ["qvStateVolume", $v->iid],
                        ["qvStateName", $v->iid],
                ]);
                last if ($s->{"ErrorStr"});
                $volnames{$q[0]} = $q[1];
                $s->getnext ($v);
        }
        if ($s->{"ErrorStr"}) {
                return $s->{"ErrorStr"};
        }

        # Get the quota information
        $v = new SNMP::Varbind (["qrVIndex"]);
        $s->getnext ($v);
        while (!$s->{"ErrorStr"} && $v->tag eq "qrVIndex") { # go through each qrVIndex
                my @q = $s->get ([
                        ["qrVType", $v->iid],
                        ["qrVId", $v->iid],
                        ["qrVKBytesUsed", $v->iid],
                        ["qrVKBytesLimit", $v->iid],
                        ["qrVFilesUsed", $v->iid],
                        ["qrVFileLimit", $v->iid],
                        ["qrVPathName", $v->iid],
                        ["qrVVolume", $v->iid],
                        ["qrVTree", $v->iid],
                ]);

                last if ($s->{"ErrorStr"}); # exit if there's a problem

                # Skip the crap quotas...
                if ( $q[0] ne "qrVTypeUnknown" && $q[0] ne "qrVTypeUserDefault" ) {
                        # Turn qrVTypeUser and qrVTypeTree into "user" and "tree"
                        if ( $q[0] =~ /^qrVType(User|Tree)$/ ) {
                                $q[0] =~ s/^.+(User|Tree)$/\L$1/;
                        }
                        else {
                                return "Unknown quota type ($q[0]) returned from 
filer!";
                        }

                        # Map volume number to volume name
                        $q[7] = $volnames{$q[7]};

                        # Map UID to Username if possible, use the system ...
                        if ($q[0] eq "user"){
                                my($user) = (getpwuid($q[1]))[0];
                                $q[1] = $user if defined $user;
                        }

                        # Setup hash of quotas.  type -> vol -> name -> key = value
                        my $id = ( $q[0] eq "user" ) ? $q[1] : $q[8];
                        $quotas->{$q[0]}->{$q[7]}->{$id}->{"qrVKBytesUsed"} = $q[2];
                        $quotas->{$q[0]}->{$q[7]}->{$id}->{"qrVKBytesLimit"} = $q[3];
                        $quotas->{$q[0]}->{$q[7]}->{$id}->{"qrVFilesUsed"} = $q[4];
                        $quotas->{$q[0]}->{$q[7]}->{$id}->{"qrVFilesLimit"} = $q[5];
                }

                $s->getnext ($v); # go on to the next one
        }

        # If we errored out, return the error.  Otherwise return the hash reference.
        return ( $s->{"ErrorStr"} ) ? $s->{"ErrorStr"} : $quotas;
}

# This subroutine will check both tree and user quotas.  Note: It's fairly nasty. 
<grrr>
# It was much much worse at one point, but it's a bit cleaner now.  There's one routine
# to handle both the tree and user quotas now instead of one for each type 
(size/files).
#
# Input : hostname, type (user|files), configuration hash ref, quota information hash 
ref
# Output: hash reference of failures, or undef if there are no failures.
#
sub quota_check {
        my($host,$type,$CONF,$q) = @_;
        my $failures = undef;
        my $actvolume;

        # sort by volume name (unnecessary, but (*) needs to be last...)
        foreach $actvolume ( sort {
                         ($a eq "*")?1:($b eq "*")?-1:$a cmp $b;
                        } keys %{$CONF->{$host}->{$type}} ) {
                my $names = $CONF->{$host}->{$type}->{$actvolume};
                my @volumes = ();
                my $done = ();

                # Generate the appropriate volume list
                if ( $actvolume eq "*" ) {
                        @volumes = grep(!exists $done->{$host}->{$type}->{$_},
                                keys %{$q->{$type}});
                }
                else {
                        @volumes = ( $actvolume );
                }

                my $volume;
                foreach $volume ( @volumes ) {
                        my $actname;

                        # sort by name (unnecessary, but (*) needs to be last...)
                        foreach $actname ( sort {
                                        ($a eq "*")?1:($b eq "*")?-1:$a cmp $b;
                                        } keys %{$names} ) {
                                my $qtype = $names->{$actname}; # quota information
                                my @names = ();

                                # Generate the appropriate quota name list
                                if ( $actname eq "*" ) {
                                        @names = grep(!exists 
$done->{$host}->{$type}->{$volume}->{$_},
                                                keys %{$q->{$type}->{$volume}});
                                }
                                else {
                                        @names = ( $actname );
                                }

                                my $name;
                                foreach $name ( @names ) {
                                        # Keep track of which stuff we've checked
                                        $done->{$host}->{$type}->{$volume}->{$name}++;

                                        # If a configured check isn't quota-ed, report 
it as error.
                                        if ( exists $q->{$type}->{$volume}->{$name} ) {
                                                my %info = 
%{$q->{$type}->{$volume}->{$name}};
                                                my $sorf;

                                                foreach $sorf ( "size", "files" ) {
                                                        my $kbofi = ($sorf eq 
"size")?"KBytes":"Files";
                                                        my $limit = 
$info{"qrV${kbofi}Limit"};
                                                        my $used = 
$info{"qrV${kbofi}Used"};

                                                        if ( exists $qtype->{$sorf} ) {
                                                                # Verify that the 
usage is being limited
                                                                if ( $limit < 0 ) {
                                                                        
$failures->{"$host:$volume:$name"}="requested quota ('$host $type $volume $name 
$sorf') isn't limited on the filer" unless ( $actname eq "*" );
                                                                        next;
                                                                }
                                                                elsif ( $limit == 0 ) {
                                                                        
$failures->{"$host:$volume:$name"}="requested quota ('$host $type $volume $name 
$sorf') has a limit of 0 $sorf on the filer";
                                                                        next;
                                                                }

                                                                # Percentage and # 
free/left
                                                                # Make sure to round 
fpct ... ;)
                                                                my $fkb = $limit-$used;
                                                                my $fpct = 
int($fkb/$limit*100+0.5);
                                                                if ( $qtype->{$sorf} 
=~ /^(\d+)\s*\%$/ ) {
                                                                        my $pct = $1;
                                                                        if ( $fpct <= 
$pct ) {
                                                                                my 
$msg = "$type $sorf quota $host:$volume:$name has $fpct% ";
                                                                                
$msg.=($sorf eq "files")?"files left":
                                                                                       
 "(".from_kb($fkb).") free";
                                                                                
$msg.=" <= $pct% ($actvolume:$actname)";
                                                                                
$failures->{"$host:$volume:$name"}=$msg;
                                                                        }
                                                                }
                                                                else { # non-percent
                                                                        if ( $fkb <= 
$qtype->{$sorf} ) {
                                                                                my 
$msg = "$type $sorf quota $host:$volume:$name has ";
                                                                                
$msg.=($sorf eq "files")?"$fkb files left":
                                                                                       
 from_kb($fkb)." free";
                                                                                
$msg.=" <= ";
                                                                                
$msg.=($sorf eq "files")?$qtype->{$sorf}:
                                                                                       
 from_kb($qtype->{$sorf});
                                                                                
$msg.=" ($actvolume:$actname)";
                                                                                
$failures->{"$host:$volume:$name"}=$msg;
                                                                        }
                                                                }
                                                        }
                                                }
                                        }
                                        else {
                                                
$failures->{"$host:$volume:$name"}="requested quota ('$host $type $volume $name') 
doesn't exist on filer" unless ( $actname eq "*" );
                                                next;
                                        }
                                }
                        }
                }
        }

        return $failures;
}

Reply via email to