aboutsummaryrefslogtreecommitdiff
path: root/NPTest.pm
blob: d4542ddc2cc5348c4b41a1b13b3e879b6e53be30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
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 = do { my @r = (q$Revision$ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # 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
#