Page 1 of 2

Writing a plugin

Posted: Sun Jul 21, 2013 10:40 pm
by withanhdammit
Is this the right place for help with writing a plugin? I couldn't find a forum on exchange.nagios.org, so I'll try here.

I'm working on fine tuning the warning/critical threshold definition. My script is being written in Python and so far this is what I have. I'm hoping that someone can put a second (or third or fourth :geek:) set of eyes on it to make sure I have the logic figured out correctly. There's some debug code as well so if I pass -d to my plugin it will print the extra information for debugging.

I'm writing the warning/critical thresholds because I want my plugin to obey the standard Nagios warning/critical usage defined at http://nagiosplug.sourceforge.net/devel ... HOLDFORMAT.

Thanks for taking a look!

Code: Select all

def get_status(item):
    if iscritical(item):
        nagios_status(critical)
    elif iswarning(item):
        nagios_status(warning)
    else:
        nagios_status(ok)

def iscritical(criticalvalue):
    debug("opts.critical=%s" % opts.critical)
    if opts.critical[:1] == "@":
        inclusive = True
        opts.critical = opts.critical[1:]
    else:
        inclusive = False
    debug("@=%s" % inclusive)
    try:
        criticallow,criticalhigh = opts.critical.split(":",1)
    except ValueError:
        if inclusive:
            if int(criticalvalue) >= int(opts.critical):
                debug("%s >= %s is critical" %(criticalvalue,opts.critical))
                return True
            else:
                debug("%s >= %s not critical" %(criticalvalue,opts.critical))
                return False
        else:
            if int(criticalvalue) > int(opts.criticalvalue):
                debug("%s > %s is critical" %(criticalvalue,opts.critical))
                return True
            else:
                debug("%s > %s not critical" %(criticalvalue,opts.critical))
                return False
    else:
        debug("criticallow=*%s*,criticalhigh=*%s*" %(criticallow,criticalhigh))
        if inclusive:
            if criticallow == "":
                debug("criticallow=*%s*" %criticallow)
                if int(criticalvalue) >= int(criticalhigh):
                    debug("%s >= %s" %(criticalvalue,criticalhigh))
                    debug("%s is critical" %criticalvalue)
                    return True
            elif criticalhigh == "":
                debug("criticalhigh=*%s*" %criticalhigh)
                if int(criticalvalue) <= int(criticallow):
                    debug("%s <= %s" %(criticalvalue,criticallow))
                    debug("%s is critical" %criticalvalue)
                    return True
            elif int(criticalvalue) >= int(criticallow) and int(criticalvalue) <= int(criticalhigh):
                debug("%s >= %s and %s <= %s" %(int(criticalvalue),int(criticallow),int(criticalvalue),int(criticalhigh)))
                debug("%s is critical" %criticalvalue)
                return True
            else:
                debug("%s >= %s and %s <= %s" %(int(criticalvalue),int(criticallow),int(criticalvalue),int(criticalhigh)))
                debug("%s is not critical" %criticalvalue)
                return False
        else:
            if criticallow == "":
                debug("criticallow=*%s*" %criticallow)
                if int(criticalvalue) > int(criticalhigh):
                    debug("%s > %s" %(criticalvalue,criticalhigh))
                    debug("%s is critical" %criticalvalue)
                    return True
            elif criticalhigh == "":
                debug("criticalhigh=*%s*" %criticalhigh)
                if int(criticalvalue) < int(criticallow):
                    debug("%s < %s" %(criticalvalue,criticallow))
                    debug("%s is critical" %criticalvalue)
                    return True
            elif int(criticalvalue) < int(criticallow) or int(criticalvalue) > int(criticalhigh):
                debug("%s < %s or %s > %s" %(int(criticalvalue),int(criticallow),int(criticalvalue),int(criticalhigh)))
                debug("%s is critical" %criticalvalue)
                return True
            else:
                debug("%s < %s or %s > %s" %(int(criticalvalue),int(criticallow),int(criticalvalue),int(criticalhigh)))
                debug("%s is not critical" %criticalvalue)
                return False

def iswarning(warningvalue):
    debug("opts.warning=%s" % opts.warning)
    if opts.warning[:1] == "@":
        inclusive = True
        opts.warning = opts.warning[1:]
    else:
        inclusive = False
    debug("@=%s" % inclusive)
    try:
        warninglow,warninghigh = opts.warning.split(":",1)
    except ValueError:
        if inclusive:
            if int(warningvalue) >= int(opts.warning):
                debug("%s >= %s is warning" %(warningvalue,opts.warning))
                return True
            else:
                debug("%s >= %s not warning" %(warningvalue,opts.warning))
                return False
        else:
            if int(warningvalue) > int(opts.warning):
                debug("%s > %s is warning" %(warningvalue,opts.warning))
                return True
            else:
                debug("%s > %s not warning" %(warningvalue,opts.warning))
                return False
    else:
        debug("warninglow=*%s*, warninghigh=*%s*" %(warninglow,warninghigh))
        if inclusive:
            if warninglow == "":
                debug("warninglow=*%s*" %warninglow)
                if int(warningvalue) >= int(warninghigh):
                    debug("%s >= %s is warning" %(warningvalue,warninghigh))
                    return True
            elif warninghigh == "":
                debug("warninghigh=*%s*" %warninghigh)
                if int(warningvalue) <= int(warninglow):
                    debut("%s <= %s is warning" %(warningvalue,warninglow))
                    return True
            elif int(warningvalue) >= int(warninglow) and int(warningvalue) <= int(warninghigh):
                debug("%s >= %s and %s <= %s is warning" %(int(warningvalue),int(warninglow),int(warningvalue),int(warninghigh)))
                return True
            else:
                debug("%s >= %s and %s <= %s is not warning" %(int(warningvalue),int(warninglow),int(warningvalue),int(warninghigh)))
                return False
        else:
            if warninglow == "":
                if int(warningvalue) > int(warninghigh):
                    debug("%s > %s is warning" %(warningvalue,warninghigh))
                    return True
            elif warninghigh == "":
                if int(warningvalue) < int(warninglow):
                    debug("%s < %s is warning" %(warningvalue,warninglow))
                    return True
            elif int(warningvalue) < int(warninglow) or int(warningvalue) > int(warninghigh):
                debug("%s < %s or %s > %s is warning" %(int(warningvalue),int(warninglow),int(warningvalue),int(warninghigh)))
                return True
            else:
                debug("%s < %s or %s > %s is not warning" %(int(warningvalue),int(warninglow),int(warningvalue),int(warninghigh)))
                return False

Re: Writing a plugin

Posted: Mon Jul 22, 2013 9:49 am
by yancy
How are the arguments for warning and critical being passed from the command line? Would be helpful to know that when looking through the code.

-Yancy

Re: Writing a plugin

Posted: Mon Jul 22, 2013 10:50 am
by withanhdammit
I'm trying to write this so it complies with the standard Nagios warning/critical thresholds like this:
Table 4. Command line examples (snipped from Nagios documentation on writing plug-ins)
Note that I do not have the "~" accounted for yet, I just want to make sure I'm on the right track. I'm not much of a coder and this is my first stab at Python.

Command line Meaning
check_stuff -w10 -c20 #Critical if "stuff" is over 20, else warn if over 10 (will be critical if "stuff" is less than 0)
check_stuff -w~:10 -c~:20 #Same as above. Negative "stuff" is OK
check_stuff -w10: -c20 #Critical if "stuff" is over 20, else warn if "stuff" is below 10 (will be critical if "stuff" is less than 0)
check_stuff -c1: #Critical if "stuff" is less than 1
check_stuff -w~:0 -c10 #Critical if "stuff" is above 10; Warn if "stuff" is above zero (will be critical if "stuff" is less than 0)
check_stuff -c5:6 #Critical if "stuff" is less than 5 or more than 6
check_stuff -c@10:20 #OK if stuff is less than 10 or higher than 20, otherwise critical

The arguments are defined here:

Code: Select all

# Parse some Arguments
from optparse import OptionParser
parser = OptionParser()
parser.add_option("-m","--mode", dest="mode",
    help="Which check mode is in use (runtime)")
parser.add_option("-s","--secondary", dest="secondary",
    help="Comma separated list of secondary options to check", default=None)
parser.add_option("-H","--host", dest="host",
    help="Hostname or IP address of the host to check")
parser.add_option("-w","--warning", dest="warning",
    help="Warning threshold", default=None)
parser.add_option("-c","--critical", dest="critical",
    help="Critical threshold", default=None)
parser.add_option("-d","--debug", dest="debug",
    help="Enable debugging (for troubleshooting)", action="store_true", default=False)
(opts,args) = parser.parse_args()

Re: Writing a plugin

Posted: Tue Jul 23, 2013 2:18 pm
by sreinhardt
It seems that it should work under the plugin standards. Are you having issues with it?

Re: Writing a plugin

Posted: Tue Jul 23, 2013 2:31 pm
by withanhdammit
Nothing specific, I just mostly wanted another set of eyes on it to make sure I was interpreting the Nagios rules for warning/critical thresholds correctly.

Re: Writing a plugin

Posted: Tue Jul 23, 2013 4:20 pm
by sreinhardt
OK cool, well ya it seems just fine. Let us know if you do see problems.

Re: Writing a plugin

Posted: Tue Jul 23, 2013 11:09 pm
by withanhdammit
Thanks for looking sreinhardt!

I think I have the final warning/critical threshold checks fully vetted, including the @ (inclusive) and ~ (negative infinity).

I'd really appreciate if someone could take a look and run this through some tests to make sure it handles all contingencies. I ran it through the checks on the Nagios plug-in development page and it passed all of them, but I'm hoping more sets of eyes on it throwing more options at it will find something I missed.

I'd also love some pointers if someone can point to a better/more efficient way to code the checks.

The below script is fully contained and can can be run with the following options (you can actually run it with no options and it will set some basic defaults):
Usage: check_test [options]

Options:
-h, --help show this help message and exit
-m MODE, --mode=MODE Which check mode is in use (test)
-t TESTVALUE, --testvalue=TESTVALUE
Test value to check
-w WARNING, --warning=WARNING
Warning threshold
-c CRITICAL, --critical=CRITICAL
Critical threshold
-d, --debug Enable debugging (for troubleshooting)

Code: Select all

#!/usr/bin/python

version=".1"
ok=0
warning=1
critical=2
unknown=3 
not_present = -1 
exit_status = -1

state = {}
state[not_present] = "Not Present"
state[ok] = "OK"
state[warning] = "WARNING"
state[critical] = "CRITICAL"
state[unknown] = "UNKNOWN"

longserviceoutput="\n"
perfdata=""
summary=""
sudo=False

from sys import exit
from sys import argv
from os import getenv,putenv,environ
import subprocess

# Parse some Arguments
from optparse import OptionParser
parser = OptionParser()
parser.add_option("-m","--mode", dest="mode",
    help="Which check mode is in use (test)", default="test")
parser.add_option("-t","--testvalue", dest="testvalue",
    help="Test value to check", default=10)
parser.add_option("-w","--warning", dest="warning",
    help="Warning threshold", default=None)
parser.add_option("-c","--critical", dest="critical",
    help="Critical threshold", default=None)
parser.add_option("-d","--debug", dest="debug",
    help="Enable debugging (for troubleshooting)", action="store_true", default=False)
(opts,args) = parser.parse_args()

if opts.warning == None:
    opts.warning=""
if opts.critical == None:
    opts.critical=""
if opts.testvalue == None:
    parser.error("Test value (-t or --testing) is required.")

def error(errortext):
    print "* Error: %s" % errortext
    exit(unknown)

def debug(debugtext):
    if opts.debug:
        print debugtext

def nagios_status(newStatus):
    global exit_status
    exit_status = max(exit_status, newStatus)
    return exit_status

def end():
    global summary
    global longserviceoutput
    global perfdata
    global exit_status
    print "%s - %s|%s" % (state[exit_status],summary,perfdata)
    print longserviceoutput
    if exit_status < 0: exit_status = unknown
    exit(exit_status)

def add_perfdata(text):
    global perfdata
    text = text.strip()
    if perfdata == "":
        perfdata += "%s" %(text)
    else:
        perfdata += " %s" %(text)

def add_long(text):
    global longserviceoutput
    longserviceoutput = longserviceoutput + text + '\n'

def add_summary(text):
    global summary
    if summary == "":
        summary += "%s" % text
    else:
        summary += ", %s" % (text)

def set_path(path):
    current_path = getenv('PATH')
    if current_path.find('C:\\') > -1: # We are on this platform
        if path == '':
            pass
        else: path = ';' + path
    else:   # Unix/Linux, etc
        if path == '': path = ":/usr/sbin"
        else: path = ':' + path
    current_path = "%s%s" % (current_path,path)
    environ['PATH'] = current_path

def get_status(item):
    if iscritical(item):
        nagios_status(critical)
    elif iswarning(item):
        nagios_status(warning)
    else:
        nagios_status(ok)

def iscritical(criticalvalue):
    if opts.critical is None or opts.critical == "":
        debug("No critical value specified")
        return False
    else:
        debug("opts.critical=%s" % opts.critical)
        if opts.critical[:2] == "~@" or opts.critical[:2] == "@~":
            nagios_status(unknown)
            add_summary("Invalid critical alert range")
            end()
        elif opts.critical[:1] == "@":
    	    opts.critical = opts.critical[1:]
            inclusive = True
            negativeinfinity = False
        elif opts.critical[:1] == "~":
            opts.critical = opts.critical[1:]
            inclusive = False
            negativeinfinity = True
        else:
            inclusive = False
            negativeinfinity = False
        debug("inclusive=%s" %inclusive)
        debug("negativeinfinity=%s" %negativeinfinity)
        try:
            criticallow,criticalhigh = opts.critical.split(":",1)
        except ValueError:
            if inclusive:
                debug("inclusive criticalvalue=*%s*" %criticalvalue)
                if int(criticalvalue) >= int(opts.critical):
                    debug("%s >= %s is critical" %(criticalvalue,opts.critical))
                    return True
                else:
                    debug("%s >= %s not critical" %(criticalvalue,opts.critical))
                    return False
            else:
                debug("not inclusive criticalvalue=*%s*" %criticalvalue)
                if int(criticalvalue) > int(opts.critical) or int(criticalvalue) < 0:
                    debug("%s > %s is critical" %(criticalvalue,opts.critical))
                    return True
                else:
                    debug("%s > %s not critical" %(criticalvalue,opts.critical))
                    return False
        else:
            debug("criticallow=*%s*,criticalhigh=*%s*" %(criticallow,criticalhigh))
            if inclusive:
                if criticallow == "":
                    debug("criticallow=*%s*" %criticallow)
                    if int(criticalvalue) >= int(criticalhigh):
                        debug("%s >= %s" %(criticalvalue,criticalhigh))
                        debug("%s is critical" %criticalvalue)
                        return True
                elif criticalhigh == "":
                    debug("criticalhigh=*%s*" %criticalhigh)
                    if int(criticalvalue) <= int(criticallow):
                        debug("%s <= %s" %(criticalvalue,criticallow))
                        debug("%s is critical" %criticalvalue)
                        return True
                elif int(criticalvalue) >= int(criticallow) and int(criticalvalue) <= int(criticalhigh):
                    debug("%s >= %s and %s <= %s" %(int(criticalvalue),int(criticallow),int(criticalvalue),int(criticalhigh)))
                    debug("%s is critical" %criticalvalue)
                    return True
                else:
                    debug("%s >= %s and %s <= %s" %(int(criticalvalue),int(criticallow),int(criticalvalue),int(criticalhigh)))
                    debug("%s is not critical" %criticalvalue)
                    return False
            else:
                if criticallow == "":
                    debug("criticallow=*%s*" %criticallow)
                    if int(criticalvalue) > int(criticalhigh):
                        debug("%s > %s" %(criticalvalue,criticalhigh))
                        debug("%s is critical" %criticalvalue)
                        return True
                elif criticalhigh == "":
                    debug("criticalhigh=*%s*" %criticalhigh)
                    if int(criticalvalue) < int(criticallow):
                        debug("%s < %s" %(criticalvalue,criticallow))
                        debug("%s is critical" %criticalvalue)
                        return True
                elif int(criticalvalue) < int(criticallow) or int(criticalvalue) > int(criticalhigh):
                    debug("%s < %s or %s > %s" %(int(criticalvalue),int(criticallow),int(criticalvalue),int(criticalhigh)))
                    debug("%s is critical" %criticalvalue)
                    return True
                else:
                    debug("%s < %s or %s > %s" %(int(criticalvalue),int(criticallow),int(criticalvalue),int(criticalhigh)))
                    debug("%s is not critical" %criticalvalue)
                    return False

def iswarning(warningvalue):
    if opts.warning is None or opts.warning == "":
        debug("No warning value specified")
        return False
    else:
        debug("opts.warning=%s" % opts.warning)
        if opts.warning[:2] == "~@" or opts.warning[:2] == "@~":
            nagios_status(unknown)
            add_summary("Invalid warning alert range")
            end()
        elif opts.warning[:1] == "@":
            inclusive = True
            negativeinfinity = False
            opts.warning = opts.warning[1:]
        elif opts.warning[:1] =="~":
            inclusive = False
            negativeinfinity = True
            opts.warning = opts.warning[1:]
        else:
            inclusive = False
            negativeinfinity = False
        debug("inclusive=%s" %inclusive)
        debug("negativeinfinity=%s" %negativeinfinity)
        try:
            warninglow,warninghigh = opts.warning.split(":",1)
        except ValueError:
            if inclusive:
                debug("inclusive warningvalue=*%s*" %warningvalue)
                if int(warningvalue) >= int(opts.warning):
                    debug("%s >= %s is warning" %(warningvalue,opts.warning))
                    return True
                else:
                    debug("%s >= %s not warning" %(warningvalue,opts.warning))
                    return False
            else:
                debug("not inclusive warningvalue=*%s*" %warningvalue)
                if int(warningvalue) > int(opts.warning) or int(warningvalue) < 0:
                    debug("%s > %s is warning" %(warningvalue,opts.warning))
                    return True
                else:
                    debug("%s > %s not warning" %(warningvalue,opts.warning))
                    return False
        else:
            debug("warninglow=*%s*,warninghigh=*%s*" %(warninglow,warninghigh))
            if inclusive:
                if warninglow == "":
                    debug("warninglow=*%s*" %warninglow)
                    if int(warningvalue) >= int(warninghigh):
                        debug("%s >= %s" %(warningvalue,warninghigh))
                        debug("%s is warning" %warningvalue)
                        return True
                elif warninghigh == "":
                    debug("warninghigh=*%s*" %warninghigh)
                    if int(warningvalue) <= int(warninglow):
                        debug("%s <= %s" %(warningvalue,warninglow))
                        debug("%s is warning" %warningvalue)
                        return True
                elif int(warningvalue) >= int(warninglow) and int(warningvalue) <= int(warninghigh):
                    debug("%s >= %s and %s <= %s" %(int(warningvalue),int(warninglow),int(warningvalue),int(warninghigh)))
                    debug("%s is warning" %warningvalue)
                    return True
                else:
                    debug("%s >= %s and %s <= %s" %(int(warningvalue),int(warninglow),int(warningvalue),int(warninghigh)))
                    debug("%s is not warning" %warningvalue)
                    return False
            else:
                if warninglow == "":
                    debug("warninglow=*%s*" %warninglow)
                    if int(warningvalue) > int(warninghigh):
                        debug("%s > %s" %(warningvalue,warninghigh))
                        debug("%s is warning" %warningvalue)
                        return True
                elif warninghigh == "":
                    debug("warninghigh=*%s*" %warninghigh)
                    if int(warningvalue) < int(warninglow):
                        debug("%s < %s" %(warningvalue,warninglow))
                        debug("%s is warning" %warningvalue)
                        return True
                elif int(warningvalue) < int(warninglow) or int(warningvalue) > int(warninghigh):
                    debug("%s < %s or %s > %s" %(int(warningvalue),int(warninglow),int(warningvalue),int(warninghigh)))
                    debug("%s is warning" %warningvalue)
                    return True
                else:
                    debug("%s < %s or %s > %s" %(int(warningvalue),int(warninglow),int(warningvalue),int(warninghigh)))
                    debug("%s is not warning" %warningvalue)
                    return False

def check_test():
    debug("testing value=*%s*" %opts.testvalue)
    get_status(opts.testvalue)
    add_summary("Test value = %s units" %(opts.testvalue))
    add_perfdata("Test value=%sunits;%s;%s" %(opts.testvalue,opts.warning,opts.critical))

if __name__ == '__main__':
    try:
        if opts.mode == 'test':
            check_test()
        else:
            parser.error("%s is not a valid option for --mode" %opts.mode)
    except Exception, e:
        print "Unhandled exception while running script: %s" %e
        exit(unknown)
    end()

Re: Writing a plugin

Posted: Wed Jul 24, 2013 10:13 am
by yancy
It would be helpful to show options if no args are run, like so:

Code: Select all

    import sys
    if len(sys.argv)==1:
        parser.print_help()
        exit(0)
-Yancy

Re: Writing a plugin

Posted: Wed Jul 24, 2013 10:35 am
by nscott
Looks good!

The most important thing is that your check works for you, so if that works, than you can safely move on to another issue if you need to get it done.

However, if you're looking at the aesthetics of the code, here are a few pointers:

Why are iswarning and iscritical different functions?

They are both doing nearly the same thing, and they are both almost 100 lines long. You've effectively doubled the code it takes to check ranges. Perhaps think about it in terms of:

Code: Select all

def is_within_range(value, threshold):
     # some code goes here to determine if a value falls into a threshold

if is_within_range(my_checked_value, my_critical_threshold):
    status = 'CRITICAL'
elif is_within_range(my_checked_value, my_warning_threshold):
    status = 'WARNING'
else:
    status = 'OK'
This is borderline pseudocode, but I hope it expresses the point that the range checking is not unique to critical or warning, so they could (and probably should) share the same logic.

Globals

Globals are really handy in some situations. However they get dicey later. Nagios plugins generally aren't long enough to expose questionable programming practices. However, reading your code gets a bit tiresome because you're constantly calling 'global' in front of variables. Is there some other workflow you can think of for the variables? Do they need to be global? Also sometimes I'm not sure why you're returning globals:

Code: Select all

def nagios_status(newStatus):
    global exit_status
    exit_status = max(exit_status, newStatus)
    return exit_status
On a side note, its naming convention to make your global capitalized, to make a clear distinction between what is global and what is local. However, since you are required to preface all use of a global with 'global', its clear whats a global, but I generally find it easier to think of in those terms, just my two cents.

Functions

You did a good job breaking the items up into functions. Perhaps the arg parsing could be placed into a function as well? That way your bottom would be:

Code: Select all

if __name__ == "__main__":
    args, options = parse_args()
    value = run_check(args, options)
    if is_within_range(value, options.critical_thresh):
        # Do the critical string making
    elif is_within_range(value, options.warning_thresh):
        # Do the warning string making
    else:
        # Do the Ok string making
    print stdout_string
    sys.exit(return_code)
Something like that.

PyNagios

There are a few packages around that make it really easy to make Nagios plugins in Python. One is called PyNagios whose homepage is https://pypi.python.org/pypi/pynagios. Might be right up your alley.

Hope this helps.

Re: Writing a plugin

Posted: Wed Jul 24, 2013 3:41 pm
by withanhdammit
yancy wrote:It would be helpful to show options if no args are run, like so:

Code: Select all

    import sys
    if len(sys.argv)==1:
        parser.print_help()
        exit(0)
-Yancy
Great idea, I'll definitely do that.