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) = "ewords('\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;
}