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::More; 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 module as the basis of testing. =head1 FUNCTIONS This module defines three public functions, C, C and C. These are exported by default via the C 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 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 This function is obsolete. Use C 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. 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<$?EE8>) and output are captured. At this point if debugging is enabled the command, its exit status and output are displayed to the tester. C allows the testing of either the exit status or the generated output or both, not testing either will result in neither the C or C 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 or C, so remember this when counting the number of tests to place in the C call. Many Nagios Plugins test network services, some of which may not be present on all systems. To cater for this, C 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 "No POP Server present?" ); $t += checkCmd( "./check_pop I", 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 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 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 If a command is missing and the test harness must C 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 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 = ; $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); my ($pkg, $file, $line) = caller(0); print "Testing: $command", $/; if ($ENV{'NPTEST_DEBUG'}) { diag("testCmd: Called from line $line in $file", $/); diag("Output: ", $object->output, $/); diag("Return code: ", $object->return_code, $/); } return $object; } 1; # # End of File #