#!/usr/local/bin/perl # nagios: +epn =pod =head1 NAME check_ldap_replication.pl =head1 SYNOPSIS Verify that LDAP replication between two LDAP v3 compliant master-master or master-slave mode servers is working properly. =head1 DESCRIPTION This script will do the following: =over 4 =item * Take a user-supplied LDIF (--ldif /path/to/ldif) and create the object specified in the LDIF on the master server. =item * Ensure the newly created object can be found by searching for it by the DN field in the LDIF supplied to the script (--ldif /path/to/ldif) on the master LDAP server. =item * Sleep a user-supplied number of seconds to allow replication to occur (--replication-wait). =item * Query for the record in the second master (or slave) LDAP server to ensure it made it to the replicant. =item * Delete the record from the second master (in master-master mode) or the only master (in master-slave) mode. =item * Sleep a user-supplied number of seconds to allow the deletion to propagate to both servers (--replication-wait). =item * Query for the record on the slave (in master-slave mode) or the primary master (in master-master mode) to make sure the record was properly deleted. =back If any step of the process fails, the script will return a CRITICAL alert status to Nagios along with a descriptive error message. This script will also output performance data for the following metrics: =over 4 =item * Time it took to bind to each LDAP server =item * Total time used for the add =item * Total time required to search =back The script accepts the following arguments: =over 4 =item * --bind-dn LDAP DN =item * --bind-password password =item * --search-base string =item * --first-ldap-server host[:port] =item * --second-ldap-server host[:port] =item * --ldif /path/to/ldif =item * --master-master-mode or --master-slave-mode =item * --replication-wait SECONDS =back All arguments are required. Example call: ./check_ldap_replication \ --ldif /home/nagios/etc/replication.ldif \ --first-ldap-server ldap01.example.com \ --second-ldap-server ldap02.example.com:2389 \ --master-master-mode --replication-wait 2 \ --search-base ou=people,,dc=example,dc=com \ --bind-dn myusername \ --bind-password mypassword LDAP-REPLICATION OK: 2 seconds to replicate 1 record | '1st_bind'=0.2s;0;0 '2nd_bind'=0.09s;0;0 'add'=0.5s;0;0 'search'=0.01s;0;0 =head1 Master-Master and Master-Slave Mode Differences This test will operate slightly differently when testing master-master mode LDAP replication then it will in master-slave mode. The differences are as follows: =over 4 =item * In master-master mode, the replication record will be deleted from the second LDAP server. In master-slave mode, the replication record will be deleted from the master server. =item * In master-master mode, the script will then query to ensure the deleted record doesn't exist on the first LDAP server; in master-slave mode, the script will query the second (slave) LDAP server to ensure the replication record deletion worked properly. =back =head1 Replication LDIF Format LDIF file must have DN and uid attributes that can be searched on, all other attributes depend on your schema. The record in this file will be added to the server given in the --first-ldap-server argument. Replication will be considered successful if all attributes in the LDIF exist on the server given in --second-ldap-server. Here is an example LDIF used based on test data that comes with the Sun One LDAP server: dn: uid=reptest1,ou=People,dc=test,dc=com uid: reptest1 objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson givenName: Reppie sn: Test cn: Replication Test initials: RT mail: reptest@example.com telephoneNumber: 555-1212 description: This is record is for testing purposes only =cut sub check_ldap_replication { use strict; use Net::LDAP::Express; use Net::LDAP::LDIF; use Nagios::Plugin; use Time::HiRes qw(time); my $USAGE = <new( 'shortname' => $LABEL, 'usage' => $USAGE ); $plugin->add_arg( 'spec' => 'bind-dn|U=s', 'help' => "-U, --bind-dn LDAP DN\n" . " LDAP DN to use for the replication test. This user\n" . " must be able to add, search, and delete records from\n" . " both LDAP servers.", 'required' => 1 ); $plugin->add_arg( 'spec' => 'bind-password|P=s', 'help' => "-P, --bind-password password\n" . " Password to use to authenticate against both LDAP\n" . " servers.", 'required' => 1 ); $plugin->add_arg( 'spec' => 'search-base|B=s', 'help' => "-B, --search-base search-base\n" . " LDAP search base to use when searching for the\n" . " record added by the script", 'required' => 1 ); $plugin->add_arg( 'spec' => 'first-ldap-server|F=s', 'help' => "-F, --first-ldap-server SERVER[:PORT]\n" . " First LDAP server to connect to, replication\n" . " record will be added to this server. Port is \n" . " optional, defaults to 389.", 'required' => 1 ); $plugin->add_arg( 'spec' => 'second-ldap-server|S=s', 'help' => "-S, --first-ldap-server SERVER[:PORT]\n" . " Second LDAP server to connect to, replication\n" . " record will be searched for on this server. Port is \n" . " optional, defaults to 389.", 'required' => 1 ); $plugin->add_arg( 'spec' => 'ldif|L=s', 'help' => "-L, --ldif /path/to/ldif\n" . " Full path and file name of LDIF file. LDIF file\n" . " must have a DN attribute that can be searched on,\n" . " all other attributes are optional. The record in\n" . " this file will be added to the server given in the\n" . " --first-ldap-server argument. Replication will\n" . " be considered successful if all attributes in the\n" . " LDIF exist on the server given in --second-ldap-server", 'required' => 1 ); $plugin->add_arg( 'spec' => 'master-master-mode', 'help' => "--master-master-mode\n" . " Replication is in master-master mode. See perldoc\n" . " for details on how this affects the test.", 'required' => 0 ); $plugin->add_arg( 'spec' => 'master-slave-mode', 'help' => "--master-slave-mode\n" . " Replication is in master-slave mode. See perldoc\n" . " for details on how this affects the test.", 'required' => 0 ); $plugin->add_arg( 'spec' => 'replication-wait|W=i', 'help' => "-W, --replication-wait SECONDS\n" . " How long (in seconds) to wait between adding the\n" . " replication record and searching for it.", 'required' => 1 ); $plugin->add_arg( 'spec' => 'debug|D', 'help' => "-D, --debug\n Enable debug mode", 'required' => 0, 'default' => 0 ); $plugin->getopts(); my %CFG = ( 'opts' => $plugin->opts, 'mode' => 0, 'ldif' => undef, 'stats' => { '1st_bind_time' => 0, '2nd_bind_time' => 0, 'search_time' => 0, 'total_time' => 0 }, 'first_conn' => undef, 'second_conn' => undef ); use constant MASTER_MASTER_MODE => 'master-master'; use constant MASTER_SLAVE_MODE => 'master-slave'; if ((! $plugin->opts->get('master-master-mode')) && (! $plugin->opts->get('master-slave-mode'))) { $plugin->nagios_die("Must specify --master-master-mode or " . "--master-slave-mode"); } if ($plugin->opts->get('master-master-mode')) { $CFG{'mode'} = MASTER_MASTER_MODE; } else { $CFG{'mode'} = MASTER_SLAVE_MODE; } debug("Running in $CFG{'mode'} mode"); # Ensure the specified LDIF exists and is valid. debug("Parsing replication LDIF file " . $CFG{'opts'}->get('ldif')); $CFG{'ldif'} = parse_ldif_file($plugin, $CFG{'opts'}->get('ldif')); # Connect to the first server, time how long it takes to bind. debug("Connecting to 1st LDAP server " . $CFG{'opts'}->get('first-ldap-server')); my $start_time = time(); my ($first_ldap, $first_bind_time) = connect_to_ldap_server( $plugin, $CFG{'opts'}->get('first-ldap-server'), $CFG{'opts'}->get('bind-dn'), $CFG{'opts'}->get('bind-password'), $CFG{'opts'}->get('search-base') ); $CFG{'1st_conn'} = $first_ldap; $CFG{'stats'}->{'1st_bind_time'} = $first_bind_time; # Add the LDIF to the first server. if ($CFG{'opts'}->get('debug')) { debug("Adding LDIF to " . $CFG{'opts'}->get('first-ldap-server') . ": " . $CFG{'ldif'}->current_entry()->dump()); } add_ldif_to_ldap($plugin, $CFG{'1st_conn'}, $CFG{'ldif'}); # Sleep --replication-wait seconds for replication to occur. my $wait = $CFG{'opts'}->get('replication-wait'); debug("Sleeping $wait seconds to allow replication to occur"); sleep($wait); debug("Connecting to 2nd LDAP server " . $CFG{'opts'}->get('second-ldap-server')); # Search for the record on the second server. my ($second_ldap, $second_bind_time) = connect_to_ldap_server( $plugin, $CFG{'opts'}->get('second-ldap-server'), $CFG{'opts'}->get('bind-dn'), $CFG{'opts'}->get('bind-password'), $CFG{'opts'}->get('search-base') ); $CFG{'2nd_conn'} = $first_ldap; $CFG{'stats'}->{'2nd_bind_time'} = $second_bind_time; debug("Ensuring LDAP record was created on secondary server"); my ($found, $search_time) = search_for_record($plugin, $CFG{'2nd_conn'}, $CFG{'ldif'}, 1); $CFG{'stats'}->{'search_time'} = $search_time; if ($found == 0) { $CFG{'stats'}->{'total_time'} = 0; $plugin->nagios_exit(CRITICAL, "Replication failed, record not found on replicant!" . make_perfdata($CFG{'stats'})); return CRITICAL; } # Delete the record from the secondary master in master-master mode # or the slave in master-slave mode. if ($CFG{'mode'} eq MASTER_MASTER_MODE) { debug("Deleting LDAP record from secondary master server"); delete_record($plugin, $CFG{'2nd_conn'}, $CFG{'ldif'}); } else { debug("Deleting LDAP record from master server"); delete_record($plugin, $CFG{'1st_conn'}, $CFG{'ldif'}); } debug("Sleeping $wait seconds to allow deletion to replicate"); sleep($wait); # Search for the record on the primary master in master-master mode # or the slave in master-slave mode to ensure the deletion worked. my $not_found = 0; if ($CFG{'mode'} eq MASTER_MASTER_MODE) { debug("Ensuring LDAP record was deleted from secondary master server"); my @results = search_for_record($plugin, $CFG{'2nd_conn'}, $CFG{'ldif'}, 0); $not_found = ! $results[0]; } else { debug("Ensuring LDAP record was deleted from master server"); my @results = search_for_record($plugin, $CFG{'1st_conn'}, $CFG{'ldif'}, 0); $not_found = ! $results[0]; } if ($not_found == 0) { $CFG{'stats'}->{'total_time'} = 0; $plugin->nagios_exit(CRITICAL, "Replication record deletion failed!" . make_perfdata($CFG{'stats'})); } my $total_time = sprintf("%0.2f", time() - $start_time); $CFG{'stats'}->{'total_time'} = $total_time; my $message = "$LABEL OK - Replicated one record in mode $CFG{'mode'} in " . "$total_time seconds"; print "$message | " . make_perfdata($CFG{'stats'}) . "\n"; return OK; sub parse_ldif_file { my $plugin = shift; my $file = shift; my $ldif = undef; eval { $ldif = Net::LDAP::LDIF->new($file, 'r', 'onerror' => 'die'); my $entry = $ldif->read_entry(); }; if ($@) { $plugin->nagios_die("Can't parse LDIF $file: $@"); } if ($CFG{'opts'}->get('debug')) { my $entry = $ldif->current_entry(); my @attrs; foreach my $attr ($entry->attributes) { push(@attrs, join('=', $attr, $entry->get_value($attr))); } debug("Parsed LDIF: " . join(', ', @attrs)); } return $ldif; } sub connect_to_ldap_server { my $plugin = shift; my $server_spec = shift; my $dn = shift; my $pass = shift; my $search_base = shift; my ($host, $port) = split(':', $server_spec, 2); $port = 389 if ! defined $port; my $ldap = undef; my $start = time(); my $debug = 0; if ($CFG{'opts'}->get('debug') == 1) { $debug = 1 | 2; } eval { $ldap = Net::LDAP::Express->new( 'host' => $host, 'port' => $port, 'bindDN' => $dn, 'bindpw' => $pass, 'base' => $search_base, 'debug' => $debug, 'searchattrs' => [qw(uid)] ); } ; my $connect_time = time() - $start; if ($@) { $plugin->nagios_die("Can't connect to ldap server $host: $@"); } return ($ldap, $connect_time); } sub add_ldif_to_ldap { my $plugin = shift; my $conn = shift; my $ldif = shift; debug("Adding new entry to replication master"); my $new_entry = $ldif->current_entry(); my $result = $conn->add($new_entry); if ($result->code) { my $msg = "Could not add record to LDAP: " . $result->error; $plugin->nagios_exit(CRITICAL, $msg); } } sub search_for_record { my $plugin = shift; my $conn = shift; my $ldif = shift; my $expect = shift; my $entry = $ldif->current_entry(); # Get UID from DN or UID fields my $dn = $entry->dn(); my $uid = ($dn =~ m/uid=(\w+),/)[0]; if ($uid eq '') { $uid = $entry->get_value('uid'); if ($uid eq '') { $plugin->nagios_exit(UNKNOWN, "Unable to parse UID from uid " . "or dn fields in LDIF!"); } } my $filter = "(uid=${uid})"; my $base = $CFG{'opts'}->get('search-base'); debug("Searching for record with search base $base and filter $filter"); my $start = time(); my $results = $conn->search('filter' => $filter) ; my $total = time() - $start; my $count = $results->count(); debug("Search return $count records"); if ($count != $expect) { my $msg = "Expected 1 entry for $filter, base $base, found $count!"; $plugin->nagios_exit(CRITICAL, $msg); } return($count, $total); } sub delete_record { my $plugin = shift; my $conn = shift; my $ldif = shift; my $result = $conn->delete($ldif->current_entry()); if ($result->code) { my $msg = "Could not delete record: " . $result->error; $plugin->nagios_exit(CRITICAL, $msg); } } sub debug { return unless $CFG{'opts'}->get('debug') == 1; my $msg = shift; print STDERR scalar(localtime(time())) . ": $msg\n"; } sub make_perfdata { my $stats = shift; my $output = ""; for my $stat (sort keys %$stats) { $output .= sprintf("'$stat'=%0.2fs;0;0 ", $stats->{$stat}); } return $output; } } exit check_ldap_replication();