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