#!/usr/bin/perl
use strict;
my $VERSION = '0.1.1';
my $COPYRIGHT = 'Copyright (C) 2008 Jonathan Buhacoff <jonathan@buhacoff.net>';
my $LICENSE = 'http://www.gnu.org/licenses/gpl.txt';
my %status = ( 'OK' => 0, 'WARNING' => 1, 'CRITICAL' => 2, 'UNKNOWN' => 3 );
my $SERVICE = "MYSQL SLAVE";

# look for required modules
exit $status{UNKNOWN} unless load_modules(qw/Getopt::Long DBI DBD::mysql/);

Getopt::Long::Configure("bundling");
my $verbose = 0;
my $help = "";
my $help_usage = "";
my $show_version = "";
my $mysql_server = "";
my $default_mysql_port = "3306";
my $mysql_port = "";
my $warntime = 15;
my $criticaltime = 30;
my $timeout = 60;
my $username = "";
my $password = "";
my $ok;
$ok = Getopt::Long::GetOptions(
	"V|version"=>\$show_version,
	"v|verbose+"=>\$verbose,"h|help"=>\$help,"usage"=>\$help_usage,
	"w|warning=i"=>\$warntime,"c|critical=i"=>\$criticaltime,"t|timeout=i"=>\$timeout,
	# mysql settings
	"H|hostname=s"=>\$mysql_server,"p|port=i"=>\$mysql_port,
	"U|username=s"=>\$username,"P|password=s"=>\$password,
	);

if( $show_version ) {
	print "$VERSION\n";
	if( $verbose ) {
		print "Default warning threshold: $warntime seconds\n";
		print "Default critical threshold: $criticaltime seconds\n";
		print "Default timeout: $timeout seconds\n";
	}
	exit $status{UNKNOWN};
}

if( $help ) {
	exec "perldoc", $0 or print "Try `perldoc $0`\n";
	exit $status{UNKNOWN};
}

$help_usage = 1 unless $mysql_server and $username;
if( $help_usage ) {
	print "Usage: $0 -H host [-p port] [-U username] [-P password] [-w <seconds>] [-c <seconds>]\n";
	exit $status{UNKNOWN};
}

# initialize
my $report = new PluginReport;
my $time_start = time;
my $actual_response = undef;

# connect to MySQL server
$mysql_port = $default_mysql_port unless $mysql_port;

eval {
	local $SIG{ALRM} = sub { die "exceeded timeout $timeout seconds\n" }; # NB: \n required, see `perldoc -f alarm`
	alarm $timeout;

	my $dbh = DBI->connect("DBI:mysql:host=$mysql_server;port=$mysql_port",$username,$password);
	# get mysql version
	my $version = undef;
	my $sth_version = $dbh->prepare("SHOW VARIABLES LIKE 'version'");
	$sth_version->execute;
	while( my ($name,$value) = $sth_version->fetchrow_array) {
		$version = $value if $name eq "version";
	}
	$report->{version} = $version || "";
	$sth_version->finish;
	# get slave status (should only be 1 result row)
	my $sth_status = $dbh->prepare("SHOW SLAVE STATUS");
	$sth_status->execute;
	while( my $status = $sth_status->fetchrow_hashref) {
		if( $verbose > 1 ) {		
			foreach( keys %$status ) {
				print "$_ = $status->{$_} \n";
			}
		}
		# mysql 3.23 has "Slave_Running" while 4.1 and above have "Slave_IO_Running" and "Slave_SQL_Running"
		$report->{Running} = "No";
		if( $version lt "4" ) {
			$report->{Running} = "Yes" if $status->{Slave_Running};
			$report->{file} = $status->{Log_File};
			$report->{position} = $status->{Pos};
		}
		else {
			$report->{Running} = "Yes" if $status->{Slave_IO_Running} eq "Yes" and $status->{Slave_SQL_Running} eq "Yes";
			$report->{file} = $status->{Master_Log_File};
			$report->{position} = $status->{Exec_Master_Log_Pos} . '/' . $status->{Read_Master_Log_Pos};
		}
		# put the master host and its bin log file and position in the report
		foreach( keys %$status ) {
			$report->{$_} = $status->{$_};
		}
	}
	$sth_status->finish;
	# threshold for seconds behind master etc.
	# $report->{behind} = ...
	# $report->{saomethaisdf} ...
	$dbh->disconnect;
};
if( $@ ) {
	$@ = $DBI::errstr if $DBI::errstr; # these can be more helpful than "Can't call method prepare on an undefined value"
	$@ =~ s/\n/ /g; # the error message can be multiline but we want our output to be just one line
	print "$SERVICE CRITICAL - $@\n";
	exit $status{CRITICAL};	
}

my @warning = ();
my @critical = ();

# overall warnings/critical cerrors
push @critical, "not running $report->{Last_Error}" if $report->{Running} ne "Yes";
#push @warning, "connection time more than $warntime" if( $time_connected - $time_start > $warntime );
#push @critical, "connection time more than $criticaltime" if( $time_connected - $time_start > $criticaltime );
#push @critical, "response was $actual_response but expected $expect_response" if ( $actual_response ne $expect_response );
# on the number line, we need to test 6 cases:
# 0-----w-----c----->
# 0, 0<lag<w, w, w<lag<c, c, c<lag
# which we simplify to 
# lag>=c, w<=lag<c, 0<=lag<warn


# print report and exit with known status
my $short_report = $report->text(qw/file position/);
my $long_report = join("", map { "$_: $report->{$_}\n" } qw/version Master_Host Log_File Pos/ );
if( scalar @critical ) {
	my $crit_alerts = join(", ", @critical);
	print "$SERVICE CRITICAL - $crit_alerts; $short_report\n";
	print $long_report if $verbose;
	exit $status{CRITICAL};
}
if( scalar @warning ) {
	my $warn_alerts = join(", ", @warning);
	print "$SERVICE WARNING - $warn_alerts; $short_report\n";
	print $long_report if $verbose;
	exit $status{WARNING};
}
print "$SERVICE OK - $short_report\n";
print $long_report if $verbose;
exit $status{OK};


# utility to load required modules. exits if unable to load one or more of the modules.
sub load_modules {
	my @missing_modules = ();
	foreach( @_ ) {
		eval "require $_";
		push @missing_modules, $_ if $@;	
	}
	if( @missing_modules ) {
		print "Missing perl modules: @missing_modules\n";
		return 0;
	}
	return 1;
}


# NAME
#	PluginReport
# SYNOPSIS
#	$report = new PluginReport;
#   $report->{label1} = "value1";
#   $report->{label2} = "value2";
#	print $report->text(qw/label1 label2/);
package PluginReport;

sub new {
	my ($proto,%p) = @_;
	my $class = ref($proto) || $proto;
	my $self  = bless {}, $class;
	$self->{$_} = $p{$_} foreach keys %p;
	return $self;
}

sub text {
	my ($self,@labels) = @_;
	my @report = map { "$_ $self->{$_}" } grep { defined $self->{$_} } @labels;
	my $text = join(", ", @report);
	return $text;
}

package main;
1;

