| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642 |
- package NPTest;
- #
- # Helper Functions for testing Nagios Plugins
- #
- require Exporter;
- @ISA = qw(Exporter);
- @EXPORT = qw(getTestParameter checkCmd skipMissingCmd);
- @EXPORT_OK = qw(DetermineTestHarnessDirectory TestsFrom SetCacheFilename);
- use strict;
- use warnings;
- use Cwd;
- use File::Basename;
- use IO::File;
- use Data::Dumper;
- use Test;
- use vars qw($VERSION);
- $VERSION = "1556."; # must be all one line, for MakeMaker
- =head1 NAME
- NPTest - Simplify the testing of Nagios Plugins
- =head1 DESCRIPTION
- This modules provides convenience functions to assist in the testing
- of Nagios Plugins, making the testing code easier to read and write;
- hopefully encouraging the development of a more complete test suite for
- the Nagios Plugins. It is based on the patterns of testing seen in the
- 1.4.0 release, and continues to use the L<Test> module as the basis of
- testing.
- =head1 FUNCTIONS
- This module defines three public functions, C<getTestParameter(...)>,
- C<checkCmd(...)> and C<skipMissingCmd(...)>. These are exported by
- default via the C<use NPTest;> statement.
- =over
- =item getTestParameter( "ENV_VARIABLE", $brief_description, $default )
- $default is optional.
- This function allows the test harness
- developer to interactively request test parameter information from the
- user. The user can accept the developer's default value or reply "none"
- which will then be returned as "" for the test to skip if appropriate.
- If a parameter needs to be entered and the test is run without a tty
- attached (such as a cronjob), the parameter will be assigned as if it
- was "none". Tests can check for the parameter and skip if not set.
- Responses are stored in an external, file-based cache so subsequent test
- runs will use these values. The user is able to change the values by
- amending the values in the file /var/tmp/NPTest.cache, or by setting
- the appropriate environment variable before running the test.
- The option exists to store parameters in a scoped means, allowing a
- test harness to a localise a parameter should the need arise. This
- allows a parameter of the same name to exist in a test harness
- specific scope, while not affecting the globally scoped parameter. The
- scoping identifier is the name of the test harness sans the trailing
- ".t". All cache searches first look to a scoped parameter before
- looking for the parameter at global scope. Thus for a test harness
- called "check_disk.t" requesting the parameter "mountpoint_valid", the
- cache is first searched for "check_disk"/"mountpoint_valid", if this
- fails, then a search is conducted for "mountpoint_valid".
- To facilitate quick testing setup, it is possible to accept all the
- developer provided defaults by setting the environment variable
- "NPTEST_ACCEPTDEFAULT" to "1" (or any other perl truth value). Note
- that, such defaults are not stored in the cache, as there is currently
- no mechanism to edit existing cache entries, save the use of text
- editor or removing the cache file completely.
- =item C<testCmd($command)>
- Call with NPTest->testCmd("./check_disk ...."). This returns a NPTest object
- which you can then run $object->return_code or $object->output against.
- Testing of results would be done in your test script, not in this module.
- =item C<checkCmd(...)>
- This function is obsolete. Use C<testCmd()> instead.
- This function attempts to encompass the majority of test styles used
- in testing Nagios Plugins. As each plug-in is a separate command, the
- typical tests we wish to perform are against the exit status of the
- command and the output (if any) it generated. Simplifying these tests
- into a single function call, makes the test harness easier to read and
- maintain and allows additional functionality (such as debugging) to be
- provided without additional effort on the part of the test harness
- developer.
- It is possible to enable debugging via the environment variable
- C<NPTEST_DEBUG>. If this environment variable exists and its value in PERL's
- boolean context evaluates to true, debugging is enabled.
- The function prototype can be expressed as follows:
- Parameter 1 : command => DEFINED SCALAR(string)
- Parameter 2 : desiredExitStatus => ONE OF
- SCALAR(integer)
- ARRAYREF(integer)
- HASHREF(integer,string)
- UNDEFINED
- Parameter 3 : desiredOutput => SCALAR(string) OR UNDEFINED
- Parameter 4 : exceptions => HASH(integer,string) OR UNDEFINED
- Returns : SCALAR(integer) as defined by Test::ok(...)
- The function treats the first parameter C<$command> as a command line
- to execute as part of the test, it is executed only once and its exit
- status (C<$?E<gt>E<gt>8>) and output are captured.
- At this point if debugging is enabled the command, its exit status and
- output are displayed to the tester.
- C<checkCmd(...)> allows the testing of either the exit status or the
- generated output or both, not testing either will result in neither
- the C<Test::ok(...)> or C<Test::skip(...)> functions being called,
- something you probably don't want. Note that each defined test
- (C<$desiredExitStatus> and C<$desiredOutput>) results in a invocation
- of either C<Test::ok(...)> or C<Test::skip(...)>, so remember this
- when counting the number of tests to place in the C<Test::plan(...)>
- call.
- Many Nagios Plugins test network services, some of which may not be
- present on all systems. To cater for this, C<checkCmd(...)> allows the
- tester to define exceptions based on the command's exit status. These
- exceptions are provided to skip tests if the test case developer
- believes the service is not being provided. For example, if a site
- does not have a POP3 server, the test harness could map the
- appropriate exit status to a useful message the person running the
- tests, telling the reason the test is being skipped.
- Example:
- my %exceptions = ( 2 =E<gt> "No POP Server present?" );
- $t += checkCmd( "./check_pop I<some args>", 0, undef, %exceptions );
- Thus, in the above example, an exit status of 2 does not result in a
- failed test case (as the exit status is not the desired value of 0),
- but a skipped test case with the message "No POP Server present?"
- given as the reason.
- Sometimes the exit status of a command should be tested against a set
- of possible values, rather than a single value, this could especially
- be the case in failure testing. C<checkCmd(...)> support two methods
- of testing against a set of desired exit status values.
- =over
- =item *
- Firstly, if C<$desiredExitStatus> is a reference to an array of exit
- stati, if the actual exit status of the command is present in the
- array, it is used in the call to C<Test::ok(...)> when testing the
- exit status.
- =item *
- Alternatively, if C<$desiredExitStatus> is a reference to a hash of
- exit stati (mapped to the strings "continue" or "skip"), similar
- processing to the above occurs with the side affect of determining if
- any generated output testing should proceed. Note: only the string
- "skip" will result in generated output testing being skipped.
- =back
- =item C<skipMissingCmd(...)>
- If a command is missing and the test harness must C<Test::skip()> some
- or all of the tests in a given test harness this function provides a
- simple iterator to issue an appropriate message the requested number
- of times.
- =back
- =head1 SEE ALSO
- L<Test>
- The rest of the code, as I have only commented on the major public
- functions that test harness writers will use, not all the code present
- in this helper module.
- =head1 AUTHOR
- Copyright (c) 2005 Peter Bray. All rights reserved.
- This package is free software and is provided "as is" without express
- or implied warranty. It may be used, redistributed and/or modified
- under the same terms as the Nagios Plugins release.
- =cut
- #
- # Package Scope Variables
- #
- my( %CACHE ) = ();
- # I'm not really sure wether to house a site-specific cache inside
- # or outside of the extracted source / build tree - lets default to outside
- my( $CACHEFILENAME ) = ( exists( $ENV{'NPTEST_CACHE'} ) && $ENV{'NPTEST_CACHE'} )
- ? $ENV{'NPTEST_CACHE'} : "/var/tmp/NPTest.cache"; # "../Cache.pdd";
- #
- # Testing Functions
- #
- sub checkCmd
- {
- my( $command, $desiredExitStatus, $desiredOutput, %exceptions ) = @_;
- my $result = NPTest->testCmd($command);
- my $output = $result->output;
- my $exitStatus = $result->return_code;
- $output = "" unless defined( $output );
- chomp( $output );
- my $testStatus;
- my $testOutput = "continue";
- if ( defined( $desiredExitStatus ) )
- {
- if ( ref $desiredExitStatus eq "ARRAY" )
- {
- if ( scalar( grep { $_ == $exitStatus } @{$desiredExitStatus} ) )
- {
- $desiredExitStatus = $exitStatus;
- }
- else
- {
- $desiredExitStatus = -1;
- }
- }
- elsif ( ref $desiredExitStatus eq "HASH" )
- {
- if ( exists( ${$desiredExitStatus}{$exitStatus} ) )
- {
- if ( defined( ${$desiredExitStatus}{$exitStatus} ) )
- {
- $testOutput = ${$desiredExitStatus}{$exitStatus};
- }
- $desiredExitStatus = $exitStatus;
- }
- else
- {
- $desiredExitStatus = -1;
- }
- }
- if ( %exceptions && exists( $exceptions{$exitStatus} ) )
- {
- $testStatus += skip( $exceptions{$exitStatus}, $exitStatus, $desiredExitStatus );
- $testOutput = "skip";
- }
- else
- {
- $testStatus += ok( $exitStatus, $desiredExitStatus );
- }
- }
- if ( defined( $desiredOutput ) )
- {
- if ( $testOutput ne "skip" )
- {
- $testStatus += ok( $output, $desiredOutput );
- }
- else
- {
- $testStatus += skip( "Skipping output test as requested", $output, $desiredOutput );
- }
- }
- return $testStatus;
- }
- sub skipMissingCmd
- {
- my( $command, $count ) = @_;
- my $testStatus;
- for ( 1 .. $count )
- {
- $testStatus += skip( "Missing ${command} - tests skipped", 1 );
- }
- return $testStatus;
- }
- sub getTestParameter
- {
- my( $param, $envvar, $default, $brief, $scoped );
- my $new_style;
- if (scalar @_ <= 3) {
- ($param, $brief, $default) = @_;
- $envvar = $param;
- $new_style = 1;
- } else {
- ( $param, $envvar, $default, $brief, $scoped ) = @_;
- $new_style = 0;
- }
- # Apply default values for optional arguments
- $scoped = ( defined( $scoped ) && $scoped );
- my $testharness = basename( (caller(0))[1], ".t" ); # used for scoping
- if ( defined( $envvar ) && exists( $ENV{$envvar} ) && $ENV{$envvar} )
- {
- return $ENV{$envvar};
- }
- my $cachedValue = SearchCache( $param, $testharness );
- if ( defined( $cachedValue ) )
- {
- # This save required to convert to new style because the key required is
- # changing to the environment variable
- if ($new_style == 0) {
- SetCacheParameter( $envvar, undef, $cachedValue );
- }
- return $cachedValue;
- }
- my $defaultValid = ( defined( $default ) && $default );
- my $autoAcceptDefault = ( exists( $ENV{'NPTEST_ACCEPTDEFAULT'} ) && $ENV{'NPTEST_ACCEPTDEFAULT'} );
- if ( $autoAcceptDefault && $defaultValid )
- {
- return $default;
- }
- # Set "none" if no terminal attached (eg, tinderbox build servers when new variables set)
- return "" unless (-t STDERR);
- my $userResponse = "";
- while ( $userResponse eq "" )
- {
- print STDERR "\n";
- print STDERR "Test Harness : $testharness\n";
- print STDERR "Test Parameter : $param\n";
- print STDERR "Environment Variable : $envvar\n" if ($param ne $envvar);
- print STDERR "Brief Description : $brief\n";
- print STDERR "Enter value (or 'none') ", ($defaultValid ? "[${default}]" : "[]"), " => ";
- $userResponse = <STDIN>;
- $userResponse = "" if ! defined( $userResponse ); # Handle EOF
- chomp( $userResponse );
- if ( $defaultValid && $userResponse eq "" )
- {
- $userResponse = $default;
- }
- }
- print STDERR "\n";
- if ($userResponse =~ /^(na|none)$/) {
- $userResponse = "";
- }
- # define all user responses at global scope
- SetCacheParameter( $param, ( $scoped ? $testharness : undef ), $userResponse );
- return $userResponse;
- }
- #
- # Internal Cache Management Functions
- #
- sub SearchCache
- {
- my( $param, $scope ) = @_;
- LoadCache();
- if ( exists( $CACHE{$scope} ) && exists( $CACHE{$scope}{$param} ) )
- {
- return $CACHE{$scope}{$param};
- }
- if ( exists( $CACHE{$param} ) )
- {
- return $CACHE{$param};
- }
- return undef; # Need this to say "nothing found"
- }
- sub SetCacheParameter
- {
- my( $param, $scope, $value ) = @_;
- if ( defined( $scope ) )
- {
- $CACHE{$scope}{$param} = $value;
- }
- else
- {
- $CACHE{$param} = $value;
- }
- SaveCache();
- }
- sub LoadCache
- {
- return if exists( $CACHE{'_cache_loaded_'} );
- if ( -f $CACHEFILENAME )
- {
- my( $fileHandle ) = new IO::File;
- if ( ! $fileHandle->open( "< ${CACHEFILENAME}" ) )
- {
- print STDERR "NPTest::LoadCache() : Problem opening ${CACHEFILENAME} : $!\n";
- return;
- }
- my( $fileContents ) = join( "\n", <$fileHandle> );
- $fileHandle->close();
- my( $contentsRef ) = eval $fileContents;
- %CACHE = %{$contentsRef};
- }
- $CACHE{'_cache_loaded_'} = 1;
- }
- sub SaveCache
- {
- delete $CACHE{'_cache_loaded_'};
- my( $fileHandle ) = new IO::File;
- if ( ! $fileHandle->open( "> ${CACHEFILENAME}" ) )
- {
- print STDERR "NPTest::LoadCache() : Problem saving ${CACHEFILENAME} : $!\n";
- return;
- }
- my( $dataDumper ) = new Data::Dumper( [ \%CACHE ] );
- $dataDumper->Terse(1);
- print $fileHandle $dataDumper->Dump();
- $fileHandle->close();
- $CACHE{'_cache_loaded_'} = 1;
- }
- #
- # (Questionable) Public Cache Management Functions
- #
- sub SetCacheFilename
- {
- my( $filename ) = @_;
- # Unfortunately we can not validate the filename
- # in any meaningful way, as it may not yet exist
- $CACHEFILENAME = $filename;
- }
- #
- # Test Harness Wrapper Functions
- #
- sub DetermineTestHarnessDirectory
- {
- my( $userSupplied ) = @_;
- # User Supplied
- if ( defined( $userSupplied ) && $userSupplied )
- {
- if ( -d $userSupplied )
- {
- return $userSupplied;
- }
- else
- {
- return undef; # userSupplied is invalid -> FAIL
- }
- }
- # Simple Case : "t" is a subdirectory of the current directory
- if ( -d "./t" )
- {
- return "./t";
- }
- # To be honest I don't understand which case satisfies the
- # original code in test.pl : when $tstdir == `pwd` w.r.t.
- # $tstdir =~ s|^(.*)/([^/]+)/?$|$1/$2|; and if (-d "../../$2/t")
- # Assuming pwd is "/a/b/c/d/e" then we are testing for "/a/b/c/e/t"
- # if I understand the code correctly (a big assumption)
- # Simple Case : the current directory is "t"
- my $pwd = cwd();
- if ( $pwd =~ m|/t$| )
- {
- return $pwd;
- # The alternate that might work better is
- # chdir( ".." );
- # return "./t";
- # As the current test harnesses assume the application
- # to be tested is in the current directory (ie "./check_disk ....")
- }
- return undef;
- }
- sub TestsFrom
- {
- my( $directory, $excludeIfAppMissing ) = @_;
- $excludeIfAppMissing = 0 unless defined( $excludeIfAppMissing );
- if ( ! opendir( DIR, $directory ) )
- {
- print STDERR "NPTest::TestsFrom() - Failed to open ${directory} : $!\n";
- return ();
- }
- my( @tests ) = ();
- my $filename;
- my $application;
- while ( $filename = readdir( DIR ) )
- {
- if ( $filename =~ m/\.t$/ )
- {
- if ( $excludeIfAppMissing )
- {
- $application = basename( $filename, ".t" );
- if ( ! -e $application )
- {
- print STDERR "No application (${application}) found for test harness (${filename})\n";
- next;
- }
- }
- push @tests, "${directory}/${filename}";
- }
- }
- closedir( DIR );
- return sort @tests;
- }
- # All the new object oriented stuff below
- sub new {
- my $type = shift;
- my $self = {};
- return bless $self, $type;
- }
- # Accessors
- sub return_code {
- my $self = shift;
- if (@_) {
- return $self->{return_code} = shift;
- } else {
- return $self->{return_code};
- }
- }
- sub output {
- my $self = shift;
- if (@_) {
- return $self->{output} = shift;
- } else {
- return $self->{output};
- }
- }
- sub perf_output {
- my $self = shift;
- $_ = $self->{output};
- /\|(.*)$/;
- return $1 || "";
- }
- sub only_output {
- my $self = shift;
- $_ = $self->{output};
- /(.*?)\|/;
- return $1 || "";
- }
- sub testCmd {
- my $class = shift;
- my $command = shift or die "No command passed to testCmd";
- my $object = $class->new;
-
- my $output = `$command`;
- $object->return_code($? >> 8);
- $_ = $? & 127;
- if ($_) {
- die "Got signal $_ for command $command";
- }
- chomp $output;
- $object->output($output);
- if ($ENV{'NPTEST_DEBUG'}) {
- my ($pkg, $file, $line) = caller(0);
- print "testCmd: Called from line $line in $file", $/;
- print "Testing: $command", $/;
- print "Output: ", $object->output, $/;
- print "Return code: ", $object->return_code, $/;
- }
- return $object;
- }
- 1;
- #
- # End of File
- #
|