vereinfacht und flexibilisiert weil der zu prüfende "cn" nicht immer an der Wurzel des DIT liegen kann
authorMatthias Förste <foerste@schlittermann.de>
Tue, 19 Apr 2016 13:34:06 +0200
changeset 11 5d59fd79e7f4
parent 10 dcdc1a43d887
child 12 7202e55a0713
vereinfacht und flexibilisiert weil der zu prüfende "cn" nicht immer an der Wurzel des DIT liegen kann
TODO
check_ldap_repl.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO	Tue Apr 19 13:34:06 2016 +0200
@@ -0,0 +1,2 @@
+* cn ist misnomer (kann ja alles mögliche sein)
+* doku aktualisieren
--- a/check_ldap_repl.pl	Tue Feb 16 10:03:05 2016 +0100
+++ b/check_ldap_repl.pl	Tue Apr 19 13:34:06 2016 +0200
@@ -19,21 +19,18 @@
 #    Matthias Förste <foerste@schlittermann.de>
 
 use strict;
+use warnings;
+
+# required? https://rt.cpan.org/Public/Bug/Display.html?id=95875
+use threads;
+
 use File::Basename;
-use Getopt::Long;
-use Config::IniFiles;
+use AppConfig;
 use Net::LDAP;
-use IO::Prompt;
 use File::stat;
 use Pod::Usage;
 use if $ENV{DEBUG} => "Smart::Comments";
 
-sub version($$);
-sub read_config();
-sub ldap_object($);
-sub get_stamp($$);
-sub compare_results(%);
-
 my %ERRORS = (
 	OK        => 0,
 	WARNING   => 1,
@@ -44,264 +41,42 @@
 
 my $ME      = basename $0;
 my $NAME    = "LDAPREPL";
-my $VERSION = "0.3.2";
-
-my $master_default = "ldap://ldap-master:389/";
-my $slave_default = "ldap://ldap-slave:389/";
-my $cn_default = "replcheck";
-
-my %opt = (
-	init	=> 0,
-	delete  => 0,
-	refresh => 0,
-	cn      => $cn_default,
-	wait    => 1,
-	file    => "/etc/nagios/ius/plugins/config/check_ldap_repl.cfg",
-	master	=> $master_default,
-	slave	=> $slave_default
-);
-
-MAIN: {
-	Getopt::Long::Configure('bundling');
-	GetOptions(
-		"i|init"       => \$opt{init},
-		"d|delete"     => \$opt{delete},
-		"r|refresh"    => \$opt{refresh},
-		"b|binddn=s"   => \$opt{binddn},
-		"p|password=s" => \$opt{password},
-		"c|cn=s"       => \$opt{cn},
-		"w|wait=i"     => \$opt{wait},
-		"M|master=s"   => \$opt{master},
-		"S|slave=s"    => \$opt{slave},
-		"f|file=s"     => \$opt{file},
-		"h|help"       => sub { pod2usage( -verbose => 1, -exitval => $ERRORS{OK} ) },
-		"m|man"        => sub { pod2usage( -verbose => 2, -exitval => $ERRORS{OK} ) },
-		"V|version"    => sub { version( $ME, $VERSION ); exit $ERRORS{OK}; }
-	) or pod2usage( -verbose => 1, -exitval => $ERRORS{CRITICAL} );
-
-	# init or delete the ldap object
-	if ($opt{init}) {
-		ldap_object("init");
-		print "new object successfully initialized\n";
-		exit $ERRORS{OK};
-	} elsif ($opt{delete}) {
-		ldap_object("delete");
-		print "object successfully deleted\n";
-		exit $ERRORS{OK};
-	}
-
-	# refresh our ldap object
-	ldap_object("refresh") if ($opt{refresh});
-
-	my ($master, $slave, $cn) = undef;
-	my @slaves = ();
-	my %results = ();
+my $VERSION = "0.3.3";
 
-	# preparing for the comparison of ldap entries
-    if (($opt{master} ne $master_default) || ($opt{slave} ne $slave_default) || ($opt{cn} ne $cn_default)) {
-        $master = $opt{master};
-        @slaves = split(/,/, $opt{slave});
-        $cn = $opt{cn};
-    } elsif (-r $opt{file}) {
-        (undef, undef, $master, $slave, $cn) = read_config();
-        @slaves = split(/,/, $slave);
-    } else {
-        $master = $opt{master};
-        @slaves = split(/,/, $opt{slave});
-        $cn = $opt{cn};
-    }
-
-	# get the values from the ldap
-	$results{$master}{'master'} = get_stamp($master, $cn);
-	sleep $opt{wait};
-	foreach (@slaves) {
-		$results{$_}{'slave'} = get_stamp($_, $cn);
-	}
-
-	# compare the time stamps and generate the output
-	compare_results(\%results);
-}
+my $defaults = {
+    'init|i!'       => 0,
+    'delete|d!'     => 0,
+    'refresh|r!'    => 1,
+    'dn=s'          => undef,
+    'binddn|D=s'    => undef,
+    'password=s'    => undef,
+    'wait|w=i'      => 1,
+    'config=s'      => '/etc/nagios/ius/plugins/config/check_ldap_repl.cfg',
+    'provider|p=s'  => 'ldap://provider:389',
+    'consumer|c=s@' => 'ldap://consumer:389',
+    'help|h!'       => sub { pod2usage(-verbose => 1, -exitval => $ERRORS{OK}) },
+    'man|m!'        => sub { pod2usage(-verbose => 2, -exitval => $ERRORS{OK}) },
+    'version|V!'    => sub { version($ME, $VERSION); exit $ERRORS{OK}; }
+};
 
-sub compare_results(%) {
-	my @output = ();
-	my %stamps = ();
-	my (%results) = %{$_[0]};
-	for my $server ( keys %results ) {
-		for my $type ( keys %{ $results{$server} } ) {
-			$stamps{$results{$server}{$type}} = "";
-			push @output, "$type: $server = $results{$server}{$type}";
-		}
-	}
+my $attr = 'description';
+
+sub critical { print STDERR "$NAME CRITICAL: ", @_; exit $ERRORS{CRITICAL}; }
+ $SIG{__DIE__} = sub { print STDERR "$NAME UNKNOWN: ", @_; exit $ERRORS{UNKNOWN}; };
+
+sub stamp {
+    my ($u, $dn) = @_;
 
-	@output = sort(@output);
-	if (scalar(keys(%stamps)) != 1 ) {
-		print "$NAME CRITICAL: server are not in sync @output\n";
-		exit $ERRORS{CRITICAL};
-	} else {
-		print "$NAME OK: servers are in sync @output\n";
-		exit $ERRORS{OK};
-	}
-}
+    my $l = ref $u eq 'Net::LDAP' ? $u : Net::LDAP->new($u, onerror => 'die') or die "$@";
+    my $r = $l->search(base => $dn, scope => 'base', filter => '(objectClass=*)');
+    die "unexpected result count: ", $r->count unless $r->count == 1;
+    my @v = $r->entry(0)->get_value($attr);
+    die "unexpected value count [@v]" unless @v == 1;
+    return $v[0];
 
-sub read_config() {
-	my ($binddn, $password, $cn, $master, $slave);
-	my $cfg = new Config::IniFiles( -file => "$opt{file}");
-	$binddn = $cfg->val('bind', 'dn')?$cfg->val('bind', 'dn'):$opt{binddn};
-	$password = $cfg->val('bind', 'password')?$cfg->val('bind', 'password'):$opt{password};
-	$master = $cfg->val('master', 'server')?$cfg->val('master', 'server'):$opt{master};
-	$slave = $cfg->val('slave', 'server')?$cfg->val('slave', 'server'):$opt{slave};
-	$cn = $cfg->val('object', 'cn')? $cfg->val('object', 'cn'):$opt{cn};
-
-	return ($binddn, $password, $master, $slave, $cn);
 }
 
-sub ldap_object($) {
-	my $type = shift;
-	my ($binddn, $password) = undef;
-
-	my $master = $opt{master};
-	my $cn = $opt{cn};
-
-	# ldap object init/delete is only allowed at the prompt
-	if ( ($type eq "init") || ($type eq "delete") ) {
-		$binddn = prompt('BindDN: ');
-		$password = prompt('Password: ', -e => '*');
-		if (($opt{master} ne $master_default) || ($opt{cn} ne $cn_default)) {
-			$master = $opt{master};
-			$cn = $opt{cn};
-		} elsif ( -r $opt{file} ) {
-			(undef, undef, $master, undef, $cn) = read_config();
-		}
-	} else {
-		if ($opt{binddn} && $opt{password}) {
-			$binddn = $opt{binddn};
-			$password = $opt{password};
-		} elsif ( -r $opt{file} ) {
-			($binddn, $password, undef, undef, undef) = read_config();
-		} else {
-			$binddn = prompt('BindDN: ');
-			$password = prompt('Password: ', -e => '*');
-		}
-
-		if (($opt{master} ne $master_default) || ($opt{cn} ne $cn_default)) {
-			$master = $opt{master};
-			$cn = $opt{cn};
-		} elsif ( -r $opt{file} ) {
-			(undef, undef, $master, undef, $cn) = read_config();
-		}
-	}
-	
-	my $ldap = Net::LDAP->new( $master );
-
-	if (!$ldap) {
-		print "$NAME CRITICAL: [$master] $!\n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	my $mesg = $ldap->bind("$binddn", password => $password);
-	if ($mesg->code) {
-		$ldap->unbind() if ($ldap);
-		print "$NAME CRITICAL: " . $mesg->error . "\n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	# get ldap naming context
-	my $dse = $ldap->root_dse();
-	my $context = $dse->get_value('namingContexts');
-
-	if (! defined $context) {
-		print "$NAME CRITICAL: can't determine ldap 'naming context'\n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	if ($mesg->code) {
-		print "$NAME CRITICAL: " . $mesg->error . "\n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	# initialize check object
-	$mesg = $ldap->add(
-		"cn=$cn,$context",
-		attr => [
-			'objectclass' => [ 'top', 'person' ],
-			'cn' => "$cn",
-			'sn' => "$cn",
-			'description' => time()
-			],
-	) if ($type eq "init");
-
-	# delete check object
-	$mesg = $ldap->delete("cn=$cn,$context") if ($type eq "delete");
-
-	# refresh check object
-	$mesg = $ldap->modify(
-		"cn=$cn,$context",
-		replace => {
-			description => time()
-		}
-	) if ($opt{refresh});
-
-	if ($mesg->code && ($type eq "delete" || $type eq "init" || $type eq "refresh")) {
-		print "$NAME WARNING: [ldapt] " . $mesg->error . "\n";
-		exit $ERRORS{WARNING};
-	}
-
-	$ldap->unbind() if ($ldap);
-	return 0;
-}
-
-sub get_stamp($$) {
-	my ($server, $cn) = @_;
-
-	my $ldap = Net::LDAP->new( $server );
-	if (!$ldap) {
-		print "$NAME CRITICAL: [$server] $!\n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	my $mesg = $ldap->bind();
-
-	if ($mesg->code) {
-		$ldap->unbind() if ($ldap);
-		print "$NAME CRITICAL: " . $mesg->error . "\n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	# get ldap naming context
-	my $dse = $ldap->root_dse();
-	my $context = $dse->get_value('namingContexts');
-
-	if (! defined $context) {
-		print "$NAME CRITICAL: can't determine ldap 'naming context'\n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	$mesg = $ldap->search(
-		base => "cn=replcheck,$context",
-		scope => "base",
-		filter => "(cn=$cn)",
-		attr => [ 'description' ]
-
-	);
-
-	if ($mesg->code) {
-		$ldap->unbind() if ($ldap);
-		print "$NAME CRITICAL: " . $mesg->error . "\n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	if ($mesg->count != 1) {
-		print "$NAME CRITICAL: \n";
-		exit $ERRORS{CRITICAL};
-	}
-
-	my $entry = $mesg->entry(0);
-	return $entry->get_value("description");
-
-	$ldap->unbind() if ($ldap);
-}
-
-sub version($$) {
+sub version {
 	my ( $progname, $version ) = @_;
 	
 	print <<_VERSION;
@@ -315,6 +90,41 @@
 _VERSION
 }
 
+MAIN: {
+
+    my $c = AppConfig->new( { CASE => 1 },
+        map { $_, { ref $defaults->{$_} eq 'CODE' ? 'ACTION' : 'DEFAULT' => $defaults->{$_} } } keys %{$defaults}
+    ) or die "Can't initialize";
+
+    my $cf = $c->get('config');
+    # ignore default configuration file if it does not exist
+    $c->file($cf) if -e $cf;
+
+    # read configuration file if passed on command line
+    $c->getopt(qw(no_ignore_case));
+    $c->file($cf) if $cf ne ($cf = $c->get('config'));
+    # make sure that command line options override any config file options
+    $c->getopt;
+
+    my %o = $c->varlist('.');
+    my $t = time();
+
+    my $p = Net::LDAP->new($o{provider}, onerror => 'die' ) or die $@;
+    $p->bind($o{binddn}, password => $o{password});
+    $p->modify($o{dn}, replace => { $attr => $t });
+
+    my $tp = stamp($p, $o{dn});
+    if ($o{refresh}) {
+        die "Provider update failed for unknown reason\n" unless $tp == $t;
+        sleep $o{wait};
+    }
+    for (@{$o{consumer}}) { critical "'$_' out of sync\n" unless $tp == stamp($_, $o{dn}); }
+
+    print "$NAME OK: servers are in sync\n";
+    exit $ERRORS{OK};
+
+}
+
 __END__
 
 =head1 NAME
@@ -323,10 +133,7 @@
 
 =head1 SYNOPSIS
 
-check_ldap_repl [-i|--init]
-                [-d|--delete]
-                [-r|--refresh]
-                [-c|--cn string]
+check_ldap_repl [-c|--cn string]
                 [-b|--binddn string]
                 [-p|--password string]
                 [-f|--file string]
@@ -341,24 +148,6 @@
 
 =over
 
-=item B<-i>|B<--init>
-
-Add the check object cn=replcheck,I<namingContext> to the master server if not exists. See also the B<--cn> option.
-You will ask for an B<binddn> and B<password>, if not given B<--binddn> and B<--password> options.
-LDAP object initialisation is only allowed at the prompt. Your B<binddn> must have write permission to the ldap master server.
-
-=item B<-d>|B<--delete>
-
-Delete the check object from the ldap master server if exists. See also the B<--cn> option.
-You will ask for an B<binddn> and B<password>, if not given B<--binddn> and B<--password> options.
-LDAP object deletion is only allowed at the prompt. Your B<binddn> must have write permission to the ldap master server.
-
-=item B<-r>|B<--refresh>
-
-Refresh the stamp attribute of the check attribute with current unix time.
-You will ask for an B<binddn> and B<password>, if not given B<--binddn> and B<--password> options. See also B<--file> option.
-Your B<binddn> must have write permission to the ldap master server.
-
 =item B<-c>|B<--cn> I<string>
 
 cn for the initialized object. See also the B<--init> option. (default: replcheck)