# -*- perl -*-
#
# $Revision: 1.1 $
#/s/nagios/bin/nagioscmd service post <host> <service> \
# <result_code> <service_message>


package raidtab;

require Exporter;
@ISA = qw(Exporter);
@EXPORT_OK = qw(&print_usage &print_version &service_test);

use FileHandle;
use Data::Dumper;

use lib $main::runtimedir;
use csl_utils qw($TIMEOUT %ERRORS);
use strict;

my $RAIDTAB = '/etc/raidtab';
#testing
# my $MDSTAT  = '/tmp/mdstat';
my $MDSTAT  = '/proc/mdstat';

my ($critical_error, $warning_error);

sub print_usage ();
sub print_version ();
sub service_test (%);

sub print_usage () {

    print "
Usage:  Check raidtab service.
Options:
    -H <hostname> -P <portnumber> [--verbose]
    --help
    --version
    --usage
";
    return $ERRORS{'OK'};
}

sub print_version () {
    my $version = '$Revision: 1.1 $';
    $version =~ s/^\$Revision: //;
    $version =~ s/ \$\s*$//;

    print "afsd.pm - $version\n";
    return $ERRORS{'OK'};
}

sub service_test (%) {
    my %options = @_;

    $critical_error = $warning_error = 0;

    if ($options{"verbose"}) {
        foreach my $opt (sort keys(%options)) {
            if ($options{$opt}) {
                print "$opt - $options{$opt}\n";
            }
        }
    }

    # my $host = $options{"opt_H"};
    #
    # if ((!$host) || ($host eq "")) {
    #    print "\nHost name not specified.\n";
    #    &print_usage();
    #    return $ERRORS{'OK'};
    #}

    # Read raidtab file
    my $raidtab = read_raidtab($RAIDTAB);
    defined ($raidtab) or critical ("# Couldn't read $RAIDTAB ");
    # print Dumper ($raidtab);

    # Read the current mdstats
    my $mdstat = read_mdstat($MDSTAT);
    defined ($mdstat) or critical ("# Couldn't read $MDSTAT ");
    # print Dumper ($mdstat);

    # Check and report differences
    checkdata ($raidtab, $mdstat);

    # See how we did, and report
    $critical_error and return $ERRORS{CRITICAL};
    $warning_error  and return $ERRORS{WARNING};
    print "# raidtab/mdstat OK ";
    return $ERRORS{OK};

}

# Parse the raidtab file into some kinda structure
sub read_raidtab {
  my $filename = shift;

  my ($raidtab, $raiddev, $param, $dtype, $dname, $fh);

  defined ($fh = new FileHandle $filename, 'r') or return undef;
  LINE: while (<$fh>) {
    chomp;
    if (/(^\s*\#)|(^\s*$)/) {
      # Skip blanks and comments
      next;
    }
    if (/^\s*raiddev\s+\/dev\/(\S+)\s*$/) {
      # New raid device
      defined ($raiddev) and delete $raiddev->{device};
      $raiddev = $raidtab->{$1} = {};
      next LINE;
    }
    # Check for several parameters
    foreach $param ('raid-level', 'nr-raid-disks', 'nr-spare-disks',
		    'chunk-size', 'device') {
      if (/^\s*${param}\s+(\S+)\s*$/) {
	$raiddev->{$param} = $1;
	next LINE;
      }
    }
    if (/\s*(raid-disk|spare-disk)\s+(\S+)\s*$/) {
      ($dtype, $dname) = ($1, $2);
      # print STDERR "Got $dtype $dname \n";
      defined ($raiddev->{device}) or next;
      $raiddev->{device} =~ s/\/dev\///g;
      $raiddev->{$dtype}->{$dname} = $raiddev->{device};
    }
  }

  $raidtab;
}

# Parse the mdstat file.  This is a little gooey, but, ya know, /proc/mdstat
# is kinda that way...
sub read_mdstat {
  my $filename = shift;
  my ($mdstat, $fh, $dev, $md, undef, $status, $level, @devs, $raiddev);
  my ($blocks, $bname, $chunk, $ndisks, $diskstat);

  defined ($fh = new FileHandle $filename, 'r') or return undef;
  while (<$fh>) {
    chomp;
    if (/^\s*(md\d+)\s*:\s*/) {
      # New Raid device
      ($md, undef, $status, $level, @devs) = split;
      $raiddev = $mdstat->{$md} = {};
      $level =~ s/raid//;
      $raiddev->{'raid-level'} = $level;
      $raiddev->{status} = $status;
      $_ = <$fh>;
      chomp;
      if ($level == 1) {
	($blocks, $bname, $ndisks, $diskstat) = split;
	($bname eq 'blocks') or critical ("# $md: mdstat: Bad raid1 format line 2 ");
	($raiddev->{'nr-wanted-disks'},$raiddev->{'nr-have-disks'})
	  = ($ndisks =~ /\[(\d+)\/(\d+)\]/);
      } elsif ($level == 5) {
	($blocks, $bname, undef, undef, $chunk, undef, undef, undef, $ndisks, $diskstat) = split;
	($bname eq 'blocks') or critical ("# $md: mdstat: Bad raid5 format line 2 ");
	$chunk =~ s/k$//;
	$raiddev->{'chunk-size'} = $chunk;
	($raiddev->{'nr-wanted-disks'},$raiddev->{'nr-have-disks'})
	  = ($ndisks =~ /\[(\d+)\/(\d+)\]/);
      } else {
	critical ("# $md: No parser for raid level $level ");
      }

      # Now assign disks to either raid-disks or spare-disks
      foreach $dev (@devs) {
	my ($dname, $dnum) = ($dev =~ /(.*)\[(.*)\]/);
	defined ($dname) or next;
	if ($dnum < $raiddev->{'nr-wanted-disks'}) {
	  # raid-disk
	  $raiddev->{'raid-disk'}->{$dnum} = $dname;
	} else {
	  #spare-disk
	  $raiddev->{'spare-disk'}->{$dnum-$raiddev->{'nr-wanted-disks'}} = $dname;
	}
	
      }	
    } elsif (/(resync|recovery)/) {
      if (/(resync|recovery)\s*=\s*(\S+)/) {
	$raiddev->{resync}=$2;
	if (/finish\s*=\s*(\S+)/) {
	  $raiddev->{resync_finish}=$1;
	}
      }
    }
  }
  $mdstat;
}


# Compare the raidtab and mdstat data and report problems
sub checkdata {
  my $raidtab = shift;
  my $mdstat = shift;

  my ($tab, $stat, $n, $ok, $td, $sd);
  foreach (keys %$raidtab) {
    $tab = $raidtab->{$_};
    if (!exists $mdstat->{$_}) {
      critical ("# $_: missing from mdstat ");
      next;
    }
    $stat = $mdstat->{$_};

    # Raid levels should agree
    if (!defined $tab->{'raid-level'} || !defined ($stat->{'raid-level'}) ||
	($tab->{'raid-level'} ne $stat->{'raid-level'})) {
      critical ("# $_: raid level mismatch ");
    }

    # Chunck sizes should also agree
    if ($tab->{'raid-level'} == 5) {
      if (!defined $tab->{'chunk-size'} || !defined ($stat->{'chunk-size'}) ||
	  ($tab->{'chunk-size'} ne $stat->{'chunk-size'})) {
	critical ("# $_: chunk size mismatch ");
      }
    }

    # The number of disks must agree
    if (!defined ($tab->{'nr-raid-disks'}) ||
	!exists ($tab->{'raid-disk'}) ||
	(scalar keys %{$tab->{'raid-disk'}} != $tab->{'nr-raid-disks'})) {
      critical ("# $_: raidtab: Not all raid disks specified ");
    } elsif (!defined ($stat->{'nr-wanted-disks'}) ||
	     !defined ($stat->{'nr-have-disks'}) ||
	     !exists ($stat->{'raid-disk'})) {
      critical ("# $_: mdstat format error ");
    } elsif ($stat->{'nr-wanted-disks'} != $stat->{'nr-have-disks'}) {
      critical ("# $_: running in degraded mode ");
    } elsif (scalar keys %{$stat->{'raid-disk'}} != $stat->{'nr-have-disks'}) {
      critical ("# $_: mdstat disk number format error ");
    } else {
      # Check that devices actually match.  This is a warning only
      ($ok, $td, $sd) = (1, $tab->{'raid-disk'}, $stat->{'raid-disk'});
      foreach $n (sort keys %$td) {
	(exists ($sd->{$n}) && ($td->{$n} eq $sd->{$n})) or $ok = 0;
      }
      $ok or warning ("# $_: raidtab/mdstat raid disk name mismatch ");
    }
    if ($tab->{'nr-spare-disks'}) {
      # Now check spare disks
      if (!exists ($tab->{'spare-disk'}) ||
	  (scalar %{$tab->{'spare-disk'}} != $tab->{'nr-spare-disks'})) {
	critical ("# $_: raidtab: spare disks not specified ");
      } elsif (!exists ($stat->{'spare-disk'}) ||
	       (scalar %{$stat->{'spare-disk'}} != $tab->{'nr-spare-disks'})) {
	critical ("# $_: mdstat: spare disk count mismatch ");
      } else {
	# Check spare device names
      ($ok, $td, $sd) = (1, $tab->{'spare-disk'}, $stat->{'spare-disk'});
      foreach $n (sort keys %$td) {
	(exists ($sd->{$n}) && ($td->{$n} eq $sd->{$n})) or $ok = 0;
      }
      $ok or warning ("# $_: raidtab/mdstat spare disk name mismatch ");

      }
    }
    if (exists $stat->{resync}) {
      my $msg = "# $_: resync = $stat->{resync} ";
      if (exists $stat->{resync_finish}) {
	$msg .= "(finish=$stat->{resync_finish}) ";
      }
      warning ($msg);
    }
  }

  # Finally check to be sure all the mdstat devices are described
  # in the raidtab
  foreach (keys %$mdstat) {
    if (!exists $raidtab->{$_}) {
      critical ("# $_: mdstat: not described in raidtab ");
    }
    if ($mdstat->{$_}->{status} ne 'active') {
      critical ("# $_: mdstat: status not active ");
    }
  }
}

# Report a warning
sub warning {
  my $warning = shift;
  print $warning;
  $warning_error = 1;
}

# Report a critical error
sub critical {
  my $error = shift;
  print $error;
  $critical_error = 1;
}

1;
