#! /usr/bin/perl -w
# Program         : check_zfs
# Author          : Michael Simoni
# Purpose         : Check the Sun Volume manager for errors
# Date            : Fri Sep 26 15:54:16 EDT 2008
################################################################################
# CVS Information
# $Source: /var/lib/cvs/nagios/check_zfs.pl,v $
# $Author: msimoni $
# $Date: 2008-10-18 16:46:38 $
# $Id: check_zfs.pl,v 1.5 2008-10-18 16:46:38 msimoni Exp $
# $Revision: 1.5 $
################################################################################
# Notes
# 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 (or with Nagios);  if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA
################################################################################
# Verbose levels:-
# 1 - Only alert us of zpool health and size stats
# 2 - ...also alert us of failed devices when things go bad
# 3 - ...alert us of the status of all devices regardless of health
#
# Usage:   check_zfs <zpool> <verbose level 1-3>
# Example: check_zfs zeepool 1
#       ZPOOL zeedata : ONLINE {Size:3.97G Used:183K Avail:3.97G Cap:0%}
#
################################################################################
$| = 1;
use strict;
use English;
use Sys::Hostname;
use Data::Dumper;
use Getopt::Long;
use vars qw($PROGNAME);
use lib "/usr/lib/nagios/plugins";
use utils qw (%ERRORS &print_revision &support);
################################################################################
# Globals
################################################################################
my $PROGNAME = $0;
my $hostname = hostname();    # always the current host

my ( $opt_e, $opt_h, $opt_f, $opt_p, $opt_q, $opt_v, $opt_V, $opt_w  );
my $opt_z = "";
my $opt_s = "";

my $result  = "OK";
my $message = "";

my $zpool         = "";
my $zfs_name      = "";
my %zpools        = ();
my %zpools_status = ();
my %zfss          = ();
my %zfs_props     = ();
my @zpool_list    = ();

################################################################################
# show a usage statement
################################################################################
sub print_usage ()
{
    print <<EOD;
Usage:
$PROGNAME:  -v <verbose level 1-3>\n";
                -p check zpool. Default is not to check
                -f check zfs file systems. Default is not to check
                   by itself it does nothing. It needs to be directed what to do
                   with other options. By default this option will check zfs
                   with type = volume. Use -w to not check zfs volumes.
  
                    -q check reservations and quotas on zfs. Default is not to check.
                       Only works with -f
                    -w used in conjuction with -q to not check zfs file systems with type = volume

                -s <zfsname> Default is to check all zfs file systems.
                -z <zpoolname> Default is to check all zpools. Only applies to -p
                -h| --help 
                -V| --version
                -v| --verbose

Error status of -p (zpool status) overwrites any zfs status checks
EOD
    exit $ERRORS{'UNKNOWN'};
}
################################################################################
# print the standard help stuff
################################################################################
sub print_help ()
{
    print_revision( $PROGNAME, '$Revision: 1.5 $' );
    print "Copyright (c) 2008 Michael Simoni\n\n";
    print_usage();
    print "\n";
    print "This check is used to check Sun Volume Manager.\n";
    print "\n";
    support();
}
################################################################################
# make some error checks
################################################################################
sub error_checks()
{

    #if ($^O ne 'solaris' && $^O ne 'freebsd')
    if ( $^O ne 'solaris' )
    {
        print "This plugin currently only works on Solaris 10, OpenSolaris distributions.\n";
        exit $ERRORS{'UNKNOWN'};
    }

    # you cant have a zfs file system without a zpool. So if no pools -> exit
    # but this will be caught in the get_zp_list subroutine
    #my $pool_names = `/usr/sbin/zpool list -H -o name`;
    #if ( $pool_names =~ /no pools available/ )
    #{
    #    $result  = "UNKNOWN";
    #    $message .= "$pool_names ";
    #    print "$result: ZFS on $hostname - $message\n";
    #    exit $ERRORS{$result};
    #}

    # there is no point in this since a datapool is a zfs file system
    #my $check_zfs = `/usr/sbin/zfs list $zfs_name`;
    #chomp($check_zfs);
    #if ( $check_zfs =~ /no datasets available/ )
    #{
    #	$result  = "UNKNOWN";
    #    $message = "$check_zfs ";
    #    print "$result: ZFS on $hostname - $message\n";
    #    exit $ERRORS{$result};
    #}

    # need put a check here to see if the zfs commands exist at all on the system.
    # maybe ...
}
################################################################################
# just get a simple list of hte zpools
# this could go int othe error check subroutine, but it's nicei to keep logical
# steps seperate for maintenance reasons.
################################################################################
sub get_zp_list
{
    my $pool_names = `/usr/sbin/zpool list -H -o name`;
    if ( $pool_names eq "no pools available\n" )
    {
	$result  = "UNKNOWN";
        $message .= "$pool_names ";
        print "$result: ZFS on $hostname - $message\n";
        exit $ERRORS{$result};
    }
    #elsif ( $pool_names =~ /invalid/ )
    #{
    #    $message = $pool_names;
    #    exit $ERRORS{'UNKNOWN'};
    #}
    @zpool_list = split( "\n", $pool_names );
    if ($opt_v)
    {
        $Data::Dumper::Varname = "zpool_list";
        print Dumper( \@zpool_list ) . "\n";
        $Data::Dumper::Varname = "VAR";
    }
}
################################################################################
# Get zpool list data
################################################################################
sub get_zpool_data()
{
    my @args = ("/usr/sbin/zpool list -H $zpool");
    open( UNAME, "@args |" );

    while (<UNAME>)
    {
        chomp;
        next if (/^NAME\s+SIZE\s+USED\s+AVAIL\s+CAP\s+HEALTH\s+ALTROOT/);
        my ( $pool, $size, $used, $avail, $cap, $health, $altroot ) = split( /\s+/, $_ );

        #@{$pools{$pool}} =  ($pool, $size, $used, $avail, $cap, $health, $altroot);
        $zpools{$pool} = {
            size    => "$size",
            used    => "$used",
            avail   => "$avail",
            cap     => "$cap",
            health  => "$health",
            altroot => "$altroot",
        };
    }
    close(UNAME);
}
################################################################################
# process the zpool list data
################################################################################
sub process_zpool_data()
{
    if ($opt_v)
    {
        $Data::Dumper::Varname = "zpools";
        print Dumper( \%zpools ) . "\n";
        $Data::Dumper::Varname = "VAR";
    }
    foreach my $pool ( keys %zpools )
    {
        my $health = $zpools{$pool}{health};
        if ( $health eq "ONLINE" )
        {
            $result = "OK" unless ( $result ne "OK" );
        }
        elsif ( $health eq "DEGRADED" )
        {
            $result = "WARNING";
            $message .= "$pool is $health; ";
        }
        else
        {
            $result = "CRITICAL";
            $message .= "$pool is $health; ";
        }
    }
    $message =~ s/^ and //;
}
################################################################################
# get the zpool status data
################################################################################
sub get_zpool_status_data()
{
    my ( $pool, $scrub, $status, $action, $see );
    my @args = ("/usr/sbin/zpool status -v $zpool");
    open( UNAME, "@args |" );
    while (<UNAME>)
    {
        next if (/\s+NAME\s+STATE\s+READ\s+WRITE\s+CKSUM/);
        next if (/^config:/);
        next if /^#/ or /^\s*$/;                              #skip comments and blank lines
        s/^\s+//;                                             #strip leading and
        s/\s+$//;                                             #trailing blanks

        if (/^pool:\s+(\w+)/)
        {
            $pool = $1;
            next;
        }
        if (/^state:\s+(\w+)/)
        {
            my $state = $1;
            $zpools_status{$pool}{state} = "$state";
            next;
        }
        if (/^status:\s+(\w+)/)
        {
            s/^status://;
            $status = $_;
            $zpools_status{$pool}{status} = "$status" . " (see: zpool status)";
            next;
        }
        if (/^scrub:\s+(\w+)/)
        {
            s/^scrub://;
            $scrub = $_;
            $zpools_status{$pool}{scrub} = "$scrub";
            next;
        }
        if (/^action:\s+(\w+)/)
        {
            s/^action://;
            $action = $_;
            $zpools_status{$pool}{action} = "$action";
            next;
        }
        if (/^see:\s+(\w+)/)
        {
            s/^see://;
            $see = $_;
            $zpools_status{$pool}{see} = "$see";
            next;
        }
        if (/^$pool/)
        {
            my ( $pool, $state, $read, $write, $cksum ) = split( /\s+/, $_ );
            $zpools_status{$pool}{$pool} = {
                'device' => "$pool",
                'state'  => "$state",
                'read'   => "$read",
                'write'  => "$write",
                'cksum'  => "$cksum"
            };
            next;
        }
        if (/^mirror/)
        {
            my ( $pool, $state, $read, $write, $cksum ) = split( /\s+/, $_ );
            $zpools_status{$pool}{$pool} = {
                'device' => "$pool",
                'state'  => "$state",
                'read'   => "$read",
                'write'  => "$write",
                'cksum'  => "$cksum"
            };
            next;
        }
        if (/^raidz/)
        {
            my ( $pool, $state, $read, $write, $cksum ) = split( /\s+/, $_ );
            $zpools_status{$pool}{$pool} = {
                'device' => "$pool",
                'state'  => "$state",
                'read'   => "$read",
                'write'  => "$write",
                'cksum'  => "$cksum"
            };
            next;
        }
        if (/^c\S+/)
        {
            my ( $device, $state, $read, $write, $cksum ) = split( /\s+/, $_ );
            $zpools_status{$pool}{$device} = {
                'device' => "$device",
                'state'  => "$state",
                'read'   => "$read",
                'write'  => "$write",
                'cksum'  => "$cksum"
            };
            next;
        }
        if (/^errors:/)
        {
            s/^errors://;
            $zpools_status{$pool}{errors} = "$_";
            undef $pool;
            undef $scrub;
            undef $action;
            undef $see;
            undef $status;
            next;
        }
    }
    close(UNAME);

    $message =~ s/^ and //;
}
################################################################################
# process the zpool_status data
# for the most part this function is only here to provide extended information.
# the zpool list command will alert sufficiently. Comment out this function
# at teh bottom of the program if you want to reduce the output.
################################################################################
sub process_zpool_status_data()
{
    my $status_message = "";
    if ($opt_v)
    {
        $Data::Dumper::Varname = "zpools_status";
        print Dumper( \%zpools_status ) . "\n";
        $Data::Dumper::Varname = "VAR";
    }
    foreach my $pool ( keys %zpools_status )
    {
        print "pool:$pool: \n";
        foreach my $item ( keys %{ $zpools_status{$pool} } )
        {

            #print "\t:$item: \n";
            # it seems as if the only thing we need is the status message.
            if ( $item eq "status" )
            {

                #print "\t:" . $zpools_status{ $pool }{status} . ":\n";
                $status_message .= " [$pool: $zpools_status{ $pool }{status}]";
                $message .= $status_message;
            }

            #if (    $item ne "state"
            #    and $item ne "action"
            #    and $item ne "see"
            #    and $item ne "errors"
            #    and $item ne "scrub"
            #    and $item ne "status" )
            #{
            #    next if ( $zpools_status{$pool}{$item}{state} eq "ONLINE" );

            #print "\t:$item:" . $zpools_status{ $pool }{$item}{state} . ":\n";
            #    $message .= " ($item: $zpools_status{ $pool }{$item}{state})";
            #}
        }

        #$message .= $status_message;
    }
}
################################################################################
# Get the data in a usable structure like a hash or an array.
# then you can do anything you want with it
################################################################################
sub get_zfs_data()
{
    my @args = ("/usr/sbin/zfs list -H $zfs_name");
    open( UNAME, "@args |" );

    while (<UNAME>)
    {
        chomp;
        next if (/^NAME\s+USED\s+AVAIL\s+REFER\s+MOUNTPOINT/);
        my ( $name, $used, $avail, $refer, $mountpoint ) = split( /\s+/, $_ );

        $zfss{$name} = {
            used       => "$used",
            avail      => "$avail",
            refer      => "$refer",
            mountpoint => "$mountpoint",
        };
    }
    close(UNAME);

    # the command "/usr/sbin/zfs get all" dos not work on all servers.
    # not every server is patched the same
    foreach my $zpool_name ( keys %zfss )
    {
        if ( $zpool_name =~ /^([ &:#-\@\w.]+)$/ )
        {
            $zpool_name = $1;    #data is now untainted
        }
        else
        {
            print "Tainted data from zpool_name variable\n";
        }

        my @args = ("/usr/sbin/zfs get all $zpool_name");
        open( UNAME, "@args |" );

        while ( my $dataline = <UNAME> )
        {
            chomp($dataline);
            if ( $dataline =~ /^NAME/ )
            {
                next;
            }
            my ( $name, $property, $value, $source ) = split( '\s+', $dataline );
            my @a = ( $value, $source );
            $zfs_props{$name}{$property} = \@a;
        }
        close(UNAME);
    }
}
################################################################################
# process the zfs data
################################################################################
sub process_zfs_data()
{
    my @reserve_warn = ();

    if ($opt_v)
    {
        $Data::Dumper::Varname = "zfss";
        print Dumper( \%zfss ) . "\n";
        $Data::Dumper::Varname = "VAR";

        $Data::Dumper::Varname = "zfs_props";
        print Dumper( \%zfs_props ) . "\n";
        $Data::Dumper::Varname = "VAR";
    }

    if ($opt_q)
    {
        foreach my $pool_name ( sort keys %zfs_props )
        {
            # here we want to skip volumes. For some reason they are sometimes created with
            # out the reservation being set.
	    if ( $opt_w )
	    {
	    	next if ( @{ $zfs_props{$pool_name}{type}}[0] eq "volume" );
            }
            for my $prop ( sort keys %{ $zfs_props{$pool_name} } )
            {
                my ( $value, $source ) = @{ $zfs_props{$pool_name}{$prop} };

                # search for a pattern in an array elements
                # all we want are the file systems. Not the underlying datapool.
                # snapshots are not checked because snapshotes do not have attributes 
                # of "reservation" or "quota".
                my @match = grep ( /$pool_name/, @zpool_list );
                if ( !@match )
                {
                    if (   ( ( $prop eq "reservation" ) and ( $value eq "none" ) )
                        or ( ( $prop eq "quota" ) and ( $value eq "none" ) ) )
                    {
                        push( @reserve_warn, "$pool_name;$prop;$value" );
                    }
                }
            }
        }
        foreach my $item (@reserve_warn)
        {
            $result = "WARNING";
            $message .= "$item ";
        }
    }
}
################################################################################
# Main
################################################################################

Getopt::Long::Configure('bundling');
GetOptions(
    "V"          => \$opt_V,
    "version"    => \$opt_V,
    "v"          => \$opt_v,
    "verbose"    => \$opt_v,
    "h"          => \$opt_h,
    "help"       => \$opt_h,
    "checkzfs"   => \$opt_f,
    "f"          => \$opt_f,
    "checkzpool" => \$opt_p,
    "p"          => \$opt_p,
    "quota"      => \$opt_q,
    "q"          => \$opt_q,
    "rawvolumes" => \$opt_w,
    "w"          => \$opt_w,
    "zfsname=s"  => \$opt_s,
    "s=s"        => \$opt_s,
    "zpool=s"    => \$opt_z,
    "z=s"        => \$opt_z
);

if ($opt_V)
{
    print_revision( $PROGNAME, '$Revision: 1.5 $' );
    exit $ERRORS{'OK'};
}

if ($opt_h)
{
    print_help();
    exit $ERRORS{'OK'};
}

if ($opt_z)
{
    $zpool = $opt_z;
}
if ($opt_s)
{
    $zfs_name = $opt_s;
}

my $check_made = 0;
error_checks();
get_zp_list();

if ($opt_f)
{
    get_zfs_data();
    process_zfs_data();
    $check_made++;
}

if ($opt_p)
{
    get_zpool_data();
    process_zpool_data();

    #get_zpool_status_data();
    #process_zpool_status_data();
    $check_made++;
}

if ( not $check_made )
{
    $result  = "UNKNOWN";
    $message = "No check options used";
}
if ( $result eq "OK" ) { $message = "No zfs failures detected"; }
print "$result: ZFS on $hostname - $message\n";
exit $ERRORS{$result};
