Home

Search this site:
 

Horshack’s
very
own site

 

This is Bavaria, Germany!
You will find links to Perl,
bootdisks for system administration,
a lot of Linux,
Windows administration
and some personal belongings.
Writing an own Service with Nagios (easier with Nagios-helper-module in Perl)

Writing an own Service with Nagios
(easier with Nagions-helper-module in Perl)

Some words of wisdom

When writing check-programs for Nagios you will recognize that you do the same things all over again. This is why I wrote a module Nagios::Clientstatus in Perl which does these things for you.

Perlmodule Nagios::Clientstatus

package Nagios::Clientstatus;
use strict;
use warnings;
use Getopt::Long;
use Data::Dumper;
use Log::Log4perl;
use Exporter;

our $VERSION = "0.02";

=head1 Function

Create a program to check the function of some service or device
for Nagios. This module helps you to check the mandatory and
optional arguments. It helps you to send the right output so that
Nagios can check wether the service works ok or not.

=cut

=head1 Synopsis

    use Nagios::Clientstatus;
    # Arguments to the program are:
    # --critical=40 --warning=35 --hostname=server1.zdf --sensor_nr=4
    my $version = "0.01";
    my $ncli    = Nagios::Clientstatus->new(
        help_subref    => \&help,
        version        => $version,
        mandatory_args => [ "hostname", "sensor_nr", "critical", "warning" ],
    );

    # ask only one time, because it's expensive
    my $temperature = &get_temperature_of_sensor(
        hostname  => $ncli->get_given_arg('hostname'),
        sensor_nr => $ncli->get_given_arg('sensor_nr'),
    );

    # Message for the user to read
    my $msg;
    my $status;

    # strange case
    if (   ( !defined $temperature )
        || ( defined $temperature && $temperature eq "" ) )
    {
        $status = "unknown";
        $msg    = "Could not get temperature from sensor";
    }
    else {

        # We got a temperature
        # worst case first
        if ( $temperature > $ncli->get_given_arg('critical') ) {
            $status = "critical";
        }
        elsif ( $temperature > $ncli->get_given_arg('warning') ) {
            $status = "warning";
        }
        else {
            $status = "ok";
        }
        $msg = sprintf "Temperature is %s degrees Celsius", $temperature;
    }
    printf "%s - %s", uc($status), $msg;
    exit $ncli->exitvalue($status);

    sub help {
        print "Usage:\n";
        print "$0 --critical=40 --warning=35"
          . " --hostname=server1.zdf --sensor_nr=4";

        # When supplying help you should exit
        # 3 is "unknown" to Nagios
        exit 3;
    }

=cut

=head1 Functions

=cut

=head2 new

Create the object. Immediately check commandline arguments which
are mandatory for every Nagios command.

Call me like this:

    my $ncli = Nagios::Clientstatus->new(
        help_subref => \&help,
        version => $version,
        # mandatory_args is optional, maybe you don't need any
        mandatory_args => [ "url" ],
    );

=cut

sub new {
    my ( $class, %args ) = @_;
    my $new_object = bless {
        help_subref    => \&help_example,
        version        => $args{version},
        mandatory_args => [],
        given_args     => {},
    }, $class;

    # help_subref
    unless ( ( exists $args{help_subref} )
        && ( ref $args{help_subref} eq "CODE" ) )
    {
        print STDERR
          "Missing ref to help-subroutine. This sub could output this:\n";
        $new_object->help_example;
        my $msg =
          sprintf "Mandatory argument help_subref must point"
          . " to a help-subroutine, but it is a '%s'",
          ref( $args{help_subref} );
        die $msg;
    }
    $new_object->{help_subref} = $args{help_subref};

    # are there mandatory arguments?
    if (   ( exists $args{mandatory_args} )
        && ( ref $args{mandatory_args} eq "ARRAY" ) )
    {
        $new_object->_set_mandatory_args( @{ $args{mandatory_args} } );
    }

    # don't set mandatory args, sometimes it's critical when
    # the service does not run -> very simple
    # $new_object->_set_mandatory_args( "critical", "warning" );

    $new_object->_check_commandline_args;
    return $new_object;
}

sub logger {
    return Log::Log4perl->get_logger('Nagios.Clientstatus');
}

=head2_set_mandatory_args

Remind arguments which user must supply when calling the program.
Can be called several times.

=cut

sub _set_mandatory_args {
    my ( $self, @args ) = @_;
    push @{ $self->{mandatory_args} }, @args;
}

=head2 _get_mandatory_args

Which args MUST be given to the programm? Each argument must have a value, too.

=cut

sub _get_mandatory_args {
    my $self = shift;
    @{ $self->{mandatory_args} };
}

=head2 _set_given_args

Which arguments where given to the program?

=cut

sub _set_given_args {
    my ( $self, $name, $value ) = @_;
    $self->{given_args}->{name} = $value;
}

=head2 get_given_arg

Object-creator can ask for the value of an argument
given to the program.

=cut

sub get_given_arg {
    my ( $self, $name ) = @_;
    return exists $self->{given_args}->{$name}
      ? $self->{given_args}->{$name}
      : undef;
}

=head2 _check_commandline_args

There are arguments which must exist when calling a Nagios-checker.
warning|critcal are mandatory, other mandatory were given by new.

=cut

sub _check_commandline_args {
    my $self   = shift;
    my $logger = $self->logger;

    my %getopt_long_arg;
    my %got_this_option;

    # Build up the argument hash for Getopt::Long
    my @optional_args = ( "version", "help", "verbose", );

    my @mandatory_args = $self->_get_mandatory_args;

    # Build the hash for Getopt::Long
    foreach (@optional_args) {

        # Getopt::Long wants a ref to a scalar where value is stored in
        $getopt_long_arg{$_} = $got_this_option{$_};
    }

    foreach (@mandatory_args) {

        # Tell Getopt::Long that there must be an argument
        # Getopt::Long wants a ref to a scalar where value is stored in
        $getopt_long_arg{"$_=s"} = $got_this_option{$_};
    }

    # Unusual syntax for daily life in GetOptions
    # Look in manpage of Getopt::Long for this:
    #     Storing option values in a hash
    # All given options are stored in hash given as first argument
    GetOptions(
        \%got_this_option,

        # all possible options are here:
        %getopt_long_arg,
    );

    # Now all arguments given to the program are in %got_this_option

    # Do mandatory args exist?
    my @mand_forgotten;
    foreach ( $self->_get_mandatory_args ) {
        unless ( exists $got_this_option{$_} ) {
            push @mand_forgotten, $_;
        }
    }
    if ( scalar @mand_forgotten > 0 ) {
        $logger->debug('%getopt_long_arg was: ');
        $logger->debug( Dumper( \%getopt_long_arg ) );
        $logger->debug('%got_this_option was: ');
        $logger->debug( Dumper( \%got_this_option ) );

        printf STDERR "Mandatory arguments not given: %s\n",
          join( ", ", @mand_forgotten );
        $self->{help_subref}->();
        exit;
    }

    # all arguments where checked, now put them into given_args
    $self->{given_args} = \%got_this_option;
}

=head2 exitvalue

Return the value the Nagios-command must return to Nagios.
This is the only value which is important for the Nagios state.

Use it like this:

    exit $ncli->exitvalue( $status );

Returnvalue can be a string of these:

    OK|WARNING|CRITICAL|UNKNOWN

=cut

sub exitvalue {
    my ( $self, $status ) = @_;
    $status = uc $status;
    my %nagios_returnvalue = (
        'OK'       => 0,
        'WARNING'  => 1,
        'CRITICAL' => 2,
        'UNKNOWN'  => 3,
    );
    unless ( exists $nagios_returnvalue{$status} ) {
        die "Wrong status to return, status can only be: " . join ",",
          sort keys %nagios_returnvalue;
    }
    return $nagios_returnvalue{$status};
}

=head2 help_example

Give the user a hint how to use this programm.

=cut

sub help_example {
    shift;
    print <<"EOUSAGE";
This is $0

Usage:

$0 --warning 60 \\
   --critical 130 \\
   --your_argument_here_1 xx \\
   --your_argument_here_2 xx \\
   [--verbose] [--version]

Tell the user what this programm does
EOUSAGE
}

1;

The programm itself

The program which gets called by Nagios is very short not, it only does the things it should check.

#!/usr/bin/perl
# usually this file is /usr/lib/nagios/plugins_local/$0
use strict;
use warnings;
use Log::Log4perl;

# Nagios calls files from a directory "somewhere"
# so my local perlmodules can not be found
# without defining this additional library:
use lib '/usr/lib/nagios/plugins_local';
use Nagios::Clientstatus;

my $version = "0.01";

# Initialize Logging
my $log_conf = q/
    log4perl.category = ERROR, Screen
    log4perl.appender.Screen        = Log::Log4perl::Appender::Screen
    log4perl.appender.Screen.layout = Log::Log4perl::Layout::SimpleLayout
/;
Log::Log4perl::init( \$log_conf );
my $logger = Log::Log4perl::get_logger();

# Arguments to the program are:
# --critical=40 --warning=35 --hostname=server1.zdf --sensor_nr=4
my $ncli = Nagios::Clientstatus->new(
    help_subref    => \&help,
    version        => $version,
    mandatory_args => [ "critical", "warning", "hostname", "sensor_nr" ],
);

my $temperature = &get_temperature_of_sensor(
    hostname  => $ncli->get_given_arg('hostname'),
    sensor_nr => $ncli->get_given_arg('sensor_nr'),
);

# Message for the user to read
my $msg;
my $status;

# strange case
if (   ( !defined $temperature )
    || ( defined $temperature && $temperature eq "" ) )
{
    $status = "unknown";
    $msg    = "Could not get temperature from sensor";
}
else {

    # We got a temperature
    # worst case first
    if ( $temperature > $ncli->get_given_arg('critical') ) {
        $status = "critical";
    }
    elsif ( $temperature > $ncli->get_given_arg('warning') ) {
        $status = "warning";
    }
    else {
        $status = "ok";
    }
    $msg = sprintf "Temperature is %s degrees Celsius", $temperature;
}
printf "%s - %s", uc($status), $msg;
exit $ncli->exitvalue($status);

sub help {
    print "Usage:\n";
    print "$0 --critical=40 --warning=35"
      . " --hostname=server1.zdf --sensor_nr=4";

    # When supplying help you should exit
    exit $ncli->exitvalue("unknown");
}

sub get_temperature_of_sensor {
    # This is the place where you place the code
    # to automatically determine the state of your
    # service
    print "Input temperature: ";
    my $t = ;
    chomp $t;
    return $t;
}

Testing the program

The output of the programm is not important for Nagios, but it's good practice to output some information like this:
CRITICAL - Temperature is 43 degrees Celsius
Take the given files here and start it
root@echolon:/some/where# su - nagios
nagios@echolon:~$ /usr/lib/nagios/plugins_local/check_temperature_testscript.pl --critical=40 --warning=35 --hostname=server1.zdf --sensor_nr=4
Input temperature: 38
WARNING - Temperature is 38 degrees Celsius

Implementing command into Nagios

You have to modify the given program at this page so that no user-interaction is needed anymore! The example is given for a more difficult service, but it's easy to change it for checking the temperature. Ok, the command works. Now I have to implement it into Nagios. Because all my "local" command not installed by the package-manager shall be in /usr/lib/nagios/plugins_local I define a $USER2$ variable for this path:
echolon:/etc/nagios# vim resource.cfg
...
# Sets $USER1$ to be the path to the plugins
$USER1$=/usr/lib/nagios/plugins

# my own check-commands live here:
$USER2$=/usr/lib/nagios/plugins_local
...
There is a special file in Debian/Sarge where I can define my own commands. it' /etc/nagios/checkcommands.cfg:
# checkcommands.cfg - Nagios configuration file for local user changes
#
# Nagios packages wont touch this file anymore. If you want to define
# your own commands for nagios use this file and add the commands here.
# Dont add any command with a name that already exists or nagios wont
# run.

define command{
        command_name    check_webage
        command_line    $USER2$/check_webage --url $ARG1$ --warning $ARG2$ --critical $ARG3$
}
In the file services.cfg I did the following.
# Servicetemplate webage
define service {
    use                      generic-service
    name                     webage-service

    service_description      WEBAGE
    is_volatile              0
    check_period             24x7
    max_check_attempts       3
    normal_check_interval    5
    retry_check_interval     1
    notification_interval    240
    notification_period      24x7
    notification_options     c,r
    ; not here, define this in service for server itself
    ; like this:
    ; check_command          check_webage!http://wwwpublish.zdf/pics_bilder_von_zirndorf/cyclop4/current_left_160x120.jpg!70m!130m
    ; do not register, this is a template only
    register                        0
}

...

# Server wwwpublish.zdf, Webcam Pindertower
# the check_command will be splitted by "!", 
# first part is the command like defined in checkcommands.cfg,
# next parts are $ARG1$, $ARG2$ etc.
define service{
    use                      webage-service
    service_description      WEBAGE-WWWPUBLISH.ZDF-PINDER
    host_name                wwwpublish.zdf
    check_command            check_webage!http://wwwpublish.zdf/pics_bilder_von_zirndorf/cyclop3/current_left_160x120.jpg!70m!130m
}

Back to my Nagios homepage.
Impressum