#! /usr/bin/perl -w

#====================================================================
# What's this ?
#====================================================================
# This script parses files generated by slurpd (OpenLDAP replication server)
# displays if errors occured and if master and slaves are up2date
# Script designed for Nagios/NRPE ( http://www.nagios.org )
#
# Usage :
# You have to set the replica's name and port (as written in slapd.conf)
# ./check_ldap_slurpd_status -w warning_level -c critical_level [-h hostname] [-p hostport] [-v]
# The level must be given with 3 integers, for exemple 100,5,30
# The first integer are the entries in transition (not picked up by slurpd)
# The second integer are the rejected entries (not accepted by the slave)
# The third integer are the waiting entries (picked up by slurpd but not sent to the slave)
# If only one integer is given, the level will be the max of all entries (in transition + rejected + waiting)
# If no port is declared, 0 will be used
# If no host is declared, localhost will be used
# Option -v displays a lot of message, use for debug only
#
# Copyright (C) 2004 Clement OUDOT
# Copyright (C) 2009 LTB-project.org
#
# 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.
#
# GPL Licence : http://www.gnu.org/licenses/gpl.txt
#
#==========================================================================

#==========================================================================
# Modules
#==========================================================================
use strict;
use Getopt::Std;
use List::Util qw(max);

#==========================================================================
# Internal Parameters
#==========================================================================
# Directory where slurpd stores his files
my $slurpd_tempdir = "/var/openldap-slurp/replica";

# Replog file beetween slapd and slurpd, as written in slapd.conf
my $slapd_replog_file = "/var/replog";

# Nagios Codes
my %code = ( 'Ok' => 0, 'Warning' => 1, 'Critical' => 2, 'Unknown' => 3 );

#==========================================================================
# Command Line Parameters
#==========================================================================
my %opts;
getopt( 'wchpv', \%opts );
my $debug = 1 if ( exists( $opts{"v"} ) );
&usage unless ( exists( $opts{"w"} ) );
&usage unless ( exists( $opts{"c"} ) );
$opts{"h"} = "localhost" unless ( exists( $opts{"h"} ) );
$opts{"p"} = 0           unless ( exists( $opts{"p"} ) );

my $slave_name     = $opts{"h"};
my $slave_port     = $opts{"p"};
my $warning_level  = $opts{"w"};
my $critical_level = $opts{"c"};

# See if warning_level and critical_level are given with 3 integers or just one

my @warning_level;

if ( $warning_level =~ /^(\d*)$/ ) {

    # This is just one integer
    $warning_level = $1;
    if ($debug) {
        print "Warning level set to $warning_level entries\n";
    }
}
elsif ( $warning_level =~ /^(\d*),(\d*),(\d*)$/ ) {

    # Three integers, comma separated
    @warning_level = ( $1, $2, $3 );
    if ($debug) {
        print
"Warning level set to $1 entries in transition, $2 rejected entries and $3 waiting entries\n";
    }
    $warning_level = 0;
}
else {

    # Bad usage
    &usage;
}

my @critical_level;

if ( $critical_level =~ /^(\d*)$/ ) {

    # This is just one integer
    $critical_level = $1;
    if ($debug) {
        print "Critical level set to $critical_level entries\n";
    }
}
elsif ( $critical_level =~ /^(\d*),(\d*),(\d*)$/ ) {

    # Three integers, comma separated
    @critical_level = ( $1, $2, $3 );
    if ($debug) {
        print
"Critical level set to $1 entries in transition, $2 rejected entries and $3 waiting entries\n";
    }
    $critical_level = 0;
}
else {

    # Bad usage
    &usage;
}

#==========================================================================
# Usage function
#==========================================================================
sub usage {
    if ($debug) {
        print
"Usage: $0 -w warning_level -c critical_level [-h hostname] [-p hostport] [-v]\n\n";
        print
"The level (warning or critical) can be given as 3 comma separated integers, for example 100,5,30\n";
        print
"The first integer is the number of entries in transition (not picked up by slurpd)\n";
        print
"The second integer is the number of rejected entries (not accepted by the slave)\n";
        print
"The third integer is the number of waiting entries (picked up by slurpd but not sent to the slave)\n";
        print
"If only one integer is given, the level will be the max (in transition, rejected and waiting)\n\n";
        print "hostname and hostport must fit what is written in slapd.conf\n";
        print "If no hostport is declared, 0 will be used\n";
        print "If no hostname is declared, localhost will be used\n\n";
        print "Option -v displays a lot of message, use for debug only !\n";
    }
    else {
        print "Bad script usage. Use -v for more inforations.\n";
    }
    exit $code{'Unknown'};
}

#==========================================================================
# Slurpd Files
#==========================================================================
my $slurpd_replog_file = $slurpd_tempdir . "/slurpd.replog";
my $status_file        = $slurpd_tempdir . "/slurpd.status";
my $rej_file = $slurpd_tempdir . "/" . $slave_name . ":" . $slave_port . ".rej";

#==========================================================================
# Welcome
#==========================================================================
if ($debug) {
    print
      "Determining replication status for $slave_name at port $slave_port\n\n";
}

#==========================================================================
# Preliminary Tests
#==========================================================================
# If one of the 4 files exists but we can't read it, it's a problem
foreach ( $slapd_replog_file, $slurpd_replog_file, $status_file, $rej_file ) {

    if ( ( -e $_ ) && ( !-r $_ ) ) {

        if ($debug) {
            print
"File $_ exists but is not readable for the user running this script\n";
        }
        else {
            print "Unable to read $_\n";
        }
        exit $code{'Unknown'};
    }
}

# If slapd replog file is not present, it can meen that :
# - the internal script config is bad
# - the config of OpenLDAP is bad
# - no modifications were done to slapd, so replog file was not created
# - slapd is not running well...
# => an Unknown status is returned

unless ( -e $slapd_replog_file ) {
    if ($debug) {
        print "File $slapd_replog_file does not exist\n";
    }
    else {
        print "No slapd replog file\n";
    }
    exit $code{'Unknown'};
}

# If slurpd status file is not present, it can meen that :
# - the internal script config is bad
# - slurpd was launched with an alternate tempdir (option -t)
# - slurpd was never launched
# - slurpd is not running well...
# => a Critical status is returned

unless ( -e $status_file ) {
    if ($debug) {
        print "File $status_file does not exist\n";
    }
    else {
        print "No slurpd status file\n";
    }
    exit $code{'Critical'};
}

# If slurpd replog file is not present, it can meen that :
# - the internal script config is bad
# - slurpd is not running well...
# => a Critical status is returned

unless ( -e $slurpd_replog_file ) {
    if ($debug) {
        print "File $slurpd_replog_file does not exists\n";
    }
    else {
        print "No slurpd replog file\n";
    }
    exit $code{'Critical'};
}

# If slurpd status file is empty, it can meen that :
# - it was manually cleared
# - slurpd is not running well...
# => a Critical status is returned

if ( -z $status_file ) {
    if ($debug) {
        print "File $status_file is empty\n";
    }
    else {
        print "No data in slurpd status file\n";
    }
    exit $code{'Critical'};
}

# If the slave is not present in status file, it can meen that :
# - the command line parameters are bad
# - the config of OpenLDAP is bad
# - the config of OpenLDAP is good but slurpd was not restarted
# => a Critical status is returned
# If it is present :
# Get the time and the range in status file in order to count how many
# entries are waiting in slurpd replog file

# Variables for further comparaison
my ( $status_time, $status_range );

# Open status file for reading

if ( open( STATUS, $status_file ) ) {

    # Parse the file
    # lines in status file are like :
    # servername:serverport:time:range
    while (<STATUS>) {
        if (/^$slave_name:$slave_port:(\d*):(\d*)$/) {
            ( $status_time, $status_range ) = ( $1, $2 );
        }
    }

    # Close file
    close(STATUS);

    # Test if something was found
    if ( defined $status_time ) {

        # Just print a message for debug
        if ($debug) {
            print
              "$slave_name (port $slave_port) is listed in $status_file\n\n";
            print "Time found in status file : $status_time\n\n";
            print "Range found in status file : $status_range\n\n";
        }
    }
    else {
        if ($debug) {
            print
              "$slave_name (port $slave_port) is not listed in $status_file\n";
            print
"Maybe slurpd needs to be restarted to read OpenLDAP configuration\n";
        }
        else {
            print "No status for $slave_name:$slave_port\n";
        }
        exit $code{'Critical'};
    }
}

# Error while opening the file
# => an Unknown status is returned

else {
    if ($debug) {
        print "Unable to open $status_file for reading\n";
    }
    else {
        print "Error while reading status file\n";
    }
    exit $code{'Unknown'};
}

#==========================================================================
# Waiting entries in Slapd Replog File
#==========================================================================
# If there are waiting entries in slapd replog file, it meens that slurpd
# has not yet get these entries ou that slurpd is disturbed.

my $waiting_entries = 0;

if ( open( SLAPDREPLOG, $slapd_replog_file ) ) {

    # Count entries that must be added to slurpd replog file
    while (<SLAPDREPLOG>) {
        if (/^replica:/) {
            if ($slave_port) {

                # The port is <> 0, so it is written after the name
                $waiting_entries++ if /^replica: $slave_name:$slave_port$/;
            }
            else {

     # The port is = 0, so only the name is written (or the port after the name)
                $waiting_entries++ if /^replica: $slave_name(:$slave_port)?$/;
            }
            next;
        }
    }

    # Close file
    close(SLAPDREPLOG);

    # Print debug message
    if ($debug) {
        print "$waiting_entries entries in slapd replog file\n\n";
    }
}

else {
    if ($debug) {
        print "Unable to read $slapd_replog_file\n";
    }
}

#==========================================================================
# Rejection File
#==========================================================================
# Rejection file exists only if errors occured.
# Keep time and range for comparaison with slurpd replog file

my $rejected_entries = 0;

if ( -e $rej_file ) {

    # The rejection file exists
    if ( open( REJ, $rej_file ) ) {

        # Count entries in error
        while (<REJ>) {
            $rejected_entries++ if (/^time: /);
            if ( ($debug) && (/^dn: (.*)$/) ) {
                print "Modification on $1 was rejected\n";
            }
        }

        # Close file
        close(REJ);

        # Print debug message
        if ($debug) {
            print "$rejected_entries entries rejected\n\n";
        }
    }

    else {

        # Error while opening the file
        if ($debug) {
            print "Unable to read $rej_file\n";
        }
    }
}

else {

    # No rejection file
    if ($debug) {
        print "There is no rejection file\n\n";
    }
}

#==========================================================================
# Slurpd Replog File
#==========================================================================
# Find all entries younger than time and range found in status file
# This entries must concern our slave
# => $slave_is_concerned is set to 1 if replica: $slave_name:$slave_port is found
# => Go to the next lines
# => Get the time and set $slave_is_concerned to 0, and continue the loop
my $slave_is_concerned = 0;
my $younger_entries    = 0;

if ( open( REPLOG, $slurpd_replog_file ) ) {

    # Parse the file
    while (<REPLOG>) {

        # If it is a "replica:" line
        if (/^replica:/) {
            if ($slave_port) {

                # The port is <> 0, so it is written after the name
                $slave_is_concerned = 1 if /^replica: $slave_name:$slave_port$/;
            }
            else {

     # The port is = 0, so only the name is written (or the port after the name)
                $slave_is_concerned = 1
                  if /^replica: $slave_name(:$slave_port)?$/;
            }

            # Go to the next line
            next;
        }

        if ( ($slave_is_concerned) && (/^time: (\d*)\.?(\d*)?$/) ) {

            # Reset $slave_is_concerned
            $slave_is_concerned = 0;

            # Next if replog entry is older than status time
            next if ( $1 < $status_time );

            # Count the entry if younger and go to next line
            if ( $1 > $status_time ) {
                $younger_entries++;
                if ($debug) {
                    print "Entry with time $1 is young\n";
                }
                next;
            }

            # Check the range if times are equal
            if ( $1 == $status_time ) {
                my $replog_range = $2 ? $2 : 0;

                # If replog range > status range, entry is younger
                if ( $replog_range > $status_range ) {
                    $younger_entries++;
                    if ($debug) {
                        print
"Entry with time $1 and range $replog_range is young\n";
                    }
                    next;
                }

                # Else it is older, nothing to do
            }
        }
    }

    # Close file
    close(REPLOG);
    if ($debug) {
        print "$younger_entries entries waiting in slurpd replog file\n\n";
    }
}

# Error while opening the file
else {
    if ($debug) {
        print "Unable to read $slurpd_replog_file\n";
    }
}

#==========================================================================
# Results and exit with Nagios codes
#==========================================================================
# All errors
my $all_errors = $waiting_entries + $rejected_entries + $younger_entries;
my $max_errors = max( $waiting_entries, $rejected_entries, $younger_entries );

if ($debug) {
    print "A total of $all_errors errors was found :\n";
    print "$waiting_entries entries are in transition\n";
    print "$rejected_entries entries are rejected\n";
    print "$younger_entries entries are waiting\n";
}

# Message to sent
print
"$waiting_entries entries in transition, $rejected_entries entries rejected, $younger_entries entries waiting\n";

# exit code

if (
    ( $critical_level && $max_errors >= $critical_level )
    || (
        @critical_level
        && (   $critical_level[0] <= $waiting_entries
            || $critical_level[1] <= $rejected_entries
            || $critical_level[2] <= $younger_entries )
    )
  )
{
    exit $code{'Critical'};
}

if (
    ( $warning_level && $max_errors >= $warning_level )
    || (
        @warning_level
        && (   $warning_level[0] <= $waiting_entries
            || $warning_level[1] <= $rejected_entries
            || $warning_level[2] <= $younger_entries )
    )
  )
{
    exit $code{'Warning'};
}

exit $code{'Ok'};
