1package NPTest; 2 3# 4# Helper Functions for testing Nagios Plugins 5# 6 7require Exporter; 8@ISA = qw(Exporter); 9@EXPORT = qw(getTestParameter checkCmd skipMissingCmd); 10@EXPORT_OK = qw(DetermineTestHarnessDirectory TestsFrom SetCacheFilename); 11 12use strict; 13use warnings; 14 15use Cwd; 16use File::Basename; 17 18use IO::File; 19use Data::Dumper; 20 21use Test; 22 23use vars qw($VERSION); 24$VERSION = "1556."; # must be all one line, for MakeMaker 25 26=head1 NAME 27 28NPTest - Simplify the testing of Nagios Plugins 29 30=head1 DESCRIPTION 31 32This modules provides convenience functions to assist in the testing 33of Nagios Plugins, making the testing code easier to read and write; 34hopefully encouraging the development of a more complete test suite for 35the Nagios Plugins. It is based on the patterns of testing seen in the 361.4.0 release, and continues to use the L<Test> module as the basis of 37testing. 38 39=head1 FUNCTIONS 40 41This module defines three public functions, C<getTestParameter(...)>, 42C<checkCmd(...)> and C<skipMissingCmd(...)>. These are exported by 43default via the C<use NPTest;> statement. 44 45=over 46 47=item getTestParameter( "ENV_VARIABLE", $brief_description, $default ) 48 49$default is optional. 50 51This function allows the test harness 52developer to interactively request test parameter information from the 53user. The user can accept the developer's default value or reply "none" 54which will then be returned as "" for the test to skip if appropriate. 55 56If a parameter needs to be entered and the test is run without a tty 57attached (such as a cronjob), the parameter will be assigned as if it 58was "none". Tests can check for the parameter and skip if not set. 59 60Responses are stored in an external, file-based cache so subsequent test 61runs will use these values. The user is able to change the values by 62amending the values in the file /var/tmp/NPTest.cache, or by setting 63the appropriate environment variable before running the test. 64 65The option exists to store parameters in a scoped means, allowing a 66test harness to a localise a parameter should the need arise. This 67allows a parameter of the same name to exist in a test harness 68specific scope, while not affecting the globally scoped parameter. The 69scoping identifier is the name of the test harness sans the trailing 70".t". All cache searches first look to a scoped parameter before 71looking for the parameter at global scope. Thus for a test harness 72called "check_disk.t" requesting the parameter "mountpoint_valid", the 73cache is first searched for "check_disk"/"mountpoint_valid", if this 74fails, then a search is conducted for "mountpoint_valid". 75 76To facilitate quick testing setup, it is possible to accept all the 77developer provided defaults by setting the environment variable 78"NPTEST_ACCEPTDEFAULT" to "1" (or any other perl truth value). Note 79that, such defaults are not stored in the cache, as there is currently 80no mechanism to edit existing cache entries, save the use of text 81editor or removing the cache file completely. 82 83=item C<testCmd($command)> 84 85Call with NPTest->testCmd("./check_disk ...."). This returns a NPTest object 86which you can then run $object->return_code or $object->output against. 87 88Testing of results would be done in your test script, not in this module. 89 90=item C<checkCmd(...)> 91 92This function is obsolete. Use C<testCmd()> instead. 93 94This function attempts to encompass the majority of test styles used 95in testing Nagios Plugins. As each plug-in is a separate command, the 96typical tests we wish to perform are against the exit status of the 97command and the output (if any) it generated. Simplifying these tests 98into a single function call, makes the test harness easier to read and 99maintain and allows additional functionality (such as debugging) to be 100provided without additional effort on the part of the test harness 101developer. 102 103It is possible to enable debugging via the environment variable 104C<NPTEST_DEBUG>. If this environment variable exists and its value in PERL's 105boolean context evaluates to true, debugging is enabled. 106 107The function prototype can be expressed as follows: 108 109 Parameter 1 : command => DEFINED SCALAR(string) 110 Parameter 2 : desiredExitStatus => ONE OF 111 SCALAR(integer) 112 ARRAYREF(integer) 113 HASHREF(integer,string) 114 UNDEFINED 115 Parameter 3 : desiredOutput => SCALAR(string) OR UNDEFINED 116 Parameter 4 : exceptions => HASH(integer,string) OR UNDEFINED 117 Returns : SCALAR(integer) as defined by Test::ok(...) 118 119The function treats the first parameter C<$command> as a command line 120to execute as part of the test, it is executed only once and its exit 121status (C<$?E<gt>E<gt>8>) and output are captured. 122 123At this point if debugging is enabled the command, its exit status and 124output are displayed to the tester. 125 126C<checkCmd(...)> allows the testing of either the exit status or the 127generated output or both, not testing either will result in neither 128the C<Test::ok(...)> or C<Test::skip(...)> functions being called, 129something you probably don't want. Note that each defined test 130(C<$desiredExitStatus> and C<$desiredOutput>) results in a invocation 131of either C<Test::ok(...)> or C<Test::skip(...)>, so remember this 132when counting the number of tests to place in the C<Test::plan(...)> 133call. 134 135Many Nagios Plugins test network services, some of which may not be 136present on all systems. To cater for this, C<checkCmd(...)> allows the 137tester to define exceptions based on the command's exit status. These 138exceptions are provided to skip tests if the test case developer 139believes the service is not being provided. For example, if a site 140does not have a POP3 server, the test harness could map the 141appropriate exit status to a useful message the person running the 142tests, telling the reason the test is being skipped. 143 144Example: 145 146my %exceptions = ( 2 =E<gt> "No POP Server present?" ); 147 148$t += checkCmd( "./check_pop I<some args>", 0, undef, %exceptions ); 149 150Thus, in the above example, an exit status of 2 does not result in a 151failed test case (as the exit status is not the desired value of 0), 152but a skipped test case with the message "No POP Server present?" 153given as the reason. 154 155Sometimes the exit status of a command should be tested against a set 156of possible values, rather than a single value, this could especially 157be the case in failure testing. C<checkCmd(...)> support two methods 158of testing against a set of desired exit status values. 159 160=over 161 162=item * 163 164Firstly, if C<$desiredExitStatus> is a reference to an array of exit 165stati, if the actual exit status of the command is present in the 166array, it is used in the call to C<Test::ok(...)> when testing the 167exit status. 168 169=item * 170 171Alternatively, if C<$desiredExitStatus> is a reference to a hash of 172exit stati (mapped to the strings "continue" or "skip"), similar 173processing to the above occurs with the side affect of determining if 174any generated output testing should proceed. Note: only the string 175"skip" will result in generated output testing being skipped. 176 177=back 178 179=item C<skipMissingCmd(...)> 180 181If a command is missing and the test harness must C<Test::skip()> some 182or all of the tests in a given test harness this function provides a 183simple iterator to issue an appropriate message the requested number 184of times. 185 186=back 187 188=head1 SEE ALSO 189 190L<Test> 191 192The rest of the code, as I have only commented on the major public 193functions that test harness writers will use, not all the code present 194in this helper module. 195 196=head1 AUTHOR 197 198Copyright (c) 2005 Peter Bray. All rights reserved. 199 200This package is free software and is provided "as is" without express 201or implied warranty. It may be used, redistributed and/or modified 202under the same terms as the Nagios Plugins release. 203 204=cut 205 206# 207# Package Scope Variables 208# 209 210my( %CACHE ) = (); 211 212# I'm not really sure whether to house a site-specific cache inside 213# or outside of the extracted source / build tree - lets default to outside 214my( $CACHEFILENAME ) = ( exists( $ENV{'NPTEST_CACHE'} ) && $ENV{'NPTEST_CACHE'} ) 215 ? $ENV{'NPTEST_CACHE'} : "/var/tmp/NPTest.cache"; # "../Cache.pdd"; 216 217# 218# Testing Functions 219# 220 221sub checkCmd 222{ 223 my( $command, $desiredExitStatus, $desiredOutput, %exceptions ) = @_; 224 225 my $result = NPTest->testCmd($command); 226 227 my $output = $result->output; 228 my $exitStatus = $result->return_code; 229 230 $output = "" unless defined( $output ); 231 chomp( $output ); 232 233 my $testStatus; 234 235 my $testOutput = "continue"; 236 237 if ( defined( $desiredExitStatus ) ) 238 { 239 if ( ref $desiredExitStatus eq "ARRAY" ) 240 { 241 if ( scalar( grep { $_ == $exitStatus } @{$desiredExitStatus} ) ) 242 { 243 $desiredExitStatus = $exitStatus; 244 } 245 else 246 { 247 $desiredExitStatus = -1; 248 } 249 } 250 elsif ( ref $desiredExitStatus eq "HASH" ) 251 { 252 if ( exists( ${$desiredExitStatus}{$exitStatus} ) ) 253 { 254 if ( defined( ${$desiredExitStatus}{$exitStatus} ) ) 255 { 256 $testOutput = ${$desiredExitStatus}{$exitStatus}; 257 } 258 $desiredExitStatus = $exitStatus; 259 } 260 else 261 { 262 $desiredExitStatus = -1; 263 } 264 } 265 266 if ( %exceptions && exists( $exceptions{$exitStatus} ) ) 267 { 268 $testStatus += skip( $exceptions{$exitStatus}, $exitStatus, $desiredExitStatus ); 269 $testOutput = "skip"; 270 } 271 else 272 { 273 $testStatus += ok( $exitStatus, $desiredExitStatus ); 274 } 275 } 276 277 if ( defined( $desiredOutput ) ) 278 { 279 if ( $testOutput ne "skip" ) 280 { 281 $testStatus += ok( $output, $desiredOutput ); 282 } 283 else 284 { 285 $testStatus += skip( "Skipping output test as requested", $output, $desiredOutput ); 286 } 287 } 288 289 return $testStatus; 290} 291 292 293sub skipMissingCmd 294{ 295 my( $command, $count ) = @_; 296 297 my $testStatus; 298 299 for ( 1 .. $count ) 300 { 301 $testStatus += skip( "Missing ${command} - tests skipped", 1 ); 302 } 303 304 return $testStatus; 305} 306 307sub getTestParameter 308{ 309 my( $param, $envvar, $default, $brief, $scoped ); 310 my $new_style; 311 if (scalar @_ <= 3) { 312 ($param, $brief, $default) = @_; 313 $envvar = $param; 314 $new_style = 1; 315 } else { 316 ( $param, $envvar, $default, $brief, $scoped ) = @_; 317 $new_style = 0; 318 } 319 320 # Apply default values for optional arguments 321 $scoped = ( defined( $scoped ) && $scoped ); 322 323 my $testharness = basename( (caller(0))[1], ".t" ); # used for scoping 324 325 if ( defined( $envvar ) && exists( $ENV{$envvar} ) && $ENV{$envvar} ) 326 { 327 return $ENV{$envvar}; 328 } 329 330 my $cachedValue = SearchCache( $param, $testharness ); 331 if ( defined( $cachedValue ) ) 332 { 333 # This save required to convert to new style because the key required is 334 # changing to the environment variable 335 if ($new_style == 0) { 336 SetCacheParameter( $envvar, undef, $cachedValue ); 337 } 338 return $cachedValue; 339 } 340 341 my $defaultValid = ( defined( $default ) && $default ); 342 my $autoAcceptDefault = ( exists( $ENV{'NPTEST_ACCEPTDEFAULT'} ) && $ENV{'NPTEST_ACCEPTDEFAULT'} ); 343 344 if ( $autoAcceptDefault && $defaultValid ) 345 { 346 return $default; 347 } 348 349 # Set "none" if no terminal attached (eg, tinderbox build servers when new variables set) 350 return "" unless (-t STDIN); 351 352 my $userResponse = ""; 353 354 while ( $userResponse eq "" ) 355 { 356 print STDERR "\n"; 357 print STDERR "Test Harness : $testharness\n"; 358 print STDERR "Test Parameter : $param\n"; 359 print STDERR "Environment Variable : $envvar\n" if ($param ne $envvar); 360 print STDERR "Brief Description : $brief\n"; 361 print STDERR "Enter value (or 'none') ", ($defaultValid ? "[${default}]" : "[]"), " => "; 362 $userResponse = <STDIN>; 363 $userResponse = "" if ! defined( $userResponse ); # Handle EOF 364 chomp( $userResponse ); 365 if ( $defaultValid && $userResponse eq "" ) 366 { 367 $userResponse = $default; 368 } 369 } 370 371 print STDERR "\n"; 372 373 if ($userResponse =~ /^(na|none)$/) { 374 $userResponse = ""; 375 } 376 377 # define all user responses at global scope 378 SetCacheParameter( $param, ( $scoped ? $testharness : undef ), $userResponse ); 379 380 return $userResponse; 381} 382 383# 384# Internal Cache Management Functions 385# 386 387sub SearchCache 388{ 389 my( $param, $scope ) = @_; 390 391 LoadCache(); 392 393 if ( exists( $CACHE{$scope} ) && exists( $CACHE{$scope}{$param} ) ) 394 { 395 return $CACHE{$scope}{$param}; 396 } 397 398 if ( exists( $CACHE{$param} ) ) 399 { 400 return $CACHE{$param}; 401 } 402 return undef; # Need this to say "nothing found" 403} 404 405sub SetCacheParameter 406{ 407 my( $param, $scope, $value ) = @_; 408 409 if ( defined( $scope ) ) 410 { 411 $CACHE{$scope}{$param} = $value; 412 } 413 else 414 { 415 $CACHE{$param} = $value; 416 } 417 418 SaveCache(); 419} 420 421sub LoadCache 422{ 423 return if exists( $CACHE{'_cache_loaded_'} ); 424 425 my $fileContents = ""; 426 if ( -f $CACHEFILENAME ) 427 { 428 my( $fileHandle ) = new IO::File; 429 430 if ( ! $fileHandle->open( "< ${CACHEFILENAME}" ) ) 431 { 432 print STDERR "NPTest::LoadCache() : Problem opening ${CACHEFILENAME} : $!\n"; 433 return; 434 } 435 436 $fileContents = join("", <$fileHandle>); 437 $fileHandle->close(); 438 439 chomp($fileContents); 440 my( $contentsRef ) = eval $fileContents; 441 %CACHE = %{$contentsRef} if (defined($contentsRef)); 442 443 } 444 445 $CACHE{'_cache_loaded_'} = 1; 446 $CACHE{'_original_cache'} = $fileContents; 447} 448 449 450sub SaveCache 451{ 452 delete $CACHE{'_cache_loaded_'}; 453 my $oldFileContents = delete $CACHE{'_original_cache'}; 454 455 my($dataDumper) = new Data::Dumper([\%CACHE]); 456 $dataDumper->Terse(1); 457 $dataDumper->Sortkeys(1); 458 my $data = $dataDumper->Dump(); 459 $data =~ s/^\s+/ /gmx; # make sure all systems use same amount of whitespace 460 $data =~ s/^\s+}/}/gmx; 461 chomp($data); 462 463 if($oldFileContents ne $data) { 464 my($fileHandle) = new IO::File; 465 if (!$fileHandle->open( "> ${CACHEFILENAME}")) { 466 print STDERR "NPTest::LoadCache() : Problem saving ${CACHEFILENAME} : $!\n"; 467 return; 468 } 469 print $fileHandle $data; 470 $fileHandle->close(); 471 } 472 473 $CACHE{'_cache_loaded_'} = 1; 474 $CACHE{'_original_cache'} = $data; 475} 476 477# 478# (Questionable) Public Cache Management Functions 479# 480 481sub SetCacheFilename 482{ 483 my( $filename ) = @_; 484 485 # Unfortunately we can not validate the filename 486 # in any meaningful way, as it may not yet exist 487 $CACHEFILENAME = $filename; 488} 489 490 491# 492# Test Harness Wrapper Functions 493# 494 495sub DetermineTestHarnessDirectory 496{ 497 my( @userSupplied ) = @_; 498 my @dirs; 499 500 # User Supplied 501 if ( @userSupplied > 0 ) 502 { 503 for my $u ( @userSupplied ) 504 { 505 if ( -d $u ) 506 { 507 push ( @dirs, $u ); 508 } 509 } 510 } 511 512 # Simple Cases: "t" and tests are subdirectories of the current directory 513 if ( -d "./t" ) 514 { 515 push ( @dirs, "./t"); 516 } 517 if ( -d "./tests" ) 518 { 519 push ( @dirs, "./tests"); 520 } 521 522 if ( @dirs > 0 ) 523 { 524 return @dirs; 525 } 526 527 # To be honest I don't understand which case satisfies the 528 # original code in test.pl : when $tstdir == `pwd` w.r.t. 529 # $tstdir =~ s|^(.*)/([^/]+)/?$|$1/$2|; and if (-d "../../$2/t") 530 # Assuming pwd is "/a/b/c/d/e" then we are testing for "/a/b/c/e/t" 531 # if I understand the code correctly (a big assumption) 532 533 # Simple Case : the current directory is "t" 534 my $pwd = cwd(); 535 536 if ( $pwd =~ m|/t$| ) 537 { 538 push ( @dirs, $pwd ); 539 540 # The alternate that might work better is 541 # chdir( ".." ); 542 # return "./t"; 543 # As the current test harnesses assume the application 544 # to be tested is in the current directory (ie "./check_disk ....") 545 } 546 547 return @dirs; 548} 549 550sub TestsFrom 551{ 552 my( $directory, $excludeIfAppMissing ) = @_; 553 554 $excludeIfAppMissing = 0 unless defined( $excludeIfAppMissing ); 555 556 if ( ! opendir( DIR, $directory ) ) 557 { 558 print STDERR "NPTest::TestsFrom() - Failed to open ${directory} : $!\n"; 559 return (); 560 } 561 562 my( @tests ) = (); 563 564 my $filename; 565 my $application; 566 567 while ( $filename = readdir( DIR ) ) 568 { 569 if ( $filename =~ m/\.t$/ ) 570 { 571 if ( $excludeIfAppMissing ) 572 { 573 $application = basename( $filename, ".t" ); 574 if ( ! -e $application and ! -e $application.'.pm' ) 575 { 576 print STDERR "No application (${application}) found for test harness (${filename})\n"; 577 next; 578 } 579 } 580 push @tests, "${directory}/${filename}"; 581 } 582 } 583 584 closedir( DIR ); 585 586 return sort @tests; 587} 588 589# All the new object oriented stuff below 590 591sub new { 592 my $type = shift; 593 my $self = {}; 594 return bless $self, $type; 595} 596 597# Accessors 598sub return_code { 599 my $self = shift; 600 if (@_) { 601 return $self->{return_code} = shift; 602 } else { 603 return $self->{return_code}; 604 } 605} 606sub output { 607 my $self = shift; 608 if (@_) { 609 return $self->{output} = shift; 610 } else { 611 return $self->{output}; 612 } 613} 614 615sub perf_output { 616 my $self = shift; 617 $_ = $self->{output}; 618 /\|(.*)$/; 619 return $1 || ""; 620} 621 622sub only_output { 623 my $self = shift; 624 $_ = $self->{output}; 625 /(.*?)\|/; 626 return $1 || ""; 627} 628 629sub testCmd { 630 my $class = shift; 631 my $command = shift or die "No command passed to testCmd"; 632 my $object = $class->new; 633 634 local $SIG{'ALRM'} = sub { die("timeout in command: $command"); }; 635 alarm(120); # no test should take longer than 120 seconds 636 637 my $output = `$command`; 638 $object->return_code($? >> 8); 639 $_ = $? & 127; 640 if ($_) { 641 die "Got signal $_ for command $command"; 642 } 643 chomp $output; 644 $object->output($output); 645 646 alarm(0); 647 648 my ($pkg, $file, $line) = caller(0); 649 print "Testing: $command", $/; 650 if ($ENV{'NPTEST_DEBUG'}) { 651 print "testCmd: Called from line $line in $file", $/; 652 print "Output: ", $object->output, $/; 653 print "Return code: ", $object->return_code, $/; 654 } 655 656 return $object; 657} 658 659# do we have ipv6 660sub has_ipv6 { 661 # assume ipv6 if a ping6 to labs.consol.de works 662 `ping6 -c 1 2a03:3680:0:2::21 2>&1`; 663 if($? == 0) { 664 return 1; 665 } 666 return; 667} 668 6691; 670# 671# End of File 672# 673